@workerdeck/ui 0.23.0 → 1.1.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.
- package/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-I5QSLLb0.mjs} +2266 -3459
- package/build/SessionPanel-I5QSLLb0.mjs.map +1 -0
- package/build/SessionPanel-yNO60Tzw.d.mts +151 -0
- package/build/format-C_lpFq0l.d.mts +16 -0
- package/build/format.d.mts +8 -76
- package/build/format.mjs +2 -2
- package/build/index.d.mts +291 -1695
- package/build/index.mjs +6 -301
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +32 -23
- package/build/{status-BE-zg88x.mjs → status-Dm9INVTt.mjs} +17 -88
- package/build/status-Dm9INVTt.mjs.map +1 -0
- package/build/workspace.d.mts +8 -187
- package/build/workspace.mjs +13 -144
- package/build/workspace.mjs.map +1 -1
- package/package.json +24 -24
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +3 -9
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
|
@@ -1,143 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The terminal theme's block model — **which rows exist**.
|
|
3
|
-
*
|
|
4
|
-
* Pure and separate from `items.tsx` because which rows exist is part of what
|
|
5
|
-
* the theme *is*: the virtualizer counts these, `height.ts` sizes them, the
|
|
6
|
-
* scrubber addresses them, and both renderers — the virtualized shell in
|
|
7
|
-
* `agent/Transcript.tsx` and the plain `TerminalTranscript` — must fold
|
|
8
|
-
* identically or two clients would be showing different transcripts of the
|
|
9
|
-
* same session. `items.tsx` re-exports everything here, so its old imports
|
|
10
|
-
* keep working; the components stay there, the model lives here.
|
|
11
|
-
*
|
|
12
|
-
* Two folds happen in one pass:
|
|
13
|
-
*
|
|
14
|
-
* - **Runs.** Consecutive tool calls fold into one row (`tool-run.ts` owns the
|
|
15
|
-
* membership rule and the summary line).
|
|
16
|
-
* - **Tasks.** A `Task` tool call *absorbs* every item whose
|
|
17
|
-
* `parentToolUseId` names it — its subagent's brief, thinking, text and
|
|
18
|
-
* tool calls — into ONE row, **wherever those items fall in the stream**.
|
|
19
|
-
* Subagents run in parallel, so their items interleave with each other and
|
|
20
|
-
* with top-level work; a consecutive-run rule cannot group them, which is
|
|
21
|
-
* why absorption is by parent id and not by adjacency. The absorbed items
|
|
22
|
-
* are folded again *within* the block (a subagent's consecutive calls
|
|
23
|
-
* become runs — `foldsTogether` already keys on `parentToolUseId`), and the
|
|
24
|
-
* block is always collapsed by default: that preserves the height
|
|
25
|
-
* calculator's invariant that an unmounted row is collapsed by definition.
|
|
26
|
-
*
|
|
27
|
-
* The absorption rule, precisely: a task block forms for a **top-level** tool
|
|
28
|
-
* call (`parentToolUseId` empty) that has at least one child in the slice,
|
|
29
|
-
* and an item is absorbed iff its parent is such a call. Everything else
|
|
30
|
-
* renders as its own row, which settles the edges deliberately:
|
|
31
|
-
*
|
|
32
|
-
* - A **childless** `Task` call is a plain tool call and folds into runs —
|
|
33
|
-
* right for a task still spawning, and for a resumed session whose
|
|
34
|
-
* children were compacted away entirely.
|
|
35
|
-
* - An **orphan** child (its parent call absent from the slice) keeps today's
|
|
36
|
-
* behaviour: its own row, stepped in behind a rule. The recap boundary is
|
|
37
|
-
* the load-bearing case — the shell folds each side separately, so a task
|
|
38
|
-
* split by the boundary shows its post-boundary children *below* the seam
|
|
39
|
-
* rather than hiding new work inside a collapsed row above it, the same
|
|
40
|
-
* claim the run fold makes about never counting across "what you already
|
|
41
|
-
* read".
|
|
42
|
-
* - A **grandchild** (parent is itself a subagent's call — unreachable from
|
|
43
|
-
* today's engines, which do not nest sidechains) is not absorbed and not
|
|
44
|
-
* dropped: it renders top-level, stepped in. An unmapped item must be
|
|
45
|
-
* visible, never gone.
|
|
46
|
-
* - Two top-level calls separated only by absorbed items **fold together**:
|
|
47
|
-
* the interleaved step was another frame's work, and once it is absorbed
|
|
48
|
-
* the two calls are adjacent on screen — the count matches what the reader
|
|
49
|
-
* sees.
|
|
50
|
-
*/
|
|
51
1
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
52
2
|
import { foldsTogether } from './tool-run.ts'
|
|
53
3
|
|
|
54
4
|
export type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
|
|
55
5
|
|
|
56
|
-
|
|
57
|
-
* The id of the tool call this item was produced inside, or `undefined` at the
|
|
58
|
-
* top level. One spelling for both shapes the reducer emits: `assistant_text`
|
|
59
|
-
* / `thinking` / `tool_call` carry `parentToolUseId: string | null` on every
|
|
60
|
-
* instance, while `user` carries it **optionally** (a human prompt has no
|
|
61
|
-
* parent at all — the key exists only on a subagent's brief). Callers must go
|
|
62
|
-
* through this rather than reading the field, or the absent-key case silently
|
|
63
|
-
* types as a compile error on one kind and a miss on another.
|
|
64
|
-
*/
|
|
6
|
+
// `user` items carry `parentToolUseId` only optionally (only a subagent's brief has the key); the other kinds carry it as `string | null`.
|
|
65
7
|
export function parentOf(item: TranscriptItem): string | undefined {
|
|
66
8
|
const parent = 'parentToolUseId' in item ? item.parentToolUseId : undefined
|
|
67
9
|
return parent ?? undefined
|
|
68
10
|
}
|
|
69
11
|
|
|
70
|
-
|
|
71
|
-
* **The frame membership rule**: the items a sub-agent produced, and nothing
|
|
72
|
-
* else — what the takeover renders instead of the whole conversation.
|
|
73
|
-
*
|
|
74
|
-
* One exported function rather than a `filter` at each call site, because this
|
|
75
|
-
* is a rule two renderers have to agree on: iOS mirrors the terminal model out
|
|
76
|
-
* of this module, and a phone that framed a sub-agent slightly differently would
|
|
77
|
-
* be a second answer to "what is this agent doing".
|
|
78
|
-
*
|
|
79
|
-
* It picks up the brief (a `user` item *with* a parent), the thinking, the
|
|
80
|
-
* streamed text (deltas are namespaced per sidechain, so `streaming:<parentId>`
|
|
81
|
-
* carries the parent like any other item), every tool call with its result, and
|
|
82
|
-
* the final report. It excludes the spawning `Task` call itself — that is the
|
|
83
|
-
* frame, not a row in it — and every other agent's work.
|
|
84
|
-
*
|
|
85
|
-
* The slice is safe to hand straight to {@link terminalBlocks} at offset 0:
|
|
86
|
-
* nothing in it is top-level, so nothing absorbs, and consecutive calls still
|
|
87
|
-
* fold into runs because {@link foldsTogether} keys on an *equal* parent rather
|
|
88
|
-
* than on absence of one. Row indices are internally consistent because the rows
|
|
89
|
-
* and the `items` they came from are the same array.
|
|
90
|
-
*/
|
|
91
|
-
export function subagentItems(
|
|
92
|
-
items: readonly TranscriptItem[],
|
|
93
|
-
parentToolUseId: string,
|
|
94
|
-
): TranscriptItem[] {
|
|
12
|
+
export function subagentItems(items: readonly TranscriptItem[], parentToolUseId: string): TranscriptItem[] {
|
|
95
13
|
return items.filter((item) => parentOf(item) === parentToolUseId)
|
|
96
14
|
}
|
|
97
15
|
|
|
98
|
-
/** Is this a row the transcript folds into a run? Any tool call is — see
|
|
99
|
-
* `tool-run.ts` for why this is no longer shell-only. */
|
|
100
16
|
export function isRunCall(item: TranscriptItem): item is ToolCallItem {
|
|
101
17
|
return item.kind === 'tool_call'
|
|
102
18
|
}
|
|
103
19
|
|
|
104
|
-
/** One transcript item as its own row. */
|
|
105
20
|
export type ItemBlock = { key: string; item: TranscriptItem; index: number }
|
|
106
|
-
/** A folded run of consecutive tool calls — one row for `run.length` items.
|
|
107
|
-
* At the top level its coverage is contiguous (`[index, index + run.length)`);
|
|
108
|
-
* inside a task block the members' global indices may be scattered (the run is
|
|
109
|
-
* consecutive in the *subagent's* stream, not the transcript's). */
|
|
110
21
|
export type RunBlock = {
|
|
111
22
|
key: string
|
|
112
23
|
run: ToolCallItem[]
|
|
113
|
-
/** Every member's global transcript index, in stream order — `childIndices`'
|
|
114
|
-
* sibling, and needed for the same reason: a run folded across an absorbed
|
|
115
|
-
* gap has no `[index, index + len)` coverage, so a member's ordinal within
|
|
116
|
-
* the run (what the scrubber anchors a failure by) is unrecoverable from
|
|
117
|
-
* `index` arithmetic. */
|
|
118
24
|
indices: number[]
|
|
119
25
|
index: number
|
|
120
26
|
}
|
|
121
|
-
/** What a task block's children fold into. Never a task block itself — the
|
|
122
|
-
* engines do not nest sidechains, and a hypothetical grandchild renders
|
|
123
|
-
* top-level rather than vanishing (see the module comment). */
|
|
124
27
|
export type LeafBlock = ItemBlock | RunBlock
|
|
125
28
|
|
|
126
|
-
/**
|
|
127
|
-
* A `Task` call and everything produced inside it, as ONE row — collapsed by
|
|
128
|
-
* default, pressable to expand, the same shape as the folded tool run.
|
|
129
|
-
*
|
|
130
|
-
* - `task` is the call itself; `index` its own transcript index, which is the
|
|
131
|
-
* row's address (rows stay ordered by `index`).
|
|
132
|
-
* - `children` are the absorbed items in stream order, folded exactly as
|
|
133
|
-
* top-level rows are; each leaf's `index` is its first member's *global*
|
|
134
|
-
* transcript index.
|
|
135
|
-
* - `childIndices` is the flat list of every absorbed item's global index, in
|
|
136
|
-
* stream order. It exists because absorption is the one exception to row
|
|
137
|
-
* contiguity: a child run's members can straddle other rows' starts, so no
|
|
138
|
-
* `[start, start + len)` arithmetic can say what this row covers —
|
|
139
|
-
* `rowIndexForItem` answers from this list instead.
|
|
140
|
-
*/
|
|
141
29
|
export type TaskBlock = {
|
|
142
30
|
key: string
|
|
143
31
|
task: ToolCallItem
|
|
@@ -146,21 +34,12 @@ export type TaskBlock = {
|
|
|
146
34
|
index: number
|
|
147
35
|
}
|
|
148
36
|
|
|
149
|
-
/**
|
|
150
|
-
* Fold consecutive tool calls into runs and absorb subagent items into task
|
|
151
|
-
* blocks, leaving everything else alone.
|
|
152
|
-
*/
|
|
153
37
|
export type TerminalBlock = ItemBlock | RunBlock | TaskBlock
|
|
154
38
|
|
|
155
|
-
/** The absorbed items, flat and in stream order — what `taskSummary` counts
|
|
156
|
-
* and the collapsed row's one line is built from. */
|
|
157
39
|
export function taskChildItems(block: TaskBlock): TranscriptItem[] {
|
|
158
40
|
return block.children.flatMap((child) => ('run' in child ? child.run : [child.item]))
|
|
159
41
|
}
|
|
160
42
|
|
|
161
|
-
/** Append one item to a leaf-block list, folding it into the previous run when
|
|
162
|
-
* the membership rule allows — the one fold implementation, used for the
|
|
163
|
-
* top-level stream and for each task's children alike. */
|
|
164
43
|
function pushLeaf(out: LeafBlock[], item: TranscriptItem, index: number): void {
|
|
165
44
|
const previous = out.at(-1)
|
|
166
45
|
if (isRunCall(item)) {
|
|
@@ -168,7 +47,6 @@ function pushLeaf(out: LeafBlock[], item: TranscriptItem, index: number): void {
|
|
|
168
47
|
previous.run.push(item)
|
|
169
48
|
previous.indices.push(index)
|
|
170
49
|
} else {
|
|
171
|
-
// Keyed by the run's *first* call, so the key is stable as the run grows.
|
|
172
50
|
out.push({ key: `run:${item.id}`, run: [item], indices: [index], index })
|
|
173
51
|
}
|
|
174
52
|
return
|
|
@@ -176,20 +54,7 @@ function pushLeaf(out: LeafBlock[], item: TranscriptItem, index: number): void {
|
|
|
176
54
|
out.push({ key: `${item.kind}:${item.id}`, item, index })
|
|
177
55
|
}
|
|
178
56
|
|
|
179
|
-
|
|
180
|
-
* @param offset What `items[0]`'s index is in the whole transcript — the
|
|
181
|
-
* virtualized shell folds each side of the recap boundary separately, and the
|
|
182
|
-
* rows still have to say where they sit for the catch-up dimming.
|
|
183
|
-
* @param fold Whether to group at all. `false` gives one block per item —
|
|
184
|
-
* no runs *and no task absorption* — which is what the cards variant
|
|
185
|
-
* renders: this is the terminal theme's rule and must not silently reshape
|
|
186
|
-
* another renderer's row list.
|
|
187
|
-
*/
|
|
188
|
-
export function terminalBlocks(
|
|
189
|
-
items: readonly TranscriptItem[],
|
|
190
|
-
offset = 0,
|
|
191
|
-
fold = true,
|
|
192
|
-
): TerminalBlock[] {
|
|
57
|
+
export function terminalBlocks(items: readonly TranscriptItem[], offset = 0, fold = true): TerminalBlock[] {
|
|
193
58
|
if (!fold) {
|
|
194
59
|
return items.map((item, position) => ({
|
|
195
60
|
key: `${item.kind}:${item.id}`,
|
|
@@ -198,21 +63,22 @@ export function terminalBlocks(
|
|
|
198
63
|
}))
|
|
199
64
|
}
|
|
200
65
|
|
|
201
|
-
// Which top-level tool calls have children in this slice, and what those
|
|
202
|
-
// children are. Collected over the whole slice before any block is built:
|
|
203
|
-
// membership is by parent id, not adjacency, so a call cannot know it is a
|
|
204
|
-
// task until every item has been seen.
|
|
205
66
|
const topLevelCalls = new Set<string>()
|
|
206
67
|
for (const item of items) {
|
|
207
|
-
if (item.kind === 'tool_call' && parentOf(item) === undefined)
|
|
68
|
+
if (item.kind === 'tool_call' && parentOf(item) === undefined) {
|
|
69
|
+
topLevelCalls.add(item.id)
|
|
70
|
+
}
|
|
208
71
|
}
|
|
209
72
|
const childrenOf = new Map<string, { item: TranscriptItem; index: number }[]>()
|
|
210
73
|
items.forEach((item, position) => {
|
|
211
74
|
const parent = parentOf(item)
|
|
212
75
|
if (parent !== undefined && topLevelCalls.has(parent)) {
|
|
213
76
|
const list = childrenOf.get(parent)
|
|
214
|
-
if (list)
|
|
215
|
-
|
|
77
|
+
if (list) {
|
|
78
|
+
list.push({ item, index: offset + position })
|
|
79
|
+
} else {
|
|
80
|
+
childrenOf.set(parent, [{ item, index: offset + position }])
|
|
81
|
+
}
|
|
216
82
|
}
|
|
217
83
|
})
|
|
218
84
|
|
|
@@ -220,13 +86,16 @@ export function terminalBlocks(
|
|
|
220
86
|
for (const [position, item] of items.entries()) {
|
|
221
87
|
const index = offset + position
|
|
222
88
|
const parent = parentOf(item)
|
|
223
|
-
|
|
224
|
-
|
|
89
|
+
if (parent !== undefined && childrenOf.has(parent)) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
225
92
|
if (item.kind === 'tool_call') {
|
|
226
93
|
const children = childrenOf.get(item.id)
|
|
227
94
|
if (children) {
|
|
228
95
|
const folded: LeafBlock[] = []
|
|
229
|
-
for (const child of children)
|
|
96
|
+
for (const child of children) {
|
|
97
|
+
pushLeaf(folded, child.item, child.index)
|
|
98
|
+
}
|
|
230
99
|
out.push({
|
|
231
100
|
key: `task:${item.id}`,
|
|
232
101
|
task: item,
|
|
@@ -242,18 +111,13 @@ export function terminalBlocks(
|
|
|
242
111
|
return out
|
|
243
112
|
}
|
|
244
113
|
|
|
245
|
-
/** Spacing between two items: a blank line, unless the pair belongs together.
|
|
246
|
-
* Tool output already sits under its call, and a run of tool calls reads as one
|
|
247
|
-
* block — the CLI leaves no blank line inside either. */
|
|
248
114
|
export function needsBlank(previous: TranscriptItem, next: TranscriptItem): boolean {
|
|
249
|
-
if (previous.kind === 'tool_call' && next.kind === 'tool_call')
|
|
115
|
+
if (previous.kind === 'tool_call' && next.kind === 'tool_call') {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
250
118
|
return true
|
|
251
119
|
}
|
|
252
120
|
|
|
253
|
-
/** The same rule over blocks: a run counts as the tool calls it folded, and a
|
|
254
|
-
* task block counts as the `Task` call it stands for — a collapsed task row
|
|
255
|
-
* sits flush with the tool rows of the same turn, exactly as the call itself
|
|
256
|
-
* did before it grew children. */
|
|
257
121
|
export function blockNeedsBlank(previous: TerminalBlock, next: TerminalBlock): boolean {
|
|
258
122
|
const kind = (block: TerminalBlock) => ('item' in block ? block.item.kind : 'tool_call')
|
|
259
123
|
return !(kind(previous) === 'tool_call' && kind(next) === 'tool_call')
|
|
@@ -1,49 +1,24 @@
|
|
|
1
1
|
import type { FilePatch, PatchHunk } from '@workerdeck/protocol'
|
|
2
2
|
import { Row } from './row.tsx'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* A file edit, drawn the way the CLI draws it: a right-aligned line-number
|
|
6
|
-
* column, a one-column `+`/`-` marker, and the line — with added and removed
|
|
7
|
-
* rows carrying a full-bleed wash.
|
|
8
|
-
*
|
|
9
|
-
* The line numbers are the engine's own, off the wire (protocol's
|
|
10
|
-
* {@link FilePatch}). Nothing here computes one: this component has never seen
|
|
11
|
-
* the file, and a number it invented would be worse than no number at all —
|
|
12
|
-
* it would look authoritative and send the reader to the wrong line.
|
|
13
|
-
*
|
|
14
|
-
* The whole row — number, marker and text — is one {@link Row}, with the number
|
|
15
|
-
* and marker as its gutter. That is what keeps a wrapped line hanging under the
|
|
16
|
-
* text rather than under the numbers, and it is why the gutter width is computed
|
|
17
|
-
* rather than fixed: a four-digit file and a two-digit one both put their first
|
|
18
|
-
* character of code on a whole column.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/** Which side of the edit a body line belongs to, from its unified-diff prefix. */
|
|
22
4
|
type LineKind = 'context' | 'add' | 'remove'
|
|
23
5
|
|
|
24
|
-
|
|
25
|
-
line.startsWith('+') ? 'add' : line.startsWith('-') ? 'remove' : 'context'
|
|
6
|
+
function kindOf(line: string): LineKind {
|
|
7
|
+
return line.startsWith('+') ? 'add' : line.startsWith('-') ? 'remove' : 'context'
|
|
8
|
+
}
|
|
26
9
|
|
|
27
|
-
/**
|
|
28
|
-
* The rows of one hunk, each with the line number it has *in the file*.
|
|
29
|
-
*
|
|
30
|
-
* Two counters, because a diff is two files interleaved: an added line has a
|
|
31
|
-
* number only in the new file, a removed line only in the old one, and context
|
|
32
|
-
* advances both. Showing the new number for adds and the old number for removes
|
|
33
|
-
* is what makes the column mean "where do I find this line", which is the only
|
|
34
|
-
* reason to print it.
|
|
35
|
-
*/
|
|
36
10
|
function hunkRows(hunk: PatchHunk): { kind: LineKind; number: number; text: string }[] {
|
|
37
11
|
let oldLine = hunk.oldStart
|
|
38
12
|
let newLine = hunk.newStart
|
|
39
13
|
const rows = []
|
|
40
14
|
for (const line of hunk.lines) {
|
|
41
15
|
const kind = kindOf(line)
|
|
42
|
-
// The prefix is diff syntax, not content — the marker column says it now.
|
|
43
16
|
const text = line.slice(1)
|
|
44
|
-
if (kind === 'add')
|
|
45
|
-
|
|
46
|
-
else {
|
|
17
|
+
if (kind === 'add') {
|
|
18
|
+
rows.push({ kind, number: newLine++, text })
|
|
19
|
+
} else if (kind === 'remove') {
|
|
20
|
+
rows.push({ kind, number: oldLine++, text })
|
|
21
|
+
} else {
|
|
47
22
|
rows.push({ kind, number: newLine, text })
|
|
48
23
|
oldLine++
|
|
49
24
|
newLine++
|
|
@@ -54,25 +29,20 @@ function hunkRows(hunk: PatchHunk): { kind: LineKind; number: number; text: stri
|
|
|
54
29
|
|
|
55
30
|
const MARKER = { context: ' ', add: '+', remove: '-' } as const
|
|
56
31
|
|
|
57
|
-
/**
|
|
58
|
-
* A patch for a change that has **not happened yet** — an approval prompt's
|
|
59
|
-
* `Edit`, where the input names the old and new text but no line numbers exist
|
|
60
|
-
* because the edit has not been applied and this client has never read the file.
|
|
61
|
-
*
|
|
62
|
-
* `newStart: 0` is how it says so, and {@link TerminalDiff} reads that back: a
|
|
63
|
-
* diff whose hunks all start at zero renders without a number column rather than
|
|
64
|
-
* printing a column of zeroes or inventing an offset.
|
|
65
|
-
*/
|
|
66
32
|
export function previewPatch(input: unknown): FilePatch | undefined {
|
|
67
33
|
const edit = input as { file_path?: unknown; old_string?: unknown; new_string?: unknown } | null
|
|
68
34
|
const before = typeof edit?.old_string === 'string' ? edit.old_string : undefined
|
|
69
35
|
const after = typeof edit?.new_string === 'string' ? edit.new_string : undefined
|
|
70
|
-
if (before === undefined && after === undefined)
|
|
36
|
+
if (before === undefined && after === undefined) {
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
71
39
|
const lines = [
|
|
72
40
|
...(before ? before.split('\n').map((line) => `-${line}`) : []),
|
|
73
41
|
...(after ? after.split('\n').map((line) => `+${line}`) : []),
|
|
74
42
|
]
|
|
75
|
-
if (lines.length === 0)
|
|
43
|
+
if (lines.length === 0) {
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
76
46
|
return {
|
|
77
47
|
...(typeof edit?.file_path === 'string' && { path: edit.file_path }),
|
|
78
48
|
hunks: [{ oldStart: 0, oldLines: 0, newStart: 0, newLines: 0, lines }],
|
|
@@ -81,47 +51,29 @@ export function previewPatch(input: unknown): FilePatch | undefined {
|
|
|
81
51
|
|
|
82
52
|
export function TerminalDiff({ patch }: { patch: FilePatch }) {
|
|
83
53
|
const rows = patch.hunks.map(hunkRows)
|
|
84
|
-
// Numbers only when the engine gave any (see `previewPatch`): a column of
|
|
85
|
-
// zeroes would look like line 0 of the file, which is a lie about where to
|
|
86
|
-
// look — and a diff with no numbers is honest about having none.
|
|
87
54
|
const numbered = patch.hunks.some((hunk) => hunk.newStart > 0)
|
|
88
|
-
// Wide enough for the largest number any row will print, so the marker and the
|
|
89
|
-
// code start on the same column throughout — including across a hunk boundary,
|
|
90
|
-
// where the numbers jump.
|
|
91
55
|
const width = numbered ? String(Math.max(...rows.flat().map((row) => row.number), 1)).length : 0
|
|
92
|
-
// number + space + marker + space (or just the marker and a space)
|
|
93
56
|
const columns = numbered ? width + 3 : 2
|
|
94
57
|
|
|
95
58
|
return (
|
|
96
|
-
<div className=
|
|
59
|
+
<div className="term-diff">
|
|
97
60
|
{rows.map((hunk, index) => (
|
|
98
61
|
<div key={index}>
|
|
99
|
-
{
|
|
100
|
-
were merely a paragraph apart. The CLI's separator is the honest
|
|
101
|
-
one: a row that says lines were skipped. */}
|
|
102
|
-
{index > 0 ? (
|
|
103
|
-
<Row columns={columns} glyph={' '.repeat(width) + ' ⋮'} tone='faint' />
|
|
104
|
-
) : null}
|
|
62
|
+
{index > 0 ? <Row columns={columns} glyph={' '.repeat(width) + ' ⋮'} tone="faint" /> : null}
|
|
105
63
|
{hunk.map((row, line) => (
|
|
106
64
|
<Row
|
|
107
65
|
key={line}
|
|
108
66
|
columns={columns}
|
|
109
67
|
data-diff={row.kind}
|
|
110
|
-
glyph={
|
|
111
|
-
|
|
112
|
-
? `${String(row.number).padStart(width)} ${MARKER[row.kind]}`
|
|
113
|
-
: MARKER[row.kind]
|
|
114
|
-
}
|
|
115
|
-
// `pre-wrap` on the body already keeps the code's own indentation;
|
|
116
|
-
// an empty line still has to occupy its row, hence the space.
|
|
117
|
-
>
|
|
68
|
+
glyph={numbered ? `${String(row.number).padStart(width)} ${MARKER[row.kind]}` : MARKER[row.kind]}
|
|
69
|
+
>
|
|
118
70
|
{row.text || ' '}
|
|
119
71
|
</Row>
|
|
120
72
|
))}
|
|
121
73
|
</div>
|
|
122
74
|
))}
|
|
123
75
|
{patch.truncated ? (
|
|
124
|
-
<Row columns={columns} tone=
|
|
76
|
+
<Row columns={columns} tone="faint">
|
|
125
77
|
… diff truncated
|
|
126
78
|
</Row>
|
|
127
79
|
) : null}
|