@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
|
@@ -11,132 +11,22 @@ import type { Step } from './SessionSteps.tsx'
|
|
|
11
11
|
import { cn } from '../../lib/utils.ts'
|
|
12
12
|
import { formatCost, formatRelativeTime, friendlyModel } from '../../lib/format.ts'
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* One session in a list — the **card**, and the only drawing of it in the
|
|
16
|
-
* product.
|
|
17
|
-
*
|
|
18
|
-
* The dashboard's `SessionBrowser` and the VS Code sidebar both render this;
|
|
19
|
-
* before it existed they rendered two hand-kept copies that agreed on the model
|
|
20
|
-
* and disagreed on every measurement, and the two lists read as two products.
|
|
21
|
-
* iOS mirrors this file's geometry rather than either copy.
|
|
22
|
-
*
|
|
23
|
-
* ## The shape
|
|
24
|
-
*
|
|
25
|
-
* A 4px-padded, 4px-rounded card whose whole surface is the hit target, holding
|
|
26
|
-
* two 20px lines and, when it is open, the work under them.
|
|
27
|
-
*
|
|
28
|
-
* **Line one is state and identity**: the status glyph leads, the title takes
|
|
29
|
-
* the rest, and the line closes with the unread badge and the context ring. The
|
|
30
|
-
* glyph leads because a sessions list is scanned for *state* first — which of
|
|
31
|
-
* these is working, which is waiting on me — and the title is what you read once
|
|
32
|
-
* the glyph has told you which row to read. The two readings that close the line
|
|
33
|
-
* are the two that change while you are looking at them.
|
|
34
|
-
*
|
|
35
|
-
* **Line two is identity and cost**: the engine's mark and its model in the
|
|
36
|
-
* vendor's own colour, then the project, then everything else the host wants
|
|
37
|
-
* said, then the age — a `·`-joined run that ends in the one part of it that
|
|
38
|
-
* keeps moving. The line's right edge belongs to the disclosure (which doubles
|
|
39
|
-
* as the count of the work below, so the card says how much there is without
|
|
40
|
-
* being opened) and to the host's own actions.
|
|
41
|
-
*
|
|
42
|
-
* **The gutter is one 16px cell, shared by both lines.** The two glyphs are
|
|
43
|
-
* different sizes and laying them out as plain flex children starts the two
|
|
44
|
-
* text columns at different x — two pixels, invisible as a measurement and
|
|
45
|
-
* obvious as a misalignment. Centring inside a fixed cell fixes the text edges
|
|
46
|
-
* and the ink centres at once. It is the terminal gutter's argument at card
|
|
47
|
-
* scale.
|
|
48
|
-
*
|
|
49
|
-
* ## Selection
|
|
50
|
-
*
|
|
51
|
-
* **Selection is the card's own fill**, not a gutter bar: the card is an inset
|
|
52
|
-
* shape with air around it, so filling it is unambiguous in a way a fill on a
|
|
53
|
-
* full-bleed row is not, and it leaves the left edge to the state glyph.
|
|
54
|
-
*
|
|
55
|
-
* There are **two selections at two grains**, and the card and its steps split
|
|
56
|
-
* them:
|
|
57
|
-
*
|
|
58
|
-
* | what is on screen | card | the step |
|
|
59
|
-
* | --- | --- | --- |
|
|
60
|
-
* | nothing | transparent, hovers | transparent, hovers |
|
|
61
|
-
* | this session | `--row-selected` (blue) | transparent, hovers |
|
|
62
|
-
* | one of its sub-agents | `--row-selected-weak` (grey) | `--row-selected` (blue) |
|
|
63
|
-
*
|
|
64
|
-
* **The blue always marks the finest thing selected.** Opening a sub-agent
|
|
65
|
-
* selects its session too, so both claims are true at once and the blue can only
|
|
66
|
-
* carry one; it goes to the agent, and the card steps back to grey — present,
|
|
67
|
-
* holding what you are looking at, not itself the thing you are looking at.
|
|
68
|
-
* Reversing that (blue card, blue row inside it) says nothing, which is what a
|
|
69
|
-
* blue-on-blue card looked like.
|
|
70
|
-
*
|
|
71
|
-
* A filled card does **not** also answer hover: the fill is already spent, and a
|
|
72
|
-
* selected row that lightened under the pointer read as a third state nobody
|
|
73
|
-
* could name. Its steps still do, on `--row-active`, which is a tint and so
|
|
74
|
-
* works on all three grounds.
|
|
75
|
-
*
|
|
76
|
-
* A **task** never takes the blue. It is a reference to a place inside a
|
|
77
|
-
* session, so pressing one selects the session and travels to its marker — see
|
|
78
|
-
* `StepRow`.
|
|
79
|
-
*/
|
|
80
14
|
export interface SessionItemProps {
|
|
81
15
|
row: SessionRow
|
|
82
|
-
/** This session is the one the host is showing. */
|
|
83
16
|
active?: boolean
|
|
84
|
-
/**
|
|
85
|
-
* Which sub-agent is open, by `Step.key` — the *finer* of the two selections.
|
|
86
|
-
* When it names one of this card's agents, that step takes the blue and the
|
|
87
|
-
* card drops to the secondary grey. A key belonging to some other card's
|
|
88
|
-
* agent, or to a task, changes nothing here.
|
|
89
|
-
*/
|
|
90
17
|
activeStepKey?: string
|
|
91
|
-
/** Shown when the list is not already grouped by gateway. */
|
|
92
18
|
showGateway?: boolean
|
|
93
|
-
/**
|
|
94
|
-
* False when the list is already grouped by project — the header above has
|
|
95
|
-
* said the name, so the card must not spend its metadata line repeating it.
|
|
96
|
-
* What takes the slot is the *sub-path inside the project*
|
|
97
|
-
* (`projectSubpath`): `packages/ui`, `packages/server` — the one thing the
|
|
98
|
-
* header cannot say and the only thing that tells two sessions in one repo
|
|
99
|
-
* apart. A session at the project root has nothing to add and the slot
|
|
100
|
-
* disappears entirely. Exactly the rule `showGateway` follows one facet over.
|
|
101
|
-
*/
|
|
102
19
|
showProject?: boolean
|
|
103
|
-
/** Resolved project-icon bytes by content hash. A hash this map has not got
|
|
104
|
-
* yet simply draws no icon. */
|
|
105
20
|
projectIcons?: Record<string, string>
|
|
106
|
-
/** Uncontrolled by default — pass both to drive expansion from the host. */
|
|
107
21
|
expanded?: boolean
|
|
108
22
|
onExpandedChange?: (expanded: boolean) => void
|
|
109
23
|
onSelect?: () => void
|
|
110
|
-
/** Open the session *at* one of its sub-agents — hand the panel body over to
|
|
111
|
-
* that agent's own work. Only ever called for an **agent** step. Absent is not
|
|
112
|
-
* a missing feature: without it a step just opens the session, which is all a
|
|
113
|
-
* host with no sub-agent surface can offer. */
|
|
114
24
|
onSelectSubagent?: (toolUseId: string) => void
|
|
115
|
-
/**
|
|
116
|
-
* Open the session and travel to a **task**'s row in the transcript — where
|
|
117
|
-
* the work was started, and where it finished.
|
|
118
|
-
*
|
|
119
|
-
* A separate seam from `onSelectSubagent` because the destinations are
|
|
120
|
-
* different, and conflating them is not a styling detail: a task has no agent
|
|
121
|
-
* behind it, so framing its tool-use id selects **no items at all** and the
|
|
122
|
-
* panel draws an empty agent view. That was the bug. A task is a reference,
|
|
123
|
-
* and the only honest thing to do with a reference is follow it.
|
|
124
|
-
*/
|
|
125
25
|
onRevealStep?: (toolUseId: string) => void
|
|
126
|
-
/** Enables in-place rename. The trigger is the host's — see `renameOn`. */
|
|
127
26
|
onRename?: (title: string) => void
|
|
128
|
-
/**
|
|
129
|
-
* How a rename starts. `doubleClick` is the editor feel (a rename is a thing
|
|
130
|
-
* you do to the word you are looking at); `external` means the host opens it
|
|
131
|
-
* from its own affordance — a menu entry, a pencil in `actions` — and drives
|
|
132
|
-
* it through `editing`/`onEditingChange`.
|
|
133
|
-
*/
|
|
134
27
|
renameOn?: 'doubleClick' | 'external'
|
|
135
28
|
editing?: boolean
|
|
136
29
|
onEditingChange?: (editing: boolean) => void
|
|
137
|
-
/** The card's trailing controls, at the end of line two. A single overflow
|
|
138
|
-
* glyph is what the design draws; a host with three always-on actions can put
|
|
139
|
-
* them here instead. */
|
|
140
30
|
actions?: ReactNode
|
|
141
31
|
className?: string
|
|
142
32
|
}
|
|
@@ -161,9 +51,6 @@ export function SessionItem({
|
|
|
161
51
|
className,
|
|
162
52
|
}: SessionItemProps) {
|
|
163
53
|
const { info } = row
|
|
164
|
-
// Expansion is the card's own state unless the host takes it, and is
|
|
165
|
-
// deliberately not persisted: a list that reopened yesterday's work on every
|
|
166
|
-
// reload would be showing a settled tail nobody asked for.
|
|
167
54
|
const [ownExpanded, setOwnExpanded] = useState(false)
|
|
168
55
|
const open = expanded ?? ownExpanded
|
|
169
56
|
const setOpen = (next: boolean) => {
|
|
@@ -178,20 +65,9 @@ export function SessionItem({
|
|
|
178
65
|
}
|
|
179
66
|
|
|
180
67
|
const engine = info.engine ?? 'claude'
|
|
181
|
-
// protocol's own spelling, so this card, the group header above it and the
|
|
182
|
-
// project facet cannot disagree about what a project is called.
|
|
183
68
|
const project = showProject ? projectLabel(row) : projectSubpath(row)
|
|
184
69
|
const projectIcon = showProject ? info.project?.icon : undefined
|
|
185
70
|
const iconSrc = projectIcon?.type === 'image' ? projectIcons?.[projectIcon.hash] : undefined
|
|
186
|
-
// Everything between the project and the age. Plain strings, because none of
|
|
187
|
-
// them wears a colour or an icon — the two that do are drawn as their own
|
|
188
|
-
// cells either side of this run.
|
|
189
|
-
//
|
|
190
|
-
// A session that has spent nothing contributes **nothing**, rather than
|
|
191
|
-
// `formatCost`'s em-dash. A dash is the right answer in a details panel, where
|
|
192
|
-
// the row exists to be read and an empty cell would look broken; here it is a
|
|
193
|
-
// segment of a `·`-joined run competing for the width the project name needs,
|
|
194
|
-
// and `· — ·` is punctuation pretending to be a reading.
|
|
195
71
|
const cost = formatCost(info.totalCostUsd)
|
|
196
72
|
const extras = [
|
|
197
73
|
showGateway ? row.hostName : undefined,
|
|
@@ -199,85 +75,50 @@ export function SessionItem({
|
|
|
199
75
|
cost === '—' ? undefined : cost,
|
|
200
76
|
].filter((part): part is string => Boolean(part))
|
|
201
77
|
|
|
202
|
-
// The metadata run, assembled as a **list of present parts** rather than as a
|
|
203
|
-
// template with conditionals in it. Every one of these is genuinely optional —
|
|
204
|
-
// a session reports no model until its first turn, a session outside any
|
|
205
|
-
// declared project has no name to show, a session at its project's root has no
|
|
206
|
-
// sub-path — and a template makes each absence a separate place to remember
|
|
207
|
-
// the separator. Built as a list, the separator is drawn *between* parts and
|
|
208
|
-
// there is exactly one rule to get right.
|
|
209
|
-
//
|
|
210
|
-
// This is not hypothetical tidiness: `friendlyModel(undefined)` is `undefined`,
|
|
211
|
-
// so a session with no model yet drew an empty span followed by the project's
|
|
212
|
-
// leading `· `, and the line opened with a separator attached to nothing.
|
|
213
78
|
const model = friendlyModel(info.model)
|
|
214
79
|
const parts: ReactNode[] = []
|
|
215
80
|
if (model) {
|
|
216
81
|
parts.push(
|
|
217
|
-
|
|
218
|
-
// and a card line has no room for a context-window suffix.
|
|
219
|
-
<span key='model' className={vendorTextClass(engine, info.model)}>
|
|
82
|
+
<span key="model" className={vendorTextClass(engine, info.model)}>
|
|
220
83
|
{model}
|
|
221
84
|
</span>,
|
|
222
85
|
)
|
|
223
86
|
}
|
|
224
87
|
if (project !== undefined) {
|
|
225
88
|
parts.push(
|
|
226
|
-
<span key=
|
|
227
|
-
<ProjectIcon
|
|
228
|
-
icon={projectIcon}
|
|
229
|
-
src={iconSrc}
|
|
230
|
-
name={project}
|
|
231
|
-
/* 16px, the same box as the engine mark in the gutter, and nudged onto
|
|
232
|
-
the text baseline: the box is taller than the line, so it sits a
|
|
233
|
-
hair proud without this. */
|
|
234
|
-
className='mr-1.5 size-4 align-[-0.3em]'
|
|
235
|
-
/>
|
|
89
|
+
<span key="project">
|
|
90
|
+
<ProjectIcon icon={projectIcon} src={iconSrc} name={project} className="mr-1.5 size-4 align-[-0.3em]" />
|
|
236
91
|
{project}
|
|
237
92
|
</span>,
|
|
238
93
|
)
|
|
239
94
|
}
|
|
240
|
-
for (const extra of extras)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// plainly opening the session, which is the whole of what a host that can do
|
|
244
|
-
// neither has to offer — and is still better than a destination that renders
|
|
245
|
-
// empty.
|
|
95
|
+
for (const extra of extras) {
|
|
96
|
+
parts.push(<span key={extra}>{extra}</span>)
|
|
97
|
+
}
|
|
246
98
|
const steps = sessionSteps(info, (toolUseId, kind) => {
|
|
247
|
-
if (kind === 'agent')
|
|
99
|
+
if (kind === 'agent') {
|
|
100
|
+
return onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.()
|
|
101
|
+
}
|
|
248
102
|
return onRevealStep ? onRevealStep(toolUseId) : onSelect?.()
|
|
249
103
|
})
|
|
250
|
-
// Whether the thing on screen is one of THIS card's sub-agents. Matched
|
|
251
|
-
// against the card's own steps rather than taken on trust from `activeStepKey`
|
|
252
|
-
// alone: every card in the list is handed the same key, and a bare truthiness
|
|
253
|
-
// check would turn all of them grey the moment any one agent opened. Tasks are
|
|
254
|
-
// excluded because a task is a place to go, not a thing to hold — see
|
|
255
|
-
// `StepRow`.
|
|
256
104
|
const holdsOpenAgent = steps.some((s) => s.kind === 'agent' && s.key === activeStepKey)
|
|
257
105
|
|
|
258
106
|
return (
|
|
259
107
|
<div
|
|
260
|
-
data-slot=
|
|
108
|
+
data-slot="session-item"
|
|
261
109
|
data-active={active || undefined}
|
|
262
|
-
role=
|
|
110
|
+
role="button"
|
|
263
111
|
tabIndex={0}
|
|
264
|
-
/* Only the FIRST click of a click-streak selects. Selecting reveals the
|
|
265
|
-
session and focuses its composer, so the second click of a double-click
|
|
266
|
-
would steal focus a beat after the rename editor mounted — the editor
|
|
267
|
-
appearing and vanishing in the same gesture. `detail` counts the
|
|
268
|
-
streak; anything past the first is the double-click the title is
|
|
269
|
-
listening for. */
|
|
270
112
|
onClick={(e) => {
|
|
271
|
-
if (e.detail > 1 || isEditing)
|
|
113
|
+
if (e.detail > 1 || isEditing) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
272
116
|
onSelect?.()
|
|
273
117
|
}}
|
|
274
118
|
onKeyDown={(e) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// separate event travelling the same path, so an unguarded handler ran
|
|
279
|
-
// the control's action AND selected the session.
|
|
280
|
-
if (e.target !== e.currentTarget) return
|
|
119
|
+
if (e.target !== e.currentTarget) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
281
122
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
282
123
|
e.preventDefault()
|
|
283
124
|
onSelect?.()
|
|
@@ -286,43 +127,23 @@ export function SessionItem({
|
|
|
286
127
|
className={cn(
|
|
287
128
|
'group flex w-full cursor-pointer flex-col p-1 text-left outline-none',
|
|
288
129
|
'rounded-[4px] transition-colors focus-visible:ring-2 focus-visible:ring-ring',
|
|
289
|
-
|
|
290
|
-
// OUTRANKS the session being selected, because both are true at once —
|
|
291
|
-
// opening an agent selects its session too — and the blue can only mean
|
|
292
|
-
// one of them. It goes to the finer claim; the card keeps the coarser
|
|
293
|
-
// one in grey. A filled card does not also answer hover: the fill is
|
|
294
|
-
// already spent, and a selected row that lightened under the pointer
|
|
295
|
-
// read as a third state nobody could name.
|
|
296
|
-
holdsOpenAgent
|
|
297
|
-
? 'bg-row-selected-weak'
|
|
298
|
-
: active
|
|
299
|
-
? 'bg-row-selected'
|
|
300
|
-
: 'hover:bg-row-hover',
|
|
130
|
+
holdsOpenAgent ? 'bg-row-selected-weak' : active ? 'bg-row-selected' : 'hover:bg-row-hover',
|
|
301
131
|
className,
|
|
302
|
-
)}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
**6px between cells, where the column between the lines is 4px.**
|
|
307
|
-
Not a rounding wobble: a horizontal gap separates a glyph from the
|
|
308
|
-
words it labels, and 4px put the two close enough to read as one
|
|
309
|
-
smudge at 13px; a vertical gap separates two lines that are already
|
|
310
|
-
separated by their own leading, so it needs less. The design was
|
|
311
|
-
redrawn to this once the vendor marks lost the transparent padding
|
|
312
|
-
that had been standing in for the missing space. */}
|
|
313
|
-
<div className='flex h-5 items-center gap-1.5 overflow-hidden'>
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<div className="flex flex-col gap-1 py-0.5 pr-0.5 pl-1.5">
|
|
135
|
+
<div className="flex h-5 items-center gap-1.5 overflow-hidden">
|
|
314
136
|
<Gutter>
|
|
315
137
|
<SessionStatusIcon row={row} />
|
|
316
138
|
</Gutter>
|
|
317
139
|
{isEditing && onRename ? (
|
|
318
|
-
/* The editor replaces the title rather than sitting inside it — an
|
|
319
|
-
input nested in a button is invalid markup, and disabling the
|
|
320
|
-
button to protect the edit disables the field with it. */
|
|
321
140
|
<NameEditor
|
|
322
141
|
initial={info.title ?? ''}
|
|
323
142
|
onCommit={(title) => {
|
|
324
143
|
setEditing(false)
|
|
325
|
-
if (title !== (info.title ?? ''))
|
|
144
|
+
if (title !== (info.title ?? '')) {
|
|
145
|
+
onRename(title)
|
|
146
|
+
}
|
|
326
147
|
}}
|
|
327
148
|
onCancel={() => setEditing(false)}
|
|
328
149
|
/>
|
|
@@ -336,81 +157,32 @@ export function SessionItem({
|
|
|
336
157
|
}
|
|
337
158
|
: undefined
|
|
338
159
|
}
|
|
339
|
-
className=
|
|
160
|
+
className="min-w-0 flex-1 truncate text-body-sm font-medium tracking-[-0.005em] text-fg-1"
|
|
161
|
+
>
|
|
340
162
|
{sessionLabel(info)}
|
|
341
163
|
</span>
|
|
342
164
|
)}
|
|
343
165
|
{row.unseen > 0 ? (
|
|
344
|
-
/* Transcript rows since this session was last on screen — the same
|
|
345
|
-
unit the VS Code activity-bar badge counts, because turns
|
|
346
|
-
undercount badly.
|
|
347
|
-
|
|
348
|
-
**The colour is the state's, not the count's.** On a live session
|
|
349
|
-
unread is a call to look, and it wears the accent. On a settled
|
|
350
|
-
one the same number is a *record* — the turn is over, nothing is
|
|
351
|
-
going to arrive, and there is nothing to answer — so it drops to
|
|
352
|
-
the neutral badge. A list where every finished session still
|
|
353
|
-
shouted in blue is a list where the blue stopped meaning
|
|
354
|
-
anything, which is the whole reason the design draws two. */
|
|
355
166
|
<span
|
|
356
|
-
|
|
167
|
+
// Prose, not rows: `unseen` counts messages a person has not read (`proseCount`).
|
|
168
|
+
title={`${row.unseen} new message${row.unseen === 1 ? '' : 's'}`}
|
|
357
169
|
className={cn(
|
|
358
170
|
'flex h-4 min-w-6 shrink-0 items-center justify-center rounded-full px-2',
|
|
359
171
|
'text-[0.75rem] leading-none tracking-[-0.005em] tabular-nums',
|
|
360
|
-
row.state === 'working' || row.state === 'attention'
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)}>
|
|
172
|
+
row.state === 'working' || row.state === 'attention' ? 'bg-accent text-accent-fg' : 'bg-badge text-badge-fg',
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
364
175
|
{row.unseen}
|
|
365
176
|
</span>
|
|
366
177
|
) : null}
|
|
367
|
-
{
|
|
368
|
-
question you cannot ask from inside one session. Absent draws
|
|
369
|
-
nothing, and absent is not zero. */}
|
|
370
|
-
<ContextRing usage={info.contextUsage} size={16} className='p-0.5' />
|
|
178
|
+
<ContextRing usage={info.contextUsage} size={16} className="p-0.5" />
|
|
371
179
|
</div>
|
|
372
180
|
|
|
373
|
-
|
|
374
|
-
<div className='flex h-5 items-center gap-1.5 overflow-hidden text-body-sm tracking-[-0.005em]'>
|
|
181
|
+
<div className="flex h-5 items-center gap-1.5 overflow-hidden text-body-sm tracking-[-0.005em]">
|
|
375
182
|
<Gutter>
|
|
376
|
-
{
|
|
377
|
-
own `text-fg-3` and only a class on the element itself merges
|
|
378
|
-
over it. `cn` is tailwind-merge, so passing one replaces the
|
|
379
|
-
default rather than losing to it. */}
|
|
380
|
-
<EngineIcon
|
|
381
|
-
engine={engine}
|
|
382
|
-
model={info.model}
|
|
383
|
-
className={cn('size-4', vendorMarkClass(engine, info.model))}
|
|
384
|
-
/>
|
|
183
|
+
<EngineIcon engine={engine} model={info.model} className={cn('size-4', vendorMarkClass(engine, info.model))} />
|
|
385
184
|
</Gutter>
|
|
386
|
-
|
|
387
|
-
span**, and both halves of that are load-bearing.
|
|
388
|
-
|
|
389
|
-
The run is one span because its parts have a priority order —
|
|
390
|
-
model, then project, then where it ran and what it cost — and a
|
|
391
|
-
single ellipsis honours it for free. As five flex children they
|
|
392
|
-
cannot: `shrink-0` everywhere clips mid-glyph (`$12.4` cut in
|
|
393
|
-
half, a project name gone entirely behind its own icon), and
|
|
394
|
-
letting them shrink costs each of them a little and leaves four
|
|
395
|
-
half-words with no ellipsis anywhere. The project glyph rides
|
|
396
|
-
inside as an inline element for the same reason — it clips with
|
|
397
|
-
the text it labels instead of holding a slot that text has already
|
|
398
|
-
lost.
|
|
399
|
-
|
|
400
|
-
**The age is not in the run.** It sat at the end of it and was
|
|
401
|
-
therefore the first thing an ellipsis ate, which is precisely
|
|
402
|
-
backwards: `4m ago` is three characters that answer "is this
|
|
403
|
-
still moving", and a truncated `4m …` answers nothing while a
|
|
404
|
-
truncated project name still says which repo. So it is its own
|
|
405
|
-
shrink-0 cell and the run yields to it. That is one ellipsis in
|
|
406
|
-
one place, which is the whole reason not to use five cells. */}
|
|
407
|
-
<span className='min-w-0 truncate text-fg-4'>
|
|
408
|
-
{/* Separators live BETWEEN parts and are never attached to one.
|
|
409
|
-
Attached, a part that turned out to be absent left its own
|
|
410
|
-
separator behind — which is what a session with no model
|
|
411
|
-
recorded yet did: `friendlyModel(undefined)` is `undefined`, the
|
|
412
|
-
model drew nothing, and the line opened with a `· ` in front of
|
|
413
|
-
the project, hanging off nothing. */}
|
|
185
|
+
<span className="min-w-0 truncate text-fg-4">
|
|
414
186
|
{parts.map((part, i) => (
|
|
415
187
|
<Fragment key={i}>
|
|
416
188
|
{i > 0 ? ' · ' : ''}
|
|
@@ -418,19 +190,11 @@ export function SessionItem({
|
|
|
418
190
|
</Fragment>
|
|
419
191
|
))}
|
|
420
192
|
</span>
|
|
421
|
-
<span className=
|
|
193
|
+
<span className="shrink-0 text-fg-4">
|
|
422
194
|
{parts.length > 0 ? '· ' : ''}
|
|
423
195
|
{formatRelativeTime(info.lastActivityAt ?? info.createdAt)}
|
|
424
196
|
</span>
|
|
425
|
-
|
|
426
|
-
Giving the run `flex-1` made it absorb the surplus, which pushed
|
|
427
|
-
the age to the far right on a card with no disclosure and left a
|
|
428
|
-
hole between a project name and its own timestamp. The design
|
|
429
|
-
draws the same spacer in the same place for the same reason: the
|
|
430
|
-
run and the age are one reading, and what floats is the gap before
|
|
431
|
-
the controls. Basis 0, so it only ever takes *surplus* — under a
|
|
432
|
-
deficit it is worth nothing and the run truncates instead. */}
|
|
433
|
-
<span className='min-w-0 flex-1' />
|
|
197
|
+
<span className="min-w-0 flex-1" />
|
|
434
198
|
{steps.length > 0 ? (
|
|
435
199
|
<StepToggle
|
|
436
200
|
expanded={open}
|
|
@@ -445,14 +209,9 @@ export function SessionItem({
|
|
|
445
209
|
</div>
|
|
446
210
|
|
|
447
211
|
{open && steps.length > 0 ? (
|
|
448
|
-
<div className=
|
|
212
|
+
<div className="flex flex-col">
|
|
449
213
|
{steps.map((step: Step) => (
|
|
450
|
-
<StepRow
|
|
451
|
-
key={step.key}
|
|
452
|
-
step={step}
|
|
453
|
-
active={step.key === activeStepKey}
|
|
454
|
-
onSelect={step.onSelect}
|
|
455
|
-
/>
|
|
214
|
+
<StepRow key={step.key} step={step} active={step.key === activeStepKey} onSelect={step.onSelect} />
|
|
456
215
|
))}
|
|
457
216
|
</div>
|
|
458
217
|
) : null}
|
|
@@ -460,39 +219,11 @@ export function SessionItem({
|
|
|
460
219
|
)
|
|
461
220
|
}
|
|
462
221
|
|
|
463
|
-
/**
|
|
464
|
-
* The one 16px cell both lines hang their glyph in.
|
|
465
|
-
*
|
|
466
|
-
* A shared column rather than a leading character: the state and the engine mark
|
|
467
|
-
* are different widths and a bare glyph would let the two text columns start at
|
|
468
|
-
* different x. Centring inside a fixed cell is what makes them agree.
|
|
469
|
-
*/
|
|
470
222
|
function Gutter({ children }: { children: ReactNode }) {
|
|
471
|
-
return <span className=
|
|
223
|
+
return <span className="flex size-4 shrink-0 items-center justify-center">{children}</span>
|
|
472
224
|
}
|
|
473
225
|
|
|
474
|
-
|
|
475
|
-
* Enter commits, Escape cancels, blur commits — the editor's inline-rename feel.
|
|
476
|
-
* An empty value is a deliberate "clear the name", which the gateway answers by
|
|
477
|
-
* handing back the derived title.
|
|
478
|
-
*
|
|
479
|
-
* The blur rule needs one guard. Selecting a session focuses a different surface
|
|
480
|
-
* — in the extension, a different VS Code view — so this document can lose focus
|
|
481
|
-
* a tick after a double-click opened the editor, and an unguarded blur reads
|
|
482
|
-
* that as "the user clicked away" and closes the editor in the frame it
|
|
483
|
-
* appeared. `document.hasFocus()` tells the two apart: focus moving WITHIN this
|
|
484
|
-
* document is the user leaving the field; the whole view losing focus is not.
|
|
485
|
-
* When the window comes back, so does the caret.
|
|
486
|
-
*/
|
|
487
|
-
function NameEditor({
|
|
488
|
-
initial,
|
|
489
|
-
onCommit,
|
|
490
|
-
onCancel,
|
|
491
|
-
}: {
|
|
492
|
-
initial: string
|
|
493
|
-
onCommit: (title: string) => void
|
|
494
|
-
onCancel: () => void
|
|
495
|
-
}) {
|
|
226
|
+
function NameEditor({ initial, onCommit, onCancel }: { initial: string; onCommit: (title: string) => void; onCancel: () => void }) {
|
|
496
227
|
const [value, setValue] = useState(initial)
|
|
497
228
|
const ref = useRef<HTMLInputElement>(null)
|
|
498
229
|
useEffect(() => {
|
|
@@ -512,23 +243,25 @@ function NameEditor({
|
|
|
512
243
|
ref={ref}
|
|
513
244
|
value={value}
|
|
514
245
|
spellCheck={false}
|
|
515
|
-
placeholder=
|
|
516
|
-
aria-label=
|
|
246
|
+
placeholder="Session name"
|
|
247
|
+
aria-label="Session name"
|
|
517
248
|
onClick={(e) => e.stopPropagation()}
|
|
518
249
|
onDoubleClick={(e) => e.stopPropagation()}
|
|
519
250
|
onChange={(e) => setValue(e.target.value)}
|
|
520
251
|
onBlur={() => {
|
|
521
|
-
|
|
252
|
+
// Guarded on `document.hasFocus()`: selecting a session focuses another surface (in the extension, another view), and an unguarded blur closes the editor in the frame it appeared.
|
|
253
|
+
if (document.hasFocus()) {
|
|
254
|
+
onCommit(value.trim())
|
|
255
|
+
}
|
|
522
256
|
}}
|
|
523
257
|
onKeyDown={(e) => {
|
|
524
258
|
e.stopPropagation()
|
|
525
|
-
if (e.key === 'Enter')
|
|
526
|
-
|
|
259
|
+
if (e.key === 'Enter') {
|
|
260
|
+
onCommit(value.trim())
|
|
261
|
+
} else if (e.key === 'Escape') {
|
|
262
|
+
onCancel()
|
|
263
|
+
}
|
|
527
264
|
}}
|
|
528
|
-
/* The negative margin is load-bearing: the border (1px) and padding (1px)
|
|
529
|
-
make the input taller than the label it replaces, and the card would
|
|
530
|
-
grow by that much the moment a rename starts. Cancelling it exactly is
|
|
531
|
-
what keeps the list from shifting under the caret. */
|
|
532
265
|
className={cn(
|
|
533
266
|
'-my-0.5 min-w-0 flex-1 rounded-sm border border-ring bg-bg px-1 py-px',
|
|
534
267
|
'text-body-sm leading-5 text-fg-1 outline-none',
|
|
@@ -18,40 +18,37 @@ export function SessionListItem({ session, active, onSelect, onDelete, className
|
|
|
18
18
|
const meta = STATUS_META[session.status]
|
|
19
19
|
return (
|
|
20
20
|
<div
|
|
21
|
-
data-slot=
|
|
21
|
+
data-slot="session-list-item"
|
|
22
22
|
data-active={active || undefined}
|
|
23
23
|
className={cn(
|
|
24
24
|
'group flex w-full items-center gap-2 rounded-md border border-transparent px-2.5 py-2 text-left transition-colors',
|
|
25
25
|
active ? 'border-border bg-surface' : 'hover:bg-surface-hover',
|
|
26
26
|
className,
|
|
27
|
-
)}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<span className='truncate text-body-sm font-medium text-fg-1'>
|
|
34
|
-
{session.title ?? session.id.slice(0, 8)}
|
|
35
|
-
</span>
|
|
36
|
-
<Badge variant={meta.variant} dot className='shrink-0'>
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
<button type="button" onClick={() => onSelect?.(session.id)} className="min-w-0 flex-1 text-left outline-none">
|
|
30
|
+
<div className="flex items-center gap-2">
|
|
31
|
+
<span className="truncate text-body-sm font-medium text-fg-1">{session.title ?? session.id.slice(0, 8)}</span>
|
|
32
|
+
<Badge variant={meta.variant} dot className="shrink-0">
|
|
37
33
|
{meta.label}
|
|
38
34
|
</Badge>
|
|
39
35
|
</div>
|
|
40
|
-
<div className=
|
|
41
|
-
<span className=
|
|
42
|
-
{session.profile ? <span className=
|
|
43
|
-
<span className=
|
|
44
|
-
<span className=
|
|
36
|
+
<div className="mt-0.5 flex items-center gap-2 font-mono text-label text-fg-4">
|
|
37
|
+
<span className="truncate">{session.cwd}</span>
|
|
38
|
+
{session.profile ? <span className="shrink-0">@{session.profile}</span> : null}
|
|
39
|
+
<span className="shrink-0">{formatCost(session.totalCostUsd)}</span>
|
|
40
|
+
<span className="shrink-0">{formatRelativeTime(session.lastActivityAt ?? session.createdAt)}</span>
|
|
45
41
|
</div>
|
|
46
42
|
</button>
|
|
47
43
|
{onDelete ? (
|
|
48
44
|
<Button
|
|
49
|
-
variant=
|
|
50
|
-
size=
|
|
51
|
-
aria-label=
|
|
52
|
-
className=
|
|
53
|
-
onClick={() => onDelete(session.id)}
|
|
54
|
-
|
|
45
|
+
variant="ghost"
|
|
46
|
+
size="icon-sm"
|
|
47
|
+
aria-label="Close session"
|
|
48
|
+
className="opacity-0 transition-opacity group-hover:opacity-100"
|
|
49
|
+
onClick={() => onDelete(session.id)}
|
|
50
|
+
>
|
|
51
|
+
<Trash2 className="size-3.5 text-fg-3" />
|
|
55
52
|
</Button>
|
|
56
53
|
) : null}
|
|
57
54
|
</div>
|
|
@@ -67,27 +64,14 @@ export interface SessionListProps {
|
|
|
67
64
|
className?: string
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
export function SessionList({
|
|
71
|
-
sessions,
|
|
72
|
-
activeId,
|
|
73
|
-
onSelect,
|
|
74
|
-
onDelete,
|
|
75
|
-
emptyText = 'No sessions yet.',
|
|
76
|
-
className,
|
|
77
|
-
}: SessionListProps) {
|
|
67
|
+
export function SessionList({ sessions, activeId, onSelect, onDelete, emptyText = 'No sessions yet.', className }: SessionListProps) {
|
|
78
68
|
return (
|
|
79
|
-
<div data-slot=
|
|
69
|
+
<div data-slot="session-list" className={cn('flex flex-col gap-1', className)}>
|
|
80
70
|
{sessions.length === 0 ? (
|
|
81
|
-
<div className=
|
|
71
|
+
<div className="px-2.5 py-6 text-center text-body-sm text-fg-4">{emptyText}</div>
|
|
82
72
|
) : (
|
|
83
73
|
sessions.map((session) => (
|
|
84
|
-
<SessionListItem
|
|
85
|
-
key={session.id}
|
|
86
|
-
session={session}
|
|
87
|
-
active={session.id === activeId}
|
|
88
|
-
onSelect={onSelect}
|
|
89
|
-
onDelete={onDelete}
|
|
90
|
-
/>
|
|
74
|
+
<SessionListItem key={session.id} session={session} active={session.id === activeId} onSelect={onSelect} onDelete={onDelete} />
|
|
91
75
|
))
|
|
92
76
|
)}
|
|
93
77
|
</div>
|