@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.10
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 +49 -3
- package/dist/bin/ai.js +83 -197
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +4 -4
- package/dist/src/api/browser-login.js +72 -19
- package/dist/src/api/chat.js +182 -35
- package/dist/src/api/http.js +65 -4
- package/dist/src/api/models.js +33 -22
- 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 +30 -13
- package/dist/src/patcher.js +97 -12
- package/dist/src/project-index.js +13 -1
- 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 +52 -21
- package/dist/src/session.js +8 -0
- 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/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +500 -71
- package/dist/src/ui/tui/bridge.js +3 -4
- package/dist/src/ui/tui/build-frame.js +393 -100
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +75 -17
- package/dist/src/ui/tui/terminal-title.js +84 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/utils.js +9 -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 +27 -16
- package/dist/src/markdown-renderer.js +0 -112
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
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';
|
|
4
|
+
import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
|
|
3
5
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
4
6
|
import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
5
7
|
import { chat, models } from '../api/index.js';
|
|
6
8
|
import { isTurnCancelledError } from '../api/chat.js';
|
|
9
|
+
import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
|
|
10
|
+
import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
11
|
+
import { setScratchSession } from '../scratch-dir.js';
|
|
7
12
|
import { cancelActiveCommand } from '../executor.js';
|
|
13
|
+
import { isTurnFailureMarker } from '../turn-failure-marker.js';
|
|
14
|
+
import { clearCliAuthConfig } from '../api/auth.js';
|
|
15
|
+
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
8
16
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
17
|
+
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
18
|
+
import { startNewConversation, } from '../session.js';
|
|
19
|
+
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../session-store.js';
|
|
11
20
|
import { truncate } from '../utils.js';
|
|
12
21
|
import { writeClipboardText } from '../core/clipboard.js';
|
|
13
22
|
import { openUrl } from '../core/open-url.js';
|
|
@@ -103,8 +112,12 @@ export const SLASH_COMMANDS = [
|
|
|
103
112
|
description: 'Open the session picker',
|
|
104
113
|
},
|
|
105
114
|
{
|
|
106
|
-
command: '/
|
|
107
|
-
description: '
|
|
115
|
+
command: '/jobs',
|
|
116
|
+
description: 'Background jobs: pick to view output or kill',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
command: '/new',
|
|
120
|
+
description: 'start a new conversation; this session remains saved',
|
|
108
121
|
},
|
|
109
122
|
{
|
|
110
123
|
command: '/exit',
|
|
@@ -550,6 +563,31 @@ function buildFileChangeEntry(event) {
|
|
|
550
563
|
title: skipped ? `${verb} skipped: ${filePath}` : `${verb} failed: ${filePath}`,
|
|
551
564
|
};
|
|
552
565
|
}
|
|
566
|
+
if (result?.scratch === true) {
|
|
567
|
+
const scratchName = truncate(filePath.split(/[\\/]/).filter(Boolean).at(-1) ?? filePath, 72);
|
|
568
|
+
if (call.name === 'delete_file') {
|
|
569
|
+
return {
|
|
570
|
+
body: '',
|
|
571
|
+
filePath,
|
|
572
|
+
kind: 'tool',
|
|
573
|
+
title: result?.deleted === true
|
|
574
|
+
? `Removed scratch file: ${scratchName}`
|
|
575
|
+
: `Scratch delete skipped: ${scratchName}`,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const content = typeof call.args?.content === 'string' ? call.args.content : '';
|
|
579
|
+
const lineSummary = call.name === 'write_file' && content
|
|
580
|
+
? ` (${splitDiffLines(content).length} lines)`
|
|
581
|
+
: '';
|
|
582
|
+
return {
|
|
583
|
+
body: '',
|
|
584
|
+
filePath,
|
|
585
|
+
kind: 'tool',
|
|
586
|
+
title: call.name === 'write_file'
|
|
587
|
+
? `Wrote scratch file: ${scratchName}${lineSummary}`
|
|
588
|
+
: `Edited scratch file: ${scratchName}`,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
553
591
|
if (call.name === 'undo_edit') {
|
|
554
592
|
const dryRun = result?.dryRun === true ||
|
|
555
593
|
result?.dry_run === true ||
|
|
@@ -643,12 +681,33 @@ function buildWorkingToolEntry(event) {
|
|
|
643
681
|
: '';
|
|
644
682
|
if (call.name === 'run_command') {
|
|
645
683
|
const command = String(call.args?.command ?? result?.command ?? '').trim();
|
|
684
|
+
if (result?.backgrounded === true && result?.jobId) {
|
|
685
|
+
return {
|
|
686
|
+
body: `$ ${truncate(command, 180)}\nBackground job ${String(result.jobId)} started.${error}`,
|
|
687
|
+
kind: result?.ok === true ? 'tool' : 'error',
|
|
688
|
+
preformatted: true,
|
|
689
|
+
title: 'Shell',
|
|
690
|
+
};
|
|
691
|
+
}
|
|
646
692
|
return {
|
|
647
693
|
body: `$ ${truncate(command, 180)}\n${formatRunCommandResultState(result)}${error}`,
|
|
648
694
|
kind: result?.ok === true ? 'tool' : 'error',
|
|
649
695
|
title: 'Shell',
|
|
650
696
|
};
|
|
651
697
|
}
|
|
698
|
+
if (call.name === 'shell_job_output' || call.name === 'shell_job_kill') {
|
|
699
|
+
const jobId = String(result?.jobId ?? call.args?.job_id ?? '').trim();
|
|
700
|
+
const status = String(result?.status ?? '').trim();
|
|
701
|
+
const exitCode = result?.exitCode;
|
|
702
|
+
const stateText = status
|
|
703
|
+
? `${status}${exitCode != null ? ` (code ${exitCode})` : ''}`
|
|
704
|
+
: formatToolResultState(result);
|
|
705
|
+
return {
|
|
706
|
+
body: `${jobId || '(unknown job)'} · ${stateText}${error}`,
|
|
707
|
+
kind: result?.ok === true ? 'tool' : 'error',
|
|
708
|
+
title: call.name === 'shell_job_kill' ? 'Kill job' : 'Job output',
|
|
709
|
+
};
|
|
710
|
+
}
|
|
652
711
|
if (call.name === 'run_node_script') {
|
|
653
712
|
return {
|
|
654
713
|
body: `node --input-type=module <script via stdin>\n${formatRunCommandResultState(result)}${error}`,
|
|
@@ -666,6 +725,32 @@ function buildWorkingToolEntry(event) {
|
|
|
666
725
|
title: 'Tool',
|
|
667
726
|
};
|
|
668
727
|
}
|
|
728
|
+
function buildBackgroundJobNoticeEntry(snapshot) {
|
|
729
|
+
const command = truncate(snapshot.command, 120);
|
|
730
|
+
const ran = formatJobElapsed((snapshot.endedAt ?? Date.now()) - snapshot.startedAt);
|
|
731
|
+
if (snapshot.status === 'error') {
|
|
732
|
+
return {
|
|
733
|
+
body: `✖ ${snapshot.id} (${command}) failed to start`,
|
|
734
|
+
kind: 'error',
|
|
735
|
+
preformatted: true,
|
|
736
|
+
title: 'Background job',
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
if (snapshot.status === 'killed') {
|
|
740
|
+
return {
|
|
741
|
+
body: `■ ${snapshot.id} (${command}) killed · ran ${ran}`,
|
|
742
|
+
kind: 'system',
|
|
743
|
+
preformatted: true,
|
|
744
|
+
title: 'Background job',
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
body: `${snapshot.exitCode === 0 ? '✓' : '✖'} ${snapshot.id} (${command}) exited (code ${snapshot.exitCode ?? 1}) · ran ${ran}`,
|
|
749
|
+
kind: snapshot.exitCode === 0 ? 'system' : 'error',
|
|
750
|
+
preformatted: true,
|
|
751
|
+
title: 'Background job',
|
|
752
|
+
};
|
|
753
|
+
}
|
|
669
754
|
function findPendingToolCallByName(pendingCalls, name) {
|
|
670
755
|
for (const [id, call] of pendingCalls) {
|
|
671
756
|
if (call.name !== name)
|
|
@@ -699,7 +784,7 @@ function displayUserTextFromHistoryEntry(entry) {
|
|
|
699
784
|
.slice(contentStart, contentEnd === -1 ? text.length : contentEnd)
|
|
700
785
|
.trim();
|
|
701
786
|
}
|
|
702
|
-
function buildTranscriptFromSessionHistory(history) {
|
|
787
|
+
export function buildTranscriptFromSessionHistory(history) {
|
|
703
788
|
const entries = [];
|
|
704
789
|
const pendingCalls = new Map();
|
|
705
790
|
for (const entry of history) {
|
|
@@ -745,6 +830,14 @@ function buildTranscriptFromSessionHistory(history) {
|
|
|
745
830
|
}
|
|
746
831
|
const text = textFromHistoryEntry(entry);
|
|
747
832
|
if ((entry.role === 'model' || entry.role === 'assistant') && text) {
|
|
833
|
+
if (isTurnFailureMarker(text)) {
|
|
834
|
+
entries.push({
|
|
835
|
+
body: 'This request did not complete.',
|
|
836
|
+
kind: 'system',
|
|
837
|
+
title: 'Previous turn',
|
|
838
|
+
});
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
748
841
|
entries.push({ body: text, kind: 'assistant', title: 'Response' });
|
|
749
842
|
}
|
|
750
843
|
}
|
|
@@ -814,11 +907,16 @@ function createShellStore(initialState) {
|
|
|
814
907
|
},
|
|
815
908
|
appendWorkingTool: (entry) => {
|
|
816
909
|
const lastEntry = state.workingTools.at(-1);
|
|
817
|
-
|
|
910
|
+
const isDuplicate = sameTranscriptDraft(lastEntry, entry);
|
|
911
|
+
if (isDuplicate && state.commandLog.length === 0) {
|
|
818
912
|
return;
|
|
913
|
+
}
|
|
819
914
|
state = {
|
|
820
915
|
...state,
|
|
821
|
-
|
|
916
|
+
commandLog: [],
|
|
917
|
+
workingTools: isDuplicate
|
|
918
|
+
? state.workingTools
|
|
919
|
+
: [...state.workingTools, entry].slice(-WORKING_TOOL_PREVIEW_ITEMS),
|
|
822
920
|
};
|
|
823
921
|
notify();
|
|
824
922
|
},
|
|
@@ -865,9 +963,11 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
865
963
|
activeTurnInput: '',
|
|
866
964
|
activeTurnInputPreformatted: false,
|
|
867
965
|
agentMode: session.agentMode,
|
|
966
|
+
analyzingImages: 0,
|
|
868
967
|
approvalCursor: getDefaultApprovalCursor(),
|
|
869
968
|
approvalPrompt: null,
|
|
870
969
|
autoYes: session.autoYes,
|
|
970
|
+
backgroundJobs: [],
|
|
871
971
|
busy: false,
|
|
872
972
|
busySince: null,
|
|
873
973
|
clockNow: Date.now(),
|
|
@@ -879,6 +979,9 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
879
979
|
exiting: false,
|
|
880
980
|
input: '',
|
|
881
981
|
maxToolSteps: session.maxToolSteps,
|
|
982
|
+
jobsPickerExpandedId: null,
|
|
983
|
+
jobsPickerIndex: 0,
|
|
984
|
+
jobsPickerOpen: false,
|
|
882
985
|
modelPickerIndex: getDefaultModelPickerIndex(session.modelId, serverModels.models),
|
|
883
986
|
modelPickerOpen: false,
|
|
884
987
|
projectRoot: session.rootDir,
|
|
@@ -899,6 +1002,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
899
1002
|
exitConfirmUntil: null,
|
|
900
1003
|
thinkingTitle: '',
|
|
901
1004
|
thinkingNotes: [],
|
|
1005
|
+
todos: listTodos(),
|
|
902
1006
|
tokenUsage: formatClientTokenUsage(null),
|
|
903
1007
|
transcript: [],
|
|
904
1008
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
@@ -951,6 +1055,14 @@ function thinkingNoteFromStatus(status) {
|
|
|
951
1055
|
const note = text.slice('Thinking:'.length).trim();
|
|
952
1056
|
return note ? note : null;
|
|
953
1057
|
}
|
|
1058
|
+
function stripMarkdownEmphasis(text) {
|
|
1059
|
+
return text
|
|
1060
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
|
|
1061
|
+
.replace(/___(.+?)___/g, '$1')
|
|
1062
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
1063
|
+
.replace(/__(.+?)__/g, '$1')
|
|
1064
|
+
.replace(/(?<!\*)\*(?!\*)([^*\n]+?)\*(?!\*)/g, '$1');
|
|
1065
|
+
}
|
|
954
1066
|
function splitThinkingLines(text) {
|
|
955
1067
|
return text
|
|
956
1068
|
.split('\n')
|
|
@@ -959,13 +1071,13 @@ function splitThinkingLines(text) {
|
|
|
959
1071
|
.flatMap((line) => line
|
|
960
1072
|
.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/)
|
|
961
1073
|
.map((part) => part.trim())
|
|
962
|
-
.filter(Boolean))
|
|
963
|
-
.map((line) => truncate(line, 120));
|
|
1074
|
+
.filter(Boolean));
|
|
964
1075
|
}
|
|
965
1076
|
function thinkingPanelFromStatus(status) {
|
|
966
|
-
const
|
|
967
|
-
if (!
|
|
1077
|
+
const rawText = thinkingNoteFromStatus(status);
|
|
1078
|
+
if (!rawText)
|
|
968
1079
|
return null;
|
|
1080
|
+
const text = stripMarkdownEmphasis(rawText);
|
|
969
1081
|
const rawLines = text
|
|
970
1082
|
.split('\n')
|
|
971
1083
|
.map((line) => line.trim())
|
|
@@ -974,12 +1086,12 @@ function thinkingPanelFromStatus(status) {
|
|
|
974
1086
|
return null;
|
|
975
1087
|
if (rawLines.length === 1 && rawLines[0].length <= 72) {
|
|
976
1088
|
return {
|
|
977
|
-
title:
|
|
1089
|
+
title: rawLines[0],
|
|
978
1090
|
notes: [],
|
|
979
1091
|
};
|
|
980
1092
|
}
|
|
981
1093
|
const title = rawLines.length > 1 && rawLines[0].length <= 72
|
|
982
|
-
?
|
|
1094
|
+
? rawLines[0]
|
|
983
1095
|
: 'Thinking';
|
|
984
1096
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
985
1097
|
return {
|
|
@@ -997,7 +1109,10 @@ export function getInputCommandToken(input) {
|
|
|
997
1109
|
if (!trimmed.startsWith('/'))
|
|
998
1110
|
return '';
|
|
999
1111
|
const firstSpaceIndex = trimmed.indexOf(' ');
|
|
1000
|
-
|
|
1112
|
+
const token = firstSpaceIndex === -1 ? trimmed : trimmed.slice(0, firstSpaceIndex);
|
|
1113
|
+
if (token.indexOf('/', 1) !== -1)
|
|
1114
|
+
return '';
|
|
1115
|
+
return token;
|
|
1001
1116
|
}
|
|
1002
1117
|
function shouldShowCommandPalette(state) {
|
|
1003
1118
|
const trimmed = String(state.input ?? '').trim();
|
|
@@ -1006,7 +1121,8 @@ function shouldShowCommandPalette(state) {
|
|
|
1006
1121
|
!state.modelPickerOpen &&
|
|
1007
1122
|
!state.resumePickerOpen &&
|
|
1008
1123
|
trimmed.startsWith('/') &&
|
|
1009
|
-
!trimmed.includes(' ')
|
|
1124
|
+
!trimmed.includes(' ') &&
|
|
1125
|
+
getInputCommandToken(trimmed) !== '');
|
|
1010
1126
|
}
|
|
1011
1127
|
export function shouldRemountLiveFrameForComposerInputChange(current, nextInput) {
|
|
1012
1128
|
const currentShowsCommands = shouldShowCommandPalette(current);
|
|
@@ -1135,7 +1251,11 @@ export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
|
1135
1251
|
return serverModels.map((model) => ({
|
|
1136
1252
|
id: model.id,
|
|
1137
1253
|
label: model.label,
|
|
1138
|
-
|
|
1254
|
+
publicId: model.id,
|
|
1255
|
+
costRating: model.costRating,
|
|
1256
|
+
current: model.id === currentModelId,
|
|
1257
|
+
disabled: false,
|
|
1258
|
+
note: model.description,
|
|
1139
1259
|
}));
|
|
1140
1260
|
}
|
|
1141
1261
|
function getDefaultModelPickerIndex(currentModelId, serverModels) {
|
|
@@ -1163,22 +1283,23 @@ function appendCommandLog(state, text) {
|
|
|
1163
1283
|
};
|
|
1164
1284
|
}
|
|
1165
1285
|
async function saveSessionBoth({ serverSessionClient, session, }) {
|
|
1286
|
+
if (!sessionHasUserMessage(session))
|
|
1287
|
+
return;
|
|
1166
1288
|
saveSessionState(session);
|
|
1167
1289
|
await serverSessionClient.save(session);
|
|
1168
1290
|
}
|
|
1169
|
-
function shouldUseRatatuiShell() {
|
|
1170
|
-
if (process.env.THEGITAI_PLAIN === '1')
|
|
1171
|
-
return false;
|
|
1172
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1173
|
-
}
|
|
1174
|
-
export function shouldUseClientRatatuiShell() {
|
|
1175
|
-
return shouldUseRatatuiShell();
|
|
1176
|
-
}
|
|
1177
1291
|
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
1178
|
-
if (
|
|
1179
|
-
throw new Error('
|
|
1292
|
+
if (process.stdin.isTTY !== true) {
|
|
1293
|
+
throw new Error('stdin is not a terminal');
|
|
1294
|
+
}
|
|
1295
|
+
if (process.stdout.isTTY !== true) {
|
|
1296
|
+
throw new Error('stdout is not a terminal');
|
|
1180
1297
|
}
|
|
1298
|
+
let fatalError = null;
|
|
1181
1299
|
await withTuiMode(async () => {
|
|
1300
|
+
setBackgroundJobSession(session.sessionId);
|
|
1301
|
+
setScratchSession(session.sessionId);
|
|
1302
|
+
setTodoSession(session.sessionId);
|
|
1182
1303
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1183
1304
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1184
1305
|
let currentServerModels = serverModels;
|
|
@@ -1189,6 +1310,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1189
1310
|
let cleanupSudoPasswordPrompt = null;
|
|
1190
1311
|
let sudoPasswordBuffer = '';
|
|
1191
1312
|
const bridge = createRatatuiBridge();
|
|
1313
|
+
captureTerminalWrites();
|
|
1192
1314
|
const { handleShellKeyEvent } = await import('./tui/shell-input.js');
|
|
1193
1315
|
let terminalCols = 80;
|
|
1194
1316
|
let terminalRows = 24;
|
|
@@ -1201,6 +1323,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1201
1323
|
let latestUsageSummary = null;
|
|
1202
1324
|
let pendingTurnEntries = [];
|
|
1203
1325
|
let activeTurnAbort = null;
|
|
1326
|
+
let newConversationInFlight = false;
|
|
1327
|
+
let todosTouchedThisTurn = false;
|
|
1328
|
+
const syncTodosState = () => {
|
|
1329
|
+
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
1330
|
+
};
|
|
1204
1331
|
let activeTurnGeneration = 0;
|
|
1205
1332
|
let exitCtrlCArmed = false;
|
|
1206
1333
|
let exitCtrlCTimer = null;
|
|
@@ -1229,14 +1356,25 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1229
1356
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1230
1357
|
}, 2500);
|
|
1231
1358
|
};
|
|
1359
|
+
const terminalTitle = createTerminalTitleController({
|
|
1360
|
+
write: (title) => bridge.setTitle(title),
|
|
1361
|
+
});
|
|
1362
|
+
const syncTerminalTitle = () => {
|
|
1363
|
+
const state = store.getState();
|
|
1364
|
+
terminalTitle.sync({
|
|
1365
|
+
awaitingReview: Boolean(state.approvalPrompt || state.sudoPrompt),
|
|
1366
|
+
busy: state.busy,
|
|
1367
|
+
});
|
|
1368
|
+
};
|
|
1232
1369
|
const renderCurrentFrame = () => {
|
|
1233
1370
|
if (!tuiReady)
|
|
1234
1371
|
return;
|
|
1235
1372
|
const state = store.getState();
|
|
1373
|
+
syncTerminalTitle();
|
|
1236
1374
|
const elapsedSeconds = state.busySince
|
|
1237
1375
|
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1238
1376
|
: 0;
|
|
1239
|
-
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds));
|
|
1377
|
+
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1240
1378
|
};
|
|
1241
1379
|
const remountTui = async () => {
|
|
1242
1380
|
if (remountPromise) {
|
|
@@ -1387,7 +1525,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1387
1525
|
}));
|
|
1388
1526
|
};
|
|
1389
1527
|
const cancelActiveTurn = () => {
|
|
1390
|
-
if (!store.getState().busy)
|
|
1528
|
+
if (!store.getState().busy || newConversationInFlight)
|
|
1391
1529
|
return;
|
|
1392
1530
|
disarmExitConfirm();
|
|
1393
1531
|
dismissPendingApproval();
|
|
@@ -1396,9 +1534,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1396
1534
|
activeTurnAbort?.abort();
|
|
1397
1535
|
activeTurnAbort = null;
|
|
1398
1536
|
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
1537
|
const queued = store.getState().queuedMessage;
|
|
1403
1538
|
const cancelledEntries = [
|
|
1404
1539
|
...takePendingTurnEntries(),
|
|
@@ -1434,14 +1569,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1434
1569
|
scheduleLiveFrameRemount();
|
|
1435
1570
|
void remountTui();
|
|
1436
1571
|
};
|
|
1437
|
-
const saveActiveSession = async () => {
|
|
1438
|
-
try {
|
|
1439
|
-
await saveSessionBoth({ serverSessionClient, session });
|
|
1440
|
-
}
|
|
1441
|
-
catch (error) {
|
|
1442
|
-
appendError(`Session save failed: ${error.message}`);
|
|
1443
|
-
}
|
|
1444
|
-
};
|
|
1445
1572
|
const syncShellStateFromSession = () => {
|
|
1446
1573
|
store.update((current) => ({
|
|
1447
1574
|
...current,
|
|
@@ -1521,6 +1648,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1521
1648
|
dismissPendingApproval();
|
|
1522
1649
|
dismissPendingSudoPassword();
|
|
1523
1650
|
exiting = true;
|
|
1651
|
+
killAllBackgroundJobs();
|
|
1524
1652
|
store.update((current) => ({
|
|
1525
1653
|
...current,
|
|
1526
1654
|
exiting: true,
|
|
@@ -1528,9 +1656,45 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1528
1656
|
status: 'Exiting...',
|
|
1529
1657
|
}));
|
|
1530
1658
|
void bridge.close().then(() => {
|
|
1659
|
+
releaseTerminalWrites();
|
|
1531
1660
|
resolveDone?.();
|
|
1532
1661
|
});
|
|
1533
1662
|
};
|
|
1663
|
+
const exitForAuthenticationError = (error) => {
|
|
1664
|
+
if (!isAuthenticationError(error))
|
|
1665
|
+
return false;
|
|
1666
|
+
clearCliAuthConfig(session.env);
|
|
1667
|
+
if (sessionHasUserMessage(session)) {
|
|
1668
|
+
saveSessionState(session);
|
|
1669
|
+
}
|
|
1670
|
+
fatalError = new Error(authenticationErrorMessage(error));
|
|
1671
|
+
requestExit();
|
|
1672
|
+
return true;
|
|
1673
|
+
};
|
|
1674
|
+
const saveActiveSession = async () => {
|
|
1675
|
+
try {
|
|
1676
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1677
|
+
return true;
|
|
1678
|
+
}
|
|
1679
|
+
catch (error) {
|
|
1680
|
+
if (exitForAuthenticationError(error))
|
|
1681
|
+
return false;
|
|
1682
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1683
|
+
return true;
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
const saveActiveSessionOrAbort = async () => {
|
|
1687
|
+
try {
|
|
1688
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1689
|
+
return true;
|
|
1690
|
+
}
|
|
1691
|
+
catch (error) {
|
|
1692
|
+
if (exitForAuthenticationError(error))
|
|
1693
|
+
return false;
|
|
1694
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1534
1698
|
const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
|
|
1535
1699
|
if (exiting || signal?.aborted) {
|
|
1536
1700
|
resolve(null);
|
|
@@ -1667,7 +1831,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1667
1831
|
selectedModelId: selected,
|
|
1668
1832
|
serverModels: fresh,
|
|
1669
1833
|
});
|
|
1670
|
-
await saveActiveSession()
|
|
1834
|
+
if (!(await saveActiveSession()))
|
|
1835
|
+
return;
|
|
1671
1836
|
syncShellStateFromSession();
|
|
1672
1837
|
appendStaticEntry({
|
|
1673
1838
|
body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
|
|
@@ -1675,6 +1840,87 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1675
1840
|
title: 'Model',
|
|
1676
1841
|
});
|
|
1677
1842
|
};
|
|
1843
|
+
const openJobsPicker = () => {
|
|
1844
|
+
syncBackgroundJobsState();
|
|
1845
|
+
store.update((current) => ({
|
|
1846
|
+
...current,
|
|
1847
|
+
commandCursor: 0,
|
|
1848
|
+
cursor: 0,
|
|
1849
|
+
input: '',
|
|
1850
|
+
jobsPickerExpandedId: null,
|
|
1851
|
+
jobsPickerIndex: 0,
|
|
1852
|
+
jobsPickerOpen: true,
|
|
1853
|
+
status: 'Background jobs',
|
|
1854
|
+
}));
|
|
1855
|
+
};
|
|
1856
|
+
const appendJobOutputEntry = async (jobId) => {
|
|
1857
|
+
const id = jobId.trim();
|
|
1858
|
+
await collectBackgroundJobUiOutputMutations({
|
|
1859
|
+
session,
|
|
1860
|
+
projectIndex,
|
|
1861
|
+
jobId: id,
|
|
1862
|
+
});
|
|
1863
|
+
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
1864
|
+
const buffered = getJobBufferedOutput(id);
|
|
1865
|
+
if (!job || !buffered) {
|
|
1866
|
+
appendError(`Unknown background job id: ${id}`);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const dropped = buffered.droppedChars > 0
|
|
1870
|
+
? `... (${buffered.droppedChars} chars of older output dropped) ...\n`
|
|
1871
|
+
: '';
|
|
1872
|
+
appendStaticEntry({
|
|
1873
|
+
body: `${dropped}${buffered.output || '(no output captured)'}`,
|
|
1874
|
+
kind: 'system',
|
|
1875
|
+
preformatted: true,
|
|
1876
|
+
title: `${job.id} output — ${truncate(job.command, 80)}`,
|
|
1877
|
+
});
|
|
1878
|
+
};
|
|
1879
|
+
const selectedJobsPickerId = () => {
|
|
1880
|
+
const current = store.getState();
|
|
1881
|
+
if (!current.backgroundJobs.length)
|
|
1882
|
+
return null;
|
|
1883
|
+
const index = Math.min(Math.max(current.jobsPickerIndex, 0), current.backgroundJobs.length - 1);
|
|
1884
|
+
return current.backgroundJobs[index]?.id ?? null;
|
|
1885
|
+
};
|
|
1886
|
+
const handleJobsPickerOutput = async () => {
|
|
1887
|
+
const jobId = selectedJobsPickerId();
|
|
1888
|
+
if (!jobId)
|
|
1889
|
+
return;
|
|
1890
|
+
await collectBackgroundJobUiOutputMutations({
|
|
1891
|
+
session,
|
|
1892
|
+
projectIndex,
|
|
1893
|
+
jobId,
|
|
1894
|
+
});
|
|
1895
|
+
syncBackgroundJobsState();
|
|
1896
|
+
store.update((current) => ({
|
|
1897
|
+
...current,
|
|
1898
|
+
jobsPickerExpandedId: current.jobsPickerExpandedId === jobId ? null : jobId,
|
|
1899
|
+
}));
|
|
1900
|
+
};
|
|
1901
|
+
const handleJobsPickerKill = async () => {
|
|
1902
|
+
const jobId = selectedJobsPickerId();
|
|
1903
|
+
if (!jobId)
|
|
1904
|
+
return;
|
|
1905
|
+
const killed = await killBackgroundJob(jobId);
|
|
1906
|
+
await collectBackgroundJobUiKillMutations({
|
|
1907
|
+
session,
|
|
1908
|
+
projectIndex,
|
|
1909
|
+
jobId,
|
|
1910
|
+
result: killed,
|
|
1911
|
+
});
|
|
1912
|
+
syncBackgroundJobsState();
|
|
1913
|
+
if (!killed.ok) {
|
|
1914
|
+
appendError(killed.error ?? 'Background job kill failed.');
|
|
1915
|
+
}
|
|
1916
|
+
else if (killed.alreadyFinished) {
|
|
1917
|
+
appendStaticEntry({
|
|
1918
|
+
body: `${jobId} had already finished.`,
|
|
1919
|
+
kind: 'system',
|
|
1920
|
+
title: 'Background jobs',
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1678
1924
|
const handleInlineModelSelection = async () => {
|
|
1679
1925
|
const current = store.getState();
|
|
1680
1926
|
const options = buildModelPickerOptions(current.currentModelId, current.serverModels);
|
|
@@ -1705,6 +1951,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1705
1951
|
await switchToModel(selected.id);
|
|
1706
1952
|
}
|
|
1707
1953
|
catch (error) {
|
|
1954
|
+
if (exitForAuthenticationError(error))
|
|
1955
|
+
return;
|
|
1708
1956
|
appendError(error.message);
|
|
1709
1957
|
}
|
|
1710
1958
|
};
|
|
@@ -1761,7 +2009,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1761
2009
|
try {
|
|
1762
2010
|
const snapshot = await loadInteractiveSession(selected.id);
|
|
1763
2011
|
applySessionSnapshot(session, snapshot);
|
|
1764
|
-
|
|
2012
|
+
setBackgroundJobSession(session.sessionId);
|
|
2013
|
+
setScratchSession(session.sessionId);
|
|
2014
|
+
syncBackgroundJobsState();
|
|
2015
|
+
setTodoSession(session.sessionId);
|
|
2016
|
+
syncTodosState();
|
|
1765
2017
|
syncShellStateFromSession();
|
|
1766
2018
|
store.update((next) => ({
|
|
1767
2019
|
...next,
|
|
@@ -1784,6 +2036,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1784
2036
|
await remountTui();
|
|
1785
2037
|
}
|
|
1786
2038
|
catch (error) {
|
|
2039
|
+
if (exitForAuthenticationError(error))
|
|
2040
|
+
return;
|
|
1787
2041
|
store.update((next) => ({
|
|
1788
2042
|
...next,
|
|
1789
2043
|
busy: false,
|
|
@@ -1802,8 +2056,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1802
2056
|
if (!queued || store.getState().busy || exiting) {
|
|
1803
2057
|
return;
|
|
1804
2058
|
}
|
|
1805
|
-
// Rehydrate attachments/chunks before submit: handleSubmit expands
|
|
1806
|
-
// pastedChunks from the store and the turn picks up imageAttachments.
|
|
1807
2059
|
store.update((current) => ({
|
|
1808
2060
|
...current,
|
|
1809
2061
|
imageAttachments: queued.imageAttachments,
|
|
@@ -1830,9 +2082,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1830
2082
|
});
|
|
1831
2083
|
return;
|
|
1832
2084
|
}
|
|
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
2085
|
const pending = store.getState();
|
|
1837
2086
|
const snapshot = {
|
|
1838
2087
|
body: String(rawInput ?? ''),
|
|
@@ -1887,6 +2136,46 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1887
2136
|
});
|
|
1888
2137
|
return;
|
|
1889
2138
|
}
|
|
2139
|
+
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2140
|
+
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2141
|
+
if (!jobsArgs) {
|
|
2142
|
+
openJobsPicker();
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
const killMatch = jobsArgs.match(/^kill\s+(\S+)$/);
|
|
2146
|
+
if (killMatch) {
|
|
2147
|
+
const jobId = killMatch[1];
|
|
2148
|
+
const killed = await killBackgroundJob(jobId);
|
|
2149
|
+
await collectBackgroundJobUiKillMutations({
|
|
2150
|
+
session,
|
|
2151
|
+
projectIndex,
|
|
2152
|
+
jobId,
|
|
2153
|
+
result: killed,
|
|
2154
|
+
});
|
|
2155
|
+
if (!killed.ok) {
|
|
2156
|
+
appendError(killed.error ?? 'Background job kill failed.');
|
|
2157
|
+
}
|
|
2158
|
+
else if (killed.alreadyFinished) {
|
|
2159
|
+
appendStaticEntry({
|
|
2160
|
+
body: `${jobId} had already finished.`,
|
|
2161
|
+
kind: 'system',
|
|
2162
|
+
title: 'Background jobs',
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const outputMatch = jobsArgs.match(/^output\s+(\S+)$/);
|
|
2168
|
+
if (outputMatch) {
|
|
2169
|
+
await appendJobOutputEntry(outputMatch[1]);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
appendStaticEntry({
|
|
2173
|
+
body: 'Usage: /jobs — open the jobs picker · /jobs output <id> — full output · /jobs kill <id> — kill one',
|
|
2174
|
+
kind: 'system',
|
|
2175
|
+
title: 'Background jobs',
|
|
2176
|
+
});
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
1890
2179
|
if (input === '/usage') {
|
|
1891
2180
|
store.update((current) => ({
|
|
1892
2181
|
...current,
|
|
@@ -1901,14 +2190,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1901
2190
|
});
|
|
1902
2191
|
}
|
|
1903
2192
|
catch (error) {
|
|
2193
|
+
if (exitForAuthenticationError(error))
|
|
2194
|
+
return;
|
|
1904
2195
|
appendError(error.message);
|
|
1905
2196
|
}
|
|
1906
2197
|
finally {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2198
|
+
if (!exiting) {
|
|
2199
|
+
store.update((current) => ({
|
|
2200
|
+
...current,
|
|
2201
|
+
busy: false,
|
|
2202
|
+
status: 'Ready',
|
|
2203
|
+
}));
|
|
2204
|
+
}
|
|
1912
2205
|
}
|
|
1913
2206
|
return;
|
|
1914
2207
|
}
|
|
@@ -1922,14 +2215,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1922
2215
|
await openResumePicker();
|
|
1923
2216
|
}
|
|
1924
2217
|
catch (error) {
|
|
2218
|
+
if (exitForAuthenticationError(error))
|
|
2219
|
+
return;
|
|
1925
2220
|
appendError(error.message);
|
|
1926
2221
|
}
|
|
1927
2222
|
finally {
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2223
|
+
if (!exiting) {
|
|
2224
|
+
store.update((current) => ({
|
|
2225
|
+
...current,
|
|
2226
|
+
busy: false,
|
|
2227
|
+
status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
|
|
2228
|
+
}));
|
|
2229
|
+
}
|
|
1933
2230
|
}
|
|
1934
2231
|
return;
|
|
1935
2232
|
}
|
|
@@ -1949,31 +2246,68 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1949
2246
|
}
|
|
1950
2247
|
}
|
|
1951
2248
|
catch (error) {
|
|
2249
|
+
if (exitForAuthenticationError(error))
|
|
2250
|
+
return;
|
|
1952
2251
|
appendError(error.message);
|
|
1953
2252
|
}
|
|
1954
2253
|
finally {
|
|
2254
|
+
if (!exiting) {
|
|
2255
|
+
store.update((current) => ({
|
|
2256
|
+
...current,
|
|
2257
|
+
busy: false,
|
|
2258
|
+
status: current.modelPickerOpen ? 'Select a model' : 'Ready',
|
|
2259
|
+
}));
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
if (input === '/new') {
|
|
2265
|
+
newConversationInFlight = true;
|
|
2266
|
+
store.update((current) => ({
|
|
2267
|
+
...current,
|
|
2268
|
+
busy: true,
|
|
2269
|
+
busySince: Date.now(),
|
|
2270
|
+
imageAttachments: [],
|
|
2271
|
+
queuedMessage: null,
|
|
2272
|
+
status: 'Starting a new conversation...',
|
|
2273
|
+
}));
|
|
2274
|
+
if (!(await saveActiveSessionOrAbort())) {
|
|
2275
|
+
newConversationInFlight = false;
|
|
1955
2276
|
store.update((current) => ({
|
|
1956
2277
|
...current,
|
|
1957
2278
|
busy: false,
|
|
1958
|
-
|
|
2279
|
+
busySince: null,
|
|
2280
|
+
status: 'Ready',
|
|
1959
2281
|
}));
|
|
2282
|
+
await flushQueuedMessage();
|
|
2283
|
+
return;
|
|
1960
2284
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2285
|
+
startNewConversation(session);
|
|
2286
|
+
setBackgroundJobSession(session.sessionId);
|
|
2287
|
+
setScratchSession(session.sessionId);
|
|
2288
|
+
setTodoSession(session.sessionId);
|
|
2289
|
+
clearTodos();
|
|
2290
|
+
syncBackgroundJobsState();
|
|
2291
|
+
syncTodosState();
|
|
1965
2292
|
latestUsageSummary = null;
|
|
1966
|
-
await saveActiveSession();
|
|
1967
2293
|
store.replaceTranscript([
|
|
1968
2294
|
{
|
|
1969
|
-
body: '
|
|
2295
|
+
body: 'Started a new conversation. The previous session remains saved.',
|
|
1970
2296
|
kind: 'system',
|
|
1971
|
-
title: '
|
|
2297
|
+
title: 'Session',
|
|
1972
2298
|
},
|
|
1973
2299
|
]);
|
|
2300
|
+
await saveActiveSession();
|
|
1974
2301
|
syncShellStateFromSession();
|
|
1975
|
-
|
|
2302
|
+
newConversationInFlight = false;
|
|
2303
|
+
store.update((current) => ({
|
|
2304
|
+
...current,
|
|
2305
|
+
busy: false,
|
|
2306
|
+
busySince: null,
|
|
2307
|
+
status: 'Ready',
|
|
2308
|
+
}));
|
|
1976
2309
|
await remountTui();
|
|
2310
|
+
await flushQueuedMessage();
|
|
1977
2311
|
return;
|
|
1978
2312
|
}
|
|
1979
2313
|
latestUsageSummary = null;
|
|
@@ -1983,6 +2317,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1983
2317
|
const turnAbort = new AbortController();
|
|
1984
2318
|
activeTurnAbort = turnAbort;
|
|
1985
2319
|
lastTurnStartedAt = turnStartedAt;
|
|
2320
|
+
todosTouchedThisTurn = false;
|
|
2321
|
+
clearTodos();
|
|
2322
|
+
syncTodosState();
|
|
1986
2323
|
const userEntry = {
|
|
1987
2324
|
body: input,
|
|
1988
2325
|
kind: 'user',
|
|
@@ -1995,6 +2332,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1995
2332
|
...current,
|
|
1996
2333
|
activeTurnInput: input,
|
|
1997
2334
|
activeTurnInputPreformatted: preformatted,
|
|
2335
|
+
analyzingImages: 0,
|
|
1998
2336
|
busy: true,
|
|
1999
2337
|
busySince: turnStartedAt,
|
|
2000
2338
|
clockNow: turnStartedAt,
|
|
@@ -2019,7 +2357,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2019
2357
|
return;
|
|
2020
2358
|
latestUsageSummary = result.usageSummary ?? null;
|
|
2021
2359
|
const turnEntries = takePendingTurnEntries();
|
|
2022
|
-
await saveActiveSession()
|
|
2360
|
+
if (!(await saveActiveSession()))
|
|
2361
|
+
return;
|
|
2023
2362
|
syncShellStateFromSession();
|
|
2024
2363
|
store.update((current) => ({
|
|
2025
2364
|
...current,
|
|
@@ -2037,6 +2376,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2037
2376
|
await appendSettledTurnEntries(turnEntries, result.text ?? '', () => turnGeneration === activeTurnGeneration && !turnAbort.signal.aborted);
|
|
2038
2377
|
if (turnGeneration !== activeTurnGeneration)
|
|
2039
2378
|
return;
|
|
2379
|
+
if (todosTouchedThisTurn) {
|
|
2380
|
+
todosTouchedThisTurn = false;
|
|
2381
|
+
const todoItems = listTodos();
|
|
2382
|
+
if (todoItems.length > 0) {
|
|
2383
|
+
appendStaticEntry({
|
|
2384
|
+
body: '',
|
|
2385
|
+
kind: 'system',
|
|
2386
|
+
title: `To-dos · ${formatTodoProgress(todoItems)}`,
|
|
2387
|
+
todoList: todoItems,
|
|
2388
|
+
});
|
|
2389
|
+
if (todoItems.every((item) => item.status === 'completed')) {
|
|
2390
|
+
clearTodos();
|
|
2391
|
+
syncTodosState();
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2040
2395
|
store.update((current) => ({
|
|
2041
2396
|
...current,
|
|
2042
2397
|
busy: false,
|
|
@@ -2054,6 +2409,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2054
2409
|
return;
|
|
2055
2410
|
}
|
|
2056
2411
|
const cancelled = isTurnCancelledError(error);
|
|
2412
|
+
if (exitForAuthenticationError(error))
|
|
2413
|
+
return;
|
|
2057
2414
|
store.update((current) => ({
|
|
2058
2415
|
...current,
|
|
2059
2416
|
activeTurnInput: '',
|
|
@@ -2078,15 +2435,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2078
2435
|
title: 'System',
|
|
2079
2436
|
},
|
|
2080
2437
|
]);
|
|
2081
|
-
await saveActiveSession()
|
|
2438
|
+
if (!(await saveActiveSession()))
|
|
2439
|
+
return;
|
|
2082
2440
|
}
|
|
2083
2441
|
else {
|
|
2084
2442
|
appendStaticEntries(turnEntries);
|
|
2085
2443
|
appendError(error.message);
|
|
2086
2444
|
}
|
|
2087
2445
|
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
2446
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2091
2447
|
await flushQueuedMessage();
|
|
2092
2448
|
}
|
|
@@ -2100,6 +2456,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2100
2456
|
}
|
|
2101
2457
|
}
|
|
2102
2458
|
};
|
|
2459
|
+
session.onImageAnalysis = (activeImageCount) => {
|
|
2460
|
+
store.update((current) => ({
|
|
2461
|
+
...current,
|
|
2462
|
+
analyzingImages: Math.max(0, activeImageCount),
|
|
2463
|
+
}));
|
|
2464
|
+
};
|
|
2103
2465
|
session.onStatus = (message) => {
|
|
2104
2466
|
const panel = thinkingPanelFromStatus(message);
|
|
2105
2467
|
store.update((current) => ({
|
|
@@ -2119,6 +2481,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2119
2481
|
};
|
|
2120
2482
|
session.onToolEvent = (event) => {
|
|
2121
2483
|
syncShellStateFromSession();
|
|
2484
|
+
if (event.call.name === 'update_todos') {
|
|
2485
|
+
syncTodosState();
|
|
2486
|
+
if (event.result?.ok === true) {
|
|
2487
|
+
todosTouchedThisTurn = true;
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2122
2491
|
if (isFileChangeTool(event.call.name)) {
|
|
2123
2492
|
const entry = buildFileChangeEntry(event);
|
|
2124
2493
|
store.appendWorkingTool(entry);
|
|
@@ -2127,6 +2496,52 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2127
2496
|
}
|
|
2128
2497
|
store.appendWorkingTool(buildWorkingToolEntry(event));
|
|
2129
2498
|
};
|
|
2499
|
+
const jobDetailLines = (jobId) => {
|
|
2500
|
+
const buffered = getJobBufferedOutput(jobId);
|
|
2501
|
+
if (!buffered)
|
|
2502
|
+
return [];
|
|
2503
|
+
const lines = buffered.output
|
|
2504
|
+
.replace(/\r\n/g, '\n')
|
|
2505
|
+
.replace(/\r/g, '\n')
|
|
2506
|
+
.split('\n')
|
|
2507
|
+
.filter((line) => line.trim().length > 0);
|
|
2508
|
+
const visible = lines.slice(-20);
|
|
2509
|
+
return buffered.droppedChars > 0
|
|
2510
|
+
? [
|
|
2511
|
+
`... (${buffered.droppedChars} chars of older output dropped) ...`,
|
|
2512
|
+
...visible,
|
|
2513
|
+
]
|
|
2514
|
+
: visible;
|
|
2515
|
+
};
|
|
2516
|
+
const syncBackgroundJobsState = () => {
|
|
2517
|
+
const jobsForDisplay = listBackgroundJobs().map((job) => {
|
|
2518
|
+
const preview = getJobOutputPreview(job.id, 3);
|
|
2519
|
+
return {
|
|
2520
|
+
id: job.id,
|
|
2521
|
+
command: job.command,
|
|
2522
|
+
status: job.status,
|
|
2523
|
+
exitCode: job.exitCode,
|
|
2524
|
+
startedAt: job.startedAt,
|
|
2525
|
+
endedAt: job.endedAt,
|
|
2526
|
+
firstOutputLine: preview?.firstLine ?? '',
|
|
2527
|
+
tailLines: preview?.tailLines ?? [],
|
|
2528
|
+
detailLines: jobDetailLines(job.id),
|
|
2529
|
+
};
|
|
2530
|
+
});
|
|
2531
|
+
store.update((current) => ({
|
|
2532
|
+
...current,
|
|
2533
|
+
backgroundJobs: jobsForDisplay,
|
|
2534
|
+
jobsPickerExpandedId: jobsForDisplay.some((job) => job.id === current.jobsPickerExpandedId)
|
|
2535
|
+
? current.jobsPickerExpandedId
|
|
2536
|
+
: null,
|
|
2537
|
+
}));
|
|
2538
|
+
};
|
|
2539
|
+
setBackgroundJobUpdateHook((snapshot) => {
|
|
2540
|
+
syncBackgroundJobsState();
|
|
2541
|
+
if (snapshot.status !== 'running') {
|
|
2542
|
+
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2130
2545
|
projectIndex.onStatus = session.onStatus;
|
|
2131
2546
|
projectIndex.onContextLog = session.onContextLog;
|
|
2132
2547
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
@@ -2186,6 +2601,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2186
2601
|
onResumeSession: handleInlineResumeSelection,
|
|
2187
2602
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2188
2603
|
onSelectModel: handleInlineModelSelection,
|
|
2604
|
+
onJobsPickerOutput: handleJobsPickerOutput,
|
|
2605
|
+
onJobsPickerKill: handleJobsPickerKill,
|
|
2189
2606
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2190
2607
|
onSubmit: handleSubmit,
|
|
2191
2608
|
};
|
|
@@ -2199,7 +2616,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2199
2616
|
}
|
|
2200
2617
|
}, 120);
|
|
2201
2618
|
const clockTimer = setInterval(() => {
|
|
2202
|
-
if (
|
|
2619
|
+
if (hasRunningBackgroundJobs()) {
|
|
2620
|
+
syncBackgroundJobsState();
|
|
2621
|
+
}
|
|
2622
|
+
if (store.getState().busy || hasRunningBackgroundJobs()) {
|
|
2203
2623
|
renderCurrentFrame();
|
|
2204
2624
|
}
|
|
2205
2625
|
}, 1000);
|
|
@@ -2243,8 +2663,17 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2243
2663
|
clearInterval(clockTimer);
|
|
2244
2664
|
unsubscribe();
|
|
2245
2665
|
clearLiveFrameRemountTimer();
|
|
2666
|
+
terminalTitle.dispose();
|
|
2667
|
+
killAllBackgroundJobs();
|
|
2668
|
+
setBackgroundJobSession(null);
|
|
2669
|
+
setScratchSession(null);
|
|
2670
|
+
setTodoSession(null);
|
|
2671
|
+
setBackgroundJobUpdateHook(null);
|
|
2246
2672
|
await bridge.close();
|
|
2673
|
+
releaseTerminalWrites();
|
|
2247
2674
|
setCommandOutputHook(null);
|
|
2248
2675
|
}
|
|
2249
2676
|
});
|
|
2677
|
+
if (fatalError)
|
|
2678
|
+
throw fatalError;
|
|
2250
2679
|
}
|