* feat(app): fork an agent session mid-run from the tab menu
Forking already existed, but only on the footer of a *completed*
assistant turn. While an agent is running, the in-flight turn renders
RunningTurnFooter with no fork menu, so exploring another path meant
scrolling back to hunt for an older turn's footer — or waiting for the
run to finish.
Add two entries to the agent tab context menu ("Fork chat in a new tab",
"Fork chat in a new workspace"), reachable regardless of run state. They
omit the fork boundary, and selectForkContextRows projects the whole
timeline when no boundary is given, so a mid-run fork carries everything
up to now — including the partially streamed in-flight turn.
Fork logic moves out of agent-stream/view.tsx into a shared useForkAgent
hook so the turn footer and the tab menu share one code path; the
per-turn behavior is unchanged (it still pins a boundary to its turn).
No server, protocol, or capability changes — this reuses the existing
agent.fork_context RPC and agentForkContext host feature. The daemon
handler is already a pure in-memory timeline read, so it was safe
mid-turn before this change.
* fix(app): resolve missing agent records before failing a fork
handleForkAgent read the agent record from the session store and gave up
if it was absent from both agents and agentDetails. A persisted agent tab
can outlive its record — restored on startup before hydration — so both
fork entries failed with a generic toast even though the agent exists on
the daemon and is perfectly forkable.
Fall back to client.fetchAgent and store the result before failing,
mirroring the resolution path agent-panel.tsx already uses. Only report
forkFailed when the agent genuinely cannot be resolved; a disconnected
host now says so instead.
The resolution logic lands in resolveForkAgentSource with injected
dependencies, since workspace-screen.tsx has no handler-level test
harness and a sibling testable module is the pattern in this directory.
storeFetchedAgentDetail moves out of panels/agent-panel.tsx into
utils/agent-detail-store.ts so the workspace screen doesn't import a
panel module — and into its own file rather than agent-directory-sync.ts,
which would close an import cycle through legacy-daemon-workspaces.
* feat(app): fork from the in-flight turn instead of the tab menu
Move the mid-run fork affordance to where the fork button already lives:
the assistant turn footer, beside the progress loader.
RunningTurnFooter previously showed only a spinner and elapsed timer, so
the fork button vanished for the whole duration of a run. It now renders
AssistantForkMenu alongside them, reachable the moment a run starts.
The in-flight fork passes no boundary, so selectForkContextRows projects
the whole timeline and the fork captures the response being streamed. A
fork button sitting next to a live response that silently dropped that
response would be worse than no button. Completed turns still pin their
own boundary, enforced by a separate handler type that keeps boundary
required.
layout.ts is untouched — the affordance is added to the running footer
component rather than by lifting the footer suppression, so no copy or
duration affordance leaks onto an incomplete turn.
Reverts the workspace tab context menu approach, along with the agent
resolution helper and store-writer extraction that existed only to serve
it. The footer path already holds the agent record.
* test(app): cover the in-flight turn fork with a Playwright spec
Extends the existing assistant fork menu spec rather than adding a
sibling file, so the completed-turn and in-flight cases sit side by side.
The new test pins the behavior that defines the feature: the in-flight
fork omits the boundary fields entirely, while the completed-turn fork
sends one. Both assertions read the actual agent.fork_context frames off
the WebSocket, using the read-only page.on("websocket") idiom from
helpers/timeline-delivery.ts so the observer cannot perturb the flow it
measures. Note the request omits the fields rather than nulling them —
daemon-client spreads them conditionally — so absence is what's asserted
there, and null only on the response.
Also asserts the fork trigger is scoped inside turn-working-indicator,
that no copy or duration affordance leaks onto the incomplete turn, that
the attachment carries the streamed text, and that the source agent keeps
streaming afterwards.
* test(app): cover the in-flight fork call site, gate, and teardown
Three behaviors this PR introduced had no coverage below the Playwright
spec. A copy-paste of resolveAssistantTurnForkBoundary into
handleForkInFlightTurn would have left every unit test green.
view-fork-in-flight.test.tsx mounts the real AgentStreamView with the
render strategy stubbed to the live-auxiliary slot, so handleForkInFlightTurn,
useStableEvent and the readOnly expression all stay real. It asserts the
fork request has no boundary KEY — matching the wire contract, where the
field is absent rather than null — for both targets, and that the read-only
gate withholds the trigger, with positive controls either side so an empty
tree cannot pass for a working gate.
turn-footer.test.tsx covers the race this PR created: the fork menu now
mounts only while a run is in flight, so a fork can be pending when the
subtree is torn down. The behavior is benign — React 18 makes the orphaned
setState a no-op — so the test asserts the observable outcome and leaves a
tripwire for unhandled rejections, since AssistantForkMenu's finally has no
catch.
Each test was verified by mutation: adding boundary: undefined, dropping the
readOnly gate, and keeping the running subtree alive each fail the matching
test.
Scoped to what this PR changed. The relocated hook's error paths, draft
setup construction, and attachment metadata predate it and stay untested
here.
* test(app): replace mocked fork coverage with Playwright
---------
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
13 KiB
13 KiB
Paseo Glossary
Authoritative terminology. UI label wins. Don't invent synonyms; use what's here.
- Project — A stable, exact selected-root record. Its host-local
projectIdis an opaqueprj_<16 hex>value. Its persistedprojectKeyis an opaque equivalence key that may group the logical project across hosts. A normalized Git remote is the current key producer, but consumers must not parse or rederive it. Git facts can update mutable kind and grouping metadata but never the ID, root, or default display name. UI: "Project" / "Add project". Forbidden: "Repo", "Repository" as UI label. - Workspace — One concrete
cwdon one daemon, with git state; belongs to exactly one project. Itsidis opaque workspace identity; itscwdis the filesystem directory. UI: "Workspace". Code:WorkspaceDescriptorPayload(packages/protocol/src/messages.ts:2178). Don't confuse with: Branch (one branch can back many workspaces via worktrees). Forbidden: "Folder", "Directory" as UI label. - Archive workspace — Removes one workspace from active use and archives everything it owns. UI, CLI, and MCP always say "Archive workspace", regardless of backing. The daemon leaves ordinary directories intact and removes a Paseo-owned worktree only when no active workspace still references it.
- Workspace kind —
"directory" | "local_checkout" | "worktree". The git-derived, persisted property of a workspace, used across its lifetime (archive safety, sidebar, grouping). Derived from the cwd's git reality byderiveWorkspaceKindinworkspace-registry-model.ts, not stored from a user choice. Don't confuse with Isolation (the create-time intent). - Isolation — Create-time choice for a new workspace: reuse the existing checkout (Local) or cut a dedicated git worktree (New worktree). A transient setup input, also remembered as a create-form preference; it is not a workspace property. UI: "Isolation" control on the New Workspace screen. Code:
isolation("local" | "worktree"),useWorkspaceIsolation(packages/app/src/screens/new-workspace-screen.tsx); persisted asFormPreferences.isolation(packages/app/src/create-agent-preferences/preferences.ts). Distinct from Workspace kind, which is the git-derived property the intent produces (Local →local_checkoutordirectoryby git-ness; New worktree →worktree). On the wire it is the create request'ssource.kind(directory | worktree,packages/protocol/src/messages.ts:1693). - Agent — See Agent session. UI still says "Agent" / "New Agent" in places, but moving toward Agent session as the canonical term. Code:
AgentSnapshotPayload(packages/protocol/src/messages.ts:608). Forbidden: "Task", "Job", "Run". - Daemon — Local Paseo server process; identified by
serverId. UI: "Daemon" (system contexts only). Code:serverIdinServerInfoStatusPayloadSchema(packages/protocol/src/messages.ts:1936),DaemonClient(packages/client/src/daemon-client.ts). - Host — Client-side connection profile pointing at a daemon; bundles one or more
HostConnections. UI: "Host" / "Add host" / "Switch host". Code:HostProfile(packages/app/src/types/host-connection.ts:37). Forbidden: "Connection" (meansHostConnection, not host). - Project host entry — One row in a project for a single (project, daemon) pair, aggregating that daemon's workspaces in the project. Internal. Code:
ProjectHostEntry(packages/app/src/utils/projects.ts:11). Don't introduce "Checkout" as a synonym. - Placement — One workspace's stable foreign-key relationship to its project plus its git checkout snapshot. Internal. An explicit creation
projectIdis authoritative when active. - Branch — Plain git branch. UI: "Switch branch". Code:
currentBranchinWorkspaceGitRuntimePayloadSchema(packages/protocol/src/messages.ts:2136);BranchSwitcher(packages/app/src/components/branch-switcher.tsx). - Forge — Git hosting service behind Paseo's change-request features: GitHub, GitLab, Gitea, Forgejo, or a future registered adapter. Code:
ForgeService,forge-registry,forge-resolver. Useforgefor internal abstraction and registry IDs; use concrete forge names only when a behavior or RPC is forge-specific. - Change request — Forge-neutral term for a proposed branch-to-branch code change. UI normally renders the forge noun instead: GitHub/Gitea/Forgejo "PR", GitLab "MR". Code:
forge_change_requestattachments,checkoutSource: { kind: "change_request" }, and PR/MR status payloads. - MR — GitLab merge request. UI label for GitLab change requests only; do not use MR for GitHub/Gitea/Forgejo.
- Worktree — Paseo-managed git worktree (
~/.paseo/worktrees/{name}); also aworkspaceKindvalue. User-facing creation treats it as theworktreeworkspace isolation choice. Code andpaseo.jsonretain worktree terminology for git lifecycle implementation. Forbidden: "Checkout" as a product synonym. - Repository / Remote — Internal Git observations. They may produce mutable kind, branch, and project-grouping metadata but never the host-local project ID, root, display name, or workspace membership. No UI label.
- Directory-backed surface — A right-sidebar surface whose content is determined by the workspace's
cwd, so two workspaces on the same directory see identical content: git diff/status, forge change-request info, file preview/explorer contents. Keyed by(serverId, cwd), neverworkspaceId. See architecture.md. - Workspace-owned state — Per-workspace state that never leaks to a same-
cwdsibling: tabs, agents, terminals, panes, title, plus review drafts, diff-mode overrides, composer attachments, and file-explorer open/expand state. Keyed byworkspaceId(cwdonly as a fallback for old payloads). See architecture.md. - Workspace status bucket — Aggregate activity signal for a workspace row. Same-
cwdworkspaces intentionally share agent and terminal status buckets, while tab, agent, and terminal visibility remains scoped byworkspaceId. - Agent session — One running instance of an agent inside a workspace (one provider, one model, one cwd, one timeline). The conceptual unit; in the UI this opens as a tab. Moving toward this as the canonical term over "Agent". Code:
AgentSnapshotPayload(packages/protocol/src/messages.ts:608). - Session — Two senses: (a) per-client connection to a daemon, internal; (b) user-facing agent session, see Agent session. Code:
Session(packages/server/src/server/session.ts) for (a). Don't confuse with: provider-side agent session log. - Profile — Internal name for the persisted shape of a host. Code:
HostProfile(packages/app/src/types/host-connection.ts:37). Never user-facing. - Provider — Agent backend (Claude Code, Codex, Copilot, OpenCode, Pi, Oh My Pi). UI: "Provider". Code:
ProviderSnapshotEntry(packages/protocol/src/messages.ts:198). - Model — A specific LLM offered by a provider. UI: "Model" / "Select model". Code:
AgentModelDefinition(packages/protocol/src/messages.ts:187). - Tab — UI surface representing one session inside a workspace. Not a conceptual unit; use Agent session when talking about the model. Code:
WorkspaceTabDescriptor(packages/app/src/screens/workspace/workspace-tabs-types.ts). - Terminal — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code:
TerminalStreamFrame(packages/protocol/src/terminal-stream-protocol.ts). - Schedule — Cron-style trigger that creates new agents. UI: CLI/MCP (
paseo schedule,create_schedule). Don't confuse with: Heartbeat (cron prompt back into the same agent) or Loop (iterative re-execution of one agent). - Heartbeat — Ephemeral cron prompt sent back into the same agent/conversation. Agent surfaces expose create, update cron, and delete only. Use for reminders and babysitting where status should return inline.
- Mode — Provider-specific operational mode (plan, default, full-access, …). UI: icon-only. Code:
modeIdinAgentSessionConfig(packages/protocol/src/messages.ts:257). - Attachment — External or local context bound to an agent prompt: forge issue/change request, review context, uploaded file, text, or image. UI: "Attach issue or PR/MR". Code:
AgentAttachment(packages/protocol/src/messages.ts:782). - Composer — The whole prompt surface for sending work to an agent. Code:
Composer(packages/app/src/composer/index.tsx). Don't call this "message input" except for the text-entry subcomponent. - Composer input — The text-entry surface inside the composer. Code:
MessageInput(packages/app/src/composer/input/input.tsx). - Composer toolbar — The bottom control row inside the composer input. Contains agent controls, attachment button, voice controls, and stop/send controls. Code:
leftContent,beforeVoiceContent, andrightContentslots inMessageInput(packages/app/src/composer/input/input.tsx). Forbidden: "Status bar". - Agent controls — Provider, model, mode, thinking, and provider-feature controls for an agent or draft agent. Code:
AgentControls/DraftAgentControls(packages/app/src/composer/agent-controls/index.tsx). Forbidden: "Agent status bar". - Composer footer — Optional area rendered below the composer input but still inside the keyboard-shifted composer layout. Code:
Composer.footer(packages/app/src/composer/index.tsx). - Composer track — A contextual lane above the composer input. Specific tracks use the
<thing> trackform: Queue track, Subagents track. Code: queue track insideComposer(packages/app/src/composer/index.tsx),SubagentsTrack(packages/app/src/subagents/track.tsx). - Subagent — User-facing term for an agent session related to a parent agent session. Use subagents in UI copy and docs. Internal daemon/provider plumbing may say "child agent" or
child_session, especially for provider-managed imports; do not surface "child agent" as a product term. - Attachment tray — The selected-attachments row inside the composer input, above the text input. Code:
renderAttachmentTray(packages/app/src/composer/index.tsx). Forbidden: "Attachment bar". - Fork — Starting a NEW agent session seeded with a copy of another session's curated chat history. The history rides along as a
chat_historycomposer attachment on a draft; the fork does not resume or branch the provider-side session, and the source session keeps running untouched. Two targets, one surface, one RPC (agent.fork_context, gated on theagentForkContexthost feature). UI: the fork menu lives in the assistant turn footer and offers "Fork in a new tab" / "Fork in a new workspace". On a completed turn it sits beside the copy button and pins a boundary to that turn, so the fork stops where the turn ended. On the in-flight turn it sits beside the progress loader and elapsed timer, and omits the boundary entirely —selectForkContextRowsprojects the whole timeline when no boundary is given, so the fork captures everything up to now, including the response still streaming. That omission is what makes mid-run forking work; pinning a boundary there would silently drop the live response. Forbidden on provider-subagent (read-only) panes. Code:useForkAgent(packages/app/src/hooks/use-fork-agent.ts),selectForkContextRows(packages/server/src/server/agent/activity-curator.ts). Don't confuse with: git fork (unrelated; Paseo has no such concept), Worktree (filesystem isolation, not history copying). - Conflict — Two distinct senses; do NOT use the bare word in UI copy without qualifying which: (a) stale-write conflict on
paseo.json("Config changed on disk", codestale_project_config,packages/app/src/screens/project-settings-screen.tsx:593); (b) git merge conflict (no current UI string). - Product discussion — Where non-bug product talk goes: GitHub Discussions and
#productin Discord. Covers workflows, features, and product direction. The container term; the ask inside it is always a workflow — what you're doing, how you do it today, where Paseo gets in the way. Issues are bugs only. Forbidden: "Feature request" as a Paseo surface, label, or invitation; the phrase is only used when answering someone who arrived with it. Don't invent "feature discussion", "idea", or "workflow thread" as synonyms. See CONTRIBUTING.md.
Inconsistencies (documented, not papered over)
- CLI
--host <host>description"Daemon host target"(packages/cli/src/utils/command-options.ts:5) blurs daemon/host; the app keeps them distinct. WorkspaceDescriptorPayloadSchema.workspaceKindaccepts legacy"checkout"on the wire (packages/protocol/src/messages.ts:2187) whilePersistedWorkspaceKinddoes not (packages/server/src/server/workspace-registry-model.ts:8).