praxis-agent 0.58.0 → 0.59.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 +5 -2
- package/dist/cli/interactive.d.ts +3 -2
- package/dist/cli/interactive.js +15 -6
- package/dist/cli/tui/diff-surface-model.js +2 -2
- package/dist/cli/tui/git-diff.d.ts +5 -0
- package/dist/cli/tui/git-diff.js +276 -89
- package/dist/cli/tui/transcript-presentation.d.ts +15 -1
- package/dist/cli/tui/transcript-presentation.js +45 -0
- package/dist/cli/tui/transcript-window-model.d.ts +7 -0
- package/dist/cli/tui/transcript-window-model.js +66 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
124
124
|
- **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
|
|
125
125
|
`❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
|
|
126
126
|
composer grammar, compact stable tool rows, responsive density,
|
|
127
|
-
terminal-native background, and a minimal composer/status row.
|
|
127
|
+
terminal-native background, and a minimal composer/status row. Successful
|
|
128
|
+
background Bash completion bursts collapse in normal reading, while
|
|
129
|
+
failed/stopped notifications remain detailed. Interactive
|
|
128
130
|
surfaces share the same presentation across terminals, with English
|
|
129
131
|
permission/configuration choices and a taught `❯` / Up/Down / Enter / Esc
|
|
130
132
|
interaction grammar. While a regular turn is active, the composer remains
|
|
@@ -167,7 +169,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
167
169
|
`Ctrl+V` text/image clipboard paste, `Ctrl+Z` shell suspension and `fg`
|
|
168
170
|
recovery, permission-gated provider-free `!` shell turns that persist shell
|
|
169
171
|
input/output for later ordinary prompts without creating an assistant turn,
|
|
170
|
-
navigable current
|
|
172
|
+
navigable session-start/current-per-turn Git diff views with bounded per-file
|
|
173
|
+
patches and readable binary/conflict/transient-path notes, semantic plan/question decision panels with complete
|
|
171
174
|
screen-reader actions, semantic screen projection across selectable surfaces,
|
|
172
175
|
deterministic resize-aware URL/form elicitation rendering, and measured
|
|
173
176
|
context budgets; print mode,
|
|
@@ -9,7 +9,7 @@ import { type ClaudePermissionMode } from '../permissions/claude-permission-reso
|
|
|
9
9
|
import { type TranscriptItem, type TuiDisplayMetadata } from './tui/claude-style.js';
|
|
10
10
|
import { type TuiMemoryFiles } from './tui/memory-files.js';
|
|
11
11
|
import { type TuiHistoryChange } from './tui/tui-view-model.js';
|
|
12
|
-
import { type TuiDiffSnapshot } from './tui/git-diff.js';
|
|
12
|
+
import { type TuiGitDiffSession, type TuiDiffSnapshot } from './tui/git-diff.js';
|
|
13
13
|
import { type TuiPermissionBehavior, type TuiPermissionRule } from './tui/permission-settings.js';
|
|
14
14
|
import { type RecentlyDeniedStore } from './tui/recently-denied.js';
|
|
15
15
|
import { type AgentColorSelection } from '../core/agent-color.js';
|
|
@@ -149,6 +149,7 @@ interface InteractiveAppProps {
|
|
|
149
149
|
allowDangerouslySkipPermissions?: boolean;
|
|
150
150
|
additionalDirectories?: readonly string[];
|
|
151
151
|
diffLoader?: () => Promise<TuiDiffSnapshot>;
|
|
152
|
+
gitDiffSession?: TuiGitDiffSession;
|
|
152
153
|
doctorLoader?: (onProgress?: DoctorProgressListener) => Promise<DoctorReport>;
|
|
153
154
|
fileLoader?: () => Promise<readonly TuiFileEntry[]>;
|
|
154
155
|
externalEditor?: (prompt: string, options: TuiEditorOptions) => Promise<TuiEditorResult>;
|
|
@@ -209,7 +210,7 @@ export interface InteractiveHistoryState {
|
|
|
209
210
|
}
|
|
210
211
|
/** Advances the React-owned transcript plus its exact local mutation fact. */
|
|
211
212
|
export declare function advanceInteractiveHistoryState(current: InteractiveHistoryState, items: TranscriptItem[], changedFrom: number): InteractiveHistoryState;
|
|
212
|
-
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;
|
|
213
|
+
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, gitDiffSession: suppliedGitDiffSession, 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;
|
|
213
214
|
export declare function runInteractive(options: {
|
|
214
215
|
dataPlane?: DataPlane;
|
|
215
216
|
configRoot?: string;
|
package/dist/cli/interactive.js
CHANGED
|
@@ -33,7 +33,7 @@ import { QuietInkFrame } from './tui/quiet-frame-adapter.js';
|
|
|
33
33
|
import { projectQuietScreenFrame, } from './tui/quiet-screen-projector.js';
|
|
34
34
|
import { projectTuiHelpSurface } from './tui/help-surface-model.js';
|
|
35
35
|
import { projectTuiHooksSurface, } from './tui/hooks-surface-model.js';
|
|
36
|
-
import {
|
|
36
|
+
import { createTuiGitDiffSession, } from './tui/git-diff.js';
|
|
37
37
|
import { projectTuiDiffSurface, } from './tui/diff-surface-model.js';
|
|
38
38
|
import { filterTuiSessionPickerChoices, projectTuiSessionPicker, tuiSessionPickerChoiceId, } from './tui/session-picker-model.js';
|
|
39
39
|
import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule, } from './tui/permission-settings.js';
|
|
@@ -257,7 +257,7 @@ export function advanceInteractiveHistoryState(current, items, changedFrom) {
|
|
|
257
257
|
: createTuiHistoryChange(revision, changedFrom, items, current.items),
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
|
-
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, }) {
|
|
260
|
+
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, gitDiffSession: suppliedGitDiffSession, 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, }) {
|
|
261
261
|
const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
|
|
262
262
|
const settingsDirectory = '.praxis';
|
|
263
263
|
const keybindingsRoot = useMemo(() => resolve(suppliedConfigRoot ?? keybindingsConfigRoot ?? resolveDataPlaneRoot()), [dataPlane, keybindingsConfigRoot, suppliedConfigRoot]);
|
|
@@ -278,7 +278,10 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
278
278
|
const [runtimeCwd, setRuntimeCwd] = useState(display.cwd);
|
|
279
279
|
const runtimeCwdRef = useRef(display.cwd);
|
|
280
280
|
const runtimeGitignoreRef = useRef(true);
|
|
281
|
-
const
|
|
281
|
+
const gitDiffSession = useMemo(() => suppliedGitDiffSession ?? createTuiGitDiffSession(display.cwd, signal), [display.cwd, signal, suppliedGitDiffSession]);
|
|
282
|
+
const loadDiffSnapshot = useMemo(() => diffLoader
|
|
283
|
+
? () => diffLoader()
|
|
284
|
+
: (signal) => gitDiffSession.load(runtimeCwdRef.current, signal), [diffLoader, gitDiffSession]);
|
|
282
285
|
const loadDoctorReport = useMemo(() => doctorLoader ??
|
|
283
286
|
(async (onProgress) => {
|
|
284
287
|
const runtimeSettings = await loadRuntimeSettings(configTarget);
|
|
@@ -1959,6 +1962,10 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1959
1962
|
append({
|
|
1960
1963
|
kind: event.status === 'failed' ? 'warning' : 'notice',
|
|
1961
1964
|
text: `Task ${event.status} · ${event.summary}`,
|
|
1965
|
+
taskNotification: {
|
|
1966
|
+
taskId: event.taskId,
|
|
1967
|
+
status: event.status,
|
|
1968
|
+
},
|
|
1962
1969
|
});
|
|
1963
1970
|
break;
|
|
1964
1971
|
case 'compact-boundary':
|
|
@@ -2817,6 +2824,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
2817
2824
|
setBusy(true);
|
|
2818
2825
|
setStatus('changing directory');
|
|
2819
2826
|
const cwd = await commands.changeCwd(sessionId ?? undefined, inspection.canonicalTarget, inspection.canonicalTarget);
|
|
2827
|
+
await gitDiffSession.prepare(cwd, signal);
|
|
2820
2828
|
activeSessionTrust.add(cwd);
|
|
2821
2829
|
runtimeCwdRef.current = cwd;
|
|
2822
2830
|
setRuntimeCwd(cwd);
|
|
@@ -3427,7 +3435,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3427
3435
|
}
|
|
3428
3436
|
if (turnMutatedFilesRef.current) {
|
|
3429
3437
|
try {
|
|
3430
|
-
const snapshot = await loadDiffSnapshot();
|
|
3438
|
+
const snapshot = await loadDiffSnapshot(signal);
|
|
3431
3439
|
setTurnDiffs((current) => [
|
|
3432
3440
|
...current.filter((source) => source.label !== `T${turnNumber}`),
|
|
3433
3441
|
{ label: `T${turnNumber}`, snapshot },
|
|
@@ -6679,7 +6687,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
6679
6687
|
setBusy(true);
|
|
6680
6688
|
setStatus('loading diff');
|
|
6681
6689
|
try {
|
|
6682
|
-
const current = await loadDiffSnapshot();
|
|
6690
|
+
const current = await loadDiffSnapshot(signal);
|
|
6683
6691
|
updateMenu({
|
|
6684
6692
|
kind: 'diff',
|
|
6685
6693
|
snapshots: [
|
|
@@ -7027,6 +7035,7 @@ export async function runInteractive(options) {
|
|
|
7027
7035
|
const signal = options.signal
|
|
7028
7036
|
? AbortSignal.any([options.signal, controller.signal])
|
|
7029
7037
|
: controller.signal;
|
|
7038
|
+
const gitDiffSession = createTuiGitDiffSession(options.display?.cwd ?? process.cwd(), signal);
|
|
7030
7039
|
let currentResume = options.resume;
|
|
7031
7040
|
let currentInitialPrompt = options.initialPrompt;
|
|
7032
7041
|
let initialAgentPromptResolved = false;
|
|
@@ -7145,7 +7154,7 @@ export async function runInteractive(options) {
|
|
|
7145
7154
|
const ansiRendererEnabled = currentRenderer === 'fullscreen' &&
|
|
7146
7155
|
interactiveTty &&
|
|
7147
7156
|
options.axScreenReader !== true;
|
|
7148
|
-
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
|
|
7157
|
+
const instance = render(_jsx(InteractiveApp, { dataPlane: dataPlane, configRoot: configRoot, statePath: statePath, factory: options.factory, gitDiffSession: gitDiffSession, initialSessions: initialSessions, slashCommands: initialSlashCommands, agents: initialAgents, initialHistory: history, ...(initialSessionColor === undefined ? {} : { initialSessionColor }), runtimeSettings: currentRuntimeSettings, runtimeSettingsTarget: runtimeSettingsTarget, ...(options.settingSources === undefined
|
|
7149
7158
|
? {}
|
|
7150
7159
|
: { settingSources: options.settingSources }), initialThemeSettings: initialThemeSettings, ...(initialThemeLoadError === undefined
|
|
7151
7160
|
? {}
|
|
@@ -46,7 +46,7 @@ export function projectTuiDiffSurface(input) {
|
|
|
46
46
|
if (!input.viewingFile || files.length === 0) {
|
|
47
47
|
return {
|
|
48
48
|
kind: 'diff',
|
|
49
|
-
title: '
|
|
49
|
+
title: 'Changes since session start',
|
|
50
50
|
sourceTabs,
|
|
51
51
|
currentSource: selectedSource
|
|
52
52
|
? { index: sourceIndex, label: source.label }
|
|
@@ -101,7 +101,7 @@ export function projectTuiDiffSurface(input) {
|
|
|
101
101
|
}));
|
|
102
102
|
return {
|
|
103
103
|
kind: 'diff',
|
|
104
|
-
title: '
|
|
104
|
+
title: 'Changes since session start',
|
|
105
105
|
sourceTabs,
|
|
106
106
|
currentSource: selectedSource
|
|
107
107
|
? { index: sourceIndex, label: source.label }
|
|
@@ -9,6 +9,11 @@ export interface TuiDiffSnapshot {
|
|
|
9
9
|
additions: number;
|
|
10
10
|
deletions: number;
|
|
11
11
|
}
|
|
12
|
+
export interface TuiGitDiffSession {
|
|
13
|
+
prepare(cwd: string, signal?: AbortSignal): Promise<void>;
|
|
14
|
+
load(cwd: string, signal?: AbortSignal): Promise<TuiDiffSnapshot>;
|
|
15
|
+
}
|
|
12
16
|
export declare function visiblePatchLines(patch: string): readonly string[];
|
|
17
|
+
export declare function createTuiGitDiffSession(initialCwd: string, sessionSignal?: AbortSignal): TuiGitDiffSession;
|
|
13
18
|
export declare function loadGitDiff(cwd: string): Promise<TuiDiffSnapshot>;
|
|
14
19
|
//# sourceMappingURL=git-diff.d.ts.map
|
package/dist/cli/tui/git-diff.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { BoundedProcessRunner } from '../../platform/bounded-process-runner.js';
|
|
4
|
+
function isOldFileHeader(line) {
|
|
5
|
+
return /^--- (?:a\/|"a\/|\/dev\/null$)/u.test(line);
|
|
6
|
+
}
|
|
7
|
+
function isNewFileHeader(line) {
|
|
8
|
+
return /^\+\+\+ (?:b\/|"b\/|\/dev\/null$)/u.test(line);
|
|
9
|
+
}
|
|
4
10
|
function changedLines(patch) {
|
|
5
11
|
let additions = 0;
|
|
6
12
|
let deletions = 0;
|
|
@@ -12,12 +18,6 @@ function changedLines(patch) {
|
|
|
12
18
|
}
|
|
13
19
|
return { additions, deletions };
|
|
14
20
|
}
|
|
15
|
-
function isOldFileHeader(line) {
|
|
16
|
-
return /^--- (?:a\/|"a\/|\/dev\/null$)/u.test(line);
|
|
17
|
-
}
|
|
18
|
-
function isNewFileHeader(line) {
|
|
19
|
-
return /^\+\+\+ (?:b\/|"b\/|\/dev\/null$)/u.test(line);
|
|
20
|
-
}
|
|
21
21
|
export function visiblePatchLines(patch) {
|
|
22
22
|
return patch
|
|
23
23
|
.split('\n')
|
|
@@ -28,95 +28,282 @@ export function visiblePatchLines(patch) {
|
|
|
28
28
|
!isNewFileHeader(line) &&
|
|
29
29
|
!line.startsWith('@@ '));
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
31
|
+
const CONTROL_LIMIT = 8 * 1024 * 1024;
|
|
32
|
+
const PATCH_LIMIT = 256 * 1024;
|
|
33
|
+
function makeRunner(root, limit = PATCH_LIMIT) {
|
|
34
|
+
return new BoundedProcessRunner({ cwd: root, maxOutputBytes: limit });
|
|
39
35
|
}
|
|
40
|
-
|
|
36
|
+
function note(path, text) {
|
|
37
|
+
return { path, additions: 0, deletions: 0, patch: text };
|
|
38
|
+
}
|
|
39
|
+
async function discover(cwd, signal) {
|
|
41
40
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
const process = makeRunner(cwd, CONTROL_LIMIT);
|
|
42
|
+
const rootPromise = process.run({
|
|
43
|
+
command: 'git',
|
|
44
|
+
args: ['-C', cwd, 'rev-parse', '--show-toplevel'],
|
|
45
|
+
timeoutMs: 5_000,
|
|
46
|
+
...(signal ? { signal } : {}),
|
|
47
|
+
});
|
|
48
|
+
const headPromise = process.run({
|
|
49
|
+
command: 'git',
|
|
50
|
+
args: ['-C', cwd, 'rev-parse', '--verify', '--quiet', 'HEAD'],
|
|
51
|
+
timeoutMs: 5_000,
|
|
52
|
+
...(signal ? { signal } : {}),
|
|
53
|
+
});
|
|
54
|
+
const [probe, head] = await Promise.all([rootPromise, headPromise]);
|
|
55
|
+
if (probe.code !== 0 ||
|
|
56
|
+
probe.timedOut ||
|
|
57
|
+
probe.truncated ||
|
|
58
|
+
!probe.stdout.trim())
|
|
59
|
+
throw new Error('Not a Git repository.');
|
|
60
|
+
const root = await realpath(resolve(probe.stdout.trim()));
|
|
61
|
+
if (head.timedOut || head.truncated)
|
|
62
|
+
throw new Error('Git HEAD inspection failed.');
|
|
63
|
+
if (head.code === 0 && head.stdout.trim())
|
|
64
|
+
return { root, oid: head.stdout.trim() };
|
|
65
|
+
if (head.code === 1 && !head.stdout.trim() && !head.stderr.trim())
|
|
66
|
+
return { root, oid: null };
|
|
67
|
+
throw new Error('Git HEAD inspection failed.');
|
|
55
68
|
}
|
|
56
69
|
catch (error) {
|
|
57
|
-
|
|
58
|
-
// `git diff --no-index` exits with code 1 when the inputs differ.
|
|
59
|
-
if (execError.code === 1) {
|
|
60
|
-
return execError.stdout ?? '';
|
|
61
|
-
}
|
|
62
|
-
throw error;
|
|
70
|
+
return { error: error instanceof Error ? error : new Error(String(error)) };
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
export function createTuiGitDiffSession(initialCwd, sessionSignal) {
|
|
74
|
+
const baselines = new Map();
|
|
75
|
+
const discoveries = new Map();
|
|
76
|
+
const initialKey = resolve(initialCwd);
|
|
77
|
+
const beginDiscovery = (key, signal) => {
|
|
78
|
+
let pending = discoveries.get(key);
|
|
79
|
+
if (!pending) {
|
|
80
|
+
pending = discover(key, signal).then((result) => {
|
|
81
|
+
if (!('error' in result) && !baselines.has(result.root)) {
|
|
82
|
+
baselines.set(result.root, Promise.resolve(result));
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
});
|
|
86
|
+
discoveries.set(key, pending);
|
|
87
|
+
}
|
|
88
|
+
return pending;
|
|
89
|
+
};
|
|
90
|
+
// Start both repository and HEAD probes before returning the session so a
|
|
91
|
+
// later commit cannot move the initial baseline while the TUI is mounting.
|
|
92
|
+
const initialReady = beginDiscovery(initialKey, sessionSignal).then(() => undefined);
|
|
93
|
+
const prepare = async (cwd, signal = sessionSignal) => {
|
|
94
|
+
const key = resolve(cwd);
|
|
95
|
+
if (key !== initialKey)
|
|
96
|
+
await initialReady;
|
|
97
|
+
await beginDiscovery(key, signal);
|
|
98
|
+
};
|
|
99
|
+
const load = async (cwd, signal) => {
|
|
100
|
+
await prepare(cwd, signal);
|
|
101
|
+
if (signal?.aborted)
|
|
102
|
+
throw new DOMException('Tool execution aborted', 'AbortError');
|
|
103
|
+
const discovery = await discoveries.get(resolve(cwd));
|
|
104
|
+
const captured = discovery && !('error' in discovery)
|
|
105
|
+
? await baselines.get(discovery.root)
|
|
106
|
+
: discovery;
|
|
107
|
+
if (!captured)
|
|
108
|
+
throw new Error('Git baseline unavailable.');
|
|
109
|
+
if ('error' in captured)
|
|
110
|
+
throw captured.error;
|
|
111
|
+
const { root, oid } = captured;
|
|
112
|
+
const git = makeRunner(root);
|
|
113
|
+
const control = makeRunner(root, CONTROL_LIMIT);
|
|
114
|
+
const runGit = (args) => git.run({
|
|
115
|
+
command: 'git',
|
|
116
|
+
args,
|
|
117
|
+
timeoutMs: 5_000,
|
|
118
|
+
...(signal ? { signal } : {}),
|
|
119
|
+
});
|
|
120
|
+
const list = async (args) => {
|
|
121
|
+
const result = await control.run({
|
|
122
|
+
command: 'git',
|
|
123
|
+
args,
|
|
124
|
+
timeoutMs: 5_000,
|
|
125
|
+
...(signal ? { signal } : {}),
|
|
126
|
+
});
|
|
127
|
+
if (result.code !== 0 || result.timedOut || result.truncated)
|
|
128
|
+
throw new Error('Git diff enumeration failed.');
|
|
129
|
+
return result.stdout.split('\0').filter(Boolean);
|
|
130
|
+
};
|
|
131
|
+
const untracked = await list([
|
|
73
132
|
'-C',
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
-
'--
|
|
77
|
-
'--
|
|
78
|
-
'
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
133
|
+
root,
|
|
134
|
+
'ls-files',
|
|
135
|
+
'--others',
|
|
136
|
+
'--exclude-standard',
|
|
137
|
+
'-z',
|
|
138
|
+
]);
|
|
139
|
+
const untrackedSet = new Set(untracked);
|
|
140
|
+
let paths;
|
|
141
|
+
if (oid === null) {
|
|
142
|
+
const cached = await list(['-C', root, 'ls-files', '--cached', '-z']);
|
|
143
|
+
const deleted = new Set(await list(['-C', root, 'ls-files', '--deleted', '-z']));
|
|
144
|
+
paths = [...new Set([...cached, ...untracked])]
|
|
145
|
+
.filter((path) => !deleted.has(path))
|
|
146
|
+
.sort();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
paths = [
|
|
150
|
+
...new Set([
|
|
151
|
+
...(await list([
|
|
152
|
+
'-C',
|
|
153
|
+
root,
|
|
154
|
+
'diff',
|
|
155
|
+
'--no-ext-diff',
|
|
156
|
+
'--no-renames',
|
|
157
|
+
'--name-only',
|
|
158
|
+
'-z',
|
|
159
|
+
oid,
|
|
160
|
+
'--',
|
|
161
|
+
])),
|
|
162
|
+
...(await list([
|
|
163
|
+
'-C',
|
|
164
|
+
root,
|
|
165
|
+
'diff',
|
|
166
|
+
'--cached',
|
|
167
|
+
'--no-ext-diff',
|
|
168
|
+
'--no-renames',
|
|
169
|
+
'--name-only',
|
|
170
|
+
'-z',
|
|
171
|
+
oid,
|
|
172
|
+
'--',
|
|
173
|
+
])),
|
|
174
|
+
...untracked,
|
|
175
|
+
]),
|
|
176
|
+
].sort();
|
|
177
|
+
}
|
|
178
|
+
const baselineOid = oid ?? '';
|
|
179
|
+
const files = [];
|
|
180
|
+
for (const path of paths) {
|
|
181
|
+
if (signal?.aborted)
|
|
182
|
+
throw new DOMException('Tool execution aborted', 'AbortError');
|
|
183
|
+
try {
|
|
184
|
+
const conflict = await runGit([
|
|
185
|
+
'-C',
|
|
186
|
+
root,
|
|
187
|
+
'ls-files',
|
|
188
|
+
'-u',
|
|
189
|
+
'--',
|
|
190
|
+
path,
|
|
191
|
+
]);
|
|
192
|
+
if (conflict.code !== 0 || conflict.timedOut || conflict.truncated) {
|
|
193
|
+
files.push(note(path, 'Diff unavailable; Git inspection failed.'));
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (conflict.stdout.trim()) {
|
|
197
|
+
files.push(note(path, 'Merge conflict; patch omitted.'));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const numstatArgs = oid === null || untrackedSet.has(path)
|
|
201
|
+
? [
|
|
202
|
+
'-C',
|
|
203
|
+
root,
|
|
204
|
+
'diff',
|
|
205
|
+
'--no-index',
|
|
206
|
+
'--no-ext-diff',
|
|
207
|
+
'--numstat',
|
|
208
|
+
'--',
|
|
209
|
+
'/dev/null',
|
|
210
|
+
path,
|
|
211
|
+
]
|
|
212
|
+
: [
|
|
213
|
+
'-C',
|
|
214
|
+
root,
|
|
215
|
+
'diff',
|
|
216
|
+
'--no-ext-diff',
|
|
217
|
+
'--no-renames',
|
|
218
|
+
'--numstat',
|
|
219
|
+
baselineOid,
|
|
220
|
+
'--',
|
|
221
|
+
path,
|
|
222
|
+
];
|
|
223
|
+
const numstat = await (untrackedSet.has(path) ? control : git).run({
|
|
224
|
+
command: 'git',
|
|
225
|
+
args: numstatArgs,
|
|
226
|
+
timeoutMs: 5_000,
|
|
227
|
+
...(signal ? { signal } : {}),
|
|
228
|
+
});
|
|
229
|
+
const validNumstatDifference = (oid === null || untrackedSet.has(path)) &&
|
|
230
|
+
numstat.code === 1 &&
|
|
231
|
+
!/error|fatal|permission|cannot|no such/iu.test(numstat.stderr);
|
|
232
|
+
if ((numstat.code !== 0 && !validNumstatDifference) ||
|
|
233
|
+
numstat.timedOut ||
|
|
234
|
+
numstat.truncated) {
|
|
235
|
+
files.push(note(path, 'Diff unavailable; Git inspection failed.'));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (/^-\s+-\s+/u.test(numstat.stdout)) {
|
|
239
|
+
files.push(note(path, 'Binary file; patch omitted.'));
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const args = oid === null || untrackedSet.has(path)
|
|
243
|
+
? [
|
|
244
|
+
'-C',
|
|
245
|
+
root,
|
|
246
|
+
'diff',
|
|
247
|
+
'--no-index',
|
|
248
|
+
'--no-ext-diff',
|
|
249
|
+
'--no-color',
|
|
250
|
+
'--unified=3',
|
|
251
|
+
'--',
|
|
252
|
+
'/dev/null',
|
|
253
|
+
path,
|
|
254
|
+
]
|
|
255
|
+
: [
|
|
256
|
+
'-C',
|
|
257
|
+
root,
|
|
258
|
+
'diff',
|
|
259
|
+
'--no-ext-diff',
|
|
260
|
+
'--no-color',
|
|
261
|
+
'--unified=3',
|
|
262
|
+
baselineOid,
|
|
263
|
+
'--',
|
|
264
|
+
path,
|
|
265
|
+
];
|
|
266
|
+
const result = await runGit(args);
|
|
267
|
+
if (result.timedOut) {
|
|
268
|
+
files.push(note(path, 'Diff unavailable; Git inspection timed out.'));
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const noIndexDifference = (oid === null || untrackedSet.has(path)) &&
|
|
272
|
+
result.code === 1 &&
|
|
273
|
+
!/error|fatal|permission|cannot|no such/iu.test(result.stderr);
|
|
274
|
+
if (result.code !== 0 && !noIndexDifference) {
|
|
275
|
+
const disappeared = /no such|cannot stat|does not exist|not found/iu.test(result.stderr);
|
|
276
|
+
files.push(note(path, disappeared
|
|
277
|
+
? 'Diff unavailable; path changed during inspection.'
|
|
278
|
+
: 'Diff unavailable; Git inspection failed.'));
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
files.push({
|
|
282
|
+
path,
|
|
283
|
+
patch: result.stdout,
|
|
284
|
+
...changedLines(result.stdout),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error instanceof DOMException && error.name === 'AbortError')
|
|
289
|
+
throw error;
|
|
290
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
291
|
+
files.push(note(path, /no such|cannot stat|does not exist|not found/iu.test(message)
|
|
292
|
+
? 'Diff unavailable; path changed during inspection.'
|
|
293
|
+
: 'Diff unavailable; Git inspection failed.'));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
files,
|
|
298
|
+
additions: files.reduce((n, f) => n + f.additions, 0),
|
|
299
|
+
deletions: files.reduce((n, f) => n + f.deletions, 0),
|
|
300
|
+
};
|
|
93
301
|
};
|
|
302
|
+
return { prepare, load };
|
|
94
303
|
}
|
|
95
304
|
export async function loadGitDiff(cwd) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const names = await execFileAsync('git', ['-C', cwd, 'diff', '--name-only', '-z', 'HEAD', '--'], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
100
|
-
const paths = names.stdout.split('\0').filter(Boolean);
|
|
101
|
-
const files = [];
|
|
102
|
-
for (const path of paths) {
|
|
103
|
-
const result = await execFileAsync('git', [
|
|
104
|
-
'-C',
|
|
105
|
-
cwd,
|
|
106
|
-
'diff',
|
|
107
|
-
'--no-ext-diff',
|
|
108
|
-
'--no-color',
|
|
109
|
-
'--unified=3',
|
|
110
|
-
'HEAD',
|
|
111
|
-
'--',
|
|
112
|
-
path,
|
|
113
|
-
], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
114
|
-
files.push({ path, patch: result.stdout, ...changedLines(result.stdout) });
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
files,
|
|
118
|
-
additions: files.reduce((total, file) => total + file.additions, 0),
|
|
119
|
-
deletions: files.reduce((total, file) => total + file.deletions, 0),
|
|
120
|
-
};
|
|
305
|
+
const session = createTuiGitDiffSession(cwd);
|
|
306
|
+
await session.prepare(cwd);
|
|
307
|
+
return session.load(cwd);
|
|
121
308
|
}
|
|
122
309
|
//# sourceMappingURL=git-diff.js.map
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { ModelToolCall } from '../../core/runtime.js';
|
|
2
|
+
export type TaskNotificationMetadata = {
|
|
3
|
+
readonly taskId: string;
|
|
4
|
+
readonly status: 'completed' | 'failed' | 'stopped';
|
|
5
|
+
};
|
|
2
6
|
export type TranscriptItem = {
|
|
3
|
-
kind: 'user' | 'assistant'
|
|
7
|
+
kind: 'user' | 'assistant';
|
|
8
|
+
text: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'notice' | 'warning';
|
|
4
11
|
text: string;
|
|
12
|
+
taskNotification?: TaskNotificationMetadata;
|
|
5
13
|
} | {
|
|
6
14
|
kind: 'local-result';
|
|
7
15
|
text: string;
|
|
@@ -102,6 +110,12 @@ export type TranscriptPresentationEntry = TranscriptPresentationEntryValue & {
|
|
|
102
110
|
export declare function transcriptPresentationEntryKey(item: TranscriptItem, sourceIndex: number): string;
|
|
103
111
|
/** Stable renderer key for a Normal-mode contiguous Read summary. */
|
|
104
112
|
export declare function transcriptReadSummaryKey(sourceIndex: number): string;
|
|
113
|
+
export declare function isCompletedBackgroundBashNotification(item: TranscriptItem): Extract<TranscriptItem, {
|
|
114
|
+
kind: 'notice' | 'warning';
|
|
115
|
+
}> | undefined;
|
|
116
|
+
export declare function completedBackgroundBashSummary(count: number): Extract<TranscriptItem, {
|
|
117
|
+
kind: 'notice' | 'warning';
|
|
118
|
+
}>;
|
|
105
119
|
export declare function projectTranscriptPresentation(items: readonly TranscriptItem[], mode: TranscriptPresentationMode): readonly TranscriptPresentationEntry[];
|
|
106
120
|
export {};
|
|
107
121
|
//# sourceMappingURL=transcript-presentation.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBackgroundBashTaskId } from '../../application/background-task-id.js';
|
|
1
2
|
/** Stable renderer key for one source Transcript item. */
|
|
2
3
|
export function transcriptPresentationEntryKey(item, sourceIndex) {
|
|
3
4
|
if (item.kind === 'tool')
|
|
@@ -14,6 +15,24 @@ export function transcriptPresentationEntryKey(item, sourceIndex) {
|
|
|
14
15
|
export function transcriptReadSummaryKey(sourceIndex) {
|
|
15
16
|
return `read-summary-${sourceIndex}`;
|
|
16
17
|
}
|
|
18
|
+
export function isCompletedBackgroundBashNotification(item) {
|
|
19
|
+
if (item.kind !== 'notice' && item.kind !== 'warning')
|
|
20
|
+
return undefined;
|
|
21
|
+
const metadata = item.taskNotification;
|
|
22
|
+
if (!metadata ||
|
|
23
|
+
metadata.status !== 'completed' ||
|
|
24
|
+
!isBackgroundBashTaskId(metadata.taskId))
|
|
25
|
+
return undefined;
|
|
26
|
+
return item;
|
|
27
|
+
}
|
|
28
|
+
export function completedBackgroundBashSummary(count) {
|
|
29
|
+
if (!Number.isInteger(count) || count <= 0)
|
|
30
|
+
throw new RangeError('count must be a positive integer');
|
|
31
|
+
return {
|
|
32
|
+
kind: 'notice',
|
|
33
|
+
text: `${count} background commands completed`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
17
36
|
function pairResults(items) {
|
|
18
37
|
const toolResults = new Map();
|
|
19
38
|
const toolResultIndexes = new Map();
|
|
@@ -124,6 +143,32 @@ export function projectTranscriptPresentation(items, mode) {
|
|
|
124
143
|
const item = items[index];
|
|
125
144
|
if (!item)
|
|
126
145
|
continue;
|
|
146
|
+
if (mode === 'normal') {
|
|
147
|
+
const notification = isCompletedBackgroundBashNotification(item);
|
|
148
|
+
if (notification) {
|
|
149
|
+
const notifications = [notification];
|
|
150
|
+
let end = index + 1;
|
|
151
|
+
while (end < items.length) {
|
|
152
|
+
const next = items[end];
|
|
153
|
+
const nextNotification = next
|
|
154
|
+
? isCompletedBackgroundBashNotification(next)
|
|
155
|
+
: undefined;
|
|
156
|
+
if (!nextNotification)
|
|
157
|
+
break;
|
|
158
|
+
notifications.push(nextNotification);
|
|
159
|
+
end += 1;
|
|
160
|
+
}
|
|
161
|
+
if (notifications.length >= 2) {
|
|
162
|
+
entries.push({
|
|
163
|
+
kind: 'item',
|
|
164
|
+
key: transcriptPresentationEntryKey(item, index),
|
|
165
|
+
item: completedBackgroundBashSummary(notifications.length),
|
|
166
|
+
});
|
|
167
|
+
index = end - 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
127
172
|
if (mode === 'normal' &&
|
|
128
173
|
item.kind === 'tool' &&
|
|
129
174
|
item.call.name === 'Read') {
|
|
@@ -76,6 +76,12 @@ interface TailReadRun {
|
|
|
76
76
|
readonly calls: readonly ReadRunCall[];
|
|
77
77
|
readonly summarized: boolean;
|
|
78
78
|
}
|
|
79
|
+
interface TailCompletedBackgroundBashRun {
|
|
80
|
+
readonly startSourceIndex: number;
|
|
81
|
+
readonly startPresentationIndex: number;
|
|
82
|
+
readonly nextSourceIndex: number;
|
|
83
|
+
readonly count: number;
|
|
84
|
+
}
|
|
79
85
|
export interface TranscriptWindowState {
|
|
80
86
|
readonly history: readonly TranscriptItem[];
|
|
81
87
|
readonly mode: TranscriptPresentationMode;
|
|
@@ -84,6 +90,7 @@ export interface TranscriptWindowState {
|
|
|
84
90
|
readonly pendingTools?: PendingNode;
|
|
85
91
|
readonly pendingShells?: PendingNode;
|
|
86
92
|
readonly tailReadRun?: TailReadRun;
|
|
93
|
+
readonly tailCompletedBackgroundBashRun?: TailCompletedBackgroundBashRun;
|
|
87
94
|
readonly revision: number;
|
|
88
95
|
}
|
|
89
96
|
export interface TranscriptWindowInput {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { projectTranscriptPresentation, transcriptPresentationEntryKey, transcriptReadSummaryKey, } from './transcript-presentation.js';
|
|
1
|
+
import { completedBackgroundBashSummary, isCompletedBackgroundBashNotification, projectTranscriptPresentation, transcriptPresentationEntryKey, transcriptReadSummaryKey, } from './transcript-presentation.js';
|
|
2
2
|
import { createTranscriptEntryViewportIndex, estimateTranscriptEntryLines, projectTranscriptEntryRange, } from './transcript-viewport.js';
|
|
3
3
|
const historyChangeLineage = Symbol('tui-history-change-lineage');
|
|
4
4
|
function retainedHistoryChange(revision, changedFrom, history, previousHistory) {
|
|
@@ -465,6 +465,29 @@ function coldTailReadRun(history, pairing) {
|
|
|
465
465
|
summarized,
|
|
466
466
|
};
|
|
467
467
|
}
|
|
468
|
+
function coldTailCompletedBackgroundBashRun(history, entries) {
|
|
469
|
+
let start = history.length;
|
|
470
|
+
while (start > 0) {
|
|
471
|
+
const candidate = history[start - 1];
|
|
472
|
+
if (!candidate || !isCompletedBackgroundBashNotification(candidate))
|
|
473
|
+
break;
|
|
474
|
+
start -= 1;
|
|
475
|
+
}
|
|
476
|
+
if (start >= history.length)
|
|
477
|
+
return undefined;
|
|
478
|
+
const first = history[start];
|
|
479
|
+
if (!first)
|
|
480
|
+
return undefined;
|
|
481
|
+
const startPresentationIndex = entries.findIndex((entry) => entry.key === transcriptPresentationEntryKey(first, start));
|
|
482
|
+
if (startPresentationIndex < 0)
|
|
483
|
+
return undefined;
|
|
484
|
+
return {
|
|
485
|
+
startSourceIndex: start,
|
|
486
|
+
startPresentationIndex,
|
|
487
|
+
nextSourceIndex: history.length,
|
|
488
|
+
count: history.length - start,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
468
491
|
function canReuseEntry(previous, next) {
|
|
469
492
|
if (previous.kind !== next.kind || previous.key !== next.key)
|
|
470
493
|
return false;
|
|
@@ -508,6 +531,9 @@ function coldState(input, previous) {
|
|
|
508
531
|
const tailReadRun = input.mode === 'normal'
|
|
509
532
|
? coldTailReadRun(input.history, pairing)
|
|
510
533
|
: undefined;
|
|
534
|
+
const tailCompletedBackgroundBashRun = input.mode === 'normal'
|
|
535
|
+
? coldTailCompletedBackgroundBashRun(input.history, entries)
|
|
536
|
+
: undefined;
|
|
511
537
|
return {
|
|
512
538
|
history: input.history,
|
|
513
539
|
mode: input.mode,
|
|
@@ -516,6 +542,9 @@ function coldState(input, previous) {
|
|
|
516
542
|
...(pairing.pendingTools ? { pendingTools: pairing.pendingTools } : {}),
|
|
517
543
|
...(pairing.pendingShells ? { pendingShells: pairing.pendingShells } : {}),
|
|
518
544
|
...(tailReadRun ? { tailReadRun } : {}),
|
|
545
|
+
...(tailCompletedBackgroundBashRun
|
|
546
|
+
? { tailCompletedBackgroundBashRun }
|
|
547
|
+
: {}),
|
|
519
548
|
revision: input.revision,
|
|
520
549
|
};
|
|
521
550
|
}
|
|
@@ -565,6 +594,7 @@ function appendState(input, previous, change) {
|
|
|
565
594
|
let pendingTools = previous.pendingTools;
|
|
566
595
|
let pendingShells = previous.pendingShells;
|
|
567
596
|
let tailReadRun = previous.tailReadRun;
|
|
597
|
+
let tailCompletedBackgroundBashRun = previous.tailCompletedBackgroundBashRun;
|
|
568
598
|
for (let offset = 0; offset < suffix.length; offset += 1) {
|
|
569
599
|
const sourceIndex = previousLength + offset;
|
|
570
600
|
const item = suffix[offset];
|
|
@@ -582,6 +612,38 @@ function appendState(input, previous, change) {
|
|
|
582
612
|
tailReadRun.calls.some((call) => call.sourceIndex === pendingTool.sourceIndex)));
|
|
583
613
|
if (!continuesReadRun)
|
|
584
614
|
tailReadRun = undefined;
|
|
615
|
+
const notification = input.mode === 'normal'
|
|
616
|
+
? isCompletedBackgroundBashNotification(item)
|
|
617
|
+
: undefined;
|
|
618
|
+
if (notification) {
|
|
619
|
+
const continuesRun = tailCompletedBackgroundBashRun?.nextSourceIndex === sourceIndex;
|
|
620
|
+
if (continuesRun && tailCompletedBackgroundBashRun) {
|
|
621
|
+
const count = tailCompletedBackgroundBashRun.count + 1;
|
|
622
|
+
const firstItem = input.history[tailCompletedBackgroundBashRun.startSourceIndex] ?? item;
|
|
623
|
+
tree = updateEntry(tree, tailCompletedBackgroundBashRun.startPresentationIndex, {
|
|
624
|
+
kind: 'item',
|
|
625
|
+
key: transcriptPresentationEntryKey(firstItem, tailCompletedBackgroundBashRun.startSourceIndex),
|
|
626
|
+
item: completedBackgroundBashSummary(count),
|
|
627
|
+
}, input.width, input.mode);
|
|
628
|
+
tailCompletedBackgroundBashRun = {
|
|
629
|
+
...tailCompletedBackgroundBashRun,
|
|
630
|
+
nextSourceIndex: sourceIndex + 1,
|
|
631
|
+
count,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
const presentationIndex = tree.entryCount;
|
|
636
|
+
tree = appendEntry(tree, entryForItem(item, sourceIndex), input.width, input.mode);
|
|
637
|
+
tailCompletedBackgroundBashRun = {
|
|
638
|
+
startSourceIndex: sourceIndex,
|
|
639
|
+
startPresentationIndex: presentationIndex,
|
|
640
|
+
nextSourceIndex: sourceIndex + 1,
|
|
641
|
+
count: 1,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
tailCompletedBackgroundBashRun = undefined;
|
|
585
647
|
if (item.kind === 'tool') {
|
|
586
648
|
if (item.call.name === 'Read' && input.mode === 'normal') {
|
|
587
649
|
if (tailReadRun?.summarized) {
|
|
@@ -704,6 +766,9 @@ function appendState(input, previous, change) {
|
|
|
704
766
|
...(pendingTools ? { pendingTools } : {}),
|
|
705
767
|
...(pendingShells ? { pendingShells } : {}),
|
|
706
768
|
...(tailReadRun ? { tailReadRun } : {}),
|
|
769
|
+
...(tailCompletedBackgroundBashRun
|
|
770
|
+
? { tailCompletedBackgroundBashRun }
|
|
771
|
+
: {}),
|
|
707
772
|
revision: input.revision,
|
|
708
773
|
};
|
|
709
774
|
}
|