@thegitai/cli 1.0.0-beta.8 → 1.0.0-preview.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/README.md +36 -2
- package/dist/bin/ai.js +135 -26
- 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 +3 -38
- package/dist/src/api/chat.js +57 -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 +1 -3
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +57 -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 +159 -18
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +3 -2
- 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 +14 -7
- package/dist/src/tools/replace-document-text.js +3 -11
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +83 -16
- package/dist/src/tools/run-node-script.js +3 -1
- 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 +3 -2
- 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 +1 -1
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +313 -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,15 @@
|
|
|
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';
|
|
7
10
|
import { cancelActiveCommand } from '../executor.js';
|
|
8
11
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
12
|
+
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
9
13
|
import { clearConversation, } from '../session.js';
|
|
10
14
|
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
|
|
11
15
|
import { truncate } from '../utils.js';
|
|
@@ -102,6 +106,10 @@ export const SLASH_COMMANDS = [
|
|
|
102
106
|
command: '/resume',
|
|
103
107
|
description: 'Open the session picker',
|
|
104
108
|
},
|
|
109
|
+
{
|
|
110
|
+
command: '/jobs',
|
|
111
|
+
description: 'Background jobs: pick to view output or kill',
|
|
112
|
+
},
|
|
105
113
|
{
|
|
106
114
|
command: '/clear',
|
|
107
115
|
description: 'Clear conversation history',
|
|
@@ -643,12 +651,33 @@ function buildWorkingToolEntry(event) {
|
|
|
643
651
|
: '';
|
|
644
652
|
if (call.name === 'run_command') {
|
|
645
653
|
const command = String(call.args?.command ?? result?.command ?? '').trim();
|
|
654
|
+
if (result?.backgrounded === true && result?.jobId) {
|
|
655
|
+
return {
|
|
656
|
+
body: `$ ${truncate(command, 180)}\nBackground job ${String(result.jobId)} started.${error}`,
|
|
657
|
+
kind: result?.ok === true ? 'tool' : 'error',
|
|
658
|
+
preformatted: true,
|
|
659
|
+
title: 'Shell',
|
|
660
|
+
};
|
|
661
|
+
}
|
|
646
662
|
return {
|
|
647
663
|
body: `$ ${truncate(command, 180)}\n${formatRunCommandResultState(result)}${error}`,
|
|
648
664
|
kind: result?.ok === true ? 'tool' : 'error',
|
|
649
665
|
title: 'Shell',
|
|
650
666
|
};
|
|
651
667
|
}
|
|
668
|
+
if (call.name === 'shell_job_output' || call.name === 'shell_job_kill') {
|
|
669
|
+
const jobId = String(result?.jobId ?? call.args?.job_id ?? '').trim();
|
|
670
|
+
const status = String(result?.status ?? '').trim();
|
|
671
|
+
const exitCode = result?.exitCode;
|
|
672
|
+
const stateText = status
|
|
673
|
+
? `${status}${exitCode != null ? ` (code ${exitCode})` : ''}`
|
|
674
|
+
: formatToolResultState(result);
|
|
675
|
+
return {
|
|
676
|
+
body: `${jobId || '(unknown job)'} · ${stateText}${error}`,
|
|
677
|
+
kind: result?.ok === true ? 'tool' : 'error',
|
|
678
|
+
title: call.name === 'shell_job_kill' ? 'Kill job' : 'Job output',
|
|
679
|
+
};
|
|
680
|
+
}
|
|
652
681
|
if (call.name === 'run_node_script') {
|
|
653
682
|
return {
|
|
654
683
|
body: `node --input-type=module <script via stdin>\n${formatRunCommandResultState(result)}${error}`,
|
|
@@ -666,6 +695,32 @@ function buildWorkingToolEntry(event) {
|
|
|
666
695
|
title: 'Tool',
|
|
667
696
|
};
|
|
668
697
|
}
|
|
698
|
+
function buildBackgroundJobNoticeEntry(snapshot) {
|
|
699
|
+
const command = truncate(snapshot.command, 120);
|
|
700
|
+
const ran = formatJobElapsed((snapshot.endedAt ?? Date.now()) - snapshot.startedAt);
|
|
701
|
+
if (snapshot.status === 'error') {
|
|
702
|
+
return {
|
|
703
|
+
body: `✖ ${snapshot.id} (${command}) failed to start`,
|
|
704
|
+
kind: 'error',
|
|
705
|
+
preformatted: true,
|
|
706
|
+
title: 'Background job',
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (snapshot.status === 'killed') {
|
|
710
|
+
return {
|
|
711
|
+
body: `■ ${snapshot.id} (${command}) killed · ran ${ran}`,
|
|
712
|
+
kind: 'system',
|
|
713
|
+
preformatted: true,
|
|
714
|
+
title: 'Background job',
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
body: `${snapshot.exitCode === 0 ? '✓' : '✖'} ${snapshot.id} (${command}) exited (code ${snapshot.exitCode ?? 1}) · ran ${ran}`,
|
|
719
|
+
kind: snapshot.exitCode === 0 ? 'system' : 'error',
|
|
720
|
+
preformatted: true,
|
|
721
|
+
title: 'Background job',
|
|
722
|
+
};
|
|
723
|
+
}
|
|
669
724
|
function findPendingToolCallByName(pendingCalls, name) {
|
|
670
725
|
for (const [id, call] of pendingCalls) {
|
|
671
726
|
if (call.name !== name)
|
|
@@ -814,11 +869,16 @@ function createShellStore(initialState) {
|
|
|
814
869
|
},
|
|
815
870
|
appendWorkingTool: (entry) => {
|
|
816
871
|
const lastEntry = state.workingTools.at(-1);
|
|
817
|
-
|
|
872
|
+
const isDuplicate = sameTranscriptDraft(lastEntry, entry);
|
|
873
|
+
if (isDuplicate && state.commandLog.length === 0) {
|
|
818
874
|
return;
|
|
875
|
+
}
|
|
819
876
|
state = {
|
|
820
877
|
...state,
|
|
821
|
-
|
|
878
|
+
commandLog: [],
|
|
879
|
+
workingTools: isDuplicate
|
|
880
|
+
? state.workingTools
|
|
881
|
+
: [...state.workingTools, entry].slice(-WORKING_TOOL_PREVIEW_ITEMS),
|
|
822
882
|
};
|
|
823
883
|
notify();
|
|
824
884
|
},
|
|
@@ -868,6 +928,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
868
928
|
approvalCursor: getDefaultApprovalCursor(),
|
|
869
929
|
approvalPrompt: null,
|
|
870
930
|
autoYes: session.autoYes,
|
|
931
|
+
backgroundJobs: [],
|
|
871
932
|
busy: false,
|
|
872
933
|
busySince: null,
|
|
873
934
|
clockNow: Date.now(),
|
|
@@ -879,6 +940,9 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
879
940
|
exiting: false,
|
|
880
941
|
input: '',
|
|
881
942
|
maxToolSteps: session.maxToolSteps,
|
|
943
|
+
jobsPickerExpandedId: null,
|
|
944
|
+
jobsPickerIndex: 0,
|
|
945
|
+
jobsPickerOpen: false,
|
|
882
946
|
modelPickerIndex: getDefaultModelPickerIndex(session.modelId, serverModels.models),
|
|
883
947
|
modelPickerOpen: false,
|
|
884
948
|
projectRoot: session.rootDir,
|
|
@@ -899,6 +963,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
899
963
|
exitConfirmUntil: null,
|
|
900
964
|
thinkingTitle: '',
|
|
901
965
|
thinkingNotes: [],
|
|
966
|
+
todos: listTodos(),
|
|
902
967
|
tokenUsage: formatClientTokenUsage(null),
|
|
903
968
|
transcript: [],
|
|
904
969
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
@@ -951,6 +1016,14 @@ function thinkingNoteFromStatus(status) {
|
|
|
951
1016
|
const note = text.slice('Thinking:'.length).trim();
|
|
952
1017
|
return note ? note : null;
|
|
953
1018
|
}
|
|
1019
|
+
function stripMarkdownEmphasis(text) {
|
|
1020
|
+
return text
|
|
1021
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
|
|
1022
|
+
.replace(/___(.+?)___/g, '$1')
|
|
1023
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
1024
|
+
.replace(/__(.+?)__/g, '$1')
|
|
1025
|
+
.replace(/(?<!\*)\*(?!\*)([^*\n]+?)\*(?!\*)/g, '$1');
|
|
1026
|
+
}
|
|
954
1027
|
function splitThinkingLines(text) {
|
|
955
1028
|
return text
|
|
956
1029
|
.split('\n')
|
|
@@ -959,13 +1032,13 @@ function splitThinkingLines(text) {
|
|
|
959
1032
|
.flatMap((line) => line
|
|
960
1033
|
.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/)
|
|
961
1034
|
.map((part) => part.trim())
|
|
962
|
-
.filter(Boolean))
|
|
963
|
-
.map((line) => truncate(line, 120));
|
|
1035
|
+
.filter(Boolean));
|
|
964
1036
|
}
|
|
965
1037
|
function thinkingPanelFromStatus(status) {
|
|
966
|
-
const
|
|
967
|
-
if (!
|
|
1038
|
+
const rawText = thinkingNoteFromStatus(status);
|
|
1039
|
+
if (!rawText)
|
|
968
1040
|
return null;
|
|
1041
|
+
const text = stripMarkdownEmphasis(rawText);
|
|
969
1042
|
const rawLines = text
|
|
970
1043
|
.split('\n')
|
|
971
1044
|
.map((line) => line.trim())
|
|
@@ -974,12 +1047,12 @@ function thinkingPanelFromStatus(status) {
|
|
|
974
1047
|
return null;
|
|
975
1048
|
if (rawLines.length === 1 && rawLines[0].length <= 72) {
|
|
976
1049
|
return {
|
|
977
|
-
title:
|
|
1050
|
+
title: rawLines[0],
|
|
978
1051
|
notes: [],
|
|
979
1052
|
};
|
|
980
1053
|
}
|
|
981
1054
|
const title = rawLines.length > 1 && rawLines[0].length <= 72
|
|
982
|
-
?
|
|
1055
|
+
? rawLines[0]
|
|
983
1056
|
: 'Thinking';
|
|
984
1057
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
985
1058
|
return {
|
|
@@ -997,7 +1070,10 @@ export function getInputCommandToken(input) {
|
|
|
997
1070
|
if (!trimmed.startsWith('/'))
|
|
998
1071
|
return '';
|
|
999
1072
|
const firstSpaceIndex = trimmed.indexOf(' ');
|
|
1000
|
-
|
|
1073
|
+
const token = firstSpaceIndex === -1 ? trimmed : trimmed.slice(0, firstSpaceIndex);
|
|
1074
|
+
if (token.indexOf('/', 1) !== -1)
|
|
1075
|
+
return '';
|
|
1076
|
+
return token;
|
|
1001
1077
|
}
|
|
1002
1078
|
function shouldShowCommandPalette(state) {
|
|
1003
1079
|
const trimmed = String(state.input ?? '').trim();
|
|
@@ -1006,7 +1082,8 @@ function shouldShowCommandPalette(state) {
|
|
|
1006
1082
|
!state.modelPickerOpen &&
|
|
1007
1083
|
!state.resumePickerOpen &&
|
|
1008
1084
|
trimmed.startsWith('/') &&
|
|
1009
|
-
!trimmed.includes(' ')
|
|
1085
|
+
!trimmed.includes(' ') &&
|
|
1086
|
+
getInputCommandToken(trimmed) !== '');
|
|
1010
1087
|
}
|
|
1011
1088
|
export function shouldRemountLiveFrameForComposerInputChange(current, nextInput) {
|
|
1012
1089
|
const currentShowsCommands = shouldShowCommandPalette(current);
|
|
@@ -1179,6 +1256,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1179
1256
|
throw new Error('Client TUI requires an interactive terminal.');
|
|
1180
1257
|
}
|
|
1181
1258
|
await withTuiMode(async () => {
|
|
1259
|
+
setBackgroundJobSession(session.sessionId);
|
|
1260
|
+
setTodoSession(session.sessionId);
|
|
1182
1261
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1183
1262
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1184
1263
|
let currentServerModels = serverModels;
|
|
@@ -1201,6 +1280,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1201
1280
|
let latestUsageSummary = null;
|
|
1202
1281
|
let pendingTurnEntries = [];
|
|
1203
1282
|
let activeTurnAbort = null;
|
|
1283
|
+
let todosTouchedThisTurn = false;
|
|
1284
|
+
const syncTodosState = () => {
|
|
1285
|
+
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
1286
|
+
};
|
|
1204
1287
|
let activeTurnGeneration = 0;
|
|
1205
1288
|
let exitCtrlCArmed = false;
|
|
1206
1289
|
let exitCtrlCTimer = null;
|
|
@@ -1229,14 +1312,23 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1229
1312
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1230
1313
|
}, 2500);
|
|
1231
1314
|
};
|
|
1315
|
+
const terminalTitle = createTerminalTitleController();
|
|
1316
|
+
const syncTerminalTitle = () => {
|
|
1317
|
+
const state = store.getState();
|
|
1318
|
+
terminalTitle.sync({
|
|
1319
|
+
awaitingReview: Boolean(state.approvalPrompt || state.sudoPrompt),
|
|
1320
|
+
busy: state.busy,
|
|
1321
|
+
});
|
|
1322
|
+
};
|
|
1232
1323
|
const renderCurrentFrame = () => {
|
|
1233
1324
|
if (!tuiReady)
|
|
1234
1325
|
return;
|
|
1235
1326
|
const state = store.getState();
|
|
1327
|
+
syncTerminalTitle();
|
|
1236
1328
|
const elapsedSeconds = state.busySince
|
|
1237
1329
|
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1238
1330
|
: 0;
|
|
1239
|
-
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds));
|
|
1331
|
+
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1240
1332
|
};
|
|
1241
1333
|
const remountTui = async () => {
|
|
1242
1334
|
if (remountPromise) {
|
|
@@ -1396,9 +1488,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1396
1488
|
activeTurnAbort?.abort();
|
|
1397
1489
|
activeTurnAbort = null;
|
|
1398
1490
|
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
1491
|
const queued = store.getState().queuedMessage;
|
|
1403
1492
|
const cancelledEntries = [
|
|
1404
1493
|
...takePendingTurnEntries(),
|
|
@@ -1521,6 +1610,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1521
1610
|
dismissPendingApproval();
|
|
1522
1611
|
dismissPendingSudoPassword();
|
|
1523
1612
|
exiting = true;
|
|
1613
|
+
killAllBackgroundJobs();
|
|
1524
1614
|
store.update((current) => ({
|
|
1525
1615
|
...current,
|
|
1526
1616
|
exiting: true,
|
|
@@ -1675,6 +1765,87 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1675
1765
|
title: 'Model',
|
|
1676
1766
|
});
|
|
1677
1767
|
};
|
|
1768
|
+
const openJobsPicker = () => {
|
|
1769
|
+
syncBackgroundJobsState();
|
|
1770
|
+
store.update((current) => ({
|
|
1771
|
+
...current,
|
|
1772
|
+
commandCursor: 0,
|
|
1773
|
+
cursor: 0,
|
|
1774
|
+
input: '',
|
|
1775
|
+
jobsPickerExpandedId: null,
|
|
1776
|
+
jobsPickerIndex: 0,
|
|
1777
|
+
jobsPickerOpen: true,
|
|
1778
|
+
status: 'Background jobs',
|
|
1779
|
+
}));
|
|
1780
|
+
};
|
|
1781
|
+
const appendJobOutputEntry = async (jobId) => {
|
|
1782
|
+
const id = jobId.trim();
|
|
1783
|
+
await collectBackgroundJobUiOutputMutations({
|
|
1784
|
+
session,
|
|
1785
|
+
projectIndex,
|
|
1786
|
+
jobId: id,
|
|
1787
|
+
});
|
|
1788
|
+
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
1789
|
+
const buffered = getJobBufferedOutput(id);
|
|
1790
|
+
if (!job || !buffered) {
|
|
1791
|
+
appendError(`Unknown background job id: ${id}`);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const dropped = buffered.droppedChars > 0
|
|
1795
|
+
? `... (${buffered.droppedChars} chars of older output dropped) ...\n`
|
|
1796
|
+
: '';
|
|
1797
|
+
appendStaticEntry({
|
|
1798
|
+
body: `${dropped}${buffered.output || '(no output captured)'}`,
|
|
1799
|
+
kind: 'system',
|
|
1800
|
+
preformatted: true,
|
|
1801
|
+
title: `${job.id} output — ${truncate(job.command, 80)}`,
|
|
1802
|
+
});
|
|
1803
|
+
};
|
|
1804
|
+
const selectedJobsPickerId = () => {
|
|
1805
|
+
const current = store.getState();
|
|
1806
|
+
if (!current.backgroundJobs.length)
|
|
1807
|
+
return null;
|
|
1808
|
+
const index = Math.min(Math.max(current.jobsPickerIndex, 0), current.backgroundJobs.length - 1);
|
|
1809
|
+
return current.backgroundJobs[index]?.id ?? null;
|
|
1810
|
+
};
|
|
1811
|
+
const handleJobsPickerOutput = async () => {
|
|
1812
|
+
const jobId = selectedJobsPickerId();
|
|
1813
|
+
if (!jobId)
|
|
1814
|
+
return;
|
|
1815
|
+
await collectBackgroundJobUiOutputMutations({
|
|
1816
|
+
session,
|
|
1817
|
+
projectIndex,
|
|
1818
|
+
jobId,
|
|
1819
|
+
});
|
|
1820
|
+
syncBackgroundJobsState();
|
|
1821
|
+
store.update((current) => ({
|
|
1822
|
+
...current,
|
|
1823
|
+
jobsPickerExpandedId: current.jobsPickerExpandedId === jobId ? null : jobId,
|
|
1824
|
+
}));
|
|
1825
|
+
};
|
|
1826
|
+
const handleJobsPickerKill = async () => {
|
|
1827
|
+
const jobId = selectedJobsPickerId();
|
|
1828
|
+
if (!jobId)
|
|
1829
|
+
return;
|
|
1830
|
+
const killed = await killBackgroundJob(jobId);
|
|
1831
|
+
await collectBackgroundJobUiKillMutations({
|
|
1832
|
+
session,
|
|
1833
|
+
projectIndex,
|
|
1834
|
+
jobId,
|
|
1835
|
+
result: killed,
|
|
1836
|
+
});
|
|
1837
|
+
syncBackgroundJobsState();
|
|
1838
|
+
if (!killed.ok) {
|
|
1839
|
+
appendError(killed.error ?? 'Background job kill failed.');
|
|
1840
|
+
}
|
|
1841
|
+
else if (killed.alreadyFinished) {
|
|
1842
|
+
appendStaticEntry({
|
|
1843
|
+
body: `${jobId} had already finished.`,
|
|
1844
|
+
kind: 'system',
|
|
1845
|
+
title: 'Background jobs',
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1678
1849
|
const handleInlineModelSelection = async () => {
|
|
1679
1850
|
const current = store.getState();
|
|
1680
1851
|
const options = buildModelPickerOptions(current.currentModelId, current.serverModels);
|
|
@@ -1761,6 +1932,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1761
1932
|
try {
|
|
1762
1933
|
const snapshot = await loadInteractiveSession(selected.id);
|
|
1763
1934
|
applySessionSnapshot(session, snapshot);
|
|
1935
|
+
setBackgroundJobSession(session.sessionId);
|
|
1936
|
+
syncBackgroundJobsState();
|
|
1937
|
+
setTodoSession(session.sessionId);
|
|
1938
|
+
syncTodosState();
|
|
1764
1939
|
await saveActiveSession();
|
|
1765
1940
|
syncShellStateFromSession();
|
|
1766
1941
|
store.update((next) => ({
|
|
@@ -1802,8 +1977,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1802
1977
|
if (!queued || store.getState().busy || exiting) {
|
|
1803
1978
|
return;
|
|
1804
1979
|
}
|
|
1805
|
-
// Rehydrate attachments/chunks before submit: handleSubmit expands
|
|
1806
|
-
// pastedChunks from the store and the turn picks up imageAttachments.
|
|
1807
1980
|
store.update((current) => ({
|
|
1808
1981
|
...current,
|
|
1809
1982
|
imageAttachments: queued.imageAttachments,
|
|
@@ -1830,9 +2003,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1830
2003
|
});
|
|
1831
2004
|
return;
|
|
1832
2005
|
}
|
|
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
2006
|
const pending = store.getState();
|
|
1837
2007
|
const snapshot = {
|
|
1838
2008
|
body: String(rawInput ?? ''),
|
|
@@ -1887,6 +2057,46 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1887
2057
|
});
|
|
1888
2058
|
return;
|
|
1889
2059
|
}
|
|
2060
|
+
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2061
|
+
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2062
|
+
if (!jobsArgs) {
|
|
2063
|
+
openJobsPicker();
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
const killMatch = jobsArgs.match(/^kill\s+(\S+)$/);
|
|
2067
|
+
if (killMatch) {
|
|
2068
|
+
const jobId = killMatch[1];
|
|
2069
|
+
const killed = await killBackgroundJob(jobId);
|
|
2070
|
+
await collectBackgroundJobUiKillMutations({
|
|
2071
|
+
session,
|
|
2072
|
+
projectIndex,
|
|
2073
|
+
jobId,
|
|
2074
|
+
result: killed,
|
|
2075
|
+
});
|
|
2076
|
+
if (!killed.ok) {
|
|
2077
|
+
appendError(killed.error ?? 'Background job kill failed.');
|
|
2078
|
+
}
|
|
2079
|
+
else if (killed.alreadyFinished) {
|
|
2080
|
+
appendStaticEntry({
|
|
2081
|
+
body: `${jobId} had already finished.`,
|
|
2082
|
+
kind: 'system',
|
|
2083
|
+
title: 'Background jobs',
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
const outputMatch = jobsArgs.match(/^output\s+(\S+)$/);
|
|
2089
|
+
if (outputMatch) {
|
|
2090
|
+
await appendJobOutputEntry(outputMatch[1]);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
appendStaticEntry({
|
|
2094
|
+
body: 'Usage: /jobs — open the jobs picker · /jobs output <id> — full output · /jobs kill <id> — kill one',
|
|
2095
|
+
kind: 'system',
|
|
2096
|
+
title: 'Background jobs',
|
|
2097
|
+
});
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
1890
2100
|
if (input === '/usage') {
|
|
1891
2101
|
store.update((current) => ({
|
|
1892
2102
|
...current,
|
|
@@ -1962,6 +2172,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1962
2172
|
}
|
|
1963
2173
|
if (input === '/clear') {
|
|
1964
2174
|
clearConversation(session);
|
|
2175
|
+
clearTodos();
|
|
2176
|
+
syncTodosState();
|
|
1965
2177
|
latestUsageSummary = null;
|
|
1966
2178
|
await saveActiveSession();
|
|
1967
2179
|
store.replaceTranscript([
|
|
@@ -1983,6 +2195,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1983
2195
|
const turnAbort = new AbortController();
|
|
1984
2196
|
activeTurnAbort = turnAbort;
|
|
1985
2197
|
lastTurnStartedAt = turnStartedAt;
|
|
2198
|
+
todosTouchedThisTurn = false;
|
|
1986
2199
|
const userEntry = {
|
|
1987
2200
|
body: input,
|
|
1988
2201
|
kind: 'user',
|
|
@@ -2037,6 +2250,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2037
2250
|
await appendSettledTurnEntries(turnEntries, result.text ?? '', () => turnGeneration === activeTurnGeneration && !turnAbort.signal.aborted);
|
|
2038
2251
|
if (turnGeneration !== activeTurnGeneration)
|
|
2039
2252
|
return;
|
|
2253
|
+
if (todosTouchedThisTurn) {
|
|
2254
|
+
todosTouchedThisTurn = false;
|
|
2255
|
+
const todoItems = listTodos();
|
|
2256
|
+
if (todoItems.length > 0) {
|
|
2257
|
+
appendStaticEntry({
|
|
2258
|
+
body: '',
|
|
2259
|
+
kind: 'system',
|
|
2260
|
+
title: `To-dos · ${formatTodoProgress(todoItems)}`,
|
|
2261
|
+
todoList: todoItems,
|
|
2262
|
+
});
|
|
2263
|
+
if (todoItems.every((item) => item.status === 'completed')) {
|
|
2264
|
+
clearTodos();
|
|
2265
|
+
syncTodosState();
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2040
2269
|
store.update((current) => ({
|
|
2041
2270
|
...current,
|
|
2042
2271
|
busy: false,
|
|
@@ -2085,8 +2314,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2085
2314
|
appendError(error.message);
|
|
2086
2315
|
}
|
|
2087
2316
|
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
2317
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2091
2318
|
await flushQueuedMessage();
|
|
2092
2319
|
}
|
|
@@ -2119,6 +2346,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2119
2346
|
};
|
|
2120
2347
|
session.onToolEvent = (event) => {
|
|
2121
2348
|
syncShellStateFromSession();
|
|
2349
|
+
if (event.call.name === 'update_todos') {
|
|
2350
|
+
syncTodosState();
|
|
2351
|
+
if (event.result?.ok === true) {
|
|
2352
|
+
todosTouchedThisTurn = true;
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2122
2356
|
if (isFileChangeTool(event.call.name)) {
|
|
2123
2357
|
const entry = buildFileChangeEntry(event);
|
|
2124
2358
|
store.appendWorkingTool(entry);
|
|
@@ -2127,6 +2361,52 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2127
2361
|
}
|
|
2128
2362
|
store.appendWorkingTool(buildWorkingToolEntry(event));
|
|
2129
2363
|
};
|
|
2364
|
+
const jobDetailLines = (jobId) => {
|
|
2365
|
+
const buffered = getJobBufferedOutput(jobId);
|
|
2366
|
+
if (!buffered)
|
|
2367
|
+
return [];
|
|
2368
|
+
const lines = buffered.output
|
|
2369
|
+
.replace(/\r\n/g, '\n')
|
|
2370
|
+
.replace(/\r/g, '\n')
|
|
2371
|
+
.split('\n')
|
|
2372
|
+
.filter((line) => line.trim().length > 0);
|
|
2373
|
+
const visible = lines.slice(-20);
|
|
2374
|
+
return buffered.droppedChars > 0
|
|
2375
|
+
? [
|
|
2376
|
+
`... (${buffered.droppedChars} chars of older output dropped) ...`,
|
|
2377
|
+
...visible,
|
|
2378
|
+
]
|
|
2379
|
+
: visible;
|
|
2380
|
+
};
|
|
2381
|
+
const syncBackgroundJobsState = () => {
|
|
2382
|
+
const jobsForDisplay = listBackgroundJobs().map((job) => {
|
|
2383
|
+
const preview = getJobOutputPreview(job.id, 3);
|
|
2384
|
+
return {
|
|
2385
|
+
id: job.id,
|
|
2386
|
+
command: job.command,
|
|
2387
|
+
status: job.status,
|
|
2388
|
+
exitCode: job.exitCode,
|
|
2389
|
+
startedAt: job.startedAt,
|
|
2390
|
+
endedAt: job.endedAt,
|
|
2391
|
+
firstOutputLine: preview?.firstLine ?? '',
|
|
2392
|
+
tailLines: preview?.tailLines ?? [],
|
|
2393
|
+
detailLines: jobDetailLines(job.id),
|
|
2394
|
+
};
|
|
2395
|
+
});
|
|
2396
|
+
store.update((current) => ({
|
|
2397
|
+
...current,
|
|
2398
|
+
backgroundJobs: jobsForDisplay,
|
|
2399
|
+
jobsPickerExpandedId: jobsForDisplay.some((job) => job.id === current.jobsPickerExpandedId)
|
|
2400
|
+
? current.jobsPickerExpandedId
|
|
2401
|
+
: null,
|
|
2402
|
+
}));
|
|
2403
|
+
};
|
|
2404
|
+
setBackgroundJobUpdateHook((snapshot) => {
|
|
2405
|
+
syncBackgroundJobsState();
|
|
2406
|
+
if (snapshot.status !== 'running') {
|
|
2407
|
+
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2130
2410
|
projectIndex.onStatus = session.onStatus;
|
|
2131
2411
|
projectIndex.onContextLog = session.onContextLog;
|
|
2132
2412
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
@@ -2186,6 +2466,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2186
2466
|
onResumeSession: handleInlineResumeSelection,
|
|
2187
2467
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2188
2468
|
onSelectModel: handleInlineModelSelection,
|
|
2469
|
+
onJobsPickerOutput: handleJobsPickerOutput,
|
|
2470
|
+
onJobsPickerKill: handleJobsPickerKill,
|
|
2189
2471
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2190
2472
|
onSubmit: handleSubmit,
|
|
2191
2473
|
};
|
|
@@ -2199,7 +2481,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2199
2481
|
}
|
|
2200
2482
|
}, 120);
|
|
2201
2483
|
const clockTimer = setInterval(() => {
|
|
2202
|
-
if (
|
|
2484
|
+
if (hasRunningBackgroundJobs()) {
|
|
2485
|
+
syncBackgroundJobsState();
|
|
2486
|
+
}
|
|
2487
|
+
if (store.getState().busy || hasRunningBackgroundJobs()) {
|
|
2203
2488
|
renderCurrentFrame();
|
|
2204
2489
|
}
|
|
2205
2490
|
}, 1000);
|
|
@@ -2243,6 +2528,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2243
2528
|
clearInterval(clockTimer);
|
|
2244
2529
|
unsubscribe();
|
|
2245
2530
|
clearLiveFrameRemountTimer();
|
|
2531
|
+
terminalTitle.dispose();
|
|
2532
|
+
killAllBackgroundJobs();
|
|
2533
|
+
setBackgroundJobSession(null);
|
|
2534
|
+
setTodoSession(null);
|
|
2535
|
+
setBackgroundJobUpdateHook(null);
|
|
2246
2536
|
await bridge.close();
|
|
2247
2537
|
setCommandOutputHook(null);
|
|
2248
2538
|
}
|
|
@@ -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', () => {
|