@thegitai/cli 1.0.0-beta.2 → 1.0.0-beta.21
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 +37 -2
- package/dist/bin/ai.js +148 -75
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +7 -41
- package/dist/src/api/chat.js +77 -20
- package/dist/src/api/http.js +81 -4
- package/dist/src/api/models.js +26 -18
- package/dist/src/artifact-policy.js +12 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +60 -0
- package/dist/src/client-environment.js +129 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +75 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/edit-journal.js +39 -6
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +24 -5
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +17 -2
- package/dist/src/scanner.js +58 -17
- package/dist/src/scratch-dir.js +57 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +64 -31
- package/dist/src/session-store.js +0 -1
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +164 -18
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +8 -0
- package/dist/src/tools/patch-file.js +16 -2
- package/dist/src/tools/path-suggest.js +139 -0
- package/dist/src/tools/read-document.js +15 -4
- package/dist/src/tools/read-file.js +23 -7
- package/dist/src/tools/replace-document-text.js +234 -0
- 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 +16 -2
- package/dist/src/tools/undo-edit.js +7 -5
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +14 -1
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +315 -24
- package/dist/src/ui/tui/bridge.js +2 -6
- package/dist/src/ui/tui/build-frame.js +225 -25
- package/dist/src/ui/tui/shell-input.js +42 -5
- package/dist/src/version.js +29 -0
- 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,17 +1,20 @@
|
|
|
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
3
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
4
4
|
import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
5
5
|
import { chat, models } from '../api/index.js';
|
|
6
6
|
import { isTurnCancelledError } from '../api/chat.js';
|
|
7
|
+
import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
|
|
8
|
+
import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
7
9
|
import { cancelActiveCommand } from '../executor.js';
|
|
8
10
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
11
|
+
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
9
12
|
import { clearConversation, } from '../session.js';
|
|
10
13
|
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
|
|
11
14
|
import { truncate } from '../utils.js';
|
|
12
15
|
import { writeClipboardText } from '../core/clipboard.js';
|
|
13
16
|
import { openUrl } from '../core/open-url.js';
|
|
14
|
-
import { formatInteractiveHelpText } from '../help-text.js';
|
|
17
|
+
import { formatAboutCard, formatInteractiveHelpText } from '../help-text.js';
|
|
15
18
|
import { expandPastedChunks, } from './paste-collapse.js';
|
|
16
19
|
import { loadPromptHistory, MAX_PROMPT_HISTORY_ENTRIES, } from './prompt-history-store.js';
|
|
17
20
|
const RESPONSE_STREAM_CHUNK_SIZE = 4;
|
|
@@ -86,6 +89,10 @@ export const SLASH_COMMANDS = [
|
|
|
86
89
|
command: '/help',
|
|
87
90
|
description: 'Show available chat commands',
|
|
88
91
|
},
|
|
92
|
+
{
|
|
93
|
+
command: '/about',
|
|
94
|
+
description: 'Show version and platform info',
|
|
95
|
+
},
|
|
89
96
|
{
|
|
90
97
|
command: '/usage',
|
|
91
98
|
description: 'Show account usage percentage and reset times',
|
|
@@ -98,6 +105,10 @@ export const SLASH_COMMANDS = [
|
|
|
98
105
|
command: '/resume',
|
|
99
106
|
description: 'Open the session picker',
|
|
100
107
|
},
|
|
108
|
+
{
|
|
109
|
+
command: '/jobs',
|
|
110
|
+
description: 'Background jobs: pick to view output or kill',
|
|
111
|
+
},
|
|
101
112
|
{
|
|
102
113
|
command: '/clear',
|
|
103
114
|
description: 'Clear conversation history',
|
|
@@ -639,12 +650,33 @@ function buildWorkingToolEntry(event) {
|
|
|
639
650
|
: '';
|
|
640
651
|
if (call.name === 'run_command') {
|
|
641
652
|
const command = String(call.args?.command ?? result?.command ?? '').trim();
|
|
653
|
+
if (result?.backgrounded === true && result?.jobId) {
|
|
654
|
+
return {
|
|
655
|
+
body: `$ ${truncate(command, 180)}\nBackground job ${String(result.jobId)} started.${error}`,
|
|
656
|
+
kind: result?.ok === true ? 'tool' : 'error',
|
|
657
|
+
preformatted: true,
|
|
658
|
+
title: 'Shell',
|
|
659
|
+
};
|
|
660
|
+
}
|
|
642
661
|
return {
|
|
643
662
|
body: `$ ${truncate(command, 180)}\n${formatRunCommandResultState(result)}${error}`,
|
|
644
663
|
kind: result?.ok === true ? 'tool' : 'error',
|
|
645
664
|
title: 'Shell',
|
|
646
665
|
};
|
|
647
666
|
}
|
|
667
|
+
if (call.name === 'shell_job_output' || call.name === 'shell_job_kill') {
|
|
668
|
+
const jobId = String(result?.jobId ?? call.args?.job_id ?? '').trim();
|
|
669
|
+
const status = String(result?.status ?? '').trim();
|
|
670
|
+
const exitCode = result?.exitCode;
|
|
671
|
+
const stateText = status
|
|
672
|
+
? `${status}${exitCode != null ? ` (code ${exitCode})` : ''}`
|
|
673
|
+
: formatToolResultState(result);
|
|
674
|
+
return {
|
|
675
|
+
body: `${jobId || '(unknown job)'} · ${stateText}${error}`,
|
|
676
|
+
kind: result?.ok === true ? 'tool' : 'error',
|
|
677
|
+
title: call.name === 'shell_job_kill' ? 'Kill job' : 'Job output',
|
|
678
|
+
};
|
|
679
|
+
}
|
|
648
680
|
if (call.name === 'run_node_script') {
|
|
649
681
|
return {
|
|
650
682
|
body: `node --input-type=module <script via stdin>\n${formatRunCommandResultState(result)}${error}`,
|
|
@@ -662,6 +694,32 @@ function buildWorkingToolEntry(event) {
|
|
|
662
694
|
title: 'Tool',
|
|
663
695
|
};
|
|
664
696
|
}
|
|
697
|
+
function buildBackgroundJobNoticeEntry(snapshot) {
|
|
698
|
+
const command = truncate(snapshot.command, 120);
|
|
699
|
+
const ran = formatJobElapsed((snapshot.endedAt ?? Date.now()) - snapshot.startedAt);
|
|
700
|
+
if (snapshot.status === 'error') {
|
|
701
|
+
return {
|
|
702
|
+
body: `✖ ${snapshot.id} (${command}) failed to start`,
|
|
703
|
+
kind: 'error',
|
|
704
|
+
preformatted: true,
|
|
705
|
+
title: 'Background job',
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
if (snapshot.status === 'killed') {
|
|
709
|
+
return {
|
|
710
|
+
body: `■ ${snapshot.id} (${command}) killed · ran ${ran}`,
|
|
711
|
+
kind: 'system',
|
|
712
|
+
preformatted: true,
|
|
713
|
+
title: 'Background job',
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
body: `${snapshot.exitCode === 0 ? '✓' : '✖'} ${snapshot.id} (${command}) exited (code ${snapshot.exitCode ?? 1}) · ran ${ran}`,
|
|
718
|
+
kind: snapshot.exitCode === 0 ? 'system' : 'error',
|
|
719
|
+
preformatted: true,
|
|
720
|
+
title: 'Background job',
|
|
721
|
+
};
|
|
722
|
+
}
|
|
665
723
|
function findPendingToolCallByName(pendingCalls, name) {
|
|
666
724
|
for (const [id, call] of pendingCalls) {
|
|
667
725
|
if (call.name !== name)
|
|
@@ -810,11 +868,16 @@ function createShellStore(initialState) {
|
|
|
810
868
|
},
|
|
811
869
|
appendWorkingTool: (entry) => {
|
|
812
870
|
const lastEntry = state.workingTools.at(-1);
|
|
813
|
-
|
|
871
|
+
const isDuplicate = sameTranscriptDraft(lastEntry, entry);
|
|
872
|
+
if (isDuplicate && state.commandLog.length === 0) {
|
|
814
873
|
return;
|
|
874
|
+
}
|
|
815
875
|
state = {
|
|
816
876
|
...state,
|
|
817
|
-
|
|
877
|
+
commandLog: [],
|
|
878
|
+
workingTools: isDuplicate
|
|
879
|
+
? state.workingTools
|
|
880
|
+
: [...state.workingTools, entry].slice(-WORKING_TOOL_PREVIEW_ITEMS),
|
|
818
881
|
};
|
|
819
882
|
notify();
|
|
820
883
|
},
|
|
@@ -864,6 +927,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
864
927
|
approvalCursor: getDefaultApprovalCursor(),
|
|
865
928
|
approvalPrompt: null,
|
|
866
929
|
autoYes: session.autoYes,
|
|
930
|
+
backgroundJobs: [],
|
|
867
931
|
busy: false,
|
|
868
932
|
busySince: null,
|
|
869
933
|
clockNow: Date.now(),
|
|
@@ -875,6 +939,9 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
875
939
|
exiting: false,
|
|
876
940
|
input: '',
|
|
877
941
|
maxToolSteps: session.maxToolSteps,
|
|
942
|
+
jobsPickerExpandedId: null,
|
|
943
|
+
jobsPickerIndex: 0,
|
|
944
|
+
jobsPickerOpen: false,
|
|
878
945
|
modelPickerIndex: getDefaultModelPickerIndex(session.modelId, serverModels.models),
|
|
879
946
|
modelPickerOpen: false,
|
|
880
947
|
projectRoot: session.rootDir,
|
|
@@ -895,6 +962,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
895
962
|
exitConfirmUntil: null,
|
|
896
963
|
thinkingTitle: '',
|
|
897
964
|
thinkingNotes: [],
|
|
965
|
+
todos: listTodos(),
|
|
898
966
|
tokenUsage: formatClientTokenUsage(null),
|
|
899
967
|
transcript: [],
|
|
900
968
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
@@ -947,6 +1015,14 @@ function thinkingNoteFromStatus(status) {
|
|
|
947
1015
|
const note = text.slice('Thinking:'.length).trim();
|
|
948
1016
|
return note ? note : null;
|
|
949
1017
|
}
|
|
1018
|
+
function stripMarkdownEmphasis(text) {
|
|
1019
|
+
return text
|
|
1020
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
|
|
1021
|
+
.replace(/___(.+?)___/g, '$1')
|
|
1022
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
1023
|
+
.replace(/__(.+?)__/g, '$1')
|
|
1024
|
+
.replace(/(?<!\*)\*(?!\*)([^*\n]+?)\*(?!\*)/g, '$1');
|
|
1025
|
+
}
|
|
950
1026
|
function splitThinkingLines(text) {
|
|
951
1027
|
return text
|
|
952
1028
|
.split('\n')
|
|
@@ -955,13 +1031,13 @@ function splitThinkingLines(text) {
|
|
|
955
1031
|
.flatMap((line) => line
|
|
956
1032
|
.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/)
|
|
957
1033
|
.map((part) => part.trim())
|
|
958
|
-
.filter(Boolean))
|
|
959
|
-
.map((line) => truncate(line, 120));
|
|
1034
|
+
.filter(Boolean));
|
|
960
1035
|
}
|
|
961
1036
|
function thinkingPanelFromStatus(status) {
|
|
962
|
-
const
|
|
963
|
-
if (!
|
|
1037
|
+
const rawText = thinkingNoteFromStatus(status);
|
|
1038
|
+
if (!rawText)
|
|
964
1039
|
return null;
|
|
1040
|
+
const text = stripMarkdownEmphasis(rawText);
|
|
965
1041
|
const rawLines = text
|
|
966
1042
|
.split('\n')
|
|
967
1043
|
.map((line) => line.trim())
|
|
@@ -970,12 +1046,12 @@ function thinkingPanelFromStatus(status) {
|
|
|
970
1046
|
return null;
|
|
971
1047
|
if (rawLines.length === 1 && rawLines[0].length <= 72) {
|
|
972
1048
|
return {
|
|
973
|
-
title:
|
|
1049
|
+
title: rawLines[0],
|
|
974
1050
|
notes: [],
|
|
975
1051
|
};
|
|
976
1052
|
}
|
|
977
1053
|
const title = rawLines.length > 1 && rawLines[0].length <= 72
|
|
978
|
-
?
|
|
1054
|
+
? rawLines[0]
|
|
979
1055
|
: 'Thinking';
|
|
980
1056
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
981
1057
|
return {
|
|
@@ -993,7 +1069,10 @@ export function getInputCommandToken(input) {
|
|
|
993
1069
|
if (!trimmed.startsWith('/'))
|
|
994
1070
|
return '';
|
|
995
1071
|
const firstSpaceIndex = trimmed.indexOf(' ');
|
|
996
|
-
|
|
1072
|
+
const token = firstSpaceIndex === -1 ? trimmed : trimmed.slice(0, firstSpaceIndex);
|
|
1073
|
+
if (token.indexOf('/', 1) !== -1)
|
|
1074
|
+
return '';
|
|
1075
|
+
return token;
|
|
997
1076
|
}
|
|
998
1077
|
function shouldShowCommandPalette(state) {
|
|
999
1078
|
const trimmed = String(state.input ?? '').trim();
|
|
@@ -1002,7 +1081,8 @@ function shouldShowCommandPalette(state) {
|
|
|
1002
1081
|
!state.modelPickerOpen &&
|
|
1003
1082
|
!state.resumePickerOpen &&
|
|
1004
1083
|
trimmed.startsWith('/') &&
|
|
1005
|
-
!trimmed.includes(' ')
|
|
1084
|
+
!trimmed.includes(' ') &&
|
|
1085
|
+
getInputCommandToken(trimmed) !== '');
|
|
1006
1086
|
}
|
|
1007
1087
|
export function shouldRemountLiveFrameForComposerInputChange(current, nextInput) {
|
|
1008
1088
|
const currentShowsCommands = shouldShowCommandPalette(current);
|
|
@@ -1175,6 +1255,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1175
1255
|
throw new Error('Client TUI requires an interactive terminal.');
|
|
1176
1256
|
}
|
|
1177
1257
|
await withTuiMode(async () => {
|
|
1258
|
+
setBackgroundJobSession(session.sessionId);
|
|
1259
|
+
setTodoSession(session.sessionId);
|
|
1178
1260
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1179
1261
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1180
1262
|
let currentServerModels = serverModels;
|
|
@@ -1197,6 +1279,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1197
1279
|
let latestUsageSummary = null;
|
|
1198
1280
|
let pendingTurnEntries = [];
|
|
1199
1281
|
let activeTurnAbort = null;
|
|
1282
|
+
let todosTouchedThisTurn = false;
|
|
1283
|
+
const syncTodosState = () => {
|
|
1284
|
+
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
1285
|
+
};
|
|
1200
1286
|
let activeTurnGeneration = 0;
|
|
1201
1287
|
let exitCtrlCArmed = false;
|
|
1202
1288
|
let exitCtrlCTimer = null;
|
|
@@ -1232,7 +1318,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1232
1318
|
const elapsedSeconds = state.busySince
|
|
1233
1319
|
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1234
1320
|
: 0;
|
|
1235
|
-
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds));
|
|
1321
|
+
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1236
1322
|
};
|
|
1237
1323
|
const remountTui = async () => {
|
|
1238
1324
|
if (remountPromise) {
|
|
@@ -1392,9 +1478,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1392
1478
|
activeTurnAbort?.abort();
|
|
1393
1479
|
activeTurnAbort = null;
|
|
1394
1480
|
cancelActiveCommand();
|
|
1395
|
-
// Ctrl+C with a queued message: recall it into the composer for editing
|
|
1396
|
-
// rather than auto-submitting it against the cancelled turn. (Esc with a
|
|
1397
|
-
// queued message clears the slot in shell-input without cancelling.)
|
|
1398
1481
|
const queued = store.getState().queuedMessage;
|
|
1399
1482
|
const cancelledEntries = [
|
|
1400
1483
|
...takePendingTurnEntries(),
|
|
@@ -1517,6 +1600,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1517
1600
|
dismissPendingApproval();
|
|
1518
1601
|
dismissPendingSudoPassword();
|
|
1519
1602
|
exiting = true;
|
|
1603
|
+
killAllBackgroundJobs();
|
|
1520
1604
|
store.update((current) => ({
|
|
1521
1605
|
...current,
|
|
1522
1606
|
exiting: true,
|
|
@@ -1671,6 +1755,87 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1671
1755
|
title: 'Model',
|
|
1672
1756
|
});
|
|
1673
1757
|
};
|
|
1758
|
+
const openJobsPicker = () => {
|
|
1759
|
+
syncBackgroundJobsState();
|
|
1760
|
+
store.update((current) => ({
|
|
1761
|
+
...current,
|
|
1762
|
+
commandCursor: 0,
|
|
1763
|
+
cursor: 0,
|
|
1764
|
+
input: '',
|
|
1765
|
+
jobsPickerExpandedId: null,
|
|
1766
|
+
jobsPickerIndex: 0,
|
|
1767
|
+
jobsPickerOpen: true,
|
|
1768
|
+
status: 'Background jobs',
|
|
1769
|
+
}));
|
|
1770
|
+
};
|
|
1771
|
+
const appendJobOutputEntry = async (jobId) => {
|
|
1772
|
+
const id = jobId.trim();
|
|
1773
|
+
await collectBackgroundJobUiOutputMutations({
|
|
1774
|
+
session,
|
|
1775
|
+
projectIndex,
|
|
1776
|
+
jobId: id,
|
|
1777
|
+
});
|
|
1778
|
+
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
1779
|
+
const buffered = getJobBufferedOutput(id);
|
|
1780
|
+
if (!job || !buffered) {
|
|
1781
|
+
appendError(`Unknown background job id: ${id}`);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const dropped = buffered.droppedChars > 0
|
|
1785
|
+
? `... (${buffered.droppedChars} chars of older output dropped) ...\n`
|
|
1786
|
+
: '';
|
|
1787
|
+
appendStaticEntry({
|
|
1788
|
+
body: `${dropped}${buffered.output || '(no output captured)'}`,
|
|
1789
|
+
kind: 'system',
|
|
1790
|
+
preformatted: true,
|
|
1791
|
+
title: `${job.id} output — ${truncate(job.command, 80)}`,
|
|
1792
|
+
});
|
|
1793
|
+
};
|
|
1794
|
+
const selectedJobsPickerId = () => {
|
|
1795
|
+
const current = store.getState();
|
|
1796
|
+
if (!current.backgroundJobs.length)
|
|
1797
|
+
return null;
|
|
1798
|
+
const index = Math.min(Math.max(current.jobsPickerIndex, 0), current.backgroundJobs.length - 1);
|
|
1799
|
+
return current.backgroundJobs[index]?.id ?? null;
|
|
1800
|
+
};
|
|
1801
|
+
const handleJobsPickerOutput = async () => {
|
|
1802
|
+
const jobId = selectedJobsPickerId();
|
|
1803
|
+
if (!jobId)
|
|
1804
|
+
return;
|
|
1805
|
+
await collectBackgroundJobUiOutputMutations({
|
|
1806
|
+
session,
|
|
1807
|
+
projectIndex,
|
|
1808
|
+
jobId,
|
|
1809
|
+
});
|
|
1810
|
+
syncBackgroundJobsState();
|
|
1811
|
+
store.update((current) => ({
|
|
1812
|
+
...current,
|
|
1813
|
+
jobsPickerExpandedId: current.jobsPickerExpandedId === jobId ? null : jobId,
|
|
1814
|
+
}));
|
|
1815
|
+
};
|
|
1816
|
+
const handleJobsPickerKill = async () => {
|
|
1817
|
+
const jobId = selectedJobsPickerId();
|
|
1818
|
+
if (!jobId)
|
|
1819
|
+
return;
|
|
1820
|
+
const killed = await killBackgroundJob(jobId);
|
|
1821
|
+
await collectBackgroundJobUiKillMutations({
|
|
1822
|
+
session,
|
|
1823
|
+
projectIndex,
|
|
1824
|
+
jobId,
|
|
1825
|
+
result: killed,
|
|
1826
|
+
});
|
|
1827
|
+
syncBackgroundJobsState();
|
|
1828
|
+
if (!killed.ok) {
|
|
1829
|
+
appendError(killed.error ?? 'Background job kill failed.');
|
|
1830
|
+
}
|
|
1831
|
+
else if (killed.alreadyFinished) {
|
|
1832
|
+
appendStaticEntry({
|
|
1833
|
+
body: `${jobId} had already finished.`,
|
|
1834
|
+
kind: 'system',
|
|
1835
|
+
title: 'Background jobs',
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1674
1839
|
const handleInlineModelSelection = async () => {
|
|
1675
1840
|
const current = store.getState();
|
|
1676
1841
|
const options = buildModelPickerOptions(current.currentModelId, current.serverModels);
|
|
@@ -1757,6 +1922,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1757
1922
|
try {
|
|
1758
1923
|
const snapshot = await loadInteractiveSession(selected.id);
|
|
1759
1924
|
applySessionSnapshot(session, snapshot);
|
|
1925
|
+
setBackgroundJobSession(session.sessionId);
|
|
1926
|
+
syncBackgroundJobsState();
|
|
1927
|
+
setTodoSession(session.sessionId);
|
|
1928
|
+
syncTodosState();
|
|
1760
1929
|
await saveActiveSession();
|
|
1761
1930
|
syncShellStateFromSession();
|
|
1762
1931
|
store.update((next) => ({
|
|
@@ -1798,8 +1967,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1798
1967
|
if (!queued || store.getState().busy || exiting) {
|
|
1799
1968
|
return;
|
|
1800
1969
|
}
|
|
1801
|
-
// Rehydrate attachments/chunks before submit: handleSubmit expands
|
|
1802
|
-
// pastedChunks from the store and the turn picks up imageAttachments.
|
|
1803
1970
|
store.update((current) => ({
|
|
1804
1971
|
...current,
|
|
1805
1972
|
imageAttachments: queued.imageAttachments,
|
|
@@ -1826,9 +1993,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1826
1993
|
});
|
|
1827
1994
|
return;
|
|
1828
1995
|
}
|
|
1829
|
-
// Snapshot the raw (placeholder) body plus chunks/images so recall and
|
|
1830
|
-
// flush round-trip the collapsed paste + attachments. Hold at most one;
|
|
1831
|
-
// a second enqueue replaces the slot.
|
|
1832
1996
|
const pending = store.getState();
|
|
1833
1997
|
const snapshot = {
|
|
1834
1998
|
body: String(rawInput ?? ''),
|
|
@@ -1875,6 +2039,54 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1875
2039
|
});
|
|
1876
2040
|
return;
|
|
1877
2041
|
}
|
|
2042
|
+
if (input === '/about') {
|
|
2043
|
+
appendStaticEntry({
|
|
2044
|
+
body: formatAboutCard(),
|
|
2045
|
+
kind: 'system',
|
|
2046
|
+
title: 'About',
|
|
2047
|
+
});
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2051
|
+
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2052
|
+
if (!jobsArgs) {
|
|
2053
|
+
openJobsPicker();
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
const killMatch = jobsArgs.match(/^kill\s+(\S+)$/);
|
|
2057
|
+
if (killMatch) {
|
|
2058
|
+
const jobId = killMatch[1];
|
|
2059
|
+
const killed = await killBackgroundJob(jobId);
|
|
2060
|
+
await collectBackgroundJobUiKillMutations({
|
|
2061
|
+
session,
|
|
2062
|
+
projectIndex,
|
|
2063
|
+
jobId,
|
|
2064
|
+
result: killed,
|
|
2065
|
+
});
|
|
2066
|
+
if (!killed.ok) {
|
|
2067
|
+
appendError(killed.error ?? 'Background job kill failed.');
|
|
2068
|
+
}
|
|
2069
|
+
else if (killed.alreadyFinished) {
|
|
2070
|
+
appendStaticEntry({
|
|
2071
|
+
body: `${jobId} had already finished.`,
|
|
2072
|
+
kind: 'system',
|
|
2073
|
+
title: 'Background jobs',
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
const outputMatch = jobsArgs.match(/^output\s+(\S+)$/);
|
|
2079
|
+
if (outputMatch) {
|
|
2080
|
+
await appendJobOutputEntry(outputMatch[1]);
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
appendStaticEntry({
|
|
2084
|
+
body: 'Usage: /jobs — open the jobs picker · /jobs output <id> — full output · /jobs kill <id> — kill one',
|
|
2085
|
+
kind: 'system',
|
|
2086
|
+
title: 'Background jobs',
|
|
2087
|
+
});
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
1878
2090
|
if (input === '/usage') {
|
|
1879
2091
|
store.update((current) => ({
|
|
1880
2092
|
...current,
|
|
@@ -1950,6 +2162,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1950
2162
|
}
|
|
1951
2163
|
if (input === '/clear') {
|
|
1952
2164
|
clearConversation(session);
|
|
2165
|
+
clearTodos();
|
|
2166
|
+
syncTodosState();
|
|
1953
2167
|
latestUsageSummary = null;
|
|
1954
2168
|
await saveActiveSession();
|
|
1955
2169
|
store.replaceTranscript([
|
|
@@ -1971,6 +2185,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1971
2185
|
const turnAbort = new AbortController();
|
|
1972
2186
|
activeTurnAbort = turnAbort;
|
|
1973
2187
|
lastTurnStartedAt = turnStartedAt;
|
|
2188
|
+
todosTouchedThisTurn = false;
|
|
1974
2189
|
const userEntry = {
|
|
1975
2190
|
body: input,
|
|
1976
2191
|
kind: 'user',
|
|
@@ -2025,6 +2240,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2025
2240
|
await appendSettledTurnEntries(turnEntries, result.text ?? '', () => turnGeneration === activeTurnGeneration && !turnAbort.signal.aborted);
|
|
2026
2241
|
if (turnGeneration !== activeTurnGeneration)
|
|
2027
2242
|
return;
|
|
2243
|
+
if (todosTouchedThisTurn) {
|
|
2244
|
+
todosTouchedThisTurn = false;
|
|
2245
|
+
const todoItems = listTodos();
|
|
2246
|
+
if (todoItems.length > 0) {
|
|
2247
|
+
appendStaticEntry({
|
|
2248
|
+
body: '',
|
|
2249
|
+
kind: 'system',
|
|
2250
|
+
title: `To-dos · ${formatTodoProgress(todoItems)}`,
|
|
2251
|
+
todoList: todoItems,
|
|
2252
|
+
});
|
|
2253
|
+
if (todoItems.every((item) => item.status === 'completed')) {
|
|
2254
|
+
clearTodos();
|
|
2255
|
+
syncTodosState();
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2028
2259
|
store.update((current) => ({
|
|
2029
2260
|
...current,
|
|
2030
2261
|
busy: false,
|
|
@@ -2073,8 +2304,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2073
2304
|
appendError(error.message);
|
|
2074
2305
|
}
|
|
2075
2306
|
await remountTui();
|
|
2076
|
-
// A cancelled turn never auto-submits the queue (Ctrl+C recalls it via
|
|
2077
|
-
// cancelActiveTurn); only a genuine error flushes a pending message.
|
|
2078
2307
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2079
2308
|
await flushQueuedMessage();
|
|
2080
2309
|
}
|
|
@@ -2107,6 +2336,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2107
2336
|
};
|
|
2108
2337
|
session.onToolEvent = (event) => {
|
|
2109
2338
|
syncShellStateFromSession();
|
|
2339
|
+
if (event.call.name === 'update_todos') {
|
|
2340
|
+
syncTodosState();
|
|
2341
|
+
if (event.result?.ok === true) {
|
|
2342
|
+
todosTouchedThisTurn = true;
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2110
2346
|
if (isFileChangeTool(event.call.name)) {
|
|
2111
2347
|
const entry = buildFileChangeEntry(event);
|
|
2112
2348
|
store.appendWorkingTool(entry);
|
|
@@ -2115,6 +2351,52 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2115
2351
|
}
|
|
2116
2352
|
store.appendWorkingTool(buildWorkingToolEntry(event));
|
|
2117
2353
|
};
|
|
2354
|
+
const jobDetailLines = (jobId) => {
|
|
2355
|
+
const buffered = getJobBufferedOutput(jobId);
|
|
2356
|
+
if (!buffered)
|
|
2357
|
+
return [];
|
|
2358
|
+
const lines = buffered.output
|
|
2359
|
+
.replace(/\r\n/g, '\n')
|
|
2360
|
+
.replace(/\r/g, '\n')
|
|
2361
|
+
.split('\n')
|
|
2362
|
+
.filter((line) => line.trim().length > 0);
|
|
2363
|
+
const visible = lines.slice(-20);
|
|
2364
|
+
return buffered.droppedChars > 0
|
|
2365
|
+
? [
|
|
2366
|
+
`... (${buffered.droppedChars} chars of older output dropped) ...`,
|
|
2367
|
+
...visible,
|
|
2368
|
+
]
|
|
2369
|
+
: visible;
|
|
2370
|
+
};
|
|
2371
|
+
const syncBackgroundJobsState = () => {
|
|
2372
|
+
const jobsForDisplay = listBackgroundJobs().map((job) => {
|
|
2373
|
+
const preview = getJobOutputPreview(job.id, 3);
|
|
2374
|
+
return {
|
|
2375
|
+
id: job.id,
|
|
2376
|
+
command: job.command,
|
|
2377
|
+
status: job.status,
|
|
2378
|
+
exitCode: job.exitCode,
|
|
2379
|
+
startedAt: job.startedAt,
|
|
2380
|
+
endedAt: job.endedAt,
|
|
2381
|
+
firstOutputLine: preview?.firstLine ?? '',
|
|
2382
|
+
tailLines: preview?.tailLines ?? [],
|
|
2383
|
+
detailLines: jobDetailLines(job.id),
|
|
2384
|
+
};
|
|
2385
|
+
});
|
|
2386
|
+
store.update((current) => ({
|
|
2387
|
+
...current,
|
|
2388
|
+
backgroundJobs: jobsForDisplay,
|
|
2389
|
+
jobsPickerExpandedId: jobsForDisplay.some((job) => job.id === current.jobsPickerExpandedId)
|
|
2390
|
+
? current.jobsPickerExpandedId
|
|
2391
|
+
: null,
|
|
2392
|
+
}));
|
|
2393
|
+
};
|
|
2394
|
+
setBackgroundJobUpdateHook((snapshot) => {
|
|
2395
|
+
syncBackgroundJobsState();
|
|
2396
|
+
if (snapshot.status !== 'running') {
|
|
2397
|
+
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2118
2400
|
projectIndex.onStatus = session.onStatus;
|
|
2119
2401
|
projectIndex.onContextLog = session.onContextLog;
|
|
2120
2402
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
@@ -2174,6 +2456,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2174
2456
|
onResumeSession: handleInlineResumeSelection,
|
|
2175
2457
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2176
2458
|
onSelectModel: handleInlineModelSelection,
|
|
2459
|
+
onJobsPickerOutput: handleJobsPickerOutput,
|
|
2460
|
+
onJobsPickerKill: handleJobsPickerKill,
|
|
2177
2461
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2178
2462
|
onSubmit: handleSubmit,
|
|
2179
2463
|
};
|
|
@@ -2187,7 +2471,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2187
2471
|
}
|
|
2188
2472
|
}, 120);
|
|
2189
2473
|
const clockTimer = setInterval(() => {
|
|
2190
|
-
if (
|
|
2474
|
+
if (hasRunningBackgroundJobs()) {
|
|
2475
|
+
syncBackgroundJobsState();
|
|
2476
|
+
}
|
|
2477
|
+
if (store.getState().busy || hasRunningBackgroundJobs()) {
|
|
2191
2478
|
renderCurrentFrame();
|
|
2192
2479
|
}
|
|
2193
2480
|
}, 1000);
|
|
@@ -2231,6 +2518,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2231
2518
|
clearInterval(clockTimer);
|
|
2232
2519
|
unsubscribe();
|
|
2233
2520
|
clearLiveFrameRemountTimer();
|
|
2521
|
+
killAllBackgroundJobs();
|
|
2522
|
+
setBackgroundJobSession(null);
|
|
2523
|
+
setTodoSession(null);
|
|
2524
|
+
setBackgroundJobUpdateHook(null);
|
|
2234
2525
|
await bridge.close();
|
|
2235
2526
|
setCommandOutputHook(null);
|
|
2236
2527
|
}
|
|
@@ -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),
|
|
@@ -114,13 +111,13 @@ export function resolveTuiBinaryPath() {
|
|
|
114
111
|
export function spawnTuiProcess(options = {}) {
|
|
115
112
|
const binaryPath = resolveTuiBinaryPath();
|
|
116
113
|
return spawn(binaryPath, [], {
|
|
117
|
-
stdio: ['pipe', '
|
|
114
|
+
stdio: ['pipe', 'inherit', 'pipe'],
|
|
118
115
|
env: { ...process.env, ...options.env },
|
|
119
116
|
});
|
|
120
117
|
}
|
|
121
118
|
export function createRatatuiBridge() {
|
|
122
119
|
const child = spawnTuiProcess();
|
|
123
|
-
const rl = createInterface({ input: child.
|
|
120
|
+
const rl = createInterface({ input: child.stderr });
|
|
124
121
|
let eventHandler = null;
|
|
125
122
|
let closed = false;
|
|
126
123
|
rl.on('line', (line) => {
|
|
@@ -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', () => {
|