praxis-agent 0.43.0 → 0.43.1

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 (34) hide show
  1. package/README.md +24 -37
  2. package/dist/application/native-session-projection.d.ts +4 -0
  3. package/dist/application/native-session-projection.js +135 -0
  4. package/dist/application/session-service.d.ts +0 -1
  5. package/dist/application/session-service.js +9 -170
  6. package/dist/cli/interactive.js +112 -156
  7. package/dist/cli/tui/ansi-frame-renderer.d.ts +2 -0
  8. package/dist/cli/tui/ansi-frame-renderer.js +109 -21
  9. package/dist/cli/tui/ansi-surface.d.ts +6 -22
  10. package/dist/cli/tui/ansi-surface.js +6 -126
  11. package/dist/cli/tui/ansi-theme.d.ts +3 -2
  12. package/dist/cli/tui/ansi-theme.js +22 -1
  13. package/dist/cli/tui/quiet-choice-rows.d.ts +26 -0
  14. package/dist/cli/tui/quiet-choice-rows.js +302 -0
  15. package/dist/cli/tui/quiet-frame-adapter.d.ts +18 -0
  16. package/dist/cli/tui/quiet-frame-adapter.js +90 -0
  17. package/dist/cli/tui/quiet-frame.d.ts +43 -0
  18. package/dist/cli/tui/quiet-frame.js +346 -0
  19. package/dist/cli/tui/quiet-operational-rows.d.ts +17 -0
  20. package/dist/cli/tui/quiet-operational-rows.js +413 -0
  21. package/dist/cli/tui/quiet-screen-projector.d.ts +25 -0
  22. package/dist/cli/tui/quiet-screen-projector.js +110 -0
  23. package/dist/cli/tui/quiet-settings-rows.d.ts +17 -0
  24. package/dist/cli/tui/quiet-settings-rows.js +490 -0
  25. package/dist/cli/tui/status-line.d.ts +15 -9
  26. package/dist/cli/tui/status-line.js +15 -6
  27. package/dist/cli/tui/theme.js +20 -20
  28. package/dist/cli/tui/transcript-viewport.d.ts +5 -0
  29. package/dist/cli/tui/transcript-viewport.js +213 -78
  30. package/dist/cli/tui/tui-row-ir.js +4 -4
  31. package/dist/persistence/in-memory-transcript-store.js +1 -7
  32. package/dist/persistence/native-transcript-store.d.ts +0 -9
  33. package/dist/persistence/native-transcript-store.js +4 -38
  34. package/package.json +3 -2
@@ -1,18 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
5
5
  import { homedir } from 'node:os';
6
6
  import { join, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { useEffect, useMemo, useReducer, useRef, useState, } from 'react';
9
- import { Box, Text, render, useApp, useInput } from 'ink';
8
+ import { useEffect, useMemo, useReducer, useRef, useState } from 'react';
9
+ import { render, useApp, useInput } from 'ink';
10
10
  import { permissionRuleValueToString } from '../permissions/permission-updates.js';
11
11
  import { resolveDataPlane, resolveDataPlaneRoot, } from '../persistence/data-plane.js';
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, LeafSurface, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, ModelMenu, EffortMenu, SessionPicker, ThemePicker, CustomThemeEditor, SessionIdentity, Transcript, WelcomePanel, } from './tui/claude-style.js';
16
15
  import { projectTuiEffortSurface, projectTuiModelSurface, } from './tui/model-effort-surface-model.js';
17
16
  import { projectTuiLeafSurface, } from './tui/leaf-surface-model.js';
18
17
  import { projectTuiThemeSurface, } from './tui/theme-surface-model.js';
@@ -24,12 +23,14 @@ import { tuiInkRenderOptions, useTuiPresentationEnvironment, } from './tui/prese
24
23
  import { StreamingFrameBuffer } from './tui/streaming-frame-buffer.js';
25
24
  import { createTuiStoreState, reduceTuiStore } from './tui/kernel/tui-store.js';
26
25
  import { TuiEffectRunner } from './tui/kernel/tui-effect-runner.js';
27
- import { createClaudeStatusLineInput, StatusLine } from './tui/status-line.js';
26
+ import { createClaudeStatusLineInput, formatClaudeStatusLineState, useClaudeStatusLine, } from './tui/status-line.js';
28
27
  import { createTuiAppendHistoryChange } from './tui/transcript-window-model.js';
29
28
  import { createTuiHistoryChange, resolveTuiRenderer, } from './tui/tui-view-model.js';
30
29
  import { projectTuiScreen, } from './tui/tui-screen-model.js';
31
- import { supportsAnsiSurface, TuiAnsiSurface } from './tui/ansi-surface.js';
32
- import { projectTuiHelpSurface, } from './tui/help-surface-model.js';
30
+ import { TuiAnsiSurface } from './tui/ansi-surface.js';
31
+ import { QuietInkFrame } from './tui/quiet-frame-adapter.js';
32
+ import { projectQuietScreenFrame, } from './tui/quiet-screen-projector.js';
33
+ import { projectTuiHelpSurface } from './tui/help-surface-model.js';
33
34
  import { projectTuiHooksSurface, } from './tui/hooks-surface-model.js';
34
35
  import { loadGitDiff } from './tui/git-diff.js';
35
36
  import { projectTuiDiffSurface, } from './tui/diff-surface-model.js';
