@thegitai/cli 1.0.0-preview.12 → 1.0.0-preview.14
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 +16 -0
- package/dist/src/api/chat.js +52 -0
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/executor.js +1 -1
- package/dist/src/session.js +2 -1
- package/dist/src/ui/repl.js +148 -14
- package/dist/src/ui/tui/build-frame.js +206 -46
- package/dist/src/ui/tui/shell-input.js +75 -22
- package/dist/src/ui/tui/user-input.js +568 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -38,6 +38,22 @@ CLI login tokens use a rolling 24-hour inactivity timeout. If one expires during
|
|
|
38
38
|
any server request, the CLI saves the local session, removes the expired
|
|
39
39
|
credential, and asks you to run `ai login` before resuming.
|
|
40
40
|
|
|
41
|
+
## Structured questions
|
|
42
|
+
|
|
43
|
+
The agent can pause its current turn to ask up to four related questions in one
|
|
44
|
+
form. Use **↑ / ↓**, the displayed option number, or **Enter** to choose, and
|
|
45
|
+
**← / →** to move between questions. **Something else / add details** is the
|
|
46
|
+
numbered final option; focus it and type, paste, or press **Enter** to add a
|
|
47
|
+
single-line note. On single-select questions it is mutually exclusive with the
|
|
48
|
+
listed choices, and **↑ / ↓** leaves its editor when the note is empty.
|
|
49
|
+
Multi-select questions toggle choices. The last question has an explicit
|
|
50
|
+
**Submit answers** row. **Esc** closes an open note first and otherwise cancels
|
|
51
|
+
the form; **Ctrl+C** cancels the whole turn.
|
|
52
|
+
|
|
53
|
+
Answers return to the same turn, and completed question/answer records remain
|
|
54
|
+
readable when the session is resumed. Default, Auto-Accept, and Plan modes all
|
|
55
|
+
support the form; Auto-Accept does not choose answers for you.
|
|
56
|
+
|
|
41
57
|
## Visible to-do list
|
|
42
58
|
|
|
43
59
|
For larger multi-step tasks, the agent keeps a compact to-do list on screen so
|
package/dist/src/api/chat.js
CHANGED
|
@@ -2,6 +2,7 @@ import { drainBackgroundJobNotifications } from '../background-jobs.js';
|
|
|
2
2
|
import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
|
|
3
3
|
import { applySessionSnapshot, saveSessionState, snapshotFromSession, } from '../session-store.js';
|
|
4
4
|
import { executeLocalToolCall } from '../tool-executor.js';
|
|
5
|
+
import { isUserInputQuestionArray } from './contracts.js';
|
|
5
6
|
import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
6
7
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
7
8
|
import { collectProjectOrientation } from '../project-orientation.js';
|
|
@@ -168,6 +169,8 @@ function publicStatusMessage(data) {
|
|
|
168
169
|
return `Running ${toolName}...`;
|
|
169
170
|
if (event.phase === 'waiting_for_tool')
|
|
170
171
|
return `Running ${toolName} locally...`;
|
|
172
|
+
if (event.phase === 'waiting_for_user_input')
|
|
173
|
+
return 'Waiting for your input...';
|
|
171
174
|
return null;
|
|
172
175
|
}
|
|
173
176
|
function normalizeShellJobToolCall(call) {
|
|
@@ -223,6 +226,28 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
223
226
|
throw await readErrorResponse(response, trace.traceId);
|
|
224
227
|
}
|
|
225
228
|
}
|
|
229
|
+
async function postUserInputResult({ config, turnId, requestId, result, fetchImpl, traceId, }) {
|
|
230
|
+
const payload = {
|
|
231
|
+
requestId,
|
|
232
|
+
result,
|
|
233
|
+
};
|
|
234
|
+
const trace = createTraceContext(traceId);
|
|
235
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/user-input-result`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
authorization: `Bearer ${config.token}`,
|
|
239
|
+
'content-type': 'application/json',
|
|
240
|
+
...trace.headers,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify(payload),
|
|
243
|
+
});
|
|
244
|
+
if (response.status === 410) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
226
251
|
const turnIdOverrides = new WeakMap();
|
|
227
252
|
function enterServerTurnId(session, serverSessionTurnId) {
|
|
228
253
|
const active = turnIdOverrides.get(session);
|
|
@@ -371,6 +396,33 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
371
396
|
}
|
|
372
397
|
return;
|
|
373
398
|
}
|
|
399
|
+
if (event.event === 'user-input-request') {
|
|
400
|
+
await drainParallelTools();
|
|
401
|
+
const data = event.data;
|
|
402
|
+
const turnId = String(data?.turnId ?? '').trim();
|
|
403
|
+
const requestId = String(data?.requestId ?? '').trim();
|
|
404
|
+
if (!turnId ||
|
|
405
|
+
!requestId ||
|
|
406
|
+
!isUserInputQuestionArray(data?.questions)) {
|
|
407
|
+
throw new Error('Server emitted an invalid user-input request.');
|
|
408
|
+
}
|
|
409
|
+
if (!session.requestUserInput) {
|
|
410
|
+
throw new Error('Interactive user input is unavailable in this client.');
|
|
411
|
+
}
|
|
412
|
+
const result = await session.requestUserInput({ questions: data.questions }, signal);
|
|
413
|
+
if (signal?.aborted) {
|
|
414
|
+
throw new TurnCancelledError();
|
|
415
|
+
}
|
|
416
|
+
await postUserInputResult({
|
|
417
|
+
config,
|
|
418
|
+
turnId,
|
|
419
|
+
requestId,
|
|
420
|
+
result,
|
|
421
|
+
fetchImpl,
|
|
422
|
+
traceId,
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
374
426
|
if (event.event === 'result') {
|
|
375
427
|
await drainParallelTools();
|
|
376
428
|
finalResult.current = event.data;
|
|
@@ -1 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
function nonEmptyString(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
3
|
+
}
|
|
4
|
+
const USER_INPUT_ID_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
5
|
+
export function isUserInputQuestionArray(value) {
|
|
6
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 4) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const questionIds = new Set();
|
|
10
|
+
return value.every((question) => {
|
|
11
|
+
if (!question || typeof question !== 'object' || Array.isArray(question)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const candidate = question;
|
|
15
|
+
if (!nonEmptyString(candidate.id) ||
|
|
16
|
+
!USER_INPUT_ID_PATTERN.test(candidate.id) ||
|
|
17
|
+
questionIds.has(candidate.id) ||
|
|
18
|
+
!nonEmptyString(candidate.header) ||
|
|
19
|
+
Array.from(candidate.header).length > 12 ||
|
|
20
|
+
!nonEmptyString(candidate.question) ||
|
|
21
|
+
typeof candidate.multiSelect !== 'boolean' ||
|
|
22
|
+
!Array.isArray(candidate.options) ||
|
|
23
|
+
candidate.options.length < 2 ||
|
|
24
|
+
candidate.options.length > 4) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
questionIds.add(candidate.id);
|
|
28
|
+
const optionIds = new Set();
|
|
29
|
+
const optionsValid = candidate.options.every((option) => {
|
|
30
|
+
if (!option || typeof option !== 'object' || Array.isArray(option)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const item = option;
|
|
34
|
+
if (!nonEmptyString(item.id) ||
|
|
35
|
+
!USER_INPUT_ID_PATTERN.test(item.id) ||
|
|
36
|
+
optionIds.has(item.id) ||
|
|
37
|
+
!nonEmptyString(item.label) ||
|
|
38
|
+
!nonEmptyString(item.description)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
optionIds.add(item.id);
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
if (!optionsValid) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (candidate.recommendedOptionId !== undefined &&
|
|
48
|
+
(!nonEmptyString(candidate.recommendedOptionId) ||
|
|
49
|
+
candidate.recommendedOptionId !==
|
|
50
|
+
candidate.options[0].id)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
}
|
package/dist/src/executor.js
CHANGED
|
@@ -596,7 +596,7 @@ export function commandUsesSudo(command) {
|
|
|
596
596
|
}
|
|
597
597
|
export function sudoPromptFromTail(text) {
|
|
598
598
|
const tail = text.slice(-1000).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
599
|
-
const match = tail.match(/(?:\[sudo\][^\r\n]*password[^\r\n]*:
|
|
599
|
+
const match = tail.match(/(?:\[sudo\][^\r\n]*password[^\r\n]*: ?|\[?sudo[^\r\n]*password[^\r\n]*: ?|password[^\r\n]*: ?)$/i);
|
|
600
600
|
return match?.[0] ?? null;
|
|
601
601
|
}
|
|
602
602
|
function isSudoPromptLine(text) {
|
package/dist/src/session.js
CHANGED
|
@@ -31,7 +31,7 @@ function preserveProviderSelection(serverState) {
|
|
|
31
31
|
: null;
|
|
32
32
|
return providerSelection ? { providerSelection } : {};
|
|
33
33
|
}
|
|
34
|
-
export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS, confirmCommand = null, confirmPatch = null, requestSudoPassword = null, onStatus = null, onContextLog = null, onToolEvent = null, env = process.env, sessionId = createSessionId(), sessionName = null, history = [], serverState = null, editJournal = [], stickyFilePaths = [], editCounter = 0, safety = createSessionSafetyState(), }) {
|
|
34
|
+
export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS, confirmCommand = null, confirmPatch = null, requestSudoPassword = null, requestUserInput = null, onStatus = null, onContextLog = null, onToolEvent = null, env = process.env, sessionId = createSessionId(), sessionName = null, history = [], serverState = null, editJournal = [], stickyFilePaths = [], editCounter = 0, safety = createSessionSafetyState(), }) {
|
|
35
35
|
const createdAt = new Date().toISOString();
|
|
36
36
|
const initialAgentMode = normalizeAgentMode(agentMode ?? (autoYes ? 'auto-accept' : 'default'));
|
|
37
37
|
return {
|
|
@@ -46,6 +46,7 @@ export function createSession({ rootDir, autoYes = false, agentMode, modelId, ma
|
|
|
46
46
|
confirmCommand,
|
|
47
47
|
confirmPatch,
|
|
48
48
|
requestSudoPassword,
|
|
49
|
+
requestUserInput,
|
|
49
50
|
history: JSON.parse(JSON.stringify(history)),
|
|
50
51
|
initialized: true,
|
|
51
52
|
sessionId,
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRatatuiBridge } from './tui/bridge.js';
|
|
2
|
-
import { buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, } from './tui/build-frame.js';
|
|
2
|
+
import { approvalScrollLimit, buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, userInputViewportForFrame, } from './tui/build-frame.js';
|
|
3
3
|
import { createTerminalTitleController } from './tui/terminal-title.js';
|
|
4
4
|
import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
|
|
5
5
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
@@ -13,6 +13,7 @@ import { cancelActiveCommand } from '../executor.js';
|
|
|
13
13
|
import { isTurnFailureMarker } from '../turn-failure-marker.js';
|
|
14
14
|
import { clearCliAuthConfig } from '../api/auth.js';
|
|
15
15
|
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
16
|
+
import { createUserInputPromptState, formatUserInputTranscript, } from './tui/user-input.js';
|
|
16
17
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
17
18
|
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
18
19
|
import { startNewConversation, } from '../session.js';
|
|
@@ -787,10 +788,18 @@ function displayUserTextFromHistoryEntry(entry) {
|
|
|
787
788
|
export function buildTranscriptFromSessionHistory(history) {
|
|
788
789
|
const entries = [];
|
|
789
790
|
const pendingCalls = new Map();
|
|
791
|
+
const pendingUserInputRequests = new Map();
|
|
790
792
|
for (const entry of history) {
|
|
791
793
|
if (!entry?.parts?.length)
|
|
792
794
|
continue;
|
|
793
795
|
for (const part of entry.parts) {
|
|
796
|
+
const userInputRequest = part?.userInputRequest;
|
|
797
|
+
if (userInputRequest && typeof userInputRequest === 'object') {
|
|
798
|
+
const requestId = String(userInputRequest.requestId ?? '').trim();
|
|
799
|
+
if (requestId && Array.isArray(userInputRequest.questions)) {
|
|
800
|
+
pendingUserInputRequests.set(requestId, userInputRequest.questions);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
794
803
|
const call = part?.functionCall;
|
|
795
804
|
if (!call || typeof call !== 'object')
|
|
796
805
|
continue;
|
|
@@ -800,6 +809,21 @@ export function buildTranscriptFromSessionHistory(history) {
|
|
|
800
809
|
pendingCalls.set(callId, call);
|
|
801
810
|
}
|
|
802
811
|
for (const part of entry.parts) {
|
|
812
|
+
const userInputResult = part?.userInputResult;
|
|
813
|
+
if (userInputResult && typeof userInputResult === 'object') {
|
|
814
|
+
const requestId = String(userInputResult.requestId ?? '').trim();
|
|
815
|
+
const questions = pendingUserInputRequests.get(requestId);
|
|
816
|
+
const result = userInputResult.result;
|
|
817
|
+
if (questions &&
|
|
818
|
+
(result?.status === 'submitted' || result?.status === 'cancelled')) {
|
|
819
|
+
pendingUserInputRequests.delete(requestId);
|
|
820
|
+
entries.push({
|
|
821
|
+
body: formatUserInputTranscript(questions, result),
|
|
822
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
823
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
803
827
|
const functionResponse = part?.functionResponse;
|
|
804
828
|
if (!functionResponse || typeof functionResponse !== 'object')
|
|
805
829
|
continue;
|
|
@@ -966,9 +990,11 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
966
990
|
analyzingImages: 0,
|
|
967
991
|
approvalCursor: getDefaultApprovalCursor(),
|
|
968
992
|
approvalPrompt: null,
|
|
993
|
+
approvalScrollOffset: 0,
|
|
969
994
|
autoYes: session.autoYes,
|
|
970
995
|
backgroundJobs: [],
|
|
971
996
|
busy: false,
|
|
997
|
+
busyPausedAt: null,
|
|
972
998
|
busySince: null,
|
|
973
999
|
clockNow: Date.now(),
|
|
974
1000
|
commandCursor: 0,
|
|
@@ -1006,6 +1032,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
1006
1032
|
tokenUsage: formatClientTokenUsage(null),
|
|
1007
1033
|
transcript: [],
|
|
1008
1034
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
1035
|
+
userInputPrompt: null,
|
|
1009
1036
|
pastedChunks: [],
|
|
1010
1037
|
imageAttachments: [],
|
|
1011
1038
|
workingTools: [],
|
|
@@ -1265,6 +1292,30 @@ export function resolveApprovalChoiceFromInput(input) {
|
|
|
1265
1292
|
return 'n';
|
|
1266
1293
|
return null;
|
|
1267
1294
|
}
|
|
1295
|
+
export function pauseBusyClock(state, nowMs) {
|
|
1296
|
+
if (state.busySince === null || state.busyPausedAt !== null)
|
|
1297
|
+
return state;
|
|
1298
|
+
return { ...state, busyPausedAt: nowMs };
|
|
1299
|
+
}
|
|
1300
|
+
export function resumeBusyClock(state, nowMs) {
|
|
1301
|
+
if (state.busyPausedAt === null)
|
|
1302
|
+
return state;
|
|
1303
|
+
return {
|
|
1304
|
+
...state,
|
|
1305
|
+
busyPausedAt: null,
|
|
1306
|
+
busySince: state.busySince === null
|
|
1307
|
+
? null
|
|
1308
|
+
: state.busySince + Math.max(0, nowMs - state.busyPausedAt),
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
export function busyElapsedMs(state, nowMs) {
|
|
1312
|
+
if (state.busySince === null)
|
|
1313
|
+
return null;
|
|
1314
|
+
return Math.max(0, (state.busyPausedAt ?? nowMs) - state.busySince);
|
|
1315
|
+
}
|
|
1316
|
+
export function busyElapsedSeconds(state, nowMs) {
|
|
1317
|
+
return Math.floor((busyElapsedMs(state, nowMs) ?? 0) / 1000);
|
|
1318
|
+
}
|
|
1268
1319
|
export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
1269
1320
|
return serverModels.map((model) => ({
|
|
1270
1321
|
id: model.id,
|
|
@@ -1325,6 +1376,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1325
1376
|
let resolveDone = null;
|
|
1326
1377
|
let resolveApprovalChoice = null;
|
|
1327
1378
|
let resolveSudoPassword = null;
|
|
1379
|
+
let pendingUserInput = null;
|
|
1328
1380
|
let cleanupSudoPasswordPrompt = null;
|
|
1329
1381
|
let sudoPasswordBuffer = '';
|
|
1330
1382
|
const bridge = createRatatuiBridge();
|
|
@@ -1419,9 +1471,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1419
1471
|
return;
|
|
1420
1472
|
const state = store.getState();
|
|
1421
1473
|
syncTerminalTitle();
|
|
1422
|
-
const elapsedSeconds = state.
|
|
1423
|
-
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1424
|
-
: 0;
|
|
1474
|
+
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1425
1475
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1426
1476
|
};
|
|
1427
1477
|
const remountTui = async () => {
|
|
@@ -1553,9 +1603,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1553
1603
|
resolveApprovalChoice = null;
|
|
1554
1604
|
pendingResolve('n');
|
|
1555
1605
|
store.update((current) => ({
|
|
1556
|
-
...current,
|
|
1606
|
+
...resumeBusyClock(current, Date.now()),
|
|
1557
1607
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1558
1608
|
approvalPrompt: null,
|
|
1609
|
+
approvalScrollOffset: 0,
|
|
1559
1610
|
}));
|
|
1560
1611
|
};
|
|
1561
1612
|
const dismissPendingSudoPassword = () => {
|
|
@@ -1568,16 +1619,32 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1568
1619
|
sudoPasswordBuffer = '';
|
|
1569
1620
|
pendingResolve(null);
|
|
1570
1621
|
store.update((current) => ({
|
|
1571
|
-
...current,
|
|
1622
|
+
...resumeBusyClock(current, Date.now()),
|
|
1572
1623
|
sudoPrompt: null,
|
|
1573
1624
|
}));
|
|
1574
1625
|
};
|
|
1626
|
+
const dismissPendingUserInput = () => {
|
|
1627
|
+
const pending = pendingUserInput;
|
|
1628
|
+
if (!pending)
|
|
1629
|
+
return;
|
|
1630
|
+
pendingUserInput = null;
|
|
1631
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1632
|
+
const error = new Error('Turn cancelled.');
|
|
1633
|
+
error.name = 'AbortError';
|
|
1634
|
+
pending.reject(error);
|
|
1635
|
+
store.update((current) => ({
|
|
1636
|
+
...resumeBusyClock(current, Date.now()),
|
|
1637
|
+
status: current.userInputPrompt?.returnStatus ?? current.status,
|
|
1638
|
+
userInputPrompt: null,
|
|
1639
|
+
}));
|
|
1640
|
+
};
|
|
1575
1641
|
const cancelActiveTurn = () => {
|
|
1576
1642
|
if (!store.getState().busy || newConversationInFlight)
|
|
1577
1643
|
return;
|
|
1578
1644
|
disarmExitConfirm();
|
|
1579
1645
|
dismissPendingApproval();
|
|
1580
1646
|
dismissPendingSudoPassword();
|
|
1647
|
+
dismissPendingUserInput();
|
|
1581
1648
|
activeTurnGeneration += 1;
|
|
1582
1649
|
resetThinkingPacer();
|
|
1583
1650
|
activeTurnAbort?.abort();
|
|
@@ -1597,6 +1664,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1597
1664
|
activeTurnInput: '',
|
|
1598
1665
|
activeTurnInputPreformatted: false,
|
|
1599
1666
|
busy: false,
|
|
1667
|
+
busyPausedAt: null,
|
|
1600
1668
|
busySince: null,
|
|
1601
1669
|
commandLog: [],
|
|
1602
1670
|
cursor: queued ? queued.body.length : current.cursor,
|
|
@@ -1610,7 +1678,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1610
1678
|
thinkingTitle: '',
|
|
1611
1679
|
thinkingNotes: [],
|
|
1612
1680
|
workingTools: [],
|
|
1613
|
-
tokenUsage: formatClientTokenUsage(current
|
|
1681
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1614
1682
|
}));
|
|
1615
1683
|
appendStaticEntries(cancelledEntries);
|
|
1616
1684
|
lastTurnStartedAt = null;
|
|
@@ -1696,6 +1764,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1696
1764
|
}
|
|
1697
1765
|
dismissPendingApproval();
|
|
1698
1766
|
dismissPendingSudoPassword();
|
|
1767
|
+
dismissPendingUserInput();
|
|
1699
1768
|
exiting = true;
|
|
1700
1769
|
killAllBackgroundJobs();
|
|
1701
1770
|
store.update((current) => ({
|
|
@@ -1761,7 +1830,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1761
1830
|
cleanupSudoPasswordPrompt = null;
|
|
1762
1831
|
}
|
|
1763
1832
|
store.update((current) => ({
|
|
1764
|
-
...current,
|
|
1833
|
+
...pauseBusyClock(current, Date.now()),
|
|
1765
1834
|
sudoPrompt: {
|
|
1766
1835
|
command,
|
|
1767
1836
|
passwordLength: 0,
|
|
@@ -1781,7 +1850,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1781
1850
|
cleanupSudoPasswordPrompt = null;
|
|
1782
1851
|
sudoPasswordBuffer = '';
|
|
1783
1852
|
store.update((current) => ({
|
|
1784
|
-
...current,
|
|
1853
|
+
...resumeBusyClock(current, Date.now()),
|
|
1785
1854
|
sudoPrompt: null,
|
|
1786
1855
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1787
1856
|
}));
|
|
@@ -1795,7 +1864,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1795
1864
|
cleanupSudoPasswordPrompt = null;
|
|
1796
1865
|
sudoPasswordBuffer = '';
|
|
1797
1866
|
store.update((current) => ({
|
|
1798
|
-
...current,
|
|
1867
|
+
...resumeBusyClock(current, Date.now()),
|
|
1799
1868
|
sudoPrompt: null,
|
|
1800
1869
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1801
1870
|
}));
|
|
@@ -1815,6 +1884,49 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1815
1884
|
: null,
|
|
1816
1885
|
}));
|
|
1817
1886
|
};
|
|
1887
|
+
const openUserInputPrompt = (questions, signal) => new Promise((resolve, reject) => {
|
|
1888
|
+
if (exiting || signal?.aborted) {
|
|
1889
|
+
const error = new Error('Turn cancelled.');
|
|
1890
|
+
error.name = 'AbortError';
|
|
1891
|
+
reject(error);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const onAbort = () => dismissPendingUserInput();
|
|
1895
|
+
pendingUserInput = {
|
|
1896
|
+
questions,
|
|
1897
|
+
resolve,
|
|
1898
|
+
reject,
|
|
1899
|
+
signal,
|
|
1900
|
+
onAbort,
|
|
1901
|
+
};
|
|
1902
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1903
|
+
store.update((current) => ({
|
|
1904
|
+
...pauseBusyClock(current, Date.now()),
|
|
1905
|
+
status: 'Waiting for your input',
|
|
1906
|
+
userInputPrompt: createUserInputPromptState(questions, current.status),
|
|
1907
|
+
}));
|
|
1908
|
+
scheduleLiveFrameRemount();
|
|
1909
|
+
});
|
|
1910
|
+
const handleInlineUserInput = async (result) => {
|
|
1911
|
+
const pending = pendingUserInput;
|
|
1912
|
+
const prompt = store.getState().userInputPrompt;
|
|
1913
|
+
if (!pending || !prompt)
|
|
1914
|
+
return;
|
|
1915
|
+
pendingUserInput = null;
|
|
1916
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1917
|
+
store.update((current) => ({
|
|
1918
|
+
...resumeBusyClock(current, Date.now()),
|
|
1919
|
+
status: prompt.returnStatus,
|
|
1920
|
+
userInputPrompt: null,
|
|
1921
|
+
}));
|
|
1922
|
+
appendTurnAwareEntry({
|
|
1923
|
+
body: formatUserInputTranscript(pending.questions, result),
|
|
1924
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
1925
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
1926
|
+
});
|
|
1927
|
+
pending.resolve(result);
|
|
1928
|
+
scheduleLiveFrameRemount();
|
|
1929
|
+
};
|
|
1818
1930
|
const openApprovalPrompt = (title, body, options = {}) => new Promise((resolve) => {
|
|
1819
1931
|
if (exiting) {
|
|
1820
1932
|
resolve('n');
|
|
@@ -1822,8 +1934,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1822
1934
|
}
|
|
1823
1935
|
resolveApprovalChoice = resolve;
|
|
1824
1936
|
store.update((current) => ({
|
|
1825
|
-
...current,
|
|
1937
|
+
...pauseBusyClock(current, Date.now()),
|
|
1826
1938
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1939
|
+
approvalScrollOffset: 0,
|
|
1827
1940
|
approvalPrompt: {
|
|
1828
1941
|
title,
|
|
1829
1942
|
body,
|
|
@@ -1841,9 +1954,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1841
1954
|
const pendingResolve = resolveApprovalChoice;
|
|
1842
1955
|
resolveApprovalChoice = null;
|
|
1843
1956
|
store.update((next) => ({
|
|
1844
|
-
...next,
|
|
1957
|
+
...resumeBusyClock(next, Date.now()),
|
|
1845
1958
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1846
1959
|
approvalPrompt: null,
|
|
1960
|
+
approvalScrollOffset: 0,
|
|
1847
1961
|
status: current.approvalPrompt?.returnStatus ?? next.status,
|
|
1848
1962
|
}));
|
|
1849
1963
|
pendingResolve?.(choice);
|
|
@@ -2051,6 +2165,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2051
2165
|
store.update((next) => ({
|
|
2052
2166
|
...next,
|
|
2053
2167
|
busy: true,
|
|
2168
|
+
busyPausedAt: null,
|
|
2054
2169
|
busySince: resumeStartedAt,
|
|
2055
2170
|
clockNow: resumeStartedAt,
|
|
2056
2171
|
status: 'Loading session...',
|
|
@@ -2067,6 +2182,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2067
2182
|
store.update((next) => ({
|
|
2068
2183
|
...next,
|
|
2069
2184
|
busy: false,
|
|
2185
|
+
busyPausedAt: null,
|
|
2070
2186
|
busySince: null,
|
|
2071
2187
|
resumePickerFilter: '',
|
|
2072
2188
|
resumePickerIndex: 0,
|
|
@@ -2090,6 +2206,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2090
2206
|
store.update((next) => ({
|
|
2091
2207
|
...next,
|
|
2092
2208
|
busy: false,
|
|
2209
|
+
busyPausedAt: null,
|
|
2093
2210
|
busySince: null,
|
|
2094
2211
|
resumePickerFilter: '',
|
|
2095
2212
|
resumePickerIndex: 0,
|
|
@@ -2315,6 +2432,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2315
2432
|
store.update((current) => ({
|
|
2316
2433
|
...current,
|
|
2317
2434
|
busy: true,
|
|
2435
|
+
busyPausedAt: null,
|
|
2318
2436
|
busySince: Date.now(),
|
|
2319
2437
|
imageAttachments: [],
|
|
2320
2438
|
queuedMessage: null,
|
|
@@ -2325,6 +2443,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2325
2443
|
store.update((current) => ({
|
|
2326
2444
|
...current,
|
|
2327
2445
|
busy: false,
|
|
2446
|
+
busyPausedAt: null,
|
|
2328
2447
|
busySince: null,
|
|
2329
2448
|
status: 'Ready',
|
|
2330
2449
|
}));
|
|
@@ -2352,6 +2471,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2352
2471
|
store.update((current) => ({
|
|
2353
2472
|
...current,
|
|
2354
2473
|
busy: false,
|
|
2474
|
+
busyPausedAt: null,
|
|
2355
2475
|
busySince: null,
|
|
2356
2476
|
status: 'Ready',
|
|
2357
2477
|
}));
|
|
@@ -2384,6 +2504,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2384
2504
|
activeTurnInputPreformatted: preformatted,
|
|
2385
2505
|
analyzingImages: 0,
|
|
2386
2506
|
busy: true,
|
|
2507
|
+
busyPausedAt: null,
|
|
2387
2508
|
busySince: turnStartedAt,
|
|
2388
2509
|
clockNow: turnStartedAt,
|
|
2389
2510
|
status: 'Running turn...',
|
|
@@ -2445,6 +2566,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2445
2566
|
store.update((current) => ({
|
|
2446
2567
|
...current,
|
|
2447
2568
|
busy: false,
|
|
2569
|
+
busyPausedAt: null,
|
|
2448
2570
|
busySince: null,
|
|
2449
2571
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2450
2572
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
@@ -2466,6 +2588,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2466
2588
|
activeTurnInput: '',
|
|
2467
2589
|
activeTurnInputPreformatted: false,
|
|
2468
2590
|
busy: false,
|
|
2591
|
+
busyPausedAt: null,
|
|
2469
2592
|
busySince: null,
|
|
2470
2593
|
commandLog: [],
|
|
2471
2594
|
imageAttachments: [],
|
|
@@ -2517,7 +2640,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2517
2640
|
store.update((current) => ({
|
|
2518
2641
|
...current,
|
|
2519
2642
|
status: message,
|
|
2520
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2643
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2521
2644
|
}));
|
|
2522
2645
|
queueThinkingUpdate({
|
|
2523
2646
|
title: panel ? panel.title : liveStatusPanelTitle(message),
|
|
@@ -2528,7 +2651,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2528
2651
|
store.update((current) => ({
|
|
2529
2652
|
...current,
|
|
2530
2653
|
contextStatus: message,
|
|
2531
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2654
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2532
2655
|
}));
|
|
2533
2656
|
};
|
|
2534
2657
|
session.onToolEvent = (event) => {
|
|
@@ -2597,6 +2720,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2597
2720
|
projectIndex.onStatus = session.onStatus;
|
|
2598
2721
|
projectIndex.onContextLog = session.onContextLog;
|
|
2599
2722
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
2723
|
+
session.requestUserInput = async (request, signal) => openUserInputPrompt(request.questions, signal);
|
|
2600
2724
|
session.confirmCommand = async (command) => {
|
|
2601
2725
|
if (exiting)
|
|
2602
2726
|
return false;
|
|
@@ -2637,11 +2761,20 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2637
2761
|
return choice === 'y';
|
|
2638
2762
|
};
|
|
2639
2763
|
const shellInputHandlers = {
|
|
2764
|
+
getApprovalScrollLimit: () => {
|
|
2765
|
+
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2766
|
+
return approvalScrollLimit(store.getState().approvalPrompt, contentWidth, terminalRows);
|
|
2767
|
+
},
|
|
2640
2768
|
getTranscriptScrollLimit: () => {
|
|
2641
2769
|
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2642
2770
|
const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
2643
2771
|
return blocks.reduce((total, block, index) => total + block.length + (index > 0 ? 1 : 0), 0);
|
|
2644
2772
|
},
|
|
2773
|
+
getUserInputViewport: () => {
|
|
2774
|
+
const nowMs = Date.now();
|
|
2775
|
+
const state = store.getState();
|
|
2776
|
+
return userInputViewportForFrame(state, terminalCols, terminalRows, spinnerFrame, busyElapsedSeconds(state, nowMs), nowMs);
|
|
2777
|
+
},
|
|
2645
2778
|
onCancelTurn: cancelActiveTurn,
|
|
2646
2779
|
onCycleAgentMode: cycleAgentMode,
|
|
2647
2780
|
onCtrlC: handleCtrlC,
|
|
@@ -2650,6 +2783,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2650
2783
|
onLiveFrameShapeChange: scheduleLiveFrameRemount,
|
|
2651
2784
|
onRequestExit: requestExit,
|
|
2652
2785
|
onResolveApproval: handleInlineApprovalChoice,
|
|
2786
|
+
onResolveUserInput: handleInlineUserInput,
|
|
2653
2787
|
onResumeSession: handleInlineResumeSelection,
|
|
2654
2788
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2655
2789
|
onSelectModel: handleInlineModelSelection,
|