Repository navigation
Continue a chat without a project in a new workspace: the agent's summary becomes the handoff - #888
danyaberezun wants to merge 24 commits into
Conversation
97fd36a to
7972b02
Compare
7972b02 to
7d19fdd
Compare
| try { | ||
| workspace = state.workspaceId | ||
| ? knownWorkspace(state.workspaceId) | ||
| : provisionInitialTerminal(await createWorkspace(project.id)); |
There was a problem hiding this comment.
Blocking:
Problem: Applying an operation has no in-flight guard; pendingFor also admits its creating phase. Pi 1.2.0 executes these tools in parallel by default.
Failure scenario: The agent emits two continue_in_workspace calls in one response. A probe using the real tool creates two distinct workspaces and chats, sends both handoffs, and records two links for one operation; the final operation state races between them.
Suggested fix: Store and join one apply promise per operation before any await, returning the same result to concurrent calls and retaining the completed-id check.
| const continuationOf = useCallback( | ||
| (operationId: string): ChatContinuation | undefined => { | ||
| const known = continuations[operationId]; | ||
| if (known || askedStatus.current.has(operationId)) return known; |
There was a problem hiding this comment.
Non-blocking:
Problem: Status is fetched only for unknown operations, while cached continuation phases survive reconnect and no reconnect handler refreshes them.
Failure scenario: Disconnect after summarizing, then reconnect after the host finishes, fails, or restarts. The cached phase remains summarizing indefinitely, so the card offers neither Open nor Retry and the initiating client never follows the completed handoff. The transport probe reconnects with zero continuationStatus reads.
Suggested fix: Refresh in-progress operations after each connected welcome, including initiated operations without mounted cards, and map a lost binding to lost so Retry becomes available.
| sourceSessionId, | ||
| projectId: project.id, | ||
| phase: "requested", | ||
| ...(leftover?.workspaceId ? { workspaceId: leftover.workspaceId } : {}), |
There was a problem hiding this comment.
Non-blocking:
Problem: Retrying blindly inherits the failed operation's workspaceId, even when that workspace has been removed.
Failure scenario: Workspace creation succeeds but target session startup fails; remove the empty workspace through its menu, then Retry. knownWorkspace rejects the missing id, and every later Continue for the same project inherits it again. Two consecutive retries reproduce the permanent failure.
Suggested fix: Validate the retained workspace before inheriting partial work; if it is gone, discard its workspaceId and dependent sessionId so a new workspace can be created.
| sessionId: targetSessionId, | ||
| }); | ||
| } catch (error) { | ||
| log.warn(`continuation link not recorded for ${state.sourceWorkspaceId}: ${String(error)}`); |
There was a problem hiding this comment.
Non-blocking:
Problem: Both catch blocks stringify the caught error into the warning instead of passing it to the logger, bypassing mandatory error reporting.
Failure scenario: If persisting continuedTo fails, this function swallows the exception and returns success; notifyLoggedError receives undefined and PostHog never records the failure. The handoff-delivery catch has the same omission.
Suggested fix: Pass the caught error as the second argument to both log.warn calls so the classifier and stack-reporting path receives it.
`Mod+K`, double Shift and a topbar button open one palette that finds chats, workspaces and projects across every open project, runs commands with their shortcuts shown, deep-links Settings sections and toggles MCP servers and extensions in place. The empty query keeps the former switcher's role as the "Waiting for you" group next to Recent and Actions. Two new web sub-modules: `commands` (the typed contract an invocable action is described with; handlers stay with their owners) and `search` (props-driven palette, prefix/filter grammar, tiered ranking over the shared matcher with decay-free frecency and learned query→pick, client-local preferences, the double-Shift state machine). Shell composes the catalog, pages and row actions and injects them; `panels` import neither module.
…ions and module specs Also the README capability line for the palette.
…es in the search spec
…ne terminal, IME, protocol gates, quoted filters - Chat-title backfill now covers every untitled workspace (active and context-project first) four requests at a time, re-asks a workspace at most once a minute and keeps the titles across opens, instead of stopping at the first 24 forever. - `terminal.new` is a host mutation (placement reserves on the host), so it is disabled offline. - The action panel's Enter ignores an IME composition like the main input does. - `settings.extensions` / `settings.mcp` follow the same protocol gates as their Settings sections, and the extension catalog / MCP list are requested only when the host serves them. - Quoted spans are tokenized before state filters, so `"is:running"` stays literal text.
… non-English catalogs
Drop the inline Theme, Extensions and MCP pages with their page stack, the per-row action panel, in-place toggles and the is:/has: filter grammar. Every setting row now opens its Settings section, the double-Shift row included; Mod+K inside the palette closes it; a command always runs.
… keep the title backfill current The palette's backfill now loads the workspace list of every open project it has not seen — not only projects that already have session-state records — so a collapsed project's workspaces are searchable, and it re-reads its queue whenever the normalized snapshot gains or loses a workspace, so a palette opened during cold-start hydration catches up.
…s the retired switcher did
…om host providers A registry-free `search` module ranks the corpora only the host can see — file names, spec titles and bodies, TODO plans, chat titles and transcripts, and the workspaces of unexpanded projects — with the shared matcher, so the browser merges host hits with its own by tier and score. Roots are the active worktree plus every open project's folder (never sibling worktrees); spec bodies only from the current root; chats only from persisted worktrees of open projects. Per-client `seq` superseding, per-provider time budgets with `truncated`, `indexing` while a file list loads (git ls-files streamed through a capped NUL parser, a bounded walk otherwise), `failed` for a throwing provider. Previews are bounded reads: a transcript window from the history index, a root-contained text head, a cached spec body, a rendered plan. Narrow reads added where the data lives: `HistoryIndex.sessions()`, `specSnapshot` (TTL + fsChanged invalidation), `listStoredPlans`, `resolveContainedFile`. Protocol v95 (`SEARCH_PROTOCOL_VERSION`), and the matcher gains the `content` tier for body matches.
Files, specs, plans and transcript hits from search.query join the browser sections under the same tiers; the preview pane shows a transcript window around a hit and the file, spec and plan heads from search.preview.
The host searches files and specs in one root — the active worktree, or the context project's folder through its Default record on Project Home — instead of every open project's folder; search.preview keeps only the transcript window. File and spec rows render through the shared resource renderers (code, images, sandboxed HTML, markdown) injected by the shell, so resolveContainedFile, the text-head reader and the plan preview go.
… the host still holds Address the review: previous host rows stay on screen while a new request is pending; a latest reply the host marked superseded ends empty instead of spinning forever, and seq is seeded from the clock so a reloaded page never reissues a number the host has seen; late host arrivals sort after every row already on screen, across tiers, so the selected row never drops out of a capped group; a scoped query asks for the host's maximum page and the host's total counts into the more row and the group meta. On the host, a reloading file list or a spec snapshot being rebuilt reports indexing so the client polls for the fresh corpus, a failed reload keeps the stale list, and transcript hits order by time alone.
A typed query in the All or Work scope ends in one row «Start work with “…”…»; Enter on it, or Mod+Enter from any row, closes the palette and opens the shell's Create-workspace dialog with the text as the prompt, so the target, base branch and model are chosen where they always are and nothing starts without the dialog. The row is disabled offline like every host mutation and is never remembered by the learning store.
A kind:"chats" container rooted at <dataDir>/chats and one kind:"chat" workspace per chat, stored apart in chats.json (partitioned on save, merged on load, set aside when corrupt) so an older host never sees them. New wire methods chat.create (folder + record + the one session), chat.delete (the only teardown door: transcripts and the folder go to the trash, the record is marked pending first) and chat.inspect; every repository door refuses a chat or the container; git never runs in a chat folder, backed by GIT_CEILING_DIRECTORIES; sockets below CHATS_PROTOCOL_VERSION never hear about chats; session state records carry pi's session title.
…Create dialog A Chats section tops the Projects panel (one row per chat with its title, status and a Delete that names the folder's file count); the topbar reads Chats › <title> for a chat with no branch or ship strip; the Create dialog gains a "Chat without a project" target (the only one when no repository is open) and the Welcome a card for it; git- and spec-backed tool bodies say "Not available in a chat without a project" while Files, terminal and Plan keep working; repository listings and fallbacks go through selectRepoProjects, and a second New chat in a chat starts another chat.
Architecture Decision 6 gains the third kind-marked exception; goal capabilities, the workspaces, persistence, host, agent, trash, contracts, server-graph, panels, shell, location-bar, store and e2e specs describe the container, the chats store, the refusal doors, the ceiling, the scoped pushes, the titles on state records and the UI surfaces.
…nd load the Chats list on a first chat Address the review: the plan read skips the unattributed-change and adopted-commit git reads for a chat and the Plan page hides its branch line, commits menu and Open PR; the setup funnel's project_present ignores the Chats container; a first chat loads the container's list instead of seeding it; the chats store latch has a test seam.
… in Chats, the topbar and its empty center
chat.continue binds one in-memory operation to the source chat and asks its agent, through a continue-in-workspace-request card delivered as a follow-up, for a handoff summary; the continue_in_workspace tool then creates the workspace with its initial terminal, the chat on the host default model and the chat-handoff first message, remembers the link on the chat record (continuedTo) and reports the phases over a scoped chat.continuation push (protocol v97). A turn that settles without the call fails the operation; a restart loses the binding and the tool says so.
A Continue in workspace… action on the Chats row and the topbar chat segment picks the repository; the request and the handoff render as cards (phase, Open, Retry; summary, Open the original chat), the store keeps the host's continuation phases, the initiating client alone follows the new chat when it starts, and the Chats row shows where a chat was continued.
… card's phase after a reload, and reuse a failed operation's workspace Address the review: the topbar chat segment gains the action the specs already described; a continuation card the client never heard about asks chat.continuationStatus once and renders a lost binding as such; Retry from the card registers the new operation so the initiating client still follows it; a new gesture after a failure inherits the workspace and chat the failed operation had already created.
7d19fdd to
1f98c7e
Compare
| await ackSend( | ||
| sendHostMessageToSession( | ||
| targetSessionId, | ||
| CHAT_HANDOFF_CUSTOM_TYPE, |
There was a problem hiding this comment.
Non-blocking:
Problem: The destination starts with a custom chat-handoff, but deriveSessionState recognizes only user entries as run boundaries. After the first turn settles, it reports runId and completion as null and completionUnread as false (reproduced on pinned pi 1.4.0).
Failure scenario: Continue a chat and switch away or unfocus the app while the destination agent works. Its completion never lights the attention dot, enters Waiting for you, or triggers a notification; Mark as unread is also unavailable.
Suggested fix: Teach the agent-owned run/completion derivation to recognize the handoff entry as a run boundary, preserving the custom-message card, and update its state spec.
Problem
A chat without a project (#887) is where a question starts — but the moment it turns into work it needs a repository, a branch and a workspace, and today the only way there is to copy the conversation by hand into a new workspace chat. The interview asked for the opposite: keep the scratch chat, and let it hand the work over.
Fifth PR of the global-search stack; stacked on #887. Design and three review rounds in
TASK-projectless-chat.md("Continue in workspace").Rebased on
main(Oct 26).CONTINUATION_PROTOCOL_VERSIONis now v94 (the stack moved up one version behind #912). The Continue dialog is titled by the chat's displayed label (name or first prompt, as the Chats row), now that #911 is in.Approach
chat.continue { workspaceId, projectId }binds one in-memory operation to the source chat and delivers acontinue-in-workspace-requestcustom message to its session as a follow-up — a card in the transcript, queued behind a running turn, never a steer, never a fake user turn. The agent answers by calling the newcontinue_in_workspace({ summary })tool; a turn that settles without the call fails the operation with "the agent finished without a summary" and the card offers Retry.applyContinuationrevalidates the binding and the project, then creates the workspace (default base, the shared initial-terminal provisioning), the chat (host default model) and sends thechat-handoffcustom message — the clamped summary plus a link back — as the new chat's first turn, reportingfirstMessage: sent | failedapart from the phase. Each created id is recorded as soon as it exists, so a second call (or a same-host retry) reuses partial work and returns the ids. The source record keepscontinuedTo: { projectId, workspaceId, sessionId }[], a durable link that survives compaction of the transcript card.requested → summarizing → creating → started | failed | losttravel on a scopedchat.continuationpush (sockets ≥ v94) andchat.continuationStatusanswers after a reconnect; a host restart loses the binding, so the tool refuses and the UI says "ask again". The request's acceptance is awaited only 1.5 s (a refusal before the turn fails the operation) so the dialog closes while the turn runs.started, that client alone loads the project's workspaces and opens the new chat; other clients just see the cards and the Chats row's "Continued in ".ContinuationRequestCardshows the host-owned phase (asking → summarizing → creating → Continued · Open / failed · Retry);HandoffCardrenders the summary as Markdown under "Continued from “<title>”" with Open the original chat. The renderers stay props-driven —ChatTranscriptsupplies the actions and the phase lookup from the store.set_title(a global inline extension with a host-injectedContinueToolHost): per-session registration was not needed once the host refuses every call without a binding, which also keeps delegation children out.sendHostMessageToSessiongeneralizes the review-fix request path, so there is one way the host authors a message the agent answers.Changes
packages/contracts:PROTOCOL_VERSION = 94=CONTINUATION_PROTOCOL_VERSION;chat.continue/chat.continuationStatus;ChatContinuation+ phases;chat.continuationchannel;Workspace.continuedTo(ChatContinuationLink); custom messagescontinue-in-workspace-request/chat-handoffwith guards;CHAT_HANDOFF_SUMMARY_MAX_LENGTH.packages/server:agent/continueTool.ts(+sendHostMessageToSession),host/continuations.ts(single-flight, phases, tool host, settle observer), handlers,server.ts(publisher with a v94 floor, tool host, settle observation),workspaces/chats.tsrecordChatContinuation. Tests:host/continuations.test.ts(a faux agent calls the real tool: workspace, chat, handoff message, link, idempotent second call; settle-without-call fails, retry starts fresh, no binding refuses, closed project refused).apps/web:chatturn kindscontinuationRequest/handoff(hydration, live events, rows, actions, height estimates) and the two cards;ChatActionscontinuation actions provided byChatTranscript; storechatContinuations+ push;panels/chatActions.tscontinue/follow +useFollowContinuations(mounted byShell);ContinueChatDialogfrom the Chats row and the topbar chat segment; "Continued in " on the row; 24 locale keys.chats.spec.tsgains the no-agent continuation flow (card, fails without a summary, Retry);chats-continue.live.spec.ts(@agent) covers the real handoff end to end.7972b0271): the topbar chat segment offers Continue in workspace…; a card whose operation this client never heard about (a reload) askschat.continuationStatusonce and renders a lost binding aslost; Retry from the card registers the operation so the initiating client still follows it; a new gesture after a failure inherits the workspace and chat the failed operation had created.contracts,host,agent,workspaces, webchat/panels/store/locationBar; README bullet; task-spec "PR5 — Implemented".Screenshots
1280×800. Before = #887's chat without a project (no Continue action, no cards); After = the dialog, the request card, and — from the live run — the new workspace chat's handoff card and the source card marked started.
Testing
bun run lint,check:deps,check:boundaries,check:seams,check:i18n,check:spec-surface,check:compiler→ green;bunx turbo run typecheck --filter='!@thinkrail/desktop'→ 21/21bun run test→ 22/22 packages green (server 1819, web 1969, contracts 99)bun run e2e(full no-agent suite, 8 shards) → all passed on this headbun run e2e e2e/chats.spec.ts→ 3 passedbun run e2e:full e2e/chats-continue.live.spec.ts→ 1 passed (real agent: summary → tool → workspace chat with the handoff card →continuedTo→ back to the source, card started with Open)Related issues
Part of the global-search stack (#804 → #805 → #806 → #887 → this).
Checklist
bun run lint,bun run typecheck,bun run testbun run e2e, orbun run e2e:fullwhen touching agent behavior)SPEC.md/ top-level specs updated to reflect any boundary, contract, or behavior change