praxis-agent 0.35.0 → 0.37.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 (111) hide show
  1. package/README.md +20 -2
  2. package/dist/application/background-agent-manager.d.ts +16 -6
  3. package/dist/application/background-agent-manager.js +139 -97
  4. package/dist/application/context-engine.d.ts +56 -0
  5. package/dist/application/context-engine.js +129 -0
  6. package/dist/application/durable-follow-up.d.ts +14 -0
  7. package/dist/application/durable-follow-up.js +52 -0
  8. package/dist/application/native-session-transcript.d.ts +72 -0
  9. package/dist/application/native-session-transcript.js +351 -0
  10. package/dist/application/native-sidechain-transcript.d.ts +34 -0
  11. package/dist/application/native-sidechain-transcript.js +156 -0
  12. package/dist/application/session-service.d.ts +23 -4
  13. package/dist/application/session-service.js +1130 -494
  14. package/dist/application/subagent-service.d.ts +5 -1
  15. package/dist/application/subagent-service.js +749 -320
  16. package/dist/application/team-agent-runtime.d.ts +29 -0
  17. package/dist/application/team-agent-runtime.js +169 -0
  18. package/dist/application/team-capability.d.ts +27 -0
  19. package/dist/application/team-capability.js +41 -0
  20. package/dist/application/team-lead-operations.d.ts +38 -0
  21. package/dist/application/team-lead-operations.js +155 -0
  22. package/dist/application/team-mailbox.d.ts +54 -0
  23. package/dist/application/team-mailbox.js +196 -0
  24. package/dist/application/team-manager.d.ts +113 -0
  25. package/dist/application/team-manager.js +681 -0
  26. package/dist/application/team-workspace.d.ts +29 -0
  27. package/dist/application/team-workspace.js +197 -0
  28. package/dist/application/top-level-agent-manager.d.ts +1 -0
  29. package/dist/application/top-level-agent-manager.js +1025 -403
  30. package/dist/application/transcript-projection.d.ts +35 -0
  31. package/dist/application/transcript-projection.js +139 -0
  32. package/dist/application/turn-lifecycle.d.ts +2 -0
  33. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  34. package/dist/application/turn-memory-coordinator.js +98 -0
  35. package/dist/cli/agents-dashboard.js +10 -5
  36. package/dist/cli/interactive.d.ts +7 -0
  37. package/dist/cli/interactive.js +645 -334
  38. package/dist/cli/tui/claude-style.d.ts +8 -25
  39. package/dist/cli/tui/claude-style.js +299 -277
  40. package/dist/cli/tui/decision-surface-model.d.ts +71 -0
  41. package/dist/cli/tui/decision-surface-model.js +131 -0
  42. package/dist/cli/tui/decision-surface.d.ts +7 -0
  43. package/dist/cli/tui/decision-surface.js +41 -0
  44. package/dist/cli/tui/help-surface-model.d.ts +68 -0
  45. package/dist/cli/tui/help-surface-model.js +131 -0
  46. package/dist/cli/tui/mcp-panel.js +1 -2
  47. package/dist/cli/tui/permission-surface-model.d.ts +195 -0
  48. package/dist/cli/tui/permission-surface-model.js +360 -0
  49. package/dist/cli/tui/permission-surface.d.ts +7 -0
  50. package/dist/cli/tui/permission-surface.js +69 -0
  51. package/dist/cli/tui/presentation-environment.d.ts +32 -0
  52. package/dist/cli/tui/presentation-environment.js +149 -0
  53. package/dist/cli/tui/task-panel.js +10 -1
  54. package/dist/cli/tui/theme.d.ts +46 -3
  55. package/dist/cli/tui/theme.js +414 -56
  56. package/dist/cli/tui/tool-permission.js +16 -8
  57. package/dist/cli/tui/transcript-presentation.d.ts +19 -1
  58. package/dist/cli/tui/transcript-presentation.js +26 -6
  59. package/dist/cli/tui/transcript-viewport.d.ts +18 -0
  60. package/dist/cli/tui/transcript-viewport.js +517 -111
  61. package/dist/cli/tui/transcript-window-model.d.ts +108 -0
  62. package/dist/cli/tui/transcript-window-model.js +772 -0
  63. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  64. package/dist/cli/tui/tui-interaction-router.js +127 -0
  65. package/dist/cli/tui/tui-screen-model.d.ts +114 -0
  66. package/dist/cli/tui/tui-screen-model.js +210 -0
  67. package/dist/cli/tui/tui-view-model.d.ts +2 -49
  68. package/dist/cli/tui/tui-view-model.js +1 -68
  69. package/dist/cli-runtime.d.ts +4 -2
  70. package/dist/cli-runtime.js +397 -32
  71. package/dist/compatibility/claude/transcript-codec.js +6 -0
  72. package/dist/core/agent-orchestration.d.ts +31 -0
  73. package/dist/core/agent-orchestration.js +235 -0
  74. package/dist/core/permission-policy.d.ts +4 -0
  75. package/dist/core/permission-policy.js +21 -0
  76. package/dist/core/runtime.d.ts +6 -0
  77. package/dist/core/runtime.js +20 -2
  78. package/dist/core/team-mailbox.d.ts +54 -0
  79. package/dist/core/team-mailbox.js +212 -0
  80. package/dist/core/team-ownership.d.ts +84 -0
  81. package/dist/core/team-ownership.js +505 -0
  82. package/dist/core/tool-execution-scheduler.js +6 -1
  83. package/dist/core/transcript-event.d.ts +5 -1
  84. package/dist/core/transcript-event.js +12 -0
  85. package/dist/persistence/claude-job-store.d.ts +58 -0
  86. package/dist/persistence/claude-job-store.js +483 -1
  87. package/dist/persistence/claude-transcript-store.d.ts +2 -1
  88. package/dist/persistence/claude-transcript-store.js +2 -10
  89. package/dist/persistence/native-team-mailbox.d.ts +68 -0
  90. package/dist/persistence/native-team-mailbox.js +542 -0
  91. package/dist/persistence/native-team-store.d.ts +34 -0
  92. package/dist/persistence/native-team-store.js +257 -0
  93. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  94. package/dist/persistence/native-transcript-reader.js +283 -0
  95. package/dist/persistence/native-transcript-store.d.ts +69 -0
  96. package/dist/persistence/native-transcript-store.js +321 -0
  97. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  98. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  99. package/dist/persistence/transcript-file-append.d.ts +2 -0
  100. package/dist/persistence/transcript-file-append.js +10 -0
  101. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  102. package/dist/platform/exclusive-file-lease.js +3 -0
  103. package/dist/tools/claude-capabilities.d.ts +4 -0
  104. package/dist/tools/claude-capabilities.js +18 -2
  105. package/dist/tools/team-lead-tools.d.ts +19 -0
  106. package/dist/tools/team-lead-tools.js +479 -0
  107. package/dist/tools/team-member-tools.d.ts +21 -0
  108. package/dist/tools/team-member-tools.js +259 -0
  109. package/package.json +1 -1
  110. package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
  111. package/dist/cli/tui/fullscreen-renderer.js +0 -13
@@ -12,13 +12,16 @@ import { resolveDataPlane, resolveDataPlaneRoot, } from '../persistence/data-pla
12
12
  import { AgentRunCancelledError } from '../core/runtime.js';
13
13
  import { claudePermissionActionKey, claudePermissionRuleMatches, } from '../permissions/claude-permission-resolver.js';
14
14
  import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
15
- import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, ModelMenu, PermissionDashboard, SelectionMenu, SessionPicker, ThemePicker, CustomThemeEditor, SessionIdentity, Transcript, WelcomePanel, useTerminalRows, useTerminalWidth, } from './tui/claude-style.js';
15
+ import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, ModelMenu, SelectionMenu, SessionPicker, ThemePicker, CustomThemeEditor, SessionIdentity, Transcript, WelcomePanel, } from './tui/claude-style.js';
16
16
  import { loadTuiMemoryFiles, openTuiMemoryFolder, } from './tui/memory-files.js';
17
17
  import { loadClaudeReleaseNotes } from './tui/release-notes.js';
18
- import { fullscreenInkRenderOptions } from './tui/fullscreen-renderer.js';
18
+ import { tuiInkRenderOptions, useTuiPresentationEnvironment, } from './tui/presentation-environment.js';
19
19
  import { StreamingFrameBuffer } from './tui/streaming-frame-buffer.js';
20
20
  import { createClaudeStatusLineInput, StatusLine } from './tui/status-line.js';
21
- import { projectTuiView, resolveTuiRenderer } from './tui/tui-view-model.js';
21
+ import { createTuiAppendHistoryChange } from './tui/transcript-window-model.js';
22
+ import { createTuiHistoryChange, resolveTuiRenderer, } from './tui/tui-view-model.js';
23
+ import { projectTuiScreen, } from './tui/tui-screen-model.js';
24
+ import { projectTuiHelpSurface, } from './tui/help-surface-model.js';
22
25
  import { loadGitDiff, visiblePatchLines, } from './tui/git-diff.js';
23
26
  import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule, } from './tui/permission-settings.js';
24
27
  import { createRecentlyDeniedStore, } from './tui/recently-denied.js';
@@ -26,23 +29,28 @@ import { agentColorMessage, parseAgentColorInput, } from '../compatibility/claud
26
29
  import { filterTuiSlashCommands, mergeTuiSlashCommands, slashCommandQuery, } from './tui/slash-commands.js';
27
30
  import { runDoctor, } from '../maintenance/doctor.js';
28
31
  import { canonicalClaudeCostModelName, formatCostSummary, } from './tui/cost-summary.js';
29
- import { createComposerEditor, deleteComposerBackward, deleteComposerForward, deleteComposerToEnd, deleteComposerToStart, deleteComposerWordBackward, insertComposerText, moveComposerCursor, moveComposerCursorByWord, } from './tui/composer-editor.js';
32
+ import { createComposerEditor, deleteComposerBackward, deleteComposerForward, insertComposerText, moveComposerCursor, } from './tui/composer-editor.js';
30
33
  import { routeComposerKey, } from './tui/composer-key-router.js';
34
+ import { routeTuiInteraction, } from './tui/tui-interaction-router.js';
31
35
  import { applyMentionReference, fileReferenceAtCursor, filterTuiMentionEntries, loadTuiFileEntries, } from './tui/file-picker.js';
32
36
  import { editTuiPrompt, openTuiEditorFile, } from './tui/external-editor.js';
33
37
  import { suspendTuiProcess } from './tui/terminal-suspend.js';
34
38
  import { readTuiClipboard, writeTuiClipboard, writeTuiOsc52Clipboard, } from './tui/clipboard.js';
35
- import { composerImageIds, deleteComposerImageBackward, deleteComposerImageForward, insertComposerImageMarker, moveComposerCursorAcrossImages, } from './tui/composer-images.js';
39
+ import { composerImageIds, insertComposerImageMarker, } from './tui/composer-images.js';
36
40
  import { defaultTuiKeybindings, ensureTuiKeybindingsFile, hasTuiKeybindingPrefix, loadTuiKeybindings, resolveTuiKeybinding, tuiKeyChord, } from './tui/keybindings.js';
