@workerdeck/ui 2.7.2 → 2.9.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 (71) hide show
  1. package/README.md +30 -30
  2. package/build/{SessionPanel-B5FfiBxE.mjs → SessionPanel-BClL7zUS.mjs} +694 -413
  3. package/build/SessionPanel-BClL7zUS.mjs.map +1 -0
  4. package/build/{SessionPanel-_lQLhWOw.d.mts → SessionPanel-BVbau2eR.d.mts} +68 -4
  5. package/build/format.mjs +1 -1
  6. package/build/index.d.mts +22 -15
  7. package/build/index.mjs +6 -6
  8. package/build/index.mjs.map +1 -1
  9. package/build/scoped.css +26 -2
  10. package/build/{status-BO9wJloK.mjs → status-kmsBhuFG.mjs} +3 -3
  11. package/build/status-kmsBhuFG.mjs.map +1 -0
  12. package/build/workspace.d.mts +3 -2
  13. package/build/workspace.mjs +7 -8
  14. package/build/workspace.mjs.map +1 -1
  15. package/package.json +18 -18
  16. package/src/components/agent/Composer.tsx +86 -84
  17. package/src/components/agent/ContextDialog.tsx +1 -1
  18. package/src/components/agent/EntryIcon.tsx +1 -1
  19. package/src/components/agent/FileTree.tsx +1 -1
  20. package/src/components/agent/FileViewer.tsx +1 -1
  21. package/src/components/agent/HostFilesDialog.tsx +2 -2
  22. package/src/components/agent/PermissionModeSelect.tsx +2 -2
  23. package/src/components/agent/PermissionPrompt.tsx +2 -2
  24. package/src/components/agent/SessionBrowser.tsx +1 -1
  25. package/src/components/agent/SessionItem.tsx +4 -2
  26. package/src/components/agent/SessionPanel.tsx +306 -226
  27. package/src/components/agent/SessionWorkspace.tsx +3 -0
  28. package/src/components/agent/SkillsDialog.tsx +3 -3
  29. package/src/components/agent/StatusBar.tsx +1 -1
  30. package/src/components/agent/TaskList.tsx +2 -2
  31. package/src/components/agent/TranscriptRows.tsx +1 -1
  32. package/src/components/agent/UsageDialog.tsx +1 -1
  33. package/src/components/agent/UsageMeters.tsx +17 -4
  34. package/src/components/agent/composer-commands.ts +142 -0
  35. package/src/components/agent/use-path-links.ts +2 -7
  36. package/src/components/agent/use-subagent-frame.ts +2 -2
  37. package/src/components/agent/use-transcript-jumps.ts +2 -2
  38. package/src/components/prompt-area/clipboard-helpers.ts +2 -2
  39. package/src/components/prompt-area/cursor-helpers.ts +7 -7
  40. package/src/components/prompt-area/dom-helpers.ts +8 -8
  41. package/src/components/prompt-area/html-to-markdown.ts +2 -2
  42. package/src/components/prompt-area/prompt-area-engine.ts +4 -4
  43. package/src/components/prompt-area/prompt-area-list-ops.ts +10 -10
  44. package/src/components/prompt-area/prompt-area.tsx +2 -2
  45. package/src/components/prompt-area/trigger-presets.ts +6 -6
  46. package/src/components/prompt-area/types.ts +6 -6
  47. package/src/components/prompt-area/use-chip-editing.ts +8 -8
  48. package/src/components/prompt-area/use-markdown-mode.ts +3 -3
  49. package/src/components/prompt-area/use-prompt-area-events.ts +3 -3
  50. package/src/components/prompt-area/use-prompt-area-keydown.ts +3 -3
  51. package/src/components/prompt-area/use-prompt-area-state.ts +4 -4
  52. package/src/components/prompt-area/use-prompt-area.ts +5 -5
  53. package/src/components/prompt-area/use-trigger-search.ts +1 -1
  54. package/src/components/terminal/PermissionPrompt.tsx +4 -4
  55. package/src/components/terminal/affordances.tsx +1 -1
  56. package/src/components/terminal/file-link.tsx +20 -0
  57. package/src/components/terminal/height.ts +2 -2
  58. package/src/components/terminal/items.tsx +2 -2
  59. package/src/components/terminal/markdown.tsx +31 -5
  60. package/src/components/terminal/scrubber.tsx +1 -1
  61. package/src/components/terminal/todos.ts +1 -1
  62. package/src/components/ui/Splitter.tsx +1 -1
  63. package/src/index.ts +11 -1
  64. package/src/lib/context-note.ts +3 -3
  65. package/src/lib/file-link.ts +70 -0
  66. package/src/lib/format.ts +2 -2
  67. package/src/styles/scoped.entry.css +2 -2
  68. package/src/styles/terminal.css +94 -79
  69. package/src/styles/theme.css +39 -39
  70. package/build/SessionPanel-B5FfiBxE.mjs.map +0 -1
  71. package/build/status-BO9wJloK.mjs.map +0 -1
