@semalt-ai/code 1.8.1 → 1.8.4
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/.claude/settings.local.json +14 -1
- package/CLAUDE.md +2 -1
- package/index.js +29 -8
- package/lib/agent.js +725 -133
- package/lib/api.js +193 -59
- package/lib/commands.js +263 -201
- package/lib/config.js +33 -4
- package/lib/constants.js +52 -2
- package/lib/metrics.js +16 -3
- package/lib/permissions.js +73 -73
- package/lib/prompts.js +90 -86
- package/lib/tool_specs.js +499 -0
- package/lib/tools.js +418 -198
- package/lib/ui/ansi.js +13 -1
- package/lib/ui/chat-history.js +212 -61
- package/lib/ui/create-ui.js +145 -377
- package/lib/ui/diff.js +91 -78
- package/lib/ui/format.js +247 -0
- package/lib/ui/input-field.js +200 -107
- package/lib/ui/layout.js +0 -2
- package/lib/ui/messages.js +44 -0
- package/lib/ui/select.js +114 -0
- package/lib/ui/status-bar.js +179 -42
- package/lib/ui/stream.js +8 -12
- package/lib/ui/terminal.js +60 -0
- package/lib/ui/theme.js +99 -0
- package/lib/ui/utils.js +135 -6
- package/lib/ui/writer.js +603 -0
- package/lib/ui.js +11 -6
- package/package.json +1 -1
- package/lib/ui/legacy.js +0 -130
package/lib/commands.js
CHANGED
|
@@ -4,10 +4,14 @@ const fs = require('fs');
|
|
|
4
4
|
|
|
5
5
|
const { CONFIG_PATH, DEFAULT_API_TIMEOUT_MS, TAG_REGISTRY } = require('./constants');
|
|
6
6
|
const { configShow } = require('./config');
|
|
7
|
-
const {
|
|
7
|
+
const { getSystemPrompt } = require('./prompts');
|
|
8
8
|
const { SessionStorage } = require('./storage');
|
|
9
9
|
const { getSkippedOps, setUIActive } = require('./tools');
|
|
10
10
|
const { AUDIT_LOG } = require('./audit');
|
|
11
|
+
const { formatToolLine } = require('./ui/format');
|
|
12
|
+
const writerModule = require('./ui/writer');
|
|
13
|
+
const writer = writerModule;
|
|
14
|
+
const msgs = require('./ui/messages');
|
|
11
15
|
|
|
12
16
|
function formatTimeAgo(ts) {
|
|
13
17
|
const diffMs = Date.now() - ts;
|
|
@@ -41,9 +45,8 @@ function createCommands({
|
|
|
41
45
|
FG_TEAL,
|
|
42
46
|
FG_YELLOW,
|
|
43
47
|
RST,
|
|
44
|
-
|
|
48
|
+
approxTokens,
|
|
45
49
|
getCols,
|
|
46
|
-
hr,
|
|
47
50
|
boxLine,
|
|
48
51
|
interactiveSelect,
|
|
49
52
|
createUI,
|
|
@@ -96,43 +99,79 @@ function createCommands({
|
|
|
96
99
|
(m) => m.model === model || (m.api_base === config.api_base && m.model === config.default_model)
|
|
97
100
|
);
|
|
98
101
|
if (match && Number.isInteger(match.context_length) && match.context_length > 0) return match.context_length;
|
|
102
|
+
if (Number.isInteger(config.context_length) && config.context_length > 0) return config.context_length;
|
|
99
103
|
return null;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
// Pick the first dashboard model when the user is authenticated but has
|
|
107
|
+
// not selected one yet. Persists credentials to config and returns
|
|
108
|
+
// { name, modelId } on success; null otherwise (not logged in, already
|
|
109
|
+
// selected, empty list, or API error).
|
|
110
|
+
async function ensureDefaultModel() {
|
|
111
|
+
const config = getConfig();
|
|
112
|
+
if (!config.auth_token) return null;
|
|
113
|
+
if (config.default_model && config.dashboard_model_id) return null;
|
|
114
|
+
let response;
|
|
115
|
+
try { response = await dashboardListModels(); } catch { return null; }
|
|
116
|
+
const models = Array.isArray(response && response.models) ? response.models : [];
|
|
117
|
+
if (!models.length) return null;
|
|
118
|
+
const first = models[0];
|
|
119
|
+
let credResp;
|
|
120
|
+
try { credResp = await dashboardGetModelForCli(first.id); } catch { return null; }
|
|
121
|
+
const model = credResp && credResp.model ? credResp.model : null;
|
|
122
|
+
if (!model) return null;
|
|
123
|
+
const contextLength = (Number.isInteger(model.context_length) && model.context_length > 0 ? model.context_length : null)
|
|
124
|
+
|| (Number.isInteger(model.max_tokens) && model.max_tokens > 0 ? model.max_tokens : null);
|
|
125
|
+
const updated = { ...config, api_base: model.base_url, api_key: model.api_key, default_model: model.model_id, dashboard_model_id: model.id };
|
|
126
|
+
if (contextLength !== null) updated.context_length = contextLength;
|
|
127
|
+
setConfig(updated);
|
|
128
|
+
return { name: model.name, modelId: model.model_id };
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
async function cmdChat(opts) {
|
|
132
|
+
await ensureDefaultModel();
|
|
133
|
+
|
|
134
|
+
// Build the three end-of-session artifacts that teardown emits as
|
|
135
|
+
// scrollback. Returning them as a plain object lets both exit paths
|
|
136
|
+
// (/exit submit and Ctrl+C onInterrupt) route through writer.teardown,
|
|
137
|
+
// which is the only place that can append them below the erased live
|
|
138
|
+
// region in a single atomic write.
|
|
139
|
+
function buildExitArtifacts() {
|
|
140
|
+
return {
|
|
141
|
+
summary: sessionMetrics ? sessionMetrics.summary() : '',
|
|
142
|
+
resumeHint: currentChatId !== null
|
|
143
|
+
? ` ${FG_DARK}Resume this chat: ${FG_CYAN}semalt-code --resume ${currentChatId}${RST}`
|
|
144
|
+
: '',
|
|
145
|
+
goodbye: ` ${FG_GRAY}Goodbye!${RST}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
115
149
|
const { chatHistory, statusBar, inputField, layout, destroy, redrawFixed } = createUI({
|
|
116
150
|
showThink: opts.showThink || false,
|
|
117
151
|
onInterrupt: (destroyFn) => {
|
|
118
152
|
saveSession();
|
|
119
|
-
destroyFn();
|
|
120
|
-
if (sessionMetrics) console.log('\n' + sessionMetrics.summary());
|
|
121
|
-
if (currentChatId !== null) {
|
|
122
|
-
console.log(` ${FG_DARK}Resume this chat: ${FG_CYAN}semalt-code --resume ${currentChatId}${RST}`);
|
|
123
|
-
}
|
|
124
|
-
console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
|
|
153
|
+
destroyFn(buildExitArtifacts());
|
|
125
154
|
process.exit(0);
|
|
126
155
|
},
|
|
127
156
|
});
|
|
128
157
|
|
|
129
158
|
setUIActive(true);
|
|
130
159
|
|
|
160
|
+
const writer = require('./ui/writer');
|
|
131
161
|
permissionManager.setUICallbacks({
|
|
132
162
|
onAddMessage: (msg) => chatHistory.addMessage(msg),
|
|
133
163
|
onRerenderMessage: (id) => chatHistory.rerenderById(id),
|
|
134
164
|
onCollapseMessage: (id) => chatHistory.collapseById(id),
|
|
135
165
|
onRemoveMessage: (id) => chatHistory.removeById(id),
|
|
166
|
+
// Modal-region API: setModal replaces the modal live band above the
|
|
167
|
+
// status region; clearModal drops it. Arrow-key redraws go through
|
|
168
|
+
// setModal only — no scrollback churn. When the picker resolves we
|
|
169
|
+
// clear the modal and push a single summary line to scrollback.
|
|
170
|
+
onShowModal: (lines) => writer.setModal(lines),
|
|
171
|
+
onCloseModal: (summary) => {
|
|
172
|
+
writer.clearModal();
|
|
173
|
+
if (summary) chatHistory.addMessage({ role: 'system', content: summary });
|
|
174
|
+
},
|
|
136
175
|
onCaptureNavigation: (handler) => {
|
|
137
176
|
inputField.captureNavigation(handler);
|
|
138
177
|
return () => inputField.releaseNavigation();
|
|
@@ -146,6 +185,10 @@ function createCommands({
|
|
|
146
185
|
let currentModel = opts.model || getConfig().default_model;
|
|
147
186
|
let resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
148
187
|
statusBar.setModel(currentModel);
|
|
188
|
+
// Seed the context indicator with the profile's limit up-front so it
|
|
189
|
+
// renders "0 / 200,000 tok (0%)" before the first API response, instead
|
|
190
|
+
// of appearing out of thin air once a turn completes.
|
|
191
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
149
192
|
let sessionMetrics = null;
|
|
150
193
|
// system prompt is prepended fresh on every API call in agent.js — never stored in history
|
|
151
194
|
let messages = [];
|
|
@@ -187,14 +230,13 @@ function createCommands({
|
|
|
187
230
|
}
|
|
188
231
|
refreshInputSearchItems();
|
|
189
232
|
|
|
190
|
-
// Banner —
|
|
191
|
-
//
|
|
192
|
-
//
|
|
233
|
+
// Banner — emit once as scrollback above the live region. In the
|
|
234
|
+
// bottom-anchored live-region TUI, scrollback flows into terminal
|
|
235
|
+
// scrollback naturally, so no absolute positioning or scroll-region
|
|
236
|
+
// trickery is needed here.
|
|
193
237
|
if (layout) {
|
|
194
|
-
const BANNER_LINES = 8; // blank + top-border + empty + title + desc + empty + bottom-border + blank
|
|
195
238
|
const w = Math.min(getCols() - 4, 60);
|
|
196
|
-
|
|
197
|
-
process.stdout.write([
|
|
239
|
+
const banner = [
|
|
198
240
|
``,
|
|
199
241
|
` ${FG_DARK}╭${'─'.repeat(w + 1)}╮${RST}`,
|
|
200
242
|
boxLine('', w),
|
|
@@ -203,19 +245,8 @@ function createCommands({
|
|
|
203
245
|
boxLine('', w),
|
|
204
246
|
` ${FG_DARK}╰${'─'.repeat(w + 1)}╯${RST}`,
|
|
205
247
|
``,
|
|
206
|
-
].join('\n')
|
|
207
|
-
|
|
208
|
-
// Keep historyStart = 1 so the banner is inside the scroll region.
|
|
209
|
-
// Growing mode uses _contentLines to position the first message below the
|
|
210
|
-
// banner (at row BANNER_LINES + 1). When the terminal fills up the banner
|
|
211
|
-
// scrolls naturally into the terminal scrollback — nothing disappears behind
|
|
212
|
-
// a fixed header.
|
|
213
|
-
layout._contentLines = BANNER_LINES;
|
|
214
|
-
layout.rows = BANNER_LINES + 1 + layout.inputHeight + 3;
|
|
215
|
-
|
|
216
|
-
// Erase the stale full-screen panels createUI drew before we compacted.
|
|
217
|
-
process.stdout.write(`\x1b[${layout.rows + 1};1H\x1b[J`);
|
|
218
|
-
|
|
248
|
+
].join('\n');
|
|
249
|
+
writer.scrollback(banner);
|
|
219
250
|
redrawFixed();
|
|
220
251
|
}
|
|
221
252
|
|
|
@@ -256,24 +287,29 @@ function createCommands({
|
|
|
256
287
|
try { await dashboardSaveMessages(currentChatId, newMessages); savedUpTo = messages.length; } catch {}
|
|
257
288
|
}
|
|
258
289
|
|
|
259
|
-
const HISTORY_DISPLAY_TURNS = 3; // user+assistant pairs to show on load
|
|
260
|
-
|
|
261
290
|
function displayLoadedMessages(loadedMessages) {
|
|
262
291
|
chatHistory.clearMessages();
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
292
|
+
for (const m of loadedMessages) {
|
|
293
|
+
if (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'tool') continue;
|
|
294
|
+
const raw = typeof m.content === 'string' ? m.content : '';
|
|
295
|
+
const ts = m.created_at ? new Date(m.created_at) : (m.ts ? new Date(m.ts) : new Date());
|
|
296
|
+
|
|
297
|
+
if (m.role === 'tool') {
|
|
298
|
+
chatHistory.addMessage({ role: 'tool', tag: 'tool', content: 'tool result', output: raw, ts });
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (m.role === 'user' && raw.startsWith('Tool execution results:')) {
|
|
303
|
+
const body = raw
|
|
304
|
+
.replace(/^Tool execution results[^\n]*\n+/, '')
|
|
305
|
+
.replace(/\n+Continue with the task\.[\s\S]*$/, '')
|
|
306
|
+
.trim();
|
|
307
|
+
chatHistory.addMessage({ role: 'tool', tag: 'tool', content: 'tool result', output: body || raw, ts });
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!raw.trim()) continue;
|
|
312
|
+
chatHistory.addMessage({ role: m.role, content: raw, ts });
|
|
277
313
|
}
|
|
278
314
|
}
|
|
279
315
|
|
|
@@ -296,32 +332,12 @@ function createCommands({
|
|
|
296
332
|
}
|
|
297
333
|
}
|
|
298
334
|
|
|
299
|
-
// Pending selection state (for in-chat /history, /models, /chats)
|
|
335
|
+
// Pending selection state (for in-chat /history, /models, /chats).
|
|
336
|
+
// The picker renders into the writer's modal region — same band as the
|
|
337
|
+
// permission picker — so navigation redraws in place and only the final
|
|
338
|
+
// selection (or cancellation) leaves a line in scrollback.
|
|
300
339
|
let pendingAction = null;
|
|
301
340
|
const PAGE_SIZE = 5;
|
|
302
|
-
let listMsg = null;
|
|
303
|
-
|
|
304
|
-
// In-place progress indicator for chunked HTTP fetches (http_get + http_get_next)
|
|
305
|
-
let httpFetchMsg = null;
|
|
306
|
-
|
|
307
|
-
function showHttpFetchProgress(url, part, total) {
|
|
308
|
-
const maxUrl = Math.max(20, getCols() - 35);
|
|
309
|
-
const shortUrl = url.length > maxUrl ? url.slice(0, maxUrl - 1) + '…' : url;
|
|
310
|
-
const content = `Fetching URL · ${shortUrl} · Part ${part}/${total}`;
|
|
311
|
-
if (!httpFetchMsg) {
|
|
312
|
-
httpFetchMsg = { role: 'tool', tag: 'http_get', content, id: `http-fetch-${Date.now()}` };
|
|
313
|
-
chatHistory.addMessage(httpFetchMsg);
|
|
314
|
-
} else {
|
|
315
|
-
httpFetchMsg.content = content;
|
|
316
|
-
chatHistory.rerenderById(httpFetchMsg.id);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function finalizeHttpFetch() {
|
|
321
|
-
if (!httpFetchMsg) return;
|
|
322
|
-
chatHistory.removeById(httpFetchMsg.id);
|
|
323
|
-
httpFetchMsg = null;
|
|
324
|
-
}
|
|
325
341
|
|
|
326
342
|
function getNavSearchText(type, item) {
|
|
327
343
|
if (type === 'history') {
|
|
@@ -379,31 +395,27 @@ function createCommands({
|
|
|
379
395
|
return parts.join('\n');
|
|
380
396
|
}
|
|
381
397
|
|
|
382
|
-
function collapseListMsg(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
listMsg = null;
|
|
398
|
+
function collapseListMsg(_type, _item) {
|
|
399
|
+
// Modal is transient — clearing it removes the picker from view; the
|
|
400
|
+
// selection's success line is emitted to scrollback by
|
|
401
|
+
// handlePendingSelection.
|
|
402
|
+
writer.clearModal();
|
|
388
403
|
}
|
|
389
404
|
|
|
390
405
|
function showPendingStep() {
|
|
391
406
|
if (!pendingAction) return;
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
407
|
+
const lines = buildListContent().split('\n');
|
|
408
|
+
// Match the system-message bubble look so the modal reads as part of
|
|
409
|
+
// the same chat block: muted bullet on the title row, indented
|
|
410
|
+
// continuations underneath.
|
|
411
|
+
const modalLines = lines.length > 0
|
|
412
|
+
? [` ${FG_GRAY}●${RST} ${FG_GRAY}${lines[0]}${RST}`].concat(lines.slice(1).map((l) => ` ${l}`))
|
|
413
|
+
: [];
|
|
414
|
+
writer.setModal(modalLines);
|
|
400
415
|
}
|
|
401
416
|
|
|
402
417
|
function finalizeListMsg() {
|
|
403
|
-
|
|
404
|
-
chatHistory.removeById(listMsg.id);
|
|
405
|
-
listMsg = null;
|
|
406
|
-
}
|
|
418
|
+
writer.clearModal();
|
|
407
419
|
}
|
|
408
420
|
|
|
409
421
|
function activateNavCapture() {
|
|
@@ -465,6 +477,7 @@ function createCommands({
|
|
|
465
477
|
currentModel = loaded.model;
|
|
466
478
|
resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
467
479
|
statusBar.setModel(currentModel);
|
|
480
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
468
481
|
}
|
|
469
482
|
displayLoadedMessages(messages);
|
|
470
483
|
chatHistory.addMessage({ role: 'system', content: `✓ Session loaded. Model → ${currentModel}` });
|
|
@@ -496,6 +509,7 @@ function createCommands({
|
|
|
496
509
|
currentModel = model.model_id;
|
|
497
510
|
resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
498
511
|
statusBar.setModel(currentModel);
|
|
512
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
499
513
|
currentChatId = null;
|
|
500
514
|
chatHistory.addMessage({ role: 'system', content: `✓ Model → ${model.name} (${model.model_id})` });
|
|
501
515
|
statusBar.update('idle');
|
|
@@ -553,7 +567,7 @@ function createCommands({
|
|
|
553
567
|
// Exit
|
|
554
568
|
if (['exit', 'quit', '/exit', '/quit'].includes(text.toLowerCase())) {
|
|
555
569
|
saveSession();
|
|
556
|
-
destroy();
|
|
570
|
+
destroy(buildExitArtifacts());
|
|
557
571
|
resolveExit();
|
|
558
572
|
return;
|
|
559
573
|
}
|
|
@@ -587,6 +601,7 @@ function createCommands({
|
|
|
587
601
|
}
|
|
588
602
|
|
|
589
603
|
if (text === '/history') {
|
|
604
|
+
if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
|
|
590
605
|
const sessions = storage.list();
|
|
591
606
|
if (!sessions.length) { chatHistory.addMessage({ role: 'system', content: 'No saved sessions.' }); return; }
|
|
592
607
|
refreshInputSearchItems();
|
|
@@ -635,6 +650,14 @@ function createCommands({
|
|
|
635
650
|
inputField.setDisabled(true);
|
|
636
651
|
statusBar.update('thinking', 'Starting login...');
|
|
637
652
|
await _loginFlow(chatHistory, statusBar);
|
|
653
|
+
const picked = await ensureDefaultModel();
|
|
654
|
+
if (picked) {
|
|
655
|
+
currentModel = picked.modelId;
|
|
656
|
+
resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
657
|
+
statusBar.setModel(currentModel);
|
|
658
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
659
|
+
chatHistory.addMessage({ role: 'system', content: `✓ Model → ${picked.name} (${picked.modelId})` });
|
|
660
|
+
}
|
|
638
661
|
statusBar.update('idle');
|
|
639
662
|
inputField.setDisabled(false);
|
|
640
663
|
return;
|
|
@@ -684,6 +707,7 @@ function createCommands({
|
|
|
684
707
|
}
|
|
685
708
|
|
|
686
709
|
if (text === '/models') {
|
|
710
|
+
if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
|
|
687
711
|
inputField.setDisabled(true);
|
|
688
712
|
statusBar.update('thinking', 'Loading models...');
|
|
689
713
|
try {
|
|
@@ -706,14 +730,17 @@ function createCommands({
|
|
|
706
730
|
}
|
|
707
731
|
|
|
708
732
|
if (text === '/model') {
|
|
733
|
+
if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
|
|
709
734
|
chatHistory.addMessage({ role: 'system', content: `Current model: ${currentModel}` });
|
|
710
735
|
return;
|
|
711
736
|
}
|
|
712
737
|
|
|
713
738
|
if (text.startsWith('/model ')) {
|
|
739
|
+
if (!getConfig().auth_token) { chatHistory.addMessage({ role: 'system', content: 'Not logged in. Run /login first.' }); return; }
|
|
714
740
|
currentModel = text.slice(7).trim();
|
|
715
741
|
resolvedTokenLimit = await resolveTokenLimit(currentModel);
|
|
716
742
|
statusBar.setModel(currentModel);
|
|
743
|
+
statusBar.setContextLimit(resolvedTokenLimit);
|
|
717
744
|
chatHistory.addMessage({ role: 'system', content: `✓ Model → ${currentModel}` });
|
|
718
745
|
return;
|
|
719
746
|
}
|
|
@@ -740,7 +767,7 @@ function createCommands({
|
|
|
740
767
|
}
|
|
741
768
|
|
|
742
769
|
if (text === '/prompt') {
|
|
743
|
-
const activePrompt = resolvedSystemPrompt !== null ? resolvedSystemPrompt :
|
|
770
|
+
const activePrompt = resolvedSystemPrompt !== null ? resolvedSystemPrompt : getSystemPrompt();
|
|
744
771
|
const src = resolvedSystemPrompt !== null ? `file: ${opts.systemPromptFile}` : 'built-in';
|
|
745
772
|
const mode = getConfig().system_prompt_mode || 'system_role';
|
|
746
773
|
chatHistory.addMessage({
|
|
@@ -821,6 +848,11 @@ function createCommands({
|
|
|
821
848
|
inputField.setDisabled(true);
|
|
822
849
|
chatHistory.addMessage({ role: 'user', content: text });
|
|
823
850
|
statusBar.update('thinking', 'Thinking...');
|
|
851
|
+
// Bump the context-size indicator with this user message's approximate
|
|
852
|
+
// token count. It'll be overwritten with the exact prompt_tokens from
|
|
853
|
+
// the API response when the first turn completes — this just keeps the
|
|
854
|
+
// indicator reactive in the gap before that.
|
|
855
|
+
statusBar.addPendingTokens(approxTokens(text));
|
|
824
856
|
await createChatIfNeeded(text);
|
|
825
857
|
messages.push({ role: 'user', content: text });
|
|
826
858
|
|
|
@@ -847,7 +879,7 @@ function createCommands({
|
|
|
847
879
|
if (entry?.type === 'tool') {
|
|
848
880
|
const actionLabel = entry.label || tag;
|
|
849
881
|
const detail = attrs.path || attrs.url || attrs.key || attrs.src || '';
|
|
850
|
-
const isDownload = tag === 'download' || tag === 'http_get'
|
|
882
|
+
const isDownload = tag === 'download' || tag === 'http_get';
|
|
851
883
|
const barState = isDownload ? 'waiting_download' : 'tool';
|
|
852
884
|
const label = isDownload
|
|
853
885
|
? `Waiting for download${detail ? ': ' + detail : ''}`
|
|
@@ -863,48 +895,57 @@ function createCommands({
|
|
|
863
895
|
chatHistory.addMessage({ role: 'think', content });
|
|
864
896
|
statusBar.update('streaming', 'Streaming response');
|
|
865
897
|
},
|
|
866
|
-
onToolStart: (tag, input,
|
|
898
|
+
onToolStart: (tag, input, ctx) => {
|
|
867
899
|
const actionLabel = TAG_REGISTRY[tag]?.label || tag;
|
|
868
|
-
const short = input.length > 40 ? input.slice(0, 40) + '…' : input;
|
|
869
|
-
const isDownload = tag === 'download' || tag === 'http_get'
|
|
900
|
+
const short = input && input.length > 40 ? input.slice(0, 40) + '…' : (input || '');
|
|
901
|
+
const isDownload = tag === 'download' || tag === 'http_get';
|
|
870
902
|
if (isDownload) {
|
|
871
903
|
statusBar.update('waiting_download', `Waiting for download: ${short}`);
|
|
872
904
|
} else {
|
|
873
905
|
statusBar.update('tool', `${actionLabel}: ${short}`);
|
|
874
906
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
907
|
+
// Register the invocation with the writer's activity region.
|
|
908
|
+
// The render function is re-invoked by the writer on every
|
|
909
|
+
// redraw so the pending line's elapsed time stays current with
|
|
910
|
+
// the ticker cadence without an explicit refresh timer.
|
|
911
|
+
if (ctx && ctx.id) {
|
|
912
|
+
writerModule.startActivity(ctx.id, (elapsedMs) => formatToolLine({
|
|
913
|
+
status: 'pending',
|
|
882
914
|
tag,
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
} else {
|
|
903
|
-
finalizeHttpFetch();
|
|
904
|
-
}
|
|
915
|
+
arg: input,
|
|
916
|
+
attrs: ctx.attrs,
|
|
917
|
+
durationMs: elapsedMs,
|
|
918
|
+
}));
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
onToolEnd: (tag, result, durationMs, ctx) => {
|
|
922
|
+
const hasError = !!(ctx && ctx.error);
|
|
923
|
+
const finalLine = formatToolLine({
|
|
924
|
+
status: hasError ? 'failure' : 'success',
|
|
925
|
+
tag,
|
|
926
|
+
arg: ctx && ctx.attrs ? (ctx.attrs.command || ctx.attrs.path || ctx.attrs.url || ctx.attrs.src || ctx.attrs.key || ctx.attrs.name || ctx.attrs.pattern) : '',
|
|
927
|
+
attrs: ctx ? ctx.attrs : null,
|
|
928
|
+
durationMs,
|
|
929
|
+
meta: ctx ? ctx.meta : null,
|
|
930
|
+
error: ctx ? ctx.error : null,
|
|
931
|
+
});
|
|
932
|
+
if (ctx && ctx.id) {
|
|
933
|
+
writerModule.endActivity(ctx.id, finalLine);
|
|
905
934
|
} else {
|
|
906
|
-
|
|
907
|
-
|
|
935
|
+
// No invocation id means the agent-loop wasn't upgraded to pass
|
|
936
|
+
// structured context (shouldn't happen in practice). Fall back
|
|
937
|
+
// to a direct scrollback line so the tool still leaves a trace.
|
|
938
|
+
writerModule.scrollback(finalLine);
|
|
939
|
+
}
|
|
940
|
+
if (hasError) {
|
|
941
|
+
// Preserve the expandable error body as a follow-up tool
|
|
942
|
+
// bubble. Empty content suppresses its header so the scrollback
|
|
943
|
+
// line above (written by endActivity) isn't duplicated.
|
|
944
|
+
const body = typeof result === 'string' && result.trim() ? result : null;
|
|
945
|
+
if (body) {
|
|
946
|
+
chatHistory.addMessage({ role: 'tool', tag, content: '', output: body, isError: true });
|
|
947
|
+
}
|
|
948
|
+
statusBar.update('streaming', 'Streaming response');
|
|
908
949
|
}
|
|
909
950
|
},
|
|
910
951
|
onToken: (token) => {
|
|
@@ -937,6 +978,11 @@ function createCommands({
|
|
|
937
978
|
onRetry: (attempt, max) => {
|
|
938
979
|
statusBar.update('thinking', `Retrying (${attempt}/${max})...`);
|
|
939
980
|
},
|
|
981
|
+
onDebug: (block) => {
|
|
982
|
+
// Render in-history as a tool-style bubble so ctrl+O expand works and
|
|
983
|
+
// the RAW RESPONSE text survives TUI redraws (stderr would be clobbered).
|
|
984
|
+
chatHistory.addMessage({ role: 'tool', tag: 'debug', content: 'DEBUG', output: block });
|
|
985
|
+
},
|
|
940
986
|
onError: (err) => {
|
|
941
987
|
if (err && err.isWarning) {
|
|
942
988
|
chatHistory.addMessage({ role: 'system', content: err.message || String(err) });
|
|
@@ -957,6 +1003,15 @@ function createCommands({
|
|
|
957
1003
|
};
|
|
958
1004
|
inputField.on('abort', _onAbort);
|
|
959
1005
|
|
|
1006
|
+
// Refresh in case a prior turn's 400 overflow persisted a learned
|
|
1007
|
+
// context_length to config after this chat started.
|
|
1008
|
+
if (resolvedTokenLimit == null) {
|
|
1009
|
+
const cfg = getConfig();
|
|
1010
|
+
if (Number.isInteger(cfg.context_length) && cfg.context_length > 0) {
|
|
1011
|
+
resolvedTokenLimit = cfg.context_length;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
960
1015
|
try {
|
|
961
1016
|
const agentResult = await runAgentLoop(messages, currentModel, undefined, resolvedTokenLimit, {
|
|
962
1017
|
showThink: opts.showThink || false,
|
|
@@ -981,18 +1036,13 @@ function createCommands({
|
|
|
981
1036
|
saveSession();
|
|
982
1037
|
});
|
|
983
1038
|
|
|
984
|
-
// Wait until user exits
|
|
1039
|
+
// Wait until user exits. The /exit submit handler already ran
|
|
1040
|
+
// destroy(buildExitArtifacts()), so the session summary, resume hint,
|
|
1041
|
+
// and goodbye have been emitted as scrollback inside teardown's
|
|
1042
|
+
// single atomic write. Nothing more to print here.
|
|
985
1043
|
await exitPromise;
|
|
986
1044
|
setUIActive(false);
|
|
987
1045
|
saveSession();
|
|
988
|
-
if (sessionMetrics) {
|
|
989
|
-
// Show summary in terminal after destroy
|
|
990
|
-
console.log('\n' + sessionMetrics.summary());
|
|
991
|
-
}
|
|
992
|
-
if (currentChatId !== null) {
|
|
993
|
-
console.log(` ${FG_DARK}Resume this chat: ${FG_CYAN}semalt-code --resume ${currentChatId}${RST}`);
|
|
994
|
-
}
|
|
995
|
-
console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
|
|
996
1046
|
}
|
|
997
1047
|
|
|
998
1048
|
async function _loginFlow(chatHistory, statusBar) {
|
|
@@ -1028,8 +1078,9 @@ function createCommands({
|
|
|
1028
1078
|
}
|
|
1029
1079
|
|
|
1030
1080
|
async function cmdCode(opts, promptArgs) {
|
|
1031
|
-
if (!promptArgs.length) {
|
|
1032
|
-
if (!getConfig().auth_token) {
|
|
1081
|
+
if (!promptArgs.length) { writer.scrollback(` ${FG_RED}Usage: semalt-code code <prompt>${RST}`); return; }
|
|
1082
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
1083
|
+
await ensureDefaultModel();
|
|
1033
1084
|
const model = opts.model || getConfig().default_model;
|
|
1034
1085
|
const userPrompt = promptArgs.join(' ');
|
|
1035
1086
|
const context = opts.file ? readFileContext(opts.file) : '';
|
|
@@ -1039,62 +1090,60 @@ function createCommands({
|
|
|
1039
1090
|
try { resolvedSystemPrompt = fs.readFileSync(opts.systemPromptFile, 'utf8'); } catch {}
|
|
1040
1091
|
}
|
|
1041
1092
|
let messages = [{ role: 'user', content: fullPrompt }];
|
|
1042
|
-
|
|
1043
|
-
statusBar.update({ model, status: 'thinking' });
|
|
1093
|
+
writer.scrollback(` ${FG_GRAY}◆ ${model}${RST}`);
|
|
1044
1094
|
const codeResult = await runAgentLoop(messages, model, undefined, null, {
|
|
1045
1095
|
debug: opts.debug || false,
|
|
1046
1096
|
systemPrompt: resolvedSystemPrompt,
|
|
1047
1097
|
systemPromptMode: getConfig().system_prompt_mode || 'system_role',
|
|
1048
1098
|
});
|
|
1049
1099
|
messages = codeResult.messages;
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
if (codeResult.metrics) console.log(codeResult.metrics.summary());
|
|
1100
|
+
writer.scrollback('\n');
|
|
1101
|
+
if (codeResult.metrics) writer.scrollback(codeResult.metrics.summary());
|
|
1053
1102
|
if (opts.dryRun) printDryRunSummary();
|
|
1054
1103
|
}
|
|
1055
1104
|
|
|
1056
1105
|
async function cmdEdit(opts, filePath, instructionArgs) {
|
|
1057
|
-
if (!filePath) {
|
|
1058
|
-
if (!getConfig().auth_token) {
|
|
1059
|
-
if (!fs.existsSync(filePath)) {
|
|
1106
|
+
if (!filePath) { writer.scrollback(` ${FG_RED}Usage: semalt-code edit <file> <instruction>${RST}`); return; }
|
|
1107
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
1108
|
+
if (!fs.existsSync(filePath)) { writer.scrollback(` ${FG_RED}✗ File not found: ${filePath}${RST}`); return; }
|
|
1109
|
+
await ensureDefaultModel();
|
|
1060
1110
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
1061
1111
|
const instruction = instructionArgs.join(' ');
|
|
1062
1112
|
const messages = [
|
|
1063
1113
|
{ role: 'system', content: 'You are Semalt.AI. Output ONLY the modified file. No explanations, no fences.' },
|
|
1064
1114
|
{ role: 'user', content: `File: ${filePath}\n\n\`\`\`\n${content}\n\`\`\`\n\nInstruction: ${instruction}` },
|
|
1065
1115
|
];
|
|
1066
|
-
|
|
1067
|
-
const editStatusBar = new StatusBar();
|
|
1068
|
-
editStatusBar.update({ model: opts.model || getConfig().default_model, status: 'editing' });
|
|
1116
|
+
writer.scrollback(` ${FG_GRAY}Editing ${filePath}...${RST}`);
|
|
1069
1117
|
let result = await chatSync(messages, { model: opts.model });
|
|
1070
|
-
editStatusBar.destroy();
|
|
1071
1118
|
if (result && !opts.dryRun) {
|
|
1072
1119
|
if (result.startsWith('```')) { const lines = result.split('\n'); result = lines.at(-1).trim() === '```' ? lines.slice(1, -1).join('\n') : lines.slice(1).join('\n'); }
|
|
1073
1120
|
fs.writeFileSync(filePath, result);
|
|
1074
|
-
|
|
1121
|
+
writer.scrollback(` ${FG_GREEN}✓ Saved: ${filePath}${RST}`);
|
|
1075
1122
|
} else if (opts.dryRun) {
|
|
1076
|
-
|
|
1123
|
+
writer.scrollback(` ${FG_YELLOW}⚠ Dry run — not modified${RST}`);
|
|
1077
1124
|
}
|
|
1078
1125
|
}
|
|
1079
1126
|
|
|
1080
1127
|
async function cmdShell(opts, commandArgs) {
|
|
1081
1128
|
const command = commandArgs.join(' ');
|
|
1082
|
-
if (!command) {
|
|
1129
|
+
if (!command) { writer.scrollback(` ${FG_RED}Usage: semalt-code shell <command>${RST}`); return; }
|
|
1083
1130
|
const result = await agentExecShell(command);
|
|
1084
1131
|
if (opts.analyze) {
|
|
1085
|
-
if (!getConfig().auth_token) {
|
|
1132
|
+
if (!getConfig().auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in. Run semalt-code login first.${RST}\n`); return; }
|
|
1133
|
+
await ensureDefaultModel();
|
|
1086
1134
|
const messages = [
|
|
1087
1135
|
{ role: 'system', content: 'You are Semalt.AI. Analyze the command output concisely.' },
|
|
1088
1136
|
{ role: 'user', content: `Command: ${command}\nExit: ${result.exit_code}\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}` },
|
|
1089
1137
|
];
|
|
1090
|
-
|
|
1138
|
+
writer.scrollback(`\n ${FG_TEAL}${BOLD}◆ Semalt.AI${RST}\n`);
|
|
1139
|
+
// audit: allowed — non-TUI streaming prefix, must precede StreamRenderer sync writes.
|
|
1091
1140
|
process.stdout.write(' ');
|
|
1092
1141
|
try {
|
|
1093
1142
|
await chatStream(messages, { model: opts.model });
|
|
1094
1143
|
} catch (err) {
|
|
1095
|
-
|
|
1144
|
+
msgs.netError(err.message);
|
|
1096
1145
|
}
|
|
1097
|
-
|
|
1146
|
+
writer.scrollback('\n');
|
|
1098
1147
|
}
|
|
1099
1148
|
}
|
|
1100
1149
|
|
|
@@ -1102,10 +1151,10 @@ function createCommands({
|
|
|
1102
1151
|
const config = getConfig();
|
|
1103
1152
|
let response;
|
|
1104
1153
|
try { response = await dashboardListModels(); }
|
|
1105
|
-
catch (err) {
|
|
1154
|
+
catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1106
1155
|
const models = Array.isArray(response && response.models) ? response.models : [];
|
|
1107
|
-
if (!models.length) {
|
|
1108
|
-
|
|
1156
|
+
if (!models.length) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}No models available.${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1157
|
+
writer.scrollback(`\n ${FG_TEAL}${BOLD}◆ Your Models${RST}\n ${FG_DARK}${'─'.repeat(60)}${RST}`);
|
|
1109
1158
|
const activeIndex = models.findIndex((m) => m.base_url === config.api_base && m.model_id === config.default_model);
|
|
1110
1159
|
const selectedIndex = await interactiveSelect(models, (model, isSelected, isFinal) => {
|
|
1111
1160
|
const active = model.base_url === config.api_base && model.model_id === config.default_model;
|
|
@@ -1114,18 +1163,18 @@ function createCommands({
|
|
|
1114
1163
|
const nameStyle = isSelected && !isFinal ? `${BG_SELECTED}${FG_CYAN}` : (isSelected ? FG_CYAN : FG_GRAY);
|
|
1115
1164
|
return ` ${marker} ${cursor} ${nameStyle}${model.name} · ${model.model_id} @ ${model.base_url}${RST}`;
|
|
1116
1165
|
}, { initialIndex: Math.max(0, activeIndex) });
|
|
1117
|
-
if (selectedIndex === null) {
|
|
1166
|
+
if (selectedIndex === null) { writer.scrollback(` ${FG_DARK}Cancelled${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1118
1167
|
const selectedModel = models[selectedIndex];
|
|
1119
1168
|
let credentialsResponse;
|
|
1120
1169
|
try { credentialsResponse = await dashboardGetModelForCli(selectedModel.id); }
|
|
1121
|
-
catch (err) {
|
|
1170
|
+
catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1122
1171
|
const model = credentialsResponse && credentialsResponse.model ? credentialsResponse.model : null;
|
|
1123
|
-
if (!model) {
|
|
1172
|
+
if (!model) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Unable to load selected model.${RST}\n`); return { model: config.default_model, dbId: config.dashboard_model_id }; }
|
|
1124
1173
|
const contextLength = (Number.isInteger(model.context_length) && model.context_length > 0 ? model.context_length : null) || (Number.isInteger(model.max_tokens) && model.max_tokens > 0 ? model.max_tokens : null);
|
|
1125
1174
|
const updatedConfig = { ...config, api_base: model.base_url, api_key: model.api_key, default_model: model.model_id, dashboard_model_id: model.id };
|
|
1126
1175
|
if (contextLength !== null) updatedConfig.context_length = contextLength;
|
|
1127
1176
|
setConfig(updatedConfig);
|
|
1128
|
-
|
|
1177
|
+
writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_GRAY}Current model → ${model.name} (${model.model_id})${RST}\n`);
|
|
1129
1178
|
return { model: model.model_id, dbId: model.id };
|
|
1130
1179
|
}
|
|
1131
1180
|
|
|
@@ -1136,55 +1185,59 @@ function createCommands({
|
|
|
1136
1185
|
api_key: opts.apiKey || 'any',
|
|
1137
1186
|
dashboard_url: opts.dashboardUrl || current.dashboard_url,
|
|
1138
1187
|
auth_token: current.auth_token || '',
|
|
1139
|
-
default_model: opts.defaultModel || '
|
|
1188
|
+
default_model: opts.defaultModel || '',
|
|
1140
1189
|
temperature: 0.7,
|
|
1141
1190
|
request_timeout_ms: DEFAULT_API_TIMEOUT_MS,
|
|
1142
1191
|
stream: true,
|
|
1143
1192
|
models: current.models,
|
|
1144
1193
|
};
|
|
1145
1194
|
setConfig(cfg);
|
|
1146
|
-
|
|
1147
|
-
console.log(` ${FG_GRAY}${JSON.stringify(cfg, null, 2)}${RST}\n`);
|
|
1195
|
+
writer.scrollback(`\n ${FG_GREEN}✓${RST} Config saved to ${CONFIG_PATH}\n ${FG_GRAY}${JSON.stringify(cfg, null, 2)}${RST}\n`);
|
|
1148
1196
|
}
|
|
1149
1197
|
|
|
1150
1198
|
async function cmdLogin() {
|
|
1151
|
-
|
|
1199
|
+
writer.scrollback(`\n ${FG_TEAL}${BOLD}◆ CLI Login${RST}\n ${FG_DARK}${'─'.repeat(40)}${RST}`);
|
|
1152
1200
|
let loginRequest;
|
|
1153
1201
|
try { loginRequest = await requestCliLogin(); }
|
|
1154
|
-
catch (err) {
|
|
1155
|
-
|
|
1156
|
-
console.log(` ${FG_CYAN}${loginRequest.verification_url}${RST}`);
|
|
1157
|
-
console.log(` ${FG_DARK}Waiting for confirmation...${RST}`);
|
|
1202
|
+
catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Unable to start login via ${getConfig().dashboard_url}: ${err.message}${RST}\n`); return; }
|
|
1203
|
+
writer.scrollback(` ${FG_GRAY}Open this URL in your browser and confirm the login:${RST}\n ${FG_CYAN}${loginRequest.verification_url}${RST}\n ${FG_DARK}Waiting for confirmation...${RST}`);
|
|
1158
1204
|
const startedAt = Date.now();
|
|
1159
1205
|
while (Date.now() - startedAt < LOGIN_TIMEOUT_MS) {
|
|
1160
1206
|
await new Promise((r) => setTimeout(r, LOGIN_POLL_INTERVAL_MS));
|
|
1161
1207
|
let status;
|
|
1162
1208
|
try { status = await getCliLoginStatus(loginRequest.id, loginRequest.hash); }
|
|
1163
|
-
catch (err) { if (err.statusCode === 404 || err.statusCode === 410) {
|
|
1164
|
-
if (status.status === 'authorized') { const config = getConfig(); setConfig({ ...config, dashboard_url: config.dashboard_url, auth_token: loginRequest.token });
|
|
1165
|
-
if (status.status === 'expired') {
|
|
1209
|
+
catch (err) { if (err.statusCode === 404 || err.statusCode === 410) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Login token is no longer valid.${RST}\n`); return; } continue; }
|
|
1210
|
+
if (status.status === 'authorized') { const config = getConfig(); setConfig({ ...config, dashboard_url: config.dashboard_url, auth_token: loginRequest.token }); writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_GRAY}CLI token saved to ${CONFIG_PATH}${RST}\n`); return; }
|
|
1211
|
+
if (status.status === 'expired') { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Login token expired.${RST}\n`); return; }
|
|
1166
1212
|
}
|
|
1167
|
-
|
|
1213
|
+
writer.scrollback(` ${FG_YELLOW}⚠${RST} ${FG_GRAY}Login timed out.${RST}\n`);
|
|
1168
1214
|
}
|
|
1169
1215
|
|
|
1170
1216
|
async function cmdWhoAmI() {
|
|
1171
1217
|
let response;
|
|
1172
|
-
try { response = await dashboardWhoAmI(); } catch (err) {
|
|
1218
|
+
try { response = await dashboardWhoAmI(); } catch (err) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return; }
|
|
1173
1219
|
const user = response && response.user ? response.user : null;
|
|
1174
|
-
if (!user) {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1220
|
+
if (!user) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Unable to load current user.${RST}\n`); return; }
|
|
1221
|
+
const lines = [
|
|
1222
|
+
'',
|
|
1223
|
+
` ${FG_TEAL}${BOLD}◆ Current User${RST}`,
|
|
1224
|
+
` ${FG_DARK}${'─'.repeat(40)}${RST}`,
|
|
1225
|
+
formatUserLine('ID', user.id),
|
|
1226
|
+
formatUserLine('Email', user.email || '-'),
|
|
1227
|
+
formatUserLine('Name', user.name || '-'),
|
|
1228
|
+
formatUserLine('Provider', user.provider || '-'),
|
|
1229
|
+
];
|
|
1230
|
+
if (user.avatar_url) lines.push(formatUserLine('Avatar', user.avatar_url));
|
|
1231
|
+
lines.push('');
|
|
1232
|
+
writer.scrollback(lines.join('\n'));
|
|
1180
1233
|
}
|
|
1181
1234
|
|
|
1182
1235
|
async function cmdLogout() {
|
|
1183
1236
|
const config = getConfig();
|
|
1184
|
-
if (!config.auth_token) {
|
|
1185
|
-
try { await dashboardLogout(); } catch (err) { if (err.statusCode !== 401) {
|
|
1237
|
+
if (!config.auth_token) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}Not logged in.${RST}\n`); return; }
|
|
1238
|
+
try { await dashboardLogout(); } catch (err) { if (err.statusCode !== 401) { writer.scrollback(` ${FG_RED}✗${RST} ${FG_GRAY}${err.message}${RST}\n`); return; } }
|
|
1186
1239
|
setConfig({ ...config, auth_token: '' });
|
|
1187
|
-
|
|
1240
|
+
writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_GRAY}Logged out and cleared local CLI token.${RST}\n`);
|
|
1188
1241
|
}
|
|
1189
1242
|
|
|
1190
1243
|
function printDryRunSummary() {
|
|
@@ -1197,15 +1250,24 @@ function createCommands({
|
|
|
1197
1250
|
const stripA = (s) => s.replace(/\x1b\[[^m]*m/g, '');
|
|
1198
1251
|
const row = (content) => { const visible = stripA(content).length; const pad = ' '.repeat(Math.max(0, INNER - visible)); return isTTY ? `${FG_TEAL}║${RST}${content}${pad}${FG_TEAL}║${RST}` : `║${stripA(content)}${pad}║`; };
|
|
1199
1252
|
const hr40 = (tl, fill, tr) => { const line = tl + fill.repeat(INNER) + tr; return isTTY ? `${FG_TEAL}${line}${RST}` : line; };
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1253
|
+
const out = [
|
|
1254
|
+
'',
|
|
1255
|
+
hr40('╔','═','╗'),
|
|
1256
|
+
row(` ${isTTY ? BOLD : ''}DRY-RUN SUMMARY${isTTY ? RST : ''}`),
|
|
1257
|
+
hr40('╠','═','╣'),
|
|
1258
|
+
row(` ✎ Files that would change: ${files.length} `),
|
|
1259
|
+
row(` ▶ Commands that would run: ${cmds.length} `),
|
|
1260
|
+
row(` ↓ Network calls: ${nets.length} `),
|
|
1261
|
+
hr40('╚','═','╝'),
|
|
1262
|
+
];
|
|
1263
|
+
if (ops.length > 0) {
|
|
1264
|
+
out.push('');
|
|
1265
|
+
for (const op of ops) {
|
|
1266
|
+
out.push(isTTY ? ` ${op.symbol} ${FG_GRAY}${op.desc}${RST}` : ` ${op.symbol} ${op.desc}`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
out.push('');
|
|
1270
|
+
writer.scrollback(out.join('\n'));
|
|
1209
1271
|
}
|
|
1210
1272
|
|
|
1211
1273
|
return {
|