@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.31
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 +39 -6
- package/dist/bin/ai.js +142 -383
- package/dist/src/agent-mode.js +1 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +258 -38
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +69 -7
- package/dist/src/api/models.js +19 -10
- package/dist/src/background-jobs.js +2 -2
- package/dist/src/cli-args.js +19 -5
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +199 -0
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +67 -18
- package/dist/src/permissions.js +243 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +14 -3
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +11 -46
- package/dist/src/tools/delete-file.js +15 -3
- package/dist/src/tools/index.js +13 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- 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 +649 -164
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +453 -115
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +206 -63
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +29 -6
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createRatatuiBridge } from './tui/bridge.js';
|
|
2
|
-
import {
|
|
2
|
+
import { bucketActionLabel, } from '../permissions.js';
|
|
3
|
+
import { approvalScrollLimit, buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, userInputViewportForFrame, } from './tui/build-frame.js';
|
|
3
4
|
import { createTerminalTitleController } from './tui/terminal-title.js';
|
|
5
|
+
import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
|
|
4
6
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
5
7
|
import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
6
8
|
import { chat, models } from '../api/index.js';
|
|
@@ -8,11 +10,16 @@ import { isTurnCancelledError } from '../api/chat.js';
|
|
|
8
10
|
import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
|
|
9
11
|
import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
10
12
|
import { setScratchSession } from '../scratch-dir.js';
|
|
13
|
+
import { setImageStoreSession } from '../core/session-image-store.js';
|
|
11
14
|
import { cancelActiveCommand } from '../executor.js';
|
|
15
|
+
import { isTurnFailureMarker } from '../turn-failure-marker.js';
|
|
16
|
+
import { clearCliAuthConfig, logoutFromServer, } from '../api/auth.js';
|
|
17
|
+
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
18
|
+
import { createUserInputPromptState, formatUserInputTranscript, } from './tui/user-input.js';
|
|
12
19
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
13
20
|
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
14
|
-
import {
|
|
15
|
-
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
|
|
21
|
+
import { startNewConversation, } from '../session.js';
|
|
22
|
+
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../session-store.js';
|
|
16
23
|
import { truncate } from '../utils.js';
|
|
17
24
|
import { writeClipboardText } from '../core/clipboard.js';
|
|
18
25
|
import { openUrl } from '../core/open-url.js';
|
|
@@ -69,23 +76,35 @@ const WORKING_TOOL_PREVIEW_ITEMS = 4;
|
|
|
69
76
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
70
77
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
71
78
|
const AGENT_MODE_LABEL_WIDTH = 16;
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
];
|
|
79
|
+
export const REPORT_ISSUE_URL = 'https://github.com/thegitai/thegitai-cli/issues/new';
|
|
80
|
+
export function buildReportTranscriptEntries(openedUrl) {
|
|
81
|
+
return openedUrl
|
|
82
|
+
? [
|
|
83
|
+
{
|
|
84
|
+
body: `Opening the public GitHub issue form in your browser. If nothing opened, visit:\n\n${REPORT_ISSUE_URL}\n\nThe tracker is public — do not paste secrets or code you may not share.`,
|
|
85
|
+
kind: 'system',
|
|
86
|
+
title: 'Report',
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
: [
|
|
90
|
+
{
|
|
91
|
+
body: `Could not open a browser. Report the bug manually at:\n\n${REPORT_ISSUE_URL}\n`,
|
|
92
|
+
kind: 'error',
|
|
93
|
+
title: 'Report',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
export function buildTurnFailureHintEntries(cancelled) {
|
|
98
|
+
if (cancelled)
|
|
99
|
+
return [];
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
body: 'Something went wrong? Use /report to submit the issue.',
|
|
103
|
+
kind: 'error',
|
|
104
|
+
title: 'Report',
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
}
|
|
89
108
|
export const SLASH_COMMANDS = [
|
|
90
109
|
{
|
|
91
110
|
command: '/help',
|
|
@@ -95,6 +114,10 @@ export const SLASH_COMMANDS = [
|
|
|
95
114
|
command: '/about',
|
|
96
115
|
description: 'Show version and platform info',
|
|
97
116
|
},
|
|
117
|
+
{
|
|
118
|
+
command: '/report',
|
|
119
|
+
description: 'Report a bug: opens the public GitHub issue form',
|
|
120
|
+
},
|
|
98
121
|
{
|
|
99
122
|
command: '/usage',
|
|
100
123
|
description: 'Show account usage percentage and reset times',
|
|
@@ -112,8 +135,12 @@ export const SLASH_COMMANDS = [
|
|
|
112
135
|
description: 'Background jobs: pick to view output or kill',
|
|
113
136
|
},
|
|
114
137
|
{
|
|
115
|
-
command: '/
|
|
116
|
-
description: '
|
|
138
|
+
command: '/new',
|
|
139
|
+
description: 'start a new conversation; this session remains saved',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
command: '/logout',
|
|
143
|
+
description: 'Sign out and quit',
|
|
117
144
|
},
|
|
118
145
|
{
|
|
119
146
|
command: '/exit',
|
|
@@ -514,6 +541,8 @@ function formatToolResultState(result) {
|
|
|
514
541
|
return 'Blocked';
|
|
515
542
|
if (result?.ok === true)
|
|
516
543
|
return 'OK';
|
|
544
|
+
if (Number.isInteger(result?.httpStatus))
|
|
545
|
+
return String(result.httpStatus);
|
|
517
546
|
return 'Failed';
|
|
518
547
|
}
|
|
519
548
|
function formatRunCommandResultState(result) {
|
|
@@ -670,7 +699,7 @@ function buildFileChangeEntry(event) {
|
|
|
670
699
|
title: `Edited ${filePath}${summary}`,
|
|
671
700
|
};
|
|
672
701
|
}
|
|
673
|
-
function buildWorkingToolEntry(event) {
|
|
702
|
+
export function buildWorkingToolEntry(event) {
|
|
674
703
|
const { call, result } = event;
|
|
675
704
|
const error = typeof result?.error === 'string' && result.error.trim()
|
|
676
705
|
? `\n${truncate(result.error.trim(), 180)}`
|
|
@@ -780,13 +809,21 @@ function displayUserTextFromHistoryEntry(entry) {
|
|
|
780
809
|
.slice(contentStart, contentEnd === -1 ? text.length : contentEnd)
|
|
781
810
|
.trim();
|
|
782
811
|
}
|
|
783
|
-
function buildTranscriptFromSessionHistory(history) {
|
|
812
|
+
export function buildTranscriptFromSessionHistory(history) {
|
|
784
813
|
const entries = [];
|
|
785
814
|
const pendingCalls = new Map();
|
|
815
|
+
const pendingUserInputRequests = new Map();
|
|
786
816
|
for (const entry of history) {
|
|
787
817
|
if (!entry?.parts?.length)
|
|
788
818
|
continue;
|
|
789
819
|
for (const part of entry.parts) {
|
|
820
|
+
const userInputRequest = part?.userInputRequest;
|
|
821
|
+
if (userInputRequest && typeof userInputRequest === 'object') {
|
|
822
|
+
const requestId = String(userInputRequest.requestId ?? '').trim();
|
|
823
|
+
if (requestId && Array.isArray(userInputRequest.questions)) {
|
|
824
|
+
pendingUserInputRequests.set(requestId, userInputRequest.questions);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
790
827
|
const call = part?.functionCall;
|
|
791
828
|
if (!call || typeof call !== 'object')
|
|
792
829
|
continue;
|
|
@@ -796,6 +833,21 @@ function buildTranscriptFromSessionHistory(history) {
|
|
|
796
833
|
pendingCalls.set(callId, call);
|
|
797
834
|
}
|
|
798
835
|
for (const part of entry.parts) {
|
|
836
|
+
const userInputResult = part?.userInputResult;
|
|
837
|
+
if (userInputResult && typeof userInputResult === 'object') {
|
|
838
|
+
const requestId = String(userInputResult.requestId ?? '').trim();
|
|
839
|
+
const questions = pendingUserInputRequests.get(requestId);
|
|
840
|
+
const result = userInputResult.result;
|
|
841
|
+
if (questions &&
|
|
842
|
+
(result?.status === 'submitted' || result?.status === 'cancelled')) {
|
|
843
|
+
pendingUserInputRequests.delete(requestId);
|
|
844
|
+
entries.push({
|
|
845
|
+
body: formatUserInputTranscript(questions, result),
|
|
846
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
847
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
799
851
|
const functionResponse = part?.functionResponse;
|
|
800
852
|
if (!functionResponse || typeof functionResponse !== 'object')
|
|
801
853
|
continue;
|
|
@@ -824,8 +876,23 @@ function buildTranscriptFromSessionHistory(history) {
|
|
|
824
876
|
}
|
|
825
877
|
continue;
|
|
826
878
|
}
|
|
879
|
+
if (entry.role === 'user' && entry.kind === 'userInterjection') {
|
|
880
|
+
const text = String(entry.userInput ?? '').trim();
|
|
881
|
+
if (text) {
|
|
882
|
+
entries.push({ body: text, kind: 'user', title: 'You · sent mid-turn' });
|
|
883
|
+
}
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
827
886
|
const text = textFromHistoryEntry(entry);
|
|
828
887
|
if ((entry.role === 'model' || entry.role === 'assistant') && text) {
|
|
888
|
+
if (isTurnFailureMarker(text)) {
|
|
889
|
+
entries.push({
|
|
890
|
+
body: 'This request did not complete.',
|
|
891
|
+
kind: 'system',
|
|
892
|
+
title: 'Previous turn',
|
|
893
|
+
});
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
829
896
|
entries.push({ body: text, kind: 'assistant', title: 'Response' });
|
|
830
897
|
}
|
|
831
898
|
}
|
|
@@ -951,11 +1018,15 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
951
1018
|
activeTurnInput: '',
|
|
952
1019
|
activeTurnInputPreformatted: false,
|
|
953
1020
|
agentMode: session.agentMode,
|
|
954
|
-
|
|
1021
|
+
analyzingImages: 0,
|
|
1022
|
+
generatingImage: false,
|
|
1023
|
+
approvalCursor: 0,
|
|
955
1024
|
approvalPrompt: null,
|
|
1025
|
+
approvalScrollOffset: 0,
|
|
956
1026
|
autoYes: session.autoYes,
|
|
957
1027
|
backgroundJobs: [],
|
|
958
1028
|
busy: false,
|
|
1029
|
+
busyPausedAt: null,
|
|
959
1030
|
busySince: null,
|
|
960
1031
|
clockNow: Date.now(),
|
|
961
1032
|
commandCursor: 0,
|
|
@@ -983,6 +1054,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
983
1054
|
resumePickerSessions: [],
|
|
984
1055
|
transcriptScrollOffset: 0,
|
|
985
1056
|
queuedMessage: null,
|
|
1057
|
+
turnMessages: [],
|
|
986
1058
|
serverModels: serverModels.models,
|
|
987
1059
|
sudoPrompt: null,
|
|
988
1060
|
status: 'Ready',
|
|
@@ -993,6 +1065,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
993
1065
|
tokenUsage: formatClientTokenUsage(null),
|
|
994
1066
|
transcript: [],
|
|
995
1067
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
1068
|
+
userInputPrompt: null,
|
|
996
1069
|
pastedChunks: [],
|
|
997
1070
|
imageAttachments: [],
|
|
998
1071
|
workingTools: [],
|
|
@@ -1009,7 +1082,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1009
1082
|
const reasoningTokens = usageSummary?.reasoningTokens ?? 0;
|
|
1010
1083
|
const cacheTokens = usageSummary?.cacheTokens ?? 0;
|
|
1011
1084
|
const cacheWriteTokens = usageSummary?.cacheWriteTokens ?? 0;
|
|
1012
|
-
const indexTokens = usageSummary?.indexTokens ?? 0;
|
|
1013
1085
|
return [
|
|
1014
1086
|
'Session tokens',
|
|
1015
1087
|
`in ${formatTokenCount(inputTokens)}`,
|
|
@@ -1017,7 +1089,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1017
1089
|
...(reasoningTokens > 0 ? [`think ${formatTokenCount(reasoningTokens)}`] : []),
|
|
1018
1090
|
`cache ${formatTokenCount(cacheTokens)}`,
|
|
1019
1091
|
`write ${formatTokenCount(cacheWriteTokens)}`,
|
|
1020
|
-
`index ${formatTokenCount(indexTokens)}`,
|
|
1021
1092
|
].join(' • ');
|
|
1022
1093
|
}
|
|
1023
1094
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
@@ -1060,6 +1131,24 @@ function splitThinkingLines(text) {
|
|
|
1060
1131
|
.map((part) => part.trim())
|
|
1061
1132
|
.filter(Boolean));
|
|
1062
1133
|
}
|
|
1134
|
+
const LIVE_STATUS_PANEL_MAX_CHARS = 72;
|
|
1135
|
+
const TOOL_PROGRESS_STATUS_PATTERN = /^(?:Tool:\s|Running\s\S+(?:\slocally)?\.\.\.$)/i;
|
|
1136
|
+
export function isToolProgressStatus(status) {
|
|
1137
|
+
return TOOL_PROGRESS_STATUS_PATTERN.test(String(status ?? '').trim());
|
|
1138
|
+
}
|
|
1139
|
+
function holdOrPickFallback(currentTitle) {
|
|
1140
|
+
return THINKING_FALLBACK_PHRASES.includes(currentTitle.trim())
|
|
1141
|
+
? currentTitle
|
|
1142
|
+
: pickThinkingFallbackPhrase();
|
|
1143
|
+
}
|
|
1144
|
+
function liveStatusPanelTitle(status) {
|
|
1145
|
+
const text = String(status ?? '').trim();
|
|
1146
|
+
if (!text || text.includes('\n'))
|
|
1147
|
+
return '';
|
|
1148
|
+
if (isToolProgressStatus(text))
|
|
1149
|
+
return '';
|
|
1150
|
+
return text.length <= LIVE_STATUS_PANEL_MAX_CHARS ? text : '';
|
|
1151
|
+
}
|
|
1063
1152
|
function thinkingPanelFromStatus(status) {
|
|
1064
1153
|
const rawText = thinkingNoteFromStatus(status);
|
|
1065
1154
|
if (!rawText)
|
|
@@ -1083,7 +1172,7 @@ function thinkingPanelFromStatus(status) {
|
|
|
1083
1172
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
1084
1173
|
return {
|
|
1085
1174
|
title,
|
|
1086
|
-
notes: splitThinkingLines(bodyLines.join('\n')).slice(
|
|
1175
|
+
notes: splitThinkingLines(bodyLines.join('\n')).slice(0, 3),
|
|
1087
1176
|
};
|
|
1088
1177
|
}
|
|
1089
1178
|
export const EXIT_CTRL_C_CONFIRM_MESSAGE = 'Press Ctrl+C again to quit.';
|
|
@@ -1213,26 +1302,37 @@ export function navigatePromptHistory(state, direction) {
|
|
|
1213
1302
|
promptHistoryCursor: nextCursor,
|
|
1214
1303
|
};
|
|
1215
1304
|
}
|
|
1216
|
-
function getDefaultApprovalCursor() {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1305
|
+
export function getDefaultApprovalCursor(optionCount) {
|
|
1306
|
+
return Math.max(0, optionCount - 1);
|
|
1307
|
+
}
|
|
1308
|
+
export function getNextApprovalCursor(currentIndex, direction, optionCount) {
|
|
1309
|
+
if (optionCount <= 0)
|
|
1310
|
+
return 0;
|
|
1311
|
+
return (currentIndex + direction + optionCount) % optionCount;
|
|
1312
|
+
}
|
|
1313
|
+
export function pauseBusyClock(state, nowMs) {
|
|
1314
|
+
if (state.busySince === null || state.busyPausedAt !== null)
|
|
1315
|
+
return state;
|
|
1316
|
+
return { ...state, busyPausedAt: nowMs };
|
|
1317
|
+
}
|
|
1318
|
+
export function resumeBusyClock(state, nowMs) {
|
|
1319
|
+
if (state.busyPausedAt === null)
|
|
1320
|
+
return state;
|
|
1321
|
+
return {
|
|
1322
|
+
...state,
|
|
1323
|
+
busyPausedAt: null,
|
|
1324
|
+
busySince: state.busySince === null
|
|
1325
|
+
? null
|
|
1326
|
+
: state.busySince + Math.max(0, nowMs - state.busyPausedAt),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
export function busyElapsedMs(state, nowMs) {
|
|
1330
|
+
if (state.busySince === null)
|
|
1331
|
+
return null;
|
|
1332
|
+
return Math.max(0, (state.busyPausedAt ?? nowMs) - state.busySince);
|
|
1333
|
+
}
|
|
1334
|
+
export function busyElapsedSeconds(state, nowMs) {
|
|
1335
|
+
return Math.floor((busyElapsedMs(state, nowMs) ?? 0) / 1000);
|
|
1236
1336
|
}
|
|
1237
1337
|
export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
1238
1338
|
return serverModels.map((model) => ({
|
|
@@ -1270,35 +1370,37 @@ function appendCommandLog(state, text) {
|
|
|
1270
1370
|
};
|
|
1271
1371
|
}
|
|
1272
1372
|
async function saveSessionBoth({ serverSessionClient, session, }) {
|
|
1373
|
+
if (!sessionHasUserMessage(session))
|
|
1374
|
+
return;
|
|
1273
1375
|
saveSessionState(session);
|
|
1274
1376
|
await serverSessionClient.save(session);
|
|
1275
1377
|
}
|
|
1276
|
-
function
|
|
1277
|
-
if (process.
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
return shouldUseRatatuiShell();
|
|
1283
|
-
}
|
|
1284
|
-
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
1285
|
-
if (!shouldUseRatatuiShell()) {
|
|
1286
|
-
throw new Error('Client TUI requires an interactive terminal.');
|
|
1378
|
+
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
1379
|
+
if (process.stdin.isTTY !== true) {
|
|
1380
|
+
throw new Error('stdin is not a terminal');
|
|
1381
|
+
}
|
|
1382
|
+
if (process.stdout.isTTY !== true) {
|
|
1383
|
+
throw new Error('stdout is not a terminal');
|
|
1287
1384
|
}
|
|
1385
|
+
let fatalError = null;
|
|
1386
|
+
let signedOut = false;
|
|
1288
1387
|
await withTuiMode(async () => {
|
|
1289
1388
|
setBackgroundJobSession(session.sessionId);
|
|
1290
1389
|
setScratchSession(session.sessionId);
|
|
1390
|
+
setImageStoreSession(session.sessionId);
|
|
1291
1391
|
setTodoSession(session.sessionId);
|
|
1292
1392
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1293
1393
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1294
1394
|
let currentServerModels = serverModels;
|
|
1295
1395
|
let sessionAutoYes = session.autoYes;
|
|
1296
1396
|
let resolveDone = null;
|
|
1297
|
-
let
|
|
1397
|
+
let resolvePermissionDecision = null;
|
|
1298
1398
|
let resolveSudoPassword = null;
|
|
1399
|
+
let pendingUserInput = null;
|
|
1299
1400
|
let cleanupSudoPasswordPrompt = null;
|
|
1300
1401
|
let sudoPasswordBuffer = '';
|
|
1301
1402
|
const bridge = createRatatuiBridge();
|
|
1403
|
+
captureTerminalWrites();
|
|
1302
1404
|
const { handleShellKeyEvent } = await import('./tui/shell-input.js');
|
|
1303
1405
|
let terminalCols = 80;
|
|
1304
1406
|
let terminalRows = 24;
|
|
@@ -1311,6 +1413,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1311
1413
|
let latestUsageSummary = null;
|
|
1312
1414
|
let pendingTurnEntries = [];
|
|
1313
1415
|
let activeTurnAbort = null;
|
|
1416
|
+
let activeServerTurnId = null;
|
|
1417
|
+
let unacknowledged = new Map();
|
|
1418
|
+
const blockedRows = new WeakMap();
|
|
1419
|
+
let newConversationInFlight = false;
|
|
1314
1420
|
let todosTouchedThisTurn = false;
|
|
1315
1421
|
const syncTodosState = () => {
|
|
1316
1422
|
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
@@ -1319,6 +1425,36 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1319
1425
|
let exitCtrlCArmed = false;
|
|
1320
1426
|
let exitCtrlCTimer = null;
|
|
1321
1427
|
let transientStatusTimer = null;
|
|
1428
|
+
const THINKING_MIN_DWELL_MS = 3_000;
|
|
1429
|
+
let thinkingAppliedAt = 0;
|
|
1430
|
+
let pendingThinking = null;
|
|
1431
|
+
const applyThinking = (next) => {
|
|
1432
|
+
thinkingAppliedAt = Date.now();
|
|
1433
|
+
pendingThinking = null;
|
|
1434
|
+
store.update((current) => ({
|
|
1435
|
+
...current,
|
|
1436
|
+
thinkingTitle: next.title || holdOrPickFallback(current.thinkingTitle),
|
|
1437
|
+
thinkingNotes: next.notes,
|
|
1438
|
+
}));
|
|
1439
|
+
};
|
|
1440
|
+
const queueThinkingUpdate = (next) => {
|
|
1441
|
+
if (Date.now() - thinkingAppliedAt >= THINKING_MIN_DWELL_MS) {
|
|
1442
|
+
applyThinking(next);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
pendingThinking = next;
|
|
1446
|
+
};
|
|
1447
|
+
const flushPendingThinking = () => {
|
|
1448
|
+
if (!pendingThinking)
|
|
1449
|
+
return;
|
|
1450
|
+
if (Date.now() - thinkingAppliedAt < THINKING_MIN_DWELL_MS)
|
|
1451
|
+
return;
|
|
1452
|
+
applyThinking(pendingThinking);
|
|
1453
|
+
};
|
|
1454
|
+
const resetThinkingPacer = () => {
|
|
1455
|
+
thinkingAppliedAt = 0;
|
|
1456
|
+
pendingThinking = null;
|
|
1457
|
+
};
|
|
1322
1458
|
const done = new Promise((resolve) => {
|
|
1323
1459
|
resolveDone = resolve;
|
|
1324
1460
|
});
|
|
@@ -1343,7 +1479,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1343
1479
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1344
1480
|
}, 2500);
|
|
1345
1481
|
};
|
|
1346
|
-
const terminalTitle = createTerminalTitleController(
|
|
1482
|
+
const terminalTitle = createTerminalTitleController({
|
|
1483
|
+
write: (title) => bridge.setTitle(title),
|
|
1484
|
+
});
|
|
1347
1485
|
const syncTerminalTitle = () => {
|
|
1348
1486
|
const state = store.getState();
|
|
1349
1487
|
terminalTitle.sync({
|
|
@@ -1356,9 +1494,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1356
1494
|
return;
|
|
1357
1495
|
const state = store.getState();
|
|
1358
1496
|
syncTerminalTitle();
|
|
1359
|
-
const elapsedSeconds = state.
|
|
1360
|
-
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1361
|
-
: 0;
|
|
1497
|
+
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1362
1498
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1363
1499
|
};
|
|
1364
1500
|
const remountTui = async () => {
|
|
@@ -1484,15 +1620,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1484
1620
|
}
|
|
1485
1621
|
};
|
|
1486
1622
|
const dismissPendingApproval = () => {
|
|
1487
|
-
if (!
|
|
1623
|
+
if (!resolvePermissionDecision)
|
|
1488
1624
|
return;
|
|
1489
|
-
const pendingResolve =
|
|
1490
|
-
|
|
1491
|
-
pendingResolve('
|
|
1625
|
+
const pendingResolve = resolvePermissionDecision;
|
|
1626
|
+
resolvePermissionDecision = null;
|
|
1627
|
+
pendingResolve({ kind: 'deny' });
|
|
1492
1628
|
store.update((current) => ({
|
|
1493
|
-
...current,
|
|
1494
|
-
approvalCursor:
|
|
1629
|
+
...resumeBusyClock(current, Date.now()),
|
|
1630
|
+
approvalCursor: 0,
|
|
1495
1631
|
approvalPrompt: null,
|
|
1632
|
+
approvalScrollOffset: 0,
|
|
1496
1633
|
}));
|
|
1497
1634
|
};
|
|
1498
1635
|
const dismissPendingSudoPassword = () => {
|
|
@@ -1505,17 +1642,34 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1505
1642
|
sudoPasswordBuffer = '';
|
|
1506
1643
|
pendingResolve(null);
|
|
1507
1644
|
store.update((current) => ({
|
|
1508
|
-
...current,
|
|
1645
|
+
...resumeBusyClock(current, Date.now()),
|
|
1509
1646
|
sudoPrompt: null,
|
|
1510
1647
|
}));
|
|
1511
1648
|
};
|
|
1649
|
+
const dismissPendingUserInput = () => {
|
|
1650
|
+
const pending = pendingUserInput;
|
|
1651
|
+
if (!pending)
|
|
1652
|
+
return;
|
|
1653
|
+
pendingUserInput = null;
|
|
1654
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1655
|
+
const error = new Error('Turn cancelled.');
|
|
1656
|
+
error.name = 'AbortError';
|
|
1657
|
+
pending.reject(error);
|
|
1658
|
+
store.update((current) => ({
|
|
1659
|
+
...resumeBusyClock(current, Date.now()),
|
|
1660
|
+
status: current.userInputPrompt?.returnStatus ?? current.status,
|
|
1661
|
+
userInputPrompt: null,
|
|
1662
|
+
}));
|
|
1663
|
+
};
|
|
1512
1664
|
const cancelActiveTurn = () => {
|
|
1513
|
-
if (!store.getState().busy)
|
|
1665
|
+
if (!store.getState().busy || newConversationInFlight)
|
|
1514
1666
|
return;
|
|
1515
1667
|
disarmExitConfirm();
|
|
1516
1668
|
dismissPendingApproval();
|
|
1517
1669
|
dismissPendingSudoPassword();
|
|
1670
|
+
dismissPendingUserInput();
|
|
1518
1671
|
activeTurnGeneration += 1;
|
|
1672
|
+
resetThinkingPacer();
|
|
1519
1673
|
activeTurnAbort?.abort();
|
|
1520
1674
|
activeTurnAbort = null;
|
|
1521
1675
|
cancelActiveCommand();
|
|
@@ -1533,6 +1687,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1533
1687
|
activeTurnInput: '',
|
|
1534
1688
|
activeTurnInputPreformatted: false,
|
|
1535
1689
|
busy: false,
|
|
1690
|
+
busyPausedAt: null,
|
|
1536
1691
|
busySince: null,
|
|
1537
1692
|
commandLog: [],
|
|
1538
1693
|
cursor: queued ? queued.body.length : current.cursor,
|
|
@@ -1545,8 +1700,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1545
1700
|
status: 'Ready',
|
|
1546
1701
|
thinkingTitle: '',
|
|
1547
1702
|
thinkingNotes: [],
|
|
1703
|
+
turnMessages: [],
|
|
1548
1704
|
workingTools: [],
|
|
1549
|
-
tokenUsage: formatClientTokenUsage(current
|
|
1705
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1550
1706
|
}));
|
|
1551
1707
|
appendStaticEntries(cancelledEntries);
|
|
1552
1708
|
lastTurnStartedAt = null;
|
|
@@ -1554,14 +1710,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1554
1710
|
scheduleLiveFrameRemount();
|
|
1555
1711
|
void remountTui();
|
|
1556
1712
|
};
|
|
1557
|
-
const saveActiveSession = async () => {
|
|
1558
|
-
try {
|
|
1559
|
-
await saveSessionBoth({ serverSessionClient, session });
|
|
1560
|
-
}
|
|
1561
|
-
catch (error) {
|
|
1562
|
-
appendError(`Session save failed: ${error.message}`);
|
|
1563
|
-
}
|
|
1564
|
-
};
|
|
1565
1713
|
const syncShellStateFromSession = () => {
|
|
1566
1714
|
store.update((current) => ({
|
|
1567
1715
|
...current,
|
|
@@ -1640,6 +1788,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1640
1788
|
}
|
|
1641
1789
|
dismissPendingApproval();
|
|
1642
1790
|
dismissPendingSudoPassword();
|
|
1791
|
+
dismissPendingUserInput();
|
|
1643
1792
|
exiting = true;
|
|
1644
1793
|
killAllBackgroundJobs();
|
|
1645
1794
|
store.update((current) => ({
|
|
@@ -1649,9 +1798,45 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1649
1798
|
status: 'Exiting...',
|
|
1650
1799
|
}));
|
|
1651
1800
|
void bridge.close().then(() => {
|
|
1801
|
+
releaseTerminalWrites();
|
|
1652
1802
|
resolveDone?.();
|
|
1653
1803
|
});
|
|
1654
1804
|
};
|
|
1805
|
+
const exitForAuthenticationError = (error) => {
|
|
1806
|
+
if (!isAuthenticationError(error))
|
|
1807
|
+
return false;
|
|
1808
|
+
clearCliAuthConfig(session.env);
|
|
1809
|
+
if (sessionHasUserMessage(session)) {
|
|
1810
|
+
saveSessionState(session);
|
|
1811
|
+
}
|
|
1812
|
+
fatalError = new Error(authenticationErrorMessage(error));
|
|
1813
|
+
requestExit();
|
|
1814
|
+
return true;
|
|
1815
|
+
};
|
|
1816
|
+
const saveActiveSession = async () => {
|
|
1817
|
+
try {
|
|
1818
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1819
|
+
return true;
|
|
1820
|
+
}
|
|
1821
|
+
catch (error) {
|
|
1822
|
+
if (exitForAuthenticationError(error))
|
|
1823
|
+
return false;
|
|
1824
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1825
|
+
return true;
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
const saveActiveSessionOrAbort = async () => {
|
|
1829
|
+
try {
|
|
1830
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1831
|
+
return true;
|
|
1832
|
+
}
|
|
1833
|
+
catch (error) {
|
|
1834
|
+
if (exitForAuthenticationError(error))
|
|
1835
|
+
return false;
|
|
1836
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1655
1840
|
const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
|
|
1656
1841
|
if (exiting || signal?.aborted) {
|
|
1657
1842
|
resolve(null);
|
|
@@ -1669,7 +1854,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1669
1854
|
cleanupSudoPasswordPrompt = null;
|
|
1670
1855
|
}
|
|
1671
1856
|
store.update((current) => ({
|
|
1672
|
-
...current,
|
|
1857
|
+
...pauseBusyClock(current, Date.now()),
|
|
1673
1858
|
sudoPrompt: {
|
|
1674
1859
|
command,
|
|
1675
1860
|
passwordLength: 0,
|
|
@@ -1689,7 +1874,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1689
1874
|
cleanupSudoPasswordPrompt = null;
|
|
1690
1875
|
sudoPasswordBuffer = '';
|
|
1691
1876
|
store.update((current) => ({
|
|
1692
|
-
...current,
|
|
1877
|
+
...resumeBusyClock(current, Date.now()),
|
|
1693
1878
|
sudoPrompt: null,
|
|
1694
1879
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1695
1880
|
}));
|
|
@@ -1703,7 +1888,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1703
1888
|
cleanupSudoPasswordPrompt = null;
|
|
1704
1889
|
sudoPasswordBuffer = '';
|
|
1705
1890
|
store.update((current) => ({
|
|
1706
|
-
...current,
|
|
1891
|
+
...resumeBusyClock(current, Date.now()),
|
|
1707
1892
|
sudoPrompt: null,
|
|
1708
1893
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1709
1894
|
}));
|
|
@@ -1723,38 +1908,88 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1723
1908
|
: null,
|
|
1724
1909
|
}));
|
|
1725
1910
|
};
|
|
1726
|
-
const
|
|
1911
|
+
const openUserInputPrompt = (questions, signal) => new Promise((resolve, reject) => {
|
|
1912
|
+
if (exiting || signal?.aborted) {
|
|
1913
|
+
const error = new Error('Turn cancelled.');
|
|
1914
|
+
error.name = 'AbortError';
|
|
1915
|
+
reject(error);
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
const onAbort = () => dismissPendingUserInput();
|
|
1919
|
+
pendingUserInput = {
|
|
1920
|
+
questions,
|
|
1921
|
+
resolve,
|
|
1922
|
+
reject,
|
|
1923
|
+
signal,
|
|
1924
|
+
onAbort,
|
|
1925
|
+
};
|
|
1926
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1927
|
+
store.update((current) => ({
|
|
1928
|
+
...pauseBusyClock(current, Date.now()),
|
|
1929
|
+
status: 'Waiting for your input',
|
|
1930
|
+
userInputPrompt: createUserInputPromptState(questions, current.status),
|
|
1931
|
+
}));
|
|
1932
|
+
scheduleLiveFrameRemount();
|
|
1933
|
+
});
|
|
1934
|
+
const handleInlineUserInput = async (result) => {
|
|
1935
|
+
const pending = pendingUserInput;
|
|
1936
|
+
const prompt = store.getState().userInputPrompt;
|
|
1937
|
+
if (!pending || !prompt)
|
|
1938
|
+
return;
|
|
1939
|
+
pendingUserInput = null;
|
|
1940
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1941
|
+
store.update((current) => ({
|
|
1942
|
+
...resumeBusyClock(current, Date.now()),
|
|
1943
|
+
status: prompt.returnStatus,
|
|
1944
|
+
userInputPrompt: null,
|
|
1945
|
+
}));
|
|
1946
|
+
appendTurnAwareEntry({
|
|
1947
|
+
body: formatUserInputTranscript(pending.questions, result),
|
|
1948
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
1949
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
1950
|
+
});
|
|
1951
|
+
pending.resolve(result);
|
|
1952
|
+
scheduleLiveFrameRemount();
|
|
1953
|
+
};
|
|
1954
|
+
const openApprovalPrompt = (request) => new Promise((resolve) => {
|
|
1955
|
+
const deny = { kind: 'deny' };
|
|
1727
1956
|
if (exiting) {
|
|
1728
|
-
resolve(
|
|
1957
|
+
resolve(deny);
|
|
1729
1958
|
return;
|
|
1730
1959
|
}
|
|
1731
|
-
|
|
1960
|
+
resolvePermissionDecision = resolve;
|
|
1732
1961
|
store.update((current) => ({
|
|
1733
|
-
...current,
|
|
1734
|
-
approvalCursor: getDefaultApprovalCursor(),
|
|
1962
|
+
...pauseBusyClock(current, Date.now()),
|
|
1963
|
+
approvalCursor: getDefaultApprovalCursor(request.options.length),
|
|
1964
|
+
approvalOpenedAt: Date.now(),
|
|
1965
|
+
approvalScrollOffset: 0,
|
|
1735
1966
|
approvalPrompt: {
|
|
1736
|
-
title,
|
|
1737
|
-
body,
|
|
1738
|
-
diffPreview:
|
|
1739
|
-
? parseDiffPreview(
|
|
1967
|
+
title: request.title,
|
|
1968
|
+
body: request.body,
|
|
1969
|
+
diffPreview: request.diff && request.filePath
|
|
1970
|
+
? parseDiffPreview(request.diff)
|
|
1740
1971
|
: undefined,
|
|
1741
|
-
filePath:
|
|
1972
|
+
filePath: request.filePath,
|
|
1973
|
+
options: request.options,
|
|
1742
1974
|
returnStatus: current.status,
|
|
1743
1975
|
},
|
|
1744
|
-
status: title,
|
|
1976
|
+
status: request.title,
|
|
1745
1977
|
}));
|
|
1746
1978
|
});
|
|
1747
|
-
const handleInlineApprovalChoice = async (
|
|
1979
|
+
const handleInlineApprovalChoice = async (index) => {
|
|
1748
1980
|
const current = store.getState();
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1981
|
+
const options = current.approvalPrompt?.options ?? [];
|
|
1982
|
+
const chosen = options[index]?.decision ?? { kind: 'deny' };
|
|
1983
|
+
const pendingResolve = resolvePermissionDecision;
|
|
1984
|
+
resolvePermissionDecision = null;
|
|
1751
1985
|
store.update((next) => ({
|
|
1752
|
-
...next,
|
|
1753
|
-
approvalCursor: getDefaultApprovalCursor(),
|
|
1986
|
+
...resumeBusyClock(next, Date.now()),
|
|
1987
|
+
approvalCursor: getDefaultApprovalCursor(options.length),
|
|
1754
1988
|
approvalPrompt: null,
|
|
1989
|
+
approvalScrollOffset: 0,
|
|
1755
1990
|
status: current.approvalPrompt?.returnStatus ?? next.status,
|
|
1756
1991
|
}));
|
|
1757
|
-
pendingResolve?.(
|
|
1992
|
+
pendingResolve?.(chosen);
|
|
1758
1993
|
};
|
|
1759
1994
|
const refreshServerModels = async () => {
|
|
1760
1995
|
currentServerModels = await models.fetchServerModels({ config: authConfig });
|
|
@@ -1788,7 +2023,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1788
2023
|
selectedModelId: selected,
|
|
1789
2024
|
serverModels: fresh,
|
|
1790
2025
|
});
|
|
1791
|
-
await saveActiveSession()
|
|
2026
|
+
if (!(await saveActiveSession()))
|
|
2027
|
+
return;
|
|
1792
2028
|
syncShellStateFromSession();
|
|
1793
2029
|
appendStaticEntry({
|
|
1794
2030
|
body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
|
|
@@ -1813,7 +2049,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1813
2049
|
const id = jobId.trim();
|
|
1814
2050
|
await collectBackgroundJobUiOutputMutations({
|
|
1815
2051
|
session,
|
|
1816
|
-
projectIndex,
|
|
1817
2052
|
jobId: id,
|
|
1818
2053
|
});
|
|
1819
2054
|
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
@@ -1845,7 +2080,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1845
2080
|
return;
|
|
1846
2081
|
await collectBackgroundJobUiOutputMutations({
|
|
1847
2082
|
session,
|
|
1848
|
-
projectIndex,
|
|
1849
2083
|
jobId,
|
|
1850
2084
|
});
|
|
1851
2085
|
syncBackgroundJobsState();
|
|
@@ -1861,7 +2095,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1861
2095
|
const killed = await killBackgroundJob(jobId);
|
|
1862
2096
|
await collectBackgroundJobUiKillMutations({
|
|
1863
2097
|
session,
|
|
1864
|
-
projectIndex,
|
|
1865
2098
|
jobId,
|
|
1866
2099
|
result: killed,
|
|
1867
2100
|
});
|
|
@@ -1907,6 +2140,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1907
2140
|
await switchToModel(selected.id);
|
|
1908
2141
|
}
|
|
1909
2142
|
catch (error) {
|
|
2143
|
+
if (exitForAuthenticationError(error))
|
|
2144
|
+
return;
|
|
1910
2145
|
appendError(error.message);
|
|
1911
2146
|
}
|
|
1912
2147
|
};
|
|
@@ -1956,6 +2191,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1956
2191
|
store.update((next) => ({
|
|
1957
2192
|
...next,
|
|
1958
2193
|
busy: true,
|
|
2194
|
+
busyPausedAt: null,
|
|
1959
2195
|
busySince: resumeStartedAt,
|
|
1960
2196
|
clockNow: resumeStartedAt,
|
|
1961
2197
|
status: 'Loading session...',
|
|
@@ -1965,14 +2201,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1965
2201
|
applySessionSnapshot(session, snapshot);
|
|
1966
2202
|
setBackgroundJobSession(session.sessionId);
|
|
1967
2203
|
setScratchSession(session.sessionId);
|
|
2204
|
+
setImageStoreSession(session.sessionId);
|
|
1968
2205
|
syncBackgroundJobsState();
|
|
1969
2206
|
setTodoSession(session.sessionId);
|
|
1970
2207
|
syncTodosState();
|
|
1971
|
-
await saveActiveSession();
|
|
1972
2208
|
syncShellStateFromSession();
|
|
1973
2209
|
store.update((next) => ({
|
|
1974
2210
|
...next,
|
|
1975
2211
|
busy: false,
|
|
2212
|
+
busyPausedAt: null,
|
|
1976
2213
|
busySince: null,
|
|
1977
2214
|
resumePickerFilter: '',
|
|
1978
2215
|
resumePickerIndex: 0,
|
|
@@ -1991,9 +2228,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1991
2228
|
await remountTui();
|
|
1992
2229
|
}
|
|
1993
2230
|
catch (error) {
|
|
2231
|
+
if (exitForAuthenticationError(error))
|
|
2232
|
+
return;
|
|
1994
2233
|
store.update((next) => ({
|
|
1995
2234
|
...next,
|
|
1996
2235
|
busy: false,
|
|
2236
|
+
busyPausedAt: null,
|
|
1997
2237
|
busySince: null,
|
|
1998
2238
|
resumePickerFilter: '',
|
|
1999
2239
|
resumePickerIndex: 0,
|
|
@@ -2017,6 +2257,127 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2017
2257
|
}));
|
|
2018
2258
|
await handleSubmit(queued.body);
|
|
2019
2259
|
};
|
|
2260
|
+
let turnMessageCounter = 0;
|
|
2261
|
+
const upsertTurnMessage = (id, patch) => {
|
|
2262
|
+
store.update((current) => ({
|
|
2263
|
+
...current,
|
|
2264
|
+
turnMessages: current.turnMessages.map((message) => message.id === id ? { ...message, ...patch } : message),
|
|
2265
|
+
}));
|
|
2266
|
+
};
|
|
2267
|
+
const recoverUnacknowledgedMessages = (autoSubmits = true) => {
|
|
2268
|
+
if (unacknowledged.size === 0)
|
|
2269
|
+
return;
|
|
2270
|
+
const stranded = [...unacknowledged.values()];
|
|
2271
|
+
unacknowledged = new Map();
|
|
2272
|
+
for (const { row } of stranded) {
|
|
2273
|
+
upsertTurnMessage(row, {
|
|
2274
|
+
state: 'queued',
|
|
2275
|
+
note: autoSubmits
|
|
2276
|
+
? 'not read — sending as the next prompt'
|
|
2277
|
+
: 'not read — left in the composer',
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
const existing = store.getState().queuedMessage;
|
|
2281
|
+
const combined = [
|
|
2282
|
+
...stranded.map((entry) => entry.queued),
|
|
2283
|
+
...(existing ? [existing] : []),
|
|
2284
|
+
];
|
|
2285
|
+
const body = combined
|
|
2286
|
+
.map((entry) => entry.body)
|
|
2287
|
+
.filter((entry) => entry.trim())
|
|
2288
|
+
.join('\n\n');
|
|
2289
|
+
if (!body.trim())
|
|
2290
|
+
return;
|
|
2291
|
+
scheduleLiveFrameRemount();
|
|
2292
|
+
store.update((current) => ({
|
|
2293
|
+
...current,
|
|
2294
|
+
queuedMessage: {
|
|
2295
|
+
body,
|
|
2296
|
+
imageAttachments: combined.flatMap((entry) => entry.imageAttachments),
|
|
2297
|
+
pastedChunks: combined.flatMap((entry) => entry.pastedChunks),
|
|
2298
|
+
},
|
|
2299
|
+
}));
|
|
2300
|
+
};
|
|
2301
|
+
const fireQueuedMessage = async () => {
|
|
2302
|
+
const state = store.getState();
|
|
2303
|
+
const queued = state.queuedMessage;
|
|
2304
|
+
if (!queued || !state.busy || exiting)
|
|
2305
|
+
return;
|
|
2306
|
+
const text = (queued.pastedChunks.length
|
|
2307
|
+
? expandPastedChunks(queued.body, queued.pastedChunks)
|
|
2308
|
+
: queued.body).trim();
|
|
2309
|
+
const images = queued.imageAttachments.filter((attachment) => queued.body.includes(`[Image #${attachment.index}]`));
|
|
2310
|
+
if (!text && images.length === 0)
|
|
2311
|
+
return;
|
|
2312
|
+
const turnId = activeServerTurnId;
|
|
2313
|
+
const blockedReason = !turnId ? 'waiting for the turn to end' : null;
|
|
2314
|
+
const existingRow = blockedRows.get(queued);
|
|
2315
|
+
if (existingRow !== undefined) {
|
|
2316
|
+
upsertTurnMessage(existingRow, {
|
|
2317
|
+
state: 'queued',
|
|
2318
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2319
|
+
});
|
|
2320
|
+
if (blockedReason || !turnId)
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const id = existingRow ?? ++turnMessageCounter;
|
|
2324
|
+
const messageId = `m${id}_${Date.now().toString(36)}`;
|
|
2325
|
+
if (existingRow === undefined) {
|
|
2326
|
+
scheduleLiveFrameRemount();
|
|
2327
|
+
store.update((current) => ({
|
|
2328
|
+
...current,
|
|
2329
|
+
turnMessages: [
|
|
2330
|
+
...current.turnMessages,
|
|
2331
|
+
{
|
|
2332
|
+
id,
|
|
2333
|
+
text,
|
|
2334
|
+
state: blockedReason ? 'queued' : 'sending',
|
|
2335
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2336
|
+
},
|
|
2337
|
+
],
|
|
2338
|
+
}));
|
|
2339
|
+
}
|
|
2340
|
+
if (blockedReason || !turnId) {
|
|
2341
|
+
blockedRows.set(queued, id);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
blockedRows.delete(queued);
|
|
2345
|
+
unacknowledged.set(messageId, { row: id, queued });
|
|
2346
|
+
scheduleLiveFrameRemount();
|
|
2347
|
+
store.update((current) => current.queuedMessage === queued
|
|
2348
|
+
? { ...current, queuedMessage: null }
|
|
2349
|
+
: current);
|
|
2350
|
+
queueTurnEntry({
|
|
2351
|
+
body: text,
|
|
2352
|
+
kind: 'user',
|
|
2353
|
+
preformatted: queued.pastedChunks.length > 0,
|
|
2354
|
+
title: 'You · sent mid-turn',
|
|
2355
|
+
});
|
|
2356
|
+
const rollback = (note) => {
|
|
2357
|
+
if (!unacknowledged.delete(messageId))
|
|
2358
|
+
return;
|
|
2359
|
+
upsertTurnMessage(id, { state: 'queued', note });
|
|
2360
|
+
scheduleLiveFrameRemount();
|
|
2361
|
+
store.update((current) => current.queuedMessage ? current : { ...current, queuedMessage: queued });
|
|
2362
|
+
};
|
|
2363
|
+
let outcome;
|
|
2364
|
+
try {
|
|
2365
|
+
outcome = await chat.postInterjection({
|
|
2366
|
+
config: authConfig,
|
|
2367
|
+
turnId,
|
|
2368
|
+
text,
|
|
2369
|
+
messageId,
|
|
2370
|
+
imageAttachments: images,
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
catch (error) {
|
|
2374
|
+
rollback(`not sent — ${error?.message ?? 'request failed'}`);
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
if (outcome === 'stale') {
|
|
2378
|
+
rollback('turn ended — sending as the next prompt');
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2020
2381
|
const handleSubmit = async (rawInput) => {
|
|
2021
2382
|
const chunks = store.getState().pastedChunks;
|
|
2022
2383
|
const expanded = chunks.length
|
|
@@ -2036,9 +2397,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2036
2397
|
return;
|
|
2037
2398
|
}
|
|
2038
2399
|
const pending = store.getState();
|
|
2400
|
+
const body = String(rawInput ?? '');
|
|
2039
2401
|
const snapshot = {
|
|
2040
|
-
body
|
|
2041
|
-
imageAttachments: pending.imageAttachments,
|
|
2402
|
+
body,
|
|
2403
|
+
imageAttachments: pending.imageAttachments.filter((attachment) => body.includes(`[Image #${attachment.index}]`)),
|
|
2042
2404
|
pastedChunks: pending.pastedChunks,
|
|
2043
2405
|
};
|
|
2044
2406
|
scheduleLiveFrameRemount();
|
|
@@ -2053,11 +2415,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2053
2415
|
}));
|
|
2054
2416
|
return;
|
|
2055
2417
|
}
|
|
2056
|
-
const imageAttachments = store
|
|
2418
|
+
const imageAttachments = store
|
|
2419
|
+
.getState()
|
|
2420
|
+
.imageAttachments.filter((attachment) => input.includes(`[Image #${attachment.index}]`));
|
|
2057
2421
|
scheduleLiveFrameRemount();
|
|
2058
2422
|
store.update((current) => ({
|
|
2059
2423
|
...current,
|
|
2060
2424
|
cursor: 0,
|
|
2425
|
+
imageAttachments: [],
|
|
2061
2426
|
input: '',
|
|
2062
2427
|
pastedChunks: [],
|
|
2063
2428
|
promptHistory: appendPromptToHistory(current.promptHistory, input),
|
|
@@ -2073,6 +2438,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2073
2438
|
requestExit();
|
|
2074
2439
|
return;
|
|
2075
2440
|
}
|
|
2441
|
+
if (input === '/logout') {
|
|
2442
|
+
store.update((current) => ({
|
|
2443
|
+
...current,
|
|
2444
|
+
busy: true,
|
|
2445
|
+
status: 'Signing out...',
|
|
2446
|
+
}));
|
|
2447
|
+
try {
|
|
2448
|
+
await logoutFromServer({ config: authConfig });
|
|
2449
|
+
}
|
|
2450
|
+
catch {
|
|
2451
|
+
}
|
|
2452
|
+
clearCliAuthConfig(session.env);
|
|
2453
|
+
signedOut = true;
|
|
2454
|
+
requestExit();
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2076
2457
|
if (input === '/help') {
|
|
2077
2458
|
appendStaticEntry({
|
|
2078
2459
|
body: formatInteractiveHelpText(),
|
|
@@ -2089,6 +2470,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2089
2470
|
});
|
|
2090
2471
|
return;
|
|
2091
2472
|
}
|
|
2473
|
+
if (input === '/report') {
|
|
2474
|
+
const opened = await openUrl(REPORT_ISSUE_URL);
|
|
2475
|
+
appendStaticEntries(buildReportTranscriptEntries(opened));
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2092
2478
|
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2093
2479
|
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2094
2480
|
if (!jobsArgs) {
|
|
@@ -2101,7 +2487,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2101
2487
|
const killed = await killBackgroundJob(jobId);
|
|
2102
2488
|
await collectBackgroundJobUiKillMutations({
|
|
2103
2489
|
session,
|
|
2104
|
-
projectIndex,
|
|
2105
2490
|
jobId,
|
|
2106
2491
|
result: killed,
|
|
2107
2492
|
});
|
|
@@ -2143,14 +2528,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2143
2528
|
});
|
|
2144
2529
|
}
|
|
2145
2530
|
catch (error) {
|
|
2531
|
+
if (exitForAuthenticationError(error))
|
|
2532
|
+
return;
|
|
2146
2533
|
appendError(error.message);
|
|
2147
2534
|
}
|
|
2148
2535
|
finally {
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2536
|
+
if (!exiting) {
|
|
2537
|
+
store.update((current) => ({
|
|
2538
|
+
...current,
|
|
2539
|
+
busy: false,
|
|
2540
|
+
status: 'Ready',
|
|
2541
|
+
}));
|
|
2542
|
+
}
|
|
2154
2543
|
}
|
|
2155
2544
|
return;
|
|
2156
2545
|
}
|
|
@@ -2164,14 +2553,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2164
2553
|
await openResumePicker();
|
|
2165
2554
|
}
|
|
2166
2555
|
catch (error) {
|
|
2556
|
+
if (exitForAuthenticationError(error))
|
|
2557
|
+
return;
|
|
2167
2558
|
appendError(error.message);
|
|
2168
2559
|
}
|
|
2169
2560
|
finally {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2561
|
+
if (!exiting) {
|
|
2562
|
+
store.update((current) => ({
|
|
2563
|
+
...current,
|
|
2564
|
+
busy: false,
|
|
2565
|
+
status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
|
|
2566
|
+
}));
|
|
2567
|
+
}
|
|
2175
2568
|
}
|
|
2176
2569
|
return;
|
|
2177
2570
|
}
|
|
@@ -2191,43 +2584,85 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2191
2584
|
}
|
|
2192
2585
|
}
|
|
2193
2586
|
catch (error) {
|
|
2587
|
+
if (exitForAuthenticationError(error))
|
|
2588
|
+
return;
|
|
2194
2589
|
appendError(error.message);
|
|
2195
2590
|
}
|
|
2196
2591
|
finally {
|
|
2592
|
+
if (!exiting) {
|
|
2593
|
+
store.update((current) => ({
|
|
2594
|
+
...current,
|
|
2595
|
+
busy: false,
|
|
2596
|
+
status: current.modelPickerOpen ? 'Select a model' : 'Ready',
|
|
2597
|
+
}));
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (input === '/new') {
|
|
2603
|
+
newConversationInFlight = true;
|
|
2604
|
+
store.update((current) => ({
|
|
2605
|
+
...current,
|
|
2606
|
+
busy: true,
|
|
2607
|
+
busyPausedAt: null,
|
|
2608
|
+
busySince: Date.now(),
|
|
2609
|
+
imageAttachments: [],
|
|
2610
|
+
queuedMessage: null,
|
|
2611
|
+
status: 'Starting a new conversation...',
|
|
2612
|
+
}));
|
|
2613
|
+
if (!(await saveActiveSessionOrAbort())) {
|
|
2614
|
+
newConversationInFlight = false;
|
|
2197
2615
|
store.update((current) => ({
|
|
2198
2616
|
...current,
|
|
2199
2617
|
busy: false,
|
|
2200
|
-
|
|
2618
|
+
busyPausedAt: null,
|
|
2619
|
+
busySince: null,
|
|
2620
|
+
status: 'Ready',
|
|
2201
2621
|
}));
|
|
2622
|
+
await flushQueuedMessage();
|
|
2623
|
+
return;
|
|
2202
2624
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2625
|
+
startNewConversation(session);
|
|
2626
|
+
setBackgroundJobSession(session.sessionId);
|
|
2627
|
+
setScratchSession(session.sessionId);
|
|
2628
|
+
setImageStoreSession(session.sessionId);
|
|
2629
|
+
setTodoSession(session.sessionId);
|
|
2207
2630
|
clearTodos();
|
|
2631
|
+
syncBackgroundJobsState();
|
|
2208
2632
|
syncTodosState();
|
|
2209
2633
|
latestUsageSummary = null;
|
|
2210
|
-
await saveActiveSession();
|
|
2211
2634
|
store.replaceTranscript([
|
|
2212
2635
|
{
|
|
2213
|
-
body: '
|
|
2636
|
+
body: 'Started a new conversation. The previous session remains saved.',
|
|
2214
2637
|
kind: 'system',
|
|
2215
|
-
title: '
|
|
2638
|
+
title: 'Session',
|
|
2216
2639
|
},
|
|
2217
2640
|
]);
|
|
2641
|
+
await saveActiveSession();
|
|
2218
2642
|
syncShellStateFromSession();
|
|
2219
|
-
|
|
2643
|
+
newConversationInFlight = false;
|
|
2644
|
+
store.update((current) => ({
|
|
2645
|
+
...current,
|
|
2646
|
+
busy: false,
|
|
2647
|
+
busyPausedAt: null,
|
|
2648
|
+
busySince: null,
|
|
2649
|
+
status: 'Ready',
|
|
2650
|
+
}));
|
|
2220
2651
|
await remountTui();
|
|
2652
|
+
await flushQueuedMessage();
|
|
2221
2653
|
return;
|
|
2222
2654
|
}
|
|
2223
2655
|
latestUsageSummary = null;
|
|
2224
2656
|
disarmExitConfirm();
|
|
2657
|
+
unacknowledged = new Map();
|
|
2225
2658
|
const turnStartedAt = Date.now();
|
|
2226
2659
|
const turnGeneration = ++activeTurnGeneration;
|
|
2227
2660
|
const turnAbort = new AbortController();
|
|
2228
2661
|
activeTurnAbort = turnAbort;
|
|
2229
2662
|
lastTurnStartedAt = turnStartedAt;
|
|
2230
2663
|
todosTouchedThisTurn = false;
|
|
2664
|
+
clearTodos();
|
|
2665
|
+
syncTodosState();
|
|
2231
2666
|
const userEntry = {
|
|
2232
2667
|
body: input,
|
|
2233
2668
|
kind: 'user',
|
|
@@ -2236,11 +2671,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2236
2671
|
};
|
|
2237
2672
|
pendingTurnEntries = [];
|
|
2238
2673
|
queueTurnEntry(userEntry);
|
|
2674
|
+
resetThinkingPacer();
|
|
2239
2675
|
store.update((current) => ({
|
|
2240
2676
|
...current,
|
|
2241
2677
|
activeTurnInput: input,
|
|
2242
2678
|
activeTurnInputPreformatted: preformatted,
|
|
2679
|
+
analyzingImages: 0,
|
|
2680
|
+
generatingImage: false,
|
|
2243
2681
|
busy: true,
|
|
2682
|
+
busyPausedAt: null,
|
|
2244
2683
|
busySince: turnStartedAt,
|
|
2245
2684
|
clockNow: turnStartedAt,
|
|
2246
2685
|
status: 'Running turn...',
|
|
@@ -2249,22 +2688,42 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2249
2688
|
transcriptScrollOffset: 0,
|
|
2250
2689
|
tokenUsage: formatClientTokenUsage(0, latestUsageSummary),
|
|
2251
2690
|
turnCounter: current.turnCounter + 1,
|
|
2691
|
+
turnMessages: [],
|
|
2252
2692
|
workingTools: [],
|
|
2253
2693
|
}));
|
|
2254
2694
|
try {
|
|
2255
2695
|
const result = await chat.sendServerUserMessage({
|
|
2256
2696
|
config: authConfig,
|
|
2257
|
-
projectIndex,
|
|
2258
2697
|
session,
|
|
2259
2698
|
input,
|
|
2260
2699
|
imageAttachments,
|
|
2261
2700
|
signal: turnAbort.signal,
|
|
2701
|
+
onTurnStart: (serverTurnId) => {
|
|
2702
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2703
|
+
return;
|
|
2704
|
+
activeServerTurnId = serverTurnId;
|
|
2705
|
+
},
|
|
2706
|
+
onInterjectionDelivered: (event) => {
|
|
2707
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2708
|
+
return;
|
|
2709
|
+
for (const messageId of event.messageIds ?? []) {
|
|
2710
|
+
const pending = unacknowledged.get(messageId);
|
|
2711
|
+
if (!pending)
|
|
2712
|
+
continue;
|
|
2713
|
+
unacknowledged.delete(messageId);
|
|
2714
|
+
upsertTurnMessage(pending.row, {
|
|
2715
|
+
state: 'delivered',
|
|
2716
|
+
note: undefined,
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
},
|
|
2262
2720
|
});
|
|
2263
2721
|
if (turnGeneration !== activeTurnGeneration)
|
|
2264
2722
|
return;
|
|
2265
2723
|
latestUsageSummary = result.usageSummary ?? null;
|
|
2266
2724
|
const turnEntries = takePendingTurnEntries();
|
|
2267
|
-
await saveActiveSession()
|
|
2725
|
+
if (!(await saveActiveSession()))
|
|
2726
|
+
return;
|
|
2268
2727
|
syncShellStateFromSession();
|
|
2269
2728
|
store.update((current) => ({
|
|
2270
2729
|
...current,
|
|
@@ -2301,10 +2760,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2301
2760
|
store.update((current) => ({
|
|
2302
2761
|
...current,
|
|
2303
2762
|
busy: false,
|
|
2763
|
+
busyPausedAt: null,
|
|
2304
2764
|
busySince: null,
|
|
2305
2765
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2306
2766
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
2307
2767
|
}));
|
|
2768
|
+
recoverUnacknowledgedMessages();
|
|
2308
2769
|
await flushQueuedMessage();
|
|
2309
2770
|
}
|
|
2310
2771
|
catch (error) {
|
|
@@ -2315,11 +2776,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2315
2776
|
return;
|
|
2316
2777
|
}
|
|
2317
2778
|
const cancelled = isTurnCancelledError(error);
|
|
2779
|
+
if (exitForAuthenticationError(error))
|
|
2780
|
+
return;
|
|
2318
2781
|
store.update((current) => ({
|
|
2319
2782
|
...current,
|
|
2320
2783
|
activeTurnInput: '',
|
|
2321
2784
|
activeTurnInputPreformatted: false,
|
|
2322
2785
|
busy: false,
|
|
2786
|
+
busyPausedAt: null,
|
|
2323
2787
|
busySince: null,
|
|
2324
2788
|
commandLog: [],
|
|
2325
2789
|
imageAttachments: [],
|
|
@@ -2339,13 +2803,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2339
2803
|
title: 'System',
|
|
2340
2804
|
},
|
|
2341
2805
|
]);
|
|
2342
|
-
await saveActiveSession()
|
|
2806
|
+
if (!(await saveActiveSession()))
|
|
2807
|
+
return;
|
|
2343
2808
|
}
|
|
2344
2809
|
else {
|
|
2345
2810
|
appendStaticEntries(turnEntries);
|
|
2346
2811
|
appendError(error.message);
|
|
2812
|
+
appendStaticEntries(buildTurnFailureHintEntries(cancelled));
|
|
2347
2813
|
}
|
|
2348
2814
|
await remountTui();
|
|
2815
|
+
recoverUnacknowledgedMessages(!cancelled);
|
|
2349
2816
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2350
2817
|
await flushQueuedMessage();
|
|
2351
2818
|
}
|
|
@@ -2356,24 +2823,42 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2356
2823
|
}
|
|
2357
2824
|
if (turnGeneration === activeTurnGeneration) {
|
|
2358
2825
|
lastTurnStartedAt = null;
|
|
2826
|
+
activeServerTurnId = null;
|
|
2359
2827
|
}
|
|
2828
|
+
recoverUnacknowledgedMessages();
|
|
2360
2829
|
}
|
|
2361
2830
|
};
|
|
2831
|
+
session.onImageAnalysis = (activeImageCount) => {
|
|
2832
|
+
store.update((current) => ({
|
|
2833
|
+
...current,
|
|
2834
|
+
analyzingImages: Math.max(0, activeImageCount),
|
|
2835
|
+
generatingImage: activeImageCount > 0 ? false : current.generatingImage,
|
|
2836
|
+
}));
|
|
2837
|
+
};
|
|
2838
|
+
session.onImageGeneration = (active) => {
|
|
2839
|
+
store.update((current) => ({
|
|
2840
|
+
...current,
|
|
2841
|
+
generatingImage: Boolean(active),
|
|
2842
|
+
analyzingImages: active ? 0 : current.analyzingImages,
|
|
2843
|
+
}));
|
|
2844
|
+
};
|
|
2362
2845
|
session.onStatus = (message) => {
|
|
2363
2846
|
const panel = thinkingPanelFromStatus(message);
|
|
2364
2847
|
store.update((current) => ({
|
|
2365
2848
|
...current,
|
|
2366
2849
|
status: message,
|
|
2367
|
-
|
|
2368
|
-
thinkingNotes: panel?.notes ?? current.thinkingNotes,
|
|
2369
|
-
tokenUsage: formatClientTokenUsage(current.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
|
|
2850
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2370
2851
|
}));
|
|
2852
|
+
queueThinkingUpdate({
|
|
2853
|
+
title: panel ? panel.title : liveStatusPanelTitle(message),
|
|
2854
|
+
notes: panel ? panel.notes : [],
|
|
2855
|
+
});
|
|
2371
2856
|
};
|
|
2372
2857
|
session.onContextLog = (message) => {
|
|
2373
2858
|
store.update((current) => ({
|
|
2374
2859
|
...current,
|
|
2375
2860
|
contextStatus: message,
|
|
2376
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2861
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2377
2862
|
}));
|
|
2378
2863
|
};
|
|
2379
2864
|
session.onToolEvent = (event) => {
|
|
@@ -2439,54 +2924,46 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2439
2924
|
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2440
2925
|
}
|
|
2441
2926
|
});
|
|
2442
|
-
projectIndex.onStatus = session.onStatus;
|
|
2443
|
-
projectIndex.onContextLog = session.onContextLog;
|
|
2444
2927
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
2445
|
-
session.
|
|
2928
|
+
session.requestUserInput = async (request, signal) => openUserInputPrompt(request.questions, signal);
|
|
2929
|
+
session.requestPermission = async (request) => {
|
|
2930
|
+
const deny = { kind: 'deny' };
|
|
2446
2931
|
if (exiting)
|
|
2447
|
-
return
|
|
2932
|
+
return deny;
|
|
2448
2933
|
if (sessionAutoYes)
|
|
2449
|
-
return
|
|
2450
|
-
const
|
|
2451
|
-
if (
|
|
2452
|
-
setAgentMode('auto-accept');
|
|
2453
|
-
syncShellStateFromSession();
|
|
2934
|
+
return { kind: 'once' };
|
|
2935
|
+
const decision = await openApprovalPrompt(request);
|
|
2936
|
+
if (decision.kind === 'always-bucket') {
|
|
2454
2937
|
appendTurnAwareEntry({
|
|
2455
|
-
body:
|
|
2938
|
+
body: `Allowed for the rest of this session: ${bucketActionLabel(request.bucket)}.`,
|
|
2456
2939
|
kind: 'system',
|
|
2457
2940
|
title: 'Approvals',
|
|
2458
2941
|
});
|
|
2459
|
-
return true;
|
|
2460
2942
|
}
|
|
2461
|
-
|
|
2462
|
-
};
|
|
2463
|
-
session.confirmPatch = async (filePath, patch) => {
|
|
2464
|
-
if (exiting)
|
|
2465
|
-
return false;
|
|
2466
|
-
if (sessionAutoYes)
|
|
2467
|
-
return true;
|
|
2468
|
-
const choice = await openApprovalPrompt('Approve patch?', 'Review changes before applying.', {
|
|
2469
|
-
diff: patch,
|
|
2470
|
-
filePath,
|
|
2471
|
-
});
|
|
2472
|
-
if (choice === 'a') {
|
|
2473
|
-
setAgentMode('auto-accept');
|
|
2474
|
-
syncShellStateFromSession();
|
|
2943
|
+
else if (decision.kind === 'always-prefix') {
|
|
2475
2944
|
appendTurnAwareEntry({
|
|
2476
|
-
body:
|
|
2945
|
+
body: `Allowed for the rest of this session: commands starting with \`${decision.prefix}\`.`,
|
|
2477
2946
|
kind: 'system',
|
|
2478
2947
|
title: 'Approvals',
|
|
2479
2948
|
});
|
|
2480
|
-
return true;
|
|
2481
2949
|
}
|
|
2482
|
-
return
|
|
2950
|
+
return decision;
|
|
2483
2951
|
};
|
|
2484
2952
|
const shellInputHandlers = {
|
|
2953
|
+
getApprovalScrollLimit: () => {
|
|
2954
|
+
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2955
|
+
return approvalScrollLimit(store.getState().approvalPrompt, contentWidth, terminalRows);
|
|
2956
|
+
},
|
|
2485
2957
|
getTranscriptScrollLimit: () => {
|
|
2486
2958
|
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2487
2959
|
const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
2488
2960
|
return blocks.reduce((total, block, index) => total + block.length + (index > 0 ? 1 : 0), 0);
|
|
2489
2961
|
},
|
|
2962
|
+
getUserInputViewport: () => {
|
|
2963
|
+
const nowMs = Date.now();
|
|
2964
|
+
const state = store.getState();
|
|
2965
|
+
return userInputViewportForFrame(state, terminalCols, terminalRows, spinnerFrame, busyElapsedSeconds(state, nowMs), nowMs);
|
|
2966
|
+
},
|
|
2490
2967
|
onCancelTurn: cancelActiveTurn,
|
|
2491
2968
|
onCycleAgentMode: cycleAgentMode,
|
|
2492
2969
|
onCtrlC: handleCtrlC,
|
|
@@ -2495,6 +2972,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2495
2972
|
onLiveFrameShapeChange: scheduleLiveFrameRemount,
|
|
2496
2973
|
onRequestExit: requestExit,
|
|
2497
2974
|
onResolveApproval: handleInlineApprovalChoice,
|
|
2975
|
+
onResolveUserInput: handleInlineUserInput,
|
|
2498
2976
|
onResumeSession: handleInlineResumeSelection,
|
|
2499
2977
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2500
2978
|
onSelectModel: handleInlineModelSelection,
|
|
@@ -2502,6 +2980,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2502
2980
|
onJobsPickerKill: handleJobsPickerKill,
|
|
2503
2981
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2504
2982
|
onSubmit: handleSubmit,
|
|
2983
|
+
onFireQueuedMessage: fireQueuedMessage,
|
|
2505
2984
|
};
|
|
2506
2985
|
const unsubscribe = store.subscribe(() => {
|
|
2507
2986
|
renderCurrentFrame();
|
|
@@ -2509,6 +2988,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2509
2988
|
const spinnerTimer = setInterval(() => {
|
|
2510
2989
|
spinnerFrame = (spinnerFrame + 1) % 6;
|
|
2511
2990
|
if (store.getState().busy) {
|
|
2991
|
+
flushPendingThinking();
|
|
2512
2992
|
renderCurrentFrame();
|
|
2513
2993
|
}
|
|
2514
2994
|
}, 120);
|
|
@@ -2564,10 +3044,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2564
3044
|
killAllBackgroundJobs();
|
|
2565
3045
|
setBackgroundJobSession(null);
|
|
2566
3046
|
setScratchSession(null);
|
|
3047
|
+
setImageStoreSession(null);
|
|
2567
3048
|
setTodoSession(null);
|
|
2568
3049
|
setBackgroundJobUpdateHook(null);
|
|
2569
3050
|
await bridge.close();
|
|
3051
|
+
releaseTerminalWrites();
|
|
2570
3052
|
setCommandOutputHook(null);
|
|
2571
3053
|
}
|
|
2572
3054
|
});
|
|
3055
|
+
if (fatalError)
|
|
3056
|
+
throw fatalError;
|
|
3057
|
+
return { signedOut };
|
|
2573
3058
|
}
|