@workerdeck/ui 0.20.0 → 0.22.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/README.md +3 -2
- package/build/{SessionPanel-CnU_IJ3-.d.mts → SessionPanel-13l25ubU.d.mts} +55 -7
- package/build/{SessionPanel-CHktI2CF.mjs → SessionPanel-DgwjH4Ve.mjs} +642 -231
- package/build/SessionPanel-DgwjH4Ve.mjs.map +1 -0
- package/build/index.d.mts +309 -53
- package/build/index.mjs +534 -397
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +3547 -0
- package/build/workspace.d.mts +10 -1
- package/build/workspace.mjs +4 -2
- package/build/workspace.mjs.map +1 -1
- package/package.json +15 -7
- package/src/components/agent/Composer.tsx +10 -10
- package/src/components/agent/ContextDialog.tsx +3 -2
- package/src/components/agent/Conversation.tsx +1 -1
- package/src/components/agent/McpDialog.tsx +3 -1
- package/src/components/agent/Scrubber.tsx +248 -0
- package/src/components/agent/SessionBrowser.tsx +113 -293
- package/src/components/agent/SessionItem.tsx +538 -0
- package/src/components/agent/SessionList.tsx +3 -1
- package/src/components/agent/SessionPanel.tsx +112 -21
- package/src/components/agent/SessionStatusIcon.tsx +43 -0
- package/src/components/agent/SessionSteps.tsx +118 -61
- package/src/components/agent/SessionWorkspace.tsx +11 -0
- package/src/components/agent/SkillsDialog.tsx +3 -2
- package/src/components/agent/StatusBar.tsx +1 -1
- package/src/components/agent/Transcript.tsx +147 -73
- package/src/components/agent/UsageDialog.tsx +3 -1
- package/src/components/agent/scrubber-marks.ts +277 -0
- package/src/components/terminal/PermissionPrompt.tsx +3 -0
- package/src/components/terminal/QuestionPrompt.tsx +3 -0
- package/src/components/terminal/StatusLine.tsx +3 -1
- package/src/components/ui/AlertDialog.tsx +23 -20
- package/src/components/ui/Dialog.tsx +26 -23
- package/src/components/ui/Menu.tsx +20 -17
- package/src/components/ui/PortalScope.tsx +27 -0
- package/src/components/ui/Select.tsx +19 -16
- package/src/components/ui/Tooltip.tsx +13 -10
- package/src/index.ts +5 -1
- package/src/styles/scoped.entry.css +16 -0
- package/src/styles/terminal.css +1 -1
- package/src/styles/theme.css +223 -8
- package/build/SessionPanel-CHktI2CF.mjs.map +0 -1
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
|
-
import {
|
|
4
|
-
BellRing,
|
|
5
|
-
CircleAlert,
|
|
6
|
-
CircleSlash,
|
|
7
|
-
Eraser,
|
|
8
|
-
Layers,
|
|
9
|
-
Moon,
|
|
10
|
-
PauseCircle,
|
|
11
|
-
Pencil,
|
|
12
|
-
Search,
|
|
13
|
-
SearchX,
|
|
14
|
-
Trash2,
|
|
15
|
-
X,
|
|
16
|
-
} from 'lucide-react'
|
|
3
|
+
import { Eraser, Layers, Pencil, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
17
4
|
import {
|
|
18
5
|
STATE_LABELS,
|
|
19
6
|
STATE_ORDER,
|
|
@@ -22,10 +9,7 @@ import {
|
|
|
22
9
|
filterRows,
|
|
23
10
|
groupRows,
|
|
24
11
|
hasFacetFilter,
|
|
25
|
-
projectLabel,
|
|
26
|
-
projectSubpath,
|
|
27
12
|
projectsOf,
|
|
28
|
-
sessionLabel,
|
|
29
13
|
subsetSummary,
|
|
30
14
|
} from '@workerdeck/protocol'
|
|
31
15
|
import type {
|
|
@@ -40,13 +24,14 @@ import { Button } from '../ui/Button.tsx'
|
|
|
40
24
|
import { Empty } from '../ui/Empty.tsx'
|
|
41
25
|
import { Input } from '../ui/Input.tsx'
|
|
42
26
|
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
43
|
-
import { Spinner } from '../ui/Spinner.tsx'
|
|
44
|
-
import { ContextRing } from './ContextRing.tsx'
|
|
45
|
-
import { EngineIcon, vendorMarkClass, vendorTextClass } from './EngineIcon.tsx'
|
|
46
27
|
import { ProjectIcon } from './ProjectIcon.tsx'
|
|
47
|
-
import {
|
|
28
|
+
import { SessionItem } from './SessionItem.tsx'
|
|
48
29
|
import { cn } from '../../lib/utils.ts'
|
|
49
|
-
|
|
30
|
+
|
|
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
|
+
export { SessionStatusIcon } from './SessionStatusIcon.tsx'
|
|
50
35
|
|
|
51
36
|
/**
|
|
52
37
|
* A sessions list with the affordances a list of thirty needs: search, facets,
|
|
@@ -71,6 +56,13 @@ export interface SessionBrowserProps {
|
|
|
71
56
|
* phone — makes the scope filter genuinely inert rather than empty. */
|
|
72
57
|
scope?: WorkspaceScope
|
|
73
58
|
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
|
+
activeSubagentId?: string
|
|
74
66
|
onSelect?: (row: SessionRow) => void
|
|
75
67
|
onDelete?: (row: SessionRow) => void
|
|
76
68
|
/**
|
|
@@ -99,6 +91,11 @@ export interface SessionBrowserProps {
|
|
|
99
91
|
* transcript to a `Task` row. Absent, the sub-agent list still expands and a
|
|
100
92
|
* step just opens its session — see `SessionRowItemProps`. */
|
|
101
93
|
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
|
+
onRevealStep?: (row: SessionRow, toolUseId: string) => void
|
|
102
99
|
/** Rendered when nothing at all exists (as opposed to nothing matching). */
|
|
103
100
|
emptyState?: React.ReactNode
|
|
104
101
|
/**
|
|
@@ -154,11 +151,13 @@ export function SessionBrowser({
|
|
|
154
151
|
onConfigChange,
|
|
155
152
|
scope,
|
|
156
153
|
activeId,
|
|
154
|
+
activeSubagentId,
|
|
157
155
|
onSelect,
|
|
158
156
|
onDelete,
|
|
159
157
|
onRename,
|
|
160
158
|
onClearContext,
|
|
161
159
|
onSelectSubagent,
|
|
160
|
+
onRevealStep,
|
|
162
161
|
emptyState,
|
|
163
162
|
showControls = true,
|
|
164
163
|
projectIcons,
|
|
@@ -299,11 +298,18 @@ export function SessionBrowser({
|
|
|
299
298
|
<Empty icon={<Layers />} title='Nothing here' description='No session to show.' />
|
|
300
299
|
)
|
|
301
300
|
) : (
|
|
302
|
-
|
|
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'>
|
|
303
309
|
{groups.map((group) => (
|
|
304
310
|
<div key={group.key} className='flex flex-col gap-1'>
|
|
305
311
|
{config.groupBy !== 'none' && group.label ? (
|
|
306
|
-
<div className='flex items-center gap-2 px-
|
|
312
|
+
<div className='flex items-center gap-2 px-2 text-label font-medium text-fg-4'>
|
|
307
313
|
{/* Only the project facet has a mark of its own, and a group
|
|
308
314
|
IS one project root, so the first row is a fair source. */}
|
|
309
315
|
{config.groupBy === 'project' ? (
|
|
@@ -322,6 +328,7 @@ export function SessionBrowser({
|
|
|
322
328
|
key={`${row.hostId}:${row.info.id}`}
|
|
323
329
|
row={row}
|
|
324
330
|
active={row.info.id === activeId}
|
|
331
|
+
activeSubagentId={activeSubagentId}
|
|
325
332
|
showGateway={gateways.length > 1 && config.groupBy !== 'gateway'}
|
|
326
333
|
showProject={config.groupBy !== 'project'}
|
|
327
334
|
projectIcons={projectIcons}
|
|
@@ -330,6 +337,7 @@ export function SessionBrowser({
|
|
|
330
337
|
onRename={onRename}
|
|
331
338
|
onClearContext={onClearContext}
|
|
332
339
|
onSelectSubagent={onSelectSubagent}
|
|
340
|
+
onRevealStep={onRevealStep}
|
|
333
341
|
/>
|
|
334
342
|
))}
|
|
335
343
|
</div>
|
|
@@ -354,6 +362,8 @@ function iconSrcOf(
|
|
|
354
362
|
interface SessionRowItemProps {
|
|
355
363
|
row: SessionRow
|
|
356
364
|
active?: boolean
|
|
365
|
+
/** See `SessionBrowserProps.activeSubagentId`. */
|
|
366
|
+
activeSubagentId?: string
|
|
357
367
|
showGateway?: boolean
|
|
358
368
|
/** False when the list is already grouped by project — the header has said the
|
|
359
369
|
* name, so the row must not spend its metadata line repeating it. What takes
|
|
@@ -377,11 +387,14 @@ interface SessionRowItemProps {
|
|
|
377
387
|
* feature: without it a step just opens the session, which is all a host with
|
|
378
388
|
* neither can offer. Only *agent* steps ever call it — see `Step.kind`. */
|
|
379
389
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
390
|
+
/** See `SessionBrowserProps.onRevealStep`. */
|
|
391
|
+
onRevealStep?: (row: SessionRow, toolUseId: string) => void
|
|
380
392
|
}
|
|
381
393
|
|
|
382
394
|
function SessionRowItem({
|
|
383
395
|
row,
|
|
384
396
|
active,
|
|
397
|
+
activeSubagentId,
|
|
385
398
|
showGateway,
|
|
386
399
|
showProject = true,
|
|
387
400
|
projectIcons,
|
|
@@ -390,292 +403,99 @@ function SessionRowItem({
|
|
|
390
403
|
onRename,
|
|
391
404
|
onClearContext,
|
|
392
405
|
onSelectSubagent,
|
|
406
|
+
onRevealStep,
|
|
393
407
|
}: SessionRowItemProps) {
|
|
394
408
|
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.
|
|
395
413
|
const [editing, setEditing] = useState(false)
|
|
396
|
-
// Expansion is the row's own state and deliberately not persisted: a list that
|
|
397
|
-
// reopened yesterday's work on every reload would be showing a settled tail
|
|
398
|
-
// nobody asked for.
|
|
399
|
-
const [expanded, setExpanded] = useState(false)
|
|
400
|
-
|
|
401
|
-
// What it is and what it has spent, in one line — the same set the extension
|
|
402
|
-
// shows, joined the same way, so the two lists read as one product.
|
|
403
|
-
// protocol's own spelling, so this row, the group header above it and the
|
|
404
|
-
// project facet cannot disagree about what a project is called. Under a
|
|
405
|
-
// project group it is the sub-path instead — see `showProject` — and a session
|
|
406
|
-
// at the root contributes nothing at all rather than a name already on screen.
|
|
407
|
-
const project = showProject ? projectLabel(row) : projectSubpath(row)
|
|
408
|
-
const projectIcon = showProject ? info.project?.icon : undefined
|
|
409
|
-
const engine = info.engine ?? 'claude'
|
|
410
|
-
// Everything after the model. The model itself is drawn separately because it
|
|
411
|
-
// is the one segment that wears a colour — see `vendorTextClass` — and a
|
|
412
|
-
// coloured run inside a joined string would have to be spliced back out.
|
|
413
|
-
const details = [
|
|
414
|
-
project,
|
|
415
|
-
showGateway ? row.hostName : undefined,
|
|
416
|
-
info.profile ? `@${info.profile}` : undefined,
|
|
417
|
-
formatCost(info.totalCostUsd),
|
|
418
|
-
].filter(Boolean)
|
|
419
|
-
const steps = sessionSteps(info, (toolUseId) =>
|
|
420
|
-
onSelectSubagent ? onSelectSubagent(row, toolUseId) : onSelect?.(row),
|
|
421
|
-
)
|
|
422
|
-
// Where the icon goes: immediately before the project's own name, wherever
|
|
423
|
-
// that landed in the joined line. Split rather than interleaved as nodes,
|
|
424
|
-
// because everything here is one truncating mono run and a flex of pieces
|
|
425
|
-
// would each shrink a little and leave several half-words.
|
|
426
|
-
const cut = project === undefined ? -1 : details.indexOf(project)
|
|
427
414
|
|
|
428
415
|
return (
|
|
429
|
-
<
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
<button
|
|
472
|
-
type='button'
|
|
473
|
-
className={cn(
|
|
474
|
-
'min-w-0 flex-1 truncate text-left text-body-sm outline-none',
|
|
475
|
-
active ? 'font-medium text-fg-1' : 'text-fg-2',
|
|
476
|
-
)}>
|
|
477
|
-
{sessionLabel(info)}
|
|
478
|
-
</button>
|
|
479
|
-
)}
|
|
480
|
-
{row.unseen > 0 ? (
|
|
481
|
-
// Transcript rows since this session was last on screen — the same
|
|
482
|
-
// unit the VS Code badge counts, because turns undercount badly.
|
|
483
|
-
<span
|
|
484
|
-
title={`${row.unseen} new`}
|
|
485
|
-
className='shrink-0 rounded-full bg-accent px-1.5 text-label text-accent-fg'>
|
|
486
|
-
{row.unseen}
|
|
487
|
-
</span>
|
|
488
|
-
) : null}
|
|
489
|
-
<span className='shrink-0 text-label text-fg-4'>
|
|
490
|
-
{formatRelativeTime(info.lastActivityAt ?? info.createdAt)}
|
|
491
|
-
</span>
|
|
492
|
-
<ContextRing usage={info.contextUsage} />
|
|
493
|
-
</div>
|
|
494
|
-
|
|
495
|
-
{/* Line two: what it is — the engine's mark and its model in the vendor's
|
|
496
|
-
own colour, then where it runs — with the actions at the far right,
|
|
497
|
-
away from the title you are actually reading. */}
|
|
498
|
-
<div className='flex items-center gap-1 text-label text-fg-4'>
|
|
499
|
-
<Gutter>
|
|
500
|
-
{/* The colour goes on the icon, not on this cell: the svg ships its
|
|
501
|
-
own `text-fg-3` and only a class on the element itself merges over
|
|
502
|
-
it. */}
|
|
503
|
-
<EngineIcon
|
|
504
|
-
engine={engine}
|
|
505
|
-
model={info.model}
|
|
506
|
-
className={vendorMarkClass(engine, info.model)}
|
|
507
|
-
/>
|
|
508
|
-
</Gutter>
|
|
509
|
-
<button
|
|
510
|
-
type='button'
|
|
511
|
-
tabIndex={-1}
|
|
512
|
-
className='min-w-0 flex-1 truncate text-left font-mono outline-none'>
|
|
513
|
-
<span className={vendorTextClass(engine, info.model)}>
|
|
514
|
-
{/* The model id as a person says it — `claude-opus-5[1m]` is a wire
|
|
515
|
-
value, and a list line has no room for a context-window suffix. */}
|
|
516
|
-
{friendlyModel(info.model)}
|
|
517
|
-
</span>
|
|
518
|
-
{details.length > 0 ? ' · ' : ''}
|
|
519
|
-
{cut < 0 ? (
|
|
520
|
-
details.join(' · ')
|
|
521
|
-
) : (
|
|
522
|
-
<>
|
|
523
|
-
{details.slice(0, cut).map((part) => `${part} · `)}
|
|
524
|
-
<ProjectIcon
|
|
525
|
-
icon={projectIcon}
|
|
526
|
-
src={iconSrcOf(row, projectIcons)}
|
|
527
|
-
name={project}
|
|
528
|
-
/* Nudged onto the text baseline: a 12px glyph box against an
|
|
529
|
-
11px line sits a hair proud without it. */
|
|
530
|
-
className='mr-1 align-[-0.2em]'
|
|
531
|
-
/>
|
|
532
|
-
{details.slice(cut).join(' · ')}
|
|
533
|
-
</>
|
|
534
|
-
)}
|
|
535
|
-
</button>
|
|
536
|
-
{steps.length > 0 ? (
|
|
537
|
-
<StepToggle
|
|
538
|
-
expanded={expanded}
|
|
539
|
-
running={runningSteps(steps)}
|
|
540
|
-
total={steps.length}
|
|
541
|
-
noun={steps[0]!.noun}
|
|
542
|
-
onToggle={() => setExpanded((v) => !v)}
|
|
543
|
-
/>
|
|
544
|
-
) : null}
|
|
545
|
-
{onRename && !editing ? (
|
|
546
|
-
<Button
|
|
547
|
-
variant='ghost'
|
|
548
|
-
size='icon-sm'
|
|
549
|
-
aria-label='Rename session'
|
|
550
|
-
className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'
|
|
551
|
-
onClick={(e) => {
|
|
552
|
-
e.stopPropagation()
|
|
553
|
-
setEditing(true)
|
|
554
|
-
}}>
|
|
555
|
-
<Pencil className='size-3 text-fg-3' />
|
|
556
|
-
</Button>
|
|
557
|
-
) : null}
|
|
558
|
-
{onClearContext && info.capabilities?.clearContext ? (
|
|
559
|
-
<Button
|
|
560
|
-
variant='ghost'
|
|
561
|
-
size='icon-sm'
|
|
562
|
-
aria-label='Clear context'
|
|
563
|
-
title='Clear the conversation — the session keeps running and the old conversation stays resumable'
|
|
564
|
-
className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'
|
|
565
|
-
onClick={(e) => {
|
|
566
|
-
e.stopPropagation()
|
|
567
|
-
onClearContext(row)
|
|
568
|
-
}}>
|
|
569
|
-
<Eraser className='size-3 text-fg-3' />
|
|
570
|
-
</Button>
|
|
571
|
-
) : null}
|
|
572
|
-
{onDelete ? (
|
|
573
|
-
<Button
|
|
574
|
-
variant='ghost'
|
|
575
|
-
size='icon-sm'
|
|
576
|
-
aria-label='Close session'
|
|
577
|
-
className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'
|
|
578
|
-
onClick={(e) => {
|
|
579
|
-
e.stopPropagation()
|
|
580
|
-
onDelete(row)
|
|
581
|
-
}}>
|
|
582
|
-
<Trash2 className='size-3 text-fg-3' />
|
|
583
|
-
</Button>
|
|
584
|
-
) : null}
|
|
585
|
-
</div>
|
|
586
|
-
{expanded ? steps.map((step) => <StepRow key={step.key} step={step} onSelect={step.onSelect} />) : null}
|
|
587
|
-
</div>
|
|
416
|
+
<SessionItem
|
|
417
|
+
row={row}
|
|
418
|
+
active={active === true}
|
|
419
|
+
activeStepKey={activeSubagentId}
|
|
420
|
+
showGateway={showGateway}
|
|
421
|
+
showProject={showProject}
|
|
422
|
+
projectIcons={projectIcons}
|
|
423
|
+
onSelect={() => onSelect?.(row)}
|
|
424
|
+
onSelectSubagent={onSelectSubagent ? (id) => onSelectSubagent(row, id) : undefined}
|
|
425
|
+
onRevealStep={onRevealStep ? (id) => onRevealStep(row, id) : undefined}
|
|
426
|
+
onRename={onRename ? (title) => onRename(row, title) : undefined}
|
|
427
|
+
renameOn='external'
|
|
428
|
+
editing={editing}
|
|
429
|
+
onEditingChange={setEditing}
|
|
430
|
+
actions={
|
|
431
|
+
<>
|
|
432
|
+
{onRename && !editing ? (
|
|
433
|
+
<RowAction
|
|
434
|
+
label='Rename session'
|
|
435
|
+
onClick={() => setEditing(true)}>
|
|
436
|
+
<Pencil className='size-3 text-fg-3' />
|
|
437
|
+
</RowAction>
|
|
438
|
+
) : 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
|
+
{onClearContext && info.capabilities?.clearContext ? (
|
|
443
|
+
<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' />
|
|
448
|
+
</RowAction>
|
|
449
|
+
) : null}
|
|
450
|
+
{onDelete ? (
|
|
451
|
+
<RowAction label='Close session' onClick={() => onDelete(row)}>
|
|
452
|
+
<Trash2 className='size-3 text-fg-3' />
|
|
453
|
+
</RowAction>
|
|
454
|
+
) : null}
|
|
455
|
+
</>
|
|
456
|
+
}
|
|
457
|
+
/>
|
|
588
458
|
)
|
|
589
459
|
}
|
|
590
460
|
|
|
591
461
|
/**
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
* A shared column rather than a leading character: the state and the engine mark
|
|
595
|
-
* are different widths and a bare glyph would let the two text columns start at
|
|
596
|
-
* different x. Centring inside a fixed cell is what makes them agree —
|
|
597
|
-
* `packages/ui`'s terminal-gutter argument at row scale.
|
|
598
|
-
*/
|
|
599
|
-
function Gutter({ children }: { children: ReactNode }) {
|
|
600
|
-
return <span className='flex w-3.5 shrink-0 items-center justify-center'>{children}</span>
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* State as one glyph — a ringing bell when it wants a human, a spinner while it
|
|
605
|
-
* works, a moon when it is only sleeping. Replaces the text badge: in a sidebar
|
|
606
|
-
* the word costs more room than it earns, and the states that matter are the two
|
|
607
|
-
* you can recognise without reading.
|
|
462
|
+
* One of the dashboard's hover actions.
|
|
608
463
|
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
* derived view model exists to prevent. The value was in scope and unread.
|
|
616
|
-
*
|
|
617
|
-
* The terminal statuses still come off `info.status`, because `ended` collapses
|
|
618
|
-
* `failed` and `closed` into one bucket and those are worth telling apart here.
|
|
619
|
-
*/
|
|
620
|
-
export function SessionStatusIcon({ row }: { row: SessionRow }) {
|
|
621
|
-
const { info } = row
|
|
622
|
-
if (row.state === 'attention') {
|
|
623
|
-
return <BellRing className='size-3.5 shrink-0 animate-pulse text-warning' />
|
|
624
|
-
}
|
|
625
|
-
if (row.state === 'working') {
|
|
626
|
-
return <Spinner className='size-3.5 shrink-0 text-info' />
|
|
627
|
-
}
|
|
628
|
-
switch (info.status) {
|
|
629
|
-
case 'failed':
|
|
630
|
-
return <CircleAlert className='size-3.5 shrink-0 text-danger' />
|
|
631
|
-
case 'closed':
|
|
632
|
-
return <CircleSlash className='size-3.5 shrink-0 text-fg-4' />
|
|
633
|
-
case 'parked':
|
|
634
|
-
return <PauseCircle className='size-3.5 shrink-0 text-fg-3' />
|
|
635
|
-
default:
|
|
636
|
-
return <Moon className='size-3.5 shrink-0 text-fg-4' />
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Inline rename. Enter commits, Escape cancels, blur commits — but only a blur
|
|
642
|
-
* that is still inside this document: switching windows must not silently commit,
|
|
643
|
-
* nor kill the editor in the frame it opened.
|
|
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.
|
|
644
470
|
*/
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
471
|
+
function RowAction({
|
|
472
|
+
label,
|
|
473
|
+
title,
|
|
474
|
+
onClick,
|
|
475
|
+
children,
|
|
649
476
|
}: {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
477
|
+
label: string
|
|
478
|
+
title?: string
|
|
479
|
+
onClick: () => void
|
|
480
|
+
children: ReactNode
|
|
653
481
|
}) {
|
|
654
|
-
const [value, setValue] = useState(initial)
|
|
655
|
-
const ref = useRef<HTMLInputElement>(null)
|
|
656
|
-
useEffect(() => {
|
|
657
|
-
ref.current?.select()
|
|
658
|
-
}, [])
|
|
659
482
|
return (
|
|
660
|
-
<
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
onClick={(e) =>
|
|
667
|
-
onChange={(e) => setValue(e.target.value)}
|
|
668
|
-
onBlur={() => (document.hasFocus() ? onCommit(value.trim()) : undefined)}
|
|
669
|
-
onKeyDown={(e) => {
|
|
670
|
-
if (e.key === 'Enter') onCommit(value.trim())
|
|
671
|
-
else if (e.key === 'Escape') onCancel()
|
|
483
|
+
<Button
|
|
484
|
+
variant='ghost'
|
|
485
|
+
size='icon-sm'
|
|
486
|
+
aria-label={label}
|
|
487
|
+
title={title ?? label}
|
|
488
|
+
className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'
|
|
489
|
+
onClick={(e) => {
|
|
672
490
|
e.stopPropagation()
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
491
|
+
onClick()
|
|
492
|
+
}}>
|
|
493
|
+
{children}
|
|
494
|
+
</Button>
|
|
676
495
|
)
|
|
677
496
|
}
|
|
678
497
|
|
|
498
|
+
|
|
679
499
|
/** A multi-select facet. Empty = no filter, which is why the trigger reads the
|
|
680
500
|
* facet's name rather than "All": nothing is being excluded. */
|
|
681
501
|
/**
|