termux-dev 1.8.2 → 1.9.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/README.md +16 -6
- package/assets/banner.svg +1 -1
- package/assets/preview.svg +1 -1
- package/dist/cli/background-ui.js +8 -8
- package/dist/cli/files.js +2 -12
- package/dist/cli/finder.js +10 -2
- package/dist/cli/headless.js +3 -2
- package/dist/cli/index.js +131 -75
- package/dist/cli/keys.js +12 -3
- package/dist/cli/liveblock.js +47 -9
- package/dist/cli/mcp-panel.js +2 -0
- package/dist/cli/picker.js +2 -0
- package/dist/cli/prompt.js +54 -7
- package/dist/cli/provider-manager.js +197 -0
- package/dist/cli/providers.js +0 -5
- package/dist/cli/settings.js +38 -17
- package/dist/cli/typeahead.js +10 -17
- package/dist/cli/ui.js +33 -13
- package/dist/cli/update-screen.js +383 -0
- package/dist/cli/updater.js +119 -145
- package/dist/cli/usage-panel.js +17 -4
- package/dist/cli/width.js +18 -0
- package/dist/core/background.js +14 -0
- package/dist/core/context-stats.js +1 -0
- package/dist/core/cost.js +25 -11
- package/dist/core/custom-providers.js +161 -0
- package/dist/core/images.js +46 -0
- package/dist/core/local-config.js +3 -1
- package/dist/core/locales/de.js +69 -7
- package/dist/core/locales/en.js +69 -7
- package/dist/core/locales/es.js +69 -7
- package/dist/core/locales/fr.js +69 -7
- package/dist/core/locales/ja.js +69 -7
- package/dist/core/locales/pt.js +69 -7
- package/dist/core/locales/ru.js +69 -7
- package/dist/core/locales/uk.js +69 -7
- package/dist/core/locales/zh.js +69 -7
- package/dist/core/loop.js +32 -1
- package/dist/core/prices.js +8 -4
- package/dist/core/session-media.js +115 -0
- package/dist/core/session.js +17 -2
- package/dist/core/title.js +19 -2
- package/dist/core/types.js +1 -1
- package/dist/core/usage.js +41 -4
- package/dist/permissions/guard.js +3 -0
- package/dist/providers/anthropic.js +67 -13
- package/dist/providers/index.js +16 -3
- package/dist/providers/openai.js +99 -41
- package/dist/providers/shared.js +83 -0
- package/dist/tools/fs.js +32 -2
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -26,13 +26,15 @@ import { savePlanFile } from '../core/planfile.js';
|
|
|
26
26
|
import { BASE_TITLE, RESTORE_TERMINAL_TITLE, SAVE_TERMINAL_TITLE, cleanTitle, generateChatTitle, provisionalTitle, terminalTitleSequence } from '../core/title.js';
|
|
27
27
|
import { runDoctor } from './doctor.js';
|
|
28
28
|
import { ALL_PROVIDERS } from './providers.js';
|
|
29
|
+
import { addProviderFlow, manageProviderFlow } from './provider-manager.js';
|
|
30
|
+
import { NO_KEY, hostOfUrl, isCustomProvider, keyHint, listCustomProviders, migrateLegacyCustom, providerDisplayName } from '../core/custom-providers.js';
|
|
29
31
|
import { getModelContextInfo, getModelContextLimit } from '../core/models.js';
|
|
30
32
|
import { SessionManager } from '../core/session.js';
|
|
31
33
|
import { MarkdownStreamer, renderMarkdown } from './markdown.js';
|
|
32
34
|
import { SmoothStreamer } from './smooth.js';
|
|
33
35
|
import { askPrompt } from './prompt.js';
|
|
34
36
|
import { resolveAtMentions } from './files.js';
|
|
35
|
-
import {
|
|
37
|
+
import { runManualUpdate, runStartupUpdateCheck } from './update-screen.js';
|
|
36
38
|
import { setActiveTheme, getCurrentTheme, listThemes, findTheme } from './theme.js';
|
|
37
39
|
import { runHeadlessMode } from './headless.js';
|
|
38
40
|
import { CustomCommandManager } from '../core/commands.js';
|
|
@@ -141,7 +143,7 @@ async function fetchModels(baseUrl, apiKey) {
|
|
|
141
143
|
if (apiKey)
|
|
142
144
|
headers['x-api-key'] = apiKey;
|
|
143
145
|
}
|
|
144
|
-
else if (apiKey && apiKey !== 'ollama') {
|
|
146
|
+
else if (apiKey && apiKey !== 'ollama' && apiKey !== NO_KEY) {
|
|
145
147
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
146
148
|
}
|
|
147
149
|
const res = await fetch(url, { headers });
|
|
@@ -248,12 +250,21 @@ async function selectModel(baseUrl, apiKey, currentModel, providerId = '', _allo
|
|
|
248
250
|
/** The host of an address, for the detail column: `api.openai.com`. */
|
|
249
251
|
const hostOf = (url) => url.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
250
252
|
async function selectProvider(currentConfig, allowCancel = true) {
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
+
// The maps are shared with the config, so what the provider screens change is seen by both.
|
|
254
|
+
const apiKeys = (currentConfig.apiKeys = currentConfig.apiKeys || {});
|
|
255
|
+
const baseUrls = (currentConfig.baseUrls = currentConfig.baseUrls || {});
|
|
253
256
|
if (currentConfig.provider && currentConfig.apiKey) {
|
|
254
257
|
apiKeys[currentConfig.provider] = currentConfig.apiKey;
|
|
255
258
|
}
|
|
259
|
+
// The single "Custom" of older versions becomes a saved provider with a name of its own.
|
|
260
|
+
migrateLegacyCustom(currentConfig);
|
|
256
261
|
const firstRun = !allowCancel;
|
|
262
|
+
const providerFlow = {
|
|
263
|
+
// The first-run setup saves everything once, at its end.
|
|
264
|
+
save: firstRun ? undefined : saveConfig,
|
|
265
|
+
listedIds: ALL_PROVIDERS.map((pr) => pr.value),
|
|
266
|
+
};
|
|
267
|
+
const ADD = '__add__';
|
|
257
268
|
const stepOf = (n) => (firstRun ? `${n}/5` : undefined);
|
|
258
269
|
let providerId = '';
|
|
259
270
|
let baseUrl = '';
|
|
@@ -268,20 +279,44 @@ async function selectProvider(currentConfig, allowCancel = true) {
|
|
|
268
279
|
placeholder: t('onboard.provider.placeholder'),
|
|
269
280
|
current: currentConfig.provider,
|
|
270
281
|
notes: firstRun ? [t('onboard.provider.note')] : undefined,
|
|
271
|
-
items:
|
|
272
|
-
value:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
items: [
|
|
283
|
+
{ value: ADD, label: t('provider.add'), detail: t('provider.addDetail'), pinned: true },
|
|
284
|
+
// The providers the user saved come first, then the long list of known ones.
|
|
285
|
+
...listCustomProviders(currentConfig).map((c) => ({
|
|
286
|
+
value: c.id,
|
|
287
|
+
label: c.name,
|
|
288
|
+
detail: `${t('provider.customTag')} · ${[keyHint(c.apiKey), hostOfUrl(c.baseUrl)].filter(Boolean).join(' ')}`,
|
|
289
|
+
keywords: `${c.id} ${hostOfUrl(c.baseUrl)} ${t('provider.customTag')}`,
|
|
290
|
+
})),
|
|
291
|
+
...ALL_PROVIDERS.map((pr) => ({
|
|
292
|
+
value: pr.value,
|
|
293
|
+
label: pr.label,
|
|
294
|
+
detail: `${apiKeys[pr.value] ? maskApiKey(apiKeys[pr.value]) + ' ' : ''}${hostOf(pr.baseUrl || '')}`.trim(),
|
|
295
|
+
keywords: `${pr.value} ${hostOf(pr.baseUrl || '')}`,
|
|
296
|
+
})),
|
|
297
|
+
],
|
|
278
298
|
});
|
|
279
299
|
if (choice === undefined)
|
|
280
300
|
return null; // Esc on the first step leaves the whole setup
|
|
301
|
+
if (choice === ADD || isCustomProvider(currentConfig, choice)) {
|
|
302
|
+
// A provider of the user's own: add one, or use / change / delete a saved one.
|
|
303
|
+
let id = choice;
|
|
304
|
+
if (choice === ADD)
|
|
305
|
+
id = await addProviderFlow(currentConfig, providerFlow);
|
|
306
|
+
else if ((await manageProviderFlow(currentConfig, choice, providerFlow)) !== 'use')
|
|
307
|
+
id = null;
|
|
308
|
+
if (!id)
|
|
309
|
+
continue; // back to the list (it shows the changes)
|
|
310
|
+
providerId = id;
|
|
311
|
+
baseUrl = currentConfig.customProviders[id].baseUrl;
|
|
312
|
+
apiKey = apiKeys[id] ?? '';
|
|
313
|
+
stage = apiKey ? 'model' : 'key';
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
281
316
|
providerId = choice;
|
|
282
317
|
const known = ALL_PROVIDERS.find(pr => pr.value === providerId);
|
|
283
318
|
baseUrl = baseUrls[providerId] || known?.baseUrl || '';
|
|
284
|
-
stage =
|
|
319
|
+
stage = !baseUrl ? 'url' : 'key';
|
|
285
320
|
}
|
|
286
321
|
else if (stage === 'url') {
|
|
287
322
|
const url = await runTextInput({
|
|
@@ -300,8 +335,8 @@ async function selectProvider(currentConfig, allowCancel = true) {
|
|
|
300
335
|
stage = 'key';
|
|
301
336
|
}
|
|
302
337
|
else if (stage === 'key') {
|
|
303
|
-
const label =
|
|
304
|
-
const back = providerId
|
|
338
|
+
const label = providerDisplayName(currentConfig, providerId, ALL_PROVIDERS);
|
|
339
|
+
const back = isCustomProvider(currentConfig, providerId) ? 'provider' : !(ALL_PROVIDERS.find(pr => pr.value === providerId)?.baseUrl) ? 'url' : 'provider';
|
|
305
340
|
if (providerId === 'ollama') {
|
|
306
341
|
apiKey = 'ollama';
|
|
307
342
|
stage = 'model';
|
|
@@ -348,7 +383,7 @@ async function selectProvider(currentConfig, allowCancel = true) {
|
|
|
348
383
|
else {
|
|
349
384
|
modelRes = await selectModel(baseUrl, apiKey, currentConfig.model || '', providerId, allowCancel, stepOf(5));
|
|
350
385
|
if (!modelRes)
|
|
351
|
-
stage = apiKey === 'ollama' ? 'provider' : 'key';
|
|
386
|
+
stage = apiKey === 'ollama' || isCustomProvider(currentConfig, providerId) ? 'provider' : 'key';
|
|
352
387
|
}
|
|
353
388
|
}
|
|
354
389
|
const updatedConfig = {
|
|
@@ -715,6 +750,7 @@ async function handleSettings(config) {
|
|
|
715
750
|
layout: config.ui?.layout === 'classic' ? 'classic' : 'clean',
|
|
716
751
|
pureBlack: config.pureBlackTheme !== false,
|
|
717
752
|
autoApprove: !!config.autoApprove,
|
|
753
|
+
extra: !!config.autoApprove && !!config.autoApproveAll,
|
|
718
754
|
maxIterations: config.maxIterations || 100,
|
|
719
755
|
memory: config.enableMemory !== false,
|
|
720
756
|
updates: config.checkUpdates !== false,
|
|
@@ -745,12 +781,20 @@ async function handleSettings(config) {
|
|
|
745
781
|
config.banner = final.layout === 'clean' ? (config.banner === 'off' ? 'off' : 'clean') : 'full';
|
|
746
782
|
config.pureBlackTheme = final.pureBlack;
|
|
747
783
|
config.autoApprove = final.autoApprove;
|
|
784
|
+
// Extra goes with YOLO and, like YOLO, is never saved: it is off again at the next start.
|
|
785
|
+
const extraWasOn = !!initial.extra;
|
|
786
|
+
config.autoApproveAll = final.autoApprove && !!final.extra;
|
|
748
787
|
config.maxIterations = final.maxIterations;
|
|
749
788
|
config.enableMemory = final.memory;
|
|
750
789
|
config.checkUpdates = final.updates;
|
|
751
790
|
config.language = final.language;
|
|
752
791
|
config.maxCostPerSession = final.costLimit;
|
|
753
792
|
await saveConfig(config);
|
|
793
|
+
// Said once more where it cannot be missed, when the panel is closed with Extra newly switched on.
|
|
794
|
+
if (config.autoApproveAll && !extraWasOn) {
|
|
795
|
+
console.log(renderNotice('warn', t('settings.extraEnabled')));
|
|
796
|
+
console.log();
|
|
797
|
+
}
|
|
754
798
|
process.stdin.resume();
|
|
755
799
|
return config;
|
|
756
800
|
}
|
|
@@ -789,6 +833,7 @@ export async function main() {
|
|
|
789
833
|
.version(DEVX_VERSION)
|
|
790
834
|
.option('-p, --prompt <task>', 'Run one-shot task non-interactively (headless mode)')
|
|
791
835
|
.option('-y, --yolo', 'Automatically approve all tool executions without confirmation')
|
|
836
|
+
.option('--extra', 'Like --yolo, but nothing is asked at all: dangerous commands and credential files too (use only where you can afford to lose everything)')
|
|
792
837
|
.option('-m, --model <model>', 'Specify AI model to use for this execution')
|
|
793
838
|
.option('-q, --quiet', 'Quiet output in headless mode (suppress banners and tool logs)')
|
|
794
839
|
.option('--json', 'Output structured JSON in headless mode')
|
|
@@ -812,9 +857,12 @@ export async function main() {
|
|
|
812
857
|
}
|
|
813
858
|
// -m and the project config are for this run: only what changes from here on is saved.
|
|
814
859
|
markConfigBaseline(config);
|
|
815
|
-
if (options.yolo) {
|
|
860
|
+
if (options.yolo || options.extra) {
|
|
816
861
|
config.autoApprove = true;
|
|
817
862
|
}
|
|
863
|
+
if (options.extra) {
|
|
864
|
+
config.autoApproveAll = true;
|
|
865
|
+
}
|
|
818
866
|
if (options.effort !== undefined) {
|
|
819
867
|
// One run only (never saved). Anything the model does not offer is ignored with a warning.
|
|
820
868
|
// A custom endpoint is asked about the model first (cached, a few seconds at most).
|
|
@@ -844,6 +892,7 @@ export async function main() {
|
|
|
844
892
|
const exitCode = await runHeadlessMode(options.prompt, config, {
|
|
845
893
|
planMode: planModeInitial,
|
|
846
894
|
yolo: !!options.yolo,
|
|
895
|
+
extra: !!options.extra,
|
|
847
896
|
quiet: !!options.quiet,
|
|
848
897
|
json: !!options.json,
|
|
849
898
|
signal: abortController.signal
|
|
@@ -857,11 +906,12 @@ export async function main() {
|
|
|
857
906
|
return {
|
|
858
907
|
version: DEVX_VERSION,
|
|
859
908
|
model: modelLabel(config),
|
|
860
|
-
provider: config.provider,
|
|
909
|
+
provider: providerDisplayName(config, config.provider, ALL_PROVIDERS),
|
|
861
910
|
cwd: process.cwd(),
|
|
862
911
|
branch: git?.branch,
|
|
863
912
|
dirty: git?.isDirty,
|
|
864
913
|
yolo: !!config.autoApprove,
|
|
914
|
+
extra: !!config.autoApprove && !!config.autoApproveAll,
|
|
865
915
|
tip: pickTip(),
|
|
866
916
|
};
|
|
867
917
|
};
|
|
@@ -884,11 +934,36 @@ export async function main() {
|
|
|
884
934
|
history.addMessage({ role: 'system', content: sysPrompt });
|
|
885
935
|
const sessionManager = new SessionManager(config.model, planMode);
|
|
886
936
|
drawLogo(config.banner || 'full');
|
|
887
|
-
await runStartupUpdateCheck(config);
|
|
937
|
+
await runStartupUpdateCheck(config, { save: saveConfig });
|
|
888
938
|
await MCPManager.getInstance().init(undefined, config.projectMcpTrusted === true);
|
|
889
939
|
// Ask a custom endpoint what the current model can do (cached for a day; never blocks the prompt).
|
|
890
940
|
void refreshEndpoint(config).catch(() => null);
|
|
891
941
|
const sessionCost = new CostLedger();
|
|
942
|
+
// For the small side jobs (chat title, commit message, summary): the lightest reasoning the model
|
|
943
|
+
// offers, and what they cost goes into the session total like everything else.
|
|
944
|
+
const sideProvider = () => {
|
|
945
|
+
const inner = createProvider(config, { light: true });
|
|
946
|
+
return {
|
|
947
|
+
id: inner.id,
|
|
948
|
+
chat: async (request) => {
|
|
949
|
+
try {
|
|
950
|
+
const res = await inner.chat(request);
|
|
951
|
+
if (res.usage)
|
|
952
|
+
sessionCost.add(res.usage);
|
|
953
|
+
return res;
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
// Timed out or cut off after the request went out: still billed, so it counts as an estimate.
|
|
957
|
+
if (err?.status === undefined || err.status === 504 || err.status === 524) {
|
|
958
|
+
const lost = inner.lostRequestUsage?.(new History().estimateTokens(request.messages), 0);
|
|
959
|
+
if (lost)
|
|
960
|
+
sessionCost.add(lost);
|
|
961
|
+
}
|
|
962
|
+
throw err;
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
};
|
|
966
|
+
};
|
|
892
967
|
const currentContext = () => contextBreakdown(history.getMessages(), getTools(planMode).map(t => t.definition), config.maxContextTokens || getModelContextLimit(config.model));
|
|
893
968
|
let currentDraft = '';
|
|
894
969
|
let autoTriggerPrompt = '';
|
|
@@ -903,8 +978,7 @@ export async function main() {
|
|
|
903
978
|
const permissionMemory = newPermissionMemory();
|
|
904
979
|
// Commands the agent keeps running in the background (servers, watchers): shown in the prompt frame, listed by /tasks.
|
|
905
980
|
const bg = getBackgroundManager();
|
|
906
|
-
const
|
|
907
|
-
const bgSummary = () => backgroundSummary(bg, seenBgFailures);
|
|
981
|
+
const bgSummary = () => backgroundSummary(bg);
|
|
908
982
|
// The most a session may cost (USD, 0 = no limit); "continue" at the limit raises it for this session only.
|
|
909
983
|
let costLimitRaised = null;
|
|
910
984
|
let costWarned = false;
|
|
@@ -937,7 +1011,6 @@ export async function main() {
|
|
|
937
1011
|
await bg.stopAll().catch(() => 0);
|
|
938
1012
|
bg.clearFinished();
|
|
939
1013
|
bg.takeNotes();
|
|
940
|
-
seenBgFailures.clear();
|
|
941
1014
|
transcript.length = 0;
|
|
942
1015
|
checkpoints.length = 0;
|
|
943
1016
|
windowTitle = '';
|
|
@@ -964,10 +1037,10 @@ export async function main() {
|
|
|
964
1037
|
while (true) {
|
|
965
1038
|
const maxTokens = config.maxContextTokens || getModelContextLimit(config.model);
|
|
966
1039
|
config.maxContextTokens = maxTokens;
|
|
967
|
-
//
|
|
968
|
-
//
|
|
1040
|
+
// An empty chat shows 0; from the first message on, everything the next request carries
|
|
1041
|
+
// is counted: the system prompt and the tool schemas too.
|
|
969
1042
|
const ctxNow = currentContext();
|
|
970
|
-
const currentTokens = ctxNow.
|
|
1043
|
+
const currentTokens = ctxNow.inUse;
|
|
971
1044
|
const usagePercent = Math.min(100, Math.round((currentTokens / maxTokens) * 100));
|
|
972
1045
|
const formatTokens = (n) => {
|
|
973
1046
|
if (n >= 1_000_000)
|
|
@@ -1044,6 +1117,7 @@ export async function main() {
|
|
|
1044
1117
|
},
|
|
1045
1118
|
planMode,
|
|
1046
1119
|
isYolo: !!config.autoApprove,
|
|
1120
|
+
isExtra: !!config.autoApprove && !!config.autoApproveAll,
|
|
1047
1121
|
ui: config.ui,
|
|
1048
1122
|
model: modelLabel(config),
|
|
1049
1123
|
cwd: process.cwd(),
|
|
@@ -1201,20 +1275,7 @@ export async function main() {
|
|
|
1201
1275
|
continue;
|
|
1202
1276
|
}
|
|
1203
1277
|
if (cmd === '/update') {
|
|
1204
|
-
|
|
1205
|
-
s.start('Checking for updates from https://github.com/apvcode/Termux-Dev...');
|
|
1206
|
-
const res = await checkForUpdates(10000);
|
|
1207
|
-
s.stop();
|
|
1208
|
-
if (res.updateAvailable) {
|
|
1209
|
-
log.info(pc.bold(pc.yellow(`Update available: v${res.currentVersion} → v${res.latestVersion}`)));
|
|
1210
|
-
const doUpdate = await askConfirm({ title: 'update', question: t('confirm.update', { version: res.latestVersion }), initial: true });
|
|
1211
|
-
if (doUpdate) {
|
|
1212
|
-
await performSelfUpdate(res.latestVersion);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
else {
|
|
1216
|
-
log.success(pc.green(`devx is up to date! (v${res.currentVersion})`));
|
|
1217
|
-
}
|
|
1278
|
+
await runManualUpdate();
|
|
1218
1279
|
continue;
|
|
1219
1280
|
}
|
|
1220
1281
|
if (cmd === '/help') {
|
|
@@ -1283,6 +1344,8 @@ export async function main() {
|
|
|
1283
1344
|
planMode = loaded.planMode;
|
|
1284
1345
|
}
|
|
1285
1346
|
sessionCost.restore(loaded.totalCost || 0, loaded.costEstimated === true);
|
|
1347
|
+
// /usage starts from the same total as the frame, and not from the chat that was open before.
|
|
1348
|
+
UsageTracker.getInstance().restoreCost(loaded.totalCost || 0, loaded.costEstimated === true);
|
|
1286
1349
|
drawLogo(config.banner || 'full');
|
|
1287
1350
|
const nonSysMessages = loaded.messages.filter(m => m.role !== 'system');
|
|
1288
1351
|
const totalMsgs = nonSysMessages.length;
|
|
@@ -1440,7 +1503,7 @@ export async function main() {
|
|
|
1440
1503
|
const s = new UiSpinner();
|
|
1441
1504
|
s.start('Generating conventional commit message with AI...');
|
|
1442
1505
|
await refreshFormats(config).catch(() => null);
|
|
1443
|
-
const provider =
|
|
1506
|
+
const provider = sideProvider();
|
|
1444
1507
|
const promptReq = {
|
|
1445
1508
|
messages: [
|
|
1446
1509
|
{
|
|
@@ -1482,7 +1545,7 @@ export async function main() {
|
|
|
1482
1545
|
s.start('Compacting conversation context with AI...');
|
|
1483
1546
|
try {
|
|
1484
1547
|
await refreshFormats(config).catch(() => null);
|
|
1485
|
-
const provider =
|
|
1548
|
+
const provider = sideProvider();
|
|
1486
1549
|
// The request itself must fit the window: shorten a copy of the history first.
|
|
1487
1550
|
const forSummary = new History();
|
|
1488
1551
|
for (const m of msgs)
|
|
@@ -1552,7 +1615,7 @@ export async function main() {
|
|
|
1552
1615
|
continue;
|
|
1553
1616
|
}
|
|
1554
1617
|
if (cmd === '/tasks' || cmd === '/bg' || cmd === '/jobs') {
|
|
1555
|
-
const rows = await runTasksCommand(parts.slice(1).filter(Boolean), bg, process.stdout.columns || 80
|
|
1618
|
+
const rows = await runTasksCommand(parts.slice(1).filter(Boolean), bg, process.stdout.columns || 80);
|
|
1556
1619
|
console.log(rows.join('\n'));
|
|
1557
1620
|
console.log();
|
|
1558
1621
|
continue;
|
|
@@ -1677,7 +1740,7 @@ export async function main() {
|
|
|
1677
1740
|
log.warn(t('rename.usage'));
|
|
1678
1741
|
}
|
|
1679
1742
|
else {
|
|
1680
|
-
const generated = await generateChatTitle(
|
|
1743
|
+
const generated = await generateChatTitle(sideProvider(), firstUser.split('\n\n--- Attached Files ---')[0], reply);
|
|
1681
1744
|
if (!generated)
|
|
1682
1745
|
log.error(t('rename.failed'));
|
|
1683
1746
|
else {
|
|
@@ -1938,7 +2001,7 @@ export async function main() {
|
|
|
1938
2001
|
if (newConfig) {
|
|
1939
2002
|
config = newConfig;
|
|
1940
2003
|
await refreshEndpoint(config).catch(() => null);
|
|
1941
|
-
announce(`provider ${config.provider || 'custom'} · ${config.model}${providerFormat(config) === 'anthropic' ? ' (Anthropic format)' : ''}`);
|
|
2004
|
+
announce(`provider ${providerDisplayName(config, config.provider, ALL_PROVIDERS) || config.provider || 'custom'} · ${config.model}${providerFormat(config) === 'anthropic' ? ' (Anthropic format)' : ''}`);
|
|
1942
2005
|
}
|
|
1943
2006
|
else {
|
|
1944
2007
|
log.info('Provider selection cancelled.');
|
|
@@ -2046,6 +2109,7 @@ export async function main() {
|
|
|
2046
2109
|
const provider = createProvider(config);
|
|
2047
2110
|
const tools = getTools(planMode);
|
|
2048
2111
|
const guard = new CLIConsoleGuard(config.autoApprove, [...(config.bashAllowlist || [])], {
|
|
2112
|
+
extra: !!config.autoApproveAll,
|
|
2049
2113
|
memory: permissionMemory,
|
|
2050
2114
|
// "Always": kept in the config for the next sessions (`/permissions` lists and clears them).
|
|
2051
2115
|
onAlways: (r) => {
|
|
@@ -2279,9 +2343,7 @@ export async function main() {
|
|
|
2279
2343
|
pendingRedraw = true;
|
|
2280
2344
|
}
|
|
2281
2345
|
};
|
|
2282
|
-
|
|
2283
|
-
let interruptPrompt = '';
|
|
2284
|
-
const stopGeneration = (quiet = false, reason) => {
|
|
2346
|
+
const stopGeneration = (reason) => {
|
|
2285
2347
|
if (aborted)
|
|
2286
2348
|
return;
|
|
2287
2349
|
aborted = true;
|
|
@@ -2299,14 +2361,6 @@ export async function main() {
|
|
|
2299
2361
|
console.log(renderNotice('warn', reason));
|
|
2300
2362
|
console.log();
|
|
2301
2363
|
}
|
|
2302
|
-
else if (quiet) {
|
|
2303
|
-
// The message that caused the stop is echoed by the next turn: only a quiet separator here.
|
|
2304
|
-
if (cleanUi)
|
|
2305
|
-
console.log(renderNotice('info', t('stop.interrupted')));
|
|
2306
|
-
else
|
|
2307
|
-
console.log(pc.yellow('\n' + t('stop.classicInterrupted')));
|
|
2308
|
-
console.log();
|
|
2309
|
-
}
|
|
2310
2364
|
else if (cleanUi) {
|
|
2311
2365
|
console.log(renderNotice('warn', isConnecting ? t('stop.connection') : t('stop.stopped')));
|
|
2312
2366
|
console.log();
|
|
@@ -2321,13 +2375,6 @@ export async function main() {
|
|
|
2321
2375
|
const onSigInt = () => {
|
|
2322
2376
|
stopGeneration();
|
|
2323
2377
|
};
|
|
2324
|
-
const sendQueuedNow = () => {
|
|
2325
|
-
if (aborted)
|
|
2326
|
-
return;
|
|
2327
|
-
interruptPrompt = composer.takeQueued().join('\n');
|
|
2328
|
-
if (interruptPrompt)
|
|
2329
|
-
stopGeneration(true);
|
|
2330
|
-
};
|
|
2331
2378
|
// The input line at the bottom while the agent works: what is typed there is queued and
|
|
2332
2379
|
// reaches the model at its next step (see TurnComposer).
|
|
2333
2380
|
// The working line (time and tokens) stays above the frame for the whole turn.
|
|
@@ -2343,10 +2390,10 @@ export async function main() {
|
|
|
2343
2390
|
});
|
|
2344
2391
|
const composer = new TurnComposer(process.stdout, () => {
|
|
2345
2392
|
const ctx = currentContext();
|
|
2346
|
-
const used = ctx.
|
|
2393
|
+
const used = ctx.inUse;
|
|
2347
2394
|
const git = GitStatusCache.getStatus();
|
|
2348
2395
|
return {
|
|
2349
|
-
mode: resolveMode(planMode, !!config.autoApprove),
|
|
2396
|
+
mode: resolveMode(planMode, !!config.autoApprove, !!config.autoApproveAll),
|
|
2350
2397
|
model: modelLabel(config),
|
|
2351
2398
|
cwd: process.cwd(),
|
|
2352
2399
|
branch: git?.branch,
|
|
@@ -2356,7 +2403,7 @@ export async function main() {
|
|
|
2356
2403
|
costState: sessionCost.state(),
|
|
2357
2404
|
background: bgSummary(),
|
|
2358
2405
|
};
|
|
2359
|
-
}, undefined, statusLine
|
|
2406
|
+
}, undefined, statusLine);
|
|
2360
2407
|
// A task started or ended during the turn: the frame shows the new count.
|
|
2361
2408
|
const onBgChange = () => composer.refresh();
|
|
2362
2409
|
bg.on('change', onBgChange);
|
|
@@ -2380,6 +2427,16 @@ export async function main() {
|
|
|
2380
2427
|
stopGeneration();
|
|
2381
2428
|
return;
|
|
2382
2429
|
}
|
|
2430
|
+
// Esc stops the answer and whatever it started (a command that runs), like Ctrl+C. Not while a
|
|
2431
|
+
// card has the keyboard: there Esc answers the card. A lone Esc comes in a chunk of its own;
|
|
2432
|
+
// Esc followed by a character in one chunk is Alt+that character.
|
|
2433
|
+
if (!composer.isSuspended) {
|
|
2434
|
+
const keys = splitKeys(text);
|
|
2435
|
+
if (keys.length === 1 && normalizeKey(keys[0]) === '\x1b') {
|
|
2436
|
+
stopGeneration();
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2383
2440
|
composer.handleData(text);
|
|
2384
2441
|
};
|
|
2385
2442
|
const onResize = () => composer.onResize();
|
|
@@ -2419,8 +2476,14 @@ export async function main() {
|
|
|
2419
2476
|
takeNotes: () => bg.takeNotes(),
|
|
2420
2477
|
hasNotes: () => bg.hasNotes(),
|
|
2421
2478
|
})) {
|
|
2422
|
-
if (aborted)
|
|
2479
|
+
if (aborted) {
|
|
2480
|
+
// The request that was cancelled is still billed by the provider: its estimate counts.
|
|
2481
|
+
if (event.type === 'usage') {
|
|
2482
|
+
sessionCost.add(event.usage);
|
|
2483
|
+
turnInputTokens += event.usage.promptTokens || 0;
|
|
2484
|
+
}
|
|
2423
2485
|
break;
|
|
2486
|
+
}
|
|
2424
2487
|
if (event.type === 'reasoning_delta') {
|
|
2425
2488
|
isConnecting = false;
|
|
2426
2489
|
telemetry.setPhase('thinking');
|
|
@@ -2624,7 +2687,7 @@ export async function main() {
|
|
|
2624
2687
|
const limit = costLimit();
|
|
2625
2688
|
if (limit > 0 && !aborted) {
|
|
2626
2689
|
if (sessionCost.total >= limit) {
|
|
2627
|
-
stopGeneration(
|
|
2690
|
+
stopGeneration(t('cost.stopped', { limit: formatCost(limit) }));
|
|
2628
2691
|
}
|
|
2629
2692
|
else if (!costWarned && sessionCost.total >= limit * 0.8) {
|
|
2630
2693
|
costWarned = true;
|
|
@@ -2735,12 +2798,7 @@ export async function main() {
|
|
|
2735
2798
|
// a message that arrived too late for the agent starts the next turn by itself.
|
|
2736
2799
|
bg.off('change', onBgChange);
|
|
2737
2800
|
const leftover = composer.stop();
|
|
2738
|
-
if (
|
|
2739
|
-
autoTriggerPrompt = interruptPrompt;
|
|
2740
|
-
if (leftover.draft)
|
|
2741
|
-
currentDraft = leftover.draft;
|
|
2742
|
-
}
|
|
2743
|
-
else if (leftover.queued.length > 0) {
|
|
2801
|
+
if (leftover.queued.length > 0) {
|
|
2744
2802
|
if (aborted)
|
|
2745
2803
|
currentDraft = [...leftover.queued, leftover.draft].filter(Boolean).join(' ');
|
|
2746
2804
|
else
|
|
@@ -2795,7 +2853,7 @@ export async function main() {
|
|
|
2795
2853
|
const forSession = sessionManager.getSession().id;
|
|
2796
2854
|
void (async () => {
|
|
2797
2855
|
try {
|
|
2798
|
-
const title = await generateChatTitle(
|
|
2856
|
+
const title = await generateChatTitle(sideProvider(), firstUser.split('\n\n--- Attached Files ---')[0], reply);
|
|
2799
2857
|
// The user may have started another chat meanwhile.
|
|
2800
2858
|
if (!title || sessionManager.getSession().id !== forSession)
|
|
2801
2859
|
return;
|
|
@@ -2863,8 +2921,6 @@ export async function main() {
|
|
|
2863
2921
|
const unsent = composer.stop();
|
|
2864
2922
|
if (process.stdout.isTTY)
|
|
2865
2923
|
process.stdout.write('\x1b[?1004l');
|
|
2866
|
-
if (interruptPrompt && !autoTriggerPrompt)
|
|
2867
|
-
autoTriggerPrompt = interruptPrompt;
|
|
2868
2924
|
releasePauseHook();
|
|
2869
2925
|
process.stdout.removeListener('resize', onResize);
|
|
2870
2926
|
if (!currentDraft && (unsent.draft || unsent.queued.length > 0)) {
|
package/dist/cli/keys.js
CHANGED
|
@@ -13,11 +13,20 @@ export function splitKeys(chunk) {
|
|
|
13
13
|
}
|
|
14
14
|
/** Erases the word before the cursor (Ctrl+W in a shell); Ctrl+Backspace and Alt+Backspace are mapped to it. */
|
|
15
15
|
export const WORD_BACKSPACE = '\x17';
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* What a terminal sends for Ctrl+Enter once the extended keyboard modes below are on. Ctrl+Enter
|
|
18
|
+
* and Alt+Enter break the line (a new paragraph) instead of sending it, like Shift+Enter.
|
|
19
|
+
*/
|
|
17
20
|
export const CTRL_ENTER = '\x1b[13;5u';
|
|
21
|
+
/** Shift+Enter in the two extended encodings (a plain terminal sends the same byte as for Enter). */
|
|
22
|
+
const SHIFT_ENTER = /^\x1b\[(?:13;2u|27;2;13~)$/;
|
|
23
|
+
/** Does this key break the line instead of sending it? Ctrl+Enter, Alt+Enter and Shift+Enter, as far as the terminal tells them from Enter. */
|
|
24
|
+
export function isNewlineKey(key) {
|
|
25
|
+
return normalizeKey(key) === CTRL_ENTER || SHIFT_ENTER.test(key);
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
|
-
* Asks the terminal to tell Ctrl+Enter apart from Enter (a plain terminal sends
|
|
20
|
-
* byte for
|
|
28
|
+
* Asks the terminal to tell Ctrl+Enter and Shift+Enter apart from Enter (a plain terminal sends
|
|
29
|
+
* the same byte for all of them). Two protocols are requested, a terminal answers the one it knows:
|
|
21
30
|
* the kitty keyboard protocol (kitty, foot, ghostty, wezterm, alacritty, ...) and xterm's
|
|
22
31
|
* modifyOtherKeys. Terminals that know neither ignore both, and Alt+Enter still works.
|
|
23
32
|
* While on, the keys arrive as `CSI … u`: run them through `normalizeKey`.
|
package/dist/cli/liveblock.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { displayWidth } from './width.js';
|
|
1
2
|
/**
|
|
2
3
|
* A block of terminal rows that is redrawn in place.
|
|
3
4
|
*
|
|
@@ -5,41 +6,78 @@
|
|
|
5
6
|
* rewrites every row and wipes the rows left over from a taller previous version, so the
|
|
6
7
|
* screen never grows however often the content changes. The cursor is hidden while the
|
|
7
8
|
* block is on screen.
|
|
9
|
+
*
|
|
10
|
+
* The way back up is counted in rows of the screen, not in lines of the block: when the window
|
|
11
|
+
* gets narrower the terminal wraps the lines it already drew onto more rows, and going up by the
|
|
12
|
+
* number of lines would leave the top of the old block on screen (the block would show up twice).
|
|
8
13
|
*/
|
|
9
14
|
export class LiveBlock {
|
|
10
15
|
out;
|
|
11
|
-
|
|
16
|
+
/** Visible width of each line of the block as it was drawn. */
|
|
17
|
+
widths = [];
|
|
12
18
|
constructor(out) {
|
|
13
19
|
this.out = out;
|
|
14
20
|
}
|
|
21
|
+
/** Number of lines of the block on screen. */
|
|
15
22
|
get height() {
|
|
16
|
-
return this.
|
|
23
|
+
return this.widths.length;
|
|
24
|
+
}
|
|
25
|
+
columns() {
|
|
26
|
+
return Math.max(1, this.out.columns || process.stdout.columns || 80);
|
|
27
|
+
}
|
|
28
|
+
/** Rows of the screen the given lines take at the current width. */
|
|
29
|
+
rowsOf(widths) {
|
|
30
|
+
const cols = this.columns();
|
|
31
|
+
return widths.reduce((n, w) => n + Math.max(1, Math.ceil(w / cols)), 0);
|
|
17
32
|
}
|
|
18
33
|
draw(lines) {
|
|
34
|
+
const before = this.rowsOf(this.widths);
|
|
19
35
|
let buf = '\x1b[?25l';
|
|
20
|
-
if (
|
|
21
|
-
buf += `\x1b[${
|
|
36
|
+
if (before > 1)
|
|
37
|
+
buf += `\x1b[${before - 1}A`;
|
|
22
38
|
buf += '\r';
|
|
23
39
|
lines.forEach((line, i) => {
|
|
24
40
|
buf += '\x1b[2K' + line + '\x1b[0m' + (i < lines.length - 1 ? '\n' : '');
|
|
25
41
|
});
|
|
42
|
+
const widths = lines.map((line) => displayWidth(line));
|
|
43
|
+
const now = this.rowsOf(widths);
|
|
26
44
|
let extra = 0;
|
|
27
|
-
for (let k =
|
|
45
|
+
for (let k = now; k < before; k++) {
|
|
28
46
|
buf += '\n\x1b[2K';
|
|
29
47
|
extra++;
|
|
30
48
|
}
|
|
31
49
|
if (extra > 0)
|
|
32
50
|
buf += `\x1b[${extra}A`;
|
|
33
51
|
this.out.write(buf);
|
|
34
|
-
this.
|
|
52
|
+
this.widths = widths;
|
|
35
53
|
}
|
|
36
54
|
/** Removes the block from the screen and shows the cursor again. */
|
|
37
55
|
erase() {
|
|
38
|
-
|
|
56
|
+
const rows = this.rowsOf(this.widths);
|
|
57
|
+
if (rows === 0) {
|
|
58
|
+
this.out.write('\x1b[?25h');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.out.write(`\r${rows > 1 ? `\x1b[${rows - 1}A` : ''}\x1b[J\x1b[?25h`);
|
|
62
|
+
this.widths = [];
|
|
63
|
+
}
|
|
64
|
+
/** Leaves what is drawn on the screen as it is: the cursor comes back under it and the next draw starts a new block. */
|
|
65
|
+
release() {
|
|
66
|
+
if (this.widths.length === 0) {
|
|
39
67
|
this.out.write('\x1b[?25h');
|
|
40
68
|
return;
|
|
41
69
|
}
|
|
42
|
-
this.out.write(
|
|
43
|
-
this.
|
|
70
|
+
this.out.write('\n\x1b[?25h');
|
|
71
|
+
this.widths = [];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Calls `redraw` when the window is resized, so the block is drawn again at the new width.
|
|
75
|
+
* Returns the function that stops watching (call it when the panel closes).
|
|
76
|
+
*/
|
|
77
|
+
watchResize(redraw) {
|
|
78
|
+
this.out.on?.('resize', redraw);
|
|
79
|
+
return () => {
|
|
80
|
+
this.out.off?.('resize', redraw);
|
|
81
|
+
};
|
|
44
82
|
}
|
|
45
83
|
}
|
package/dist/cli/mcp-panel.js
CHANGED
|
@@ -314,11 +314,13 @@ export function runMcpPanel(controller, options = {}) {
|
|
|
314
314
|
const wasRaw = !!input.isRaw;
|
|
315
315
|
input.setRawMode?.(true);
|
|
316
316
|
input.resume();
|
|
317
|
+
const stopWatching = block.watchResize(draw);
|
|
317
318
|
const close = () => {
|
|
318
319
|
if (closed)
|
|
319
320
|
return;
|
|
320
321
|
closed = true;
|
|
321
322
|
input.off('data', onData);
|
|
323
|
+
stopWatching();
|
|
322
324
|
block.erase();
|
|
323
325
|
if (!wasRaw)
|
|
324
326
|
input.setRawMode?.(false);
|
package/dist/cli/picker.js
CHANGED
|
@@ -99,9 +99,11 @@ export function runPicker(options, io = { input: process.stdin, output: process.
|
|
|
99
99
|
const wasRaw = !!input.isRaw;
|
|
100
100
|
input.setRawMode?.(true);
|
|
101
101
|
input.resume();
|
|
102
|
+
const stopWatching = block.watchResize(draw);
|
|
102
103
|
const finish = (value) => {
|
|
103
104
|
done = true;
|
|
104
105
|
input.off('data', onData);
|
|
106
|
+
stopWatching();
|
|
105
107
|
block.erase();
|
|
106
108
|
if (!wasRaw)
|
|
107
109
|
input.setRawMode?.(false);
|