diff --git a/README.md b/README.md
index 0f74891bf..4b5717ac7 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,8 @@ Open http://127.0.0.1:7681 (or the address you chose above).
- Command palette (Cmd/Ctrl+K) — search terminals, switch themes, run actions
- Worktree-naming flow — drilling into `New terminal → ` opens a leaf with the worktree name pre-filled (random ADJ-NOUN, auto-selected) and an agent picker below; type a custom name and hit Enter to land in a freshly-branched worktree, or pick an agent to launch it in one step. The typed name becomes the branch name and surfaces verbatim on the workspace-switcher pill, so worktrees stay identifiable at a glance
+- Terminal intent — `Set terminal intent` attaches a short user-authored note to a live terminal. It persists with the session and appears in the tile title bar, inspector, mobile sheet, and workspace switcher search/cards
+- Queued worktrees — `Queue worktree → ` stores a repo + intent as a durable backlog item without opening a terminal. The workspace switcher's `Queued` column can later start it as a worktree; the shell/agent choice happens at start time, not when the item is queued
- Agent-aware command palette — once you've run a known agent CLI (`claude`, `aider`, `opencode`, `codex`, `goose`, `gemini`, `cursor-agent`) in any kolu terminal, it surfaces in two places: as a row in the worktree-naming leaf above (so the same Enter creates the worktree and launches the agent), and under `Debug → Recent agents` as a prefill-into-active-terminal affordance. Prompt/message flag values (`-p`/`--prompt`/`-m`/`--message`) are stripped before storage so ephemeral prompt text never lands in the persisted MRU
- Workspace-switcher pings — when an agent is waiting on you (or has finished with an unread completion), its collapsed switcher pill pulses an alert dot so you can spot it without panning. Ctrl+Tab (or Alt+Tab) cycles terminals in MRU order: hold the modifier, press Tab to advance, release to commit
- Keyboard-driven — Cmd+T new terminal, Cmd+1…Cmd+9 jump, Cmd+Shift+[ / Cmd+Shift+] cycle, Cmd+/ shortcuts help
@@ -53,7 +55,7 @@ The desktop workspace is mode-less — every terminal renders as a draggable, re
- **Infinite pan & zoom** — two-finger scroll / trackpad to pan, pinch or Ctrl+scroll to zoom. Hold Shift to force pan even with the cursor over a terminal tile (hand-tool style). No boundaries — the canvas extends freely in every direction via CSS `transform: translate() scale()` (Figma/Excalidraw model)
- **Snap-to-grid** — tiles snap to a 24px grid on drag and resize for tidy layouts
- **Maximize a tile** — double-click any tile's title bar (or click the maximize button) to fill the viewport; the maximized posture persists across reload via localStorage so you land back where you left off
-- **Floating workspace switcher** — a compact repo/branch pill strip sits at the top of the canvas, ghosted at rest and behind any tile that overlaps it; hover opens a searchable panel grouped by live agent state (`Awaiting you`, `Working`, `No agent`) with repo facets. Ordering is **agent recency** (most recent agent semantic-key transition first) with canvas position as the secondary key. The collapsed pill strip always keeps the active-agent terminals plus the user's currently-active terminal in view, then fills remaining slots with up to five idle peers per repo. Repo positions in the strip are alphabetical so they don't shuffle as agents come and go; pills within a repo follow recency order. Recency is persisted, so a restored session lands where it left off. Click a pill or card to focus and center its tile
+- **Floating workspace switcher** — a compact repo/branch pill strip sits at the top of the canvas, ghosted at rest and behind any tile that overlaps it; hover opens a searchable panel grouped by live agent state (`Awaiting you`, `Working`, `No agent`) with repo facets, plus a `Queued` column for backlog worktrees. Ordering is **agent recency** (most recent agent semantic-key transition first) with canvas position as the secondary key. The collapsed pill strip always keeps the active-agent terminals plus the user's currently-active terminal in view, then fills remaining slots with up to five idle peers per repo. Repo positions in the strip are alphabetical so they don't shuffle as agents come and go; pills within a repo follow recency order. Recency is persisted, so a restored session lands where it left off. Click a pill or card to focus and center its tile; start a queued card to create its worktree terminal
- **Switcher border encodes state** — each collapsed pill or panel card border doubles as identity (repo color) and live status: a conic-gradient sweep while the agent is `thinking`/`tool_use`, a breathing pulse while `waiting`, a static ring when the terminal is just active, and an inset glow when the active tile also has a working agent
- **Auto-park stale terminals** — when a terminal's last observed agent transition is more than 4 hours old, its switcher pill, panel card, and canvas tile fade and the agent-state border drops, so a wall of "awaiting" tiles you parked yesterday doesn't drown out the one that genuinely needs you now. It also stops counting toward the awaiting bucket and is dropped from the OS/PWA dock badge. Any fresh agent transition unparks automatically — pure derivation off `lastActivityAt`, no persisted state
- **Minimap awaiting heatmap** — the canvas minimap dots any tile whose agent is currently `waiting` (and not auto-parked) with a small alert indicator, so you can scan a 20-tile workspace for "who needs me" without opening the switcher
@@ -267,9 +269,9 @@ flowchart TB
[^client-state]: Local-only view state (active terminal, MRU order, attention flags) lives in SolidJS [signals and stores](https://docs.solidjs.com/reference/store-utilities/create-store) inside singleton `useXxx.ts` modules — separate from server-derived subscription state.
-**Persistence** — sessions auto-save to `~/.config/kolu/state.json` via [`conf`](https://github.com/sindresorhus/conf), debounced at 500 ms[^persistence].
+**Persistence** — sessions and queued worktrees auto-save to `~/.config/kolu/state.json` via [`conf`](https://github.com/sindresorhus/conf), debounced at 500 ms for terminal snapshots[^persistence].
-[^persistence]: Schema is versioned with explicit migrations. Stores CWD, sort order, and parent relationships per terminal.
+[^persistence]: Schema is versioned with explicit migrations. Terminal snapshots store CWD, git metadata, theme, intent, parent relationships, layout, sub-panel state, and agent recency; queued worktrees store repo path, optional worktree name, intent, and creation time.
[PartySocket](https://docs.partykit.io/reference/partysocket-api/) handles WebSocket auto-reconnect; `@kolu/surface/solid`'s `surfaceClient` (wired up in `packages/client/src/wire.ts`) installs oRPC's [`ClientRetryPlugin`](https://orpc.dev/docs/plugins/client-retry) so every Cell/Collection/Stream/Event hook (and the `streamCall` escape hatch for raw streams like terminal `attach`) transparently re-subscribes after a drop — every server-side streaming handler is already snapshot-then-deltas and the reducer in `useTerminalMetadata.ts` pattern-matches an `ActivityStreamEvent` discriminated union (`snapshot` replaces, `delta` appends) so re-subscribe resume is structural, not defensive. Transport events (`connecting` / `connected` / `disconnected` / `reconnected` / `restarted`) are exposed as a single `ServerLifecycleEvent` signal in `packages/client/src/rpc/rpc.ts`, and `TransportOverlay` pattern-matches it into one dim-backdrop card: `disconnected` shows "Reconnecting…" (the backdrop is pointer-events-none, so users can still scroll and read buffers underneath), and `restarted` swaps to "Server updated" with the Reload button inline in the card.
diff --git a/ci/lib.just b/ci/lib.just
index d15ff5308..537d31d3e 100644
--- a/ci/lib.just
+++ b/ci/lib.just
@@ -158,11 +158,14 @@ _summary:
echo ""
echo "━━━ CI Summary ({{ sha }}) ━━━"
# Fetch all statuses once
- all_statuses=$(gh api "repos/{{ repo }}/statuses/{{ sha }}" --jq '
- [.[] | select(.context | startswith("ci/"))]
- | group_by(.context)
- | map(max_by(.updated_at))
- | .[] | {(.context): .state}' 2>/dev/null | jq -s 'add // {}')
+ all_statuses=$(gh api --paginate "repos/{{ repo }}/statuses/{{ sha }}" \
+ --jq '.[] | select(.context | startswith("ci/"))' 2>/dev/null \
+ | jq -s '
+ sort_by(.context)
+ | group_by(.context)
+ | map(max_by(.updated_at))
+ | map({(.context): .state})
+ | add // {}')
failed=0
while IFS= read -r ctx; do
context="ci/$ctx"
diff --git a/packages/client/package.json b/packages/client/package.json
index edb518e41..ae4fd932e 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -18,9 +18,9 @@
"@kolu/solid-pierre": "workspace:*",
"@kolu/surface": "workspace:*",
"@orpc/client": "^1.13.13",
+ "@orpc/contract": "^1.13.13",
"@pierre/diffs": "^1.1.20",
"@pierre/trees": "^1.0.0-beta.3",
- "@orpc/contract": "^1.13.13",
"@solid-primitives/event-listener": "^2.4.5",
"@solid-primitives/media": "^2.3.5",
"@solid-primitives/resize-observer": "^2.1.5",
@@ -37,6 +37,7 @@
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0",
"fix-webm-duration": "^1.0.6",
+ "marked": "^18.0.2",
"memorable-names": "workspace:*",
"neverthrow": "^8.2.0",
"nonempty": "workspace:*",
diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx
index 4e88ccb7e..704daa648 100644
--- a/packages/client/src/App.tsx
+++ b/packages/client/src/App.tsx
@@ -44,7 +44,7 @@ import { useRecorder } from "./recorder/useRecorder";
import WebcamOverlay from "./recorder/WebcamOverlay";
import RightPanelLayout from "./right-panel/RightPanelLayout";
import { useRightPanel } from "./right-panel/useRightPanel";
-import { client } from "./wire";
+import { client, recentAgents } from "./wire";
import { serverProcessId, wsStatus } from "./rpc/rpc";
import TransportOverlay from "./rpc/TransportOverlay";
import ShortcutsHelp from "./ShortcutsHelp";
@@ -52,7 +52,10 @@ import { screenshotTerminal } from "./screenshotTerminal";
import { useColorScheme } from "./settings/useColorScheme";
import { useTips } from "./settings/useTips";
import TerminalContent from "./terminal/TerminalContent";
+import IntentEditorDialog from "./intent/IntentEditorDialog";
+import { useIntentEditor } from "./intent/useIntentEditor";
import TerminalMeta from "./terminal/TerminalMeta";
+import { useQueuedWorktrees } from "./terminal/useQueuedWorktrees";
import { useSubPanel } from "./terminal/useSubPanel";
import { useTerminals } from "./terminal/useTerminals";
import ModalDialog, { refocusTerminal } from "./ui/ModalDialog";
@@ -178,11 +181,29 @@ const App: Component = () => {
if (id) store.activate(id);
}
+ function handleSetTerminalIntent(intent?: string) {
+ const id = store.activeId();
+ if (!id) return;
+ crud.setIntent(id, intent);
+ }
+
const arrange = useCanvasArrange({
store,
crud,
isMobile,
});
+ const queuedWorktrees = useQueuedWorktrees({
+ handleCreateWorktree: worktree.handleCreateWorktree,
+ });
+
+ const intentEditor = useIntentEditor({
+ queuedWorktrees: queuedWorktrees.items,
+ getTerminalIntent: (id) => store.getMetadata(id)?.intent,
+ setTerminalIntent: crud.setIntent,
+ enqueueQueuedWorktree: queuedWorktrees.enqueue,
+ updateQueuedWorktreeIntent: queuedWorktrees.updateIntent,
+ onClose: () => requestAnimationFrame(refocusTerminal),
+ });
// Shared between the keyboard dispatcher and the command palette so a single
// wiring keeps both surfaces in sync. Palette-only deps (theme management,
@@ -270,8 +291,19 @@ const App: Component = () => {
handleSetTheme,
setAboutOpen,
setDiagnosticInfoOpen,
- handleCreateWorktree: (repoPath, name, initialCommand) =>
- void worktree.handleCreateWorktree(repoPath, name, initialCommand),
+ handleCreateWorktree: (repoPath, name, options) =>
+ void worktree.handleCreateWorktree(repoPath, name, options),
+ queuedWorktrees: queuedWorktrees.items,
+ openQueueWorktreeIntent: intentEditor.openNewQueued,
+ startQueuedWorktree: (id, name, initialCommand) =>
+ void queuedWorktrees.start(id, {
+ worktreeName: name,
+ agentCommand: initialCommand,
+ }),
+ deleteQueuedWorktree: queuedWorktrees.remove,
+ openActiveTerminalIntent: () =>
+ intentEditor.openActiveTerminal(store.activeId()),
+ handleSetTerminalIntent,
handleClose: () => {
const id = store.activeId();
if (id) closeTerminal(id);
@@ -397,6 +429,15 @@ const App: Component = () => {
onOpenChange={setDiagnosticInfoOpen}
activeId={store.activeId()}
/>
+ {
workspaceSwitcher={
a.command)}
activeId={store.activeId()}
getRecency={recencyOf}
openRequest={workspaceSwitcherOpenRequest()}
onSelect={store.activate}
+ onStartQueuedWorktree={(id, initialCommand) =>
+ void queuedWorktrees.start(id, {
+ agentCommand: initialCommand,
+ })
+ }
+ onDeleteQueuedWorktree={queuedWorktrees.remove}
+ onEditQueuedWorktree={intentEditor.openQueued}
+ onEditTerminalIntent={intentEditor.openTerminal}
onCreate={() => openPaletteGroup("New terminal")}
/>
}
@@ -544,7 +595,10 @@ const App: Component = () => {
onSelect={store.setActiveSilently}
onClose={(id) => closeTerminal(id)}
renderTileTitle={(id) => (
-
+ intentEditor.openTerminal(id)}
+ />
)}
renderTileTitleActions={(id) => (
└─
-
- {item.label}
+
+
+ {item.label}
+
+
+ {(intent) => (
+
+ {intent()}
+
+ )}
+
diff --git a/packages/client/src/canvas/workspace-switcher/Collapsed.tsx b/packages/client/src/canvas/workspace-switcher/Collapsed.tsx
index b5a52a495..42da2632a 100644
--- a/packages/client/src/canvas/workspace-switcher/Collapsed.tsx
+++ b/packages/client/src/canvas/workspace-switcher/Collapsed.tsx
@@ -119,7 +119,11 @@ const CollapsedWorkspaceSwitcher: Component<{
: {}),
}}
onClick={() => props.onSelect(item().id)}
- title={item().info.meta.cwd}
+ title={
+ item().info.meta.intent
+ ? `${item().info.meta.intent} - ${item().info.meta.cwd}`
+ : item().info.meta.cwd
+ }
>
void;
onRepoFilterChange: (repoName: string | null) => void;
onSelect: (id: TerminalId) => void;
+ recentAgentCommands: string[];
+ onStartQueuedWorktree: (id: string, agentCommand?: string) => void;
+ onDeleteQueuedWorktree: (id: string) => void;
+ onEditQueuedWorktree: (id: string) => void;
+ onEditTerminalIntent: (id: TerminalId) => void;
onClose: () => void;
}> = (props) => {
const store = useTerminalStore();
const tileTheme = useTileTheme();
- const columnCount = () => Math.max(1, props.model.columns.length);
+ const columnCount = () =>
+ Math.max(
+ 1,
+ props.model.columns.length +
+ (props.model.queuedWorktrees.length > 0 ? 1 : 0),
+ );
const totalCount = () =>
props.model.repoFacets.reduce((sum, facet) => sum + facet.count, 0);
let searchInputRef: HTMLInputElement | undefined;
@@ -83,7 +95,7 @@ const WorkspaceSearchPanel: Component<{
value={props.query}
onInput={(e) => props.onQueryChange(e.currentTarget.value)}
class="flex-1 min-w-0 bg-transparent border-0 outline-none font-mono text-[0.8rem] text-fg placeholder:text-fg-3/60 caret-accent"
- placeholder="repo, branch, pr, agent, cwd…"
+ placeholder="repo, intent, branch, pr, agent, cwd..."
aria-label="Search workspaces"
spellcheck={false}
autocomplete="off"
@@ -149,6 +161,55 @@ const WorkspaceSearchPanel: Component<{
"grid-template-columns": `repeat(${columnCount()}, minmax(0, 1fr))`,
}}
>
+ 0}>
+
+
+ )}
+
+
{/* Git */}
{(git) => (
diff --git a/packages/client/src/terminal/TerminalMeta.tsx b/packages/client/src/terminal/TerminalMeta.tsx
index 56ad3f875..87bb1874d 100644
--- a/packages/client/src/terminal/TerminalMeta.tsx
+++ b/packages/client/src/terminal/TerminalMeta.tsx
@@ -1,5 +1,6 @@
-/** Terminal metadata for the canvas tile title bar — two rows:
+/** Terminal metadata for the canvas tile title bar — optional intent tab + two rows:
*
+ * Intent: attached tab on the tile's top border
* Row 1: name [suffix] [worktree] [foreground] [agent progress]
* Row 2: branch [PR icon checks #N title]
*
@@ -12,8 +13,9 @@
import { prUnavailableSource, prValue } from "kolu-github/schemas";
import { type Component, Show } from "solid-js";
-import { PrStateIcon, WorktreeIcon } from "../ui/Icons";
+import { PencilIcon, PrStateIcon, WorktreeIcon } from "../ui/Icons";
import Tip from "../ui/Tip";
+import { IntentSummary } from "../intent/IntentSurface";
import ChecksIndicator from "./ChecksIndicator";
import { copyTextWithToast } from "./clipboard";
import { PrUnavailableButton } from "./PrUnavailablePopover";
@@ -21,12 +23,24 @@ import type { TerminalDisplayInfo } from "./terminalDisplay";
const TerminalMeta: Component<{
info: TerminalDisplayInfo | undefined;
+ onEditIntent?: () => void;
}> = (props) => {
const i = () => props.info;
return (
}>
{(info) => (
- <>
+
+
+ {(intent) => (
+
+ )}
+
{/* Name row — `name suffix [worktree-icon] [fg-title] [progress]`.
* Sub-count lives on the title-bar split toggle (one source
* of truth for "this tile has children"); the agent task
@@ -35,7 +49,8 @@ const TerminalMeta: Component<{
* shown by the title bar's agent indicator button — no
* separate agent row here. CWD is implicit (tooltip on the
* repo name) — visible space is reserved for the OSC 2
- * process title. */}
+ * process title. Intent gets its own attached top tab, so it
+ * never competes with or suppresses the title/process slot. */}
@@ -52,6 +67,9 @@ const TerminalMeta: Component<{
+
+ {(onEdit) => }
+
{/* Foreground process title — OSC 2 string when present.
* Replaces what used to be the cwd slot; cwd is now a
* tooltip on the repo name. `flex-1` so it fills until
@@ -144,7 +162,7 @@ const TerminalMeta: Component<{
)}
- >
+
)}
);
@@ -177,6 +195,17 @@ export const TerminalMetaCompact: Component<{
)}
+
+ {(intent) => (
+
+
+
+ )}
+
{/* Anchor stops propagation so a tap on the PR doesn't toggle
* the enclosing Drawer.Trigger. */}
@@ -217,6 +246,7 @@ export const TerminalMetaCompact: Component<{
);
};
+/** Repo/cwd identity label used by desktop and compact title bars. */
const NameSpan: Component<{ info: TerminalDisplayInfo }> = (props) => (
= (props) => (
);
+/** Attached top-border pill showing the terminal intent's first line. */
+const TerminalIntentPill: Component<{
+ intent: string;
+ onEdit?: () => void;
+}> = (props) => (
+
+);
+
+/** Empty-titlebar affordance for creating the active terminal's intent. */
+const IntentEditButton: Component<{ onEdit: () => void }> = (props) => (
+
+);
+
+/** Small title-bar marker for terminals backed by git worktrees. */
const WorktreeBadge: Component = () => (
(
);
+/** Compact completed/total progress indicator for agent task lists. */
const AgentTaskProgress: Component<{ completed: number; total: number }> = (
props,
) => (
@@ -260,6 +344,7 @@ const AgentTaskProgress: Component<{ completed: number; total: number }> = (
);
+/** Placeholder while terminal display metadata is not available yet. */
const TerminalMetaSkeleton: Component = () => (
diff --git a/packages/client/src/terminal/clipboard.ts b/packages/client/src/terminal/clipboard.ts
index 370196f58..3baca1b5d 100644
--- a/packages/client/src/terminal/clipboard.ts
+++ b/packages/client/src/terminal/clipboard.ts
@@ -1,59 +1,10 @@
-/**
- * Clipboard write with a fallback for non-secure contexts.
- *
- * `navigator.clipboard` is only defined in secure contexts (https, localhost,
- * 127.0.0.1). On plain http to any other host it is undefined, so both
- * direct `navigator.clipboard.writeText` calls and xterm's `ClipboardAddon`
- * OSC 52 handler throw `TypeError: Cannot read properties of undefined`.
- *
- * The fallback selects a hidden textarea and runs `document.execCommand("copy")`,
- * which works in any browsing context at the cost of a brief focus steal.
- */
-
import type {
ClipboardSelectionType,
IClipboardProvider,
} from "@xterm/addon-clipboard";
-import { toast } from "solid-sonner";
-
-/** Write `text` to the system clipboard, falling back to execCommand when
- * navigator.clipboard is unavailable or throws. Throws if both paths fail. */
-export async function writeTextToClipboard(text: string): Promise {
- if (navigator.clipboard?.writeText) {
- try {
- await navigator.clipboard.writeText(text);
- return;
- } catch {
- // Fall through to execCommand — navigator.clipboard can reject for
- // reasons other than missing secure context (permission denied, etc.).
- }
- }
- const textarea = document.createElement("textarea");
- textarea.value = text;
- textarea.style.position = "fixed";
- textarea.style.opacity = "0";
- document.body.appendChild(textarea);
- try {
- textarea.select();
- const ok = document.execCommand("copy");
- if (!ok) throw new Error("clipboard access blocked");
- } finally {
- document.body.removeChild(textarea);
- }
-}
+import { writeTextToClipboard } from "../clipboard";
-export async function copyTextWithToast(
- text: string,
- messages: { success: string; failure: string },
-): Promise {
- try {
- await writeTextToClipboard(text);
- toast.success(messages.success);
- } catch (err) {
- console.error(`${messages.failure}:`, err);
- toast.error(`${messages.failure}: ${(err as Error).message}`);
- }
-}
+export { copyTextWithToast, writeTextToClipboard } from "../clipboard";
/** xterm `IClipboardProvider` that uses `writeTextToClipboard` for writes
* (survives non-secure contexts) and returns empty on reads when
diff --git a/packages/client/src/terminal/useQueuedWorktrees.ts b/packages/client/src/terminal/useQueuedWorktrees.ts
new file mode 100644
index 000000000..121aeddae
--- /dev/null
+++ b/packages/client/src/terminal/useQueuedWorktrees.ts
@@ -0,0 +1,77 @@
+import type { InitialTerminalMetadata, TerminalId } from "kolu-common/surface";
+import { randomName } from "memorable-names";
+import { queuedWorktrees, setQueuedWorktrees } from "../wire";
+
+/** Client-side owner for queued-worktree lifecycle operations. */
+export function useQueuedWorktrees(deps: {
+ handleCreateWorktree: (
+ repoPath: string,
+ name: string,
+ options?: {
+ initialCommand?: string;
+ initial?: InitialTerminalMetadata;
+ },
+ ) => Promise;
+}) {
+ function enqueue(repoPath: string, intent: string): void {
+ const trimmed = intent.trim();
+ if (!trimmed) return;
+ setQueuedWorktrees([
+ ...queuedWorktrees(),
+ {
+ id: crypto.randomUUID(),
+ repoPath,
+ intent: trimmed,
+ createdAt: Date.now(),
+ },
+ ]);
+ }
+
+ function remove(id: string): void {
+ setQueuedWorktrees(queuedWorktrees().filter((q) => q.id !== id));
+ }
+
+ function updateIntent(id: string, intent: string): void {
+ const trimmed = intent.trim();
+ if (!trimmed) return;
+ setQueuedWorktrees(
+ queuedWorktrees().map((q) =>
+ q.id === id ? { ...q, intent: trimmed } : q,
+ ),
+ );
+ }
+
+ function rememberWorktreeName(id: string, worktreeName: string): void {
+ setQueuedWorktrees(
+ queuedWorktrees().map((q) => (q.id === id ? { ...q, worktreeName } : q)),
+ );
+ }
+
+ async function start(
+ id: string,
+ options: { worktreeName?: string; agentCommand?: string } = {},
+ ): Promise {
+ const item = queuedWorktrees().find((q) => q.id === id);
+ if (!item) return;
+ const worktreeName =
+ options.worktreeName?.trim() || item.worktreeName || randomName();
+ rememberWorktreeName(id, worktreeName);
+ const terminalId = await deps.handleCreateWorktree(
+ item.repoPath,
+ worktreeName,
+ {
+ initialCommand: options.agentCommand,
+ initial: { intent: item.intent },
+ },
+ );
+ if (terminalId) remove(id);
+ }
+
+ return {
+ items: queuedWorktrees,
+ enqueue,
+ remove,
+ updateIntent,
+ start,
+ };
+}
diff --git a/packages/client/src/terminal/useSessionRestore.ts b/packages/client/src/terminal/useSessionRestore.ts
index 695f28508..cf1e02266 100644
--- a/packages/client/src/terminal/useSessionRestore.ts
+++ b/packages/client/src/terminal/useSessionRestore.ts
@@ -8,6 +8,7 @@ import type {
TerminalInfo,
TerminalMetadata,
} from "kolu-common/surface";
+import { initialMetadataFromSavedTerminal } from "kolu-common/surface";
import { createEffect, createSignal } from "solid-js";
import { toast } from "solid-sonner";
import { lifecycle } from "../rpc/rpc";
@@ -170,12 +171,10 @@ export function useSessionRestore(deps: {
// so the canvas cascade effect sees the saved layout on its first run
// and skips the default-cascade branch (#642).
for (const t of topLevel) {
- const newId = await deps.handleCreate(t.cwd, {
- themeName: t.themeName,
- canvasLayout: t.canvasLayout,
- subPanel: t.subPanel,
- lastActivityAt: t.lastActivityAt,
- });
+ const newId = await deps.handleCreate(
+ t.cwd,
+ initialMetadataFromSavedTerminal(t),
+ );
oldToNew.set(t.id, newId);
// Client-side sub-panel state (activeSubTab, focusTarget) isn't
// server-persisted — seed it locally so the restored panel reopens
diff --git a/packages/client/src/terminal/useTerminalCrud.ts b/packages/client/src/terminal/useTerminalCrud.ts
index 77ba2c2a1..249a50fa4 100644
--- a/packages/client/src/terminal/useTerminalCrud.ts
+++ b/packages/client/src/terminal/useTerminalCrud.ts
@@ -47,6 +47,16 @@ export function useTerminalCrud(deps: {
);
}
+ /** Set or clear a terminal's user-authored intent. */
+ function setIntent(id: TerminalId, intent?: string) {
+ const next = intent?.trim() || undefined;
+ void client.terminal
+ .setIntent({ id, intent: next })
+ .catch((err: Error) =>
+ toast.error(`Failed to save intent: ${err.message}`),
+ );
+ }
+
/** Persist a terminal's canvas tile position/size on the server. */
function setCanvasLayout(id: TerminalId, layout: CanvasLayout) {
void client.terminal
@@ -122,13 +132,14 @@ export function useTerminalCrud(deps: {
(peerBgs
? pickTheme(availableThemes, { spread: true, peerBgs })
: undefined);
+ const nextInitial: InitialTerminalMetadata = {
+ ...initial,
+ themeName: theme,
+ };
const info = await client.terminal
.create({
cwd,
- themeName: theme,
- canvasLayout: initial?.canvasLayout,
- subPanel: initial?.subPanel,
- lastActivityAt: initial?.lastActivityAt,
+ initial: nextInitial,
})
.catch((err: Error) => {
toast.error(`Failed to create terminal: ${err.message}`);
@@ -211,6 +222,7 @@ export function useTerminalCrud(deps: {
return {
setThemeName,
+ setIntent,
setCanvasLayout,
removeAndAutoSwitch,
handleCreate,
diff --git a/packages/client/src/terminal/useWorktreeOps.ts b/packages/client/src/terminal/useWorktreeOps.ts
index 17e332980..b928468df 100644
--- a/packages/client/src/terminal/useWorktreeOps.ts
+++ b/packages/client/src/terminal/useWorktreeOps.ts
@@ -1,13 +1,16 @@
/** Worktree operations — create and remove git worktrees with associated terminals. */
-import type { TerminalId } from "kolu-common/surface";
+import type { InitialTerminalMetadata, TerminalId } from "kolu-common/surface";
import { toast } from "solid-sonner";
import { client } from "../wire";
import type { TerminalStore } from "./useTerminalStore";
export function useWorktreeOps(deps: {
store: TerminalStore;
- handleCreate: (cwd?: string) => Promise;
+ handleCreate: (
+ cwd?: string,
+ initial?: InitialTerminalMetadata,
+ ) => Promise;
handleKill: (id: TerminalId) => Promise;
}) {
const { store } = deps;
@@ -15,13 +18,19 @@ export function useWorktreeOps(deps: {
async function handleCreateWorktree(
repoPath: string,
name: string,
- initialCommand?: string,
- ) {
+ options: {
+ initialCommand?: string;
+ initial?: InitialTerminalMetadata;
+ } = {},
+ ): Promise {
const id = toast.loading("Creating worktree…");
try {
const result = await client.git.worktreeCreate({ repoPath, name });
toast.success(`Created worktree at ${result.path}`, { id });
- const newTerminalId = await deps.handleCreate(result.path);
+ const newTerminalId = await deps.handleCreate(
+ result.path,
+ options.initial,
+ );
// Recent repos update reactively via trackRecentRepo → publishSystem
// Optional initial command (phase 2 of #452): write the agent command
@@ -35,13 +44,14 @@ export function useWorktreeOps(deps: {
// server-side createTerminal parameter gated on a shell-ready
// signal (OSC 133;A prompt mark) — a contract change deliberately
// deferred out of phase 2 scope.
- if (initialCommand !== undefined) {
+ if (options.initialCommand !== undefined) {
await client.terminal
- .sendInput({ id: newTerminalId, data: `${initialCommand}\r` })
+ .sendInput({ id: newTerminalId, data: `${options.initialCommand}\r` })
.catch((err: Error) =>
toast.error(`Failed to start agent: ${err.message}`),
);
}
+ return newTerminalId;
} catch (err) {
// Toast surfaces the message; don't rethrow — the caller (palette
// value-mode onSubmit) is fire-and-forget, and a rethrow leaks as
@@ -50,6 +60,7 @@ export function useWorktreeOps(deps: {
toast.error(`Failed to create worktree: ${(err as Error).message}`, {
id,
});
+ return undefined;
}
}
diff --git a/packages/client/src/ui/Icons.tsx b/packages/client/src/ui/Icons.tsx
index c2dfdf910..1fc455fb7 100644
--- a/packages/client/src/ui/Icons.tsx
+++ b/packages/client/src/ui/Icons.tsx
@@ -140,6 +140,23 @@ export const CloseIcon: Component<{ class?: string }> = (props) => (
);
+/** Copy — overlapping sheets for clipboard-style copy actions. */
+export const CopyIcon: Component<{ class?: string }> = (props) => (
+
+);
+
export const GitMergeIcon: Component<{ class?: string }> = (props) => (
);
+/** Pencil — edit affordance for user-authored metadata. */
+export const PencilIcon: Component<{ class?: string }> = (props) => (
+
+);
+
/** Resume/play icon — right-pointing triangle. Mirrors `PauseIcon`. */
export const ResumeIcon: Component<{ class?: string }> = (props) => (