@thegitai/cli 1.0.0-preview.7 → 1.0.0-preview.9
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/dist/bin/ai.js +3 -1
- package/dist/src/api/browser-login.js +70 -1
- package/dist/src/api/chat.js +10 -0
- package/dist/src/help-text.js +1 -1
- package/dist/src/session-store.js +52 -20
- package/dist/src/session.js +8 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +68 -13
- package/dist/src/ui/tui/build-frame.js +56 -9
- package/dist/src/utils.js +9 -0
- package/package.json +5 -5
package/dist/bin/ai.js
CHANGED
|
@@ -8,7 +8,7 @@ import { authenticationErrorMessage, isAuthenticationError, isTransientNetworkEr
|
|
|
8
8
|
import { formatCliHelpText } from '../src/help-text.js';
|
|
9
9
|
import { createIndex } from '../src/project-index.js';
|
|
10
10
|
import { createSession } from '../src/session.js';
|
|
11
|
-
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
|
|
11
|
+
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../src/session-store.js';
|
|
12
12
|
import { runClientInteractive } from '../src/ui/repl.js';
|
|
13
13
|
import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
|
|
14
14
|
import { formatSessionExitNotice } from '../src/session-exit.js';
|
|
@@ -116,6 +116,8 @@ function modelLabel(serverModels, modelId) {
|
|
|
116
116
|
`Model ${modelId}`);
|
|
117
117
|
}
|
|
118
118
|
async function saveSessionBoth({ session, serverSessionClient, }) {
|
|
119
|
+
if (!sessionHasUserMessage(session))
|
|
120
|
+
return;
|
|
119
121
|
saveSessionState(session);
|
|
120
122
|
try {
|
|
121
123
|
await serverSessionClient.save(session);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
2
4
|
import http from 'node:http';
|
|
3
5
|
import os from 'node:os';
|
|
4
6
|
import { openUrl } from '../core/open-url.js';
|
|
@@ -21,6 +23,73 @@ function defaultDeviceName() {
|
|
|
21
23
|
return os.hostname();
|
|
22
24
|
}
|
|
23
25
|
}
|
|
26
|
+
function readLinuxOsPrettyName() {
|
|
27
|
+
try {
|
|
28
|
+
const content = readFileSync('/etc/os-release', 'utf8');
|
|
29
|
+
return content.match(/^PRETTY_NAME="?([^"\n]*)"?$/m)?.[1]?.trim() ?? '';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function macArchLabel() {
|
|
36
|
+
try {
|
|
37
|
+
if (process.arch === 'arm64')
|
|
38
|
+
return 'Apple Silicon';
|
|
39
|
+
if (os.cpus().some((cpu) => cpu.model.includes('Apple'))) {
|
|
40
|
+
return 'Apple Silicon';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
}
|
|
45
|
+
return 'Intel';
|
|
46
|
+
}
|
|
47
|
+
export function describeOperatingSystem() {
|
|
48
|
+
try {
|
|
49
|
+
if (process.platform === 'linux') {
|
|
50
|
+
const base = readLinuxOsPrettyName() || `Linux ${os.release()}`;
|
|
51
|
+
return process.arch === 'arm64' ? `${base}, ARM64` : base;
|
|
52
|
+
}
|
|
53
|
+
if (process.platform === 'darwin') {
|
|
54
|
+
let version = '';
|
|
55
|
+
try {
|
|
56
|
+
version = execSync('sw_vers -productVersion', {
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
58
|
+
})
|
|
59
|
+
.toString()
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
return `${version ? `macOS ${version}` : 'macOS'}, ${macArchLabel()}`;
|
|
65
|
+
}
|
|
66
|
+
if (process.platform === 'win32') {
|
|
67
|
+
let label = '';
|
|
68
|
+
try {
|
|
69
|
+
label = os.version();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
}
|
|
73
|
+
const build = Number(os.release().split('.')[2] ?? '0');
|
|
74
|
+
if (build >= 22000)
|
|
75
|
+
label = label.replace(/Windows 10/i, 'Windows 11');
|
|
76
|
+
const base = label || `Windows ${os.release()}`;
|
|
77
|
+
return process.arch === 'arm64' ? `${base}, ARM64` : base;
|
|
78
|
+
}
|
|
79
|
+
return `${process.platform} ${os.release()}`;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return process.platform;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function withOperatingSystemInfo(name) {
|
|
86
|
+
const trimmed = name.trim();
|
|
87
|
+
const osLabel = describeOperatingSystem();
|
|
88
|
+
const combined = osLabel && !trimmed.includes(osLabel)
|
|
89
|
+
? `${trimmed} (${osLabel})`
|
|
90
|
+
: trimmed;
|
|
91
|
+
return combined.slice(0, 180);
|
|
92
|
+
}
|
|
24
93
|
export function generatePkce() {
|
|
25
94
|
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
26
95
|
const challenge = crypto
|
|
@@ -77,7 +146,7 @@ export async function loginViaBrowser(options) {
|
|
|
77
146
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
78
147
|
const openBrowser = options.openBrowser ?? openUrl;
|
|
79
148
|
const onUrl = options.onUrl ?? (() => { });
|
|
80
|
-
const deviceName = options.deviceName ?? defaultDeviceName();
|
|
149
|
+
const deviceName = withOperatingSystemInfo(options.deviceName ?? defaultDeviceName());
|
|
81
150
|
const { verifier, challenge } = generatePkce();
|
|
82
151
|
if (options.noBrowser) {
|
|
83
152
|
const authUrl = buildAuthUrl(websiteUrl, {
|
package/dist/src/api/chat.js
CHANGED
|
@@ -146,6 +146,9 @@ function publicStatusMessage(data) {
|
|
|
146
146
|
: 'tool';
|
|
147
147
|
if (event.phase === 'thinking')
|
|
148
148
|
return 'Thinking...';
|
|
149
|
+
if (event.phase === 'analyzing_image') {
|
|
150
|
+
return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
|
|
151
|
+
}
|
|
149
152
|
if (event.phase === 'running_tool')
|
|
150
153
|
return `Running ${toolName}...`;
|
|
151
154
|
if (event.phase === 'waiting_for_tool')
|
|
@@ -303,6 +306,13 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
303
306
|
}
|
|
304
307
|
async function handleEvent(event) {
|
|
305
308
|
if (event.event === 'status') {
|
|
309
|
+
const data = event.data;
|
|
310
|
+
if (data?.phase === 'analyzing_image') {
|
|
311
|
+
session.onImageAnalysis?.(Math.max(1, Number(data.imageCount ?? 1) || 1));
|
|
312
|
+
}
|
|
313
|
+
else if (data?.phase) {
|
|
314
|
+
session.onImageAnalysis?.(0);
|
|
315
|
+
}
|
|
306
316
|
const message = publicStatusMessage(event.data);
|
|
307
317
|
if (message)
|
|
308
318
|
session.onStatus(message);
|
package/dist/src/help-text.js
CHANGED
|
@@ -79,7 +79,7 @@ const HELP_MARKDOWN = [
|
|
|
79
79
|
' browse them, press Enter to expand one and read its output, k to stop it',
|
|
80
80
|
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
81
81
|
'- `/jobs kill <id>` — stop one background job',
|
|
82
|
-
'- `/
|
|
82
|
+
'- `/new` — start a new conversation; this session remains saved',
|
|
83
83
|
'- `/exit` — quit the session',
|
|
84
84
|
'',
|
|
85
85
|
'## Safety & approvals',
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { getClientStateDir } from './client-state.js';
|
|
5
6
|
import { normalizeAssistantEditJournal } from './edit-journal.js';
|
|
6
7
|
import { cloneSessionSafetyState, createSessionSafetyState, mergeLocalSessionSafetyState, normalizeSessionSafetyState, } from './session-safety.js';
|
|
7
|
-
import {
|
|
8
|
+
import { singleLinePreview } from './utils.js';
|
|
8
9
|
const SESSION_STORE_VERSION = 1;
|
|
9
|
-
const MAX_RECENT_SESSIONS =
|
|
10
|
+
export const MAX_RECENT_SESSIONS = 10;
|
|
10
11
|
function cloneJson(value) {
|
|
11
12
|
return JSON.parse(JSON.stringify(value ?? null));
|
|
12
13
|
}
|
|
@@ -77,6 +78,12 @@ function listSessionFiles(rootDir, env = process.env) {
|
|
|
77
78
|
.filter((name) => name.endsWith('.json'))
|
|
78
79
|
.map((name) => path.join(dir, name));
|
|
79
80
|
}
|
|
81
|
+
function normalizeBranch(value) {
|
|
82
|
+
const text = String(value ?? '')
|
|
83
|
+
.replace(/[\r\n\t]/g, ' ')
|
|
84
|
+
.trim();
|
|
85
|
+
return text ? text.slice(0, 120) : null;
|
|
86
|
+
}
|
|
80
87
|
function normalizeHistory(value) {
|
|
81
88
|
if (!Array.isArray(value))
|
|
82
89
|
return [];
|
|
@@ -107,6 +114,7 @@ function normalizeSnapshot(raw, rootDir) {
|
|
|
107
114
|
createdAt: normalizeIsoDate(raw.createdAt),
|
|
108
115
|
updatedAt: normalizeIsoDate(raw.updatedAt),
|
|
109
116
|
modelId,
|
|
117
|
+
branch: normalizeBranch(raw.branch),
|
|
110
118
|
history: cloneJson(normalizeHistory(raw.history)),
|
|
111
119
|
clientState: sanitizeClientState(raw.clientState),
|
|
112
120
|
serverState: sanitizeOpaqueState(raw.serverState),
|
|
@@ -154,6 +162,21 @@ export function pruneSavedSessions(rootDir, env = process.env) {
|
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
}
|
|
165
|
+
export function readGitBranch(rootDir) {
|
|
166
|
+
try {
|
|
167
|
+
const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
168
|
+
cwd: rootDir,
|
|
169
|
+
encoding: 'utf8',
|
|
170
|
+
timeout: 1500,
|
|
171
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
172
|
+
});
|
|
173
|
+
const branch = result.status === 0 ? String(result.stdout ?? '').trim() : '';
|
|
174
|
+
return branch && branch !== 'HEAD' ? branch : null;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
157
180
|
export function snapshotFromSession(session) {
|
|
158
181
|
const now = new Date().toISOString();
|
|
159
182
|
const createdAt = normalizeIsoDate(session.sessionCreatedAt ?? now);
|
|
@@ -168,6 +191,7 @@ export function snapshotFromSession(session) {
|
|
|
168
191
|
createdAt,
|
|
169
192
|
updatedAt: now,
|
|
170
193
|
modelId: session.modelId,
|
|
194
|
+
branch: readGitBranch(session.rootDir),
|
|
171
195
|
history: cloneJson(session.history),
|
|
172
196
|
clientState: {
|
|
173
197
|
editCounter: session.clientState.editCounter,
|
|
@@ -210,6 +234,9 @@ export function applySessionSnapshot(session, snapshot, options = {}) {
|
|
|
210
234
|
safety: mergeLocalSessionSafetyState(session.clientState.safety, snapshot.clientState.safety),
|
|
211
235
|
};
|
|
212
236
|
}
|
|
237
|
+
export function sessionHasUserMessage(session) {
|
|
238
|
+
return session.history.some((entry) => Boolean(userPromptText(entry)));
|
|
239
|
+
}
|
|
213
240
|
function extractMarkedSection(text, marker) {
|
|
214
241
|
const index = text.indexOf(marker);
|
|
215
242
|
if (index === -1)
|
|
@@ -218,23 +245,27 @@ function extractMarkedSection(text, marker) {
|
|
|
218
245
|
const end = after.indexOf('\n\n');
|
|
219
246
|
return (end === -1 ? after : after.slice(0, end)).trim();
|
|
220
247
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
export function userPromptText(entry) {
|
|
249
|
+
if (!entry || entry.role !== 'user' || entry.kind !== 'turnStart')
|
|
250
|
+
return '';
|
|
251
|
+
const text = (entry.parts ?? [])
|
|
252
|
+
.map((part) => (typeof part?.text === 'string' ? part.text : ''))
|
|
253
|
+
.filter(Boolean)
|
|
254
|
+
.join('\n')
|
|
255
|
+
.trim();
|
|
256
|
+
if (!text)
|
|
257
|
+
return '';
|
|
258
|
+
const request = extractMarkedSection(text, 'Current user request:') ||
|
|
259
|
+
extractMarkedSection(text, 'User request:');
|
|
260
|
+
const message = extractMarkedSection(text, 'Current user message:') ||
|
|
261
|
+
extractMarkedSection(text, 'User message:');
|
|
262
|
+
return (request || message || text).trim();
|
|
263
|
+
}
|
|
264
|
+
function extractFirstUserPrompt(history) {
|
|
265
|
+
for (const entry of history) {
|
|
266
|
+
const text = userPromptText(entry);
|
|
267
|
+
if (text)
|
|
268
|
+
return singleLinePreview(text, 120);
|
|
238
269
|
}
|
|
239
270
|
return '';
|
|
240
271
|
}
|
|
@@ -247,8 +278,9 @@ function metadataFromSnapshot(snapshot) {
|
|
|
247
278
|
updatedAt: snapshot.updatedAt,
|
|
248
279
|
modelId: snapshot.modelId,
|
|
249
280
|
messageCount: snapshot.history.length,
|
|
250
|
-
lastUserMessage:
|
|
281
|
+
lastUserMessage: extractFirstUserPrompt(snapshot.history),
|
|
251
282
|
summaryPreview: '',
|
|
283
|
+
branch: snapshot.branch ?? null,
|
|
252
284
|
};
|
|
253
285
|
}
|
|
254
286
|
export function listSessionMetadata(rootDir, env = process.env) {
|
package/dist/src/session.js
CHANGED
|
@@ -69,6 +69,14 @@ export function createSession({ rootDir, autoYes = false, agentMode, modelId, ma
|
|
|
69
69
|
serverState: cloneOpaqueState(serverState),
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
export function startNewConversation(session) {
|
|
73
|
+
clearConversation(session);
|
|
74
|
+
const createdAt = new Date().toISOString();
|
|
75
|
+
session.sessionId = createSessionId();
|
|
76
|
+
session.sessionName = null;
|
|
77
|
+
session.sessionCreatedAt = createdAt;
|
|
78
|
+
session.sessionUpdatedAt = createdAt;
|
|
79
|
+
}
|
|
72
80
|
export function clearConversation(session) {
|
|
73
81
|
session.history = [];
|
|
74
82
|
session.serverState = preserveProviderSelection(session.serverState);
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { getClientStateDir } from '../client-state.js';
|
|
4
4
|
const HISTORY_FILE = 'prompt-history.json';
|
|
5
5
|
const LEGACY_HISTORY_FILE = 'prompt-history.txt';
|
|
6
|
-
export const MAX_PROMPT_HISTORY_ENTRIES =
|
|
6
|
+
export const MAX_PROMPT_HISTORY_ENTRIES = 20;
|
|
7
7
|
const ENTRY_SEPARATOR = '\x1e';
|
|
8
8
|
function getHistoryFilePath(env = process.env) {
|
|
9
9
|
return path.join(getClientStateDir(env), HISTORY_FILE);
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -15,8 +15,8 @@ import { clearCliAuthConfig } from '../api/auth.js';
|
|
|
15
15
|
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
16
16
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
17
17
|
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
18
|
-
import {
|
|
19
|
-
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
|
|
18
|
+
import { startNewConversation, } from '../session.js';
|
|
19
|
+
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../session-store.js';
|
|
20
20
|
import { truncate } from '../utils.js';
|
|
21
21
|
import { writeClipboardText } from '../core/clipboard.js';
|
|
22
22
|
import { openUrl } from '../core/open-url.js';
|
|
@@ -116,8 +116,8 @@ export const SLASH_COMMANDS = [
|
|
|
116
116
|
description: 'Background jobs: pick to view output or kill',
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
|
-
command: '/
|
|
120
|
-
description: '
|
|
119
|
+
command: '/new',
|
|
120
|
+
description: 'start a new conversation; this session remains saved',
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
command: '/exit',
|
|
@@ -963,6 +963,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
963
963
|
activeTurnInput: '',
|
|
964
964
|
activeTurnInputPreformatted: false,
|
|
965
965
|
agentMode: session.agentMode,
|
|
966
|
+
analyzingImages: 0,
|
|
966
967
|
approvalCursor: getDefaultApprovalCursor(),
|
|
967
968
|
approvalPrompt: null,
|
|
968
969
|
autoYes: session.autoYes,
|
|
@@ -1282,6 +1283,8 @@ function appendCommandLog(state, text) {
|
|
|
1282
1283
|
};
|
|
1283
1284
|
}
|
|
1284
1285
|
async function saveSessionBoth({ serverSessionClient, session, }) {
|
|
1286
|
+
if (!sessionHasUserMessage(session))
|
|
1287
|
+
return;
|
|
1285
1288
|
saveSessionState(session);
|
|
1286
1289
|
await serverSessionClient.save(session);
|
|
1287
1290
|
}
|
|
@@ -1320,6 +1323,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1320
1323
|
let latestUsageSummary = null;
|
|
1321
1324
|
let pendingTurnEntries = [];
|
|
1322
1325
|
let activeTurnAbort = null;
|
|
1326
|
+
let newConversationInFlight = false;
|
|
1323
1327
|
let todosTouchedThisTurn = false;
|
|
1324
1328
|
const syncTodosState = () => {
|
|
1325
1329
|
store.update((current) => ({ ...current, todos: listTodos() }));
|
|
@@ -1521,7 +1525,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1521
1525
|
}));
|
|
1522
1526
|
};
|
|
1523
1527
|
const cancelActiveTurn = () => {
|
|
1524
|
-
if (!store.getState().busy)
|
|
1528
|
+
if (!store.getState().busy || newConversationInFlight)
|
|
1525
1529
|
return;
|
|
1526
1530
|
disarmExitConfirm();
|
|
1527
1531
|
dismissPendingApproval();
|
|
@@ -1660,7 +1664,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1660
1664
|
if (!isAuthenticationError(error))
|
|
1661
1665
|
return false;
|
|
1662
1666
|
clearCliAuthConfig(session.env);
|
|
1663
|
-
|
|
1667
|
+
if (sessionHasUserMessage(session)) {
|
|
1668
|
+
saveSessionState(session);
|
|
1669
|
+
}
|
|
1664
1670
|
fatalError = new Error(authenticationErrorMessage(error));
|
|
1665
1671
|
requestExit();
|
|
1666
1672
|
return true;
|
|
@@ -1677,6 +1683,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1677
1683
|
return true;
|
|
1678
1684
|
}
|
|
1679
1685
|
};
|
|
1686
|
+
const saveActiveSessionOrAbort = async () => {
|
|
1687
|
+
try {
|
|
1688
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1689
|
+
return true;
|
|
1690
|
+
}
|
|
1691
|
+
catch (error) {
|
|
1692
|
+
if (exitForAuthenticationError(error))
|
|
1693
|
+
return false;
|
|
1694
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1680
1698
|
const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
|
|
1681
1699
|
if (exiting || signal?.aborted) {
|
|
1682
1700
|
resolve(null);
|
|
@@ -2243,23 +2261,53 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2243
2261
|
}
|
|
2244
2262
|
return;
|
|
2245
2263
|
}
|
|
2246
|
-
if (input === '/
|
|
2247
|
-
|
|
2264
|
+
if (input === '/new') {
|
|
2265
|
+
newConversationInFlight = true;
|
|
2266
|
+
store.update((current) => ({
|
|
2267
|
+
...current,
|
|
2268
|
+
busy: true,
|
|
2269
|
+
busySince: Date.now(),
|
|
2270
|
+
imageAttachments: [],
|
|
2271
|
+
queuedMessage: null,
|
|
2272
|
+
status: 'Starting a new conversation...',
|
|
2273
|
+
}));
|
|
2274
|
+
if (!(await saveActiveSessionOrAbort())) {
|
|
2275
|
+
newConversationInFlight = false;
|
|
2276
|
+
store.update((current) => ({
|
|
2277
|
+
...current,
|
|
2278
|
+
busy: false,
|
|
2279
|
+
busySince: null,
|
|
2280
|
+
status: 'Ready',
|
|
2281
|
+
}));
|
|
2282
|
+
await flushQueuedMessage();
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
startNewConversation(session);
|
|
2286
|
+
setBackgroundJobSession(session.sessionId);
|
|
2287
|
+
setScratchSession(session.sessionId);
|
|
2288
|
+
setTodoSession(session.sessionId);
|
|
2248
2289
|
clearTodos();
|
|
2290
|
+
syncBackgroundJobsState();
|
|
2249
2291
|
syncTodosState();
|
|
2250
2292
|
latestUsageSummary = null;
|
|
2251
|
-
if (!(await saveActiveSession()))
|
|
2252
|
-
return;
|
|
2253
2293
|
store.replaceTranscript([
|
|
2254
2294
|
{
|
|
2255
|
-
body: '
|
|
2295
|
+
body: 'Started a new conversation. The previous session remains saved.',
|
|
2256
2296
|
kind: 'system',
|
|
2257
|
-
title: '
|
|
2297
|
+
title: 'Session',
|
|
2258
2298
|
},
|
|
2259
2299
|
]);
|
|
2300
|
+
await saveActiveSession();
|
|
2260
2301
|
syncShellStateFromSession();
|
|
2261
|
-
|
|
2302
|
+
newConversationInFlight = false;
|
|
2303
|
+
store.update((current) => ({
|
|
2304
|
+
...current,
|
|
2305
|
+
busy: false,
|
|
2306
|
+
busySince: null,
|
|
2307
|
+
status: 'Ready',
|
|
2308
|
+
}));
|
|
2262
2309
|
await remountTui();
|
|
2310
|
+
await flushQueuedMessage();
|
|
2263
2311
|
return;
|
|
2264
2312
|
}
|
|
2265
2313
|
latestUsageSummary = null;
|
|
@@ -2284,6 +2332,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2284
2332
|
...current,
|
|
2285
2333
|
activeTurnInput: input,
|
|
2286
2334
|
activeTurnInputPreformatted: preformatted,
|
|
2335
|
+
analyzingImages: 0,
|
|
2287
2336
|
busy: true,
|
|
2288
2337
|
busySince: turnStartedAt,
|
|
2289
2338
|
clockNow: turnStartedAt,
|
|
@@ -2407,6 +2456,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2407
2456
|
}
|
|
2408
2457
|
}
|
|
2409
2458
|
};
|
|
2459
|
+
session.onImageAnalysis = (activeImageCount) => {
|
|
2460
|
+
store.update((current) => ({
|
|
2461
|
+
...current,
|
|
2462
|
+
analyzingImages: Math.max(0, activeImageCount),
|
|
2463
|
+
}));
|
|
2464
|
+
};
|
|
2410
2465
|
session.onStatus = (message) => {
|
|
2411
2466
|
const panel = thinkingPanelFromStatus(message);
|
|
2412
2467
|
store.update((current) => ({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agentModeLabel } from '../../agent-mode.js';
|
|
2
|
-
import { truncate } from '../../utils.js';
|
|
2
|
+
import { singleLinePreview, truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
4
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
5
|
import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
@@ -163,7 +163,7 @@ const CLIENT_SLASH_COMMANDS = [
|
|
|
163
163
|
{ command: '/model', description: 'Switch the active model' },
|
|
164
164
|
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
165
165
|
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
166
|
-
{ command: '/
|
|
166
|
+
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
167
167
|
{ command: '/exit', description: 'Quit the current session' },
|
|
168
168
|
];
|
|
169
169
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
@@ -212,6 +212,9 @@ export function getSlashCommandSuggestions(input) {
|
|
|
212
212
|
function formatModelLabel(modelId, serverModels) {
|
|
213
213
|
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
214
214
|
}
|
|
215
|
+
function pickerModelLabel(modelId, serverModels) {
|
|
216
|
+
return formatModelLabel(modelId, serverModels).replace(/\s*\([^)]*\)\s*$/, '');
|
|
217
|
+
}
|
|
215
218
|
function filterResumeSessions(sessions, filter, serverModels) {
|
|
216
219
|
const q = filter.trim().toLowerCase();
|
|
217
220
|
if (!q)
|
|
@@ -549,7 +552,12 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
549
552
|
}
|
|
550
553
|
lines.push(plainLine(''));
|
|
551
554
|
}
|
|
552
|
-
|
|
555
|
+
const busyLabel = state.analyzingImages > 0
|
|
556
|
+
? state.analyzingImages > 1
|
|
557
|
+
? 'Analyzing images'
|
|
558
|
+
: 'Analyzing image'
|
|
559
|
+
: 'Working';
|
|
560
|
+
lines.push(plainLine(`${WORKING_CLOCK_ICON} ${busyLabel} · ${elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`}`, { color: 'yellow' }));
|
|
553
561
|
const todos = state.todos ?? [];
|
|
554
562
|
if (todos.length > 0) {
|
|
555
563
|
lines.push(plainLine(''));
|
|
@@ -865,18 +873,54 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
865
873
|
}
|
|
866
874
|
if (state.resumePickerOpen) {
|
|
867
875
|
const filtered = filterResumeSessions(state.resumePickerSessions, state.resumePickerFilter, state.serverModels);
|
|
876
|
+
const pickerWidth = Math.max(20, width - 2);
|
|
877
|
+
const divider = () => plainLine('─'.repeat(pickerWidth), { color: 'gray', dim: true });
|
|
868
878
|
lines.push(plainLine('Resume a previous session', { color: 'cyan', bold: true }));
|
|
869
879
|
lines.push(line(span('Search: ', { color: 'gray' }), span(state.resumePickerFilter, {}), span('█', { color: 'gray' })));
|
|
870
|
-
|
|
880
|
+
lines.push(divider());
|
|
881
|
+
const maxCards = Math.max(2, Math.min(filtered.length, Math.floor((height - 10) / 3)));
|
|
882
|
+
let start = 0;
|
|
883
|
+
if (filtered.length > maxCards) {
|
|
884
|
+
start = Math.min(Math.max(0, state.resumePickerIndex - Math.floor(maxCards / 2)), filtered.length - maxCards);
|
|
885
|
+
}
|
|
886
|
+
const visible = filtered.slice(start, start + maxCards);
|
|
887
|
+
if (start > 0) {
|
|
888
|
+
lines.push(plainLine(` … ${start} newer`, { color: 'gray', dim: true }));
|
|
889
|
+
}
|
|
890
|
+
for (const [offset, session] of visible.entries()) {
|
|
891
|
+
const index = start + offset;
|
|
871
892
|
const selected = index === state.resumePickerIndex;
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
893
|
+
const prompt = singleLinePreview(session.lastUserMessage, pickerWidth) ||
|
|
894
|
+
session.name ||
|
|
895
|
+
`Session ${session.id}`;
|
|
896
|
+
lines.push(line(span(selected ? '› ' : ' ', { color: 'cyan' }), span(fitLine(prompt, Math.max(10, pickerWidth - 2)), selected
|
|
897
|
+
? { color: 'cyan', bold: true }
|
|
898
|
+
: session.lastUserMessage
|
|
899
|
+
? {}
|
|
900
|
+
: { color: 'gray', dim: true })));
|
|
901
|
+
const meta = [
|
|
902
|
+
formatRelativeTime(session.updatedAt),
|
|
903
|
+
pickerModelLabel(session.modelId, state.serverModels),
|
|
904
|
+
session.branch ?? null,
|
|
905
|
+
]
|
|
906
|
+
.filter(Boolean)
|
|
907
|
+
.join(' · ');
|
|
908
|
+
lines.push(line(span(' '), span(fitLine(meta, Math.max(10, pickerWidth - 4)), {
|
|
909
|
+
color: selected ? 'cyan' : 'gray',
|
|
910
|
+
dim: !selected,
|
|
911
|
+
})));
|
|
912
|
+
if (offset < visible.length - 1)
|
|
913
|
+
lines.push(plainLine(''));
|
|
914
|
+
}
|
|
915
|
+
const remaining = filtered.length - (start + visible.length);
|
|
916
|
+
if (remaining > 0) {
|
|
917
|
+
lines.push(plainLine(` … ${remaining} older`, { color: 'gray', dim: true }));
|
|
875
918
|
}
|
|
876
919
|
if (filtered.length === 0) {
|
|
877
920
|
lines.push(plainLine('No sessions match.', { color: 'gray' }));
|
|
878
921
|
}
|
|
879
|
-
lines.push(
|
|
922
|
+
lines.push(divider());
|
|
923
|
+
lines.push(plainLine('↑/↓ move · enter resume · esc start new · ctrl+c quit', {
|
|
880
924
|
color: 'gray',
|
|
881
925
|
}));
|
|
882
926
|
}
|
|
@@ -923,7 +967,10 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
923
967
|
const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
|
|
924
968
|
if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
|
|
925
969
|
const composerLines = [];
|
|
926
|
-
if (state.
|
|
970
|
+
if (state.busy && state.status === 'Starting a new conversation...') {
|
|
971
|
+
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
972
|
+
}
|
|
973
|
+
else if (state.queuedMessage) {
|
|
927
974
|
const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
|
|
928
975
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
929
976
|
composerLines.push(line(span(`↳ Queued · "${preview}"`, { color: 'gray', dim: true }), ...(imageCount > 0
|
package/dist/src/utils.js
CHANGED
|
@@ -17,6 +17,15 @@ export function truncate(text, maxChars = 4000) {
|
|
|
17
17
|
return text;
|
|
18
18
|
return `${text.slice(0, maxChars)}\n... (truncated)`;
|
|
19
19
|
}
|
|
20
|
+
export function singleLinePreview(text, maxChars) {
|
|
21
|
+
const collapsed = String(text ?? '')
|
|
22
|
+
.replace(/\n\.\.\. \(truncated\)\s*$/, '…')
|
|
23
|
+
.replace(/\s+/g, ' ')
|
|
24
|
+
.trim();
|
|
25
|
+
if (collapsed.length <= maxChars)
|
|
26
|
+
return collapsed;
|
|
27
|
+
return `${collapsed.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
|
|
28
|
+
}
|
|
20
29
|
export function clampInteger(value, fallback, max) {
|
|
21
30
|
const numeric = Number(value);
|
|
22
31
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.9",
|
|
4
4
|
"description": "TheGitAI is an AI coding agent for your terminal. It indexes your repository, writes and edits files, runs commands, and builds features with you.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
38
38
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
39
39
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
40
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
41
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
42
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
43
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
40
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.9",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.9",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.9",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.9",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|