@cspeach/cli 1.1.33 → 1.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/anthropic-provider.js +2 -0
- package/dist/agent/azure-profile.js +7 -0
- package/dist/agent/connection-profile.js +49 -0
- package/dist/agent/loop.js +182 -39
- package/dist/agent/provider-shape.js +36 -0
- package/dist/agent/providers/ai-hub-provider.js +6 -5
- package/dist/agent/providers/azure-provider.js +125 -0
- package/dist/agent/providers/byok-provider.js +8 -7
- package/dist/agent/providers/factory.js +8 -3
- package/dist/agent/providers/local-provider.js +18 -22
- package/dist/agent/risk-gate.js +202 -0
- package/dist/agent/summarise-via-provider.js +55 -8
- package/dist/agent/tool-dispatch.js +19 -0
- package/dist/agent/turn-error-ux.js +22 -0
- package/dist/auth/api-key.js +10 -5
- package/dist/auth/auth-file.js +35 -6
- package/dist/auth/azure/azure-key.js +21 -0
- package/dist/auth/azure/azure-log.js +45 -0
- package/dist/auth/azure/cache-store.js +171 -0
- package/dist/auth/azure/clear.js +28 -0
- package/dist/auth/azure/loopback.js +192 -0
- package/dist/auth/azure/msal-client.js +41 -0
- package/dist/auth/azure/sign-in.js +219 -0
- package/dist/auth/azure/startup-sign-in.js +64 -0
- package/dist/auth/azure/token-provider.js +116 -0
- package/dist/auth/key-resolver.js +258 -0
- package/dist/auth/me.js +11 -5
- package/dist/cli.js +21 -2
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +74 -3
- package/dist/commands/config-show.js +25 -0
- package/dist/commands/cost.js +19 -6
- package/dist/commands/help.js +2 -2
- package/dist/commands/login-azure.js +33 -0
- package/dist/commands/login.js +56 -8
- package/dist/commands/logout.js +34 -10
- package/dist/commands/plan-boundary.js +4 -4
- package/dist/commands/plan-model-tier.js +8 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/whoami.js +113 -9
- package/dist/config/loader.js +5 -0
- package/dist/connection/azure-auth.js +135 -0
- package/dist/connection/azure-errors.js +103 -0
- package/dist/connection/azure-facts.js +21 -0
- package/dist/connection/azure-gate.js +40 -0
- package/dist/connection/azure-last-call.js +5 -0
- package/dist/connection/azure-turn-error.js +25 -0
- package/dist/connection/cache.js +70 -0
- package/dist/connection/fetch.js +33 -0
- package/dist/connection/guard.js +94 -0
- package/dist/connection/limit-gate.js +26 -0
- package/dist/connection/local-usage.js +71 -0
- package/dist/connection/login-step.js +62 -0
- package/dist/connection/startup.js +67 -0
- package/dist/connection/state.js +68 -0
- package/dist/connection/types.js +1 -0
- package/dist/cost/cost-log.js +3 -1
- package/dist/cost/credits-wire.js +1 -1
- package/dist/cost/credits.js +4 -0
- package/dist/cost/display-mode.js +7 -0
- package/dist/cost/router-spend.js +3 -0
- package/dist/cost/session-cost.js +4 -0
- package/dist/doctor/checks/azure.js +100 -0
- package/dist/doctor/checks/credits.js +4 -3
- package/dist/doctor/checks/usage-reporting.js +22 -0
- package/dist/doctor/run.js +7 -0
- package/dist/lib/graceful-exit.js +17 -0
- package/dist/licence/licence-check.js +205 -0
- package/dist/licence/paths.js +6 -0
- package/dist/licence/turn-gate.js +66 -0
- package/dist/models/model-identity.js +65 -0
- package/dist/models/resolve.js +8 -0
- package/dist/models/server-config.js +3 -2
- package/dist/one-shot.js +86 -2
- package/dist/projects/answer-blockers.js +25 -12
- package/dist/renderer/change-card.js +28 -4
- package/dist/renderer/footer-line.js +7 -1
- package/dist/renderer/notices.js +27 -13
- package/dist/renderer/progress-chatter.js +3 -1
- package/dist/renderer/status-footer.js +13 -6
- package/dist/renderer/tool-labels.js +8 -2
- package/dist/renderer/verify-chain.js +41 -18
- package/dist/renderer/wrap-rows.js +87 -0
- package/dist/repl/ask-or-no.js +18 -0
- package/dist/repl/builtin-commands.js +10 -2
- package/dist/repl/post-turn-status.js +3 -2
- package/dist/repl/safety-confirm.js +47 -2
- package/dist/repl/session-spend-line.js +4 -1
- package/dist/repl/startup-account.js +51 -0
- package/dist/repl.js +217 -48
- package/dist/router/byok-classifier.js +41 -14
- package/dist/router/classifier.js +5 -16
- package/dist/router/router-target.js +41 -0
- package/dist/sap/connection-manager.js +15 -0
- package/dist/sap/onboarding.js +70 -64
- package/dist/sap/session-end.js +66 -0
- package/dist/session/audit-export.js +11 -4
- package/dist/skills/manifest-client.js +37 -4
- package/dist/skills/skills-cache.js +115 -0
- package/dist/skills/source-authed.js +276 -0
- package/dist/tools/ask-question.js +1 -1
- package/dist/tools/filesystem/glob.js +12 -3
- package/dist/tools/filesystem/grep.js +14 -3
- package/dist/tools/sap-read.js +22 -22
- package/dist/tools/sap-write.js +249 -18
- package/dist/tools/snapshot.js +26 -0
- package/dist/tools/tool-loading.js +9 -10
- package/dist/ui/app.js +19 -9
- package/dist/ui/approval-modal.js +37 -5
- package/dist/ui/live-area.js +14 -1
- package/dist/ui/widgets/ask-card.js +183 -107
- package/dist/ui/widgets/coaching-picker.js +22 -1
- package/dist/ui/widgets/confirm-card.js +102 -76
- package/dist/usage/usage-queue.js +323 -0
- package/dist/usage/usage-reporter.js +270 -0
- package/package.json +4 -3
- package/dist/skills/bundled-skills.js +0 -286
- package/dist/skills/source-bundled.js +0 -20
- package/dist/skills/source-manifest.js +0 -36
|
@@ -59,6 +59,9 @@ export function applyBlockerAnswers(env, answers, answerer, nowIso) {
|
|
|
59
59
|
lastEditedBy: answerer,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
function plural(n, word) {
|
|
63
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
64
|
+
}
|
|
62
65
|
function affirmative(reply) {
|
|
63
66
|
const v = reply.trim().toLowerCase();
|
|
64
67
|
return v === 'y' || v === 'yes';
|
|
@@ -83,7 +86,9 @@ export async function offerAnswerBlockers(args) {
|
|
|
83
86
|
if (blockers.length === 0)
|
|
84
87
|
return null;
|
|
85
88
|
const reply = await askYesNo({
|
|
86
|
-
question:
|
|
89
|
+
question: blockers.length === 1
|
|
90
|
+
? 'Answer the open blocker now?'
|
|
91
|
+
: `Answer the ${blockers.length} open blockers now?`,
|
|
87
92
|
confirm: args.confirm,
|
|
88
93
|
prompt: args.prompt,
|
|
89
94
|
});
|
|
@@ -92,16 +97,15 @@ export async function offerAnswerBlockers(args) {
|
|
|
92
97
|
const answers = new Map();
|
|
93
98
|
for (let i = 0; i < blockers.length; i++) {
|
|
94
99
|
const b = blockers[i];
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// Choices first, where the question has them and a picker is available.
|
|
100
|
+
// The "Blocker i of n: …" line is printed only on the free-text fallback
|
|
101
|
+
// below. The picker shows the question itself and leaves its own trace.
|
|
102
|
+
// The picker first, for every blocker (options or free text).
|
|
99
103
|
// 'write' falls through to the text box; 'skip' leaves the blocker open.
|
|
100
104
|
let askText = true;
|
|
101
|
-
if (
|
|
105
|
+
if (args.choose) {
|
|
102
106
|
const c = await args.choose({
|
|
103
107
|
question: b.text,
|
|
104
|
-
options: b.options,
|
|
108
|
+
options: b.options ?? [],
|
|
105
109
|
multiSelect: !!b.multiSelect,
|
|
106
110
|
index: i,
|
|
107
111
|
total: blockers.length,
|
|
@@ -114,19 +118,26 @@ export async function offerAnswerBlockers(args) {
|
|
|
114
118
|
else if (c.kind === 'skip') {
|
|
115
119
|
askText = false;
|
|
116
120
|
}
|
|
121
|
+
else if (c.kind === 'stop') {
|
|
122
|
+
// Ctrl+C (Task 4, m2): ask none of the rest. Answers already given
|
|
123
|
+
// are kept and saved below; the others stay open.
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
117
126
|
// c.kind === 'write' → askText stays true
|
|
118
127
|
}
|
|
119
128
|
if (askText) {
|
|
120
129
|
args.log('');
|
|
121
|
-
args.log(`Blocker ${i + 1}
|
|
122
|
-
const a = (await args.prompt('
|
|
130
|
+
args.log(`Blocker ${i + 1} of ${blockers.length}: ${b.text}`);
|
|
131
|
+
const a = (await args.prompt('Your answer (Enter to skip): ')).trim();
|
|
123
132
|
if (a.length > 0)
|
|
124
133
|
answers.set(b.id, a);
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
if (answers.size === 0) {
|
|
128
137
|
args.log('');
|
|
129
|
-
args.log(
|
|
138
|
+
args.log(blockers.length === 1
|
|
139
|
+
? 'No answer entered. The blocker stays open. Answer it later at cspeach.dev/edit.'
|
|
140
|
+
: `No answers entered. The ${blockers.length} blockers stay open. Answer them later at cspeach.dev/edit.`);
|
|
130
141
|
return null;
|
|
131
142
|
}
|
|
132
143
|
const nowIso = (args.now ?? (() => new Date().toISOString()))();
|
|
@@ -139,7 +150,9 @@ export async function offerAnswerBlockers(args) {
|
|
|
139
150
|
writeFileSync(args.path, JSON.stringify(revised, null, 2), 'utf8');
|
|
140
151
|
const stillOpen = blockers.length - answers.size;
|
|
141
152
|
args.log('');
|
|
142
|
-
args.log(`Recorded ${answers.size
|
|
143
|
-
+ (stillOpen
|
|
153
|
+
args.log(`Recorded ${plural(answers.size, 'answer')} in the file.`
|
|
154
|
+
+ (stillOpen === 0 ? ''
|
|
155
|
+
: stillOpen === 1 ? ' 1 blocker is still open. Answer it later at cspeach.dev/edit.'
|
|
156
|
+
: ` ${stillOpen} blockers are still open. Answer them later at cspeach.dev/edit.`));
|
|
144
157
|
return answers.size;
|
|
145
158
|
}
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import { diffLines } from 'diff';
|
|
7
7
|
import { paint } from './theme.js';
|
|
8
8
|
import { getBoxChars } from '../ui/ascii-chars.js';
|
|
9
|
+
import { ERROR_DETAIL_ROWS, SNAPSHOT_DETAIL_ROWS } from './verify-chain.js';
|
|
9
10
|
import { ANSI_RE } from './tool-labels.js';
|
|
11
|
+
import { wrapRows } from './wrap-rows.js';
|
|
10
12
|
export const MIN_CARD_WIDTH = 40;
|
|
11
13
|
const DIFF_LINES = 6;
|
|
12
|
-
const DETAIL_MAX = 60;
|
|
13
14
|
/** Myers diff is O(N·D) and synchronous: past this budget we show no diff
|
|
14
15
|
* at all (object + footer only), never a partial one. */
|
|
15
16
|
const DIFF_TIMEOUT_MS = 200;
|
|
@@ -33,7 +34,7 @@ function footerParts(input) {
|
|
|
33
34
|
else if (input.snapshot === 'new_object')
|
|
34
35
|
parts.push(paint('dim', '– snapshot (new object)'));
|
|
35
36
|
else if (input.snapshot === 'source_read_failed')
|
|
36
|
-
parts.push(paint('dim',
|
|
37
|
+
parts.push(paint('dim', '– snapshot (no source read)'));
|
|
37
38
|
else
|
|
38
39
|
parts.push(paint('dim', '– snapshot (skipped)'));
|
|
39
40
|
parts.push(input.written ? `${paint('ok', '✓')} written` : `${paint('bad', '✗')} written`);
|
|
@@ -54,15 +55,35 @@ function footerParts(input) {
|
|
|
54
55
|
else if (a.state === 'unverified')
|
|
55
56
|
parts.push(paint('warn', '· activation unverified'));
|
|
56
57
|
else
|
|
57
|
-
parts.push(`${paint('bad', '✗')} activation
|
|
58
|
+
parts.push(`${paint('bad', '✗')} activation`);
|
|
58
59
|
}
|
|
59
60
|
else {
|
|
60
61
|
// Activation is blocked — say so; a pending step would lie.
|
|
61
|
-
parts.push(`${paint('bad', '✗')} syntax
|
|
62
|
+
parts.push(`${paint('bad', '✗')} syntax`);
|
|
62
63
|
parts.push(paint('dim', '· activation blocked'));
|
|
63
64
|
}
|
|
64
65
|
return parts;
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Fix batch 2026-10-12 (Task 2): the reasons behind a failed step — the
|
|
69
|
+
* snapshot read failure, the first syntax error, SAP's activation refusal —
|
|
70
|
+
* are what the user needs, so they are rows of their own (wrapped at word
|
|
71
|
+
* breaks to `cells`, up to the verify-chain caps), never cut on the footer.
|
|
72
|
+
*/
|
|
73
|
+
function detailLines(input, cells) {
|
|
74
|
+
const out = [];
|
|
75
|
+
const wrap = (text, rows) => wrapRows(flat(clean(text)), Math.max(1, cells), rows);
|
|
76
|
+
if (input.snapshot === 'source_read_failed' && input.snapshotDetail) {
|
|
77
|
+
out.push(...wrap(`No source read: ${input.snapshotDetail}`, SNAPSHOT_DETAIL_ROWS).map((r) => paint('dim', r)));
|
|
78
|
+
}
|
|
79
|
+
if (input.syntax !== 'not_checked' && !input.syntax.ok) {
|
|
80
|
+
out.push(...wrap(input.syntax.errors[0] ?? 'syntax errors', ERROR_DETAIL_ROWS).map((r) => paint('bad', r)));
|
|
81
|
+
}
|
|
82
|
+
else if (input.syntax !== 'not_checked' && input.activation?.state === 'failed' && input.activation.detail) {
|
|
83
|
+
out.push(...wrap(input.activation.detail, ERROR_DETAIL_ROWS).map((r) => paint('bad', r)));
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
66
87
|
/** Changed lines for the card. Exported for tests only. */
|
|
67
88
|
export function diffBody(card, differ = defaultDiffer) {
|
|
68
89
|
if (card.oldSource === undefined || card.newSource === undefined)
|
|
@@ -99,6 +120,7 @@ export function buildChangeCard(input, width, box = getBoxChars()) {
|
|
|
99
120
|
// cut to the width so nothing wraps (colour dropped only on a cut part).
|
|
100
121
|
for (const p of parts)
|
|
101
122
|
out.push(visibleLen(p) > width ? cut(p.replace(ANSI, ''), width) : p);
|
|
123
|
+
out.push(...detailLines(input, width));
|
|
102
124
|
if (card.transport)
|
|
103
125
|
out.push(paint('dim', cut(`· ${clean(card.transport)}`, width)));
|
|
104
126
|
return out.join('\n');
|
|
@@ -117,6 +139,8 @@ export function buildChangeCard(input, width, box = getBoxChars()) {
|
|
|
117
139
|
lines.push(row(paint(l.startsWith('+') ? 'ok' : 'bad', cut(l, inner))));
|
|
118
140
|
if (body.hidden > 0)
|
|
119
141
|
lines.push(row(paint('dim', ` … +${body.hidden} lines`)));
|
|
142
|
+
for (const d of detailLines(input, inner))
|
|
143
|
+
lines.push(row(d));
|
|
120
144
|
lines.push(...footerLines(parts, chain, transportText, width, b, row));
|
|
121
145
|
return lines.join('\n');
|
|
122
146
|
}
|
|
@@ -8,7 +8,8 @@ import { paint } from './theme.js';
|
|
|
8
8
|
import { truncateToWidth } from './ui-width.js';
|
|
9
9
|
import { sanitizeControlChars } from './sanitize.js';
|
|
10
10
|
import { formatCost } from '../cost/pricing.js';
|
|
11
|
-
import { formatCredits, CREDITS_PENDING, creditsActive } from '../cost/credits.js';
|
|
11
|
+
import { formatCredits, CREDITS_PENDING, creditsActive, tokensOnlyActive } from '../cost/credits.js';
|
|
12
|
+
import { AZURE_INVOICE_LINE } from '../cost/display-mode.js';
|
|
12
13
|
export const SHORTCUTS_HINT = '? shortcuts';
|
|
13
14
|
const SEP = ' · ';
|
|
14
15
|
/**
|
|
@@ -17,6 +18,9 @@ const SEP = ' · ';
|
|
|
17
18
|
* total, BYOK/local see the session dollars — never the other.
|
|
18
19
|
*/
|
|
19
20
|
export function formatSpendSegment(s) {
|
|
21
|
+
// Azure (Task 5b): the customer's own Azure bill — no price of ours to show.
|
|
22
|
+
if (tokensOnlyActive())
|
|
23
|
+
return AZURE_INVOICE_LINE;
|
|
20
24
|
if (!s.creditsMode)
|
|
21
25
|
return `$${s.dollarSpendToday.toFixed(2)}`;
|
|
22
26
|
if (s.creditsPending && s.creditsSessionTotal === 0)
|
|
@@ -78,6 +82,8 @@ export function formatFooterLine(s, width) {
|
|
|
78
82
|
* cost otherwise, '' when there is none.
|
|
79
83
|
*/
|
|
80
84
|
export function turnSpendText(p) {
|
|
85
|
+
if (tokensOnlyActive())
|
|
86
|
+
return '';
|
|
81
87
|
if (creditsActive()) {
|
|
82
88
|
const c = p.credits;
|
|
83
89
|
// 2026-10-02: the whole user request (every model round), not its last round.
|
package/dist/renderer/notices.js
CHANGED
|
@@ -36,13 +36,18 @@ import stringWidth from 'string-width';
|
|
|
36
36
|
import { getLook } from './look.js';
|
|
37
37
|
import { getGlyphs } from './glyphs.js';
|
|
38
38
|
import { truncateToWidth, uiWidth } from './ui-width.js';
|
|
39
|
+
import { wrapRows } from './wrap-rows.js';
|
|
40
|
+
import { paint } from './theme.js';
|
|
41
|
+
import chalk from 'chalk';
|
|
39
42
|
import { getNoticeLogPath, recordFullNotice } from './notice-log.js';
|
|
40
43
|
import { getEffectiveBodyWidth, isDeclaredOneShot } from './tty.js';
|
|
41
44
|
import { EventEmitter } from 'node:events';
|
|
45
|
+
/** Fix batch 2026-10-12 — a panels notice wraps onto at most this many rows. */
|
|
46
|
+
export const NOTICE_MAX_ROWS = 3;
|
|
42
47
|
/**
|
|
43
|
-
* Format one notice
|
|
44
|
-
*
|
|
45
|
-
*
|
|
48
|
+
* Format one notice (trailing newline included). Multi-line messages are
|
|
49
|
+
* collapsed to one line of text; in the panels look that text then wraps
|
|
50
|
+
* onto up to NOTICE_MAX_ROWS screen rows (fix batch 2026-10-12).
|
|
46
51
|
*
|
|
47
52
|
* Piece 2 §9: warn → the attention shape ("! message"), info → the info
|
|
48
53
|
* shape ("· message", dim). Same glyphs as every other notice.
|
|
@@ -50,10 +55,11 @@ import { EventEmitter } from 'node:events';
|
|
|
50
55
|
export function formatNoticeLine(level, message) {
|
|
51
56
|
// Live fix 2026-09-26: a SAP HTML error page must never leak tags/CSS here.
|
|
52
57
|
const readable = looksLikeMarkup(message) ? cleanErrorText(message, 400) : message;
|
|
53
|
-
|
|
54
|
-
// Live fix 2026-09-26 (panels): never wrap mid-word —
|
|
55
|
-
//
|
|
56
|
-
// session's notice log. Legacy stays
|
|
58
|
+
const oneLine = readable.replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
59
|
+
// Live fix 2026-09-26 (panels): never let the terminal wrap mid-word —
|
|
60
|
+
// the text is pre-wrapped at word breaks inside uiWidth (below) and a cut
|
|
61
|
+
// notice keeps its full text in the session's notice log. Legacy stays
|
|
62
|
+
// byte-identical.
|
|
57
63
|
// Review I3: only when a screen is there to wrap (stdout is a TTY) or the
|
|
58
64
|
// full text has somewhere to go (a notice log). One-shot / piped output
|
|
59
65
|
// keeps the whole single line — nothing else would record it.
|
|
@@ -61,15 +67,23 @@ export function formatNoticeLine(level, message) {
|
|
|
61
67
|
// keeps the whole line too.
|
|
62
68
|
const wrapsOnScreen = process.stdout.isTTY === true && !isDeclaredOneShot();
|
|
63
69
|
if (getLook() === 'panels' && (wrapsOnScreen || getNoticeLogPath() !== undefined)) {
|
|
70
|
+
// Fix batch 2026-10-12 (Task 2): a notice may carry what the user must
|
|
71
|
+
// act on (a transport number, SAP's reason), so it wraps at word breaks
|
|
72
|
+
// onto up to NOTICE_MAX_ROWS rows — further rows indented two spaces, the
|
|
73
|
+
// notice shape — instead of being cut to one. Only a longer notice is
|
|
74
|
+
// cut; its full text goes to the notice log and the screen says so.
|
|
64
75
|
const g = getGlyphs();
|
|
65
76
|
const room = Math.min(uiWidth(), getEffectiveBodyWidth()) - stringWidth(level === 'warn' ? g.attention : g.info) - 1;
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (cut
|
|
77
|
+
const rows = wrapRows(oneLine, room, NOTICE_MAX_ROWS);
|
|
78
|
+
const cut = wrapRows(oneLine, room, NOTICE_MAX_ROWS + 1).length > NOTICE_MAX_ROWS;
|
|
79
|
+
const logPath = getNoticeLogPath();
|
|
80
|
+
if (cut)
|
|
70
81
|
recordFullNotice(level, message);
|
|
71
|
-
|
|
72
|
-
}
|
|
82
|
+
const head = level === 'warn' ? formatAttention({ text: rows[0] }) : formatInfo(rows[0]);
|
|
83
|
+
const more = rows.slice(1).map((r) => (level === 'warn' ? ` ${chalk.bold(r)}` : paint('dim', ` ${r}`)));
|
|
84
|
+
if (cut && logPath)
|
|
85
|
+
more.push(paint('dim', truncateToWidth(` Full text: ${logPath}`, room + 2)));
|
|
86
|
+
return head + more.map((r) => `${r}\n`).join('');
|
|
73
87
|
}
|
|
74
88
|
return level === 'warn' ? formatAttention({ text: oneLine }) : formatInfo(oneLine);
|
|
75
89
|
}
|
|
@@ -165,7 +165,9 @@ const MESSAGES = {
|
|
|
165
165
|
'Walking enhancement spots…',
|
|
166
166
|
],
|
|
167
167
|
sap_message_maintain: [
|
|
168
|
-
'
|
|
168
|
+
'Reading the message class…',
|
|
169
|
+
'Writing the messages…',
|
|
170
|
+
'Reading the messages back…',
|
|
169
171
|
],
|
|
170
172
|
sap_number_range_intervals: [
|
|
171
173
|
'(stub — short-circuit)',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatCost } from '../cost/pricing.js';
|
|
2
|
-
import { creditsActive, formatCreditsLine, CREDITS_PENDING } from '../cost/credits.js';
|
|
2
|
+
import { creditsActive, tokensOnlyActive, formatCreditsLine, CREDITS_PENDING } from '../cost/credits.js';
|
|
3
|
+
import { AZURE_INVOICE_LINE } from '../cost/display-mode.js';
|
|
3
4
|
import { formatTurnReceipt, turnSpendText } from './footer-line.js';
|
|
4
5
|
import { formatAttention } from './notice-shape.js';
|
|
5
6
|
import { paint } from './theme.js';
|
|
@@ -79,12 +80,18 @@ export function formatStatusFooter(params) {
|
|
|
79
80
|
const tokStr = inCredits ? '' : tokens.toLocaleString() + ' tok this turn';
|
|
80
81
|
const cachedStr = !inCredits && cachedTokens > 0 ? `${cachedTokens.toLocaleString()} cached` : '';
|
|
81
82
|
const elapsed = elapsedMs > 0 ? `${(elapsedMs / 1000).toFixed(1)}s` : '';
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const costSessStr = inCredits
|
|
83
|
+
// Azure (Task 5b): tokens only, the price line says where the bill is.
|
|
84
|
+
const tokensOnly = tokensOnlyActive();
|
|
85
|
+
const costTurnStr = tokensOnly
|
|
86
86
|
? ''
|
|
87
|
-
:
|
|
87
|
+
: inCredits
|
|
88
|
+
? (credits ? formatCreditsLine(credits) : CREDITS_PENDING)
|
|
89
|
+
: (costThisTurn != null ? formatCost(costThisTurn) : '');
|
|
90
|
+
const costSessStr = tokensOnly
|
|
91
|
+
? AZURE_INVOICE_LINE
|
|
92
|
+
: inCredits
|
|
93
|
+
? ''
|
|
94
|
+
: (costSession != null ? `session: ${formatCost(costSession)}` : '');
|
|
88
95
|
// Classic full receipt (piece 2 restyle — same facts, reviewed pin):
|
|
89
96
|
// rules at uiWidth() in the frame role, ' · ' separators, the large-session
|
|
90
97
|
// advisory as a `!` notice (largeSessionNotice).
|
|
@@ -177,7 +177,13 @@ const RULES = {
|
|
|
177
177
|
sap_unlock: sapObject('Unlocked'),
|
|
178
178
|
sap_service_binding_publish: sapObject('Published'),
|
|
179
179
|
sap_service_binding_unpublish: sapObject('Unpublished'),
|
|
180
|
-
sap_message_maintain: (a) =>
|
|
180
|
+
sap_message_maintain: (a) => {
|
|
181
|
+
const n = Array.isArray(a['messages']) ? a['messages'].length : 0;
|
|
182
|
+
if (n > 0) {
|
|
183
|
+
return { verb: 'Messages', object: shortRight(str(a['messageClass']) ?? ''), detail: n === 1 ? '1 message' : `${n} messages` };
|
|
184
|
+
}
|
|
185
|
+
return { verb: 'Messages', object: shortRight([str(a['messageClass']), str(a['messageNumber'])].filter(Boolean).join(' ')) };
|
|
186
|
+
},
|
|
181
187
|
sap_number_range_intervals: (a) => ({ verb: 'Number range', object: shortRight(str(a['nrObject']) ?? '') }),
|
|
182
188
|
ask_question: (a) => ({ verb: 'Question', object: shortRight(str(a['question']) ?? '') }),
|
|
183
189
|
todo_set: () => ({ verb: 'Updated todos', object: '' }),
|
|
@@ -257,7 +263,7 @@ const PRESENT = {
|
|
|
257
263
|
'Created transport': 'Creating transport', 'Transport for': 'Finding transport for',
|
|
258
264
|
'Listed transports': 'Listing transports', Released: 'Releasing', 'Created domain': 'Creating domain',
|
|
259
265
|
'Created data element': 'Creating data element', Locked: 'Locking', Unlocked: 'Unlocking',
|
|
260
|
-
Published: 'Publishing', Unpublished: 'Unpublishing',
|
|
266
|
+
Published: 'Publishing', Unpublished: 'Unpublishing', Messages: 'Maintaining messages in',
|
|
261
267
|
'Number range': 'Maintaining number range', Question: 'Asking', 'Updated todos': 'Updating todos',
|
|
262
268
|
'Ran skill': 'Running skill', 'Read document': 'Reading document', Fetched: 'Fetching',
|
|
263
269
|
'Searched web': 'Searching web', 'Read project context': 'Reading project context',
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
* never synthesized. States render truthfully:
|
|
9
9
|
* - snapshot 'new_object' → "① snapshot – (new object)" (genuine ADT 404 —
|
|
10
10
|
* nothing to save; NEVER used for other read failures)
|
|
11
|
-
* - snapshot 'source_read_failed' → "① snapshot – (no source read
|
|
11
|
+
* - snapshot 'source_read_failed' → "① snapshot – (no source read)" and the
|
|
12
|
+
* reason on its own row under the chain —
|
|
12
13
|
* the pre-write read failed for a non-404 reason; the object may exist
|
|
13
14
|
* but there is no recovery copy. Hard-gate tools block instead of
|
|
14
15
|
* emitting this; it only appears on soft (best-effort) gates.
|
|
15
16
|
* - snapshot 'best_effort_failed' → "① snapshot – (skipped)" (write tools
|
|
16
17
|
* where the gate is soft, e.g. class includes)
|
|
17
|
-
* - syntax fail → "③ syntax ✗
|
|
18
|
+
* - syntax fail → "③ syntax ✗", the first error whole on the rows under
|
|
19
|
+
* the chain (fix batch 2026-10-12: no 60-character cut) AND no ④ at all —
|
|
18
20
|
* activation is blocked, so we don't show a fake pending step
|
|
19
21
|
* - syntax 'not_checked' → "③ syntax – (checked at activation)" —
|
|
20
22
|
* sap_set_class_include has no standalone syntax gate
|
|
@@ -28,21 +30,43 @@
|
|
|
28
30
|
* result row in both renderers.
|
|
29
31
|
*/
|
|
30
32
|
import chalk from 'chalk';
|
|
33
|
+
import { wrapRows } from './wrap-rows.js';
|
|
34
|
+
import { uiWidth } from './ui-width.js';
|
|
31
35
|
const pass = (s) => `${s} ${chalk.green('✓')}`;
|
|
32
36
|
const fail = (s, detail) => `${s} ${chalk.red('✗')}${detail ? ' ' + chalk.red(`(${detail})`) : ''}`;
|
|
33
37
|
const skip = (s, why) => chalk.dim(`${s} – (${why})`);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Fix batch 2026-10-12 (Task 2): the SAP error is what the user needs, so it
|
|
40
|
+
* is never cut to a few characters. Details go on their own rows under the
|
|
41
|
+
* chain, wrapped at word breaks inside the width, up to these many rows (the
|
|
42
|
+
* last ends with … only when even that is not enough; the model gets the
|
|
43
|
+
* full text in the tool result).
|
|
44
|
+
*/
|
|
45
|
+
export const ERROR_DETAIL_ROWS = 4;
|
|
46
|
+
export const SNAPSHOT_DETAIL_ROWS = 2;
|
|
47
|
+
/** Detail rows sit under the chain: its two-space indent plus two. */
|
|
48
|
+
const DETAIL_INDENT = ' ';
|
|
49
|
+
const flatDetail = (s) => s.replace(/\s+/g, ' ').trim();
|
|
50
|
+
/** Detail rows: indented, wrapped inside `width`, at most `maxRows`. */
|
|
51
|
+
export function detailRows(text, width, maxRows) {
|
|
52
|
+
return wrapRows(flatDetail(text), Math.max(1, width - DETAIL_INDENT.length), maxRows).map((r) => `${DETAIL_INDENT}${r}`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Detail-row width: the UI width less the Ink body's 4-cell border overhead
|
|
56
|
+
* (tty.ts BORDER_OVERHEAD), so a row never reaches the body edge in either
|
|
57
|
+
* renderer. Pure (no tty import): many tool tests mock tty.js partially.
|
|
58
|
+
*/
|
|
59
|
+
export function chainWidth() {
|
|
60
|
+
return Math.max(20, uiWidth() - 4);
|
|
61
|
+
}
|
|
40
62
|
/**
|
|
41
63
|
* Build the chain line (no trailing newline). Returns the full line
|
|
42
|
-
* including the two-space indent that aligns with the ⎿ result row
|
|
64
|
+
* including the two-space indent that aligns with the ⎿ result row, then
|
|
65
|
+
* any detail rows (snapshot read failure, first syntax error) under it.
|
|
43
66
|
*/
|
|
44
|
-
export function buildWriteChainLine(input) {
|
|
67
|
+
export function buildWriteChainLine(input, width = chainWidth()) {
|
|
45
68
|
const parts = [];
|
|
69
|
+
const details = [];
|
|
46
70
|
// ① snapshot
|
|
47
71
|
if (input.snapshot === 'taken')
|
|
48
72
|
parts.push(pass('① snapshot'));
|
|
@@ -51,10 +75,10 @@ export function buildWriteChainLine(input) {
|
|
|
51
75
|
else if (input.snapshot === 'source_read_failed') {
|
|
52
76
|
// Honest: the read failed — we do NOT know the object is new, and there
|
|
53
77
|
// is no recovery copy. Never render this as "(new object)".
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
:
|
|
57
|
-
|
|
78
|
+
parts.push(skip('① snapshot', 'no source read'));
|
|
79
|
+
if (input.snapshotDetail) {
|
|
80
|
+
details.push(...detailRows(`No source read: ${input.snapshotDetail}`, width, SNAPSHOT_DETAIL_ROWS).map((r) => chalk.dim(r)));
|
|
81
|
+
}
|
|
58
82
|
}
|
|
59
83
|
else
|
|
60
84
|
parts.push(skip('① snapshot', 'skipped'));
|
|
@@ -70,10 +94,9 @@ export function buildWriteChainLine(input) {
|
|
|
70
94
|
parts.push(chalk.dim('④ activate pending'));
|
|
71
95
|
}
|
|
72
96
|
else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
parts.push(fail('③ syntax', detail));
|
|
97
|
+
parts.push(fail('③ syntax'));
|
|
98
|
+
details.push(...detailRows(input.syntax.errors[0] ?? 'syntax errors', width, ERROR_DETAIL_ROWS).map((r) => chalk.red(r)));
|
|
76
99
|
// No ④ — activation is blocked; rendering a pending step would lie.
|
|
77
100
|
}
|
|
78
|
-
return ' ' + parts.join(' ');
|
|
101
|
+
return [' ' + parts.join(' '), ...details].join('\n');
|
|
79
102
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/renderer/wrap-rows.ts
|
|
2
|
+
//
|
|
3
|
+
// The one wrap helper for card content (fix batch 2026-10-12, Task 2).
|
|
4
|
+
//
|
|
5
|
+
// THE RULE: card content wraps; chrome may cut. Text a person reads to make
|
|
6
|
+
// a decision — card titles, facts, option labels and descriptions, risk and
|
|
7
|
+
// failure reasons, SAP error details — is pre-wrapped here into a KNOWN
|
|
8
|
+
// number of rows. Ink never soft-wraps a live-frame row (its erase
|
|
9
|
+
// accounting counts rows), so callers draw each returned row as its own
|
|
10
|
+
// truncate-end <Text> and count every row in their height estimate. Status
|
|
11
|
+
// rows, hints, palettes, spinners, traces and "… N more" counters stay one
|
|
12
|
+
// row and cut with truncateToWidth.
|
|
13
|
+
//
|
|
14
|
+
// Replaces factRows (confirm-card) and wrapCardText (ask-card), which did the
|
|
15
|
+
// same job twice.
|
|
16
|
+
import stringWidth from 'string-width';
|
|
17
|
+
import { truncateToWidth } from './ui-width.js';
|
|
18
|
+
import { sanitizeControlChars } from './sanitize.js';
|
|
19
|
+
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
20
|
+
/** The longest grapheme-whole prefix of `word` that fits `cells` (at least one grapheme). */
|
|
21
|
+
function takeCells(word, cells) {
|
|
22
|
+
let out = '';
|
|
23
|
+
let used = 0;
|
|
24
|
+
for (const { segment } of GRAPHEMES.segment(word)) {
|
|
25
|
+
const w = stringWidth(segment);
|
|
26
|
+
if (used + w > cells && out.length > 0)
|
|
27
|
+
break;
|
|
28
|
+
out += segment;
|
|
29
|
+
used += w;
|
|
30
|
+
if (used >= cells)
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `text` as the rows it is drawn on, each at most `cells` terminal cells.
|
|
37
|
+
* Control characters are neutralised, tabs become two spaces and newlines
|
|
38
|
+
* one space. Text that fits is one row, unchanged. Otherwise it wraps at
|
|
39
|
+
* spaces (a word wider than the row is cut where it must be; wide characters
|
|
40
|
+
* count two cells and are never split) onto at most `maxRows` rows; when
|
|
41
|
+
* more is left, the last row ends with the `more` glyph (… / ...). With
|
|
42
|
+
* `maxRows` 1 this is truncateToWidth: one row, cut.
|
|
43
|
+
*/
|
|
44
|
+
export function wrapRows(text, cells, maxRows = 1) {
|
|
45
|
+
const flat = sanitizeControlChars(text).replace(/\t/g, ' ').replace(/\r?\n/g, ' ');
|
|
46
|
+
const width = Math.max(1, cells);
|
|
47
|
+
const max = Math.max(1, Math.floor(maxRows));
|
|
48
|
+
if (stringWidth(flat) <= width)
|
|
49
|
+
return [flat];
|
|
50
|
+
if (max === 1)
|
|
51
|
+
return [truncateToWidth(flat, width)];
|
|
52
|
+
const indent = /^ */.exec(flat)[0].slice(0, Math.max(0, width - 1));
|
|
53
|
+
const rows = [];
|
|
54
|
+
let cur = indent;
|
|
55
|
+
const curEmpty = () => cur.length === 0 || cur === indent;
|
|
56
|
+
for (let word of flat.trim().split(/ +/).filter((w) => w.length > 0)) {
|
|
57
|
+
while (stringWidth(word) > width) {
|
|
58
|
+
if (!curEmpty()) {
|
|
59
|
+
rows.push(cur);
|
|
60
|
+
cur = '';
|
|
61
|
+
}
|
|
62
|
+
const head = takeCells(word, width - stringWidth(cur));
|
|
63
|
+
rows.push(cur + head);
|
|
64
|
+
cur = '';
|
|
65
|
+
word = word.slice(head.length);
|
|
66
|
+
}
|
|
67
|
+
if (word.length === 0)
|
|
68
|
+
continue;
|
|
69
|
+
if (curEmpty())
|
|
70
|
+
cur += word;
|
|
71
|
+
else if (stringWidth(cur) + 1 + stringWidth(word) <= width)
|
|
72
|
+
cur += ` ${word}`;
|
|
73
|
+
else {
|
|
74
|
+
rows.push(cur);
|
|
75
|
+
cur = word;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!curEmpty() || rows.length === 0)
|
|
79
|
+
rows.push(cur);
|
|
80
|
+
if (rows.length <= max)
|
|
81
|
+
return rows;
|
|
82
|
+
const kept = rows.slice(0, max);
|
|
83
|
+
// The trailing space guarantees a cut (and so the more glyph) even when the
|
|
84
|
+
// joined rest would happen to fit.
|
|
85
|
+
kept[max - 1] = truncateToWidth(`${rows.slice(max - 1).join(' ')} `, width);
|
|
86
|
+
return kept;
|
|
87
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure tenant mode, Task 6b fix round 1 — a yes/no question at REPL start
|
|
3
|
+
* (the endpoint confirmation, "Sign in to Microsoft now?"). Ctrl+C there makes
|
|
4
|
+
* inquirer reject with ExitPromptError; that is the person's "no", never an
|
|
5
|
+
* unhandled error that ends the CLI with a stack. Any other error still throws.
|
|
6
|
+
*/
|
|
7
|
+
export function askOrNo(ask) {
|
|
8
|
+
return async (q) => {
|
|
9
|
+
try {
|
|
10
|
+
return await ask(q);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (err?.name === 'ExitPromptError')
|
|
14
|
+
return false;
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -8,22 +8,30 @@
|
|
|
8
8
|
* read of the process-wide cost display mode (cost/credits.ts); no
|
|
9
9
|
* inquirer, so the router can import it.
|
|
10
10
|
*/
|
|
11
|
-
import { creditsActive } from '../cost/credits.js';
|
|
11
|
+
import { creditsActive, tokensOnlyActive } from '../cost/credits.js';
|
|
12
12
|
// Pure generated data (type-only import inside), so this file stays light.
|
|
13
13
|
import { BUNDLED_RUNS } from '../runs/bundled-runs.js';
|
|
14
14
|
export const COST_DESCRIPTION_USD = 'Show this session\'s spend so far ($ and tokens)';
|
|
15
15
|
export const COST_DESCRIPTION_CREDITS = 'Show the credits this session has used so far';
|
|
16
|
+
/** Azure tenant mode (Task 7, ruling 5): tokens only — never "$" or credits. */
|
|
17
|
+
export const COST_DESCRIPTION_TOKENS = "Show this session's tokens so far";
|
|
16
18
|
/** Piece 2 §10: managed customers see credits, never "$" (cost/display-mode.ts). */
|
|
17
19
|
/** /usage (2026-10-09): this month in credits — see commands/usage.ts. */
|
|
18
20
|
export const USAGE_DESCRIPTION = 'Show this month’s credits, your balance and the top skills';
|
|
21
|
+
export const USAGE_DESCRIPTION_TOKENS = "Show where your usage is: your company's Azure invoice";
|
|
19
22
|
export function costCommandDescription() {
|
|
23
|
+
if (tokensOnlyActive())
|
|
24
|
+
return COST_DESCRIPTION_TOKENS;
|
|
20
25
|
return creditsActive() ? COST_DESCRIPTION_CREDITS : COST_DESCRIPTION_USD;
|
|
21
26
|
}
|
|
27
|
+
export function usageCommandDescription() {
|
|
28
|
+
return tokensOnlyActive() ? USAGE_DESCRIPTION_TOKENS : USAGE_DESCRIPTION;
|
|
29
|
+
}
|
|
22
30
|
export const BUILTIN_ENTRIES = [
|
|
23
31
|
// Piece 2: /cancel is not listed — it exists only in the classic loop
|
|
24
32
|
// (repl.tsx), and Esc / Ctrl+C already cancel under Ink.
|
|
25
33
|
{ name: '/cost', get description() { return costCommandDescription(); } },
|
|
26
|
-
{ name: '/usage', description
|
|
34
|
+
{ name: '/usage', get description() { return usageCommandDescription(); } },
|
|
27
35
|
{ name: '/compact', description: 'Shrink a long session and keep the facts (later turns cost far less)' },
|
|
28
36
|
{ name: '/export', description: 'Export the signable session audit record — /export audit [filename]' },
|
|
29
37
|
{ name: '/context', description: 'Show what fills the model context window — a composition grid of bars' },
|
|
@@ -21,7 +21,7 @@ import { computeCost } from '../cost/pricing.js';
|
|
|
21
21
|
import { computeSessionCost } from '../cost/session-cost.js';
|
|
22
22
|
import { globalStore } from '../ui/sap-state-store.js';
|
|
23
23
|
import { turnStatusEmitter } from '../ui/turn-status-emitter.js';
|
|
24
|
-
import { globalCredits } from '../cost/credits.js';
|
|
24
|
+
import { globalCredits, tokensOnlyActive } from '../cost/credits.js';
|
|
25
25
|
import { printStatusFooter, formatStatusFooter } from '../renderer/status-footer.js';
|
|
26
26
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
27
27
|
import { isUserPrompt } from '../session/user-prompt.js';
|
|
@@ -49,7 +49,8 @@ export function computeTurnFacts(session, snapshot, turnModel) {
|
|
|
49
49
|
const userTurns = session.messages.filter(isUserPrompt).length;
|
|
50
50
|
const lifetimeAfter = session.usage.input_tokens + session.usage.output_tokens;
|
|
51
51
|
const turnTokens = Math.max(0, lifetimeAfter - (snapshot.input + snapshot.output));
|
|
52
|
-
|
|
52
|
+
// Azure: no price for the customer's deployment — never consult one.
|
|
53
|
+
const turnCost = tokensOnlyActive() ? 0 : computeCost(turnModel ?? session.model, {
|
|
53
54
|
input: session.usage.input_tokens - snapshot.input,
|
|
54
55
|
output: session.usage.output_tokens - snapshot.output,
|
|
55
56
|
cacheRead: session.usage.cache_read_input_tokens - snapshot.cacheRead,
|