37
41
  import { completeTuiWorkspaceDirectory, resolveTuiWorkspaceDirectory, } from './tui/workspace-directories.js';
38
42
  import { conversationExportPath, conversationExportText, defaultConversationExportFilename, writeConversationExport, } from './tui/conversation-export.js';
39
43
  import { loadTuiThemeSettings, saveTuiThemeSettings, themeSettingsWithCustomTheme, } from './tui/theme-settings.js';
40
- import { DEFAULT_TUI_THEME_SETTINGS, TUI_THEMES, TuiThemeProvider, tuiPalette, } from './tui/theme.js';
44
+ import { DEFAULT_TUI_THEME_SETTINGS, TUI_THEMES, TuiThemeProvider, useTuiTheme, } from './tui/theme.js';
41
45
  import { CUSTOM_THEME_TOKENS, createTuiCustomTheme, deleteTuiCustomTheme, loadTuiCustomThemes, updateTuiCustomTheme, } from './tui/custom-themes.js';
42
46
  import { setupTuiTerminal, terminalSetupTuiSlashCommand, } from './tui/terminal-setup.js';
43
47
  import { commitElicitationText, createTuiElicitationForm, elicitationFormIsValid, elicitationTextValue, expandElicitationOptions, focusedElicitationField, McpElicitationForm, McpElicitationUrl, moveElicitationFocus, moveElicitationOption, selectElicitationOption, toggleElicitationBoolean, typeaheadElicitationOption, unsetElicitationField, validateTuiElicitationForm, } from './tui/mcp-elicitation.js';
44
48
  import { openTuiUrl } from './tui/open-url.js';
45
- import { projectTuiToolPermission, ToolPermissionDialog, } from './tui/tool-permission.js';
49
+ import { projectTuiToolPermission } from './tui/tool-permission.js';
50
+ import { projectTuiPermissionSurface, } from './tui/permission-surface-model.js';
51
+ import { PermissionSurface } from './tui/permission-surface.js';
52
+ import { projectTuiDecisionSurface, } from './tui/decision-surface-model.js';
53
+ import { DecisionSurface } from './tui/decision-surface.js';
46
54
  import { ConfigDashboard, projectConfigRows } from './tui/config-dashboard.js';
47
55
  import { DoctorDashboard } from './tui/doctor-dashboard.js';
48
56
  import { SandboxDashboard, tuiSandboxTabs } from './tui/sandbox-dashboard.js';
@@ -111,11 +119,6 @@ const ZERO_COST_SUMMARY = {
111
119
  hasUnknownModelCost: false,
112
120
  modelUsage: [],
113
121
  };
114
- function selectionPrefix(selected, screenReader) {
115
- if (selected)
116
- return screenReader ? 'Selected: ' : '❯ ';
117
- return screenReader ? '' : ' ';
118
- }
119
122
  function rewindActions(point) {
120
123
  const shared = [
121
124
  { action: 'summarize-from', label: 'Summarize from here' },
@@ -187,20 +190,6 @@ function questionAnswer(question, input) {
187
190
  })
188
191
  .join(', ');
189
192
  }
