@workerdeck/ui 0.23.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
  2. package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
  3. package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
  4. package/build/format-B8ZM9LPy.d.mts +16 -0
  5. package/build/format.d.mts +8 -75
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +63 -1138
  8. package/build/index.mjs +4 -300
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +15 -23
  11. package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
  12. package/build/status-C0HSLZas.mjs.map +1 -0
  13. package/build/workspace.d.mts +5 -126
  14. package/build/workspace.mjs +11 -143
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +4 -4
  17. package/src/components/agent/CodeEditor.tsx +31 -103
  18. package/src/components/agent/Composer.tsx +114 -333
  19. package/src/components/agent/ContextDialog.tsx +20 -41
  20. package/src/components/agent/ContextRing.tsx +6 -28
  21. package/src/components/agent/Conversation.tsx +21 -33
  22. package/src/components/agent/EditorTabs.tsx +39 -67
  23. package/src/components/agent/EngineIcon.tsx +29 -72
  24. package/src/components/agent/EntryIcon.tsx +16 -0
  25. package/src/components/agent/FileCard.tsx +10 -16
  26. package/src/components/agent/FileTree.tsx +52 -125
  27. package/src/components/agent/FileViewer.tsx +30 -60
  28. package/src/components/agent/HostFilesDialog.tsx +40 -69
  29. package/src/components/agent/Loader.tsx +3 -24
  30. package/src/components/agent/McpDialog.tsx +64 -137
  31. package/src/components/agent/Message.tsx +2 -21
  32. package/src/components/agent/ModelSelect.tsx +33 -62
  33. package/src/components/agent/PermissionModeSelect.tsx +21 -79
  34. package/src/components/agent/PermissionPrompt.tsx +59 -63
  35. package/src/components/agent/ProjectIcon.tsx +228 -93
  36. package/src/components/agent/PromptTokenText.tsx +10 -16
  37. package/src/components/agent/QuestionPrompt.tsx +43 -64
  38. package/src/components/agent/Reasoning.tsx +13 -16
  39. package/src/components/agent/Response.tsx +3 -9
  40. package/src/components/agent/Scrubber.tsx +31 -80
  41. package/src/components/agent/SessionBrowser.tsx +64 -243
  42. package/src/components/agent/SessionEmptyState.tsx +10 -40
  43. package/src/components/agent/SessionInfoDialog.tsx +46 -78
  44. package/src/components/agent/SessionItem.tsx +53 -320
  45. package/src/components/agent/SessionList.tsx +23 -39
  46. package/src/components/agent/SessionPanel.tsx +315 -996
  47. package/src/components/agent/SessionStatusIcon.tsx +14 -28
  48. package/src/components/agent/SessionSteps.tsx +34 -187
  49. package/src/components/agent/SessionWorkspace.tsx +104 -219
  50. package/src/components/agent/SkillsDialog.tsx +35 -80
  51. package/src/components/agent/StatusBar.tsx +51 -136
  52. package/src/components/agent/SubagentStrip.tsx +12 -77
  53. package/src/components/agent/ToolCallCard.tsx +94 -135
  54. package/src/components/agent/Transcript.tsx +103 -1288
  55. package/src/components/agent/TranscriptItemView.tsx +155 -0
  56. package/src/components/agent/TranscriptRows.tsx +418 -0
  57. package/src/components/agent/UsageDialog.tsx +7 -29
  58. package/src/components/agent/UsageMeters.tsx +18 -73
  59. package/src/components/agent/pulse.tsx +9 -32
  60. package/src/components/agent/scrubber-marks.ts +33 -103
  61. package/src/components/agent/status.ts +1 -5
  62. package/src/components/agent/tool-result-fetch.tsx +3 -25
  63. package/src/components/agent/tool-result-image.tsx +32 -111
  64. package/src/components/agent/transcript-rows.ts +41 -118
  65. package/src/components/agent/transcript-variant.tsx +3 -80
  66. package/src/components/agent/use-height-epoch.ts +10 -24
  67. package/src/components/agent/use-path-links.ts +45 -63
  68. package/src/components/agent/use-subagent-frame.ts +106 -0
  69. package/src/components/agent/use-transcript-jumps.ts +49 -107
  70. package/src/components/prompt-area/animated-placeholder.tsx +6 -5
  71. package/src/components/prompt-area/clipboard-helpers.ts +26 -21
  72. package/src/components/prompt-area/cursor-helpers.ts +79 -37
  73. package/src/components/prompt-area/dom-helpers.ts +97 -69
  74. package/src/components/prompt-area/file-strip.tsx +90 -80
  75. package/src/components/prompt-area/html-to-markdown.ts +67 -46
  76. package/src/components/prompt-area/image-strip.tsx +8 -18
  77. package/src/components/prompt-area/index.ts +2 -15
  78. package/src/components/prompt-area/prompt-area-engine.ts +100 -115
  79. package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
  80. package/src/components/prompt-area/prompt-area.tsx +31 -38
  81. package/src/components/prompt-area/remove-button.tsx +3 -9
  82. package/src/components/prompt-area/segment-helpers.ts +4 -12
  83. package/src/components/prompt-area/trigger-popover.tsx +15 -27
  84. package/src/components/prompt-area/trigger-presets.ts +0 -10
  85. package/src/components/prompt-area/types.ts +3 -8
  86. package/src/components/prompt-area/use-chip-editing.ts +305 -0
  87. package/src/components/prompt-area/use-markdown-mode.ts +7 -17
  88. package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
  89. package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
  90. package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
  91. package/src/components/prompt-area/use-prompt-area.ts +170 -896
  92. package/src/components/prompt-area/use-trigger-search.ts +21 -19
  93. package/src/components/terminal/PermissionPrompt.tsx +56 -59
  94. package/src/components/terminal/QuestionPrompt.tsx +28 -79
  95. package/src/components/terminal/StatusLine.tsx +13 -63
  96. package/src/components/terminal/TerminalTranscript.tsx +44 -160
  97. package/src/components/terminal/affordances.tsx +68 -98
  98. package/src/components/terminal/blocks.ts +20 -156
  99. package/src/components/terminal/diff.tsx +19 -67
  100. package/src/components/terminal/height.ts +163 -313
  101. package/src/components/terminal/image-box.ts +0 -44
  102. package/src/components/terminal/items.tsx +150 -305
  103. package/src/components/terminal/markdown.tsx +36 -95
  104. package/src/components/terminal/press.tsx +26 -53
  105. package/src/components/terminal/prompt.tsx +40 -170
  106. package/src/components/terminal/result-preview.ts +8 -56
  107. package/src/components/terminal/row.tsx +11 -98
  108. package/src/components/terminal/scrubber.tsx +108 -401
  109. package/src/components/terminal/surface.tsx +6 -54
  110. package/src/components/terminal/todos.ts +66 -0
  111. package/src/components/terminal/tool-run.ts +21 -192
  112. package/src/components/ui/AlertDialog.tsx +8 -23
  113. package/src/components/ui/Badge.tsx +18 -29
  114. package/src/components/ui/Button.tsx +7 -15
  115. package/src/components/ui/Card.tsx +2 -13
  116. package/src/components/ui/CodeBlock.tsx +10 -37
  117. package/src/components/ui/CopyButton.tsx +10 -23
  118. package/src/components/ui/Dialog.tsx +23 -36
  119. package/src/components/ui/Empty.tsx +6 -29
  120. package/src/components/ui/Input.tsx +14 -16
  121. package/src/components/ui/Menu.tsx +6 -18
  122. package/src/components/ui/PortalScope.tsx +1 -21
  123. package/src/components/ui/ProgressRing.tsx +8 -19
  124. package/src/components/ui/Select.tsx +18 -31
  125. package/src/components/ui/Sonner.tsx +1 -2
  126. package/src/components/ui/Spinner.tsx +1 -1
  127. package/src/components/ui/Splitter.tsx +30 -64
  128. package/src/components/ui/Textarea.tsx +2 -5
  129. package/src/components/ui/Tooltip.tsx +3 -13
  130. package/src/format.ts +0 -9
  131. package/src/index.ts +16 -113
  132. package/src/lib/clipboard.ts +8 -28
  133. package/src/lib/css.ts +8 -0
  134. package/src/lib/format.ts +101 -86
  135. package/src/lib/plan-request.ts +16 -0
  136. package/src/lib/status.ts +45 -95
  137. package/src/lib/tool-icon.ts +39 -47
  138. package/src/lib/utils.ts +1 -3
  139. package/src/styles/terminal.css +81 -49
  140. package/src/styles/theme.css +96 -32
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-BobynUo4.d.mts +0 -669
  143. package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. package/build/status-BE-zg88x.mjs.map +0 -1
