@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.30
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 +63 -16
- 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 +610 -164
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +452 -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,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,12 @@ 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',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
command: '/logout',
|
|
110
|
+
description: 'Sign out and quit',
|
|
117
111
|
},
|
|
118
112
|
{
|
|
119
113
|
command: '/exit',
|
|
@@ -514,6 +508,8 @@ function formatToolResultState(result) {
|
|
|
514
508
|
return 'Blocked';
|
|
515
509
|
if (result?.ok === true)
|
|
516
510
|
return 'OK';
|
|
511
|
+
if (Number.isInteger(result?.httpStatus))
|
|
512
|
+
return String(result.httpStatus);
|
|
517
513
|
return 'Failed';
|
|
518
514
|
}
|
|
519
515
|
function formatRunCommandResultState(result) {
|
|
@@ -670,7 +666,7 @@ function buildFileChangeEntry(event) {
|
|
|
670
666
|
title: `Edited ${filePath}${summary}`,
|
|
671
667
|
};
|
|
672
668
|
}
|
|
673
|
-
function buildWorkingToolEntry(event) {
|
|
669
|
+
export function buildWorkingToolEntry(event) {
|
|
674
670
|
const { call, result } = event;
|
|
675
671
|
const error = typeof result?.error === 'string' && result.error.trim()
|
|
676
672
|
? `\n${truncate(result.error.trim(), 180)}`
|
|
@@ -780,13 +776,21 @@ function displayUserTextFromHistoryEntry(entry) {
|
|
|
780
776
|
.slice(contentStart, contentEnd === -1 ? text.length : contentEnd)
|
|
781
777
|
.trim();
|
|
782
778
|
}
|
|
783
|
-
function buildTranscriptFromSessionHistory(history) {
|
|
779
|
+
export function buildTranscriptFromSessionHistory(history) {
|
|
784
780
|
const entries = [];
|
|
785
781
|
const pendingCalls = new Map();
|
|
782
|
+
const pendingUserInputRequests = new Map();
|
|
786
783
|
for (const entry of history) {
|
|
787
784
|
if (!entry?.parts?.length)
|
|
788
785
|
continue;
|
|
789
786
|
for (const part of entry.parts) {
|
|
787
|
+
const userInputRequest = part?.userInputRequest;
|
|
788
|
+
if (userInputRequest && typeof userInputRequest === 'object') {
|
|
789
|
+
const requestId = String(userInputRequest.requestId ?? '').trim();
|
|
790
|
+
if (requestId && Array.isArray(userInputRequest.questions)) {
|
|
791
|
+
pendingUserInputRequests.set(requestId, userInputRequest.questions);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
790
794
|
const call = part?.functionCall;
|
|
791
795
|
if (!call || typeof call !== 'object')
|
|
792
796
|
continue;
|
|
@@ -796,6 +800,21 @@ function buildTranscriptFromSessionHistory(history) {
|
|
|
796
800
|
pendingCalls.set(callId, call);
|
|
797
801
|
}
|
|
798
802
|
for (const part of entry.parts) {
|
|
803
|
+
const userInputResult = part?.userInputResult;
|
|
804
|
+
if (userInputResult && typeof userInputResult === 'object') {
|
|
805
|
+
const requestId = String(userInputResult.requestId ?? '').trim();
|
|
806
|
+
const questions = pendingUserInputRequests.get(requestId);
|
|
807
|
+
const result = userInputResult.result;
|
|
808
|
+
if (questions &&
|
|
809
|
+
(result?.status === 'submitted' || result?.status === 'cancelled')) {
|
|
810
|
+
pendingUserInputRequests.delete(requestId);
|
|
811
|
+
entries.push({
|
|
812
|
+
body: formatUserInputTranscript(questions, result),
|
|
813
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
814
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
799
818
|
const functionResponse = part?.functionResponse;
|
|
800
819
|
if (!functionResponse || typeof functionResponse !== 'object')
|
|
801
820
|
continue;
|
|
@@ -824,8 +843,23 @@ function buildTranscriptFromSessionHistory(history) {
|
|
|
824
843
|
}
|
|
825
844
|
continue;
|
|
826
845
|
}
|
|
846
|
+
if (entry.role === 'user' && entry.kind === 'userInterjection') {
|
|
847
|
+
const text = String(entry.userInput ?? '').trim();
|
|
848
|
+
if (text) {
|
|
849
|
+
entries.push({ body: text, kind: 'user', title: 'You · sent mid-turn' });
|
|
850
|
+
}
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
827
853
|
const text = textFromHistoryEntry(entry);
|
|
828
854
|
if ((entry.role === 'model' || entry.role === 'assistant') && text) {
|
|
855
|
+
if (isTurnFailureMarker(text)) {
|
|
856
|
+
entries.push({
|
|
857
|
+
body: 'This request did not complete.',
|
|
858
|
+
kind: 'system',
|
|
859
|
+
title: 'Previous turn',
|
|
860
|
+
});
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
829
863
|
entries.push({ body: text, kind: 'assistant', title: 'Response' });
|
|
830
864
|
}
|
|
831
865
|
}
|
|
@@ -951,11 +985,15 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
951
985
|
activeTurnInput: '',
|
|
952
986
|
activeTurnInputPreformatted: false,
|
|
953
987
|
agentMode: session.agentMode,
|
|
954
|
-
|
|
988
|
+
analyzingImages: 0,
|
|
989
|
+
generatingImage: false,
|
|
990
|
+
approvalCursor: 0,
|
|
955
991
|
approvalPrompt: null,
|
|
992
|
+
approvalScrollOffset: 0,
|
|
956
993
|
autoYes: session.autoYes,
|
|
957
994
|
backgroundJobs: [],
|
|
958
995
|
busy: false,
|
|
996
|
+
busyPausedAt: null,
|
|
959
997
|
busySince: null,
|
|
960
998
|
clockNow: Date.now(),
|
|
961
999
|
commandCursor: 0,
|
|
@@ -983,6 +1021,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
983
1021
|
resumePickerSessions: [],
|
|
984
1022
|
transcriptScrollOffset: 0,
|
|
985
1023
|
queuedMessage: null,
|
|
1024
|
+
turnMessages: [],
|
|
986
1025
|
serverModels: serverModels.models,
|
|
987
1026
|
sudoPrompt: null,
|
|
988
1027
|
status: 'Ready',
|
|
@@ -993,6 +1032,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
993
1032
|
tokenUsage: formatClientTokenUsage(null),
|
|
994
1033
|
transcript: [],
|
|
995
1034
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
1035
|
+
userInputPrompt: null,
|
|
996
1036
|
pastedChunks: [],
|
|
997
1037
|
imageAttachments: [],
|
|
998
1038
|
workingTools: [],
|
|
@@ -1009,7 +1049,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1009
1049
|
const reasoningTokens = usageSummary?.reasoningTokens ?? 0;
|
|
1010
1050
|
const cacheTokens = usageSummary?.cacheTokens ?? 0;
|
|
1011
1051
|
const cacheWriteTokens = usageSummary?.cacheWriteTokens ?? 0;
|
|
1012
|
-
const indexTokens = usageSummary?.indexTokens ?? 0;
|
|
1013
1052
|
return [
|
|
1014
1053
|
'Session tokens',
|
|
1015
1054
|
`in ${formatTokenCount(inputTokens)}`,
|
|
@@ -1017,7 +1056,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1017
1056
|
...(reasoningTokens > 0 ? [`think ${formatTokenCount(reasoningTokens)}`] : []),
|
|
1018
1057
|
`cache ${formatTokenCount(cacheTokens)}`,
|
|
1019
1058
|
`write ${formatTokenCount(cacheWriteTokens)}`,
|
|
1020
|
-
`index ${formatTokenCount(indexTokens)}`,
|
|
1021
1059
|
].join(' • ');
|
|
1022
1060
|
}
|
|
1023
1061
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
@@ -1060,6 +1098,24 @@ function splitThinkingLines(text) {
|
|
|
1060
1098
|
.map((part) => part.trim())
|
|
1061
1099
|
.filter(Boolean));
|
|
1062
1100
|
}
|
|
1101
|
+
const LIVE_STATUS_PANEL_MAX_CHARS = 72;
|
|
1102
|
+
const TOOL_PROGRESS_STATUS_PATTERN = /^(?:Tool:\s|Running\s\S+(?:\slocally)?\.\.\.$)/i;
|
|
1103
|
+
export function isToolProgressStatus(status) {
|
|
1104
|
+
return TOOL_PROGRESS_STATUS_PATTERN.test(String(status ?? '').trim());
|
|
1105
|
+
}
|
|
1106
|
+
function holdOrPickFallback(currentTitle) {
|
|
1107
|
+
return THINKING_FALLBACK_PHRASES.includes(currentTitle.trim())
|
|
1108
|
+
? currentTitle
|
|
1109
|
+
: pickThinkingFallbackPhrase();
|
|
1110
|
+
}
|
|
1111
|
+
function liveStatusPanelTitle(status) {
|
|
1112
|
+
const text = String(status ?? '').trim();
|
|
1113
|
+
if (!text || text.includes('\n'))
|
|
1114
|
+
return '';
|
|
1115
|
+
if (isToolProgressStatus(text))
|
|
1116
|
+
return '';
|
|
1117
|
+
return text.length <= LIVE_STATUS_PANEL_MAX_CHARS ? text : '';
|
|
1118
|
+
}
|
|
1063
1119
|
function thinkingPanelFromStatus(status) {
|
|
1064
1120
|
const rawText = thinkingNoteFromStatus(status);
|
|
1065
1121
|
if (!rawText)
|
|
@@ -1083,7 +1139,7 @@ function thinkingPanelFromStatus(status) {
|
|
|
1083
1139
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
1084
1140
|
return {
|
|
1085
1141
|
title,
|
|
1086
|
-
notes: splitThinkingLines(bodyLines.join('\n')).slice(
|
|
1142
|
+
notes: splitThinkingLines(bodyLines.join('\n')).slice(0, 3),
|
|
1087
1143
|
};
|
|
1088
1144
|
}
|
|
1089
1145
|
export const EXIT_CTRL_C_CONFIRM_MESSAGE = 'Press Ctrl+C again to quit.';
|
|
@@ -1213,26 +1269,37 @@ export function navigatePromptHistory(state, direction) {
|
|
|
1213
1269
|
promptHistoryCursor: nextCursor,
|
|
1214
1270
|
};
|
|
1215
1271
|
}
|
|
1216
|
-
function getDefaultApprovalCursor() {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1272
|
+
export function getDefaultApprovalCursor(optionCount) {
|
|
1273
|
+
return Math.max(0, optionCount - 1);
|
|
1274
|
+
}
|
|
1275
|
+
export function getNextApprovalCursor(currentIndex, direction, optionCount) {
|
|
1276
|
+
if (optionCount <= 0)
|
|
1277
|
+
return 0;
|
|
1278
|
+
return (currentIndex + direction + optionCount) % optionCount;
|
|
1279
|
+
}
|
|
1280
|
+
export function pauseBusyClock(state, nowMs) {
|
|
1281
|
+
if (state.busySince === null || state.busyPausedAt !== null)
|
|
1282
|
+
return state;
|
|
1283
|
+
return { ...state, busyPausedAt: nowMs };
|
|
1284
|
+
}
|
|
1285
|
+
export function resumeBusyClock(state, nowMs) {
|
|
1286
|
+
if (state.busyPausedAt === null)
|
|
1287
|
+
return state;
|
|
1288
|
+
return {
|
|
1289
|
+
...state,
|
|
1290
|
+
busyPausedAt: null,
|
|
1291
|
+
busySince: state.busySince === null
|
|
1292
|
+
? null
|
|
1293
|
+
: state.busySince + Math.max(0, nowMs - state.busyPausedAt),
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
export function busyElapsedMs(state, nowMs) {
|
|
1297
|
+
if (state.busySince === null)
|
|
1298
|
+
return null;
|
|
1299
|
+
return Math.max(0, (state.busyPausedAt ?? nowMs) - state.busySince);
|
|
1300
|
+
}
|
|
1301
|
+
export function busyElapsedSeconds(state, nowMs) {
|
|
1302
|
+
return Math.floor((busyElapsedMs(state, nowMs) ?? 0) / 1000);
|
|
1236
1303
|
}
|
|
1237
1304
|
export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
1238
1305
|
return serverModels.map((model) => ({
|
|
@@ -1270,35 +1337,37 @@ function appendCommandLog(state, text) {
|
|
|
1270
1337
|
};
|
|
1271
1338
|
}
|
|
1272
1339
|
async function saveSessionBoth({ serverSessionClient, session, }) {
|
|
1340
|
+
if (!sessionHasUserMessage(session))
|
|
1341
|
+
return;
|
|
1273
1342
|
saveSessionState(session);
|
|
1274
1343
|
await serverSessionClient.save(session);
|
|
1275
1344
|
}
|
|
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.');
|
|
1345
|
+
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
1346
|
+
if (process.stdin.isTTY !== true) {
|
|
1347
|
+
throw new Error('stdin is not a terminal');
|
|
1348
|
+
}
|
|
1349
|
+
if (process.stdout.isTTY !== true) {
|
|
1350
|
+
throw new Error('stdout is not a terminal');
|
|
1287
1351
|
}
|
|
1352
|
+
let fatalError = null;
|
|
1353
|
+
let signedOut = false;
|
|
1288
1354
|
await withTuiMode(async () => {
|
|
1289
1355
|
setBackgroundJobSession(session.sessionId);
|
|
1290
1356
|
setScratchSession(session.sessionId);
|
|
1357
|
+
setImageStoreSession(session.sessionId);
|
|
1291
1358
|
setTodoSession(session.sessionId);
|
|
1292
1359
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1293
1360
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1294
1361
|
let currentServerModels = serverModels;
|
|
1295
1362
|
let sessionAutoYes = session.autoYes;
|
|
1296
1363
|
let resolveDone = null;
|
|
1297
|
-
let
|
|
1364
|
+
let resolvePermissionDecision = null;
|
|
1298
1365
|
let resolveSudoPassword = null;
|
|
1366
|
+
let pendingUserInput = null;
|
|
1299
1367
|
let cleanupSudoPasswordPrompt = null;
|
|
1300
1368
|
let sudoPasswordBuffer = '';
|
|
1301
1369
|
const bridge = createRatatuiBridge();
|
|
1370
|
+
captureTerminalWrites();
|
|
1302
1371
|
const { handleShellKeyEvent } = await import('./tui/shell-input.js');
|
|
1303
1372
|
let terminalCols = 80;
|
|
1304
1373
|
let terminalRows = 24;
|
|
@@ -1311,6 +1380,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1311
1380
|
let latestUsageSummary = null;
|
|
1312
1381
|
let pendingTurnEntries = [];
|
|
1313
1382
|
let activeTurnAbort = null;
|
|
1383
|
+
let activeServerTurnId = null;
|
|
1384
|
+
let unacknowledged = new Map();
|
|
1385
|
+
const blockedRows = new WeakMap();
|
|
1386
|
+
let newConversationInFlight = false;
|
|
1314
1387
|
let todosTouchedThisTurn = false;
|
|
1315
1388
|
const syncTodosState = () => {
|
|
1316
1389
|
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
@@ -1319,6 +1392,36 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1319
1392
|
let exitCtrlCArmed = false;
|
|
1320
1393
|
let exitCtrlCTimer = null;
|
|
1321
1394
|
let transientStatusTimer = null;
|
|
1395
|
+
const THINKING_MIN_DWELL_MS = 3_000;
|
|
1396
|
+
let thinkingAppliedAt = 0;
|
|
1397
|
+
let pendingThinking = null;
|
|
1398
|
+
const applyThinking = (next) => {
|
|
1399
|
+
thinkingAppliedAt = Date.now();
|
|
1400
|
+
pendingThinking = null;
|
|
1401
|
+
store.update((current) => ({
|
|
1402
|
+
...current,
|
|
1403
|
+
thinkingTitle: next.title || holdOrPickFallback(current.thinkingTitle),
|
|
1404
|
+
thinkingNotes: next.notes,
|
|
1405
|
+
}));
|
|
1406
|
+
};
|
|
1407
|
+
const queueThinkingUpdate = (next) => {
|
|
1408
|
+
if (Date.now() - thinkingAppliedAt >= THINKING_MIN_DWELL_MS) {
|
|
1409
|
+
applyThinking(next);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
pendingThinking = next;
|
|
1413
|
+
};
|
|
1414
|
+
const flushPendingThinking = () => {
|
|
1415
|
+
if (!pendingThinking)
|
|
1416
|
+
return;
|
|
1417
|
+
if (Date.now() - thinkingAppliedAt < THINKING_MIN_DWELL_MS)
|
|
1418
|
+
return;
|
|
1419
|
+
applyThinking(pendingThinking);
|
|
1420
|
+
};
|
|
1421
|
+
const resetThinkingPacer = () => {
|
|
1422
|
+
thinkingAppliedAt = 0;
|
|
1423
|
+
pendingThinking = null;
|
|
1424
|
+
};
|
|
1322
1425
|
const done = new Promise((resolve) => {
|
|
1323
1426
|
resolveDone = resolve;
|
|
1324
1427
|
});
|
|
@@ -1343,7 +1446,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1343
1446
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1344
1447
|
}, 2500);
|
|
1345
1448
|
};
|
|
1346
|
-
const terminalTitle = createTerminalTitleController(
|
|
1449
|
+
const terminalTitle = createTerminalTitleController({
|
|
1450
|
+
write: (title) => bridge.setTitle(title),
|
|
1451
|
+
});
|
|
1347
1452
|
const syncTerminalTitle = () => {
|
|
1348
1453
|
const state = store.getState();
|
|
1349
1454
|
terminalTitle.sync({
|
|
@@ -1356,9 +1461,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1356
1461
|
return;
|
|
1357
1462
|
const state = store.getState();
|
|
1358
1463
|
syncTerminalTitle();
|
|
1359
|
-
const elapsedSeconds = state.
|
|
1360
|
-
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1361
|
-
: 0;
|
|
1464
|
+
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1362
1465
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1363
1466
|
};
|
|
1364
1467
|
const remountTui = async () => {
|
|
@@ -1484,15 +1587,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1484
1587
|
}
|
|
1485
1588
|
};
|
|
1486
1589
|
const dismissPendingApproval = () => {
|
|
1487
|
-
if (!
|
|
1590
|
+
if (!resolvePermissionDecision)
|
|
1488
1591
|
return;
|
|
1489
|
-
const pendingResolve =
|
|
1490
|
-
|
|
1491
|
-
pendingResolve('
|
|
1592
|
+
const pendingResolve = resolvePermissionDecision;
|
|
1593
|
+
resolvePermissionDecision = null;
|
|
1594
|
+
pendingResolve({ kind: 'deny' });
|
|
1492
1595
|
store.update((current) => ({
|
|
1493
|
-
...current,
|
|
1494
|
-
approvalCursor:
|
|
1596
|
+
...resumeBusyClock(current, Date.now()),
|
|
1597
|
+
approvalCursor: 0,
|
|
1495
1598
|
approvalPrompt: null,
|
|
1599
|
+
approvalScrollOffset: 0,
|
|
1496
1600
|
}));
|
|
1497
1601
|
};
|
|
1498
1602
|
const dismissPendingSudoPassword = () => {
|
|
@@ -1505,17 +1609,34 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1505
1609
|
sudoPasswordBuffer = '';
|
|
1506
1610
|
pendingResolve(null);
|
|
1507
1611
|
store.update((current) => ({
|
|
1508
|
-
...current,
|
|
1612
|
+
...resumeBusyClock(current, Date.now()),
|
|
1509
1613
|
sudoPrompt: null,
|
|
1510
1614
|
}));
|
|
1511
1615
|
};
|
|
1616
|
+
const dismissPendingUserInput = () => {
|
|
1617
|
+
const pending = pendingUserInput;
|
|
1618
|
+
if (!pending)
|
|
1619
|
+
return;
|
|
1620
|
+
pendingUserInput = null;
|
|
1621
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1622
|
+
const error = new Error('Turn cancelled.');
|
|
1623
|
+
error.name = 'AbortError';
|
|
1624
|
+
pending.reject(error);
|
|
1625
|
+
store.update((current) => ({
|
|
1626
|
+
...resumeBusyClock(current, Date.now()),
|
|
1627
|
+
status: current.userInputPrompt?.returnStatus ?? current.status,
|
|
1628
|
+
userInputPrompt: null,
|
|
1629
|
+
}));
|
|
1630
|
+
};
|
|
1512
1631
|
const cancelActiveTurn = () => {
|
|
1513
|
-
if (!store.getState().busy)
|
|
1632
|
+
if (!store.getState().busy || newConversationInFlight)
|
|
1514
1633
|
return;
|
|
1515
1634
|
disarmExitConfirm();
|
|
1516
1635
|
dismissPendingApproval();
|
|
1517
1636
|
dismissPendingSudoPassword();
|
|
1637
|
+
dismissPendingUserInput();
|
|
1518
1638
|
activeTurnGeneration += 1;
|
|
1639
|
+
resetThinkingPacer();
|
|
1519
1640
|
activeTurnAbort?.abort();
|
|
1520
1641
|
activeTurnAbort = null;
|
|
1521
1642
|
cancelActiveCommand();
|
|
@@ -1533,6 +1654,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1533
1654
|
activeTurnInput: '',
|
|
1534
1655
|
activeTurnInputPreformatted: false,
|
|
1535
1656
|
busy: false,
|
|
1657
|
+
busyPausedAt: null,
|
|
1536
1658
|
busySince: null,
|
|
1537
1659
|
commandLog: [],
|
|
1538
1660
|
cursor: queued ? queued.body.length : current.cursor,
|
|
@@ -1545,8 +1667,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1545
1667
|
status: 'Ready',
|
|
1546
1668
|
thinkingTitle: '',
|
|
1547
1669
|
thinkingNotes: [],
|
|
1670
|
+
turnMessages: [],
|
|
1548
1671
|
workingTools: [],
|
|
1549
|
-
tokenUsage: formatClientTokenUsage(current
|
|
1672
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1550
1673
|
}));
|
|
1551
1674
|
appendStaticEntries(cancelledEntries);
|
|
1552
1675
|
lastTurnStartedAt = null;
|
|
@@ -1554,14 +1677,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1554
1677
|
scheduleLiveFrameRemount();
|
|
1555
1678
|
void remountTui();
|
|
1556
1679
|
};
|
|
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
1680
|
const syncShellStateFromSession = () => {
|
|
1566
1681
|
store.update((current) => ({
|
|
1567
1682
|
...current,
|
|
@@ -1640,6 +1755,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1640
1755
|
}
|
|
1641
1756
|
dismissPendingApproval();
|
|
1642
1757
|
dismissPendingSudoPassword();
|
|
1758
|
+
dismissPendingUserInput();
|
|
1643
1759
|
exiting = true;
|
|
1644
1760
|
killAllBackgroundJobs();
|
|
1645
1761
|
store.update((current) => ({
|
|
@@ -1649,9 +1765,45 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1649
1765
|
status: 'Exiting...',
|
|
1650
1766
|
}));
|
|
1651
1767
|
void bridge.close().then(() => {
|
|
1768
|
+
releaseTerminalWrites();
|
|
1652
1769
|
resolveDone?.();
|
|
1653
1770
|
});
|
|
1654
1771
|
};
|
|
1772
|
+
const exitForAuthenticationError = (error) => {
|
|
1773
|
+
if (!isAuthenticationError(error))
|
|
1774
|
+
return false;
|
|
1775
|
+
clearCliAuthConfig(session.env);
|
|
1776
|
+
if (sessionHasUserMessage(session)) {
|
|
1777
|
+
saveSessionState(session);
|
|
1778
|
+
}
|
|
1779
|
+
fatalError = new Error(authenticationErrorMessage(error));
|
|
1780
|
+
requestExit();
|
|
1781
|
+
return true;
|
|
1782
|
+
};
|
|
1783
|
+
const saveActiveSession = async () => {
|
|
1784
|
+
try {
|
|
1785
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1788
|
+
catch (error) {
|
|
1789
|
+
if (exitForAuthenticationError(error))
|
|
1790
|
+
return false;
|
|
1791
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1792
|
+
return true;
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
const saveActiveSessionOrAbort = async () => {
|
|
1796
|
+
try {
|
|
1797
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1798
|
+
return true;
|
|
1799
|
+
}
|
|
1800
|
+
catch (error) {
|
|
1801
|
+
if (exitForAuthenticationError(error))
|
|
1802
|
+
return false;
|
|
1803
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1655
1807
|
const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
|
|
1656
1808
|
if (exiting || signal?.aborted) {
|
|
1657
1809
|
resolve(null);
|
|
@@ -1669,7 +1821,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1669
1821
|
cleanupSudoPasswordPrompt = null;
|
|
1670
1822
|
}
|
|
1671
1823
|
store.update((current) => ({
|
|
1672
|
-
...current,
|
|
1824
|
+
...pauseBusyClock(current, Date.now()),
|
|
1673
1825
|
sudoPrompt: {
|
|
1674
1826
|
command,
|
|
1675
1827
|
passwordLength: 0,
|
|
@@ -1689,7 +1841,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1689
1841
|
cleanupSudoPasswordPrompt = null;
|
|
1690
1842
|
sudoPasswordBuffer = '';
|
|
1691
1843
|
store.update((current) => ({
|
|
1692
|
-
...current,
|
|
1844
|
+
...resumeBusyClock(current, Date.now()),
|
|
1693
1845
|
sudoPrompt: null,
|
|
1694
1846
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1695
1847
|
}));
|
|
@@ -1703,7 +1855,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1703
1855
|
cleanupSudoPasswordPrompt = null;
|
|
1704
1856
|
sudoPasswordBuffer = '';
|
|
1705
1857
|
store.update((current) => ({
|
|
1706
|
-
...current,
|
|
1858
|
+
...resumeBusyClock(current, Date.now()),
|
|
1707
1859
|
sudoPrompt: null,
|
|
1708
1860
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1709
1861
|
}));
|
|
@@ -1723,38 +1875,88 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1723
1875
|
: null,
|
|
1724
1876
|
}));
|
|
1725
1877
|
};
|
|
1726
|
-
const
|
|
1878
|
+
const openUserInputPrompt = (questions, signal) => new Promise((resolve, reject) => {
|
|
1879
|
+
if (exiting || signal?.aborted) {
|
|
1880
|
+
const error = new Error('Turn cancelled.');
|
|
1881
|
+
error.name = 'AbortError';
|
|
1882
|
+
reject(error);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
const onAbort = () => dismissPendingUserInput();
|
|
1886
|
+
pendingUserInput = {
|
|
1887
|
+
questions,
|
|
1888
|
+
resolve,
|
|
1889
|
+
reject,
|
|
1890
|
+
signal,
|
|
1891
|
+
onAbort,
|
|
1892
|
+
};
|
|
1893
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1894
|
+
store.update((current) => ({
|
|
1895
|
+
...pauseBusyClock(current, Date.now()),
|
|
1896
|
+
status: 'Waiting for your input',
|
|
1897
|
+
userInputPrompt: createUserInputPromptState(questions, current.status),
|
|
1898
|
+
}));
|
|
1899
|
+
scheduleLiveFrameRemount();
|
|
1900
|
+
});
|
|
1901
|
+
const handleInlineUserInput = async (result) => {
|
|
1902
|
+
const pending = pendingUserInput;
|
|
1903
|
+
const prompt = store.getState().userInputPrompt;
|
|
1904
|
+
if (!pending || !prompt)
|
|
1905
|
+
return;
|
|
1906
|
+
pendingUserInput = null;
|
|
1907
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1908
|
+
store.update((current) => ({
|
|
1909
|
+
...resumeBusyClock(current, Date.now()),
|
|
1910
|
+
status: prompt.returnStatus,
|
|
1911
|
+
userInputPrompt: null,
|
|
1912
|
+
}));
|
|
1913
|
+
appendTurnAwareEntry({
|
|
1914
|
+
body: formatUserInputTranscript(pending.questions, result),
|
|
1915
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
1916
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
1917
|
+
});
|
|
1918
|
+
pending.resolve(result);
|
|
1919
|
+
scheduleLiveFrameRemount();
|
|
1920
|
+
};
|
|
1921
|
+
const openApprovalPrompt = (request) => new Promise((resolve) => {
|
|
1922
|
+
const deny = { kind: 'deny' };
|
|
1727
1923
|
if (exiting) {
|
|
1728
|
-
resolve(
|
|
1924
|
+
resolve(deny);
|
|
1729
1925
|
return;
|
|
1730
1926
|
}
|
|
1731
|
-
|
|
1927
|
+
resolvePermissionDecision = resolve;
|
|
1732
1928
|
store.update((current) => ({
|
|
1733
|
-
...current,
|
|
1734
|
-
approvalCursor: getDefaultApprovalCursor(),
|
|
1929
|
+
...pauseBusyClock(current, Date.now()),
|
|
1930
|
+
approvalCursor: getDefaultApprovalCursor(request.options.length),
|
|
1931
|
+
approvalOpenedAt: Date.now(),
|
|
1932
|
+
approvalScrollOffset: 0,
|
|
1735
1933
|
approvalPrompt: {
|
|
1736
|
-
title,
|
|
1737
|
-
body,
|
|
1738
|
-
diffPreview:
|
|
1739
|
-
? parseDiffPreview(
|
|
1934
|
+
title: request.title,
|
|
1935
|
+
body: request.body,
|
|
1936
|
+
diffPreview: request.diff && request.filePath
|
|
1937
|
+
? parseDiffPreview(request.diff)
|
|
1740
1938
|
: undefined,
|
|
1741
|
-
filePath:
|
|
1939
|
+
filePath: request.filePath,
|
|
1940
|
+
options: request.options,
|
|
1742
1941
|
returnStatus: current.status,
|
|
1743
1942
|
},
|
|
1744
|
-
status: title,
|
|
1943
|
+
status: request.title,
|
|
1745
1944
|
}));
|
|
1746
1945
|
});
|
|
1747
|
-
const handleInlineApprovalChoice = async (
|
|
1946
|
+
const handleInlineApprovalChoice = async (index) => {
|
|
1748
1947
|
const current = store.getState();
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1948
|
+
const options = current.approvalPrompt?.options ?? [];
|
|
1949
|
+
const chosen = options[index]?.decision ?? { kind: 'deny' };
|
|
1950
|
+
const pendingResolve = resolvePermissionDecision;
|
|
1951
|
+
resolvePermissionDecision = null;
|
|
1751
1952
|
store.update((next) => ({
|
|
1752
|
-
...next,
|
|
1753
|
-
approvalCursor: getDefaultApprovalCursor(),
|
|
1953
|
+
...resumeBusyClock(next, Date.now()),
|
|
1954
|
+
approvalCursor: getDefaultApprovalCursor(options.length),
|
|
1754
1955
|
approvalPrompt: null,
|
|
1956
|
+
approvalScrollOffset: 0,
|
|
1755
1957
|
status: current.approvalPrompt?.returnStatus ?? next.status,
|
|
1756
1958
|
}));
|
|
1757
|
-
pendingResolve?.(
|
|
1959
|
+
pendingResolve?.(chosen);
|
|
1758
1960
|
};
|
|
1759
1961
|
const refreshServerModels = async () => {
|
|
1760
1962
|
currentServerModels = await models.fetchServerModels({ config: authConfig });
|
|
@@ -1788,7 +1990,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1788
1990
|
selectedModelId: selected,
|
|
1789
1991
|
serverModels: fresh,
|
|
1790
1992
|
});
|
|
1791
|
-
await saveActiveSession()
|
|
1993
|
+
if (!(await saveActiveSession()))
|
|
1994
|
+
return;
|
|
1792
1995
|
syncShellStateFromSession();
|
|
1793
1996
|
appendStaticEntry({
|
|
1794
1997
|
body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
|
|
@@ -1813,7 +2016,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1813
2016
|
const id = jobId.trim();
|
|
1814
2017
|
await collectBackgroundJobUiOutputMutations({
|
|
1815
2018
|
session,
|
|
1816
|
-
projectIndex,
|
|
1817
2019
|
jobId: id,
|
|
1818
2020
|
});
|
|
1819
2021
|
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
@@ -1845,7 +2047,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1845
2047
|
return;
|
|
1846
2048
|
await collectBackgroundJobUiOutputMutations({
|
|
1847
2049
|
session,
|
|
1848
|
-
projectIndex,
|
|
1849
2050
|
jobId,
|
|
1850
2051
|
});
|
|
1851
2052
|
syncBackgroundJobsState();
|
|
@@ -1861,7 +2062,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1861
2062
|
const killed = await killBackgroundJob(jobId);
|
|
1862
2063
|
await collectBackgroundJobUiKillMutations({
|
|
1863
2064
|
session,
|
|
1864
|
-
projectIndex,
|
|
1865
2065
|
jobId,
|
|
1866
2066
|
result: killed,
|
|
1867
2067
|
});
|
|
@@ -1907,6 +2107,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1907
2107
|
await switchToModel(selected.id);
|
|
1908
2108
|
}
|
|
1909
2109
|
catch (error) {
|
|
2110
|
+
if (exitForAuthenticationError(error))
|
|
2111
|
+
return;
|
|
1910
2112
|
appendError(error.message);
|
|
1911
2113
|
}
|
|
1912
2114
|
};
|
|
@@ -1956,6 +2158,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1956
2158
|
store.update((next) => ({
|
|
1957
2159
|
...next,
|
|
1958
2160
|
busy: true,
|
|
2161
|
+
busyPausedAt: null,
|
|
1959
2162
|
busySince: resumeStartedAt,
|
|
1960
2163
|
clockNow: resumeStartedAt,
|
|
1961
2164
|
status: 'Loading session...',
|
|
@@ -1965,14 +2168,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1965
2168
|
applySessionSnapshot(session, snapshot);
|
|
1966
2169
|
setBackgroundJobSession(session.sessionId);
|
|
1967
2170
|
setScratchSession(session.sessionId);
|
|
2171
|
+
setImageStoreSession(session.sessionId);
|
|
1968
2172
|
syncBackgroundJobsState();
|
|
1969
2173
|
setTodoSession(session.sessionId);
|
|
1970
2174
|
syncTodosState();
|
|
1971
|
-
await saveActiveSession();
|
|
1972
2175
|
syncShellStateFromSession();
|
|
1973
2176
|
store.update((next) => ({
|
|
1974
2177
|
...next,
|
|
1975
2178
|
busy: false,
|
|
2179
|
+
busyPausedAt: null,
|
|
1976
2180
|
busySince: null,
|
|
1977
2181
|
resumePickerFilter: '',
|
|
1978
2182
|
resumePickerIndex: 0,
|
|
@@ -1991,9 +2195,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1991
2195
|
await remountTui();
|
|
1992
2196
|
}
|
|
1993
2197
|
catch (error) {
|
|
2198
|
+
if (exitForAuthenticationError(error))
|
|
2199
|
+
return;
|
|
1994
2200
|
store.update((next) => ({
|
|
1995
2201
|
...next,
|
|
1996
2202
|
busy: false,
|
|
2203
|
+
busyPausedAt: null,
|
|
1997
2204
|
busySince: null,
|
|
1998
2205
|
resumePickerFilter: '',
|
|
1999
2206
|
resumePickerIndex: 0,
|
|
@@ -2017,6 +2224,127 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2017
2224
|
}));
|
|
2018
2225
|
await handleSubmit(queued.body);
|
|
2019
2226
|
};
|
|
2227
|
+
let turnMessageCounter = 0;
|
|
2228
|
+
const upsertTurnMessage = (id, patch) => {
|
|
2229
|
+
store.update((current) => ({
|
|
2230
|
+
...current,
|
|
2231
|
+
turnMessages: current.turnMessages.map((message) => message.id === id ? { ...message, ...patch } : message),
|
|
2232
|
+
}));
|
|
2233
|
+
};
|
|
2234
|
+
const recoverUnacknowledgedMessages = (autoSubmits = true) => {
|
|
2235
|
+
if (unacknowledged.size === 0)
|
|
2236
|
+
return;
|
|
2237
|
+
const stranded = [...unacknowledged.values()];
|
|
2238
|
+
unacknowledged = new Map();
|
|
2239
|
+
for (const { row } of stranded) {
|
|
2240
|
+
upsertTurnMessage(row, {
|
|
2241
|
+
state: 'queued',
|
|
2242
|
+
note: autoSubmits
|
|
2243
|
+
? 'not read — sending as the next prompt'
|
|
2244
|
+
: 'not read — left in the composer',
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
const existing = store.getState().queuedMessage;
|
|
2248
|
+
const combined = [
|
|
2249
|
+
...stranded.map((entry) => entry.queued),
|
|
2250
|
+
...(existing ? [existing] : []),
|
|
2251
|
+
];
|
|
2252
|
+
const body = combined
|
|
2253
|
+
.map((entry) => entry.body)
|
|
2254
|
+
.filter((entry) => entry.trim())
|
|
2255
|
+
.join('\n\n');
|
|
2256
|
+
if (!body.trim())
|
|
2257
|
+
return;
|
|
2258
|
+
scheduleLiveFrameRemount();
|
|
2259
|
+
store.update((current) => ({
|
|
2260
|
+
...current,
|
|
2261
|
+
queuedMessage: {
|
|
2262
|
+
body,
|
|
2263
|
+
imageAttachments: combined.flatMap((entry) => entry.imageAttachments),
|
|
2264
|
+
pastedChunks: combined.flatMap((entry) => entry.pastedChunks),
|
|
2265
|
+
},
|
|
2266
|
+
}));
|
|
2267
|
+
};
|
|
2268
|
+
const fireQueuedMessage = async () => {
|
|
2269
|
+
const state = store.getState();
|
|
2270
|
+
const queued = state.queuedMessage;
|
|
2271
|
+
if (!queued || !state.busy || exiting)
|
|
2272
|
+
return;
|
|
2273
|
+
const text = (queued.pastedChunks.length
|
|
2274
|
+
? expandPastedChunks(queued.body, queued.pastedChunks)
|
|
2275
|
+
: queued.body).trim();
|
|
2276
|
+
const images = queued.imageAttachments.filter((attachment) => queued.body.includes(`[Image #${attachment.index}]`));
|
|
2277
|
+
if (!text && images.length === 0)
|
|
2278
|
+
return;
|
|
2279
|
+
const turnId = activeServerTurnId;
|
|
2280
|
+
const blockedReason = !turnId ? 'waiting for the turn to end' : null;
|
|
2281
|
+
const existingRow = blockedRows.get(queued);
|
|
2282
|
+
if (existingRow !== undefined) {
|
|
2283
|
+
upsertTurnMessage(existingRow, {
|
|
2284
|
+
state: 'queued',
|
|
2285
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2286
|
+
});
|
|
2287
|
+
if (blockedReason || !turnId)
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
const id = existingRow ?? ++turnMessageCounter;
|
|
2291
|
+
const messageId = `m${id}_${Date.now().toString(36)}`;
|
|
2292
|
+
if (existingRow === undefined) {
|
|
2293
|
+
scheduleLiveFrameRemount();
|
|
2294
|
+
store.update((current) => ({
|
|
2295
|
+
...current,
|
|
2296
|
+
turnMessages: [
|
|
2297
|
+
...current.turnMessages,
|
|
2298
|
+
{
|
|
2299
|
+
id,
|
|
2300
|
+
text,
|
|
2301
|
+
state: blockedReason ? 'queued' : 'sending',
|
|
2302
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2303
|
+
},
|
|
2304
|
+
],
|
|
2305
|
+
}));
|
|
2306
|
+
}
|
|
2307
|
+
if (blockedReason || !turnId) {
|
|
2308
|
+
blockedRows.set(queued, id);
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
blockedRows.delete(queued);
|
|
2312
|
+
unacknowledged.set(messageId, { row: id, queued });
|
|
2313
|
+
scheduleLiveFrameRemount();
|
|
2314
|
+
store.update((current) => current.queuedMessage === queued
|
|
2315
|
+
? { ...current, queuedMessage: null }
|
|
2316
|
+
: current);
|
|
2317
|
+
queueTurnEntry({
|
|
2318
|
+
body: text,
|
|
2319
|
+
kind: 'user',
|
|
2320
|
+
preformatted: queued.pastedChunks.length > 0,
|
|
2321
|
+
title: 'You · sent mid-turn',
|
|
2322
|
+
});
|
|
2323
|
+
const rollback = (note) => {
|
|
2324
|
+
if (!unacknowledged.delete(messageId))
|
|
2325
|
+
return;
|
|
2326
|
+
upsertTurnMessage(id, { state: 'queued', note });
|
|
2327
|
+
scheduleLiveFrameRemount();
|
|
2328
|
+
store.update((current) => current.queuedMessage ? current : { ...current, queuedMessage: queued });
|
|
2329
|
+
};
|
|
2330
|
+
let outcome;
|
|
2331
|
+
try {
|
|
2332
|
+
outcome = await chat.postInterjection({
|
|
2333
|
+
config: authConfig,
|
|
2334
|
+
turnId,
|
|
2335
|
+
text,
|
|
2336
|
+
messageId,
|
|
2337
|
+
imageAttachments: images,
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
catch (error) {
|
|
2341
|
+
rollback(`not sent — ${error?.message ?? 'request failed'}`);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
if (outcome === 'stale') {
|
|
2345
|
+
rollback('turn ended — sending as the next prompt');
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2020
2348
|
const handleSubmit = async (rawInput) => {
|
|
2021
2349
|
const chunks = store.getState().pastedChunks;
|
|
2022
2350
|
const expanded = chunks.length
|
|
@@ -2036,9 +2364,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2036
2364
|
return;
|
|
2037
2365
|
}
|
|
2038
2366
|
const pending = store.getState();
|
|
2367
|
+
const body = String(rawInput ?? '');
|
|
2039
2368
|
const snapshot = {
|
|
2040
|
-
body
|
|
2041
|
-
imageAttachments: pending.imageAttachments,
|
|
2369
|
+
body,
|
|
2370
|
+
imageAttachments: pending.imageAttachments.filter((attachment) => body.includes(`[Image #${attachment.index}]`)),
|
|
2042
2371
|
pastedChunks: pending.pastedChunks,
|
|
2043
2372
|
};
|
|
2044
2373
|
scheduleLiveFrameRemount();
|
|
@@ -2053,11 +2382,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2053
2382
|
}));
|
|
2054
2383
|
return;
|
|
2055
2384
|
}
|
|
2056
|
-
const imageAttachments = store
|
|
2385
|
+
const imageAttachments = store
|
|
2386
|
+
.getState()
|
|
2387
|
+
.imageAttachments.filter((attachment) => input.includes(`[Image #${attachment.index}]`));
|
|
2057
2388
|
scheduleLiveFrameRemount();
|
|
2058
2389
|
store.update((current) => ({
|
|
2059
2390
|
...current,
|
|
2060
2391
|
cursor: 0,
|
|
2392
|
+
imageAttachments: [],
|
|
2061
2393
|
input: '',
|
|
2062
2394
|
pastedChunks: [],
|
|
2063
2395
|
promptHistory: appendPromptToHistory(current.promptHistory, input),
|
|
@@ -2073,6 +2405,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2073
2405
|
requestExit();
|
|
2074
2406
|
return;
|
|
2075
2407
|
}
|
|
2408
|
+
if (input === '/logout') {
|
|
2409
|
+
store.update((current) => ({
|
|
2410
|
+
...current,
|
|
2411
|
+
busy: true,
|
|
2412
|
+
status: 'Signing out...',
|
|
2413
|
+
}));
|
|
2414
|
+
try {
|
|
2415
|
+
await logoutFromServer({ config: authConfig });
|
|
2416
|
+
}
|
|
2417
|
+
catch {
|
|
2418
|
+
}
|
|
2419
|
+
clearCliAuthConfig(session.env);
|
|
2420
|
+
signedOut = true;
|
|
2421
|
+
requestExit();
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2076
2424
|
if (input === '/help') {
|
|
2077
2425
|
appendStaticEntry({
|
|
2078
2426
|
body: formatInteractiveHelpText(),
|
|
@@ -2101,7 +2449,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2101
2449
|
const killed = await killBackgroundJob(jobId);
|
|
2102
2450
|
await collectBackgroundJobUiKillMutations({
|
|
2103
2451
|
session,
|
|
2104
|
-
projectIndex,
|
|
2105
2452
|
jobId,
|
|
2106
2453
|
result: killed,
|
|
2107
2454
|
});
|
|
@@ -2143,14 +2490,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2143
2490
|
});
|
|
2144
2491
|
}
|
|
2145
2492
|
catch (error) {
|
|
2493
|
+
if (exitForAuthenticationError(error))
|
|
2494
|
+
return;
|
|
2146
2495
|
appendError(error.message);
|
|
2147
2496
|
}
|
|
2148
2497
|
finally {
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2498
|
+
if (!exiting) {
|
|
2499
|
+
store.update((current) => ({
|
|
2500
|
+
...current,
|
|
2501
|
+
busy: false,
|
|
2502
|
+
status: 'Ready',
|
|
2503
|
+
}));
|
|
2504
|
+
}
|
|
2154
2505
|
}
|
|
2155
2506
|
return;
|
|
2156
2507
|
}
|
|
@@ -2164,14 +2515,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2164
2515
|
await openResumePicker();
|
|
2165
2516
|
}
|
|
2166
2517
|
catch (error) {
|
|
2518
|
+
if (exitForAuthenticationError(error))
|
|
2519
|
+
return;
|
|
2167
2520
|
appendError(error.message);
|
|
2168
2521
|
}
|
|
2169
2522
|
finally {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2523
|
+
if (!exiting) {
|
|
2524
|
+
store.update((current) => ({
|
|
2525
|
+
...current,
|
|
2526
|
+
busy: false,
|
|
2527
|
+
status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
|
|
2528
|
+
}));
|
|
2529
|
+
}
|
|
2175
2530
|
}
|
|
2176
2531
|
return;
|
|
2177
2532
|
}
|
|
@@ -2191,43 +2546,85 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2191
2546
|
}
|
|
2192
2547
|
}
|
|
2193
2548
|
catch (error) {
|
|
2549
|
+
if (exitForAuthenticationError(error))
|
|
2550
|
+
return;
|
|
2194
2551
|
appendError(error.message);
|
|
2195
2552
|
}
|
|
2196
2553
|
finally {
|
|
2554
|
+
if (!exiting) {
|
|
2555
|
+
store.update((current) => ({
|
|
2556
|
+
...current,
|
|
2557
|
+
busy: false,
|
|
2558
|
+
status: current.modelPickerOpen ? 'Select a model' : 'Ready',
|
|
2559
|
+
}));
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
if (input === '/new') {
|
|
2565
|
+
newConversationInFlight = true;
|
|
2566
|
+
store.update((current) => ({
|
|
2567
|
+
...current,
|
|
2568
|
+
busy: true,
|
|
2569
|
+
busyPausedAt: null,
|
|
2570
|
+
busySince: Date.now(),
|
|
2571
|
+
imageAttachments: [],
|
|
2572
|
+
queuedMessage: null,
|
|
2573
|
+
status: 'Starting a new conversation...',
|
|
2574
|
+
}));
|
|
2575
|
+
if (!(await saveActiveSessionOrAbort())) {
|
|
2576
|
+
newConversationInFlight = false;
|
|
2197
2577
|
store.update((current) => ({
|
|
2198
2578
|
...current,
|
|
2199
2579
|
busy: false,
|
|
2200
|
-
|
|
2580
|
+
busyPausedAt: null,
|
|
2581
|
+
busySince: null,
|
|
2582
|
+
status: 'Ready',
|
|
2201
2583
|
}));
|
|
2584
|
+
await flushQueuedMessage();
|
|
2585
|
+
return;
|
|
2202
2586
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2587
|
+
startNewConversation(session);
|
|
2588
|
+
setBackgroundJobSession(session.sessionId);
|
|
2589
|
+
setScratchSession(session.sessionId);
|
|
2590
|
+
setImageStoreSession(session.sessionId);
|
|
2591
|
+
setTodoSession(session.sessionId);
|
|
2207
2592
|
clearTodos();
|
|
2593
|
+
syncBackgroundJobsState();
|
|
2208
2594
|
syncTodosState();
|
|
2209
2595
|
latestUsageSummary = null;
|
|
2210
|
-
await saveActiveSession();
|
|
2211
2596
|
store.replaceTranscript([
|
|
2212
2597
|
{
|
|
2213
|
-
body: '
|
|
2598
|
+
body: 'Started a new conversation. The previous session remains saved.',
|
|
2214
2599
|
kind: 'system',
|
|
2215
|
-
title: '
|
|
2600
|
+
title: 'Session',
|
|
2216
2601
|
},
|
|
2217
2602
|
]);
|
|
2603
|
+
await saveActiveSession();
|
|
2218
2604
|
syncShellStateFromSession();
|
|
2219
|
-
|
|
2605
|
+
newConversationInFlight = false;
|
|
2606
|
+
store.update((current) => ({
|
|
2607
|
+
...current,
|
|
2608
|
+
busy: false,
|
|
2609
|
+
busyPausedAt: null,
|
|
2610
|
+
busySince: null,
|
|
2611
|
+
status: 'Ready',
|
|
2612
|
+
}));
|
|
2220
2613
|
await remountTui();
|
|
2614
|
+
await flushQueuedMessage();
|
|
2221
2615
|
return;
|
|
2222
2616
|
}
|
|
2223
2617
|
latestUsageSummary = null;
|
|
2224
2618
|
disarmExitConfirm();
|
|
2619
|
+
unacknowledged = new Map();
|
|
2225
2620
|
const turnStartedAt = Date.now();
|
|
2226
2621
|
const turnGeneration = ++activeTurnGeneration;
|
|
2227
2622
|
const turnAbort = new AbortController();
|
|
2228
2623
|
activeTurnAbort = turnAbort;
|
|
2229
2624
|
lastTurnStartedAt = turnStartedAt;
|
|
2230
2625
|
todosTouchedThisTurn = false;
|
|
2626
|
+
clearTodos();
|
|
2627
|
+
syncTodosState();
|
|
2231
2628
|
const userEntry = {
|
|
2232
2629
|
body: input,
|
|
2233
2630
|
kind: 'user',
|
|
@@ -2236,11 +2633,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2236
2633
|
};
|
|
2237
2634
|
pendingTurnEntries = [];
|
|
2238
2635
|
queueTurnEntry(userEntry);
|
|
2636
|
+
resetThinkingPacer();
|
|
2239
2637
|
store.update((current) => ({
|
|
2240
2638
|
...current,
|
|
2241
2639
|
activeTurnInput: input,
|
|
2242
2640
|
activeTurnInputPreformatted: preformatted,
|
|
2641
|
+
analyzingImages: 0,
|
|
2642
|
+
generatingImage: false,
|
|
2243
2643
|
busy: true,
|
|
2644
|
+
busyPausedAt: null,
|
|
2244
2645
|
busySince: turnStartedAt,
|
|
2245
2646
|
clockNow: turnStartedAt,
|
|
2246
2647
|
status: 'Running turn...',
|
|
@@ -2249,22 +2650,42 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2249
2650
|
transcriptScrollOffset: 0,
|
|
2250
2651
|
tokenUsage: formatClientTokenUsage(0, latestUsageSummary),
|
|
2251
2652
|
turnCounter: current.turnCounter + 1,
|
|
2653
|
+
turnMessages: [],
|
|
2252
2654
|
workingTools: [],
|
|
2253
2655
|
}));
|
|
2254
2656
|
try {
|
|
2255
2657
|
const result = await chat.sendServerUserMessage({
|
|
2256
2658
|
config: authConfig,
|
|
2257
|
-
projectIndex,
|
|
2258
2659
|
session,
|
|
2259
2660
|
input,
|
|
2260
2661
|
imageAttachments,
|
|
2261
2662
|
signal: turnAbort.signal,
|
|
2663
|
+
onTurnStart: (serverTurnId) => {
|
|
2664
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2665
|
+
return;
|
|
2666
|
+
activeServerTurnId = serverTurnId;
|
|
2667
|
+
},
|
|
2668
|
+
onInterjectionDelivered: (event) => {
|
|
2669
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2670
|
+
return;
|
|
2671
|
+
for (const messageId of event.messageIds ?? []) {
|
|
2672
|
+
const pending = unacknowledged.get(messageId);
|
|
2673
|
+
if (!pending)
|
|
2674
|
+
continue;
|
|
2675
|
+
unacknowledged.delete(messageId);
|
|
2676
|
+
upsertTurnMessage(pending.row, {
|
|
2677
|
+
state: 'delivered',
|
|
2678
|
+
note: undefined,
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
},
|
|
2262
2682
|
});
|
|
2263
2683
|
if (turnGeneration !== activeTurnGeneration)
|
|
2264
2684
|
return;
|
|
2265
2685
|
latestUsageSummary = result.usageSummary ?? null;
|
|
2266
2686
|
const turnEntries = takePendingTurnEntries();
|
|
2267
|
-
await saveActiveSession()
|
|
2687
|
+
if (!(await saveActiveSession()))
|
|
2688
|
+
return;
|
|
2268
2689
|
syncShellStateFromSession();
|
|
2269
2690
|
store.update((current) => ({
|
|
2270
2691
|
...current,
|
|
@@ -2301,10 +2722,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2301
2722
|
store.update((current) => ({
|
|
2302
2723
|
...current,
|
|
2303
2724
|
busy: false,
|
|
2725
|
+
busyPausedAt: null,
|
|
2304
2726
|
busySince: null,
|
|
2305
2727
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2306
2728
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
2307
2729
|
}));
|
|
2730
|
+
recoverUnacknowledgedMessages();
|
|
2308
2731
|
await flushQueuedMessage();
|
|
2309
2732
|
}
|
|
2310
2733
|
catch (error) {
|
|
@@ -2315,11 +2738,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2315
2738
|
return;
|
|
2316
2739
|
}
|
|
2317
2740
|
const cancelled = isTurnCancelledError(error);
|
|
2741
|
+
if (exitForAuthenticationError(error))
|
|
2742
|
+
return;
|
|
2318
2743
|
store.update((current) => ({
|
|
2319
2744
|
...current,
|
|
2320
2745
|
activeTurnInput: '',
|
|
2321
2746
|
activeTurnInputPreformatted: false,
|
|
2322
2747
|
busy: false,
|
|
2748
|
+
busyPausedAt: null,
|
|
2323
2749
|
busySince: null,
|
|
2324
2750
|
commandLog: [],
|
|
2325
2751
|
imageAttachments: [],
|
|
@@ -2339,13 +2765,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2339
2765
|
title: 'System',
|
|
2340
2766
|
},
|
|
2341
2767
|
]);
|
|
2342
|
-
await saveActiveSession()
|
|
2768
|
+
if (!(await saveActiveSession()))
|
|
2769
|
+
return;
|
|
2343
2770
|
}
|
|
2344
2771
|
else {
|
|
2345
2772
|
appendStaticEntries(turnEntries);
|
|
2346
2773
|
appendError(error.message);
|
|
2347
2774
|
}
|
|
2348
2775
|
await remountTui();
|
|
2776
|
+
recoverUnacknowledgedMessages(!cancelled);
|
|
2349
2777
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2350
2778
|
await flushQueuedMessage();
|
|
2351
2779
|
}
|
|
@@ -2356,24 +2784,42 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2356
2784
|
}
|
|
2357
2785
|
if (turnGeneration === activeTurnGeneration) {
|
|
2358
2786
|
lastTurnStartedAt = null;
|
|
2787
|
+
activeServerTurnId = null;
|
|
2359
2788
|
}
|
|
2789
|
+
recoverUnacknowledgedMessages();
|
|
2360
2790
|
}
|
|
2361
2791
|
};
|
|
2792
|
+
session.onImageAnalysis = (activeImageCount) => {
|
|
2793
|
+
store.update((current) => ({
|
|
2794
|
+
...current,
|
|
2795
|
+
analyzingImages: Math.max(0, activeImageCount),
|
|
2796
|
+
generatingImage: activeImageCount > 0 ? false : current.generatingImage,
|
|
2797
|
+
}));
|
|
2798
|
+
};
|
|
2799
|
+
session.onImageGeneration = (active) => {
|
|
2800
|
+
store.update((current) => ({
|
|
2801
|
+
...current,
|
|
2802
|
+
generatingImage: Boolean(active),
|
|
2803
|
+
analyzingImages: active ? 0 : current.analyzingImages,
|
|
2804
|
+
}));
|
|
2805
|
+
};
|
|
2362
2806
|
session.onStatus = (message) => {
|
|
2363
2807
|
const panel = thinkingPanelFromStatus(message);
|
|
2364
2808
|
store.update((current) => ({
|
|
2365
2809
|
...current,
|
|
2366
2810
|
status: message,
|
|
2367
|
-
|
|
2368
|
-
thinkingNotes: panel?.notes ?? current.thinkingNotes,
|
|
2369
|
-
tokenUsage: formatClientTokenUsage(current.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
|
|
2811
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2370
2812
|
}));
|
|
2813
|
+
queueThinkingUpdate({
|
|
2814
|
+
title: panel ? panel.title : liveStatusPanelTitle(message),
|
|
2815
|
+
notes: panel ? panel.notes : [],
|
|
2816
|
+
});
|
|
2371
2817
|
};
|
|
2372
2818
|
session.onContextLog = (message) => {
|
|
2373
2819
|
store.update((current) => ({
|
|
2374
2820
|
...current,
|
|
2375
2821
|
contextStatus: message,
|
|
2376
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2822
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2377
2823
|
}));
|
|
2378
2824
|
};
|
|
2379
2825
|
session.onToolEvent = (event) => {
|
|
@@ -2439,54 +2885,46 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2439
2885
|
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2440
2886
|
}
|
|
2441
2887
|
});
|
|
2442
|
-
projectIndex.onStatus = session.onStatus;
|
|
2443
|
-
projectIndex.onContextLog = session.onContextLog;
|
|
2444
2888
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
2445
|
-
session.
|
|
2889
|
+
session.requestUserInput = async (request, signal) => openUserInputPrompt(request.questions, signal);
|
|
2890
|
+
session.requestPermission = async (request) => {
|
|
2891
|
+
const deny = { kind: 'deny' };
|
|
2446
2892
|
if (exiting)
|
|
2447
|
-
return
|
|
2893
|
+
return deny;
|
|
2448
2894
|
if (sessionAutoYes)
|
|
2449
|
-
return
|
|
2450
|
-
const
|
|
2451
|
-
if (
|
|
2452
|
-
setAgentMode('auto-accept');
|
|
2453
|
-
syncShellStateFromSession();
|
|
2895
|
+
return { kind: 'once' };
|
|
2896
|
+
const decision = await openApprovalPrompt(request);
|
|
2897
|
+
if (decision.kind === 'always-bucket') {
|
|
2454
2898
|
appendTurnAwareEntry({
|
|
2455
|
-
body:
|
|
2899
|
+
body: `Allowed for the rest of this session: ${bucketActionLabel(request.bucket)}.`,
|
|
2456
2900
|
kind: 'system',
|
|
2457
2901
|
title: 'Approvals',
|
|
2458
2902
|
});
|
|
2459
|
-
return true;
|
|
2460
2903
|
}
|
|
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();
|
|
2904
|
+
else if (decision.kind === 'always-prefix') {
|
|
2475
2905
|
appendTurnAwareEntry({
|
|
2476
|
-
body:
|
|
2906
|
+
body: `Allowed for the rest of this session: commands starting with \`${decision.prefix}\`.`,
|
|
2477
2907
|
kind: 'system',
|
|
2478
2908
|
title: 'Approvals',
|
|
2479
2909
|
});
|
|
2480
|
-
return true;
|
|
2481
2910
|
}
|
|
2482
|
-
return
|
|
2911
|
+
return decision;
|
|
2483
2912
|
};
|
|
2484
2913
|
const shellInputHandlers = {
|
|
2914
|
+
getApprovalScrollLimit: () => {
|
|
2915
|
+
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2916
|
+
return approvalScrollLimit(store.getState().approvalPrompt, contentWidth, terminalRows);
|
|
2917
|
+
},
|
|
2485
2918
|
getTranscriptScrollLimit: () => {
|
|
2486
2919
|
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2487
2920
|
const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
2488
2921
|
return blocks.reduce((total, block, index) => total + block.length + (index > 0 ? 1 : 0), 0);
|
|
2489
2922
|
},
|
|
2923
|
+
getUserInputViewport: () => {
|
|
2924
|
+
const nowMs = Date.now();
|
|
2925
|
+
const state = store.getState();
|
|
2926
|
+
return userInputViewportForFrame(state, terminalCols, terminalRows, spinnerFrame, busyElapsedSeconds(state, nowMs), nowMs);
|
|
2927
|
+
},
|
|
2490
2928
|
onCancelTurn: cancelActiveTurn,
|
|
2491
2929
|
onCycleAgentMode: cycleAgentMode,
|
|
2492
2930
|
onCtrlC: handleCtrlC,
|
|
@@ -2495,6 +2933,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2495
2933
|
onLiveFrameShapeChange: scheduleLiveFrameRemount,
|
|
2496
2934
|
onRequestExit: requestExit,
|
|
2497
2935
|
onResolveApproval: handleInlineApprovalChoice,
|
|
2936
|
+
onResolveUserInput: handleInlineUserInput,
|
|
2498
2937
|
onResumeSession: handleInlineResumeSelection,
|
|
2499
2938
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2500
2939
|
onSelectModel: handleInlineModelSelection,
|
|
@@ -2502,6 +2941,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2502
2941
|
onJobsPickerKill: handleJobsPickerKill,
|
|
2503
2942
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2504
2943
|
onSubmit: handleSubmit,
|
|
2944
|
+
onFireQueuedMessage: fireQueuedMessage,
|
|
2505
2945
|
};
|
|
2506
2946
|
const unsubscribe = store.subscribe(() => {
|
|
2507
2947
|
renderCurrentFrame();
|
|
@@ -2509,6 +2949,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2509
2949
|
const spinnerTimer = setInterval(() => {
|
|
2510
2950
|
spinnerFrame = (spinnerFrame + 1) % 6;
|
|
2511
2951
|
if (store.getState().busy) {
|
|
2952
|
+
flushPendingThinking();
|
|
2512
2953
|
renderCurrentFrame();
|
|
2513
2954
|
}
|
|
2514
2955
|
}, 120);
|
|
@@ -2564,10 +3005,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2564
3005
|
killAllBackgroundJobs();
|
|
2565
3006
|
setBackgroundJobSession(null);
|
|
2566
3007
|
setScratchSession(null);
|
|
3008
|
+
setImageStoreSession(null);
|
|
2567
3009
|
setTodoSession(null);
|
|
2568
3010
|
setBackgroundJobUpdateHook(null);
|
|
2569
3011
|
await bridge.close();
|
|
3012
|
+
releaseTerminalWrites();
|
|
2570
3013
|
setCommandOutputHook(null);
|
|
2571
3014
|
}
|
|
2572
3015
|
});
|
|
3016
|
+
if (fatalError)
|
|
3017
|
+
throw fatalError;
|
|
3018
|
+
return { signedOut };
|
|
2573
3019
|
}
|