@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
package/src/lib/format.ts
CHANGED
|
@@ -1,96 +1,131 @@
|
|
|
1
1
|
export function formatCost(usd: number | undefined): string {
|
|
2
|
-
if (usd === undefined || Number.isNaN(usd))
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
if (usd === undefined || Number.isNaN(usd)) {
|
|
3
|
+
return '—'
|
|
4
|
+
}
|
|
5
|
+
if (usd === 0) {
|
|
6
|
+
return '$0.00'
|
|
7
|
+
}
|
|
8
|
+
if (usd < 0.01) {
|
|
9
|
+
return '<$0.01'
|
|
10
|
+
}
|
|
5
11
|
return `$${usd.toFixed(2)}`
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
export function formatDuration(ms: number): string {
|
|
9
|
-
if (ms < 1000)
|
|
15
|
+
if (ms < 1000) {
|
|
16
|
+
return `${Math.round(ms)}ms`
|
|
17
|
+
}
|
|
10
18
|
const s = ms / 1000
|
|
11
|
-
if (s < 60)
|
|
19
|
+
if (s < 60) {
|
|
20
|
+
return `${s.toFixed(1)}s`
|
|
21
|
+
}
|
|
12
22
|
const m = Math.floor(s / 60)
|
|
13
23
|
return `${m}m ${Math.round(s % 60)}s`
|
|
14
24
|
}
|
|
15
25
|
|
|
16
|
-
/** Compact token count, Claude Code-style: 850 → "850", 359_000 → "359.0k", 1_200_000 → "1.2M". */
|
|
17
26
|
export function formatTokens(tokens: number): string {
|
|
18
|
-
if (tokens >= 1_000_000)
|
|
19
|
-
|
|
27
|
+
if (tokens >= 1_000_000) {
|
|
28
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`
|
|
29
|
+
}
|
|
30
|
+
if (tokens >= 1000) {
|
|
31
|
+
return `${(tokens / 1000).toFixed(1)}k`
|
|
32
|
+
}
|
|
20
33
|
return String(Math.round(tokens))
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export function formatBytes(bytes: number): string {
|
|
24
|
-
if (bytes >= 1024 * 1024)
|
|
25
|
-
|
|
37
|
+
if (bytes >= 1024 * 1024) {
|
|
38
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
39
|
+
}
|
|
40
|
+
if (bytes >= 1024) {
|
|
41
|
+
return `${(bytes / 1024).toFixed(1)} KB`
|
|
42
|
+
}
|
|
26
43
|
return `${bytes} B`
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
/** Countdown to an epoch-ms deadline: "2h 18m", "12m", "<1m"; "now" once passed. */
|
|
30
46
|
export function formatCountdown(untilEpochMs: number, now = Date.now()): string {
|
|
31
47
|
const remaining = untilEpochMs - now
|
|
32
|
-
if (remaining <= 0)
|
|
48
|
+
if (remaining <= 0) {
|
|
49
|
+
return 'now'
|
|
50
|
+
}
|
|
33
51
|
const minutes = Math.ceil(remaining / 60_000)
|
|
34
|
-
if (minutes < 1)
|
|
35
|
-
|
|
52
|
+
if (minutes < 1) {
|
|
53
|
+
return '<1m'
|
|
54
|
+
}
|
|
55
|
+
if (minutes < 60) {
|
|
56
|
+
return `${minutes}m`
|
|
57
|
+
}
|
|
36
58
|
const days = Math.floor(minutes / (60 * 24))
|
|
37
|
-
if (days >= 1)
|
|
59
|
+
if (days >= 1) {
|
|
60
|
+
return `${days}d ${Math.floor((minutes % (60 * 24)) / 60)}h`
|
|
61
|
+
}
|
|
38
62
|
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
export function formatRelativeTime(epochMs: number | undefined, now = Date.now()): string {
|
|
42
|
-
if (!epochMs)
|
|
66
|
+
if (!epochMs) {
|
|
67
|
+
return '—'
|
|
68
|
+
}
|
|
43
69
|
const diff = Math.max(0, now - epochMs)
|
|
44
70
|
const s = Math.floor(diff / 1000)
|
|
45
|
-
if (s < 60)
|
|
71
|
+
if (s < 60) {
|
|
72
|
+
return 'just now'
|
|
73
|
+
}
|
|
46
74
|
const m = Math.floor(s / 60)
|
|
47
|
-
if (m < 60)
|
|
75
|
+
if (m < 60) {
|
|
76
|
+
return `${m}m ago`
|
|
77
|
+
}
|
|
48
78
|
const h = Math.floor(m / 60)
|
|
49
|
-
if (h < 24)
|
|
79
|
+
if (h < 24) {
|
|
80
|
+
return `${h}h ago`
|
|
81
|
+
}
|
|
50
82
|
const d = Math.floor(h / 24)
|
|
51
83
|
return `${d}d ago`
|
|
52
84
|
}
|
|
53
85
|
|
|
54
|
-
/**
|
|
55
|
-
* Human label for a rate-limit window key, compact: 'five_hour' → "5h",
|
|
56
|
-
* 'seven_day_opus' → "7d opus". The per-model suffix is an open set — the CLI
|
|
57
|
-
* adds buckets as plans gain them — so it is rewritten rather than enumerated.
|
|
58
|
-
*/
|
|
59
86
|
export function formatRateLimitWindow(key: string): string {
|
|
60
|
-
if (key === 'five_hour')
|
|
61
|
-
|
|
87
|
+
if (key === 'five_hour') {
|
|
88
|
+
return '5h'
|
|
89
|
+
}
|
|
90
|
+
if (key === 'seven_day') {
|
|
91
|
+
return '7d'
|
|
92
|
+
}
|
|
62
93
|
const spaced = key.replaceAll('_', ' ')
|
|
63
94
|
return key.startsWith('seven_day_') ? `7d ${spaced.slice('seven day '.length)}` : spaced
|
|
64
95
|
}
|
|
65
96
|
|
|
66
|
-
/** The same key spelled out, where there is room: 'five_hour' → "5-hour
|
|
67
|
-
* session", 'seven_day_fable' → "Weekly · Fable". */
|
|
68
97
|
export function formatRateLimitWindowLong(key: string): string {
|
|
69
|
-
if (key === 'five_hour')
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
if (key === 'five_hour') {
|
|
99
|
+
return '5-hour session'
|
|
100
|
+
}
|
|
101
|
+
if (key === 'seven_day') {
|
|
102
|
+
return 'Weekly'
|
|
103
|
+
}
|
|
104
|
+
if (key === 'seven_day_oauth_apps') {
|
|
105
|
+
return 'Weekly · apps'
|
|
106
|
+
}
|
|
72
107
|
const capitalize = (s: string) => s.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
73
|
-
if (!key.startsWith('seven_day_'))
|
|
108
|
+
if (!key.startsWith('seven_day_')) {
|
|
109
|
+
return capitalize(key.replaceAll('_', ' '))
|
|
110
|
+
}
|
|
74
111
|
return `Weekly · ${capitalize(key.slice('seven_day_'.length).replaceAll('_', ' '))}`
|
|
75
112
|
}
|
|
76
113
|
|
|
77
|
-
/**
|
|
78
|
-
* How long a rate-limit window is, in seconds — the denominator behind the pace
|
|
79
|
-
* marker. Derived from the key rather than reported: the CLI sends a reset time
|
|
80
|
-
* and a percentage, never a duration. `undefined` for a window whose key doesn't
|
|
81
|
-
* say, and the marker is then simply not drawn rather than guessed.
|
|
82
|
-
*/
|
|
83
114
|
export function rateLimitWindowSeconds(key: string): number | undefined {
|
|
84
|
-
if (key === 'five_hour')
|
|
85
|
-
|
|
115
|
+
if (key === 'five_hour') {
|
|
116
|
+
return 5 * 3600
|
|
117
|
+
}
|
|
118
|
+
if (key.startsWith('seven_day')) {
|
|
119
|
+
return 7 * 86_400
|
|
120
|
+
}
|
|
86
121
|
return undefined
|
|
87
122
|
}
|
|
88
123
|
|
|
89
|
-
/** "8 secs ago" / "3 mins ago" — a freshness line finer-grained than
|
|
90
|
-
* {@link formatRelativeTime}, because a poll that just landed should say so. */
|
|
91
124
|
export function formatAgoPrecise(epochMs: number, now = Date.now()): string {
|
|
92
125
|
const seconds = Math.max(0, Math.floor((now - epochMs) / 1000))
|
|
93
|
-
if (seconds < 60)
|
|
126
|
+
if (seconds < 60) {
|
|
127
|
+
return `${seconds} sec${seconds === 1 ? '' : 's'} ago`
|
|
128
|
+
}
|
|
94
129
|
if (seconds < 3600) {
|
|
95
130
|
const minutes = Math.floor(seconds / 60)
|
|
96
131
|
return `${minutes} min${minutes === 1 ? '' : 's'} ago`
|
|
@@ -99,13 +134,13 @@ export function formatAgoPrecise(epochMs: number, now = Date.now()): string {
|
|
|
99
134
|
return `${hours} hour${hours === 1 ? '' : 's'} ago`
|
|
100
135
|
}
|
|
101
136
|
|
|
102
|
-
/** Compact one-line preview of a tool input for card headers. */
|
|
103
137
|
export function toolInputPreview(input: unknown, max = 80): string {
|
|
104
|
-
if (input === null || input === undefined)
|
|
138
|
+
if (input === null || input === undefined) {
|
|
139
|
+
return ''
|
|
140
|
+
}
|
|
105
141
|
if (typeof input === 'object') {
|
|
106
142
|
const o = input as Record<string, unknown>
|
|
107
|
-
const primary =
|
|
108
|
-
o.command ?? o.file_path ?? o.path ?? o.url ?? o.pattern ?? o.query ?? o.description
|
|
143
|
+
const primary = o.command ?? o.file_path ?? o.path ?? o.url ?? o.pattern ?? o.query ?? o.description
|
|
109
144
|
if (typeof primary === 'string') {
|
|
110
145
|
return primary.length > max ? primary.slice(0, max - 1) + '…' : primary
|
|
111
146
|
}
|
|
@@ -114,9 +149,6 @@ export function toolInputPreview(input: unknown, max = 80): string {
|
|
|
114
149
|
return text.length > max ? text.slice(0, max - 1) + '…' : text
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
/** Families whose name isn't just a capitalised first letter, and how the
|
|
118
|
-
* vendor writes the version after it. GPT is `GPT-5.6`; everyone else spaces
|
|
119
|
-
* it. Anything unlisted is title-cased and spaced. */
|
|
120
152
|
const MODEL_FAMILIES: Record<string, { name: string; joiner?: string }> = {
|
|
121
153
|
gpt: { name: 'GPT', joiner: '-' },
|
|
122
154
|
deepseek: { name: 'DeepSeek' },
|
|
@@ -128,53 +160,36 @@ const MODEL_FAMILIES: Record<string, { name: string; joiner?: string }> = {
|
|
|
128
160
|
grok: { name: 'Grok' },
|
|
129
161
|
}
|
|
130
162
|
|
|
131
|
-
/**
|
|
132
|
-
* The name a person says, from a wire model id:
|
|
133
|
-
*
|
|
134
|
-
* - `claude-opus-5[1m]` → "Opus 5"
|
|
135
|
-
* - `claude-haiku-4-5-20251001` → "Haiku 4.5"
|
|
136
|
-
* - `gpt-5.6-luna` → "GPT-5.6 Luna"
|
|
137
|
-
* - `gemini-2.5-pro` → "Gemini 2.5 Pro"
|
|
138
|
-
* - `o3-mini` → "o3 Mini"
|
|
139
|
-
*
|
|
140
|
-
* Three kinds of token after the family, because vendors mix them freely: a
|
|
141
|
-
* **version** (`5`, `4-5`, `5.6` — joined with dots, since Anthropic splits what
|
|
142
|
-
* OpenAI writes as one token), a **code name or tier** (`luna`, `codex`, `pro`,
|
|
143
|
-
* `mini` — kept and capitalised, since it is often the only thing telling two
|
|
144
|
-
* models apart), and a **snapshot date** (`20251001` — dropped; it is a build,
|
|
145
|
-
* not a version).
|
|
146
|
-
*
|
|
147
|
-
* Anything genuinely unreadable falls back to the id: a wrong name is worse than
|
|
148
|
-
* a raw one, which is at least true.
|
|
149
|
-
*
|
|
150
|
-
* The server has a narrower version of this (`friendlyModelName` in core's
|
|
151
|
-
* `normalize.ts`) that derives Claude catalog names at authoring time. This one
|
|
152
|
-
* is the *render-time* fallback for an id with no catalog row behind it — the
|
|
153
|
-
* sidebar has only `SessionInfo.model` — so it has to cope with every vendor the
|
|
154
|
-
* provider engine can reach, not just the CLI's own.
|
|
155
|
-
*/
|
|
156
163
|
export function friendlyModel(id: string | undefined): string | undefined {
|
|
157
|
-
if (!id)
|
|
164
|
+
if (!id) {
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
158
167
|
const withoutVariant = id.split('[')[0] ?? id
|
|
159
168
|
const parts = withoutVariant.toLowerCase().split('-').filter(Boolean)
|
|
160
|
-
if (parts[0] === 'claude')
|
|
169
|
+
if (parts[0] === 'claude') {
|
|
170
|
+
parts.shift()
|
|
171
|
+
}
|
|
161
172
|
const familyToken = parts.shift()
|
|
162
|
-
if (!familyToken)
|
|
173
|
+
if (!familyToken) {
|
|
174
|
+
return id
|
|
175
|
+
}
|
|
163
176
|
const family = MODEL_FAMILIES[familyToken]
|
|
164
177
|
const name =
|
|
165
178
|
family?.name ??
|
|
166
|
-
// OpenAI's reasoning series is lower-case by its own convention ('o3-mini')
|
|
167
|
-
|
|
168
|
-
(/^o\d+$/.test(familyToken)
|
|
169
|
-
? familyToken
|
|
170
|
-
: `${familyToken.charAt(0).toUpperCase()}${familyToken.slice(1)}`)
|
|
179
|
+
// OpenAI's reasoning series is lower-case by its own convention ('o3-mini'); "O3" reads as a different product.
|
|
180
|
+
(/^o\d+$/.test(familyToken) ? familyToken : `${familyToken.charAt(0).toUpperCase()}${familyToken.slice(1)}`)
|
|
171
181
|
|
|
172
182
|
const version: string[] = []
|
|
173
183
|
const words: string[] = []
|
|
174
184
|
for (const part of parts) {
|
|
175
|
-
if (/^\d{8}$/.test(part))
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
if (/^\d{8}$/.test(part)) {
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if (/^\d+(\.\d+)?$/.test(part)) {
|
|
189
|
+
version.push(part)
|
|
190
|
+
} else {
|
|
191
|
+
words.push(`${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
192
|
+
}
|
|
178
193
|
}
|
|
179
194
|
const versioned = version.length > 0 ? `${name}${family?.joiner ?? ' '}${version.join('.')}` : name
|
|
180
195
|
return [versioned, ...words].join(' ')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type PlanRequestLike = { toolName?: unknown; input?: unknown }
|
|
2
|
+
|
|
3
|
+
export function planFromRequest(request: PlanRequestLike | null | undefined): string | undefined {
|
|
4
|
+
if (request?.toolName !== 'ExitPlanMode') {
|
|
5
|
+
return undefined
|
|
6
|
+
}
|
|
7
|
+
const input = request.input
|
|
8
|
+
if (input === null || typeof input !== 'object') {
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
11
|
+
const plan = (input as { plan?: unknown }).plan
|
|
12
|
+
if (typeof plan !== 'string' || plan.trim().length === 0) {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
return plan
|
|
16
|
+
}
|
package/src/lib/status.ts
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import type { ModelOption, RateLimitInfo, SessionStatus } from '@workerdeck/protocol'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* How a session's live readings become a status line — the pure half, so every
|
|
5
|
-
* host spells "Needs approval", "80% is a warning" and "which window is the
|
|
6
|
-
* binding one" the same way.
|
|
7
|
-
*
|
|
8
|
-
* Structurally typed against `SessionVitals` rather than importing it: this file
|
|
9
|
-
* ships from the React-free `@workerdeck/ui/format` entry, and a host drawing
|
|
10
|
-
* the readings outside React (the VS Code extension host in the window status
|
|
11
|
-
* bar) must not pull a component graph in to do it. A real `SessionVitals`
|
|
12
|
-
* satisfies every shape here.
|
|
13
|
-
*/
|
|
14
3
|
export type StatusSeverity = 'none' | 'warning' | 'error'
|
|
15
4
|
|
|
16
|
-
/** What a status slot shows, before any host's icon vocabulary gets involved.
|
|
17
|
-
* `icon` is a VS Code codicon name — the one host-shaped thing left, because the
|
|
18
|
-
* alternative is a second mapping table in the only consumer. */
|
|
19
5
|
export type StatusPresentation = {
|
|
20
6
|
icon: string
|
|
21
7
|
label: string
|
|
@@ -24,8 +10,6 @@ export type StatusPresentation = {
|
|
|
24
10
|
|
|
25
11
|
export type StatusReadings = {
|
|
26
12
|
status: SessionStatus
|
|
27
|
-
/** `@workerdeck/react`'s `ConnectionState`, structurally — the link state wins
|
|
28
|
-
* the slot, so it has to be part of the reading. */
|
|
29
13
|
connection?: 'live' | 'reconnecting' | 'offline'
|
|
30
14
|
}
|
|
31
15
|
|
|
@@ -39,13 +23,10 @@ const STATUS_META: Record<SessionStatus, StatusPresentation> = {
|
|
|
39
23
|
closed: { icon: 'circle-slash', label: 'Closed', severity: 'none' },
|
|
40
24
|
}
|
|
41
25
|
|
|
42
|
-
/**
|
|
43
|
-
* The status slot, connection first. A session status held over a dead socket is
|
|
44
|
-
* the last thing we heard, not the current state — so a lost link takes the slot
|
|
45
|
-
* rather than letting "Running" imply a turn is still streaming.
|
|
46
|
-
*/
|
|
47
26
|
export function statusPresentation(vitals: StatusReadings | undefined): StatusPresentation {
|
|
48
|
-
if (!vitals)
|
|
27
|
+
if (!vitals) {
|
|
28
|
+
return { icon: 'hubot', label: 'Connecting…', severity: 'none' }
|
|
29
|
+
}
|
|
49
30
|
if (vitals.connection === 'offline') {
|
|
50
31
|
return { icon: 'debug-disconnect', label: 'Offline', severity: 'error' }
|
|
51
32
|
}
|
|
@@ -55,46 +36,28 @@ export function statusPresentation(vitals: StatusReadings | undefined): StatusPr
|
|
|
55
36
|
return STATUS_META[vitals.status] ?? { icon: 'hubot', label: vitals.status, severity: 'none' }
|
|
56
37
|
}
|
|
57
38
|
|
|
58
|
-
/** 0–100 → the colour a meter wears. One pair of thresholds for every surface. */
|
|
59
39
|
export function meterSeverity(pct: number | undefined): StatusSeverity {
|
|
60
|
-
if (pct === undefined)
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
if (pct === undefined) {
|
|
41
|
+
return 'none'
|
|
42
|
+
}
|
|
43
|
+
if (pct >= 95) {
|
|
44
|
+
return 'error'
|
|
45
|
+
}
|
|
46
|
+
if (pct >= 80) {
|
|
47
|
+
return 'warning'
|
|
48
|
+
}
|
|
63
49
|
return 'none'
|
|
64
50
|
}
|
|
65
51
|
|
|
66
|
-
/**
|
|
67
|
-
* The three *lanes* a plan-usage reading can occupy, and the whole reason this
|
|
68
|
-
* is a rule rather than a per-client `if`.
|
|
69
|
-
*
|
|
70
|
-
* The CLI reports one window per limit (`five_hour`, `seven_day`, and a
|
|
71
|
-
* `seven_day_<model>` bucket per model-scoped limit — `seven_day_opus`,
|
|
72
|
-
* `seven_day_sonnet`, and whatever `model_scoped` names next). A surface with
|
|
73
|
-
* one slot shows the fullest of them, which is why `tightestWindow` exists —
|
|
74
|
-
* but that answers "what is closest to blocking me", not "how much of *this
|
|
75
|
-
* session's* budget have I spent", and those are different questions a reader
|
|
76
|
-
* asks at different times. A weekly window at 71% will win the single slot over
|
|
77
|
-
* a five-hour window at 60% every time, so the reading you actually watch while
|
|
78
|
-
* working is the one you can never see.
|
|
79
|
-
*
|
|
80
|
-
* Hence three lanes, each independently showable:
|
|
81
|
-
*
|
|
82
|
-
* - `'session'` — the five-hour window. The one that resets while you work.
|
|
83
|
-
* - `'weekly'` — the plain seven-day window, the account-wide ceiling.
|
|
84
|
-
* - `'model'` — the fullest of the *model-scoped* weekly buckets. Deliberately
|
|
85
|
-
* not a named model: which models have their own bucket is the plan's
|
|
86
|
-
* business and changes without notice, so a client that hardcoded
|
|
87
|
-
* `seven_day_opus` would show nothing the month it becomes something else.
|
|
88
|
-
* The label comes from the key, so this lane names whatever it found.
|
|
89
|
-
*/
|
|
90
52
|
export type UsageLane = 'session' | 'weekly' | 'model'
|
|
91
53
|
|
|
92
|
-
/** The window a lane points at, or `undefined` when this account has none. */
|
|
93
54
|
export function usageWindow(
|
|
94
55
|
rateLimits: Record<string, RateLimitInfo> | undefined,
|
|
95
56
|
lane: UsageLane,
|
|
96
57
|
): { key: string; info: RateLimitInfo } | undefined {
|
|
97
|
-
if (!rateLimits)
|
|
58
|
+
if (!rateLimits) {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
98
61
|
if (lane === 'session') {
|
|
99
62
|
const info = rateLimits.five_hour
|
|
100
63
|
return info ? { key: 'five_hour', info } : undefined
|
|
@@ -103,27 +66,19 @@ export function usageWindow(
|
|
|
103
66
|
const info = rateLimits.seven_day
|
|
104
67
|
return info ? { key: 'seven_day', info } : undefined
|
|
105
68
|
}
|
|
106
|
-
// Model-scoped: same "fullest wins" rule as the single slot, over the subset.
|
|
107
69
|
const scoped = Object.fromEntries(
|
|
108
|
-
Object.entries(rateLimits).filter(
|
|
109
|
-
([key]) => key.startsWith('seven_day_') && key !== 'seven_day_oauth_apps',
|
|
110
|
-
),
|
|
70
|
+
Object.entries(rateLimits).filter(([key]) => key.startsWith('seven_day_') && key !== 'seven_day_oauth_apps'),
|
|
111
71
|
)
|
|
112
72
|
return tightestWindow(scoped)
|
|
113
73
|
}
|
|
114
74
|
|
|
115
|
-
|
|
116
|
-
* since the binding constraint is the one worth glancing at. Still the right
|
|
117
|
-
* rule for a surface with exactly one slot; {@link usageWindow} is for one with
|
|
118
|
-
* three. */
|
|
119
|
-
export function tightestWindow(
|
|
120
|
-
rateLimits: Record<string, RateLimitInfo> | undefined,
|
|
121
|
-
): { key: string; info: RateLimitInfo } | undefined {
|
|
75
|
+
export function tightestWindow(rateLimits: Record<string, RateLimitInfo> | undefined): { key: string; info: RateLimitInfo } | undefined {
|
|
122
76
|
const entries = Object.entries(rateLimits ?? {})
|
|
123
|
-
if (entries.length === 0)
|
|
77
|
+
if (entries.length === 0) {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
124
80
|
let best: { key: string; info: RateLimitInfo } | undefined
|
|
125
81
|
for (const [key, info] of entries) {
|
|
126
|
-
// A rejected window outranks any utilization: it is the one actually blocking.
|
|
127
82
|
const rank = info.status === 'rejected' ? Number.POSITIVE_INFINITY : (info.utilization ?? -1)
|
|
128
83
|
const bestRank =
|
|
129
84
|
best === undefined
|
|
@@ -131,18 +86,20 @@ export function tightestWindow(
|
|
|
131
86
|
: best.info.status === 'rejected'
|
|
132
87
|
? Number.POSITIVE_INFINITY
|
|
133
88
|
: (best.info.utilization ?? -1)
|
|
134
|
-
if (rank > bestRank)
|
|
89
|
+
if (rank > bestRank) {
|
|
90
|
+
best = { key, info }
|
|
91
|
+
}
|
|
135
92
|
}
|
|
136
93
|
return best
|
|
137
94
|
}
|
|
138
95
|
|
|
139
|
-
/** A rate-limit window's key, named for a human. A model-scoped bucket is named
|
|
140
|
-
* for its model alone (`seven_day_fable` → "Fable"): the lane it sits in
|
|
141
|
-
* already says weekly, and "Seven day fable" in a status bar is three words to
|
|
142
|
-
* say one. */
|
|
143
96
|
export function windowLabel(key: string): string {
|
|
144
|
-
if (key === 'five_hour')
|
|
145
|
-
|
|
97
|
+
if (key === 'five_hour') {
|
|
98
|
+
return 'Session'
|
|
99
|
+
}
|
|
100
|
+
if (key === 'seven_day') {
|
|
101
|
+
return 'Weekly'
|
|
102
|
+
}
|
|
146
103
|
const scoped = key.startsWith('seven_day_') ? key.slice('seven_day_'.length) : key
|
|
147
104
|
const words = scoped.replaceAll('_', ' ')
|
|
148
105
|
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
@@ -150,44 +107,37 @@ export function windowLabel(key: string): string {
|
|
|
150
107
|
|
|
151
108
|
export type ModelReadings = { model?: string; models: readonly ModelOption[] }
|
|
152
109
|
|
|
153
|
-
/**
|
|
154
|
-
* The catalog row a session is actually running, or `undefined` for a model the
|
|
155
|
-
* list doesn't name. Matched leniently: a session reports the *resolved* id
|
|
156
|
-
* (`claude-sonnet-5`) where the row may be keyed on the alias (`sonnet`), and
|
|
157
|
-
* either can carry a `[1m]` context-window suffix.
|
|
158
|
-
*/
|
|
159
110
|
export function currentModel(vitals: ModelReadings | undefined): ModelOption | undefined {
|
|
160
111
|
const id = vitals?.model
|
|
161
|
-
if (!id)
|
|
112
|
+
if (!id) {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
162
115
|
const bare = (value: string) => value.replace(/\[.*\]$/, '')
|
|
163
116
|
const wanted = bare(id)
|
|
164
|
-
return vitals.models.find(
|
|
165
|
-
(m) => bare(m.value) === wanted || (m.resolvedModel && bare(m.resolvedModel) === wanted),
|
|
166
|
-
)
|
|
117
|
+
return vitals.models.find((m) => bare(m.value) === wanted || (m.resolvedModel && bare(m.resolvedModel) === wanted))
|
|
167
118
|
}
|
|
168
119
|
|
|
169
|
-
/** A session's model, named the way the picker names it. Falls back to the raw
|
|
170
|
-
* id, and to "Default" while the session is on the CLI's own pick. */
|
|
171
120
|
export function modelLabel(vitals: ModelReadings | undefined): string {
|
|
172
|
-
if (!vitals?.model)
|
|
121
|
+
if (!vitals?.model) {
|
|
122
|
+
return 'Default'
|
|
123
|
+
}
|
|
173
124
|
return currentModel(vitals)?.displayName ?? vitals.model
|
|
174
125
|
}
|
|
175
126
|
|
|
176
|
-
/** Context percentage as its meter severity — the reading and the colour come
|
|
177
|
-
* from one place so a panel and a status bar never disagree.
|
|
178
|
-
*
|
|
179
|
-
* Takes only the number it reads, so the compact `ContextReading` that rides
|
|
180
|
-
* the sessions list and the full `ContextUsage` on the event stream are coloured
|
|
181
|
-
* by one rule rather than two. */
|
|
182
127
|
export function contextSeverity(usage: { percentage: number } | undefined): StatusSeverity {
|
|
183
128
|
return meterSeverity(usage?.percentage)
|
|
184
129
|
}
|
|
185
130
|
|
|
186
|
-
/** {@link meterSeverity} as a text colour class. Here rather than beside each
|
|
187
|
-
* meter because the status bar, the context dialog and a sessions-list row all
|
|
188
|
-
* paint the same reading, and a fourth copy of the thresholds is a fourth
|
|
189
|
-
* chance for one surface to call 81% orange and another call it grey. */
|
|
190
131
|
export function meterColorClass(pct: number | undefined): string {
|
|
191
132
|
const severity = meterSeverity(pct)
|
|
192
133
|
return severity === 'error' ? 'text-danger' : severity === 'warning' ? 'text-warning' : 'text-fg-3'
|
|
193
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The fill colour for a meter bar. Deliberately a different ramp from
|
|
138
|
+
* {@link meterColorClass}: a bar reads as alarming later than a number does, so the tint
|
|
139
|
+
* turns at 70/90 where the text severity turns at 80/95.
|
|
140
|
+
*/
|
|
141
|
+
export function meterTintClass(pct: number): string {
|
|
142
|
+
return pct >= 90 ? 'bg-danger' : pct >= 70 ? 'bg-warning' : 'bg-accent'
|
|
143
|
+
}
|
package/src/lib/tool-icon.ts
CHANGED
|
@@ -18,83 +18,73 @@ import {
|
|
|
18
18
|
Wrench,
|
|
19
19
|
} from 'lucide-react'
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* An icon per tool, so a transcript can be skimmed by shape rather than read.
|
|
23
|
-
*
|
|
24
|
-
* The same mapping the iOS app makes, in lucide's vocabulary rather than SF
|
|
25
|
-
* Symbols — the two clients should be recognisably showing the same thing. An
|
|
26
|
-
* unknown tool falls back to a wrench, and an MCP tool (`mcp__server__name`) to
|
|
27
|
-
* the puzzle piece the MCP screens use, because "which server is this from" is
|
|
28
|
-
* the useful thing to see at a glance.
|
|
29
|
-
*/
|
|
30
21
|
export function toolIcon(toolName: string): LucideIcon {
|
|
31
22
|
switch (toolName) {
|
|
32
23
|
case 'Bash':
|
|
33
24
|
case 'BashOutput':
|
|
34
|
-
case 'KillShell':
|
|
25
|
+
case 'KillShell': {
|
|
35
26
|
return Terminal
|
|
36
|
-
|
|
27
|
+
}
|
|
28
|
+
case 'Read': {
|
|
37
29
|
return FileText
|
|
38
|
-
|
|
30
|
+
}
|
|
31
|
+
case 'Write': {
|
|
39
32
|
return SquarePen
|
|
33
|
+
}
|
|
40
34
|
case 'Edit':
|
|
41
35
|
case 'MultiEdit':
|
|
42
|
-
case 'NotebookEdit':
|
|
36
|
+
case 'NotebookEdit': {
|
|
43
37
|
return PencilLine
|
|
44
|
-
|
|
38
|
+
}
|
|
39
|
+
case 'Glob': {
|
|
45
40
|
return FolderSearch
|
|
46
|
-
|
|
41
|
+
}
|
|
42
|
+
case 'Grep': {
|
|
47
43
|
return Search
|
|
48
|
-
|
|
44
|
+
}
|
|
45
|
+
case 'WebFetch': {
|
|
49
46
|
return ArrowDownCircle
|
|
50
|
-
|
|
47
|
+
}
|
|
48
|
+
case 'WebSearch': {
|
|
51
49
|
return Globe
|
|
50
|
+
}
|
|
52
51
|
case 'Task':
|
|
53
|
-
case 'Agent':
|
|
52
|
+
case 'Agent': {
|
|
54
53
|
return UsersRound
|
|
55
|
-
|
|
54
|
+
}
|
|
55
|
+
case 'TodoWrite': {
|
|
56
56
|
return CheckSquare
|
|
57
|
-
|
|
57
|
+
}
|
|
58
|
+
case 'Skill': {
|
|
58
59
|
return Sparkles
|
|
59
|
-
|
|
60
|
+
}
|
|
61
|
+
case 'AskUserQuestion': {
|
|
60
62
|
return MessageCircleQuestion
|
|
61
|
-
|
|
62
|
-
case 'CodexCommand':
|
|
63
|
+
}
|
|
64
|
+
case 'CodexCommand': {
|
|
63
65
|
return Terminal
|
|
64
|
-
|
|
66
|
+
}
|
|
67
|
+
case 'CodexFileChange': {
|
|
65
68
|
return FileDiff
|
|
66
|
-
|
|
69
|
+
}
|
|
70
|
+
case 'CodexWebSearch': {
|
|
67
71
|
return Globe
|
|
72
|
+
}
|
|
68
73
|
case 'CodexImageGeneration':
|
|
69
|
-
case 'CodexImageView':
|
|
74
|
+
case 'CodexImageView': {
|
|
70
75
|
return Image
|
|
71
|
-
|
|
76
|
+
}
|
|
77
|
+
default: {
|
|
72
78
|
return toolName.startsWith('mcp__') ? Puzzle : Wrench
|
|
79
|
+
}
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
* Is this tool a shell command?
|
|
78
|
-
*
|
|
79
|
-
* Its own question because a run of them is *one* thing the reader skims past:
|
|
80
|
-
* the terminal theme collapses consecutive shell calls into a single "Ran N
|
|
81
|
-
* shell commands" line, the way the CLI does. Names from both first-party
|
|
82
|
-
* engines. `BashOutput`/`KillShell` are excluded on purpose — they manage a
|
|
83
|
-
* background shell rather than run something, and folding them into the count
|
|
84
|
-
* would inflate it.
|
|
85
|
-
*/
|
|
83
|
+
// `BashOutput`/`KillShell` are excluded on purpose: they manage a background shell rather than run one.
|
|
86
84
|
export function isShellTool(toolName: string): boolean {
|
|
87
85
|
return toolName === 'Bash' || toolName === 'CodexCommand'
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
/**
|
|
91
|
-
* Does this tool *change* the workspace?
|
|
92
|
-
*
|
|
93
|
-
* Worth its own colour in a transcript: skimming a run, "what did it edit" is a
|
|
94
|
-
* different question from "what did it look at", and a write is the one you
|
|
95
|
-
* might need to undo. Names from both first-party engines; an MCP tool is
|
|
96
|
-
* unknowable from its name, so it reads as neutral rather than guessed.
|
|
97
|
-
*/
|
|
98
88
|
export function isMutatingTool(toolName: string): boolean {
|
|
99
89
|
switch (toolName) {
|
|
100
90
|
case 'Write':
|
|
@@ -102,9 +92,11 @@ export function isMutatingTool(toolName: string): boolean {
|
|
|
102
92
|
case 'MultiEdit':
|
|
103
93
|
case 'NotebookEdit':
|
|
104
94
|
case 'Update':
|
|
105
|
-
case 'CodexFileChange':
|
|
95
|
+
case 'CodexFileChange': {
|
|
106
96
|
return true
|
|
107
|
-
|
|
97
|
+
}
|
|
98
|
+
default: {
|
|
108
99
|
return false
|
|
100
|
+
}
|
|
109
101
|
}
|
|
110
102
|
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { clsx, type ClassValue } from 'clsx'
|
|
2
2
|
import { extendTailwindMerge } from 'tailwind-merge'
|
|
3
3
|
|
|
4
|
-
// Register the custom text-*
|
|
5
|
-
// tailwind-merge collapses a size class and a color class (e.g. `text-label
|
|
6
|
-
// text-muted-foreground`) into one bucket and silently drops the size.
|
|
4
|
+
// Register the custom text-* utilities as a font-size group, or tailwind-merge buckets a size class with a colour class and drops the size.
|
|
7
5
|
const twMerge = extendTailwindMerge({
|
|
8
6
|
extend: {
|
|
9
7
|
classGroups: {
|