@@ -1,69 +1,20 @@
1
1
  import type { CSSProperties, HTMLAttributes } from 'react'
2
2
  import { cn } from '../../lib/utils.ts'
3
- import {
4
- AffordanceProvider,
5
- resolveAffordances,
6
- type TerminalAffordances,
7
- } from './affordances.tsx'
3
+ import { AffordanceProvider, resolveAffordances, type TerminalAffordances } from './affordances.tsx'
8
4
 
9
- /**
10
- * The root of the terminal theme: the element that establishes the character
11
- * cell every row inside it lands on.
12
- *
13
- * It exists as a component rather than a class because the cell has to be *set*
14
- * somewhere — the font, the size and the line height together are what make
15
- * `1ch` mean one column and `--term-line` mean one row — and because that is the
16
- * one place a host is allowed to change the metrics. Everything below this
17
- * element measures itself in those two units and never in pixels.
18
- *
19
- * The geometry and the palette live in `styles/terminal.css`; this only names
20
- * the element and hands down the two numbers.
21
- */
22
5
  export interface TerminalSurfaceProps extends HTMLAttributes<HTMLDivElement> {
23
- /**
24
- * Cell size in **whole pixels**. Fractional values put every other row on a
25
- * half-pixel: text softens and the diff bands show a seam, which is exactly
26
- * what a grid renderer must not do. Defaults (13/18) are the CLI's own.
27
- */
28
6
  fontSize?: number
29
7
  lineHeight?: number
30
- /**
31
- * How far a full-bleed band reaches past the content edge — normally the
32
- * scroller's own horizontal padding. A band cancels it with matched negative
33
- * margin and padding, so a diff hunk's wash runs to the viewport edge the way
34
- * a terminal's line does, instead of stopping at a gutter.
35
- */
36
8
  bleed?: string
37
- /**
38
- * The things a real terminal cannot do — the pointer hover fill, the
39
- * hover-revealed copy actions. `false` turns them all off, an object picks.
40
- * Default: all on. See {@link TerminalAffordances}; none of them costs layout,
41
- * so switching them off changes no glyph's position.
42
- */
43
9
  affordances?: TerminalAffordances | boolean
44
10
  }
