@workerdeck/ui 3.2.0 → 3.4.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-BNNc8grK.d.mts → SessionPanel-DIi4sEgh.d.mts} +5 -2
- package/build/index.d.mts +24 -5
- package/build/index.mjs +297 -62
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +52 -3
- package/build/{use-path-links-C6Fh5zdx.mjs → use-path-links-WCsysQPy.mjs} +337 -83
- package/build/use-path-links-WCsysQPy.mjs.map +1 -0
- package/build/workspace.d.mts +1 -1
- package/build/workspace.mjs +1 -1
- package/package.json +4 -4
- package/src/components/agent/Composer.tsx +29 -6
- package/src/components/agent/EffortSelect.tsx +64 -0
- package/src/components/agent/ModelSelect.tsx +1 -1
- package/src/components/agent/SessionBrowser.tsx +106 -17
- package/src/components/agent/SessionFilters.tsx +2 -2
- package/src/components/agent/SessionGroups.tsx +248 -0
- package/src/components/agent/SessionItem.tsx +3 -1
- package/src/components/agent/SessionPanel.tsx +75 -28
- package/src/components/agent/TranscriptItemView.tsx +20 -2
- package/src/components/agent/composer-commands.ts +24 -0
- package/src/components/terminal/affordances.tsx +29 -1
- package/src/components/terminal/height.ts +12 -2
- package/src/components/terminal/items.tsx +35 -3
- package/src/components/terminal/live-tool.ts +71 -0
- package/src/components/terminal/tool-run.ts +6 -3
- package/src/index.ts +8 -1
- package/src/styles/terminal.css +9 -2
- package/src/styles/theme.css +3 -1
- package/build/use-path-links-C6Fh5zdx.mjs.map +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Fragment, useEffect, useState, type ReactNode } from 'react'
|
|
2
|
-
import type
|
|
2
|
+
import { peerShortId, type MessageOrigin } from '@workerdeck/protocol'
|
|
3
3
|
import type { ShellItem, TranscriptItem } from '@workerdeck/react'
|
|
4
4
|
import { compactionText, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
5
5
|
import { isMutatingTool } from '../../lib/tool-icon.ts'
|
|
6
6
|
import { usePulse } from '../agent/pulse.tsx'
|
|
7
7
|
import { PromptTokenText } from '../agent/PromptTokenText.tsx'
|
|
8
|
-
import { ActionPlacementProvider, BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
8
|
+
import { ActionPlacementProvider, BackgroundAction, BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
9
|
+
import { canBackground, elapsedLabel, liveTailLines, runStartedAt, runTailLines } from './live-tool.ts'
|
|
9
10
|
import { TerminalDiff } from './diff.tsx'
|
|
10
11
|
import { TerminalMarkdown } from './markdown.tsx'
|
|
11
12
|
import { usePeerNames } from './peer-names.tsx'
|
|
@@ -41,7 +42,7 @@ function clipToChars(lines: string[], maxChars: number): string[] {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function peerLabel(origin: MessageOrigin): string {
|
|
44
|
-
const who = origin.name ? `${origin.name} (${origin.sessionId
|
|
45
|
+
const who = origin.name ? `${origin.name} (${peerShortId(origin.sessionId)})` : peerShortId(origin.sessionId)
|
|
45
46
|
return `message from ${origin.engine ? `${origin.engine} session ` : 'session '}${who}`
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -191,6 +192,9 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
191
192
|
const busy = status === 'running' || status === 'pending'
|
|
192
193
|
const isError = status === 'failed' || item.result?.isError === true
|
|
193
194
|
const pulse = usePulse(busy)
|
|
195
|
+
const now = useTicker(busy && item.ts !== undefined)
|
|
196
|
+
const elapsed = busy ? elapsedLabel(item.ts, now) : undefined
|
|
197
|
+
const tail = liveTailLines(item)
|
|
194
198
|
|
|
195
199
|
const text = item.result?.text ?? ''
|
|
196
200
|
const lines = text.trimEnd().split('\n')
|
|
@@ -214,6 +218,7 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
214
218
|
<WithActions
|
|
215
219
|
actions={
|
|
216
220
|
<>
|
|
221
|
+
{canBackground(item) ? <BackgroundAction toolUseId={item.id} /> : null}
|
|
217
222
|
<BookmarkAction id={item.id} />
|
|
218
223
|
{copyable ? <CopyAction text={copyable} label="Copy" /> : null}
|
|
219
224
|
</>
|
|
@@ -225,10 +230,18 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
225
230
|
{title ?? item.name}
|
|
226
231
|
</Ink>
|
|
227
232
|
<Ink tone="dim">({todos ? todos.summary : toolInputPreview(item.input)})</Ink>
|
|
233
|
+
{elapsed ? (
|
|
234
|
+
<Ink tone="faint">
|
|
235
|
+
{' '}
|
|
236
|
+
·{'\u00a0'}
|
|
237
|
+
{elapsed}
|
|
238
|
+
</Ink>
|
|
239
|
+
) : null}
|
|
228
240
|
{title && open ? <Ink tone="faint"> · {item.name}</Ink> : null}
|
|
229
241
|
{item.backend && item.backend !== 'server' ? <Ink tone="faint"> · {item.backend}</Ink> : null}
|
|
230
242
|
</Row>
|
|
231
243
|
</Pressable>
|
|
244
|
+
<LiveTail lines={tail} />
|
|
232
245
|
{hostPath !== undefined ? <TerminalHostImage path={hostPath} /> : null}
|
|
233
246
|
{item.result?.images?.map((image) => (
|
|
234
247
|
<TerminalImage key={image.partIndex} toolUseId={item.id} name={resultImageName(item.name, image)} image={image} />
|
|
@@ -373,14 +386,25 @@ export function ToolRunRow({ items }: { items: ToolCallItem[] }) {
|
|
|
373
386
|
})
|
|
374
387
|
const failed = runFailed(items)
|
|
375
388
|
const pulse = usePulse(busy)
|
|
389
|
+
const startedAt = runStartedAt(items)
|
|
390
|
+
const now = useTicker(startedAt !== undefined)
|
|
391
|
+
const elapsed = elapsedLabel(startedAt, now)
|
|
376
392
|
|
|
377
393
|
return (
|
|
378
394
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
379
395
|
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
380
396
|
<Row glyph={busy ? pulse : undefined} glyphTone={busy ? 'mark' : undefined} tone={failed ? 'red' : 'dim'}>
|
|
381
397
|
{busy ? <span className="term-shimmer">{runSummary(items, busy)}</span> : runSummary(items, busy)}
|
|
398
|
+
{elapsed ? (
|
|
399
|
+
<Ink tone="faint">
|
|
400
|
+
{' '}
|
|
401
|
+
·{'\u00a0'}
|
|
402
|
+
{elapsed}
|
|
403
|
+
</Ink>
|
|
404
|
+
) : null}
|
|
382
405
|
</Row>
|
|
383
406
|
</Pressable>
|
|
407
|
+
{open ? null : <LiveTail lines={runTailLines(items)} />}
|
|
384
408
|
{open ? (
|
|
385
409
|
<div>
|
|
386
410
|
{items.map((item, index) => (
|
|
@@ -511,6 +535,14 @@ export function FileRow({ item, href }: { item: Extract<TranscriptItem, { kind:
|
|
|
511
535
|
)
|
|
512
536
|
}
|
|
513
537
|
|
|
538
|
+
function LiveTail({ lines }: { lines: string[] }) {
|
|
539
|
+
return lines.map((line, index) => (
|
|
540
|
+
<Row key={index} indent={1} columns={3} glyph={index === 0 ? '⎿' : undefined} tone="faint">
|
|
541
|
+
{line || ' '}
|
|
542
|
+
</Row>
|
|
543
|
+
))
|
|
544
|
+
}
|
|
545
|
+
|
|
514
546
|
export function useTicker(on: boolean): number {
|
|
515
547
|
const [now, setNow] = useState(() => Date.now())
|
|
516
548
|
useEffect(() => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { formatDuration } from '../../lib/format.ts'
|
|
2
|
+
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
|
+
import type { ToolCallItem } from './blocks.ts'
|
|
4
|
+
|
|
5
|
+
export const LIVE_TAIL_LINES = 5
|
|
6
|
+
export const ELAPSED_AFTER_MS = 5000
|
|
7
|
+
export const SEND_NOW_AFTER_MS = 5000
|
|
8
|
+
// The widest label the header grows by while busy, so a height estimate never undercounts a wrap.
|
|
9
|
+
export const ELAPSED_WIDEST = ' ·\u00a059m\u00a059s'
|
|
10
|
+
|
|
11
|
+
const BACKGROUNDABLE = new Set(['Bash', 'Task', 'Agent'])
|
|
12
|
+
|
|
13
|
+
export function toolBusy(item: ToolCallItem): boolean {
|
|
14
|
+
const status = item.status ?? (item.result === undefined ? 'running' : 'settled')
|
|
15
|
+
return status === 'running' || status === 'pending'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function liveTailLines(item: ToolCallItem): string[] {
|
|
19
|
+
if (!item.liveTail || !toolBusy(item)) {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
return item.liveTail.split('\n').slice(-LIVE_TAIL_LINES)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function elapsedLabel(startedAt: number | undefined, now: number): string | undefined {
|
|
26
|
+
if (startedAt === undefined || now - startedAt < ELAPSED_AFTER_MS) {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
return formatDuration(Math.floor((now - startedAt) / 1000) * 1000).replace(' ', '\u00a0')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function runStartedAt(items: readonly ToolCallItem[]): number | undefined {
|
|
33
|
+
let earliest: number | undefined
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
if (toolBusy(item) && item.ts !== undefined && (earliest === undefined || item.ts < earliest)) {
|
|
36
|
+
earliest = item.ts
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return earliest
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function canBackground(item: ToolCallItem): boolean {
|
|
43
|
+
return BACKGROUNDABLE.has(item.name) && item.parentToolUseId == null && toolBusy(item)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runTailLines(items: readonly ToolCallItem[]): string[] {
|
|
47
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
48
|
+
const lines = liveTailLines(items[index]!)
|
|
49
|
+
if (lines.length > 0) {
|
|
50
|
+
return lines
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Walks back only through the current turn: a running call is never older than the last prompt.
|
|
57
|
+
// The earliest start among them, or 0 when a call carries no `ts`.
|
|
58
|
+
export function backgroundableSince(items: readonly TranscriptItem[]): number | undefined {
|
|
59
|
+
let since: number | undefined
|
|
60
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
61
|
+
const item = items[index]!
|
|
62
|
+
if (item.kind === 'user' && item.parentToolUseId == null) {
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
if (item.kind === 'tool_call' && canBackground(item)) {
|
|
66
|
+
const ts = item.ts ?? 0
|
|
67
|
+
since = since === undefined ? ts : Math.min(since, ts)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return since
|
|
71
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isPeerSendTool, peerDeliveredTo, type MessageOrigin } from '@workerdeck/protocol'
|
|
1
|
+
import { isPeerSendTool, peerDeliveredTo, peerShortId, type MessageOrigin } from '@workerdeck/protocol'
|
|
2
2
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
3
|
import { toolInputPreview } from '../../lib/format.ts'
|
|
4
4
|
import { isShellTool } from '../../lib/tool-icon.ts'
|
|
@@ -12,7 +12,10 @@ export function isPeerSend(item: TranscriptItem): boolean {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function peerName(origin: MessageOrigin): string {
|
|
15
|
-
|
|
15
|
+
if (!origin.name) {
|
|
16
|
+
return peerShortId(origin.sessionId)
|
|
17
|
+
}
|
|
18
|
+
return origin.hostId ? `${origin.name}@${origin.hostId}` : origin.name
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
// Every peer this transcript has named, by session id. A conversation names its peers twice over: an
|
|
@@ -48,7 +51,7 @@ export function peerSendTarget(item: ToolCallItem, names?: ReadonlyMap<string, s
|
|
|
48
51
|
if (sessionId === undefined) {
|
|
49
52
|
return 'peer'
|
|
50
53
|
}
|
|
51
|
-
return delivered?.name ?? names?.get(sessionId) ?? sessionId
|
|
54
|
+
return delivered?.name ?? names?.get(sessionId) ?? peerShortId(sessionId)
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export function peerSendText(item: ToolCallItem): string {
|
package/src/index.ts
CHANGED
|
@@ -123,6 +123,7 @@ export {
|
|
|
123
123
|
type ComposerRow,
|
|
124
124
|
} from './components/agent/composer-commands.ts'
|
|
125
125
|
export { ModelSelect, type ModelSelectProps } from './components/agent/ModelSelect.tsx'
|
|
126
|
+
export { EffortSelect, effortChoices, type EffortSelectProps } from './components/agent/EffortSelect.tsx'
|
|
126
127
|
export {
|
|
127
128
|
PERMISSION_MODES,
|
|
128
129
|
PermissionModeSelect,
|
|
@@ -156,7 +157,13 @@ export {
|
|
|
156
157
|
export { SkillsDialog, type SkillsDialogProps } from './components/agent/SkillsDialog.tsx'
|
|
157
158
|
export { HostFilesDialog, type HostFilesDialogProps } from './components/agent/HostFilesDialog.tsx'
|
|
158
159
|
export { SessionList, SessionListItem, type SessionListItemProps, type SessionListProps } from './components/agent/SessionList.tsx'
|
|
159
|
-
export {
|
|
160
|
+
export {
|
|
161
|
+
SessionBrowser,
|
|
162
|
+
SessionSearch,
|
|
163
|
+
rowShapeClass,
|
|
164
|
+
type GroupTarget,
|
|
165
|
+
type SessionBrowserProps,
|
|
166
|
+
} from './components/agent/SessionBrowser.tsx'
|
|
160
167
|
export {
|
|
161
168
|
STEP_DISPLAY_OPTIONS,
|
|
162
169
|
SessionFilters,
|
package/src/styles/terminal.css
CHANGED
|
@@ -483,9 +483,13 @@
|
|
|
483
483
|
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:not(.absolute) {
|
|
484
484
|
position: relative;
|
|
485
485
|
}
|
|
486
|
-
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:
|
|
486
|
+
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:focus-within {
|
|
487
487
|
z-index: 1;
|
|
488
488
|
}
|
|
489
|
+
/* Above a focused row too: the row just clicked open keeps focus and would cover the bar beneath the hovered one. */
|
|
490
|
+
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:hover {
|
|
491
|
+
z-index: 2;
|
|
492
|
+
}
|
|
489
493
|
@media (hover: none) {
|
|
490
494
|
:is([data-terminal], [data-cards]) .term-actions {
|
|
491
495
|
opacity: 1;
|
|
@@ -785,9 +789,12 @@ button.term-link {
|
|
|
785
789
|
* it is the same `--bg`, and the row it covers is its own content.
|
|
786
790
|
*/
|
|
787
791
|
[data-terminal] [data-slot='transcript-rows'] > [data-sticky-lane] {
|
|
788
|
-
z-index: 3;
|
|
789
792
|
pointer-events: none;
|
|
790
793
|
}
|
|
794
|
+
/* Raised only while pinned: a raised idle lane covered the hover bar of the row above its prompt. */
|
|
795
|
+
[data-terminal] [data-slot='transcript-rows'] > [data-sticky-lane]:has(> [data-sticky-headlane] > [data-stuck]) {
|
|
796
|
+
z-index: 3;
|
|
797
|
+
}
|
|
791
798
|
|
|
792
799
|
/* The prompt itself is *inside* the lane, not beneath it, so the lane's `none`
|
|
793
800
|
reached it too and made a user message the one block in the transcript the
|
package/src/styles/theme.css
CHANGED
|
@@ -743,9 +743,11 @@
|
|
|
743
743
|
* row's first line, so it has no in-flow y to match, and the 1st prompt (no
|
|
744
744
|
* gap) and every later one (pt-4/pt-2) share these rules unchanged. */
|
|
745
745
|
[data-slot='transcript-rows'] > [data-sticky-lane='cards'] {
|
|
746
|
-
z-index: 3;
|
|
747
746
|
pointer-events: none;
|
|
748
747
|
}
|
|
748
|
+
[data-slot='transcript-rows'] > [data-sticky-lane='cards']:has(> [data-sticky-headlane] > [data-stuck]) {
|
|
749
|
+
z-index: 3;
|
|
750
|
+
}
|
|
749
751
|
|
|
750
752
|
/* The head's own strip: the full lane, out of flow - see terminal.css for why
|
|
751
753
|
sticky confinement wants the border box, not a cancelled margin box. */
|