@thegitai/cli 1.0.0-preview.2 → 1.0.0-preview.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/dist/bin/ai.js +57 -291
- package/dist/src/agent-mode.js +1 -1
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +72 -3
- package/dist/src/api/chat.js +236 -33
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +9 -4
- 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 +1 -1
- package/dist/src/help-text.js +51 -11
- package/dist/src/permissions.js +243 -0
- package/dist/src/project-index.js +13 -1
- package/dist/src/session-store.js +119 -20
- package/dist/src/session.js +14 -3
- package/dist/src/tool-executor.js +2 -2
- package/dist/src/tools/delete-file.js +14 -0
- package/dist/src/tools/index.js +2 -0
- package/dist/src/tools/patch-file.js +12 -16
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/run-command.js +13 -27
- package/dist/src/tools/run-node-script.js +11 -26
- package/dist/src/tools/str-replace.js +12 -16
- package/dist/src/tools/write-file.js +66 -0
- 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 +579 -154
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +535 -159
- 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 +18 -6
- package/dist/src/markdown-renderer.js +0 -112
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 } 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,6 @@ 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 APPROVAL_OPTIONS = [
|
|
73
|
-
{
|
|
74
|
-
value: 'y',
|
|
75
|
-
label: 'Approve once',
|
|
76
|
-
description: 'Run this action only this time',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
value: 'a',
|
|
80
|
-
label: 'Approve all remaining actions',
|
|
81
|
-
description: 'Turn on auto-approve for the rest of the session',
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
value: 'n',
|
|
85
|
-
label: 'Deny',
|
|
86
|
-
description: 'Reject this action',
|
|
87
|
-
},
|
|
88
|
-
];
|
|
89
79
|
export const SLASH_COMMANDS = [
|
|
90
80
|
{
|
|
91
81
|
command: '/help',
|
|
@@ -112,8 +102,8 @@ export const SLASH_COMMANDS = [
|
|
|
112
102
|
description: 'Background jobs: pick to view output or kill',
|
|
113
103
|
},
|
|
114
104
|
{
|
|
115
|
-
command: '/
|
|
116
|
-
description: '
|
|
105
|
+
command: '/new',
|
|
106
|
+
description: 'start a new conversation; this session remains saved',
|
|
117
107
|
},
|
|
118
108
|
{
|
|
119
109
|
command: '/exit',
|
|
@@ -780,13 +770,21 @@ function displayUserTextFromHistoryEntry(entry) {
|
|
|
780
770
|
.slice(contentStart, contentEnd === -1 ? text.length : contentEnd)
|
|
781
771
|
.trim();
|
|
782
772
|
}
|
|
783
|
-
function buildTranscriptFromSessionHistory(history) {
|
|
773
|
+
export function buildTranscriptFromSessionHistory(history) {
|
|
784
774
|
const entries = [];
|
|
785
775
|
const pendingCalls = new Map();
|
|
776
|
+
const pendingUserInputRequests = new Map();
|
|
786
777
|
for (const entry of history) {
|
|
787
778
|
if (!entry?.parts?.length)
|
|
788
779
|
continue;
|
|
789
780
|
for (const part of entry.parts) {
|
|
781
|
+
const userInputRequest = part?.userInputRequest;
|
|
782
|
+
if (userInputRequest && typeof userInputRequest === 'object') {
|
|
783
|
+
const requestId = String(userInputRequest.requestId ?? '').trim();
|
|
784
|
+
if (requestId && Array.isArray(userInputRequest.questions)) {
|
|
785
|
+
pendingUserInputRequests.set(requestId, userInputRequest.questions);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
790
788
|
const call = part?.functionCall;
|
|
791
789
|
if (!call || typeof call !== 'object')
|
|
792
790
|
continue;
|
|
@@ -796,6 +794,21 @@ function buildTranscriptFromSessionHistory(history) {
|
|
|
796
794
|
pendingCalls.set(callId, call);
|
|
797
795
|
}
|
|
798
796
|
for (const part of entry.parts) {
|
|
797
|
+
const userInputResult = part?.userInputResult;
|
|
798
|
+
if (userInputResult && typeof userInputResult === 'object') {
|
|
799
|
+
const requestId = String(userInputResult.requestId ?? '').trim();
|
|
800
|
+
const questions = pendingUserInputRequests.get(requestId);
|
|
801
|
+
const result = userInputResult.result;
|
|
802
|
+
if (questions &&
|
|
803
|
+
(result?.status === 'submitted' || result?.status === 'cancelled')) {
|
|
804
|
+
pendingUserInputRequests.delete(requestId);
|
|
805
|
+
entries.push({
|
|
806
|
+
body: formatUserInputTranscript(questions, result),
|
|
807
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
808
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
799
812
|
const functionResponse = part?.functionResponse;
|
|
800
813
|
if (!functionResponse || typeof functionResponse !== 'object')
|
|
801
814
|
continue;
|
|
@@ -824,8 +837,23 @@ function buildTranscriptFromSessionHistory(history) {
|
|
|
824
837
|
}
|
|
825
838
|
continue;
|
|
826
839
|
}
|
|
840
|
+
if (entry.role === 'user' && entry.kind === 'userInterjection') {
|
|
841
|
+
const text = String(entry.userInput ?? '').trim();
|
|
842
|
+
if (text) {
|
|
843
|
+
entries.push({ body: text, kind: 'user', title: 'You · sent mid-turn' });
|
|
844
|
+
}
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
827
847
|
const text = textFromHistoryEntry(entry);
|
|
828
848
|
if ((entry.role === 'model' || entry.role === 'assistant') && text) {
|
|
849
|
+
if (isTurnFailureMarker(text)) {
|
|
850
|
+
entries.push({
|
|
851
|
+
body: 'This request did not complete.',
|
|
852
|
+
kind: 'system',
|
|
853
|
+
title: 'Previous turn',
|
|
854
|
+
});
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
829
857
|
entries.push({ body: text, kind: 'assistant', title: 'Response' });
|
|
830
858
|
}
|
|
831
859
|
}
|
|
@@ -951,11 +979,14 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
951
979
|
activeTurnInput: '',
|
|
952
980
|
activeTurnInputPreformatted: false,
|
|
953
981
|
agentMode: session.agentMode,
|
|
954
|
-
|
|
982
|
+
analyzingImages: 0,
|
|
983
|
+
approvalCursor: 0,
|
|
955
984
|
approvalPrompt: null,
|
|
985
|
+
approvalScrollOffset: 0,
|
|
956
986
|
autoYes: session.autoYes,
|
|
957
987
|
backgroundJobs: [],
|
|
958
988
|
busy: false,
|
|
989
|
+
busyPausedAt: null,
|
|
959
990
|
busySince: null,
|
|
960
991
|
clockNow: Date.now(),
|
|
961
992
|
commandCursor: 0,
|
|
@@ -983,6 +1014,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
983
1014
|
resumePickerSessions: [],
|
|
984
1015
|
transcriptScrollOffset: 0,
|
|
985
1016
|
queuedMessage: null,
|
|
1017
|
+
turnMessages: [],
|
|
986
1018
|
serverModels: serverModels.models,
|
|
987
1019
|
sudoPrompt: null,
|
|
988
1020
|
status: 'Ready',
|
|
@@ -993,6 +1025,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
993
1025
|
tokenUsage: formatClientTokenUsage(null),
|
|
994
1026
|
transcript: [],
|
|
995
1027
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
1028
|
+
userInputPrompt: null,
|
|
996
1029
|
pastedChunks: [],
|
|
997
1030
|
imageAttachments: [],
|
|
998
1031
|
workingTools: [],
|
|
@@ -1060,6 +1093,24 @@ function splitThinkingLines(text) {
|
|
|
1060
1093
|
.map((part) => part.trim())
|
|
1061
1094
|
.filter(Boolean));
|
|
1062
1095
|
}
|
|
1096
|
+
const LIVE_STATUS_PANEL_MAX_CHARS = 72;
|
|
1097
|
+
const TOOL_PROGRESS_STATUS_PATTERN = /^(?:Tool:\s|Running\s\S+(?:\slocally)?\.\.\.$)/i;
|
|
1098
|
+
export function isToolProgressStatus(status) {
|
|
1099
|
+
return TOOL_PROGRESS_STATUS_PATTERN.test(String(status ?? '').trim());
|
|
1100
|
+
}
|
|
1101
|
+
function holdOrPickFallback(currentTitle) {
|
|
1102
|
+
return THINKING_FALLBACK_PHRASES.includes(currentTitle.trim())
|
|
1103
|
+
? currentTitle
|
|
1104
|
+
: pickThinkingFallbackPhrase();
|
|
1105
|
+
}
|
|
1106
|
+
function liveStatusPanelTitle(status) {
|
|
1107
|
+
const text = String(status ?? '').trim();
|
|
1108
|
+
if (!text || text.includes('\n'))
|
|
1109
|
+
return '';
|
|
1110
|
+
if (isToolProgressStatus(text))
|
|
1111
|
+
return '';
|
|
1112
|
+
return text.length <= LIVE_STATUS_PANEL_MAX_CHARS ? text : '';
|
|
1113
|
+
}
|
|
1063
1114
|
function thinkingPanelFromStatus(status) {
|
|
1064
1115
|
const rawText = thinkingNoteFromStatus(status);
|
|
1065
1116
|
if (!rawText)
|
|
@@ -1083,7 +1134,7 @@ function thinkingPanelFromStatus(status) {
|
|
|
1083
1134
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
1084
1135
|
return {
|
|
1085
1136
|
title,
|
|
1086
|
-
notes: splitThinkingLines(bodyLines.join('\n')).slice(
|
|
1137
|
+
notes: splitThinkingLines(bodyLines.join('\n')).slice(0, 3),
|
|
1087
1138
|
};
|
|
1088
1139
|
}
|
|
1089
1140
|
export const EXIT_CTRL_C_CONFIRM_MESSAGE = 'Press Ctrl+C again to quit.';
|
|
@@ -1213,32 +1264,47 @@ export function navigatePromptHistory(state, direction) {
|
|
|
1213
1264
|
promptHistoryCursor: nextCursor,
|
|
1214
1265
|
};
|
|
1215
1266
|
}
|
|
1216
|
-
function getDefaultApprovalCursor() {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1267
|
+
export function getDefaultApprovalCursor(optionCount) {
|
|
1268
|
+
return Math.max(0, optionCount - 1);
|
|
1269
|
+
}
|
|
1270
|
+
export function getNextApprovalCursor(currentIndex, direction, optionCount) {
|
|
1271
|
+
if (optionCount <= 0)
|
|
1272
|
+
return 0;
|
|
1273
|
+
return (currentIndex + direction + optionCount) % optionCount;
|
|
1274
|
+
}
|
|
1275
|
+
export function pauseBusyClock(state, nowMs) {
|
|
1276
|
+
if (state.busySince === null || state.busyPausedAt !== null)
|
|
1277
|
+
return state;
|
|
1278
|
+
return { ...state, busyPausedAt: nowMs };
|
|
1279
|
+
}
|
|
1280
|
+
export function resumeBusyClock(state, nowMs) {
|
|
1281
|
+
if (state.busyPausedAt === null)
|
|
1282
|
+
return state;
|
|
1283
|
+
return {
|
|
1284
|
+
...state,
|
|
1285
|
+
busyPausedAt: null,
|
|
1286
|
+
busySince: state.busySince === null
|
|
1287
|
+
? null
|
|
1288
|
+
: state.busySince + Math.max(0, nowMs - state.busyPausedAt),
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
export function busyElapsedMs(state, nowMs) {
|
|
1292
|
+
if (state.busySince === null)
|
|
1293
|
+
return null;
|
|
1294
|
+
return Math.max(0, (state.busyPausedAt ?? nowMs) - state.busySince);
|
|
1295
|
+
}
|
|
1296
|
+
export function busyElapsedSeconds(state, nowMs) {
|
|
1297
|
+
return Math.floor((busyElapsedMs(state, nowMs) ?? 0) / 1000);
|
|
1236
1298
|
}
|
|
1237
1299
|
export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
1238
1300
|
return serverModels.map((model) => ({
|
|
1239
1301
|
id: model.id,
|
|
1240
1302
|
label: model.label,
|
|
1241
|
-
|
|
1303
|
+
publicId: model.id,
|
|
1304
|
+
costRating: model.costRating,
|
|
1305
|
+
current: model.id === currentModelId,
|
|
1306
|
+
disabled: false,
|
|
1307
|
+
note: model.description,
|
|
1242
1308
|
}));
|
|
1243
1309
|
}
|
|
1244
1310
|
function getDefaultModelPickerIndex(currentModelId, serverModels) {
|
|
@@ -1266,35 +1332,36 @@ function appendCommandLog(state, text) {
|
|
|
1266
1332
|
};
|
|
1267
1333
|
}
|
|
1268
1334
|
async function saveSessionBoth({ serverSessionClient, session, }) {
|
|
1335
|
+
if (!sessionHasUserMessage(session))
|
|
1336
|
+
return;
|
|
1269
1337
|
saveSessionState(session);
|
|
1270
1338
|
await serverSessionClient.save(session);
|
|
1271
1339
|
}
|
|
1272
|
-
function shouldUseRatatuiShell() {
|
|
1273
|
-
if (process.env.THEGITAI_PLAIN === '1')
|
|
1274
|
-
return false;
|
|
1275
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1276
|
-
}
|
|
1277
|
-
export function shouldUseClientRatatuiShell() {
|
|
1278
|
-
return shouldUseRatatuiShell();
|
|
1279
|
-
}
|
|
1280
1340
|
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
1281
|
-
if (
|
|
1282
|
-
throw new Error('
|
|
1341
|
+
if (process.stdin.isTTY !== true) {
|
|
1342
|
+
throw new Error('stdin is not a terminal');
|
|
1343
|
+
}
|
|
1344
|
+
if (process.stdout.isTTY !== true) {
|
|
1345
|
+
throw new Error('stdout is not a terminal');
|
|
1283
1346
|
}
|
|
1347
|
+
let fatalError = null;
|
|
1284
1348
|
await withTuiMode(async () => {
|
|
1285
1349
|
setBackgroundJobSession(session.sessionId);
|
|
1286
1350
|
setScratchSession(session.sessionId);
|
|
1351
|
+
setImageStoreSession(session.sessionId);
|
|
1287
1352
|
setTodoSession(session.sessionId);
|
|
1288
1353
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1289
1354
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1290
1355
|
let currentServerModels = serverModels;
|
|
1291
1356
|
let sessionAutoYes = session.autoYes;
|
|
1292
1357
|
let resolveDone = null;
|
|
1293
|
-
let
|
|
1358
|
+
let resolvePermissionDecision = null;
|
|
1294
1359
|
let resolveSudoPassword = null;
|
|
1360
|
+
let pendingUserInput = null;
|
|
1295
1361
|
let cleanupSudoPasswordPrompt = null;
|
|
1296
1362
|
let sudoPasswordBuffer = '';
|
|
1297
1363
|
const bridge = createRatatuiBridge();
|
|
1364
|
+
captureTerminalWrites();
|
|
1298
1365
|
const { handleShellKeyEvent } = await import('./tui/shell-input.js');
|
|
1299
1366
|
let terminalCols = 80;
|
|
1300
1367
|
let terminalRows = 24;
|
|
@@ -1307,6 +1374,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1307
1374
|
let latestUsageSummary = null;
|
|
1308
1375
|
let pendingTurnEntries = [];
|
|
1309
1376
|
let activeTurnAbort = null;
|
|
1377
|
+
let activeServerTurnId = null;
|
|
1378
|
+
let unacknowledged = new Map();
|
|
1379
|
+
const blockedRows = new WeakMap();
|
|
1380
|
+
let newConversationInFlight = false;
|
|
1310
1381
|
let todosTouchedThisTurn = false;
|
|
1311
1382
|
const syncTodosState = () => {
|
|
1312
1383
|
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
@@ -1315,6 +1386,36 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1315
1386
|
let exitCtrlCArmed = false;
|
|
1316
1387
|
let exitCtrlCTimer = null;
|
|
1317
1388
|
let transientStatusTimer = null;
|
|
1389
|
+
const THINKING_MIN_DWELL_MS = 3_000;
|
|
1390
|
+
let thinkingAppliedAt = 0;
|
|
1391
|
+
let pendingThinking = null;
|
|
1392
|
+
const applyThinking = (next) => {
|
|
1393
|
+
thinkingAppliedAt = Date.now();
|
|
1394
|
+
pendingThinking = null;
|
|
1395
|
+
store.update((current) => ({
|
|
1396
|
+
...current,
|
|
1397
|
+
thinkingTitle: next.title || holdOrPickFallback(current.thinkingTitle),
|
|
1398
|
+
thinkingNotes: next.notes,
|
|
1399
|
+
}));
|
|
1400
|
+
};
|
|
1401
|
+
const queueThinkingUpdate = (next) => {
|
|
1402
|
+
if (Date.now() - thinkingAppliedAt >= THINKING_MIN_DWELL_MS) {
|
|
1403
|
+
applyThinking(next);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
pendingThinking = next;
|
|
1407
|
+
};
|
|
1408
|
+
const flushPendingThinking = () => {
|
|
1409
|
+
if (!pendingThinking)
|
|
1410
|
+
return;
|
|
1411
|
+
if (Date.now() - thinkingAppliedAt < THINKING_MIN_DWELL_MS)
|
|
1412
|
+
return;
|
|
1413
|
+
applyThinking(pendingThinking);
|
|
1414
|
+
};
|
|
1415
|
+
const resetThinkingPacer = () => {
|
|
1416
|
+
thinkingAppliedAt = 0;
|
|
1417
|
+
pendingThinking = null;
|
|
1418
|
+
};
|
|
1318
1419
|
const done = new Promise((resolve) => {
|
|
1319
1420
|
resolveDone = resolve;
|
|
1320
1421
|
});
|
|
@@ -1339,7 +1440,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1339
1440
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1340
1441
|
}, 2500);
|
|
1341
1442
|
};
|
|
1342
|
-
const terminalTitle = createTerminalTitleController(
|
|
1443
|
+
const terminalTitle = createTerminalTitleController({
|
|
1444
|
+
write: (title) => bridge.setTitle(title),
|
|
1445
|
+
});
|
|
1343
1446
|
const syncTerminalTitle = () => {
|
|
1344
1447
|
const state = store.getState();
|
|
1345
1448
|
terminalTitle.sync({
|
|
@@ -1352,9 +1455,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1352
1455
|
return;
|
|
1353
1456
|
const state = store.getState();
|
|
1354
1457
|
syncTerminalTitle();
|
|
1355
|
-
const elapsedSeconds = state.
|
|
1356
|
-
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1357
|
-
: 0;
|
|
1458
|
+
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1358
1459
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1359
1460
|
};
|
|
1360
1461
|
const remountTui = async () => {
|
|
@@ -1480,15 +1581,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1480
1581
|
}
|
|
1481
1582
|
};
|
|
1482
1583
|
const dismissPendingApproval = () => {
|
|
1483
|
-
if (!
|
|
1584
|
+
if (!resolvePermissionDecision)
|
|
1484
1585
|
return;
|
|
1485
|
-
const pendingResolve =
|
|
1486
|
-
|
|
1487
|
-
pendingResolve('
|
|
1586
|
+
const pendingResolve = resolvePermissionDecision;
|
|
1587
|
+
resolvePermissionDecision = null;
|
|
1588
|
+
pendingResolve({ kind: 'deny' });
|
|
1488
1589
|
store.update((current) => ({
|
|
1489
|
-
...current,
|
|
1490
|
-
approvalCursor:
|
|
1590
|
+
...resumeBusyClock(current, Date.now()),
|
|
1591
|
+
approvalCursor: 0,
|
|
1491
1592
|
approvalPrompt: null,
|
|
1593
|
+
approvalScrollOffset: 0,
|
|
1492
1594
|
}));
|
|
1493
1595
|
};
|
|
1494
1596
|
const dismissPendingSudoPassword = () => {
|
|
@@ -1501,17 +1603,34 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1501
1603
|
sudoPasswordBuffer = '';
|
|
1502
1604
|
pendingResolve(null);
|
|
1503
1605
|
store.update((current) => ({
|
|
1504
|
-
...current,
|
|
1606
|
+
...resumeBusyClock(current, Date.now()),
|
|
1505
1607
|
sudoPrompt: null,
|
|
1506
1608
|
}));
|
|
1507
1609
|
};
|
|
1610
|
+
const dismissPendingUserInput = () => {
|
|
1611
|
+
const pending = pendingUserInput;
|
|
1612
|
+
if (!pending)
|
|
1613
|
+
return;
|
|
1614
|
+
pendingUserInput = null;
|
|
1615
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1616
|
+
const error = new Error('Turn cancelled.');
|
|
1617
|
+
error.name = 'AbortError';
|
|
1618
|
+
pending.reject(error);
|
|
1619
|
+
store.update((current) => ({
|
|
1620
|
+
...resumeBusyClock(current, Date.now()),
|
|
1621
|
+
status: current.userInputPrompt?.returnStatus ?? current.status,
|
|
1622
|
+
userInputPrompt: null,
|
|
1623
|
+
}));
|
|
1624
|
+
};
|
|
1508
1625
|
const cancelActiveTurn = () => {
|
|
1509
|
-
if (!store.getState().busy)
|
|
1626
|
+
if (!store.getState().busy || newConversationInFlight)
|
|
1510
1627
|
return;
|
|
1511
1628
|
disarmExitConfirm();
|
|
1512
1629
|
dismissPendingApproval();
|
|
1513
1630
|
dismissPendingSudoPassword();
|
|
1631
|
+
dismissPendingUserInput();
|
|
1514
1632
|
activeTurnGeneration += 1;
|
|
1633
|
+
resetThinkingPacer();
|
|
1515
1634
|
activeTurnAbort?.abort();
|
|
1516
1635
|
activeTurnAbort = null;
|
|
1517
1636
|
cancelActiveCommand();
|
|
@@ -1529,6 +1648,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1529
1648
|
activeTurnInput: '',
|
|
1530
1649
|
activeTurnInputPreformatted: false,
|
|
1531
1650
|
busy: false,
|
|
1651
|
+
busyPausedAt: null,
|
|
1532
1652
|
busySince: null,
|
|
1533
1653
|
commandLog: [],
|
|
1534
1654
|
cursor: queued ? queued.body.length : current.cursor,
|
|
@@ -1541,8 +1661,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1541
1661
|
status: 'Ready',
|
|
1542
1662
|
thinkingTitle: '',
|
|
1543
1663
|
thinkingNotes: [],
|
|
1664
|
+
turnMessages: [],
|
|
1544
1665
|
workingTools: [],
|
|
1545
|
-
tokenUsage: formatClientTokenUsage(current
|
|
1666
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1546
1667
|
}));
|
|
1547
1668
|
appendStaticEntries(cancelledEntries);
|
|
1548
1669
|
lastTurnStartedAt = null;
|
|
@@ -1550,14 +1671,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1550
1671
|
scheduleLiveFrameRemount();
|
|
1551
1672
|
void remountTui();
|
|
1552
1673
|
};
|
|
1553
|
-
const saveActiveSession = async () => {
|
|
1554
|
-
try {
|
|
1555
|
-
await saveSessionBoth({ serverSessionClient, session });
|
|
1556
|
-
}
|
|
1557
|
-
catch (error) {
|
|
1558
|
-
appendError(`Session save failed: ${error.message}`);
|
|
1559
|
-
}
|
|
1560
|
-
};
|
|
1561
1674
|
const syncShellStateFromSession = () => {
|
|
1562
1675
|
store.update((current) => ({
|
|
1563
1676
|
...current,
|
|
@@ -1636,6 +1749,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1636
1749
|
}
|
|
1637
1750
|
dismissPendingApproval();
|
|
1638
1751
|
dismissPendingSudoPassword();
|
|
1752
|
+
dismissPendingUserInput();
|
|
1639
1753
|
exiting = true;
|
|
1640
1754
|
killAllBackgroundJobs();
|
|
1641
1755
|
store.update((current) => ({
|
|
@@ -1645,9 +1759,45 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1645
1759
|
status: 'Exiting...',
|
|
1646
1760
|
}));
|
|
1647
1761
|
void bridge.close().then(() => {
|
|
1762
|
+
releaseTerminalWrites();
|
|
1648
1763
|
resolveDone?.();
|
|
1649
1764
|
});
|
|
1650
1765
|
};
|
|
1766
|
+
const exitForAuthenticationError = (error) => {
|
|
1767
|
+
if (!isAuthenticationError(error))
|
|
1768
|
+
return false;
|
|
1769
|
+
clearCliAuthConfig(session.env);
|
|
1770
|
+
if (sessionHasUserMessage(session)) {
|
|
1771
|
+
saveSessionState(session);
|
|
1772
|
+
}
|
|
1773
|
+
fatalError = new Error(authenticationErrorMessage(error));
|
|
1774
|
+
requestExit();
|
|
1775
|
+
return true;
|
|
1776
|
+
};
|
|
1777
|
+
const saveActiveSession = async () => {
|
|
1778
|
+
try {
|
|
1779
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
catch (error) {
|
|
1783
|
+
if (exitForAuthenticationError(error))
|
|
1784
|
+
return false;
|
|
1785
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
const saveActiveSessionOrAbort = async () => {
|
|
1790
|
+
try {
|
|
1791
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1792
|
+
return true;
|
|
1793
|
+
}
|
|
1794
|
+
catch (error) {
|
|
1795
|
+
if (exitForAuthenticationError(error))
|
|
1796
|
+
return false;
|
|
1797
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1651
1801
|
const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
|
|
1652
1802
|
if (exiting || signal?.aborted) {
|
|
1653
1803
|
resolve(null);
|
|
@@ -1665,7 +1815,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1665
1815
|
cleanupSudoPasswordPrompt = null;
|
|
1666
1816
|
}
|
|
1667
1817
|
store.update((current) => ({
|
|
1668
|
-
...current,
|
|
1818
|
+
...pauseBusyClock(current, Date.now()),
|
|
1669
1819
|
sudoPrompt: {
|
|
1670
1820
|
command,
|
|
1671
1821
|
passwordLength: 0,
|
|
@@ -1685,7 +1835,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1685
1835
|
cleanupSudoPasswordPrompt = null;
|
|
1686
1836
|
sudoPasswordBuffer = '';
|
|
1687
1837
|
store.update((current) => ({
|
|
1688
|
-
...current,
|
|
1838
|
+
...resumeBusyClock(current, Date.now()),
|
|
1689
1839
|
sudoPrompt: null,
|
|
1690
1840
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1691
1841
|
}));
|
|
@@ -1699,7 +1849,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1699
1849
|
cleanupSudoPasswordPrompt = null;
|
|
1700
1850
|
sudoPasswordBuffer = '';
|
|
1701
1851
|
store.update((current) => ({
|
|
1702
|
-
...current,
|
|
1852
|
+
...resumeBusyClock(current, Date.now()),
|
|
1703
1853
|
sudoPrompt: null,
|
|
1704
1854
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1705
1855
|
}));
|
|
@@ -1719,38 +1869,88 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1719
1869
|
: null,
|
|
1720
1870
|
}));
|
|
1721
1871
|
};
|
|
1722
|
-
const
|
|
1872
|
+
const openUserInputPrompt = (questions, signal) => new Promise((resolve, reject) => {
|
|
1873
|
+
if (exiting || signal?.aborted) {
|
|
1874
|
+
const error = new Error('Turn cancelled.');
|
|
1875
|
+
error.name = 'AbortError';
|
|
1876
|
+
reject(error);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const onAbort = () => dismissPendingUserInput();
|
|
1880
|
+
pendingUserInput = {
|
|
1881
|
+
questions,
|
|
1882
|
+
resolve,
|
|
1883
|
+
reject,
|
|
1884
|
+
signal,
|
|
1885
|
+
onAbort,
|
|
1886
|
+
};
|
|
1887
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1888
|
+
store.update((current) => ({
|
|
1889
|
+
...pauseBusyClock(current, Date.now()),
|
|
1890
|
+
status: 'Waiting for your input',
|
|
1891
|
+
userInputPrompt: createUserInputPromptState(questions, current.status),
|
|
1892
|
+
}));
|
|
1893
|
+
scheduleLiveFrameRemount();
|
|
1894
|
+
});
|
|
1895
|
+
const handleInlineUserInput = async (result) => {
|
|
1896
|
+
const pending = pendingUserInput;
|
|
1897
|
+
const prompt = store.getState().userInputPrompt;
|
|
1898
|
+
if (!pending || !prompt)
|
|
1899
|
+
return;
|
|
1900
|
+
pendingUserInput = null;
|
|
1901
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1902
|
+
store.update((current) => ({
|
|
1903
|
+
...resumeBusyClock(current, Date.now()),
|
|
1904
|
+
status: prompt.returnStatus,
|
|
1905
|
+
userInputPrompt: null,
|
|
1906
|
+
}));
|
|
1907
|
+
appendTurnAwareEntry({
|
|
1908
|
+
body: formatUserInputTranscript(pending.questions, result),
|
|
1909
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
1910
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
1911
|
+
});
|
|
1912
|
+
pending.resolve(result);
|
|
1913
|
+
scheduleLiveFrameRemount();
|
|
1914
|
+
};
|
|
1915
|
+
const openApprovalPrompt = (request) => new Promise((resolve) => {
|
|
1916
|
+
const deny = { kind: 'deny' };
|
|
1723
1917
|
if (exiting) {
|
|
1724
|
-
resolve(
|
|
1918
|
+
resolve(deny);
|
|
1725
1919
|
return;
|
|
1726
1920
|
}
|
|
1727
|
-
|
|
1921
|
+
resolvePermissionDecision = resolve;
|
|
1728
1922
|
store.update((current) => ({
|
|
1729
|
-
...current,
|
|
1730
|
-
approvalCursor: getDefaultApprovalCursor(),
|
|
1923
|
+
...pauseBusyClock(current, Date.now()),
|
|
1924
|
+
approvalCursor: getDefaultApprovalCursor(request.options.length),
|
|
1925
|
+
approvalOpenedAt: Date.now(),
|
|
1926
|
+
approvalScrollOffset: 0,
|
|
1731
1927
|
approvalPrompt: {
|
|
1732
|
-
title,
|
|
1733
|
-
body,
|
|
1734
|
-
diffPreview:
|
|
1735
|
-
? parseDiffPreview(
|
|
1928
|
+
title: request.title,
|
|
1929
|
+
body: request.body,
|
|
1930
|
+
diffPreview: request.diff && request.filePath
|
|
1931
|
+
? parseDiffPreview(request.diff)
|
|
1736
1932
|
: undefined,
|
|
1737
|
-
filePath:
|
|
1933
|
+
filePath: request.filePath,
|
|
1934
|
+
options: request.options,
|
|
1738
1935
|
returnStatus: current.status,
|
|
1739
1936
|
},
|
|
1740
|
-
status: title,
|
|
1937
|
+
status: request.title,
|
|
1741
1938
|
}));
|
|
1742
1939
|
});
|
|
1743
|
-
const handleInlineApprovalChoice = async (
|
|
1940
|
+
const handleInlineApprovalChoice = async (index) => {
|
|
1744
1941
|
const current = store.getState();
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1942
|
+
const options = current.approvalPrompt?.options ?? [];
|
|
1943
|
+
const chosen = options[index]?.decision ?? { kind: 'deny' };
|
|
1944
|
+
const pendingResolve = resolvePermissionDecision;
|
|
1945
|
+
resolvePermissionDecision = null;
|
|
1747
1946
|
store.update((next) => ({
|
|
1748
|
-
...next,
|
|
1749
|
-
approvalCursor: getDefaultApprovalCursor(),
|
|
1947
|
+
...resumeBusyClock(next, Date.now()),
|
|
1948
|
+
approvalCursor: getDefaultApprovalCursor(options.length),
|
|
1750
1949
|
approvalPrompt: null,
|
|
1950
|
+
approvalScrollOffset: 0,
|
|
1751
1951
|
status: current.approvalPrompt?.returnStatus ?? next.status,
|
|
1752
1952
|
}));
|
|
1753
|
-
pendingResolve?.(
|
|
1953
|
+
pendingResolve?.(chosen);
|
|
1754
1954
|
};
|
|
1755
1955
|
const refreshServerModels = async () => {
|
|
1756
1956
|
currentServerModels = await models.fetchServerModels({ config: authConfig });
|
|
@@ -1784,7 +1984,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1784
1984
|
selectedModelId: selected,
|
|
1785
1985
|
serverModels: fresh,
|
|
1786
1986
|
});
|
|
1787
|
-
await saveActiveSession()
|
|
1987
|
+
if (!(await saveActiveSession()))
|
|
1988
|
+
return;
|
|
1788
1989
|
syncShellStateFromSession();
|
|
1789
1990
|
appendStaticEntry({
|
|
1790
1991
|
body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
|
|
@@ -1903,6 +2104,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1903
2104
|
await switchToModel(selected.id);
|
|
1904
2105
|
}
|
|
1905
2106
|
catch (error) {
|
|
2107
|
+
if (exitForAuthenticationError(error))
|
|
2108
|
+
return;
|
|
1906
2109
|
appendError(error.message);
|
|
1907
2110
|
}
|
|
1908
2111
|
};
|
|
@@ -1952,6 +2155,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1952
2155
|
store.update((next) => ({
|
|
1953
2156
|
...next,
|
|
1954
2157
|
busy: true,
|
|
2158
|
+
busyPausedAt: null,
|
|
1955
2159
|
busySince: resumeStartedAt,
|
|
1956
2160
|
clockNow: resumeStartedAt,
|
|
1957
2161
|
status: 'Loading session...',
|
|
@@ -1961,14 +2165,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1961
2165
|
applySessionSnapshot(session, snapshot);
|
|
1962
2166
|
setBackgroundJobSession(session.sessionId);
|
|
1963
2167
|
setScratchSession(session.sessionId);
|
|
2168
|
+
setImageStoreSession(session.sessionId);
|
|
1964
2169
|
syncBackgroundJobsState();
|
|
1965
2170
|
setTodoSession(session.sessionId);
|
|
1966
2171
|
syncTodosState();
|
|
1967
|
-
await saveActiveSession();
|
|
1968
2172
|
syncShellStateFromSession();
|
|
1969
2173
|
store.update((next) => ({
|
|
1970
2174
|
...next,
|
|
1971
2175
|
busy: false,
|
|
2176
|
+
busyPausedAt: null,
|
|
1972
2177
|
busySince: null,
|
|
1973
2178
|
resumePickerFilter: '',
|
|
1974
2179
|
resumePickerIndex: 0,
|
|
@@ -1987,9 +2192,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1987
2192
|
await remountTui();
|
|
1988
2193
|
}
|
|
1989
2194
|
catch (error) {
|
|
2195
|
+
if (exitForAuthenticationError(error))
|
|
2196
|
+
return;
|
|
1990
2197
|
store.update((next) => ({
|
|
1991
2198
|
...next,
|
|
1992
2199
|
busy: false,
|
|
2200
|
+
busyPausedAt: null,
|
|
1993
2201
|
busySince: null,
|
|
1994
2202
|
resumePickerFilter: '',
|
|
1995
2203
|
resumePickerIndex: 0,
|
|
@@ -2013,6 +2221,127 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2013
2221
|
}));
|
|
2014
2222
|
await handleSubmit(queued.body);
|
|
2015
2223
|
};
|
|
2224
|
+
let turnMessageCounter = 0;
|
|
2225
|
+
const upsertTurnMessage = (id, patch) => {
|
|
2226
|
+
store.update((current) => ({
|
|
2227
|
+
...current,
|
|
2228
|
+
turnMessages: current.turnMessages.map((message) => message.id === id ? { ...message, ...patch } : message),
|
|
2229
|
+
}));
|
|
2230
|
+
};
|
|
2231
|
+
const recoverUnacknowledgedMessages = (autoSubmits = true) => {
|
|
2232
|
+
if (unacknowledged.size === 0)
|
|
2233
|
+
return;
|
|
2234
|
+
const stranded = [...unacknowledged.values()];
|
|
2235
|
+
unacknowledged = new Map();
|
|
2236
|
+
for (const { row } of stranded) {
|
|
2237
|
+
upsertTurnMessage(row, {
|
|
2238
|
+
state: 'queued',
|
|
2239
|
+
note: autoSubmits
|
|
2240
|
+
? 'not read — sending as the next prompt'
|
|
2241
|
+
: 'not read — left in the composer',
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
const existing = store.getState().queuedMessage;
|
|
2245
|
+
const combined = [
|
|
2246
|
+
...stranded.map((entry) => entry.queued),
|
|
2247
|
+
...(existing ? [existing] : []),
|
|
2248
|
+
];
|
|
2249
|
+
const body = combined
|
|
2250
|
+
.map((entry) => entry.body)
|
|
2251
|
+
.filter((entry) => entry.trim())
|
|
2252
|
+
.join('\n\n');
|
|
2253
|
+
if (!body.trim())
|
|
2254
|
+
return;
|
|
2255
|
+
scheduleLiveFrameRemount();
|
|
2256
|
+
store.update((current) => ({
|
|
2257
|
+
...current,
|
|
2258
|
+
queuedMessage: {
|
|
2259
|
+
body,
|
|
2260
|
+
imageAttachments: combined.flatMap((entry) => entry.imageAttachments),
|
|
2261
|
+
pastedChunks: combined.flatMap((entry) => entry.pastedChunks),
|
|
2262
|
+
},
|
|
2263
|
+
}));
|
|
2264
|
+
};
|
|
2265
|
+
const fireQueuedMessage = async () => {
|
|
2266
|
+
const state = store.getState();
|
|
2267
|
+
const queued = state.queuedMessage;
|
|
2268
|
+
if (!queued || !state.busy || exiting)
|
|
2269
|
+
return;
|
|
2270
|
+
const text = (queued.pastedChunks.length
|
|
2271
|
+
? expandPastedChunks(queued.body, queued.pastedChunks)
|
|
2272
|
+
: queued.body).trim();
|
|
2273
|
+
const images = queued.imageAttachments.filter((attachment) => queued.body.includes(`[Image #${attachment.index}]`));
|
|
2274
|
+
if (!text && images.length === 0)
|
|
2275
|
+
return;
|
|
2276
|
+
const turnId = activeServerTurnId;
|
|
2277
|
+
const blockedReason = !turnId ? 'waiting for the turn to end' : null;
|
|
2278
|
+
const existingRow = blockedRows.get(queued);
|
|
2279
|
+
if (existingRow !== undefined) {
|
|
2280
|
+
upsertTurnMessage(existingRow, {
|
|
2281
|
+
state: 'queued',
|
|
2282
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2283
|
+
});
|
|
2284
|
+
if (blockedReason || !turnId)
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const id = existingRow ?? ++turnMessageCounter;
|
|
2288
|
+
const messageId = `m${id}_${Date.now().toString(36)}`;
|
|
2289
|
+
if (existingRow === undefined) {
|
|
2290
|
+
scheduleLiveFrameRemount();
|
|
2291
|
+
store.update((current) => ({
|
|
2292
|
+
...current,
|
|
2293
|
+
turnMessages: [
|
|
2294
|
+
...current.turnMessages,
|
|
2295
|
+
{
|
|
2296
|
+
id,
|
|
2297
|
+
text,
|
|
2298
|
+
state: blockedReason ? 'queued' : 'sending',
|
|
2299
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2300
|
+
},
|
|
2301
|
+
],
|
|
2302
|
+
}));
|
|
2303
|
+
}
|
|
2304
|
+
if (blockedReason || !turnId) {
|
|
2305
|
+
blockedRows.set(queued, id);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
blockedRows.delete(queued);
|
|
2309
|
+
unacknowledged.set(messageId, { row: id, queued });
|
|
2310
|
+
scheduleLiveFrameRemount();
|
|
2311
|
+
store.update((current) => current.queuedMessage === queued
|
|
2312
|
+
? { ...current, queuedMessage: null }
|
|
2313
|
+
: current);
|
|
2314
|
+
queueTurnEntry({
|
|
2315
|
+
body: text,
|
|
2316
|
+
kind: 'user',
|
|
2317
|
+
preformatted: queued.pastedChunks.length > 0,
|
|
2318
|
+
title: 'You · sent mid-turn',
|
|
2319
|
+
});
|
|
2320
|
+
const rollback = (note) => {
|
|
2321
|
+
if (!unacknowledged.delete(messageId))
|
|
2322
|
+
return;
|
|
2323
|
+
upsertTurnMessage(id, { state: 'queued', note });
|
|
2324
|
+
scheduleLiveFrameRemount();
|
|
2325
|
+
store.update((current) => current.queuedMessage ? current : { ...current, queuedMessage: queued });
|
|
2326
|
+
};
|
|
2327
|
+
let outcome;
|
|
2328
|
+
try {
|
|
2329
|
+
outcome = await chat.postInterjection({
|
|
2330
|
+
config: authConfig,
|
|
2331
|
+
turnId,
|
|
2332
|
+
text,
|
|
2333
|
+
messageId,
|
|
2334
|
+
imageAttachments: images,
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
catch (error) {
|
|
2338
|
+
rollback(`not sent — ${error?.message ?? 'request failed'}`);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (outcome === 'stale') {
|
|
2342
|
+
rollback('turn ended — sending as the next prompt');
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2016
2345
|
const handleSubmit = async (rawInput) => {
|
|
2017
2346
|
const chunks = store.getState().pastedChunks;
|
|
2018
2347
|
const expanded = chunks.length
|
|
@@ -2032,9 +2361,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2032
2361
|
return;
|
|
2033
2362
|
}
|
|
2034
2363
|
const pending = store.getState();
|
|
2364
|
+
const body = String(rawInput ?? '');
|
|
2035
2365
|
const snapshot = {
|
|
2036
|
-
body
|
|
2037
|
-
imageAttachments: pending.imageAttachments,
|
|
2366
|
+
body,
|
|
2367
|
+
imageAttachments: pending.imageAttachments.filter((attachment) => body.includes(`[Image #${attachment.index}]`)),
|
|
2038
2368
|
pastedChunks: pending.pastedChunks,
|
|
2039
2369
|
};
|
|
2040
2370
|
scheduleLiveFrameRemount();
|
|
@@ -2049,11 +2379,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2049
2379
|
}));
|
|
2050
2380
|
return;
|
|
2051
2381
|
}
|
|
2052
|
-
const imageAttachments = store
|
|
2382
|
+
const imageAttachments = store
|
|
2383
|
+
.getState()
|
|
2384
|
+
.imageAttachments.filter((attachment) => input.includes(`[Image #${attachment.index}]`));
|
|
2053
2385
|
scheduleLiveFrameRemount();
|
|
2054
2386
|
store.update((current) => ({
|
|
2055
2387
|
...current,
|
|
2056
2388
|
cursor: 0,
|
|
2389
|
+
imageAttachments: [],
|
|
2057
2390
|
input: '',
|
|
2058
2391
|
pastedChunks: [],
|
|
2059
2392
|
promptHistory: appendPromptToHistory(current.promptHistory, input),
|
|
@@ -2139,14 +2472,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2139
2472
|
});
|
|
2140
2473
|
}
|
|
2141
2474
|
catch (error) {
|
|
2475
|
+
if (exitForAuthenticationError(error))
|
|
2476
|
+
return;
|
|
2142
2477
|
appendError(error.message);
|
|
2143
2478
|
}
|
|
2144
2479
|
finally {
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2480
|
+
if (!exiting) {
|
|
2481
|
+
store.update((current) => ({
|
|
2482
|
+
...current,
|
|
2483
|
+
busy: false,
|
|
2484
|
+
status: 'Ready',
|
|
2485
|
+
}));
|
|
2486
|
+
}
|
|
2150
2487
|
}
|
|
2151
2488
|
return;
|
|
2152
2489
|
}
|
|
@@ -2160,14 +2497,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2160
2497
|
await openResumePicker();
|
|
2161
2498
|
}
|
|
2162
2499
|
catch (error) {
|
|
2500
|
+
if (exitForAuthenticationError(error))
|
|
2501
|
+
return;
|
|
2163
2502
|
appendError(error.message);
|
|
2164
2503
|
}
|
|
2165
2504
|
finally {
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2505
|
+
if (!exiting) {
|
|
2506
|
+
store.update((current) => ({
|
|
2507
|
+
...current,
|
|
2508
|
+
busy: false,
|
|
2509
|
+
status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
|
|
2510
|
+
}));
|
|
2511
|
+
}
|
|
2171
2512
|
}
|
|
2172
2513
|
return;
|
|
2173
2514
|
}
|
|
@@ -2187,43 +2528,85 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2187
2528
|
}
|
|
2188
2529
|
}
|
|
2189
2530
|
catch (error) {
|
|
2531
|
+
if (exitForAuthenticationError(error))
|
|
2532
|
+
return;
|
|
2190
2533
|
appendError(error.message);
|
|
2191
2534
|
}
|
|
2192
2535
|
finally {
|
|
2536
|
+
if (!exiting) {
|
|
2537
|
+
store.update((current) => ({
|
|
2538
|
+
...current,
|
|
2539
|
+
busy: false,
|
|
2540
|
+
status: current.modelPickerOpen ? 'Select a model' : 'Ready',
|
|
2541
|
+
}));
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
if (input === '/new') {
|
|
2547
|
+
newConversationInFlight = true;
|
|
2548
|
+
store.update((current) => ({
|
|
2549
|
+
...current,
|
|
2550
|
+
busy: true,
|
|
2551
|
+
busyPausedAt: null,
|
|
2552
|
+
busySince: Date.now(),
|
|
2553
|
+
imageAttachments: [],
|
|
2554
|
+
queuedMessage: null,
|
|
2555
|
+
status: 'Starting a new conversation...',
|
|
2556
|
+
}));
|
|
2557
|
+
if (!(await saveActiveSessionOrAbort())) {
|
|
2558
|
+
newConversationInFlight = false;
|
|
2193
2559
|
store.update((current) => ({
|
|
2194
2560
|
...current,
|
|
2195
2561
|
busy: false,
|
|
2196
|
-
|
|
2562
|
+
busyPausedAt: null,
|
|
2563
|
+
busySince: null,
|
|
2564
|
+
status: 'Ready',
|
|
2197
2565
|
}));
|
|
2566
|
+
await flushQueuedMessage();
|
|
2567
|
+
return;
|
|
2198
2568
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2569
|
+
startNewConversation(session);
|
|
2570
|
+
setBackgroundJobSession(session.sessionId);
|
|
2571
|
+
setScratchSession(session.sessionId);
|
|
2572
|
+
setImageStoreSession(session.sessionId);
|
|
2573
|
+
setTodoSession(session.sessionId);
|
|
2203
2574
|
clearTodos();
|
|
2575
|
+
syncBackgroundJobsState();
|
|
2204
2576
|
syncTodosState();
|
|
2205
2577
|
latestUsageSummary = null;
|
|
2206
|
-
await saveActiveSession();
|
|
2207
2578
|
store.replaceTranscript([
|
|
2208
2579
|
{
|
|
2209
|
-
body: '
|
|
2580
|
+
body: 'Started a new conversation. The previous session remains saved.',
|
|
2210
2581
|
kind: 'system',
|
|
2211
|
-
title: '
|
|
2582
|
+
title: 'Session',
|
|
2212
2583
|
},
|
|
2213
2584
|
]);
|
|
2585
|
+
await saveActiveSession();
|
|
2214
2586
|
syncShellStateFromSession();
|
|
2215
|
-
|
|
2587
|
+
newConversationInFlight = false;
|
|
2588
|
+
store.update((current) => ({
|
|
2589
|
+
...current,
|
|
2590
|
+
busy: false,
|
|
2591
|
+
busyPausedAt: null,
|
|
2592
|
+
busySince: null,
|
|
2593
|
+
status: 'Ready',
|
|
2594
|
+
}));
|
|
2216
2595
|
await remountTui();
|
|
2596
|
+
await flushQueuedMessage();
|
|
2217
2597
|
return;
|
|
2218
2598
|
}
|
|
2219
2599
|
latestUsageSummary = null;
|
|
2220
2600
|
disarmExitConfirm();
|
|
2601
|
+
unacknowledged = new Map();
|
|
2221
2602
|
const turnStartedAt = Date.now();
|
|
2222
2603
|
const turnGeneration = ++activeTurnGeneration;
|
|
2223
2604
|
const turnAbort = new AbortController();
|
|
2224
2605
|
activeTurnAbort = turnAbort;
|
|
2225
2606
|
lastTurnStartedAt = turnStartedAt;
|
|
2226
2607
|
todosTouchedThisTurn = false;
|
|
2608
|
+
clearTodos();
|
|
2609
|
+
syncTodosState();
|
|
2227
2610
|
const userEntry = {
|
|
2228
2611
|
body: input,
|
|
2229
2612
|
kind: 'user',
|
|
@@ -2232,11 +2615,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2232
2615
|
};
|
|
2233
2616
|
pendingTurnEntries = [];
|
|
2234
2617
|
queueTurnEntry(userEntry);
|
|
2618
|
+
resetThinkingPacer();
|
|
2235
2619
|
store.update((current) => ({
|
|
2236
2620
|
...current,
|
|
2237
2621
|
activeTurnInput: input,
|
|
2238
2622
|
activeTurnInputPreformatted: preformatted,
|
|
2623
|
+
analyzingImages: 0,
|
|
2239
2624
|
busy: true,
|
|
2625
|
+
busyPausedAt: null,
|
|
2240
2626
|
busySince: turnStartedAt,
|
|
2241
2627
|
clockNow: turnStartedAt,
|
|
2242
2628
|
status: 'Running turn...',
|
|
@@ -2245,6 +2631,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2245
2631
|
transcriptScrollOffset: 0,
|
|
2246
2632
|
tokenUsage: formatClientTokenUsage(0, latestUsageSummary),
|
|
2247
2633
|
turnCounter: current.turnCounter + 1,
|
|
2634
|
+
turnMessages: [],
|
|
2248
2635
|
workingTools: [],
|
|
2249
2636
|
}));
|
|
2250
2637
|
try {
|
|
@@ -2255,12 +2642,32 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2255
2642
|
input,
|
|
2256
2643
|
imageAttachments,
|
|
2257
2644
|
signal: turnAbort.signal,
|
|
2645
|
+
onTurnStart: (serverTurnId) => {
|
|
2646
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2647
|
+
return;
|
|
2648
|
+
activeServerTurnId = serverTurnId;
|
|
2649
|
+
},
|
|
2650
|
+
onInterjectionDelivered: (event) => {
|
|
2651
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2652
|
+
return;
|
|
2653
|
+
for (const messageId of event.messageIds ?? []) {
|
|
2654
|
+
const pending = unacknowledged.get(messageId);
|
|
2655
|
+
if (!pending)
|
|
2656
|
+
continue;
|
|
2657
|
+
unacknowledged.delete(messageId);
|
|
2658
|
+
upsertTurnMessage(pending.row, {
|
|
2659
|
+
state: 'delivered',
|
|
2660
|
+
note: undefined,
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
},
|
|
2258
2664
|
});
|
|
2259
2665
|
if (turnGeneration !== activeTurnGeneration)
|
|
2260
2666
|
return;
|
|
2261
2667
|
latestUsageSummary = result.usageSummary ?? null;
|
|
2262
2668
|
const turnEntries = takePendingTurnEntries();
|
|
2263
|
-
await saveActiveSession()
|
|
2669
|
+
if (!(await saveActiveSession()))
|
|
2670
|
+
return;
|
|
2264
2671
|
syncShellStateFromSession();
|
|
2265
2672
|
store.update((current) => ({
|
|
2266
2673
|
...current,
|
|
@@ -2297,10 +2704,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2297
2704
|
store.update((current) => ({
|
|
2298
2705
|
...current,
|
|
2299
2706
|
busy: false,
|
|
2707
|
+
busyPausedAt: null,
|
|
2300
2708
|
busySince: null,
|
|
2301
2709
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2302
2710
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
2303
2711
|
}));
|
|
2712
|
+
recoverUnacknowledgedMessages();
|
|
2304
2713
|
await flushQueuedMessage();
|
|
2305
2714
|
}
|
|
2306
2715
|
catch (error) {
|
|
@@ -2311,11 +2720,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2311
2720
|
return;
|
|
2312
2721
|
}
|
|
2313
2722
|
const cancelled = isTurnCancelledError(error);
|
|
2723
|
+
if (exitForAuthenticationError(error))
|
|
2724
|
+
return;
|
|
2314
2725
|
store.update((current) => ({
|
|
2315
2726
|
...current,
|
|
2316
2727
|
activeTurnInput: '',
|
|
2317
2728
|
activeTurnInputPreformatted: false,
|
|
2318
2729
|
busy: false,
|
|
2730
|
+
busyPausedAt: null,
|
|
2319
2731
|
busySince: null,
|
|
2320
2732
|
commandLog: [],
|
|
2321
2733
|
imageAttachments: [],
|
|
@@ -2335,13 +2747,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2335
2747
|
title: 'System',
|
|
2336
2748
|
},
|
|
2337
2749
|
]);
|
|
2338
|
-
await saveActiveSession()
|
|
2750
|
+
if (!(await saveActiveSession()))
|
|
2751
|
+
return;
|
|
2339
2752
|
}
|
|
2340
2753
|
else {
|
|
2341
2754
|
appendStaticEntries(turnEntries);
|
|
2342
2755
|
appendError(error.message);
|
|
2343
2756
|
}
|
|
2344
2757
|
await remountTui();
|
|
2758
|
+
recoverUnacknowledgedMessages(!cancelled);
|
|
2345
2759
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2346
2760
|
await flushQueuedMessage();
|
|
2347
2761
|
}
|
|
@@ -2352,24 +2766,34 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2352
2766
|
}
|
|
2353
2767
|
if (turnGeneration === activeTurnGeneration) {
|
|
2354
2768
|
lastTurnStartedAt = null;
|
|
2769
|
+
activeServerTurnId = null;
|
|
2355
2770
|
}
|
|
2771
|
+
recoverUnacknowledgedMessages();
|
|
2356
2772
|
}
|
|
2357
2773
|
};
|
|
2774
|
+
session.onImageAnalysis = (activeImageCount) => {
|
|
2775
|
+
store.update((current) => ({
|
|
2776
|
+
...current,
|
|
2777
|
+
analyzingImages: Math.max(0, activeImageCount),
|
|
2778
|
+
}));
|
|
2779
|
+
};
|
|
2358
2780
|
session.onStatus = (message) => {
|
|
2359
2781
|
const panel = thinkingPanelFromStatus(message);
|
|
2360
2782
|
store.update((current) => ({
|
|
2361
2783
|
...current,
|
|
2362
2784
|
status: message,
|
|
2363
|
-
|
|
2364
|
-
thinkingNotes: panel?.notes ?? current.thinkingNotes,
|
|
2365
|
-
tokenUsage: formatClientTokenUsage(current.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
|
|
2785
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2366
2786
|
}));
|
|
2787
|
+
queueThinkingUpdate({
|
|
2788
|
+
title: panel ? panel.title : liveStatusPanelTitle(message),
|
|
2789
|
+
notes: panel ? panel.notes : [],
|
|
2790
|
+
});
|
|
2367
2791
|
};
|
|
2368
2792
|
session.onContextLog = (message) => {
|
|
2369
2793
|
store.update((current) => ({
|
|
2370
2794
|
...current,
|
|
2371
2795
|
contextStatus: message,
|
|
2372
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2796
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2373
2797
|
}));
|
|
2374
2798
|
};
|
|
2375
2799
|
session.onToolEvent = (event) => {
|
|
@@ -2438,51 +2862,45 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2438
2862
|
projectIndex.onStatus = session.onStatus;
|
|
2439
2863
|
projectIndex.onContextLog = session.onContextLog;
|
|
2440
2864
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
2441
|
-
session.
|
|
2865
|
+
session.requestUserInput = async (request, signal) => openUserInputPrompt(request.questions, signal);
|
|
2866
|
+
session.requestPermission = async (request) => {
|
|
2867
|
+
const deny = { kind: 'deny' };
|
|
2442
2868
|
if (exiting)
|
|
2443
|
-
return
|
|
2869
|
+
return deny;
|
|
2444
2870
|
if (sessionAutoYes)
|
|
2445
|
-
return
|
|
2446
|
-
const
|
|
2447
|
-
if (
|
|
2448
|
-
setAgentMode('auto-accept');
|
|
2449
|
-
syncShellStateFromSession();
|
|
2871
|
+
return { kind: 'once' };
|
|
2872
|
+
const decision = await openApprovalPrompt(request);
|
|
2873
|
+
if (decision.kind === 'always-bucket') {
|
|
2450
2874
|
appendTurnAwareEntry({
|
|
2451
|
-
body:
|
|
2875
|
+
body: `Allowed for the rest of this session: ${bucketActionLabel(request.bucket)}.`,
|
|
2452
2876
|
kind: 'system',
|
|
2453
2877
|
title: 'Approvals',
|
|
2454
2878
|
});
|
|
2455
|
-
return true;
|
|
2456
2879
|
}
|
|
2457
|
-
|
|
2458
|
-
};
|
|
2459
|
-
session.confirmPatch = async (filePath, patch) => {
|
|
2460
|
-
if (exiting)
|
|
2461
|
-
return false;
|
|
2462
|
-
if (sessionAutoYes)
|
|
2463
|
-
return true;
|
|
2464
|
-
const choice = await openApprovalPrompt('Approve patch?', 'Review changes before applying.', {
|
|
2465
|
-
diff: patch,
|
|
2466
|
-
filePath,
|
|
2467
|
-
});
|
|
2468
|
-
if (choice === 'a') {
|
|
2469
|
-
setAgentMode('auto-accept');
|
|
2470
|
-
syncShellStateFromSession();
|
|
2880
|
+
else if (decision.kind === 'always-prefix') {
|
|
2471
2881
|
appendTurnAwareEntry({
|
|
2472
|
-
body:
|
|
2882
|
+
body: `Allowed for the rest of this session: commands starting with \`${decision.prefix}\`.`,
|
|
2473
2883
|
kind: 'system',
|
|
2474
2884
|
title: 'Approvals',
|
|
2475
2885
|
});
|
|
2476
|
-
return true;
|
|
2477
2886
|
}
|
|
2478
|
-
return
|
|
2887
|
+
return decision;
|
|
2479
2888
|
};
|
|
2480
2889
|
const shellInputHandlers = {
|
|
2890
|
+
getApprovalScrollLimit: () => {
|
|
2891
|
+
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2892
|
+
return approvalScrollLimit(store.getState().approvalPrompt, contentWidth, terminalRows);
|
|
2893
|
+
},
|
|
2481
2894
|
getTranscriptScrollLimit: () => {
|
|
2482
2895
|
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2483
2896
|
const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
2484
2897
|
return blocks.reduce((total, block, index) => total + block.length + (index > 0 ? 1 : 0), 0);
|
|
2485
2898
|
},
|
|
2899
|
+
getUserInputViewport: () => {
|
|
2900
|
+
const nowMs = Date.now();
|
|
2901
|
+
const state = store.getState();
|
|
2902
|
+
return userInputViewportForFrame(state, terminalCols, terminalRows, spinnerFrame, busyElapsedSeconds(state, nowMs), nowMs);
|
|
2903
|
+
},
|
|
2486
2904
|
onCancelTurn: cancelActiveTurn,
|
|
2487
2905
|
onCycleAgentMode: cycleAgentMode,
|
|
2488
2906
|
onCtrlC: handleCtrlC,
|
|
@@ -2491,6 +2909,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2491
2909
|
onLiveFrameShapeChange: scheduleLiveFrameRemount,
|
|
2492
2910
|
onRequestExit: requestExit,
|
|
2493
2911
|
onResolveApproval: handleInlineApprovalChoice,
|
|
2912
|
+
onResolveUserInput: handleInlineUserInput,
|
|
2494
2913
|
onResumeSession: handleInlineResumeSelection,
|
|
2495
2914
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2496
2915
|
onSelectModel: handleInlineModelSelection,
|
|
@@ -2498,6 +2917,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2498
2917
|
onJobsPickerKill: handleJobsPickerKill,
|
|
2499
2918
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2500
2919
|
onSubmit: handleSubmit,
|
|
2920
|
+
onFireQueuedMessage: fireQueuedMessage,
|
|
2501
2921
|
};
|
|
2502
2922
|
const unsubscribe = store.subscribe(() => {
|
|
2503
2923
|
renderCurrentFrame();
|
|
@@ -2505,6 +2925,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2505
2925
|
const spinnerTimer = setInterval(() => {
|
|
2506
2926
|
spinnerFrame = (spinnerFrame + 1) % 6;
|
|
2507
2927
|
if (store.getState().busy) {
|
|
2928
|
+
flushPendingThinking();
|
|
2508
2929
|
renderCurrentFrame();
|
|
2509
2930
|
}
|
|
2510
2931
|
}, 120);
|
|
@@ -2560,10 +2981,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2560
2981
|
killAllBackgroundJobs();
|
|
2561
2982
|
setBackgroundJobSession(null);
|
|
2562
2983
|
setScratchSession(null);
|
|
2984
|
+
setImageStoreSession(null);
|
|
2563
2985
|
setTodoSession(null);
|
|
2564
2986
|
setBackgroundJobUpdateHook(null);
|
|
2565
2987
|
await bridge.close();
|
|
2988
|
+
releaseTerminalWrites();
|
|
2566
2989
|
setCommandOutputHook(null);
|
|
2567
2990
|
}
|
|
2568
2991
|
});
|
|
2992
|
+
if (fatalError)
|
|
2993
|
+
throw fatalError;
|
|
2569
2994
|
}
|