190
- function permissionDeleteTitle(behavior) {
191
- return `Delete ${behavior === 'allow' ? 'allowed' : behavior === 'deny' ? 'denied' : 'ask'} tool?`;
192
- }
193
- function permissionScopeLabel(scope) {
194
- return scope === 'local'
195
- ? 'From project local settings'
196
- : scope === 'project'
197
- ? 'From project settings'
198
- : 'From user settings';
199
- }
200
- function permissionRuleDescription(rule) {
201
- const bashPrefix = /^Bash\((.*?)(?::\*| \*)\)$/u.exec(rule)?.[1]?.trim();
202
- return bashPrefix ? `Any Bash command starting with ${bashPrefix}` : undefined;
203
- }
204
193
  function describeTool(call, sensitiveValues) {
205
194
  const name = redactSensitiveText(call.name, sensitiveValues);
206
195
  return `${name} ${describeToolInput(call, sensitiveValues)}`;
@@ -266,10 +255,41 @@ const HIDDEN_TUI_SLASH_COMMANDS = new Set([
266
255
  'update',
267
256
  'usage',
268
257
  ]);
258
+ function RewindWarning() {
259
+ const theme = useTuiTheme();
260
+ return (_jsx(Text, { ...theme.text.warning, children: "\u26A0 Rewinding does not affect files edited manually or via bash." }));
261
+ }
262
+ function ExitWarning() {
263
+ const theme = useTuiTheme();
264
+ return _jsx(Text, { ...theme.text.warning, children: "Press Ctrl-C again to exit" });
265
+ }
266
+ function DecisionOption({ selected, screenReader, children, selectedPrefix = '❯ ', unselectedPrefix = ' ', }) {
267
+ const theme = useTuiTheme();
268
+ const accessible = screenReader || theme.noColor;
269
+ return (_jsxs(Text, { ...(selected ? theme.text.selectedRow : {}), children: [selected
270
+ ? accessible
271
+ ? 'Selected: '
272
+ : selectedPrefix
273
+ : accessible
274
+ ? ''
275
+ : unselectedPrefix, children] }));
276
+ }
277
+ /** Advances the React-owned transcript plus its exact local mutation fact. */
278
+ export function advanceInteractiveHistoryState(current, items, changedFrom) {
279
+ if (!Number.isInteger(changedFrom) ||
280
+ changedFrom < 0 ||
281
+ changedFrom > items.length)
282
+ throw new RangeError('changedFrom must be a valid next-history index');
283
+ const revision = current.change.revision + 1;
284
+ return {
285
+ items,
286
+ change: changedFrom === current.items.length && items.length > changedFrom
287
+ ? createTuiAppendHistoryChange(revision, current.items, items)
288
+ : createTuiHistoryChange(revision, changedFrom, items, current.items),
289
+ };
290
+ }
269
291
  export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: suppliedConfigRoot, statePath: suppliedStatePath, factory, initialSessions, initialPrompt, initialHistory = [], initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, doctorLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader, memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, notificationDelayMs = 6_000, elicitationUrlOpener = openTuiUrl, releaseNotesLoader = (configRoot) => loadClaudeReleaseNotes({ configRoot }), settingSources, }) {
270
292
  const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
271
- const width = useTerminalWidth(terminalWidth);
272
- const rows = useTerminalRows();
273
293
  const settingsDirectory = dataPlane === 'native' ? '.praxis' : '.claude';
274
294
  const keybindingsRoot = useMemo(() => resolve(suppliedConfigRoot ??
275
295
  keybindingsConfigRoot ??
@@ -340,14 +360,20 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
340
360
  const [themeSettings, setThemeSettings] = useState(initialThemeSettings ?? DEFAULT_TUI_THEME_SETTINGS);
341
361
  const [runtimeSettings, setRuntimeSettings] = useState(suppliedRuntimeSettings ??
342
362
  projectRuntimeSettings({ settings: {}, state: {} }));
343
- const fixedViewport = runtimeSettings.tui === 'fullscreen' && rows !== undefined;
363
+ const presentation = useTuiPresentationEnvironment({
364
+ renderer: runtimeSettings.tui,
365
+ screenReader: axScreenReader,
366
+ ...(terminalWidth === undefined
367
+ ? {}
368
+ : { viewportOverride: { columns: terminalWidth } }),
369
+ });
370
+ const width = presentation.viewport.columns;
371
+ const rows = presentation.viewport.rows;
372
+ const fixedViewport = presentation.fixedViewport;
344
373
  const runtimeSettingsRef = useRef(runtimeSettings);
345
374
  runtimeSettingsRef.current = runtimeSettings;
346
375
  runtimeGitignoreRef.current = runtimeSettings.gitignore;
347
376
  const [vimInsertMode, setVimInsertMode] = useState(true);
348
- const activePalette = tuiPalette(TUI_THEMES.includes(themeSettings.theme)
349
- ? themeSettings.theme
350
- : 'dark', themeSettings.syntaxHighlightingDisabled, process.env, themeSettings.customTheme);
351
377
  const choices = useMemo(() => allowNewSession ? [null, ...initialSessions] : initialSessions, [allowNewSession, initialSessions]);
352
378
  const [pickerIncludesNewSession, setPickerIncludesNewSession] = useState(allowNewSession);
353
379
  const pickerIncludesNewSessionRef = useRef(allowNewSession);
@@ -447,46 +473,36 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
447
473
  const [usage, setUsage] = useState();
448
474
  const [costUsd, setCostUsd] = useState();
449
475
  const [contextWindowTokens, setContextWindowTokens] = useState(display.contextWindowTokens);
450
- const [history, setHistory] = useState([...initialHistory]);
476
+ const [historyState, setHistoryState] = useState(() => {
477
+ const items = [...initialHistory];
478
+ return {
479
+ items,
480
+ change: createTuiHistoryChange(0, 0, items),
481
+ };
482
+ });
483
+ const history = historyState.items;
484
+ const setHistory = (update, knownChangedFrom) => {
485
+ setHistoryState((current) => {
486
+ const next = typeof update === 'function' ? update(current.items) : update;
487
+ let changedFrom = typeof knownChangedFrom === 'function'
488
+ ? knownChangedFrom(current.items, next)
489
+ : (knownChangedFrom ?? 0);
490
+ if (knownChangedFrom === undefined) {
491
+ const limit = Math.min(next.length, current.items.length);
492
+ while (changedFrom < limit &&
493
+ next[changedFrom] === current.items[changedFrom])
494
+ changedFrom += 1;
495
+ }
496
+ return advanceInteractiveHistoryState(current, next, changedFrom);
497
+ });
498
+ };
451
499
  const activeAttemptThinkingItemsRef = useRef([]);
452
- const [transcriptScrollOffset, setTranscriptScrollOffset] = useState(0);
453
- // The pure TUI view model classifies the session identity (fresh/resumed/
454
- // started) and projects the rendered transcript. Startup diagnostics are
455
- // useful before the first prompt, but they are not conversation history and
456
- // must not suppress the new-session welcome panel. Only real user/assistant
457
- // transcript entries start a conversation; every other kind (thinking,
458
- // context, tool, shell, notices, results, and so on) is operational
459
- // bookkeeping that must not hide the fresh-session welcome. The original
460
- // loaded transcript decides whether the session was resumed, separately from
461
- // the live history that grows while the session runs. A session is resumed
462
- // only when it was opened through `resume` and the original transcript
463
- // already contained real conversation content. Fullscreen projects only the
464
- // newest transcript tail that fits the fixed viewport, leaving the
465
- // composer/status chrome intact and keeping the active stream visible.
466
- // Classic and screen-reader modes always render the full history exactly as
467
- // before.
468
- const { transcriptEntries, transcriptPageRows, maxTranscriptScrollOffset, resumed, freshSession, hasConversationHistory, } = useMemo(() => projectTuiView({
469
- initialHistory,
470
- history,
471
- resume: resume !== undefined,
472
- fixedViewport,
473
- screenReader: axScreenReader,
474
- rows,
475
- width,
476
- scrollOffset: transcriptScrollOffset,
477
- detailedTranscript: thinkingExpanded || runtimeSettings.verbose,
478
- }), [
479
- initialHistory,
480
- history,
481
- resume,
482
- fixedViewport,
483
- axScreenReader,
484
- rows,
485
- width,
486
- transcriptScrollOffset,
487
- thinkingExpanded,
488
- runtimeSettings.verbose,
489
- ]);
500
+ const [transcriptScrollOffset, setTranscriptScrollOffsetState] = useState(0);
501
+ const transcriptScrollOffsetRef = useRef(0);
502
+ const setTranscriptScrollOffset = (offset) => {
503
+ transcriptScrollOffsetRef.current = offset;
504
+ setTranscriptScrollOffsetState(offset);
505
+ };
490
506
  const sessionLoadRef = useRef(0);
491
507
  const [turnDiffs, setTurnDiffs] = useState([]);
492
508
  const turnNumberRef = useRef(0);
@@ -521,6 +537,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
521
537
  const runtimePreferencesRef = useRef(runtimePreferences);
522
538
  const [permission, setPermission] = useState(null);
523
539
  const permissionRef = useRef(null);
540
+ const permissionQueueRef = useRef([]);
524
541
  const [permissionSelection, setPermissionSelection] = useState(0);
525
542
  const [permissionFeedbackMode, setPermissionFeedbackMode] = useState(false);
526
543
  const [permissionRuleEditor, setPermissionRuleEditor] = useState(null);
@@ -528,6 +545,59 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
528
545
  const toolPermissionModel = useMemo(() => permission?.kind === 'tool'
529
546
  ? projectTuiToolPermission(permission.call, runtimeCwd, sensitiveValues, permission.decision, dataPlane)
530
547
  : null, [dataPlane, permission, runtimeCwd, sensitiveValues]);
548
+ const activatePermission = (pending) => {
549
+ const projected = pending.kind === 'tool'
550
+ ? projectTuiToolPermission(pending.call, runtimeCwdRef.current, sensitiveValues, pending.decision, dataPlane)
551
+ : null;
552
+ const editableRule = projected?.options.find((option) => option.editableRule)?.editableRule;
553
+ clearComposerInput();
554
+ setPermissionSelection(0);
555
+ setPermissionFeedbackMode(false);
556
+ setPermissionRuleEditor(editableRule ? createComposerEditor(editableRule.initialValue) : null);
557
+ permissionRef.current = pending;
558
+ setPermission(pending);
559
+ pending.notificationTimer = setTimeout(() => {
560
+ if (pending.settled)
561
+ return;
562
+ serviceRef.current?.notify?.(sessionIdRef.current ?? undefined, `Approval required for ${pending.call.name}`, 'permission_prompt', 'Praxis');
563
+ }, notificationDelayMs);
564
+ };
565
+ const resolvePermission = (pending, approval) => {
566
+ if (pending.settled)
567
+ return;
568
+ pending.settled = true;
569
+ if (pending.notificationTimer) {
570
+ clearTimeout(pending.notificationTimer);
571
+ pending.notificationTimer = null;
572
+ }
573
+ const queue = permissionQueueRef.current;
574
+ const index = queue.indexOf(pending);
575
+ if (index >= 0)
576
+ queue.splice(index, 1);
577
+ if (permissionRef.current === pending) {
578
+ permissionRef.current = null;
579
+ setPermission((current) => (current === pending ? null : current));
580
+ }
581
+ pending.settle(approval);
582
+ const next = queue[0];
583
+ if (next)
584
+ activatePermission(next);
585
+ };
586
+ const drainPermissionQueue = () => {
587
+ const queued = permissionQueueRef.current.splice(0);
588
+ permissionRef.current = null;
589
+ setPermission(null);
590
+ for (const pending of queued) {
591
+ if (pending.settled)
592
+ continue;
593
+ pending.settled = true;
594
+ if (pending.notificationTimer) {
595
+ clearTimeout(pending.notificationTimer);
596
+ pending.notificationTimer = null;
597
+ }
598
+ pending.settle(false);
599
+ }
600
+ };
531
601
  const [elicitation, setElicitation] = useState(null);
532
602
  const elicitationRef = useRef(null);
533
603
  const [elicitationForm, setElicitationForm] = useState(null);
@@ -617,6 +687,245 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
617
687
  permissionMode: runtimePreferences.permissionMode,
618
688
  ...(contextWindowTokens === undefined ? {} : { contextWindowTokens }),
619
689
  };
690
+ const workspaceDirectories = useMemo(() => [
691
+ runtimeCwd,
692
+ ...runtimePreferences.additionalDirectories.filter((path) => path !== runtimeCwd),
693
+ ].map((path) => ({ path, original: path === runtimeCwd })), [runtimeCwd, runtimePreferences.additionalDirectories]);
694
+ const permissionPrioritySurface = useMemo(() => {
695
+ if (!permission)
696
+ return null;
697
+ if (permission.kind === 'tool' && toolPermissionModel) {
698
+ return projectTuiPermissionSurface({
699
+ kind: 'tool-request',
700
+ model: toolPermissionModel,
701
+ selection: permissionSelection,
702
+ feedbackMode: permissionFeedbackMode,
703
+ feedback: input,
704
+ ruleEditor: permissionRuleEditor,
705
+ });
706
+ }
707
+ return projectTuiPermissionSurface({
708
+ kind: 'recovery-request',
709
+ heading: `Retry interrupted ${permission.call.name}?`,
710
+ display: describeTool(permission.call, sensitiveValues),
711
+ selection: permissionSelection,
712
+ feedbackMode: permissionFeedbackMode,
713
+ feedback: input,
714
+ });
715
+ }, [
716
+ input,
717
+ permission,
718
+ permissionFeedbackMode,
719
+ permissionRuleEditor,
720
+ permissionSelection,
721
+ sensitiveValues,
722
+ toolPermissionModel,
723
+ ]);
724
+ const permissionManagementSurface = useMemo(() => {
725
+ if (!menu)
726
+ return null;
727
+ switch (menu.kind) {
728
+ case 'permission-dashboard':
729
+ return projectTuiPermissionSurface({
730
+ kind: menu.kind,
731
+ tabIndex: menu.tabIndex,
732
+ selectedIndex: menu.selectedIndex,
733
+ query: menu.query,
734
+ rules: menu.rules,
735
+ recentDenied,
736
+ retryingDeniedId,
737
+ workspaceDirectories,
738
+ });
739
+ case 'permission-rule-input':
740
+ return projectTuiPermissionSurface({
741
+ kind: menu.kind,
742
+ behavior: menu.behavior,
743
+ value: input,
744
+ });
745
+ case 'permission-scope':
746
+ return projectTuiPermissionSurface({
747
+ kind: menu.kind,
748
+ behavior: menu.behavior,
749
+ rule: menu.rule,
750
+ selectedIndex: menu.selectedIndex,
751
+ settingsDirectory,
752
+ });
753
+ case 'permission-delete':
754
+ return projectTuiPermissionSurface({
755
+ kind: menu.kind,
756
+ rule: menu.rule,
757
+ selectedIndex: menu.selectedIndex,
758
+ });
759
+ case 'workspace-directory-input':
760
+ return projectTuiPermissionSurface({
761
+ kind: menu.kind,
762
+ value: input,
763
+ });
764
+ case 'workspace-directory-delete':
765
+ return projectTuiPermissionSurface({
766
+ kind: menu.kind,
767
+ path: menu.path,
768
+ selectedIndex: menu.selectedIndex,
769
+ });
770
+ default:
771
+ return null;
772
+ }
773
+ }, [
774
+ input,
775
+ menu,
776
+ recentDenied,
777
+ retryingDeniedId,
778
+ settingsDirectory,
779
+ workspaceDirectories,
780
+ ]);
781
+ const legacySecondarySurface = useMemo(() => ({ kind: 'legacy-secondary' }), []);
782
+ const helpMenu = menu?.kind === 'help' ? menu : null;
783
+ const helpSurface = useMemo(() => helpMenu === null
784
+ ? null
785
+ : projectTuiHelpSurface({
786
+ invocation: helpMenu.invocation,
787
+ tabIndex: helpMenu.tabIndex,
788
+ selectedIndex: helpMenu.selectedIndex,
789
+ builtinCommands: builtinSlashCommands,
790
+ customCommands: customSlashCommands,
791
+ }), [helpMenu, builtinSlashCommands, customSlashCommands]);
792
+ const shortcutHelpSurface = useMemo(() => projectTuiHelpSurface({
793
+ invocation: '?',
794
+ tabIndex: 0,
795
+ selectedIndex: 0,
796
+ builtinCommands: builtinSlashCommands,
797
+ customCommands: customSlashCommands,
798
+ }), [builtinSlashCommands, customSlashCommands]);
799
+ const decisionElevatedMode = runtimeSettings.useAutoModeDuringPlan
800
+ ? 'auto'
801
+ : allowDangerouslySkipPermissions
802
+ ? 'bypassPermissions'
803
+ : 'acceptEdits';
804
+ const planDecisionSurface = useMemo(() => planApproval
805
+ ? projectTuiDecisionSurface({
806
+ kind: 'plan-approval',
807
+ request: planApproval.request,
808
+ selectedIndex: planApprovalSelection,
809
+ feedbackMode: planApprovalFeedbackMode,
810
+ feedback: input,
811
+ elevatedMode: decisionElevatedMode,
812
+ })
813
+ : null, [
814
+ planApproval,
815
+ planApprovalFeedbackMode,
816
+ planApprovalSelection,
817
+ input,
818
+ decisionElevatedMode,
819
+ ]);
820
+ const questionDecisionSurface = useMemo(() => question
821
+ ? projectTuiDecisionSurface({
822
+ kind: 'question',
823
+ questions: question.questions,
824
+ questionIndex: question.index,
825
+ answer: input,
826
+ })
827
+ : null, [question, input]);
828
+ const secondarySurface = menu === null
829
+ ? undefined
830
+ : menu.kind === 'help'
831
+ ? (helpSurface ?? undefined)
832
+ : (permissionManagementSurface ?? legacySecondarySurface);
833
+ const screenSurfaces = useMemo(() => ({
834
+ ...(selectingSession
835
+ ? { sessionPicker: { kind: 'session-picker' } }
836
+ : {}),
837
+ ...(externalEditorRequest !== null ||
838
+ keybindingsEditing ||
839
+ memoryEditorRequest !== null
840
+ ? { priority: { kind: 'editor-wait' } }
841
+ : permission !== null && permissionPrioritySurface !== null
842
+ ? {
843
+ priority: {
844
+ kind: 'permission',
845
+ surface: permissionPrioritySurface,
846
+ },
847
+ }
848
+ : planApproval !== null && planDecisionSurface !== null
849
+ ? { priority: planDecisionSurface }
850
+ : question !== null && questionDecisionSurface !== null
851
+ ? { priority: questionDecisionSurface }
852
+ : elicitation !== null
853
+ ? { priority: { kind: 'elicitation' } }
854
+ : {}),
855
+ ...(secondarySurface === undefined
856
+ ? {}
857
+ : { secondary: secondarySurface }),
858
+ overlays: [
859
+ ...(commandPaletteVisible
860
+ ? [{ kind: 'command-palette' }]
861
+ : []),
862
+ ...(filePickerVisible ? [{ kind: 'file-picker' }] : []),
863
+ ...(exitConfirmation ? [{ kind: 'exit-confirmation' }] : []),
864
+ ],
865
+ }), [
866
+ selectingSession,
867
+ externalEditorRequest,
868
+ keybindingsEditing,
869
+ memoryEditorRequest,
870
+ permission,
871
+ permissionPrioritySurface,
872
+ planApproval,
873
+ question,
874
+ planDecisionSurface,
875
+ questionDecisionSurface,
876
+ elicitation,
877
+ secondarySurface,
878
+ commandPaletteVisible,
879
+ filePickerVisible,
880
+ exitConfirmation,
881
+ ]);
882
+ const previousTuiScreenRef = useRef(undefined);
883
+ const screen = useMemo(() => projectTuiScreen({
884
+ presentation,
885
+ conversation: {
886
+ initialHistory,
887
+ history,
888
+ resumeRequested: resume !== undefined,
889
+ scrollOffset: transcriptScrollOffset,
890
+ detailed: thinkingExpanded || runtimeSettings.verbose,
891
+ activeText,
892
+ activeThinking,
893
+ historyChange: historyState.change,
894
+ },
895
+ sessionId,
896
+ surfaces: screenSurfaces,
897
+ }, previousTuiScreenRef.current), [
898
+ presentation,
899
+ initialHistory,
900
+ history,
901
+ resume,
902
+ transcriptScrollOffset,
903
+ thinkingExpanded,
904
+ runtimeSettings.verbose,
905
+ activeText,
906
+ activeThinking,
907
+ historyState.change,
908
+ sessionId,
909
+ screenSurfaces,
910
+ ]);
911
+ useEffect(() => {
912
+ previousTuiScreenRef.current = screen;
913
+ }, [screen]);
914
+ const conversationScreen = screen.body.kind === 'conversation' ? screen.body : undefined;
915
+ const selectedPriority = conversationScreen?.foreground.kind === 'priority'
916
+ ? conversationScreen.foreground.surface
917
+ : undefined;
918
+ const selectedSecondarySurface = conversationScreen?.foreground.kind === 'secondary'
919
+ ? (conversationScreen.foreground.surface ?? null)
920
+ : null;
921
+ const selectedOverlays = conversationScreen?.foreground.kind === 'compose'
922
+ ? conversationScreen.foreground.overlays
923
+ : [];
924
+ const selectedCommandPalette = selectedOverlays.find((overlay) => overlay.kind === 'command-palette');
925
+ const selectedFilePicker = selectedOverlays.find((overlay) => overlay.kind === 'file-picker');
926
+ const selectedExitConfirmation = selectedOverlays.find((overlay) => overlay.kind === 'exit-confirmation');
927
+ const transcriptPageRows = conversationScreen?.transcript.pageRows ?? 2;
928
+ const maxTranscriptScrollOffset = conversationScreen?.transcript.maxScrollOffset ?? 0;
620
929
  const statusAuthSource = process.env.PRAXIS_API_KEY
621
930
  ? 'PRAXIS_API_KEY'
622
931
  : process.env.ANTHROPIC_API_KEY
@@ -631,10 +940,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
631
940
  const projectSettings = existsSync(join(runtimeCwd, dataPlane === 'native' ? '.praxis' : '.claude', 'settings.json')) || existsSync(join(runtimeCwd, 'settings.json'));
632
941
  return projectSettings ? 'User settings, Project settings' : 'User settings';
633
942
  })();