@@ -40,7 +40,15 @@ import { ChartPie, FolderTree, Gauge, Info, MoreHorizontal, Plug, Sparkles, Tria
40
40
  import { cn } from '../../lib/utils.ts'
41
41
  import { Button } from '../ui/Button.tsx'
42
42
  import { Menu, MenuContent, MenuItem, MenuTrigger } from '../ui/Menu.tsx'
43
- import { Composer, skillPrompt, type ComposerHandle } from './Composer.tsx'
43
+ import { Composer, type ComposerHandle } from './Composer.tsx'
44
+ import {
45
+ composerCommandRows,
46
+ matchClientCommand,
47
+ mergeComposerRows,
48
+ skillPrompt,
49
+ type ClientCommand,
50
+ type ComposerCommandRow,
51
+ } from './composer-commands.ts'
44
52
  import { ContextDialog } from './ContextDialog.tsx'
45
53
  import { TasksDialog } from './TasksDialog.tsx'
46
54
  import { HostFilesDialog } from './HostFilesDialog.tsx'
@@ -56,6 +64,7 @@ import { TerminalPermissionPrompt } from '../terminal/PermissionPrompt.tsx'
56
64
  import { TerminalQuestionPrompt } from '../terminal/QuestionPrompt.tsx'
57
65
  import { TerminalSurface } from '../terminal/surface.tsx'
58
66
  import { BookmarkProvider, type BookmarkHandle, type TerminalAffordances } from '../terminal/affordances.tsx'
67
+ import { FileLinkProvider, type FileLinkOpener } from '../terminal/file-link.tsx'
59
68
 
60
69
  import { SessionInfoDialog } from './SessionInfoDialog.tsx'
61
70
  import { StatusBar } from './StatusBar.tsx'
@@ -122,9 +131,11 @@ export interface SessionPanelProps {
122
131
  affordances?: TerminalAffordances | boolean
123
132
  terminalMetrics?: TerminalMetrics
124
133
  scrubber?: boolean
125
- // Bookmarked transcript item ids — the host owns membership and persistence.
134
+ // Bookmarked transcript item ids - the host owns membership and persistence.
126
135
  bookmarks?: readonly string[]
127
136
  onToggleBookmark?: (itemId: string) => void
137
+ // Where a file link in the transcript goes. Without it, such a link stays an ordinary anchor.
138
+ onOpenFile?: FileLinkOpener
128
139
  reveal?: { toolUseId: string; nonce: number }
129
140
  openSubagent?: { toolUseId: string; nonce: number }
130
141
  onSubagentChange?: (toolUseId: string | undefined) => void
@@ -185,11 +196,13 @@ export type SessionVitals = {
185
196
  permissionMode: TranscriptState['permissionMode']
186
197
  permissionModes: PermissionModeChoice[]
187
198
  skills: SkillInfo[] | undefined
199
+ // Everything `/` offers this session, flattened for a native host that cannot run the composer's own merge.
200
+ composerCommands: ComposerCommandRow[]
188
201
  cwd: TranscriptState['cwd']
189
202
  contextUsage: TranscriptState['contextUsage']
190
203
  tasks: SessionTask[]
191
204
  rateLimits: TranscriptState['rateLimits']
192
- // When the newest window in `rateLimits` was reported, as event time — not receive time. External chrome (the
205
+ // When the newest window in `rateLimits` was reported, as event time - not receive time. External chrome (the
193
206
  // VS Code status bar, iOS) cannot otherwise tell a live reading from one a days-old session just replayed.
194
207
  // Absent means unknown, which is not the same as fresh.
195
208
  rateLimitsUpdatedAt: number | undefined
@@ -230,6 +243,7 @@ export function SessionPanel({
230
243
  cacheTranscript,
231
244
  emptyState,
232
245
  onLinkClick,
246
+ onOpenFile,
233
247
  fontSize,
234
248
  className,
235
249
  }: SessionPanelProps) {
@@ -258,6 +272,7 @@ export function SessionPanel({
258
272
  approve,
259
273
  deny,
260
274
  interrupt,
275
+ clearContext,
261
276
  setModel,
262
277
  setPermissionMode,
263
278
  reconnectNow,
@@ -334,6 +349,79 @@ export function SessionPanel({
334
349
  [state.checklist, subagents, state.session?.subagents],
335
350
  )
336
351
 
352
+ const clientCommands = useMemo((): ClientCommand[] => {
353
+ const modes = capabilities.permissionModes
354
+ const built: ClientCommand[] = []
355
+ if (models.length > 0) {
356
+ built.push({
357
+ name: 'model',
358
+ description: 'Switch the model for this session',
359
+ argumentHint: '<model>',
360
+ requiresArgs: true,
361
+ run: (args) => {
362
+ const wanted = args.split(/\s+/)[0]
363
+ if (!wanted) {
364
+ return false
365
+ }
366
+ setModel(wanted)
367
+ return true
368
+ },
369
+ })
370
+ }
371
+ if (modes.length > 1) {
372
+ built.push({
373
+ name: 'permissions',
374
+ description: `Set the permission mode (${modes.join(', ')})`,
375
+ argumentHint: '<mode>',
376
+ requiresArgs: true,
377
+ run: (args) => {
378
+ const wanted = args.split(/\s+/)[0] as PermissionMode
379
+ if (!modes.includes(wanted)) {
380
+ return false
381
+ }
382
+ setPermissionMode(wanted)
383
+ return true
384
+ },
385
+ })
386
+ }
387
+ if (capabilities.clearContext) {
388
+ built.push({
389
+ name: 'clear',
390
+ description: 'Clear the conversation - the session keeps running and the old one stays resumable',
391
+ run: () => {
392
+ clearContext()
393
+ return true
394
+ },
395
+ })
396
+ }
397
+ if (capabilities.mcpStatus) {
398
+ built.push({ name: 'mcp', description: 'MCP servers and their status', run: () => (openPanel('mcp'), true) })
399
+ }
400
+ if (capabilities.contextUsage) {
401
+ built.push({ name: 'context', description: 'Context window usage', run: () => (openPanel('context'), true) })
402
+ }
403
+ built.push({ name: 'status', description: 'Session details', run: () => (openPanel('info'), true) })
404
+ if (capabilities.rateLimits) {
405
+ built.push({ name: 'usage', description: 'Rate limits and spend', run: () => (openPanel('usage'), true) })
406
+ }
407
+ if (capabilities.skillsList) {
408
+ built.push({ name: 'skills', description: 'Browse the skills this session can use', run: () => (openPanel('skills'), true) })
409
+ }
410
+ return built
411
+ }, [capabilities, models.length, openPanel, clearContext, setModel, setPermissionMode])
412
+
413
+ const composerCommands = useMemo(
414
+ () =>
415
+ composerCommandRows(
416
+ mergeComposerRows({
417
+ commands: capabilities.slashCommands ? state.commands : undefined,
418
+ clientCommands,
419
+ skills: capabilities.skillsList ? state.skills : undefined,
420
+ }),
421
+ ),
422
+ [capabilities.slashCommands, capabilities.skillsList, state.commands, state.skills, clientCommands],
423
+ )
424
+
337
425
  const onVitalsRef = useRef(onVitals)
338
426
  onVitalsRef.current = onVitals
339
427
  const vitalsModel = effectiveModel ?? state.model
@@ -352,6 +440,7 @@ export function SessionPanel({
352
440
  permissionMode: state.permissionMode,
353
441
  permissionModes,
354
442
  skills: state.skills,
443
+ composerCommands,
355
444
  cwd: state.cwd,
356
445
  contextUsage: state.contextUsage,
357
446
  tasks,
@@ -370,6 +459,7 @@ export function SessionPanel({
370
459
  state.permissionMode,
371
460
  permissionModes,
372
461
  state.skills,
462
+ composerCommands,
373
463
  state.cwd,
374
464
  state.contextUsage,
375
465
  tasks,
@@ -423,25 +513,10 @@ export function SessionPanel({
423
513
  const jumpToRecap = useRef<(() => void) | null>(null)
424
514
  const repinTranscript = useRef<(() => void) | null>(null)
425
515
 
426
- const commands = useMemo(() => {
427
- if (!state.commands) {
428
- return undefined
429
- }
430
- if (state.commands.some((c) => c.name === 'model')) {
431
- return state.commands
432
- }
433
- return [{ name: 'model', description: 'Switch the model for this session', argumentHint: '<model>' }, ...state.commands]
434
- }, [state.commands])
435
-
436
516
  const handleSend = (text: string, attachmentIds: string[]) => {
437
517
  if (attachmentIds.length === 0) {
438
- const modelCommand = /^\/model\s+(\S+)$/.exec(text)
439
- if (modelCommand) {
440
- setModel(modelCommand[1])
441
- return
442
- }
443
- if (capabilities.mcpStatus && text.trim() === '/mcp') {
444
- openPanel('mcp')
518
+ const local = matchClientCommand(text, clientCommands)
519
+ if (local && local.command.run(local.args)) {
445
520
  return
446
521
  }
447
522
  }
@@ -535,6 +610,8 @@ export function SessionPanel({
535
610
  />
536
611
  )
537
612
 
613
+ const fileLinks = useMemo(() => (onOpenFile ? { cwd: state.cwd, open: onOpenFile } : undefined), [onOpenFile, state.cwd])
614
+
538
615
  const panelRef = useRef<HTMLDivElement>(null)
539
616
  useEffect(() => {
540
617
  if (!onLinkClick) {
@@ -579,222 +656,225 @@ export function SessionPanel({
579
656
  return (
580
657
  <TranscriptVariantProvider value={transcriptVariant}>
581
658
  <TranscriptDensityProvider value={transcriptDensity}>
582
- <ToolResultFetchProvider value={loadFullResult}>
583
- <ToolTitleProvider value={state.toolTitles}>
584
- <ToolResultImageProvider value={resultImages}>
585
- <div
586
- ref={panelRef}
587
- data-slot="session-panel"
588
- data-agent-font={transcriptFont}
589
- onClick={handleClick}
590
- className={cn('flex h-full min-h-0 flex-col overflow-hidden bg-bg', className)}
591
- style={fontSize !== undefined ? ({ '--wd-font-size': `${Math.round(fontSize)}px` } as React.CSSProperties) : undefined}
592
- >
593
- {headerTakesActions ? header({ actions: menu }) : header}
594
- {statusPlacement === 'top' ? statusBar : null}
595
- {protocolMismatch !== undefined ? (
596
- <Notice level="warning">
597
- Server speaks protocol v{protocolMismatch}, this build renders v{PROTOCOL_VERSION}. Some events may not render.
598
- </Notice>
599
- ) : null}
600
- {protocolError ? (
601
- <Notice level="error" onDismiss={() => setProtocolError(undefined)}>
602
- {protocolError}
603
- </Notice>
604
- ) : null}
605
- {subagentId !== undefined ? (
606
- <SubagentStrip
607
- task={subagentTask}
608
- items={subagentFrameItems}
609
- label={subagentFallbackLabel}
610
- onBack={leaveSubagent}
611
- terminal={terminal}
612
- fontSize={effectiveTermFontSize}
613
- lineHeight={effectiveTermLineHeight}
614
- />
615
- ) : null}
616
- <BookmarkProvider value={bookmarkHandle}>
617
- <Transcript
618
- key={subagentId ?? 'session'}
619
- state={state}
620
- fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
621
- attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
622
- canBrowseFiles={hostFiles.available}
623
- hostImage={hostImage}
624
- variant={transcriptVariant}
625
- density={transcriptDensity}
626
- fontSize={effectiveTermFontSize}
627
- lineHeight={effectiveTermLineHeight}
628
- affordances={affordances}
629
- stickyPrompt={stickyPrompt}
630
- scrubber={scrubber}
631
- bookmarks={bookmarks}
632
- replaying={replaying}
633
- catchUp={catchUp && newCount > 0 ? { from: catchUp.itemCount, since: catchUp.since } : undefined}
634
- reveal={taskReveal ?? returnReveal ?? reveal}
635
- frame={subagentId === undefined ? undefined : { parentToolUseId: subagentId }}
636
- onOpenSubagent={enterSubagent}
637
- emptyState={emptyState}
638
- jumpToRecapRef={jumpToRecap}
639
- repinRef={repinTranscript}
640
- />
641
- </BookmarkProvider>
642
- {catchUp && newCount > 0 && !replaying && subagentId === undefined ? (
643
- <div className="px-3 pb-1">
644
- <div
645
- data-slot="catch-up"
646
- className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3"
647
- >
648
- <span aria-hidden className={cn('select-none', terminal ? 'text-fg-3' : 'text-accent')}>
649
- ※
650
- </span>
651
- <span className="min-w-0 flex-1 truncate">
652
- {newCount} new {newCount === 1 ? 'row' : 'rows'}
653
- {catchUp.since !== undefined ? ` since you were last here` : ''}
654
- </span>
655
- <button
656
- type="button"
657
- onClick={() => jumpToRecap.current?.()}
658
- className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
659
- >
660
- jump
661
- </button>
662
- <button
663
- type="button"
664
- onClick={() => setCaughtUp(true)}
665
- className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
666
- >
667
- dismiss
668
- </button>
669
- </div>
670
- </div>
671
- ) : null}
672
- {!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
673
- <div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
674
- <PromptSurface
659
+ <FileLinkProvider value={fileLinks}>
660
+ <ToolResultFetchProvider value={loadFullResult}>
661
+ <ToolTitleProvider value={state.toolTitles}>
662
+ <ToolResultImageProvider value={resultImages}>
663
+ <div
664
+ ref={panelRef}
665
+ data-slot="session-panel"
666
+ data-agent-font={transcriptFont}
667
+ onClick={handleClick}
668
+ className={cn('flex h-full min-h-0 flex-col overflow-hidden bg-bg', className)}
669
+ style={fontSize !== undefined ? ({ '--wd-font-size': `${Math.round(fontSize)}px` } as React.CSSProperties) : undefined}
670
+ >
671
+ {headerTakesActions ? header({ actions: menu }) : header}
672
+ {statusPlacement === 'top' ? statusBar : null}
673
+ {protocolMismatch !== undefined ? (
674
+ <Notice level="warning">
675
+ Server speaks protocol v{protocolMismatch}, this build renders v{PROTOCOL_VERSION}. Some events may not render.
676
+ </Notice>
677
+ ) : null}
678
+ {protocolError ? (
679
+ <Notice level="error" onDismiss={() => setProtocolError(undefined)}>
680
+ {protocolError}
681
+ </Notice>
682
+ ) : null}
683
+ {subagentId !== undefined ? (
684
+ <SubagentStrip
685
+ task={subagentTask}
686
+ items={subagentFrameItems}
687
+ label={subagentFallbackLabel}
688
+ onBack={leaveSubagent}
675
689
  terminal={terminal}
676
- metrics={{ fontSize: effectiveTermFontSize, lineHeight: effectiveTermLineHeight }}
690
+ fontSize={effectiveTermFontSize}
691
+ lineHeight={effectiveTermLineHeight}
692
+ />
693
+ ) : null}
694
+ <BookmarkProvider value={bookmarkHandle}>
695
+ <Transcript
696
+ key={subagentId ?? 'session'}
697
+ state={state}
698
+ fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
699
+ attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
700
+ canBrowseFiles={hostFiles.available}
701
+ hostImage={hostImage}
702
+ variant={transcriptVariant}
703
+ density={transcriptDensity}
704
+ fontSize={effectiveTermFontSize}
705
+ lineHeight={effectiveTermLineHeight}
677
706
  affordances={affordances}
678
- >
679
- {state.pendingApprovals.map((request) => {
680
- // Before the variant split: a host's entry is the renderer for its tool in
681
- // both themes, and it overrides the built-in entries rather than racing them.
682
- const HostPrompt = approvalPrompts?.[request.toolName]
683
- if (HostPrompt) {
684
- return <HostPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
685
- }
686
- const isQuestion = request.toolName === 'AskUserQuestion' && parseUserQuestions(request.input).length > 0
687
- if (terminal) {
707
+ stickyPrompt={stickyPrompt}
708
+ scrubber={scrubber}
709
+ bookmarks={bookmarks}
710
+ replaying={replaying}
711
+ catchUp={catchUp && newCount > 0 ? { from: catchUp.itemCount, since: catchUp.since } : undefined}
712
+ reveal={taskReveal ?? returnReveal ?? reveal}
713
+ frame={subagentId === undefined ? undefined : { parentToolUseId: subagentId }}
714
+ onOpenSubagent={enterSubagent}
715
+ emptyState={emptyState}
716
+ jumpToRecapRef={jumpToRecap}
717
+ repinRef={repinTranscript}
718
+ />
719
+ </BookmarkProvider>
720
+ {catchUp && newCount > 0 && !replaying && subagentId === undefined ? (
721
+ <div className="px-3 pb-1">
722
+ <div
723
+ data-slot="catch-up"
724
+ className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3"
725
+ >
726
+ <span aria-hidden className={cn('select-none', terminal ? 'text-fg-3' : 'text-accent')}>
727
+ ※
728
+ </span>
729
+ <span className="min-w-0 flex-1 truncate">
730
+ {newCount} new {newCount === 1 ? 'row' : 'rows'}
731
+ {catchUp.since !== undefined ? ` since you were last here` : ''}
732
+ </span>
733
+ <button
734
+ type="button"
735
+ onClick={() => jumpToRecap.current?.()}
736
+ className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
737
+ >
738
+ jump
739
+ </button>
740
+ <button
741
+ type="button"
742
+ onClick={() => setCaughtUp(true)}
743
+ className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline"
744
+ >
745
+ dismiss
746
+ </button>
747
+ </div>
748
+ </div>
749
+ ) : null}
750
+ {!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
751
+ <div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
752
+ <PromptSurface
753
+ terminal={terminal}
754
+ metrics={{ fontSize: effectiveTermFontSize, lineHeight: effectiveTermLineHeight }}
755
+ affordances={affordances}
756
+ >
757
+ {state.pendingApprovals.map((request) => {
758
+ // Before the variant split: a host's entry is the renderer for its tool in
759
+ // both themes, and it overrides the built-in entries rather than racing them.
760
+ const HostPrompt = approvalPrompts?.[request.toolName]
761
+ if (HostPrompt) {
762
+ return <HostPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
763
+ }
764
+ const isQuestion = request.toolName === 'AskUserQuestion' && parseUserQuestions(request.input).length > 0
765
+ if (terminal) {
766
+ return isQuestion ? (
767
+ <TerminalQuestionPrompt
768
+ key={request.id}
769
+ request={request}
770
+ onAnswer={approve}
771
+ onDismiss={(id) => deny(id, 'Question dismissed by user')}
772
+ />
773
+ ) : (
774
+ <TerminalPermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
775
+ )
776
+ }
688
777
  return isQuestion ? (
689
- <TerminalQuestionPrompt
778
+ <QuestionPrompt
690
779
  key={request.id}
691
780
  request={request}
692
781
  onAnswer={approve}
693
782
  onDismiss={(id) => deny(id, 'Question dismissed by user')}
694
783
  />
695
784
  ) : (
696
- <TerminalPermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
785
+ <PermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
697
786
  )
698
- }
699
- return isQuestion ? (
700
- <QuestionPrompt
701
- key={request.id}
702
- request={request}
703
- onAnswer={approve}
704
- onDismiss={(id) => deny(id, 'Question dismissed by user')}
705
- />
706
- ) : (
707
- <PermissionPrompt key={request.id} request={request} onApprove={approve} onDeny={deny} />
708
- )
709
- })}
710
- </PromptSurface>
711
- </div>
712
- ) : null}
713
- {readOnly || subagentId !== undefined ? null : (
714
- <>
715
- <Composer
716
- ref={composerRef}
717
- onSend={handleSend}
718
- onInterrupt={interrupt}
719
- busy={busy}
720
- disabled={ended || !sessionId}
721
- commands={capabilities.slashCommands ? commands : undefined}
722
- skills={capabilities.skillsList ? state.skills : undefined}
723
- attachments={attachments}
724
- draft={draft}
725
- onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
726
- onShellCommand={shell ? runShell : undefined}
727
- layout={controlsExternal ? 'inline' : 'stacked'}
728
- toolbar={controlsExternal ? undefined : sessionControls}
729
- fontSize={effectiveTermFontSize}
730
- lineHeight={effectiveTermLineHeight}
731
- affordances={affordances}
732
- />
733
- </>
734
- )}
735
- {statusPlacement === 'bottom' ? statusBar : null}
736
-
737
- {!external ? (
738
- <>
739
- <SessionInfoDialog
740
- state={state}
741
- client={client}
742
- sessionId={sessionId}
743
- open={panel === 'info'}
744
- onOpenChange={(next) => setPanel(next ? 'info' : undefined)}
745
- />
746
- <ContextDialog
747
- usage={state.contextUsage}
748
- engine={state.engine ?? 'claude'}
749
- open={panel === 'context'}
750
- onOpenChange={(next) => setPanel(next ? 'context' : undefined)}
751
- />
752
- <UsageDialog
753
- rateLimits={windows}
754
- subscriptionType={state.subscriptionType}
755
- engine={state.engine ?? 'claude'}
756
- totalCostUsd={state.totalCostUsd}
757
- updatedAt={usageUpdatedAt}
758
- open={panel === 'usage'}
759
- onOpenChange={(next) => setPanel(next ? 'usage' : undefined)}
760
- />
761
- <McpDialog
762
- client={client}
763
- sessionId={sessionId}
764
- canManageServers={capabilities.mcpServerActions}
765
- open={panel === 'mcp'}
766
- onOpenChange={(next) => setPanel(next ? 'mcp' : undefined)}
767
- />
768
- <TasksDialog
769
- tasks={tasks}
770
- showCompleted={showCompletedTasks}
771
- onShowCompletedChange={setShowCompletedTasks}
772
- onSelectTask={(task) => {
773
- if (task.toolUseId) {
774
- setTaskReveal({ toolUseId: task.toolUseId, nonce: Date.now() })
775
- }
776
- }}
777
- open={panel === 'tasks'}
778
- onOpenChange={(next) => setPanel(next ? 'tasks' : undefined)}
779
- />
780
- <SkillsDialog
781
- skills={state.skills}
782
- open={panel === 'skills'}
783
- onOpenChange={(next) => setPanel(next ? 'skills' : undefined)}
784
- onUse={(skill) => composerRef.current?.insertText(skillPrompt(skill))}
785
- />
786
- <HostFilesDialog
787
- client={client}
788
- cwd={state.cwd}
789
- open={panel === 'files'}
790
- onOpenChange={(next) => setPanel(next ? 'files' : undefined)}
791
- />
792
- </>
793
- ) : null}
794
- </div>
795
- </ToolResultImageProvider>
796
- </ToolTitleProvider>
797
- </ToolResultFetchProvider>
787
+ })}
788
+ </PromptSurface>
789
+ </div>
790
+ ) : null}
791
+ {readOnly || subagentId !== undefined ? null : (
792
+ <>
793
+ <Composer
794
+ ref={composerRef}
795
+ onSend={handleSend}
796
+ onInterrupt={interrupt}
797
+ busy={busy}
798
+ disabled={ended || !sessionId}
799
+ commands={capabilities.slashCommands ? state.commands : undefined}
800
+ skills={capabilities.skillsList ? state.skills : undefined}
801
+ clientCommands={clientCommands}
802
+ attachments={attachments}
803
+ draft={draft}
804
+ onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
805
+ onShellCommand={shell ? runShell : undefined}
806
+ layout={controlsExternal ? 'inline' : 'stacked'}
807
+ toolbar={controlsExternal ? undefined : sessionControls}
808
+ fontSize={effectiveTermFontSize}
809
+ lineHeight={effectiveTermLineHeight}
810
+ affordances={affordances}
811
+ />
812
+ </>
813
+ )}
814
+ {statusPlacement === 'bottom' ? statusBar : null}
815
+
816
+ {!external ? (
817
+ <>
818
+ <SessionInfoDialog
819
+ state={state}
820
+ client={client}
821
+ sessionId={sessionId}
822
+ open={panel === 'info'}
823
+ onOpenChange={(next) => setPanel(next ? 'info' : undefined)}
824
+ />
825
+ <ContextDialog
826
+ usage={state.contextUsage}
827
+ engine={state.engine ?? 'claude'}
828
+ open={panel === 'context'}
829
+ onOpenChange={(next) => setPanel(next ? 'context' : undefined)}
830
+ />
831
+ <UsageDialog
832
+ rateLimits={windows}
833
+ subscriptionType={state.subscriptionType}
834
+ engine={state.engine ?? 'claude'}
835
+ totalCostUsd={state.totalCostUsd}
836
+ updatedAt={usageUpdatedAt}
837
+ open={panel === 'usage'}
838
+ onOpenChange={(next) => setPanel(next ? 'usage' : undefined)}
839
+ />
840
+ <McpDialog
841
+ client={client}
842
+ sessionId={sessionId}
843
+ canManageServers={capabilities.mcpServerActions}
844
+ open={panel === 'mcp'}
845
+ onOpenChange={(next) => setPanel(next ? 'mcp' : undefined)}
846
+ />
847
+ <TasksDialog
848
+ tasks={tasks}
849
+ showCompleted={showCompletedTasks}
850
+ onShowCompletedChange={setShowCompletedTasks}
851
+ onSelectTask={(task) => {
852
+ if (task.toolUseId) {
853
+ setTaskReveal({ toolUseId: task.toolUseId, nonce: Date.now() })
854
+ }
855
+ }}
856
+ open={panel === 'tasks'}
857
+ onOpenChange={(next) => setPanel(next ? 'tasks' : undefined)}
858
+ />
859
+ <SkillsDialog
860
+ skills={state.skills}
861
+ open={panel === 'skills'}
862
+ onOpenChange={(next) => setPanel(next ? 'skills' : undefined)}
863
+ onUse={(skill) => composerRef.current?.insertText(skillPrompt(skill))}
864
+ />
865
+ <HostFilesDialog
866
+ client={client}
867
+ cwd={state.cwd}
868
+ open={panel === 'files'}
869
+ onOpenChange={(next) => setPanel(next ? 'files' : undefined)}
870
+ />
871
+ </>
872
+ ) : null}
873
+ </div>
874
+ </ToolResultImageProvider>
875
+ </ToolTitleProvider>
876
+ </ToolResultFetchProvider>
877
+ </FileLinkProvider>
798
878
  </TranscriptDensityProvider>
799
879
  </TranscriptVariantProvider>
800
880
  )
@@ -31,6 +31,7 @@ export interface SessionWorkspaceProps {
31
31
  controlsSurface?: SessionPanelProps['controlsSurface']
32
32
  fontSize?: SessionPanelProps['fontSize']
33
33
  onLinkClick?: SessionPanelProps['onLinkClick']
34
+ onOpenFile?: SessionPanelProps['onOpenFile']
34
35
  unseen?: SessionPanelProps['unseen']
35
36
  readOnly?: SessionPanelProps['readOnly']
36
37
  onVitals?: SessionPanelProps['onVitals']
@@ -64,6 +65,7 @@ export function SessionWorkspace({
64
65
  controlsSurface,
65
66
  fontSize,
66
67
  onLinkClick,
68
+ onOpenFile,
67
69
  unseen,
68
70
  readOnly,
69
71
  onVitals,
@@ -222,6 +224,7 @@ export function SessionWorkspace({
222
224
  controlsSurface={controlsSurface}
223
225
  fontSize={fontSize}
224
226
  onLinkClick={onLinkClick}
227
+ onOpenFile={onOpenFile ?? files.open}
225
228
  statusPlacement={statusPlacement}
226
229
  unseen={unseen}
227
230
  readOnly={readOnly}