feat(later): save prompts and run them later in the session - #13
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 5 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
getBranch() returns live entry objects, so assigning the persisted array by reference let push/splice mutate the historical session entry. That corrupted the state restored when navigating the session tree or forking. Clone the array instead.
sendUserMessage() rejects when no turn can start (e.g. no model or provider auth), but the prompt was already spliced and persisted, losing it without running it. Await delivery and keep the prompt in the list on failure.
There was a problem hiding this comment.
All reported issues were addressed across 1 file (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…first reply Pi creates the session file only after the first assistant response, so custom entries appended before that live only in memory and cannot survive a resume.
Pi exposes sendUserMessage as fire-and-forget, so awaiting it cannot confirm that an idle turn passed preflight. Track acceptance through the agent lifecycle and flush unpersisted prompts during graceful shutdown.
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Input handlers may change the text before delivery acknowledgement, so match the pending idle turn by order instead. Avoid writing Pi's shared session file before later shutdown handlers can append their state.
Picking a saved prompt no longer runs it immediately. A second dialog now offers Confirm to send the prompt or Remove to delete it without running, so saved prompts can be discarded without executing them.
The busy path removed the prompt right after the fire-and-forget sendUserMessage call, so a follow-up that never ran (quit, error, dropped queue) was lost from the persisted list. Track a queue of pending deliveries and remove only when before_agent_start confirms the turn, matching by prompt text so a follow-up overtaken by a user-typed message is not acknowledged early.
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="plugins/later/extensions/later.ts">
<violation number="1" location="plugins/later/extensions/later.ts:64">
P2: When a queued follow-up has the same text as a user-typed message, this match acknowledges and removes the saved prompt on the user's turn. Track delivery identity or use a lifecycle signal that distinguishes follow-up turns before removing it.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
|
||
| // Match by text so a follow-up overtaken by a user-typed message is not | ||
| // acknowledged before it is actually delivered. | ||
| let index = pendingDeliveries.findIndex((delivery) => delivery.prompt.text === event.prompt); |
There was a problem hiding this comment.
P2: When a queued follow-up has the same text as a user-typed message, this match acknowledges and removes the saved prompt on the user's turn. Track delivery identity or use a lifecycle signal that distinguishes follow-up turns before removing it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/later/extensions/later.ts, line 64:
<comment>When a queued follow-up has the same text as a user-typed message, this match acknowledges and removes the saved prompt on the user's turn. Track delivery identity or use a lifecycle signal that distinguishes follow-up turns before removing it.</comment>
<file context>
@@ -48,14 +56,19 @@ export default function (pi: ExtensionAPI) {
+
+ // Match by text so a follow-up overtaken by a user-typed message is not
+ // acknowledged before it is actually delivered.
+ let index = pendingDeliveries.findIndex((delivery) => delivery.prompt.text === event.prompt);
+ if (index === -1) {
+ if (!pendingDeliveries[0].idle) return;
</file context>
There was a problem hiding this comment.
1 existing issue remains and 3 new issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="plugins/later/test/later.test.ts">
<violation number="1" location="plugins/later/test/later.test.ts:77">
P2: The persistence path is never tested. reconstructState() (wired to session_start/session_tree) is the plugin's core feature — prompts "survive /reload and session resume" per the README/PR — and it clones arrays to avoid mutating previously persisted entries. None of the 9 tests invoke the session_start or session_tree handlers, so a regression that drops prompts on reload, reads the wrong branch entry, or mutates persisted history would ship undetected. The mock already supports it (sessionManager.getBranch returns entries), so add a test that saves prompts, calls handlers.get('session_start')({}, ctx), and asserts the saved prompts are restored. Also note the documented 80-char label truncation (MAX_LABEL_LENGTH) is likewise untested.</violation>
</file>
<file name="plugins/later/extensions/later.ts">
<violation number="1" location="plugins/later/extensions/later.ts:65">
P2: This fallback acknowledges and removes the first idle delivery whenever the next turn's prompt text matches no pending delivery, assuming any mismatch means an input handler transformed an idle send. That assumption fails whenever the turn that fires is unrelated to the deferred prompt (a turn started by a user-typed message or by another extension before the idle send is picked up). In that case a saved prompt is removed from the list even though its turn never ran, silently losing the user's saved prompt. Limit the transformation fallback to the case where the pending idle delivery is the only delivery and therefore must be the very next turn.</violation>
<violation number="2" location="plugins/later/extensions/later.ts:119">
P1: When the agent starts during the authentication await, this idle check becomes stale and Pi rejects the unqueued `sendUserMessage` call. Because the call is fire-and-forget, the next unrelated `before_agent_start` removes the saved prompt; re-check the state after preflight or dispatch with a race-safe delivery mode and handle failure.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Re-trigger cubic
|
|
||
| if (action !== ACTION_CONFIRM) return; | ||
|
|
||
| if (ctx.isIdle()) { |
There was a problem hiding this comment.
P1: When the agent starts during the authentication await, this idle check becomes stale and Pi rejects the unqueued sendUserMessage call. Because the call is fire-and-forget, the next unrelated before_agent_start removes the saved prompt; re-check the state after preflight or dispatch with a race-safe delivery mode and handle failure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/later/extensions/later.ts, line 119:
<comment>When the agent starts during the authentication await, this idle check becomes stale and Pi rejects the unqueued `sendUserMessage` call. Because the call is fire-and-forget, the next unrelated `before_agent_start` removes the saved prompt; re-check the state after preflight or dispatch with a race-safe delivery mode and handle failure.</comment>
<file context>
@@ -0,0 +1,145 @@
+
+ if (action !== ACTION_CONFIRM) return;
+
+ if (ctx.isIdle()) {
+ if (ctx.model === undefined) {
+ ctx.ui.notify("Could not run saved prompt, kept in list: no model selected", "error");
</file context>
| @@ -0,0 +1,185 @@ | |||
| import assert from "node:assert/strict"; | |||
There was a problem hiding this comment.
P2: The persistence path is never tested. reconstructState() (wired to session_start/session_tree) is the plugin's core feature — prompts "survive /reload and session resume" per the README/PR — and it clones arrays to avoid mutating previously persisted entries. None of the 9 tests invoke the session_start or session_tree handlers, so a regression that drops prompts on reload, reads the wrong branch entry, or mutates persisted history would ship undetected. The mock already supports it (sessionManager.getBranch returns entries), so add a test that saves prompts, calls handlers.get('session_start')({}, ctx), and asserts the saved prompts are restored. Also note the documented 80-char label truncation (MAX_LABEL_LENGTH) is likewise untested.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/later/test/later.test.ts, line 77:
<comment>The persistence path is never tested. reconstructState() (wired to session_start/session_tree) is the plugin's core feature — prompts "survive /reload and session resume" per the README/PR — and it clones arrays to avoid mutating previously persisted entries. None of the 9 tests invoke the session_start or session_tree handlers, so a regression that drops prompts on reload, reads the wrong branch entry, or mutates persisted history would ship undetected. The mock already supports it (sessionManager.getBranch returns entries), so add a test that saves prompts, calls handlers.get('session_start')({}, ctx), and asserts the saved prompts are restored. Also note the documented 80-char label truncation (MAX_LABEL_LENGTH) is likewise untested.</comment>
<file context>
@@ -0,0 +1,185 @@
+
+const latestPrompts = (entries: CustomEntry[]) => entries.at(-1)?.data.prompts;
+
+test("keeps an idle prompt when no model is selected", async () => {
+ const fixture = setup();
+ fixture.ctx.model = undefined;
</file context>
| // Match by text so a follow-up overtaken by a user-typed message is not | ||
| // acknowledged before it is actually delivered. | ||
| let index = pendingDeliveries.findIndex((delivery) => delivery.prompt.text === event.prompt); | ||
| if (index === -1) { |
There was a problem hiding this comment.
P2: This fallback acknowledges and removes the first idle delivery whenever the next turn's prompt text matches no pending delivery, assuming any mismatch means an input handler transformed an idle send. That assumption fails whenever the turn that fires is unrelated to the deferred prompt (a turn started by a user-typed message or by another extension before the idle send is picked up). In that case a saved prompt is removed from the list even though its turn never ran, silently losing the user's saved prompt. Limit the transformation fallback to the case where the pending idle delivery is the only delivery and therefore must be the very next turn.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/later/extensions/later.ts, line 65:
<comment>This fallback acknowledges and removes the first idle delivery whenever the next turn's prompt text matches no pending delivery, assuming any mismatch means an input handler transformed an idle send. That assumption fails whenever the turn that fires is unrelated to the deferred prompt (a turn started by a user-typed message or by another extension before the idle send is picked up). In that case a saved prompt is removed from the list even though its turn never ran, silently losing the user's saved prompt. Limit the transformation fallback to the case where the pending idle delivery is the only delivery and therefore must be the very next turn.</comment>
<file context>
@@ -0,0 +1,145 @@
+ // Match by text so a follow-up overtaken by a user-typed message is not
+ // acknowledged before it is actually delivered.
+ let index = pendingDeliveries.findIndex((delivery) => delivery.prompt.text === event.prompt);
+ if (index === -1) {
+ if (!pendingDeliveries[0].idle) return;
+ index = 0;
</file context>
Summary
Adds a new Pi plugin,
later, that lets you postpone prompts mid-session and rerun them on demand through a single/latercommand.Changes
Plugin (
plugins/later/)/later <prompt>saves a prompt to a session-scoped list/lateropens a picker; selecting a prompt sends it as a user message (runs it) and removes it from the list; queues as follow-up when the agent is busy/reloadand resume, and follows session-tree navigationpackage.jsonfornpm:@derogab/pi-later+ README with install/usageCI
publish-pi-later.ymlworkflow publishing to npm onplugins/later/package.jsonchanges (mirrorspi-clear)Docs
plugins/README.mdtable row + install lineTest plan
pi install ./plugins/laterand verified/latersave + persistence in a live session@derogab/pi-later@0.1.0on merge to masterSummary by cubic
Adds the
laterPi plugin to save prompts mid-session and run them on demand via/later. Previously you couldn’t defer prompts; now you can pick a saved prompt, confirm to run it or remove it, and the prompt leaves the list only after Pi accepts the turn (including queued follow-ups)./later <prompt>;/latershows a picker (oldest-first, labels truncated to 80 chars). Choosing a prompt opens Confirm or Remove; duplicates are selectable by position.before_agent_startand remove then so input transformations don’t break acknowledgement.followUp, keep it in the list until its turn actually starts, and match by prompt text so a different turn starting first doesn’t drop it. Notify when queued.customType: "later"with{ prompts: string[] }); state reconstruction clones arrays to avoid mutating history. Prompts saved before the first assistant reply live only in memory and are lost on exit/session switches.Rollout
.github/workflows/publish-pi-later.ymlpublishes@derogab/pi-lateronmasterpushes touchingplugins/later/package.jsonor via manual dispatch; requiresNPM_TOKEN. Install withpi install npm:@derogab/pi-later.Written for commit fb45883. Summary will update on new commits.