@workerdeck/ui 0.15.0 → 0.16.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 +53 -0
- package/build/{SessionPanel-J2U8v88q.d.mts → SessionPanel-B9CHoq8x.d.mts} +158 -21
- package/build/{SessionPanel-DI1NO4l8.mjs → SessionPanel-DII9MmQ8.mjs} +4254 -1661
- package/build/SessionPanel-DII9MmQ8.mjs.map +1 -0
- package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
- package/build/format.d.mts +39 -4
- package/build/format.mjs +2 -118
- package/build/index.d.mts +442 -45
- package/build/index.mjs +105 -2
- package/build/index.mjs.map +1 -1
- package/build/status-Ydzi7n6j.mjs +143 -0
- package/build/status-Ydzi7n6j.mjs.map +1 -0
- package/build/workspace.d.mts +9 -1
- package/build/workspace.mjs +108 -5
- package/build/workspace.mjs.map +1 -1
- package/package.json +14 -7
- package/src/components/agent/Composer.tsx +189 -89
- package/src/components/agent/Conversation.tsx +12 -12
- package/src/components/agent/FileCard.tsx +0 -26
- package/src/components/agent/FileTree.tsx +9 -8
- package/src/components/agent/Loader.tsx +22 -72
- package/src/components/agent/Message.tsx +11 -46
- package/src/components/agent/PermissionPrompt.tsx +0 -92
- package/src/components/agent/QuestionPrompt.tsx +0 -122
- package/src/components/agent/Reasoning.tsx +5 -19
- package/src/components/agent/Response.tsx +1 -132
- package/src/components/agent/SessionPanel.tsx +224 -28
- package/src/components/agent/SessionWorkspace.tsx +29 -0
- package/src/components/agent/StatusBar.tsx +20 -4
- package/src/components/agent/ToolCallCard.tsx +17 -111
- package/src/components/agent/Transcript.tsx +710 -203
- package/src/components/agent/UsageDialog.tsx +20 -106
- package/src/components/agent/UsageMeters.tsx +133 -0
- package/src/components/agent/pulse.tsx +3 -2
- package/src/components/agent/transcript-rows.ts +82 -0
- package/src/components/agent/transcript-variant.tsx +29 -51
- package/src/components/agent/use-height-epoch.ts +60 -0
- package/src/components/agent/use-path-links.ts +147 -0
- package/src/components/agent/use-transcript-jumps.ts +190 -0
- package/src/components/prompt-area/cursor-helpers.ts +65 -0
- package/src/components/prompt-area/use-prompt-area.ts +16 -10
- package/src/components/terminal/PermissionPrompt.tsx +119 -0
- package/src/components/terminal/QuestionPrompt.tsx +322 -0
- package/src/components/terminal/StatusLine.tsx +159 -0
- package/src/components/terminal/TerminalTranscript.tsx +147 -0
- package/src/components/terminal/affordances.tsx +118 -0
- package/src/components/terminal/diff.tsx +130 -0
- package/src/components/terminal/height.ts +727 -0
- package/src/components/terminal/items.tsx +449 -0
- package/src/components/terminal/markdown.tsx +191 -0
- package/src/components/terminal/press.tsx +120 -0
- package/src/components/terminal/prompt.tsx +343 -0
- package/src/components/terminal/result-preview.ts +72 -0
- package/src/components/terminal/row.tsx +132 -0
- package/src/components/terminal/scrubber.tsx +663 -0
- package/src/components/terminal/surface.tsx +80 -0
- package/src/components/terminal/tool-run.ts +91 -0
- package/src/index.ts +34 -0
- package/src/lib/status.ts +59 -3
- package/src/lib/tool-icon.ts +14 -0
- package/src/styles/terminal.css +1011 -0
- package/src/styles/theme.css +41 -0
- package/build/SessionPanel-DI1NO4l8.mjs.map +0 -1
- package/build/format.mjs.map +0 -1
- package/src/components/agent/line-prompt.tsx +0 -249
|
@@ -8,12 +8,20 @@ import {
|
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from 'react'
|
|
10
10
|
import type { WorkerDeckClient } from '@workerdeck/client'
|
|
11
|
-
import { PROTOCOL_VERSION, type ModelOption, type PermissionMode } from '@workerdeck/protocol'
|
|
12
11
|
import {
|
|
13
|
-
|
|
12
|
+
PROTOCOL_VERSION,
|
|
13
|
+
mergeUsage,
|
|
14
|
+
orderUsageWindows,
|
|
15
|
+
usageInfos,
|
|
16
|
+
type ModelOption,
|
|
17
|
+
type PermissionMode,
|
|
18
|
+
type RateLimitInfo,
|
|
19
|
+
} from '@workerdeck/protocol'
|
|
20
|
+
import {
|
|
14
21
|
useAttachments,
|
|
15
22
|
useClaudeSession,
|
|
16
23
|
useHostFileSearch,
|
|
24
|
+
useProfileUsage,
|
|
17
25
|
useToolCallHost,
|
|
18
26
|
type ConnectionState,
|
|
19
27
|
type ProducedFileRef,
|
|
@@ -47,6 +55,11 @@ import {
|
|
|
47
55
|
} from './PermissionModeSelect.tsx'
|
|
48
56
|
import { PermissionPrompt } from './PermissionPrompt.tsx'
|
|
49
57
|
import { QuestionPrompt, parseUserQuestions } from './QuestionPrompt.tsx'
|
|
58
|
+
import { TerminalPermissionPrompt } from '../terminal/PermissionPrompt.tsx'
|
|
59
|
+
import { TerminalQuestionPrompt } from '../terminal/QuestionPrompt.tsx'
|
|
60
|
+
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
61
|
+
import type { TerminalAffordances } from '../terminal/affordances.tsx'
|
|
62
|
+
|
|
50
63
|
import { SessionInfoDialog } from './SessionInfoDialog.tsx'
|
|
51
64
|
import { StatusBar } from './StatusBar.tsx'
|
|
52
65
|
import { Transcript } from './Transcript.tsx'
|
|
@@ -59,6 +72,59 @@ import {
|
|
|
59
72
|
} from './transcript-variant.tsx'
|
|
60
73
|
import { UsageDialog } from './UsageDialog.tsx'
|
|
61
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The box the pending prompts sit in.
|
|
77
|
+
*
|
|
78
|
+
* Under the terminal theme they are rows of the same run as the transcript, so
|
|
79
|
+
* they need that theme's cell — and its `--term-bleed`, so an approval's diff
|
|
80
|
+
* bands reach the same edges the transcript's do. Every other variant keeps the
|
|
81
|
+
* centred content column the panel uses everywhere else.
|
|
82
|
+
*/
|
|
83
|
+
function PromptSurface({
|
|
84
|
+
terminal,
|
|
85
|
+
metrics,
|
|
86
|
+
affordances,
|
|
87
|
+
children,
|
|
88
|
+
}: {
|
|
89
|
+
terminal: boolean
|
|
90
|
+
metrics?: TerminalMetrics
|
|
91
|
+
affordances?: TerminalAffordances | boolean
|
|
92
|
+
children: ReactNode
|
|
93
|
+
}) {
|
|
94
|
+
if (!terminal) {
|
|
95
|
+
return (
|
|
96
|
+
<div className='mx-auto flex w-full max-w-[var(--wd-content-max-w,48rem)] flex-col gap-2'>
|
|
97
|
+
{children}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
return (
|
|
102
|
+
<TerminalSurface
|
|
103
|
+
fontSize={metrics?.fontSize}
|
|
104
|
+
lineHeight={metrics?.lineHeight}
|
|
105
|
+
affordances={affordances}
|
|
106
|
+
bleed='1ch'
|
|
107
|
+
className='term-transcript'>
|
|
108
|
+
{children}
|
|
109
|
+
</TerminalSurface>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The character cell the terminal theme draws on, in **whole pixels**.
|
|
115
|
+
*
|
|
116
|
+
* One object rather than two props because the panel mounts three separate
|
|
117
|
+
* `TerminalSurface`s — the transcript, the pending prompts, the composer — and
|
|
118
|
+
* they must agree: a prompt drawn at a different line height from the rows above
|
|
119
|
+
* it is three surfaces on three grids, which is the failure this theme is built
|
|
120
|
+
* to make impossible. Passing one value through one prop is what keeps them from
|
|
121
|
+
* drifting.
|
|
122
|
+
*
|
|
123
|
+
* Absent means the CLI's own 13/18. A host that follows an editor font size
|
|
124
|
+
* (VS Code) hands that down instead.
|
|
125
|
+
*/
|
|
126
|
+
export type TerminalMetrics = { fontSize?: number; lineHeight?: number }
|
|
127
|
+
|
|
62
128
|
export interface SessionPanelProps {
|
|
63
129
|
client: WorkerDeckClient
|
|
64
130
|
sessionId: string | undefined
|
|
@@ -119,12 +185,55 @@ export interface SessionPanelProps {
|
|
|
119
185
|
* internal ref, so an inline closure is fine). */
|
|
120
186
|
onVitals?: (vitals: SessionVitals) => void
|
|
121
187
|
/**
|
|
122
|
-
* How the transcript draws a turn — `'cards'` (default
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
188
|
+
* How the transcript draws a turn — `'cards'` (default, the chat convention)
|
|
189
|
+
* or `'terminal'`, the CLI's own form: every row on a character cell, no boxes
|
|
190
|
+
* anywhere, diffs as full-width bands. An embedder in a dock (the VS Code
|
|
191
|
+
* panel) wants `'terminal'`; a full-width dashboard may prefer cards.
|
|
126
192
|
*/
|
|
127
193
|
transcriptVariant?: TranscriptVariant
|
|
194
|
+
/**
|
|
195
|
+
* Terminal theme only: the pointer affordances a real terminal cannot offer —
|
|
196
|
+
* the hover fill, the hover-revealed copy. `false` for none. None of them
|
|
197
|
+
* costs layout, so turning them off changes no glyph's position. See
|
|
198
|
+
* {@link TerminalAffordances}.
|
|
199
|
+
*/
|
|
200
|
+
affordances?: TerminalAffordances | boolean
|
|
201
|
+
/**
|
|
202
|
+
* Terminal theme only: the character cell, in whole pixels. See
|
|
203
|
+
* {@link TerminalMetrics} — it reaches all three of the panel's terminal
|
|
204
|
+
* surfaces, which is why it is one prop and not two per surface.
|
|
205
|
+
*/
|
|
206
|
+
terminalMetrics?: TerminalMetrics
|
|
207
|
+
/**
|
|
208
|
+
* Terminal theme only: replace the scrollbar with the **overview ruler** — a
|
|
209
|
+
* `2ch` rail of coloured marks in three lanes (what you typed, the answer and
|
|
210
|
+
* its turn end, errors and a waiting approval), which you can hover to peek,
|
|
211
|
+
* click to jump, and drag to scrub.
|
|
212
|
+
*
|
|
213
|
+
* Its premise is the terminal theme's own: one line height and one cell make
|
|
214
|
+
* every row's height derivable, so a mark's position is *computed* rather than
|
|
215
|
+
* guessed from rows that have not mounted. That is why it is not offered under
|
|
216
|
+
* `cards` — there the flag is inert.
|
|
217
|
+
*
|
|
218
|
+
* `false` keeps the native scrollbar. So does `affordances={false}`, which
|
|
219
|
+
* leaves the marks painted but inert rather than removing a reader's only way
|
|
220
|
+
* to scroll.
|
|
221
|
+
*/
|
|
222
|
+
scrubber?: boolean
|
|
223
|
+
/**
|
|
224
|
+
* Bookmarked **item indices**, painted full-width on the rail. Paint only —
|
|
225
|
+
* the panel neither stores bookmarks nor offers a way to set one, because who
|
|
226
|
+
* owns that store is the embedder's question (a private pin belongs with the
|
|
227
|
+
* client's watermarks; a shared one is session metadata on the gateway).
|
|
228
|
+
*/
|
|
229
|
+
scrubberMarks?: readonly number[]
|
|
230
|
+
/**
|
|
231
|
+
* Terminal theme only: hold the prompt of the turn you are reading at the top
|
|
232
|
+
* of the transcript, as the Claude Code CLI does. The **real row** is pinned
|
|
233
|
+
* rather than a copy drawn above it, so it lines up with the rows beneath by
|
|
234
|
+
* construction — see `TranscriptRows`.
|
|
235
|
+
*/
|
|
236
|
+
stickyPrompt?: boolean
|
|
128
237
|
/**
|
|
129
238
|
* How much air the transcript gives each row — `'comfortable'` (default: a
|
|
130
239
|
* blank line between messages, as the Claude Code CLI leaves) or `'compact'`
|
|
@@ -224,6 +333,14 @@ export interface SessionPanelProps {
|
|
|
224
333
|
* a wildcard.
|
|
225
334
|
*/
|
|
226
335
|
toolHost?: UseToolCallHostOptions | false
|
|
336
|
+
/**
|
|
337
|
+
* Keep this session's transcript warm across remounts (default true), so
|
|
338
|
+
* switching back to a recently viewed session paints instantly and replays
|
|
339
|
+
* only what it missed — `UseClaudeSessionOptions.cacheTranscript`. Set
|
|
340
|
+
* `false` for an embedder whose principal varies on one gateway URL by means
|
|
341
|
+
* the client cannot see.
|
|
342
|
+
*/
|
|
343
|
+
cacheTranscript?: boolean
|
|
227
344
|
className?: string
|
|
228
345
|
}
|
|
229
346
|
|
|
@@ -320,12 +437,18 @@ export function SessionPanel({
|
|
|
320
437
|
transcriptVariant = 'cards',
|
|
321
438
|
transcriptDensity = 'comfortable',
|
|
322
439
|
transcriptFont = 'sans',
|
|
440
|
+
affordances,
|
|
441
|
+
terminalMetrics,
|
|
442
|
+
scrubber = false,
|
|
443
|
+
scrubberMarks,
|
|
444
|
+
stickyPrompt = false,
|
|
323
445
|
controlsSurface = 'internal',
|
|
324
446
|
onControls,
|
|
325
447
|
focusComposerOnClick = false,
|
|
326
448
|
unseen,
|
|
327
449
|
readOnly = false,
|
|
328
450
|
toolHost,
|
|
451
|
+
cacheTranscript,
|
|
329
452
|
className,
|
|
330
453
|
}: SessionPanelProps) {
|
|
331
454
|
const external = panelSurface === 'external'
|
|
@@ -344,6 +467,7 @@ export function SessionPanel({
|
|
|
344
467
|
const {
|
|
345
468
|
state,
|
|
346
469
|
connection,
|
|
470
|
+
replaying,
|
|
347
471
|
protocolMismatch,
|
|
348
472
|
models,
|
|
349
473
|
effectiveModel,
|
|
@@ -355,7 +479,7 @@ export function SessionPanel({
|
|
|
355
479
|
setModel,
|
|
356
480
|
setPermissionMode,
|
|
357
481
|
reconnectNow,
|
|
358
|
-
} = useClaudeSession(client, sessionId, { onProtocolError: setProtocolError })
|
|
482
|
+
} = useClaudeSession(client, sessionId, { onProtocolError: setProtocolError, cacheTranscript })
|
|
359
483
|
// Callers are told to remount on a session switch, but a changed prop must not leave
|
|
360
484
|
// the previous session's failure on screen.
|
|
361
485
|
useEffect(() => setProtocolError(undefined), [sessionId])
|
|
@@ -398,8 +522,39 @@ export function SessionPanel({
|
|
|
398
522
|
// client. Free for Claude sessions: the guest loads lazily on the first call,
|
|
399
523
|
// which for them never comes.
|
|
400
524
|
useToolCallHost(handle, toolHost === false ? { enabled: false } : toolHost)
|
|
525
|
+
const terminal = transcriptVariant === 'terminal'
|
|
401
526
|
const capabilities = state.capabilities
|
|
402
527
|
|
|
528
|
+
// Plan usage as the *gateway* knows it, merged over this session's own
|
|
529
|
+
// reading — one derivation, feeding the bar, the terminal status line, the
|
|
530
|
+
// Usage panel and the vitals an external chrome renders from, because four
|
|
531
|
+
// surfaces disagreeing about the same percentage is worse than any of them
|
|
532
|
+
// being stale. A session's own `rate_limit` readings land only at a turn's
|
|
533
|
+
// edges, so an idle session's numbers age silently and a sibling session's
|
|
534
|
+
// spend never shows up here at all; `mergeUsage` is where that rule is
|
|
535
|
+
// written down. Skipped entirely for an engine that reports no windows.
|
|
536
|
+
const { usage: profileUsage } = useProfileUsage(client, state.session?.profile, {
|
|
537
|
+
enabled: capabilities.rateLimits,
|
|
538
|
+
})
|
|
539
|
+
const usage = useMemo(
|
|
540
|
+
() =>
|
|
541
|
+
mergeUsage(
|
|
542
|
+
{ rateLimits: state.rateLimits, updatedAt: state.rateLimitsUpdatedAt },
|
|
543
|
+
profileUsage,
|
|
544
|
+
),
|
|
545
|
+
[state.rateLimits, state.rateLimitsUpdatedAt, profileUsage],
|
|
546
|
+
)
|
|
547
|
+
// Undefined rather than an empty map when nothing has been reported: absent is
|
|
548
|
+
// *unknown*, and a surface that can't tell the two apart draws 0%.
|
|
549
|
+
const rateLimits: Record<string, RateLimitInfo> | undefined = useMemo(
|
|
550
|
+
() => (Object.keys(usage).length > 0 ? usageInfos(usage) : undefined),
|
|
551
|
+
[usage],
|
|
552
|
+
)
|
|
553
|
+
const usageUpdatedAt = useMemo(() => {
|
|
554
|
+
const stamps = Object.values(usage).map((w) => w.updatedAt)
|
|
555
|
+
return stamps.length > 0 ? Math.max(...stamps) : undefined
|
|
556
|
+
}, [usage])
|
|
557
|
+
|
|
403
558
|
// Vitals out to the embedder, keyed on the readings themselves so an inline
|
|
404
559
|
// closure prop doesn't retrigger it every render.
|
|
405
560
|
const onVitalsRef = useRef(onVitals)
|
|
@@ -423,7 +578,7 @@ export function SessionPanel({
|
|
|
423
578
|
permissionModes,
|
|
424
579
|
cwd: state.cwd,
|
|
425
580
|
contextUsage: state.contextUsage,
|
|
426
|
-
rateLimits
|
|
581
|
+
rateLimits,
|
|
427
582
|
itemCount: state.items.length,
|
|
428
583
|
})
|
|
429
584
|
}, [
|
|
@@ -437,7 +592,7 @@ export function SessionPanel({
|
|
|
437
592
|
permissionModes,
|
|
438
593
|
state.cwd,
|
|
439
594
|
state.contextUsage,
|
|
440
|
-
|
|
595
|
+
rateLimits,
|
|
441
596
|
state.items.length,
|
|
442
597
|
])
|
|
443
598
|
|
|
@@ -468,7 +623,9 @@ export function SessionPanel({
|
|
|
468
623
|
// inert for the moment before it does, and stays inert on a gateway that
|
|
469
624
|
// serves no host files.
|
|
470
625
|
const hostFiles = useHostFileSearch(client, state.cwd)
|
|
471
|
-
|
|
626
|
+
// Protocol's ordering, over the merged readings — each row keeping its own
|
|
627
|
+
// date, since they no longer share one clock.
|
|
628
|
+
const windows = useMemo(() => orderUsageWindows(usage), [usage])
|
|
472
629
|
// Reads a picture the engine left on the host (codex's `image_gen` reports a
|
|
473
630
|
// path, never bytes). Stable and memoized per path: transcript rows re-render
|
|
474
631
|
// on every delta, and a fresh function would re-fetch each time.
|
|
@@ -477,6 +634,8 @@ export function SessionPanel({
|
|
|
477
634
|
// The catch-up strip's way of scrolling the (virtualized, usually unmounted)
|
|
478
635
|
// recap row into view — the transcript fills it in. See TranscriptProps.
|
|
479
636
|
const jumpToRecap = useRef<(() => void) | null>(null)
|
|
637
|
+
// Filled by the transcript; pressed on send. See `handleSend`.
|
|
638
|
+
const repinTranscript = useRef<(() => void) | null>(null)
|
|
480
639
|
|
|
481
640
|
// "/model" is handled panel-side (see handleSend) — surface it in the autocomplete
|
|
482
641
|
// even though the CLI's command list doesn't include it.
|
|
@@ -510,6 +669,13 @@ export function SessionPanel({
|
|
|
510
669
|
// Typing into a session is the clearest possible statement that you have
|
|
511
670
|
// read it — nothing left to catch up on.
|
|
512
671
|
setCaughtUp(true)
|
|
672
|
+
// ...and sending is the statement that you want to watch what happens next,
|
|
673
|
+
// so following resumes here too. Scrolling up escapes the bottom lock, and
|
|
674
|
+
// without this the reply streams in off screen: the transcript stays parked
|
|
675
|
+
// where you were reading and sending looks like it did nothing. Ordered
|
|
676
|
+
// before `send` only for readability — the re-pin is instant and the first
|
|
677
|
+
// row is a round trip away either way.
|
|
678
|
+
repinTranscript.current?.()
|
|
513
679
|
send(text, attachmentIds)
|
|
514
680
|
}
|
|
515
681
|
|
|
@@ -613,6 +779,7 @@ export function SessionPanel({
|
|
|
613
779
|
const statusBar = statusExternal ? null : (
|
|
614
780
|
<StatusBar
|
|
615
781
|
state={state}
|
|
782
|
+
rateLimits={rateLimits}
|
|
616
783
|
connection={connection}
|
|
617
784
|
placement={statusPlacement}
|
|
618
785
|
controls={controlsInStatus && !readOnly ? sessionControls : undefined}
|
|
@@ -635,9 +802,9 @@ export function SessionPanel({
|
|
|
635
802
|
}
|
|
636
803
|
|
|
637
804
|
return (
|
|
638
|
-
// The variant is a panel-wide fact, not a transcript-only one: the
|
|
639
|
-
// and
|
|
640
|
-
//
|
|
805
|
+
// The variant is a panel-wide fact, not a transcript-only one: the composer
|
|
806
|
+
// and the pending prompts live outside the scroller but belong to the same
|
|
807
|
+
// run, and they read it from this context rather than a prop chain.
|
|
641
808
|
<TranscriptVariantProvider value={transcriptVariant}>
|
|
642
809
|
<TranscriptDensityProvider value={transcriptDensity}>
|
|
643
810
|
<div
|
|
@@ -669,27 +836,34 @@ export function SessionPanel({
|
|
|
669
836
|
hostImage={hostImage}
|
|
670
837
|
variant={transcriptVariant}
|
|
671
838
|
density={transcriptDensity}
|
|
839
|
+
fontSize={terminalMetrics?.fontSize}
|
|
840
|
+
lineHeight={terminalMetrics?.lineHeight}
|
|
841
|
+
affordances={affordances}
|
|
842
|
+
stickyPrompt={stickyPrompt}
|
|
843
|
+
scrubber={scrubber}
|
|
844
|
+
scrubberMarks={scrubberMarks}
|
|
845
|
+
replaying={replaying}
|
|
672
846
|
catchUp={
|
|
673
847
|
catchUp && newCount > 0
|
|
674
848
|
? { from: catchUp.itemCount, since: catchUp.since }
|
|
675
849
|
: undefined
|
|
676
850
|
}
|
|
677
851
|
jumpToRecapRef={jumpToRecap}
|
|
852
|
+
repinRef={repinTranscript}
|
|
678
853
|
/>
|
|
679
854
|
{/* The way back into what you missed. Above the composer because that is
|
|
680
855
|
where the eye already is on returning, and because the transcript
|
|
681
|
-
itself opens pinned to the newest row.
|
|
682
|
-
|
|
856
|
+
itself opens pinned to the newest row. Held with the transcript
|
|
857
|
+
while the replay lands — its count is `state.items.length`, which
|
|
858
|
+
during the replay is a number visibly climbing toward its answer. */}
|
|
859
|
+
{catchUp && newCount > 0 && !replaying ? (
|
|
683
860
|
<div className='px-3 pb-1'>
|
|
684
861
|
<div
|
|
685
862
|
data-slot='catch-up'
|
|
686
863
|
className='mx-auto flex w-full max-w-[var(--wd-content-max-w,48rem)] items-center gap-2 text-label text-fg-3'>
|
|
687
864
|
<span
|
|
688
865
|
aria-hidden
|
|
689
|
-
className={cn(
|
|
690
|
-
'select-none',
|
|
691
|
-
transcriptVariant === 'lines' ? 'text-fg-3' : 'text-accent',
|
|
692
|
-
)}>
|
|
866
|
+
className={cn('select-none', terminal ? 'text-fg-3' : 'text-accent')}>
|
|
693
867
|
※
|
|
694
868
|
</span>
|
|
695
869
|
<span className='min-w-0 flex-1 truncate'>
|
|
@@ -718,11 +892,30 @@ export function SessionPanel({
|
|
|
718
892
|
`awaiting approval`, and the place that can act on it is wherever the
|
|
719
893
|
session is actually driven. */}
|
|
720
894
|
{!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
|
|
721
|
-
<div className='px-3 pb-2'>
|
|
722
|
-
<
|
|
723
|
-
{state.pendingApprovals.map((request) =>
|
|
724
|
-
|
|
725
|
-
|
|
895
|
+
<div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
|
|
896
|
+
<PromptSurface terminal={terminal} metrics={terminalMetrics} affordances={affordances}>
|
|
897
|
+
{state.pendingApprovals.map((request) => {
|
|
898
|
+
const isQuestion =
|
|
899
|
+
request.toolName === 'AskUserQuestion' &&
|
|
900
|
+
parseUserQuestions(request.input).length > 0
|
|
901
|
+
if (terminal) {
|
|
902
|
+
return isQuestion ? (
|
|
903
|
+
<TerminalQuestionPrompt
|
|
904
|
+
key={request.id}
|
|
905
|
+
request={request}
|
|
906
|
+
onAnswer={approve}
|
|
907
|
+
onDismiss={(id) => deny(id, 'Question dismissed by user')}
|
|
908
|
+
/>
|
|
909
|
+
) : (
|
|
910
|
+
<TerminalPermissionPrompt
|
|
911
|
+
key={request.id}
|
|
912
|
+
request={request}
|
|
913
|
+
onApprove={approve}
|
|
914
|
+
onDeny={deny}
|
|
915
|
+
/>
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
return isQuestion ? (
|
|
726
919
|
<QuestionPrompt
|
|
727
920
|
key={request.id}
|
|
728
921
|
request={request}
|
|
@@ -736,9 +929,9 @@ export function SessionPanel({
|
|
|
736
929
|
onApprove={approve}
|
|
737
930
|
onDeny={deny}
|
|
738
931
|
/>
|
|
739
|
-
)
|
|
740
|
-
)}
|
|
741
|
-
</
|
|
932
|
+
)
|
|
933
|
+
})}
|
|
934
|
+
</PromptSurface>
|
|
742
935
|
</div>
|
|
743
936
|
) : null}
|
|
744
937
|
{readOnly ? null : (
|
|
@@ -758,6 +951,9 @@ export function SessionPanel({
|
|
|
758
951
|
}
|
|
759
952
|
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
760
953
|
toolbar={controlsExternal ? undefined : sessionControls}
|
|
954
|
+
fontSize={terminalMetrics?.fontSize}
|
|
955
|
+
lineHeight={terminalMetrics?.lineHeight}
|
|
956
|
+
affordances={affordances}
|
|
761
957
|
/>
|
|
762
958
|
)}
|
|
763
959
|
{/* Below the composer, along the foot of the panel — the editor
|
|
@@ -785,7 +981,7 @@ export function SessionPanel({
|
|
|
785
981
|
subscriptionType={state.subscriptionType}
|
|
786
982
|
engine={state.engine ?? 'claude'}
|
|
787
983
|
totalCostUsd={state.totalCostUsd}
|
|
788
|
-
updatedAt={
|
|
984
|
+
updatedAt={usageUpdatedAt}
|
|
789
985
|
open={panel === 'usage'}
|
|
790
986
|
onOpenChange={(next) => setPanel(next ? 'usage' : undefined)}
|
|
791
987
|
/>
|
|
@@ -17,6 +17,7 @@ import { EditorTabs } from './EditorTabs.tsx'
|
|
|
17
17
|
import { FileTree } from './FileTree.tsx'
|
|
18
18
|
import { FileViewer } from './FileViewer.tsx'
|
|
19
19
|
import { SessionPanel, type SessionPanelProps } from './SessionPanel.tsx'
|
|
20
|
+
import { resolveAgainstCwd, usePathLinks } from './use-path-links.ts'
|
|
20
21
|
|
|
21
22
|
export interface SessionWorkspaceProps {
|
|
22
23
|
client: WorkerDeckClient
|
|
@@ -35,6 +36,11 @@ export interface SessionWorkspaceProps {
|
|
|
35
36
|
transcriptVariant?: SessionPanelProps['transcriptVariant']
|
|
36
37
|
transcriptDensity?: SessionPanelProps['transcriptDensity']
|
|
37
38
|
transcriptFont?: SessionPanelProps['transcriptFont']
|
|
39
|
+
/** The overview ruler in place of the scrollbar — see `SessionPanel`. */
|
|
40
|
+
scrubber?: SessionPanelProps['scrubber']
|
|
41
|
+
scrubberMarks?: SessionPanelProps['scrubberMarks']
|
|
42
|
+
/** The last prompt pinned above the transcript — see `SessionPanel`. */
|
|
43
|
+
stickyPrompt?: SessionPanelProps['stickyPrompt']
|
|
38
44
|
/** Which end of the panel the status bar sits at — see `SessionPanel`. */
|
|
39
45
|
statusPlacement?: SessionPanelProps['statusPlacement']
|
|
40
46
|
controlsSurface?: SessionPanelProps['controlsSurface']
|
|
@@ -94,6 +100,9 @@ export function SessionWorkspace({
|
|
|
94
100
|
transcriptVariant,
|
|
95
101
|
transcriptDensity,
|
|
96
102
|
transcriptFont,
|
|
103
|
+
scrubber,
|
|
104
|
+
scrubberMarks,
|
|
105
|
+
stickyPrompt,
|
|
97
106
|
statusPlacement,
|
|
98
107
|
controlsSurface,
|
|
99
108
|
unseen,
|
|
@@ -118,6 +127,7 @@ export function SessionWorkspace({
|
|
|
118
127
|
// editor asks before it offers to save anything.
|
|
119
128
|
const { canWrite } = useHostFileRoots(client)
|
|
120
129
|
|
|
130
|
+
|
|
121
131
|
// Nothing here can save on the user's behalf when the tab is going away, so
|
|
122
132
|
// the browser's own guard is the last line. Registered only while there is
|
|
123
133
|
// something to lose — an unconditional handler makes every navigation prompt.
|
|
@@ -172,6 +182,22 @@ export function SessionWorkspace({
|
|
|
172
182
|
|
|
173
183
|
const column = useRef<HTMLDivElement>(null)
|
|
174
184
|
const columnHeight = useElementHeight(column)
|
|
185
|
+
// Cmd/Ctrl+click a file the agent named and it opens in the editor above —
|
|
186
|
+
// the workspace is the only surface that *has* an editor, which is why this
|
|
187
|
+
// lives here and not in the panel. Gated on the tree being available: without
|
|
188
|
+
// a host filesystem there is nothing to open, and a link that cannot resolve
|
|
189
|
+
// is worse than plain text. Monaco is excluded because Cmd+click already
|
|
190
|
+
// means go-to-definition in there, and every identifier would match.
|
|
191
|
+
const openPath = useCallback(
|
|
192
|
+
({ path }: { path: string }) => files.open(resolveAgainstCwd(path, cwd)),
|
|
193
|
+
[files.open, cwd],
|
|
194
|
+
)
|
|
195
|
+
usePathLinks({
|
|
196
|
+
container: column,
|
|
197
|
+
onOpen: openPath,
|
|
198
|
+
enabled: tree.available,
|
|
199
|
+
ignore: '.monaco-editor',
|
|
200
|
+
})
|
|
175
201
|
const editorMax = Math.max(EDITOR_MIN, columnHeight - AGENT_MIN)
|
|
176
202
|
|
|
177
203
|
// The embedder's header is app chrome and belongs above everything — but only
|
|
@@ -278,6 +304,9 @@ export function SessionWorkspace({
|
|
|
278
304
|
transcriptVariant={transcriptVariant}
|
|
279
305
|
transcriptDensity={transcriptDensity}
|
|
280
306
|
transcriptFont={transcriptFont}
|
|
307
|
+
scrubber={scrubber}
|
|
308
|
+
scrubberMarks={scrubberMarks}
|
|
309
|
+
stickyPrompt={stickyPrompt}
|
|
281
310
|
controlsSurface={controlsSurface}
|
|
282
311
|
statusPlacement={statusPlacement}
|
|
283
312
|
unseen={unseen}
|
|
@@ -9,9 +9,19 @@ import { Tip } from '../ui/Tooltip.tsx'
|
|
|
9
9
|
import { cn } from '../../lib/utils.ts'
|
|
10
10
|
import { formatCost, formatCountdown, formatTokens } from '../../lib/format.ts'
|
|
11
11
|
import { STATUS_META } from './status.ts'
|
|
12
|
+
import { useTranscriptVariant } from './transcript-variant.tsx'
|
|
12
13
|
|
|
13
14
|
export interface StatusBarProps {
|
|
14
15
|
state: TranscriptState
|
|
16
|
+
/**
|
|
17
|
+
* Plan windows to draw, when they should not be the session's own. The panel
|
|
18
|
+
* hands over the gateway's per-profile state merged over this transcript's
|
|
19
|
+
* reading (`mergeUsage`): a session's own `rate_limit` readings arrive only at
|
|
20
|
+
* a turn's edges, so one idle since yesterday would otherwise render
|
|
21
|
+
* yesterday's number as current. Absent = `state.rateLimits`, which is what a
|
|
22
|
+
* bar composed by hand outside the panel gets.
|
|
23
|
+
*/
|
|
24
|
+
rateLimits?: Record<string, RateLimitInfo>
|
|
15
25
|
/** @deprecated Pass {@link StatusBarProps.connection}; kept so an embedder
|
|
16
26
|
* still handing over a boolean keeps working. */
|
|
17
27
|
connected?: boolean
|
|
@@ -167,6 +177,7 @@ function Slot({ onClick, hint, children }: { onClick?: () => void; hint: string;
|
|
|
167
177
|
|
|
168
178
|
export function StatusBar({
|
|
169
179
|
state,
|
|
180
|
+
rateLimits,
|
|
170
181
|
connected,
|
|
171
182
|
connection,
|
|
172
183
|
onOpenStatus,
|
|
@@ -179,9 +190,11 @@ export function StatusBar({
|
|
|
179
190
|
}: StatusBarProps) {
|
|
180
191
|
const meta = STATUS_META[state.status]
|
|
181
192
|
const now = useNow()
|
|
182
|
-
const
|
|
183
|
-
const
|
|
193
|
+
const windows = rateLimits ?? state.rateLimits
|
|
194
|
+
const session = windows?.five_hour
|
|
195
|
+
const weekly = windows?.seven_day
|
|
184
196
|
const link: ConnectionState = connection ?? (connected === false ? 'reconnecting' : 'live')
|
|
197
|
+
const terminal = useTranscriptVariant() === 'terminal'
|
|
185
198
|
return (
|
|
186
199
|
<div
|
|
187
200
|
data-slot='status-bar'
|
|
@@ -198,8 +211,11 @@ export function StatusBar({
|
|
|
198
211
|
// one piece of chrome, and a pixel of drift between them shows.
|
|
199
212
|
'flex h-[38px] items-baseline gap-2 border-border bg-surface p-1.5',
|
|
200
213
|
// The rule goes between the bar and the content, so which edge it sits
|
|
201
|
-
// on follows the placement
|
|
202
|
-
|
|
214
|
+
// on follows the placement — except under the terminal theme at the
|
|
215
|
+
// foot, where the composer directly above already closes itself with a
|
|
216
|
+
// rule of its own. Two adjacent 1px rules is a 2px rule with a seam in
|
|
217
|
+
// it, and it is off the line grid besides.
|
|
218
|
+
placement === 'bottom' ? (terminal ? undefined : 'border-t') : 'border-b',
|
|
203
219
|
className,
|
|
204
220
|
)}>
|
|
205
221
|
{/* One slot, two meanings: connection trouble wins it, because a session
|