634
- const workspaceDirectories = [
635
- runtimeCwd,
636
- ...runtimePreferences.additionalDirectories.filter((path) => path !== runtimeCwd),
637
- ].map((path) => ({ path, original: path === runtimeCwd }));
638
943
  const permissionOptions = useMemo(() => [
639
944
  ...PERMISSION_OPTIONS,
640
945
  ...(allowDangerouslySkipPermissions
@@ -722,7 +1027,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
722
1027
  if (!signal)
723
1028
  return;
724
1029
  const cancel = () => {
725
- permissionRef.current?.resolve(false);
1030
+ drainPermissionQueue();
726
1031
  elicitationRef.current?.resolve({ action: 'cancel' });
727
1032
  questionRef.current?.resolve(null);
728
1033
  planApprovalRef.current?.resolve({ behavior: 'deny' });
@@ -737,7 +1042,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
737
1042
  useEffect(() => () => {
738
1043
  componentMountedRef.current = false;
739
1044
  streamingFrameRef.current?.dispose();
740
- permissionRef.current?.resolve(false);
1045
+ drainPermissionQueue();
741
1046
  elicitationRef.current?.resolve({ action: 'cancel' });
742
1047
  questionRef.current?.resolve(null);
743
1048
  planApprovalRef.current?.resolve({ behavior: 'deny' });
@@ -760,7 +1065,10 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
760
1065
  // thinking, or completion entry that it textually precedes.
761
1066
  streamingFrameRef.current?.flush();
762
1067
  setTranscriptScrollOffset(0);
763
- setHistory((current) => [...current, line]);
1068
+ setHistoryState((current) => {
1069
+ const items = [...current.items, line];
1070
+ return advanceInteractiveHistoryState(current, items, current.items.length);
1071
+ });
764
1072
  };
765
1073
  useEffect(() => {
766
1074
  if (initialThemeSettings !== undefined) {
@@ -1152,25 +1460,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1152
1460
  exitConfirmationTimerRef.current = null;
1153
1461
  setExitConfirmation(false);
1154
1462
  };
1155
- const armExitConfirmation = () => {
1156
- if (exitConfirmation) {
1157
- permissionRef.current?.resolve(false);
1158
- elicitationRef.current?.resolve({ action: 'cancel' });
1159
- questionRef.current?.resolve(null);
1160
- planApprovalRef.current?.resolve({ behavior: 'deny' });
1161
- onCancel?.();
1162
- exit();
1163
- return;
1164
- }
1165
- clearComposerInput();
1166
- setExitConfirmation(true);
1167
- if (exitConfirmationTimerRef.current)
1168
- clearTimeout(exitConfirmationTimerRef.current);
1169
- exitConfirmationTimerRef.current = setTimeout(() => {
1170
- exitConfirmationTimerRef.current = null;
1171
- setExitConfirmation(false);
1172
- }, 1_500);
1173
- };
1174
1463
  useEffect(() => {
1175
1464
  setCommandSelection((current) => Math.min(current, Math.max(0, matchingSlashCommands.length - 1)));
1176
1465
  }, [matchingSlashCommands.length]);
@@ -1371,37 +1660,18 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1371
1660
  }
1372
1661
  };
1373
1662
  const requestApproval = (call, kind, decision) => new Promise((resolveApproval) => {
1374
- const projected = kind === 'tool'
1375
- ? projectTuiToolPermission(call, runtimeCwdRef.current, sensitiveValues, decision, dataPlane)
1376
- : null;
1377
- const editableRule = projected?.options.find((option) => option.editableRule)?.editableRule;
1378
- let settled = false;
1379
- const notificationTimer = setTimeout(() => {
1380
- if (settled)
1381
- return;
1382
- serviceRef.current?.notify?.(sessionIdRef.current ?? undefined, `Approval required for ${call.name}`, 'permission_prompt', 'Praxis');
1383
- }, notificationDelayMs);
1384
1663
  const pending = {
1385
1664
  kind,
1386
1665
  call,
1387
1666
  ...(decision ? { decision } : {}),
1388
- resolve: (approved) => {
1389
- if (settled)
1390
- return;
1391
- settled = true;
1392
- clearTimeout(notificationTimer);
1393
- if (permissionRef.current === pending)
1394
- permissionRef.current = null;
1395
- setPermission((current) => (current === pending ? null : current));
1396
- resolveApproval(approved);
1397
- },
1667
+ notificationTimer: null,
1668
+ settled: false,
1669
+ settle: resolveApproval,
1670
+ resolve: (approved) => resolvePermission(pending, approved),
1398
1671
  };
1399
- clearComposerInput();
1400
- setPermissionSelection(0);
1401
- setPermissionFeedbackMode(false);
1402
- setPermissionRuleEditor(editableRule ? createComposerEditor(editableRule.initialValue) : null);
1403
- permissionRef.current = pending;
1404
- setPermission(pending);
1672
+ permissionQueueRef.current.push(pending);
1673
+ if (permissionQueueRef.current.length === 1)
1674
+ activatePermission(pending);
1405
1675
  });
1406
1676
  const approveTool = (call, _originalCall, decision) => requestApproval(call, 'tool', decision);
1407
1677
  const requestElicitation = (request) => new Promise((resolveResult) => {
@@ -1514,6 +1784,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1514
1784
  const retireService = () => {
1515
1785
  serviceEpochRef.current += 1;
1516
1786
  scheduledWaitRef.current?.abort();
1787
+ drainPermissionQueue();
1517
1788
  const current = serviceRef.current;
1518
1789
  serviceRef.current = null;
1519
1790
  if (!current?.close)
@@ -2872,74 +3143,150 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
2872
3143
  const codePoint = character.codePointAt(0) ?? 0;
2873
3144
  return codePoint >= 32 && codePoint !== 127;
2874
3145
  });
3146
+ const composerKey = {
3147
+ value,
3148
+ left: key.leftArrow,
3149
+ right: key.rightArrow,
3150
+ backspace: key.backspace,
3151
+ delete: key.delete,
3152
+ ctrl: key.ctrl,
3153
+ meta: key.meta,
3154
+ escape: key.escape || value === '\u001B',
3155
+ };
2875
3156
  const editor = () => createComposerEditor(inputRef.current, inputCursorRef.current);
2876
3157
  const editComposer = () => {
2877
- if (key.leftArrow) {
2878
- updateComposerEditor(key.meta
2879
- ? moveComposerCursorByWord(editor(), 'backward')
2880
- : moveComposerCursorAcrossImages(editor(), -1));
3158
+ const transition = routeComposerKey(editor(), composerKey);
3159
+ if (transition.kind === 'edit') {
3160
+ updateComposerEditor(transition.editor);
2881
3161
  return true;
2882
3162
  }
2883
- if (key.rightArrow) {
2884
- updateComposerEditor(key.meta
2885
- ? moveComposerCursorByWord(editor(), 'forward')
2886
- : moveComposerCursorAcrossImages(editor(), 1));
2887
- return true;
2888
- }
2889
- if (controlKey('a')) {
2890
- updateComposerEditor(createComposerEditor(inputRef.current, 0));
2891
- return true;
2892
- }
2893
- if (controlKey('e')) {
2894
- updateComposerEditor(createComposerEditor(inputRef.current));
2895
- return true;
2896
- }
2897
- if (controlKey('b')) {
2898
- updateComposerEditor(moveComposerCursorAcrossImages(editor(), -1));
2899
- return true;
2900
- }
2901
- if (controlKey('f')) {
2902
- updateComposerEditor(moveComposerCursorAcrossImages(editor(), 1));
2903
- return true;
2904
- }
2905
- if (controlKey('w')) {
2906
- updateComposerEditor(deleteComposerWordBackward(editor()));
2907
- return true;
2908
- }
2909
- if (controlKey('u')) {
2910
- updateComposerEditor(deleteComposerToStart(editor()));
2911
- return true;
2912
- }
2913
- if (controlKey('k')) {
2914
- updateComposerEditor(deleteComposerToEnd(editor()));
2915
- return true;
2916
- }
2917
- if (key.backspace) {
2918
- updateComposerEditor(deleteComposerImageBackward(editor()));
2919
- return true;
2920
- }
2921
- if (key.delete) {
2922
- updateComposerEditor(deleteComposerImageForward(editor()));
2923
- return true;
2924
- }
2925
- if (!key.ctrl && !key.meta && value && printable) {
2926
- updateComposerEditor(insertComposerText(editor(), value));
2927
- return true;
2928
- }
2929
- return false;
3163
+ if (transition.kind === 'cancel')
3164
+ clearComposerInput();
3165
+ return transition.kind === 'cancel';
2930
3166
  };
2931
- if (controlKey('c')) {
2932
- armExitConfirmation();
2933
- return;
2934
- }
2935
- if (controlKey('z')) {
2936
- if (!processSuspendRequestedRef.current) {
2937
- processSuspendRequestedRef.current = true;
2938
- setProcessSuspendRequested(true);
3167
+ const interactionComposer = () => ({
3168
+ mode: runtimeSettingsRef.current.editor === 'vim'
3169
+ ? vimInsertMode
3170
+ ? 'vim-insert'
3171
+ : 'vim-normal'
3172
+ : 'readline',
3173
+ editor: editor(),
3174
+ lastCancelAtMs: lastEscapeAtRef.current,
3175
+ });
3176
+ const applyInteractionEffects = (effects) => {
3177
+ for (const effect of effects) {
3178
+ if (effect.kind === 'request-process-suspension') {
3179
+ processSuspendRequestedRef.current = true;
3180
+ setProcessSuspendRequested(true);
3181
+ }
3182
+ else if (effect.kind === 'set-transcript-scroll-offset') {
3183
+ setTranscriptScrollOffset(effect.offset);
3184
+ }
3185
+ else if (effect.kind === 'interrupt-turn') {
3186
+ turnControllerRef.current?.abort();
3187
+ }
3188
+ else if (effect.kind === 'arm-exit-confirmation') {
3189
+ setExitConfirmation(true);
3190
+ if (exitConfirmationTimerRef.current)
3191
+ clearTimeout(exitConfirmationTimerRef.current);
3192
+ exitConfirmationTimerRef.current = setTimeout(() => {
3193
+ exitConfirmationTimerRef.current = null;
3194
+ setExitConfirmation(false);
3195
+ }, 1_500);
3196
+ }
3197
+ else if (effect.kind === 'dismiss-exit-confirmation') {
3198
+ dismissExitConfirmation();
3199
+ }
3200
+ else if (effect.kind === 'exit-application') {
3201
+ drainPermissionQueue();
3202
+ elicitationRef.current?.resolve({ action: 'cancel' });
3203
+ questionRef.current?.resolve(null);
3204
+ planApprovalRef.current?.resolve({ behavior: 'deny' });
3205
+ onCancel?.();
3206
+ exit();
3207
+ }
3208
+ else if (effect.kind === 'set-vim-insert-mode') {
3209
+ setVimInsertMode(effect.insert);
3210
+ }
3211
+ else if (effect.kind === 'set-composer-editor') {
3212
+ updateComposerEditor(effect.editor);
3213
+ }
3214
+ else if (effect.kind === 'clear-composer') {
3215
+ clearComposerInput();
3216
+ }
3217
+ else if (effect.kind === 'record-composer-cancel') {
3218
+ lastEscapeAtRef.current = effect.timestamp;
3219
+ }
3220
+ else if (effect.kind === 'cancel-tui-layer') {
3221
+ switch (effect.target) {
3222
+ case 'permission':
3223
+ permissionRef.current?.resolve(false);
3224
+ break;
3225
+ case 'plan-approval':
3226
+ planApprovalRef.current?.resolve({ behavior: 'deny' });
3227
+ break;
3228
+ case 'question':
3229
+ questionRef.current?.resolve(null);
3230
+ break;
3231
+ case 'elicitation-url-waiting':
3232
+ if (elicitationUrlWaitingRef.current) {
3233
+ elicitationUrlWaitingRef.current = null;
3234
+ setElicitationUrlWaiting(false);
3235
+ setElicitation(null);
3236
+ setElicitationForm(null);
3237
+ clearComposerInput();
3238
+ }
3239
+ break;
3240
+ case 'elicitation-options':
3241
+ setElicitationForm((current) => current?.expandedField
3242
+ ? { ...current, expandedField: undefined }
3243
+ : current);
3244
+ break;
3245
+ case 'elicitation':
3246
+ elicitationRef.current?.resolve({ action: 'cancel' });
3247
+ break;
3248
+ case 'file-picker':
3249
+ setFilePickerOpen(false);
3250
+ break;
3251
+ case 'command-palette':
3252
+ setCommandPaletteOpen(false);
3253
+ break;
3254
+ default: {
3255
+ const unhandledTarget = effect.target;
3256
+ return unhandledTarget;
3257
+ }
3258
+ }
3259
+ }
3260
+ else {
3261
+ const unhandledEffect = effect;
3262
+ return unhandledEffect;
3263
+ }
2939
3264
  }
3265
+ };
3266
+ if (controlKey('c') || controlKey('z')) {
3267
+ const globalInteraction = routeTuiInteraction({
3268
+ suspensionPending: processSuspendRequestedRef.current,
3269
+ exitConfirmationArmed: exitConfirmation,
3270
+ layer: { kind: 'none' },
3271
+ busy,
3272
+ viewport: {
3273
+ enabled: false,
3274
+ offset: 0,
3275
+ maxOffset: 0,
3276
+ pageRows: 0,
3277
+ },
3278
+ composer: interactionComposer(),
3279
+ }, {
3280
+ globalIntent: controlKey('c') ? 'exit' : 'suspend',
3281
+ scrollIntent: 'none',
3282
+ action: undefined,
3283
+ composerKey,
3284
+ timestamp: Date.now(),
3285
+ callerIntent: 'none',
3286
+ });
3287
+ applyInteractionEffects(globalInteraction.effects);
2940
3288
  return;
2941
3289
  }
2942
- dismissExitConfirmation();
2943
3290
  const keybindingContexts = menuRef.current
2944
3291
  ? menuRef.current.kind === 'diff'
2945
3292
  ? ['DiffDialog']
@@ -2976,57 +3323,90 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
2976
3323
  if (!keybindingAction) {
2977
3324
  keybindingAction = resolveTuiKeybinding(keybindings, keybindingContexts, inputChord);
2978
3325
  }
2979
- if (!keybindingAction &&
2980
- inputChord &&
2981
- hasTuiKeybindingPrefix(keybindings, keybindingContexts, inputChord)) {
3326
+ const scrollIntent = key.pageUp
3327
+ ? 'page-older'
3328
+ : key.pageDown
3329
+ ? 'page-newer'
3330
+ : controlKey('u') || controlKey('b')
3331
+ ? 'half-page-older'
3332
+ : controlKey('f') || controlKey('n')
3333
+ ? 'half-page-newer'
3334
+ : inputRef.current.length === 0 && key.upArrow
3335
+ ? 'line-older'
3336
+ : inputRef.current.length === 0 && key.downArrow
3337
+ ? 'line-newer'
3338
+ : 'none';
3339
+ const hasPendingPrefix = !keybindingAction &&
3340
+ inputChord !== null &&
3341
+ hasTuiKeybindingPrefix(keybindings, keybindingContexts, inputChord);
3342
+ if (hasPendingPrefix) {
2982
3343
  keySequenceRef.current = { chord: inputChord, at: Date.now() };
3344
+ }
3345
+ const layer = hasPendingPrefix
3346
+ ? { kind: 'pending-prefix' }
3347
+ : permission
3348
+ ? { kind: 'cancelable', target: 'permission' }
3349
+ : planApproval
3350
+ ? { kind: 'cancelable', target: 'plan-approval' }
3351
+ : question
3352
+ ? { kind: 'cancelable', target: 'question' }
3353
+ : elicitation
3354
+ ? {
3355
+ kind: 'cancelable',
3356
+ target: elicitationUrlWaiting
3357
+ ? 'elicitation-url-waiting'
3358
+ : elicitationForm?.expandedField
3359
+ ? 'elicitation-options'
3360
+ : 'elicitation',
3361
+ }
3362
+ : selectingSession
3363
+ ? { kind: 'delegated', target: 'session-picker' }
3364
+ : menuRef.current
3365
+ ? { kind: 'delegated', target: 'menu' }
3366
+ : filePickerVisible
3367
+ ? { kind: 'cancelable', target: 'file-picker' }
3368
+ : commandPaletteVisible
3369
+ ? { kind: 'cancelable', target: 'command-palette' }
3370
+ : { kind: 'none' };
3371
+ const callerIntent = !busy && value === '?' && inputRef.current.length === 0
3372
+ ? 'toggle-shortcuts'
3373
+ : runtimeSettingsRef.current.leftArrowOpensAgents &&
3374
+ key.leftArrow &&
3375
+ inputRef.current.length === 0 &&
3376
+ availableAgents.length > 0
3377
+ ? 'open-agents'
3378
+ : key.return &&
3379
+ (key.shift || key.meta) &&
3380
+ keybindingAction === undefined
3381
+ ? 'implicit-newline'
3382
+ : 'none';
3383
+ const interaction = routeTuiInteraction({
3384
+ suspensionPending: processSuspendRequestedRef.current,
3385
+ exitConfirmationArmed: exitConfirmation,
3386
+ layer,
3387
+ busy,
3388
+ viewport: {
3389
+ enabled: fixedViewport,
3390
+ offset: transcriptScrollOffsetRef.current,
3391
+ maxOffset: maxTranscriptScrollOffset,
3392
+ pageRows: transcriptPageRows,
3393
+ },
3394
+ composer: interactionComposer(),
3395
+ }, {
3396
+ globalIntent: 'none',
3397
+ scrollIntent,
3398
+ action: keybindingAction,
3399
+ composerKey,
3400
+ timestamp: Date.now(),
3401
+ callerIntent,
3402
+ });
3403
+ applyInteractionEffects(interaction.effects);
3404
+ if (interaction.disposition === 'handled') {
2983
3405
  return;
2984
3406
  }
2985
3407
  const isKeybinding = (action) => keybindingAction === action;
2986
- if (fixedViewport &&
2987
- menuRef.current === null &&
2988
- !permission &&
2989
- !planApproval &&
2990
- !question &&
2991
- !elicitation &&
2992
- !selectingSession) {
2993
- const page = transcriptPageRows;
2994
- const scrollDelta = key.pageUp
2995
- ? page
2996
- : key.pageDown
2997
- ? -page
2998
- : controlKey('u') || controlKey('b')
2999
- ? page / 2
3000
- : controlKey('f') || controlKey('n')
3001
- ? -page / 2
3002
- : inputRef.current.length === 0 && key.upArrow
3003
- ? 1
3004
- : inputRef.current.length === 0 && key.downArrow
3005
- ? -1
3006
- : 0;
3007
- if (scrollDelta !== 0) {
3008
- setTranscriptScrollOffset((current) => Math.min(Math.max(0, maxTranscriptScrollOffset), Math.max(0, current + Math.trunc(scrollDelta))));
3009
- return;
3010
- }
3011
- }
3012
- if (runtimeSettingsRef.current.editor === 'vim' &&
3013
- !permission &&
3014
- !planApproval &&
3015
- !question &&
3016
- !elicitation &&
3017
- menuRef.current === null) {
3018
- if (!vimInsertMode) {
3019
- if (value === 'i' || value === 'a')
3020
- setVimInsertMode(true);
3021
- else if (key.leftArrow || key.rightArrow)
3022
- editComposer();
3023
- return;
3024
- }
3025
- if (key.escape || value === '\u001B') {
3026
- setVimInsertMode(false);
3027
- return;
3028
- }
3029
- }
3408
+ if (hasPendingPrefix)
3409
+ return;
3030
3410
  if (permission) {
3031
3411
  const options = permission.kind === 'tool' && toolPermissionModel
3032
3412
  ? toolPermissionModel.options
@@ -3172,10 +3552,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3172
3552
  }
3173
3553
  permission.resolve(feedback ? { behavior: 'deny', message: feedback } : false);
3174
3554
  };
3175
- if (key.escape) {
3176
- permission.resolve(false);
3177
- }
3178
- else if (permissionFeedbackMode) {
3555
+ if (permissionFeedbackMode) {
3179
3556
  if (key.tab) {
3180
3557
  clearComposerInput();
3181
3558
  setPermissionFeedbackMode(false);
@@ -3256,10 +3633,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3256
3633
  ...(feedback ? { feedback } : {}),
3257
3634
  });
3258
3635
  };
3259
- if (key.escape) {
3260
- planApproval.resolve({ behavior: 'deny' });
3261
- }
3262
- else if (planApprovalFeedbackMode) {
3636
+ if (planApprovalFeedbackMode) {
3263
3637
  if (key.tab) {
3264
3638
  clearComposerInput();
3265
3639
  setPlanApprovalFeedbackMode(false);
@@ -3296,10 +3670,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3296
3670
  return;
3297
3671
  }
3298
3672
  if (question) {
3299
- if (key.escape) {
3300
- question.resolve(null);
3301
- }
3302
- else if (key.return) {
3673
+ if (key.return) {
3303
3674
  try {
3304
3675
  const current = question.questions[question.index];
3305
3676
  if (!current)
@@ -3353,13 +3724,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3353
3724
  setElicitationForm(null);
3354
3725
  clearComposerInput();
3355
3726
  };
3356
- if (key.escape) {
3357
- if (elicitationUrlWaiting)
3358
- dismissUrlDialog();
3359
- else
3360
- elicitation.resolve({ action: 'cancel' });
3361
- }
3362
- else if (key.leftArrow || key.rightArrow) {
3727
+ if (key.leftArrow || key.rightArrow) {
3363
3728
  setElicitationForm({
3364
3729
  ...form,
3365
3730
  focusIndex: form.focusIndex === 0 ? 1 : 0,
@@ -3385,7 +3750,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3385
3750
  return;
3386
3751
  }
3387
3752
  if (form.expandedField && currentField) {
3388
- if (key.escape || key.leftArrow) {
3753
+ if (key.leftArrow) {
3389
3754
  setElicitationForm({ ...form, expandedField: undefined });
3390
3755
  }
3391
3756
  else if (key.upArrow) {
@@ -3404,9 +3769,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3404
3769
  setElicitationForm(typeaheadElicitationOption(form, value));
3405
3770
  }
3406
3771
  }
3407
- else if (key.escape) {
3408
- elicitation.resolve({ action: 'cancel' });
3409
- }
3410
3772
  else if (key.upArrow) {
3411
3773
  move(-1);
3412
3774
  }
@@ -3973,13 +4335,14 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3973
4335
  return;
3974
4336
  }
3975
4337
  if (activeMenu.kind === 'help') {
3976
- if (key.escape) {
4338
+ if (key.escape || value === '\u001B') {
3977
4339
  updateMenu(null);
3978
4340
  return;
3979
4341
  }
3980
4342
  if (key.leftArrow || key.rightArrow) {
3981
4343
  updateMenu({
3982
4344
  kind: 'help',
4345
+ invocation: activeMenu.invocation,
3983
4346
  tabIndex: Math.max(0, Math.min(2, activeMenu.tabIndex + (key.leftArrow ? -1 : 1))),
3984
4347
  selectedIndex: 0,
3985
4348
  });
@@ -5175,8 +5538,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5175
5538
  void backgrounding;
5176
5539
  return;
5177
5540
  }
5178
- if (isKeybinding('chat:cancel'))
5179
- turnControllerRef.current?.abort();
5180
5541
  return;
5181
5542
  }
5182
5543
  if (isKeybinding('chat:imagePaste')) {
@@ -5192,10 +5553,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5192
5553
  return;
5193
5554
  }
5194
5555
  if (filePickerVisible) {
5195
- if (key.escape || value === '\u001B') {
5196
- setFilePickerOpen(false);
5197
- return;
5198
- }
5199
5556
  if (key.upArrow) {
5200
5557
  setFileSelection((current) => Math.max(0, current - 1));
5201
5558
  return;
@@ -5220,13 +5577,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5220
5577
  updateMenu({ kind: 'agents', agents: availableAgents, selectedIndex: 0 });
5221
5578
  return;
5222
5579
  }
5223
- if (isKeybinding('chat:cancel')) {
5224
- const now = Date.now();
5225
- if (now - lastEscapeAtRef.current <= 500)
5226
- clearComposerInput();
5227
- lastEscapeAtRef.current = now;
5228
- return;
5229
- }
5230
5580
  if (isKeybinding('chat:cycleMode')) {
5231
5581
  const currentIndex = permissionOptions.findIndex((option) => option.mode === runtimePreferences.permissionMode);
5232
5582
  const next = permissionOptions[(currentIndex + 1) % permissionOptions.length];
@@ -5235,10 +5585,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5235
5585
  return;
5236
5586
  }
5237
5587
  if (commandPaletteVisible) {
5238
- if (key.escape) {
5239
- setCommandPaletteOpen(false);
5240
- return;
5241
- }
5242
5588
  if (key.upArrow) {
5243
5589
  if (matchingSlashCommands.length > 0) {
5244
5590
  setCommandSelection((current) => Math.max(0, current - 1));
@@ -5300,7 +5646,12 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5300
5646
  exit();
5301
5647
  }
5302
5648
  else if (prompt === '/help' || prompt === '?') {
5303
- updateMenu({ kind: 'help', tabIndex: 0, selectedIndex: 0 });
5649
+ updateMenu({
5650
+ kind: 'help',
5651
+ invocation: prompt,
5652
+ tabIndex: 0,
5653
+ selectedIndex: 0,
5654
+ });
5304
5655
  }
5305
5656
  else if (prompt === '/new') {
5306
5657
  const previousSessionId = sessionIdRef.current;
@@ -5678,7 +6029,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5678
6029
  ...current,
5679
6030
  { kind: 'user', text: '/context' },
5680
6031
  contextEntry,
5681
- ]);
6032
+ ], (current) => current.length);
5682
6033
  const loading = (async () => {
5683
6034
  try {
5684
6035
  const files = await loadMemoryFiles(keybindingsRoot, runtimeCwdRef.current);
@@ -5695,7 +6046,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5695
6046
  next[next.length - 1] = { ...last, memoryFiles };
5696
6047
  }
5697
6048
  return next;
5698
- });
6049
+ }, (_current, next) => Math.max(0, next.length - 1));
5699
6050
  }
5700
6051
  catch {
5701
6052
  // Leave the Memory files section empty if files cannot be read.
@@ -5842,50 +6193,20 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5842
6193
  clearComposerInput();
5843
6194
  return;
5844
6195
  }
5845
- const transition = routeComposerKey(editor(), {
5846
- value,
5847
- left: key.leftArrow,
5848
- right: key.rightArrow,
5849
- backspace: key.backspace,
5850
- delete: key.delete,
5851
- ctrl: key.ctrl,
5852
- meta: key.meta,
5853
- escape: key.escape || value === '\u001B',
5854
- });
5855
- if (transition.kind === 'cancel') {
5856
- clearComposerInput();
5857
- }
5858
- else if (transition.kind === 'edit') {
5859
- updateComposerEditor(transition.editor);
6196
+ if (filePickerVisible || commandPaletteVisible) {
6197
+ editComposer();
6198
+ return;
5860
6199
  }
5861
6200
  });
5862
- return (_jsx(TuiThemeProvider, { settings: themeSettings, children: _jsx(Box, { flexDirection: "column", ...(!fixedViewport
6201
+ return (_jsx(TuiThemeProvider, { settings: themeSettings, screenReader: axScreenReader, children: _jsx(Box, { flexDirection: "column", ...(!fixedViewport
5863
6202
  ? {}
5864
- : { height: rows, overflowY: 'hidden' }), children: selectingSession ? (_jsx(SessionPicker, { sessions: filteredPickerChoices, selectedIndex: selectedIndex, screenReader: axScreenReader, query: sessionSearch })) : (_jsxs(_Fragment, { children: [!axScreenReader && freshSession ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width, showTips: runtimeSettings.tips, dataPlane: dataPlane })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, !axScreenReader && !resumed && hasConversationHistory ? (_jsx(SessionIdentity, { display: runtimeDisplay, width: width })) : null, _jsx(Box, { ...(fixedViewport
6203
+ : { height: rows, overflowY: 'hidden' }), children: screen.body.kind === 'session-picker' ? (_jsx(SessionPicker, { sessions: filteredPickerChoices, selectedIndex: selectedIndex, screenReader: axScreenReader, query: sessionSearch })) : (_jsxs(_Fragment, { children: [screen.body.intro === 'welcome' ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width, showTips: runtimeSettings.tips, dataPlane: dataPlane })) : null, screen.body.sessionLabel ? (_jsxs(Text, { dimColor: true, children: ["Session ", screen.body.sessionLabel] })) : null, screen.body.intro === 'identity' ? (_jsx(SessionIdentity, { display: runtimeDisplay, width: width })) : null, _jsx(Box, { ...(fixedViewport
5865
6204
  ? {
5866
6205
  flexShrink: 1,
5867
6206
  minHeight: 0,
5868
6207
  overflowY: 'hidden',
5869
6208
  }
5870
- : {}), children: _jsx(Transcript, { entries: transcriptEntries, activeText: activeText, activeThinking: activeThinking, activeStreamVisible: transcriptScrollOffset === 0, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }) }), externalEditorRequest !== null ||
5871
- keybindingsEditing ||
5872
- memoryEditorRequest !== null ? (_jsx(ExternalEditorWait, { screenReader: axScreenReader })) : permission ? (permission.kind === 'tool' && toolPermissionModel ? (_jsx(ToolPermissionDialog, { model: toolPermissionModel, selection: permissionSelection, feedbackMode: permissionFeedbackMode, feedback: input, ruleEditor: permissionRuleEditor, screenReader: axScreenReader })) : (_jsxs(DialogFrame, { title: `Retry interrupted ${permission.call.name}?`, screenReader: axScreenReader, children: [_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: _jsx(Text, { bold: true, children: describeTool(permission.call, sensitiveValues) }) }), _jsx(Text, { children: "Do you want to proceed?" }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 0, children: [selectionPrefix(permissionSelection === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 1, children: [selectionPrefix(permissionSelection === 1, axScreenReader), "2. No"] }), permissionFeedbackMode ? (_jsxs(Text, { children: ["\u203A", ' ', input ||
5873
- (permissionSelection === 0
5874
- ? 'tell Praxis what to do next'
5875
- : 'tell Praxis what to do differently')] })) : null, _jsx(Text, { dimColor: true, children: permissionFeedbackMode
5876
- ? 'Enter to submit · Tab to collapse · Esc to cancel'
5877
- : 'Enter to confirm · Tab to add feedback · Esc to cancel' })] }))) : planApproval ? (_jsxs(DialogFrame, { title: "Ready to code?", screenReader: axScreenReader, children: [_jsx(Text, { children: "Here is Praxis's plan:" }), planApproval.request.plan ? (_jsx(Box, { borderStyle: axScreenReader ? undefined : 'classic', borderLeft: false, borderRight: false, paddingX: axScreenReader ? 0 : 1, marginY: 1, children: _jsx(Text, { children: planApproval.request.plan }) })) : null, _jsx(Text, { dimColor: true, children: "Praxis has written up a plan and is ready to execute. Would you like to proceed?" }), _jsxs(Text, { inverse: !axScreenReader && planApprovalSelection === 0, children: [selectionPrefix(planApprovalSelection === 0, axScreenReader), "1.", ' ', runtimeSettings.useAutoModeDuringPlan
5878
- ? 'Yes, and use auto mode'
5879
- : allowDangerouslySkipPermissions
5880
- ? 'Yes, and bypass permissions'
5881
- : 'Yes, auto-accept edits'] }), _jsxs(Text, { inverse: !axScreenReader && planApprovalSelection === 1, children: [selectionPrefix(planApprovalSelection === 1, axScreenReader), "2. Yes, manually approve edits"] }), _jsxs(Text, { inverse: !axScreenReader && planApprovalSelection === 2, children: [selectionPrefix(planApprovalSelection === 2, axScreenReader), "3. No, keep planning"] }), planApprovalFeedbackMode ? (_jsxs(Text, { children: ["\u203A", ' ', input ||
5882
- (planApprovalSelection === 2
5883
- ? 'Tell Praxis what to change'
5884
- : 'Add feedback for implementation')] })) : null, _jsx(Text, { dimColor: true, children: planApproval.request.planPath }), _jsx(Text, { dimColor: true, children: planApprovalFeedbackMode
5885
- ? 'Enter to submit · Tab to collapse · Esc to cancel'
5886
- : 'Enter to confirm · Tab to add feedback · Esc to cancel' })] })) : question ? (_jsxs(DialogFrame, { title: `${question.questions[question.index]?.header}: ${question.questions[question.index]?.question}`, screenReader: axScreenReader, children: [question.questions[question.index]?.options.map((option, index) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [index + 1, ". ", option.label, " \u2014 ", option.description] }), option.preview ? (_jsx(Text, { dimColor: true, children: option.preview })) : null] }, `${index}-${option.label}`))), _jsxs(Text, { children: [axScreenReader ? 'Current answer: ' : '› ', input || (axScreenReader ? '(empty)' : '')] }), _jsx(Text, { dimColor: true, children: question.questions[question.index]?.multiSelect
5887
- ? 'Enter comma-separated option numbers or custom text · Esc cancels'
5888
- : 'Enter one option number or custom text · Esc cancels' })] })) : elicitation ? (elicitation.request.mode === 'url' ? (_jsx(McpElicitationUrl, { serverName: elicitation.request.serverName, message: elicitation.request.message, url: elicitation.request.url ?? '', waiting: elicitationUrlWaiting, actionLabel: elicitation.request.elicitationId
6209
+ : {}), children: _jsx(Transcript, { entries: screen.body.transcript.entries, activeText: screen.body.transcript.active.text, activeThinking: screen.body.transcript.active.thinking, activeStreamVisible: screen.body.transcript.active.visible, thinkingExpanded: thinkingExpanded, detailedTranscript: screen.body.transcript.readingMode !== 'normal', screenReader: axScreenReader }) }), selectedPriority?.kind === 'editor-wait' ? (_jsx(ExternalEditorWait, { screenReader: axScreenReader })) : selectedPriority?.kind === 'permission' ? (_jsx(PermissionSurface, { model: selectedPriority.surface, width: width, screenReader: axScreenReader })) : selectedPriority?.kind === 'plan-approval' ? (_jsx(DecisionSurface, { model: selectedPriority, width: width, screenReader: axScreenReader })) : selectedPriority?.kind === 'question' ? (_jsx(DecisionSurface, { model: selectedPriority, width: width, screenReader: axScreenReader })) : selectedPriority?.kind === 'elicitation' && elicitation ? (elicitation.request.mode === 'url' ? (_jsx(McpElicitationUrl, { serverName: elicitation.request.serverName, message: elicitation.request.message, url: elicitation.request.url ?? '', waiting: elicitationUrlWaiting, actionLabel: elicitation.request.elicitationId
5889
6210
  ? 'Skip confirmation'
5890
6211
  : 'Continue without waiting', selection: elicitationForm?.focusIndex ?? 0, screenReader: axScreenReader })) : (_jsx(McpElicitationForm, { serverName: elicitation.request.serverName, message: elicitation.request.message, state: elicitationForm ??
5891
6212
  createTuiElicitationForm(elicitation.request.requestedSchema), input: input, screenReader: axScreenReader }))) : menu?.kind === 'model-input' ? (_jsxs(DialogFrame, { title: "Enter model ID", screenReader: axScreenReader, children: [_jsx(Text, { dimColor: true, children: "Enter a model ID supported by the configured provider." }), _jsxs(Text, { children: ["\u203A ", input] }), _jsx(Text, { dimColor: true, children: "Enter confirms \u00B7 Esc cancels" })] })) : menu?.kind === 'export-filename' ? (_jsxs(DialogFrame, { title: "Enter filename:", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["> ", input] }), _jsx(Text, { dimColor: true, children: "Enter to save \u00B7 Esc to go back" })] })) : menu?.kind === 'compact-progress' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "\u273B Compacting conversation\u2026" }), _jsxs(Text, { children: ['▰'.repeat(Math.floor(compactProgress / 2)), '▱'.repeat(50 - Math.floor(compactProgress / 2)), ' ', compactProgress, "%"] })] })) : menu?.kind === 'btw' ? (_jsx(BtwPanel, { entries: btwHistory, selectedIndex: menu.selectedIndex, scrollOffset: menu.scrollOffset, copied: btwCopied, width: width, screenReader: axScreenReader })) : menu?.kind === 'rewind' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: " Rewind" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [' ', "Restore the code and/or conversation to the point before\u2026"] }), _jsx(Text, { children: " " }), (() => {
@@ -5894,9 +6215,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5894
6215
  .slice(window.start, window.end)
5895
6216
  .map((point, offset) => {
5896
6217
  const index = window.start + offset;
5897
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { inverse: menu.selectedIndex === index, children: [menu.selectedIndex === index ? ' ❯ ' : ' ', point.prompt
5898
- .replace(/\s+/gu, ' ')
5899
- .slice(0, 72)] }), _jsxs(Text, { dimColor: true, children: [' ', point.fileChanges.length > 0
6218
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(DecisionOption, { selected: menu.selectedIndex === index, screenReader: axScreenReader, selectedPrefix: " \u276F ", unselectedPrefix: " ", children: point.prompt
6219
+ .replace(/\s+/gu, ' ')
6220
+ .slice(0, 72) }), _jsxs(Text, { dimColor: true, children: [' ', point.fileChanges.length > 0
5900
6221
  ? point.fileChanges
5901
6222
  .map((path) => path.startsWith(`${runtimeCwd}/`)
5902
6223
  ? path.slice(runtimeCwd.length + 1)
@@ -5906,26 +6227,16 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5906
6227
  ? 'No code changes'
5907
6228
  : '⚠ No code restore'] })] }, point.messageId));
5908
6229
  }), window.end < menu.points.length ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2193 ", menu.points.length - window.end, " more below"] })) : null] }));
5909
- })(), _jsxs(Text, { inverse: menu.selectedIndex === menu.points.length, children: [menu.selectedIndex === menu.points.length ? ' ❯ ' : ' ', "(current)"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Enter to continue \u00B7 Esc to cancel" })] })) : menu?.kind === 'rewind-confirm' ? (_jsxs(DialogFrame, { title: "Confirm restore point", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["\u2502 ", menu.point.prompt] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "The conversation will be forked." }), _jsxs(Text, { children: ["The code will", ' ', menu.point.fileChanges.length > 0
6230
+ })(), _jsx(DecisionOption, { selected: menu.selectedIndex === menu.points.length, screenReader: axScreenReader, selectedPrefix: " \u276F ", unselectedPrefix: " ", children: "(current)" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Enter to continue \u00B7 Esc to cancel" })] })) : menu?.kind === 'rewind-confirm' ? (_jsxs(DialogFrame, { title: "Confirm restore point", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["\u2502 ", menu.point.prompt] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "The conversation will be forked." }), _jsxs(Text, { children: ["The code will", ' ', menu.point.fileChanges.length > 0
5910
6231
  ? `restore ${menu.point.fileChanges.join(', ')}.`
5911
- : 'be unchanged.'] }), _jsx(Text, { children: " " }), rewindActions(menu.point).map((option, index) => (_jsxs(Text, { inverse: menu.selectedIndex === index, children: [menu.selectedIndex === index ? '❯ ' : ' ', index + 1, ". ", option.label] }, option.action))), _jsx(Text, { children: " " }), _jsx(Text, { color: activePalette.warning, children: "\u26A0 Rewinding does not affect files edited manually or via bash." })] })) : menu?.kind === 'rewind-context' ? (_jsxs(DialogFrame, { title: `Summarize ${menu.direction === 'from' ? 'from' : 'up to'} here`, screenReader: axScreenReader, children: [_jsx(Text, { children: menu.direction === 'from'
6232
+ : 'be unchanged.'] }), _jsx(Text, { children: " " }), rewindActions(menu.point).map((option, index) => (_jsxs(DecisionOption, { selected: menu.selectedIndex === index, screenReader: axScreenReader, children: [index + 1, ". ", option.label] }, option.action))), _jsx(Text, { children: " " }), _jsx(RewindWarning, {})] })) : menu?.kind === 'rewind-context' ? (_jsxs(DialogFrame, { title: `Summarize ${menu.direction === 'from' ? 'from' : 'up to'} here`, screenReader: axScreenReader, children: [_jsx(Text, { children: menu.direction === 'from'
5912
6233
  ? 'Messages after this point will be summarized.'
5913
- : 'Messages up to this point will be summarized.' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["add context (optional): ", input] }), _jsx(Text, { dimColor: true, children: "Enter to summarize \u00B7 Esc to go back" })] })) : menu?.kind === 'permission-rule-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Add ", menu.behavior, " permission rule"] }), _jsx(Text, { children: "Permission rules are a tool name, optionally followed by a specifier in parentheses." }), _jsx(Text, { children: "e.g., WebFetch or Bash(ls *)" }), _jsx(Text, { children: " " }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Enter permission rule…' }) }), _jsx(Text, { dimColor: true, children: "Enter to submit \u00B7 Esc to cancel" })] })) : menu?.kind === 'workspace-directory-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Add directory to workspace" }), _jsx(Text, { children: "Praxis Code will be able to read files in this directory and make edits when auto-accept edits is on." }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter the path to the directory:" }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Directory path…' }) }), _jsx(Text, { dimColor: true, children: "Tab to complete \u00B7 Enter to add \u00B7 Esc to cancel" })] })) : menu?.kind === 'permission-delete' ? (_jsxs(DialogFrame, { title: permissionDeleteTitle(menu.rule.behavior), screenReader: axScreenReader, children: [_jsx(Text, { children: menu.rule.rule }), permissionRuleDescription(menu.rule.rule) ? (_jsx(Text, { dimColor: true, children: permissionRuleDescription(menu.rule.rule) })) : null, _jsx(Text, { dimColor: true, children: permissionScopeLabel(menu.rule.scope) }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Are you sure you want to delete this permission rule?" }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] })) : menu?.kind === 'workspace-directory-delete' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Remove directory from workspace?" }), _jsxs(Text, { children: [" ", menu.path] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Praxis Code will no longer have access to files in this directory." }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to cancel" })] })) : menu ? (menu.kind === 'help' ? (_jsx(HelpMenu, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, builtinCommands: builtinSlashCommands, customCommands: customSlashCommands, width: width, screenReader: axScreenReader })) : menu.kind === 'diff' ? (_jsx(DiffDashboard, { snapshots: menu.snapshots, sourceIndex: menu.sourceIndex, selectedIndex: menu.selectedIndex, viewingFile: menu.viewingFile, scrollOffset: menu.scrollOffset, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-dashboard' ? (_jsx(PermissionDashboard, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, query: menu.query, rules: menu.rules, recentDenied: recentDenied, retryingDeniedId: retryingDeniedId, workspaceDirectories: workspaceDirectories, width: width, screenReader: axScreenReader })) : menu.kind === 'sandbox' ? (_jsx(SandboxDashboard, { snapshot: menu.snapshot, tab: menu.tab, selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-scope' ? (_jsx(SelectionMenu, { title: "Where should this rule be saved?", description: `${menu.rule}${permissionRuleDescription(menu.rule)
5914
- ? ` · ${permissionRuleDescription(menu.rule)}`
5915
- : ''}`, options: [
5916
- {
5917
- label: 'Project settings (local)',
5918
- description: `Saved in ${settingsDirectory}/settings.local.json`,
5919
- },
5920
- {
5921
- label: 'Project settings',
5922
- description: `Checked in at ${settingsDirectory}/settings.json`,
5923
- },
5924
- {
5925
- label: 'User settings',
5926
- description: `Saved in ~/${settingsDirectory}/settings.json`,
5927
- },
5928
- ], selectedIndex: menu.selectedIndex, footer: "Enter to confirm \u00B7 Esc to cancel", width: width, screenReader: axScreenReader })) : menu.kind === 'agents' ? (_jsx(ListDashboard, { title: "Agents", rows: menu.agents.map((agent) => ({
6234
+ : 'Messages up to this point will be summarized.' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["add context (optional): ", input] }), _jsx(Text, { dimColor: true, children: "Enter to summarize \u00B7 Esc to go back" })] })) : selectedSecondarySurface !== null && menu !== null ? (selectedSecondarySurface.kind === 'permission-dashboard' ||
6235
+ selectedSecondarySurface.kind === 'permission-rule-input' ||
6236
+ selectedSecondarySurface.kind === 'permission-scope' ||
6237
+ selectedSecondarySurface.kind === 'permission-delete' ||
6238
+ selectedSecondarySurface.kind === 'workspace-directory-input' ||
6239
+ selectedSecondarySurface.kind === 'workspace-directory-delete' ? (_jsx(PermissionSurface, { model: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'help' ? (_jsx(HelpMenu, { model: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : menu.kind === 'diff' ? (_jsx(DiffDashboard, { snapshots: menu.snapshots, sourceIndex: menu.sourceIndex, selectedIndex: menu.selectedIndex, viewingFile: menu.viewingFile, scrollOffset: menu.scrollOffset, width: width, screenReader: axScreenReader })) : menu.kind === 'sandbox' ? (_jsx(SandboxDashboard, { snapshot: menu.snapshot, tab: menu.tab, selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'agents' ? (_jsx(ListDashboard, { title: "Agents", rows: menu.agents.map((agent) => ({
5929
6240
  label: agent.name,
5930
6241
  description: agent.description,
5931
6242
  })), emptyText: "No agents configured", selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'config' ? (_jsx(ConfigDashboard, { tab: menu.tab, snapshot: menu.snapshot, query: menu.query, selectedIndex: menu.selectedIndex, searchFocused: menu.searchFocused, status: {
@@ -5957,7 +6268,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5957
6268
  label: 'Save to file',
5958
6269
  description: 'Save the conversation to a file in the current directory',
5959
6270
  },
5960
- ], selectedIndex: menu.selectedIndex, footer: "Esc to cancel", width: width, screenReader: axScreenReader })) : menu.kind === 'copy' ? (_jsx(SelectionMenu, { title: "Copy", description: "Select content to copy:", options: menu.candidates, selectedIndex: menu.selectedIndex, footer: "Enter to copy \u00B7 w to write to /tmp/claude \u00B7 Esc to cancel", width: width, screenReader: axScreenReader })) : null) : (_jsxs(_Fragment, { children: [commandPaletteVisible ? (_jsx(CommandPalette, { commands: matchingSlashCommands, selectedIndex: selectedSlashCommandIndex, width: width, screenReader: axScreenReader })) : null, filePickerVisible ? (_jsx(MentionPicker, { entries: matchingMentionEntries, selectedIndex: selectedFileIndex, width: width, screenReader: axScreenReader })) : null, exitConfirmation ? (_jsx(Text, { color: activePalette.warning, children: "Press Ctrl-C again to exit" })) : null, fixedViewport ? _jsx(Box, { flexGrow: 1 }) : null, _jsx(Composer, { input: shellMode ? input.slice(1) : input, cursor: shellMode ? Math.max(0, inputCursor - 1) : inputCursor, shellMode: shellMode, ...(sessionColor === undefined ? {} : { sessionColor }), ...(commandArgumentHint === undefined
6271
+ ], selectedIndex: menu.selectedIndex, footer: "Esc to cancel", width: width, screenReader: axScreenReader })) : menu.kind === 'copy' ? (_jsx(SelectionMenu, { title: "Copy", description: "Select content to copy:", options: menu.candidates, selectedIndex: menu.selectedIndex, footer: "Enter to copy \u00B7 w to write to /tmp/claude \u00B7 Esc to cancel", width: width, screenReader: axScreenReader })) : null) : (_jsxs(_Fragment, { children: [selectedCommandPalette !== undefined ? (_jsx(CommandPalette, { commands: matchingSlashCommands, selectedIndex: selectedSlashCommandIndex, width: width, screenReader: axScreenReader })) : null, selectedFilePicker !== undefined ? (_jsx(MentionPicker, { entries: matchingMentionEntries, selectedIndex: selectedFileIndex, width: width, screenReader: axScreenReader })) : null, selectedExitConfirmation !== undefined ? (_jsx(ExitWarning, {})) : null, fixedViewport ? _jsx(Box, { flexGrow: 1 }) : null, _jsx(Composer, { input: shellMode ? input.slice(1) : input, cursor: shellMode ? Math.max(0, inputCursor - 1) : inputCursor, shellMode: shellMode, ...(sessionColor === undefined ? {} : { sessionColor }), ...(commandArgumentHint === undefined
5961
6272
  ? {}
5962
6273
  : { commandArgumentHint }), busy: busy, clipboardBusy: clipboardBusy, status: status, display: runtimeDisplay, ...(usage === undefined ? {} : { usage }), ...(costUsd === undefined ? {} : { costUsd }), width: width, screenReader: axScreenReader, hasThinking: hasDetailedTranscript, thinkingExpanded: thinkingExpanded, reduceMotion: runtimeSettings.reduceMotion, progressBar: runtimeSettings.progressBar, ...(runtimeSettings.turnDuration
5963
6274
  ? (() => {
@@ -5969,7 +6280,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
5969
6280
  : {}), editorMode: runtimeSettings.editor, ...(runtimeSettings.prStatus &&
5970
6281
  activeSessionSummary?.prNumber
5971
6282
  ? { prStatus: `PR #${activeSessionSummary.prNumber}` }
5972
- : {}), shortcutsVisible: shortcutsVisible, ...(editorFooterMessage === undefined
6283
+ : {}), shortcutsVisible: shortcutsVisible, shortcutHelp: shortcutHelpSurface, ...(editorFooterMessage === undefined
5973
6284
  ? {}
5974
6285
  : { footerMessage: editorFooterMessage }) }), _jsx(StatusLine, { configRoot: keybindingsRoot, cwd: runtimeCwd, dataPlane: dataPlane, input: createClaudeStatusLineInput({
5975
6286
  configRoot: keybindingsRoot,
@@ -6176,7 +6487,7 @@ export async function runInteractive(options) {
6176
6487
  ? { allowDangerouslySkipPermissions: true }
6177
6488
  : {}) }), {
6178
6489
  exitOnCtrlC: false,
6179
- ...fullscreenInkRenderOptions(currentRenderer, options.axScreenReader),
6490
+ ...tuiInkRenderOptions(currentRenderer, options.axScreenReader === true),
6180
6491
  interactive: true,
6181
6492
  });
6182
6493
  await instance.waitUntilExit();