@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.
- package/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DDgBkHsi.mjs} +2196 -3364
- 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 +80 -24
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +9 -124
- package/build/workspace.mjs +13 -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 +4 -6
- 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 +345 -923
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +108 -215
- 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 +136 -19
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-13l25ubU.d.mts +0 -609
- package/build/SessionPanel-DgwjH4Ve.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- 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)
|
|
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=
|
|
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=
|
|
190
|
-
<Search className=
|
|
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=
|
|
195
|
-
aria-label=
|
|
196
|
-
className=
|
|
90
|
+
placeholder="Search sessions"
|
|
91
|
+
aria-label="Search sessions"
|
|
92
|
+
className="pl-8"
|
|
197
93
|
/>
|
|
198
94
|
</div>
|
|
199
|
-
<FilterRow label=
|
|
95
|
+
<FilterRow label="State">
|
|
200
96
|
<FacetSelect
|
|
201
|
-
label=
|
|
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=
|
|
104
|
+
<FilterRow label="Engine">
|
|
209
105
|
<FacetSelect
|
|
210
|
-
label=
|
|
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=
|
|
114
|
+
<FilterRow label="Gateway">
|
|
219
115
|
<FacetSelect
|
|
220
|
-
label=
|
|
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=
|
|
124
|
+
<FilterRow label="Project">
|
|
229
125
|
<FacetSelect
|
|
230
|
-
label=
|
|
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=
|
|
133
|
+
<FilterRow label="Group">
|
|
238
134
|
<OneOfSelect
|
|
239
|
-
label=
|
|
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=
|
|
147
|
+
<FilterRow label="Sort">
|
|
252
148
|
<OneOfSelect
|
|
253
|
-
label=
|
|
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=
|
|
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=
|
|
275
|
-
className=
|
|
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=
|
|
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=
|
|
293
|
-
description=
|
|
294
|
-
action=
|
|
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=
|
|
191
|
+
<Empty icon={<Layers />} title="Nothing here" description="No session to show." />
|
|
299
192
|
)
|
|
300
193
|
) : (
|
|
301
|
-
|
|
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=
|
|
196
|
+
<div key={group.key} className="flex flex-col gap-1">
|
|
311
197
|
{config.groupBy !== 'none' && group.label ? (
|
|
312
|
-
<div className=
|
|
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=
|
|
323
|
-
<span className=
|
|
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
|
-
|
|
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=
|
|
280
|
+
renameOn="external"
|
|
428
281
|
editing={editing}
|
|
429
282
|
onEditingChange={setEditing}
|
|
430
283
|
actions={
|
|
431
284
|
<>
|
|
432
285
|
{onRename && !editing ? (
|
|
433
|
-
<RowAction
|
|
434
|
-
|
|
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=
|
|
445
|
-
title=
|
|
446
|
-
onClick={() => onClearContext(row)}
|
|
447
|
-
|
|
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=
|
|
452
|
-
<Trash2 className=
|
|
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=
|
|
485
|
-
size=
|
|
313
|
+
variant="ghost"
|
|
314
|
+
size="icon-sm"
|
|
486
315
|
aria-label={label}
|
|
487
316
|
title={title ?? label}
|
|
488
|
-
className=
|
|
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=
|
|
509
|
-
<span aria-hidden className=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
551
|
-
<X className=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
52
|
-
<Terminal className=
|
|
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=
|
|
55
|
-
<ul className=
|
|
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=
|
|
58
|
-
<hint.icon className=
|
|
59
|
-
<span className=
|
|
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>
|