@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.2
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 +36 -2
- package/dist/bin/ai.js +138 -18
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +3 -3
- package/dist/src/api/browser-login.js +0 -16
- package/dist/src/api/chat.js +59 -11
- package/dist/src/api/http.js +49 -1
- package/dist/src/api/models.js +26 -20
- package/dist/src/artifact-policy.js +3 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +0 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +19 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +11 -6
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +97 -12
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +75 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +0 -19
- package/dist/src/session-store.js +0 -1
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +194 -21
- package/dist/src/tools/delete-file.js +23 -5
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +33 -7
- package/dist/src/tools/path-suggest.js +81 -8
- package/dist/src/tools/read-document.js +2 -2
- package/dist/src/tools/read-file.js +17 -8
- package/dist/src/tools/replace-document-text.js +10 -12
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +109 -24
- package/dist/src/tools/run-node-script.js +27 -5
- package/dist/src/tools/shell-job-kill.js +48 -0
- package/dist/src/tools/shell-job-output.js +51 -0
- package/dist/src/tools/str-replace.js +33 -7
- package/dist/src/tools/undo-edit.js +1 -1
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +26 -6
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +342 -23
- package/dist/src/ui/tui/bridge.js +0 -4
- package/dist/src/ui/tui/build-frame.js +220 -24
- package/dist/src/ui/tui/shell-input.js +33 -4
- package/dist/src/ui/tui/terminal-title.js +81 -0
- package/dist/src/version.js +0 -6
- package/dist/vendor/web-tree-sitter/LICENSE +21 -0
- package/dist/vendor/web-tree-sitter/NOTICE +13 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/package.json +14 -15
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createRatatuiBridge } from './tui/bridge.js';
|
|
2
|
-
import { buildTuiFrame, renderTranscriptEntryLines } from './tui/build-frame.js';
|
|
2
|
+
import { buildTuiFrame, formatJobElapsed, formatTodoProgress, renderTranscriptEntryLines, } from './tui/build-frame.js';
|
|
3
|
+
import { createTerminalTitleController } from './tui/terminal-title.js';
|
|
3
4
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
4
5
|
import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
5
6
|
import { chat, models } from '../api/index.js';
|
|
6
7
|
import { isTurnCancelledError } from '../api/chat.js';
|
|
8
|
+
import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
|
|
9
|
+
import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
10
|
+
import { setScratchSession } from '../scratch-dir.js';
|
|
7
11
|
import { cancelActiveCommand } from '../executor.js';
|
|
8
12
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
13
|
+
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
9
14
|
import { clearConversation, } from '../session.js';
|
|
10
15
|
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
|
|
11
16
|
import { truncate } from '../utils.js';
|
|
@@ -102,6 +107,10 @@ export const SLASH_COMMANDS = [
|
|
|
102
107
|
command: '/resume',
|
|
103
108
|
description: 'Open the session picker',
|
|
104
109
|
},
|
|
110
|
+
{
|
|
111
|
+
command: '/jobs',
|
|
112
|
+
description: 'Background jobs: pick to view output or kill',
|
|
113
|
+
},
|
|
105
114
|
{
|
|
106
115
|
command: '/clear',
|
|
107
116
|
description: 'Clear conversation history',
|
|
@@ -550,6 +559,31 @@ function buildFileChangeEntry(event) {
|
|
|
550
559
|
title: skipped ? `${verb} skipped: ${filePath}` : `${verb} failed: ${filePath}`,
|
|
551
560
|
};
|
|
552
561
|
}
|
|
562
|
+
if (result?.scratch === true) {
|
|
563
|
+
const scratchName = truncate(filePath.split(/[\\/]/).filter(Boolean).at(-1) ?? filePath, 72);
|
|
564
|
+
if (call.name === 'delete_file') {
|
|
565
|
+
return {
|
|
566
|
+
body: '',
|
|
567
|
+
filePath,
|
|
568
|
+
kind: 'tool',
|
|
569
|
+
title: result?.deleted === true
|
|
570
|
+
? `Removed scratch file: ${scratchName}`
|
|
571
|
+
: `Scratch delete skipped: ${scratchName}`,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const content = typeof call.args?.content === 'string' ? call.args.content : '';
|
|
575
|
+
const lineSummary = call.name === 'write_file' && content
|
|
576
|
+
? ` (${splitDiffLines(content).length} lines)`
|
|
577
|
+
: '';
|
|
578
|
+
return {
|
|
579
|
+
body: '',
|
|
580
|
+
filePath,
|
|
581
|
+
kind: 'tool',
|
|
582
|
+
title: call.name === 'write_file'
|
|
583
|
+
? `Wrote scratch file: ${scratchName}${lineSummary}`
|
|
584
|
+
: `Edited scratch file: ${scratchName}`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
553
587
|
if (call.name === 'undo_edit') {
|
|
554
588
|
const dryRun = result?.dryRun === true ||
|
|
555
589
|
result?.dry_run === true ||
|
|
@@ -643,12 +677,33 @@ function buildWorkingToolEntry(event) {
|
|
|
643
677
|
: '';
|
|
644
678
|
if (call.name === 'run_command') {
|
|
645
679
|
const command = String(call.args?.command ?? result?.command ?? '').trim();
|
|
680
|
+
if (result?.backgrounded === true && result?.jobId) {
|
|
681
|
+
return {
|
|
682
|
+
body: `$ ${truncate(command, 180)}\nBackground job ${String(result.jobId)} started.${error}`,
|
|
683
|
+
kind: result?.ok === true ? 'tool' : 'error',
|
|
684
|
+
preformatted: true,
|
|
685
|
+
title: 'Shell',
|
|
686
|
+
};
|
|
687
|
+
}
|
|
646
688
|
return {
|
|
647
689
|
body: `$ ${truncate(command, 180)}\n${formatRunCommandResultState(result)}${error}`,
|
|
648
690
|
kind: result?.ok === true ? 'tool' : 'error',
|
|
649
691
|
title: 'Shell',
|
|
650
692
|
};
|
|
651
693
|
}
|
|
694
|
+
if (call.name === 'shell_job_output' || call.name === 'shell_job_kill') {
|
|
695
|
+
const jobId = String(result?.jobId ?? call.args?.job_id ?? '').trim();
|
|
696
|
+
const status = String(result?.status ?? '').trim();
|
|
697
|
+
const exitCode = result?.exitCode;
|
|
698
|
+
const stateText = status
|
|
699
|
+
? `${status}${exitCode != null ? ` (code ${exitCode})` : ''}`
|
|
700
|
+
: formatToolResultState(result);
|
|
701
|
+
return {
|
|
702
|
+
body: `${jobId || '(unknown job)'} · ${stateText}${error}`,
|
|
703
|
+
kind: result?.ok === true ? 'tool' : 'error',
|
|
704
|
+
title: call.name === 'shell_job_kill' ? 'Kill job' : 'Job output',
|
|
705
|
+
};
|
|
706
|
+
}
|
|
652
707
|
if (call.name === 'run_node_script') {
|
|
653
708
|
return {
|
|
654
709
|
body: `node --input-type=module <script via stdin>\n${formatRunCommandResultState(result)}${error}`,
|
|
@@ -666,6 +721,32 @@ function buildWorkingToolEntry(event) {
|
|
|
666
721
|
title: 'Tool',
|
|
667
722
|
};
|
|
668
723
|
}
|
|
724
|
+
function buildBackgroundJobNoticeEntry(snapshot) {
|
|
725
|
+
const command = truncate(snapshot.command, 120);
|
|
726
|
+
const ran = formatJobElapsed((snapshot.endedAt ?? Date.now()) - snapshot.startedAt);
|
|
727
|
+
if (snapshot.status === 'error') {
|
|
728
|
+
return {
|
|
729
|
+
body: `✖ ${snapshot.id} (${command}) failed to start`,
|
|
730
|
+
kind: 'error',
|
|
731
|
+
preformatted: true,
|
|
732
|
+
title: 'Background job',
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (snapshot.status === 'killed') {
|
|
736
|
+
return {
|
|
737
|
+
body: `■ ${snapshot.id} (${command}) killed · ran ${ran}`,
|
|
738
|
+
kind: 'system',
|
|
739
|
+
preformatted: true,
|
|
740
|
+
title: 'Background job',
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
body: `${snapshot.exitCode === 0 ? '✓' : '✖'} ${snapshot.id} (${command}) exited (code ${snapshot.exitCode ?? 1}) · ran ${ran}`,
|
|
745
|
+
kind: snapshot.exitCode === 0 ? 'system' : 'error',
|
|
746
|
+
preformatted: true,
|
|
747
|
+
title: 'Background job',
|
|
748
|
+
};
|
|
749
|
+
}
|
|
669
750
|
function findPendingToolCallByName(pendingCalls, name) {
|
|
670
751
|
for (const [id, call] of pendingCalls) {
|
|
671
752
|
if (call.name !== name)
|
|
@@ -814,11 +895,16 @@ function createShellStore(initialState) {
|
|
|
814
895
|
},
|
|
815
896
|
appendWorkingTool: (entry) => {
|
|
816
897
|
const lastEntry = state.workingTools.at(-1);
|
|
817
|
-
|
|
898
|
+
const isDuplicate = sameTranscriptDraft(lastEntry, entry);
|
|
899
|
+
if (isDuplicate && state.commandLog.length === 0) {
|
|
818
900
|
return;
|
|
901
|
+
}
|
|
819
902
|
state = {
|
|
820
903
|
...state,
|
|
821
|
-
|
|
904
|
+
commandLog: [],
|
|
905
|
+
workingTools: isDuplicate
|
|
906
|
+
? state.workingTools
|
|
907
|
+
: [...state.workingTools, entry].slice(-WORKING_TOOL_PREVIEW_ITEMS),
|
|
822
908
|
};
|
|
823
909
|
notify();
|
|
824
910
|
},
|
|
@@ -868,6 +954,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
868
954
|
approvalCursor: getDefaultApprovalCursor(),
|
|
869
955
|
approvalPrompt: null,
|
|
870
956
|
autoYes: session.autoYes,
|
|
957
|
+
backgroundJobs: [],
|
|
871
958
|
busy: false,
|
|
872
959
|
busySince: null,
|
|
873
960
|
clockNow: Date.now(),
|
|
@@ -879,6 +966,9 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
879
966
|
exiting: false,
|
|
880
967
|
input: '',
|
|
881
968
|
maxToolSteps: session.maxToolSteps,
|
|
969
|
+
jobsPickerExpandedId: null,
|
|
970
|
+
jobsPickerIndex: 0,
|
|
971
|
+
jobsPickerOpen: false,
|
|
882
972
|
modelPickerIndex: getDefaultModelPickerIndex(session.modelId, serverModels.models),
|
|
883
973
|
modelPickerOpen: false,
|
|
884
974
|
projectRoot: session.rootDir,
|
|
@@ -899,6 +989,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
899
989
|
exitConfirmUntil: null,
|
|
900
990
|
thinkingTitle: '',
|
|
901
991
|
thinkingNotes: [],
|
|
992
|
+
todos: listTodos(),
|
|
902
993
|
tokenUsage: formatClientTokenUsage(null),
|
|
903
994
|
transcript: [],
|
|
904
995
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
@@ -951,6 +1042,14 @@ function thinkingNoteFromStatus(status) {
|
|
|
951
1042
|
const note = text.slice('Thinking:'.length).trim();
|
|
952
1043
|
return note ? note : null;
|
|
953
1044
|
}
|
|
1045
|
+
function stripMarkdownEmphasis(text) {
|
|
1046
|
+
return text
|
|
1047
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
|
|
1048
|
+
.replace(/___(.+?)___/g, '$1')
|
|
1049
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
1050
|
+
.replace(/__(.+?)__/g, '$1')
|
|
1051
|
+
.replace(/(?<!\*)\*(?!\*)([^*\n]+?)\*(?!\*)/g, '$1');
|
|
1052
|
+
}
|
|
954
1053
|
function splitThinkingLines(text) {
|
|
955
1054
|
return text
|
|
956
1055
|
.split('\n')
|
|
@@ -959,13 +1058,13 @@ function splitThinkingLines(text) {
|
|
|
959
1058
|
.flatMap((line) => line
|
|
960
1059
|
.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/)
|
|
961
1060
|
.map((part) => part.trim())
|
|
962
|
-
.filter(Boolean))
|
|
963
|
-
.map((line) => truncate(line, 120));
|
|
1061
|
+
.filter(Boolean));
|
|
964
1062
|
}
|
|
965
1063
|
function thinkingPanelFromStatus(status) {
|
|
966
|
-
const
|
|
967
|
-
if (!
|
|
1064
|
+
const rawText = thinkingNoteFromStatus(status);
|
|
1065
|
+
if (!rawText)
|
|
968
1066
|
return null;
|
|
1067
|
+
const text = stripMarkdownEmphasis(rawText);
|
|
969
1068
|
const rawLines = text
|
|
970
1069
|
.split('\n')
|
|
971
1070
|
.map((line) => line.trim())
|
|
@@ -974,12 +1073,12 @@ function thinkingPanelFromStatus(status) {
|
|
|
974
1073
|
return null;
|
|
975
1074
|
if (rawLines.length === 1 && rawLines[0].length <= 72) {
|
|
976
1075
|
return {
|
|
977
|
-
title:
|
|
1076
|
+
title: rawLines[0],
|
|
978
1077
|
notes: [],
|
|
979
1078
|
};
|
|
980
1079
|
}
|
|
981
1080
|
const title = rawLines.length > 1 && rawLines[0].length <= 72
|
|
982
|
-
?
|
|
1081
|
+
? rawLines[0]
|
|
983
1082
|
: 'Thinking';
|
|
984
1083
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
985
1084
|
return {
|
|
@@ -997,7 +1096,10 @@ export function getInputCommandToken(input) {
|
|
|
997
1096
|
if (!trimmed.startsWith('/'))
|
|
998
1097
|
return '';
|
|
999
1098
|
const firstSpaceIndex = trimmed.indexOf(' ');
|
|
1000
|
-
|
|
1099
|
+
const token = firstSpaceIndex === -1 ? trimmed : trimmed.slice(0, firstSpaceIndex);
|
|
1100
|
+
if (token.indexOf('/', 1) !== -1)
|
|
1101
|
+
return '';
|
|
1102
|
+
return token;
|
|
1001
1103
|
}
|
|
1002
1104
|
function shouldShowCommandPalette(state) {
|
|
1003
1105
|
const trimmed = String(state.input ?? '').trim();
|
|
@@ -1006,7 +1108,8 @@ function shouldShowCommandPalette(state) {
|
|
|
1006
1108
|
!state.modelPickerOpen &&
|
|
1007
1109
|
!state.resumePickerOpen &&
|
|
1008
1110
|
trimmed.startsWith('/') &&
|
|
1009
|
-
!trimmed.includes(' ')
|
|
1111
|
+
!trimmed.includes(' ') &&
|
|
1112
|
+
getInputCommandToken(trimmed) !== '');
|
|
1010
1113
|
}
|
|
1011
1114
|
export function shouldRemountLiveFrameForComposerInputChange(current, nextInput) {
|
|
1012
1115
|
const currentShowsCommands = shouldShowCommandPalette(current);
|
|
@@ -1179,6 +1282,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1179
1282
|
throw new Error('Client TUI requires an interactive terminal.');
|
|
1180
1283
|
}
|
|
1181
1284
|
await withTuiMode(async () => {
|
|
1285
|
+
setBackgroundJobSession(session.sessionId);
|
|
1286
|
+
setScratchSession(session.sessionId);
|
|
1287
|
+
setTodoSession(session.sessionId);
|
|
1182
1288
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1183
1289
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1184
1290
|
let currentServerModels = serverModels;
|
|
@@ -1201,6 +1307,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1201
1307
|
let latestUsageSummary = null;
|
|
1202
1308
|
let pendingTurnEntries = [];
|
|
1203
1309
|
let activeTurnAbort = null;
|
|
1310
|
+
let todosTouchedThisTurn = false;
|
|
1311
|
+
const syncTodosState = () => {
|
|
1312
|
+
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
1313
|
+
};
|
|
1204
1314
|
let activeTurnGeneration = 0;
|
|
1205
1315
|
let exitCtrlCArmed = false;
|
|
1206
1316
|
let exitCtrlCTimer = null;
|
|
@@ -1229,14 +1339,23 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1229
1339
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1230
1340
|
}, 2500);
|
|
1231
1341
|
};
|
|
1342
|
+
const terminalTitle = createTerminalTitleController();
|
|
1343
|
+
const syncTerminalTitle = () => {
|
|
1344
|
+
const state = store.getState();
|
|
1345
|
+
terminalTitle.sync({
|
|
1346
|
+
awaitingReview: Boolean(state.approvalPrompt || state.sudoPrompt),
|
|
1347
|
+
busy: state.busy,
|
|
1348
|
+
});
|
|
1349
|
+
};
|
|
1232
1350
|
const renderCurrentFrame = () => {
|
|
1233
1351
|
if (!tuiReady)
|
|
1234
1352
|
return;
|
|
1235
1353
|
const state = store.getState();
|
|
1354
|
+
syncTerminalTitle();
|
|
1236
1355
|
const elapsedSeconds = state.busySince
|
|
1237
1356
|
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1238
1357
|
: 0;
|
|
1239
|
-
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds));
|
|
1358
|
+
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1240
1359
|
};
|
|
1241
1360
|
const remountTui = async () => {
|
|
1242
1361
|
if (remountPromise) {
|
|
@@ -1396,9 +1515,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1396
1515
|
activeTurnAbort?.abort();
|
|
1397
1516
|
activeTurnAbort = null;
|
|
1398
1517
|
cancelActiveCommand();
|
|
1399
|
-
// Ctrl+C with a queued message: recall it into the composer for editing
|
|
1400
|
-
// rather than auto-submitting it against the cancelled turn. (Esc with a
|
|
1401
|
-
// queued message clears the slot in shell-input without cancelling.)
|
|
1402
1518
|
const queued = store.getState().queuedMessage;
|
|
1403
1519
|
const cancelledEntries = [
|
|
1404
1520
|
...takePendingTurnEntries(),
|
|
@@ -1521,6 +1637,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1521
1637
|
dismissPendingApproval();
|
|
1522
1638
|
dismissPendingSudoPassword();
|
|
1523
1639
|
exiting = true;
|
|
1640
|
+
killAllBackgroundJobs();
|
|
1524
1641
|
store.update((current) => ({
|
|
1525
1642
|
...current,
|
|
1526
1643
|
exiting: true,
|
|
@@ -1675,6 +1792,87 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1675
1792
|
title: 'Model',
|
|
1676
1793
|
});
|
|
1677
1794
|
};
|
|
1795
|
+
const openJobsPicker = () => {
|
|
1796
|
+
syncBackgroundJobsState();
|
|
1797
|
+
store.update((current) => ({
|
|
1798
|
+
...current,
|
|
1799
|
+
commandCursor: 0,
|
|
1800
|
+
cursor: 0,
|
|
1801
|
+
input: '',
|
|
1802
|
+
jobsPickerExpandedId: null,
|
|
1803
|
+
jobsPickerIndex: 0,
|
|
1804
|
+
jobsPickerOpen: true,
|
|
1805
|
+
status: 'Background jobs',
|
|
1806
|
+
}));
|
|
1807
|
+
};
|
|
1808
|
+
const appendJobOutputEntry = async (jobId) => {
|
|
1809
|
+
const id = jobId.trim();
|
|
1810
|
+
await collectBackgroundJobUiOutputMutations({
|
|
1811
|
+
session,
|
|
1812
|
+
projectIndex,
|
|
1813
|
+
jobId: id,
|
|
1814
|
+
});
|
|
1815
|
+
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
1816
|
+
const buffered = getJobBufferedOutput(id);
|
|
1817
|
+
if (!job || !buffered) {
|
|
1818
|
+
appendError(`Unknown background job id: ${id}`);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const dropped = buffered.droppedChars > 0
|
|
1822
|
+
? `... (${buffered.droppedChars} chars of older output dropped) ...\n`
|
|
1823
|
+
: '';
|
|
1824
|
+
appendStaticEntry({
|
|
1825
|
+
body: `${dropped}${buffered.output || '(no output captured)'}`,
|
|
1826
|
+
kind: 'system',
|
|
1827
|
+
preformatted: true,
|
|
1828
|
+
title: `${job.id} output — ${truncate(job.command, 80)}`,
|
|
1829
|
+
});
|
|
1830
|
+
};
|
|
1831
|
+
const selectedJobsPickerId = () => {
|
|
1832
|
+
const current = store.getState();
|
|
1833
|
+
if (!current.backgroundJobs.length)
|
|
1834
|
+
return null;
|
|
1835
|
+
const index = Math.min(Math.max(current.jobsPickerIndex, 0), current.backgroundJobs.length - 1);
|
|
1836
|
+
return current.backgroundJobs[index]?.id ?? null;
|
|
1837
|
+
};
|
|
1838
|
+
const handleJobsPickerOutput = async () => {
|
|
1839
|
+
const jobId = selectedJobsPickerId();
|
|
1840
|
+
if (!jobId)
|
|
1841
|
+
return;
|
|
1842
|
+
await collectBackgroundJobUiOutputMutations({
|
|
1843
|
+
session,
|
|
1844
|
+
projectIndex,
|
|
1845
|
+
jobId,
|
|
1846
|
+
});
|
|
1847
|
+
syncBackgroundJobsState();
|
|
1848
|
+
store.update((current) => ({
|
|
1849
|
+
...current,
|
|
1850
|
+
jobsPickerExpandedId: current.jobsPickerExpandedId === jobId ? null : jobId,
|
|
1851
|
+
}));
|
|
1852
|
+
};
|
|
1853
|
+
const handleJobsPickerKill = async () => {
|
|
1854
|
+
const jobId = selectedJobsPickerId();
|
|
1855
|
+
if (!jobId)
|
|
1856
|
+
return;
|
|
1857
|
+
const killed = await killBackgroundJob(jobId);
|
|
1858
|
+
await collectBackgroundJobUiKillMutations({
|
|
1859
|
+
session,
|
|
1860
|
+
projectIndex,
|
|
1861
|
+
jobId,
|
|
1862
|
+
result: killed,
|
|
1863
|
+
});
|
|
1864
|
+
syncBackgroundJobsState();
|
|
1865
|
+
if (!killed.ok) {
|
|
1866
|
+
appendError(killed.error ?? 'Background job kill failed.');
|
|
1867
|
+
}
|
|
1868
|
+
else if (killed.alreadyFinished) {
|
|
1869
|
+
appendStaticEntry({
|
|
1870
|
+
body: `${jobId} had already finished.`,
|
|
1871
|
+
kind: 'system',
|
|
1872
|
+
title: 'Background jobs',
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1678
1876
|
const handleInlineModelSelection = async () => {
|
|
1679
1877
|
const current = store.getState();
|
|
1680
1878
|
const options = buildModelPickerOptions(current.currentModelId, current.serverModels);
|
|
@@ -1761,6 +1959,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1761
1959
|
try {
|
|
1762
1960
|
const snapshot = await loadInteractiveSession(selected.id);
|
|
1763
1961
|
applySessionSnapshot(session, snapshot);
|
|
1962
|
+
setBackgroundJobSession(session.sessionId);
|
|
1963
|
+
setScratchSession(session.sessionId);
|
|
1964
|
+
syncBackgroundJobsState();
|
|
1965
|
+
setTodoSession(session.sessionId);
|
|
1966
|
+
syncTodosState();
|
|
1764
1967
|
await saveActiveSession();
|
|
1765
1968
|
syncShellStateFromSession();
|
|
1766
1969
|
store.update((next) => ({
|
|
@@ -1802,8 +2005,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1802
2005
|
if (!queued || store.getState().busy || exiting) {
|
|
1803
2006
|
return;
|
|
1804
2007
|
}
|
|
1805
|
-
// Rehydrate attachments/chunks before submit: handleSubmit expands
|
|
1806
|
-
// pastedChunks from the store and the turn picks up imageAttachments.
|
|
1807
2008
|
store.update((current) => ({
|
|
1808
2009
|
...current,
|
|
1809
2010
|
imageAttachments: queued.imageAttachments,
|
|
@@ -1830,9 +2031,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1830
2031
|
});
|
|
1831
2032
|
return;
|
|
1832
2033
|
}
|
|
1833
|
-
// Snapshot the raw (placeholder) body plus chunks/images so recall and
|
|
1834
|
-
// flush round-trip the collapsed paste + attachments. Hold at most one;
|
|
1835
|
-
// a second enqueue replaces the slot.
|
|
1836
2034
|
const pending = store.getState();
|
|
1837
2035
|
const snapshot = {
|
|
1838
2036
|
body: String(rawInput ?? ''),
|
|
@@ -1887,6 +2085,46 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1887
2085
|
});
|
|
1888
2086
|
return;
|
|
1889
2087
|
}
|
|
2088
|
+
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2089
|
+
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2090
|
+
if (!jobsArgs) {
|
|
2091
|
+
openJobsPicker();
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const killMatch = jobsArgs.match(/^kill\s+(\S+)$/);
|
|
2095
|
+
if (killMatch) {
|
|
2096
|
+
const jobId = killMatch[1];
|
|
2097
|
+
const killed = await killBackgroundJob(jobId);
|
|
2098
|
+
await collectBackgroundJobUiKillMutations({
|
|
2099
|
+
session,
|
|
2100
|
+
projectIndex,
|
|
2101
|
+
jobId,
|
|
2102
|
+
result: killed,
|
|
2103
|
+
});
|
|
2104
|
+
if (!killed.ok) {
|
|
2105
|
+
appendError(killed.error ?? 'Background job kill failed.');
|
|
2106
|
+
}
|
|
2107
|
+
else if (killed.alreadyFinished) {
|
|
2108
|
+
appendStaticEntry({
|
|
2109
|
+
body: `${jobId} had already finished.`,
|
|
2110
|
+
kind: 'system',
|
|
2111
|
+
title: 'Background jobs',
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
const outputMatch = jobsArgs.match(/^output\s+(\S+)$/);
|
|
2117
|
+
if (outputMatch) {
|
|
2118
|
+
await appendJobOutputEntry(outputMatch[1]);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
appendStaticEntry({
|
|
2122
|
+
body: 'Usage: /jobs — open the jobs picker · /jobs output <id> — full output · /jobs kill <id> — kill one',
|
|
2123
|
+
kind: 'system',
|
|
2124
|
+
title: 'Background jobs',
|
|
2125
|
+
});
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
1890
2128
|
if (input === '/usage') {
|
|
1891
2129
|
store.update((current) => ({
|
|
1892
2130
|
...current,
|
|
@@ -1962,6 +2200,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1962
2200
|
}
|
|
1963
2201
|
if (input === '/clear') {
|
|
1964
2202
|
clearConversation(session);
|
|
2203
|
+
clearTodos();
|
|
2204
|
+
syncTodosState();
|
|
1965
2205
|
latestUsageSummary = null;
|
|
1966
2206
|
await saveActiveSession();
|
|
1967
2207
|
store.replaceTranscript([
|
|
@@ -1983,6 +2223,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1983
2223
|
const turnAbort = new AbortController();
|
|
1984
2224
|
activeTurnAbort = turnAbort;
|
|
1985
2225
|
lastTurnStartedAt = turnStartedAt;
|
|
2226
|
+
todosTouchedThisTurn = false;
|
|
1986
2227
|
const userEntry = {
|
|
1987
2228
|
body: input,
|
|
1988
2229
|
kind: 'user',
|
|
@@ -2037,6 +2278,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2037
2278
|
await appendSettledTurnEntries(turnEntries, result.text ?? '', () => turnGeneration === activeTurnGeneration && !turnAbort.signal.aborted);
|
|
2038
2279
|
if (turnGeneration !== activeTurnGeneration)
|
|
2039
2280
|
return;
|
|
2281
|
+
if (todosTouchedThisTurn) {
|
|
2282
|
+
todosTouchedThisTurn = false;
|
|
2283
|
+
const todoItems = listTodos();
|
|
2284
|
+
if (todoItems.length > 0) {
|
|
2285
|
+
appendStaticEntry({
|
|
2286
|
+
body: '',
|
|
2287
|
+
kind: 'system',
|
|
2288
|
+
title: `To-dos · ${formatTodoProgress(todoItems)}`,
|
|
2289
|
+
todoList: todoItems,
|
|
2290
|
+
});
|
|
2291
|
+
if (todoItems.every((item) => item.status === 'completed')) {
|
|
2292
|
+
clearTodos();
|
|
2293
|
+
syncTodosState();
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2040
2297
|
store.update((current) => ({
|
|
2041
2298
|
...current,
|
|
2042
2299
|
busy: false,
|
|
@@ -2085,8 +2342,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2085
2342
|
appendError(error.message);
|
|
2086
2343
|
}
|
|
2087
2344
|
await remountTui();
|
|
2088
|
-
// A cancelled turn never auto-submits the queue (Ctrl+C recalls it via
|
|
2089
|
-
// cancelActiveTurn); only a genuine error flushes a pending message.
|
|
2090
2345
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2091
2346
|
await flushQueuedMessage();
|
|
2092
2347
|
}
|
|
@@ -2119,6 +2374,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2119
2374
|
};
|
|
2120
2375
|
session.onToolEvent = (event) => {
|
|
2121
2376
|
syncShellStateFromSession();
|
|
2377
|
+
if (event.call.name === 'update_todos') {
|
|
2378
|
+
syncTodosState();
|
|
2379
|
+
if (event.result?.ok === true) {
|
|
2380
|
+
todosTouchedThisTurn = true;
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2122
2384
|
if (isFileChangeTool(event.call.name)) {
|
|
2123
2385
|
const entry = buildFileChangeEntry(event);
|
|
2124
2386
|
store.appendWorkingTool(entry);
|
|
@@ -2127,6 +2389,52 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2127
2389
|
}
|
|
2128
2390
|
store.appendWorkingTool(buildWorkingToolEntry(event));
|
|
2129
2391
|
};
|
|
2392
|
+
const jobDetailLines = (jobId) => {
|
|
2393
|
+
const buffered = getJobBufferedOutput(jobId);
|
|
2394
|
+
if (!buffered)
|
|
2395
|
+
return [];
|
|
2396
|
+
const lines = buffered.output
|
|
2397
|
+
.replace(/\r\n/g, '\n')
|
|
2398
|
+
.replace(/\r/g, '\n')
|
|
2399
|
+
.split('\n')
|
|
2400
|
+
.filter((line) => line.trim().length > 0);
|
|
2401
|
+
const visible = lines.slice(-20);
|
|
2402
|
+
return buffered.droppedChars > 0
|
|
2403
|
+
? [
|
|
2404
|
+
`... (${buffered.droppedChars} chars of older output dropped) ...`,
|
|
2405
|
+
...visible,
|
|
2406
|
+
]
|
|
2407
|
+
: visible;
|
|
2408
|
+
};
|
|
2409
|
+
const syncBackgroundJobsState = () => {
|
|
2410
|
+
const jobsForDisplay = listBackgroundJobs().map((job) => {
|
|
2411
|
+
const preview = getJobOutputPreview(job.id, 3);
|
|
2412
|
+
return {
|
|
2413
|
+
id: job.id,
|
|
2414
|
+
command: job.command,
|
|
2415
|
+
status: job.status,
|
|
2416
|
+
exitCode: job.exitCode,
|
|
2417
|
+
startedAt: job.startedAt,
|
|
2418
|
+
endedAt: job.endedAt,
|
|
2419
|
+
firstOutputLine: preview?.firstLine ?? '',
|
|
2420
|
+
tailLines: preview?.tailLines ?? [],
|
|
2421
|
+
detailLines: jobDetailLines(job.id),
|
|
2422
|
+
};
|
|
2423
|
+
});
|
|
2424
|
+
store.update((current) => ({
|
|
2425
|
+
...current,
|
|
2426
|
+
backgroundJobs: jobsForDisplay,
|
|
2427
|
+
jobsPickerExpandedId: jobsForDisplay.some((job) => job.id === current.jobsPickerExpandedId)
|
|
2428
|
+
? current.jobsPickerExpandedId
|
|
2429
|
+
: null,
|
|
2430
|
+
}));
|
|
2431
|
+
};
|
|
2432
|
+
setBackgroundJobUpdateHook((snapshot) => {
|
|
2433
|
+
syncBackgroundJobsState();
|
|
2434
|
+
if (snapshot.status !== 'running') {
|
|
2435
|
+
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2130
2438
|
projectIndex.onStatus = session.onStatus;
|
|
2131
2439
|
projectIndex.onContextLog = session.onContextLog;
|
|
2132
2440
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
@@ -2186,6 +2494,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2186
2494
|
onResumeSession: handleInlineResumeSelection,
|
|
2187
2495
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2188
2496
|
onSelectModel: handleInlineModelSelection,
|
|
2497
|
+
onJobsPickerOutput: handleJobsPickerOutput,
|
|
2498
|
+
onJobsPickerKill: handleJobsPickerKill,
|
|
2189
2499
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2190
2500
|
onSubmit: handleSubmit,
|
|
2191
2501
|
};
|
|
@@ -2199,7 +2509,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2199
2509
|
}
|
|
2200
2510
|
}, 120);
|
|
2201
2511
|
const clockTimer = setInterval(() => {
|
|
2202
|
-
if (
|
|
2512
|
+
if (hasRunningBackgroundJobs()) {
|
|
2513
|
+
syncBackgroundJobsState();
|
|
2514
|
+
}
|
|
2515
|
+
if (store.getState().busy || hasRunningBackgroundJobs()) {
|
|
2203
2516
|
renderCurrentFrame();
|
|
2204
2517
|
}
|
|
2205
2518
|
}, 1000);
|
|
@@ -2243,6 +2556,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2243
2556
|
clearInterval(clockTimer);
|
|
2244
2557
|
unsubscribe();
|
|
2245
2558
|
clearLiveFrameRemountTimer();
|
|
2559
|
+
terminalTitle.dispose();
|
|
2560
|
+
killAllBackgroundJobs();
|
|
2561
|
+
setBackgroundJobSession(null);
|
|
2562
|
+
setScratchSession(null);
|
|
2563
|
+
setTodoSession(null);
|
|
2564
|
+
setBackgroundJobUpdateHook(null);
|
|
2246
2565
|
await bridge.close();
|
|
2247
2566
|
setCommandOutputHook(null);
|
|
2248
2567
|
}
|
|
@@ -91,14 +91,11 @@ function normalizeChildMessage(raw) {
|
|
|
91
91
|
export function resolveTuiBinaryPath() {
|
|
92
92
|
const binaryName = process.platform === 'win32' ? 'thegitai-tui.exe' : 'thegitai-tui';
|
|
93
93
|
const platformPackage = `@thegitai/tui-${process.platform}-${process.arch}`;
|
|
94
|
-
// 1) Published per-platform optional dependency (the installed-from-npm path).
|
|
95
94
|
try {
|
|
96
95
|
return requireFromHere.resolve(`${platformPackage}/${binaryName}`);
|
|
97
96
|
}
|
|
98
97
|
catch {
|
|
99
|
-
// Not installed (unsupported platform yet, or local dev) — fall through.
|
|
100
98
|
}
|
|
101
|
-
// 2) Local dev build: `npm run build:tui` populates the workspace bin/.
|
|
102
99
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
103
100
|
const devCandidates = [
|
|
104
101
|
path.join(moduleDir, '../../../bin', binaryName),
|
|
@@ -137,7 +134,6 @@ export function createRatatuiBridge() {
|
|
|
137
134
|
}
|
|
138
135
|
}
|
|
139
136
|
catch {
|
|
140
|
-
// ignore malformed protocol lines
|
|
141
137
|
}
|
|
142
138
|
});
|
|
143
139
|
child.on('exit', () => {
|