termux-dev 1.8.0 → 1.8.2
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 +3 -0
- package/assets/banner.svg +1 -1
- package/assets/preview.svg +14 -5
- package/dist/cli/background-ui.js +116 -0
- package/dist/cli/banner.js +2 -3
- package/dist/cli/finder.js +241 -0
- package/dist/cli/headless.js +3 -1
- package/dist/cli/index.js +371 -339
- package/dist/cli/logo.js +30 -0
- package/dist/cli/markdown.js +17 -2
- package/dist/cli/picker.js +11 -3
- package/dist/cli/prompt.js +39 -1
- package/dist/cli/transcript.js +21 -1
- package/dist/cli/ui.js +168 -30
- package/dist/cli/updater.js +10 -8
- package/dist/cli/welcome.js +56 -0
- package/dist/core/background.js +376 -0
- package/dist/core/config-merge.js +67 -0
- package/dist/core/i18n.js +2 -2
- package/dist/core/locales/de.js +55 -0
- package/dist/core/locales/en.js +55 -0
- package/dist/core/locales/es.js +55 -0
- package/dist/core/locales/fr.js +55 -0
- package/dist/core/locales/ja.js +55 -0
- package/dist/core/locales/pt.js +55 -0
- package/dist/core/locales/ru.js +55 -0
- package/dist/core/locales/uk.js +55 -0
- package/dist/core/locales/zh.js +55 -0
- package/dist/core/loop.js +18 -1
- package/dist/core/types.js +1 -1
- package/dist/permissions/guard.js +3 -1
- package/dist/prompts/builder.js +2 -1
- package/dist/tools/background.js +118 -0
- package/dist/tools/bash.js +23 -1
- package/dist/tools/index.js +2 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
|
-
import { search, password, input } from '@inquirer/prompts';
|
|
4
3
|
import pc from 'picocolors';
|
|
5
4
|
import os from 'os';
|
|
6
5
|
import path from 'path';
|
|
@@ -21,6 +20,8 @@ import { notifyDevice, acquireWakeLock, isTermux, notificationBody, sendDesktopN
|
|
|
21
20
|
import { copyToClipboard } from './clipboard-write.js';
|
|
22
21
|
import { REWIND_CURRENT, messageDraft, rewindItems, userEntries } from './rewind.js';
|
|
23
22
|
import { runPicker } from './picker.js';
|
|
23
|
+
import { runFinder, runTextInput } from './finder.js';
|
|
24
|
+
import { renderWelcome } from './welcome.js';
|
|
24
25
|
import { savePlanFile } from '../core/planfile.js';
|
|
25
26
|
import { BASE_TITLE, RESTORE_TERMINAL_TITLE, SAVE_TERMINAL_TITLE, cleanTitle, generateChatTitle, provisionalTitle, terminalTitleSequence } from '../core/title.js';
|
|
26
27
|
import { runDoctor } from './doctor.js';
|
|
@@ -32,7 +33,7 @@ import { SmoothStreamer } from './smooth.js';
|
|
|
32
33
|
import { askPrompt } from './prompt.js';
|
|
33
34
|
import { resolveAtMentions } from './files.js';
|
|
34
35
|
import { runStartupUpdateCheck, checkForUpdates, performSelfUpdate } from './updater.js';
|
|
35
|
-
import { setActiveTheme, getCurrentTheme, listThemes, findTheme
|
|
36
|
+
import { setActiveTheme, getCurrentTheme, listThemes, findTheme } from './theme.js';
|
|
36
37
|
import { runHeadlessMode } from './headless.js';
|
|
37
38
|
import { CustomCommandManager } from '../core/commands.js';
|
|
38
39
|
import { UsageTracker } from '../core/usage.js';
|
|
@@ -41,6 +42,9 @@ import { SessionExporter } from './export.js';
|
|
|
41
42
|
import { MCPManager, discoverProjectMcpServers, hashProjectMcpServers, readClaudeServers, removeUserServer, saveUserServers } from '../mcp/manager.js';
|
|
42
43
|
import { writeSecureFile } from '../core/secure-file.js';
|
|
43
44
|
import { applyLocalConfig, persistedConfig, withoutSessionOnlySettings } from '../core/local-config.js';
|
|
45
|
+
import { cloneConfig, mergeConfigChanges } from '../core/config-merge.js';
|
|
46
|
+
import { getBackgroundManager } from '../core/background.js';
|
|
47
|
+
import { backgroundSummary, noteRows, runTasksCommand } from './background-ui.js';
|
|
44
48
|
import { LiveTelemetryTracker } from './telemetry.js';
|
|
45
49
|
import { renderPowerlineStatus, GitStatusCache } from './statusline.js';
|
|
46
50
|
import { formatDiffBox } from './diff.js';
|
|
@@ -54,7 +58,7 @@ import { askConfirm, askPlanDecision, printFarewell } from './confirm.js';
|
|
|
54
58
|
import { createCheckpoint, diffCheckpoint, restoreCheckpoint } from '../core/checkpoint.js';
|
|
55
59
|
import { log, note } from './log.js';
|
|
56
60
|
import { displayWidth, tailWidth } from './width.js';
|
|
57
|
-
import { collapsedThoughtLine, messagesToItems, renderTranscript, toolResultRows } from './transcript.js';
|
|
61
|
+
import { COMPACT_MARK, collapsedThoughtLine, renderItem, messagesToItems, renderTranscript, toolResultRows } from './transcript.js';
|
|
58
62
|
import { chooseEffort, effortCommand } from './effort.js';
|
|
59
63
|
import { effectiveReasoning, parseEffortArg, detectRoute, thinkLabel } from '../core/reasoning.js';
|
|
60
64
|
import { catalogInfo, installCapabilities, refreshCapabilities, refreshCatalog } from '../core/capabilities.js';
|
|
@@ -63,7 +67,7 @@ import { contextBreakdown } from '../core/context-stats.js';
|
|
|
63
67
|
import { runUsagePanel } from './usage-panel.js';
|
|
64
68
|
import { TurnComposer } from './typeahead.js';
|
|
65
69
|
import { EXTENDED_KEYS_OFF, normalizeKey, splitKeys } from './keys.js';
|
|
66
|
-
import { getLanguage, setLanguage, t } from '../core/i18n.js';
|
|
70
|
+
import { LANGUAGES, getLanguage, setLanguage, t } from '../core/i18n.js';
|
|
67
71
|
import { anthropicOnlyModels, formatFromBaseUrl, refreshFormats } from '../core/formats.js';
|
|
68
72
|
import { providerFormat } from '../providers/index.js';
|
|
69
73
|
const CONFIG_PATH = path.join(os.homedir(), '.devxrc.json');
|
|
@@ -74,7 +78,18 @@ function maskApiKey(key) {
|
|
|
74
78
|
return '****';
|
|
75
79
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
76
80
|
}
|
|
77
|
-
|
|
81
|
+
/**
|
|
82
|
+
* What this process last knew of the config file (its own settings after the start-up overrides,
|
|
83
|
+
* or what it wrote last). A save writes only the difference to it, see core/config-merge.ts.
|
|
84
|
+
*/
|
|
85
|
+
let configBaseline = null;
|
|
86
|
+
let saveChain = Promise.resolve();
|
|
87
|
+
/** Call once the start-up overrides (-m, project config) are applied: those are not saved by accident. */
|
|
88
|
+
function markConfigBaseline(config) {
|
|
89
|
+
syncKeyMaps(config);
|
|
90
|
+
configBaseline = cloneConfig(persistedConfig(config));
|
|
91
|
+
}
|
|
92
|
+
function syncKeyMaps(config) {
|
|
78
93
|
config.apiKeys = config.apiKeys || {};
|
|
79
94
|
config.baseUrls = config.baseUrls || {};
|
|
80
95
|
if (config.provider && config.apiKey) {
|
|
@@ -83,10 +98,38 @@ async function saveConfig(config) {
|
|
|
83
98
|
if (config.provider && config.baseUrl) {
|
|
84
99
|
config.baseUrls[config.provider] = config.baseUrl;
|
|
85
100
|
}
|
|
101
|
+
}
|
|
102
|
+
async function writeConfigFile(config) {
|
|
103
|
+
syncKeyMaps(config);
|
|
86
104
|
// Runtime-only flags and YOLO (one run only) must not end up in the user's config file.
|
|
87
|
-
const
|
|
88
|
-
//
|
|
89
|
-
|
|
105
|
+
const mine = cloneConfig(persistedConfig(config));
|
|
106
|
+
// Another devx may have saved since we looked: write only our own changes on top of the file.
|
|
107
|
+
let disk = null;
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(await fs.readFile(CONFIG_PATH, 'utf8'));
|
|
110
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
111
|
+
disk = withoutSessionOnlySettings(parsed);
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
const merged = disk ? mergeConfigChanges(configBaseline ?? {}, mine, disk) : mine;
|
|
115
|
+
// The file holds API keys: owner-only permissions (0600). Written aside and renamed, so a second
|
|
116
|
+
// process never reads half a file.
|
|
117
|
+
const tmp = `${CONFIG_PATH}.${process.pid}.tmp`;
|
|
118
|
+
try {
|
|
119
|
+
await writeSecureFile(tmp, JSON.stringify(merged, null, 2));
|
|
120
|
+
await fs.rename(tmp, CONFIG_PATH);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
await fs.rm(tmp, { force: true }).catch(() => null);
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
configBaseline = mine;
|
|
127
|
+
}
|
|
128
|
+
async function saveConfig(config) {
|
|
129
|
+
// One save at a time inside this process; the read-merge-write of the next one sees the previous.
|
|
130
|
+
const run = saveChain.then(() => writeConfigFile(config));
|
|
131
|
+
saveChain = run.catch(() => undefined);
|
|
132
|
+
return run;
|
|
90
133
|
}
|
|
91
134
|
async function fetchModels(baseUrl, apiKey) {
|
|
92
135
|
try {
|
|
@@ -138,9 +181,11 @@ const POPULAR_PROVIDER_MODELS = {
|
|
|
138
181
|
groq: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'],
|
|
139
182
|
mistral: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'],
|
|
140
183
|
};
|
|
141
|
-
|
|
184
|
+
const formatContext = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${Math.round(n / 1000)}k`);
|
|
185
|
+
/** `step` is "3/4" during the first run, so the card says where in the setup you are. */
|
|
186
|
+
async function selectModel(baseUrl, apiKey, currentModel, providerId = '', _allowCancel = true, step) {
|
|
142
187
|
const s = new UiSpinner();
|
|
143
|
-
s.start('
|
|
188
|
+
s.start(t('onboard.model.fetching'));
|
|
144
189
|
const result = await fetchModels(baseUrl, apiKey);
|
|
145
190
|
// The same gateway may list more models (Claude) in its Anthropic format, with the same key.
|
|
146
191
|
if (result.ok && probeEndpointsAllowed) {
|
|
@@ -160,201 +205,152 @@ async function selectModel(baseUrl, apiKey, currentModel, providerId = '', allow
|
|
|
160
205
|
contextLength: getModelContextLimit(id)
|
|
161
206
|
}));
|
|
162
207
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
if (modelChoice === '__cancel__') {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
if (modelChoice === 'custom_input') {
|
|
200
|
-
try {
|
|
201
|
-
const customModel = await input({
|
|
202
|
-
message: 'Type the model name (e.g. gpt-4o or claude-3-7-sonnet):',
|
|
203
|
-
});
|
|
204
|
-
if (!customModel || !customModel.trim())
|
|
205
|
-
return null;
|
|
206
|
-
const name = customModel.trim();
|
|
207
|
-
return { model: name, contextLimit: getModelContextLimit(name) };
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
const found = models.find(m => m.id === modelChoice);
|
|
214
|
-
const limit = (found && found.contextLength) || getModelContextLimit(modelChoice);
|
|
215
|
-
return { model: modelChoice, contextLimit: limit };
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
log.warn('Could not fetch models automatically.');
|
|
219
|
-
try {
|
|
220
|
-
const typedModel = await input({
|
|
221
|
-
message: 'Type the model name you want to use (e.g. gpt-4o):',
|
|
222
|
-
default: currentModel || ''
|
|
223
|
-
});
|
|
224
|
-
if (!typedModel || !typedModel.trim())
|
|
225
|
-
return null;
|
|
226
|
-
const name = typedModel.trim();
|
|
227
|
-
return { model: name, contextLimit: getModelContextLimit(name) };
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
208
|
+
const typeName = async (note) => {
|
|
209
|
+
const name = await runTextInput({
|
|
210
|
+
title: t('onboard.model.title'),
|
|
211
|
+
subtitle: step,
|
|
212
|
+
label: t('onboard.model.label'),
|
|
213
|
+
initial: models.length === 0 ? currentModel : '',
|
|
214
|
+
notes: note ? [note] : undefined,
|
|
215
|
+
});
|
|
216
|
+
return name ? { model: name, contextLimit: getModelContextLimit(name) } : null;
|
|
217
|
+
};
|
|
218
|
+
if (models.length === 0)
|
|
219
|
+
return typeName(t('onboard.model.manual'));
|
|
220
|
+
const CUSTOM = '__custom__';
|
|
221
|
+
for (;;) {
|
|
222
|
+
const choice = await runFinder({
|
|
223
|
+
title: t('onboard.model.title'),
|
|
224
|
+
subtitle: step ?? `${models.length}`,
|
|
225
|
+
placeholder: t('onboard.model.placeholder'),
|
|
226
|
+
current: currentModel,
|
|
227
|
+
items: [
|
|
228
|
+
{ value: CUSTOM, label: t('onboard.model.custom'), detail: t('onboard.model.customDetail'), pinned: true },
|
|
229
|
+
...models.map((m) => ({
|
|
230
|
+
value: m.id,
|
|
231
|
+
label: m.id,
|
|
232
|
+
detail: [m.viaAnthropic ? t('onboard.model.anthropic') : '', m.contextLength ? t('onboard.model.context', { n: formatContext(m.contextLength) }) : ''].filter(Boolean).join(', '),
|
|
233
|
+
})),
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
if (choice === undefined)
|
|
230
237
|
return null;
|
|
238
|
+
if (choice === CUSTOM) {
|
|
239
|
+
const typed = await typeName();
|
|
240
|
+
if (typed)
|
|
241
|
+
return typed;
|
|
242
|
+
continue; // Esc in the field: back to the list
|
|
231
243
|
}
|
|
244
|
+
const found = models.find(m => m.id === choice);
|
|
245
|
+
return { model: choice, contextLimit: (found && found.contextLength) || getModelContextLimit(choice) };
|
|
232
246
|
}
|
|
233
247
|
}
|
|
248
|
+
/** The host of an address, for the detail column: `api.openai.com`. */
|
|
249
|
+
const hostOf = (url) => url.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
234
250
|
async function selectProvider(currentConfig, allowCancel = true) {
|
|
235
251
|
const apiKeys = currentConfig.apiKeys || {};
|
|
236
252
|
const baseUrls = currentConfig.baseUrls || {};
|
|
237
253
|
if (currentConfig.provider && currentConfig.apiKey) {
|
|
238
254
|
apiKeys[currentConfig.provider] = currentConfig.apiKey;
|
|
239
255
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
value: '__cancel__',
|
|
263
|
-
description: 'Keep current provider'
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
return list;
|
|
267
|
-
},
|
|
268
|
-
pageSize: 10
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
if (providerId === '__cancel__') {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
const selectedProvider = ALL_PROVIDERS.find(pr => pr.value === providerId) || { baseUrl: '', label: providerId };
|
|
278
|
-
let baseUrl = baseUrls[providerId] || selectedProvider.baseUrl;
|
|
279
|
-
if (providerId === 'custom' || !baseUrl) {
|
|
280
|
-
try {
|
|
281
|
-
const customUrl = await input({
|
|
282
|
-
message: 'Enter Base URL (e.g. https://api.openai.com/v1):',
|
|
283
|
-
default: baseUrl || ''
|
|
256
|
+
const firstRun = !allowCancel;
|
|
257
|
+
const stepOf = (n) => (firstRun ? `${n}/5` : undefined);
|
|
258
|
+
let providerId = '';
|
|
259
|
+
let baseUrl = '';
|
|
260
|
+
let apiKey = '';
|
|
261
|
+
let stage = 'provider';
|
|
262
|
+
let modelRes = null;
|
|
263
|
+
while (!modelRes) {
|
|
264
|
+
if (stage === 'provider') {
|
|
265
|
+
const choice = await runFinder({
|
|
266
|
+
title: t('onboard.provider.title'),
|
|
267
|
+
subtitle: stepOf(3),
|
|
268
|
+
placeholder: t('onboard.provider.placeholder'),
|
|
269
|
+
current: currentConfig.provider,
|
|
270
|
+
notes: firstRun ? [t('onboard.provider.note')] : undefined,
|
|
271
|
+
items: ALL_PROVIDERS.map((pr) => ({
|
|
272
|
+
value: pr.value,
|
|
273
|
+
label: pr.label,
|
|
274
|
+
detail: `${apiKeys[pr.value] ? maskApiKey(apiKeys[pr.value]) + ' ' : ''}${hostOf(pr.baseUrl || '')}`.trim(),
|
|
275
|
+
keywords: `${pr.value} ${hostOf(pr.baseUrl || '')}`,
|
|
276
|
+
pinned: pr.value === 'custom',
|
|
277
|
+
})),
|
|
284
278
|
});
|
|
285
|
-
if (
|
|
286
|
-
return null;
|
|
287
|
-
|
|
288
|
-
|
|
279
|
+
if (choice === undefined)
|
|
280
|
+
return null; // Esc on the first step leaves the whole setup
|
|
281
|
+
providerId = choice;
|
|
282
|
+
const known = ALL_PROVIDERS.find(pr => pr.value === providerId);
|
|
283
|
+
baseUrl = baseUrls[providerId] || known?.baseUrl || '';
|
|
284
|
+
stage = providerId === 'custom' || !baseUrl ? 'url' : 'key';
|
|
289
285
|
}
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
else if (stage === 'url') {
|
|
287
|
+
const url = await runTextInput({
|
|
288
|
+
title: t('onboard.baseUrl.title'),
|
|
289
|
+
subtitle: stepOf(3),
|
|
290
|
+
label: t('onboard.baseUrl.label'),
|
|
291
|
+
initial: baseUrl,
|
|
292
|
+
placeholder: t('onboard.baseUrl.placeholder'),
|
|
293
|
+
});
|
|
294
|
+
if (url === undefined) {
|
|
295
|
+
stage = 'provider';
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
baseUrl = url;
|
|
299
|
+
baseUrls[providerId] = baseUrl;
|
|
300
|
+
stage = 'key';
|
|
292
301
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
{
|
|
302
|
+
else if (stage === 'key') {
|
|
303
|
+
const label = ALL_PROVIDERS.find(pr => pr.value === providerId)?.label || providerId;
|
|
304
|
+
const back = providerId === 'custom' || !(ALL_PROVIDERS.find(pr => pr.value === providerId)?.baseUrl) ? 'url' : 'provider';
|
|
305
|
+
if (providerId === 'ollama') {
|
|
306
|
+
apiKey = 'ollama';
|
|
307
|
+
stage = 'model';
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const saved = apiKeys[providerId];
|
|
311
|
+
let useSaved = false;
|
|
312
|
+
if (saved) {
|
|
313
|
+
const choice = await runPicker({
|
|
314
|
+
title: t('onboard.key.title'),
|
|
315
|
+
subtitle: label,
|
|
316
|
+
items: [
|
|
317
|
+
{ value: 'saved', label: t('onboard.key.useSaved'), detail: maskApiKey(saved) },
|
|
318
|
+
{ value: 'new', label: t('onboard.key.enterNew') },
|
|
309
319
|
],
|
|
310
|
-
|
|
320
|
+
notes: [t('onboard.key.savedTitle', { provider: label })],
|
|
311
321
|
});
|
|
322
|
+
if (choice === undefined) {
|
|
323
|
+
stage = back;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
useSaved = choice === 'saved';
|
|
312
327
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
if (keyChoice === '__cancel__')
|
|
317
|
-
return null;
|
|
318
|
-
if (keyChoice === 'use_saved') {
|
|
319
|
-
apiKey = savedKey;
|
|
328
|
+
if (useSaved) {
|
|
329
|
+
apiKey = saved;
|
|
320
330
|
}
|
|
321
331
|
else {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
catch {
|
|
333
|
-
return null;
|
|
332
|
+
const key = await runTextInput({
|
|
333
|
+
title: t('onboard.key.title'),
|
|
334
|
+
subtitle: stepOf(4) ?? label,
|
|
335
|
+
label: t('onboard.key.label', { provider: label }),
|
|
336
|
+
secret: true,
|
|
337
|
+
notes: [t('onboard.key.note')],
|
|
338
|
+
});
|
|
339
|
+
if (key === undefined) {
|
|
340
|
+
stage = back;
|
|
341
|
+
continue;
|
|
334
342
|
}
|
|
343
|
+
apiKey = key;
|
|
344
|
+
apiKeys[providerId] = apiKey;
|
|
335
345
|
}
|
|
346
|
+
stage = 'model';
|
|
336
347
|
}
|
|
337
348
|
else {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
mask: '•'
|
|
342
|
-
});
|
|
343
|
-
if (!key || !key.trim())
|
|
344
|
-
return null;
|
|
345
|
-
apiKey = key.trim();
|
|
346
|
-
apiKeys[providerId] = apiKey;
|
|
347
|
-
}
|
|
348
|
-
catch {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
349
|
+
modelRes = await selectModel(baseUrl, apiKey, currentConfig.model || '', providerId, allowCancel, stepOf(5));
|
|
350
|
+
if (!modelRes)
|
|
351
|
+
stage = apiKey === 'ollama' ? 'provider' : 'key';
|
|
351
352
|
}
|
|
352
353
|
}
|
|
353
|
-
// Pick a model for this provider
|
|
354
|
-
const modelRes = await selectModel(baseUrl, apiKey, currentConfig.model || '', providerId, allowCancel);
|
|
355
|
-
if (!modelRes) {
|
|
356
|
-
return null;
|
|
357
|
-
}
|
|
358
354
|
const updatedConfig = {
|
|
359
355
|
...currentConfig,
|
|
360
356
|
provider: providerId,
|
|
@@ -369,15 +365,54 @@ async function selectProvider(currentConfig, allowCancel = true) {
|
|
|
369
365
|
await saveConfig(updatedConfig);
|
|
370
366
|
return updatedConfig;
|
|
371
367
|
}
|
|
368
|
+
/** The themes as a list; the interface takes the colours of the focused one at once, so you see what you pick. */
|
|
369
|
+
async function chooseTheme(current, step) {
|
|
370
|
+
const before = current ?? getCurrentTheme().id;
|
|
371
|
+
const picked = await runPicker({
|
|
372
|
+
title: t('onboard.theme.title'),
|
|
373
|
+
subtitle: step,
|
|
374
|
+
items: listThemes().map((th) => ({ value: th.id, label: th.name, detail: th.desc })),
|
|
375
|
+
current: before,
|
|
376
|
+
labelWidth: 20,
|
|
377
|
+
notes: [t('onboard.theme.note')],
|
|
378
|
+
onFocus: (id) => { setActiveTheme(id); },
|
|
379
|
+
});
|
|
380
|
+
setActiveTheme(picked ?? before);
|
|
381
|
+
return picked;
|
|
382
|
+
}
|
|
383
|
+
/** First run: the welcome card, the language, then provider, key and model, each on its own small card. */
|
|
372
384
|
async function runOnboarding() {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
385
|
+
const cols = () => process.stdout.columns || 80;
|
|
386
|
+
const show = (done) => {
|
|
387
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
388
|
+
console.log();
|
|
389
|
+
for (const line of renderWelcome(DEVX_VERSION, cols(), done))
|
|
390
|
+
console.log(line);
|
|
391
|
+
console.log();
|
|
392
|
+
};
|
|
393
|
+
show(0);
|
|
394
|
+
const language = await runPicker({
|
|
395
|
+
title: t('onboard.language.title'),
|
|
396
|
+
subtitle: '1/5',
|
|
397
|
+
items: LANGUAGES.map((l) => ({ value: l.id, label: l.name })),
|
|
398
|
+
current: getLanguage(),
|
|
399
|
+
notes: [t('onboard.language.note')],
|
|
400
|
+
labelWidth: 14,
|
|
401
|
+
});
|
|
402
|
+
if (language === undefined) {
|
|
403
|
+
process.exit(0);
|
|
404
|
+
}
|
|
405
|
+
setLanguage(language);
|
|
406
|
+
show(1);
|
|
407
|
+
const theme = await chooseTheme(undefined, '2/5');
|
|
408
|
+
show(2);
|
|
409
|
+
const res = await selectProvider({ apiKeys: {}, baseUrls: {}, language, ...(theme ? { theme } : {}), onboarded: true }, false);
|
|
377
410
|
if (!res) {
|
|
378
411
|
process.exit(0);
|
|
379
412
|
}
|
|
380
|
-
|
|
413
|
+
show(5);
|
|
414
|
+
console.log(renderNotice('ok', t('onboard.done')));
|
|
415
|
+
console.log();
|
|
381
416
|
return res;
|
|
382
417
|
}
|
|
383
418
|
/**
|
|
@@ -609,6 +644,11 @@ function formatToolGeneratingLabel(name, targetHint) {
|
|
|
609
644
|
return `→ Clarifying questions...`;
|
|
610
645
|
return `→ ${name}${target}...`;
|
|
611
646
|
}
|
|
647
|
+
/** `#ab526 · 2h ago · 12 msgs`: what tells two sessions with the same title apart. */
|
|
648
|
+
function sessionDetail(s) {
|
|
649
|
+
const count = s.messages.filter((m) => m.role !== 'system').length;
|
|
650
|
+
return `#${s.id.split('_').pop()} · ${formatSessionTime(s.updatedAt)} · ${t('resume.msgs', { n: count })}`;
|
|
651
|
+
}
|
|
612
652
|
async function handleSessionDelete(bannerMode) {
|
|
613
653
|
while (true) {
|
|
614
654
|
const sessions = await SessionManager.listSessions();
|
|
@@ -617,36 +657,18 @@ async function handleSessionDelete(bannerMode) {
|
|
|
617
657
|
log.warn('No saved sessions found.');
|
|
618
658
|
break;
|
|
619
659
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const msgCount = s.messages.filter(m => m.role !== 'system').length;
|
|
633
|
-
const shortId = s.id.split('_').pop();
|
|
634
|
-
list.push({
|
|
635
|
-
name: `${pc.red('× ')} ${pc.dim(`[#${shortId}]`)} ${s.title}`,
|
|
636
|
-
value: s.id,
|
|
637
|
-
description: `${timeStr} • ${msgCount} msgs • ${s.model || 'default'}`
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
if (!q)
|
|
641
|
-
return list;
|
|
642
|
-
return list.filter(item => item.name.toLowerCase().includes(q) || item.value.toLowerCase().includes(q));
|
|
643
|
-
},
|
|
644
|
-
pageSize: 10
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
catch {
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
660
|
+
const DELETE_ALL = '__all__';
|
|
661
|
+
const chosen = await runFinder({
|
|
662
|
+
title: t('sessions.deleteTitle'),
|
|
663
|
+
subtitle: `${sessions.length}`,
|
|
664
|
+
placeholder: t('resume.placeholder'),
|
|
665
|
+
labelWidth: 36,
|
|
666
|
+
items: [
|
|
667
|
+
{ value: DELETE_ALL, label: t('sessions.deleteAll', { n: sessions.length }), pinned: true },
|
|
668
|
+
...sessions.map((s) => ({ value: s.id, label: s.title, detail: sessionDetail(s), keywords: `${s.id} ${s.model || ''}` })),
|
|
669
|
+
],
|
|
670
|
+
});
|
|
671
|
+
const chosenSessionId = chosen ?? '__cancel__';
|
|
650
672
|
if (!chosenSessionId || chosenSessionId === '__cancel__') {
|
|
651
673
|
drawLogo(bannerMode);
|
|
652
674
|
log.info('Session deletion finished.');
|
|
@@ -675,38 +697,17 @@ async function handleSessionDelete(bannerMode) {
|
|
|
675
697
|
}
|
|
676
698
|
}
|
|
677
699
|
async function handleThemeSelect(config) {
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
theme
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const selected = await search({
|
|
689
|
-
message: `${pc.bold('Select UI Theme / Выберите цветовую тему')} ${pc.dim(`(${themeChoices.length} themes, type to filter):`)}`,
|
|
690
|
-
pageSize: 8,
|
|
691
|
-
source: async (term) => {
|
|
692
|
-
const q = (term || '').toLowerCase().trim();
|
|
693
|
-
return themeChoices
|
|
694
|
-
.filter(({ theme: t }) => !q || `${t.id} ${t.name} ${t.desc}`.toLowerCase().includes(q))
|
|
695
|
-
.map(({ choice }) => choice);
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
if (selected) {
|
|
699
|
-
config.theme = selected;
|
|
700
|
-
await saveConfig(config);
|
|
701
|
-
const th = setActiveTheme(selected);
|
|
702
|
-
// No redraw of the screen: that would wipe the conversation. New output uses the new colours.
|
|
703
|
-
if (config.ui?.layout === 'classic')
|
|
704
|
-
log.success(th.boldFn(`Theme switched to ${th.colorFn(th.symbol)} ${th.name}!`));
|
|
705
|
-
else
|
|
706
|
-
console.log(renderNotice('ok', `theme ${th.name}`));
|
|
707
|
-
}
|
|
700
|
+
const selected = await chooseTheme(config.theme);
|
|
701
|
+
if (selected) {
|
|
702
|
+
config.theme = selected;
|
|
703
|
+
await saveConfig(config);
|
|
704
|
+
const th = setActiveTheme(selected);
|
|
705
|
+
// No redraw of the screen: that would wipe the conversation. New output uses the new colours.
|
|
706
|
+
if (config.ui?.layout === 'classic')
|
|
707
|
+
log.success(th.boldFn(`Theme switched to ${th.colorFn(th.symbol)} ${th.name}!`));
|
|
708
|
+
else
|
|
709
|
+
console.log(renderNotice('ok', `theme ${th.name}`));
|
|
708
710
|
}
|
|
709
|
-
catch { }
|
|
710
711
|
}
|
|
711
712
|
async function handleSettings(config) {
|
|
712
713
|
const initial = {
|
|
@@ -809,6 +810,8 @@ export async function main() {
|
|
|
809
810
|
config.model = options.model;
|
|
810
811
|
config.maxContextTokens = resolveContextLimit({ ...config, maxContextTokens: 0 }).limit;
|
|
811
812
|
}
|
|
813
|
+
// -m and the project config are for this run: only what changes from here on is saved.
|
|
814
|
+
markConfigBaseline(config);
|
|
812
815
|
if (options.yolo) {
|
|
813
816
|
config.autoApprove = true;
|
|
814
817
|
}
|
|
@@ -863,31 +866,11 @@ export async function main() {
|
|
|
863
866
|
};
|
|
864
867
|
};
|
|
865
868
|
if (!config.onboarded) {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
description: `${renderThemePreview(t)}\n\n${pc.dim('You can change this anytime with /theme')}`
|
|
872
|
-
}
|
|
873
|
-
}));
|
|
874
|
-
try {
|
|
875
|
-
clearTerminalScreen();
|
|
876
|
-
const selected = await search({
|
|
877
|
-
message: `${pc.bold('Pick a theme to personalize your workspace:')}`,
|
|
878
|
-
source: async (term) => {
|
|
879
|
-
const q = (term || '').toLowerCase().trim();
|
|
880
|
-
return themeChoices
|
|
881
|
-
.filter(({ theme: t }) => !q || `${t.id} ${t.name} ${t.desc}`.toLowerCase().includes(q))
|
|
882
|
-
.map(({ choice }) => choice);
|
|
883
|
-
},
|
|
884
|
-
pageSize: 8
|
|
885
|
-
});
|
|
886
|
-
if (selected) {
|
|
887
|
-
config.theme = selected;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
catch { }
|
|
869
|
+
// A config from before the first-run setup had a theme step: ask it once.
|
|
870
|
+
clearTerminalScreen();
|
|
871
|
+
const selected = await chooseTheme(config.theme);
|
|
872
|
+
if (selected)
|
|
873
|
+
config.theme = selected;
|
|
891
874
|
config.onboarded = true;
|
|
892
875
|
await saveConfig(config);
|
|
893
876
|
}
|
|
@@ -918,6 +901,10 @@ export async function main() {
|
|
|
918
901
|
const checkpoints = [];
|
|
919
902
|
// What the user allowed on the approval card for this session ("Session"); "Always" goes to the config.
|
|
920
903
|
const permissionMemory = newPermissionMemory();
|
|
904
|
+
// Commands the agent keeps running in the background (servers, watchers): shown in the prompt frame, listed by /tasks.
|
|
905
|
+
const bg = getBackgroundManager();
|
|
906
|
+
const seenBgFailures = new Set();
|
|
907
|
+
const bgSummary = () => backgroundSummary(bg, seenBgFailures);
|
|
921
908
|
// The most a session may cost (USD, 0 = no limit); "continue" at the limit raises it for this session only.
|
|
922
909
|
let costLimitRaised = null;
|
|
923
910
|
let costWarned = false;
|
|
@@ -946,6 +933,11 @@ export async function main() {
|
|
|
946
933
|
// and start from a clean slate: history, cost, usage counters, undo stack, screen.
|
|
947
934
|
const startFreshSession = async () => {
|
|
948
935
|
sessionManager.startNewSession(config.model, planMode);
|
|
936
|
+
// The tasks belonged to the conversation that is being left.
|
|
937
|
+
await bg.stopAll().catch(() => 0);
|
|
938
|
+
bg.clearFinished();
|
|
939
|
+
bg.takeNotes();
|
|
940
|
+
seenBgFailures.clear();
|
|
949
941
|
transcript.length = 0;
|
|
950
942
|
checkpoints.length = 0;
|
|
951
943
|
windowTitle = '';
|
|
@@ -1026,6 +1018,8 @@ export async function main() {
|
|
|
1026
1018
|
}
|
|
1027
1019
|
}
|
|
1028
1020
|
let answer = '';
|
|
1021
|
+
// Set when this turn starts by itself, from the news of a background task (not from the user).
|
|
1022
|
+
let wakeNotes = '';
|
|
1029
1023
|
if (autoTriggerPrompt) {
|
|
1030
1024
|
answer = autoTriggerPrompt;
|
|
1031
1025
|
autoTriggerPrompt = '';
|
|
@@ -1057,7 +1051,19 @@ export async function main() {
|
|
|
1057
1051
|
dirty: GitStatusCache.getStatus()?.isDirty,
|
|
1058
1052
|
contextPercent: usagePercent,
|
|
1059
1053
|
cost: sessionCost.total,
|
|
1060
|
-
costState: sessionCost.state()
|
|
1054
|
+
costState: sessionCost.state(),
|
|
1055
|
+
background: bgSummary,
|
|
1056
|
+
subscribe: (signal) => {
|
|
1057
|
+
const onChange = () => signal('redraw');
|
|
1058
|
+
// A task ended or printed what the agent asked to hear about: the agent takes it up by itself.
|
|
1059
|
+
const onNote = () => signal(config.backgroundAutoWake === false ? 'redraw' : 'wake');
|
|
1060
|
+
bg.on('change', onChange);
|
|
1061
|
+
bg.on('note', onNote);
|
|
1062
|
+
return () => {
|
|
1063
|
+
bg.off('change', onChange);
|
|
1064
|
+
bg.off('note', onNote);
|
|
1065
|
+
};
|
|
1066
|
+
},
|
|
1061
1067
|
});
|
|
1062
1068
|
if (inputStr.startsWith('__TOGGLE_MODE__:')) {
|
|
1063
1069
|
currentDraft = inputStr.slice('__TOGGLE_MODE__:'.length);
|
|
@@ -1072,19 +1078,31 @@ export async function main() {
|
|
|
1072
1078
|
}
|
|
1073
1079
|
continue;
|
|
1074
1080
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
printFarewell(t('confirm.goodbye'));
|
|
1081
|
-
break;
|
|
1082
|
-
}
|
|
1083
|
-
else {
|
|
1081
|
+
if (inputStr.startsWith('__WAKE__:')) {
|
|
1082
|
+
// The prompt was closed by news from a background task: the agent answers it. What was typed stays.
|
|
1083
|
+
currentDraft = inputStr.slice('__WAKE__:'.length);
|
|
1084
|
+
wakeNotes = bg.takeNotes();
|
|
1085
|
+
if (!wakeNotes)
|
|
1084
1086
|
continue;
|
|
1087
|
+
console.log(noteRows(wakeNotes).map((r) => renderNotice('info', r)).join('\n'));
|
|
1088
|
+
console.log();
|
|
1089
|
+
answer = wakeNotes;
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
currentDraft = '';
|
|
1093
|
+
if (inputStr === '__CANCEL__') {
|
|
1094
|
+
const confirmExit = await askConfirm({ title: t('confirm.exitTitle'), question: t('confirm.exit'), record: false });
|
|
1095
|
+
if (confirmExit) {
|
|
1096
|
+
resetTerminalTheme();
|
|
1097
|
+
printFarewell(t('confirm.goodbye'));
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1085
1103
|
}
|
|
1104
|
+
answer = inputStr.trim();
|
|
1086
1105
|
}
|
|
1087
|
-
answer = inputStr.trim();
|
|
1088
1106
|
}
|
|
1089
1107
|
if (!answer)
|
|
1090
1108
|
continue;
|
|
@@ -1106,7 +1124,7 @@ export async function main() {
|
|
|
1106
1124
|
'/settings', '/update', '/model', '/provider', '/providers',
|
|
1107
1125
|
'/plan', '/agent', '/image', '/serve', '/memory', '/permissions', '/copy', '/rename', '/title', '/rewind', '/undo',
|
|
1108
1126
|
'/diff', '/commit', '/status', '/compact', '/init', '/doctor',
|
|
1109
|
-
'/config', '/clear', '/exit', '/quit', '/help', '/effort'
|
|
1127
|
+
'/config', '/clear', '/exit', '/quit', '/help', '/effort', '/tasks', '/bg', '/jobs'
|
|
1110
1128
|
];
|
|
1111
1129
|
if (!VALID_COMMANDS.includes(cmd)) {
|
|
1112
1130
|
const customList = await CustomCommandManager.listCommands();
|
|
@@ -1131,6 +1149,7 @@ export async function main() {
|
|
|
1131
1149
|
{ name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
|
|
1132
1150
|
{ name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
|
|
1133
1151
|
{ name: '/serve - Start local web server for web preview', value: '/serve' },
|
|
1152
|
+
{ name: '/tasks - Background tasks (list, output, stop)', value: '/tasks' },
|
|
1134
1153
|
{ name: '/memory - View or edit project memory bank', value: '/memory' },
|
|
1135
1154
|
{ name: '/permissions - What is allowed without asking', value: '/permissions' },
|
|
1136
1155
|
{ name: '/copy - Copy the last answer to the clipboard', value: '/copy' },
|
|
@@ -1148,21 +1167,17 @@ export async function main() {
|
|
|
1148
1167
|
...customSlashItems
|
|
1149
1168
|
];
|
|
1150
1169
|
try {
|
|
1151
|
-
const picked = await
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
return list;
|
|
1161
|
-
return list.filter(item => item.name.toLowerCase().includes(q) || item.value.toLowerCase().includes(q));
|
|
1162
|
-
},
|
|
1163
|
-
pageSize: 12
|
|
1170
|
+
const picked = await runFinder({
|
|
1171
|
+
title: t('commands.title'),
|
|
1172
|
+
subtitle: `${SLASH_COMMANDS.length}`,
|
|
1173
|
+
placeholder: t('commands.placeholder'),
|
|
1174
|
+
labelWidth: 14,
|
|
1175
|
+
items: SLASH_COMMANDS.map((c) => {
|
|
1176
|
+
const [name, ...rest] = c.name.split(/\s+-\s+/);
|
|
1177
|
+
return { value: c.value, label: name.trim(), detail: rest.join(' - ') };
|
|
1178
|
+
}),
|
|
1164
1179
|
});
|
|
1165
|
-
if (!picked
|
|
1180
|
+
if (!picked) {
|
|
1166
1181
|
continue;
|
|
1167
1182
|
}
|
|
1168
1183
|
if (picked === '/session del') {
|
|
@@ -1238,43 +1253,21 @@ export async function main() {
|
|
|
1238
1253
|
log.warn('No saved sessions found.');
|
|
1239
1254
|
continue;
|
|
1240
1255
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
const shortId = s.id.split('_').pop();
|
|
1255
|
-
list.push({
|
|
1256
|
-
name: `${pc.dim(`[#${shortId}]`)} ${s.title}`,
|
|
1257
|
-
value: s.id,
|
|
1258
|
-
description: `${timeStr} • ${msgCount} msgs • ${s.model || 'default'}`
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
if (!q)
|
|
1262
|
-
return list;
|
|
1263
|
-
return list.filter(item => item.name.toLowerCase().includes(q) || item.value.toLowerCase().includes(q));
|
|
1264
|
-
},
|
|
1265
|
-
pageSize: 10
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
catch {
|
|
1269
|
-
continue;
|
|
1270
|
-
}
|
|
1271
|
-
if (chosenSessionId === '__cancel__') {
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
if (chosenSessionId === '__delete_mode__') {
|
|
1275
|
-
await handleSessionDelete(config.banner || 'full');
|
|
1256
|
+
const picked = await runFinder({
|
|
1257
|
+
title: t('resume.title'),
|
|
1258
|
+
subtitle: `${sessions.length}`,
|
|
1259
|
+
placeholder: t('resume.placeholder'),
|
|
1260
|
+
labelWidth: 36,
|
|
1261
|
+
items: sessions.map((s) => ({
|
|
1262
|
+
value: s.id,
|
|
1263
|
+
label: s.title,
|
|
1264
|
+
detail: sessionDetail(s),
|
|
1265
|
+
keywords: `${s.id} ${s.model || ''}`,
|
|
1266
|
+
})),
|
|
1267
|
+
});
|
|
1268
|
+
if (picked === undefined)
|
|
1276
1269
|
continue;
|
|
1277
|
-
|
|
1270
|
+
const chosenSessionId = picked;
|
|
1278
1271
|
const loaded = await SessionManager.loadSession(chosenSessionId);
|
|
1279
1272
|
if (loaded) {
|
|
1280
1273
|
sessionManager.setLoadedSession(loaded);
|
|
@@ -1514,9 +1507,12 @@ export async function main() {
|
|
|
1514
1507
|
// A user message, not an assistant one: the Anthropic format cannot start with the assistant.
|
|
1515
1508
|
history.addMessage({
|
|
1516
1509
|
role: 'user',
|
|
1517
|
-
content:
|
|
1510
|
+
content: `${COMPACT_MARK} Summary of our conversation so far:\n${res.content}`
|
|
1518
1511
|
});
|
|
1519
|
-
|
|
1512
|
+
const item = { kind: 'compact', summary: res.content };
|
|
1513
|
+
transcript.push(item);
|
|
1514
|
+
console.log();
|
|
1515
|
+
console.log(renderItem(item, { cols: process.stdout.columns || 80, thinkingExpanded, outputExpanded }).join('\n'));
|
|
1520
1516
|
}
|
|
1521
1517
|
catch (err) {
|
|
1522
1518
|
s.stop();
|
|
@@ -1555,6 +1551,12 @@ export async function main() {
|
|
|
1555
1551
|
await runDoctor(config);
|
|
1556
1552
|
continue;
|
|
1557
1553
|
}
|
|
1554
|
+
if (cmd === '/tasks' || cmd === '/bg' || cmd === '/jobs') {
|
|
1555
|
+
const rows = await runTasksCommand(parts.slice(1).filter(Boolean), bg, process.stdout.columns || 80, (ids) => ids.forEach((id) => seenBgFailures.add(id)));
|
|
1556
|
+
console.log(rows.join('\n'));
|
|
1557
|
+
console.log();
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1558
1560
|
if (cmd === '/serve') {
|
|
1559
1561
|
const sub = parts[1]?.toLowerCase();
|
|
1560
1562
|
if (sub === 'stop') {
|
|
@@ -1981,7 +1983,10 @@ export async function main() {
|
|
|
1981
1983
|
}
|
|
1982
1984
|
}
|
|
1983
1985
|
// Resolve @file and @image mentions
|
|
1984
|
-
|
|
1986
|
+
// News from a background task is output of a program, not a message of the user: no @file in it is read.
|
|
1987
|
+
const { text: cleanAnswer, attachments, images, warnings: attachWarnings } = wakeNotes
|
|
1988
|
+
? { text: answer, attachments: [], images: [], warnings: [] }
|
|
1989
|
+
: await resolveAtMentions(answer);
|
|
1985
1990
|
for (const warning of attachWarnings)
|
|
1986
1991
|
log.warn(warning);
|
|
1987
1992
|
let finalContent = cleanAnswer;
|
|
@@ -2022,9 +2027,12 @@ export async function main() {
|
|
|
2022
2027
|
if (turnCheckpoint)
|
|
2023
2028
|
checkpoints.push(turnCheckpoint);
|
|
2024
2029
|
}
|
|
2025
|
-
|
|
2030
|
+
if (wakeNotes)
|
|
2031
|
+
transcript.push({ kind: 'raw', rows: noteRows(wakeNotes).map((r) => renderNotice('info', r)) });
|
|
2032
|
+
else
|
|
2033
|
+
transcript.push({ kind: 'user', text: answer });
|
|
2026
2034
|
// Until the model has named the chat, the start of the first message stands in for the name.
|
|
2027
|
-
if (!windowTitle && !sessionManager.hasGeneratedTitle)
|
|
2035
|
+
if (!wakeNotes && !windowTitle && !sessionManager.hasGeneratedTitle)
|
|
2028
2036
|
setWindowTitle(provisionalTitle(answer));
|
|
2029
2037
|
history.addMessage({
|
|
2030
2038
|
role: 'user',
|
|
@@ -2346,8 +2354,12 @@ export async function main() {
|
|
|
2346
2354
|
contextPercent: Math.min(100, Math.round((used / (config.maxContextTokens || getModelContextLimit(config.model))) * 100)),
|
|
2347
2355
|
cost: sessionCost.total,
|
|
2348
2356
|
costState: sessionCost.state(),
|
|
2357
|
+
background: bgSummary(),
|
|
2349
2358
|
};
|
|
2350
2359
|
}, undefined, statusLine, sendQueuedNow);
|
|
2360
|
+
// A task started or ended during the turn: the frame shows the new count.
|
|
2361
|
+
const onBgChange = () => composer.refresh();
|
|
2362
|
+
bg.on('change', onBgChange);
|
|
2351
2363
|
composer.live = liveThought;
|
|
2352
2364
|
composer.onToggle = toggleView;
|
|
2353
2365
|
const releasePauseHook = registerPauseHook(() => {
|
|
@@ -2404,6 +2416,8 @@ export async function main() {
|
|
|
2404
2416
|
for await (const event of agent.run(abortController.signal, {
|
|
2405
2417
|
takePending: async () => Promise.all(composer.takeQueued().map(queuedToMessage)),
|
|
2406
2418
|
hasPending: () => composer.hasQueued(),
|
|
2419
|
+
takeNotes: () => bg.takeNotes(),
|
|
2420
|
+
hasNotes: () => bg.hasNotes(),
|
|
2407
2421
|
})) {
|
|
2408
2422
|
if (aborted)
|
|
2409
2423
|
break;
|
|
@@ -2693,6 +2707,22 @@ export async function main() {
|
|
|
2693
2707
|
else
|
|
2694
2708
|
log.info(event.message);
|
|
2695
2709
|
}
|
|
2710
|
+
else if (event.type === 'background_note') {
|
|
2711
|
+
// A background task ended or printed what the agent asked to hear about.
|
|
2712
|
+
if (spinnerActive) {
|
|
2713
|
+
s.stop();
|
|
2714
|
+
spinnerActive = false;
|
|
2715
|
+
}
|
|
2716
|
+
finishThinking(true);
|
|
2717
|
+
streamer.finish();
|
|
2718
|
+
endText();
|
|
2719
|
+
const rows = noteRows(event.content);
|
|
2720
|
+
transcript.push({ kind: 'raw', rows: rows.map((r) => renderNotice('info', r)) });
|
|
2721
|
+
for (const row of rows)
|
|
2722
|
+
console.log(cleanUi ? renderNotice('info', row) : pc.dim(row));
|
|
2723
|
+
s.start(cleanUi ? t('status.waiting') : 'Connecting...');
|
|
2724
|
+
spinnerActive = true;
|
|
2725
|
+
}
|
|
2696
2726
|
}
|
|
2697
2727
|
finishThinking(true);
|
|
2698
2728
|
streamer.finish();
|
|
@@ -2703,6 +2733,7 @@ export async function main() {
|
|
|
2703
2733
|
endText();
|
|
2704
2734
|
// Hand the bottom of the screen back. Whatever was typed but not sent waits in the next prompt;
|
|
2705
2735
|
// a message that arrived too late for the agent starts the next turn by itself.
|
|
2736
|
+
bg.off('change', onBgChange);
|
|
2706
2737
|
const leftover = composer.stop();
|
|
2707
2738
|
if (interruptPrompt) {
|
|
2708
2739
|
autoTriggerPrompt = interruptPrompt;
|
|
@@ -2828,6 +2859,7 @@ export async function main() {
|
|
|
2828
2859
|
}
|
|
2829
2860
|
}
|
|
2830
2861
|
finally {
|
|
2862
|
+
bg.off('change', onBgChange);
|
|
2831
2863
|
const unsent = composer.stop();
|
|
2832
2864
|
if (process.stdout.isTTY)
|
|
2833
2865
|
process.stdout.write('\x1b[?1004l');
|