feat(web): prompt stash — cmd+S saves the composer to a per-provider queue#4453
feat(web): prompt stash — cmd+S saves the composer to a per-provider queue#4453t3dotgg wants to merge 1 commit into
Conversation
…queue Pressing mod+S snapshots the composer (prompt + image attachments + model selection) into a localStorage-backed stash queue scoped to the active provider instance, then clears the composer with an Undo toast. A bookmark badge on the composer shoulder shows the queue count with a one-shot ghost-fly animation on save; mod+S on an empty composer (or clicking the badge) opens a picker to restore or delete entries. - composer.stash keybinding command, default mod+s (!terminalFocus) - promptStashStore: zustand persist under t3code:prompt-stash:v1, schema-validated, 20 entries/queue, per-entry attachment budget with dropped-image tracking, quota-safe writes - restore rehydrates images to live Files via the existing persisted attachment codec and reapplies the saved model selection when the provider is still available Client-only; terminal/element contexts are not stashed since they reference live sessions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7d14ced. Configure here.
| }, | ||
| }, | ||
| data: { hideCopyButton: true }, | ||
| }); |
There was a problem hiding this comment.
Async stash wipes newer composer input
High Severity
The stashCurrentPrompt function clears the composer after asynchronously processing images, without an in-flight guard. This allows user edits or subsequent stash attempts during the async operation to be lost or cause duplicate stashes.
Reviewed by Cursor Bugbot for commit 7d14ced. Configure here.
| ).slice(0, Math.max(0, capacity)); | ||
| if (restoredImages.length > 0) { | ||
| addComposerDraftImages(composerDraftTarget, restoredImages); | ||
| } |
There was a problem hiding this comment.
Restore drops excess images permanently
Medium Severity
When restoring a stashed prompt, restoreStashEntry removes the entry from the queue before processing attachments. If the restored attachments exceed the provider's limit, the excess are silently discarded and permanently lost without user notification.
Reviewed by Cursor Bugbot for commit 7d14ced. Configure here.
| attachments: kept, | ||
| providerInstanceId: stashScopeInstanceId, | ||
| modelSelection: noProviderAvailable ? null : selectedModelSelection, | ||
| droppedImageNames: allDroppedNames, |
There was a problem hiding this comment.
Unreadable images blamed on size limit
Low Severity
Names from failed readFileAsDataUrl calls are merged into droppedImageNames with true attachment-budget drops. Stash and restore toasts then always describe those files as too large for the stash, even when the real failure was an unreadable file.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 7d14ced. Configure here.
| flushPromptStashStorage(); | ||
|
|
||
| promptRef.current = ""; | ||
| clearComposerDraftContent(composerDraftTarget); |
There was a problem hiding this comment.
Stash wipes unrestorable contexts
Medium Severity
Stash clears the composer via clearComposerContent, which also drops terminal contexts, element contexts, preview annotations, and review comments. Those are not stored on the stash entry, so Undo and restore cannot bring them back.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7d14ced. Configure here.
| queuesByScopeKey: {}, | ||
| stashEntry: (entry) => { | ||
| const scopeKey = promptStashScopeKey(entry.providerInstanceId); | ||
| const queue = get().queuesByScopeKey[scopeKey] ?? []; |
There was a problem hiding this comment.
🟡 Medium src/promptStashStore.ts:128
When providerInstanceId is __proto__ (a valid custom slug), stashEntry looks up queuesByScopeKey["__proto__"] which returns Object.prototype instead of undefined. Spreading Object.prototype with [entry, ...queue] then throws a TypeError because it is not iterable, so stashing a prompt for that provider crashes. The same prototype-pollution issue affects takeEntry's lookup. Consider guarding with Object.prototype.hasOwnProperty.call or using a Map/null-prototype object for queuesByScopeKey.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/promptStashStore.ts around line 128:
When `providerInstanceId` is `__proto__` (a valid custom slug), `stashEntry` looks up `queuesByScopeKey["__proto__"]` which returns `Object.prototype` instead of `undefined`. Spreading `Object.prototype` with `[entry, ...queue]` then throws a `TypeError` because it is not iterable, so stashing a prompt for that provider crashes. The same prototype-pollution issue affects `takeEntry`'s lookup. Consider guarding with `Object.prototype.hasOwnProperty.call` or using a `Map`/null-prototype object for `queuesByScopeKey`.
| const decodePersistedPromptStashState = Schema.decodeUnknownSync(PersistedPromptStashState); | ||
|
|
||
| /** Maps the composer's active provider instance to a stash queue bucket. */ | ||
| export function promptStashScopeKey(instanceId: ProviderInstanceId | null | undefined): string { |
There was a problem hiding this comment.
🟡 Medium src/promptStashStore.ts:44
promptStashScopeKey returns the literal string __none__ for both stashed-unscoped entries and any provider whose ID is __none__, so both buckets share the same queue. A __none__ provider's stashed prompts appear under the unscoped bucket and can be restored or deleted out of the wrong scope. Use a collision-proof representation (e.g. prefix provider keys with a sigil like provider:__none__ while keeping PROMPT_STASH_UNSCOPED_KEY distinct) or escape provider IDs so the unscoped key can never collide.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/promptStashStore.ts around line 44:
`promptStashScopeKey` returns the literal string `__none__` for both stashed-unscoped entries and any provider whose ID is `__none__`, so both buckets share the same queue. A `__none__` provider's stashed prompts appear under the unscoped bucket and can be restored or deleted out of the wrong scope. Use a collision-proof representation (e.g. prefix provider keys with a sigil like `provider:__none__` while keeping `PROMPT_STASH_UNSCOPED_KEY` distinct) or escape provider IDs so the unscoped key can never collide.
| if (event.key === "Enter") { | ||
| if (!highlightedEntry) return; | ||
| event.preventDefault(); | ||
| event.stopPropagation(); | ||
| onRestore(highlightedEntry); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🟡 Medium chat/ComposerStashMenu.tsx:67
When the stash menu is open and keyboard focus is on the delete Button, pressing Enter restores the highlighted entry instead of activating the button's onClick. The capture-phase keydown handler matches Enter unconditionally and calls event.preventDefault() + event.stopPropagation() + onRestore(highlightedEntry), so the focused button never receives the activation. Check event.target (or document.activeElement) and skip the handler when the event originates from a nested interactive element like the delete button.
+ if (event.key === "Enter") {
+ if (event.target instanceof HTMLButtonElement) return;
if (!highlightedEntry) return;
event.preventDefault();
event.stopPropagation();
onRestore(highlightedEntry);
return;🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ComposerStashMenu.tsx around lines 67-73:
When the stash menu is open and keyboard focus is on the delete `Button`, pressing Enter restores the highlighted entry instead of activating the button's `onClick`. The capture-phase `keydown` handler matches `Enter` unconditionally and calls `event.preventDefault()` + `event.stopPropagation()` + `onRestore(highlightedEntry)`, so the focused button never receives the activation. Check `event.target` (or `document.activeElement`) and skip the handler when the event originates from a nested interactive element like the delete button.
ApprovabilityVerdict: Needs human review 3 blocking correctness issues found. New feature introducing new components, state management, and user-facing workflows. Multiple unresolved review comments including a high-severity race condition require human attention. You can customize Macroscope's approvability policy. Learn more. |


What
Copies Claude Code's "cmd+S to stash a prompt" flow into the web composer. Pressing ⌘S / Ctrl+S snapshots the in-progress prompt — text and image attachments plus the model selection — into a client-only stash queue, then clears the composer with an Undo toast. The queue is stored in localStorage and scoped per connection method (
ProviderInstanceId, so Claude Code / Codex / Cursor / custom instances each get their own queue).Design + interactive mocks: https://puxe56fxtfjc.postplan.dev (this ships mocks D + B).
UX
prefers-reduced-motionrespected), success toast with Undo.Files through the existing persisted-attachment codec, and the saved model selection (only if that provider instance is still enabled). Restoring removes the entry — it's a queue, not a library.Implementation
composer.stashcommand in the keybinding contract + defaultmod+s(!terminalFocus) — rebindable via the existing registry. The handler runs capture-phase onwindowand alwayspreventDefault()s so the browser save dialog never appears, even in states that can't stash (approvals, plan questions, palette open).promptStashStore(t3code:prompt-stash:v1): zustandpersist+ Effect-Schema validation + debounced storage +beforeunloadflush, cloned from the composer draft store's conventions. 20 entries per queue (oldest evicted with a toast note), ~2MB per-entry attachment budget with dropped images surfaced by name, quota-safe writes that degrade to in-memory instead of throwing in the debounce timer.Testing
__none__bucket, LIFO order, eviction at cap, take-once semantics, attachment budget partitioning).🤖 Generated with Claude Code
Note
Low Risk
Client-only localStorage UX with guarded shortcut handling and existing draft-store patterns; no server, auth, or send-path changes. Main caveat is shared origin storage quota with composer drafts.
Overview
Adds a prompt stash to the web composer, modeled on Claude Code’s cmd+S flow. ⌘S / Ctrl+S (
composer.stash, defaultmod+swhen the terminal isn’t focused) snapshots the in-progress prompt, image attachments, and model selection into a client-only queue in localStorage, scoped per provider instance, then clears the composer. The shortcut always callspreventDefault()so the browser save dialog never opens.Stash saves strip inline terminal-context placeholders, enforce per-entry attachment size limits, and cap each queue at 20 entries (oldest evicted). Restore merges prompt text, rehydrates images via exported
hydrateImagesFromPersisted, and reapplies model selection when the instance is still available; entries are removed on restore (queue semantics). Empty composer + ⌘S or the shoulder bookmark badge opens a keyboard-navigable picker (ComposerStashMenu); save shows a fly-into-badge animation and an Undo toast.New
promptStashStore(zustand persist, schema validation, debounced quota-safe storage) plus unit tests; UI inComposerStashBadge/ComposerStashMenuwired fromChatComposer.Reviewed by Cursor Bugbot for commit 7d14ced. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add prompt stash feature to chat composer with Cmd+S per-provider queue
Mod+Sin the composer (when terminal is not focused) stashes the current prompt and attachments to a per-provider queue via the newcomposer.stashkeybinding, clearing the composer and showing an Undo toast.ComposerStashBadgepill shows the stash count and plays a one-shot ghost animation on stash; clicking it opens aComposerStashMenupopover for keyboard- and mouse-navigable restore/delete.📊 Macroscope summarized 7d14ced. 7 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.