@workerdeck/ui 0.22.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DDgBkHsi.mjs} +2196 -3364
  2. package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
  3. package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
  4. package/build/format-B8ZM9LPy.d.mts +16 -0
  5. package/build/format.d.mts +8 -75
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +63 -1138
  8. package/build/index.mjs +4 -300
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +80 -24
  11. package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
  12. package/build/status-C0HSLZas.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -124
  14. package/build/workspace.mjs +13 -143
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +4 -4
  17. package/src/components/agent/CodeEditor.tsx +31 -103
  18. package/src/components/agent/Composer.tsx +114 -333
  19. package/src/components/agent/ContextDialog.tsx +20 -41
  20. package/src/components/agent/ContextRing.tsx +6 -28
  21. package/src/components/agent/Conversation.tsx +21 -33
  22. package/src/components/agent/EditorTabs.tsx +39 -67
  23. package/src/components/agent/EngineIcon.tsx +29 -72
  24. package/src/components/agent/EntryIcon.tsx +16 -0
  25. package/src/components/agent/FileCard.tsx +10 -16
  26. package/src/components/agent/FileTree.tsx +52 -125
  27. package/src/components/agent/FileViewer.tsx +30 -60
  28. package/src/components/agent/HostFilesDialog.tsx +40 -69
  29. package/src/components/agent/Loader.tsx +3 -24
  30. package/src/components/agent/McpDialog.tsx +64 -137
  31. package/src/components/agent/Message.tsx +2 -21
  32. package/src/components/agent/ModelSelect.tsx +33 -62
  33. package/src/components/agent/PermissionModeSelect.tsx +21 -79
  34. package/src/components/agent/PermissionPrompt.tsx +59 -63
  35. package/src/components/agent/ProjectIcon.tsx +228 -93
  36. package/src/components/agent/PromptTokenText.tsx +10 -16
  37. package/src/components/agent/QuestionPrompt.tsx +43 -64
  38. package/src/components/agent/Reasoning.tsx +13 -16
  39. package/src/components/agent/Response.tsx +4 -6
  40. package/src/components/agent/Scrubber.tsx +31 -80
  41. package/src/components/agent/SessionBrowser.tsx +64 -243
  42. package/src/components/agent/SessionEmptyState.tsx +10 -40
  43. package/src/components/agent/SessionInfoDialog.tsx +46 -78
  44. package/src/components/agent/SessionItem.tsx +53 -320
  45. package/src/components/agent/SessionList.tsx +23 -39
  46. package/src/components/agent/SessionPanel.tsx +345 -923
  47. package/src/components/agent/SessionStatusIcon.tsx +14 -28
  48. package/src/components/agent/SessionSteps.tsx +34 -187
  49. package/src/components/agent/SessionWorkspace.tsx +108 -215
  50. package/src/components/agent/SkillsDialog.tsx +35 -80
  51. package/src/components/agent/StatusBar.tsx +51 -136
  52. package/src/components/agent/SubagentStrip.tsx +12 -77
  53. package/src/components/agent/ToolCallCard.tsx +94 -135
  54. package/src/components/agent/Transcript.tsx +103 -1288
  55. package/src/components/agent/TranscriptItemView.tsx +155 -0
  56. package/src/components/agent/TranscriptRows.tsx +418 -0
  57. package/src/components/agent/UsageDialog.tsx +7 -29
  58. package/src/components/agent/UsageMeters.tsx +18 -73
  59. package/src/components/agent/pulse.tsx +9 -32
  60. package/src/components/agent/scrubber-marks.ts +33 -103
  61. package/src/components/agent/status.ts +1 -5
  62. package/src/components/agent/tool-result-fetch.tsx +3 -25
  63. package/src/components/agent/tool-result-image.tsx +32 -111
  64. package/src/components/agent/transcript-rows.ts +41 -118
  65. package/src/components/agent/transcript-variant.tsx +3 -80
  66. package/src/components/agent/use-height-epoch.ts +10 -24
  67. package/src/components/agent/use-path-links.ts +45 -63
  68. package/src/components/agent/use-subagent-frame.ts +106 -0
  69. package/src/components/agent/use-transcript-jumps.ts +49 -107
  70. package/src/components/prompt-area/animated-placeholder.tsx +6 -5
  71. package/src/components/prompt-area/clipboard-helpers.ts +26 -21
  72. package/src/components/prompt-area/cursor-helpers.ts +79 -37
  73. package/src/components/prompt-area/dom-helpers.ts +97 -69
  74. package/src/components/prompt-area/file-strip.tsx +90 -80
  75. package/src/components/prompt-area/html-to-markdown.ts +67 -46
  76. package/src/components/prompt-area/image-strip.tsx +8 -18
  77. package/src/components/prompt-area/index.ts +2 -15
  78. package/src/components/prompt-area/prompt-area-engine.ts +100 -115
  79. package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
  80. package/src/components/prompt-area/prompt-area.tsx +31 -38
  81. package/src/components/prompt-area/remove-button.tsx +3 -9
  82. package/src/components/prompt-area/segment-helpers.ts +4 -12
  83. package/src/components/prompt-area/trigger-popover.tsx +15 -27
  84. package/src/components/prompt-area/trigger-presets.ts +0 -10
  85. package/src/components/prompt-area/types.ts +3 -8
  86. package/src/components/prompt-area/use-chip-editing.ts +305 -0
  87. package/src/components/prompt-area/use-markdown-mode.ts +7 -17
  88. package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
  89. package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
  90. package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
  91. package/src/components/prompt-area/use-prompt-area.ts +170 -896
  92. package/src/components/prompt-area/use-trigger-search.ts +21 -19
  93. package/src/components/terminal/PermissionPrompt.tsx +56 -59
  94. package/src/components/terminal/QuestionPrompt.tsx +28 -79
  95. package/src/components/terminal/StatusLine.tsx +13 -63
  96. package/src/components/terminal/TerminalTranscript.tsx +44 -160
  97. package/src/components/terminal/affordances.tsx +68 -98
  98. package/src/components/terminal/blocks.ts +20 -156
  99. package/src/components/terminal/diff.tsx +19 -67
  100. package/src/components/terminal/height.ts +163 -313
  101. package/src/components/terminal/image-box.ts +0 -44
  102. package/src/components/terminal/items.tsx +150 -305
  103. package/src/components/terminal/markdown.tsx +36 -95
  104. package/src/components/terminal/press.tsx +26 -53
  105. package/src/components/terminal/prompt.tsx +40 -170
  106. package/src/components/terminal/result-preview.ts +8 -56
  107. package/src/components/terminal/row.tsx +11 -98
  108. package/src/components/terminal/scrubber.tsx +108 -401
  109. package/src/components/terminal/surface.tsx +6 -54
  110. package/src/components/terminal/todos.ts +66 -0
  111. package/src/components/terminal/tool-run.ts +21 -192
  112. package/src/components/ui/AlertDialog.tsx +8 -23
  113. package/src/components/ui/Badge.tsx +18 -29
  114. package/src/components/ui/Button.tsx +7 -15
  115. package/src/components/ui/Card.tsx +2 -13
  116. package/src/components/ui/CodeBlock.tsx +10 -37
  117. package/src/components/ui/CopyButton.tsx +10 -23
  118. package/src/components/ui/Dialog.tsx +23 -36
  119. package/src/components/ui/Empty.tsx +6 -29
  120. package/src/components/ui/Input.tsx +14 -16
  121. package/src/components/ui/Menu.tsx +6 -18
  122. package/src/components/ui/PortalScope.tsx +1 -21
  123. package/src/components/ui/ProgressRing.tsx +8 -19
  124. package/src/components/ui/Select.tsx +18 -31
  125. package/src/components/ui/Sonner.tsx +1 -2
  126. package/src/components/ui/Spinner.tsx +1 -1
  127. package/src/components/ui/Splitter.tsx +30 -64
  128. package/src/components/ui/Textarea.tsx +2 -5
  129. package/src/components/ui/Tooltip.tsx +3 -13
  130. package/src/format.ts +0 -9
  131. package/src/index.ts +16 -113
  132. package/src/lib/clipboard.ts +8 -28
  133. package/src/lib/css.ts +8 -0
  134. package/src/lib/format.ts +101 -86
  135. package/src/lib/plan-request.ts +16 -0
  136. package/src/lib/status.ts +45 -95
  137. package/src/lib/tool-icon.ts +39 -47
  138. package/src/lib/utils.ts +1 -3
  139. package/src/styles/terminal.css +81 -49
  140. package/src/styles/theme.css +136 -19
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-13l25ubU.d.mts +0 -609
  143. package/build/SessionPanel-DgwjH4Ve.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. package/build/status-BE-zg88x.mjs.map +0 -1
