praxis-agent 0.39.0 → 0.41.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 (41) hide show
  1. package/README.md +27 -2
  2. package/dist/cli/controls.js +2 -0
  3. package/dist/cli/interactive.d.ts +2 -1
  4. package/dist/cli/interactive.js +79 -53
  5. package/dist/cli/protocol.d.ts +1 -0
  6. package/dist/cli/protocol.js +6 -0
  7. package/dist/cli/tui/ansi-frame-renderer.d.ts +24 -0
  8. package/dist/cli/tui/ansi-frame-renderer.js +120 -0
  9. package/dist/cli/tui/ansi-surface.d.ts +26 -0
  10. package/dist/cli/tui/ansi-surface.js +187 -0
  11. package/dist/cli/tui/ansi-theme.d.ts +3 -0
  12. package/dist/cli/tui/ansi-theme.js +83 -0
  13. package/dist/cli/tui/claude-style.js +2 -1
  14. package/dist/cli/tui/kernel/tui-effect-runner.d.ts +21 -0
  15. package/dist/cli/tui/kernel/tui-effect-runner.js +90 -0
  16. package/dist/cli/tui/kernel/tui-runtime-kernel.d.ts +23 -0
  17. package/dist/cli/tui/kernel/tui-runtime-kernel.js +35 -0
  18. package/dist/cli/tui/kernel/tui-store.d.ts +22 -0
  19. package/dist/cli/tui/kernel/tui-store.js +37 -0
  20. package/dist/cli/tui/praxis-logo.d.ts +5 -0
  21. package/dist/cli/tui/praxis-logo.js +17 -0
  22. package/dist/cli/tui/transcript-viewport.d.ts +1 -0
  23. package/dist/cli/tui/transcript-viewport.js +3 -0
  24. package/dist/cli/tui/tui-focus-stack.d.ts +19 -0
  25. package/dist/cli/tui/tui-focus-stack.js +72 -0
  26. package/dist/cli/tui/tui-row-ir.d.ts +19 -0
  27. package/dist/cli/tui/tui-row-ir.js +126 -0
  28. package/dist/cli/tui/tui-screen-model.d.ts +2 -0
  29. package/dist/cli/tui/tui-screen-model.js +6 -0
  30. package/dist/cli/workspace-trust-prompt.d.ts +10 -0
  31. package/dist/cli/workspace-trust-prompt.js +53 -0
  32. package/dist/cli-runtime.d.ts +2 -0
  33. package/dist/cli-runtime.js +159 -25
  34. package/dist/core/resources.d.ts +5 -0
  35. package/dist/plugins/claude-plugin-mcpb.d.ts +2 -0
  36. package/dist/plugins/claude-plugin-mcpb.js +11 -2
  37. package/dist/plugins/claude-plugin-runtime.d.ts +2 -0
  38. package/dist/plugins/claude-plugin-runtime.js +99 -9
  39. package/dist/security/workspace-trust.d.ts +30 -0
  40. package/dist/security/workspace-trust.js +278 -0
  41. package/package.json +2 -2
package/README.md CHANGED
@@ -90,7 +90,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
90
90
 
91
91
  - **Local agent runtime** — Claude-style responsive TUI with a fixed fullscreen
92
92
  viewport, non-shrinking composer/status area, and complete bounded welcome
93
- surface, plus a shared-command
93
+ surface with the reusable P-loop + spark Praxis mark and ANSI/no-color
94
+ fallbacks, plus a shared-command
94
95
  slash palette, tabbed help and shortcut surfaces, searchable resume picker,
95
96
  restored active-branch conversation history, streaming and expandable
96
97
  thinking, grouped multi-file reads, globally expandable tool results,
@@ -105,7 +106,29 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
105
106
  skills/hooks flow, provider-free per-session `/color` prompt-bar styling,
106
107
  `/mcp`, `/memory` shared instruction and auto-memory
107
108
  access, and live extension-reload controls,
