@workerdeck/ui 2.13.0 → 2.15.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.
Files changed (36) hide show
  1. package/build/{SessionPanel-CJwp5B3q.d.mts → SessionPanel-BVKqPs_3.d.mts} +9 -3
  2. package/build/index.d.mts +84 -6
  3. package/build/index.mjs +83 -37
  4. package/build/index.mjs.map +1 -1
  5. package/build/scoped.css +24 -3
  6. package/build/{use-path-links-CepFsMdW.mjs → use-path-links-BgWXmfAS.mjs} +849 -236
  7. package/build/use-path-links-BgWXmfAS.mjs.map +1 -0
  8. package/build/workspace.d.mts +19 -4
  9. package/build/workspace.mjs +7 -4
  10. package/build/workspace.mjs.map +1 -1
  11. package/package.json +16 -6
  12. package/src/components/agent/Composer.tsx +18 -13
  13. package/src/components/agent/SessionBrowser.tsx +24 -1
  14. package/src/components/agent/SessionItem.tsx +13 -3
  15. package/src/components/agent/SessionPanel.tsx +264 -211
  16. package/src/components/agent/SessionSteps.tsx +78 -27
  17. package/src/components/agent/SessionWorkspace.tsx +10 -1
  18. package/src/components/agent/ShellStrip.tsx +71 -0
  19. package/src/components/agent/ShellTerminal.tsx +170 -0
  20. package/src/components/agent/SubagentToggle.tsx +7 -1
  21. package/src/components/agent/TranscriptItemView.tsx +74 -1
  22. package/src/components/agent/scrubber-marks.ts +9 -1
  23. package/src/components/agent/shell-actions.tsx +25 -0
  24. package/src/components/agent/use-shell-frame.ts +111 -0
  25. package/src/components/terminal/TerminalTranscript.tsx +4 -0
  26. package/src/components/terminal/affordances.tsx +37 -0
  27. package/src/components/terminal/height.ts +14 -0
  28. package/src/components/terminal/items.tsx +72 -3
  29. package/src/components/terminal/press.tsx +5 -3
  30. package/src/components/terminal/scrubber.tsx +9 -1
  31. package/src/components/terminal/shell-row.ts +106 -0
  32. package/src/index.ts +12 -1
  33. package/src/styles/terminal.css +11 -0
  34. package/src/styles/theme.css +3 -0
  35. package/src/workspace.ts +1 -0
  36. package/build/use-path-links-CepFsMdW.mjs.map +0 -1
@@ -20,6 +20,7 @@ import {
20
20
  type PermissionRequest,
21
21
  type RateLimitInfo,
22
22
  type SessionTask,
23
+ type ShellInfo,
23
24
  type SkillInfo,
24
25
  type SubagentInfo,
25
26
  } from '@workerdeck/protocol'