@@ -56,34 +57,30 @@ import { defaultTuiKeybindings, ensureTuiKeybindingsFile, hasTuiKeybindingPrefix
56
57
  import { completeTuiWorkspaceDirectory, resolveTuiWorkspaceDirectory, } from './tui/workspace-directories.js';
57
58
  import { conversationExportPath, conversationExportText, defaultConversationExportFilename, writeConversationExport, } from './tui/conversation-export.js';
58
59
  import { loadTuiThemeSettings, saveTuiThemeSettings, themeSettingsWithCustomTheme, } from './tui/theme-settings.js';
59
- import { DEFAULT_TUI_THEME_SETTINGS, TUI_THEMES, TuiThemeProvider, useTuiTheme, } from './tui/theme.js';
60
+ import { DEFAULT_TUI_THEME_SETTINGS, TUI_THEMES, TuiThemeProvider, } from './tui/theme.js';
60
61
  import { CUSTOM_THEME_TOKENS, createTuiCustomTheme, deleteTuiCustomTheme, loadTuiCustomThemes, updateTuiCustomTheme, } from './tui/custom-themes.js';
61
62
  import { setupTuiTerminal, terminalSetupTuiSlashCommand, } from './tui/terminal-setup.js';
62
- import { commitElicitationText, createTuiElicitationForm, elicitationFormIsValid, elicitationTextValue, expandElicitationOptions, focusedElicitationField, ElicitationSurface, moveElicitationFocus, moveElicitationOption, selectElicitationOption, toggleElicitationBoolean, typeaheadElicitationOption, unsetElicitationField, validateTuiElicitationForm, } from './tui/mcp-elicitation.js';
63
+ import { commitElicitationText, createTuiElicitationForm, elicitationFormIsValid, elicitationTextValue, expandElicitationOptions, focusedElicitationField, moveElicitationFocus, moveElicitationOption, selectElicitationOption, toggleElicitationBoolean, typeaheadElicitationOption, unsetElicitationField, validateTuiElicitationForm, } from './tui/mcp-elicitation.js';
63
64
  import { projectTuiElicitationSurface, } from './tui/mcp-elicitation-surface-model.js';
64
65
  import { openTuiUrl } from './tui/open-url.js';
65
66
  import { projectTuiToolPermission } from './tui/tool-permission.js';
66
67
  import { projectTuiPermissionSurface, } from './tui/permission-surface-model.js';
67
- import { PermissionSurface } from './tui/permission-surface.js';
68
68
  import { projectTuiDecisionSurface, } from './tui/decision-surface-model.js';
69
- import { DecisionSurface } from './tui/decision-surface.js';
70
- import { ConfigDashboard, projectConfigRows } from './tui/config-dashboard.js';
69
+ import { projectConfigRows } from './tui/config-dashboard.js';
71
70
  import { projectTuiConfigSurface, } from './tui/config-surface-model.js';
72
- import { DoctorDashboard } from './tui/doctor-dashboard.js';
73
71
  import { projectTuiDoctorSurface, } from './tui/doctor-surface-model.js';
74
72
  import { projectTuiMemorySurface, } from './tui/memory-surface-model.js';
75
- import { SandboxDashboard, tuiSandboxTabs } from './tui/sandbox-dashboard.js';
73
+ import { tuiSandboxTabs } from './tui/sandbox-dashboard.js';
76
74
  import { projectTuiSandboxSurface, } from './tui/sandbox-surface-model.js';
77
75
  import { createTuiSandboxStore, } from './tui/sandbox-settings.js';
78
76
  import { loadConfigSettings, saveConfigSetting, configSettingDefinition, resolveConfigSettingsLocation, } from './tui/config-settings.js';
79
77
  import { projectRuntimeSettings, loadRuntimeSettings, } from './tui/runtime-settings.js';
80
- import { autoUpdateTarget, copyCandidates, externalEditorInitialContent, formatTurnDuration, questionTimeoutMilliseconds, sessionRecap, spinnerTip, shouldShowCopyPicker, } from './tui/runtime-interactions.js';
78
+ import { autoUpdateTarget, copyCandidates, externalEditorInitialContent, questionTimeoutMilliseconds, sessionRecap, spinnerTip, shouldShowCopyPicker, } from './tui/runtime-interactions.js';
81
79
  import { notifyTerminal, } from './tui/terminal-notifications.js';
82
- import { McpPanel } from './tui/mcp-panel.js';
83
80
  import { McpPanelController, mcpRuntimeFromSession, } from './tui/mcp-panel-controller.js';
84
81
  import { initialTuiMcpPanelState, } from './tui/mcp-panel-projector.js';
85
82
  import { projectTuiMcpSurface, } from './tui/mcp-surface-model.js';
86
- import { TaskPanel, initialTuiTaskPanelState, projectTuiTasks, reconcileTuiTaskPanelState, updateTuiTaskPanelState, } from './tui/task-panel.js';
83
+ import { initialTuiTaskPanelState, projectTuiTasks, reconcileTuiTaskPanelState, updateTuiTaskPanelState, } from './tui/task-panel.js';
87
84
  import { projectTuiTaskSurface, } from './tui/task-surface-model.js';
88
85
  import { ClaudeMcpManagement } from '../mcp/claude-mcp-management.js';
89
86
  const EMPTY_SLASH_COMMANDS = [];
@@ -241,25 +238,6 @@ const HIDDEN_TUI_SLASH_COMMANDS = new Set([
241
238
  'update',
242
239
  'usage',
243
240
  ]);
244
- function RewindWarning() {
245
- const theme = useTuiTheme();
246
- return (_jsx(Text, { ...theme.text.warning, children: "\u26A0 Rewinding does not affect files edited manually or via bash." }));
247
- }
248
- function ExitWarning() {
249
- const theme = useTuiTheme();
250
- return _jsx(Text, { ...theme.text.warning, children: "Press Ctrl-C again to exit" });
251
- }
252
- function DecisionOption({ selected, screenReader, children, selectedPrefix = '❯ ', unselectedPrefix = ' ', }) {
253
- const theme = useTuiTheme();
254
- const accessible = screenReader || theme.noColor;
255
- return (_jsxs(Text, { ...(selected ? theme.text.selectedRow : {}), children: [selected
256
- ? accessible
257
- ? 'Selected: '
258
- : selectedPrefix
259
- : accessible
260
- ? ''
261
- : unselectedPrefix, children] }));
262
- }
263
241
  /** Advances the React-owned transcript plus its exact local mutation fact. */
264
242
  export function advanceInteractiveHistoryState(current, items, changedFrom) {
265
243
  if (!Number.isInteger(changedFrom) ||
@@ -359,7 +337,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
359
337
  : { viewportOverride: { columns: terminalWidth } }),
360
338
  });