@@ -12,14 +12,7 @@ import {
12
12
  projectsOf,
13
13
  subsetSummary,
14
14
  } from '@workerdeck/protocol'
15
- import type {
16
- GroupBy,
17
- SessionRow,
18
- SessionState,
19
- SortBy,
20
- ViewConfig,
21
- WorkspaceScope,
22
- } from '@workerdeck/protocol'
15
+ import type { GroupBy, SessionRow, SessionState, SortBy, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
23
16
  import { Button } from '../ui/Button.tsx'
24
17
  import { Empty } from '../ui/Empty.tsx'
25
18
  import { Input } from '../ui/Input.tsx'
@@ -28,121 +21,29 @@ import { ProjectIcon } from './ProjectIcon.tsx'
28
21
  import { SessionItem } from './SessionItem.tsx'
29
22
  import { cn } from '../../lib/utils.ts'
30
23
 
31
- // The state glyph moved to its own module when the card did — the extension's
32
- // list draws it too, and this file re-exports it only so the package's public
33
- // surface does not shift under a host that imports it from here.
34
24
  export { SessionStatusIcon } from './SessionStatusIcon.tsx'
35
25
 
36
- /**
37
- * A sessions list with the affordances a list of thirty needs: search, facets,
38
- * grouping, sorting, unread counts, and one honest line about what is hidden.
39
- *
40
- * The *rules* are `@workerdeck/protocol`'s (`filterRows`/`groupRows`/
41
- * `subsetSummary`), not this component's — the VS Code sidebar renders the same
42
- * model with workbench chrome, its activity-bar badge counts the same rows this
43
- * would show, and iOS mirrors them in Swift. What lives here is the styled
44
- * rendering of that model, so a host that wants the dashboard's look gets it
45
- * without reimplementing the model behind it.
46
- *
47
- * `SessionList` remains beside this for the plain case (a fixed set of rows, no
48
- * controls); this is what you reach for when the list is the screen.
49
- */
50
-
51
26
  export interface SessionBrowserProps {
52
27
  rows: SessionRow[]
53
28
  config: ViewConfig
54
29
  onConfigChange: (config: ViewConfig) => void
55
- /** The host's own folders, if it has such a notion. Absent — a dashboard, a
56
- * phone — makes the scope filter genuinely inert rather than empty. */
57
30
  scope?: WorkspaceScope
58
31
  activeId?: string
59
- /**
60
- * Which sub-agent the host currently has open, by tool-use id — the *finer*
61
- * of the two selections. The card holding it drops to the secondary grey and
62
- * that step takes the blue; see `SessionItem`. A host with no sub-agent
63
- * surface leaves it undefined and nothing changes.
64
- */
65
32
  activeSubagentId?: string
66
33
  onSelect?: (row: SessionRow) => void
67
34
  onDelete?: (row: SessionRow) => void
68
- /**
69
- * Rename, from the row's pencil. Empty string restores the derived title. A
70
- * gateway edit (`PATCH /sessions/:id`), never a local override — every client
71
- * should see the same name. Omit to make titles read-only.
72
- *
73
- * A hover affordance rather than the extension's double-click-the-title,
74
- * because here a single click on the row navigates: the *first* click of a
75
- * double-click would have already left the page.
76
- */
77
35
  onRename?: (row: SessionRow, title: string) => void
78
- /**
79
- * Clear the conversation in place, from the row's eraser. Same session, empty
80
- * context — **not** a delete: the engine keeps the old conversation and it
81
- * stays resumable, which is why the row's copy never says "deleted".
82
- *
83
- * Rendered only where the session's own capability record allows it
84
- * (`capabilities.clearContext`, absent = false), so a row that cannot be
85
- * cleared has no eraser rather than one that errors. Omit the prop to have
86
- * none at all — a host with no way to send a session command has nothing to
87
- * hang it on (this is a WS command, not a REST route).
88
- */
89
36
  onClearContext?: (row: SessionRow) => void
90
- /** Open a session *at* one of its sub-agents, when the host can scroll a
91
- * transcript to a `Task` row. Absent, the sub-agent list still expands and a
92
- * step just opens its session — see `SessionRowItemProps`. */
93
37
  onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
94
- /** Open a session and travel to one of its **tasks** — see
95
- * `SessionItem.onRevealStep`. A task has no agent behind it, so it is a place
96
- * to go rather than a thing to open, and framing it as an agent drew an empty
97
- * view. */
98
38
  onRevealStep?: (row: SessionRow, toolUseId: string) => void
99
- /** Rendered when nothing at all exists (as opposed to nothing matching). */
100
39
  emptyState?: React.ReactNode
101
- /**
102
- * Whether the search + facet bar is shown. Defaults to `true` — a list that
103
- * *is* the screen shows its controls.
104
- *
105
- * A host with somewhere better to put the toggle (a view title bar) passes
106
- * `false` and owns the boolean itself, the way the VS Code extension does: the
107
- * key lives where the commands do. Two rules come with it, and they are why
108
- * this hides only the bar and nothing else — **closing the bar never clears
109
- * the filters**, and the subset line below it renders either way, so a list
110
- * filtered by a control you can't currently see still says so.
111
- */
112
40
  showControls?: boolean
113
- /**
114
- * Resolved project-icon bytes by content hash — `useProjectIcons`' output.
115
- *
116
- * Passed in rather than fetched here for the reason `ProjectIcon` states: the
117
- * wire carries an *address*, and who can fetch it differs per client. Absent,
118
- * or a hash not in it yet, simply draws no picture; the project's name is
119
- * already there.
120
- */
121
41
  projectIcons?: Record<string, string>
122
42
  className?: string
123
43
  }
124
44
 
125
- /**
126
- * How a list row is drawn, in one place, so `SidebarRow` in `web` matches this
127
- * exactly rather than approximating it — the dashboard's other three sidebars
128
- * are that component, and a sessions list that hovered differently from the
129
- * gateways list beside it would read as a different product.
130
- *
131
- * Two rules are load-bearing:
132
- *
133
- * - **Fill means hover, and only hover.** It stays on the row whether or not
134
- * the row is selected, because a selected row still has to answer the
135
- * pointer. Selection gets the gutter instead.
136
- * - **`ml-0` on the selected row is not cosmetic.** It hands the accent border
137
- * the 4px the margin was holding, so the text does not shift sideways as a
138
- * row becomes the selected one. The squared left corners are what let the bar
139
- * sit flush against the sidebar edge.
140
- */
141
45
  export function rowShapeClass(active: boolean): string {
142
- return cn(
143
- 'px-2 py-1.5 hover:bg-row-hover',
144
- active ? 'mr-1 ml-0 rounded-r-md border-l-4 border-l-accent' : 'mx-1 rounded-md',
145
- )
46
+ return cn('px-2 py-1.5 hover:bg-row-hover', active ? 'mr-1 ml-0 rounded-r-md border-l-4 border-l-accent' : 'mx-1 rounded-md')
146
47
  }
147
48
 
148
49
  export function SessionBrowser({
@@ -166,48 +67,43 @@ export function SessionBrowser({
166
67
  const visible = useMemo(() => filterRows(rows, config, scope), [rows, config, scope])
167
68
  const groups = useMemo(() => groupRows(visible, config), [visible, config])
168
69
  const subset = subsetSummary(config, scope, visible.length, rows.length)
169
- // Derived, not enumerated: a new engine or a new gateway needs no change here,
170
- // and a facet with one possible value is not a choice worth showing.
171
70
  const adapters = useMemo(() => adaptersOf(rows), [rows])
172
71
  const projects = useMemo(() => projectsOf(rows), [rows])
173
72
  const gateways = useMemo(() => {
174
73
  const seen = new Map<string, string>()
175
- for (const row of rows) seen.set(row.hostId, row.hostName)
74
+ for (const row of rows) {
75
+ seen.set(row.hostId, row.hostName)
76
+ }
176
77
  return [...seen].map(([id, name]) => ({ id, name }))
177
78
  }, [rows])
178
79
 
179
80
  const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })
180
81
 
181
82
  return (
182
- <div data-slot='session-browser' className={cn('flex flex-col gap-3', className)}>
183
- {/* One control per row, label left, input right — the shape VS Code uses
184
- for its own filter surfaces. A wrapping row of pill-shaped selects
185
- reflows into an unpredictable number of lines as facets appear and
186
- disappear; a column of labelled rows is the same height every time and
187
- reads at sidebar width, which is the only width this has. */}
83
+ <div data-slot="session-browser" className={cn('flex flex-col gap-3', className)}>
188
84
  <div className={cn('flex flex-col gap-1.5 px-2', !showControls && 'hidden')}>
189
- <div className='relative'>
190
- <Search className='pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-fg-4' />
85
+ <div className="relative">
86
+ <Search className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-fg-4" />
191
87
  <Input
192
88
  value={config.search}
193
89
  onChange={(e) => set({ search: e.target.value })}
194
- placeholder='Search sessions'
195
- aria-label='Search sessions'
196
- className='pl-8'
90
+ placeholder="Search sessions"
91
+ aria-label="Search sessions"
92
+ className="pl-8"
197
93
  />
198
94
  </div>
199
- <FilterRow label='State'>
95
+ <FilterRow label="State">
200
96
  <FacetSelect
201
- label='State'
97
+ label="State"
202
98
  value={config.states}
203
99
  options={STATE_ORDER.map((s) => ({ value: s, label: STATE_LABELS[s] }))}
204
100
  onChange={(states) => set({ states: states as SessionState[] })}
205
101
  />
206
102
  </FilterRow>
207
103
  {adapters.length > 1 ? (
208
- <FilterRow label='Engine'>
104
+ <FilterRow label="Engine">
209
105
  <FacetSelect
210
- label='Engine'
106
+ label="Engine"
211
107
  value={config.adapters}
212
108
  options={adapters.map((a) => ({ value: a, label: a }))}
213
109
  onChange={(adapters) => set({ adapters })}
@@ -215,9 +111,9 @@ export function SessionBrowser({
215
111
  </FilterRow>
216
112
  ) : null}
217
113
  {gateways.length > 1 ? (
218
- <FilterRow label='Gateway'>
114
+ <FilterRow label="Gateway">
219
115
  <FacetSelect
220
- label='Gateway'
116
+ label="Gateway"
221
117
  value={config.gateways}
222
118
  options={gateways.map((g) => ({ value: g.id, label: g.name }))}
223
119
  onChange={(gateways) => set({ gateways })}
@@ -225,18 +121,18 @@ export function SessionBrowser({
225
121
  </FilterRow>
226
122
  ) : null}
227
123
  {projects.length > 1 ? (
228
- <FilterRow label='Project'>
124
+ <FilterRow label="Project">
229
125
  <FacetSelect
230
- label='Project'
126
+ label="Project"
231
127
  value={config.projects ?? []}
232
128
  options={projects.map((p) => ({ value: p.key, label: p.label }))}
233
129
  onChange={(next) => set({ projects: next })}
234
130
  />
235
131
  </FilterRow>
236
132
  ) : null}
237
- <FilterRow label='Group'>
133
+ <FilterRow label="Group">
238
134
  <OneOfSelect
239
- label='Group'
135
+ label="Group"
240
136
  value={config.groupBy}
241
137
  options={[
242
138
  { value: 'none', label: 'No grouping' },
@@ -248,9 +144,9 @@ export function SessionBrowser({
248
144
  onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}
249
145
  />
250
146
  </FilterRow>
251
- <FilterRow label='Sort'>
147
+ <FilterRow label="Sort">
252
148
  <OneOfSelect
253
- label='Sort'
149
+ label="Sort"
254
150
  value={config.sortBy}
255
151
  options={[
256
152
  { value: 'recent', label: 'Recent' },
@@ -265,62 +161,46 @@ export function SessionBrowser({
265
161
  </div>
266
162
 
267
163
  {subset ? (
268
- <div className='flex items-center gap-2 px-3 text-label text-fg-4'>
164
+ <div className="flex items-center gap-2 px-3 text-label text-fg-4">
269
165
  <span>
270
166
  {subset.shown} of {subset.total}
271
167
  {subset.causes.length ? ` · ${subset.causes.join(' · ')}` : null}
272
168
  </span>
273
169
  <button
274
- type='button'
275
- className='text-fg-3 underline underline-offset-2 hover:text-fg-1'
276
- onClick={() => onConfigChange(clearFilters(config))}>
170
+ type="button"
171
+ className="text-fg-3 underline underline-offset-2 hover:text-fg-1"
172
+ onClick={() => onConfigChange(clearFilters(config))}
173
+ >
277
174
  Show all
278
175
  </button>
279
176
  </div>
280
177
  ) : null}
281
178
 
282
179
  {rows.length === 0 ? (
283
- (emptyState ?? <Empty icon={<Layers />} title='No sessions yet' />)
180
+ (emptyState ?? <Empty icon={<Layers />} title="No sessions yet" />)
284
181
  ) : visible.length === 0 ? (
285
- // Two different dead ends, two different ways out. "Nothing matches" is
286
- // a filter someone set; anything else is the state of the world — and
287
- // only the first has a button, because an action that does nothing is
288
- // worse than none.
289
182
  hasFacetFilter(config) ? (
290
183
  <Empty
291
184
  icon={<SearchX />}
292
- title='No matches'
293
- description='No session matches the current search and filters.'
294
- action='Clear filters'
185
+ title="No matches"
186
+ description="No session matches the current search and filters."
187
+ action="Clear filters"
295
188
  onAction={() => onConfigChange(clearFilters(config))}
296
189
  />
297
190
  ) : (
298
- <Empty icon={<Layers />} title='Nothing here' description='No session to show.' />
191
+ <Empty icon={<Layers />} title="Nothing here" description="No session to show." />
299
192
  )
300
193
  ) : (
301
- /* The list's own 4px inset, as **padding on the list** rather than
302
- margins on the cards. `SessionItem` is `w-full` — it fills the slot it
303
- is given, which is what lets a host size it — and `w-full` plus
304
- `mx-1` is `100% + 8px`: an overflow by construction, which is exactly
305
- what it did. The cards ran past the sidebar's right edge and the
306
- column grew a horizontal scrollbar. Padding here cannot do that,
307
- whatever the card's own width rule turns out to be. */
308
- <div className='flex flex-col gap-4 px-1'>
194
+ <div className="flex flex-col gap-4 px-1">
309
195
  {groups.map((group) => (
310
- <div key={group.key} className='flex flex-col gap-1'>
196
+ <div key={group.key} className="flex flex-col gap-1">
311
197
  {config.groupBy !== 'none' && group.label ? (
312
- <div className='flex items-center gap-2 px-2 text-label font-medium text-fg-4'>
313
- {/* Only the project facet has a mark of its own, and a group
314
- IS one project root, so the first row is a fair source. */}
198
+ <div className="flex items-center gap-2 px-2 text-label font-medium text-fg-4">
315
199
  {config.groupBy === 'project' ? (
316
- <ProjectIcon
317
- icon={group.rows[0]?.info.project?.icon}
318
- src={iconSrcOf(group.rows[0], projectIcons)}
319
- name={group.label}
320
- />
200
+ <ProjectIcon icon={group.rows[0]?.info.project?.icon} src={iconSrcOf(group.rows[0], projectIcons)} name={group.label} />
321
201
  ) : null}
322
- <span className='uppercase tracking-wide'>{group.label}</span>
323
- <span className='text-fg-4/70'>{group.rows.length}</span>
202
+ <span className="uppercase tracking-wide">{group.label}</span>
203
+ <span className="text-fg-4/70">{group.rows.length}</span>
324
204
  </div>
325
205
  ) : null}
326
206
  {group.rows.map((row) => (
@@ -348,13 +228,7 @@ export function SessionBrowser({
348
228
  )
349
229
  }
350
230
 
351
- /** The bytes for a row's project icon, if it has an image one and the caller has
352
- * fetched it yet. Shared by the row and its group header so the two cannot draw
353
- * different pictures for one project. */
354
- function iconSrcOf(
355
- row: SessionRow | undefined,
356
- icons: Record<string, string> | undefined,
357
- ): string | undefined {
231
+ function iconSrcOf(row: SessionRow | undefined, icons: Record<string, string> | undefined): string | undefined {
358
232
  const icon = row?.info.project?.icon
359
233
  return icon?.type === 'image' ? icons?.[icon.hash] : undefined
360
234
  }
@@ -362,32 +236,15 @@ function iconSrcOf(
362
236
  interface SessionRowItemProps {
363
237
  row: SessionRow
364
238
  active?: boolean
365
- /** See `SessionBrowserProps.activeSubagentId`. */
366
239
  activeSubagentId?: string
367
240
  showGateway?: boolean
368
- /** False when the list is already grouped by project — the header has said the
369
- * name, so the row must not spend its metadata line repeating it. What takes
370
- * the slot is the *sub-path inside the project* (`projectSubpath`), which is
371
- * the one thing the header cannot say and the only thing that tells two
372
- * sessions in the same repo apart; a session sitting at the project root has
373
- * nothing to add and the slot disappears entirely. The rule `showGateway`
374
- * follows one facet over. */
375
241
  showProject?: boolean
376
242
  projectIcons?: Record<string, string>
377
243
  onSelect?: (row: SessionRow) => void
378
244
  onDelete?: (row: SessionRow) => void
379
245
  onRename?: (row: SessionRow, title: string) => void
380
- /** See `SessionBrowserProps.onClearContext` — gated on the row's own
381
- * capability record, not on the engine name. */
382
246
  onClearContext?: (row: SessionRow) => void
383
- /** Open one of a session's sub-agents — which now means handing the session
384
- * panel over to that agent's own work (`SessionPanel.openSubagent`), and meant
385
- * "scroll to its `Task` row" before that surface existed. Both are honest; a
386
- * host picks what it can draw. Optional, and its absence is not a missing
387
- * feature: without it a step just opens the session, which is all a host with
388
- * neither can offer. Only *agent* steps ever call it — see `Step.kind`. */
389
247
  onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
390
- /** See `SessionBrowserProps.onRevealStep`. */
391
248
  onRevealStep?: (row: SessionRow, toolUseId: string) => void
392
249
  }
393
250
 
@@ -406,10 +263,6 @@ function SessionRowItem({
406
263
  onRevealStep,
407
264
  }: SessionRowItemProps) {
408
265
  const { info } = row
409
- // The rename affordance here is a pencil, not the card's own double-click:
410
- // the dashboard already spends the row's hover on two other actions, and a
411
- // gesture that only one of the three responds to is a gesture nobody finds.
412
- // So the trigger is `external` and this owns the flag.
413
266
  const [editing, setEditing] = useState(false)
414
267
 
415
268
  return (
@@ -424,32 +277,28 @@ function SessionRowItem({
424
277
  onSelectSubagent={onSelectSubagent ? (id) => onSelectSubagent(row, id) : undefined}
425
278
  onRevealStep={onRevealStep ? (id) => onRevealStep(row, id) : undefined}
426
279
  onRename={onRename ? (title) => onRename(row, title) : undefined}
427
- renameOn='external'
280
+ renameOn="external"
428
281
  editing={editing}
429
282
  onEditingChange={setEditing}
430
283
  actions={
431
284
  <>
432
285
  {onRename && !editing ? (
433
- <RowAction
434
- label='Rename session'
435
- onClick={() => setEditing(true)}>
436
- <Pencil className='size-3 text-fg-3' />
286
+ <RowAction label="Rename session" onClick={() => setEditing(true)}>
287
+ <Pencil className="size-3 text-fg-3" />
437
288
  </RowAction>
438
289
  ) : null}
439
- {/* Gated on the row's own capability record, not on the engine name —
440
- an engine that grows the ability gets the button with no change
441
- here, and one that loses it stops offering a control that fails. */}
442
290
  {onClearContext && info.capabilities?.clearContext ? (
443
291
  <RowAction
444
- label='Clear context'
445
- title='Clear the conversation — the session keeps running and the old conversation stays resumable'
446
- onClick={() => onClearContext(row)}>
447
- <Eraser className='size-3 text-fg-3' />
292
+ label="Clear context"
293
+ title="Clear the conversation — the session keeps running and the old conversation stays resumable"
294
+ onClick={() => onClearContext(row)}
295
+ >
296
+ <Eraser className="size-3 text-fg-3" />
448
297
  </RowAction>
449
298
  ) : null}
450
299
  {onDelete ? (
451
- <RowAction label='Close session' onClick={() => onDelete(row)}>
452
- <Trash2 className='size-3 text-fg-3' />
300
+ <RowAction label="Close session" onClick={() => onDelete(row)}>
301
+ <Trash2 className="size-3 text-fg-3" />
453
302
  </RowAction>
454
303
  ) : null}
455
304
  </>
@@ -458,58 +307,31 @@ function SessionRowItem({
458
307
  )
459
308
  }
460
309
 
461
- /**
462
- * One of the dashboard's hover actions.
463
- *
464
- * Hover-revealed rather than always on, which is the opposite of the
465
- * extension's single overflow glyph and the right call for each: there are
466
- * three of them here and a sidebar card has room for one, so the extension
467
- * spends that room on a menu and this spends the row's hover on the actions
468
- * themselves. Both stop the click — the whole card is pressable underneath, and
469
- * an action that also selected the session would do two things per press.
470
- */
471
- function RowAction({
472
- label,
473
- title,
474
- onClick,
475
- children,
476
- }: {
477
- label: string
478
- title?: string
479
- onClick: () => void
480
- children: ReactNode
481
- }) {
310
+ function RowAction({ label, title, onClick, children }: { label: string; title?: string; onClick: () => void; children: ReactNode }) {
482
311
  return (
483
312
  <Button
484
- variant='ghost'
485
- size='icon-sm'
313
+ variant="ghost"
314
+ size="icon-sm"
486
315
  aria-label={label}
487
316
  title={title ?? label}
488
- className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'
317
+ className="size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
489
318
  onClick={(e) => {
490
319
  e.stopPropagation()
491
320
  onClick()
492
- }}>
321
+ }}
322
+ >
493
323
  {children}
494
324
  </Button>
495
325
  )
496
326
  }
497
327
 
498
-
499
- /** A multi-select facet. Empty = no filter, which is why the trigger reads the
500
- * facet's name rather than "All": nothing is being excluded. */
501
- /**
502
- * One labelled control row. The label column is fixed so every control starts
503
- * on the same x — the thing that makes a stack of them read as a form rather
504
- * than as five unrelated widgets.
505
- */
506
328
  function FilterRow({ label, children }: { label: string; children: React.ReactNode }) {
507
329
  return (
508
- <div className='flex items-center gap-2'>
509
- <span aria-hidden className='w-14 shrink-0 truncate text-label text-fg-3'>
330
+ <div className="flex items-center gap-2">
331
+ <span aria-hidden className="w-14 shrink-0 truncate text-label text-fg-3">
510
332
  {label}
511
333
  </span>
512
- <div className='min-w-0 flex-1'>{children}</div>
334
+ <div className="min-w-0 flex-1">{children}</div>
513
335
  </div>
514
336
  )
515
337
  }
@@ -526,11 +348,10 @@ function FacetSelect({
526
348
  onChange: (value: string[]) => void
527
349
  }) {
528
350
  return (
529
- <div className='flex items-center gap-1'>
351
+ <div className="flex items-center gap-1">
530
352
  <Select multiple value={value} onValueChange={(v) => onChange(v as string[])}>
531
- <SelectTrigger aria-label={label} className='min-w-0 flex-1'>
353
+ <SelectTrigger aria-label={label} className="min-w-0 flex-1">
532
354
  <SelectValue>
533
- {/* "All" rather than the label, which the row already carries. */}
534
355
  {value.length === 0
535
356
  ? 'All'
536
357
  : value.length === 1
@@ -547,8 +368,8 @@ function FacetSelect({
547
368
  </SelectContent>
548
369
  </Select>
549
370
  {value.length > 0 ? (
550
- <Button variant='ghost' size='icon-sm' aria-label={`Clear ${label}`} onClick={() => onChange([])}>
551
- <X className='size-3 text-fg-4' />
371
+ <Button variant="ghost" size="icon-sm" aria-label={`Clear ${label}`} onClick={() => onChange([])}>
372
+ <X className="size-3 text-fg-4" />
552
373
  </Button>
553
374
  ) : null}
554
375
  </div>
@@ -568,7 +389,7 @@ function OneOfSelect({
568
389
  }) {
569
390
  return (
570
391
  <Select value={value} onValueChange={(v) => onChange(v as string)}>
571
- <SelectTrigger aria-label={label} className='w-full min-w-0'>
392
+ <SelectTrigger aria-label={label} className="w-full min-w-0">
572
393
  <SelectValue>{options.find((o) => o.value === value)?.label ?? label}</SelectValue>
573
394
  </SelectTrigger>
574
395
  <SelectContent>
@@ -3,60 +3,30 @@ import { cn } from '../../lib/utils.ts'
3
3
 
4
4
  export interface SessionEmptyStateProps {
5
5
  cwd?: string
6
- /** Whether `/command` completion is live yet — the CLI reports its commands a
7
- * beat after the session starts, and promising a feature that isn't wired up
8
- * yet is worse than not mentioning it. */
9
6
  hasCommands?: boolean
10
- /** Whether the engine has reported skills the `/` popover can offer. Its own
11
- * flag, not a variant of `hasCommands`: what `/` does differs — a command is
12
- * submitted, a skill is typed for you to edit — and an engine can have one
13
- * without the other. */
14
7
  hasSkills?: boolean
15
- /** Whether this gateway serves `@file` search for the session's directory. */
16
8
  canBrowseFiles?: boolean
17
9
  className?: string
18
10
  }
19
11
 
20
- /**
21
- * What a session shows before it has said anything: where the agent is sitting,
22
- * and what the composer accepts beyond prose.
23
- *
24
- * Every hint is conditional on the affordance actually existing — an engine
25
- * without slash commands, or a gateway without host files, is not told about
26
- * them. Deliberately no project name (the header already carries it) and no
27
- * brand mark (its geometry is inlined in several places already and they are
28
- * meant to stay identical).
29
- */
30
- export function SessionEmptyState({
31
- cwd,
32
- hasCommands,
33
- hasSkills,
34
- canBrowseFiles,
35
- className,
36
- }: SessionEmptyStateProps) {
12
+ export function SessionEmptyState({ cwd, hasCommands, hasSkills, canBrowseFiles, className }: SessionEmptyStateProps) {
37
13
  const hints = [
38
14
  { icon: MessageSquareText, text: 'Tell the agent what to do.' },
39
- // Two keys, two hints — they are different features, not two spellings of
40
- // one. `$` is codex's own sigil for skills; `/` stays the CLI's commands.
41
15
  ...(hasCommands ? [{ icon: SlashSquare, text: 'Type / for the CLI’s slash commands.' }] : []),
42
- ...(hasSkills
43
- ? [{ icon: Sparkles, text: 'Type $ to draft a message for one of the agent’s skills.' }]
44
- : []),
45
- ...(canBrowseFiles
46
- ? [{ icon: AtSign, text: 'Type @ to search this project’s files.' }]
47
- : []),
16
+ ...(hasSkills ? [{ icon: Sparkles, text: 'Type $ to draft a message for one of the agent’s skills.' }] : []),
17
+ ...(canBrowseFiles ? [{ icon: AtSign, text: 'Type @ to search this project’s files.' }] : []),
48
18
  ]
49
19
  return (
50
20
  <div className={cn('flex flex-col items-center gap-4 py-10', className)}>
51
- <div className='flex size-12 items-center justify-center rounded-xl bg-surface text-fg-3'>
52
- <Terminal className='size-5' />
21
+ <div className="flex size-12 items-center justify-center rounded-xl bg-surface text-fg-3">
22
+ <Terminal className="size-5" />
53
23
  </div>
54
- {cwd ? <p className='max-w-full truncate font-mono text-label text-fg-4'>{cwd}</p> : null}
55
- <ul className='w-full max-w-sm divide-y divide-border overflow-hidden rounded-lg bg-surface'>
24
+ {cwd ? <p className="max-w-full truncate font-mono text-label text-fg-4">{cwd}</p> : null}
25
+ <ul className="w-full max-w-sm divide-y divide-border overflow-hidden rounded-lg bg-surface">
56
26
  {hints.map((hint) => (
57
- <li key={hint.text} className='flex items-center gap-3 px-3.5 py-2.5'>
58
- <hint.icon className='size-4 shrink-0 text-fg-4' />
59
- <span className='text-body-sm text-fg-3'>{hint.text}</span>
27
+ <li key={hint.text} className="flex items-center gap-3 px-3.5 py-2.5">
28
+ <hint.icon className="size-4 shrink-0 text-fg-4" />
29
+ <span className="text-body-sm text-fg-3">{hint.text}</span>
60
30
  </li>
61
31
  ))}
62
32
  </ul>