109
+ plus a framework-free runtime kernel that atomically publishes streaming
110
+ frames and keeps status/busy transitions deterministic, with a unified
111
+ framework-free TuiStore that retains runtime and composer state together,
112
+ with a shared semantic transcript Row IR for renderer-neutral layout and
113
+ stable incremental updates,
114
+ backed by the authoritative grapheme/Markdown viewport layout so narrow and
115
+ Unicode-heavy transcripts wrap consistently across renderers,
116
+ and an independent ANSI fullscreen frame renderer with alternate-screen
117
+ lifecycle, synchronized output, and dirty-row diffing,
118
+ opt-in TTY wiring with automatic Ink fallback on renderer failure,
119
+ with complex overlays and dialogs intentionally remaining on the mature Ink
120
+ surface until their semantic ANSI projections are complete,
121
+ while welcome and identity intros also remain on Ink for a complete first
122
+ frame,
123
+ and a pure FocusStack that centralizes overlay/dialog precedence and Esc
124
+ cancellation routing,
125
+ plus a generation-aware effect runner that drops stale asynchronous results
126
+ and aborts cleanly on replacement or unmount,
127
+ with ANSI fullscreen text styles derived from the active semantic theme and
128
+ automatic no-color/screen-reader suppression,
108
129
  cursor/history composer, provider-free `/cost` usage and pricing summaries,
130
+ with a hermetic PTY smoke covering real `runInteractive` ANSI entry,
131
+ resize-safe lifecycle, and Ctrl-C restoration,
109
132
  interactive `/doctor` diagnostics, per-session model/effort/permission controls,
110
133
  context/status/skill/task dashboards, prompt stash and continuation shortcuts,
111
134
  filterable `@` file and agent references, composer undo, `Ctrl+G` external
@@ -147,7 +170,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
147
170
  exclusions, violation reporting, and bare-repository control-file cleanup,
148
171
  safe-property Skill auto-allow, interactive
149
172
  workspace-directory add/remove controls, path confinement, credential
150
- redaction, and sanitized child processes.
173
+ redaction, sanitized child processes, and exact-fingerprint workspace trust
174
+ that blocks automatically discovered project/local hooks and MCP servers
175
+ until the canonical workspace configuration is explicitly accepted.
151
176
  - **Durable local work** — resumable sessions, full-history forks, file
152
177
  checkpoints, tasks, foreground/background subagents, top-level agents, and
153
178
  Claude-compatible main-thread agent definitions with native prompt, model,