45
11
 
46
- export function TerminalSurface({
47
- fontSize,
48
- lineHeight,
49
- bleed,
50
- affordances,
51
- className,
52
- style,
53
- children,
54
- ...props
55
- }: TerminalSurfaceProps) {
12
+ export function TerminalSurface({ fontSize, lineHeight, bleed, affordances, className, style, children, ...props }: TerminalSurfaceProps) {
56
13
  const resolved = resolveAffordances(affordances)
57
14
  return (
58
15
  <div
59
- data-terminal=''
60
- // A space-separated list so CSS can ask for one with `~=` — the styling
61
- // half of the switch, where the JS half (whether a button exists at all)
62
- // rides the context.
63
- data-affordances={
64
- [resolved.hover && 'hover', resolved.actions && 'actions'].filter(Boolean).join(' ') ||
65
- undefined
66
- }
16
+ data-terminal=""
17
+ data-affordances={[resolved.hover && 'hover', resolved.actions && 'actions'].filter(Boolean).join(' ') || undefined}
67
18
  className={cn('min-w-0', className)}
68
19
  style={
69
20
  {
@@ -73,7 +24,8 @@ export function TerminalSurface({
73
24
  ...style,
74
25
  } as CSSProperties
75
26
  }
76
- {...props}>
27
+ {...props}
28
+ >
77
29
  <AffordanceProvider value={resolved}>{children}</AffordanceProvider>
78
30
  </div>
79
31
  )
@@ -0,0 +1,66 @@
1
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed'
2
+
3
+ export type TodoEntry = { status: TodoStatus; text: string }
4
+
5
+ export type TodoPreview = { summary: string; shown: TodoEntry[]; more?: string }
6
+
7
+ const PREVIEW_TODOS = 8
8
+
9
+ const GLYPH: Record<TodoStatus, string> = { pending: '☐', in_progress: '◐', completed: '☒' }
10
+
11
+ function todoEntry(value: unknown): TodoEntry | undefined {
12
+ if (typeof value !== 'object' || value === null) {
13
+ return undefined
14
+ }
15
+ const raw = value as { content?: unknown; status?: unknown; activeForm?: unknown }
16
+ const status = raw.status
17
+ if (status !== 'pending' && status !== 'in_progress' && status !== 'completed') {
18
+ return undefined
19
+ }
20
+ const content = typeof raw.content === 'string' ? raw.content.trim() : ''
21
+ if (content === '') {
22
+ return undefined
23
+ }
24
+ const active = typeof raw.activeForm === 'string' ? raw.activeForm.trim() : ''
25
+ return { status, text: status === 'in_progress' && active !== '' ? active : content }
26
+ }
27
+
28
+ // Whole-or-nothing: a malformed entry (streaming, partial input) falls back to the generic preview rather than a half-drawn checklist.
29
+ export function parseTodos(input: unknown): TodoEntry[] | undefined {
30
+ const todos = (input as { todos?: unknown } | null)?.todos
31
+ if (!Array.isArray(todos) || todos.length === 0) {
32
+ return undefined
33
+ }
34
+ const out: TodoEntry[] = []
35
+ for (const value of todos) {
36
+ const entry = todoEntry(value)
37
+ if (entry === undefined) {
38
+ return undefined
39
+ }
40
+ out.push(entry)
41
+ }
42
+ return out
43
+ }
44
+
45
+ export function todoPreview(name: string, input: unknown): TodoPreview | undefined {
46
+ if (name !== 'TodoWrite') {
47
+ return undefined
48
+ }
49
+ const todos = parseTodos(input)
50
+ if (todos === undefined) {
51
+ return undefined
52
+ }
53
+ const done = todos.reduce((n, todo) => n + (todo.status === 'completed' ? 1 : 0), 0)
54
+ const shown = todos.slice(0, PREVIEW_TODOS)
55
+ const hidden = todos.length - shown.length
56
+ return {
57
+ summary: `${done}/${todos.length} done`,
58
+ shown,
59
+ more: hidden > 0 ? `… +${hidden} more` : undefined,
60
+ }
61
+ }
62
+
63
+ // Height and render both draw this exact string — `toolRowHeight` counts what `TerminalTodos` paints.
64
+ export function todoLine(todo: TodoEntry): string {
65
+ return `${GLYPH[todo.status]} ${todo.text}`
66
+ }
@@ -1,71 +1,24 @@
1
- /**
2
- * What a folded run of tool calls is, and the one line that stands for it.
3
- *
4
- * The fold was shell-only, and the screenshot that started this made the cost
5
- * obvious: a run of six calls that happened to alternate `Bash` with an MCP tool
6
- * folded into *four* rows reading "Ran 1 shell command", "Ran 2 shell commands",
7
- * "Ran 1 shell command" — a count for every gap between the calls it could not
8
- * group. The grouping rule was right and the *membership* rule was too narrow.
9
- *
10
- * So any consecutive tool calls fold. The CLI's own line is the target
11
- * (`called roam-code, ran 1 shell command`), and the claim is unchanged from the
12
- * shell version: a tool call is almost never what you came back to read, and six
13
- * of them bury the sentence that is.
14
- *
15
- * Pure and separate because `items.tsx` draws this line and `height.ts` wraps it
16
- * to predict the row's pixel height without a DOM — the same reason
17
- * `result-preview.ts` exists. Two spellings would be two different heights.
18
- */
19
1
  import type { TranscriptItem } from '@workerdeck/react'
20
2
  import { toolInputPreview } from '../../lib/format.ts'
21
3
  import { isShellTool } from '../../lib/tool-icon.ts'
22
4
 
23
5
  type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
24
6
 
25
- /**
26
- * What breaks a run.
27
- *
28
- * *Consecutive* is still the whole rule, and it is the reason the fold is honest:
29
- * anything the model said between two calls breaks the run, because that
30
- * sentence is the reason the second one happened and a count spanning it would
31
- * claim the two were one act. The recap boundary breaks it too — the virtualized
32
- * shell folds each side separately — so a count never spans "what you already
33
- * read".
34
- *
35
- * `parentToolUseId` is the one addition the wider membership rule needs: a
36
- * subagent's calls are drawn stepped in behind a rule, and folding one together
37
- * with a top-level call would put rows from two different frames of reference
38
- * under a single count.
39
- */
40
7
  export function foldsTogether(a: ToolCallItem, b: ToolCallItem): boolean {
41
8
  return a.parentToolUseId === b.parentToolUseId
42
9
  }
43
10
 
44
- /**
45
- * The family a tool counts as in a run's breakdown.
46
- *
47
- * An MCP tool is `mcp__<server>__<tool>`, and the *server* is the useful unit:
48
- * "3 roam-code" is a thing that happened, where three separate tool names are a
49
- * list to read. Shell tools from both engines collapse to "shell" for the same
50
- * reason. Everything else is its own name, lowercased so the breakdown reads as
51
- * prose rather than as identifiers.
52
- */
53
11
  export function toolFamily(name: string): string {
54
- if (isShellTool(name)) return 'shell'
12
+ if (isShellTool(name)) {
13
+ return 'shell'
14
+ }
55
15
  const mcp = /^mcp__([^_]+(?:_[^_]+)*?)__/.exec(name)
56
- if (mcp?.[1]) return mcp[1].replace(/_/g, '-')
16
+ if (mcp?.[1]) {
17
+ return mcp[1].replace(/_/g, '-')
18
+ }
57
19
  return name.toLowerCase()
58
20
  }
59
21
 
60
- /**
61
- * The run's one line.
62
- *
63
- * A shell-only run keeps its old wording exactly — "Ran 3 shell commands" — both
64
- * because it is the commonest run by far and because it is already the sentence
65
- * people read here; widening the fold should not have re-worded the case that
66
- * was working. Anything mixed gets the count plus a breakdown, loudest family
67
- * first.
68
- */
69
22
  export function runSummary(items: readonly ToolCallItem[], busy: boolean): string {
70
23
  const verb = busy ? 'Running ' : 'Ran '
71
24
  const tail = busy ? '…' : ''
@@ -79,57 +32,25 @@ export function runSummary(items: readonly ToolCallItem[], busy: boolean): strin
79
32
  if (counts.size === 1 && counts.has('shell')) {
80
33
  return `${verb}${n} shell command${n === 1 ? '' : 's'}${tail}`
81
34
  }
82
- // Descending by count, then alphabetical — a stable order matters more than it
83
- // looks: this string is the row's measured height, so a run whose breakdown
84
- // reordered between renders would remeasure for no reason.
85
35
  const breakdown = [...counts.entries()]
86
36
  .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
87
37
  .map(([family, count]) => `${count} ${family}`)
88
38
  .join(', ')
89
- // The ellipsis trails the whole line, not the count — "Running 2 tools… · 1
90
- // read, 1 shell" reads as though the sentence ended and then carried on.
91
39
  return `${verb}${n} tool${n === 1 ? '' : 's'} · ${breakdown}${tail}`
92
40
  }
93
41
 
94
- /* ── The task block's one line ─────────────────────────────────────────────
95
- *
96
- * A `Task` call and everything its subagent produced collapse to one row (see
97
- * `blocks.ts`), and these are that row's words. Same contract as `runSummary`:
98
- * `height.ts` wraps these exact strings to predict the row's pixel height with
99
- * no DOM, so the component must render them verbatim — two spellings would be
100
- * two different heights.
101
- */
102
-
103
- const clip = (text: string, max = 80): string =>
104
- text.length > max ? text.slice(0, max - 1) + '…' : text
42
+ function clip(text: string, max = 80): string {
43
+ return text.length > max ? text.slice(0, max - 1) + '…' : text
44
+ }
105
45
 
106
- const trimmed = (value: unknown): string | undefined =>
107
- typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
46
+ function trimmed(value: unknown): string | undefined {
47
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
48
+ }
108
49
 
109
- /**
110
- * The row's identity half: which task this is.
111
- *
112
- * The Claude SDK's `Task` input carries `subagent_type` (e.g. "Explore") and a
113
- * 3–5 word `description`, and both are worth the line: parallel tasks are the
114
- * whole reason the block exists, and two rows both reading `Task(…)` answer
115
- * nothing. `Task(Explore · find the auth check)` — falling back to the
116
- * ordinary input preview when an engine sends neither, so the header is never
117
- * emptier than a plain tool row's.
118
- */
119
50
  export function taskLabel(task: ToolCallItem): string {
120
51
  return `${task.name}(${taskIdentity(task)})`
121
52
  }
122
53
 
123
- /**
124
- * The inner half of {@link taskLabel} — `Explore · find the auth check` — without
125
- * the `Task(…)` wrapper.
126
- *
127
- * Split out for the sub-agent takeover's header, which names the agent it is
128
- * showing and has no room (or reason) to repeat the tool's own name: the whole
129
- * surface *is* that Task. Extracted rather than re-spelled so the header and the
130
- * row it was opened from cannot drift, and so both keep matching protocol's
131
- * `subagentLabel`, which reads the same two input fields for the sessions list.
132
- */
133
54
  export function taskIdentity(task: ToolCallItem): string {
134
55
  const input = task.input as { description?: unknown; subagent_type?: unknown } | null
135
56
  const description = trimmed(input?.description)
@@ -139,130 +60,38 @@ export function taskIdentity(task: ToolCallItem): string {
139
60
  : (agent ?? (description ? clip(description) : toolInputPreview(task.input)))
140
61
  }
141
62
 
142
- /**
143
- * **What this agent was actually asked** — the sub-agent's brief, or undefined
144
- * when the engine did not give us one.
145
- *
146
- * Measured against a live session rather than assumed: the Agent SDK's call
147
- * carries `{description, subagent_type, run_in_background, prompt}`, and
148
- * `prompt` is the instruction.
149
- *
150
- * **Whether it also arrives in the stream depends on the agent.** A foreground
151
- * `Task` forwards its brief as a real nested `user_message` (that is what
152
- * `forwardSubagentText` buys and what the reducer stamps a `parentToolUseId`
153
- * on), so `subagentItems` picks it up and the frame already has it. A
154
- * **background** agent does not: measured on a session with eight of them,
155
- * zero items carried a `parentToolUseId` on a `user` kind. Those runs are
156
- * exactly the ones you open a takeover on — an agent working while you read
157
- * something else — and their brief was nowhere at all.
158
- *
159
- * So this is the *fallback*, not the source: the callers splice it in only when
160
- * the frame carries no brief of its own, or the same instruction would be drawn
161
- * twice.
162
- *
163
- * `description` is deliberately not a fallback: it is the 3–5 word label the
164
- * header already prints, and repeating it as a brief would claim we know the
165
- * instruction when we do not. **Codex genuinely has none** — its `spawn_agent`
166
- * message is an encrypted blob on the wire — so on that engine this is
167
- * undefined and the row is not drawn, rather than drawn empty.
168
- */
169
63
  export function taskBrief(task: ToolCallItem): string | undefined {
170
64
  const input = task.input as { prompt?: unknown } | null
171
65
  return trimmed(input?.prompt)
172
66
  }
173
67
 
174
- const callBusy = (call: ToolCallItem): boolean =>
175
- call.status === 'running' || call.status === 'pending'
68
+ function callBusy(call: ToolCallItem): boolean {
69
+ return call.status === 'running' || call.status === 'pending'
70
+ }
176
71
 
177
- /** Did this one call fail? Both spellings are needed: an out-of-loop execution
178
- * failure sets `status` with no `is_error` block to read, and an engine can flag
179
- * `is_error` on a call the reducer has not settled yet. */
180
- export const callFailed = (call: ToolCallItem): boolean =>
181
- call.status === 'failed' || call.result?.isError === true
72
+ export function callFailed(call: ToolCallItem): boolean {
73
+ return call.status === 'failed' || call.result?.isError === true
74
+ }
182
75
 
183
- /**
184
- * Does a folded run colour red? **Only when its last call failed.**
185
- *
186
- * It used to be `some`, on the argument that a failure should colour the block
187
- * rather than fragment it. The argument was right about not fragmenting and
188
- * wrong about `some`: a run is a sequence the model worked through, and a
189
- * failure it recovered from two calls later is how work goes — a grep that
190
- * matched nothing, a build fixed on the second go. Reddening the whole run for
191
- * it means a normal working session is painted red, which spends the colour
192
- * that should have been left for the one thing still broken.
193
- *
194
- * The last call is the run's *outcome*, and an outcome is what a collapsed row
195
- * can honestly claim. The failures inside it are not hidden — they are one
196
- * press away, each red on its own row, and the recap counts every one. The
197
- * **scrubber agrees with this rule** rather than overriding it: it marks a
198
- * failed call only when the call is its row's outcome, which for a run is
199
- * exactly this one. It used to mark every member on the argument that the
200
- * rail asks a different question; against a real session that was nine alarms
201
- * on the rail for a transcript reddening one row.
202
- */
203
76
  export function runFailed(items: readonly ToolCallItem[]): boolean {
204
77
  const last = items[items.length - 1]
205
78
  return last !== undefined && callFailed(last)
206
79
  }
207
80
 
208
- /** Is anything inside still going? The call itself, normally — the Task
209
- * settles only when its subagent finishes — but a bridged or deferred child
210
- * can outlive it, and a pulse that stopped while a child still worked would
211
- * read as a hang. */
212
81
  export function taskBusy(task: ToolCallItem, children: readonly TranscriptItem[]): boolean {
213
82
  return callBusy(task) || children.some((child) => child.kind === 'tool_call' && callBusy(child))
214
83
  }
215
84
 
216
- /**
217
- * Does the row colour red? **The task's own outcome, and nothing else.**
218
- *
219
- * It used to be "or any child call's", which does not survive contact with a
220
- * real subagent: an agent that ran a hundred calls, one of them a grep that
221
- * matched nothing, came back with a red line saying it had failed. It had not —
222
- * it had done exactly what it was asked, and the transcript said otherwise in
223
- * the one colour reserved for things that need a human.
224
- *
225
- * This is the call `SubagentInfo.status` already makes, and it made it for this
226
- * reason (see `packages/protocol`): the sub-agent's **own** `tool_result`
227
- * `is_error`, deliberately not `taskFailed`. The argument there was that a
228
- * nothing-matched grep must not read as a failed run *beside a session name*;
229
- * what a hundred-call agent shows is that it must not read that way beside the
230
- * `Task` row either. Two surfaces, one rule, one spelling.
231
- *
232
- * Nothing is concealed by this. A failed child is red on its own row, one press
233
- * away, and the recap counts it. The **scrubber follows this rule too** and no
234
- * longer marks such a child: a red tick on the rail says precisely what this
235
- * row is forbidden from saying. The sub-agent band still says an agent ran
236
- * here, and the task's own red tick still says it came back broken — which is
237
- * what the two channels are for.
238
- */
239
85
  export function taskFailed(task: ToolCallItem): boolean {
240
86
  return callFailed(task)
241
87
  }
242
88
 
243
- /**
244
- * The collapsed task row's one line: identity, then scale.
245
- *
246
- * `Task(Explore · find the auth check) · 7 tools…` while the subagent works —
247
- * the count grows as it does, which is the row's progress reading, and the
248
- * trailing ellipsis is the same in-flight signal `runSummary` uses (the pulse
249
- * in the gutter carries the beat). Settled, the ellipsis drops:
250
- * `… · 7 tools`. "Tools" and not "tool calls" because `runSummary` already
251
- * chose that word for the same count one row over.
252
- *
253
- * The counts are counted from the absorbed children, never read from the
254
- * engine's structured Task output — WorkerDeck does not plumb structured tool
255
- * results to clients, so a transcript replayed tomorrow must spell the same
256
- * line from the same items it holds today.
257
- *
258
- * With no tool calls yet — the subagent thinking, or only its brief arrived —
259
- * the line says `working…`, because `0 tools…` reads as a stall; settled with
260
- * none it says `done`.
261
- */
262
89
  export function taskSummary(task: ToolCallItem, children: readonly TranscriptItem[]): string {
263
90
  const busy = taskBusy(task, children)
264
91
  const calls = children.reduce((n, child) => n + (child.kind === 'tool_call' ? 1 : 0), 0)
265
92
  const label = taskLabel(task)
266
- if (calls === 0) return busy ? `${label} · working…` : `${label} · done`
93
+ if (calls === 0) {
94
+ return busy ? `${label} · working…` : `${label} · done`
95
+ }
267
96
  return `${label} · ${calls} tool${calls === 1 ? '' : 's'}${busy ? '…' : ''}`
268
97
  }
@@ -7,11 +7,7 @@ export const AlertDialog = AlertDialogPrimitive.Root
7
7
  export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
8
8
  export const AlertDialogClose = AlertDialogPrimitive.Close
9
9
 
10
- export const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Props> = ({
11
- className,
12
- children,
13
- ...props
14
- }) => (
10
+ export const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Props> = ({ className, children, ...props }) => (
15
11
  <AlertDialogPrimitive.Portal>
16
12
  <PortalScope>
17
13
  <AlertDialogPrimitive.Backdrop
@@ -22,7 +18,7 @@ export const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Pr
22
18
  )}
23
19
  />
24
20
  <AlertDialogPrimitive.Popup
25
- data-slot='alert-dialog-content'
21
+ data-slot="alert-dialog-content"
26
22
  className={cn(
27
23
  'fixed top-1/2 left-1/2 z-70 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2',
28
24
  'rounded-lg border border-border bg-surface p-5 shadow-(--shadow-lg) outline-none',
@@ -31,29 +27,18 @@ export const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Pr
31
27
  'data-ending-style:scale-95 data-ending-style:opacity-0',
32
28
  className,
33
29
  )}
34
- {...props}>
30
+ {...props}
31
+ >
35
32
  {children}
36
33
  </AlertDialogPrimitive.Popup>
37
34
  </PortalScope>
38
35
  </AlertDialogPrimitive.Portal>
39
36
  )
40
37
 
41
- export const AlertDialogTitle: FunctionComponent<AlertDialogPrimitive.Title.Props> = ({
42
- className,
43
- ...props
44
- }) => (
45
- <AlertDialogPrimitive.Title
46
- className={cn('text-heading-3 font-semibold text-text', className)}
47
- {...props}
48
- />
38
+ export const AlertDialogTitle: FunctionComponent<AlertDialogPrimitive.Title.Props> = ({ className, ...props }) => (
39
+ <AlertDialogPrimitive.Title className={cn('text-heading-3 font-semibold text-text', className)} {...props} />
49
40
  )
50
41
 
51
- export const AlertDialogDescription: FunctionComponent<AlertDialogPrimitive.Description.Props> = ({
52
- className,
53
- ...props
54
- }) => (
55
- <AlertDialogPrimitive.Description
56
- className={cn('mt-1.5 text-body-sm text-muted-foreground', className)}
57
- {...props}
58
- />
42
+ export const AlertDialogDescription: FunctionComponent<AlertDialogPrimitive.Description.Props> = ({ className, ...props }) => (
43
+ <AlertDialogPrimitive.Description className={cn('mt-1.5 text-body-sm text-muted-foreground', className)} {...props} />
59
44
  )
@@ -2,43 +2,32 @@ import { type HTMLAttributes } from 'react'
2
2
  import { type VariantProps, cva } from 'class-variance-authority'
3
3
  import { cn } from '../../lib/utils.ts'
4
4
 
5
- const badgeVariants = cva(
6
- 'inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-label font-medium whitespace-nowrap',
7
- {
8
- variants: {
9
- variant: {
10
- neutral: 'border-border bg-surface text-fg-2',
11
- accent: 'border-transparent bg-accent-bg text-fg-1',
12
- success: 'border-transparent bg-success-bg text-success',
13
- warning: 'border-transparent bg-warning-bg text-warning',
14
- danger: 'border-transparent bg-danger-bg text-danger',
15
- info: 'border-transparent bg-info-bg text-info',
16
- },
17
- mono: {
18
- true: 'font-mono text-code',
19
- false: '',
20
- },
5
+ const badgeVariants = cva('inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-label font-medium whitespace-nowrap', {
6
+ variants: {
7
+ variant: {
8
+ neutral: 'border-border bg-surface text-fg-2',
9
+ accent: 'border-transparent bg-accent-bg text-fg-1',
10
+ success: 'border-transparent bg-success-bg text-success',
11
+ warning: 'border-transparent bg-warning-bg text-warning',
12
+ danger: 'border-transparent bg-danger-bg text-danger',
13
+ info: 'border-transparent bg-info-bg text-info',
14
+ },
15
+ mono: {
16
+ true: 'font-mono text-code',
17
+ false: '',
21
18
  },
22
- defaultVariants: { variant: 'neutral', mono: false },
23
19
  },
24
- )
20
+ defaultVariants: { variant: 'neutral', mono: false },
21
+ })
25
22
 
26
- export interface BadgeProps
27
- extends HTMLAttributes<HTMLSpanElement>,
28
- VariantProps<typeof badgeVariants> {
29
- /** Render a leading status dot in the variant's color. */
23
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {
30
24
  dot?: boolean
31
25
  }
32
26
 
33
27
  export function Badge({ className, variant = 'neutral', mono, dot, children, ...props }: BadgeProps) {
34
28
  return (
35
- <span data-slot='badge' className={cn(badgeVariants({ variant, mono, className }))} {...props}>
36
- {/* `self-center` so the dot stays put if a caller baseline-aligns the
37
- badge — a no-op under the default `items-center`, and what lets the
38
- badge's *text* be the baseline it contributes to a row (a status bar
39
- aligning readings on one line needs that, and the dot's own box would
40
- otherwise answer for it). */}
41
- {dot ? <span aria-hidden className='size-1.5 shrink-0 self-center rounded-full bg-current' /> : null}
29
+ <span data-slot="badge" className={cn(badgeVariants({ variant, mono, className }))} {...props}>
30
+ {dot ? <span aria-hidden className="size-1.5 shrink-0 self-center rounded-full bg-current" /> : null}
42
31
  {children}
43
32
  </span>
44
33
  )
@@ -15,11 +15,11 @@ const buttonVariants = cva(
15
15
  link: 'text-primary underline-offset-4 hover:underline',
16
16
  },
17
17
  size: {
18
- 'default': 'h-8 gap-1.5 px-3',
19
- 'xs': "h-6 gap-1 px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
20
- 'sm': 'h-7 gap-1 px-2.5 text-xs',
21
- 'lg': 'h-9 gap-1.5 px-3.5',
22
- 'icon': 'size-8',
18
+ default: 'h-8 gap-1.5 px-3',
19
+ xs: "h-6 gap-1 px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
20
+ sm: 'h-7 gap-1 px-2.5 text-xs',
21
+ lg: 'h-9 gap-1.5 px-3.5',
22
+ icon: 'size-8',
23
23
  'icon-sm': 'size-7',
24
24
  },
25
25
  },
@@ -27,19 +27,11 @@ const buttonVariants = cva(
27
27
  },
28
28
  )
29
29
 
30
- export interface ButtonProps
31
- extends ButtonHTMLAttributes<HTMLButtonElement>,
32
- VariantProps<typeof buttonVariants> {}
30
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
33
31
 
34
32
  export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
35
33
  ({ className, variant = 'default', size = 'default', type = 'button', ...props }, ref) => (
36
- <button
37
- ref={ref}
38
- type={type}
39
- data-slot='button'
40
- className={cn(buttonVariants({ variant, size, className }))}
41
- {...props}
42
- />
34
+ <button ref={ref} type={type} data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />
43
35
  ),
44
36
  )
45
37
  Button.displayName = 'Button'
@@ -2,22 +2,11 @@ import { type HTMLAttributes } from 'react'
2
2
  import { cn } from '../../lib/utils.ts'
3
3
 
4
4
  export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
5
- return (
6
- <div
7
- data-slot='card'
8
- className={cn('rounded-lg border border-border bg-surface', className)}
9
- {...props}
10
- />
11
- )
5
+ return <div data-slot="card" className={cn('rounded-lg border border-border bg-surface', className)} {...props} />
12
6
  }
13
7
 
14
8
  export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
15
- return (
16
- <div
17
- className={cn('flex items-start justify-between gap-3 px-4 pt-3.5 pb-3', className)}
18
- {...props}
19
- />
20
- )
9
+ return <div className={cn('flex items-start justify-between gap-3 px-4 pt-3.5 pb-3', className)} {...props} />
21
10
  }
22
11
 
23
12
  export function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {