@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
|
@@ -46,7 +46,9 @@ import { BookmarkProvider, type BookmarkHandle, type TerminalAffordances } from
|
|
|
46
46
|
import { SessionInfoDialog } from './SessionInfoDialog.tsx'
|
|
47
47
|
import { StatusBar } from './StatusBar.tsx'
|
|
48
48
|
import { Transcript } from './Transcript.tsx'
|
|
49
|
+
import { HeldSendsBar, useHeldSends } from './held-sends.tsx'
|
|
49
50
|
import { ToolResultFetchProvider } from './tool-result-fetch.tsx'
|
|
51
|
+
import { ToolTitleProvider } from './tool-titles.tsx'
|
|
50
52
|
import { ToolResultImageProvider, useToolResultImages } from './tool-result-image.tsx'
|
|
51
53
|
import {
|
|
52
54
|
TranscriptDensityProvider,
|
|
@@ -99,7 +101,7 @@ export interface SessionPanelProps {
|
|
|
99
101
|
affordances?: TerminalAffordances | boolean
|
|
100
102
|
terminalMetrics?: TerminalMetrics
|
|
101
103
|
scrubber?: boolean
|
|
102
|
-
|
|
104
|
+
// Bookmarked transcript item ids — the host owns membership and persistence.
|
|
103
105
|
bookmarks?: readonly string[]
|
|
104
106
|
onToggleBookmark?: (itemId: string) => void
|
|
105
107
|
reveal?: { toolUseId: string; nonce: number }
|
|
@@ -108,6 +110,9 @@ export interface SessionPanelProps {
|
|
|
108
110
|
stickyPrompt?: boolean
|
|
109
111
|
transcriptDensity?: TranscriptDensity
|
|
110
112
|
transcriptFont?: TranscriptFont
|
|
113
|
+
// 'fold' sends a mid-turn message straight through, and the engine folds it into the running
|
|
114
|
+
// turn (catch-up mode). 'hold' keeps it here until the turn ends.
|
|
115
|
+
midTurnSend?: 'fold' | 'hold'
|
|
111
116
|
controlsSurface?: 'internal' | 'external' | 'status'
|
|
112
117
|
onControls?: (controls: SessionControls | undefined) => void
|
|
113
118
|
focusComposerOnClick?: boolean
|
|
@@ -162,11 +167,9 @@ export type SessionVitals = {
|
|
|
162
167
|
cwd: TranscriptState['cwd']
|
|
163
168
|
contextUsage: TranscriptState['contextUsage']
|
|
164
169
|
rateLimits: TranscriptState['rateLimits']
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
* Absent means unknown, which is not the same as fresh.
|
|
169
|
-
*/
|
|
170
|
+
// When the newest window in `rateLimits` was reported, as event time — not receive time. External chrome (the
|
|
171
|
+
// VS Code status bar, iOS) cannot otherwise tell a live reading from one a days-old session just replayed.
|
|
172
|
+
// Absent means unknown, which is not the same as fresh.
|
|
170
173
|
rateLimitsUpdatedAt: number | undefined
|
|
171
174
|
itemCount: number
|
|
172
175
|
totalCostUsd: number
|
|
@@ -184,6 +187,7 @@ export function SessionPanel({
|
|
|
184
187
|
transcriptVariant = 'cards',
|
|
185
188
|
transcriptDensity = 'comfortable',
|
|
186
189
|
transcriptFont = 'sans',
|
|
190
|
+
midTurnSend = 'fold',
|
|
187
191
|
affordances,
|
|
188
192
|
terminalMetrics,
|
|
189
193
|
scrubber = false,
|
|
@@ -395,6 +399,8 @@ export function SessionPanel({
|
|
|
395
399
|
return [{ name: 'model', description: 'Switch the model for this session', argumentHint: '<model>' }, ...state.commands]
|
|
396
400
|
}, [state.commands])
|
|
397
401
|
|
|
402
|
+
const heldSends = useHeldSends({ hold: midTurnSend === 'hold', busy: busy && !ended, send })
|
|
403
|
+
|
|
398
404
|
const handleSend = (text: string, attachmentIds: string[]) => {
|
|
399
405
|
if (attachmentIds.length === 0) {
|
|
400
406
|
const modelCommand = /^\/model\s+(\S+)$/.exec(text)
|
|
@@ -409,7 +415,7 @@ export function SessionPanel({
|
|
|
409
415
|
}
|
|
410
416
|
setCaughtUp(true)
|
|
411
417
|
repinTranscript.current?.()
|
|
412
|
-
|
|
418
|
+
heldSends.submit(text, attachmentIds)
|
|
413
419
|
}
|
|
414
420
|
|
|
415
421
|
const actionsMenu = (
|
|
@@ -540,196 +546,202 @@ export function SessionPanel({
|
|
|
540
546
|
<TranscriptVariantProvider value={transcriptVariant}>
|
|
541
547
|
<TranscriptDensityProvider value={transcriptDensity}>
|
|
542
548
|
<ToolResultFetchProvider value={loadFullResult}>
|
|
543
|
-
<
|
|
544
|
-
<
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
{
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
fontSize={effectiveTermFontSize}
|
|
572
|
-
lineHeight={effectiveTermLineHeight}
|
|
573
|
-
/>
|
|
574
|
-
) : null}
|
|
575
|
-
<BookmarkProvider value={bookmarkHandle}>
|
|
576
|
-
<Transcript
|
|
577
|
-
key={subagentId ?? 'session'}
|
|
578
|
-
state={state}
|
|
579
|
-
fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
|
|
580
|
-
attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
|
|
581
|
-
canBrowseFiles={hostFiles.available}
|
|
582
|
-
hostImage={hostImage}
|
|
583
|
-
variant={transcriptVariant}
|
|
584
|
-
density={transcriptDensity}
|
|
585
|
-
fontSize={effectiveTermFontSize}
|
|
586
|
-
lineHeight={effectiveTermLineHeight}
|
|
587
|
-
affordances={affordances}
|
|
588
|
-
stickyPrompt={stickyPrompt}
|
|
589
|
-
scrubber={scrubber}
|
|
590
|
-
bookmarks={bookmarks}
|
|
591
|
-
replaying={replaying}
|
|
592
|
-
catchUp={catchUp && newCount > 0 ? { from: catchUp.itemCount, since: catchUp.since } : undefined}
|
|
593
|
-
reveal={returnReveal ?? reveal}
|
|
594
|
-
frame={subagentId === undefined ? undefined : { parentToolUseId: subagentId }}
|
|
595
|
-
onOpenSubagent={enterSubagent}
|
|
596
|
-
emptyState={emptyState}
|
|
597
|
-
jumpToRecapRef={jumpToRecap}
|
|
598
|
-
repinRef={repinTranscript}
|
|
599
|
-
/>
|
|
600
|
-
</BookmarkProvider>
|
|
601
|
-
{catchUp && newCount > 0 && !replaying && subagentId === undefined ? (
|
|
602
|
-
<div className="px-3 pb-1">
|
|
603
|
-
<div
|
|
604
|
-
data-slot="catch-up"
|
|
605
|
-
className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3"
|
|
606
|
-
>
|
|
607
|
-
<span aria-hidden className={cn('select-none', terminal ? 'text-fg-3' : 'text-accent')}>
|
|
608
|
-
※
|
|
609
|
-
</span>
|
|
610
|
-
<span className="min-w-0 flex-1 truncate">
|
|
611
|
-
{newCount} new {newCount === 1 ? 'row' : 'rows'}
|
|
612
|
-
{catchUp.since !== undefined ? ` since you were last here` : ''}
|
|
613
|
-
</span>
|
|
614
|
-
<button
|
|
615
|
-
type="button"
|
|
616
|
-
onClick={() => jumpToRecap.current?.()}
|
|
617
|
-
className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
|
|
618
|
-
>
|
|
619
|
-
jump
|
|
620
|
-
</button>
|
|
621
|
-
<button
|
|
622
|
-
type="button"
|
|
623
|
-
onClick={() => setCaughtUp(true)}
|
|
624
|
-
className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
|
|
625
|
-
>
|
|
626
|
-
dismiss
|
|
627
|
-
</button>
|
|
628
|
-
</div>
|
|
629
|
-
</div>
|
|
630
|
-
) : null}
|
|
631
|
-
{!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
|
|
632
|
-
<div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
|
|
633
|
-
<PromptSurface
|
|
549
|
+
<ToolTitleProvider value={state.toolTitles}>
|
|
550
|
+
<ToolResultImageProvider value={resultImages}>
|
|
551
|
+
<div
|
|
552
|
+
ref={panelRef}
|
|
553
|
+
data-slot="session-panel"
|
|
554
|
+
data-agent-font={transcriptFont}
|
|
555
|
+
onClick={handleClick}
|
|
556
|
+
className={cn('flex h-full min-h-0 flex-col overflow-hidden bg-bg', className)}
|
|
557
|
+
style={fontSize !== undefined ? ({ '--wd-font-size': `${Math.round(fontSize)}px` } as React.CSSProperties) : undefined}
|
|
558
|
+
>
|
|
559
|
+
{headerTakesActions ? header({ actions: menu }) : header}
|
|
560
|
+
{statusPlacement === 'top' ? statusBar : null}
|
|
561
|
+
{protocolMismatch !== undefined ? (
|
|
562
|
+
<Notice level="warning">
|
|
563
|
+
Server speaks protocol v{protocolMismatch}, this build renders v{PROTOCOL_VERSION}. Some events may not render.
|
|
564
|
+
</Notice>
|
|
565
|
+
) : null}
|
|
566
|
+
{protocolError ? (
|
|
567
|
+
<Notice level="error" onDismiss={() => setProtocolError(undefined)}>
|
|
568
|
+
{protocolError}
|
|
569
|
+
</Notice>
|
|
570
|
+
) : null}
|
|
571
|
+
{subagentId !== undefined ? (
|
|
572
|
+
<SubagentStrip
|
|
573
|
+
task={subagentTask}
|
|
574
|
+
items={subagentFrameItems}
|
|
575
|
+
label={subagentFallbackLabel}
|
|
576
|
+
onBack={leaveSubagent}
|
|
634
577
|
terminal={terminal}
|
|
635
|
-
|
|
578
|
+
fontSize={effectiveTermFontSize}
|
|
579
|
+
lineHeight={effectiveTermLineHeight}
|
|
580
|
+
/>
|
|
581
|
+
) : null}
|
|
582
|
+
<BookmarkProvider value={bookmarkHandle}>
|
|
583
|
+
<Transcript
|
|
584
|
+
key={subagentId ?? 'session'}
|
|
585
|
+
state={state}
|
|
586
|
+
fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
|
|
587
|
+
attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
|
|
588
|
+
canBrowseFiles={hostFiles.available}
|
|
589
|
+
hostImage={hostImage}
|
|
590
|
+
variant={transcriptVariant}
|
|
591
|
+
density={transcriptDensity}
|
|
592
|
+
fontSize={effectiveTermFontSize}
|
|
593
|
+
lineHeight={effectiveTermLineHeight}
|
|
636
594
|
affordances={affordances}
|
|
637
|
-
|
|
638
|
-
{
|
|
639
|
-
|
|
640
|
-
|
|
595
|
+
stickyPrompt={stickyPrompt}
|
|
596
|
+
scrubber={scrubber}
|
|
597
|
+
bookmarks={bookmarks}
|
|
598
|
+
replaying={replaying}
|
|
599
|
+
catchUp={catchUp && newCount > 0 ? { from: catchUp.itemCount, since: catchUp.since } : undefined}
|
|
600
|
+
reveal={returnReveal ?? reveal}
|
|
601
|
+
frame={subagentId === undefined ? undefined : { parentToolUseId: subagentId }}
|
|
602
|
+
onOpenSubagent={enterSubagent}
|
|
603
|
+
emptyState={emptyState}
|
|
604
|
+
jumpToRecapRef={jumpToRecap}
|
|
605
|
+
repinRef={repinTranscript}
|
|
606
|
+
/>
|
|
607
|
+
</BookmarkProvider>
|
|
608
|
+
{catchUp && newCount > 0 && !replaying && subagentId === undefined ? (
|
|
609
|
+
<div className="px-3 pb-1">
|
|
610
|
+
<div
|
|
611
|
+
data-slot="catch-up"
|
|
612
|
+
className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3"
|
|
613
|
+
>
|
|
614
|
+
<span aria-hidden className={cn('select-none', terminal ? 'text-fg-3' : 'text-accent')}>
|
|
615
|
+
※
|
|
616
|
+
</span>
|
|
617
|
+
<span className="min-w-0 flex-1 truncate">
|
|
618
|
+
{newCount} new {newCount === 1 ? 'row' : 'rows'}
|
|
619
|
+
{catchUp.since !== undefined ? ` since you were last here` : ''}
|
|
620
|
+
</span>
|
|
621
|
+
<button
|
|
622
|
+
type="button"
|
|
623
|
+
onClick={() => jumpToRecap.current?.()}
|
|
624
|
+
className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
|
|
625
|
+
>
|
|
626
|
+
jump
|
|
627
|
+
</button>
|
|
628
|
+
<button
|
|
629
|
+
type="button"
|
|
630
|
+
onClick={() => setCaughtUp(true)}
|
|
631
|
+
className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
|
|
632
|
+
>
|
|
633
|
+
dismiss
|
|
634
|
+
</button>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
) : null}
|
|
638
|
+
{!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
|
|
639
|
+
<div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
|
|
640
|
+
<PromptSurface
|
|
641
|
+
terminal={terminal}
|
|
642
|
+
metrics={{ fontSize: effectiveTermFontSize, lineHeight: effectiveTermLineHeight }}
|
|
643
|
+
affordances={affordances}
|
|
644
|
+
>
|
|
645
|
+
{state.pendingApprovals.map((request) => {
|
|
646
|
+
const isQuestion = request.toolName === 'AskUserQuestion' && parseUserQuestions(request.input).length > 0
|
|
647
|
+
if (terminal) {
|
|
648
|
+
return isQuestion ? (
|
|
649
|
+
<TerminalQuestionPrompt
|
|
650
|
+
key={request.id}
|
|
651
|
+
request={request}
|
|
652
|
+
onAnswer={approve}
|
|
653
|
+
onDismiss={(id) => deny(id, 'Question dismissed by user')}
|
|
654
|
+
/>
|
|
655
|
+
) : (
|
|
656
|
+
<TerminalPermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
|
|
657
|
+
)
|
|
658
|
+
}
|
|
641
659
|
return isQuestion ? (
|
|
642
|
-
<
|
|
660
|
+
<QuestionPrompt
|
|
643
661
|
key={request.id}
|
|
644
662
|
request={request}
|
|
645
663
|
onAnswer={approve}
|
|
646
664
|
onDismiss={(id) => deny(id, 'Question dismissed by user')}
|
|
647
665
|
/>
|
|
648
666
|
) : (
|
|
649
|
-
<
|
|
667
|
+
<PermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
|
|
650
668
|
)
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
679
|
-
toolbar={controlsExternal ? undefined : sessionControls}
|
|
680
|
-
fontSize={effectiveTermFontSize}
|
|
681
|
-
lineHeight={effectiveTermLineHeight}
|
|
682
|
-
affordances={affordances}
|
|
683
|
-
/>
|
|
684
|
-
)}
|
|
685
|
-
{statusPlacement === 'bottom' ? statusBar : null}
|
|
669
|
+
})}
|
|
670
|
+
</PromptSurface>
|
|
671
|
+
</div>
|
|
672
|
+
) : null}
|
|
673
|
+
{readOnly || subagentId !== undefined ? null : (
|
|
674
|
+
<>
|
|
675
|
+
<HeldSendsBar held={heldSends.held} onSendNow={heldSends.flush} />
|
|
676
|
+
<Composer
|
|
677
|
+
ref={composerRef}
|
|
678
|
+
onSend={handleSend}
|
|
679
|
+
onInterrupt={interrupt}
|
|
680
|
+
busy={busy}
|
|
681
|
+
disabled={ended || !sessionId}
|
|
682
|
+
commands={capabilities.slashCommands ? commands : undefined}
|
|
683
|
+
skills={capabilities.skillsList ? state.skills : undefined}
|
|
684
|
+
attachments={attachments}
|
|
685
|
+
draft={draft}
|
|
686
|
+
onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
|
|
687
|
+
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
688
|
+
toolbar={controlsExternal ? undefined : sessionControls}
|
|
689
|
+
fontSize={effectiveTermFontSize}
|
|
690
|
+
lineHeight={effectiveTermLineHeight}
|
|
691
|
+
affordances={affordances}
|
|
692
|
+
/>
|
|
693
|
+
</>
|
|
694
|
+
)}
|
|
695
|
+
{statusPlacement === 'bottom' ? statusBar : null}
|
|
686
696
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
697
|
+
{!external ? (
|
|
698
|
+
<>
|
|
699
|
+
<SessionInfoDialog
|
|
700
|
+
state={state}
|
|
701
|
+
client={client}
|
|
702
|
+
sessionId={sessionId}
|
|
703
|
+
open={panel === 'info'}
|
|
704
|
+
onOpenChange={(next) => setPanel(next ? 'info' : undefined)}
|
|
705
|
+
/>
|
|
706
|
+
<ContextDialog
|
|
707
|
+
usage={state.contextUsage}
|
|
708
|
+
engine={state.engine ?? 'claude'}
|
|
709
|
+
open={panel === 'context'}
|
|
710
|
+
onOpenChange={(next) => setPanel(next ? 'context' : undefined)}
|
|
711
|
+
/>
|
|
712
|
+
<UsageDialog
|
|
713
|
+
rateLimits={windows}
|
|
714
|
+
subscriptionType={state.subscriptionType}
|
|
715
|
+
engine={state.engine ?? 'claude'}
|
|
716
|
+
totalCostUsd={state.totalCostUsd}
|
|
717
|
+
updatedAt={usageUpdatedAt}
|
|
718
|
+
open={panel === 'usage'}
|
|
719
|
+
onOpenChange={(next) => setPanel(next ? 'usage' : undefined)}
|
|
720
|
+
/>
|
|
721
|
+
<McpDialog
|
|
722
|
+
client={client}
|
|
723
|
+
sessionId={sessionId}
|
|
724
|
+
canManageServers={capabilities.mcpServerActions}
|
|
725
|
+
open={panel === 'mcp'}
|
|
726
|
+
onOpenChange={(next) => setPanel(next ? 'mcp' : undefined)}
|
|
727
|
+
/>
|
|
728
|
+
<SkillsDialog
|
|
729
|
+
skills={state.skills}
|
|
730
|
+
open={panel === 'skills'}
|
|
731
|
+
onOpenChange={(next) => setPanel(next ? 'skills' : undefined)}
|
|
732
|
+
onUse={(skill) => composerRef.current?.insertText(skillPrompt(skill))}
|
|
733
|
+
/>
|
|
734
|
+
<HostFilesDialog
|
|
735
|
+
client={client}
|
|
736
|
+
cwd={state.cwd}
|
|
737
|
+
open={panel === 'files'}
|
|
738
|
+
onOpenChange={(next) => setPanel(next ? 'files' : undefined)}
|
|
739
|
+
/>
|
|
740
|
+
</>
|
|
741
|
+
) : null}
|
|
742
|
+
</div>
|
|
743
|
+
</ToolResultImageProvider>
|
|
744
|
+
</ToolTitleProvider>
|
|
733
745
|
</ToolResultFetchProvider>
|
|
734
746
|
</TranscriptDensityProvider>
|
|
735
747
|
</TranscriptVariantProvider>
|
|
@@ -19,6 +19,7 @@ export interface SessionWorkspaceProps {
|
|
|
19
19
|
transcriptVariant?: SessionPanelProps['transcriptVariant']
|
|
20
20
|
transcriptDensity?: SessionPanelProps['transcriptDensity']
|
|
21
21
|
transcriptFont?: SessionPanelProps['transcriptFont']
|
|
22
|
+
midTurnSend?: SessionPanelProps['midTurnSend']
|
|
22
23
|
scrubber?: SessionPanelProps['scrubber']
|
|
23
24
|
bookmarks?: SessionPanelProps['bookmarks']
|
|
24
25
|
onToggleBookmark?: SessionPanelProps['onToggleBookmark']
|
|
@@ -51,6 +52,7 @@ export function SessionWorkspace({
|
|
|
51
52
|
transcriptVariant,
|
|
52
53
|
transcriptDensity,
|
|
53
54
|
transcriptFont,
|
|
55
|
+
midTurnSend,
|
|
54
56
|
scrubber,
|
|
55
57
|
bookmarks,
|
|
56
58
|
onToggleBookmark,
|
|
@@ -209,6 +211,7 @@ export function SessionWorkspace({
|
|
|
209
211
|
transcriptVariant={transcriptVariant}
|
|
210
212
|
transcriptDensity={transcriptDensity}
|
|
211
213
|
transcriptFont={transcriptFont}
|
|
214
|
+
midTurnSend={midTurnSend}
|
|
212
215
|
scrubber={scrubber}
|
|
213
216
|
bookmarks={bookmarks}
|
|
214
217
|
onToggleBookmark={onToggleBookmark}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState, type ReactNode } from 'react'
|
|
2
2
|
import type { ConnectionState, TranscriptState } from '@workerdeck/react'
|
|
3
|
-
import type { ContextUsage, RateLimitInfo } from '@workerdeck/protocol'
|
|
3
|
+
import type { ContextUsage, ProfileEngine, RateLimitInfo } from '@workerdeck/protocol'
|
|
4
4
|
import { RefreshCw, WifiOff } from 'lucide-react'
|
|
5
5
|
import { Badge } from '../ui/Badge.tsx'
|
|
6
6
|
import { ProgressRing } from '../ui/ProgressRing.tsx'
|
|
@@ -8,6 +8,7 @@ import { Spinner } from '../ui/Spinner.tsx'
|
|
|
8
8
|
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
|
+
import { contextNote } from '../../lib/context-note.ts'
|
|
11
12
|
import { meterColorClass } from '../../lib/status.ts'
|
|
12
13
|
import { cssColor } from '../../lib/css.ts'
|
|
13
14
|
import { STATUS_META } from './status.ts'
|
|
@@ -37,11 +38,12 @@ function useNow(intervalMs = 30_000): number {
|
|
|
37
38
|
return now
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function ContextMeter({ usage }: { usage: ContextUsage }) {
|
|
41
|
+
function ContextMeter({ usage, engine }: { usage: ContextUsage; engine: ProfileEngine | undefined }) {
|
|
42
|
+
const note = contextNote(engine)
|
|
41
43
|
return (
|
|
42
44
|
<Tip
|
|
43
45
|
content={
|
|
44
|
-
<div className="flex min-w-44 flex-col gap-1 py-0.5">
|
|
46
|
+
<div className="flex max-w-64 min-w-44 flex-col gap-1 py-0.5">
|
|
45
47
|
{usage.categories.map((c) => (
|
|
46
48
|
<div key={c.name} className="flex items-center gap-2">
|
|
47
49
|
<span className="size-2 shrink-0 rounded-full bg-fg-4" style={cssColor(c.color) ? { backgroundColor: c.color } : undefined} />
|
|
@@ -55,6 +57,12 @@ function ContextMeter({ usage }: { usage: ContextUsage }) {
|
|
|
55
57
|
{formatTokens(usage.totalTokens)} / {formatTokens(usage.maxTokens)} ({usage.percentage.toFixed(0)}%)
|
|
56
58
|
</span>
|
|
57
59
|
</div>
|
|
60
|
+
{note ? (
|
|
61
|
+
<div className="mt-0.5 flex flex-col gap-1 border-t border-border pt-1 text-fg-3">
|
|
62
|
+
<span>{note.summary}</span>
|
|
63
|
+
<span>{note.hint}</span>
|
|
64
|
+
</div>
|
|
65
|
+
) : null}
|
|
58
66
|
</div>
|
|
59
67
|
}
|
|
60
68
|
>
|
|
@@ -166,7 +174,7 @@ export function StatusBar({
|
|
|
166
174
|
</Slot>
|
|
167
175
|
{state.capabilities.contextUsage && state.contextUsage ? (
|
|
168
176
|
<Slot onClick={onOpenContext} hint="Context breakdown">
|
|
169
|
-
<ContextMeter usage={state.contextUsage} />
|
|
177
|
+
<ContextMeter usage={state.contextUsage} engine={state.engine} />
|
|
170
178
|
</Slot>
|
|
171
179
|
) : null}
|
|
172
180
|
{session || weekly ? (
|
|
@@ -8,6 +8,7 @@ import { cn } from '../../lib/utils.ts'
|
|
|
8
8
|
import { toolInputPreview } from '../../lib/format.ts'
|
|
9
9
|
import { toolIcon } from '../../lib/tool-icon.ts'
|
|
10
10
|
import { useToolResultFetcher } from './tool-result-fetch.tsx'
|
|
11
|
+
import { useToolTitle } from './tool-titles.tsx'
|
|
11
12
|
import { useToolResultImageSrc } from './tool-result-image.tsx'
|
|
12
13
|
import { IMAGE_UNAVAILABLE, imagePlaceholder } from '../terminal/image-box.ts'
|
|
13
14
|
|
|
@@ -110,6 +111,7 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
|
|
|
110
111
|
const badge = STATE_BADGE[status]
|
|
111
112
|
const isError = status === 'failed' || item.result?.isError === true
|
|
112
113
|
const Icon = toolIcon(item.name)
|
|
114
|
+
const title = useToolTitle(item.name)
|
|
113
115
|
|
|
114
116
|
const resultText = item.result?.text ?? ''
|
|
115
117
|
const clipped = !fullResult && resultText.length > RESULT_PREVIEW_CHARS
|
|
@@ -119,6 +121,7 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
|
|
|
119
121
|
|
|
120
122
|
const details = open ? (
|
|
121
123
|
<div className="flex flex-col gap-2 border-t border-border p-2.5">
|
|
124
|
+
{title ? <p className="font-mono text-label text-fg-4">{item.name}</p> : null}
|
|
122
125
|
<PlainPayload code={JSON.stringify(item.input, null, 2)} language="json" label="Parameters" />
|
|
123
126
|
{item.logs?.length ? <PlainPayload code={item.logs.join('\n')} label="Logs" /> : null}
|
|
124
127
|
{item.result !== undefined ? (
|
|
@@ -177,7 +180,9 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
|
|
|
177
180
|
)}
|
|
178
181
|
>
|
|
179
182
|
<Icon className="size-3.5 shrink-0 text-fg-3" />
|
|
180
|
-
<span className=
|
|
183
|
+
<span className={cn('shrink-0 text-body-sm font-medium text-fg-1', title === undefined && 'font-mono')} title={item.name}>
|
|
184
|
+
{title ?? item.name}
|
|
185
|
+
</span>
|
|
181
186
|
{item.backend && item.backend !== 'server' ? (
|
|
182
187
|
<Badge variant="neutral" className="shrink-0">
|
|
183
188
|
{item.backend}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MessageAttachment } from '@workerdeck/protocol'
|
|
2
2
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
|
-
import { formatCost, formatDuration, formatRelativeTime } from '../../lib/format.ts'
|
|
4
|
+
import { COMPACTION_TEXT, formatCost, formatDuration, formatRelativeTime } from '../../lib/format.ts'
|
|
5
5
|
import { FileCard } from './FileCard.tsx'
|
|
6
6
|
import { Message, MessageContent } from './Message.tsx'
|
|
7
7
|
import { PromptTokenText } from './PromptTokenText.tsx'
|
|
@@ -48,7 +48,7 @@ function NoticeRow({ item }: { item: Extract<TranscriptItem, { kind: 'notice' }>
|
|
|
48
48
|
)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// The item-view switch: one transcript item, drawn for the active variant.
|
|
52
52
|
export function TranscriptItemView({
|
|
53
53
|
item,
|
|
54
54
|
fileUrl,
|
|
@@ -99,6 +99,9 @@ export function TranscriptItemView({
|
|
|
99
99
|
case 'notice': {
|
|
100
100
|
return <NoticeRow item={item} />
|
|
101
101
|
}
|
|
102
|
+
case 'compaction': {
|
|
103
|
+
return <CompactionRow />
|
|
104
|
+
}
|
|
102
105
|
case 'file_delivered': {
|
|
103
106
|
return <FileCard item={item} href={fileUrl?.(item.path)} />
|
|
104
107
|
}
|
|
@@ -108,6 +111,16 @@ export function TranscriptItemView({
|
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
function CompactionRow() {
|
|
115
|
+
return (
|
|
116
|
+
<div data-slot="compaction" className="flex items-center gap-2 py-1">
|
|
117
|
+
<div className="h-px flex-1 bg-border" />
|
|
118
|
+
<span className="font-mono text-label text-fg-3">≡ {COMPACTION_TEXT}</span>
|
|
119
|
+
<div className="h-px flex-1 bg-border" />
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
export function RecapRow({ line, since, terminal }: { line: string; since?: number; terminal?: boolean }) {
|
|
112
125
|
const away = since === undefined ? undefined : formatRelativeTime(since)
|
|
113
126
|
const text = away ? `${line} · last here ${away}` : line
|
|
@@ -90,12 +90,10 @@ function StickyPromptLane({
|
|
|
90
90
|
)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* corrects so the scrollback holds still (GOTCHAS "The transcript is virtualized").
|
|
98
|
-
*/
|
|
93
|
+
// The virtualized row lane and its scroll-ownership regime. Two things want to write
|
|
94
|
+
// `scrollTop` — the follow spring and the virtualizer's size-change corrections — and they are
|
|
95
|
+
// split by regime here: pinned, corrections are suppressed outright; escaped, the virtualizer
|
|
96
|
+
// corrects so the scrollback holds still (GOTCHAS "The transcript is virtualized").
|
|
99
97
|
export function TranscriptRows({
|
|
100
98
|
rows,
|
|
101
99
|
boundary,
|