@@ -7,6 +7,7 @@ export const DEFAULT_CLI_CONTROLS = {
7
7
  settingSources: undefined,
8
8
  safeMode: false,
9
9
  bare: false,
10
+ trustProject: false,
10
11
  systemPrompt: undefined,
11
12
  systemPromptFile: undefined,
12
13
  appendSystemPrompt: undefined,
@@ -180,6 +181,7 @@ export async function resolveCliControls(controls, cwd) {
180
181
  settingSources: controls.settingSources,
181
182
  safeMode: controls.safeMode,
182
183
  bare: controls.bare,
184
+ trustProject: controls.trustProject,
183
185
  systemPrompt,
184
186
  appendSystemPrompt,
185
187
  excludeDynamicSystemPromptSections: controls.excludeDynamicSystemPromptSections,
@@ -135,6 +135,7 @@ interface InteractiveAppProps {
135
135
  onRendererChange?: (mode: PraxisRuntimeSettings['tui'], sessionId: string | null) => void;
136
136
  onBackground?: (request: InteractiveBackgroundRequest) => Promise<InteractiveBackgroundResult>;
137
137
  axScreenReader?: boolean;
138
+ ansiRenderer?: boolean;
138
139
  allowNewSession?: boolean;
139
140
  resume?: InteractiveResumeOptions;
140
141
  display?: TuiDisplayMetadata;
@@ -204,7 +205,7 @@ export interface InteractiveHistoryState {
204
205
  }
205
206
  /** Advances the React-owned transcript plus its exact local mutation fact. */
206
207
  export declare function advanceInteractiveHistoryState(current: InteractiveHistoryState, items: TranscriptItem[], changedFrom: number): InteractiveHistoryState;
207
- export declare function InteractiveApp({ dataPlane, configRoot: suppliedConfigRoot, statePath: suppliedStatePath, factory, initialSessions, initialPrompt, initialHistory, initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, doctorLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, notificationDelayMs, elicitationUrlOpener, releaseNotesLoader, settingSources, }: InteractiveAppProps): import("react").JSX.Element;
208
+ export declare function InteractiveApp({ dataPlane, configRoot: suppliedConfigRoot, statePath: suppliedStatePath, factory, initialSessions, initialPrompt, initialHistory, initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, ansiRenderer, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, doctorLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, notificationDelayMs, elicitationUrlOpener, releaseNotesLoader, settingSources, }: InteractiveAppProps): import("react").JSX.Element;
208
209
  export declare function runInteractive(options: {
209
210
  dataPlane?: DataPlane;
210
211
  configRoot?: string;
@@ -5,7 +5,7 @@ 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, useRef, useState } from 'react';
8
+ import { useEffect, useMemo, useReducer, useRef, useState, } from 'react';
9
9
  import { Box, Text, render, useApp, useInput } from 'ink';
10
10
  import { permissionRuleValueToString } from '../permissions/permission-updates.js';
11
11
  import { resolveDataPlane, resolveDataPlaneRoot, } from '../persistence/data-plane.js';
@@ -22,10 +22,13 @@ import { loadTuiMemoryFiles, openTuiMemoryFolder, } from './tui/memory-files.js'
22
22
  import { loadClaudeReleaseNotes } from './tui/release-notes.js';
23
23
  import { tuiInkRenderOptions, useTuiPresentationEnvironment, } from './tui/presentation-environment.js';
24
24
  import { StreamingFrameBuffer } from './tui/streaming-frame-buffer.js';
25
+ import { createTuiStoreState, reduceTuiStore } from './tui/kernel/tui-store.js';
26
+ import { TuiEffectRunner } from './tui/kernel/tui-effect-runner.js';
25
27
  import { createClaudeStatusLineInput, StatusLine } from './tui/status-line.js';
26
28
  import { createTuiAppendHistoryChange } from './tui/transcript-window-model.js';
27
29
  import { createTuiHistoryChange, resolveTuiRenderer, } from './tui/tui-view-model.js';
28
30
  import { projectTuiScreen, } from './tui/tui-screen-model.js';
31
+ import { supportsAnsiSurface, TuiAnsiSurface } from './tui/ansi-surface.js';
29
32
  import { projectTuiHelpSurface, } from './tui/help-surface-model.js';
30
33
  import { projectTuiHooksSurface, } from './tui/hooks-surface-model.js';
31
34
  import { loadGitDiff } from './tui/git-diff.js';
@@ -41,6 +44,7 @@ import { canonicalClaudeCostModelName, formatCostSummary, } from './tui/cost-sum
41
44
  import { createComposerEditor, deleteComposerBackward, deleteComposerForward, insertComposerText, moveComposerCursor, } from './tui/composer-editor.js';
42
45
  import { routeComposerKey, } from './tui/composer-key-router.js';
43
46
  import { routeTuiInteraction, } from './tui/tui-interaction-router.js';
47
+ import { currentTuiInteractionLayer, projectTuiFocusStack, } from './tui/tui-focus-stack.js';
44
48
  import { applyMentionReference, fileReferenceAtCursor, loadTuiFileEntries, } from './tui/file-picker.js';
45
49
  import { projectTuiMentionPicker, tuiMentionEntryId, } from './tui/mention-picker-model.js';
46
50
  import { projectTuiListSurface, } from './tui/list-surface-model.js';
@@ -270,7 +274,7 @@ export function advanceInteractiveHistoryState(current, items, changedFrom) {
270
274
  : createTuiHistoryChange(revision, changedFrom, items, current.items),
271
275
  };
272
276
  }
273
- 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, }) {
277
+ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: suppliedConfigRoot, statePath: suppliedStatePath, factory, initialSessions, initialPrompt, initialHistory = [], initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, ansiRenderer = 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, }) {
274
278
  const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
275
279
  const settingsDirectory = '.praxis';
276
280
  const keybindingsRoot = useMemo(() => resolve(suppliedConfigRoot ?? keybindingsConfigRoot ?? resolveDataPlaneRoot()), [dataPlane, keybindingsConfigRoot, suppliedConfigRoot]);
@@ -338,8 +342,17 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
338
342
  const [themeSettings, setThemeSettings] = useState(initialThemeSettings ?? DEFAULT_TUI_THEME_SETTINGS);
339
343
  const [runtimeSettings, setRuntimeSettings] = useState(suppliedRuntimeSettings ??
340
344
  projectRuntimeSettings({ settings: {}, state: {} }));
345
+ const [ansiRendererFailed, setAnsiRendererFailed] = useState(false);
346
+ const ansiRequested = ansiRenderer &&
347
+ !ansiRendererFailed &&
348
+ !axScreenReader &&
349
+ runtimeSettings.tui === 'fullscreen';
341
350
  const presentation = useTuiPresentationEnvironment({
342
- renderer: runtimeSettings.tui,
351
+ renderer: ansiRenderer
352
+ ? ansiRequested
353
+ ? 'fullscreen'
354
+ : 'default'
355
+ : runtimeSettings.tui,
343
356
  screenReader: axScreenReader,
344
357
  ...(terminalWidth === undefined
345
358
  ? {}
@@ -379,9 +392,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
379
392
  ? initialSessions.find((session) => session.sessionId === resume.sessionId)
380
393
  : undefined);
381
394
  const [pendingFork, setPendingFork] = useState(resume?.forkSession === true);
382
- const [input, setInput] = useState('');
383
395
  const inputRef = useRef('');
384
- const [inputCursor, setInputCursor] = useState(0);
385
396
  const inputCursorRef = useRef(0);
386
397
  const inputHistoryRef = useRef([]);
387
398
  const inputHistoryIndexRef = useRef(null);
@@ -392,6 +403,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
392
403
  const clipboardQueueRef = useRef(Promise.resolve());
393
404
  const clipboardPendingRef = useRef(0);
394
405
  const componentMountedRef = useRef(true);
406
+ const themeEffectRunnerRef = useRef(null);
407
+ if (themeEffectRunnerRef.current === null)
408
+ themeEffectRunnerRef.current = new TuiEffectRunner();
395
409
  const [clipboardBusy, setClipboardBusy] = useState(false);
396
410
  const stashedInputRef = useRef('');
397
411
  const lastEscapeAtRef = useRef(0);
@@ -430,15 +444,20 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
430
444
  const configOperationRef = useRef(null);
431
445
  const doctorMenuGenerationRef = useRef(0);
432
446
  const doctorOperationRef = useRef(null);
433
- const [busy, setBusy] = useState(false);
447
+ const [tuiStore, dispatchKernel] = useReducer(reduceTuiStore, undefined, createTuiStoreState);
448
+ const busy = tuiStore.busy;
449
+ const status = tuiStore.status;
450
+ const activeText = tuiStore.activeText;
451
+ const activeThinking = tuiStore.activeThinking;
452
+ const input = tuiStore.composer.text;
453
+ const inputCursor = tuiStore.composer.cursor;
454
+ const setBusy = (value) => dispatchKernel({ type: 'set-busy', busy: value });
455
+ const setStatus = (value) => dispatchKernel({ type: 'set-status', status: value });
434
456
  const taskEntriesRef = useRef([]);
435
457
  const mcpControllerRef = useRef(null);
436
458
  const [compactProgress, setCompactProgress] = useState(0);
437
459
  const initialPromptRef = useRef(initialPrompt?.trim() ?? '');
438
460
  const [initialPromptPending, setInitialPromptPending] = useState(initialPromptRef.current.length > 0);
439
- const [status, setStatus] = useState('ready');
440
- const [activeText, setActiveText] = useState('');
441
- const [activeThinking, setActiveThinking] = useState('');
442
461
  // One provider-neutral streaming frame buffer per mounted app. RuntimeEvent
443
462
  // text/thinking deltas accumulate here and are coalesced into bounded frames
444
463
  // instead of causing a React state update per delta. The React state is only
@@ -448,8 +467,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
448
467
  if (streamingFrameRef.current === null) {
449
468
  streamingFrameRef.current = new StreamingFrameBuffer({
450
469
  publish: (frame) => {
451
- setActiveText(frame.text);
452
- setActiveThinking(frame.thinking);
470
+ dispatchKernel({
471
+ type: 'publish-stream-frame',
472
+ text: frame.text,
473
+ thinking: frame.thinking,
474
+ });
453
475
  },
454
476
  });
455
477
  }
@@ -1194,6 +1216,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1194
1216
  sessionId,
1195
1217
  screenSurfaces,
1196
1218
  ]);
1219
+ const ansiActive = ansiRequested && supportsAnsiSurface(screen);
1197
1220
  useEffect(() => {
1198
1221
  previousTuiScreenRef.current = screen;
1199
1222
  }, [screen]);
@@ -1273,6 +1296,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1273
1296
  }, [exit, signal]);
