praxis-agent 0.22.0 → 0.23.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.
- package/dist/application/session-memory.d.ts +5 -2
- package/dist/application/session-memory.js +5 -2
- package/dist/cli/interactive.js +83 -31
- package/dist/cli/tui/claude-style.js +7 -4
- package/dist/cli/tui/composer-key-router.d.ts +31 -0
- package/dist/cli/tui/composer-key-router.js +68 -0
- package/dist/cli/tui/composer-layout.d.ts +7 -0
- package/dist/cli/tui/composer-layout.js +13 -0
- package/dist/cli/tui/fullscreen-renderer.d.ts +14 -0
- package/dist/cli/tui/fullscreen-renderer.js +13 -0
- package/dist/cli/tui/streaming-frame-buffer.d.ts +6 -0
- package/dist/cli/tui/streaming-frame-buffer.js +39 -16
- package/dist/cli/tui/tool-permission.js +5 -3
- package/dist/cli/tui/tui-view-model.d.ts +46 -0
- package/dist/cli/tui/tui-view-model.js +52 -0
- package/package.json +2 -1
|
@@ -25,8 +25,11 @@ export interface SessionMemoryStoreOptions {
|
|
|
25
25
|
sidecarRoot?: string;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Durable sidecar for one session's extracted memory
|
|
29
|
-
* `<configRoot>/praxis/session-memory/<sessionId
|
|
28
|
+
* Durable sidecar for one session's extracted memory. Claude compatibility
|
|
29
|
+
* defaults to `<configRoot>/praxis/session-memory/<sessionId>/`; callers may
|
|
30
|
+
* select another data plane with
|
|
31
|
+
* `<sidecarRoot>/session-memory/<sessionId>/` (for example, native
|
|
32
|
+
* `<configRoot>/state/session-memory/<sessionId>/`). All writes are atomic
|
|
30
33
|
* (same-directory temp file, fsync, then rename) and version-checked. Never
|
|
31
34
|
* touches shared Claude transcript entries.
|
|
32
35
|
*/
|
|
@@ -77,8 +77,11 @@ function parseSessionMemoryState(source) {
|
|
|
77
77
|
return value;
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
* Durable sidecar for one session's extracted memory
|
|
81
|
-
* `<configRoot>/praxis/session-memory/<sessionId
|
|
80
|
+
* Durable sidecar for one session's extracted memory. Claude compatibility
|
|
81
|
+
* defaults to `<configRoot>/praxis/session-memory/<sessionId>/`; callers may
|
|
82
|
+
* select another data plane with
|
|
83
|
+
* `<sidecarRoot>/session-memory/<sessionId>/` (for example, native
|
|
84
|
+
* `<configRoot>/state/session-memory/<sessionId>/`). All writes are atomic
|
|
82
85
|
* (same-directory temp file, fsync, then rename) and version-checked. Never
|
|
83
86
|
* touches shared Claude transcript entries.
|
|
84
87
|
*/
|
package/dist/cli/interactive.js
CHANGED
|
@@ -15,9 +15,11 @@ import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/se
|
|
|
15
15
|
import { CommandPalette, BtwPanel, Composer, DiffDashboard, DialogFrame, ExternalEditorWait, HelpMenu, HookDashboard, ListDashboard, MemoryDashboard, MentionPicker, ModelMenu, PermissionDashboard, SelectionMenu, SessionPicker, ThemePicker, CustomThemeEditor, SessionIdentity, Transcript, WelcomePanel, useTerminalRows, useTerminalWidth, } from './tui/claude-style.js';
|
|
16
16
|
import { loadTuiMemoryFiles, openTuiMemoryFolder, } from './tui/memory-files.js';
|
|
17
17
|
import { loadClaudeReleaseNotes } from './tui/release-notes.js';
|
|
18
|
+
import { fullscreenInkRenderOptions } from './tui/fullscreen-renderer.js';
|
|
18
19
|
import { StreamingFrameBuffer } from './tui/streaming-frame-buffer.js';
|
|
19
20
|
import { createClaudeStatusLineInput, StatusLine } from './tui/status-line.js';
|
|
20
|
-
import { FULLSCREEN_TRANSCRIPT_RESERVED_ROWS,
|
|
21
|
+
import { FULLSCREEN_TRANSCRIPT_RESERVED_ROWS, transcriptLineCount, } from './tui/transcript-viewport.js';
|
|
22
|
+
import { projectTuiView, resolveTuiRenderer } from './tui/tui-view-model.js';
|
|
21
23
|
import { loadGitDiff, visiblePatchLines, } from './tui/git-diff.js';
|
|
22
24
|
import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule, } from './tui/permission-settings.js';
|
|
23
25
|
import { createRecentlyDeniedStore, } from './tui/recently-denied.js';
|
|
@@ -26,6 +28,7 @@ import { filterTuiSlashCommands, mergeTuiSlashCommands, slashCommandQuery, } fro
|
|
|
26
28
|
import { runDoctor, } from '../maintenance/doctor.js';
|
|
27
29
|
import { canonicalClaudeCostModelName, formatCostSummary, } from './tui/cost-summary.js';
|
|
28
30
|
import { createComposerEditor, deleteComposerBackward, deleteComposerForward, deleteComposerToEnd, deleteComposerToStart, deleteComposerWordBackward, insertComposerText, moveComposerCursor, moveComposerCursorByWord, } from './tui/composer-editor.js';
|
|
31
|
+
import { routeComposerKey, } from './tui/composer-key-router.js';
|
|
29
32
|
import { applyMentionReference, fileReferenceAtCursor, filterTuiMentionEntries, loadTuiFileEntries, } from './tui/file-picker.js';
|
|
30
33
|
import { editTuiPrompt, openTuiEditorFile, } from './tui/external-editor.js';
|
|
31
34
|
import { suspendTuiProcess } from './tui/terminal-suspend.js';
|
|
@@ -447,32 +450,31 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
447
450
|
const [contextWindowTokens, setContextWindowTokens] = useState(display.contextWindowTokens);
|
|
448
451
|
const [history, setHistory] = useState([...initialHistory]);
|
|
449
452
|
const [transcriptScrollOffset, setTranscriptScrollOffset] = useState(0);
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
const resumed
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
: history;
|
|
453
|
+
// The pure TUI view model classifies the session identity (fresh/resumed/
|
|
454
|
+
// started) and projects the rendered transcript. Startup diagnostics are
|
|
455
|
+
// useful before the first prompt, but they are not conversation history and
|
|
456
|
+
// must not suppress the new-session welcome panel. Only real user/assistant
|
|
457
|
+
// transcript entries start a conversation; every other kind (thinking,
|
|
458
|
+
// context, tool, shell, notices, results, and so on) is operational
|
|
459
|
+
// bookkeeping that must not hide the fresh-session welcome. The original
|
|
460
|
+
// loaded transcript decides whether the session was resumed, separately from
|
|
461
|
+
// the live history that grows while the session runs. A session is resumed
|
|
462
|
+
// only when it was opened through `resume` and the original transcript
|
|
463
|
+
// already contained real conversation content. Fullscreen projects only the
|
|
464
|
+
// newest transcript tail that fits the fixed viewport, leaving the
|
|
465
|
+
// composer/status chrome intact and keeping the active stream visible.
|
|
466
|
+
// Classic and screen-reader modes always render the full history exactly as
|
|
467
|
+
// before.
|
|
468
|
+
const { projectedHistory, resumed, freshSession, hasConversationHistory } = projectTuiView({
|
|
469
|
+
initialHistory,
|
|
470
|
+
history,
|
|
471
|
+
resume: resume !== undefined,
|
|
472
|
+
fixedViewport,
|
|
473
|
+
screenReader: axScreenReader,
|
|
474
|
+
rows,
|
|
475
|
+
width,
|
|
476
|
+
scrollOffset: transcriptScrollOffset,
|
|
477
|
+
});
|
|
476
478
|
const sessionLoadRef = useRef(0);
|
|
477
479
|
const [turnDiffs, setTurnDiffs] = useState([]);
|
|
478
480
|
const turnNumberRef = useRef(0);
|
|
@@ -2691,6 +2693,14 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
2691
2693
|
}
|
|
2692
2694
|
setUsage(result.usage);
|
|
2693
2695
|
setCostUsd(result.costUsd);
|
|
2696
|
+
// The active stream and its committed final item must never render in the
|
|
2697
|
+
// same frame. Publish the cleared stream before appending the final
|
|
2698
|
+
// assistant entry; there is no await between these operations, and the
|
|
2699
|
+
// renderer's frame buffer prevents the identical text from being shown
|
|
2700
|
+
// twice during the transition.
|
|
2701
|
+
streamingFrameRef.current?.resetText();
|
|
2702
|
+
streamingFrameRef.current?.resetThinking();
|
|
2703
|
+
streamingFrameRef.current?.flush();
|
|
2694
2704
|
append({ kind: 'assistant', text: result.text });
|
|
2695
2705
|
if (turnMutatedFilesRef.current) {
|
|
2696
2706
|
try {
|
|
@@ -5735,7 +5745,22 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
5735
5745
|
clearComposerInput();
|
|
5736
5746
|
return;
|
|
5737
5747
|
}
|
|
5738
|
-
|
|
5748
|
+
const transition = routeComposerKey(editor(), {
|
|
5749
|
+
value,
|
|
5750
|
+
left: key.leftArrow,
|
|
5751
|
+
right: key.rightArrow,
|
|
5752
|
+
backspace: key.backspace,
|
|
5753
|
+
delete: key.delete,
|
|
5754
|
+
ctrl: key.ctrl,
|
|
5755
|
+
meta: key.meta,
|
|
5756
|
+
escape: key.escape || value === '\u001B',
|
|
5757
|
+
});
|
|
5758
|
+
if (transition.kind === 'cancel') {
|
|
5759
|
+
clearComposerInput();
|
|
5760
|
+
}
|
|
5761
|
+
else if (transition.kind === 'edit') {
|
|
5762
|
+
updateComposerEditor(transition.editor);
|
|
5763
|
+
}
|
|
5739
5764
|
});
|
|
5740
5765
|
return (_jsx(TuiThemeProvider, { settings: themeSettings, children: _jsx(Box, { flexDirection: "column", ...(!fixedViewport
|
|
5741
5766
|
? {}
|
|
@@ -5884,6 +5909,22 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
5884
5909
|
usage?.outputTokens,
|
|
5885
5910
|
].join(':'), width: width, ...(settingSources === undefined ? {} : { settingSources }) })] }))] })) }) }));
|
|
5886
5911
|
}
|
|
5912
|
+
/**
|
|
5913
|
+
* Whether the user explicitly saved a `tui` renderer value in configuration.
|
|
5914
|
+
* A fresh install leaves the setting unset, so the interactive TTY session can
|
|
5915
|
+
* default to the fullscreen renderer; once the user runs `/tui default` or
|
|
5916
|
+
* `/tui fullscreen`, the saved value is honored. If configuration cannot be
|
|
5917
|
+
* read, the loaded runtime setting is honored instead of guessing.
|
|
5918
|
+
*/
|
|
5919
|
+
async function tuiRendererExplicitlyConfigured(target) {
|
|
5920
|
+
try {
|
|
5921
|
+
const snapshot = await loadConfigSettings(target);
|
|
5922
|
+
return snapshot.settings.tui !== undefined;
|
|
5923
|
+
}
|
|
5924
|
+
catch {
|
|
5925
|
+
return true;
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5887
5928
|
export async function runInteractive(options) {
|
|
5888
5929
|
const controller = new AbortController();
|
|
5889
5930
|
const signal = options.signal
|
|
@@ -5903,7 +5944,19 @@ export async function runInteractive(options) {
|
|
|
5903
5944
|
const runtimeSettingsTarget = { configRoot, statePath };
|
|
5904
5945
|
let currentRuntimeSettings = options.runtimeSettings ??
|
|
5905
5946
|
(await loadRuntimeSettings(runtimeSettingsTarget));
|
|
5906
|
-
|
|
5947
|
+
// Fullscreen is the default interactive TTY renderer. Classic remains the
|
|
5948
|
+
// fallback for screen-reader and non-interactive execution, and an explicit
|
|
5949
|
+
// renderer saved in configuration is honored over the default.
|
|
5950
|
+
const rendererExplicitlyConfigured = await tuiRendererExplicitlyConfigured(runtimeSettingsTarget);
|
|
5951
|
+
let currentRenderer = resolveTuiRenderer({
|
|
5952
|
+
configured: currentRuntimeSettings.tui,
|
|
5953
|
+
explicitlyConfigured: rendererExplicitlyConfigured,
|
|
5954
|
+
interactiveTty: process.stdin.isTTY === true,
|
|
5955
|
+
screenReader: options.axScreenReader ?? false,
|
|
5956
|
+
});
|
|
5957
|
+
if (currentRenderer !== currentRuntimeSettings.tui) {
|
|
5958
|
+
currentRuntimeSettings = { ...currentRuntimeSettings, tui: currentRenderer };
|
|
5959
|
+
}
|
|
5907
5960
|
let rendererChange = null;
|
|
5908
5961
|
let rendererNotice;
|
|
5909
5962
|
const readRendererChange = () => rendererChange;
|
|
@@ -6026,9 +6079,8 @@ export async function runInteractive(options) {
|
|
|
6026
6079
|
? { allowDangerouslySkipPermissions: true }
|
|
6027
6080
|
: {}) }), {
|
|
6028
6081
|
exitOnCtrlC: false,
|
|
6029
|
-
|
|
6082
|
+
...fullscreenInkRenderOptions(currentRenderer, options.axScreenReader),
|
|
6030
6083
|
interactive: true,
|
|
6031
|
-
alternateScreen: currentRenderer === 'fullscreen',
|
|
6032
6084
|
});
|
|
6033
6085
|
await instance.waitUntilExit();
|
|
6034
6086
|
if (activeTurn)
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { cloneElement, useEffect, useState } from 'react';
|
|
3
3
|
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { composerEditorSegments } from './composer-editor.js';
|
|
5
|
+
import { composerLayoutForWidth } from './composer-layout.js';
|
|
5
6
|
import { visiblePatchLines } from './git-diff.js';
|
|
6
7
|
import { TUI_HOOK_MENU } from './hook-settings.js';
|
|
7
8
|
import { tuiPalette, tuiSyntaxStyle, useTuiPalette, } from './theme.js';
|
|
@@ -379,7 +380,9 @@ function ToolTranscriptEntry({ call, detail, result, detailed, }) {
|
|
|
379
380
|
: displayResult;
|
|
380
381
|
const oldLines = contentLines(inputString(call, 'old_string'));
|
|
381
382
|
const newLines = contentLines(inputString(call, 'new_string'));
|
|
382
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: palette.accent, children: "\u23FA" }), ' ', _jsx(Text, { bold: true, children: toolHeading(call) })] }), !['Bash', 'Read', 'Edit'].includes(call.name) && detail ? (_jsxs(Text, { dimColor: true, children: [" ", detail] })) : null, result ? (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: result.isError ? (_jsxs(Text, { color: palette.error, children: ["\u23BF Error: ", errorText] })) : call.name === 'Edit' ? (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["\u23BF Added ", newLines.length, " line", newLines.length === 1 ? '' : 's', ", removed ", oldLines.length, " line", oldLines.length === 1 ? '' : 's'] }),
|
|
383
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: palette.accent, children: "\u23FA" }), ' ', _jsx(Text, { bold: true, children: toolHeading(call) })] }), !['Bash', 'Read', 'Edit'].includes(call.name) && detail ? (_jsxs(Text, { dimColor: true, children: [" ", detail] })) : null, result ? (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: result.isError ? (_jsxs(Text, { color: palette.error, children: ["\u23BF Error: ", errorText] })) : call.name === 'Edit' ? (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["\u23BF Added ", newLines.length, " line", newLines.length === 1 ? '' : 's', ", removed ", oldLines.length, " line", oldLines.length === 1 ? '' : 's'] }), detailed &&
|
|
384
|
+
oldLines.map((line, index) => (_jsx(SyntaxCodeLine, { prefix: ` ${index + 1} -`, text: line, change: "removed" }, `old-${index}`))), detailed &&
|
|
385
|
+
newLines.map((line, index) => (_jsx(SyntaxCodeLine, { prefix: ` ${index + 1} +`, text: line, change: "added" }, `new-${index}`)))] })) : resultIsDiff ? (_jsx(ToolResultText, { text: visible.join('\n'), prefix: "\u23BF " })) : (_jsxs(_Fragment, { children: [visible.map((line, index) => (_jsxs(Text, { dimColor: true, children: [index === 0 ? '⎿ ' : ' ', line || ' '] }, index))), hidden > 0 ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2026 +", hidden, " lines (ctrl+o to expand)"] })) : null] })) })) : null] }));
|
|
383
386
|
}
|
|
384
387
|
function ThinkingBlock({ text, active, expanded, screenReader, }) {
|
|
385
388
|
const palette = useTuiPalette();
|
|
@@ -976,9 +979,9 @@ export function Composer({ input, cursor, busy, clipboardBusy = false, status, d
|
|
|
976
979
|
: shellMode
|
|
977
980
|
? `Shell command: ${input}`
|
|
978
981
|
: `Prompt: ${input}` }));
|
|
979
|
-
const
|
|
982
|
+
const { lineWidth, footerWidth, showEditorHint } = composerLayoutForWidth(width);
|
|
983
|
+
const line = '─'.repeat(lineWidth);
|
|
980
984
|
const separatorColor = sessionColor === undefined ? undefined : palette.sessionColors[sessionColor];
|
|
981
|
-
const footerWidth = Math.min(100, width);
|
|
982
985
|
const footerMode = `⏵⏵ ${permissionLabel(display.permissionMode)}`;
|
|
983
986
|
const footerCompactMode = `⏵⏵ ${compactPermissionLabel(display.permissionMode)}`;
|
|
984
987
|
const footerLeft = composerFooterLeft(footerWidth, busy, hasThinking, thinkingExpanded, footerMode, footerCompactMode);
|
|
@@ -995,7 +998,7 @@ export function Composer({ input, cursor, busy, clipboardBusy = false, status, d
|
|
|
995
998
|
? 'Enter a shell command'
|
|
996
999
|
: 'Try "review this project"' }))] })), _jsx(Text, { ...(separatorColor === undefined
|
|
997
1000
|
? { dimColor: true }
|
|
998
|
-
: { color: separatorColor }), children: line }), shortcutsVisible ? (_jsx(ShortcutHelp, { width: width })) : (_jsx(Box, { width: footerWidth, children: _jsx(Text, { wrap: "truncate", children: shellMode ? (_jsx(Text, { dimColor: true, children: "! for bash mode" })) : footerMessage ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [footerMessageLeft, " \u00B7 "] }), footerMessage.isError ? (_jsx(Text, { color: palette.error, children: footerMessage.text })) : (_jsx(Text, { dimColor: true, children: footerMessage.text }))] })) : prStatus ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [footerLeft, " \u00B7 "] }), _jsx(Text, { dimColor: true, children: prStatus })] })) : turnDuration ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [footerLeft, " \u00B7 "] }), _jsxs(Text, { dimColor: true, children: ["Cooked for ", turnDuration] })] })) : display.effort ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: footerLeft }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsxs(Text, { color: palette.accent, children: ["\u25CF ", display.effort] }),
|
|
1001
|
+
: { color: separatorColor }), children: line }), shortcutsVisible ? (_jsx(ShortcutHelp, { width: width })) : (_jsx(Box, { width: footerWidth, children: _jsx(Text, { wrap: "truncate", children: shellMode ? (_jsx(Text, { dimColor: true, children: "! for bash mode" })) : footerMessage ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [footerMessageLeft, " \u00B7 "] }), footerMessage.isError ? (_jsx(Text, { color: palette.error, children: footerMessage.text })) : (_jsx(Text, { dimColor: true, children: footerMessage.text }))] })) : prStatus ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [footerLeft, " \u00B7 "] }), _jsx(Text, { dimColor: true, children: prStatus })] })) : turnDuration ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [footerLeft, " \u00B7 "] }), _jsxs(Text, { dimColor: true, children: ["Cooked for ", turnDuration] })] })) : display.effort ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: footerLeft }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsxs(Text, { color: palette.accent, children: ["\u25CF ", display.effort] }), showEditorHint ? (_jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", editorMode === 'vim' ? 'vim' : '/effort'] })) : null] })) : (_jsx(Text, { dimColor: true, children: footerLeft })) }) }))] }));
|
|
999
1002
|
}
|
|
1000
1003
|
export function DialogFrame({ title, children, screenReader, }) {
|
|
1001
1004
|
const palette = useTuiPalette();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ComposerEditorState } from './composer-editor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Projection of an ink useInput key event that the pure composer router
|
|
4
|
+
* consumes. It deliberately omits terminal/resolution concerns so the
|
|
5
|
+
* router can never submit, exit, or resolve a modal.
|
|
6
|
+
*/
|
|
7
|
+
export interface ComposerKeyProjection {
|
|
8
|
+
value: string;
|
|
9
|
+
left: boolean;
|
|
10
|
+
right: boolean;
|
|
11
|
+
backspace: boolean;
|
|
12
|
+
delete: boolean;
|
|
13
|
+
ctrl: boolean;
|
|
14
|
+
meta: boolean;
|
|
15
|
+
escape: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type ComposerKeyTransition = {
|
|
18
|
+
kind: 'edit';
|
|
19
|
+
editor: ComposerEditorState;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'cancel';
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'noop';
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Pure normal-composer key transition router. Recognized editing input maps
|
|
27
|
+
* onto the existing composer-editor/composer-images primitives; Escape maps to
|
|
28
|
+
* a cancel result without mutating state; everything else is a noop.
|
|
29
|
+
*/
|
|
30
|
+
export declare function routeComposerKey(editor: ComposerEditorState, key: ComposerKeyProjection): ComposerKeyTransition;
|
|
31
|
+
//# sourceMappingURL=composer-key-router.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createComposerEditor, deleteComposerToEnd, deleteComposerToStart, deleteComposerWordBackward, insertComposerText, moveComposerCursorByWord, } from './composer-editor.js';
|
|
2
|
+
import { deleteComposerImageBackward, deleteComposerImageForward, moveComposerCursorAcrossImages, } from './composer-images.js';
|
|
3
|
+
/**
|
|
4
|
+
* Pure normal-composer key transition router. Recognized editing input maps
|
|
5
|
+
* onto the existing composer-editor/composer-images primitives; Escape maps to
|
|
6
|
+
* a cancel result without mutating state; everything else is a noop.
|
|
7
|
+
*/
|
|
8
|
+
export function routeComposerKey(editor, key) {
|
|
9
|
+
const state = createComposerEditor(editor.text, editor.cursor);
|
|
10
|
+
const lower = key.value.toLowerCase();
|
|
11
|
+
const controlKey = (letter) => (key.ctrl && lower === letter) ||
|
|
12
|
+
key.value === String.fromCharCode(letter.charCodeAt(0) - 96);
|
|
13
|
+
const printable = key.value.length > 0 &&
|
|
14
|
+
[...key.value].every((character) => {
|
|
15
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
16
|
+
return codePoint >= 32 && codePoint !== 127;
|
|
17
|
+
});
|
|
18
|
+
if (key.escape)
|
|
19
|
+
return { kind: 'cancel' };
|
|
20
|
+
if (key.left) {
|
|
21
|
+
return {
|
|
22
|
+
kind: 'edit',
|
|
23
|
+
editor: key.meta
|
|
24
|
+
? moveComposerCursorByWord(state, 'backward')
|
|
25
|
+
: moveComposerCursorAcrossImages(state, -1),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (key.right) {
|
|
29
|
+
return {
|
|
30
|
+
kind: 'edit',
|
|
31
|
+
editor: key.meta
|
|
32
|
+
? moveComposerCursorByWord(state, 'forward')
|
|
33
|
+
: moveComposerCursorAcrossImages(state, 1),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (controlKey('a')) {
|
|
37
|
+
return { kind: 'edit', editor: createComposerEditor(state.text, 0) };
|
|
38
|
+
}
|
|
39
|
+
if (controlKey('e')) {
|
|
40
|
+
return { kind: 'edit', editor: createComposerEditor(state.text) };
|
|
41
|
+
}
|
|
42
|
+
if (controlKey('b')) {
|
|
43
|
+
return { kind: 'edit', editor: moveComposerCursorAcrossImages(state, -1) };
|
|
44
|
+
}
|
|
45
|
+
if (controlKey('f')) {
|
|
46
|
+
return { kind: 'edit', editor: moveComposerCursorAcrossImages(state, 1) };
|
|
47
|
+
}
|
|
48
|
+
if (controlKey('w')) {
|
|
49
|
+
return { kind: 'edit', editor: deleteComposerWordBackward(state) };
|
|
50
|
+
}
|
|
51
|
+
if (controlKey('u')) {
|
|
52
|
+
return { kind: 'edit', editor: deleteComposerToStart(state) };
|
|
53
|
+
}
|
|
54
|
+
if (controlKey('k')) {
|
|
55
|
+
return { kind: 'edit', editor: deleteComposerToEnd(state) };
|
|
56
|
+
}
|
|
57
|
+
if (key.backspace) {
|
|
58
|
+
return { kind: 'edit', editor: deleteComposerImageBackward(state) };
|
|
59
|
+
}
|
|
60
|
+
if (key.delete) {
|
|
61
|
+
return { kind: 'edit', editor: deleteComposerImageForward(state) };
|
|
62
|
+
}
|
|
63
|
+
if (!key.ctrl && !key.meta && key.value && printable) {
|
|
64
|
+
return { kind: 'edit', editor: insertComposerText(state, key.value) };
|
|
65
|
+
}
|
|
66
|
+
return { kind: 'noop' };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=composer-key-router.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const MIN_COMPOSER_WIDTH = 12;
|
|
2
|
+
const MAX_COMPOSER_WIDTH = 100;
|
|
3
|
+
export function composerLayoutForWidth(width) {
|
|
4
|
+
const usableWidth = Number.isFinite(width)
|
|
5
|
+
? Math.min(Math.max(width, MIN_COMPOSER_WIDTH), MAX_COMPOSER_WIDTH)
|
|
6
|
+
: MIN_COMPOSER_WIDTH;
|
|
7
|
+
return {
|
|
8
|
+
lineWidth: usableWidth,
|
|
9
|
+
footerWidth: usableWidth,
|
|
10
|
+
showEditorHint: usableWidth >= MAX_COMPOSER_WIDTH,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=composer-layout.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TuiRendererMode } from './tui-view-model.js';
|
|
2
|
+
/** Stable terminal-frame policy for the fullscreen renderer. */
|
|
3
|
+
export interface FullscreenInkRenderOptions {
|
|
4
|
+
incrementalRendering: boolean;
|
|
5
|
+
alternateScreen: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The Ink render options for an interactive session. Fullscreen uses a stable
|
|
9
|
+
* terminal-frame policy: incremental rendering is disabled (every frame is a
|
|
10
|
+
* full redraw that erases the previous viewport) while classic and screen-reader
|
|
11
|
+
* modes keep their current behavior.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fullscreenInkRenderOptions(currentRenderer: TuiRendererMode, axScreenReader: boolean | undefined): FullscreenInkRenderOptions;
|
|
14
|
+
//# sourceMappingURL=fullscreen-renderer.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Ink render options for an interactive session. Fullscreen uses a stable
|
|
3
|
+
* terminal-frame policy: incremental rendering is disabled (every frame is a
|
|
4
|
+
* full redraw that erases the previous viewport) while classic and screen-reader
|
|
5
|
+
* modes keep their current behavior.
|
|
6
|
+
*/
|
|
7
|
+
export function fullscreenInkRenderOptions(currentRenderer, axScreenReader) {
|
|
8
|
+
return {
|
|
9
|
+
incrementalRendering: currentRenderer !== 'fullscreen' && !axScreenReader,
|
|
10
|
+
alternateScreen: currentRenderer === 'fullscreen',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=fullscreen-renderer.js.map
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* tail, and every published frame is the exact concatenation of every delta
|
|
9
9
|
* received since the previous frame.
|
|
10
10
|
*
|
|
11
|
+
* Pending deltas are buffered as chunks and concatenated exactly once per
|
|
12
|
+
* published frame, so appending a delta stores the delta alone (amortized
|
|
13
|
+
* O(1)) instead of re-concatenating the full accumulated string.
|
|
14
|
+
*
|
|
11
15
|
* `flush()` publishes any pending deltas immediately and is used at lifecycle
|
|
12
16
|
* boundaries (thinking-stop, tool-call/result, permission/dialog transitions,
|
|
13
17
|
* turn completion/cancellation) so the transcript boundary state is always
|
|
@@ -71,6 +75,8 @@ export declare class StreamingFrameBuffer {
|
|
|
71
75
|
dispose(): void;
|
|
72
76
|
private scheduleFrame;
|
|
73
77
|
private cancelScheduledFrame;
|
|
78
|
+
private isTextResetRedundant;
|
|
79
|
+
private isThinkingResetRedundant;
|
|
74
80
|
private publishPending;
|
|
75
81
|
}
|
|
76
82
|
//# sourceMappingURL=streaming-frame-buffer.d.ts.map
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* tail, and every published frame is the exact concatenation of every delta
|
|
9
9
|
* received since the previous frame.
|
|
10
10
|
*
|
|
11
|
+
* Pending deltas are buffered as chunks and concatenated exactly once per
|
|
12
|
+
* published frame, so appending a delta stores the delta alone (amortized
|
|
13
|
+
* O(1)) instead of re-concatenating the full accumulated string.
|
|
14
|
+
*
|
|
11
15
|
* `flush()` publishes any pending deltas immediately and is used at lifecycle
|
|
12
16
|
* boundaries (thinking-stop, tool-call/result, permission/dialog transitions,
|
|
13
17
|
* turn completion/cancellation) so the transcript boundary state is always
|
|
@@ -45,11 +49,15 @@ export class StreamingFrameBuffer {
|
|
|
45
49
|
}
|
|
46
50
|
/** Full effective text, including any deltas not yet published. */
|
|
47
51
|
get text() {
|
|
48
|
-
return this.pendingText
|
|
52
|
+
return this.pendingText === null
|
|
53
|
+
? this.committedText
|
|
54
|
+
: this.pendingText.base + this.pendingText.chunks.join('');
|
|
49
55
|
}
|
|
50
56
|
/** Full effective thinking, including any deltas not yet published. */
|
|
51
57
|
get thinking() {
|
|
52
|
-
return this.pendingThinking
|
|
58
|
+
return this.pendingThinking === null
|
|
59
|
+
? this.committedThinking
|
|
60
|
+
: this.pendingThinking.base + this.pendingThinking.chunks.join('');
|
|
53
61
|
}
|
|
54
62
|
get hasPending() {
|
|
55
63
|
return this.pendingText !== null || this.pendingThinking !== null;
|
|
@@ -61,37 +69,38 @@ export class StreamingFrameBuffer {
|
|
|
61
69
|
appendText(delta) {
|
|
62
70
|
if (this.disposed)
|
|
63
71
|
return;
|
|
64
|
-
|
|
72
|
+
if (this.pendingText === null) {
|
|
73
|
+
this.pendingText = { base: this.committedText, chunks: [] };
|
|
74
|
+
}
|
|
75
|
+
this.pendingText.chunks.push(delta);
|
|
65
76
|
this.scheduleFrame();
|
|
66
77
|
}
|
|
67
78
|
/** Append a thinking delta and schedule a frame publish. */
|
|
68
79
|
appendThinking(delta) {
|
|
69
80
|
if (this.disposed)
|
|
70
81
|
return;
|
|
71
|
-
this.pendingThinking
|
|
72
|
-
|
|
82
|
+
if (this.pendingThinking === null) {
|
|
83
|
+
this.pendingThinking = { base: this.committedThinking, chunks: [] };
|
|
84
|
+
}
|
|
85
|
+
this.pendingThinking.chunks.push(delta);
|
|
73
86
|
this.scheduleFrame();
|
|
74
87
|
}
|
|
75
88
|
/** Discard pending text and clear the active text on the next frame. */
|
|
76
89
|
resetText() {
|
|
77
90
|
if (this.disposed)
|
|
78
91
|
return;
|
|
79
|
-
if (this.
|
|
80
|
-
(this.pendingText === null && this.committedText === '')) {
|
|
92
|
+
if (this.isTextResetRedundant())
|
|
81
93
|
return;
|
|
82
|
-
}
|
|
83
|
-
this.pendingText = '';
|
|
94
|
+
this.pendingText = { base: '', chunks: [] };
|
|
84
95
|
this.scheduleFrame();
|
|
85
96
|
}
|
|
86
97
|
/** Discard pending thinking and clear the active thinking on the next frame. */
|
|
87
98
|
resetThinking() {
|
|
88
99
|
if (this.disposed)
|
|
89
100
|
return;
|
|
90
|
-
if (this.
|
|
91
|
-
(this.pendingThinking === null && this.committedThinking === '')) {
|
|
101
|
+
if (this.isThinkingResetRedundant())
|
|
92
102
|
return;
|
|
93
|
-
}
|
|
94
|
-
this.pendingThinking = '';
|
|
103
|
+
this.pendingThinking = { base: '', chunks: [] };
|
|
95
104
|
this.scheduleFrame();
|
|
96
105
|
}
|
|
97
106
|
/** Publish every pending delta immediately, canceling any scheduled frame. */
|
|
@@ -127,15 +136,29 @@ export class StreamingFrameBuffer {
|
|
|
127
136
|
this.scheduled = false;
|
|
128
137
|
}
|
|
129
138
|
}
|
|
139
|
+
isTextResetRedundant() {
|
|
140
|
+
if (this.pendingText === null)
|
|
141
|
+
return this.committedText === '';
|
|
142
|
+
return (this.pendingText.base === '' && this.pendingText.chunks.join('') === '');
|
|
143
|
+
}
|
|
144
|
+
isThinkingResetRedundant() {
|
|
145
|
+
if (this.pendingThinking === null)
|
|
146
|
+
return this.committedThinking === '';
|
|
147
|
+
return (this.pendingThinking.base === '' &&
|
|
148
|
+
this.pendingThinking.chunks.join('') === '');
|
|
149
|
+
}
|
|
130
150
|
publishPending() {
|
|
131
151
|
if (this.disposed)
|
|
132
152
|
return;
|
|
133
153
|
if (this.pendingText === null && this.pendingThinking === null)
|
|
134
154
|
return;
|
|
135
|
-
if (this.pendingText !== null)
|
|
136
|
-
this.committedText =
|
|
155
|
+
if (this.pendingText !== null) {
|
|
156
|
+
this.committedText =
|
|
157
|
+
this.pendingText.base + this.pendingText.chunks.join('');
|
|
158
|
+
}
|
|
137
159
|
if (this.pendingThinking !== null) {
|
|
138
|
-
this.committedThinking =
|
|
160
|
+
this.committedThinking =
|
|
161
|
+
this.pendingThinking.base + this.pendingThinking.chunks.join('');
|
|
139
162
|
}
|
|
140
163
|
this.pendingText = null;
|
|
141
164
|
this.pendingThinking = null;
|
|
@@ -8,6 +8,7 @@ import { extractPermissionRules, permissionRuleValueToString, shellPermissionSug
|
|
|
8
8
|
import { redactSensitiveText } from '../../platform/sensitive-data.js';
|
|
9
9
|
import { resolveDataPlaneRoot, } from '../../persistence/data-plane.js';
|
|
10
10
|
import { composerEditorSegments, } from './composer-editor.js';
|
|
11
|
+
import { useTuiPalette } from './theme.js';
|
|
11
12
|
function stringInput(call, name) {
|
|
12
13
|
const value = call.input[name];
|
|
13
14
|
return typeof value === 'string' ? value : undefined;
|
|
@@ -369,9 +370,10 @@ export function projectTuiToolPermission(call, cwd, sensitiveValues, decision, d
|
|
|
369
370
|
});
|
|
370
371
|
}
|
|
371
372
|
export function ToolPermissionDialog({ model, selection, feedbackMode, feedback, ruleEditor, screenReader, }) {
|
|
372
|
-
|
|
373
|
+
const palette = useTuiPalette();
|
|
374
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: screenReader ? undefined : 'round', borderColor: palette.warning, paddingX: screenReader ? 0 : 1, marginTop: 1, children: [_jsx(Text, { bold: true, color: palette.warning, children: model.title }), model.subtitle ? _jsx(Text, { dimColor: true, children: model.subtitle }) : null, _jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [model.detail.map((line, index) => {
|
|
373
375
|
const content = `${line.prefix ? `${line.prefix} ` : ''}${line.text}`;
|
|
374
|
-
return line.prefix ? (_jsx(Text, { color: line.prefix === '+' ?
|
|
376
|
+
return line.prefix ? (_jsx(Text, { color: line.prefix === '+' ? palette.success : palette.error, children: content }, `${index}-${line.prefix}`)) : (_jsx(Text, { children: content }, `${index}-`));
|
|
375
377
|
}), model.description ? _jsx(Text, { dimColor: true, children: model.description }) : null] }), model.explanation ? _jsx(Text, { dimColor: true, children: model.explanation }) : null, _jsx(Text, { children: model.question }), model.options.map((option, index) => {
|
|
376
378
|
const selected = selection === index;
|
|
377
379
|
const editor = option.editableRule
|
|
@@ -381,7 +383,7 @@ export function ToolPermissionDialog({ model, selection, feedbackMode, feedback,
|
|
|
381
383
|
})
|
|
382
384
|
: null;
|
|
383
385
|
const segments = editor ? composerEditorSegments(editor) : null;
|
|
384
|
-
return (_jsxs(Text, { bold: selected, children: [selected ? (screenReader ? 'Selected: ' : '❯ ') : ' ', index + 1, ". ", option.label, segments ? (screenReader || !selected ? (`: ${editor?.text ?? ''}`) : (_jsxs(Text, { children: [": ", segments.before, _jsx(Text, { inverse: true, children: segments.current ?? ' ' }), segments.after] }))) : null] }, `${option.action}-${index}`));
|
|
386
|
+
return (_jsxs(Text, { bold: selected, ...(selected ? { color: palette.brand } : {}), children: [selected ? (screenReader ? 'Selected: ' : '❯ ') : ' ', index + 1, ". ", option.label, segments ? (screenReader || !selected ? (`: ${editor?.text ?? ''}`) : (_jsxs(Text, { children: [": ", segments.before, _jsx(Text, { inverse: true, children: segments.current ?? ' ' }), segments.after] }))) : null] }, `${option.action}-${index}`));
|
|
385
387
|
}), feedbackMode ? (_jsxs(Text, { children: ["\u203A", ' ', feedback ||
|
|
386
388
|
(model.options[selection]?.action === 'deny'
|
|
387
389
|
? 'tell Praxis what to do differently'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { TranscriptItem } from './claude-style.js';
|
|
2
|
+
export type TuiRendererMode = 'default' | 'fullscreen';
|
|
3
|
+
/**
|
|
4
|
+
* Inputs for the pure TUI view projection. `history` is the live transcript
|
|
5
|
+
* that grows while the session runs; `initialHistory` is the transcript loaded
|
|
6
|
+
* before rendering and decides the resumed/fresh identity. `resume` is true
|
|
7
|
+
* only when the session was opened through a resume option.
|
|
8
|
+
*/
|
|
9
|
+
export interface TuiViewInput {
|
|
10
|
+
initialHistory: readonly TranscriptItem[];
|
|
11
|
+
history: readonly TranscriptItem[];
|
|
12
|
+
resume: boolean;
|
|
13
|
+
fixedViewport: boolean;
|
|
14
|
+
screenReader: boolean;
|
|
15
|
+
rows: number | undefined;
|
|
16
|
+
width: number;
|
|
17
|
+
scrollOffset: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TuiViewModel {
|
|
20
|
+
projectedHistory: readonly TranscriptItem[];
|
|
21
|
+
resumed: boolean;
|
|
22
|
+
freshSession: boolean;
|
|
23
|
+
hasConversationHistory: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pure, deterministic TUI projection. It classifies the session identity
|
|
27
|
+
* (fresh/resumed/started) and projects the rendered transcript exactly as the
|
|
28
|
+
* fullscreen renderer needs it, preserving TranscriptItem identity and order
|
|
29
|
+
* and the fullscreen suffix/window behavior. It never writes to transcripts or
|
|
30
|
+
* alters runtime events.
|
|
31
|
+
*/
|
|
32
|
+
export declare function projectTuiView(input: TuiViewInput): TuiViewModel;
|
|
33
|
+
export interface TuiRendererInput {
|
|
34
|
+
configured: TuiRendererMode;
|
|
35
|
+
explicitlyConfigured: boolean;
|
|
36
|
+
interactiveTty: boolean;
|
|
37
|
+
screenReader: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolves the renderer for an interactive session. Fullscreen is the default
|
|
41
|
+
* for interactive TTY execution; classic remains the fallback for screen-reader
|
|
42
|
+
* and non-interactive paths, and an explicit renderer configuration is always
|
|
43
|
+
* honored over the default.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveTuiRenderer(input: TuiRendererInput): TuiRendererMode;
|
|
46
|
+
//# sourceMappingURL=tui-view-model.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FULLSCREEN_TRANSCRIPT_RESERVED_ROWS, projectTranscriptTail, projectTranscriptWindow, } from './transcript-viewport.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure, deterministic TUI projection. It classifies the session identity
|
|
4
|
+
* (fresh/resumed/started) and projects the rendered transcript exactly as the
|
|
5
|
+
* fullscreen renderer needs it, preserving TranscriptItem identity and order
|
|
6
|
+
* and the fullscreen suffix/window behavior. It never writes to transcripts or
|
|
7
|
+
* alters runtime events.
|
|
8
|
+
*/
|
|
9
|
+
export function projectTuiView(input) {
|
|
10
|
+
// Startup diagnostics are useful before the first prompt, but they are not
|
|
11
|
+
// conversation history and must not suppress the new-session welcome panel.
|
|
12
|
+
// Only real user/assistant transcript entries start a conversation; every
|
|
13
|
+
// other kind (thinking, context, tool, shell, notices, results, and so on)
|
|
14
|
+
// is operational bookkeeping that must not hide the fresh-session welcome.
|
|
15
|
+
const isRealConversation = (item) => item.kind === 'user' || item.kind === 'assistant';
|
|
16
|
+
// The original loaded transcript decides whether the session was resumed,
|
|
17
|
+
// separately from the live history that grows while the session runs.
|
|
18
|
+
const resumed = input.resume && input.initialHistory.some(isRealConversation);
|
|
19
|
+
const hasConversationHistory = input.history.some(isRealConversation);
|
|
20
|
+
// A session is resumed only when it was opened through `resume` and the
|
|
21
|
+
// original transcript already contained real conversation content. Supplying
|
|
22
|
+
// a session ID alone with an empty transcript keeps the session fresh, so the
|
|
23
|
+
// full welcome panel renders and the compact identity stays hidden until real
|
|
24
|
+
// conversation content appears.
|
|
25
|
+
const freshSession = !resumed && !hasConversationHistory;
|
|
26
|
+
// Fullscreen projects only the newest transcript tail that fits the fixed
|
|
27
|
+
// viewport, leaving the composer/status chrome intact and keeping the active
|
|
28
|
+
// stream visible. Classic and screen-reader modes always render the full
|
|
29
|
+
// history exactly as before.
|
|
30
|
+
const projectedHistory = input.fixedViewport && !input.screenReader
|
|
31
|
+
? input.scrollOffset > 0
|
|
32
|
+
? projectTranscriptWindow(input.history, Math.max(1, (input.rows ?? 0) - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS), input.width, input.scrollOffset)
|
|
33
|
+
: projectTranscriptTail(input.history, Math.max(1, (input.rows ?? 0) - FULLSCREEN_TRANSCRIPT_RESERVED_ROWS), input.width)
|
|
34
|
+
: input.history;
|
|
35
|
+
return { projectedHistory, resumed, freshSession, hasConversationHistory };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the renderer for an interactive session. Fullscreen is the default
|
|
39
|
+
* for interactive TTY execution; classic remains the fallback for screen-reader
|
|
40
|
+
* and non-interactive paths, and an explicit renderer configuration is always
|
|
41
|
+
* honored over the default.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveTuiRenderer(input) {
|
|
44
|
+
if (input.screenReader)
|
|
45
|
+
return 'default';
|
|
46
|
+
if (!input.interactiveTty)
|
|
47
|
+
return 'default';
|
|
48
|
+
if (input.explicitlyConfigured)
|
|
49
|
+
return input.configured;
|
|
50
|
+
return 'fullscreen';
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=tui-view-model.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "praxis-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "Local-first, single-user general agent for the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wuqisen",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"test:docs": "node scripts/verify-docs.mjs",
|
|
45
45
|
"test:compat": "npm run build && node scripts/verify-claude-compatibility.mjs && node scripts/verify-claude-advanced-fixtures.mjs",
|
|
46
46
|
"test:compat:all": "npm run build && node scripts/verify-compatibility-matrix.mjs",
|
|
47
|
+
"test:tui:pty": "npm run build && node scripts/verify-fullscreen-rendering.mjs",
|
|
47
48
|
"test:cli-surface-compat": "npm run build && node scripts/verify-cli-surface-parity.mjs",
|
|
48
49
|
"test:cli-controls-compat": "npm run build && node scripts/verify-cli-controls.mjs",
|
|
49
50
|
"test:conditional-compat": "npm run build && node scripts/verify-conditional-rules.mjs",
|