Repository navigation
Replies: 3 comments
|
Additional use case: two-way sync for unsent drafts across connected clients T3 Connect currently makes the environment and submitted thread state reachable from another authorized client, but an unsubmitted prompt remains in the originating client’s local composer storage. In practice, a prompt started on one macOS desktop is absent when the same connected environment is opened on a second desktop, even though both clients are connected. This makes the current sync one-way from the user’s perspective: work is shared after submission, while work still being composed is not. The public source reflects this boundary: A focused acceptance boundary for the server-side draft persistence follow-on described in this discussion would be:
This narrows the existing proposal to the two-way connected-client workflow and keeps the prompt unsubmitted until the user explicitly sends it. |
|
Adding a mobile case to this proposal:
The draft thread is absent from the mobile thread list, so I cannot find or resume that unsent prompt on my phone. I expected the draft to appear on mobile and remain unsent until I explicitly submit it. The desktop screenshot shows the draft row after switching threads. This seems to match the client-local draft storage described above. If maintainers are open to a focused PR for cross-device draft visibility/sync, I would be happy to help. What scope would you prefer? |


Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Before submitting
Area
apps/web
Problem or use case
I often know what I want an agent to do next, but can't start it right now — a turn is already running in that project, I'm out of usage for the window, the task depends on something that isn't merged yet, or I want to think about the wording before spending a run.
Today an unsent draft is invisible, singular, and local:
composerDraftStoreexposesgetDraftThreadByProjectRef(projectRef)— and "new thread" reuses that project's existing draft rather than creating another. I can't line up two or three prompts for the same repo.localStorageunder a ~5MB origin-wide quota shared with the prompt stash, and never leave the browser profile that created them.The net effect is that there's no way to build up a short list of "tasks I want to run in this project, but not yet."
Proposed solution
Make a draft an actual thread that hasn't taken its first turn yet, rather than a client-side record that becomes a thread at send time.
The pieces for this already exist and are already separated correctly:
thread.createis a standalone command whose decider (apps/server/src/orchestration/decider.ts) does nothing but check that the project exists and the thread ID is unused, then emitthread.created. It has no filesystem or git side effects.thread.createinside the turn-start handler inapps/server/src/ws.ts(prepareWorktree,runSetupScript).thread.createintothread.turn.start'sbootstrap.createThreadat send time.So the change is: dispatch
thread.createwhen the draft is started, instead of deferring it to send. Environment materialization stays exactly where it is — on the first turn — so a draft thread costs nothing but a row.Everything else follows from that:
resolveSidebarV2Status(apps/web/src/components/Sidebar.logic.ts) currently returnsreadywhen a thread has no session, so it needs a branch returningdraftfor a thread that has never had a turn, andSidebarV2.tsx'stopStatusblock needs a correspondingDraftlabel in the slot that showsWorking/Approval/Input/Failed/Woke/Done.composerDraftStoregoes away rather than being generalized.createAttachmentId(threadId)inapps/server/src/attachmentStore.ts), so a draft thread fits it directly, where aDraftIddoes not.Two gaps this does not close for free, and which are the real work:
thread.turn.start; an unsent prompt is not a message yet. This needs a small addition — athread.draft.setcommand and event carrying text plus attachment references, and a column on the thread projection to hold it. This is the one genuinely new piece of persistence.titleis a requiredTrimmedNonEmptyStringonthread.create, but a draft has no title until it's sent (today the title is derived fromtitleSeedat send). Drafts need either a nullable title or an explicit placeholder that the sidebar renders as the draft's prompt text.Why this matters
It closes the gap between "I thought of a task" and "I ran the task" without leaving the app or holding it in my head — specifically when the reason you can't run it right now is temporary: a turn is already in flight, you've hit a usage limit and want everything queued for the reset, the task depends on a review or merge that hasn't landed, or you're switching between repos and want each one's next step visible where you already look.
Modeling drafts as threads also removes a class of bug rather than adding one. #4647 is caused precisely by the current split: the client holds a pre-allocated thread ID, defers
thread.createto send, and when bootstrap fails and emitsthread.deleted, the persisted draft keeps retryingthread.createagainst a tombstoned aggregate ID — permanently, across restarts. If the thread is created when the draft is created, there is no create-at-send-time step to retry against a dead ID, and bootstrap failure becomes an ordinary retryable turn failure on a thread that already exists.It also makes the existing draft feature discoverable. Right now a draft you left behind is silently invisible until you happen to return to the same route, which is a small but real way to lose written work.
Smallest useful scope
Create the thread at draft time, add a
draftstatus toresolveSidebarV2Status, and render aDraftlabel in Sidebar V2 — with the draft's prompt text still held client-side, exactly as it is now.That alone makes pending work visible, and makes multiple drafts per project fall out, because they are threads. Server-side draft text (
thread.draft.set) is a second step that adds cross-device sync and removes the quota ceiling, and can land separately.Alternatives considered
Risks or tradeoffs
thread.createdevent and a projection row, where today it's a localStorage entry that gets overwritten. Creating the thread only once the composer is non-empty avoids the worst of it, but some cleanup story — auto-delete of empty draft threads after N days, or a bulk clear — is probably needed rather than optional.thread.draft.setis dispatched per change it becomes an RPC and a SQLite write per debounce interval per client, and if it's an event-sourced event it also accumulates in the event log. [Bug]: Per-chunk assistant persistence causes progressive live output lag and rapid SQLite growth #5110 tracks an existing case of this shape (per-chunk persistence causing progressive lag and rapid SQLite growth). Draft text should be coalesced, debounced, and probably flushed on blur or navigation — and it may be a case for a mutable projection column updated in place rather than an event per edit.draftstatus must not leak into places that assume a live thread. Anything iterating threads — filters, counts, "needs review" style prioritization ([Feature]: Prioritize Sidebar V2 threads that need review #4695), mobile thread lists, notifications — will start seeing threads with no session and no turns. Draft threads should be excluded from attention/unread treatment by default, since they're waiting on the user, not the agent.updatedAtis likely sufficient for drafts, but should be stated rather than left implicit. [Bug]: Thread interaction and runtime modes do not sync between desktop clients #5278 (thread modes not syncing between desktop clients) is a fair indicator of how much "this syncs correctly everywhere" actually costs.thread.delete, so it should be possible directly from the sidebar row without opening it, and shouldn't prompt with the same confirmation weight as deleting a thread with real history (confirmThreadDelete).Drafta low-prominence treatment should contain this, but it deserves a look on projects with many threads.Examples or references
apps/web/src/composerDraftStore.ts(DraftId,getDraftThreadByProjectRef,markPromotedDraftThread),apps/web/src/components/Sidebar.logic.ts(resolveSidebarV2Status),apps/web/src/components/SidebarV2.tsx(thetopStatuslabel block),apps/web/src/promptStashStore.tspackages/contracts/src/orchestration.ts(ThreadCreateCommand,ThreadTurnStartBootstrap),apps/server/src/orchestration/decider.ts(the side-effect-freethread.createbranch),apps/server/src/ws.ts(bootstrap sequencing),apps/server/src/attachmentStore.ts(thread-keyed attachment ids)Contribution
All reactions