361
339
  const width = presentation.viewport.columns;
362
- const rows = presentation.viewport.rows;
363
340
  const fixedViewport = presentation.fixedViewport;
364
341
  const runtimeSettingsRef = useRef(runtimeSettings);
365
342
  runtimeSettingsRef.current = runtimeSettings;
@@ -386,9 +363,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
386
363
  const statusLineSessionId = useRef(resume?.sessionId ?? randomUUID());
387
364
  const sessionIdRef = useRef(resume?.sessionId ?? null);
388
365
  sessionIdRef.current = sessionId;
389
- const [sessionColor, setSessionColor] = useState(initialSessionColor);
366
+ const [, setSessionColor] = useState(initialSessionColor);
390
367
  const [sessionName, setSessionName] = useState(null);
391
- const [activeSessionSummary, setActiveSessionSummary] = useState(() => resume?.sessionId
368
+ const [, setActiveSessionSummary] = useState(() => resume?.sessionId
392
369
  ? initialSessions.find((session) => session.sessionId === resume.sessionId)
393
370
  : undefined);
394
371
  const [pendingFork, setPendingFork] = useState(resume?.forkSession === true);
@@ -406,7 +383,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
406
383
  const themeEffectRunnerRef = useRef(null);
407
384
  if (themeEffectRunnerRef.current === null)
408
385
  themeEffectRunnerRef.current = new TuiEffectRunner();
409
- const [clipboardBusy, setClipboardBusy] = useState(false);
386
+ const [, setClipboardBusy] = useState(false);
410
387
  const stashedInputRef = useRef('');
411
388
  const lastEscapeAtRef = useRef(0);
412
389
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
@@ -476,7 +453,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
476
453
  });
477
454
  }
478
455
  const [thinkingExpanded, setThinkingExpanded] = useState(false);
479
- const [turnDuration, setTurnDuration] = useState();
456
+ const [, setTurnDuration] = useState();
480
457
  const [usage, setUsage] = useState();
481
458
  const [costUsd, setCostUsd] = useState();
482
459
  const [contextWindowTokens, setContextWindowTokens] = useState(display.contextWindowTokens);
@@ -646,17 +623,6 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
646
623
  query: commandQuery ?? '',
647
624
  selectedIndex: commandSelection,
648
625
  }), [allSlashCommands, commandQuery, commandSelection]);
649
- const commandArgumentHint = useMemo(() => {
650
- if (shellMode || !input.startsWith('/'))
651
- return undefined;
652
- if (inputCursor !== input.length)
653
- return undefined;
654
- const match = /^\/(\S+) $/u.exec(input);
655
- if (!match?.[1])
656
- return undefined;
657
- const command = allSlashCommands.find((candidate) => candidate.name === match[1]);
658
- return command?.argumentHint;
659
- }, [allSlashCommands, input, inputCursor, shellMode]);
660
626
  const commandPaletteVisible = !busy &&
661
627
  !permission &&
662
628
  !planApproval &&
@@ -1089,7 +1055,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1089
1055
  return null;
1090
1056
  }, [menu, input, compactProgress]);
1091
1057
  const secondarySurface = menu === null
1092
- ? undefined
1058
+ ? shortcutsVisible
1059
+ ? shortcutHelpSurface
1060
+ : undefined
1093
1061
  : menu.kind === 'help'
1094
1062
  ? (helpSurface ?? undefined)
1095
1063
  : menu.kind === 'diff'
@@ -1216,23 +1184,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1216
1184
  sessionId,
1217
1185
  screenSurfaces,
1218
1186
  ]);
1219
- const ansiActive = ansiRequested && supportsAnsiSurface(screen);
1187
+ const ansiActive = ansiRequested;
1220
1188
  useEffect(() => {
1221
1189
  previousTuiScreenRef.current = screen;
1222
1190
  }, [screen]);
1223
1191
  const conversationScreen = screen.body.kind === 'conversation' ? screen.body : undefined;