1274
1297
  useEffect(() => () => {
1275
1298
  componentMountedRef.current = false;
1299
+ themeEffectRunnerRef.current?.dispose();
1276
1300
  streamingFrameRef.current?.dispose();
1277
1301
  drainPermissionQueue();
1278
1302
  elicitationRef.current?.resolve({ action: 'cancel' });
@@ -1308,20 +1332,23 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1308
1332
  append({ kind: 'warning', text: initialThemeLoadError });
1309
1333
  return;
1310
1334
  }
1311
- let cancelled = false;
1312
- void presentationThemeStore.load().then((loaded) => {
1313
- if (!cancelled)
1335
+ const themeEffectRunner = themeEffectRunnerRef.current;
1336
+ if (themeEffectRunner === null)
1337
+ return;
1338
+ const run = themeEffectRunner.run(async ({ isCurrent }) => {
1339
+ const loaded = await presentationThemeStore.load();
1340
+ return isCurrent() ? loaded : undefined;
1341
+ });
1342
+ void run.promise.then((loaded) => {
1343
+ if (loaded !== undefined)
1314
1344
  setThemeSettings(themeSettingsWithCustomTheme(loaded, customThemes));
1315
1345
  }, (error) => {
1316
- if (!cancelled)
1317
- append({
1318
- kind: 'warning',
1319
- text: `Unable to load theme settings: ${error instanceof Error ? error.message : String(error)}`,
1320
- });
1346
+ append({
1347
+ kind: 'warning',
1348
+ text: `Unable to load theme settings: ${error instanceof Error ? error.message : String(error)}`,
1349
+ });
1321
1350
  });
1322
- return () => {
1323
- cancelled = true;
1324
- };
1351
+ return run.cancel;
1325
1352
  }, [
1326
1353
  customThemes,
1327
1354
  initialThemeLoadError,
@@ -1404,8 +1431,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1404
1431
  const editor = createComposerEditor(next, cursor);
1405
1432
  inputRef.current = editor.text;
1406
1433
  inputCursorRef.current = editor.cursor;
1407
- setInput(editor.text);
1408
- setInputCursor(editor.cursor);
1434
+ dispatchKernel({
1435
+ type: 'set-composer',
1436
+ text: editor.text,
1437
+ cursor: editor.cursor,
1438
+ });
1409
1439
  setShortcutsVisible(false);
1410
1440
  setCommandPaletteOpen(slashCommandQuery(editor.text) !== null);
1411
1441
  setCommandSelection(0);
@@ -3578,32 +3608,23 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
3578
3608
  if (hasPendingPrefix) {
3579
3609
  keySequenceRef.current = { chord: inputChord, at: Date.now() };
3580
3610
  }
3581
- const layer = hasPendingPrefix
3582
- ? { kind: 'pending-prefix' }
3583
- : permission
3584
- ? { kind: 'cancelable', target: 'permission' }
3585
- : planApproval
3586
- ? { kind: 'cancelable', target: 'plan-approval' }
3587
- : question
3588
- ? { kind: 'cancelable', target: 'question' }
3589
- : elicitation
3590
- ? {
3591
- kind: 'cancelable',
3592
- target: elicitationUrlWaiting
3593
- ? 'elicitation-url-waiting'
3594
- : elicitationForm?.expandedField
3595
- ? 'elicitation-options'
3596
- : 'elicitation',
3597
- }
3598
- : selectingSession
3599
- ? { kind: 'delegated', target: 'session-picker' }
3600
- : menuRef.current
3601
- ? { kind: 'delegated', target: 'menu' }
3602
- : filePickerVisible
3603
- ? { kind: 'cancelable', target: 'file-picker' }
3604
- : commandPaletteVisible
3605
- ? { kind: 'cancelable', target: 'command-palette' }
3606
- : { kind: 'none' };
3611
+ const layer = currentTuiInteractionLayer(projectTuiFocusStack({
3612
+ pendingPrefix: hasPendingPrefix,
3613
+ permission: Boolean(permission),
3614
+ planApproval: Boolean(planApproval),
3615
+ question: Boolean(question),
3616
+ elicitation: elicitation
3617
+ ? elicitationUrlWaiting
3618
+ ? 'url-waiting'
3619
+ : elicitationForm?.expandedField
3620
+ ? 'expanded-options'
3621
+ : 'plain'
3622
+ : false,
3623
+ selectingSession: Boolean(selectingSession),
3624
+ menu: Boolean(menuRef.current),
3625
+ filePicker: filePickerVisible,
3626
+ commandPalette: commandPaletteVisible,
3627
+ }));
3607
3628
  const callerIntent = !busy && value === '?' && inputRef.current.length === 0
3608
3629
  ? 'toggle-shortcuts'
3609
3630
  : runtimeSettingsRef.current.leftArrowOpensAgents &&
@@ -6482,7 +6503,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
6482
6503
  });
6483
6504
  return (_jsx(TuiThemeProvider, { settings: themeSettings, screenReader: axScreenReader, children: _jsx(Box, { flexDirection: "column", ...(!fixedViewport
6484
6505
  ? {}
6485
- : { height: rows, overflowY: 'hidden' }), children: 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
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
6486
6509
  ? {
6487
6510
  flexShrink: 1,
6488
6511
  minHeight: 0,
@@ -6702,13 +6725,16 @@ export async function runInteractive(options) {
6702
6725
  let cleanup = null;
6703
6726
  let backgrounded;
6704
6727
  rendererChange = null;
6728
+ const ansiRendererEnabled = currentRenderer === 'fullscreen' &&
6729
+ process.stdin.isTTY === true &&
6730
+ options.axScreenReader !== true;
6705
6731
  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
6706
6732
  ? {}
6707
6733
  : { settingSources: options.settingSources }), initialThemeSettings: initialThemeSettings, ...(initialThemeLoadError === undefined
6708
6734
  ? {}
6709
6735
  : { initialThemeLoadError }), ...(currentInitialPrompt === undefined
6710
6736
  ? {}
6711
- : { initialPrompt: currentInitialPrompt }), signal: signal, onCancel: () => controller.abort(), onTurnChange: (turn) => {
6737
+ : { initialPrompt: currentInitialPrompt }), signal: signal, ansiRenderer: ansiRendererEnabled, onCancel: () => controller.abort(), onTurnChange: (turn) => {
6712
6738
  activeTurn = turn;
6713
6739
  }, onCleanup: (closing) => {
6714
6740
  cleanup = closing;
@@ -6732,7 +6758,7 @@ export async function runInteractive(options) {
6732
6758
  ? { allowDangerouslySkipPermissions: true }
6733
6759
  : {}) }), {
6734
6760
  exitOnCtrlC: false,
6735
- ...tuiInkRenderOptions(currentRenderer, options.axScreenReader === true),
6761
+ ...tuiInkRenderOptions(ansiRendererEnabled ? 'default' : currentRenderer, options.axScreenReader === true),
6736
6762
  interactive: true,
6737
6763
  });
6738
6764
  await instance.waitUntilExit();
@@ -14,6 +14,7 @@ export interface CliControls {
14
14
  settingSources: readonly ('user' | 'project' | 'local')[] | undefined;
15
15
  safeMode: boolean;
16
16
  bare: boolean;
17
+ trustProject: boolean;
17
18
  systemPrompt: string | undefined;
18
19
  systemPromptFile: string | undefined;
19
20
  appendSystemPrompt: string | undefined;
@@ -359,6 +359,7 @@ export function parseCliInvocation(argv) {
359
359
  let autocompact;
360
360
  let safeMode = false;
361
361
  let bare = false;
362
+ let trustProject = false;
362
363
  let systemPrompt;
363
364
  let systemPromptFile;
364
365
  let appendSystemPrompt;
@@ -1082,6 +1083,10 @@ export function parseCliInvocation(argv) {
1082
1083
  bare = true;
1083
1084
  continue;
1084
1085
  }
1086
+ if (value === '--trust-project') {
1087
+ trustProject = true;
1088
+ continue;
1089
+ }
1085
1090
  if (value === '--init') {
1086
1091
  init = true;
1087
1092
  continue;
@@ -1343,6 +1348,7 @@ export function parseCliInvocation(argv) {
1343
1348
  settingSources,
1344
1349
  safeMode,
1345
1350
  bare,
1351
+ trustProject,
1346
1352
  systemPrompt,
1347
1353
  systemPromptFile,
1348
1354
  appendSystemPrompt,
@@ -0,0 +1,24 @@
1
+ import type { TuiRow, TuiTextRole } from './tui-row-ir.js';
2
+ export interface AnsiFrameWriter {
3
+ write(chunk: string): void;
4
+ }
5
+ export interface AnsiFrame {
6
+ readonly columns: number;
7
+ readonly rows: number;
8
+ readonly lines: readonly TuiRow[];
9
+ }
10
+ export interface AnsiFullscreenRendererOptions {
11
+ readonly writer: AnsiFrameWriter;
12
+ readonly synchronizedOutput?: boolean;
13
+ readonly styles?: Partial<Record<TuiTextRole, string>>;
14
+ }
15
+ export declare class AnsiFullscreenRenderer {
16
+ #private;
17
+ constructor(options: AnsiFullscreenRendererOptions);
18
+ setStyles(styles?: Partial<Record<TuiTextRole, string>>): void;
19
+ get mounted(): boolean;
20
+ mount(): void;
21
+ draw(frame: AnsiFrame): void;
22
+ dispose(): void;
23
+ }
24
+ //# sourceMappingURL=ansi-frame-renderer.d.ts.map
@@ -0,0 +1,120 @@
1
+ import { stripVTControlCharacters } from 'node:util';
2
+ const ALTERNATE_SCREEN_ENTER = '\u001b[?1049h';
3
+ const ALTERNATE_SCREEN_LEAVE = '\u001b[?1049l';
4
+ const HIDE_CURSOR = '\u001b[?25l';
5
+ const SHOW_CURSOR = '\u001b[?25h';
6
+ const SYNCHRONIZED_BEGIN = '\u001b[?2026h';
7
+ const SYNCHRONIZED_END = '\u001b[?2026l';
8
+ const RESET = '\u001b[0m';
9
+ function validateFrame(frame) {
10
+ if (frame === null || typeof frame !== 'object') {
11
+ throw new TypeError('ANSI frame must be an object');
12
+ }
13
+ for (const key of ['columns', 'rows']) {
14
+ const value = frame[key];
15
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
16
+ throw new TypeError(`ANSI frame ${key} must be a positive finite integer`);
17
+ }
18
+ }
19
+ if (!Array.isArray(frame.lines)) {
20
+ throw new TypeError('ANSI frame lines must be an array');
21
+ }
22
+ }
23
+ function renderLine(row, styles, columns) {
24
+ const segments = row.segments.map((segment) => ({
25
+ text: stripVTControlCharacters(segment.text).replace(/[\r\n]/gu, ''),
26
+ role: segment.role,
27
+ }));
28
+ const visibleLength = segments.reduce((length, segment) => length + Array.from(segment.text).length, 0);
29
+ const wasTruncated = visibleLength > columns;
30
+ const targetLength = wasTruncated ? Math.max(0, columns - 1) : columns;
31
+ let remaining = targetLength;
32
+ let line = '';
33
+ for (const segment of segments) {
34
+ if (remaining === 0)
35
+ break;
36
+ const text = Array.from(segment.text).slice(0, remaining).join('');
37
+ remaining -= Array.from(text).length;
38
+ const style = styles?.[segment.role];
39
+ line += style === undefined ? text : `${style}${text}${RESET}`;
40
+ }
41
+ if (wasTruncated)
42
+ line += '…';
43
+ if (line.length === 0)
44
+ line = ' ';
45
+ return line;
46
+ }
47
+ export class AnsiFullscreenRenderer {
48
+ #writer;
49
+ #synchronizedOutput;
50
+ #styles;
51
+ #mounted = false;
52
+ #previousLines = [];
53
+ constructor(options) {
54
+ this.#writer = options.writer;
55
+ this.#synchronizedOutput = options.synchronizedOutput === true;
56
+ this.#styles = options.styles;
57
+ }
58
+ setStyles(styles) {
59
+ this.#styles = styles;
60
+ }
61
+ get mounted() {
62
+ return this.#mounted;
63
+ }
64
+ mount() {
65
+ if (this.#mounted)
66
+ 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;
72
+ }
73
+ draw(frame) {
74
+ if (!this.#mounted)
75
+ throw new Error('ANSI fullscreen renderer is not mounted');
76
+ validateFrame(frame);
77
+ const lines = frame.lines.map((row) => renderLine(row, this.#styles, frame.columns));
78
+ const previousLines = this.#previousLines;
79
+ const changed = lines.length !== previousLines.length ||
80
+ lines.some((line, index) => line !== previousLines[index]);
81
+ if (!changed)
82
+ return;
83
+ if (this.#synchronizedOutput)
84
+ this.#writer.write(SYNCHRONIZED_BEGIN);
85
+ 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`);
96
+ }
97
+ }
98
+ this.#previousLines = lines;
99
+ }
100
+ finally {
101
+ if (this.#synchronizedOutput)
102
+ this.#writer.write(SYNCHRONIZED_END);
103
+ }
104
+ }
105
+ dispose() {
106
+ if (!this.#mounted)
107
+ return;
108
+ try {
109
+ if (this.#synchronizedOutput)
110
+ this.#writer.write(SYNCHRONIZED_END);
111
+ this.#writer.write(SHOW_CURSOR);
112
+ this.#writer.write(ALTERNATE_SCREEN_LEAVE);
113
+ }
114
+ finally {
115
+ this.#mounted = false;
116
+ this.#previousLines = [];
117
+ }
118
+ }
119
+ }
120
+ //# sourceMappingURL=ansi-frame-renderer.js.map
@@ -0,0 +1,26 @@
1
+ import { type AnsiFrame } from './ansi-frame-renderer.js';
2
+ import type { TuiScreenModel } from './tui-screen-model.js';
3
+ export interface TuiAnsiSurfaceProps {
4
+ screen: TuiScreenModel;
5
+ width: number;
6
+ rows: number | undefined;
7
+ input: string;
8
+ busy: boolean;
9
+ status: string;
10
+ screenReader?: boolean;
11
+ display?: {
12
+ cwd: string;
13
+ model?: string;
14
+ effort?: string;
15
+ permissionMode?: string;
16
+ version?: string;
17
+ };
18
+ sessionLabel?: string;
19
+ cursor?: number;
20
+ onError: (error: unknown) => void;
21
+ }
22
+ /** ANSI mode is intentionally limited to the plain conversation surface. */
23
+ export declare function supportsAnsiSurface(screen: TuiScreenModel): boolean;
24
+ export declare function projectAnsiSurfaceFrame(props: TuiAnsiSurfaceProps): AnsiFrame;
25
+ export declare function TuiAnsiSurface(props: TuiAnsiSurfaceProps): null;
26
+ //# sourceMappingURL=ansi-surface.d.ts.map