@@ -60,6 +61,9 @@ import { PermissionModeSelect, permissionModeChoices, type PermissionModeChoice
60
61
  import { PermissionPrompt } from './PermissionPrompt.tsx'
61
62
  import { SubagentStrip } from './SubagentStrip.tsx'
62
63
  import { useSubagentFrame } from './use-subagent-frame.ts'
64
+ import { ShellStrip } from './ShellStrip.tsx'
65
+ import { ShellTerminal } from './ShellTerminal.tsx'
66
+ import { useShellFrame } from './use-shell-frame.ts'
63
67
  import { QuestionPrompt, parseUserQuestions } from './QuestionPrompt.tsx'
64
68
  import { TerminalPermissionPrompt } from '../terminal/PermissionPrompt.tsx'
65
69
  import { TerminalQuestionPrompt } from '../terminal/QuestionPrompt.tsx'
@@ -71,6 +75,7 @@ import { SessionInfoDialog } from './SessionInfoDialog.tsx'
71
75
  import { StatusBar } from './StatusBar.tsx'
72
76
  import { Transcript } from './Transcript.tsx'
73
77
  import { ToolResultFetchProvider } from './tool-result-fetch.tsx'
78
+ import { ShellActionsProvider } from './shell-actions.tsx'
74
79
  import { ToolTitleProvider } from './tool-titles.tsx'
75
80
  import { ToolResultImageProvider, useToolResultImages } from './tool-result-image.tsx'
76
81
  import {
@@ -143,8 +148,12 @@ export interface SessionPanelProps {
143
148
  reveal?: { toolUseId: string; nonce: number }
144
149
  openSubagent?: { toolUseId: string; nonce: number }
145
150
  onSubagentChange?: (toolUseId: string | undefined) => void
151
+ // Host request to drill in to a shell's terminal; `shellId: undefined` withdraws it (Back/Forward).
152
+ openShell?: { shellId: string; nonce: number }
153
+ onShellChange?: (shellId: string | undefined) => void
146
154
  // The attach frame's `state.session` never refreshes, so a host that polls the sessions list passes the live records here.
147
155
  subagents?: SubagentInfo[]
156
+ shells?: ShellInfo[]
148
157
  stickyPrompt?: boolean
149
158
  transcriptDensity?: TranscriptDensity
150
159
  transcriptFont?: TranscriptFont
@@ -234,8 +243,11 @@ export function SessionPanel({
234
243
  onToggleBookmark,
235
244
  reveal,
236
245
  subagents,
246
+ shells,
237
247
  openSubagent,
238
248
  onSubagentChange,
249
+ openShell,
250
+ onShellChange,
239
251
  stickyPrompt = false,
240
252
  controlsSurface = 'internal',
241
253
  onControls,
@@ -283,7 +295,22 @@ export function SessionPanel({
283
295
  setPermissionMode,
284
296
  reconnectNow,
285
297
  loadFullResult,
298
+ loadShellOutput,
299
+ verifyShell,
300
+ killShell,
286
301
  } = useClaudeSession(client, sessionId, { onProtocolError: setProtocolError, cacheTranscript })
302
+ const {
303
+ shellId: framedShellId,
304
+ enterShell,
305
+ leaveShell,
306
+ returnReveal: shellReturnReveal,
307
+ shell: framedShell,
308
+ label: framedShellLabel,
309
+ } = useShellFrame({ sessionId, items: state.items, session: state.session, shells, reveal, openShell, onShellChange })
310
+ const shellActions = useMemo(
311
+ () => ({ loadOutput: loadShellOutput, verify: verifyShell, kill: killShell, open: enterShell }),
312
+ [loadShellOutput, verifyShell, killShell, enterShell],
313
+ )
287
314
  useEffect(() => setProtocolError(undefined), [sessionId])
288
315
  const {
289
316
  subagentId,
@@ -295,6 +322,15 @@ export function SessionPanel({
295
322
  fallbackLabel: subagentFallbackLabel,
296
323
  } = useSubagentFrame({ sessionId, items: state.items, session: state.session, reveal, openSubagent, onSubagentChange })
297
324
 
325
+ // Two frames, one reveal slot: the more recent exit wins, so leaving a shell never lands on the
326
+ // row a sub-agent frame left behind.
327
+ const frameReturnReveal = useMemo(() => {
328
+ if (returnReveal && shellReturnReveal) {
329
+ return returnReveal.nonce >= shellReturnReveal.nonce ? returnReveal : shellReturnReveal
330
+ }
331
+ return returnReveal ?? shellReturnReveal
332
+ }, [returnReveal, shellReturnReveal])
333
+
298
334
  const [caughtUp, setCaughtUp] = useState(false)
299
335
  useEffect(() => {
300
336
  setCaughtUp(false)
@@ -667,227 +703,244 @@ export function SessionPanel({
667
703
  <TranscriptDensityProvider value={transcriptDensity}>
668
704
  <FileLinkProvider value={fileLinks}>
669
705
  <ToolResultFetchProvider value={loadFullResult}>
670
- <ToolTitleProvider value={state.toolTitles}>
671
- <ToolResultImageProvider value={resultImages}>
672
- <div
673
- ref={panelRef}
674
- data-slot="session-panel"
675
- data-agent-font={transcriptFont}
676
- onClick={handleClick}
677
- className={cn('flex h-full min-h-0 flex-col overflow-hidden bg-bg', className)}
678
- style={fontSize !== undefined ? ({ '--wd-font-size': `${Math.round(fontSize)}px` } as React.CSSProperties) : undefined}
679
- >
680
- {headerTakesActions ? header({ actions: menu }) : header}
681
- {statusPlacement === 'top' ? statusBar : null}
682
- {protocolMismatch !== undefined ? (
683
- <Notice level="warning">
684
- Server speaks protocol v{protocolMismatch}, this build renders v{PROTOCOL_VERSION}. Some events may not render.
685
- </Notice>
686
- ) : null}
687
- {protocolError ? (
688
- <Notice level="error" onDismiss={() => setProtocolError(undefined)}>
689
- {protocolError}
690
- </Notice>
691
- ) : null}
692
- {subagentId !== undefined ? (
693
- <SubagentStrip
694
- task={subagentTask}
695
- items={subagentFrameItems}
696
- label={subagentFallbackLabel}
697
- onBack={leaveSubagent}
698
- terminal={terminal}
699
- fontSize={effectiveTermFontSize}
700
- lineHeight={effectiveTermLineHeight}
701
- />
702
- ) : null}
703
- <BookmarkProvider value={bookmarkHandle}>
704
- <Transcript
705
- key={subagentId ?? 'session'}
706
- state={state}
707
- fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
708
- attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
709
- canBrowseFiles={hostFiles.available}
710
- sessionNames={peers.names}
711
- hostImage={hostImage}
712
- variant={transcriptVariant}
713
- density={transcriptDensity}
714
- fontSize={effectiveTermFontSize}
715
- lineHeight={effectiveTermLineHeight}
716
- affordances={affordances}
717
- stickyPrompt={stickyPrompt}
718
- scrubber={scrubber}
719
- bookmarks={bookmarks}
720
- replaying={replaying}
721
- catchUp={catchUp && newCount > 0 ? { from: catchUp.itemCount, since: catchUp.since } : undefined}
722
- reveal={taskReveal ?? returnReveal ?? reveal}
723
- frame={subagentId === undefined ? undefined : { parentToolUseId: subagentId }}
724
- onOpenSubagent={enterSubagent}
725
- emptyState={emptyState}
726
- jumpToRecapRef={jumpToRecap}
727
- repinRef={repinTranscript}
728
- />
729
- </BookmarkProvider>
730
- {catchUp && newCount > 0 && !replaying && subagentId === undefined ? (
731
- <div className="px-3 pb-1">
732
- <div
733
- data-slot="catch-up"
734
- className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3"
735
- >
736
- <span aria-hidden className={cn('select-none', terminal ? 'text-fg-3' : 'text-accent')}>
737
- ※
738
- </span>
739
- <span className="min-w-0 flex-1 truncate">
740
- {newCount} new {newCount === 1 ? 'row' : 'rows'}
741
- {catchUp.since !== undefined ? ` since you were last here` : ''}
742
- </span>
743
- <button
744
- type="button"
745
- onClick={() => jumpToRecap.current?.()}
746
- className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
747
- >
748
- jump
749
- </button>
750
- <button
751
- type="button"
752
- onClick={() => setCaughtUp(true)}
753
- className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
706
+ <ShellActionsProvider value={shellActions}>
707
+ <ToolTitleProvider value={state.toolTitles}>
708
+ <ToolResultImageProvider value={resultImages}>
709
+ <div
710
+ ref={panelRef}
711
+ data-slot="session-panel"
712
+ data-agent-font={transcriptFont}
713
+ onClick={handleClick}
714
+ className={cn('flex h-full min-h-0 flex-col overflow-hidden bg-bg', className)}
715
+ style={fontSize !== undefined ? ({ '--wd-font-size': `${Math.round(fontSize)}px` } as React.CSSProperties) : undefined}
716
+ >
717
+ {headerTakesActions ? header({ actions: menu }) : header}
718
+ {statusPlacement === 'top' ? statusBar : null}
719
+ {protocolMismatch !== undefined ? (
720
+ <Notice level="warning">
721
+ Server speaks protocol v{protocolMismatch}, this build renders v{PROTOCOL_VERSION}. Some events may not render.
722
+ </Notice>
723
+ ) : null}
724
+ {protocolError ? (
725
+ <Notice level="error" onDismiss={() => setProtocolError(undefined)}>
726
+ {protocolError}
727
+ </Notice>
728
+ ) : null}
729
+ {framedShellId !== undefined ? (
730
+ <ShellStrip
731
+ shell={framedShell}
732
+ label={framedShellLabel}
733
+ onBack={leaveShell}
734
+ onKill={() => void killShell(framedShellId)}
735
+ terminal={terminal}
736
+ fontSize={effectiveTermFontSize}
737
+ lineHeight={effectiveTermLineHeight}
738
+ />
739
+ ) : null}
740
+ {framedShellId === undefined && subagentId !== undefined ? (
741
+ <SubagentStrip
742
+ task={subagentTask}
743
+ items={subagentFrameItems}
744
+ label={subagentFallbackLabel}
745
+ onBack={leaveSubagent}
746
+ terminal={terminal}
747
+ fontSize={effectiveTermFontSize}
748
+ lineHeight={effectiveTermLineHeight}
749
+ />
750
+ ) : null}
751
+ {framedShellId !== undefined ? (
752
+ <ShellTerminal key={framedShellId} handle={handle} shellId={framedShellId} fontSize={effectiveTermFontSize} />
753
+ ) : (
754
+ <BookmarkProvider value={bookmarkHandle}>
755
+ <Transcript
756
+ key={subagentId ?? 'session'}
757
+ state={state}
758
+ fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
759
+ attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
760
+ canBrowseFiles={hostFiles.available}
761
+ sessionNames={peers.names}
762
+ hostImage={hostImage}
763
+ variant={transcriptVariant}
764
+ density={transcriptDensity}
765
+ fontSize={effectiveTermFontSize}
766
+ lineHeight={effectiveTermLineHeight}
767
+ affordances={affordances}
768
+ stickyPrompt={stickyPrompt}
769
+ scrubber={scrubber}
770
+ bookmarks={bookmarks}
771
+ replaying={replaying}
772
+ catchUp={catchUp && newCount > 0 ? { from: catchUp.itemCount, since: catchUp.since } : undefined}
773
+ reveal={taskReveal ?? frameReturnReveal ?? reveal}
774
+ frame={subagentId === undefined ? undefined : { parentToolUseId: subagentId }}
775
+ onOpenSubagent={enterSubagent}
776
+ emptyState={emptyState}
777
+ jumpToRecapRef={jumpToRecap}
778
+ repinRef={repinTranscript}
779
+ />
780
+ </BookmarkProvider>
781
+ )}
782
+ {catchUp && newCount > 0 && !replaying && subagentId === undefined && framedShellId === undefined ? (
783
+ <div className="px-3 pb-1">
784
+ <div
785
+ data-slot="catch-up"
786
+ className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3"
754
787
  >
755
- dismiss
756
- </button>
788
+ <span aria-hidden className={cn('select-none', terminal ? 'text-fg-3' : 'text-accent')}>
789
+ ※
790
+ </span>
791
+ <span className="min-w-0 flex-1 truncate">
792
+ {newCount} new {newCount === 1 ? 'row' : 'rows'}
793
+ {catchUp.since !== undefined ? ` since you were last here` : ''}
794
+ </span>
795
+ <button
796
+ type="button"
797
+ onClick={() => jumpToRecap.current?.()}
798
+ className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
799
+ >
800
+ jump
801
+ </button>
802
+ <button
803
+ type="button"
804
+ onClick={() => setCaughtUp(true)}
805
+ className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
806
+ >
807
+ dismiss
808
+ </button>
809
+ </div>
757
810
  </div>
758
- </div>
759
- ) : null}
760
- {!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
761
- <div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
762
- <PromptSurface
763
- terminal={terminal}
764
- metrics={{ fontSize: effectiveTermFontSize, lineHeight: effectiveTermLineHeight }}
765
- affordances={affordances}
766
- >
767
- {state.pendingApprovals.map((request) => {
768
- // Before the variant split: a host's entry is the renderer for its tool in
769
- // both themes, and it overrides the built-in entries rather than racing them.
770
- const HostPrompt = approvalPrompts?.[request.toolName]
771
- if (HostPrompt) {
772
- return <HostPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
773
- }
774
- const isQuestion = request.toolName === 'AskUserQuestion' && parseUserQuestions(request.input).length > 0
775
- if (terminal) {
811
+ ) : null}
812
+ {!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
813
+ <div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
814
+ <PromptSurface
815
+ terminal={terminal}
816
+ metrics={{ fontSize: effectiveTermFontSize, lineHeight: effectiveTermLineHeight }}
817
+ affordances={affordances}
818
+ >
819
+ {state.pendingApprovals.map((request) => {
820
+ // Before the variant split: a host's entry is the renderer for its tool in
821
+ // both themes, and it overrides the built-in entries rather than racing them.
822
+ const HostPrompt = approvalPrompts?.[request.toolName]
823
+ if (HostPrompt) {
824
+ return <HostPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
825
+ }
826
+ const isQuestion = request.toolName === 'AskUserQuestion' && parseUserQuestions(request.input).length > 0
827
+ if (terminal) {
828
+ return isQuestion ? (
829
+ <TerminalQuestionPrompt
830
+ key={request.id}
831
+ request={request}
832
+ onAnswer={approve}
833
+ onDismiss={(id) => deny(id, 'Question dismissed by user')}
834
+ />
835
+ ) : (
836
+ <TerminalPermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
837
+ )
838
+ }
776
839
  return isQuestion ? (
777
- <TerminalQuestionPrompt
840
+ <QuestionPrompt
778
841
  key={request.id}
779
842
  request={request}
780
843
  onAnswer={approve}
781
844
  onDismiss={(id) => deny(id, 'Question dismissed by user')}
782
845
  />
783
846
  ) : (
784
- <TerminalPermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
847
+ <PermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
785
848
  )
786
- }
787
- return isQuestion ? (
788
- <QuestionPrompt
789
- key={request.id}
790
- request={request}
791
- onAnswer={approve}
792
- onDismiss={(id) => deny(id, 'Question dismissed by user')}
793
- />
794
- ) : (
795
- <PermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
796
- )
797
- })}
798
- </PromptSurface>
799
- </div>
800
- ) : null}
801
- {readOnly || subagentId !== undefined ? null : (
802
- <>
803
- <Composer
804
- ref={composerRef}
805
- onSend={handleSend}
806
- onInterrupt={interrupt}
807
- busy={busy}
808
- disabled={ended || !sessionId}
809
- commands={capabilities.slashCommands ? state.commands : undefined}
810
- skills={capabilities.skillsList ? state.skills : undefined}
811
- clientCommands={clientCommands}
812
- attachments={attachments}
813
- draft={draft}
814
- onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
815
- peers={peers.peers}
816
- onShellCommand={shell ? runShell : undefined}
817
- layout={controlsExternal ? 'inline' : 'stacked'}
818
- toolbar={controlsExternal ? undefined : sessionControls}
819
- fontSize={effectiveTermFontSize}
820
- lineHeight={effectiveTermLineHeight}
821
- affordances={affordances}
822
- />
823
- </>
824
- )}
825
- {statusPlacement === 'bottom' ? statusBar : null}
826
-
827
- {!external ? (
828
- <>
829
- <SessionInfoDialog
830
- state={state}
831
- client={client}
832
- sessionId={sessionId}
833
- open={panel === 'info'}
834
- onOpenChange={(next) => setPanel(next ? 'info' : undefined)}
835
- />
836
- <ContextDialog
837
- usage={state.contextUsage}
838
- engine={state.engine ?? 'claude'}
839
- open={panel === 'context'}
840
- onOpenChange={(next) => setPanel(next ? 'context' : undefined)}
841
- />
842
- <UsageDialog
843
- rateLimits={windows}
844
- subscriptionType={state.subscriptionType}
845
- engine={state.engine ?? 'claude'}
846
- totalCostUsd={state.totalCostUsd}
847
- costUsd={state.costUsd}
848
- usageByModel={state.usageByModel}
849
- pricing={pricing}
850
- spend={profileSpend}
851
- updatedAt={usageUpdatedAt}
852
- open={panel === 'usage'}
853
- onOpenChange={(next) => setPanel(next ? 'usage' : undefined)}
854
- />
855
- <McpDialog
856
- client={client}
857
- sessionId={sessionId}
858
- canManageServers={capabilities.mcpServerActions}
859
- open={panel === 'mcp'}
860
- onOpenChange={(next) => setPanel(next ? 'mcp' : undefined)}
861
- />
862
- <TasksDialog
863
- tasks={tasks}
864
- showCompleted={showCompletedTasks}
865
- onShowCompletedChange={setShowCompletedTasks}
866
- onSelectTask={(task) => {
867
- if (task.toolUseId) {
868
- setTaskReveal({ toolUseId: task.toolUseId, nonce: Date.now() })
869
- }
870
- }}
871
- open={panel === 'tasks'}
872
- onOpenChange={(next) => setPanel(next ? 'tasks' : undefined)}
873
- />
874
- <SkillsDialog
875
- skills={state.skills}
876
- open={panel === 'skills'}
877
- onOpenChange={(next) => setPanel(next ? 'skills' : undefined)}
878
- onUse={(skill) => composerRef.current?.insertText(skillPrompt(skill))}
879
- />
880
- <HostFilesDialog
881
- client={client}
882
- cwd={state.cwd}
883
- open={panel === 'files'}
884
- onOpenChange={(next) => setPanel(next ? 'files' : undefined)}
885
- />
886
- </>
887
- ) : null}
888
- </div>
889
- </ToolResultImageProvider>
890
- </ToolTitleProvider>
849
+ })}
850
+ </PromptSurface>
851
+ </div>
852
+ ) : null}
853
+ {readOnly || subagentId !== undefined || framedShellId !== undefined ? null : (
854
+ <>
855
+ <Composer
856
+ ref={composerRef}
857
+ onSend={handleSend}
858
+ onInterrupt={interrupt}
859
+ busy={busy}
860
+ disabled={ended || !sessionId}
861
+ commands={capabilities.slashCommands ? state.commands : undefined}
862
+ skills={capabilities.skillsList ? state.skills : undefined}
863
+ clientCommands={clientCommands}
864
+ attachments={attachments}
865
+ draft={draft}
866
+ onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
867
+ peers={peers.peers}
868
+ onShellCommand={shell ? runShell : undefined}
869
+ layout={controlsExternal ? 'inline' : 'stacked'}
870
+ toolbar={controlsExternal ? undefined : sessionControls}
871
+ fontSize={effectiveTermFontSize}
872
+ lineHeight={effectiveTermLineHeight}
873
+ affordances={affordances}
874
+ />
875
+ </>
876
+ )}
877
+ {statusPlacement === 'bottom' ? statusBar : null}
878
+
879
+ {!external ? (
880
+ <>
881
+ <SessionInfoDialog
882
+ state={state}
883
+ client={client}
884
+ sessionId={sessionId}
885
+ open={panel === 'info'}
886
+ onOpenChange={(next) => setPanel(next ? 'info' : undefined)}
887
+ />
888
+ <ContextDialog
889
+ usage={state.contextUsage}
890
+ engine={state.engine ?? 'claude'}
891
+ open={panel === 'context'}
892
+ onOpenChange={(next) => setPanel(next ? 'context' : undefined)}
893
+ />
894
+ <UsageDialog
895
+ rateLimits={windows}
896
+ subscriptionType={state.subscriptionType}
897
+ engine={state.engine ?? 'claude'}
898
+ totalCostUsd={state.totalCostUsd}
899
+ costUsd={state.costUsd}
900
+ usageByModel={state.usageByModel}
901
+ pricing={pricing}
902
+ spend={profileSpend}
903
+ updatedAt={usageUpdatedAt}
904
+ open={panel === 'usage'}
905
+ onOpenChange={(next) => setPanel(next ? 'usage' : undefined)}
906
+ />
907
+ <McpDialog
908
+ client={client}
909
+ sessionId={sessionId}
910
+ canManageServers={capabilities.mcpServerActions}
911
+ open={panel === 'mcp'}
912
+ onOpenChange={(next) => setPanel(next ? 'mcp' : undefined)}
913
+ />
914
+ <TasksDialog
915
+ tasks={tasks}
916
+ showCompleted={showCompletedTasks}
917
+ onShowCompletedChange={setShowCompletedTasks}
918
+ onSelectTask={(task) => {
919
+ if (task.toolUseId) {
920
+ setTaskReveal({ toolUseId: task.toolUseId, nonce: Date.now() })
921
+ }
922
+ }}
923
+ open={panel === 'tasks'}
924
+ onOpenChange={(next) => setPanel(next ? 'tasks' : undefined)}
925
+ />
926
+ <SkillsDialog
927
+ skills={state.skills}
928
+ open={panel === 'skills'}
929
+ onOpenChange={(next) => setPanel(next ? 'skills' : undefined)}
930
+ onUse={(skill) => composerRef.current?.insertText(skillPrompt(skill))}
931
+ />
932
+ <HostFilesDialog
933
+ client={client}
934
+ cwd={state.cwd}
935
+ open={panel === 'files'}
936
+ onOpenChange={(next) => setPanel(next ? 'files' : undefined)}
937
+ />
938
+ </>
939
+ ) : null}
940
+ </div>
941
+ </ToolResultImageProvider>
942
+ </ToolTitleProvider>
943
+ </ShellActionsProvider>
891
944
  </ToolResultFetchProvider>
892
945
  </FileLinkProvider>
893
946
  </TranscriptDensityProvider>