termux-dev 1.8.0 → 1.8.1
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/assets/banner.svg +1 -1
- package/assets/preview.svg +14 -5
- package/dist/cli/banner.js +2 -3
- package/dist/cli/finder.js +241 -0
- package/dist/cli/index.js +236 -320
- package/dist/cli/logo.js +30 -0
- package/dist/cli/markdown.js +17 -2
- package/dist/cli/picker.js +11 -3
- package/dist/cli/transcript.js +21 -1
- package/dist/cli/ui.js +6 -5
- package/dist/cli/welcome.js +56 -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/types.js +1 -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';
|
|
@@ -54,7 +55,7 @@ import { askConfirm, askPlanDecision, printFarewell } from './confirm.js';
|
|
|
54
55
|
import { createCheckpoint, diffCheckpoint, restoreCheckpoint } from '../core/checkpoint.js';
|
|
55
56
|
import { log, note } from './log.js';
|
|
56
57
|
import { displayWidth, tailWidth } from './width.js';
|
|
57
|
-
import { collapsedThoughtLine, messagesToItems, renderTranscript, toolResultRows } from './transcript.js';
|
|
58
|
+
import { COMPACT_MARK, collapsedThoughtLine, renderItem, messagesToItems, renderTranscript, toolResultRows } from './transcript.js';
|
|
58
59
|
import { chooseEffort, effortCommand } from './effort.js';
|
|
59
60
|
import { effectiveReasoning, parseEffortArg, detectRoute, thinkLabel } from '../core/reasoning.js';
|
|
60
61
|
import { catalogInfo, installCapabilities, refreshCapabilities, refreshCatalog } from '../core/capabilities.js';
|
|
@@ -63,7 +64,7 @@ import { contextBreakdown } from '../core/context-stats.js';
|
|
|
63
64
|
import { runUsagePanel } from './usage-panel.js';
|
|
64
65
|
import { TurnComposer } from './typeahead.js';
|
|
65
66
|
import { EXTENDED_KEYS_OFF, normalizeKey, splitKeys } from './keys.js';
|
|
66
|
-
import { getLanguage, setLanguage, t } from '../core/i18n.js';
|
|
67
|
+
import { LANGUAGES, getLanguage, setLanguage, t } from '../core/i18n.js';
|
|
67
68
|
import { anthropicOnlyModels, formatFromBaseUrl, refreshFormats } from '../core/formats.js';
|
|
68
69
|
import { providerFormat } from '../providers/index.js';
|
|
69
70
|
const CONFIG_PATH = path.join(os.homedir(), '.devxrc.json');
|
|
@@ -138,9 +139,11 @@ const POPULAR_PROVIDER_MODELS = {
|
|
|
138
139
|
groq: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'],
|
|
139
140
|
mistral: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'],
|
|
140
141
|
};
|
|
141
|
-
|
|
142
|
+
const formatContext = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${Math.round(n / 1000)}k`);
|
|
143
|
+
/** `step` is "3/4" during the first run, so the card says where in the setup you are. */
|
|
144
|
+
async function selectModel(baseUrl, apiKey, currentModel, providerId = '', _allowCancel = true, step) {
|
|
142
145
|
const s = new UiSpinner();
|
|
143
|
-
s.start('
|
|
146
|
+
s.start(t('onboard.model.fetching'));
|
|
144
147
|
const result = await fetchModels(baseUrl, apiKey);
|
|
145
148
|
// The same gateway may list more models (Claude) in its Anthropic format, with the same key.
|
|
146
149
|
if (result.ok && probeEndpointsAllowed) {
|
|
@@ -160,201 +163,152 @@ async function selectModel(baseUrl, apiKey, currentModel, providerId = '', allow
|
|
|
160
163
|
contextLength: getModelContextLimit(id)
|
|
161
164
|
}));
|
|
162
165
|
}
|
|
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 {
|
|
166
|
+
const typeName = async (note) => {
|
|
167
|
+
const name = await runTextInput({
|
|
168
|
+
title: t('onboard.model.title'),
|
|
169
|
+
subtitle: step,
|
|
170
|
+
label: t('onboard.model.label'),
|
|
171
|
+
initial: models.length === 0 ? currentModel : '',
|
|
172
|
+
notes: note ? [note] : undefined,
|
|
173
|
+
});
|
|
174
|
+
return name ? { model: name, contextLimit: getModelContextLimit(name) } : null;
|
|
175
|
+
};
|
|
176
|
+
if (models.length === 0)
|
|
177
|
+
return typeName(t('onboard.model.manual'));
|
|
178
|
+
const CUSTOM = '__custom__';
|
|
179
|
+
for (;;) {
|
|
180
|
+
const choice = await runFinder({
|
|
181
|
+
title: t('onboard.model.title'),
|
|
182
|
+
subtitle: step ?? `${models.length}`,
|
|
183
|
+
placeholder: t('onboard.model.placeholder'),
|
|
184
|
+
current: currentModel,
|
|
185
|
+
items: [
|
|
186
|
+
{ value: CUSTOM, label: t('onboard.model.custom'), detail: t('onboard.model.customDetail'), pinned: true },
|
|
187
|
+
...models.map((m) => ({
|
|
188
|
+
value: m.id,
|
|
189
|
+
label: m.id,
|
|
190
|
+
detail: [m.viaAnthropic ? t('onboard.model.anthropic') : '', m.contextLength ? t('onboard.model.context', { n: formatContext(m.contextLength) }) : ''].filter(Boolean).join(', '),
|
|
191
|
+
})),
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
if (choice === undefined)
|
|
230
195
|
return null;
|
|
196
|
+
if (choice === CUSTOM) {
|
|
197
|
+
const typed = await typeName();
|
|
198
|
+
if (typed)
|
|
199
|
+
return typed;
|
|
200
|
+
continue; // Esc in the field: back to the list
|
|
231
201
|
}
|
|
202
|
+
const found = models.find(m => m.id === choice);
|
|
203
|
+
return { model: choice, contextLimit: (found && found.contextLength) || getModelContextLimit(choice) };
|
|
232
204
|
}
|
|
233
205
|
}
|
|
206
|
+
/** The host of an address, for the detail column: `api.openai.com`. */
|
|
207
|
+
const hostOf = (url) => url.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
234
208
|
async function selectProvider(currentConfig, allowCancel = true) {
|
|
235
209
|
const apiKeys = currentConfig.apiKeys || {};
|
|
236
210
|
const baseUrls = currentConfig.baseUrls || {};
|
|
237
211
|
if (currentConfig.provider && currentConfig.apiKey) {
|
|
238
212
|
apiKeys[currentConfig.provider] = currentConfig.apiKey;
|
|
239
213
|
}
|
|
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 || ''
|
|
214
|
+
const firstRun = !allowCancel;
|
|
215
|
+
const stepOf = (n) => (firstRun ? `${n}/5` : undefined);
|
|
216
|
+
let providerId = '';
|
|
217
|
+
let baseUrl = '';
|
|
218
|
+
let apiKey = '';
|
|
219
|
+
let stage = 'provider';
|
|
220
|
+
let modelRes = null;
|
|
221
|
+
while (!modelRes) {
|
|
222
|
+
if (stage === 'provider') {
|
|
223
|
+
const choice = await runFinder({
|
|
224
|
+
title: t('onboard.provider.title'),
|
|
225
|
+
subtitle: stepOf(3),
|
|
226
|
+
placeholder: t('onboard.provider.placeholder'),
|
|
227
|
+
current: currentConfig.provider,
|
|
228
|
+
notes: firstRun ? [t('onboard.provider.note')] : undefined,
|
|
229
|
+
items: ALL_PROVIDERS.map((pr) => ({
|
|
230
|
+
value: pr.value,
|
|
231
|
+
label: pr.label,
|
|
232
|
+
detail: `${apiKeys[pr.value] ? maskApiKey(apiKeys[pr.value]) + ' ' : ''}${hostOf(pr.baseUrl || '')}`.trim(),
|
|
233
|
+
keywords: `${pr.value} ${hostOf(pr.baseUrl || '')}`,
|
|
234
|
+
pinned: pr.value === 'custom',
|
|
235
|
+
})),
|
|
284
236
|
});
|
|
285
|
-
if (
|
|
286
|
-
return null;
|
|
287
|
-
|
|
288
|
-
|
|
237
|
+
if (choice === undefined)
|
|
238
|
+
return null; // Esc on the first step leaves the whole setup
|
|
239
|
+
providerId = choice;
|
|
240
|
+
const known = ALL_PROVIDERS.find(pr => pr.value === providerId);
|
|
241
|
+
baseUrl = baseUrls[providerId] || known?.baseUrl || '';
|
|
242
|
+
stage = providerId === 'custom' || !baseUrl ? 'url' : 'key';
|
|
289
243
|
}
|
|
290
|
-
|
|
291
|
-
|
|
244
|
+
else if (stage === 'url') {
|
|
245
|
+
const url = await runTextInput({
|
|
246
|
+
title: t('onboard.baseUrl.title'),
|
|
247
|
+
subtitle: stepOf(3),
|
|
248
|
+
label: t('onboard.baseUrl.label'),
|
|
249
|
+
initial: baseUrl,
|
|
250
|
+
placeholder: t('onboard.baseUrl.placeholder'),
|
|
251
|
+
});
|
|
252
|
+
if (url === undefined) {
|
|
253
|
+
stage = 'provider';
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
baseUrl = url;
|
|
257
|
+
baseUrls[providerId] = baseUrl;
|
|
258
|
+
stage = 'key';
|
|
292
259
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
{
|
|
260
|
+
else if (stage === 'key') {
|
|
261
|
+
const label = ALL_PROVIDERS.find(pr => pr.value === providerId)?.label || providerId;
|
|
262
|
+
const back = providerId === 'custom' || !(ALL_PROVIDERS.find(pr => pr.value === providerId)?.baseUrl) ? 'url' : 'provider';
|
|
263
|
+
if (providerId === 'ollama') {
|
|
264
|
+
apiKey = 'ollama';
|
|
265
|
+
stage = 'model';
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const saved = apiKeys[providerId];
|
|
269
|
+
let useSaved = false;
|
|
270
|
+
if (saved) {
|
|
271
|
+
const choice = await runPicker({
|
|
272
|
+
title: t('onboard.key.title'),
|
|
273
|
+
subtitle: label,
|
|
274
|
+
items: [
|
|
275
|
+
{ value: 'saved', label: t('onboard.key.useSaved'), detail: maskApiKey(saved) },
|
|
276
|
+
{ value: 'new', label: t('onboard.key.enterNew') },
|
|
309
277
|
],
|
|
310
|
-
|
|
278
|
+
notes: [t('onboard.key.savedTitle', { provider: label })],
|
|
311
279
|
});
|
|
280
|
+
if (choice === undefined) {
|
|
281
|
+
stage = back;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
useSaved = choice === 'saved';
|
|
312
285
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
if (keyChoice === '__cancel__')
|
|
317
|
-
return null;
|
|
318
|
-
if (keyChoice === 'use_saved') {
|
|
319
|
-
apiKey = savedKey;
|
|
286
|
+
if (useSaved) {
|
|
287
|
+
apiKey = saved;
|
|
320
288
|
}
|
|
321
289
|
else {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
catch {
|
|
333
|
-
return null;
|
|
290
|
+
const key = await runTextInput({
|
|
291
|
+
title: t('onboard.key.title'),
|
|
292
|
+
subtitle: stepOf(4) ?? label,
|
|
293
|
+
label: t('onboard.key.label', { provider: label }),
|
|
294
|
+
secret: true,
|
|
295
|
+
notes: [t('onboard.key.note')],
|
|
296
|
+
});
|
|
297
|
+
if (key === undefined) {
|
|
298
|
+
stage = back;
|
|
299
|
+
continue;
|
|
334
300
|
}
|
|
301
|
+
apiKey = key;
|
|
302
|
+
apiKeys[providerId] = apiKey;
|
|
335
303
|
}
|
|
304
|
+
stage = 'model';
|
|
336
305
|
}
|
|
337
306
|
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
|
-
}
|
|
307
|
+
modelRes = await selectModel(baseUrl, apiKey, currentConfig.model || '', providerId, allowCancel, stepOf(5));
|
|
308
|
+
if (!modelRes)
|
|
309
|
+
stage = apiKey === 'ollama' ? 'provider' : 'key';
|
|
351
310
|
}
|
|
352
311
|
}
|
|
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
312
|
const updatedConfig = {
|
|
359
313
|
...currentConfig,
|
|
360
314
|
provider: providerId,
|
|
@@ -369,15 +323,54 @@ async function selectProvider(currentConfig, allowCancel = true) {
|
|
|
369
323
|
await saveConfig(updatedConfig);
|
|
370
324
|
return updatedConfig;
|
|
371
325
|
}
|
|
326
|
+
/** The themes as a list; the interface takes the colours of the focused one at once, so you see what you pick. */
|
|
327
|
+
async function chooseTheme(current, step) {
|
|
328
|
+
const before = current ?? getCurrentTheme().id;
|
|
329
|
+
const picked = await runPicker({
|
|
330
|
+
title: t('onboard.theme.title'),
|
|
331
|
+
subtitle: step,
|
|
332
|
+
items: listThemes().map((th) => ({ value: th.id, label: th.name, detail: th.desc })),
|
|
333
|
+
current: before,
|
|
334
|
+
labelWidth: 20,
|
|
335
|
+
notes: [t('onboard.theme.note')],
|
|
336
|
+
onFocus: (id) => { setActiveTheme(id); },
|
|
337
|
+
});
|
|
338
|
+
setActiveTheme(picked ?? before);
|
|
339
|
+
return picked;
|
|
340
|
+
}
|
|
341
|
+
/** First run: the welcome card, the language, then provider, key and model, each on its own small card. */
|
|
372
342
|
async function runOnboarding() {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
343
|
+
const cols = () => process.stdout.columns || 80;
|
|
344
|
+
const show = (done) => {
|
|
345
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
346
|
+
console.log();
|
|
347
|
+
for (const line of renderWelcome(DEVX_VERSION, cols(), done))
|
|
348
|
+
console.log(line);
|
|
349
|
+
console.log();
|
|
350
|
+
};
|
|
351
|
+
show(0);
|
|
352
|
+
const language = await runPicker({
|
|
353
|
+
title: t('onboard.language.title'),
|
|
354
|
+
subtitle: '1/5',
|
|
355
|
+
items: LANGUAGES.map((l) => ({ value: l.id, label: l.name })),
|
|
356
|
+
current: getLanguage(),
|
|
357
|
+
notes: [t('onboard.language.note')],
|
|
358
|
+
labelWidth: 14,
|
|
359
|
+
});
|
|
360
|
+
if (language === undefined) {
|
|
361
|
+
process.exit(0);
|
|
362
|
+
}
|
|
363
|
+
setLanguage(language);
|
|
364
|
+
show(1);
|
|
365
|
+
const theme = await chooseTheme(undefined, '2/5');
|
|
366
|
+
show(2);
|
|
367
|
+
const res = await selectProvider({ apiKeys: {}, baseUrls: {}, language, ...(theme ? { theme } : {}), onboarded: true }, false);
|
|
377
368
|
if (!res) {
|
|
378
369
|
process.exit(0);
|
|
379
370
|
}
|
|
380
|
-
|
|
371
|
+
show(5);
|
|
372
|
+
console.log(renderNotice('ok', t('onboard.done')));
|
|
373
|
+
console.log();
|
|
381
374
|
return res;
|
|
382
375
|
}
|
|
383
376
|
/**
|
|
@@ -609,6 +602,11 @@ function formatToolGeneratingLabel(name, targetHint) {
|
|
|
609
602
|
return `→ Clarifying questions...`;
|
|
610
603
|
return `→ ${name}${target}...`;
|
|
611
604
|
}
|
|
605
|
+
/** `#ab526 · 2h ago · 12 msgs`: what tells two sessions with the same title apart. */
|
|
606
|
+
function sessionDetail(s) {
|
|
607
|
+
const count = s.messages.filter((m) => m.role !== 'system').length;
|
|
608
|
+
return `#${s.id.split('_').pop()} · ${formatSessionTime(s.updatedAt)} · ${t('resume.msgs', { n: count })}`;
|
|
609
|
+
}
|
|
612
610
|
async function handleSessionDelete(bannerMode) {
|
|
613
611
|
while (true) {
|
|
614
612
|
const sessions = await SessionManager.listSessions();
|
|
@@ -617,36 +615,18 @@ async function handleSessionDelete(bannerMode) {
|
|
|
617
615
|
log.warn('No saved sessions found.');
|
|
618
616
|
break;
|
|
619
617
|
}
|
|
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
|
-
}
|
|
618
|
+
const DELETE_ALL = '__all__';
|
|
619
|
+
const chosen = await runFinder({
|
|
620
|
+
title: t('sessions.deleteTitle'),
|
|
621
|
+
subtitle: `${sessions.length}`,
|
|
622
|
+
placeholder: t('resume.placeholder'),
|
|
623
|
+
labelWidth: 36,
|
|
624
|
+
items: [
|
|
625
|
+
{ value: DELETE_ALL, label: t('sessions.deleteAll', { n: sessions.length }), pinned: true },
|
|
626
|
+
...sessions.map((s) => ({ value: s.id, label: s.title, detail: sessionDetail(s), keywords: `${s.id} ${s.model || ''}` })),
|
|
627
|
+
],
|
|
628
|
+
});
|
|
629
|
+
const chosenSessionId = chosen ?? '__cancel__';
|
|
650
630
|
if (!chosenSessionId || chosenSessionId === '__cancel__') {
|
|
651
631
|
drawLogo(bannerMode);
|
|
652
632
|
log.info('Session deletion finished.');
|
|
@@ -675,38 +655,17 @@ async function handleSessionDelete(bannerMode) {
|
|
|
675
655
|
}
|
|
676
656
|
}
|
|
677
657
|
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
|
-
}
|
|
658
|
+
const selected = await chooseTheme(config.theme);
|
|
659
|
+
if (selected) {
|
|
660
|
+
config.theme = selected;
|
|
661
|
+
await saveConfig(config);
|
|
662
|
+
const th = setActiveTheme(selected);
|
|
663
|
+
// No redraw of the screen: that would wipe the conversation. New output uses the new colours.
|
|
664
|
+
if (config.ui?.layout === 'classic')
|
|
665
|
+
log.success(th.boldFn(`Theme switched to ${th.colorFn(th.symbol)} ${th.name}!`));
|
|
666
|
+
else
|
|
667
|
+
console.log(renderNotice('ok', `theme ${th.name}`));
|
|
708
668
|
}
|
|
709
|
-
catch { }
|
|
710
669
|
}
|
|
711
670
|
async function handleSettings(config) {
|
|
712
671
|
const initial = {
|
|
@@ -863,31 +822,11 @@ export async function main() {
|
|
|
863
822
|
};
|
|
864
823
|
};
|
|
865
824
|
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 { }
|
|
825
|
+
// A config from before the first-run setup had a theme step: ask it once.
|
|
826
|
+
clearTerminalScreen();
|
|
827
|
+
const selected = await chooseTheme(config.theme);
|
|
828
|
+
if (selected)
|
|
829
|
+
config.theme = selected;
|
|
891
830
|
config.onboarded = true;
|
|
892
831
|
await saveConfig(config);
|
|
893
832
|
}
|
|
@@ -1148,21 +1087,17 @@ export async function main() {
|
|
|
1148
1087
|
...customSlashItems
|
|
1149
1088
|
];
|
|
1150
1089
|
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
|
|
1090
|
+
const picked = await runFinder({
|
|
1091
|
+
title: t('commands.title'),
|
|
1092
|
+
subtitle: `${SLASH_COMMANDS.length}`,
|
|
1093
|
+
placeholder: t('commands.placeholder'),
|
|
1094
|
+
labelWidth: 14,
|
|
1095
|
+
items: SLASH_COMMANDS.map((c) => {
|
|
1096
|
+
const [name, ...rest] = c.name.split(/\s+-\s+/);
|
|
1097
|
+
return { value: c.value, label: name.trim(), detail: rest.join(' - ') };
|
|
1098
|
+
}),
|
|
1164
1099
|
});
|
|
1165
|
-
if (!picked
|
|
1100
|
+
if (!picked) {
|
|
1166
1101
|
continue;
|
|
1167
1102
|
}
|
|
1168
1103
|
if (picked === '/session del') {
|
|
@@ -1238,43 +1173,21 @@ export async function main() {
|
|
|
1238
1173
|
log.warn('No saved sessions found.');
|
|
1239
1174
|
continue;
|
|
1240
1175
|
}
|
|
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');
|
|
1176
|
+
const picked = await runFinder({
|
|
1177
|
+
title: t('resume.title'),
|
|
1178
|
+
subtitle: `${sessions.length}`,
|
|
1179
|
+
placeholder: t('resume.placeholder'),
|
|
1180
|
+
labelWidth: 36,
|
|
1181
|
+
items: sessions.map((s) => ({
|
|
1182
|
+
value: s.id,
|
|
1183
|
+
label: s.title,
|
|
1184
|
+
detail: sessionDetail(s),
|
|
1185
|
+
keywords: `${s.id} ${s.model || ''}`,
|
|
1186
|
+
})),
|
|
1187
|
+
});
|
|
1188
|
+
if (picked === undefined)
|
|
1276
1189
|
continue;
|
|
1277
|
-
|
|
1190
|
+
const chosenSessionId = picked;
|
|
1278
1191
|
const loaded = await SessionManager.loadSession(chosenSessionId);
|
|
1279
1192
|
if (loaded) {
|
|
1280
1193
|
sessionManager.setLoadedSession(loaded);
|
|
@@ -1514,9 +1427,12 @@ export async function main() {
|
|
|
1514
1427
|
// A user message, not an assistant one: the Anthropic format cannot start with the assistant.
|
|
1515
1428
|
history.addMessage({
|
|
1516
1429
|
role: 'user',
|
|
1517
|
-
content:
|
|
1430
|
+
content: `${COMPACT_MARK} Summary of our conversation so far:\n${res.content}`
|
|
1518
1431
|
});
|
|
1519
|
-
|
|
1432
|
+
const item = { kind: 'compact', summary: res.content };
|
|
1433
|
+
transcript.push(item);
|
|
1434
|
+
console.log();
|
|
1435
|
+
console.log(renderItem(item, { cols: process.stdout.columns || 80, thinkingExpanded, outputExpanded }).join('\n'));
|
|
1520
1436
|
}
|
|
1521
1437
|
catch (err) {
|
|
1522
1438
|
s.stop();
|