@cspeach/cli 1.0.0 → 1.1.0
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/loop.js +22 -9
- package/dist/approvals/op-labels.js +124 -0
- package/dist/approvals/render.js +42 -36
- package/dist/cli.js +15 -0
- package/dist/commands/compact.js +28 -2
- package/dist/commands/config-set.js +189 -0
- package/dist/commands/config-show.js +20 -0
- package/dist/commands/export-audit.js +43 -0
- package/dist/commands/help.js +5 -0
- package/dist/commands/plan-audit-evidence.js +266 -0
- package/dist/commands/plan-audit.js +692 -0
- package/dist/commands/plan-chain.js +671 -0
- package/dist/commands/plan-continue.js +179 -0
- package/dist/commands/plan-gate.js +154 -0
- package/dist/commands/plan-resume.js +588 -33
- package/dist/config/loader.js +128 -4
- package/dist/config/model-defaults.js +14 -0
- package/dist/cost/pricing.js +27 -1
- package/dist/doctor/checks/system-roles.js +41 -0
- package/dist/doctor/run.js +2 -0
- package/dist/models/resolve.js +61 -0
- package/dist/models/server-config.js +155 -0
- package/dist/one-shot.js +25 -3
- package/dist/projects/extract-cca.js +3 -1
- package/dist/projects/extract-modernize.js +3 -1
- package/dist/projects/extract-plan.js +60 -6
- package/dist/projects/extract-test-coverage.js +3 -1
- package/dist/projects/extract-upgrade.js +3 -1
- package/dist/projects/handover-md.js +195 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-run.js +137 -13
- package/dist/projects/plan-schema.js +73 -0
- package/dist/projects/run-lease.js +157 -0
- package/dist/projects/save-command.js +26 -15
- package/dist/renderer/status-footer.js +22 -12
- package/dist/renderer/thinking-heartbeat.js +64 -8
- package/dist/renderer/todo-block.js +51 -0
- package/dist/renderer/tool-widget.js +37 -0
- package/dist/repl/bracketed-paste.js +28 -19
- package/dist/repl/builtin-commands.js +5 -0
- package/dist/repl/current-transport.js +10 -0
- package/dist/repl/history.js +86 -0
- package/dist/repl/ink-stdin-guard.js +64 -0
- package/dist/repl/mode-ceiling.js +16 -0
- package/dist/repl/mode-cycle.js +104 -0
- package/dist/repl/post-turn-status.js +24 -4
- package/dist/repl/slash-completer.js +5 -0
- package/dist/repl.js +954 -83
- package/dist/rewind/candidates.js +194 -0
- package/dist/rewind/cli.js +137 -0
- package/dist/rewind/format.js +27 -0
- package/dist/rewind/restore.js +245 -0
- package/dist/session/audit-export.js +459 -0
- package/dist/session/context-report.js +163 -0
- package/dist/session/recap.js +160 -0
- package/dist/skill-catalog.js +9 -3
- package/dist/skills/bundled-skills.js +59 -66
- package/dist/tools/approval.js +115 -7
- package/dist/tools/ask-question.js +304 -3
- package/dist/tools/extend-model/anchored-insert.js +604 -0
- package/dist/tools/extend-model/tool.js +162 -10
- package/dist/tools/fiori/fe-extend.js +76 -0
- package/dist/tools/fiori/fe-scaffold.js +29 -3
- package/dist/tools/fiori/floorplan-map.js +19 -0
- package/dist/tools/fiori/samples/data/index.json +13602 -0
- package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
- package/dist/tools/fiori/samples/loader.js +248 -0
- package/dist/tools/fiori/samples/search.js +63 -0
- package/dist/tools/fiori/samples/types.js +2 -0
- package/dist/tools/fiori/smoke/assertions.js +74 -0
- package/dist/tools/fiori/smoke/browser.js +52 -0
- package/dist/tools/fiori/smoke/driver.js +89 -0
- package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
- package/dist/tools/fiori/smoke/run-smoke.js +149 -0
- package/dist/tools/fiori/tools.js +328 -3
- package/dist/tools/local-build.js +11 -1
- package/dist/tools/sap-read.js +79 -11
- package/dist/tools/sap-write.js +24 -4
- package/dist/tools/snapshot.js +27 -1
- package/dist/tools/subagent/agent_run.js +27 -3
- package/dist/tools/todo.js +144 -0
- package/dist/ui/app.js +372 -19
- package/dist/ui/approval-modal.js +49 -16
- package/dist/ui/ask-question-emitter.js +14 -0
- package/dist/ui/context-grid.js +108 -0
- package/dist/ui/footer.js +109 -30
- package/dist/ui/header.js +7 -0
- package/dist/ui/line-resolution.js +18 -2
- package/dist/ui/rewind-emitter.js +10 -0
- package/dist/ui/rewind-panel.js +81 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/status-line.js +43 -0
- package/dist/ui/text-input.js +72 -8
- package/dist/ui/todo-emitter.js +25 -0
- package/dist/ui/todo-panel.js +64 -0
- package/dist/ui/turn-status-emitter.js +50 -4
- package/dist/ui/turn-status.js +18 -3
- package/dist/ui/widgets/ask-form.js +242 -0
- package/dist/ui/widgets/ask-question-modal.js +17 -7
- package/package.json +4 -1
package/dist/agent/loop.js
CHANGED
|
@@ -5,9 +5,10 @@ import { listTools, getTool, toAnthropicTools } from '../tools/index.js';
|
|
|
5
5
|
import { saveSession } from '../session/store.js';
|
|
6
6
|
import { recordCompletedToolCall } from '../session/pending.js';
|
|
7
7
|
import { loadConfig } from '../config/loader.js';
|
|
8
|
+
import { resolveModelRole } from '../models/resolve.js';
|
|
8
9
|
import { retryWithBackoff } from './retry.js';
|
|
9
10
|
import { renderChunk, initialBufferState, render } from '../renderer/pipeline.js';
|
|
10
|
-
import { renderToolCallTop, renderToolCallBottom, startToolSpinner, startThinkingSpinner } from '../renderer/tool-widget.js';
|
|
11
|
+
import { renderToolCallTop, renderToolCallBottom, startToolSpinner, startThinkingSpinner, isWidgetSuppressedTool } from '../renderer/tool-widget.js';
|
|
11
12
|
import { formatToolErrorSummary } from '../sap-errors/parse-adt-exception.js';
|
|
12
13
|
import { startThinkingHeartbeat } from '../renderer/thinking-heartbeat.js';
|
|
13
14
|
// 2026-06-07 — status-row narration. Safe to import in every mode: the
|
|
@@ -520,8 +521,10 @@ export async function runTurn(params) {
|
|
|
520
521
|
healSessionMessagesInPlace(session.messages);
|
|
521
522
|
const streamParams = {
|
|
522
523
|
// A4 — per-turn override (plan model tiering) wins over the
|
|
523
|
-
// configured default; absent on every non-plan turn.
|
|
524
|
-
model
|
|
524
|
+
// configured default; absent on every non-plan turn. model-governance
|
|
525
|
+
// step 2d — the session-default model now resolves env > local >
|
|
526
|
+
// server > built-in (byte-identical to cfg.default_model when nothing set).
|
|
527
|
+
model: params.modelOverride ?? resolveModelRole('session_default', cfg),
|
|
525
528
|
// v0.3.1 — was 8192. Bumped because code-gen skills (abap-generate
|
|
526
529
|
// etc.) kept running out of budget mid-turn: adaptive thinking +
|
|
527
530
|
// multiple tool-result prompts + TL;DR mandate + summary prose
|
|
@@ -764,7 +767,8 @@ export async function runTurn(params) {
|
|
|
764
767
|
// never let a save error crash the turn.
|
|
765
768
|
if (params.onPlanManifest) {
|
|
766
769
|
const turnText = collectTurnAssistantText(session.messages, turnStartMessageCount);
|
|
767
|
-
|
|
770
|
+
// E2 dual-read: accept both the legacy `csforge:` and current `cspeach:` prefixes.
|
|
771
|
+
const blocks = turnText.match(/<!--\s*(?:csforge|cspeach):plan-manifest\s*\n[\s\S]*?\n\s*-->/g);
|
|
768
772
|
const latest = blocks ? blocks[blocks.length - 1] : null;
|
|
769
773
|
if (latest && latest !== lastFiredManifest) {
|
|
770
774
|
lastFiredManifest = latest;
|
|
@@ -872,7 +876,7 @@ export async function runTurn(params) {
|
|
|
872
876
|
// A4 — record the actual turn model if an override ran the stream
|
|
873
877
|
// (unreachable today: plan-resume turns suppress this hook, and
|
|
874
878
|
// they are the only modelOverride caller — kept honest anyway).
|
|
875
|
-
model: params.modelOverride ?? cfg
|
|
879
|
+
model: params.modelOverride ?? resolveModelRole('session_default', cfg) ?? session.model ?? 'unknown',
|
|
876
880
|
emit,
|
|
877
881
|
promotedFrom: promotedFromForSave,
|
|
878
882
|
});
|
|
@@ -885,14 +889,17 @@ export async function runTurn(params) {
|
|
|
885
889
|
// throttle live in config.compact — see config/loader.ts:CompactConfig.
|
|
886
890
|
try {
|
|
887
891
|
const { maybeAutoCompact } = await import('../commands/auto-compact.js');
|
|
888
|
-
const { buildSummarisationPrompt, serialiseForSummariser
|
|
892
|
+
const { buildSummarisationPrompt, serialiseForSummariser } = await import('../commands/compact.js');
|
|
889
893
|
const { summariseViaProvider } = await import('./summarise-via-provider.js');
|
|
890
894
|
await maybeAutoCompact({
|
|
891
895
|
session,
|
|
892
896
|
config: cfg.compact,
|
|
893
897
|
turnNumber,
|
|
894
898
|
emit,
|
|
895
|
-
summarise: async (toSummarise) => summariseViaProvider(params.provider, buildSummarisationPrompt(), serialiseForSummariser(toSummarise),
|
|
899
|
+
summarise: async (toSummarise) => summariseViaProvider(params.provider, buildSummarisationPrompt(), serialiseForSummariser(toSummarise),
|
|
900
|
+
// model-governance step 2d — compact model resolves env > local >
|
|
901
|
+
// server > built-in (== COMPACTION_MODEL when nothing set).
|
|
902
|
+
{ model: resolveModelRole('compact', cfg), maxTokens: 4000, headers: { 'X-CSForge-Skill': 'compact' } }),
|
|
896
903
|
});
|
|
897
904
|
}
|
|
898
905
|
catch (err) {
|
|
@@ -943,6 +950,8 @@ export async function runTurn(params) {
|
|
|
943
950
|
for (const block of currentAssistantContent) {
|
|
944
951
|
if (block.type === 'tool_use') {
|
|
945
952
|
// Phase 1: print the dispatch line (CC-style: ⏺ name(args)).
|
|
953
|
+
// No-op for widget-suppressed tools (ask_question) — the form/modal
|
|
954
|
+
// is the visible representation; see tool-widget.ts.
|
|
946
955
|
renderToolCallTop({
|
|
947
956
|
name: block.name,
|
|
948
957
|
args: (block.input ?? {}),
|
|
@@ -951,8 +960,12 @@ export async function runTurn(params) {
|
|
|
951
960
|
// Phase 2a: start the peach-themed spinner. No-op outside TTY / Ink
|
|
952
961
|
// / CI — the result line still prints, just without the in-place
|
|
953
962
|
// animation. Spinner is purely a "still working" cue, not
|
|
954
|
-
// load-bearing for output.
|
|
955
|
-
|
|
963
|
+
// load-bearing for output. Skipped entirely for widget-suppressed
|
|
964
|
+
// tools (ask_question): with the ⏺ line gone the spinner would
|
|
965
|
+
// orphan on its own row above the form.
|
|
966
|
+
const spinner = isWidgetSuppressedTool(block.name)
|
|
967
|
+
? { stop: () => undefined }
|
|
968
|
+
: startToolSpinner({ chunkEmitter: params.chunkEmitter });
|
|
956
969
|
// 2026-06-06 (turn-liveness, B5 smoke feedback) — the in-place tool
|
|
957
970
|
// spinner self-disables under Ink (phantom cursor #25), which left
|
|
958
971
|
// slow tool calls (SAP over VPN: 10-60s) as DEAD AIR between the ⏺
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable operation + object labels for the approval surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the cryptic op-code glyph (`A`/`M`/`D`/`>`/`^`) the live review
|
|
5
|
+
* found dishonest and information-poor. Every approval box, plan gate, and
|
|
6
|
+
* classic prompt now speaks a verb ("Create table ZMNT_NOTE") instead of a
|
|
7
|
+
* one-letter code where a word belongs.
|
|
8
|
+
*/
|
|
9
|
+
const VERBS = {
|
|
10
|
+
create: 'Create',
|
|
11
|
+
modify: 'Change',
|
|
12
|
+
delete: 'Delete',
|
|
13
|
+
activate: 'Activate',
|
|
14
|
+
release: 'Release',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* SAP ADT type codes → plain words. The `type` field is free text supplied by
|
|
18
|
+
* the model, so an unknown/decorated value (e.g. "testclasses include") falls
|
|
19
|
+
* through unchanged rather than being mangled.
|
|
20
|
+
*/
|
|
21
|
+
const TYPE_WORDS = {
|
|
22
|
+
TABL: 'table',
|
|
23
|
+
CLAS: 'class',
|
|
24
|
+
INTF: 'interface',
|
|
25
|
+
PROG: 'program',
|
|
26
|
+
FUGR: 'function group',
|
|
27
|
+
DDLS: 'CDS view',
|
|
28
|
+
DDLX: 'metadata extension',
|
|
29
|
+
DCLS: 'access control',
|
|
30
|
+
BDEF: 'behavior definition',
|
|
31
|
+
SRVD: 'service definition',
|
|
32
|
+
SRVB: 'service binding',
|
|
33
|
+
DTEL: 'data element',
|
|
34
|
+
DOMA: 'domain',
|
|
35
|
+
STRU: 'structure',
|
|
36
|
+
TTYP: 'table type',
|
|
37
|
+
MSAG: 'message class',
|
|
38
|
+
DEVC: 'package',
|
|
39
|
+
ENHO: 'enhancement',
|
|
40
|
+
XSLT: 'transformation',
|
|
41
|
+
SHLP: 'search help',
|
|
42
|
+
};
|
|
43
|
+
export function humanVerb(op) {
|
|
44
|
+
return VERBS[op] ?? op.charAt(0).toUpperCase() + op.slice(1);
|
|
45
|
+
}
|
|
46
|
+
export function humanType(type) {
|
|
47
|
+
if (!type)
|
|
48
|
+
return '';
|
|
49
|
+
const key = type.trim().toUpperCase();
|
|
50
|
+
if (TYPE_WORDS[key])
|
|
51
|
+
return TYPE_WORDS[key];
|
|
52
|
+
// Already a word (has a lowercase letter or a space)? use it verbatim.
|
|
53
|
+
if (/[a-z ]/.test(type))
|
|
54
|
+
return type;
|
|
55
|
+
return type.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
/** e.g. "Create table ZMNT_NOTE", "Change class ZCL_X", "Delete table ZOLD". */
|
|
58
|
+
export function verbLine(change) {
|
|
59
|
+
return [humanVerb(change.op), humanType(change.type), change.object]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(' ');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The transport to DISPLAY on the approval surface for one change — mirrors
|
|
65
|
+
* how the write tools actually resolve it (traced in sap-write.ts, 2026-07-05
|
|
66
|
+
* review):
|
|
67
|
+
* - create-family (sap_create_object / set_class_include / create_domain /
|
|
68
|
+
* create_data_element): `args.transport ?? session current` — so the
|
|
69
|
+
* session fallback is honest to show.
|
|
70
|
+
* - modify/delete (sap_set_source / sap_update_method / sap_delete_object):
|
|
71
|
+
* resolveWriteTransport(args only, + owning-TR override) — NO session
|
|
72
|
+
* fallback in the actual write, so we only show a transport the request
|
|
73
|
+
* explicitly carries.
|
|
74
|
+
* - activate/release: the tools take no transport arg at all — never show
|
|
75
|
+
* a Transport segment.
|
|
76
|
+
* When nothing is resolvable, return undefined and the caller OMITS the
|
|
77
|
+
* segment — no "new (session)" promise (a $TMP write never creates one).
|
|
78
|
+
*/
|
|
79
|
+
export function displayTransport(op, explicit, session) {
|
|
80
|
+
switch (op) {
|
|
81
|
+
case 'create':
|
|
82
|
+
return explicit ?? session ?? undefined;
|
|
83
|
+
case 'modify':
|
|
84
|
+
case 'delete':
|
|
85
|
+
return explicit;
|
|
86
|
+
default: // activate / release
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A create/delete "diff" is really a source dump (all-adds, or plain source).
|
|
92
|
+
* Strip unified-diff scaffolding (hunk + file headers) and the leading +/-
|
|
93
|
+
* marker so View source shows clean source — no +/- colouring that would
|
|
94
|
+
* imply a before/after that does not exist for a create. Shared by the Ink
|
|
95
|
+
* modal's [v] view and the classic prompt's View source branch.
|
|
96
|
+
*/
|
|
97
|
+
export function toSourceLines(content) {
|
|
98
|
+
return content
|
|
99
|
+
.split('\n')
|
|
100
|
+
.filter((l) => !l.startsWith('@@') && !l.startsWith('+++') && !l.startsWith('---'))
|
|
101
|
+
.map((l) => l.replace(/^[+-] ?/, ''));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The review key that MATCHES the operation — honesty over a blanket "Show
|
|
105
|
+
* diff":
|
|
106
|
+
* - modify → [d] Show diff (there IS a before/after to diff)
|
|
107
|
+
* - create → [v] View source, only when the new source is carried
|
|
108
|
+
* - delete → [v] View current source, only when the source is carried
|
|
109
|
+
* - activate/release → nothing to review (no source in the payload)
|
|
110
|
+
*/
|
|
111
|
+
export function viewAffordance(change) {
|
|
112
|
+
switch (change.op) {
|
|
113
|
+
case 'modify':
|
|
114
|
+
// Existing behaviour: [d] always offered; the renderer warns when the
|
|
115
|
+
// model supplied a description rather than a real unified diff.
|
|
116
|
+
return { key: 'd', label: 'Show diff' };
|
|
117
|
+
case 'create':
|
|
118
|
+
return change.diff ? { key: 'v', label: 'View source' } : null;
|
|
119
|
+
case 'delete':
|
|
120
|
+
return change.diff ? { key: 'v', label: 'View current source' } : null;
|
|
121
|
+
default:
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/dist/approvals/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { select, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { verbLine, viewAffordance, toSourceLines } from './op-labels.js';
|
|
3
4
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
4
5
|
import { approvalEmitter } from '../ui/approval-emitter.js';
|
|
5
6
|
import { askQuestionEmitter } from '../ui/ask-question-emitter.js';
|
|
@@ -7,26 +8,6 @@ import { withInquirer } from '../repl/inquirer-guard.js';
|
|
|
7
8
|
import { inquirerTheme } from '../repl/inquirer-theme.js';
|
|
8
9
|
import { PEACH } from '../renderer/banners.js';
|
|
9
10
|
const peach = (s) => chalk.hex(PEACH)(s);
|
|
10
|
-
/**
|
|
11
|
-
* Op-row glyph. 2026-05-01: dropped the per-op color spread (yellow M,
|
|
12
|
-
* green A, blue >, magenta ^). Using 5 different hues for routine plan
|
|
13
|
-
* rows trains the user to ignore color as semantic signal — by the time
|
|
14
|
-
* an actual delete shows up, red has already appeared on three other
|
|
15
|
-
* lines and lost its punch. Now: only `D` (delete) gets red, because
|
|
16
|
-
* delete is the only op that's destructive enough to deserve a hot
|
|
17
|
-
* accent. Everything else renders dim — the row layout already tells
|
|
18
|
-
* the user it's a change row.
|
|
19
|
-
*/
|
|
20
|
-
function opIcon(op) {
|
|
21
|
-
switch (op) {
|
|
22
|
-
case 'create': return chalk.dim('A');
|
|
23
|
-
case 'modify': return chalk.dim('M');
|
|
24
|
-
case 'delete': return chalk.red('D');
|
|
25
|
-
case 'activate': return chalk.dim('>');
|
|
26
|
-
case 'release': return chalk.dim('^');
|
|
27
|
-
default: return chalk.dim('?');
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
11
|
/**
|
|
31
12
|
* Risk tag. 2026-05-01: yellow used to fire on `medium` too, which
|
|
32
13
|
* meant most plans had a yellow tag — defeating yellow's purpose as
|
|
@@ -119,12 +100,14 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk)
|
|
|
119
100
|
if (effectiveRisk.raisedBy.length > 0) {
|
|
120
101
|
ctx.push(`(raised from low: ${effectiveRisk.raisedBy.join(', ')})`);
|
|
121
102
|
}
|
|
122
|
-
|
|
123
|
-
|
|
103
|
+
// Numbered, operation-aware verb rows ("1. Create table ZMNT_NOTE") —
|
|
104
|
+
// no cryptic op-code glyph. The Ink modal renders `context` itself, so
|
|
105
|
+
// no chalk here.
|
|
106
|
+
visibleChanges.forEach((c, i) => ctx.push(` ${i + 1}. ${verbLine(c)}`));
|
|
124
107
|
const result = await askQuestionEmitter.request({
|
|
125
108
|
id: 'plan-gate',
|
|
126
109
|
question: 'Apply this plan?',
|
|
127
|
-
// Strip chalk ANSI from
|
|
110
|
+
// Strip chalk ANSI from riskTag — the Ink modal renders `context`
|
|
128
111
|
// itself (dimColor), so embedded escape codes would show as raw text.
|
|
129
112
|
context: ctx.join('\n').replace(/\x1b\[[0-9;]*m/g, ''),
|
|
130
113
|
kind: 'choice',
|
|
@@ -149,9 +132,9 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk)
|
|
|
149
132
|
if (effectiveRisk.raisedBy.length > 0) {
|
|
150
133
|
console.log(` ${chalk.dim(`(raised from low: ${effectiveRisk.raisedBy.join(', ')})`)}`);
|
|
151
134
|
}
|
|
152
|
-
|
|
153
|
-
console.log(` ${
|
|
154
|
-
}
|
|
135
|
+
visibleChanges.forEach((c, i) => {
|
|
136
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${verbLine(c)}`);
|
|
137
|
+
});
|
|
155
138
|
console.log('');
|
|
156
139
|
const choice = await withInquirer(() => select({
|
|
157
140
|
message: 'Apply?',
|
|
@@ -173,17 +156,20 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
173
156
|
// fires standalone (single-change request_approval) AND risk is non-low.
|
|
174
157
|
void effectiveRisk;
|
|
175
158
|
void transport;
|
|
176
|
-
|
|
159
|
+
// Verb-first label ("Create table ZMNT_NOTE — apply?") — no op-code glyph.
|
|
160
|
+
const objLabel = verbLine(change);
|
|
161
|
+
// Op-aware review choice: [d] Show diff for modify, [v] View source for
|
|
162
|
+
// create/delete-with-source, omitted for activate/release.
|
|
163
|
+
const aff = viewAffordance(change);
|
|
164
|
+
const choices = [{ value: 'apply', name: 'Apply' }];
|
|
165
|
+
if (aff)
|
|
166
|
+
choices.push({ value: 'diff', name: aff.label });
|
|
167
|
+
choices.push({ value: 'skip', name: 'Skip (tell model why)' }, { value: 'cancel', name: 'Cancel turn' });
|
|
177
168
|
const choice = await withInquirer(() => select({
|
|
178
169
|
message: `${objLabel} — apply?`,
|
|
179
170
|
default: 'apply',
|
|
180
171
|
theme: inquirerTheme,
|
|
181
|
-
choices
|
|
182
|
-
{ value: 'apply', name: 'Apply' },
|
|
183
|
-
{ value: 'diff', name: 'Show diff' },
|
|
184
|
-
{ value: 'skip', name: 'Skip (tell model why)' },
|
|
185
|
-
{ value: 'cancel', name: 'Cancel turn' },
|
|
186
|
-
],
|
|
172
|
+
choices,
|
|
187
173
|
}));
|
|
188
174
|
if (choice === 'apply')
|
|
189
175
|
return { approved: true };
|
|
@@ -193,9 +179,29 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
193
179
|
const reason = await withInquirer(() => input({ message: 'Why skip?', theme: inquirerTheme }));
|
|
194
180
|
return { approved: false, reason };
|
|
195
181
|
}
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
182
|
+
// Review choice.
|
|
183
|
+
//
|
|
184
|
+
// I3 (2026-07-05 review): a 'v' affordance (create/delete View source) is
|
|
185
|
+
// NOT a diff — the payload is plain/all-adds source. Routing it through the
|
|
186
|
+
// diff printer fired the "⚠ Model did not provide a unified diff" warning
|
|
187
|
+
// under a View source label. Print numbered plain source instead — never
|
|
188
|
+
// the diff path or its warning. (The affordance is only offered when
|
|
189
|
+
// change.diff is present, so the fallback line is belt-and-braces.)
|
|
190
|
+
if (aff?.key === 'v') {
|
|
191
|
+
if (change.diff) {
|
|
192
|
+
const numbered = toSourceLines(change.diff)
|
|
193
|
+
.map((line, i) => `${chalk.dim(String(i + 1).padStart(4) + ' ')}${line}`)
|
|
194
|
+
.join('\n');
|
|
195
|
+
console.log('\n' + numbered + '\n');
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(chalk.dim('(No source provided by the model)'));
|
|
199
|
+
}
|
|
200
|
+
return renderPerChangeApproval(change, effectiveRisk, transport);
|
|
201
|
+
}
|
|
202
|
+
// 'd' (modify) — print colorized + lines (green) / - lines (red) / hunk
|
|
203
|
+
// headers (peach) so the user can scan the change at a glance instead of
|
|
204
|
+
// squinting at flat-gray text.
|
|
199
205
|
//
|
|
200
206
|
// 2026-05-01: detect "model sent a description, not a diff" — when the
|
|
201
207
|
// payload has no +/-/@ lines and no `--- /+++ ` headers, colorizeDiff
|
package/dist/cli.js
CHANGED
|
@@ -62,6 +62,20 @@ export async function main(argv) {
|
|
|
62
62
|
}
|
|
63
63
|
return 0;
|
|
64
64
|
}
|
|
65
|
+
if (args[0] === 'config' && args[1] === 'unset') {
|
|
66
|
+
const { runConfigUnset, ConfigSetError } = await import('./commands/config-set.js');
|
|
67
|
+
try {
|
|
68
|
+
await runConfigUnset(args.slice(2));
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (e instanceof ConfigSetError) {
|
|
72
|
+
console.error(chalk.red(`✗ ${e.message}`));
|
|
73
|
+
process.exit(e.exitCode);
|
|
74
|
+
}
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
65
79
|
if (args[0] === 'config' && args[1] === 'show') {
|
|
66
80
|
const { runConfigShow } = await import('./commands/config-show.js');
|
|
67
81
|
await runConfigShow(args.slice(2));
|
|
@@ -134,6 +148,7 @@ Usage:
|
|
|
134
148
|
cspeach config add-sap Add a SAP system interactively
|
|
135
149
|
cspeach configure Alias for \`cspeach config add-sap\`
|
|
136
150
|
cspeach config set <key> <value> Update a config key
|
|
151
|
+
cspeach config unset <key> Remove a config key (revert to default)
|
|
137
152
|
cspeach config show [key] Show config (full or single key)
|
|
138
153
|
cspeach doctor Diagnose install + connectivity
|
|
139
154
|
cspeach login Sign in via device-code flow
|
package/dist/commands/compact.js
CHANGED
|
@@ -161,6 +161,24 @@ export async function backupBeforeCompact(session) {
|
|
|
161
161
|
await fs.writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8');
|
|
162
162
|
return filePath;
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* UX Wave 2 / Task 4 — render session.todos as a plain-text,
|
|
166
|
+
* status-prefixed block for the compacted context. Returns '' when there
|
|
167
|
+
* is no list (undefined or empty) so callers can append unconditionally.
|
|
168
|
+
*
|
|
169
|
+
* Format (explicit status words, not glyphs — the reader is the NEXT
|
|
170
|
+
* turn's model, and `[in_progress]` is unambiguous where `[>]` isn't):
|
|
171
|
+
*
|
|
172
|
+
* Current task list:
|
|
173
|
+
* - [completed] Create table ZORDER
|
|
174
|
+
* - [in_progress] Write behavior definition
|
|
175
|
+
* - [pending] Publish service binding
|
|
176
|
+
*/
|
|
177
|
+
export function renderTodoSection(todos) {
|
|
178
|
+
if (!todos || todos.length === 0)
|
|
179
|
+
return '';
|
|
180
|
+
return ['', 'Current task list:', ...todos.map((t) => `- [${t.status}] ${t.text}`)].join('\n');
|
|
181
|
+
}
|
|
164
182
|
/**
|
|
165
183
|
* The compacted "synthesised earlier history" message that replaces the
|
|
166
184
|
* summarised slice in session.messages. Role=user because (a) Anthropic
|
|
@@ -168,9 +186,16 @@ export async function backupBeforeCompact(session) {
|
|
|
168
186
|
* makes it visually distinct from the model's prior assistant outputs, and
|
|
169
187
|
* (c) the explicit "context compacted by /compact" framing tells the next
|
|
170
188
|
* turn's model exactly what it's looking at.
|
|
189
|
+
*
|
|
190
|
+
* UX Wave 2 / Task 4: when `todos` is provided (runCompact passes
|
|
191
|
+
* session.todos), a `Current task list:` section is appended after the
|
|
192
|
+
* summary so the model retains its plan across /compact — the summarised
|
|
193
|
+
* turns are where the list was built, and Haiku isn't trusted to
|
|
194
|
+
* reproduce it verbatim.
|
|
171
195
|
*/
|
|
172
|
-
export function buildSummaryMessage(summary, droppedTurns) {
|
|
196
|
+
export function buildSummaryMessage(summary, droppedTurns, todos) {
|
|
173
197
|
const ts = new Date().toISOString();
|
|
198
|
+
const todoSection = renderTodoSection(todos);
|
|
174
199
|
return {
|
|
175
200
|
role: 'user',
|
|
176
201
|
content: [
|
|
@@ -180,6 +205,7 @@ export function buildSummaryMessage(summary, droppedTurns) {
|
|
|
180
205
|
`to ~/.cspeach/sessions/${'<session-id>'}.pre-compact-<timestamp>.json]`,
|
|
181
206
|
'',
|
|
182
207
|
summary,
|
|
208
|
+
...(todoSection ? [todoSection] : []),
|
|
183
209
|
].join('\n'),
|
|
184
210
|
};
|
|
185
211
|
}
|
|
@@ -228,7 +254,7 @@ export async function runCompact(opts) {
|
|
|
228
254
|
// which it will be unless the session is very long. (For very long sessions
|
|
229
255
|
// we accept losing the original first prompt because the summary should
|
|
230
256
|
// mention what the goal was.)
|
|
231
|
-
const summaryMsg = buildSummaryMessage(summary, plan.toSummarise.length);
|
|
257
|
+
const summaryMsg = buildSummaryMessage(summary, plan.toSummarise.length, session.todos);
|
|
232
258
|
session.messages = [summaryMsg, ...plan.toKeep];
|
|
233
259
|
await saveSession(session);
|
|
234
260
|
return {
|