Skip to content

feat(later): save prompts and run them later in the session - #13

Open
derogab wants to merge 8 commits into
masterfrom
feat/later
Open

feat(later): save prompts and run them later in the session#13
derogab wants to merge 8 commits into
masterfrom
feat/later

Conversation

@derogab

@derogab derogab commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a new Pi plugin, later, that lets you postpone prompts mid-session and rerun them on demand through a single /later command.

Changes

Plugin (plugins/later/)

  • /later <prompt> saves a prompt to a session-scoped list
  • /later opens 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
  • List persisted via session entries: survives /reload and resume, and follows session-tree navigation
  • package.json for npm:@derogab/pi-later + README with install/usage

CI

  • publish-pi-later.yml workflow publishing to npm on plugins/later/package.json changes (mirrors pi-clear)

Docs

  • plugins/README.md table row + install line

Test plan

  • Smoke-tested with jiti + mocked ExtensionAPI: save, state reconstruction, pick-and-run, label truncation, busy-agent follow-up
  • Installed locally via pi install ./plugins/later and verified /later save + persistence in a live session
  • Publish workflow publishes @derogab/pi-later@0.1.0 on merge to master

Summary by cubic

Adds the later Pi 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).

  • Save with /later <prompt>; /later shows a picker (oldest-first, labels truncated to 80 chars). Choosing a prompt opens Confirm or Remove; duplicates are selectable by position.
  • Idle: run only after preflight checks; acknowledge on before_agent_start and remove then so input transformations don’t break acknowledgement.
  • Busy: send as 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.
  • Persist via session custom entries (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.yml publishes @derogab/pi-later on master pushes touching plugins/later/package.json or via manual dispatch; requires NPM_TOKEN. Install with pi install npm:@derogab/pi-later.

Written for commit fb45883. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread plugins/later/extensions/later.ts Outdated
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread plugins/later/extensions/later.ts Outdated
Comment thread plugins/later/extensions/later.ts Outdated
…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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread plugins/later/extensions/later.ts Outdated
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant