@thegitai/cli 1.0.0-preview.5 → 1.0.0-preview.50
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 +93 -4
- package/dist/bin/ai.js +142 -112
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +32 -7
- package/dist/src/api/auth.js +10 -4
- package/dist/src/api/browser-login.js +149 -37
- package/dist/src/api/chat.js +680 -166
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +151 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/api/startup-handshake.js +36 -0
- package/dist/src/api/turn-recovery.js +189 -0
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +262 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +219 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +130 -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 +112 -21
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -0
- package/dist/src/preferences.js +39 -0
- package/dist/src/quota-notice.js +35 -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 +13 -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 +949 -279
- package/dist/src/ui/slash-commands.js +121 -0
- package/dist/src/ui/tui/attention.js +25 -0
- package/dist/src/ui/tui/bridge.js +38 -5
- package/dist/src/ui/tui/build-frame.js +686 -223
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/desktop-notification.js +118 -0
- package/dist/src/ui/tui/markdown-render.js +11 -2
- package/dist/src/ui/tui/shell-input.js +256 -73
- package/dist/src/ui/tui/shortcuts.js +61 -0
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/user-input.js +593 -0
- package/dist/src/update-check.js +205 -0
- package/dist/src/usage.js +5 -2
- package/dist/src/utils.js +13 -0
- package/dist/src/version.js +52 -2
- package/package.json +22 -9
- package/dist/src/project-index.js +0 -233
- 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,20 +1,33 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import { createRatatuiBridge } from './tui/bridge.js';
|
|
2
|
-
import {
|
|
3
|
+
import { bucketActionLabel, } from '../permissions.js';
|
|
4
|
+
import { AGENT_MODE_LOCKED_STATUS, approvalScrollLimit, buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, userInputViewportForFrame, } from './tui/build-frame.js';
|
|
5
|
+
import { createAttentionController, INITIAL_TERMINAL_FOCUS, } from './tui/attention.js';
|
|
6
|
+
import { backendFor, createNotificationQueue, } from './tui/desktop-notification.js';
|
|
7
|
+
import { readPreferences, writePreferences } from '../preferences.js';
|
|
3
8
|
import { createTerminalTitleController } from './tui/terminal-title.js';
|
|
4
9
|
import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
|
|
5
|
-
|
|
10
|
+
import { getInputCommandToken, getSlashCommandSuggestions, SLASH_COMMANDS, slashCommandMatchesToken, } from './slash-commands.js';
|
|
11
|
+
export { getInputCommandToken, getSlashCommandSuggestions, SLASH_COMMANDS, slashCommandMatchesToken, };
|
|
12
|
+
import { composerInputWidth, frameContentWidth } from './tui/composer-layout.js';
|
|
6
13
|
import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
7
14
|
import { chat, models } from '../api/index.js';
|
|
8
|
-
import { isTurnCancelledError } from '../api/chat.js';
|
|
15
|
+
import { isTurnCancelledError, isTurnOutcomeKnown } from '../api/chat.js';
|
|
16
|
+
import { formatTurnRecoveryStatus } from '../api/turn-recovery.js';
|
|
9
17
|
import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
|
|
10
18
|
import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
11
19
|
import { setScratchSession } from '../scratch-dir.js';
|
|
20
|
+
import { setImageStoreSession } from '../core/session-image-store.js';
|
|
12
21
|
import { cancelActiveCommand } from '../executor.js';
|
|
13
22
|
import { isTurnFailureMarker } from '../turn-failure-marker.js';
|
|
23
|
+
import { clearCliAuthConfig, logoutFromServer, } from '../api/auth.js';
|
|
24
|
+
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
25
|
+
import { buildQuotaNotice } from '../quota-notice.js';
|
|
26
|
+
import { createUserInputPromptState, formatUserInputTranscript, } from './tui/user-input.js';
|
|
14
27
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
15
28
|
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
16
|
-
import {
|
|
17
|
-
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
|
|
29
|
+
import { startNewConversation, } from '../session.js';
|
|
30
|
+
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../session-store.js';
|
|
18
31
|
import { truncate } from '../utils.js';
|
|
19
32
|
import { writeClipboardText } from '../core/clipboard.js';
|
|
20
33
|
import { openUrl } from '../core/open-url.js';
|
|
@@ -70,58 +83,46 @@ const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
|
70
83
|
const WORKING_TOOL_PREVIEW_ITEMS = 4;
|
|
71
84
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
72
85
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
export
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
command: '/jobs',
|
|
114
|
-
description: 'Background jobs: pick to view output or kill',
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
command: '/clear',
|
|
118
|
-
description: 'Clear conversation history',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
command: '/exit',
|
|
122
|
-
description: 'Quit the current session',
|
|
123
|
-
},
|
|
124
|
-
];
|
|
86
|
+
export const REPORT_ISSUE_URL = 'https://thegit.ai/report';
|
|
87
|
+
export function buildReportTranscriptEntries(openedUrl) {
|
|
88
|
+
return openedUrl
|
|
89
|
+
? [
|
|
90
|
+
{
|
|
91
|
+
body: `Opening the private bug report form in your browser. If nothing opened, visit:\n\n${REPORT_ISSUE_URL}\n`,
|
|
92
|
+
kind: 'system',
|
|
93
|
+
title: 'Report',
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
: [
|
|
97
|
+
{
|
|
98
|
+
body: `Could not open a browser. Report the bug manually at:\n\n${REPORT_ISSUE_URL}\n`,
|
|
99
|
+
kind: 'error',
|
|
100
|
+
title: 'Report',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
export function buildTurnFailureHintEntries(cancelled) {
|
|
105
|
+
if (cancelled)
|
|
106
|
+
return [];
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
body: 'Something went wrong? Use /report to submit the issue.',
|
|
110
|
+
kind: 'error',
|
|
111
|
+
title: 'Report',
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
export function composerAttachmentsAfterCancel(queuedAttachments, inFlightAttachments) {
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const kept = [];
|
|
118
|
+
for (const attachment of [...queuedAttachments, ...inFlightAttachments]) {
|
|
119
|
+
if (seen.has(attachment.index))
|
|
120
|
+
continue;
|
|
121
|
+
seen.add(attachment.index);
|
|
122
|
+
kept.push(attachment);
|
|
123
|
+
}
|
|
124
|
+
return kept;
|
|
125
|
+
}
|
|
125
126
|
function getShellWidth(columns) {
|
|
126
127
|
const safeColumns = Math.max(columns, 20);
|
|
127
128
|
const targetWidth = Math.floor(safeColumns * TUI_WIDTH_RATIO);
|
|
@@ -490,11 +491,43 @@ function buildWriteFileDiff(content) {
|
|
|
490
491
|
return '';
|
|
491
492
|
return `@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}`).join('\n')}`;
|
|
492
493
|
}
|
|
494
|
+
const MAX_DELETE_PREVIEW_LINES = 400;
|
|
493
495
|
function buildDeleteFileDiff(content) {
|
|
494
496
|
const lines = splitDiffLines(String(content ?? ''));
|
|
495
|
-
if (lines.length === 0)
|
|
497
|
+
if (lines.length === 0) {
|
|
498
|
+
return { removed: 0, text: '' };
|
|
499
|
+
}
|
|
500
|
+
const shown = lines.slice(0, MAX_DELETE_PREVIEW_LINES);
|
|
501
|
+
const rows = [
|
|
502
|
+
`@@ -1,${lines.length} +0,0 @@`,
|
|
503
|
+
...shown.map((line) => `-${line}`),
|
|
504
|
+
];
|
|
505
|
+
const omitted = lines.length - shown.length;
|
|
506
|
+
if (omitted > 0) {
|
|
507
|
+
rows.push(`@@ ${omitted} more removed line(s) not shown @@`);
|
|
508
|
+
}
|
|
509
|
+
return { removed: lines.length, text: rows.join('\n') };
|
|
510
|
+
}
|
|
511
|
+
function deletedFileSummary(result) {
|
|
512
|
+
const bytes = typeof result?.bytes === 'number' ? result.bytes : null;
|
|
513
|
+
if (bytes === null) {
|
|
496
514
|
return '';
|
|
497
|
-
|
|
515
|
+
}
|
|
516
|
+
const reason = result?.contentOmitted === 'binary' ? ', binary' : '';
|
|
517
|
+
return ` (${formatFileSize(bytes)}${reason})`;
|
|
518
|
+
}
|
|
519
|
+
function formatFileSize(bytes) {
|
|
520
|
+
if (bytes < 1024) {
|
|
521
|
+
return `${bytes} B`;
|
|
522
|
+
}
|
|
523
|
+
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
|
|
524
|
+
let value = bytes / 1024;
|
|
525
|
+
let unit = 0;
|
|
526
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
527
|
+
value /= 1024;
|
|
528
|
+
unit += 1;
|
|
529
|
+
}
|
|
530
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
498
531
|
}
|
|
499
532
|
function isFileChangeTool(name) {
|
|
500
533
|
return (name === 'patch_file' ||
|
|
@@ -510,13 +543,7 @@ function getToolFilePath(call, result) {
|
|
|
510
543
|
'').trim();
|
|
511
544
|
}
|
|
512
545
|
function formatToolResultState(result) {
|
|
513
|
-
|
|
514
|
-
return 'Skipped';
|
|
515
|
-
if (result?.blocked)
|
|
516
|
-
return 'Blocked';
|
|
517
|
-
if (result?.ok === true)
|
|
518
|
-
return 'OK';
|
|
519
|
-
return 'Failed';
|
|
546
|
+
return result?.skipped ? 'Skipped' : 'OK';
|
|
520
547
|
}
|
|
521
548
|
function formatRunCommandResultState(result) {
|
|
522
549
|
if (result?.timedOut)
|
|
@@ -554,12 +581,15 @@ function buildFileChangeEntry(event) {
|
|
|
554
581
|
: call.name === 'undo_edit'
|
|
555
582
|
? 'Undo'
|
|
556
583
|
: 'Edit';
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
584
|
+
if (skipped) {
|
|
585
|
+
return {
|
|
586
|
+
body: error || `${call.name} ${filePath}`,
|
|
587
|
+
filePath,
|
|
588
|
+
kind: 'system',
|
|
589
|
+
title: `${verb} skipped: ${filePath}`,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return { body: '', filePath, kind: 'system', title: `${verb}: ${filePath}` };
|
|
563
593
|
}
|
|
564
594
|
if (result?.scratch === true) {
|
|
565
595
|
const scratchName = truncate(filePath.split(/[\\/]/).filter(Boolean).at(-1) ?? filePath, 72);
|
|
@@ -620,9 +650,11 @@ function buildFileChangeEntry(event) {
|
|
|
620
650
|
};
|
|
621
651
|
}
|
|
622
652
|
const content = typeof result?.content === 'string' ? result.content : '';
|
|
623
|
-
const
|
|
624
|
-
const preview =
|
|
625
|
-
const summary = preview
|
|
653
|
+
const diff = buildDeleteFileDiff(content);
|
|
654
|
+
const preview = diff.text ? parseDiffPreview(diff.text) : undefined;
|
|
655
|
+
const summary = preview
|
|
656
|
+
? ` (+0 -${diff.removed})`
|
|
657
|
+
: deletedFileSummary(result);
|
|
626
658
|
return {
|
|
627
659
|
body: '',
|
|
628
660
|
diffPreview: preview,
|
|
@@ -672,24 +704,27 @@ function buildFileChangeEntry(event) {
|
|
|
672
704
|
title: `Edited ${filePath}${summary}`,
|
|
673
705
|
};
|
|
674
706
|
}
|
|
675
|
-
function buildWorkingToolEntry(event) {
|
|
707
|
+
export function buildWorkingToolEntry(event) {
|
|
676
708
|
const { call, result } = event;
|
|
677
|
-
const
|
|
709
|
+
const failed = result?.ok !== true && result?.skipped !== true;
|
|
710
|
+
const kind = result?.ok === true ? 'tool' : 'system';
|
|
711
|
+
const state = (text) => (failed ? '' : text);
|
|
712
|
+
const error = !failed && typeof result?.error === 'string' && result.error.trim()
|
|
678
713
|
? `\n${truncate(result.error.trim(), 180)}`
|
|
679
714
|
: '';
|
|
680
715
|
if (call.name === 'run_command') {
|
|
681
716
|
const command = String(call.args?.command ?? result?.command ?? '').trim();
|
|
682
717
|
if (result?.backgrounded === true && result?.jobId) {
|
|
683
718
|
return {
|
|
684
|
-
body: `$ ${truncate(command, 180)}
|
|
685
|
-
kind
|
|
719
|
+
body: `$ ${truncate(command, 180)}${state(`\nBackground job ${String(result.jobId)} started.`)}${error}`,
|
|
720
|
+
kind,
|
|
686
721
|
preformatted: true,
|
|
687
722
|
title: 'Shell',
|
|
688
723
|
};
|
|
689
724
|
}
|
|
690
725
|
return {
|
|
691
|
-
body: `$ ${truncate(command, 180)}
|
|
692
|
-
kind
|
|
726
|
+
body: `$ ${truncate(command, 180)}${state(`\n${formatRunCommandResultState(result)}`)}${error}`,
|
|
727
|
+
kind,
|
|
693
728
|
title: 'Shell',
|
|
694
729
|
};
|
|
695
730
|
}
|
|
@@ -701,15 +736,15 @@ function buildWorkingToolEntry(event) {
|
|
|
701
736
|
? `${status}${exitCode != null ? ` (code ${exitCode})` : ''}`
|
|
702
737
|
: formatToolResultState(result);
|
|
703
738
|
return {
|
|
704
|
-
body: `${jobId || '(unknown job)'} · ${stateText}${error}`,
|
|
705
|
-
kind
|
|
739
|
+
body: `${jobId || '(unknown job)'}${state(` · ${stateText}`)}${error}`,
|
|
740
|
+
kind,
|
|
706
741
|
title: call.name === 'shell_job_kill' ? 'Kill job' : 'Job output',
|
|
707
742
|
};
|
|
708
743
|
}
|
|
709
744
|
if (call.name === 'run_node_script') {
|
|
710
745
|
return {
|
|
711
|
-
body: `node --input-type=module <script via stdin
|
|
712
|
-
kind
|
|
746
|
+
body: `node --input-type=module <script via stdin>${state(`\n${formatRunCommandResultState(result)}`)}${error}`,
|
|
747
|
+
kind,
|
|
713
748
|
title: 'Node',
|
|
714
749
|
};
|
|
715
750
|
}
|
|
@@ -718,8 +753,8 @@ function buildWorkingToolEntry(event) {
|
|
|
718
753
|
}
|
|
719
754
|
const args = formatToolArgs(call, result);
|
|
720
755
|
return {
|
|
721
|
-
body: `${call.name}${args ? ` ${args}` : ''} -> ${formatToolResultState(result)}${error}`,
|
|
722
|
-
kind
|
|
756
|
+
body: `${call.name}${args ? ` ${args}` : ''}${state(` -> ${formatToolResultState(result)}`)}${error}`,
|
|
757
|
+
kind,
|
|
723
758
|
title: 'Tool',
|
|
724
759
|
};
|
|
725
760
|
}
|
|
@@ -785,10 +820,18 @@ function displayUserTextFromHistoryEntry(entry) {
|
|
|
785
820
|
export function buildTranscriptFromSessionHistory(history) {
|
|
786
821
|
const entries = [];
|
|
787
822
|
const pendingCalls = new Map();
|
|
823
|
+
const pendingUserInputRequests = new Map();
|
|
788
824
|
for (const entry of history) {
|
|
789
825
|
if (!entry?.parts?.length)
|
|
790
826
|
continue;
|
|
791
827
|
for (const part of entry.parts) {
|
|
828
|
+
const userInputRequest = part?.userInputRequest;
|
|
829
|
+
if (userInputRequest && typeof userInputRequest === 'object') {
|
|
830
|
+
const requestId = String(userInputRequest.requestId ?? '').trim();
|
|
831
|
+
if (requestId && Array.isArray(userInputRequest.questions)) {
|
|
832
|
+
pendingUserInputRequests.set(requestId, userInputRequest.questions);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
792
835
|
const call = part?.functionCall;
|
|
793
836
|
if (!call || typeof call !== 'object')
|
|
794
837
|
continue;
|
|
@@ -798,6 +841,21 @@ export function buildTranscriptFromSessionHistory(history) {
|
|
|
798
841
|
pendingCalls.set(callId, call);
|
|
799
842
|
}
|
|
800
843
|
for (const part of entry.parts) {
|
|
844
|
+
const userInputResult = part?.userInputResult;
|
|
845
|
+
if (userInputResult && typeof userInputResult === 'object') {
|
|
846
|
+
const requestId = String(userInputResult.requestId ?? '').trim();
|
|
847
|
+
const questions = pendingUserInputRequests.get(requestId);
|
|
848
|
+
const result = userInputResult.result;
|
|
849
|
+
if (questions &&
|
|
850
|
+
(result?.status === 'submitted' || result?.status === 'cancelled')) {
|
|
851
|
+
pendingUserInputRequests.delete(requestId);
|
|
852
|
+
entries.push({
|
|
853
|
+
body: formatUserInputTranscript(questions, result),
|
|
854
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
855
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
801
859
|
const functionResponse = part?.functionResponse;
|
|
802
860
|
if (!functionResponse || typeof functionResponse !== 'object')
|
|
803
861
|
continue;
|
|
@@ -814,10 +872,10 @@ export function buildTranscriptFromSessionHistory(history) {
|
|
|
814
872
|
}
|
|
815
873
|
if (!call || !isFileChangeTool(call.name))
|
|
816
874
|
continue;
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}));
|
|
875
|
+
const result = (functionResponse.response ?? {});
|
|
876
|
+
if (result.ok !== true && result.skipped !== true)
|
|
877
|
+
continue;
|
|
878
|
+
entries.push(buildFileChangeEntry({ call, result }));
|
|
821
879
|
}
|
|
822
880
|
if (entry.role === 'user' && entry.kind === 'turnStart') {
|
|
823
881
|
const text = displayUserTextFromHistoryEntry(entry);
|
|
@@ -826,6 +884,13 @@ export function buildTranscriptFromSessionHistory(history) {
|
|
|
826
884
|
}
|
|
827
885
|
continue;
|
|
828
886
|
}
|
|
887
|
+
if (entry.role === 'user' && entry.kind === 'userInterjection') {
|
|
888
|
+
const text = String(entry.userInput ?? '').trim();
|
|
889
|
+
if (text) {
|
|
890
|
+
entries.push({ body: text, kind: 'user', title: 'You · sent mid-turn' });
|
|
891
|
+
}
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
829
894
|
const text = textFromHistoryEntry(entry);
|
|
830
895
|
if ((entry.role === 'model' || entry.role === 'assistant') && text) {
|
|
831
896
|
if (isTurnFailureMarker(text)) {
|
|
@@ -854,6 +919,23 @@ function createSessionTranscript(session) {
|
|
|
854
919
|
const transcript = buildTranscriptFromSessionHistory(session.history);
|
|
855
920
|
return transcript.length ? transcript : createInitialTranscript();
|
|
856
921
|
}
|
|
922
|
+
export function isJobsPickerCommand(input) {
|
|
923
|
+
return String(input ?? '').trim() === '/jobs';
|
|
924
|
+
}
|
|
925
|
+
export function statusBehindJobsPicker(state) {
|
|
926
|
+
return state.jobsPickerOpen
|
|
927
|
+
? (state.jobsPickerReturnStatus ?? 'Ready')
|
|
928
|
+
: state.status;
|
|
929
|
+
}
|
|
930
|
+
export function closedJobsPickerFields(state) {
|
|
931
|
+
if (!state.jobsPickerOpen)
|
|
932
|
+
return {};
|
|
933
|
+
return {
|
|
934
|
+
jobsPickerExpandedId: null,
|
|
935
|
+
jobsPickerOpen: false,
|
|
936
|
+
jobsPickerReturnStatus: null,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
857
939
|
function createShellStore(initialState) {
|
|
858
940
|
let state = initialState;
|
|
859
941
|
let nextEntryId = 1;
|
|
@@ -961,11 +1043,15 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
961
1043
|
activeTurnInput: '',
|
|
962
1044
|
activeTurnInputPreformatted: false,
|
|
963
1045
|
agentMode: session.agentMode,
|
|
964
|
-
|
|
1046
|
+
analyzingImages: 0,
|
|
1047
|
+
generatingImage: false,
|
|
1048
|
+
approvalCursor: 0,
|
|
965
1049
|
approvalPrompt: null,
|
|
1050
|
+
approvalScrollOffset: 0,
|
|
966
1051
|
autoYes: session.autoYes,
|
|
967
1052
|
backgroundJobs: [],
|
|
968
1053
|
busy: false,
|
|
1054
|
+
busyPausedAt: null,
|
|
969
1055
|
busySince: null,
|
|
970
1056
|
clockNow: Date.now(),
|
|
971
1057
|
commandCursor: 0,
|
|
@@ -976,11 +1062,15 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
976
1062
|
exiting: false,
|
|
977
1063
|
input: '',
|
|
978
1064
|
maxToolSteps: session.maxToolSteps,
|
|
1065
|
+
notificationsEnabled: readPreferences(session.env).notifications,
|
|
1066
|
+
terminalFocused: INITIAL_TERMINAL_FOCUS,
|
|
979
1067
|
jobsPickerExpandedId: null,
|
|
980
1068
|
jobsPickerIndex: 0,
|
|
981
1069
|
jobsPickerOpen: false,
|
|
982
1070
|
modelPickerIndex: getDefaultModelPickerIndex(session.modelId, serverModels.models),
|
|
983
1071
|
modelPickerOpen: false,
|
|
1072
|
+
shortcutsOpen: false,
|
|
1073
|
+
transientStatus: null,
|
|
984
1074
|
projectRoot: session.rootDir,
|
|
985
1075
|
sessionId: session.sessionId,
|
|
986
1076
|
showSessionId: debugUi.showSessionId,
|
|
@@ -993,6 +1083,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
993
1083
|
resumePickerSessions: [],
|
|
994
1084
|
transcriptScrollOffset: 0,
|
|
995
1085
|
queuedMessage: null,
|
|
1086
|
+
turnMessages: [],
|
|
996
1087
|
serverModels: serverModels.models,
|
|
997
1088
|
sudoPrompt: null,
|
|
998
1089
|
status: 'Ready',
|
|
@@ -1003,6 +1094,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
1003
1094
|
tokenUsage: formatClientTokenUsage(null),
|
|
1004
1095
|
transcript: [],
|
|
1005
1096
|
turnCounter: Math.max(0, session.history.filter((entry) => entry.role === 'user').length),
|
|
1097
|
+
userInputPrompt: null,
|
|
1006
1098
|
pastedChunks: [],
|
|
1007
1099
|
imageAttachments: [],
|
|
1008
1100
|
workingTools: [],
|
|
@@ -1019,7 +1111,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1019
1111
|
const reasoningTokens = usageSummary?.reasoningTokens ?? 0;
|
|
1020
1112
|
const cacheTokens = usageSummary?.cacheTokens ?? 0;
|
|
1021
1113
|
const cacheWriteTokens = usageSummary?.cacheWriteTokens ?? 0;
|
|
1022
|
-
const indexTokens = usageSummary?.indexTokens ?? 0;
|
|
1023
1114
|
return [
|
|
1024
1115
|
'Session tokens',
|
|
1025
1116
|
`in ${formatTokenCount(inputTokens)}`,
|
|
@@ -1027,7 +1118,6 @@ export function formatClientTokenUsage(_responseTimeMs, usageSummary = null) {
|
|
|
1027
1118
|
...(reasoningTokens > 0 ? [`think ${formatTokenCount(reasoningTokens)}`] : []),
|
|
1028
1119
|
`cache ${formatTokenCount(cacheTokens)}`,
|
|
1029
1120
|
`write ${formatTokenCount(cacheWriteTokens)}`,
|
|
1030
|
-
`index ${formatTokenCount(indexTokens)}`,
|
|
1031
1121
|
].join(' • ');
|
|
1032
1122
|
}
|
|
1033
1123
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
@@ -1070,6 +1160,24 @@ function splitThinkingLines(text) {
|
|
|
1070
1160
|
.map((part) => part.trim())
|
|
1071
1161
|
.filter(Boolean));
|
|
1072
1162
|
}
|
|
1163
|
+
const LIVE_STATUS_PANEL_MAX_CHARS = 72;
|
|
1164
|
+
const TOOL_PROGRESS_STATUS_PATTERN = /^(?:Tool:\s|Running\s\S+(?:\slocally)?\.\.\.$)/i;
|
|
1165
|
+
export function isToolProgressStatus(status) {
|
|
1166
|
+
return TOOL_PROGRESS_STATUS_PATTERN.test(String(status ?? '').trim());
|
|
1167
|
+
}
|
|
1168
|
+
function holdOrPickFallback(currentTitle) {
|
|
1169
|
+
return THINKING_FALLBACK_PHRASES.includes(currentTitle.trim())
|
|
1170
|
+
? currentTitle
|
|
1171
|
+
: pickThinkingFallbackPhrase();
|
|
1172
|
+
}
|
|
1173
|
+
function liveStatusPanelTitle(status) {
|
|
1174
|
+
const text = String(status ?? '').trim();
|
|
1175
|
+
if (!text || text.includes('\n'))
|
|
1176
|
+
return '';
|
|
1177
|
+
if (isToolProgressStatus(text))
|
|
1178
|
+
return '';
|
|
1179
|
+
return text.length <= LIVE_STATUS_PANEL_MAX_CHARS ? text : '';
|
|
1180
|
+
}
|
|
1073
1181
|
function thinkingPanelFromStatus(status) {
|
|
1074
1182
|
const rawText = thinkingNoteFromStatus(status);
|
|
1075
1183
|
if (!rawText)
|
|
@@ -1093,7 +1201,7 @@ function thinkingPanelFromStatus(status) {
|
|
|
1093
1201
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
1094
1202
|
return {
|
|
1095
1203
|
title,
|
|
1096
|
-
notes: splitThinkingLines(bodyLines.join('\n')).slice(
|
|
1204
|
+
notes: splitThinkingLines(bodyLines.join('\n')).slice(0, 3),
|
|
1097
1205
|
};
|
|
1098
1206
|
}
|
|
1099
1207
|
export const EXIT_CTRL_C_CONFIRM_MESSAGE = 'Press Ctrl+C again to quit.';
|
|
@@ -1101,16 +1209,6 @@ export const EXIT_CTRL_C_CONFIRM_MS = 3000;
|
|
|
1101
1209
|
export function isExitConfirmActive(exitConfirmUntil, now = Date.now()) {
|
|
1102
1210
|
return exitConfirmUntil != null && now < exitConfirmUntil;
|
|
1103
1211
|
}
|
|
1104
|
-
export function getInputCommandToken(input) {
|
|
1105
|
-
const trimmed = String(input ?? '').trim();
|
|
1106
|
-
if (!trimmed.startsWith('/'))
|
|
1107
|
-
return '';
|
|
1108
|
-
const firstSpaceIndex = trimmed.indexOf(' ');
|
|
1109
|
-
const token = firstSpaceIndex === -1 ? trimmed : trimmed.slice(0, firstSpaceIndex);
|
|
1110
|
-
if (token.indexOf('/', 1) !== -1)
|
|
1111
|
-
return '';
|
|
1112
|
-
return token;
|
|
1113
|
-
}
|
|
1114
1212
|
function shouldShowCommandPalette(state) {
|
|
1115
1213
|
const trimmed = String(state.input ?? '').trim();
|
|
1116
1214
|
return (!state.busy &&
|
|
@@ -1118,8 +1216,9 @@ function shouldShowCommandPalette(state) {
|
|
|
1118
1216
|
!state.modelPickerOpen &&
|
|
1119
1217
|
!state.resumePickerOpen &&
|
|
1120
1218
|
trimmed.startsWith('/') &&
|
|
1121
|
-
!
|
|
1122
|
-
getInputCommandToken(trimmed) !== ''
|
|
1219
|
+
!String(state.input ?? '').includes(' ') &&
|
|
1220
|
+
getInputCommandToken(trimmed) !== '' &&
|
|
1221
|
+
getSlashCommandSuggestions(trimmed).length > 0);
|
|
1123
1222
|
}
|
|
1124
1223
|
export function shouldRemountLiveFrameForComposerInputChange(current, nextInput) {
|
|
1125
1224
|
const currentShowsCommands = shouldShowCommandPalette(current);
|
|
@@ -1142,9 +1241,6 @@ export function applySlashCommandSuggestion(currentInput, suggestion) {
|
|
|
1142
1241
|
input: `${before}${suggestion.command}${after}`,
|
|
1143
1242
|
};
|
|
1144
1243
|
}
|
|
1145
|
-
export function isExactSlashCommandToken(token) {
|
|
1146
|
-
return SLASH_COMMANDS.some((command) => command.command === token);
|
|
1147
|
-
}
|
|
1148
1244
|
export function insertAtCursor(state, text) {
|
|
1149
1245
|
const before = state.input.slice(0, state.cursor);
|
|
1150
1246
|
const after = state.input.slice(state.cursor);
|
|
@@ -1223,26 +1319,37 @@ export function navigatePromptHistory(state, direction) {
|
|
|
1223
1319
|
promptHistoryCursor: nextCursor,
|
|
1224
1320
|
};
|
|
1225
1321
|
}
|
|
1226
|
-
function getDefaultApprovalCursor() {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1322
|
+
export function getDefaultApprovalCursor(optionCount) {
|
|
1323
|
+
return Math.max(0, optionCount - 1);
|
|
1324
|
+
}
|
|
1325
|
+
export function getNextApprovalCursor(currentIndex, direction, optionCount) {
|
|
1326
|
+
if (optionCount <= 0)
|
|
1327
|
+
return 0;
|
|
1328
|
+
return (currentIndex + direction + optionCount) % optionCount;
|
|
1329
|
+
}
|
|
1330
|
+
export function pauseBusyClock(state, nowMs) {
|
|
1331
|
+
if (state.busySince === null || state.busyPausedAt !== null)
|
|
1332
|
+
return state;
|
|
1333
|
+
return { ...state, busyPausedAt: nowMs };
|
|
1334
|
+
}
|
|
1335
|
+
export function resumeBusyClock(state, nowMs) {
|
|
1336
|
+
if (state.busyPausedAt === null)
|
|
1337
|
+
return state;
|
|
1338
|
+
return {
|
|
1339
|
+
...state,
|
|
1340
|
+
busyPausedAt: null,
|
|
1341
|
+
busySince: state.busySince === null
|
|
1342
|
+
? null
|
|
1343
|
+
: state.busySince + Math.max(0, nowMs - state.busyPausedAt),
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
export function busyElapsedMs(state, nowMs) {
|
|
1347
|
+
if (state.busySince === null)
|
|
1348
|
+
return null;
|
|
1349
|
+
return Math.max(0, (state.busyPausedAt ?? nowMs) - state.busySince);
|
|
1350
|
+
}
|
|
1351
|
+
export function busyElapsedSeconds(state, nowMs) {
|
|
1352
|
+
return Math.floor((busyElapsedMs(state, nowMs) ?? 0) / 1000);
|
|
1246
1353
|
}
|
|
1247
1354
|
export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
1248
1355
|
return serverModels.map((model) => ({
|
|
@@ -1280,27 +1387,33 @@ function appendCommandLog(state, text) {
|
|
|
1280
1387
|
};
|
|
1281
1388
|
}
|
|
1282
1389
|
async function saveSessionBoth({ serverSessionClient, session, }) {
|
|
1390
|
+
if (!sessionHasUserMessage(session))
|
|
1391
|
+
return;
|
|
1283
1392
|
saveSessionState(session);
|
|
1284
1393
|
await serverSessionClient.save(session);
|
|
1285
1394
|
}
|
|
1286
|
-
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi,
|
|
1395
|
+
export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
1287
1396
|
if (process.stdin.isTTY !== true) {
|
|
1288
1397
|
throw new Error('stdin is not a terminal');
|
|
1289
1398
|
}
|
|
1290
1399
|
if (process.stdout.isTTY !== true) {
|
|
1291
1400
|
throw new Error('stdout is not a terminal');
|
|
1292
1401
|
}
|
|
1402
|
+
let fatalError = null;
|
|
1403
|
+
let signedOut = false;
|
|
1293
1404
|
await withTuiMode(async () => {
|
|
1294
1405
|
setBackgroundJobSession(session.sessionId);
|
|
1295
1406
|
setScratchSession(session.sessionId);
|
|
1407
|
+
setImageStoreSession(session.sessionId);
|
|
1296
1408
|
setTodoSession(session.sessionId);
|
|
1297
1409
|
const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
|
|
1298
1410
|
store.replaceTranscript(createSessionTranscript(session));
|
|
1299
1411
|
let currentServerModels = serverModels;
|
|
1300
1412
|
let sessionAutoYes = session.autoYes;
|
|
1301
1413
|
let resolveDone = null;
|
|
1302
|
-
let
|
|
1414
|
+
let resolvePermissionDecision = null;
|
|
1303
1415
|
let resolveSudoPassword = null;
|
|
1416
|
+
let pendingUserInput = null;
|
|
1304
1417
|
let cleanupSudoPasswordPrompt = null;
|
|
1305
1418
|
let sudoPasswordBuffer = '';
|
|
1306
1419
|
const bridge = createRatatuiBridge();
|
|
@@ -1317,6 +1430,11 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1317
1430
|
let latestUsageSummary = null;
|
|
1318
1431
|
let pendingTurnEntries = [];
|
|
1319
1432
|
let activeTurnAbort = null;
|
|
1433
|
+
let activeTurnImageAttachments = [];
|
|
1434
|
+
let activeServerTurnId = null;
|
|
1435
|
+
let unacknowledged = new Map();
|
|
1436
|
+
const blockedRows = new WeakMap();
|
|
1437
|
+
let newConversationInFlight = false;
|
|
1320
1438
|
let todosTouchedThisTurn = false;
|
|
1321
1439
|
const syncTodosState = () => {
|
|
1322
1440
|
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
@@ -1325,6 +1443,36 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1325
1443
|
let exitCtrlCArmed = false;
|
|
1326
1444
|
let exitCtrlCTimer = null;
|
|
1327
1445
|
let transientStatusTimer = null;
|
|
1446
|
+
const THINKING_MIN_DWELL_MS = 3_000;
|
|
1447
|
+
let thinkingAppliedAt = 0;
|
|
1448
|
+
let pendingThinking = null;
|
|
1449
|
+
const applyThinking = (next) => {
|
|
1450
|
+
thinkingAppliedAt = Date.now();
|
|
1451
|
+
pendingThinking = null;
|
|
1452
|
+
store.update((current) => ({
|
|
1453
|
+
...current,
|
|
1454
|
+
thinkingTitle: next.title || holdOrPickFallback(current.thinkingTitle),
|
|
1455
|
+
thinkingNotes: next.notes,
|
|
1456
|
+
}));
|
|
1457
|
+
};
|
|
1458
|
+
const queueThinkingUpdate = (next) => {
|
|
1459
|
+
if (Date.now() - thinkingAppliedAt >= THINKING_MIN_DWELL_MS) {
|
|
1460
|
+
applyThinking(next);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
pendingThinking = next;
|
|
1464
|
+
};
|
|
1465
|
+
const flushPendingThinking = () => {
|
|
1466
|
+
if (!pendingThinking)
|
|
1467
|
+
return;
|
|
1468
|
+
if (Date.now() - thinkingAppliedAt < THINKING_MIN_DWELL_MS)
|
|
1469
|
+
return;
|
|
1470
|
+
applyThinking(pendingThinking);
|
|
1471
|
+
};
|
|
1472
|
+
const resetThinkingPacer = () => {
|
|
1473
|
+
thinkingAppliedAt = 0;
|
|
1474
|
+
pendingThinking = null;
|
|
1475
|
+
};
|
|
1328
1476
|
const done = new Promise((resolve) => {
|
|
1329
1477
|
resolveDone = resolve;
|
|
1330
1478
|
});
|
|
@@ -1338,15 +1486,17 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1338
1486
|
void remountTui();
|
|
1339
1487
|
}, 0);
|
|
1340
1488
|
};
|
|
1341
|
-
const showTransientStatus = (
|
|
1489
|
+
const showTransientStatus = (transientStatus) => {
|
|
1342
1490
|
if (transientStatusTimer) {
|
|
1343
1491
|
clearTimeout(transientStatusTimer);
|
|
1344
1492
|
transientStatusTimer = null;
|
|
1345
1493
|
}
|
|
1346
|
-
store.update((current) => ({ ...current,
|
|
1494
|
+
store.update((current) => ({ ...current, transientStatus }));
|
|
1347
1495
|
transientStatusTimer = setTimeout(() => {
|
|
1348
1496
|
transientStatusTimer = null;
|
|
1349
|
-
store.update((current) => current.
|
|
1497
|
+
store.update((current) => current.transientStatus === transientStatus
|
|
1498
|
+
? { ...current, transientStatus: null }
|
|
1499
|
+
: current);
|
|
1350
1500
|
}, 2500);
|
|
1351
1501
|
};
|
|
1352
1502
|
const terminalTitle = createTerminalTitleController({
|
|
@@ -1359,14 +1509,43 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1359
1509
|
busy: state.busy,
|
|
1360
1510
|
});
|
|
1361
1511
|
};
|
|
1512
|
+
const notifier = backendFor();
|
|
1513
|
+
let notificationHandle = null;
|
|
1514
|
+
const projectLabel = basename(session.rootDir) || 'TheGitAI';
|
|
1515
|
+
const notifications = createNotificationQueue();
|
|
1516
|
+
const attention = createAttentionController({
|
|
1517
|
+
notify: (kind) => {
|
|
1518
|
+
notifications.run(async () => {
|
|
1519
|
+
notificationHandle = await notifier.post({
|
|
1520
|
+
title: projectLabel,
|
|
1521
|
+
body: kind === 'blocked'
|
|
1522
|
+
? 'Waiting for your answer'
|
|
1523
|
+
: 'Finished \u2014 your turn',
|
|
1524
|
+
}, notificationHandle);
|
|
1525
|
+
});
|
|
1526
|
+
},
|
|
1527
|
+
withdraw: () => {
|
|
1528
|
+
notifications.run(async () => {
|
|
1529
|
+
notifier.withdraw(notificationHandle);
|
|
1530
|
+
notificationHandle = null;
|
|
1531
|
+
});
|
|
1532
|
+
},
|
|
1533
|
+
});
|
|
1534
|
+
const syncAttention = () => {
|
|
1535
|
+
const state = store.getState();
|
|
1536
|
+
attention.sync({
|
|
1537
|
+
awaitingAnswer: Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt),
|
|
1538
|
+
busy: state.busy,
|
|
1539
|
+
focused: state.terminalFocused,
|
|
1540
|
+
}, state.notificationsEnabled);
|
|
1541
|
+
};
|
|
1362
1542
|
const renderCurrentFrame = () => {
|
|
1363
1543
|
if (!tuiReady)
|
|
1364
1544
|
return;
|
|
1365
1545
|
const state = store.getState();
|
|
1366
1546
|
syncTerminalTitle();
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
: 0;
|
|
1547
|
+
syncAttention();
|
|
1548
|
+
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1370
1549
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1371
1550
|
};
|
|
1372
1551
|
const remountTui = async () => {
|
|
@@ -1492,15 +1671,16 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1492
1671
|
}
|
|
1493
1672
|
};
|
|
1494
1673
|
const dismissPendingApproval = () => {
|
|
1495
|
-
if (!
|
|
1674
|
+
if (!resolvePermissionDecision)
|
|
1496
1675
|
return;
|
|
1497
|
-
const pendingResolve =
|
|
1498
|
-
|
|
1499
|
-
pendingResolve('
|
|
1676
|
+
const pendingResolve = resolvePermissionDecision;
|
|
1677
|
+
resolvePermissionDecision = null;
|
|
1678
|
+
pendingResolve({ kind: 'deny' });
|
|
1500
1679
|
store.update((current) => ({
|
|
1501
|
-
...current,
|
|
1502
|
-
approvalCursor:
|
|
1680
|
+
...resumeBusyClock(current, Date.now()),
|
|
1681
|
+
approvalCursor: 0,
|
|
1503
1682
|
approvalPrompt: null,
|
|
1683
|
+
approvalScrollOffset: 0,
|
|
1504
1684
|
}));
|
|
1505
1685
|
};
|
|
1506
1686
|
const dismissPendingSudoPassword = () => {
|
|
@@ -1513,21 +1693,40 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1513
1693
|
sudoPasswordBuffer = '';
|
|
1514
1694
|
pendingResolve(null);
|
|
1515
1695
|
store.update((current) => ({
|
|
1516
|
-
...current,
|
|
1696
|
+
...resumeBusyClock(current, Date.now()),
|
|
1517
1697
|
sudoPrompt: null,
|
|
1518
1698
|
}));
|
|
1519
1699
|
};
|
|
1700
|
+
const dismissPendingUserInput = () => {
|
|
1701
|
+
const pending = pendingUserInput;
|
|
1702
|
+
if (!pending)
|
|
1703
|
+
return;
|
|
1704
|
+
pendingUserInput = null;
|
|
1705
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1706
|
+
const error = new Error('Turn cancelled.');
|
|
1707
|
+
error.name = 'AbortError';
|
|
1708
|
+
pending.reject(error);
|
|
1709
|
+
store.update((current) => ({
|
|
1710
|
+
...resumeBusyClock(current, Date.now()),
|
|
1711
|
+
status: current.userInputPrompt?.returnStatus ?? current.status,
|
|
1712
|
+
userInputPrompt: null,
|
|
1713
|
+
}));
|
|
1714
|
+
};
|
|
1520
1715
|
const cancelActiveTurn = () => {
|
|
1521
|
-
if (!store.getState().busy)
|
|
1716
|
+
if (!store.getState().busy || newConversationInFlight)
|
|
1522
1717
|
return;
|
|
1523
1718
|
disarmExitConfirm();
|
|
1524
1719
|
dismissPendingApproval();
|
|
1525
1720
|
dismissPendingSudoPassword();
|
|
1721
|
+
dismissPendingUserInput();
|
|
1526
1722
|
activeTurnGeneration += 1;
|
|
1723
|
+
resetThinkingPacer();
|
|
1527
1724
|
activeTurnAbort?.abort();
|
|
1528
1725
|
activeTurnAbort = null;
|
|
1529
1726
|
cancelActiveCommand();
|
|
1530
1727
|
const queued = store.getState().queuedMessage;
|
|
1728
|
+
const restoredAttachments = composerAttachmentsAfterCancel(queued ? queued.imageAttachments : [], activeTurnImageAttachments);
|
|
1729
|
+
activeTurnImageAttachments = [];
|
|
1531
1730
|
const cancelledEntries = [
|
|
1532
1731
|
...takePendingTurnEntries(),
|
|
1533
1732
|
{
|
|
@@ -1541,20 +1740,23 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1541
1740
|
activeTurnInput: '',
|
|
1542
1741
|
activeTurnInputPreformatted: false,
|
|
1543
1742
|
busy: false,
|
|
1743
|
+
busyPausedAt: null,
|
|
1544
1744
|
busySince: null,
|
|
1545
1745
|
commandLog: [],
|
|
1546
1746
|
cursor: queued ? queued.body.length : current.cursor,
|
|
1547
|
-
imageAttachments:
|
|
1747
|
+
imageAttachments: restoredAttachments,
|
|
1548
1748
|
input: queued ? queued.body : current.input,
|
|
1549
1749
|
pastedChunks: queued ? queued.pastedChunks : current.pastedChunks,
|
|
1550
1750
|
promptHistoryCursor: queued ? null : current.promptHistoryCursor,
|
|
1551
1751
|
promptHistoryDraft: queued ? '' : current.promptHistoryDraft,
|
|
1552
1752
|
queuedMessage: null,
|
|
1753
|
+
recoveryStatus: '',
|
|
1553
1754
|
status: 'Ready',
|
|
1554
1755
|
thinkingTitle: '',
|
|
1555
1756
|
thinkingNotes: [],
|
|
1757
|
+
turnMessages: [],
|
|
1556
1758
|
workingTools: [],
|
|
1557
|
-
tokenUsage: formatClientTokenUsage(current
|
|
1759
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1558
1760
|
}));
|
|
1559
1761
|
appendStaticEntries(cancelledEntries);
|
|
1560
1762
|
lastTurnStartedAt = null;
|
|
@@ -1562,14 +1764,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1562
1764
|
scheduleLiveFrameRemount();
|
|
1563
1765
|
void remountTui();
|
|
1564
1766
|
};
|
|
1565
|
-
const saveActiveSession = async () => {
|
|
1566
|
-
try {
|
|
1567
|
-
await saveSessionBoth({ serverSessionClient, session });
|
|
1568
|
-
}
|
|
1569
|
-
catch (error) {
|
|
1570
|
-
appendError(`Session save failed: ${error.message}`);
|
|
1571
|
-
}
|
|
1572
|
-
};
|
|
1573
1767
|
const syncShellStateFromSession = () => {
|
|
1574
1768
|
store.update((current) => ({
|
|
1575
1769
|
...current,
|
|
@@ -1600,6 +1794,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1600
1794
|
setAgentMode(nextAgentMode(session.agentMode));
|
|
1601
1795
|
scheduleLiveFrameRemount();
|
|
1602
1796
|
};
|
|
1797
|
+
const reportAgentModeLocked = () => {
|
|
1798
|
+
showTransientStatus(AGENT_MODE_LOCKED_STATUS);
|
|
1799
|
+
};
|
|
1603
1800
|
const clearLiveFrameRemountTimer = () => {
|
|
1604
1801
|
if (!liveFrameRemountTimer)
|
|
1605
1802
|
return;
|
|
@@ -1648,6 +1845,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1648
1845
|
}
|
|
1649
1846
|
dismissPendingApproval();
|
|
1650
1847
|
dismissPendingSudoPassword();
|
|
1848
|
+
dismissPendingUserInput();
|
|
1651
1849
|
exiting = true;
|
|
1652
1850
|
killAllBackgroundJobs();
|
|
1653
1851
|
store.update((current) => ({
|
|
@@ -1661,6 +1859,41 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1661
1859
|
resolveDone?.();
|
|
1662
1860
|
});
|
|
1663
1861
|
};
|
|
1862
|
+
const exitForAuthenticationError = (error) => {
|
|
1863
|
+
if (!isAuthenticationError(error))
|
|
1864
|
+
return false;
|
|
1865
|
+
clearCliAuthConfig(session.env);
|
|
1866
|
+
if (sessionHasUserMessage(session)) {
|
|
1867
|
+
saveSessionState(session);
|
|
1868
|
+
}
|
|
1869
|
+
fatalError = new Error(authenticationErrorMessage(error));
|
|
1870
|
+
requestExit();
|
|
1871
|
+
return true;
|
|
1872
|
+
};
|
|
1873
|
+
const saveActiveSession = async () => {
|
|
1874
|
+
try {
|
|
1875
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
catch (error) {
|
|
1879
|
+
if (exitForAuthenticationError(error))
|
|
1880
|
+
return false;
|
|
1881
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
const saveActiveSessionOrAbort = async () => {
|
|
1886
|
+
try {
|
|
1887
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1888
|
+
return true;
|
|
1889
|
+
}
|
|
1890
|
+
catch (error) {
|
|
1891
|
+
if (exitForAuthenticationError(error))
|
|
1892
|
+
return false;
|
|
1893
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1664
1897
|
const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
|
|
1665
1898
|
if (exiting || signal?.aborted) {
|
|
1666
1899
|
resolve(null);
|
|
@@ -1678,7 +1911,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1678
1911
|
cleanupSudoPasswordPrompt = null;
|
|
1679
1912
|
}
|
|
1680
1913
|
store.update((current) => ({
|
|
1681
|
-
...current,
|
|
1914
|
+
...pauseBusyClock(current, Date.now()),
|
|
1682
1915
|
sudoPrompt: {
|
|
1683
1916
|
command,
|
|
1684
1917
|
passwordLength: 0,
|
|
@@ -1698,7 +1931,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1698
1931
|
cleanupSudoPasswordPrompt = null;
|
|
1699
1932
|
sudoPasswordBuffer = '';
|
|
1700
1933
|
store.update((current) => ({
|
|
1701
|
-
...current,
|
|
1934
|
+
...resumeBusyClock(current, Date.now()),
|
|
1702
1935
|
sudoPrompt: null,
|
|
1703
1936
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1704
1937
|
}));
|
|
@@ -1712,7 +1945,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1712
1945
|
cleanupSudoPasswordPrompt = null;
|
|
1713
1946
|
sudoPasswordBuffer = '';
|
|
1714
1947
|
store.update((current) => ({
|
|
1715
|
-
...current,
|
|
1948
|
+
...resumeBusyClock(current, Date.now()),
|
|
1716
1949
|
sudoPrompt: null,
|
|
1717
1950
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1718
1951
|
}));
|
|
@@ -1732,38 +1965,95 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1732
1965
|
: null,
|
|
1733
1966
|
}));
|
|
1734
1967
|
};
|
|
1735
|
-
const
|
|
1736
|
-
if (exiting) {
|
|
1737
|
-
|
|
1968
|
+
const openUserInputPrompt = (questions, signal) => new Promise((resolve, reject) => {
|
|
1969
|
+
if (exiting || signal?.aborted) {
|
|
1970
|
+
const error = new Error('Turn cancelled.');
|
|
1971
|
+
error.name = 'AbortError';
|
|
1972
|
+
reject(error);
|
|
1738
1973
|
return;
|
|
1739
1974
|
}
|
|
1740
|
-
|
|
1975
|
+
const onAbort = () => dismissPendingUserInput();
|
|
1976
|
+
pendingUserInput = {
|
|
1977
|
+
questions,
|
|
1978
|
+
resolve,
|
|
1979
|
+
reject,
|
|
1980
|
+
signal,
|
|
1981
|
+
onAbort,
|
|
1982
|
+
};
|
|
1983
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1741
1984
|
store.update((current) => ({
|
|
1742
|
-
...current,
|
|
1743
|
-
|
|
1985
|
+
...pauseBusyClock(current, Date.now()),
|
|
1986
|
+
...closedJobsPickerFields(current),
|
|
1987
|
+
status: 'Waiting for your input',
|
|
1988
|
+
userInputPrompt: createUserInputPromptState(questions, statusBehindJobsPicker(current)),
|
|
1989
|
+
}));
|
|
1990
|
+
scheduleLiveFrameRemount();
|
|
1991
|
+
});
|
|
1992
|
+
const handleInlineUserInput = async (result) => {
|
|
1993
|
+
const pending = pendingUserInput;
|
|
1994
|
+
const prompt = store.getState().userInputPrompt;
|
|
1995
|
+
if (!pending || !prompt)
|
|
1996
|
+
return;
|
|
1997
|
+
pendingUserInput = null;
|
|
1998
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
1999
|
+
store.update((current) => ({
|
|
2000
|
+
...resumeBusyClock(current, Date.now()),
|
|
2001
|
+
status: prompt.returnStatus,
|
|
2002
|
+
userInputPrompt: null,
|
|
2003
|
+
}));
|
|
2004
|
+
appendTurnAwareEntry({
|
|
2005
|
+
body: formatUserInputTranscript(pending.questions, result),
|
|
2006
|
+
kind: result.status === 'submitted' ? 'user' : 'system',
|
|
2007
|
+
title: result.status === 'submitted' ? 'Your answers' : 'Question',
|
|
2008
|
+
});
|
|
2009
|
+
pending.resolve(result);
|
|
2010
|
+
scheduleLiveFrameRemount();
|
|
2011
|
+
};
|
|
2012
|
+
const openApprovalPrompt = (request, signal) => new Promise((resolve) => {
|
|
2013
|
+
const deny = { kind: 'deny' };
|
|
2014
|
+
if (exiting || signal?.aborted) {
|
|
2015
|
+
resolve(deny);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
const onAbort = () => dismissPendingApproval();
|
|
2019
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
2020
|
+
resolvePermissionDecision = (decision) => {
|
|
2021
|
+
signal?.removeEventListener('abort', onAbort);
|
|
2022
|
+
resolve(decision);
|
|
2023
|
+
};
|
|
2024
|
+
store.update((current) => ({
|
|
2025
|
+
...pauseBusyClock(current, Date.now()),
|
|
2026
|
+
...closedJobsPickerFields(current),
|
|
2027
|
+
approvalCursor: getDefaultApprovalCursor(request.options.length),
|
|
2028
|
+
approvalOpenedAt: Date.now(),
|
|
2029
|
+
approvalScrollOffset: 0,
|
|
1744
2030
|
approvalPrompt: {
|
|
1745
|
-
title,
|
|
1746
|
-
body,
|
|
1747
|
-
diffPreview:
|
|
1748
|
-
? parseDiffPreview(
|
|
2031
|
+
title: request.title,
|
|
2032
|
+
body: request.body,
|
|
2033
|
+
diffPreview: request.diff && request.filePath
|
|
2034
|
+
? parseDiffPreview(request.diff)
|
|
1749
2035
|
: undefined,
|
|
1750
|
-
filePath:
|
|
1751
|
-
|
|
2036
|
+
filePath: request.filePath,
|
|
2037
|
+
options: request.options,
|
|
2038
|
+
returnStatus: statusBehindJobsPicker(current),
|
|
1752
2039
|
},
|
|
1753
|
-
status: title,
|
|
2040
|
+
status: request.title,
|
|
1754
2041
|
}));
|
|
1755
2042
|
});
|
|
1756
|
-
const handleInlineApprovalChoice = async (
|
|
2043
|
+
const handleInlineApprovalChoice = async (index) => {
|
|
1757
2044
|
const current = store.getState();
|
|
1758
|
-
const
|
|
1759
|
-
|
|
2045
|
+
const options = current.approvalPrompt?.options ?? [];
|
|
2046
|
+
const chosen = options[index]?.decision ?? { kind: 'deny' };
|
|
2047
|
+
const pendingResolve = resolvePermissionDecision;
|
|
2048
|
+
resolvePermissionDecision = null;
|
|
1760
2049
|
store.update((next) => ({
|
|
1761
|
-
...next,
|
|
1762
|
-
approvalCursor: getDefaultApprovalCursor(),
|
|
2050
|
+
...resumeBusyClock(next, Date.now()),
|
|
2051
|
+
approvalCursor: getDefaultApprovalCursor(options.length),
|
|
1763
2052
|
approvalPrompt: null,
|
|
2053
|
+
approvalScrollOffset: 0,
|
|
1764
2054
|
status: current.approvalPrompt?.returnStatus ?? next.status,
|
|
1765
2055
|
}));
|
|
1766
|
-
pendingResolve?.(
|
|
2056
|
+
pendingResolve?.(chosen);
|
|
1767
2057
|
};
|
|
1768
2058
|
const refreshServerModels = async () => {
|
|
1769
2059
|
currentServerModels = await models.fetchServerModels({ config: authConfig });
|
|
@@ -1797,7 +2087,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1797
2087
|
selectedModelId: selected,
|
|
1798
2088
|
serverModels: fresh,
|
|
1799
2089
|
});
|
|
1800
|
-
await saveActiveSession()
|
|
2090
|
+
if (!(await saveActiveSession()))
|
|
2091
|
+
return;
|
|
1801
2092
|
syncShellStateFromSession();
|
|
1802
2093
|
appendStaticEntry({
|
|
1803
2094
|
body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
|
|
@@ -1805,24 +2096,35 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1805
2096
|
title: 'Model',
|
|
1806
2097
|
});
|
|
1807
2098
|
};
|
|
2099
|
+
const toggleNotifications = () => {
|
|
2100
|
+
const enabled = !store.getState().notificationsEnabled;
|
|
2101
|
+
store.update((current) => ({ ...current, notificationsEnabled: enabled }));
|
|
2102
|
+
try {
|
|
2103
|
+
writePreferences({ ...readPreferences(session.env), notifications: enabled }, session.env);
|
|
2104
|
+
}
|
|
2105
|
+
catch {
|
|
2106
|
+
}
|
|
2107
|
+
showTransientStatus(enabled ? 'Notifications on' : 'Notifications off');
|
|
2108
|
+
};
|
|
1808
2109
|
const openJobsPicker = () => {
|
|
2110
|
+
if (store.getState().jobsPickerOpen)
|
|
2111
|
+
return;
|
|
1809
2112
|
syncBackgroundJobsState();
|
|
1810
2113
|
store.update((current) => ({
|
|
1811
2114
|
...current,
|
|
1812
2115
|
commandCursor: 0,
|
|
1813
|
-
cursor: 0,
|
|
1814
|
-
input: '',
|
|
1815
2116
|
jobsPickerExpandedId: null,
|
|
1816
2117
|
jobsPickerIndex: 0,
|
|
1817
2118
|
jobsPickerOpen: true,
|
|
2119
|
+
jobsPickerReturnStatus: current.status,
|
|
1818
2120
|
status: 'Background jobs',
|
|
1819
2121
|
}));
|
|
2122
|
+
scheduleLiveFrameRemount();
|
|
1820
2123
|
};
|
|
1821
2124
|
const appendJobOutputEntry = async (jobId) => {
|
|
1822
2125
|
const id = jobId.trim();
|
|
1823
2126
|
await collectBackgroundJobUiOutputMutations({
|
|
1824
2127
|
session,
|
|
1825
|
-
projectIndex,
|
|
1826
2128
|
jobId: id,
|
|
1827
2129
|
});
|
|
1828
2130
|
const job = listBackgroundJobs().find((candidate) => candidate.id === id);
|
|
@@ -1848,32 +2150,51 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1848
2150
|
const index = Math.min(Math.max(current.jobsPickerIndex, 0), current.backgroundJobs.length - 1);
|
|
1849
2151
|
return current.backgroundJobs[index]?.id ?? null;
|
|
1850
2152
|
};
|
|
2153
|
+
const deferredJobMutationIds = new Set();
|
|
2154
|
+
const drainDeferredJobMutations = async () => {
|
|
2155
|
+
if (deferredJobMutationIds.size === 0)
|
|
2156
|
+
return;
|
|
2157
|
+
const pending = [...deferredJobMutationIds];
|
|
2158
|
+
deferredJobMutationIds.clear();
|
|
2159
|
+
for (const jobId of pending) {
|
|
2160
|
+
await collectBackgroundJobUiOutputMutations({ session, jobId });
|
|
2161
|
+
}
|
|
2162
|
+
syncBackgroundJobsState();
|
|
2163
|
+
};
|
|
1851
2164
|
const handleJobsPickerOutput = async () => {
|
|
1852
2165
|
const jobId = selectedJobsPickerId();
|
|
1853
2166
|
if (!jobId)
|
|
1854
2167
|
return;
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2168
|
+
if (store.getState().busy) {
|
|
2169
|
+
deferredJobMutationIds.add(jobId);
|
|
2170
|
+
}
|
|
2171
|
+
else {
|
|
2172
|
+
await collectBackgroundJobUiOutputMutations({
|
|
2173
|
+
session,
|
|
2174
|
+
jobId,
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
1861
2177
|
store.update((current) => ({
|
|
1862
2178
|
...current,
|
|
1863
2179
|
jobsPickerExpandedId: current.jobsPickerExpandedId === jobId ? null : jobId,
|
|
1864
2180
|
}));
|
|
2181
|
+
syncBackgroundJobsState();
|
|
1865
2182
|
};
|
|
1866
2183
|
const handleJobsPickerKill = async () => {
|
|
1867
2184
|
const jobId = selectedJobsPickerId();
|
|
1868
2185
|
if (!jobId)
|
|
1869
2186
|
return;
|
|
1870
2187
|
const killed = await killBackgroundJob(jobId);
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2188
|
+
if (store.getState().busy) {
|
|
2189
|
+
deferredJobMutationIds.add(jobId);
|
|
2190
|
+
}
|
|
2191
|
+
else {
|
|
2192
|
+
await collectBackgroundJobUiKillMutations({
|
|
2193
|
+
session,
|
|
2194
|
+
jobId,
|
|
2195
|
+
result: killed,
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
1877
2198
|
syncBackgroundJobsState();
|
|
1878
2199
|
if (!killed.ok) {
|
|
1879
2200
|
appendError(killed.error ?? 'Background job kill failed.');
|
|
@@ -1916,6 +2237,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1916
2237
|
await switchToModel(selected.id);
|
|
1917
2238
|
}
|
|
1918
2239
|
catch (error) {
|
|
2240
|
+
if (exitForAuthenticationError(error))
|
|
2241
|
+
return;
|
|
1919
2242
|
appendError(error.message);
|
|
1920
2243
|
}
|
|
1921
2244
|
};
|
|
@@ -1965,15 +2288,24 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1965
2288
|
store.update((next) => ({
|
|
1966
2289
|
...next,
|
|
1967
2290
|
busy: true,
|
|
2291
|
+
busyPausedAt: null,
|
|
1968
2292
|
busySince: resumeStartedAt,
|
|
1969
2293
|
clockNow: resumeStartedAt,
|
|
1970
2294
|
status: 'Loading session...',
|
|
1971
2295
|
}));
|
|
1972
2296
|
try {
|
|
1973
2297
|
const snapshot = await loadInteractiveSession(selected.id);
|
|
1974
|
-
|
|
2298
|
+
const requestedModelId = Number(snapshot.modelId);
|
|
2299
|
+
const knownModels = currentServerModels.models.some((model) => model.id === requestedModelId)
|
|
2300
|
+
? currentServerModels
|
|
2301
|
+
: await refreshServerModels();
|
|
2302
|
+
const { modelId: resolvedModelId, fellBack } = models.resolveResumedSessionModel(requestedModelId, knownModels);
|
|
2303
|
+
applySessionSnapshot(session, snapshot, {
|
|
2304
|
+
modelOverride: fellBack ? resolvedModelId : undefined,
|
|
2305
|
+
});
|
|
1975
2306
|
setBackgroundJobSession(session.sessionId);
|
|
1976
2307
|
setScratchSession(session.sessionId);
|
|
2308
|
+
setImageStoreSession(session.sessionId);
|
|
1977
2309
|
syncBackgroundJobsState();
|
|
1978
2310
|
setTodoSession(session.sessionId);
|
|
1979
2311
|
syncTodosState();
|
|
@@ -1981,6 +2313,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1981
2313
|
store.update((next) => ({
|
|
1982
2314
|
...next,
|
|
1983
2315
|
busy: false,
|
|
2316
|
+
busyPausedAt: null,
|
|
1984
2317
|
busySince: null,
|
|
1985
2318
|
resumePickerFilter: '',
|
|
1986
2319
|
resumePickerIndex: 0,
|
|
@@ -1994,14 +2327,26 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1994
2327
|
kind: 'system',
|
|
1995
2328
|
title: 'Session',
|
|
1996
2329
|
},
|
|
2330
|
+
...(fellBack
|
|
2331
|
+
? [
|
|
2332
|
+
{
|
|
2333
|
+
body: `This session's model is no longer available — switched to ${formatModelLabel(resolvedModelId, knownModels.models)}. Conversation history preserved.`,
|
|
2334
|
+
kind: 'system',
|
|
2335
|
+
title: 'Model',
|
|
2336
|
+
},
|
|
2337
|
+
]
|
|
2338
|
+
: []),
|
|
1997
2339
|
...buildTranscriptFromSessionHistory(session.history),
|
|
1998
2340
|
]);
|
|
1999
2341
|
await remountTui();
|
|
2000
2342
|
}
|
|
2001
2343
|
catch (error) {
|
|
2344
|
+
if (exitForAuthenticationError(error))
|
|
2345
|
+
return;
|
|
2002
2346
|
store.update((next) => ({
|
|
2003
2347
|
...next,
|
|
2004
2348
|
busy: false,
|
|
2349
|
+
busyPausedAt: null,
|
|
2005
2350
|
busySince: null,
|
|
2006
2351
|
resumePickerFilter: '',
|
|
2007
2352
|
resumePickerIndex: 0,
|
|
@@ -2025,6 +2370,127 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2025
2370
|
}));
|
|
2026
2371
|
await handleSubmit(queued.body);
|
|
2027
2372
|
};
|
|
2373
|
+
let turnMessageCounter = 0;
|
|
2374
|
+
const upsertTurnMessage = (id, patch) => {
|
|
2375
|
+
store.update((current) => ({
|
|
2376
|
+
...current,
|
|
2377
|
+
turnMessages: current.turnMessages.map((message) => message.id === id ? { ...message, ...patch } : message),
|
|
2378
|
+
}));
|
|
2379
|
+
};
|
|
2380
|
+
const recoverUnacknowledgedMessages = (autoSubmits = true) => {
|
|
2381
|
+
if (unacknowledged.size === 0)
|
|
2382
|
+
return;
|
|
2383
|
+
const stranded = [...unacknowledged.values()];
|
|
2384
|
+
unacknowledged = new Map();
|
|
2385
|
+
for (const { row } of stranded) {
|
|
2386
|
+
upsertTurnMessage(row, {
|
|
2387
|
+
state: 'queued',
|
|
2388
|
+
note: autoSubmits
|
|
2389
|
+
? 'not read — sending as the next prompt'
|
|
2390
|
+
: 'not read — left in the composer',
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
const existing = store.getState().queuedMessage;
|
|
2394
|
+
const combined = [
|
|
2395
|
+
...stranded.map((entry) => entry.queued),
|
|
2396
|
+
...(existing ? [existing] : []),
|
|
2397
|
+
];
|
|
2398
|
+
const body = combined
|
|
2399
|
+
.map((entry) => entry.body)
|
|
2400
|
+
.filter((entry) => entry.trim())
|
|
2401
|
+
.join('\n\n');
|
|
2402
|
+
if (!body.trim())
|
|
2403
|
+
return;
|
|
2404
|
+
scheduleLiveFrameRemount();
|
|
2405
|
+
store.update((current) => ({
|
|
2406
|
+
...current,
|
|
2407
|
+
queuedMessage: {
|
|
2408
|
+
body,
|
|
2409
|
+
imageAttachments: combined.flatMap((entry) => entry.imageAttachments),
|
|
2410
|
+
pastedChunks: combined.flatMap((entry) => entry.pastedChunks),
|
|
2411
|
+
},
|
|
2412
|
+
}));
|
|
2413
|
+
};
|
|
2414
|
+
const fireQueuedMessage = async () => {
|
|
2415
|
+
const state = store.getState();
|
|
2416
|
+
const queued = state.queuedMessage;
|
|
2417
|
+
if (!queued || !state.busy || exiting)
|
|
2418
|
+
return;
|
|
2419
|
+
const text = (queued.pastedChunks.length
|
|
2420
|
+
? expandPastedChunks(queued.body, queued.pastedChunks)
|
|
2421
|
+
: queued.body).trim();
|
|
2422
|
+
const images = queued.imageAttachments.filter((attachment) => queued.body.includes(`[Image #${attachment.index}]`));
|
|
2423
|
+
if (!text && images.length === 0)
|
|
2424
|
+
return;
|
|
2425
|
+
const turnId = activeServerTurnId;
|
|
2426
|
+
const blockedReason = !turnId ? 'waiting for the turn to end' : null;
|
|
2427
|
+
const existingRow = blockedRows.get(queued);
|
|
2428
|
+
if (existingRow !== undefined) {
|
|
2429
|
+
upsertTurnMessage(existingRow, {
|
|
2430
|
+
state: 'queued',
|
|
2431
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2432
|
+
});
|
|
2433
|
+
if (blockedReason || !turnId)
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const id = existingRow ?? ++turnMessageCounter;
|
|
2437
|
+
const messageId = `m${id}_${Date.now().toString(36)}`;
|
|
2438
|
+
if (existingRow === undefined) {
|
|
2439
|
+
scheduleLiveFrameRemount();
|
|
2440
|
+
store.update((current) => ({
|
|
2441
|
+
...current,
|
|
2442
|
+
turnMessages: [
|
|
2443
|
+
...current.turnMessages,
|
|
2444
|
+
{
|
|
2445
|
+
id,
|
|
2446
|
+
text,
|
|
2447
|
+
state: blockedReason ? 'queued' : 'sending',
|
|
2448
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2449
|
+
},
|
|
2450
|
+
],
|
|
2451
|
+
}));
|
|
2452
|
+
}
|
|
2453
|
+
if (blockedReason || !turnId) {
|
|
2454
|
+
blockedRows.set(queued, id);
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
blockedRows.delete(queued);
|
|
2458
|
+
unacknowledged.set(messageId, { row: id, queued });
|
|
2459
|
+
scheduleLiveFrameRemount();
|
|
2460
|
+
store.update((current) => current.queuedMessage === queued
|
|
2461
|
+
? { ...current, queuedMessage: null }
|
|
2462
|
+
: current);
|
|
2463
|
+
queueTurnEntry({
|
|
2464
|
+
body: text,
|
|
2465
|
+
kind: 'user',
|
|
2466
|
+
preformatted: queued.pastedChunks.length > 0,
|
|
2467
|
+
title: 'You · sent mid-turn',
|
|
2468
|
+
});
|
|
2469
|
+
const rollback = (note) => {
|
|
2470
|
+
if (!unacknowledged.delete(messageId))
|
|
2471
|
+
return;
|
|
2472
|
+
upsertTurnMessage(id, { state: 'queued', note });
|
|
2473
|
+
scheduleLiveFrameRemount();
|
|
2474
|
+
store.update((current) => current.queuedMessage ? current : { ...current, queuedMessage: queued });
|
|
2475
|
+
};
|
|
2476
|
+
let outcome;
|
|
2477
|
+
try {
|
|
2478
|
+
outcome = await chat.postInterjection({
|
|
2479
|
+
config: authConfig,
|
|
2480
|
+
turnId,
|
|
2481
|
+
text,
|
|
2482
|
+
messageId,
|
|
2483
|
+
imageAttachments: images,
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
catch (error) {
|
|
2487
|
+
rollback(`not sent — ${error?.message ?? 'request failed'}`);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
if (outcome === 'stale') {
|
|
2491
|
+
rollback('turn ended — sending as the next prompt');
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2028
2494
|
const handleSubmit = async (rawInput) => {
|
|
2029
2495
|
const chunks = store.getState().pastedChunks;
|
|
2030
2496
|
const expanded = chunks.length
|
|
@@ -2036,17 +2502,39 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2036
2502
|
if (!input || exiting)
|
|
2037
2503
|
return;
|
|
2038
2504
|
if (input.startsWith('/')) {
|
|
2505
|
+
if (isJobsPickerCommand(input)) {
|
|
2506
|
+
store.update((current) => ({
|
|
2507
|
+
...current,
|
|
2508
|
+
cursor: 0,
|
|
2509
|
+
input: '',
|
|
2510
|
+
pastedChunks: [],
|
|
2511
|
+
}));
|
|
2512
|
+
openJobsPicker();
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
if (input === '/notify') {
|
|
2516
|
+
store.update((current) => ({
|
|
2517
|
+
...current,
|
|
2518
|
+
cursor: 0,
|
|
2519
|
+
input: '',
|
|
2520
|
+
pastedChunks: [],
|
|
2521
|
+
}));
|
|
2522
|
+
toggleNotifications();
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2039
2525
|
appendTurnAwareEntry({
|
|
2040
|
-
body: "Slash commands can't be queued while a turn is running."
|
|
2526
|
+
body: "Slash commands can't be queued while a turn is running. " +
|
|
2527
|
+
'Ctrl+B opens background jobs, Ctrl+G toggles notifications.',
|
|
2041
2528
|
kind: 'system',
|
|
2042
2529
|
title: 'Queued',
|
|
2043
2530
|
});
|
|
2044
2531
|
return;
|
|
2045
2532
|
}
|
|
2046
2533
|
const pending = store.getState();
|
|
2534
|
+
const body = String(rawInput ?? '');
|
|
2047
2535
|
const snapshot = {
|
|
2048
|
-
body
|
|
2049
|
-
imageAttachments: pending.imageAttachments,
|
|
2536
|
+
body,
|
|
2537
|
+
imageAttachments: pending.imageAttachments.filter((attachment) => body.includes(`[Image #${attachment.index}]`)),
|
|
2050
2538
|
pastedChunks: pending.pastedChunks,
|
|
2051
2539
|
};
|
|
2052
2540
|
scheduleLiveFrameRemount();
|
|
@@ -2061,11 +2549,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2061
2549
|
}));
|
|
2062
2550
|
return;
|
|
2063
2551
|
}
|
|
2064
|
-
const imageAttachments = store
|
|
2552
|
+
const imageAttachments = store
|
|
2553
|
+
.getState()
|
|
2554
|
+
.imageAttachments.filter((attachment) => input.includes(`[Image #${attachment.index}]`));
|
|
2065
2555
|
scheduleLiveFrameRemount();
|
|
2066
2556
|
store.update((current) => ({
|
|
2067
2557
|
...current,
|
|
2068
2558
|
cursor: 0,
|
|
2559
|
+
imageAttachments: [],
|
|
2069
2560
|
input: '',
|
|
2070
2561
|
pastedChunks: [],
|
|
2071
2562
|
promptHistory: appendPromptToHistory(current.promptHistory, input),
|
|
@@ -2081,6 +2572,22 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2081
2572
|
requestExit();
|
|
2082
2573
|
return;
|
|
2083
2574
|
}
|
|
2575
|
+
if (input === '/logout') {
|
|
2576
|
+
store.update((current) => ({
|
|
2577
|
+
...current,
|
|
2578
|
+
busy: true,
|
|
2579
|
+
status: 'Signing out...',
|
|
2580
|
+
}));
|
|
2581
|
+
try {
|
|
2582
|
+
await logoutFromServer({ config: authConfig });
|
|
2583
|
+
}
|
|
2584
|
+
catch {
|
|
2585
|
+
}
|
|
2586
|
+
clearCliAuthConfig(session.env);
|
|
2587
|
+
signedOut = true;
|
|
2588
|
+
requestExit();
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2084
2591
|
if (input === '/help') {
|
|
2085
2592
|
appendStaticEntry({
|
|
2086
2593
|
body: formatInteractiveHelpText(),
|
|
@@ -2097,6 +2604,15 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2097
2604
|
});
|
|
2098
2605
|
return;
|
|
2099
2606
|
}
|
|
2607
|
+
if (input === '/report') {
|
|
2608
|
+
const opened = await openUrl(REPORT_ISSUE_URL);
|
|
2609
|
+
appendStaticEntries(buildReportTranscriptEntries(opened));
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (input === '/notify') {
|
|
2613
|
+
toggleNotifications();
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2100
2616
|
if (input === '/jobs' || input.startsWith('/jobs ')) {
|
|
2101
2617
|
const jobsArgs = input.slice('/jobs'.length).trim();
|
|
2102
2618
|
if (!jobsArgs) {
|
|
@@ -2109,7 +2625,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2109
2625
|
const killed = await killBackgroundJob(jobId);
|
|
2110
2626
|
await collectBackgroundJobUiKillMutations({
|
|
2111
2627
|
session,
|
|
2112
|
-
projectIndex,
|
|
2113
2628
|
jobId,
|
|
2114
2629
|
result: killed,
|
|
2115
2630
|
});
|
|
@@ -2151,14 +2666,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2151
2666
|
});
|
|
2152
2667
|
}
|
|
2153
2668
|
catch (error) {
|
|
2669
|
+
if (exitForAuthenticationError(error))
|
|
2670
|
+
return;
|
|
2154
2671
|
appendError(error.message);
|
|
2155
2672
|
}
|
|
2156
2673
|
finally {
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2674
|
+
if (!exiting) {
|
|
2675
|
+
store.update((current) => ({
|
|
2676
|
+
...current,
|
|
2677
|
+
busy: false,
|
|
2678
|
+
status: 'Ready',
|
|
2679
|
+
}));
|
|
2680
|
+
}
|
|
2162
2681
|
}
|
|
2163
2682
|
return;
|
|
2164
2683
|
}
|
|
@@ -2172,14 +2691,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2172
2691
|
await openResumePicker();
|
|
2173
2692
|
}
|
|
2174
2693
|
catch (error) {
|
|
2694
|
+
if (exitForAuthenticationError(error))
|
|
2695
|
+
return;
|
|
2175
2696
|
appendError(error.message);
|
|
2176
2697
|
}
|
|
2177
2698
|
finally {
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2699
|
+
if (!exiting) {
|
|
2700
|
+
store.update((current) => ({
|
|
2701
|
+
...current,
|
|
2702
|
+
busy: false,
|
|
2703
|
+
status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
|
|
2704
|
+
}));
|
|
2705
|
+
}
|
|
2183
2706
|
}
|
|
2184
2707
|
return;
|
|
2185
2708
|
}
|
|
@@ -2199,37 +2722,77 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2199
2722
|
}
|
|
2200
2723
|
}
|
|
2201
2724
|
catch (error) {
|
|
2725
|
+
if (exitForAuthenticationError(error))
|
|
2726
|
+
return;
|
|
2202
2727
|
appendError(error.message);
|
|
2203
2728
|
}
|
|
2204
2729
|
finally {
|
|
2730
|
+
if (!exiting) {
|
|
2731
|
+
store.update((current) => ({
|
|
2732
|
+
...current,
|
|
2733
|
+
busy: false,
|
|
2734
|
+
status: current.modelPickerOpen ? 'Select a model' : 'Ready',
|
|
2735
|
+
}));
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
if (input === '/new') {
|
|
2741
|
+
newConversationInFlight = true;
|
|
2742
|
+
store.update((current) => ({
|
|
2743
|
+
...current,
|
|
2744
|
+
busy: true,
|
|
2745
|
+
busyPausedAt: null,
|
|
2746
|
+
busySince: Date.now(),
|
|
2747
|
+
imageAttachments: [],
|
|
2748
|
+
queuedMessage: null,
|
|
2749
|
+
status: 'Starting a new conversation...',
|
|
2750
|
+
}));
|
|
2751
|
+
if (!(await saveActiveSessionOrAbort())) {
|
|
2752
|
+
newConversationInFlight = false;
|
|
2205
2753
|
store.update((current) => ({
|
|
2206
2754
|
...current,
|
|
2207
2755
|
busy: false,
|
|
2208
|
-
|
|
2756
|
+
busyPausedAt: null,
|
|
2757
|
+
busySince: null,
|
|
2758
|
+
status: 'Ready',
|
|
2209
2759
|
}));
|
|
2760
|
+
await flushQueuedMessage();
|
|
2761
|
+
return;
|
|
2210
2762
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2763
|
+
startNewConversation(session);
|
|
2764
|
+
setBackgroundJobSession(session.sessionId);
|
|
2765
|
+
setScratchSession(session.sessionId);
|
|
2766
|
+
setImageStoreSession(session.sessionId);
|
|
2767
|
+
setTodoSession(session.sessionId);
|
|
2215
2768
|
clearTodos();
|
|
2769
|
+
syncBackgroundJobsState();
|
|
2216
2770
|
syncTodosState();
|
|
2217
2771
|
latestUsageSummary = null;
|
|
2218
|
-
await saveActiveSession();
|
|
2219
2772
|
store.replaceTranscript([
|
|
2220
2773
|
{
|
|
2221
|
-
body: '
|
|
2774
|
+
body: 'Started a new conversation. The previous session remains saved.',
|
|
2222
2775
|
kind: 'system',
|
|
2223
|
-
title: '
|
|
2776
|
+
title: 'Session',
|
|
2224
2777
|
},
|
|
2225
2778
|
]);
|
|
2779
|
+
await saveActiveSession();
|
|
2226
2780
|
syncShellStateFromSession();
|
|
2227
|
-
|
|
2781
|
+
newConversationInFlight = false;
|
|
2782
|
+
store.update((current) => ({
|
|
2783
|
+
...current,
|
|
2784
|
+
busy: false,
|
|
2785
|
+
busyPausedAt: null,
|
|
2786
|
+
busySince: null,
|
|
2787
|
+
status: 'Ready',
|
|
2788
|
+
}));
|
|
2228
2789
|
await remountTui();
|
|
2790
|
+
await flushQueuedMessage();
|
|
2229
2791
|
return;
|
|
2230
2792
|
}
|
|
2231
2793
|
latestUsageSummary = null;
|
|
2232
2794
|
disarmExitConfirm();
|
|
2795
|
+
unacknowledged = new Map();
|
|
2233
2796
|
const turnStartedAt = Date.now();
|
|
2234
2797
|
const turnGeneration = ++activeTurnGeneration;
|
|
2235
2798
|
const turnAbort = new AbortController();
|
|
@@ -2246,36 +2809,63 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2246
2809
|
};
|
|
2247
2810
|
pendingTurnEntries = [];
|
|
2248
2811
|
queueTurnEntry(userEntry);
|
|
2812
|
+
resetThinkingPacer();
|
|
2813
|
+
activeTurnImageAttachments = imageAttachments;
|
|
2249
2814
|
store.update((current) => ({
|
|
2250
2815
|
...current,
|
|
2251
2816
|
activeTurnInput: input,
|
|
2252
2817
|
activeTurnInputPreformatted: preformatted,
|
|
2818
|
+
analyzingImages: 0,
|
|
2819
|
+
generatingImage: false,
|
|
2253
2820
|
busy: true,
|
|
2821
|
+
busyPausedAt: null,
|
|
2254
2822
|
busySince: turnStartedAt,
|
|
2255
2823
|
clockNow: turnStartedAt,
|
|
2824
|
+
recoveryStatus: '',
|
|
2256
2825
|
status: 'Running turn...',
|
|
2257
2826
|
thinkingTitle: '',
|
|
2258
2827
|
thinkingNotes: [],
|
|
2259
2828
|
transcriptScrollOffset: 0,
|
|
2260
2829
|
tokenUsage: formatClientTokenUsage(0, latestUsageSummary),
|
|
2261
2830
|
turnCounter: current.turnCounter + 1,
|
|
2831
|
+
turnMessages: [],
|
|
2262
2832
|
workingTools: [],
|
|
2263
2833
|
}));
|
|
2264
2834
|
try {
|
|
2265
2835
|
const result = await chat.sendServerUserMessage({
|
|
2266
2836
|
config: authConfig,
|
|
2267
|
-
projectIndex,
|
|
2268
2837
|
session,
|
|
2269
2838
|
input,
|
|
2270
2839
|
imageAttachments,
|
|
2271
2840
|
signal: turnAbort.signal,
|
|
2841
|
+
onTurnStart: (serverTurnId) => {
|
|
2842
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2843
|
+
return;
|
|
2844
|
+
activeServerTurnId = serverTurnId;
|
|
2845
|
+
},
|
|
2846
|
+
onInterjectionDelivered: (event) => {
|
|
2847
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2848
|
+
return;
|
|
2849
|
+
for (const messageId of event.messageIds ?? []) {
|
|
2850
|
+
const pending = unacknowledged.get(messageId);
|
|
2851
|
+
if (!pending)
|
|
2852
|
+
continue;
|
|
2853
|
+
unacknowledged.delete(messageId);
|
|
2854
|
+
upsertTurnMessage(pending.row, {
|
|
2855
|
+
state: 'delivered',
|
|
2856
|
+
note: undefined,
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
},
|
|
2272
2860
|
});
|
|
2273
2861
|
if (turnGeneration !== activeTurnGeneration)
|
|
2274
2862
|
return;
|
|
2275
2863
|
latestUsageSummary = result.usageSummary ?? null;
|
|
2276
2864
|
const turnEntries = takePendingTurnEntries();
|
|
2277
|
-
await saveActiveSession()
|
|
2865
|
+
if (!(await saveActiveSession()))
|
|
2866
|
+
return;
|
|
2278
2867
|
syncShellStateFromSession();
|
|
2868
|
+
activeTurnImageAttachments = [];
|
|
2279
2869
|
store.update((current) => ({
|
|
2280
2870
|
...current,
|
|
2281
2871
|
activeTurnInput: '',
|
|
@@ -2311,10 +2901,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2311
2901
|
store.update((current) => ({
|
|
2312
2902
|
...current,
|
|
2313
2903
|
busy: false,
|
|
2904
|
+
busyPausedAt: null,
|
|
2314
2905
|
busySince: null,
|
|
2906
|
+
recoveryStatus: '',
|
|
2315
2907
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2316
2908
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
2317
2909
|
}));
|
|
2910
|
+
recoverUnacknowledgedMessages();
|
|
2318
2911
|
await flushQueuedMessage();
|
|
2319
2912
|
}
|
|
2320
2913
|
catch (error) {
|
|
@@ -2325,15 +2918,29 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2325
2918
|
return;
|
|
2326
2919
|
}
|
|
2327
2920
|
const cancelled = isTurnCancelledError(error);
|
|
2921
|
+
if (exitForAuthenticationError(error))
|
|
2922
|
+
return;
|
|
2923
|
+
dismissPendingApproval();
|
|
2924
|
+
dismissPendingSudoPassword();
|
|
2925
|
+
dismissPendingUserInput();
|
|
2926
|
+
const failedTurnAttachments = activeTurnImageAttachments;
|
|
2927
|
+
activeTurnImageAttachments = [];
|
|
2928
|
+
const quotaNotice = cancelled ? null : buildQuotaNotice(error);
|
|
2328
2929
|
store.update((current) => ({
|
|
2329
2930
|
...current,
|
|
2330
2931
|
activeTurnInput: '',
|
|
2331
2932
|
activeTurnInputPreformatted: false,
|
|
2332
2933
|
busy: false,
|
|
2934
|
+
busyPausedAt: null,
|
|
2333
2935
|
busySince: null,
|
|
2334
2936
|
commandLog: [],
|
|
2335
|
-
imageAttachments:
|
|
2336
|
-
|
|
2937
|
+
imageAttachments: failedTurnAttachments,
|
|
2938
|
+
recoveryStatus: '',
|
|
2939
|
+
status: cancelled
|
|
2940
|
+
? 'Ready'
|
|
2941
|
+
: quotaNotice
|
|
2942
|
+
? quotaNotice.title
|
|
2943
|
+
: 'Turn failed',
|
|
2337
2944
|
thinkingTitle: '',
|
|
2338
2945
|
thinkingNotes: [],
|
|
2339
2946
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
@@ -2349,14 +2956,28 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2349
2956
|
title: 'System',
|
|
2350
2957
|
},
|
|
2351
2958
|
]);
|
|
2352
|
-
await saveActiveSession()
|
|
2959
|
+
if (!(await saveActiveSession()))
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
else if (quotaNotice) {
|
|
2963
|
+
appendStaticEntries([
|
|
2964
|
+
...turnEntries,
|
|
2965
|
+
{
|
|
2966
|
+
body: quotaNotice.body,
|
|
2967
|
+
kind: 'system',
|
|
2968
|
+
title: quotaNotice.title,
|
|
2969
|
+
},
|
|
2970
|
+
]);
|
|
2353
2971
|
}
|
|
2354
2972
|
else {
|
|
2355
2973
|
appendStaticEntries(turnEntries);
|
|
2356
2974
|
appendError(error.message);
|
|
2975
|
+
appendStaticEntries(buildTurnFailureHintEntries(cancelled));
|
|
2357
2976
|
}
|
|
2358
2977
|
await remountTui();
|
|
2359
|
-
|
|
2978
|
+
const autoSubmits = !cancelled && isTurnOutcomeKnown(error);
|
|
2979
|
+
recoverUnacknowledgedMessages(autoSubmits);
|
|
2980
|
+
if (autoSubmits && turnGeneration === activeTurnGeneration) {
|
|
2360
2981
|
await flushQueuedMessage();
|
|
2361
2982
|
}
|
|
2362
2983
|
}
|
|
@@ -2366,24 +2987,60 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2366
2987
|
}
|
|
2367
2988
|
if (turnGeneration === activeTurnGeneration) {
|
|
2368
2989
|
lastTurnStartedAt = null;
|
|
2990
|
+
activeServerTurnId = null;
|
|
2369
2991
|
}
|
|
2992
|
+
recoverUnacknowledgedMessages();
|
|
2993
|
+
await drainDeferredJobMutations();
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2996
|
+
session.onImageAnalysis = (activeImageCount) => {
|
|
2997
|
+
store.update((current) => ({
|
|
2998
|
+
...current,
|
|
2999
|
+
analyzingImages: Math.max(0, activeImageCount),
|
|
3000
|
+
generatingImage: activeImageCount > 0 ? false : current.generatingImage,
|
|
3001
|
+
}));
|
|
3002
|
+
};
|
|
3003
|
+
session.onRecovery = (status) => {
|
|
3004
|
+
if (status.phase === 'restored' || status.phase === 'continued') {
|
|
3005
|
+
store.update((current) => ({ ...current, recoveryStatus: '' }));
|
|
3006
|
+
appendTurnAwareEntry(status.phase === 'restored'
|
|
3007
|
+
? { body: 'Continued the same turn.', kind: 'system', title: 'Connection restored' }
|
|
3008
|
+
: {
|
|
3009
|
+
body: 'Continued from the steps that had finished.',
|
|
3010
|
+
kind: 'system',
|
|
3011
|
+
title: 'Server restarted',
|
|
3012
|
+
});
|
|
3013
|
+
return;
|
|
2370
3014
|
}
|
|
3015
|
+
store.update((current) => ({
|
|
3016
|
+
...current,
|
|
3017
|
+
recoveryStatus: status.phase === 'reconnected' ? '' : formatTurnRecoveryStatus(status),
|
|
3018
|
+
}));
|
|
3019
|
+
};
|
|
3020
|
+
session.onImageGeneration = (active) => {
|
|
3021
|
+
store.update((current) => ({
|
|
3022
|
+
...current,
|
|
3023
|
+
generatingImage: Boolean(active),
|
|
3024
|
+
analyzingImages: active ? 0 : current.analyzingImages,
|
|
3025
|
+
}));
|
|
2371
3026
|
};
|
|
2372
3027
|
session.onStatus = (message) => {
|
|
2373
3028
|
const panel = thinkingPanelFromStatus(message);
|
|
2374
3029
|
store.update((current) => ({
|
|
2375
3030
|
...current,
|
|
2376
3031
|
status: message,
|
|
2377
|
-
|
|
2378
|
-
thinkingNotes: panel?.notes ?? current.thinkingNotes,
|
|
2379
|
-
tokenUsage: formatClientTokenUsage(current.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
|
|
3032
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2380
3033
|
}));
|
|
3034
|
+
queueThinkingUpdate({
|
|
3035
|
+
title: panel ? panel.title : liveStatusPanelTitle(message),
|
|
3036
|
+
notes: panel ? panel.notes : [],
|
|
3037
|
+
});
|
|
2381
3038
|
};
|
|
2382
3039
|
session.onContextLog = (message) => {
|
|
2383
3040
|
store.update((current) => ({
|
|
2384
3041
|
...current,
|
|
2385
3042
|
contextStatus: message,
|
|
2386
|
-
tokenUsage: formatClientTokenUsage(current
|
|
3043
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2387
3044
|
}));
|
|
2388
3045
|
};
|
|
2389
3046
|
session.onToolEvent = (event) => {
|
|
@@ -2398,31 +3055,27 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2398
3055
|
if (isFileChangeTool(event.call.name)) {
|
|
2399
3056
|
const entry = buildFileChangeEntry(event);
|
|
2400
3057
|
store.appendWorkingTool(entry);
|
|
2401
|
-
|
|
3058
|
+
const result = event.result;
|
|
3059
|
+
if (result?.ok === true || result?.skipped === true)
|
|
3060
|
+
appendTurnAwareEntry(entry);
|
|
2402
3061
|
return;
|
|
2403
3062
|
}
|
|
2404
3063
|
store.appendWorkingTool(buildWorkingToolEntry(event));
|
|
2405
3064
|
};
|
|
2406
|
-
const jobDetailLines = (jobId) => {
|
|
2407
|
-
const buffered = getJobBufferedOutput(jobId);
|
|
2408
|
-
if (!buffered)
|
|
2409
|
-
return [];
|
|
2410
|
-
const lines = buffered.output
|
|
2411
|
-
.replace(/\r\n/g, '\n')
|
|
2412
|
-
.replace(/\r/g, '\n')
|
|
2413
|
-
.split('\n')
|
|
2414
|
-
.filter((line) => line.trim().length > 0);
|
|
2415
|
-
const visible = lines.slice(-20);
|
|
2416
|
-
return buffered.droppedChars > 0
|
|
2417
|
-
? [
|
|
2418
|
-
`... (${buffered.droppedChars} chars of older output dropped) ...`,
|
|
2419
|
-
...visible,
|
|
2420
|
-
]
|
|
2421
|
-
: visible;
|
|
2422
|
-
};
|
|
2423
3065
|
const syncBackgroundJobsState = () => {
|
|
3066
|
+
const expandedId = store.getState().jobsPickerExpandedId;
|
|
2424
3067
|
const jobsForDisplay = listBackgroundJobs().map((job) => {
|
|
2425
|
-
const
|
|
3068
|
+
const expanded = job.id === expandedId;
|
|
3069
|
+
const preview = getJobOutputPreview(job.id, expanded ? 20 : 3);
|
|
3070
|
+
const dropped = preview?.droppedChars ?? 0;
|
|
3071
|
+
const detailLines = !expanded
|
|
3072
|
+
? []
|
|
3073
|
+
: dropped > 0
|
|
3074
|
+
? [
|
|
3075
|
+
`... (${dropped} chars of older output dropped) ...`,
|
|
3076
|
+
...(preview?.tailLines ?? []),
|
|
3077
|
+
]
|
|
3078
|
+
: (preview?.tailLines ?? []);
|
|
2426
3079
|
return {
|
|
2427
3080
|
id: job.id,
|
|
2428
3081
|
command: job.command,
|
|
@@ -2431,8 +3084,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2431
3084
|
startedAt: job.startedAt,
|
|
2432
3085
|
endedAt: job.endedAt,
|
|
2433
3086
|
firstOutputLine: preview?.firstLine ?? '',
|
|
2434
|
-
tailLines: preview?.tailLines ?? [],
|
|
2435
|
-
detailLines
|
|
3087
|
+
tailLines: (preview?.tailLines ?? []).slice(-3),
|
|
3088
|
+
detailLines,
|
|
2436
3089
|
};
|
|
2437
3090
|
});
|
|
2438
3091
|
store.update((current) => ({
|
|
@@ -2449,69 +3102,74 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2449
3102
|
appendTurnAwareEntry(buildBackgroundJobNoticeEntry(snapshot));
|
|
2450
3103
|
}
|
|
2451
3104
|
});
|
|
2452
|
-
projectIndex.onStatus = session.onStatus;
|
|
2453
|
-
projectIndex.onContextLog = session.onContextLog;
|
|
2454
3105
|
session.requestSudoPassword = async ({ command, prompt, signal }) => openSudoPasswordPrompt(command, prompt, signal);
|
|
2455
|
-
session.
|
|
3106
|
+
session.requestUserInput = async (request, signal) => openUserInputPrompt(request.questions, signal);
|
|
3107
|
+
session.requestPermission = async (request) => {
|
|
3108
|
+
const deny = { kind: 'deny' };
|
|
2456
3109
|
if (exiting)
|
|
2457
|
-
return
|
|
3110
|
+
return deny;
|
|
2458
3111
|
if (sessionAutoYes)
|
|
2459
|
-
return
|
|
2460
|
-
const
|
|
2461
|
-
if (
|
|
2462
|
-
setAgentMode('auto-accept');
|
|
2463
|
-
syncShellStateFromSession();
|
|
3112
|
+
return { kind: 'once' };
|
|
3113
|
+
const decision = await openApprovalPrompt(request, session.turnAbortSignal);
|
|
3114
|
+
if (decision.kind === 'always-bucket') {
|
|
2464
3115
|
appendTurnAwareEntry({
|
|
2465
|
-
body:
|
|
3116
|
+
body: `Allowed for the rest of this session: ${bucketActionLabel(request.bucket)}.`,
|
|
2466
3117
|
kind: 'system',
|
|
2467
3118
|
title: 'Approvals',
|
|
2468
3119
|
});
|
|
2469
|
-
return true;
|
|
2470
3120
|
}
|
|
2471
|
-
|
|
2472
|
-
};
|
|
2473
|
-
session.confirmPatch = async (filePath, patch) => {
|
|
2474
|
-
if (exiting)
|
|
2475
|
-
return false;
|
|
2476
|
-
if (sessionAutoYes)
|
|
2477
|
-
return true;
|
|
2478
|
-
const choice = await openApprovalPrompt('Approve patch?', 'Review changes before applying.', {
|
|
2479
|
-
diff: patch,
|
|
2480
|
-
filePath,
|
|
2481
|
-
});
|
|
2482
|
-
if (choice === 'a') {
|
|
2483
|
-
setAgentMode('auto-accept');
|
|
2484
|
-
syncShellStateFromSession();
|
|
3121
|
+
else if (decision.kind === 'always-prefix') {
|
|
2485
3122
|
appendTurnAwareEntry({
|
|
2486
|
-
body:
|
|
3123
|
+
body: `Allowed for the rest of this session: commands starting with \`${decision.prefix}\`.`,
|
|
3124
|
+
kind: 'system',
|
|
3125
|
+
title: 'Approvals',
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
else if (decision.kind === 'always-origin') {
|
|
3129
|
+
appendTurnAwareEntry({
|
|
3130
|
+
body: `Allowed for the rest of this session: browser actions on ${decision.origin}.`,
|
|
2487
3131
|
kind: 'system',
|
|
2488
3132
|
title: 'Approvals',
|
|
2489
3133
|
});
|
|
2490
|
-
return true;
|
|
2491
3134
|
}
|
|
2492
|
-
return
|
|
3135
|
+
return decision;
|
|
2493
3136
|
};
|
|
2494
3137
|
const shellInputHandlers = {
|
|
3138
|
+
getApprovalScrollLimit: () => {
|
|
3139
|
+
const contentWidth = frameContentWidth(terminalCols);
|
|
3140
|
+
return approvalScrollLimit(store.getState().approvalPrompt, contentWidth, terminalRows);
|
|
3141
|
+
},
|
|
3142
|
+
getComposerInputWidth: () => composerInputWidth(terminalCols, store.getState()),
|
|
2495
3143
|
getTranscriptScrollLimit: () => {
|
|
2496
|
-
const contentWidth =
|
|
3144
|
+
const contentWidth = frameContentWidth(terminalCols);
|
|
2497
3145
|
const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
2498
3146
|
return blocks.reduce((total, block, index) => total + block.length + (index > 0 ? 1 : 0), 0);
|
|
2499
3147
|
},
|
|
3148
|
+
getUserInputViewport: () => {
|
|
3149
|
+
const nowMs = Date.now();
|
|
3150
|
+
const state = store.getState();
|
|
3151
|
+
return userInputViewportForFrame(state, terminalCols, terminalRows, spinnerFrame, busyElapsedSeconds(state, nowMs), nowMs);
|
|
3152
|
+
},
|
|
2500
3153
|
onCancelTurn: cancelActiveTurn,
|
|
2501
3154
|
onCycleAgentMode: cycleAgentMode,
|
|
3155
|
+
onAgentModeChangeBlocked: reportAgentModeLocked,
|
|
2502
3156
|
onCtrlC: handleCtrlC,
|
|
2503
3157
|
onLinkCopy: handleLinkCopy,
|
|
2504
3158
|
onLinkOpen: handleLinkOpen,
|
|
2505
3159
|
onLiveFrameShapeChange: scheduleLiveFrameRemount,
|
|
2506
3160
|
onRequestExit: requestExit,
|
|
2507
3161
|
onResolveApproval: handleInlineApprovalChoice,
|
|
3162
|
+
onResolveUserInput: handleInlineUserInput,
|
|
2508
3163
|
onResumeSession: handleInlineResumeSelection,
|
|
2509
3164
|
onSelectionCopy: handleAppSelectionCopy,
|
|
2510
3165
|
onSelectModel: handleInlineModelSelection,
|
|
3166
|
+
onOpenJobsPicker: openJobsPicker,
|
|
3167
|
+
onToggleNotifications: toggleNotifications,
|
|
2511
3168
|
onJobsPickerOutput: handleJobsPickerOutput,
|
|
2512
3169
|
onJobsPickerKill: handleJobsPickerKill,
|
|
2513
3170
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2514
3171
|
onSubmit: handleSubmit,
|
|
3172
|
+
onFireQueuedMessage: fireQueuedMessage,
|
|
2515
3173
|
};
|
|
2516
3174
|
const unsubscribe = store.subscribe(() => {
|
|
2517
3175
|
renderCurrentFrame();
|
|
@@ -2519,6 +3177,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2519
3177
|
const spinnerTimer = setInterval(() => {
|
|
2520
3178
|
spinnerFrame = (spinnerFrame + 1) % 6;
|
|
2521
3179
|
if (store.getState().busy) {
|
|
3180
|
+
flushPendingThinking();
|
|
2522
3181
|
renderCurrentFrame();
|
|
2523
3182
|
}
|
|
2524
3183
|
}, 120);
|
|
@@ -2549,6 +3208,13 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2549
3208
|
return;
|
|
2550
3209
|
}
|
|
2551
3210
|
if (message.op === 'event') {
|
|
3211
|
+
if (message.kind === 'focus') {
|
|
3212
|
+
store.update((current) => current.terminalFocused === message.focused
|
|
3213
|
+
? current
|
|
3214
|
+
: { ...current, terminalFocused: message.focused });
|
|
3215
|
+
renderCurrentFrame();
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
2552
3218
|
if (message.kind === 'resize') {
|
|
2553
3219
|
terminalCols = message.cols;
|
|
2554
3220
|
terminalRows = message.rows;
|
|
@@ -2574,6 +3240,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2574
3240
|
killAllBackgroundJobs();
|
|
2575
3241
|
setBackgroundJobSession(null);
|
|
2576
3242
|
setScratchSession(null);
|
|
3243
|
+
setImageStoreSession(null);
|
|
2577
3244
|
setTodoSession(null);
|
|
2578
3245
|
setBackgroundJobUpdateHook(null);
|
|
2579
3246
|
await bridge.close();
|
|
@@ -2581,4 +3248,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2581
3248
|
setCommandOutputHook(null);
|
|
2582
3249
|
}
|
|
2583
3250
|
});
|
|
3251
|
+
if (fatalError)
|
|
3252
|
+
throw fatalError;
|
|
3253
|
+
return { signedOut };
|
|
2584
3254
|
}
|