@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.
- package/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
- package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
- package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
- package/build/format-B8ZM9LPy.d.mts +16 -0
- package/build/format.d.mts +8 -75
- package/build/format.mjs +2 -2
- package/build/index.d.mts +63 -1138
- package/build/index.mjs +4 -300
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -23
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +5 -126
- package/build/workspace.mjs +11 -143
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- 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
|
@@ -9,52 +9,25 @@ import { Tip } from '../ui/Tooltip.tsx'
|
|
|
9
9
|
import { cn } from '../../lib/utils.ts'
|
|
10
10
|
import { formatCost, formatCountdown, formatTokens } from '../../lib/format.ts'
|
|
11
11
|
import { meterColorClass } from '../../lib/status.ts'
|
|
12
|
+
import { cssColor } from '../../lib/css.ts'
|
|
12
13
|
import { STATUS_META } from './status.ts'
|
|
13
14
|
import { useTranscriptVariant } from './transcript-variant.tsx'
|
|
14
15
|
|
|
15
16
|
export interface StatusBarProps {
|
|
16
17
|
state: TranscriptState
|
|
17
|
-
/**
|
|
18
|
-
* Plan windows to draw, when they should not be the session's own. The panel
|
|
19
|
-
* hands over the gateway's per-profile state merged over this transcript's
|
|
20
|
-
* reading (`mergeUsage`): a session's own `rate_limit` readings arrive only at
|
|
21
|
-
* a turn's edges, so one idle since yesterday would otherwise render
|
|
22
|
-
* yesterday's number as current. Absent = `state.rateLimits`, which is what a
|
|
23
|
-
* bar composed by hand outside the panel gets.
|
|
24
|
-
*/
|
|
25
18
|
rateLimits?: Record<string, RateLimitInfo>
|
|
26
|
-
/** @deprecated Pass {@link StatusBarProps.connection}
|
|
27
|
-
* still handing over a boolean keeps working. */
|
|
19
|
+
/** @deprecated Pass {@link StatusBarProps.connection}. */
|
|
28
20
|
connected?: boolean
|
|
29
|
-
/** How the client is doing at reaching the gateway. A dropped socket wins the
|
|
30
|
-
* status slot: the session status held over a dead socket is a stale reading,
|
|
31
|
-
* and presenting it as a live one is the thing worth avoiding. */
|
|
32
21
|
connection?: ConnectionState
|
|
33
|
-
/**
|
|
34
|
-
* Where the gauges lead. Each one opens the panel that answers *its* question
|
|
35
|
-
* — the two meters measure different things, so sending both to one "details"
|
|
36
|
-
* list would be a detour every time. Omit a handler and that gauge stays a
|
|
37
|
-
* read-only tooltip.
|
|
38
|
-
*/
|
|
39
22
|
onOpenStatus?: () => void
|
|
40
23
|
onOpenContext?: () => void
|
|
41
24
|
onOpenUsage?: () => void
|
|
42
|
-
/**
|
|
43
|
-
* The session's own controls (model, permission mode), for
|
|
44
|
-
* `controlsSurface: 'status'`. They sit at the end of the readings cluster —
|
|
45
|
-
* status, context, usage, then what you can *change* — rather than out by the
|
|
46
|
-
* actions menu, because they belong with the session facts they act on.
|
|
47
|
-
*/
|
|
48
25
|
controls?: ReactNode
|
|
49
|
-
/** Trailing slot — the session-actions menu, at the bar's trailing edge. */
|
|
50
26
|
actions?: ReactNode
|
|
51
|
-
/** Which edge the bar sits on, so its separating rule goes on the other side.
|
|
52
|
-
* Placement is the panel's decision; this only styles it. */
|
|
53
27
|
placement?: 'top' | 'bottom'
|
|
54
28
|
className?: string
|
|
55
29
|
}
|
|
56
30
|
|
|
57
|
-
/** Ticking clock for reset countdowns — rate_limit events are sparse, so tick locally. */
|
|
58
31
|
function useNow(intervalMs = 30_000): number {
|
|
59
32
|
const [now, setNow] = useState(() => Date.now())
|
|
60
33
|
useEffect(() => {
|
|
@@ -64,44 +37,28 @@ function useNow(intervalMs = 30_000): number {
|
|
|
64
37
|
return now
|
|
65
38
|
}
|
|
66
39
|
|
|
67
|
-
/** The shared ramp, aliased for the two call sites below — the thresholds live
|
|
68
|
-
* in `lib/status.ts` beside the severity they mirror. */
|
|
69
|
-
const utilizationColor = (pct: number) => meterColorClass(pct)
|
|
70
|
-
|
|
71
|
-
/** The CLI reports category colors as its own theme token names ('inactive',
|
|
72
|
-
* 'promptBorder', ...), not CSS colors — only pass through what CSS can render. */
|
|
73
|
-
const cssColor = (color: string): string | undefined =>
|
|
74
|
-
typeof CSS !== 'undefined' && CSS.supports('color', color) ? color : undefined
|
|
75
|
-
|
|
76
40
|
function ContextMeter({ usage }: { usage: ContextUsage }) {
|
|
77
41
|
return (
|
|
78
42
|
<Tip
|
|
79
43
|
content={
|
|
80
|
-
<div className=
|
|
44
|
+
<div className="flex min-w-44 flex-col gap-1 py-0.5">
|
|
81
45
|
{usage.categories.map((c) => (
|
|
82
|
-
<div key={c.name} className=
|
|
83
|
-
<span
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/>
|
|
87
|
-
<span className='flex-1'>{c.name}</span>
|
|
88
|
-
<span className='font-mono text-fg-3'>{formatTokens(c.tokens)}</span>
|
|
46
|
+
<div key={c.name} className="flex items-center gap-2">
|
|
47
|
+
<span className="size-2 shrink-0 rounded-full bg-fg-4" style={cssColor(c.color) ? { backgroundColor: c.color } : undefined} />
|
|
48
|
+
<span className="flex-1">{c.name}</span>
|
|
49
|
+
<span className="font-mono text-fg-3">{formatTokens(c.tokens)}</span>
|
|
89
50
|
</div>
|
|
90
51
|
))}
|
|
91
|
-
<div className=
|
|
52
|
+
<div className="mt-0.5 flex items-center justify-between gap-2 border-t border-border pt-1">
|
|
92
53
|
<span>Total</span>
|
|
93
|
-
<span className=
|
|
94
|
-
{formatTokens(usage.totalTokens)} / {formatTokens(usage.maxTokens)} (
|
|
95
|
-
{usage.percentage.toFixed(0)}%)
|
|
54
|
+
<span className="font-mono text-fg-3">
|
|
55
|
+
{formatTokens(usage.totalTokens)} / {formatTokens(usage.maxTokens)} ({usage.percentage.toFixed(0)}%)
|
|
96
56
|
</span>
|
|
97
57
|
</div>
|
|
98
58
|
</div>
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'inline-flex cursor-default items-center gap-1 font-mono text-label',
|
|
103
|
-
utilizationColor(usage.percentage),
|
|
104
|
-
)}>
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
<span className={cn('inline-flex cursor-default items-center gap-1 font-mono text-label', meterColorClass(usage.percentage))}>
|
|
105
62
|
Ctx {formatTokens(usage.totalTokens)}
|
|
106
63
|
</span>
|
|
107
64
|
</Tip>
|
|
@@ -109,69 +66,53 @@ function ContextMeter({ usage }: { usage: ContextUsage }) {
|
|
|
109
66
|
}
|
|
110
67
|
|
|
111
68
|
function RateLimitMeter({ label, info, now }: { label: string; info: RateLimitInfo; now: number }) {
|
|
112
|
-
// The CLI omits utilization on some updates — show the window without a made-up 0%.
|
|
113
69
|
const pct = info.utilization
|
|
114
70
|
const resetsAtMs = info.resetsAt !== undefined ? info.resetsAt * 1000 : undefined
|
|
115
71
|
return (
|
|
116
72
|
<Tip
|
|
117
73
|
content={
|
|
118
|
-
<div className=
|
|
119
|
-
<div className=
|
|
74
|
+
<div className="flex min-w-36 flex-col gap-1 py-0.5">
|
|
75
|
+
<div className="flex items-center justify-between gap-2">
|
|
120
76
|
<span>{label} usage</span>
|
|
121
|
-
<span className=
|
|
122
|
-
{pct !== undefined ? `${pct.toFixed(1)}%` : '—'}
|
|
123
|
-
</span>
|
|
77
|
+
<span className="font-mono text-fg-3">{pct !== undefined ? `${pct.toFixed(1)}%` : '—'}</span>
|
|
124
78
|
</div>
|
|
125
79
|
{resetsAtMs !== undefined ? (
|
|
126
|
-
<div className=
|
|
80
|
+
<div className="flex items-center justify-between gap-2">
|
|
127
81
|
<span>Resets in</span>
|
|
128
|
-
<span className=
|
|
82
|
+
<span className="font-mono text-fg-3">{formatCountdown(resetsAtMs, now)}</span>
|
|
129
83
|
</div>
|
|
130
84
|
) : null}
|
|
131
|
-
{info.isUsingOverage ? <div className=
|
|
132
|
-
{info.status === 'rejected' ? <div className=
|
|
85
|
+
{info.isUsingOverage ? <div className="text-warning">Using overage</div> : null}
|
|
86
|
+
{info.status === 'rejected' ? <div className="text-danger">Limit reached</div> : null}
|
|
133
87
|
</div>
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
item's baseline, and the first item here is the ring — so a flex
|
|
137
|
-
version hands the row a 13px circle's baseline instead of the number's,
|
|
138
|
-
and every reading beside it sits on a different line. Inline text has
|
|
139
|
-
only one baseline to give. `align-middle` centres the ring on the
|
|
140
|
-
x-height, which is where it looked right anyway. */}
|
|
88
|
+
}
|
|
89
|
+
>
|
|
141
90
|
<span
|
|
142
91
|
className={cn(
|
|
143
92
|
'cursor-default font-mono text-label whitespace-nowrap',
|
|
144
|
-
info.status === 'rejected' ? 'text-danger' :
|
|
145
|
-
)}
|
|
146
|
-
|
|
93
|
+
info.status === 'rejected' ? 'text-danger' : meterColorClass(pct ?? 0),
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<ProgressRing value={pct ?? 0} className="mr-1 inline-block align-middle" />
|
|
147
97
|
{label}
|
|
148
98
|
{pct !== undefined ? ` ${pct.toFixed(0)}%` : ''}
|
|
149
|
-
{resetsAtMs !== undefined ? (
|
|
150
|
-
<span className='text-fg-4'> · {formatCountdown(resetsAtMs, now)}</span>
|
|
151
|
-
) : null}
|
|
99
|
+
{resetsAtMs !== undefined ? <span className="text-fg-4"> · {formatCountdown(resetsAtMs, now)}</span> : null}
|
|
152
100
|
</span>
|
|
153
101
|
</Tip>
|
|
154
102
|
)
|
|
155
103
|
}
|
|
156
104
|
|
|
157
|
-
/** A gauge that leads somewhere. `undefined` handler leaves it inert, so an
|
|
158
|
-
* embedder that mounts no panels doesn't get buttons that do nothing. */
|
|
159
105
|
function Slot({ onClick, hint, children }: { onClick?: () => void; hint: string; children: ReactNode }) {
|
|
160
|
-
if (!onClick)
|
|
106
|
+
if (!onClick) {
|
|
107
|
+
return <>{children}</>
|
|
108
|
+
}
|
|
161
109
|
return (
|
|
162
110
|
<button
|
|
163
|
-
type=
|
|
111
|
+
type="button"
|
|
164
112
|
onClick={onClick}
|
|
165
113
|
aria-label={hint}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// that nothing was using.
|
|
169
|
-
// No horizontal padding: the bar's own 6px is the inset, and a slot that
|
|
170
|
-
// added its own would start the first reading 10px in. The hover surface
|
|
171
|
-
// hugs the text instead, which is what a status line's items do anyway.
|
|
172
|
-
// `leading-4` matches the label metrics inside — a button's own line box
|
|
173
|
-
// is the page's 24px one, and the strut padded the bar with nothing.
|
|
174
|
-
className='rounded-md py-0.5 leading-4 transition-colors outline-none hover:bg-surface-hover focus-visible:bg-surface-hover'>
|
|
114
|
+
className="rounded-md py-0.5 leading-4 transition-colors outline-none hover:bg-surface-hover focus-visible:bg-surface-hover"
|
|
115
|
+
>
|
|
175
116
|
{children}
|
|
176
117
|
</button>
|
|
177
118
|
)
|
|
@@ -199,73 +140,47 @@ export function StatusBar({
|
|
|
199
140
|
const terminal = useTranscriptVariant() === 'terminal'
|
|
200
141
|
return (
|
|
201
142
|
<div
|
|
202
|
-
data-slot=
|
|
143
|
+
data-slot="status-bar"
|
|
203
144
|
className={cn(
|
|
204
|
-
// Baselines, not boxes. `items-center` centres each child's *box*, and
|
|
205
|
-
// this bar's children are boxes of different heights for reasons that
|
|
206
|
-
// have nothing to do with their text — a badge's pill padding, a ring
|
|
207
|
-
// beside a number, a select's border and chevron. Centring those lands
|
|
208
|
-
// their text on four slightly different lines. A flex item's baseline is
|
|
209
|
-
// its first line's baseline, so aligning on it puts every reading on one
|
|
210
|
-
// line by construction, whatever is drawn around it.
|
|
211
|
-
// One explicit height, shared with the docked composer above it (see
|
|
212
|
-
// `Composer.tsx`) — the two strips along the foot of the panel read as
|
|
213
|
-
// one piece of chrome, and a pixel of drift between them shows.
|
|
214
145
|
'flex h-[var(--wd-status-bar-height)] items-baseline gap-2 border-border bg-surface p-1.5',
|
|
215
|
-
// The rule goes between the bar and the content, so which edge it sits
|
|
216
|
-
// on follows the placement — except under the terminal theme at the
|
|
217
|
-
// foot, where the composer directly above already closes itself with a
|
|
218
|
-
// rule of its own. Two adjacent 1px rules is a 2px rule with a seam in
|
|
219
|
-
// it, and it is off the line grid besides.
|
|
220
146
|
placement === 'bottom' ? (terminal ? undefined : 'border-t') : 'border-b',
|
|
221
147
|
className,
|
|
222
|
-
)}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
one. Tapping it opens the session's own facts — where it runs, on what,
|
|
226
|
-
with which credentials — which is the question a status prompts. */}
|
|
227
|
-
<Slot onClick={onOpenStatus} hint='Session info'>
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
<Slot onClick={onOpenStatus} hint="Session info">
|
|
228
151
|
{link === 'live' ? (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// their own centring.
|
|
232
|
-
<Badge variant={meta.variant} dot={!meta.busy} className='items-baseline'>
|
|
233
|
-
{meta.busy ? <Spinner className='size-3 self-center text-current' /> : null}
|
|
152
|
+
<Badge variant={meta.variant} dot={!meta.busy} className="items-baseline">
|
|
153
|
+
{meta.busy ? <Spinner className="size-3 self-center text-current" /> : null}
|
|
234
154
|
{meta.label}
|
|
235
155
|
</Badge>
|
|
236
156
|
) : (
|
|
237
|
-
<Badge
|
|
238
|
-
variant={link === 'offline' ? 'danger' : 'warning'}
|
|
239
|
-
dot={false}
|
|
240
|
-
className='items-baseline'>
|
|
157
|
+
<Badge variant={link === 'offline' ? 'danger' : 'warning'} dot={false} className="items-baseline">
|
|
241
158
|
{link === 'offline' ? (
|
|
242
|
-
<WifiOff className=
|
|
159
|
+
<WifiOff className="size-3 self-center text-current" />
|
|
243
160
|
) : (
|
|
244
|
-
<RefreshCw className=
|
|
161
|
+
<RefreshCw className="size-3 animate-spin self-center text-current" />
|
|
245
162
|
)}
|
|
246
163
|
{link === 'offline' ? 'Offline' : 'Reconnecting…'}
|
|
247
164
|
</Badge>
|
|
248
165
|
)}
|
|
249
166
|
</Slot>
|
|
250
|
-
{/* Never a 0% meter for an engine that doesn't measure: an absent reading
|
|
251
|
-
and a full window are not the same claim. */}
|
|
252
167
|
{state.capabilities.contextUsage && state.contextUsage ? (
|
|
253
|
-
<Slot onClick={onOpenContext} hint=
|
|
168
|
+
<Slot onClick={onOpenContext} hint="Context breakdown">
|
|
254
169
|
<ContextMeter usage={state.contextUsage} />
|
|
255
170
|
</Slot>
|
|
256
171
|
) : null}
|
|
257
172
|
{session || weekly ? (
|
|
258
|
-
<Slot onClick={onOpenUsage} hint=
|
|
259
|
-
<span className=
|
|
260
|
-
{session ? <RateLimitMeter label=
|
|
261
|
-
{weekly ? <RateLimitMeter label=
|
|
173
|
+
<Slot onClick={onOpenUsage} hint="Plan usage">
|
|
174
|
+
<span className="inline-flex items-baseline gap-2">
|
|
175
|
+
{session ? <RateLimitMeter label="Session" info={session} now={now} /> : null}
|
|
176
|
+
{weekly ? <RateLimitMeter label="Weekly" info={weekly} now={now} /> : null}
|
|
262
177
|
</span>
|
|
263
178
|
</Slot>
|
|
264
179
|
) : null}
|
|
265
|
-
{controls ? <span className=
|
|
266
|
-
<span className=
|
|
267
|
-
<span className=
|
|
268
|
-
{actions ? <span className=
|
|
180
|
+
{controls ? <span className="self-center">{controls}</span> : null}
|
|
181
|
+
<span className="flex-1" />
|
|
182
|
+
<span className="font-mono text-label text-fg-3">{formatCost(state.totalCostUsd)}</span>
|
|
183
|
+
{actions ? <span className="self-center">{actions}</span> : null}
|
|
269
184
|
</div>
|
|
270
185
|
)
|
|
271
186
|
}
|
|
@@ -10,25 +10,6 @@ import type { ToolCallItem } from '../terminal/blocks.ts'
|
|
|
10
10
|
import { usePulse } from './pulse.tsx'
|
|
11
11
|
import { useTicker } from '../terminal/items.tsx'
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* The one line above a sub-agent takeover: who this is, how it is doing, and the
|
|
15
|
-
* way back.
|
|
16
|
-
*
|
|
17
|
-
* **It claims exactly what the `TaskRow` it was opened from claims** — the same
|
|
18
|
-
* `taskBusy` / `taskFailed` / tool count over the same items — and deliberately
|
|
19
|
-
* *not* `SubagentInfo.status`. Protocol documents those two as divergent on
|
|
20
|
-
* purpose (`index.ts:1476-1487`), but that divergence is transcript-versus-*list*
|
|
21
|
-
* and this surface **is** the transcript. The disagreement that must not exist
|
|
22
|
-
* here is between a header and the rows directly beneath it.
|
|
23
|
-
*
|
|
24
|
-
* The rollup is still allowed one job — naming an agent whose `Task` call is not
|
|
25
|
-
* in the transcript — because a label is not content, and `SessionInfo.subagents`
|
|
26
|
-
* keeps only eight settled records (`SUBAGENT_HISTORY`), so it can never be the
|
|
27
|
-
* source of what a frame *shows*.
|
|
28
|
-
*
|
|
29
|
-
* Both variants, because the panel has two and a strip that only existed in one
|
|
30
|
-
* would make the takeover a terminal-theme feature rather than a session feature.
|
|
31
|
-
*/
|
|
32
13
|
export function SubagentStrip({
|
|
33
14
|
task,
|
|
34
15
|
items,
|
|
@@ -38,12 +19,8 @@ export function SubagentStrip({
|
|
|
38
19
|
fontSize,
|
|
39
20
|
lineHeight,
|
|
40
21
|
}: {
|
|
41
|
-
/** The spawning call. Absent when the transcript does not have it — the strip
|
|
42
|
-
* still draws, because the way back must exist even when the content cannot. */
|
|
43
22
|
task: ToolCallItem | undefined
|
|
44
|
-
/** The frame's items, for the busy reading and the tool count. */
|
|
45
23
|
items: readonly TranscriptItem[]
|
|
46
|
-
/** Fallback name when there is no task item. */
|
|
47
24
|
label: string
|
|
48
25
|
onBack: () => void
|
|
49
26
|
terminal: boolean
|
|
@@ -54,55 +31,22 @@ export function SubagentStrip({
|
|
|
54
31
|
const failed = task ? taskFailed(task) : false
|
|
55
32
|
const pulse = usePulse(busy)
|
|
56
33
|
const tools = items.reduce((n, item) => n + (item.kind === 'tool_call' ? 1 : 0), 0)
|
|
57
|
-
// Ticks only while it works, off the same once-a-second clock the working
|
|
58
|
-
// line uses. A settled agent has no end timestamp to measure against, and
|
|
59
|
-
// "N tools" is the settled reading anyway — a frozen clock would read as a
|
|
60
|
-
// stall. Absent `ts` (an item from before the reducer stamped it) simply
|
|
61
|
-
// draws no elapsed rather than counting from the epoch.
|
|
62
34
|
const startedAt = busy ? task?.ts : undefined
|
|
63
35
|
const now = useTicker(startedAt !== undefined)
|
|
64
36
|
const elapsed = startedAt === undefined ? undefined : formatDuration(now - startedAt)
|
|
65
37
|
|
|
66
38
|
const name = task ? taskIdentity(task) : label
|
|
67
|
-
/**
|
|
68
|
-
* Running or finished, in the theme's own words rather than new ones:
|
|
69
|
-
* `taskSummary` already says `working…` and `done` for exactly this state one
|
|
70
|
-
* row over, and a header that said "running"/"completed" about the same agent
|
|
71
|
-
* would be a second vocabulary for one fact. The trailing ellipsis is the
|
|
72
|
-
* theme's in-flight signal; the pulse beside it is the beat.
|
|
73
|
-
*
|
|
74
|
-
* Unknown when the transcript has no `Task` call to read — better silent than
|
|
75
|
-
* confidently wrong about an agent we cannot see.
|
|
76
|
-
*/
|
|
77
39
|
const status = !task ? undefined : failed ? 'failed' : busy ? `${pulse} working…` : 'done'
|
|
78
|
-
const detail = [tools > 0 ? `${tools} tool${tools === 1 ? '' : 's'}` : undefined, elapsed]
|
|
79
|
-
.filter(Boolean)
|
|
80
|
-
.join(' · ')
|
|
40
|
+
const detail = [tools > 0 ? `${tools} tool${tools === 1 ? '' : 's'}` : undefined, elapsed].filter(Boolean).join(' · ')
|
|
81
41
|
|
|
82
42
|
if (terminal) {
|
|
83
43
|
return (
|
|
84
|
-
<TerminalSurface fontSize={fontSize} lineHeight={lineHeight} className=
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
a different application's. The whole line is the target — there is
|
|
88
|
-
exactly one thing to do here. */}
|
|
89
|
-
<button
|
|
90
|
-
type='button'
|
|
91
|
-
onClick={onBack}
|
|
92
|
-
aria-label='Back to the session'
|
|
93
|
-
className='block w-full cursor-pointer text-left'>
|
|
94
|
-
{/* The arrow is in the gutter unconditionally. It used to give way to
|
|
95
|
-
the pulse while the agent worked, which put the way *out* of the
|
|
96
|
-
frame on a timer — the one control here should not come and go.
|
|
97
|
-
The beat moved into the status instead. `indent` because this is a
|
|
98
|
-
frame around the rows rather than one of them, and a marker flush
|
|
99
|
-
against the panel edge reads as a clipped row. */}
|
|
100
|
-
<Row glyph='←' glyphTone='dim' indent={1} tone={failed ? 'red' : 'green'}>
|
|
44
|
+
<TerminalSurface fontSize={fontSize} lineHeight={lineHeight} className="shrink-0">
|
|
45
|
+
<button type="button" onClick={onBack} aria-label="Back to the session" className="block w-full cursor-pointer text-left">
|
|
46
|
+
<Row glyph="←" glyphTone="dim" indent={1} tone={failed ? 'red' : 'green'}>
|
|
101
47
|
{name}
|
|
102
|
-
{status ?
|
|
103
|
-
|
|
104
|
-
) : null}
|
|
105
|
-
{detail ? <Ink tone='faint'> · {detail}</Ink> : null}
|
|
48
|
+
{status ? <Ink tone={failed ? 'red' : busy ? 'mark' : 'dim'}> · {status}</Ink> : null}
|
|
49
|
+
{detail ? <Ink tone="faint"> · {detail}</Ink> : null}
|
|
106
50
|
</Row>
|
|
107
51
|
</button>
|
|
108
52
|
</TerminalSurface>
|
|
@@ -110,25 +54,16 @@ export function SubagentStrip({
|
|
|
110
54
|
}
|
|
111
55
|
|
|
112
56
|
return (
|
|
113
|
-
<div className=
|
|
114
|
-
<Button variant=
|
|
115
|
-
<ArrowLeft className=
|
|
57
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1.5">
|
|
58
|
+
<Button variant="ghost" size="sm" onClick={onBack} className="h-6 gap-1 px-1.5">
|
|
59
|
+
<ArrowLeft className="size-3.5" />
|
|
116
60
|
Back
|
|
117
61
|
</Button>
|
|
118
|
-
<span
|
|
119
|
-
className={cn('min-w-0 flex-1 truncate text-body-sm', failed ? 'text-danger' : 'text-fg-2')}>
|
|
120
|
-
{name}
|
|
121
|
-
</span>
|
|
62
|
+
<span className={cn('min-w-0 flex-1 truncate text-body-sm', failed ? 'text-danger' : 'text-fg-2')}>{name}</span>
|
|
122
63
|
{status ? (
|
|
123
|
-
<span
|
|
124
|
-
className={cn(
|
|
125
|
-
'shrink-0 text-label',
|
|
126
|
-
failed ? 'text-danger' : busy ? 'text-accent' : 'text-fg-3',
|
|
127
|
-
)}>
|
|
128
|
-
{status}
|
|
129
|
-
</span>
|
|
64
|
+
<span className={cn('shrink-0 text-label', failed ? 'text-danger' : busy ? 'text-accent' : 'text-fg-3')}>{status}</span>
|
|
130
65
|
) : null}
|
|
131
|
-
{detail ? <span className=
|
|
66
|
+
{detail ? <span className="shrink-0 text-label text-fg-4">{detail}</span> : null}
|
|
132
67
|
</div>
|
|
133
68
|
)
|
|
134
69
|
}
|