@workerdeck/ui 1.1.0 → 1.3.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 +4 -0
- package/build/{SessionPanel-yNO60Tzw.d.mts → SessionPanel-BSwUJDwg.d.mts} +3 -8
- package/build/{SessionPanel-I5QSLLb0.mjs → SessionPanel-DQp2oStr.mjs} +409 -267
- package/build/SessionPanel-DQp2oStr.mjs.map +1 -0
- package/build/{format-C_lpFq0l.d.mts → format-TVRJzYUj.d.mts} +3 -2
- package/build/format.d.mts +2 -7
- package/build/format.mjs +2 -2
- package/build/index.d.mts +34 -9
- package/build/index.mjs +38 -12
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +11 -0
- package/build/{status-Dm9INVTt.mjs → status-C4TAgVSc.mjs} +3 -7
- package/build/status-C4TAgVSc.mjs.map +1 -0
- package/build/workspace.d.mts +3 -2
- package/build/workspace.mjs +4 -3
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/Composer.tsx +2 -4
- package/src/components/agent/ContextDialog.tsx +17 -2
- package/src/components/agent/ContextRing.tsx +36 -7
- package/src/components/agent/EntryIcon.tsx +2 -4
- package/src/components/agent/McpDialog.tsx +2 -0
- package/src/components/agent/SessionItem.tsx +1 -1
- package/src/components/agent/SessionPanel.tsx +198 -186
- package/src/components/agent/SessionWorkspace.tsx +3 -0
- package/src/components/agent/StatusBar.tsx +12 -4
- package/src/components/agent/ToolCallCard.tsx +6 -1
- package/src/components/agent/TranscriptItemView.tsx +15 -2
- package/src/components/agent/TranscriptRows.tsx +4 -6
- package/src/components/agent/held-sends.tsx +73 -0
- package/src/components/agent/tool-titles.tsx +12 -0
- package/src/components/agent/use-subagent-frame.ts +8 -10
- package/src/components/agent/use-transcript-jumps.ts +7 -9
- package/src/components/terminal/TerminalTranscript.tsx +15 -1
- package/src/components/terminal/height.ts +4 -1
- package/src/components/terminal/items.tsx +14 -3
- package/src/index.ts +2 -0
- package/src/lib/context-note.ts +24 -0
- package/src/lib/css.ts +3 -5
- package/src/lib/format.ts +3 -0
- package/src/lib/status.ts +3 -5
- package/build/SessionPanel-I5QSLLb0.mjs.map +0 -1
- package/build/status-Dm9INVTt.mjs.map +0 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Clock } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export type HeldSend = { text: string; attachmentIds: string[] }
|
|
5
|
+
|
|
6
|
+
export type HeldSends = {
|
|
7
|
+
held: HeldSend[]
|
|
8
|
+
submit: (text: string, attachmentIds: string[]) => void
|
|
9
|
+
flush: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Catch-up mode is the engine's behaviour, not ours: a message sent mid-turn is folded into the
|
|
13
|
+
// running turn. Holding it here is the only way to turn that off, and it is a client preference,
|
|
14
|
+
// so nothing about it travels on the wire.
|
|
15
|
+
export function useHeldSends(options: { hold: boolean; busy: boolean; send: (text: string, attachmentIds: string[]) => void }): HeldSends {
|
|
16
|
+
const [held, setHeld] = useState<HeldSend[]>([])
|
|
17
|
+
// The queue is a ref and the state is its shadow: flushing sends, and a state updater that
|
|
18
|
+
// sends would fire twice under StrictMode.
|
|
19
|
+
const queue = useRef<HeldSend[]>([])
|
|
20
|
+
const sendRef = useRef(options.send)
|
|
21
|
+
sendRef.current = options.send
|
|
22
|
+
|
|
23
|
+
const flush = useCallback(() => {
|
|
24
|
+
const queued = queue.current
|
|
25
|
+
if (queued.length === 0) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
queue.current = []
|
|
29
|
+
setHeld([])
|
|
30
|
+
for (const item of queued) {
|
|
31
|
+
sendRef.current(item.text, item.attachmentIds)
|
|
32
|
+
}
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!options.busy) {
|
|
37
|
+
flush()
|
|
38
|
+
}
|
|
39
|
+
}, [options.busy, flush])
|
|
40
|
+
|
|
41
|
+
const submit = useCallback(
|
|
42
|
+
(text: string, attachmentIds: string[]) => {
|
|
43
|
+
if (!options.hold || !options.busy) {
|
|
44
|
+
sendRef.current(text, attachmentIds)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
queue.current = [...queue.current, { text, attachmentIds }]
|
|
48
|
+
setHeld(queue.current)
|
|
49
|
+
},
|
|
50
|
+
[options.hold, options.busy],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return { held, submit, flush }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function HeldSendsBar({ held, onSendNow }: { held: HeldSend[]; onSendNow: () => void }) {
|
|
57
|
+
if (held.length === 0) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
return (
|
|
61
|
+
<div className="px-3 pt-2">
|
|
62
|
+
<div className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 rounded-md border border-border bg-surface px-3 py-1.5 text-label text-fg-3">
|
|
63
|
+
<Clock className="size-3.5 shrink-0" />
|
|
64
|
+
<span className="min-w-0 flex-1 truncate">
|
|
65
|
+
{held.length === 1 ? '1 message' : `${held.length} messages`} waiting for this turn to end — {held[held.length - 1]!.text}
|
|
66
|
+
</span>
|
|
67
|
+
<button type="button" onClick={onSendNow} className="shrink-0 underline-offset-2 hover:underline">
|
|
68
|
+
Send now
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
2
|
+
import { toolTitle } from '@workerdeck/protocol'
|
|
3
|
+
|
|
4
|
+
const TitlesContext = createContext<Record<string, string> | undefined>(undefined)
|
|
5
|
+
|
|
6
|
+
export function ToolTitleProvider({ value, children }: { value: Record<string, string> | undefined; children: ReactNode }) {
|
|
7
|
+
return <TitlesContext.Provider value={value}>{children}</TitlesContext.Provider>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useToolTitle(name: string): string | undefined {
|
|
11
|
+
return toolTitle(name, useContext(TitlesContext))
|
|
12
|
+
}
|
|
@@ -3,21 +3,19 @@ import { subagentLabel, type SessionInfo } from '@workerdeck/protocol'
|
|
|
3
3
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
4
4
|
import { subagentItems, type ToolCallItem } from '../terminal/blocks.ts'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* report is deduped through a ref, so an echo of our own report is inert on arrival.
|
|
13
|
-
*/
|
|
6
|
+
// The sub-agent frame machine: which agent frame is on screen, how it is entered (a host
|
|
7
|
+
// `openSubagent` request or a Task row in the transcript), how it is left (Escape, the strip's
|
|
8
|
+
// Back, or the host withdrawing), and what the transcript reveals on the way out. The frame
|
|
9
|
+
// round-trips through the host's URL — the anti-loop rules live in GOTCHAS ("The sub-agent
|
|
10
|
+
// frame round-trips through the URL"); here they mean: entry keys on the nonce alone, and the
|
|
11
|
+
// report is deduped through a ref, so an echo of our own report is inert on arrival.
|
|
14
12
|
export function useSubagentFrame(options: {
|
|
15
13
|
sessionId: string | undefined
|
|
16
14
|
items: TranscriptItem[]
|
|
17
15
|
session: SessionInfo | undefined
|
|
18
|
-
|
|
16
|
+
// Host request to reveal a transcript row; entering a frame must not eat a pending one.
|
|
19
17
|
reveal?: { toolUseId: string; nonce: number }
|
|
20
|
-
|
|
18
|
+
// Host request to enter a frame; `toolUseId: undefined` withdraws it (Back/Forward).
|
|
21
19
|
openSubagent?: { toolUseId: string; nonce: number }
|
|
22
20
|
onSubagentChange?: (toolUseId: string | undefined) => void
|
|
23
21
|
}) {
|
|
@@ -7,19 +7,17 @@ import { gapBefore, type TranscriptRow } from './transcript-rows.ts'
|
|
|
7
7
|
const AIM_PASSES = 4
|
|
8
8
|
const AIM_SETTLE_MS = 50
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// How long a send's re-pin outlasts the click — long enough to eat a trackpad's momentum tail.
|
|
11
11
|
export const REPIN_HOLD_MS = 750
|
|
12
12
|
|
|
13
13
|
type RepinTarget = Pick<ReturnType<typeof useStickToBottomContext>, 'scrollToBottom' | 'state'>
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* (`stopScroll`, drag-selection) still wins immediately.
|
|
22
|
-
*/
|
|
15
|
+
// The send re-pin: a held, escape-proof pin, not one `scrollToBottom('instant')` — a trackpad's
|
|
16
|
+
// trailing momentum tick reads as escape intent and aborts the one-shot before its first frame
|
|
17
|
+
// (GOTCHAS "The send re-pin is a held pin"). Every line is load-bearing: clear the stale escape
|
|
18
|
+
// flag, hold through the momentum tail, seed the `ignoreEscapes` record the library only
|
|
19
|
+
// installs one rAF too late, and press the first scroll synchronously. Deliberate detach
|
|
20
|
+
// (`stopScroll`, drag-selection) still wins immediately.
|
|
23
21
|
export function repinToBottom(stick: RepinTarget): void {
|
|
24
22
|
const { state } = stick
|
|
25
23
|
state.escapedFromLock = false
|
|
@@ -3,7 +3,18 @@ import type { TranscriptItem, TranscriptState } from '@workerdeck/react'
|
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
4
|
import type { TerminalAffordances } from './affordances.tsx'
|
|
5
5
|
import { OpenSubagentAction, WithActions } from './affordances.tsx'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AssistantRow,
|
|
8
|
+
CompactionRow,
|
|
9
|
+
FileRow,
|
|
10
|
+
NoticeRow,
|
|
11
|
+
ToolRunRow,
|
|
12
|
+
ThinkingRow,
|
|
13
|
+
ToolRow,
|
|
14
|
+
TurnResultRow,
|
|
15
|
+
UserRow,
|
|
16
|
+
WorkingRow,
|
|
17
|
+
} from './items.tsx'
|
|
7
18
|
import { blockNeedsBlank, taskChildItems, terminalBlocks, type TaskBlock } from './blocks.ts'
|
|
8
19
|
import { usePulse } from '../agent/pulse.tsx'
|
|
9
20
|
import { Pressable, useRevealOnOpen } from './press.tsx'
|
|
@@ -41,6 +52,9 @@ export function TerminalItemView({ item, fileUrl }: { item: TranscriptItem; file
|
|
|
41
52
|
case 'notice': {
|
|
42
53
|
return <NoticeRow item={item} />
|
|
43
54
|
}
|
|
55
|
+
case 'compaction': {
|
|
56
|
+
return <CompactionRow />
|
|
57
|
+
}
|
|
44
58
|
case 'file_delivered': {
|
|
45
59
|
return <FileRow item={item} href={fileUrl?.(item.path)} />
|
|
46
60
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FilePatch, PatchHunk } from '@workerdeck/protocol'
|
|
2
2
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
|
-
import { formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
3
|
+
import { COMPACTION_TEXT, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
4
4
|
import { taskChildItems, type TerminalBlock, type ToolCallItem } from './blocks.ts'
|
|
5
5
|
import { IMAGE_BOX_LINES } from './image-box.ts'
|
|
6
6
|
import { collapsedResult } from './result-preview.ts'
|
|
@@ -622,6 +622,9 @@ export function itemHeight(item: TranscriptItem, m: CellMetrics): ComputedHeight
|
|
|
622
622
|
case 'notice': {
|
|
623
623
|
return rowH(item.text, m, { extraPx })
|
|
624
624
|
}
|
|
625
|
+
case 'compaction': {
|
|
626
|
+
return rowH(COMPACTION_TEXT, m, { extraPx })
|
|
627
|
+
}
|
|
625
628
|
case 'file_delivered': {
|
|
626
629
|
const text = `${item.path} · ${formatBytes(item.bytes)}` + (item.description ? ` · ${item.description}` : '')
|
|
627
630
|
return rowH(text, m, { extraPx })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
|
-
import { formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
3
|
+
import { COMPACTION_TEXT, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
4
4
|
import { isMutatingTool } from '../../lib/tool-icon.ts'
|
|
5
5
|
import { usePulse } from '../agent/pulse.tsx'
|
|
6
6
|
import { BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
@@ -10,6 +10,7 @@ import { Pressable, useRevealOnOpen } from './press.tsx'
|
|
|
10
10
|
import { IMAGE_BOX_LINES, IMAGE_UNAVAILABLE, imagePlaceholder } from './image-box.ts'
|
|
11
11
|
import { collapsedResult } from './result-preview.ts'
|
|
12
12
|
import { useToolResultFetcher } from '../agent/tool-result-fetch.tsx'
|
|
13
|
+
import { useToolTitle } from '../agent/tool-titles.tsx'
|
|
13
14
|
import { useToolResultImageSrc } from '../agent/tool-result-image.tsx'
|
|
14
15
|
import { runFailed, runSummary } from './tool-run.ts'
|
|
15
16
|
import { todoLine, todoPreview, type TodoPreview, type TodoStatus } from './todos.ts'
|
|
@@ -112,6 +113,7 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
112
113
|
const command = (item.input as { command?: unknown } | null)?.command
|
|
113
114
|
const copyable = typeof command === 'string' ? command : text
|
|
114
115
|
const todos = todoPreview(item.name, item.input)
|
|
116
|
+
const title = useToolTitle(item.name)
|
|
115
117
|
|
|
116
118
|
return (
|
|
117
119
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
@@ -124,11 +126,12 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
124
126
|
}
|
|
125
127
|
>
|
|
126
128
|
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
127
|
-
<Row glyph={busy ? pulse : '●'} glyphTone={tone} tone="fg">
|
|
129
|
+
<Row glyph={busy ? pulse : '●'} glyphTone={tone} tone="fg" title={title ? item.name : undefined}>
|
|
128
130
|
<Ink bold tone="bright">
|
|
129
|
-
{item.name}
|
|
131
|
+
{title ?? item.name}
|
|
130
132
|
</Ink>
|
|
131
133
|
<Ink tone="dim">({todos ? todos.summary : toolInputPreview(item.input)})</Ink>
|
|
134
|
+
{title && open ? <Ink tone="faint"> · {item.name}</Ink> : null}
|
|
132
135
|
{item.backend && item.backend !== 'server' ? <Ink tone="faint"> · {item.backend}</Ink> : null}
|
|
133
136
|
</Row>
|
|
134
137
|
</Pressable>
|
|
@@ -278,6 +281,14 @@ export function TurnResultRow({ item }: { item: Extract<TranscriptItem, { kind:
|
|
|
278
281
|
)
|
|
279
282
|
}
|
|
280
283
|
|
|
284
|
+
export function CompactionRow() {
|
|
285
|
+
return (
|
|
286
|
+
<Row glyph="≡" glyphTone="yellow" tone="faint">
|
|
287
|
+
{COMPACTION_TEXT}
|
|
288
|
+
</Row>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
281
292
|
export function NoticeRow({ item }: { item: Extract<TranscriptItem, { kind: 'notice' }> }) {
|
|
282
293
|
const error = item.level === 'error'
|
|
283
294
|
return (
|
package/src/index.ts
CHANGED
|
@@ -74,6 +74,8 @@ export { Response, type ResponseProps } from './components/agent/Response.tsx'
|
|
|
74
74
|
export { Reasoning, type ReasoningProps } from './components/agent/Reasoning.tsx'
|
|
75
75
|
export { Loader } from './components/agent/Loader.tsx'
|
|
76
76
|
export { ToolCallCard, type ToolCallCardProps, type ToolCallItem } from './components/agent/ToolCallCard.tsx'
|
|
77
|
+
export { ToolTitleProvider, useToolTitle } from './components/agent/tool-titles.tsx'
|
|
78
|
+
export { HeldSendsBar, useHeldSends, type HeldSend, type HeldSends } from './components/agent/held-sends.tsx'
|
|
77
79
|
export { FileCard, type FileCardProps, type FileDeliveredItem } from './components/agent/FileCard.tsx'
|
|
78
80
|
export { PermissionPrompt, type PermissionPromptProps } from './components/agent/PermissionPrompt.tsx'
|
|
79
81
|
export {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProfileEngine } from '@workerdeck/protocol'
|
|
2
|
+
|
|
3
|
+
export interface ContextNote {
|
|
4
|
+
summary: string
|
|
5
|
+
hint: string
|
|
6
|
+
detail: string[]
|
|
7
|
+
caveat: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CODEX_CONTEXT_NOTE: ContextNote = {
|
|
11
|
+
summary:
|
|
12
|
+
'Codex budgets 272K input by default and reports 95% of it, so a 1M-class model reads ~258k. That is codex’s price tier, not the model’s ceiling — and it is the number auto-compaction fires on, so the meter is honest.',
|
|
13
|
+
hint: 'Raising it is yours to set: model_context_window in ~/.codex/config.toml — and watch for auto-compaction failing afterwards.',
|
|
14
|
+
detail: [
|
|
15
|
+
'To raise it, set model_context_window in ~/.codex/config.toml, up to 872000 (reported as 828400). Codex clamps to its own max_context_window, itself below OpenAI’s stated 922,000 max input.',
|
|
16
|
+
'Above 272K input, a request bills at 2x input / 1.5x output for the whole request. WorkerDeck never writes this setting — it is a cost decision on your account.',
|
|
17
|
+
],
|
|
18
|
+
caveat:
|
|
19
|
+
'Raise it and watch for auto-compaction failing: openai/codex #16068 reports it stops firing after the first overflow. Unverified here on 0.149.0.',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function contextNote(engine: ProfileEngine | undefined): ContextNote | undefined {
|
|
23
|
+
return (engine ?? 'claude') === 'codex' ? CODEX_CONTEXT_NOTE : undefined
|
|
24
|
+
}
|
package/src/lib/css.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* colour rather than reaching the DOM.
|
|
5
|
-
*/
|
|
1
|
+
// Pass a colour through only if this engine can actually parse it. Category colours arrive
|
|
2
|
+
// from the CLI as untrusted strings, and an unparseable one must fall back to the token
|
|
3
|
+
// colour rather than reaching the DOM.
|
|
6
4
|
export function cssColor(color: string): string | undefined {
|
|
7
5
|
return typeof CSS !== 'undefined' && CSS.supports('color', color) ? color : undefined
|
|
8
6
|
}
|
package/src/lib/format.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Here rather than beside the row: height.ts must measure the string items.tsx draws, and cannot import a .tsx.
|
|
2
|
+
export const COMPACTION_TEXT = 'context compacted · earlier turns summarised to fit the window'
|
|
3
|
+
|
|
1
4
|
export function formatCost(usd: number | undefined): string {
|
|
2
5
|
if (usd === undefined || Number.isNaN(usd)) {
|
|
3
6
|
return '—'
|
package/src/lib/status.ts
CHANGED
|
@@ -133,11 +133,9 @@ export function meterColorClass(pct: number | undefined): string {
|
|
|
133
133
|
return severity === 'error' ? 'text-danger' : severity === 'warning' ? 'text-warning' : 'text-fg-3'
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
* turns at 70/90 where the text severity turns at 80/95.
|
|
140
|
-
*/
|
|
136
|
+
// The fill colour for a meter bar. Deliberately a different ramp from
|
|
137
|
+
// {@link meterColorClass}: a bar reads as alarming later than a number does, so the tint
|
|
138
|
+
// turns at 70/90 where the text severity turns at 80/95.
|
|
141
139
|
export function meterTintClass(pct: number): string {
|
|
142
140
|
return pct >= 90 ? 'bg-danger' : pct >= 70 ? 'bg-warning' : 'bg-accent'
|
|
143
141
|
}
|