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.
- package/README.md +27 -2
- package/dist/cli/controls.js +2 -0
- package/dist/cli/interactive.d.ts +2 -1
- package/dist/cli/interactive.js +79 -53
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.js +6 -0
- package/dist/cli/tui/ansi-frame-renderer.d.ts +24 -0
- package/dist/cli/tui/ansi-frame-renderer.js +120 -0
- package/dist/cli/tui/ansi-surface.d.ts +26 -0
- package/dist/cli/tui/ansi-surface.js +187 -0
- package/dist/cli/tui/ansi-theme.d.ts +3 -0
- package/dist/cli/tui/ansi-theme.js +83 -0
- package/dist/cli/tui/claude-style.js +2 -1
- package/dist/cli/tui/kernel/tui-effect-runner.d.ts +21 -0
- package/dist/cli/tui/kernel/tui-effect-runner.js +90 -0
- package/dist/cli/tui/kernel/tui-runtime-kernel.d.ts +23 -0
- package/dist/cli/tui/kernel/tui-runtime-kernel.js +35 -0
- package/dist/cli/tui/kernel/tui-store.d.ts +22 -0
- package/dist/cli/tui/kernel/tui-store.js +37 -0
- package/dist/cli/tui/praxis-logo.d.ts +5 -0
- package/dist/cli/tui/praxis-logo.js +17 -0
- package/dist/cli/tui/transcript-viewport.d.ts +1 -0
- package/dist/cli/tui/transcript-viewport.js +3 -0
- package/dist/cli/tui/tui-focus-stack.d.ts +19 -0
- package/dist/cli/tui/tui-focus-stack.js +72 -0
- package/dist/cli/tui/tui-row-ir.d.ts +19 -0
- package/dist/cli/tui/tui-row-ir.js +126 -0
- package/dist/cli/tui/tui-screen-model.d.ts +2 -0
- package/dist/cli/tui/tui-screen-model.js +6 -0
- package/dist/cli/workspace-trust-prompt.d.ts +10 -0
- package/dist/cli/workspace-trust-prompt.js +53 -0
- package/dist/cli-runtime.d.ts +2 -0
- package/dist/cli-runtime.js +159 -25
- package/dist/core/resources.d.ts +5 -0
- package/dist/plugins/claude-plugin-mcpb.d.ts +2 -0
- package/dist/plugins/claude-plugin-mcpb.js +11 -2
- package/dist/plugins/claude-plugin-runtime.d.ts +2 -0
- package/dist/plugins/claude-plugin-runtime.js +99 -9
- package/dist/security/workspace-trust.d.ts +30 -0
- package/dist/security/workspace-trust.js +278 -0
- 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
|
|
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,
|
|
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,
|
package/dist/cli/controls.js
CHANGED
|
@@ -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;
|
package/dist/cli/interactive.js
CHANGED
|
@@ -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:
|
|
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 [
|
|
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
|
-
|
|
452
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
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 =
|
|
3582
|
-
|
|
3583
|
-
: permission
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
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:
|
|
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();
|
package/dist/cli/protocol.d.ts
CHANGED
|
@@ -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;
|
package/dist/cli/protocol.js
CHANGED
|
@@ -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
|