@thegitai/cli 1.0.0-preview.4 → 1.0.0-preview.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -4
- package/dist/bin/ai.js +119 -111
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +11 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +286 -44
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +127 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +232 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +209 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +104 -0
- package/dist/src/cli-args.js +19 -5
- package/dist/src/client-environment.js +2 -0
- 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 +246 -0
- package/dist/src/edit-journal.js +33 -2
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +58 -17
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +19 -4
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +12 -46
- package/dist/src/tools/browser.js +628 -0
- package/dist/src/tools/delete-file.js +20 -4
- package/dist/src/tools/index.js +32 -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/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +802 -195
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +517 -165
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +227 -67
- 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 +20 -8
- 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,19 +1,26 @@
|
|
|
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';
|
|
7
|
+
import { composerInputWidth, frameContentWidth } from './tui/composer-layout.js';
|
|
5
8
|
import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
6
9
|
import { chat, models } from '../api/index.js';
|
|
7
10
|
import { isTurnCancelledError } from '../api/chat.js';
|
|
8
11
|
import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
|
|
9
12
|
import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
10
13
|
import { setScratchSession } from '../scratch-dir.js';
|
|
14
|
+
import { setImageStoreSession } from '../core/session-image-store.js';
|
|
11
15
|
import { cancelActiveCommand } from '../executor.js';
|
|
12
16
|
import { isTurnFailureMarker } from '../turn-failure-marker.js';
|
|
17
|
+
import { clearCliAuthConfig, logoutFromServer, } from '../api/auth.js';
|
|
18
|
+
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
19
|
+
import { createUserInputPromptState, formatUserInputTranscript, } from './tui/user-input.js';
|
|
13
20
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
14
21
|
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
15
|
-
import {
|
|
16
|
-
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
|
|
22
|
+
import { startNewConversation, } from '../session.js';
|
|
23
|
+
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../session-store.js';
|
|
17
24
|
import { truncate } from '../utils.js';
|
|
18
25
|
import { writeClipboardText } from '../core/clipboard.js';
|
|
19
26
|
import { openUrl } from '../core/open-url.js';
|
|
@@ -70,23 +77,35 @@ const WORKING_TOOL_PREVIEW_ITEMS = 4;
|
|
|
70
77
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
71
78
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
72
79
|
const AGENT_MODE_LABEL_WIDTH = 16;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
];
|
|
80
|
+
export const REPORT_ISSUE_URL = 'https://thegit.ai/report';
|
|
81
|
+
export function buildReportTranscriptEntries(openedUrl) {
|
|
82
|
+
return openedUrl
|
|
83
|
+
? [
|
|
84
|
+
{
|
|
85
|
+
body: `Opening the private bug report form in your browser. If nothing opened, visit:\n\n${REPORT_ISSUE_URL}\n`,
|
|
86
|
+
kind: 'system',
|
|
87
|
+
title: 'Report',
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
: [
|
|
91
|
+
{
|
|
92
|
+
body: `Could not open a browser. Report the bug manually at:\n\n${REPORT_ISSUE_URL}\n`,
|
|
93
|
+
kind: 'error',
|
|
94
|
+
title: 'Report',
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
export function buildTurnFailureHintEntries(cancelled) {
|
|
99
|
+
if (cancelled)
|
|
100
|
+
return [];
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
body: 'Something went wrong? Use /report to submit the issue.',
|
|
104
|
+
kind: 'error',
|
|
105
|
+
title: 'Report',
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
}
|
|
90
109
|
export const SLASH_COMMANDS = [
|
|
91
110
|
{
|
|
92
111
|
command: '/help',
|
|
@@ -96,6 +115,10 @@ export const SLASH_COMMANDS = [
|
|
|
96
115
|
command: '/about',
|
|
97
116
|
description: 'Show version and platform info',
|
|
98
117
|
},
|
|
118
|
+
{
|
|
119
|
+
command: '/report',
|
|
120
|
+
description: 'Report a bug privately on the TheGitAI website',
|
|
121
|
+
},
|
|
99
122
|
{
|
|
100
123
|
command: '/usage',
|
|
101
124
|
description: 'Show account usage percentage and reset times',
|
|
@@ -113,14 +136,29 @@ export const SLASH_COMMANDS = [
|
|
|
113
136
|
description: 'Background jobs: pick to view output or kill',
|
|
114
137
|
},
|
|
115
138
|
{
|
|
116
|
-
command: '/
|
|
117
|
-
description: '
|
|
139
|
+
command: '/new',
|
|
140
|
+
description: 'start a new conversation; this session remains saved',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
command: '/logout',
|
|
144
|
+
description: 'Sign out and quit',
|
|
118
145
|
},
|
|
119
146
|
{
|
|
120
147
|
command: '/exit',
|
|
121
148
|
description: 'Quit the current session',
|
|
122
149
|
},
|
|
123
150
|
];
|
|
151
|
+
export function composerAttachmentsAfterCancel(queuedAttachments, inFlightAttachments) {
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const kept = [];
|
|
154
|
+
for (const attachment of [...queuedAttachments, ...inFlightAttachments]) {
|
|
155
|
+
if (seen.has(attachment.index))
|
|
156
|
+
continue;
|
|
157
|
+
seen.add(attachment.index);
|
|
158
|
+
kept.push(attachment);
|
|
159
|
+
}
|
|
160
|
+
return kept;
|
|
161
|
+
}
|
|
124
162
|
function getShellWidth(columns) {
|
|
125
163
|
const safeColumns = Math.max(columns, 20);
|
|
126
164
|
const targetWidth = Math.floor(safeColumns * TUI_WIDTH_RATIO);
|
|
@@ -489,11 +527,43 @@ function buildWriteFileDiff(content) {
|
|
|
489
527
|
return '';
|
|
490
528
|
return `@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}`).join('\n')}`;
|
|
491
529
|
}
|
|
530
|
+
const MAX_DELETE_PREVIEW_LINES = 400;
|
|
492
531
|
function buildDeleteFileDiff(content) {
|
|
493
532
|
const lines = splitDiffLines(String(content ?? ''));
|
|
494
|
-
if (lines.length === 0)
|
|
533
|
+
if (lines.length === 0) {
|
|
534
|
+
return { removed: 0, text: '' };
|
|
535
|
+
}
|
|
536
|
+
const shown = lines.slice(0, MAX_DELETE_PREVIEW_LINES);
|
|
537
|
+
const rows = [
|
|
538
|
+
`@@ -1,${lines.length} +0,0 @@`,
|
|
539
|
+
...shown.map((line) => `-${line}`),
|
|
540
|
+
];
|
|
541
|
+
const omitted = lines.length - shown.length;
|
|
542
|
+
if (omitted > 0) {
|
|
543
|
+
rows.push(`@@ ${omitted} more removed line(s) not shown @@`);
|
|
544
|
+
}
|
|
545
|
+
return { removed: lines.length, text: rows.join('\n') };
|
|
546
|
+
}
|
|
547
|
+
function deletedFileSummary(result) {
|
|
548
|
+
const bytes = typeof result?.bytes === 'number' ? result.bytes : null;
|
|
549
|
+
if (bytes === null) {
|
|
495
550
|
return '';
|
|
496
|
-
|
|
551
|
+
}
|
|
552
|
+
const reason = result?.contentOmitted === 'binary' ? ', binary' : '';
|
|
553
|
+
return ` (${formatFileSize(bytes)}${reason})`;
|
|
554
|
+
}
|
|
555
|
+
function formatFileSize(bytes) {
|
|
556
|
+
if (bytes < 1024) {
|
|
557
|
+
return `${bytes} B`;
|
|
558
|
+
}
|
|
559
|
+
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
|
|
560
|
+
let value = bytes / 1024;
|
|
561
|
+
let unit = 0;
|
|
562
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
563
|
+
value /= 1024;
|
|
564
|
+
unit += 1;
|
|
565
|
+
}
|
|
566
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
497
567
|
}
|
|
498
568
|
function isFileChangeTool(name) {
|
|
499
569
|
return (name === 'patch_file' ||
|
|
@@ -515,6 +585,8 @@ function formatToolResultState(result) {
|
|
|
515
585
|
return 'Blocked';
|
|
516
586
|
if (result?.ok === true)
|
|
517
587
|
return 'OK';
|
|
588
|
+
if (Number.isInteger(result?.httpStatus))
|
|
589
|
+
return String(result.httpStatus);
|
|
518
590
|
return 'Failed';
|
|
519
591
|
}
|
|
520
592
|
function formatRunCommandResultState(result) {
|
|
@@ -619,9 +691,11 @@ function buildFileChangeEntry(event) {
|
|
|
619
691
|
};
|
|
620
692
|
}
|
|
621
693
|
const content = typeof result?.content === 'string' ? result.content : '';
|
|
622
|
-
const
|
|
623
|
-
const preview =
|
|
624
|
-
const summary = preview
|
|
694
|
+
const diff = buildDeleteFileDiff(content);
|
|
695
|
+
const preview = diff.text ? parseDiffPreview(diff.text) : undefined;
|
|
696
|
+
const summary = preview
|
|
697
|
+
? ` (+0 -${diff.removed})`
|
|
698
|
+
: deletedFileSummary(result);
|
|
625
699
|
return {
|
|
626
700
|
body: '',
|
|
627
701
|
diffPreview: preview,
|
|
@@ -671,7 +745,7 @@ function buildFileChangeEntry(event) {
|
|
|
671
745
|
title: `Edited ${filePath}${summary}`,
|
|
672
746
|
};
|
|
673
747
|
}
|
|
674
|
-
function buildWorkingToolEntry(event) {
|
|
748
|
+
export function buildWorkingToolEntry(event) {
|
|
675
749
|
const { call, result } = event;
|
|
676
750
|
const error = typeof result?.error === 'string' && result.error.trim()
|
|
677
751
|
? `\n${truncate(result.error.trim(), 180)}`
|
|
@@ -784,10 +858,18 @@ function displayUserTextFromHistoryEntry(entry) {
|
|
|
784
858
|
export function buildTranscriptFromSessionHistory(history) {
|
|
785
859
|
const entries = [];
|
|
786
860
|
const pendingCalls = new Map();
|
|
861
|
+
const pendingUserInputRequests = new Map();
|
|
787
862
|
for (const entry of history) {
|
|
788
863
|
if (!entry?.parts?.length)
|
|
789
864
|
continue;
|
|
790
865
|
for (const part of entry.parts) {
|
|
866
|
+
const userInputRequest = part?.userInputRequest;
|
|
867
|
+
if (userInputRequest && typeof userInputRequest === 'object') {
|
|
868
|
+
const requestId = String(userInputRequest.requestId ?? '').trim();
|
|
869
|
+
if (requestId && Array.isArray(userInputRequest.questions)) {
|
|
870
|
+
pendingUserInputRequests.set(requestId, userInputRequest.questions);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
791
873
|
const call = part?.functionCall;
|
|
792
874
|
if (!call || typeof call !== 'object')
|
|
793
875
|
continue;
|
|
@@ -797,6 +879,21 @@ export function buildTranscriptFromSessionHistory(history) {
|
|
|
797
879
|
pendingCalls.set(callId, call);
|
|
798
880
|
}
|
|
799
881
|
for (const part of entry.parts) {
|
|
882
|
+
const userInputResult = part?.userInputResult;
|
|
883
|
+
if (userInputResult && typeof userInputResult === 'object') {
|
|
884
|
+
const requestId = String(userInputResult.requestId ?? '').trim();
|
|
885
|
+
const questions = pendingUserInputRequests.get(requestId);
|
|
886
|
+
const result = userInputResult.result;
|
|
887
|
+
if (questions &&
|
|
888
|
+
(result?.status === 'submitted' || result?.status === 'cancelled')) {
|
|
889
|
+
pendingUserInputRequests.delete(requestId);
|
|
890
|
+
entries.push({
|
|
891
|
+
body: formatUserInputTranscript(questions, result),
|
|
892
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
893
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
800
897
|
const functionResponse = part?.functionResponse;
|
|
801
898
|
if (!functionResponse || typeof functionResponse !== 'object')
|
|
802
899
|
continue;
|
|
@@ -825,6 +922,13 @@ export function buildTranscriptFromSessionHistory(history) {
|
|
|
825
922
|
}
|
|
826
923
|
continue;
|
|
827
924
|
}
|
|
925
|
+
if (entry.role === 'user' && entry.kind === 'userInterjection') {
|
|
926
|
+
const text = String(entry.userInput ?? '').trim();
|
|
927
|
+
if (text) {
|
|
928
|
+
entries.push({ body: text, kind: 'user', title: 'You · sent mid-turn' });
|
|
929
|
+
}
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
828
932
|
const text = textFromHistoryEntry(entry);
|
|
829
933
|
if ((entry.role === 'model' || entry.role === 'assistant') && text) {
|
|
830
934
|
if (isTurnFailureMarker(text)) {
|
|
@@ -853,6 +957,23 @@ function createSessionTranscript(session) {
|
|
|
853
957
|
const transcript = buildTranscriptFromSessionHistory(session.history);
|
|
854
958
|
return transcript.length ? transcript : createInitialTranscript();
|
|
855
959
|
}
|
|
960
|
+
export function isJobsPickerCommand(input) {
|
|
961
|
+
return String(input ?? '').trim() === '/jobs';
|
|
962
|
+
}
|
|
963
|
+
export function statusBehindJobsPicker(state) {
|
|
964
|
+
return state.jobsPickerOpen
|
|
965
|
+
? (state.jobsPickerReturnStatus ?? 'Ready')
|
|
966
|
+
: state.status;
|
|
967
|
+
}
|
|
968
|
+
export function closedJobsPickerFields(state) {
|
|
969
|
+
if (!state.jobsPickerOpen)
|
|
970
|
+
return {};
|
|
971
|
+
return {
|
|
972
|
+
jobsPickerExpandedId: null,
|
|
973
|
+
jobsPickerOpen: false,
|
|
974
|
+
jobsPickerReturnStatus: null,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
856
977
|
function createShellStore(initialState) {
|
|
857
978
|
let state = initialState;
|
|
858
979
|
let nextEntryId = 1;
|
|
@@ -960,11 +1081,15 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
960
1081
|
activeTurnInput: '',
|
|
961
1082
|
activeTurnInputPreformatted: false,
|
|
962
1083
|
agentMode: session.agentMode,
|
|
963
|
-
|
|
1084
|
+
analyzingImages: 0,
|
|
1085
|
+
generatingImage: false,
|
|
1086
|
+
approvalCursor: 0,
|
|
964
1087
|
approvalPrompt: null,
|
|
1088
|
+
approvalScrollOffset: 0,
|
|
965
1089
|
autoYes: session.autoYes,
|
|
966
1090
|
backgroundJobs: [],
|
|
967
1091
|
busy: false,
|
|
1092
|
+
busyPausedAt: null,
|
|
968
1093
|
busySince: null,
|
|
969
1094
|
clockNow: Date.now(),
|
|
970
1095
|
commandCursor: 0,
|
|
@@ -992,6 +1117,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
992
1117
|
resumePickerSessions: [],
|
|
993
1118
|
transcriptScrollOffset: 0,
|
|
994
1119
|
queuedMessage: null,
|
|
1120
|
+
turnMessages: [],
|
|
995
1121
|
serverModels: serverModels.models,
|
|
996
1122
|
sudoPrompt: null,
|
|
997
1123
|
status: 'Ready',
|
|
@@ -1002,6 +1128,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
1002
1128
|
tokenUsage: formatClientTokenUsage(null),
|
|
1003
1129
|
transcript: [],
|
|
1004
1130
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
1131
|
+
userInputPrompt: null,
|
|
1005
1132
|
pastedChunks: [],
|
|
1006
1133
|
imageAttachments: [],
|
|
1007
1134
|
workingTools: [],
|
|
@@ -1018,7 +1145,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1018
1145
|
const reasoningTokens = usageSummary?.reasoningTokens ?? 0;
|
|
1019
1146
|
const cacheTokens = usageSummary?.cacheTokens ?? 0;
|
|
1020
1147
|
const cacheWriteTokens = usageSummary?.cacheWriteTokens ?? 0;
|
|
1021
|
-
const indexTokens = usageSummary?.indexTokens ?? 0;
|
|
1022
1148
|
return [
|
|
1023
1149
|
'Session tokens',
|
|
1024
1150
|
`in ${formatTokenCount(inputTokens)}`,
|
|
@@ -1026,7 +1152,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1026
1152
|
...(reasoningTokens > 0 ? [`think ${formatTokenCount(reasoningTokens)}`] : []),
|
|
1027
1153
|
`cache ${formatTokenCount(cacheTokens)}`,
|
|
1028
1154
|
`write ${formatTokenCount(cacheWriteTokens)}`,
|
|
1029
|
-
`index ${formatTokenCount(indexTokens)}`,
|
|
1030
1155
|
].join(' • ');
|
|
1031
1156
|
}
|
|
1032
1157
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
@@ -1069,6 +1194,24 @@ function splitThinkingLines(text) {
|
|
|
1069
1194
|
.map((part) => part.trim())
|
|
1070
1195
|
.filter(Boolean));
|
|
1071
1196
|
}
|
|
1197
|
+
const LIVE_STATUS_PANEL_MAX_CHARS = 72;
|
|
1198
|
+
const TOOL_PROGRESS_STATUS_PATTERN = /^(?:Tool:\s|Running\s\S+(?:\slocally)?\.\.\.$)/i;
|
|
1199
|
+
export function isToolProgressStatus(status) {
|
|
1200
|
+
return TOOL_PROGRESS_STATUS_PATTERN.test(String(status ?? '').trim());
|
|
1201
|
+
}
|
|
1202
|
+
function holdOrPickFallback(currentTitle) {
|
|
1203
|
+
return THINKING_FALLBACK_PHRASES.includes(currentTitle.trim())
|
|
1204
|
+
? currentTitle
|
|
1205
|
+
: pickThinkingFallbackPhrase();
|
|
1206
|
+
}
|
|
1207
|
+
function liveStatusPanelTitle(status) {
|
|
1208
|
+
const text = String(status ?? '').trim();
|
|
1209
|
+
if (!text || text.includes('\n'))
|
|
1210
|
+
return '';
|
|
1211
|
+
if (isToolProgressStatus(text))
|
|
1212
|
+
return '';
|
|
1213
|
+
return text.length <= LIVE_STATUS_PANEL_MAX_CHARS ? text : '';
|
|
1214
|
+
}
|
|
1072
1215
|
function thinkingPanelFromStatus(status) {
|
|
1073
1216
|
const rawText = thinkingNoteFromStatus(status);
|
|
1074
1217
|
if (!rawText)
|
|
@@ -1092,7 +1235,7 @@ function thinkingPanelFromStatus(status) {
|
|
|
1092
1235
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
1093
1236
|
return {
|
|
1094
1237
|
title,
|
|
1095
|
-
notes: splitThinkingLines(bodyLines.join('\n')).slice(
|
|
1238
|
+
notes: splitThinkingLines(bodyLines.join('\n')).slice(0, 3),
|
|
1096
1239
|
};
|
|
1097
1240
|
}
|
|
1098
1241
|
export const EXIT_CTRL_C_CONFIRM_MESSAGE = 'Press Ctrl+C again to quit.';
|
|
@@ -1222,26 +1365,37 @@ export function navigatePromptHistory(state, direction) {
|
|
|
1222
1365
|
promptHistoryCursor: nextCursor,
|
|
1223
1366
|
};
|
|
1224
1367
|
}
|
|
1225
|
-
function getDefaultApprovalCursor() {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1368
|
+
export function getDefaultApprovalCursor(optionCount) {
|
|
1369
|
+
return Math.max(0, optionCount - 1);
|
|
1370
|
+
}
|
|
1371
|
+
export function getNextApprovalCursor(currentIndex, direction, optionCount) {
|
|
1372
|
+
if (optionCount <= 0)
|
|
1373
|
+
return 0;
|
|
1374
|
+
return (currentIndex + direction + optionCount) % optionCount;
|
|
1375
|
+
}
|
|
1376
|
+
export function pauseBusyClock(state, nowMs) {
|
|
1377
|
+
if (state.busySince === null || state.busyPausedAt !== null)
|
|
1378
|
+
return state;
|
|
1379
|
+
return { ...state, busyPausedAt: nowMs };
|
|
1380
|
+
}
|
|
1381
|
+
export function resumeBusyClock(state, nowMs) {
|
|
1382
|
+
if (state.busyPausedAt === null)
|
|
1383
|
+
return state;
|
|
1384
|
+
return {
|
|
1385
|
+
...state,
|
|
1386
|
+
busyPausedAt: null,
|
|
1387
|
+
busySince: state.busySince === null
|
|
1388
|
+
? null
|
|
1389
|
+
: state.busySince + Math.max(0, nowMs - state.busyPausedAt),
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
export function busyElapsedMs(state, nowMs) {
|
|
1393
|
+
if (state.busySince === null)
|
|
1394
|
+
return null;
|
|
1395
|
+
return Math.max(0, (state.busyPausedAt ?? nowMs) - state.busySince);
|
|
1396
|
+
}
|
|
1397
|
+
export function busyElapsedSeconds(state, nowMs) {
|
|
1398
|
+
return Math.floor((busyElapsedMs(state, nowMs) ?? 0) / 1000);
|
|
1245
1399
|
}
|
|
1246
1400
|
export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
1247
1401
|
return serverModels.map((model) => ({
|
|
@@ -1279,30 +1433,37 @@ function appendCommandLog(state, text) {
|
|
|
1279
1433
|
};
|
|
1280
1434
|
}
|
|
1281
1435
|
async function saveSessionBoth({ serverSessionClient, session, }) {
|
|
1436
|
+
if (!sessionHasUserMessage(session))
|
|
1437
|
+
return;
|
|
1282
1438
|
saveSessionState(session);
|
|
1283
1439
|
await serverSessionClient.save(session);
|
|
1284
1440
|
}
|
|
1285
|
-
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi,
|
|
1441
|
+
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
1286
1442
|
if (process.stdin.isTTY !== true) {
|
|
1287
1443
|
throw new Error('stdin is not a terminal');
|
|
1288
1444
|
}
|
|
1289
1445
|
if (process.stdout.isTTY !== true) {
|
|
1290
1446
|
throw new Error('stdout is not a terminal');
|
|
1291
1447
|
}
|
|
1448
|
+
let fatalError = null;
|
|
1449
|
+
let signedOut = false;
|
|
1292
1450
|
await withTuiMode(async () => {
|
|
1293
1451
|
setBackgroundJobSession(session.sessionId);
|
|
1294
1452
|
setScratchSession(session.sessionId);
|
|
1453
|
+
setImageStoreSession(session.sessionId);
|
|
1295
1454
|
setTodoSession(session.sessionId);
|
|
1296
1455
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1297
1456
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1298
1457
|
let currentServerModels = serverModels;
|
|
1299
1458
|
let sessionAutoYes = session.autoYes;
|
|
1300
1459
|
let resolveDone = null;
|
|
1301
|
-
let
|
|
1460
|
+
let resolvePermissionDecision = null;
|
|
1302
1461
|
let resolveSudoPassword = null;
|
|
1462
|
+
let pendingUserInput = null;
|
|
1303
1463
|
let cleanupSudoPasswordPrompt = null;
|
|
1304
1464
|
let sudoPasswordBuffer = '';
|
|
1305
1465
|
const bridge = createRatatuiBridge();
|
|
1466
|
+
captureTerminalWrites();
|
|
1306
1467
|
const { handleShellKeyEvent } = await import('./tui/shell-input.js');
|
|
1307
1468
|
let terminalCols = 80;
|
|
1308
1469
|
let terminalRows = 24;
|
|
@@ -1315,6 +1476,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1315
1476
|
let latestUsageSummary = null;
|
|
1316
1477
|
let pendingTurnEntries = [];
|
|
1317
1478
|
let activeTurnAbort = null;
|
|
1479
|
+
let activeTurnImageAttachments = [];
|
|
1480
|
+
let activeServerTurnId = null;
|
|
1481
|
+
let unacknowledged = new Map();
|
|
1482
|
+
const blockedRows = new WeakMap();
|
|
1483
|
+
let newConversationInFlight = false;
|
|
1318
1484
|
let todosTouchedThisTurn = false;
|
|
1319
1485
|
const syncTodosState = () => {
|
|
1320
1486
|
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
@@ -1323,6 +1489,36 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1323
1489
|
let exitCtrlCArmed = false;
|
|
1324
1490
|
let exitCtrlCTimer = null;
|
|
1325
1491
|
let transientStatusTimer = null;
|
|
1492
|
+
const THINKING_MIN_DWELL_MS = 3_000;
|
|
1493
|
+
let thinkingAppliedAt = 0;
|
|
1494
|
+
let pendingThinking = null;
|
|
1495
|
+
const applyThinking = (next) => {
|
|
1496
|
+
thinkingAppliedAt = Date.now();
|
|
1497
|
+
pendingThinking = null;
|
|
1498
|
+
store.update((current) => ({
|
|
1499
|
+
...current,
|
|
1500
|
+
thinkingTitle: next.title || holdOrPickFallback(current.thinkingTitle),
|
|
1501
|
+
thinkingNotes: next.notes,
|
|
1502
|
+
}));
|
|
1503
|
+
};
|
|
1504
|
+
const queueThinkingUpdate = (next) => {
|
|
1505
|
+
if (Date.now() - thinkingAppliedAt >= THINKING_MIN_DWELL_MS) {
|
|
1506
|
+
applyThinking(next);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
pendingThinking = next;
|
|
1510
|
+
};
|
|
1511
|
+
const flushPendingThinking = () => {
|
|
1512
|
+
if (!pendingThinking)
|
|
1513
|
+
return;
|
|
1514
|
+
if (Date.now() - thinkingAppliedAt < THINKING_MIN_DWELL_MS)
|
|
1515
|
+
return;
|
|
1516
|
+
applyThinking(pendingThinking);
|
|
1517
|
+
};
|
|
1518
|
+
const resetThinkingPacer = () => {
|
|
1519
|
+
thinkingAppliedAt = 0;
|
|
1520
|
+
pendingThinking = null;
|
|
1521
|
+
};
|
|
1326
1522
|
const done = new Promise((resolve) => {
|
|
1327
1523
|
resolveDone = resolve;
|
|
1328
1524
|
});
|
|
@@ -1347,7 +1543,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1347
1543
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1348
1544
|
}, 2500);
|
|
1349
1545
|
};
|
|
1350
|
-
const terminalTitle = createTerminalTitleController(
|
|
1546
|
+
const terminalTitle = createTerminalTitleController({
|
|
1547
|
+
write: (title) => bridge.setTitle(title),
|
|
1548
|
+
});
|
|
1351
1549
|
const syncTerminalTitle = () => {
|
|
1352
1550
|
const state = store.getState();
|
|
1353
1551
|
terminalTitle.sync({
|
|
@@ -1360,9 +1558,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1360
1558
|
return;
|
|
1361
1559
|
const state = store.getState();
|
|
1362
1560
|
syncTerminalTitle();
|
|
1363
|
-
const elapsedSeconds = state.
|
|
1364
|
-
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1365
|
-
: 0;
|
|
1561
|
+
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1366
1562
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1367
1563
|
};
|
|
1368
1564
|
const remountTui = async () => {
|
|
@@ -1488,15 +1684,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1488
1684
|
}
|
|
1489
1685
|
};
|
|
1490
1686
|
const dismissPendingApproval = () => {
|
|
1491
|
-
if (!
|
|
1687
|
+
if (!resolvePermissionDecision)
|
|
1492
1688
|
return;
|
|
1493
|
-
const pendingResolve =
|
|
1494
|
-
|
|
1495
|
-
pendingResolve('
|
|
1689
|
+
const pendingResolve = resolvePermissionDecision;
|
|
1690
|
+
resolvePermissionDecision = null;
|
|
1691
|
+
pendingResolve({ kind: 'deny' });
|
|
1496
1692
|
store.update((current) => ({
|
|
1497
|
-
...current,
|
|
1498
|
-
approvalCursor:
|
|
1693
|
+
...resumeBusyClock(current, Date.now()),
|
|
1694
|
+
approvalCursor: 0,
|
|
1499
1695
|
approvalPrompt: null,
|
|
1696
|
+
approvalScrollOffset: 0,
|
|
1500
1697
|
}));
|
|
1501
1698
|
};
|
|
1502
1699
|
const dismissPendingSudoPassword = () => {
|
|
@@ -1509,21 +1706,40 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1509
1706
|
sudoPasswordBuffer = '';
|
|
1510
1707
|
pendingResolve(null);
|
|
1511
1708
|
store.update((current) => ({
|
|
1512
|
-
...current,
|
|
1709
|
+
...resumeBusyClock(current, Date.now()),
|
|
1513
1710
|
sudoPrompt: null,
|
|
1514
1711
|
}));
|
|
1515
1712
|
};
|
|
1713
|
+
const dismissPendingUserInput = () => {
|
|
1714
|
+
const pending = pendingUserInput;
|
|
1715
|
+
if (!pending)
|
|
1716
|
+
return;
|
|
1717
|
+
pendingUserInput = null;
|
|
1718
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1719
|
+
const error = new Error('Turn cancelled.');
|
|
1720
|
+
error.name = 'AbortError';
|
|
1721
|
+
pending.reject(error);
|
|
1722
|
+
store.update((current) => ({
|
|
1723
|
+
...resumeBusyClock(current, Date.now()),
|
|
1724
|
+
status: current.userInputPrompt?.returnStatus ?? current.status,
|
|
1725
|
+
userInputPrompt: null,
|
|
1726
|
+
}));
|
|
1727
|
+
};
|
|
1516
1728
|
const cancelActiveTurn = () => {
|
|
1517
|
-
if (!store.getState().busy)
|
|
1729
|
+
if (!store.getState().busy || newConversationInFlight)
|
|
1518
1730
|
return;
|
|
1519
1731
|
disarmExitConfirm();
|
|
1520
1732
|
dismissPendingApproval();
|
|
1521
1733
|
dismissPendingSudoPassword();
|
|
1734
|
+
dismissPendingUserInput();
|
|
1522
1735
|
activeTurnGeneration += 1;
|
|
1736
|
+
resetThinkingPacer();
|
|
1523
1737
|
activeTurnAbort?.abort();
|
|
1524
1738
|
activeTurnAbort = null;
|
|
1525
1739
|
cancelActiveCommand();
|
|
1526
1740
|
const queued = store.getState().queuedMessage;
|
|
1741
|
+
const restoredAttachments = composerAttachmentsAfterCancel(queued ? queued.imageAttachments : [], activeTurnImageAttachments);
|
|
1742
|
+
activeTurnImageAttachments = [];
|
|
1527
1743
|
const cancelledEntries = [
|
|
1528
1744
|
...takePendingTurnEntries(),
|
|
1529
1745
|
{
|
|
@@ -1537,10 +1753,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1537
1753
|
activeTurnInput: '',
|
|
1538
1754
|
activeTurnInputPreformatted: false,
|
|
1539
1755
|
busy: false,
|
|
1756
|
+
busyPausedAt: null,
|
|
1540
1757
|
busySince: null,
|
|
1541
1758
|
commandLog: [],
|
|
1542
1759
|
cursor: queued ? queued.body.length : current.cursor,
|
|
1543
|
-
imageAttachments:
|
|
1760
|
+
imageAttachments: restoredAttachments,
|
|
1544
1761
|
input: queued ? queued.body : current.input,
|
|
1545
1762
|
pastedChunks: queued ? queued.pastedChunks : current.pastedChunks,
|
|
1546
1763
|
promptHistoryCursor: queued ? null : current.promptHistoryCursor,
|
|
@@ -1549,8 +1766,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1549
1766
|
status: 'Ready',
|
|
1550
1767
|
thinkingTitle: '',
|
|
1551
1768
|
thinkingNotes: [],
|
|
1769
|
+
turnMessages: [],
|
|
1552
1770
|
workingTools: [],
|
|
1553
|
-
tokenUsage: formatClientTokenUsage(current
|
|
1771
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1554
1772
|
}));
|
|
1555
1773
|
appendStaticEntries(cancelledEntries);
|
|
1556
1774
|
lastTurnStartedAt = null;
|
|
@@ -1558,14 +1776,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1558
1776
|
scheduleLiveFrameRemount();
|
|
1559
1777
|
void remountTui();
|
|
1560
1778
|
};
|
|
1561
|
-
const saveActiveSession = async () => {
|
|
1562
|
-
try {
|
|
1563
|
-
await saveSessionBoth({ serverSessionClient, session });
|
|
1564
|
-
}
|
|
1565
|
-
catch (error) {
|
|
1566
|
-
appendError(`Session save failed: ${error.message}`);
|
|
1567
|
-
}
|
|
1568
|
-
};
|
|
1569
1779
|
const syncShellStateFromSession = () => {
|
|
1570
1780
|
store.update((current) => ({
|
|
1571
1781
|
...current,
|
|
@@ -1644,6 +1854,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1644
1854
|
}
|
|
1645
1855
|
dismissPendingApproval();
|
|
1646
1856
|
dismissPendingSudoPassword();
|
|
1857
|
+
dismissPendingUserInput();
|
|
1647
1858
|
exiting = true;
|
|
1648
1859
|
killAllBackgroundJobs();
|
|
1649
1860
|
store.update((current) => ({
|
|
@@ -1653,9 +1864,45 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1653
1864
|
status: 'Exiting...',
|
|
1654
1865
|
}));
|
|
1655
1866
|
void bridge.close().then(() => {
|
|
1867
|
+
releaseTerminalWrites();
|
|
1656
1868
|
resolveDone?.();
|
|
1657
1869
|
});
|
|
1658
1870
|
};
|
|
1871
|
+
const exitForAuthenticationError = (error) => {
|
|
1872
|
+
if (!isAuthenticationError(error))
|
|
1873
|
+
return false;
|
|
1874
|
+
clearCliAuthConfig(session.env);
|
|
1875
|
+
if (sessionHasUserMessage(session)) {
|
|
1876
|
+
saveSessionState(session);
|
|
1877
|
+
}
|
|
1878
|
+
fatalError = new Error(authenticationErrorMessage(error));
|
|
1879
|
+
requestExit();
|
|
1880
|
+
return true;
|
|
1881
|
+
};
|
|
1882
|
+
const saveActiveSession = async () => {
|
|
1883
|
+
try {
|
|
1884
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1885
|
+
return true;
|
|
1886
|
+
}
|
|
1887
|
+
catch (error) {
|
|
1888
|
+
if (exitForAuthenticationError(error))
|
|
1889
|
+
return false;
|
|
1890
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
const saveActiveSessionOrAbort = async () => {
|
|
1895
|
+
try {
|
|
1896
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1897
|
+
return true;
|
|
1898
|
+
}
|
|
1899
|
+
catch (error) {
|
|
1900
|
+
if (exitForAuthenticationError(error))
|
|
1901
|
+
return false;
|
|
1902
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1659
1906
|
const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
|
|
1660
1907
|
if (exiting || signal?.aborted) {
|
|
1661
1908
|
resolve(null);
|
|
@@ -1673,7 +1920,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1673
1920
|
cleanupSudoPasswordPrompt = null;
|
|
1674
1921
|
}
|
|
1675
1922
|
store.update((current) => ({
|
|
1676
|
-
...current,
|
|
1923
|
+
...pauseBusyClock(current, Date.now()),
|
|
1677
1924
|
sudoPrompt: {
|
|
1678
1925
|
command,
|
|
1679
1926
|
passwordLength: 0,
|
|
@@ -1693,7 +1940,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1693
1940
|
cleanupSudoPasswordPrompt = null;
|
|
1694
1941
|
sudoPasswordBuffer = '';
|
|
1695
1942
|
store.update((current) => ({
|
|
1696
|
-
...current,
|
|
1943
|
+
...resumeBusyClock(current, Date.now()),
|
|
1697
1944
|
sudoPrompt: null,
|
|
1698
1945
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1699
1946
|
}));
|
|
@@ -1707,7 +1954,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1707
1954
|
cleanupSudoPasswordPrompt = null;
|
|
1708
1955
|
sudoPasswordBuffer = '';
|
|
1709
1956
|
store.update((current) => ({
|
|
1710
|
-
...current,
|
|
1957
|
+
...resumeBusyClock(current, Date.now()),
|
|
1711
1958
|
sudoPrompt: null,
|
|
1712
1959
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1713
1960
|
}));
|
|
@@ -1727,38 +1974,90 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1727
1974
|
: null,
|
|
1728
1975
|
}));
|
|
1729
1976
|
};
|
|
1730
|
-
const
|
|
1977
|
+
const openUserInputPrompt = (questions, signal) => new Promise((resolve, reject) => {
|
|
1978
|
+
if (exiting || signal?.aborted) {
|
|
1979
|
+
const error = new Error('Turn cancelled.');
|
|
1980
|
+
error.name = 'AbortError';
|
|
1981
|
+
reject(error);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const onAbort = () => dismissPendingUserInput();
|
|
1985
|
+
pendingUserInput = {
|
|
1986
|
+
questions,
|
|
1987
|
+
resolve,
|
|
1988
|
+
reject,
|
|
1989
|
+
signal,
|
|
1990
|
+
onAbort,
|
|
1991
|
+
};
|
|
1992
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1993
|
+
store.update((current) => ({
|
|
1994
|
+
...pauseBusyClock(current, Date.now()),
|
|
1995
|
+
...closedJobsPickerFields(current),
|
|
1996
|
+
status: 'Waiting for your input',
|
|
1997
|
+
userInputPrompt: createUserInputPromptState(questions, statusBehindJobsPicker(current)),
|
|
1998
|
+
}));
|
|
1999
|
+
scheduleLiveFrameRemount();
|
|
2000
|
+
});
|
|
2001
|
+
const handleInlineUserInput = async (result) => {
|
|
2002
|
+
const pending = pendingUserInput;
|
|
2003
|
+
const prompt = store.getState().userInputPrompt;
|
|
2004
|
+
if (!pending || !prompt)
|
|
2005
|
+
return;
|
|
2006
|
+
pendingUserInput = null;
|
|
2007
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
2008
|
+
store.update((current) => ({
|
|
2009
|
+
...resumeBusyClock(current, Date.now()),
|
|
2010
|
+
status: prompt.returnStatus,
|
|
2011
|
+
userInputPrompt: null,
|
|
2012
|
+
}));
|
|
2013
|
+
appendTurnAwareEntry({
|
|
2014
|
+
body: formatUserInputTranscript(pending.questions, result),
|
|
2015
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
2016
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
2017
|
+
});
|
|
2018
|
+
pending.resolve(result);
|
|
2019
|
+
scheduleLiveFrameRemount();
|
|
2020
|
+
};
|
|
2021
|
+
const openApprovalPrompt = (request) => new Promise((resolve) => {
|
|
2022
|
+
const deny = { kind: 'deny' };
|
|
1731
2023
|
if (exiting) {
|
|
1732
|
-
resolve(
|
|
2024
|
+
resolve(deny);
|
|
1733
2025
|
return;
|
|
1734
2026
|
}
|
|
1735
|
-
|
|
2027
|
+
resolvePermissionDecision = resolve;
|
|
1736
2028
|
store.update((current) => ({
|
|
1737
|
-
...current,
|
|
1738
|
-
|
|
2029
|
+
...pauseBusyClock(current, Date.now()),
|
|
2030
|
+
...closedJobsPickerFields(current),
|
|
2031
|
+
approvalCursor: getDefaultApprovalCursor(request.options.length),
|
|
2032
|
+
approvalOpenedAt: Date.now(),
|
|
2033
|
+
approvalScrollOffset: 0,
|
|
1739
2034
|
approvalPrompt: {
|
|
1740
|
-
title,
|
|
1741
|
-
body,
|
|
1742
|
-
diffPreview:
|
|
1743
|
-
? parseDiffPreview(
|
|
2035
|
+
title: request.title,
|
|
2036
|
+
body: request.body,
|
|
2037
|
+
diffPreview: request.diff && request.filePath
|
|
2038
|
+
? parseDiffPreview(request.diff)
|
|
1744
2039
|
: undefined,
|
|
1745
|
-
filePath:
|
|
1746
|
-
|
|
2040
|
+
filePath: request.filePath,
|
|
2041
|
+
options: request.options,
|
|
2042
|
+
returnStatus: statusBehindJobsPicker(current),
|
|
1747
2043
|
},
|
|
1748
|
-
status: title,
|
|
2044
|
+
status: request.title,
|
|
1749
2045
|
}));
|
|
1750
2046
|
});
|
|
1751
|
-
const handleInlineApprovalChoice = async (
|
|
2047
|
+
const handleInlineApprovalChoice = async (index) => {
|
|
1752
2048
|
const current = store.getState();
|
|
1753
|
-
const
|
|
1754
|
-
|
|
2049
|
+
const options = current.approvalPrompt?.options ?? [];
|
|
2050
|
+
const chosen = options[index]?.decision ?? { kind: 'deny' };
|
|
2051
|
+
const pendingResolve = resolvePermissionDecision;
|
|
2052
|
+
resolvePermissionDecision = null;
|
|
1755
2053
|
store.update((next) => ({
|
|
1756
|
-
...next,
|
|
1757
|
-
approvalCursor: getDefaultApprovalCursor(),
|
|
2054
|
+
...resumeBusyClock(next, Date.now()),
|
|
2055
|
+
approvalCursor: getDefaultApprovalCursor(options.length),
|
|
1758
2056
|
approvalPrompt: null,
|
|
2057
|
+
approvalScrollOffset: 0,
|
|
1759
2058
|
status: current.approvalPrompt?.returnStatus ?? next.status,
|
|
1760
2059
|
}));
|
|
1761
|
-
pendingResolve?.(
|
|
2060
|
+
pendingResolve?.(chosen);
|
|
1762
2061
|
};
|
|
1763
2062
|
const refreshServerModels = async () => {
|
|
1764
2063
|
currentServerModels = await models.fetchServerModels({ config: authConfig });
|
|
@@ -1792,7 +2091,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1792
2091
|
selectedModelId: selected,
|
|
1793
2092
|
serverModels: fresh,
|
|
1794
2093
|
});
|
|
1795
|
-
await saveActiveSession()
|
|
2094
|
+
if (!(await saveActiveSession()))
|
|
2095
|
+
return;
|
|
1796
2096
|
syncShellStateFromSession();
|
|
1797
2097
|
appendStaticEntry({
|
|
1798
2098
|
body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
|
|
@@ -1801,23 +2101,24 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1801
2101
|
});
|
|
1802
2102
|
};
|
|
1803
2103
|
const openJobsPicker = () => {
|
|
2104
|
+
if (store.getState().jobsPickerOpen)
|
|
2105
|
+
return;
|
|
1804
2106
|
syncBackgroundJobsState();
|
|
1805
2107
|
store.update((current) => ({
|
|
1806
2108
|
...current,
|
|
1807
2109
|
commandCursor: 0,
|
|
1808
|
-
cursor: 0,
|
|
1809
|
-
input: '',
|
|
1810
2110
|
jobsPickerExpandedId: null,
|
|
1811
2111
|
jobsPickerIndex: 0,
|
|
1812
2112
|
jobsPickerOpen: true,
|
|
2113
|
+
jobsPickerReturnStatus: current.status,
|
|
1813
2114
|
status: 'Background jobs',
|
|
1814
2115
|
}));
|
|
2116
|
+
scheduleLiveFrameRemount();
|
|
1815
2117
|
};
|
|
1816
2118
|
const appendJobOutputEntry = async (jobId) => {
|
|
1817
2119
|
const id = jobId.trim();
|
|
1818
2120
|
await collectBackgroundJobUiOutputMutations({
|
|
1819
2121
|
session,
|
|
1820
|
-
projectIndex,
|
|
1821
2122
|
jobId: id,
|
|
1822
2123
|
});
|
|
1823
2124
|
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
@@ -1843,32 +2144,51 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1843
2144
|
const index = Math.min(Math.max(current.jobsPickerIndex, 0), current.backgroundJobs.length - 1);
|
|
1844
2145
|
return current.backgroundJobs[index]?.id ?? null;
|
|
1845
2146
|
};
|
|
2147
|
+
const deferredJobMutationIds = new Set();
|
|
2148
|
+
const drainDeferredJobMutations = async () => {
|
|
2149
|
+
if (deferredJobMutationIds.size === 0)
|
|
2150
|
+
return;
|
|
2151
|
+
const pending = [...deferredJobMutationIds];
|
|
2152
|
+
deferredJobMutationIds.clear();
|
|
2153
|
+
for (const jobId of pending) {
|
|
2154
|
+
await collectBackgroundJobUiOutputMutations({ session, jobId });
|
|
2155
|
+
}
|
|
2156
|
+
syncBackgroundJobsState();
|
|
2157
|
+
};
|
|
1846
2158
|
const handleJobsPickerOutput = async () => {
|
|
1847
2159
|
const jobId = selectedJobsPickerId();
|
|
1848
2160
|
if (!jobId)
|
|
1849
2161
|
return;
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2162
|
+
if (store.getState().busy) {
|
|
2163
|
+
deferredJobMutationIds.add(jobId);
|
|
2164
|
+
}
|
|
2165
|
+
else {
|
|
2166
|
+
await collectBackgroundJobUiOutputMutations({
|
|
2167
|
+
session,
|
|
2168
|
+
jobId,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
1856
2171
|
store.update((current) => ({
|
|
1857
2172
|
...current,
|
|
1858
2173
|
jobsPickerExpandedId: current.jobsPickerExpandedId === jobId ? null : jobId,
|
|
1859
2174
|
}));
|
|
2175
|
+
syncBackgroundJobsState();
|
|
1860
2176
|
};
|
|
1861
2177
|
const handleJobsPickerKill = async () => {
|
|
1862
2178
|
const jobId = selectedJobsPickerId();
|
|
1863
2179
|
if (!jobId)
|
|
1864
2180
|
return;
|
|
1865
2181
|
const killed = await killBackgroundJob(jobId);
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2182
|
+
if (store.getState().busy) {
|
|
2183
|
+
deferredJobMutationIds.add(jobId);
|
|
2184
|
+
}
|
|
2185
|
+
else {
|
|
2186
|
+
await collectBackgroundJobUiKillMutations({
|
|
2187
|
+
session,
|
|
2188
|
+
jobId,
|
|
2189
|
+
result: killed,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
1872
2192
|
syncBackgroundJobsState();
|
|
1873
2193
|
if (!killed.ok) {
|
|
1874
2194
|
appendError(killed.error ?? 'Background job kill failed.');
|
|
@@ -1911,6 +2231,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1911
2231
|
await switchToModel(selected.id);
|
|
1912
2232
|
}
|
|
1913
2233
|
catch (error) {
|
|
2234
|
+
if (exitForAuthenticationError(error))
|
|
2235
|
+
return;
|
|
1914
2236
|
appendError(error.message);
|
|
1915
2237
|
}
|
|
1916
2238
|
};
|
|
@@ -1960,15 +2282,24 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1960
2282
|
store.update((next) => ({
|
|
1961
2283
|
...next,
|
|
1962
2284
|
busy: true,
|
|
2285
|
+
busyPausedAt: null,
|
|
1963
2286
|
busySince: resumeStartedAt,
|
|
1964
2287
|
clockNow: resumeStartedAt,
|
|
1965
2288
|
status: 'Loading session...',
|
|
1966
2289
|
}));
|
|
1967
2290
|
try {
|
|
1968
2291
|
const snapshot = await loadInteractiveSession(selected.id);
|
|
1969
|
-
|
|
2292
|
+
const requestedModelId = Number(snapshot.modelId);
|
|
2293
|
+
const knownModels = currentServerModels.models.some((model) => model.id === requestedModelId)
|
|
2294
|
+
? currentServerModels
|
|
2295
|
+
: await refreshServerModels();
|
|
2296
|
+
const { modelId: resolvedModelId, fellBack } = models.resolveResumedSessionModel(requestedModelId, knownModels);
|
|
2297
|
+
applySessionSnapshot(session, snapshot, {
|
|
2298
|
+
modelOverride: fellBack ? resolvedModelId : undefined,
|
|
2299
|
+
});
|
|
1970
2300
|
setBackgroundJobSession(session.sessionId);
|
|
1971
2301
|
setScratchSession(session.sessionId);
|
|
2302
|
+
setImageStoreSession(session.sessionId);
|
|
1972
2303
|
syncBackgroundJobsState();
|
|
1973
2304
|
setTodoSession(session.sessionId);
|
|
1974
2305
|
syncTodosState();
|
|
@@ -1976,6 +2307,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1976
2307
|
store.update((next) => ({
|
|
1977
2308
|
...next,
|
|
1978
2309
|
busy: false,
|
|
2310
|
+
busyPausedAt: null,
|
|
1979
2311
|
busySince: null,
|
|
1980
2312
|
resumePickerFilter: '',
|
|
1981
2313
|
resumePickerIndex: 0,
|
|
@@ -1989,14 +2321,26 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1989
2321
|
kind: 'system',
|
|
1990
2322
|
title: 'Session',
|
|
1991
2323
|
},
|
|
2324
|
+
...(fellBack
|
|
2325
|
+
? [
|
|
2326
|
+
{
|
|
2327
|
+
body: `This session's model is no longer available — switched to ${formatModelLabel(resolvedModelId, knownModels.models)}. Conversation history preserved.`,
|
|
2328
|
+
kind: 'system',
|
|
2329
|
+
title: 'Model',
|
|
2330
|
+
},
|
|
2331
|
+
]
|
|
2332
|
+
: []),
|
|
1992
2333
|
...buildTranscriptFromSessionHistory(session.history),
|
|
1993
2334
|
]);
|
|
1994
2335
|
await remountTui();
|
|
1995
2336
|
}
|
|
1996
2337
|
catch (error) {
|
|
2338
|
+
if (exitForAuthenticationError(error))
|
|
2339
|
+
return;
|
|
1997
2340
|
store.update((next) => ({
|
|
1998
2341
|
...next,
|
|
1999
2342
|
busy: false,
|
|
2343
|
+
busyPausedAt: null,
|
|
2000
2344
|
busySince: null,
|
|
2001
2345
|
resumePickerFilter: '',
|
|
2002
2346
|
resumePickerIndex: 0,
|
|
@@ -2020,6 +2364,127 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2020
2364
|
}));
|
|
2021
2365
|
await handleSubmit(queued.body);
|
|
2022
2366
|
};
|
|
2367
|
+
let turnMessageCounter = 0;
|
|
2368
|
+
const upsertTurnMessage = (id, patch) => {
|
|
2369
|
+
store.update((current) => ({
|
|
2370
|
+
...current,
|
|
2371
|
+
turnMessages: current.turnMessages.map((message) => message.id === id ? { ...message, ...patch } : message),
|
|
2372
|
+
}));
|
|
2373
|
+
};
|
|
2374
|
+
const recoverUnacknowledgedMessages = (autoSubmits = true) => {
|
|
2375
|
+
if (unacknowledged.size === 0)
|
|
2376
|
+
return;
|
|
2377
|
+
const stranded = [...unacknowledged.values()];
|
|
2378
|
+
unacknowledged = new Map();
|
|
2379
|
+
for (const { row } of stranded) {
|
|
2380
|
+
upsertTurnMessage(row, {
|
|
2381
|
+
state: 'queued',
|
|
2382
|
+
note: autoSubmits
|
|
2383
|
+
? 'not read — sending as the next prompt'
|
|
2384
|
+
: 'not read — left in the composer',
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
const existing = store.getState().queuedMessage;
|
|
2388
|
+
const combined = [
|
|
2389
|
+
...stranded.map((entry) => entry.queued),
|
|
2390
|
+
...(existing ? [existing] : []),
|
|
2391
|
+
];
|
|
2392
|
+
const body = combined
|
|
2393
|
+
.map((entry) => entry.body)
|
|
2394
|
+
.filter((entry) => entry.trim())
|
|
2395
|
+
.join('\n\n');
|
|
2396
|
+
if (!body.trim())
|
|
2397
|
+
return;
|
|
2398
|
+
scheduleLiveFrameRemount();
|
|
2399
|
+
store.update((current) => ({
|
|
2400
|
+
...current,
|
|
2401
|
+
queuedMessage: {
|
|
2402
|
+
body,
|
|
2403
|
+
imageAttachments: combined.flatMap((entry) => entry.imageAttachments),
|
|
2404
|
+
pastedChunks: combined.flatMap((entry) => entry.pastedChunks),
|
|
2405
|
+
},
|
|
2406
|
+
}));
|
|
2407
|
+
};
|
|
2408
|
+
const fireQueuedMessage = async () => {
|
|
2409
|
+
const state = store.getState();
|
|
2410
|
+
const queued = state.queuedMessage;
|
|
2411
|
+
if (!queued || !state.busy || exiting)
|
|
2412
|
+
return;
|
|
2413
|
+
const text = (queued.pastedChunks.length
|
|
2414
|
+
? expandPastedChunks(queued.body, queued.pastedChunks)
|
|
2415
|
+
: queued.body).trim();
|
|
2416
|
+
const images = queued.imageAttachments.filter((attachment) => queued.body.includes(`[Image #${attachment.index}]`));
|
|
2417
|
+
if (!text && images.length === 0)
|
|
2418
|
+
return;
|
|
2419
|
+
const turnId = activeServerTurnId;
|
|
2420
|
+
const blockedReason = !turnId ? 'waiting for the turn to end' : null;
|
|
2421
|
+
const existingRow = blockedRows.get(queued);
|
|
2422
|
+
if (existingRow !== undefined) {
|
|
2423
|
+
upsertTurnMessage(existingRow, {
|
|
2424
|
+
state: 'queued',
|
|
2425
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2426
|
+
});
|
|
2427
|
+
if (blockedReason || !turnId)
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
const id = existingRow ?? ++turnMessageCounter;
|
|
2431
|
+
const messageId = `m${id}_${Date.now().toString(36)}`;
|
|
2432
|
+
if (existingRow === undefined) {
|
|
2433
|
+
scheduleLiveFrameRemount();
|
|
2434
|
+
store.update((current) => ({
|
|
2435
|
+
...current,
|
|
2436
|
+
turnMessages: [
|
|
2437
|
+
...current.turnMessages,
|
|
2438
|
+
{
|
|
2439
|
+
id,
|
|
2440
|
+
text,
|
|
2441
|
+
state: blockedReason ? 'queued' : 'sending',
|
|
2442
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2443
|
+
},
|
|
2444
|
+
],
|
|
2445
|
+
}));
|
|
2446
|
+
}
|
|
2447
|
+
if (blockedReason || !turnId) {
|
|
2448
|
+
blockedRows.set(queued, id);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
blockedRows.delete(queued);
|
|
2452
|
+
unacknowledged.set(messageId, { row: id, queued });
|
|
2453
|
+
scheduleLiveFrameRemount();
|
|
2454
|
+
store.update((current) => current.queuedMessage === queued
|
|
2455
|
+
? { ...current, queuedMessage: null }
|
|
2456
|
+
: current);
|
|
2457
|
+
queueTurnEntry({
|
|
2458
|
+
body: text,
|
|
2459
|
+
kind: 'user',
|
|
2460
|
+
preformatted: queued.pastedChunks.length > 0,
|
|
2461
|
+
title: 'You · sent mid-turn',
|
|
2462
|
+
});
|
|
2463
|
+
const rollback = (note) => {
|
|
2464
|
+
if (!unacknowledged.delete(messageId))
|
|
2465
|
+
return;
|
|
2466
|
+
upsertTurnMessage(id, { state: 'queued', note });
|
|
2467
|
+
scheduleLiveFrameRemount();
|
|
2468
|
+
store.update((current) => current.queuedMessage ? current : { ...current, queuedMessage: queued });
|
|
2469
|
+
};
|
|
2470
|
+
let outcome;
|
|
2471
|
+
try {
|
|
2472
|
+
outcome = await chat.postInterjection({
|
|
2473
|
+
config: authConfig,
|
|
2474
|
+
turnId,
|
|
2475
|
+
text,
|
|
2476
|
+
messageId,
|
|
2477
|
+
imageAttachments: images,
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
catch (error) {
|
|
2481
|
+
rollback(`not sent — ${error?.message ?? 'request failed'}`);
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
if (outcome === 'stale') {
|
|
2485
|
+
rollback('turn ended — sending as the next prompt');
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2023
2488
|
const handleSubmit = async (rawInput) => {
|
|
2024
2489
|
const chunks = store.getState().pastedChunks;
|
|
2025
2490
|
const expanded = chunks.length
|
|
@@ -2031,17 +2496,28 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2031
2496
|
if (!input || exiting)
|
|
2032
2497
|
return;
|
|
2033
2498
|
if (input.startsWith('/')) {
|
|
2499
|
+
if (isJobsPickerCommand(input)) {
|
|
2500
|
+
store.update((current) => ({
|
|
2501
|
+
...current,
|
|
2502
|
+
cursor: 0,
|
|
2503
|
+
input: '',
|
|
2504
|
+
pastedChunks: [],
|
|
2505
|
+
}));
|
|
2506
|
+
openJobsPicker();
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2034
2509
|
appendTurnAwareEntry({
|
|
2035
|
-
body: "Slash commands can't be queued while a turn is running.",
|
|
2510
|
+
body: "Slash commands can't be queued while a turn is running. Ctrl+B opens background jobs.",
|
|
2036
2511
|
kind: 'system',
|
|
2037
2512
|
title: 'Queued',
|
|
2038
2513
|
});
|
|
2039
2514
|
return;
|
|
2040
2515
|
}
|
|
2041
2516
|
const pending = store.getState();
|
|
2517
|
+
const body = String(rawInput ?? '');
|
|
2042
2518
|
const snapshot = {
|
|
2043
|
-
body
|
|
2044
|
-
imageAttachments: pending.imageAttachments,
|
|
2519
|
+
body,
|
|
2520
|
+
imageAttachments: pending.imageAttachments.filter((attachment) => body.includes(`[Image #${attachment.index}]`)),
|
|
2045
2521
|
pastedChunks: pending.pastedChunks,
|
|
2046
2522
|
};
|
|
2047
2523
|
scheduleLiveFrameRemount();
|
|
@@ -2056,11 +2532,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2056
2532
|
}));
|
|
2057
2533
|
return;
|
|
2058
2534
|
}
|
|
2059
|
-
const imageAttachments = store
|
|
2535
|
+
const imageAttachments = store
|
|
2536
|
+
.getState()
|
|
2537
|
+
.imageAttachments.filter((attachment) => input.includes(`[Image #${attachment.index}]`));
|
|
2060
2538
|
scheduleLiveFrameRemount();
|
|
2061
2539
|
store.update((current) => ({
|
|
2062
2540
|
...current,
|
|
2063
2541
|
cursor: 0,
|
|
2542
|
+
imageAttachments: [],
|
|
2064
2543
|
input: '',
|
|
2065
2544
|
pastedChunks: [],
|
|
2066
2545
|
promptHistory: appendPromptToHistory(current.promptHistory, input),
|
|
@@ -2076,6 +2555,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2076
2555
|
requestExit();
|
|
2077
2556
|
return;
|
|
2078
2557
|
}
|
|
2558
|
+
if (input === '/logout') {
|
|
2559
|
+
store.update((current) => ({
|
|
2560
|
+
...current,
|
|
2561
|
+
busy: true,
|
|
2562
|
+
status: 'Signing out...',
|
|
2563
|
+
}));
|
|
2564
|
+
try {
|
|
2565
|
+
await logoutFromServer({ config: authConfig });
|
|
2566
|
+
}
|
|
2567
|
+
catch {
|
|
2568
|
+
}
|
|
2569
|
+
clearCliAuthConfig(session.env);
|
|
2570
|
+
signedOut = true;
|
|
2571
|
+
requestExit();
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2079
2574
|
if (input === '/help') {
|
|
2080
2575
|
appendStaticEntry({
|
|
2081
2576
|
body: formatInteractiveHelpText(),
|
|
@@ -2092,6 +2587,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2092
2587
|
});
|
|
2093
2588
|
return;
|
|
2094
2589
|
}
|
|
2590
|
+
if (input === '/report') {
|
|
2591
|
+
const opened = await openUrl(REPORT_ISSUE_URL);
|
|
2592
|
+
appendStaticEntries(buildReportTranscriptEntries(opened));
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2095
2595
|
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2096
2596
|
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2097
2597
|
if (!jobsArgs) {
|
|
@@ -2104,7 +2604,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2104
2604
|
const killed = await killBackgroundJob(jobId);
|
|
2105
2605
|
await collectBackgroundJobUiKillMutations({
|
|
2106
2606
|
session,
|
|
2107
|
-
projectIndex,
|
|
2108
2607
|
jobId,
|
|
2109
2608
|
result: killed,
|
|
2110
2609
|
});
|
|
@@ -2146,14 +2645,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2146
2645
|
});
|
|
2147
2646
|
}
|
|
2148
2647
|
catch (error) {
|
|
2648
|
+
if (exitForAuthenticationError(error))
|
|
2649
|
+
return;
|
|
2149
2650
|
appendError(error.message);
|
|
2150
2651
|
}
|
|
2151
2652
|
finally {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2653
|
+
if (!exiting) {
|
|
2654
|
+
store.update((current) => ({
|
|
2655
|
+
...current,
|
|
2656
|
+
busy: false,
|
|
2657
|
+
status: 'Ready',
|
|
2658
|
+
}));
|
|
2659
|
+
}
|
|
2157
2660
|
}
|
|
2158
2661
|
return;
|
|
2159
2662
|
}
|
|
@@ -2167,14 +2670,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2167
2670
|
await openResumePicker();
|
|
2168
2671
|
}
|
|
2169
2672
|
catch (error) {
|
|
2673
|
+
if (exitForAuthenticationError(error))
|
|
2674
|
+
return;
|
|
2170
2675
|
appendError(error.message);
|
|
2171
2676
|
}
|
|
2172
2677
|
finally {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2678
|
+
if (!exiting) {
|
|
2679
|
+
store.update((current) => ({
|
|
2680
|
+
...current,
|
|
2681
|
+
busy: false,
|
|
2682
|
+
status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
|
|
2683
|
+
}));
|
|
2684
|
+
}
|
|
2178
2685
|
}
|
|
2179
2686
|
return;
|
|
2180
2687
|
}
|
|
@@ -2194,43 +2701,85 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2194
2701
|
}
|
|
2195
2702
|
}
|
|
2196
2703
|
catch (error) {
|
|
2704
|
+
if (exitForAuthenticationError(error))
|
|
2705
|
+
return;
|
|
2197
2706
|
appendError(error.message);
|
|
2198
2707
|
}
|
|
2199
2708
|
finally {
|
|
2709
|
+
if (!exiting) {
|
|
2710
|
+
store.update((current) => ({
|
|
2711
|
+
...current,
|
|
2712
|
+
busy: false,
|
|
2713
|
+
status: current.modelPickerOpen ? 'Select a model' : 'Ready',
|
|
2714
|
+
}));
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (input === '/new') {
|
|
2720
|
+
newConversationInFlight = true;
|
|
2721
|
+
store.update((current) => ({
|
|
2722
|
+
...current,
|
|
2723
|
+
busy: true,
|
|
2724
|
+
busyPausedAt: null,
|
|
2725
|
+
busySince: Date.now(),
|
|
2726
|
+
imageAttachments: [],
|
|
2727
|
+
queuedMessage: null,
|
|
2728
|
+
status: 'Starting a new conversation...',
|
|
2729
|
+
}));
|
|
2730
|
+
if (!(await saveActiveSessionOrAbort())) {
|
|
2731
|
+
newConversationInFlight = false;
|
|
2200
2732
|
store.update((current) => ({
|
|
2201
2733
|
...current,
|
|
2202
2734
|
busy: false,
|
|
2203
|
-
|
|
2735
|
+
busyPausedAt: null,
|
|
2736
|
+
busySince: null,
|
|
2737
|
+
status: 'Ready',
|
|
2204
2738
|
}));
|
|
2739
|
+
await flushQueuedMessage();
|
|
2740
|
+
return;
|
|
2205
2741
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2742
|
+
startNewConversation(session);
|
|
2743
|
+
setBackgroundJobSession(session.sessionId);
|
|
2744
|
+
setScratchSession(session.sessionId);
|
|
2745
|
+
setImageStoreSession(session.sessionId);
|
|
2746
|
+
setTodoSession(session.sessionId);
|
|
2210
2747
|
clearTodos();
|
|
2748
|
+
syncBackgroundJobsState();
|
|
2211
2749
|
syncTodosState();
|
|
2212
2750
|
latestUsageSummary = null;
|
|
2213
|
-
await saveActiveSession();
|
|
2214
2751
|
store.replaceTranscript([
|
|
2215
2752
|
{
|
|
2216
|
-
body: '
|
|
2753
|
+
body: 'Started a new conversation. The previous session remains saved.',
|
|
2217
2754
|
kind: 'system',
|
|
2218
|
-
title: '
|
|
2755
|
+
title: 'Session',
|
|
2219
2756
|
},
|
|
2220
2757
|
]);
|
|
2758
|
+
await saveActiveSession();
|
|
2221
2759
|
syncShellStateFromSession();
|
|
2222
|
-
|
|
2760
|
+
newConversationInFlight = false;
|
|
2761
|
+
store.update((current) => ({
|
|
2762
|
+
...current,
|
|
2763
|
+
busy: false,
|
|
2764
|
+
busyPausedAt: null,
|
|
2765
|
+
busySince: null,
|
|
2766
|
+
status: 'Ready',
|
|
2767
|
+
}));
|
|
2223
2768
|
await remountTui();
|
|
2769
|
+
await flushQueuedMessage();
|
|
2224
2770
|
return;
|
|
2225
2771
|
}
|
|
2226
2772
|
latestUsageSummary = null;
|
|
2227
2773
|
disarmExitConfirm();
|
|
2774
|
+
unacknowledged = new Map();
|
|
2228
2775
|
const turnStartedAt = Date.now();
|
|
2229
2776
|
const turnGeneration = ++activeTurnGeneration;
|
|
2230
2777
|
const turnAbort = new AbortController();
|
|
2231
2778
|
activeTurnAbort = turnAbort;
|
|
2232
2779
|
lastTurnStartedAt = turnStartedAt;
|
|
2233
2780
|
todosTouchedThisTurn = false;
|
|
2781
|
+
clearTodos();
|
|
2782
|
+
syncTodosState();
|
|
2234
2783
|
const userEntry = {
|
|
2235
2784
|
body: input,
|
|
2236
2785
|
kind: 'user',
|
|
@@ -2239,11 +2788,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2239
2788
|
};
|
|
2240
2789
|
pendingTurnEntries = [];
|
|
2241
2790
|
queueTurnEntry(userEntry);
|
|
2791
|
+
resetThinkingPacer();
|
|
2792
|
+
activeTurnImageAttachments = imageAttachments;
|
|
2242
2793
|
store.update((current) => ({
|
|
2243
2794
|
...current,
|
|
2244
2795
|
activeTurnInput: input,
|
|
2245
2796
|
activeTurnInputPreformatted: preformatted,
|
|
2797
|
+
analyzingImages: 0,
|
|
2798
|
+
generatingImage: false,
|
|
2246
2799
|
busy: true,
|
|
2800
|
+
busyPausedAt: null,
|
|
2247
2801
|
busySince: turnStartedAt,
|
|
2248
2802
|
clockNow: turnStartedAt,
|
|
2249
2803
|
status: 'Running turn...',
|
|
@@ -2252,23 +2806,44 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2252
2806
|
transcriptScrollOffset: 0,
|
|
2253
2807
|
tokenUsage: formatClientTokenUsage(0, latestUsageSummary),
|
|
2254
2808
|
turnCounter: current.turnCounter + 1,
|
|
2809
|
+
turnMessages: [],
|
|
2255
2810
|
workingTools: [],
|
|
2256
2811
|
}));
|
|
2257
2812
|
try {
|
|
2258
2813
|
const result = await chat.sendServerUserMessage({
|
|
2259
2814
|
config: authConfig,
|
|
2260
|
-
projectIndex,
|
|
2261
2815
|
session,
|
|
2262
2816
|
input,
|
|
2263
2817
|
imageAttachments,
|
|
2264
2818
|
signal: turnAbort.signal,
|
|
2819
|
+
onTurnStart: (serverTurnId) => {
|
|
2820
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2821
|
+
return;
|
|
2822
|
+
activeServerTurnId = serverTurnId;
|
|
2823
|
+
},
|
|
2824
|
+
onInterjectionDelivered: (event) => {
|
|
2825
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2826
|
+
return;
|
|
2827
|
+
for (const messageId of event.messageIds ?? []) {
|
|
2828
|
+
const pending = unacknowledged.get(messageId);
|
|
2829
|
+
if (!pending)
|
|
2830
|
+
continue;
|
|
2831
|
+
unacknowledged.delete(messageId);
|
|
2832
|
+
upsertTurnMessage(pending.row, {
|
|
2833
|
+
state: 'delivered',
|
|
2834
|
+
note: undefined,
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
},
|
|
2265
2838
|
});
|
|
2266
2839
|
if (turnGeneration !== activeTurnGeneration)
|
|
2267
2840
|
return;
|
|
2268
2841
|
latestUsageSummary = result.usageSummary ?? null;
|
|
2269
2842
|
const turnEntries = takePendingTurnEntries();
|
|
2270
|
-
await saveActiveSession()
|
|
2843
|
+
if (!(await saveActiveSession()))
|
|
2844
|
+
return;
|
|
2271
2845
|
syncShellStateFromSession();
|
|
2846
|
+
activeTurnImageAttachments = [];
|
|
2272
2847
|
store.update((current) => ({
|
|
2273
2848
|
...current,
|
|
2274
2849
|
activeTurnInput: '',
|
|
@@ -2304,10 +2879,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2304
2879
|
store.update((current) => ({
|
|
2305
2880
|
...current,
|
|
2306
2881
|
busy: false,
|
|
2882
|
+
busyPausedAt: null,
|
|
2307
2883
|
busySince: null,
|
|
2308
2884
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2309
2885
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
2310
2886
|
}));
|
|
2887
|
+
recoverUnacknowledgedMessages();
|
|
2311
2888
|
await flushQueuedMessage();
|
|
2312
2889
|
}
|
|
2313
2890
|
catch (error) {
|
|
@@ -2318,14 +2895,19 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2318
2895
|
return;
|
|
2319
2896
|
}
|
|
2320
2897
|
const cancelled = isTurnCancelledError(error);
|
|
2898
|
+
if (exitForAuthenticationError(error))
|
|
2899
|
+
return;
|
|
2900
|
+
const failedTurnAttachments = activeTurnImageAttachments;
|
|
2901
|
+
activeTurnImageAttachments = [];
|
|
2321
2902
|
store.update((current) => ({
|
|
2322
2903
|
...current,
|
|
2323
2904
|
activeTurnInput: '',
|
|
2324
2905
|
activeTurnInputPreformatted: false,
|
|
2325
2906
|
busy: false,
|
|
2907
|
+
busyPausedAt: null,
|
|
2326
2908
|
busySince: null,
|
|
2327
2909
|
commandLog: [],
|
|
2328
|
-
imageAttachments:
|
|
2910
|
+
imageAttachments: failedTurnAttachments,
|
|
2329
2911
|
status: cancelled ? 'Ready' : 'Turn failed',
|
|
2330
2912
|
thinkingTitle: '',
|
|
2331
2913
|
thinkingNotes: [],
|
|
@@ -2342,13 +2924,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2342
2924
|
title: 'System',
|
|
2343
2925
|
},
|
|
2344
2926
|
]);
|
|
2345
|
-
await saveActiveSession()
|
|
2927
|
+
if (!(await saveActiveSession()))
|
|
2928
|
+
return;
|
|
2346
2929
|
}
|
|
2347
2930
|
else {
|
|
2348
2931
|
appendStaticEntries(turnEntries);
|
|
2349
2932
|
appendError(error.message);
|
|
2933
|
+
appendStaticEntries(buildTurnFailureHintEntries(cancelled));
|
|
2350
2934
|
}
|
|
2351
2935
|
await remountTui();
|
|
2936
|
+
recoverUnacknowledgedMessages(!cancelled);
|
|
2352
2937
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2353
2938
|
await flushQueuedMessage();
|
|
2354
2939
|
}
|
|
@@ -2359,24 +2944,43 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2359
2944
|
}
|
|
2360
2945
|
if (turnGeneration === activeTurnGeneration) {
|
|
2361
2946
|
lastTurnStartedAt = null;
|
|
2947
|
+
activeServerTurnId = null;
|
|
2362
2948
|
}
|
|
2949
|
+
recoverUnacknowledgedMessages();
|
|
2950
|
+
await drainDeferredJobMutations();
|
|
2363
2951
|
}
|
|
2364
2952
|
};
|
|
2953
|
+
session.onImageAnalysis = (activeImageCount) => {
|
|
2954
|
+
store.update((current) => ({
|
|
2955
|
+
...current,
|
|
2956
|
+
analyzingImages: Math.max(0, activeImageCount),
|
|
2957
|
+
generatingImage: activeImageCount > 0 ? false : current.generatingImage,
|
|
2958
|
+
}));
|
|
2959
|
+
};
|
|
2960
|
+
session.onImageGeneration = (active) => {
|
|
2961
|
+
store.update((current) => ({
|
|
2962
|
+
...current,
|
|
2963
|
+
generatingImage: Boolean(active),
|
|
2964
|
+
analyzingImages: active ? 0 : current.analyzingImages,
|
|
2965
|
+
}));
|
|
2966
|
+
};
|
|
2365
2967
|
session.onStatus = (message) => {
|
|
2366
2968
|
const panel = thinkingPanelFromStatus(message);
|
|
2367
2969
|
store.update((current) => ({
|
|
2368
2970
|
...current,
|
|
2369
2971
|
status: message,
|
|
2370
|
-
|
|
2371
|
-
thinkingNotes: panel?.notes ?? current.thinkingNotes,
|
|
2372
|
-
tokenUsage: formatClientTokenUsage(current.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
|
|
2972
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2373
2973
|
}));
|
|
2974
|
+
queueThinkingUpdate({
|
|
2975
|
+
title: panel ? panel.title : liveStatusPanelTitle(message),
|
|
2976
|
+
notes: panel ? panel.notes : [],
|
|
2977
|
+
});
|
|
2374
2978
|
};
|
|
2375
2979
|
session.onContextLog = (message) => {
|
|
2376
2980
|
store.update((current) => ({
|
|
2377
2981
|
...current,
|
|
2378
2982
|
contextStatus: message,
|
|
2379
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2983
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2380
2984
|
}));
|
|
2381
2985
|
};
|
|
2382
2986
|
session.onToolEvent = (event) => {
|
|
@@ -2396,26 +3000,20 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2396
3000
|
}
|
|
2397
3001
|
store.appendWorkingTool(buildWorkingToolEntry(event));
|
|
2398
3002
|
};
|
|
2399
|
-
const jobDetailLines = (jobId) => {
|
|
2400
|
-
const buffered = getJobBufferedOutput(jobId);
|
|
2401
|
-
if (!buffered)
|
|
2402
|
-
return [];
|
|
2403
|
-
const lines = buffered.output
|
|
2404
|
-
.replace(/\r\n/g, '\n')
|
|
2405
|
-
.replace(/\r/g, '\n')
|
|
2406
|
-
.split('\n')
|
|
2407
|
-
.filter((line) => line.trim().length > 0);
|
|
2408
|
-
const visible = lines.slice(-20);
|
|
2409
|
-
return buffered.droppedChars > 0
|
|
2410
|
-
? [
|
|
2411
|
-
`... (${buffered.droppedChars} chars of older output dropped) ...`,
|
|
2412
|
-
...visible,
|
|
2413
|
-
]
|
|
2414
|
-
: visible;
|
|
2415
|
-
};
|
|
2416
3003
|
const syncBackgroundJobsState = () => {
|
|
3004
|
+
const expandedId = store.getState().jobsPickerExpandedId;
|
|
2417
3005
|
const jobsForDisplay = listBackgroundJobs().map((job) => {
|
|
2418
|
-
const
|
|
3006
|
+
const expanded = job.id === expandedId;
|
|
3007
|
+
const preview = getJobOutputPreview(job.id, expanded ? 20 : 3);
|
|
3008
|
+
const dropped = preview?.droppedChars ?? 0;
|
|
3009
|
+
const detailLines = !expanded
|
|
3010
|
+
? []
|
|
3011
|
+
: dropped > 0
|
|
3012
|
+
? [
|
|
3013
|
+
`... (${dropped} chars of older output dropped) ...`,
|
|
3014
|
+
...(preview?.tailLines ?? []),
|
|
3015
|
+
]
|
|
3016
|
+
: (preview?.tailLines ?? []);
|
|
2419
3017
|
return {
|
|
2420
3018
|
id: job.id,
|
|
2421
3019
|
command: job.command,
|
|
@@ -2424,8 +3022,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2424
3022
|
startedAt: job.startedAt,
|
|
2425
3023
|
endedAt: job.endedAt,
|
|
2426
3024
|
firstOutputLine: preview?.firstLine ?? '',
|
|
2427
|
-
tailLines: preview?.tailLines ?? [],
|
|
2428
|
-
detailLines
|
|
3025
|
+
tailLines: (preview?.tailLines ?? []).slice(-3),
|
|
3026
|
+
detailLines,
|
|
2429
3027
|
};
|
|
2430
3028
|
});
|
|
2431
3029
|
store.update((current) => ({
|
|
@@ -2442,54 +3040,54 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2442
3040
|
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2443
3041
|
}
|
|
2444
3042
|
});
|
|
2445
|
-
projectIndex.onStatus = session.onStatus;
|
|
2446
|
-
projectIndex.onContextLog = session.onContextLog;
|
|
2447
3043
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
2448
|
-
session.
|
|
3044
|
+
session.requestUserInput = async (request, signal) => openUserInputPrompt(request.questions, signal);
|
|
3045
|
+
session.requestPermission = async (request) => {
|
|
3046
|
+
const deny = { kind: 'deny' };
|
|
2449
3047
|
if (exiting)
|
|
2450
|
-
return
|
|
3048
|
+
return deny;
|
|
2451
3049
|
if (sessionAutoYes)
|
|
2452
|
-
return
|
|
2453
|
-
const
|
|
2454
|
-
if (
|
|
2455
|
-
setAgentMode('auto-accept');
|
|
2456
|
-
syncShellStateFromSession();
|
|
3050
|
+
return { kind: 'once' };
|
|
3051
|
+
const decision = await openApprovalPrompt(request);
|
|
3052
|
+
if (decision.kind === 'always-bucket') {
|
|
2457
3053
|
appendTurnAwareEntry({
|
|
2458
|
-
body:
|
|
3054
|
+
body: `Allowed for the rest of this session: ${bucketActionLabel(request.bucket)}.`,
|
|
2459
3055
|
kind: 'system',
|
|
2460
3056
|
title: 'Approvals',
|
|
2461
3057
|
});
|
|
2462
|
-
return true;
|
|
2463
3058
|
}
|
|
2464
|
-
|
|
2465
|
-
};
|
|
2466
|
-
session.confirmPatch = async (filePath, patch) => {
|
|
2467
|
-
if (exiting)
|
|
2468
|
-
return false;
|
|
2469
|
-
if (sessionAutoYes)
|
|
2470
|
-
return true;
|
|
2471
|
-
const choice = await openApprovalPrompt('Approve patch?', 'Review changes before applying.', {
|
|
2472
|
-
diff: patch,
|
|
2473
|
-
filePath,
|
|
2474
|
-
});
|
|
2475
|
-
if (choice === 'a') {
|
|
2476
|
-
setAgentMode('auto-accept');
|
|
2477
|
-
syncShellStateFromSession();
|
|
3059
|
+
else if (decision.kind === 'always-prefix') {
|
|
2478
3060
|
appendTurnAwareEntry({
|
|
2479
|
-
body:
|
|
3061
|
+
body: `Allowed for the rest of this session: commands starting with \`${decision.prefix}\`.`,
|
|
2480
3062
|
kind: 'system',
|
|
2481
3063
|
title: 'Approvals',
|
|
2482
3064
|
});
|
|
2483
|
-
return true;
|
|
2484
3065
|
}
|
|
2485
|
-
|
|
3066
|
+
else if (decision.kind === 'always-origin') {
|
|
3067
|
+
appendTurnAwareEntry({
|
|
3068
|
+
body: `Allowed for the rest of this session: browser actions on ${decision.origin}.`,
|
|
3069
|
+
kind: 'system',
|
|
3070
|
+
title: 'Approvals',
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
return decision;
|
|
2486
3074
|
};
|
|
2487
3075
|
const shellInputHandlers = {
|
|
3076
|
+
getApprovalScrollLimit: () => {
|
|
3077
|
+
const contentWidth = frameContentWidth(terminalCols);
|
|
3078
|
+
return approvalScrollLimit(store.getState().approvalPrompt, contentWidth, terminalRows);
|
|
3079
|
+
},
|
|
3080
|
+
getComposerInputWidth: () => composerInputWidth(terminalCols, store.getState()),
|
|
2488
3081
|
getTranscriptScrollLimit: () => {
|
|
2489
|
-
const contentWidth =
|
|
3082
|
+
const contentWidth = frameContentWidth(terminalCols);
|
|
2490
3083
|
const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
2491
3084
|
return blocks.reduce((total, block, index) => total + block.length + (index > 0 ? 1 : 0), 0);
|
|
2492
3085
|
},
|
|
3086
|
+
getUserInputViewport: () => {
|
|
3087
|
+
const nowMs = Date.now();
|
|
3088
|
+
const state = store.getState();
|
|
3089
|
+
return userInputViewportForFrame(state, terminalCols, terminalRows, spinnerFrame, busyElapsedSeconds(state, nowMs), nowMs);
|
|
3090
|
+
},
|
|
2493
3091
|
onCancelTurn: cancelActiveTurn,
|
|
2494
3092
|
onCycleAgentMode: cycleAgentMode,
|
|
2495
3093
|
onCtrlC: handleCtrlC,
|
|
@@ -2498,13 +3096,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2498
3096
|
onLiveFrameShapeChange: scheduleLiveFrameRemount,
|
|
2499
3097
|
onRequestExit: requestExit,
|
|
2500
3098
|
onResolveApproval: handleInlineApprovalChoice,
|
|
3099
|
+
onResolveUserInput: handleInlineUserInput,
|
|
2501
3100
|
onResumeSession: handleInlineResumeSelection,
|
|
2502
3101
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2503
3102
|
onSelectModel: handleInlineModelSelection,
|
|
3103
|
+
onOpenJobsPicker: openJobsPicker,
|
|
2504
3104
|
onJobsPickerOutput: handleJobsPickerOutput,
|
|
2505
3105
|
onJobsPickerKill: handleJobsPickerKill,
|
|
2506
3106
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2507
3107
|
onSubmit: handleSubmit,
|
|
3108
|
+
onFireQueuedMessage: fireQueuedMessage,
|
|
2508
3109
|
};
|
|
2509
3110
|
const unsubscribe = store.subscribe(() => {
|
|
2510
3111
|
renderCurrentFrame();
|
|
@@ -2512,6 +3113,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2512
3113
|
const spinnerTimer = setInterval(() => {
|
|
2513
3114
|
spinnerFrame = (spinnerFrame + 1) % 6;
|
|
2514
3115
|
if (store.getState().busy) {
|
|
3116
|
+
flushPendingThinking();
|
|
2515
3117
|
renderCurrentFrame();
|
|
2516
3118
|
}
|
|
2517
3119
|
}, 120);
|
|
@@ -2567,10 +3169,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2567
3169
|
killAllBackgroundJobs();
|
|
2568
3170
|
setBackgroundJobSession(null);
|
|
2569
3171
|
setScratchSession(null);
|
|
3172
|
+
setImageStoreSession(null);
|
|
2570
3173
|
setTodoSession(null);
|
|
2571
3174
|
setBackgroundJobUpdateHook(null);
|
|
2572
3175
|
await bridge.close();
|
|
3176
|
+
releaseTerminalWrites();
|
|
2573
3177
|
setCommandOutputHook(null);
|
|
2574
3178
|
}
|
|
2575
3179
|
});
|
|
3180
|
+
if (fatalError)
|
|
3181
|
+
throw fatalError;
|
|
3182
|
+
return { signedOut };
|
|
2576
3183
|
}
|