1224
- const selectedPriority = conversationScreen?.foreground.kind === 'priority'
1225
- ? conversationScreen.foreground.surface
1226
- : undefined;
1227
- const selectedSecondarySurface = conversationScreen?.foreground.kind === 'secondary'
1228
- ? (conversationScreen.foreground.surface ?? null)
1229
- : null;
1230
- const selectedOverlays = conversationScreen?.foreground.kind === 'compose'
1231
- ? conversationScreen.foreground.overlays
1232
- : [];
1233
- const selectedCommandPalette = selectedOverlays.find((overlay) => overlay.kind === 'command-palette');
1234
- const selectedFilePicker = selectedOverlays.find((overlay) => overlay.kind === 'mention-picker');
1235
- const selectedExitConfirmation = selectedOverlays.find((overlay) => overlay.kind === 'exit-confirmation');
1236
1192
  const transcriptPageRows = conversationScreen?.transcript.pageRows ?? 2;
1237
1193
  const maxTranscriptScrollOffset = conversationScreen?.transcript.maxScrollOffset ?? 0;
1238
1194
  const permissionOptions = useMemo(() => [
@@ -6501,94 +6457,93 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
6501
6457
  return;
6502
6458
  }
6503
6459
  });
6504
- return (_jsx(TuiThemeProvider, { settings: themeSettings, screenReader: axScreenReader, children: _jsx(Box, { flexDirection: "column", ...(!fixedViewport
6505
- ? {}
6506
- : { height: rows, overflowY: 'hidden' }), children: ansiActive ? (_jsx(TuiAnsiSurface, { screen: screen, width: width, rows: rows, input: shellMode ? input.slice(1) : input, busy: busy, status: status, display: runtimeDisplay, ...(conversationScreen?.sessionLabel === undefined
6507
- ? {}
6508
- : { sessionLabel: conversationScreen.sessionLabel }), cursor: shellMode ? Math.max(0, inputCursor - 1) : inputCursor, screenReader: axScreenReader, onError: () => setAnsiRendererFailed(true) })) : screen.body.kind === 'session-picker' ? (_jsx(SessionPicker, { model: screen.body.surface, screenReader: axScreenReader })) : (_jsxs(_Fragment, { children: [screen.body.intro === 'welcome' ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width, showTips: runtimeSettings.tips })) : 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
6509
- ? {
6510
- flexShrink: 1,
6511
- minHeight: 0,
6512
- overflowY: 'hidden',
6513
- }
6514
- : {}), 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' ? (_jsx(ElicitationSurface, { model: selectedPriority.surface, screenReader: axScreenReader })) : selectedSecondarySurface?.kind === 'model-input' ||
6515
- selectedSecondarySurface?.kind === 'export' ||
6516
- selectedSecondarySurface?.kind === 'copy' ||
6517
- selectedSecondarySurface?.kind === 'export-filename' ||
6518
- selectedSecondarySurface?.kind === 'compact-progress' ? (_jsx(LeafSurface, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface?.kind === 'rewind-panel' ? (selectedSecondarySurface.view === 'points' ? (_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: " " }), selectedSecondarySurface.window.start > 0 ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2191 ", selectedSecondarySurface.window.start, " more above"] })) : null, selectedSecondarySurface.points
6519
- .slice(selectedSecondarySurface.window.start, selectedSecondarySurface.window.end)
6520
- .map((point, offset) => {
6521
- const index = selectedSecondarySurface.window.start + offset;
6522
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(DecisionOption, { selected: selectedSecondarySurface.selectedIndex === index, screenReader: axScreenReader, selectedPrefix: " \u276F ", unselectedPrefix: " ", children: point.prompt.replace(/\s+/gu, ' ').slice(0, 72) }), _jsxs(Text, { dimColor: true, children: [' ', point.fileChanges.length > 0
6523
- ? point.fileChanges
6524
- .map((path) => path.startsWith(`${runtimeCwd}/`)
6525
- ? path.slice(runtimeCwd.length + 1)
6526
- : path)
6527
- .join(', ')
6528
- : point.fileRestoreAvailable
6529
- ? 'No code changes'
6530
- : '⚠ No code restore'] })] }, point.messageId));
6531
- }), selectedSecondarySurface.window.end <
6532
- selectedSecondarySurface.points.length ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2193", ' ', selectedSecondarySurface.points.length -
6533
- selectedSecondarySurface.window.end, ' ', "more below"] })) : null, _jsx(DecisionOption, { selected: selectedSecondarySurface.selectedIndex ===
6534
- selectedSecondarySurface.points.length, screenReader: axScreenReader, selectedPrefix: " \u276F ", unselectedPrefix: " ", children: "(current)" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Enter to continue \u00B7 Esc to cancel" })] })) : selectedSecondarySurface.view === 'confirm' ? (_jsxs(DialogFrame, { title: "Confirm restore point", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["\u2502 ", selectedSecondarySurface.point.prompt] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "The conversation will be forked." }), _jsxs(Text, { children: ["The code will", ' ', selectedSecondarySurface.point.fileChanges.length > 0
6535
- ? `restore ${selectedSecondarySurface.point.fileChanges.join(', ')}.`
6536
- : 'be unchanged.'] }), _jsx(Text, { children: " " }), selectedSecondarySurface.actions.map((option, index) => (_jsxs(DecisionOption, { selected: selectedSecondarySurface.selectedIndex === index, screenReader: axScreenReader, children: [index + 1, ". ", option.label] }, option.action))), _jsx(Text, { children: " " }), _jsx(RewindWarning, {})] })) : (_jsxs(DialogFrame, { title: `Summarize ${selectedSecondarySurface.direction === 'from' ? 'from' : 'up to'} here`, screenReader: axScreenReader, children: [_jsx(Text, { children: selectedSecondarySurface.direction === 'from'
6537
- ? 'Messages after this point will be summarized.'
6538
- : 'Messages up to this point will be summarized.' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["add context (optional): ", selectedSecondarySurface.context] }), _jsx(Text, { dimColor: true, children: "Enter to summarize \u00B7 Esc to go back" })] }))) : selectedSecondarySurface !== null && menu !== null ? (selectedSecondarySurface.kind === 'permission-dashboard' ||
6539
- selectedSecondarySurface.kind === 'permission-rule-input' ||
6540
- selectedSecondarySurface.kind === 'permission-scope' ||
6541
- selectedSecondarySurface.kind === 'permission-delete' ||
6542
- selectedSecondarySurface.kind === 'workspace-directory-input' ||
6543
- selectedSecondarySurface.kind === 'workspace-directory-delete' ? (_jsx(PermissionSurface, { model: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'mcp-panel' ? (_jsx(McpPanel, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'help' ? (_jsx(HelpMenu, { model: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'diff' ? (_jsx(DiffDashboard, { model: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'tasks-panel' ? (_jsx(TaskPanel, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'doctor-panel' ? (_jsx(DoctorDashboard, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'memory-panel' ? (_jsx(MemoryDashboard, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'hooks-panel' ? (_jsx(HookDashboard, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'config-panel' ? (_jsx(ConfigDashboard, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'sandbox-panel' ? (_jsx(SandboxDashboard, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'list-panel' ? (_jsx(ListDashboard, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'btw-panel' ? (_jsx(BtwPanel, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'model-panel' ? (_jsx(ModelMenu, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'effort-panel' ? (_jsx(EffortMenu, { surface: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'theme-panel' ? (_jsx(ThemePicker, { model: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'custom-theme-create' ? (_jsxs(DialogFrame, { title: "New custom theme", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["Name: ", selectedSecondarySurface.name || 'my-theme'] }), _jsxs(Text, { children: ["based on ", selectedSecondarySurface.base, " \u00B7 saved to ~/", settingsDirectory, "/themes/theme.json"] }), _jsx(Text, { dimColor: true, children: "Enter to create \u00B7 Esc to cancel" })] })) : selectedSecondarySurface.kind === 'custom-theme-editor' ||
6544
- selectedSecondarySurface.kind === 'custom-theme-token' ? (_jsx(CustomThemeEditor, { model: selectedSecondarySurface, width: width, screenReader: axScreenReader })) : selectedSecondarySurface.kind === 'custom-theme-delete' ? (_jsxs(DialogFrame, { title: "Delete custom theme", screenReader: axScreenReader, children: [_jsxs(Text, { children: ["Delete ", selectedSecondarySurface.theme.name, " permanently?"] }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to cancel" })] })) : null) : (_jsxs(_Fragment, { children: [selectedCommandPalette !== undefined ? (_jsx(CommandPalette, { model: selectedCommandPalette, width: width, screenReader: axScreenReader })) : null, selectedFilePicker !== undefined ? (_jsx(MentionPicker, { model: selectedFilePicker, 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
6545
- ? {}
6546
- : { 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
6547
- ? (() => {
6548
- const duration = formatTurnDuration(turnDuration);
6549
- return duration === undefined
6550
- ? {}
6551
- : { turnDuration: duration };
6552
- })()
6553
- : {}), editorMode: runtimeSettings.editor, ...(runtimeSettings.prStatus &&
6554
- activeSessionSummary?.prNumber
6555
- ? { prStatus: `PR #${activeSessionSummary.prNumber}` }
6556
- : {}), shortcutsVisible: shortcutsVisible, shortcutHelp: shortcutHelpSurface, ...(editorFooterMessage === undefined
6557
- ? {}
6558
- : { footerMessage: editorFooterMessage }) }), _jsx(StatusLine, { configRoot: keybindingsRoot, cwd: runtimeCwd, dataPlane: dataPlane, input: createClaudeStatusLineInput({
6559
- configRoot: keybindingsRoot,
6560
- cwd: runtimeCwd,
6561
- projectDir: display.cwd,
6562
- sessionId: sessionId ?? statusLineSessionId.current,
6563
- sessionName,
6564
- ...(runtimeDisplay.model === undefined
6565
- ? {}
6566
- : { model: runtimeDisplay.model }),
6567
- version: runtimeDisplay.version,
6568
- outputStyle: runtimeSettings.outputStyle,
6569
- permissionMode: runtimePreferences.permissionMode,
6570
- additionalDirectories: runtimePreferences.additionalDirectories,
6571
- dataPlane,
6572
- ...(usage === undefined ? {} : { usage }),
6573
- ...(costUsd === undefined ? {} : { costUsd }),
6574
- ...(runtimeDisplay.contextWindowTokens === undefined
6575
- ? {}
6576
- : {
6577
- contextWindowTokens: runtimeDisplay.contextWindowTokens,
6578
- }),
6579
- ...(runtimeSettings.editor === 'vim'
6580
- ? { vimMode: vimInsertMode ? 'INSERT' : 'NORMAL' }
6581
- : {}),
6582
- }), refreshKey: [
6583
- history.length,
6584
- runtimePreferences.permissionMode,
6585
- runtimeDisplay.model,
6586
- runtimeSettings.outputStyle,
6587
- vimInsertMode,
6588
- sessionName,
6589
- usage?.inputTokens,
6590
- usage?.outputTokens,
6591
- ].join(':'), width: width, ...(settingSources === undefined ? {} : { settingSources }) })] }))] })) }) }));
6460
+ const statusLineInput = useMemo(() => createClaudeStatusLineInput({
6461
+ configRoot: keybindingsRoot,
6462
+ cwd: runtimeCwd,
6463
+ projectDir: display.cwd,
6464
+ sessionId: sessionId ?? statusLineSessionId.current,
6465
+ sessionName,
6466
+ ...(runtimeDisplay.model === undefined
6467
+ ? {}
6468
+ : { model: runtimeDisplay.model }),
6469
+ version: runtimeDisplay.version,
6470
+ outputStyle: runtimeSettings.outputStyle,
6471
+ permissionMode: runtimePreferences.permissionMode,
6472
+ additionalDirectories: runtimePreferences.additionalDirectories,
6473
+ dataPlane,
6474
+ ...(usage === undefined ? {} : { usage }),
6475
+ ...(costUsd === undefined ? {} : { costUsd }),
6476
+ ...(runtimeDisplay.contextWindowTokens === undefined
6477
+ ? {}
6478
+ : { contextWindowTokens: runtimeDisplay.contextWindowTokens }),
6479
+ ...(runtimeSettings.editor === 'vim'
6480
+ ? { vimMode: vimInsertMode ? 'INSERT' : 'NORMAL' }
6481
+ : {}),
6482
+ }), [
6483
+ keybindingsRoot,
6484
+ runtimeCwd,
6485
+ display.cwd,
6486
+ sessionId,
6487
+ sessionName,
6488
+ runtimeDisplay.model,
6489
+ runtimeDisplay.version,
6490
+ runtimeSettings.outputStyle,
6491
+ runtimePreferences.permissionMode,
6492
+ runtimePreferences.additionalDirectories,
6493
+ dataPlane,
6494
+ usage,
6495
+ costUsd,
6496
+ runtimeDisplay.contextWindowTokens,
6497
+ runtimeSettings.editor,
6498
+ vimInsertMode,
6499
+ ]);
6500
+ const statusLineRefreshKey = [
6501
+ history.length,
6502
+ runtimePreferences.permissionMode,
6503
+ runtimeDisplay.model,
6504
+ runtimeSettings.outputStyle,
6505
+ vimInsertMode,
6506
+ sessionName,
6507
+ usage?.inputTokens,
6508
+ usage?.outputTokens,
6509
+ ].join(':');
6510
+ const configuredStatusLine = useClaudeStatusLine({
6511
+ configRoot: keybindingsRoot,
6512
+ cwd: runtimeCwd,
6513
+ dataPlane,
6514
+ input: statusLineInput,
6515
+ refreshKey: statusLineRefreshKey,
6516
+ width,
6517
+ ...(settingSources === undefined ? {} : { settingSources }),
6518
+ });
6519
+ const configuredStatusText = formatClaudeStatusLineState(configuredStatusLine, width);
6520
+ const conciseStatus = [
6521
+ status,
6522
+ editorFooterMessage
6523
+ ? `${editorFooterMessage.isError ? 'Error: ' : ''}${editorFooterMessage.text}`
6524
+ : undefined,
6525
+ configuredStatusText,
6526
+ ]
6527
+ .filter((value, index, values) => Boolean(value) && (index === 0 || value !== values[index - 1]))
6528
+ .join(' · ');
6529
+ const quietFrame = useMemo(() => projectQuietScreenFrame({
6530
+ screen,
6531
+ composerText: shellMode ? input.slice(1) : input,
6532
+ composerCursor: shellMode ? Math.max(0, inputCursor - 1) : inputCursor,
6533
+ shellMode,
6534
+ busy,
6535
+ status: conciseStatus,
6536
+ display: runtimeDisplay,
6537
+ }), [
6538
+ screen,
6539
+ shellMode,
6540
+ input,
6541
+ inputCursor,
6542
+ busy,
6543
+ conciseStatus,
6544
+ runtimeDisplay,
6545
+ ]);
6546
+ return (_jsx(TuiThemeProvider, { settings: themeSettings, screenReader: axScreenReader, children: ansiActive ? (_jsx(TuiAnsiSurface, { frame: quietFrame, onError: () => setAnsiRendererFailed(true) })) : (_jsx(QuietInkFrame, { frame: quietFrame, screenReader: axScreenReader })) }));
6592
6547
  }
6593
6548
  /**
6594
6549
  * Whether the user explicitly saved a `tui` renderer value in configuration.
@@ -6624,10 +6579,11 @@ export async function runInteractive(options) {
6624
6579
  // fallback for screen-reader and non-interactive execution, and an explicit
6625
6580
  // renderer saved in configuration is honored over the default.
6626
6581
  const rendererExplicitlyConfigured = await tuiRendererExplicitlyConfigured(runtimeSettingsTarget);
6582
+ const interactiveTty = process.stdin.isTTY === true && process.stdout.isTTY === true;
6627
6583
  let currentRenderer = resolveTuiRenderer({
6628
6584
  configured: currentRuntimeSettings.tui,
6629
6585
  explicitlyConfigured: rendererExplicitlyConfigured,
6630
- interactiveTty: process.stdin.isTTY === true,
6586
+ interactiveTty,
6631
6587
  screenReader: options.axScreenReader ?? false,
6632
6588
  });
6633
6589
  if (currentRenderer !== currentRuntimeSettings.tui) {
@@ -6726,7 +6682,7 @@ export async function runInteractive(options) {
6726
6682
  let backgrounded;
6727
6683
  rendererChange = null;
6728
6684
  const ansiRendererEnabled = currentRenderer === 'fullscreen' &&
6729
- process.stdin.isTTY === true &&
6685
+ interactiveTty &&
6730
6686
  options.axScreenReader !== true;
6731
6687
  const instance = render(_jsx(InteractiveApp, { dataPlane: dataPlane, configRoot: configRoot, statePath: statePath, factory: options.factory, initialSessions: initialSessions, slashCommands: initialSlashCommands, agents: initialAgents, initialHistory: history, ...(initialSessionColor === undefined ? {} : { initialSessionColor }), runtimeSettings: currentRuntimeSettings, runtimeSettingsTarget: runtimeSettingsTarget, ...(options.settingSources === undefined
6732
6688
  ? {}
@@ -1,3 +1,4 @@
1
+ import type { QuietFrameCursor } from './quiet-frame.js';
1
2
  import type { TuiRow, TuiTextRole } from './tui-row-ir.js';
2
3
  export interface AnsiFrameWriter {
3
4
  write(chunk: string): void;
@@ -6,6 +7,7 @@ export interface AnsiFrame {
6
7
  readonly columns: number;
7
8
  readonly rows: number;
8
9
  readonly lines: readonly TuiRow[];
10
+ readonly cursor?: QuietFrameCursor;
9
11
  }
10
12
  export interface AnsiFullscreenRendererOptions {
11
13
  readonly writer: AnsiFrameWriter;
@@ -1,4 +1,5 @@
1
1
  import { stripVTControlCharacters } from 'node:util';
2
+ import { terminalGraphemes, terminalGraphemeWidth, terminalTextWidth, } from './transcript-viewport.js';
2
3
  const ALTERNATE_SCREEN_ENTER = '\u001b[?1049h';
3
4
  const ALTERNATE_SCREEN_LEAVE = '\u001b[?1049l';
4
5
  const HIDE_CURSOR = '\u001b[?25l';
@@ -19,22 +20,44 @@ function validateFrame(frame) {
19
20
  if (!Array.isArray(frame.lines)) {
20
21
  throw new TypeError('ANSI frame lines must be an array');
21
22
  }
23
+ if (frame.cursor !== undefined) {
24
+ if (frame.cursor === null ||
25
+ typeof frame.cursor !== 'object' ||
26
+ typeof frame.cursor.rowKey !== 'string' ||
27
+ frame.cursor.rowKey.length === 0 ||
28
+ !Number.isFinite(frame.cursor.column) ||
29
+ !Number.isInteger(frame.cursor.column) ||
30
+ frame.cursor.column < 0) {
31
+ throw new TypeError('ANSI frame cursor must have a non-empty row key and non-negative integer column');
32
+ }
33
+ }
22
34
  }
23
35
  function renderLine(row, styles, columns) {
24
36
  const segments = row.segments.map((segment) => ({
25
37
  text: stripVTControlCharacters(segment.text).replace(/[\r\n]/gu, ''),
26
38
  role: segment.role,
27
39
  }));
28
- const visibleLength = segments.reduce((length, segment) => length + Array.from(segment.text).length, 0);
40
+ const visibleLength = segments.reduce((length, segment) => length + terminalTextWidth(segment.text), 0);
29
41
  const wasTruncated = visibleLength > columns;
30
42
  const targetLength = wasTruncated ? Math.max(0, columns - 1) : columns;
31
43
  let remaining = targetLength;
32
44
  let line = '';
45
+ let stopped = false;
33
46
  for (const segment of segments) {
34
- if (remaining === 0)
47
+ if (remaining === 0 || stopped)
35
48
  break;
36
- const text = Array.from(segment.text).slice(0, remaining).join('');
37
- remaining -= Array.from(text).length;
49
+ let text = '';
50
+ for (const grapheme of terminalGraphemes(segment.text)) {
51
+ const width = terminalGraphemeWidth(grapheme);
52
+ if (remaining < width) {
53
+ stopped = true;
54
+ break;
55
+ }
56
+ text += grapheme;
57
+ remaining -= width;
58
+ }
59
+ if (text.length === 0)
60
+ continue;
38
61
  const style = styles?.[segment.role];
39
62
  line += style === undefined ? text : `${style}${text}${RESET}`;
40
63
  }
@@ -50,6 +73,7 @@ export class AnsiFullscreenRenderer {
50
73
  #styles;
51
74
  #mounted = false;
52
75
  #previousLines = [];
76
+ #previousCursor;
53
77
  constructor(options) {
54
78
  this.#writer = options.writer;
55
79
  this.#synchronizedOutput = options.synchronizedOutput === true;
@@ -64,11 +88,50 @@ export class AnsiFullscreenRenderer {
64
88
  mount() {
65
89
  if (this.#mounted)
66
90
  return;
67
- this.#writer.write(ALTERNATE_SCREEN_ENTER);
68
- this.#writer.write(HIDE_CURSOR);
69
- if (this.#synchronizedOutput)
70
- this.#writer.write(SYNCHRONIZED_BEGIN);
71
- this.#mounted = true;
91
+ let alternateScreenEntered = false;
92
+ let cursorHidden = false;
93
+ let synchronizedOutputBegun = false;
94
+ try {
95
+ alternateScreenEntered = true;
96
+ this.#writer.write(ALTERNATE_SCREEN_ENTER);
97
+ cursorHidden = true;
98
+ this.#writer.write(HIDE_CURSOR);
99
+ if (this.#synchronizedOutput) {
100
+ synchronizedOutputBegun = true;
101
+ this.#writer.write(SYNCHRONIZED_BEGIN);
102
+ }
103
+ this.#mounted = true;
104
+ }
105
+ catch (error) {
106
+ this.#mounted = false;
107
+ this.#previousLines = [];
108
+ this.#previousCursor = undefined;
109
+ if (synchronizedOutputBegun) {
110
+ try {
111
+ this.#writer.write(SYNCHRONIZED_END);
112
+ }
113
+ catch {
114
+ // Rollback must continue if one restoration write fails.
115
+ }
116
+ }
117
+ if (cursorHidden) {
118
+ try {
119
+ this.#writer.write(SHOW_CURSOR);
120
+ }
121
+ catch {
122
+ // Rollback must continue if one restoration write fails.
123
+ }
124
+ }
125
+ if (alternateScreenEntered) {
126
+ try {
127
+ this.#writer.write(ALTERNATE_SCREEN_LEAVE);
128
+ }
129
+ catch {
130
+ // Rollback must continue if one restoration write fails.
131
+ }
132
+ }
133
+ throw error;
134
+ }
72
135
  }
73
136
  draw(frame) {
74
137
  if (!this.#mounted)
@@ -78,24 +141,48 @@ export class AnsiFullscreenRenderer {
78
141
  const previousLines = this.#previousLines;
79
142
  const changed = lines.length !== previousLines.length ||
80
143
  lines.some((line, index) => line !== previousLines[index]);
81
- if (!changed)
144
+ const cursorRow = frame.cursor
145
+ ? frame.lines.findIndex((row) => row.key === frame.cursor?.rowKey)
146
+ : -1;
147
+ const nextCursor = cursorRow >= 0 && frame.cursor !== undefined
148
+ ? {
149
+ rowKey: frame.cursor.rowKey,
150
+ column: Math.min(frame.cursor.column, frame.columns - 1),
151
+ row: cursorRow,
152
+ }
153
+ : undefined;
154
+ const cursorChanged = nextCursor?.rowKey !== this.#previousCursor?.rowKey ||
155
+ nextCursor?.column !== this.#previousCursor?.column ||
156
+ nextCursor?.row !== this.#previousCursor?.row;
157
+ if (!changed && !cursorChanged)
82
158
  return;
83
159
  if (this.#synchronizedOutput)
84
160
  this.#writer.write(SYNCHRONIZED_BEGIN);
85
161
  try {
86
- const commonLength = Math.max(lines.length, previousLines.length);
87
- for (let index = 0; index < commonLength; index += 1) {
88
- const line = lines[index];
89
- if (line !== undefined) {
90
- if (line === previousLines[index])
91
- continue;
92
- this.#writer.write(`\u001b[${index + 1};1H\u001b[2K${line}`);
93
- }
94
- else {
95
- this.#writer.write(`\u001b[${index + 1};1H\u001b[2K`);
162
+ if (changed) {
163
+ if (this.#previousCursor !== undefined || nextCursor !== undefined)
164
+ this.#writer.write(HIDE_CURSOR);
165
+ const commonLength = Math.max(lines.length, previousLines.length);
166
+ for (let index = 0; index < commonLength; index += 1) {
167
+ const line = lines[index];
168
+ if (line !== undefined) {
169
+ if (line === previousLines[index])
170
+ continue;
171
+ this.#writer.write(`\u001b[${index + 1};1H\u001b[2K${line}`);
172
+ }
173
+ else {
174
+ this.#writer.write(`\u001b[${index + 1};1H\u001b[2K`);
175
+ }
96
176
  }
177
+ this.#previousLines = lines;
178
+ }
179
+ if (nextCursor !== undefined) {
180
+ this.#writer.write(`\u001b[${cursorRow + 1};${nextCursor.column + 1}H`);
181
+ this.#writer.write(SHOW_CURSOR);
97
182
  }
98
- this.#previousLines = lines;
183
+ else if (this.#previousCursor !== undefined)
184
+ this.#writer.write(HIDE_CURSOR);
185
+ this.#previousCursor = nextCursor;
99
186
  }
100
187
  finally {
101
188
  if (this.#synchronizedOutput)
@@ -114,6 +201,7 @@ export class AnsiFullscreenRenderer {
114
201
  finally {
115
202
  this.#mounted = false;
116
203
  this.#previousLines = [];
204
+ this.#previousCursor = undefined;
117
205
  }
118
206
  }
119
207
  }