@cspeach/cli 1.1.51 → 1.1.53
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/chat-prompt.js +4 -1
- package/dist/agent/conversation-prompt.js +31 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +304 -49
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +21 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +25 -9
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +20 -3
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +22 -10
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +10 -2
- package/dist/commands/compact.js +6 -1
- package/dist/commands/config-set.js +13 -0
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-conversation.js +7 -4
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +113 -3
- package/dist/commands/plan-startup.js +3 -3
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/docx/doc-meta.js +1 -0
- package/dist/docx/markdown-to-docx.js +2 -0
- package/dist/lane/app-chain.js +13 -6
- package/dist/models/server-config.js +1 -1
- package/dist/one-shot.js +1 -0
- package/dist/project-context/conventions.js +8 -1
- package/dist/project-context/index-files.js +97 -34
- package/dist/project-context/index.js +1 -1
- package/dist/project-context/render.js +38 -4
- package/dist/projects/answer-blockers.js +107 -16
- package/dist/projects/blocker-answers-note.js +89 -0
- package/dist/projects/change-options.js +70 -0
- package/dist/projects/document-save.js +86 -0
- package/dist/projects/extract-design.js +4 -0
- package/dist/projects/extract-estimate.js +4 -0
- package/dist/projects/extract-spec-gap.js +8 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-objects.js +24 -0
- package/dist/projects/plan-run.js +12 -2
- package/dist/projects/save-command.js +50 -18
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +17 -1
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +5 -1
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/chat-lane.js +15 -3
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +155 -24
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +12 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +49 -8
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/project/spec_answers_record.js +190 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +18 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +13 -7
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/ask-card.js +171 -29
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readdirSync } from 'node:fs';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
|
-
import { extractSpecGap } from './extract-spec-gap.js';
|
|
4
|
-
import { extractDesign } from './extract-design.js';
|
|
5
|
-
import { extractEstimate } from './extract-estimate.js';
|
|
3
|
+
import { extractSpecGap, hasSpecGapSign } from './extract-spec-gap.js';
|
|
4
|
+
import { extractDesign, hasDesignSign } from './extract-design.js';
|
|
5
|
+
import { extractEstimate, hasEstimateSign } from './extract-estimate.js';
|
|
6
6
|
import { extractUpgradeScan, extractUpgradeProgress, extractUpgradeReport } from './extract-upgrade.js';
|
|
7
7
|
import { extractCcaAssessment } from './extract-cca.js';
|
|
8
8
|
import { extractModernizeResult } from './extract-modernize.js';
|
|
@@ -29,16 +29,22 @@ const SKILL_REGISTRY = {
|
|
|
29
29
|
artefactType: 'spec-gap',
|
|
30
30
|
extract: (md) => extractSpecGap(md),
|
|
31
31
|
itemNoun: 'questions',
|
|
32
|
+
hasDocumentSign: hasSpecGapSign,
|
|
33
|
+
documentNoun: 'a spec-gap document',
|
|
32
34
|
},
|
|
33
35
|
'abap-design': {
|
|
34
36
|
artefactType: 'design',
|
|
35
37
|
extract: (md) => extractDesign(md),
|
|
36
38
|
itemNoun: 'objects',
|
|
39
|
+
hasDocumentSign: hasDesignSign,
|
|
40
|
+
documentNoun: 'a design document',
|
|
37
41
|
},
|
|
38
42
|
'abap-estimate': {
|
|
39
43
|
artefactType: 'estimate',
|
|
40
44
|
extract: (md) => extractEstimate(md),
|
|
41
45
|
itemNoun: 'components',
|
|
46
|
+
hasDocumentSign: hasEstimateSign,
|
|
47
|
+
documentNoun: 'an estimate document',
|
|
42
48
|
},
|
|
43
49
|
'abap-upgrade-scan': {
|
|
44
50
|
artefactType: 'upgrade-scan',
|
|
@@ -231,6 +237,18 @@ export function resolvePriorPlanEnvelope(title, dir, opts = {}) {
|
|
|
231
237
|
opts.log?.(`[save] More than one plan is titled '${pick.envelope.title}'; revising the newest: ${basename(pick.path)}`);
|
|
232
238
|
return pick;
|
|
233
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Task 17 (F-N) — the person's message asks us to save the document:
|
|
242
|
+
* "save this/it/that", "save the spec-gap/design/estimate/document",
|
|
243
|
+
* "can you save", "please save". Ordinary wording about saving in the app
|
|
244
|
+
* ("validation on save", "the user clicks Save", "saving changes") is not.
|
|
245
|
+
*/
|
|
246
|
+
const ASKED_TO_SAVE_RE = new RegExp([
|
|
247
|
+
String.raw `\b(?:can|could|would|will)\s+you\s+(?:please\s+)?save\b`,
|
|
248
|
+
String.raw `\bplease\s+save\b`,
|
|
249
|
+
String.raw `\bsave\s+(?:this|it|that|them)\b`,
|
|
250
|
+
String.raw `\bsave\s+(?:the|my|our)\s+(?:spec[\s-]*gaps?|design|estimate|document|doc|result|analysis|file|project\s+file)\b`,
|
|
251
|
+
].join('|'), 'i');
|
|
234
252
|
export async function runSaveCommand(args) {
|
|
235
253
|
const spec = SKILL_REGISTRY[args.skillName];
|
|
236
254
|
if (!spec)
|
|
@@ -323,22 +341,31 @@ export async function runSaveCommand(args) {
|
|
|
323
341
|
return runSaveCommand({ ...args, skillName: detected });
|
|
324
342
|
}
|
|
325
343
|
}
|
|
326
|
-
if (spec.manifestMarker
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
344
|
+
if (spec.manifestMarker) {
|
|
345
|
+
// Task 17 fix round 1 (F-N): the manifest skills send every turn here.
|
|
346
|
+
// A turn with no manifest block is a conversational turn, not a failed
|
|
347
|
+
// save: say nothing (the B5 contract on manifestMarker above). A block
|
|
348
|
+
// that is present but fails to parse keeps its loud lines.
|
|
349
|
+
if (args.skillOutput.includes(spec.manifestMarker)) {
|
|
350
|
+
args.log('');
|
|
351
|
+
args.log(`[save] ${args.skillName}: a ${spec.manifestMarker} block IS present but failed to parse — artefact NOT saved.`);
|
|
352
|
+
args.log(`[save] ${e instanceof Error ? e.message : String(e)}`);
|
|
353
|
+
args.log(`[save] The output above is otherwise complete — re-run the skill or report this as a manifest bug.`);
|
|
354
|
+
}
|
|
331
355
|
}
|
|
332
|
-
else {
|
|
333
|
-
// 2026-
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
356
|
+
else if (spec.hasDocumentSign && spec.documentNoun) {
|
|
357
|
+
// Task 17 (F-N, 2026-10-26) — the save step runs on every turn of the
|
|
358
|
+
// session skill. A turn with no sign of the document (a recap after a
|
|
359
|
+
// choice card) is not a failed save: say nothing, unless the person
|
|
360
|
+
// asked to save. A text that does look like the document gets one
|
|
361
|
+
// plain line.
|
|
362
|
+
if (spec.hasDocumentSign(args.skillOutput) || ASKED_TO_SAVE_RE.test(args.skillInput ?? '')) {
|
|
363
|
+
args.log('');
|
|
364
|
+
args.log(`We couldn't save this as ${spec.documentNoun}. Ask us to write it again in the usual layout.`);
|
|
365
|
+
}
|
|
341
366
|
}
|
|
367
|
+
// (2026-09-17's "[save] … could not read the output" lines are gone: every
|
|
368
|
+
// registry skill now has a manifest marker or a document sign above.)
|
|
342
369
|
args.onNotSaved?.('unreadable');
|
|
343
370
|
return null;
|
|
344
371
|
}
|
|
@@ -398,7 +425,12 @@ export async function runSaveCommand(args) {
|
|
|
398
425
|
// maybeOfferSave never does headless — so the blanket 'y' never reaches it.)
|
|
399
426
|
// Inside a journey a triage is saved without the question: the journey needs the rows.
|
|
400
427
|
const journey = spec.artefactType === 'clean-core-decision' ? args.journey : undefined;
|
|
401
|
-
const answer = journey ? 'y' : await askYesNo({
|
|
428
|
+
const answer = journey ? 'y' : await askYesNo({
|
|
429
|
+
question: 'Save as project file?', confirm: args.confirm, prompt: args.prompt,
|
|
430
|
+
// F-A (live check 2026-10-26) — a plan is only built from its saved file.
|
|
431
|
+
// Task 20 (owner 2026-10-26) — the spec-gap document defaults to Yes too.
|
|
432
|
+
...(spec.artefactType === 'plan' || spec.artefactType === 'spec-gap' ? { defaultYes: true } : {}),
|
|
433
|
+
});
|
|
402
434
|
if (!isYes(answer)) {
|
|
403
435
|
args.onNotSaved?.('declined');
|
|
404
436
|
return null;
|
package/dist/projects/yes-no.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export async function askYesNo(a) {
|
|
2
2
|
if (a.confirm)
|
|
3
|
-
return a.confirm(a.question, { default: 'n' });
|
|
3
|
+
return a.confirm(a.question, { default: a.defaultYes ? 'y' : 'n' });
|
|
4
4
|
if (!a.prompt)
|
|
5
5
|
return '';
|
|
6
|
+
if (a.defaultYes) {
|
|
7
|
+
const reply = (await a.prompt(`${a.question} [Y/n]: `)).trim().toLowerCase();
|
|
8
|
+
return reply === '' ? 'y' : reply;
|
|
9
|
+
}
|
|
6
10
|
return (await a.prompt(`${a.question} [y/N]: `)).trim().toLowerCase();
|
|
7
11
|
}
|
|
8
12
|
/** Today's decision rule at all six sites. */
|
|
@@ -14,7 +14,9 @@ const DIFF_LINES = 6;
|
|
|
14
14
|
/** Myers diff is O(N·D) and synchronous: past this budget we show no diff
|
|
15
15
|
* at all (object + footer only), never a partial one. */
|
|
16
16
|
const DIFF_TIMEOUT_MS = 200;
|
|
17
|
-
|
|
17
|
+
// LT-7: a last line that only gains a newline (because lines follow it) is
|
|
18
|
+
// the same line, never a -/+ pair.
|
|
19
|
+
const defaultDiffer = (a, b) => diffLines(a, b, { timeout: DIFF_TIMEOUT_MS, ignoreNewlineAtEof: true });
|
|
18
20
|
// eslint-disable-next-line no-control-regex
|
|
19
21
|
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
20
22
|
const visibleLen = (s) => s.replace(ANSI, '').length;
|
|
@@ -101,6 +103,20 @@ export function diffBody(card, differ = defaultDiffer) {
|
|
|
101
103
|
for (const l of part.value.replace(/\r?\n$/, '').split(/\r?\n/))
|
|
102
104
|
changed.push(`${part.added ? '+' : '-'} ${clean(l)}`);
|
|
103
105
|
}
|
|
106
|
+
// 1.1.53 (live F-E): the first write after a create replaces SAP's template,
|
|
107
|
+
// whose removed lines are only comments. Myers puts those first and hides the
|
|
108
|
+
// new code, so in that case the added code leads, then added comment/blank
|
|
109
|
+
// lines, then the removals. Any removal that carries code keeps Myers order.
|
|
110
|
+
const isCommentOrBlank = (l) => {
|
|
111
|
+
const code = l.slice(2);
|
|
112
|
+
return code.trim() === '' || code.startsWith('*') || /^\s*"/.test(code);
|
|
113
|
+
};
|
|
114
|
+
const removed = changed.filter((l) => l.startsWith('-'));
|
|
115
|
+
const added = changed.filter((l) => l.startsWith('+'));
|
|
116
|
+
const addedCode = added.filter((l) => !isCommentOrBlank(l));
|
|
117
|
+
if (removed.length > 0 && removed.every(isCommentOrBlank) && addedCode.length > 0) {
|
|
118
|
+
changed.splice(0, changed.length, ...addedCode, ...added.filter(isCommentOrBlank), ...removed);
|
|
119
|
+
}
|
|
104
120
|
return { lines: changed.slice(0, DIFF_LINES), hidden: Math.max(0, changed.length - DIFF_LINES) };
|
|
105
121
|
}
|
|
106
122
|
export function buildChangeCard(input, width, box = getBoxChars()) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.1.53 item 5 — the model sometimes writes `<` / `>` / `&` in the
|
|
3
|
+
* text and diffs it puts on an approval card. Cards show the real characters.
|
|
4
|
+
* One pass only: `&lt;` becomes `<`, never `<`.
|
|
5
|
+
*
|
|
6
|
+
* Display only. Never apply this to anything written to SAP or saved.
|
|
7
|
+
*/
|
|
8
|
+
const ENTITY = /&(lt|gt|amp);/g;
|
|
9
|
+
const CHAR = { lt: '<', gt: '>', amp: '&' };
|
|
10
|
+
export function decodeDisplayEntities(s) {
|
|
11
|
+
return s.replace(ENTITY, (_m, name) => CHAR[name]);
|
|
12
|
+
}
|
|
13
|
+
/** A raw field symbol or raw markup: `<` followed by a letter, `_`, `/` or `!`. */
|
|
14
|
+
const RAW_TAG = /<[A-Za-z_/!]/;
|
|
15
|
+
/**
|
|
16
|
+
* The same decode for a whole source or diff block, except when the block
|
|
17
|
+
* holds a raw field symbol or raw markup (`<fs>`, `<b>`, `</p>`, `<!--`).
|
|
18
|
+
* A model that escapes field symbols escapes them throughout; real source
|
|
19
|
+
* that holds `<` in a literal almost always has a raw `<fs>` or markup
|
|
20
|
+
* too, so it stays as written. Operators and comparisons (`->`, `=>`, `>=`,
|
|
21
|
+
* `<=`, `<>`, ` < `) do not count. Display only, like decodeDisplayEntities.
|
|
22
|
+
*/
|
|
23
|
+
export function decodeDisplayBlock(s) {
|
|
24
|
+
return RAW_TAG.test(s) ? s : decodeDisplayEntities(s);
|
|
25
|
+
}
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
// at the START of a line. It ends at a `-->` on the opener's own line, or
|
|
17
17
|
// else at a `-->` that starts a later line (one newline straight after it is
|
|
18
18
|
// dropped too). An inline mention mid-line is prose and stays.
|
|
19
|
+
// 1.1.53: a line that starts with a closing marker (`<!-- /cspeach:…` or
|
|
20
|
+
// `<!-- /csforge:…`) is a block like an opener. A one-line change options
|
|
21
|
+
// marker (`<!-- csforge:change-options OBJECT=X -->`, `<!-- /csforge:change-options -->`)
|
|
22
|
+
// is removed wherever it sits on a line; the text around it stays. Other
|
|
23
|
+
// mid-line mentions stay prose, and so does a mid-line marker whose `-->` is
|
|
24
|
+
// not on the same line.
|
|
19
25
|
// A chunk boundary can fall anywhere: a line that could still become an
|
|
20
26
|
// opener is held until it can be decided; inside a block a tail that could
|
|
21
27
|
// still become the closing `-->` is held the same way.
|
|
@@ -24,8 +30,10 @@ export function initialMachineBlockState() {
|
|
|
24
30
|
}
|
|
25
31
|
// A leading `\n` marks a line start (the probe prepends one when the text
|
|
26
32
|
// begins at a line start) — no `^`, which would match a mid-line chunk start.
|
|
27
|
-
const OPENER_RE = /\n[ \t]*<!--[ \t]
|
|
28
|
-
const PREFIXES = ['cspeach:', 'csforge:'];
|
|
33
|
+
const OPENER_RE = /\n[ \t]*<!--[ \t]*\/?(?:cspeach|csforge):/;
|
|
34
|
+
const PREFIXES = ['cspeach:', 'csforge:', '/cspeach:', '/csforge:'];
|
|
35
|
+
const INLINE_MARKER_RE = /<!--[ \t]*\/?(?:cspeach|csforge):change-options\b[^\n]*?-->/g;
|
|
36
|
+
const INLINE_PREFIXES = ['cspeach:change-options', 'csforge:change-options'];
|
|
29
37
|
const CLOSER_RE = /\n[ \t]*-->/;
|
|
30
38
|
const PARTIAL_CLOSER_RE = /^\n[ \t]*-{0,2}$/;
|
|
31
39
|
/** Could `frag` (one incomplete line) still grow into a block opener? */
|
|
@@ -38,6 +46,27 @@ function couldBeOpener(frag) {
|
|
|
38
46
|
const r = s.slice(4).replace(/^[ \t]*/, '');
|
|
39
47
|
return PREFIXES.some((p) => p.startsWith(r));
|
|
40
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Could `frag` (text from a mid-line `<`, no newline yet) still grow into a
|
|
51
|
+
* one-line change options marker? A full prefix stays held until its `-->`
|
|
52
|
+
* or the end of the line decides it.
|
|
53
|
+
*/
|
|
54
|
+
function couldBeInlineMarker(frag) {
|
|
55
|
+
if (frag.includes('-->') || frag.includes('\n'))
|
|
56
|
+
return false;
|
|
57
|
+
if (frag.length <= 4)
|
|
58
|
+
return '<!--'.startsWith(frag);
|
|
59
|
+
if (!frag.startsWith('<!--'))
|
|
60
|
+
return false;
|
|
61
|
+
let r = frag.slice(4).replace(/^[ \t]*/, '');
|
|
62
|
+
if (r.startsWith('/'))
|
|
63
|
+
r = r.slice(1);
|
|
64
|
+
return INLINE_PREFIXES.some((p) => p.startsWith(r) || r.startsWith(p));
|
|
65
|
+
}
|
|
66
|
+
/** Remove every complete one-line change options marker. */
|
|
67
|
+
function stripInlineMarkers(text) {
|
|
68
|
+
return text.replace(INLINE_MARKER_RE, '');
|
|
69
|
+
}
|
|
41
70
|
export function filterMachineBlocks(chunk, prev) {
|
|
42
71
|
const st = { ...prev };
|
|
43
72
|
let work = st.held + chunk;
|
|
@@ -103,7 +132,7 @@ export function filterMachineBlocks(chunk, prev) {
|
|
|
103
132
|
if (m) {
|
|
104
133
|
const offset = st.lineStart ? 1 : 0;
|
|
105
134
|
const lineBegin = m.index + 1 - offset; // index in `work`, just after the newline
|
|
106
|
-
output += work.slice(0, lineBegin);
|
|
135
|
+
output += stripInlineMarkers(work.slice(0, lineBegin));
|
|
107
136
|
work = work.slice(m.index + m[0].length - offset);
|
|
108
137
|
st.inBlock = true;
|
|
109
138
|
st.firstLine = true;
|
|
@@ -113,14 +142,32 @@ export function filterMachineBlocks(chunk, prev) {
|
|
|
113
142
|
const lastNl = work.lastIndexOf('\n');
|
|
114
143
|
const fragStart = lastNl >= 0 ? lastNl + 1 : st.lineStart ? 0 : -1;
|
|
115
144
|
if (fragStart >= 0 && fragStart < work.length && couldBeOpener(work.slice(fragStart))) {
|
|
116
|
-
output += work.slice(0, fragStart);
|
|
145
|
+
output += stripInlineMarkers(work.slice(0, fragStart));
|
|
117
146
|
st.held = work.slice(fragStart);
|
|
118
147
|
st.lineStart = true;
|
|
119
148
|
}
|
|
120
149
|
else {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
// Remove complete one-line markers; hold a mid-line tail that could
|
|
151
|
+
// still become one until its `-->` or the newline decides it.
|
|
152
|
+
const shown = stripInlineMarkers(work);
|
|
153
|
+
const tailStart = shown.lastIndexOf('\n') + 1;
|
|
154
|
+
let holdAt = -1;
|
|
155
|
+
for (let i = shown.indexOf('<', tailStart); i >= 0; i = shown.indexOf('<', i + 1)) {
|
|
156
|
+
if (couldBeInlineMarker(shown.slice(i))) {
|
|
157
|
+
holdAt = i;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (holdAt >= 0) {
|
|
162
|
+
output += shown.slice(0, holdAt);
|
|
163
|
+
st.held = shown.slice(holdAt);
|
|
164
|
+
st.lineStart = holdAt === 0 ? st.lineStart : shown[holdAt - 1] === '\n';
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
output += shown;
|
|
168
|
+
if (shown.length > 0)
|
|
169
|
+
st.lineStart = shown.endsWith('\n');
|
|
170
|
+
}
|
|
124
171
|
}
|
|
125
172
|
break;
|
|
126
173
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/renderer/steering-echo.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// How a mid-turn message is shown (spec §7). LT-15 Feature 1 (D5): typing it
|
|
4
|
+
// leaves no scrollback receipt any more; the App's live queue row shows it
|
|
5
|
+
// while it waits, and the loop prints "↳ read · …" when the model reads it.
|
|
6
6
|
import { paint } from './theme.js';
|
|
7
7
|
import { sanitizeControlChars } from './sanitize.js';
|
|
8
8
|
/** Wave1 fast-follow — only the first line, clipped, with a line count. */
|
|
@@ -20,12 +20,57 @@ export function formatSteeringEcho(text) {
|
|
|
20
20
|
return out;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* One waiting or read message as shown: first line, clipped, no control
|
|
24
|
+
* characters, and each tab as one space (a tab is 0 wide to string-width, so
|
|
25
|
+
* Ink's truncate-end would cut too late and the line could wrap).
|
|
26
26
|
*/
|
|
27
|
-
|
|
28
|
-
return
|
|
27
|
+
function shownSteer(text) {
|
|
28
|
+
return sanitizeControlChars(formatSteeringEcho(text)).replace(/\t/g, ' ');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* LT-15 Feature 1 (D5, F7) — the receipt the loop prints when the model reads
|
|
32
|
+
* a waiting message: `↳ read · <first line…>` in the dim role, one line per
|
|
33
|
+
* message (the caller adds the newline). The text may come from a paste, so
|
|
34
|
+
* it is clipped and control characters are neutralised.
|
|
35
|
+
*/
|
|
36
|
+
export function steeringReadLine(text) {
|
|
37
|
+
return paint('dim', `↳ read · ${shownSteer(text)}`);
|
|
38
|
+
}
|
|
39
|
+
/** The queue row shows at most this many messages; the rest are counted. */
|
|
40
|
+
export const QUEUE_ROW_MAX_ITEMS = 3;
|
|
41
|
+
/**
|
|
42
|
+
* LT-15 Feature 1 — the live row's lines (plain text, one terminal row each;
|
|
43
|
+
* App paints them dim and truncates to the width). Empty queue → no lines.
|
|
44
|
+
*
|
|
45
|
+
* 4 waiting · use ZPKG
|
|
46
|
+
* · also check the tests… (+1 line)
|
|
47
|
+
* · and the docs
|
|
48
|
+
* + 1 more
|
|
49
|
+
*
|
|
50
|
+
* `maxLines` is the room left in the live area. When the full row does not
|
|
51
|
+
* fit it folds to ONE line, never fewer: `4 waiting · use ZPKG + 3 more`.
|
|
52
|
+
*
|
|
53
|
+
* LT-15 Feature 2 (review I6) — `canEdit` adds `· ↑ to edit` right after the
|
|
54
|
+
* count, only while ↑ would really take the last message back (the App
|
|
55
|
+
* decides). It sits before the messages so a long message never cuts it off.
|
|
56
|
+
* Without it the bytes are exactly the Task 2 ones.
|
|
57
|
+
*/
|
|
58
|
+
export function formatQueueRow(items, maxLines = Infinity, opts = {}) {
|
|
59
|
+
if (items.length === 0)
|
|
60
|
+
return [];
|
|
61
|
+
const count = `${items.length} waiting${opts.canEdit ? ' · ↑ to edit' : ''}`;
|
|
62
|
+
const full = Math.min(items.length, QUEUE_ROW_MAX_ITEMS) + (items.length > QUEUE_ROW_MAX_ITEMS ? 1 : 0);
|
|
63
|
+
if (full > Math.max(1, maxLines)) {
|
|
64
|
+
return [`${count} · ${shownSteer(items[0])} + ${items.length - 1} more`];
|
|
65
|
+
}
|
|
66
|
+
const head = `${count} `;
|
|
67
|
+
const pad = ' '.repeat(head.length);
|
|
68
|
+
const shown = items.slice(0, QUEUE_ROW_MAX_ITEMS);
|
|
69
|
+
const lines = shown.map((t, i) => `${i === 0 ? head : pad}· ${shownSteer(t)}`);
|
|
70
|
+
const rest = items.length - shown.length;
|
|
71
|
+
if (rest > 0)
|
|
72
|
+
lines.push(`${pad}+ ${rest} more`);
|
|
73
|
+
return lines;
|
|
29
74
|
}
|
|
30
75
|
export function classifyMidTurnLine(text) {
|
|
31
76
|
const first = text.trim().split(/\s+/)[0] ?? '';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spillLabelFor } from '../lib/spill-labels.js';
|
|
2
|
+
import { writtenObjectName } from '../approvals/op-labels.js';
|
|
2
3
|
const OBJECT_MAX = 60;
|
|
3
4
|
/**
|
|
4
5
|
* Every value shown in a label goes through here: ANSI escape sequences and
|
|
@@ -108,7 +109,8 @@ const RULES = {
|
|
|
108
109
|
},
|
|
109
110
|
background_run: (a) => ({ verb: 'Started', object: shortRight(commandLine(a)) }),
|
|
110
111
|
sap_get_source: sapObject('Read'),
|
|
111
|
-
|
|
112
|
+
// LT-11 — an access control, metadata extension or behavior definition names its kind.
|
|
113
|
+
sap_set_source: (a) => ({ verb: 'Wrote', object: shortRight(writtenObjectName(str(a['type']), str(a['name']) ?? '')) }),
|
|
112
114
|
sap_activate: (a) => ({ verb: 'Activated', object: shortRight(activateObject(a)) }),
|
|
113
115
|
sap_syntax_check: sapObject('Checked syntax'),
|
|
114
116
|
sap_atc_run: sapObject('Ran ATC on'),
|
|
@@ -166,6 +168,7 @@ const RULES = {
|
|
|
166
168
|
team_findings: (a) => ({ verb: 'Read team findings', object: str(a['lane']) ?? str(a['packages']) ?? 'all objects' }),
|
|
167
169
|
rules_propose: () => ({ verb: 'Check a proposed decision rule', object: 'nothing is saved' }),
|
|
168
170
|
team_estate: (a) => ({ verb: 'Read estate inventory', object: str(a['packages']) ?? 'all packages' }),
|
|
171
|
+
spec_answers_record: (a) => ({ verb: 'Recorded answers in', object: shortLeft(str(a['path']) ?? '') }),
|
|
169
172
|
team_lane_move: (a) => ({ verb: 'Move packages to another person', object: shortRight(Array.isArray(a['packages']) ? a['packages'].map(String).join(', ') : 'packages') }),
|
|
170
173
|
sap_badi_list: (a) => ({ verb: 'Searched BAdIs', object: shortRight(`"${str(a['query']) ?? ''}"`) }),
|
|
171
174
|
sap_badi_read: (a) => ({ verb: 'Read BAdI', object: shortRight([str(a['kind']), str(a['name'])].filter(Boolean).join(' ')) }),
|
|
@@ -287,6 +290,7 @@ const PRESENT = {
|
|
|
287
290
|
'Audited metadata': 'Auditing metadata', 'Extended model': 'Extending model',
|
|
288
291
|
'Checked capability': 'Checking capability',
|
|
289
292
|
'Read texts of': 'Reading texts of', 'Read team findings': 'Reading team findings', 'Check a proposed decision rule': 'Checking a proposed decision rule', 'Read estate inventory': 'Reading estate inventory', 'Move packages to another person': 'Moving packages to another person', 'Wrote texts': 'Writing texts',
|
|
293
|
+
'Recorded answers in': 'Recording answers in',
|
|
290
294
|
};
|
|
291
295
|
export function toolActivityLabel(name, args) {
|
|
292
296
|
const l = toolLabel(name, args);
|
|
@@ -6,6 +6,16 @@ import { toolLabel, toolActivityLabel, foldOutput, clean } from './tool-labels.j
|
|
|
6
6
|
import { getEffectiveBodyWidth } from './tty.js';
|
|
7
7
|
import { flowFor, stdoutFlow, writeStdout, canAnimateInPlace } from './transcript-flow.js';
|
|
8
8
|
import { humanErrorDetail } from './error-detail.js';
|
|
9
|
+
import { STOPPED_BEFORE_START_RESULT } from '../agent/repair-partial.js';
|
|
10
|
+
/**
|
|
11
|
+
* 1.1.53 item 10 — a call stopped before it started (Esc / Ctrl+C) changed
|
|
12
|
+
* nothing: the row is a neutral dim stop, not a red failure. The model still
|
|
13
|
+
* receives STOPPED_BEFORE_START_RESULT unchanged; this is display only.
|
|
14
|
+
*/
|
|
15
|
+
const STOPPED_BEFORE_START_LINE = 'Stopped before it started. Nothing changed in SAP.';
|
|
16
|
+
function stoppedBeforeStart(isError, content) {
|
|
17
|
+
return isError && content === STOPPED_BEFORE_START_RESULT;
|
|
18
|
+
}
|
|
9
19
|
export { canAnimateInPlace };
|
|
10
20
|
/**
|
|
11
21
|
* Tool-dispatch renderer — CC-style two-line format with peach spinner.
|
|
@@ -316,7 +326,8 @@ export function renderToolCallBottom(params) {
|
|
|
316
326
|
write(panelsRow({ name: name ?? '', args: args ?? {}, isError, resultSummary, resultContent, durationMs }));
|
|
317
327
|
return;
|
|
318
328
|
}
|
|
319
|
-
const
|
|
329
|
+
const stopped = stoppedBeforeStart(isError, resultContent);
|
|
330
|
+
const icon = stopped ? chalk.dim('·') : isError ? chalk.red('✗') : chalk.green('✓');
|
|
320
331
|
// Label: plain (not bold — the top line keeps visual primacy) name +
|
|
321
332
|
// dim arg summary, mirroring the top-line shape so orphaned rows still
|
|
322
333
|
// read as "which tool, on what object".
|
|
@@ -326,9 +337,9 @@ export function renderToolCallBottom(params) {
|
|
|
326
337
|
const wrapped = summary ? chalk.gray('(') + chalk.gray(summary) + chalk.gray(')') : '';
|
|
327
338
|
label = ` ${name}${wrapped}`;
|
|
328
339
|
}
|
|
329
|
-
const detailRaw = resultSummary ? truncate(resultSummary, RESULT_SUMMARY_MAX) : '';
|
|
340
|
+
const detailRaw = stopped ? STOPPED_BEFORE_START_LINE : resultSummary ? truncate(resultSummary, RESULT_SUMMARY_MAX) : '';
|
|
330
341
|
const detail = detailRaw
|
|
331
|
-
? (isError ? chalk.red(detailRaw) : chalk.gray(detailRaw))
|
|
342
|
+
? (stopped ? chalk.dim(detailRaw) : isError ? chalk.red(detailRaw) : chalk.gray(detailRaw))
|
|
332
343
|
: '';
|
|
333
344
|
const timing = chalk.dim(formatDuration(durationMs));
|
|
334
345
|
const sep = chalk.dim('·');
|
|
@@ -424,7 +435,8 @@ function panelsRow(p) {
|
|
|
424
435
|
// Round 8 item 2 — a cancelled / rejected approval is not a success: no
|
|
425
436
|
// green tick, and the row says what happened.
|
|
426
437
|
const declined = !p.isError && p.name === 'request_approval' ? approvalDeclined(p.resultContent) : undefined;
|
|
427
|
-
const
|
|
438
|
+
const stopped = stoppedBeforeStart(p.isError, p.resultContent);
|
|
439
|
+
const icon = stopped ? paint('dim', '·') : p.isError ? paint('bad', '✗') : declined ? paint('warn', '·') : paint('ok', '✓');
|
|
428
440
|
const timing = p.durationMs >= 1000 ? formatDuration(p.durationMs) : '';
|
|
429
441
|
// 1.1.31: a setting said yes, not a person — the row says so.
|
|
430
442
|
const auto = !p.isError && !declined && p.name === 'request_approval' && approvalAutoApproved(p.resultContent);
|
|
@@ -440,7 +452,10 @@ function panelsRow(p) {
|
|
|
440
452
|
// Live fix 2026-09-26: a JSON tool result (e.g. sap_activate's
|
|
441
453
|
// activation_results) is turned into readable lines, never shown raw.
|
|
442
454
|
const errorSource = p.resultContent?.trim().startsWith('{') ? p.resultContent : p.resultSummary;
|
|
443
|
-
if (
|
|
455
|
+
if (stopped) {
|
|
456
|
+
out.push(' ' + paint('dim', truncate(STOPPED_BEFORE_START_LINE, width - 4)));
|
|
457
|
+
}
|
|
458
|
+
else if (p.isError && errorSource) {
|
|
444
459
|
// Raw error text can carry ANSI codes, \r redraws and tabs: clean() each
|
|
445
460
|
// line (the one sanitiser, shared with the labels) before truncate + paint.
|
|
446
461
|
for (const d of humanErrorDetail(errorSource).map(clean).filter(Boolean).slice(0, ERROR_DETAIL_LINES)) {
|
package/dist/repl/chat-lane.js
CHANGED
|
@@ -85,9 +85,18 @@ const THANKS_PHRASES = new Set(['thanks', 'thank you', 'thank u', 'thx', 'tx', '
|
|
|
85
85
|
function isThanksOnly(trimmed) {
|
|
86
86
|
return THANKS_PHRASES.has(stripOkPrefix(trimmed).toLowerCase().replace(/[.!?,]+$/, ''));
|
|
87
87
|
}
|
|
88
|
+
/** Change options (1.1.53): the reply names a level (1-4) AND the document's
|
|
89
|
+
* object (case-insensitive). */
|
|
90
|
+
export function isLevelDecisionFor(trimmed, last) {
|
|
91
|
+
return /\blevel\s*[1-4]\b/i.test(trimmed) && trimmed.toUpperCase().includes(last.object.toUpperCase());
|
|
92
|
+
}
|
|
88
93
|
/** Today's continuation predicate (repl.tsx Ink + classic paths), as one
|
|
89
|
-
* function: the skill that keeps the turn, or null.
|
|
94
|
+
* function: the skill that keeps the turn, or null. A level decision for the
|
|
95
|
+
* last change options object goes back to the skill that showed it (only the
|
|
96
|
+
* skills-lane call sites pass lastChangeOptions). */
|
|
90
97
|
export function wantsContinuation(i) {
|
|
98
|
+
if (i.lastChangeOptions && isLevelDecisionFor(i.trimmed, i.lastChangeOptions))
|
|
99
|
+
return i.lastChangeOptions.skill;
|
|
91
100
|
if (!i.sessionSkill)
|
|
92
101
|
return null;
|
|
93
102
|
return (i.awaitingSkillAnswer || (i.resumedSkill !== null && isShortFollowUp(i.trimmed)) || isAckContinuation(i.trimmed, i.sessionSkill)) ? i.sessionSkill : null;
|
|
@@ -113,8 +122,11 @@ export function turnStatusLabel(skill) {
|
|
|
113
122
|
}
|
|
114
123
|
/** D11 — the one dim line shown in place of the echo when the message re-runs through today's router. */
|
|
115
124
|
export const CHAT_UNAVAILABLE_LINE = "Chat isn't available on this server yet — using skill routing.";
|
|
116
|
-
export function chatFallbackOrigin() {
|
|
117
|
-
|
|
125
|
+
export function chatFallbackOrigin(prose = false) {
|
|
126
|
+
// LT-15 Task 4 fix round 1 — a withdrawn prose replay re-runs as prose.
|
|
127
|
+
return prose
|
|
128
|
+
? { by: 'chat_fallback', line: CHAT_UNAVAILABLE_LINE, prose: true }
|
|
129
|
+
: { by: 'chat_fallback', line: CHAT_UNAVAILABLE_LINE };
|
|
118
130
|
}
|
|
119
131
|
/**
|
|
120
132
|
* The one place a process lane's tool side effect is applied (D5, §6.1).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// cspeach-cli/src/repl/inline-mode.ts
|
|
2
|
+
//
|
|
3
|
+
// Stage 1A, Task 1A.1 — when `assistant.skills = inline` is active.
|
|
4
|
+
//
|
|
5
|
+
// Decided at EVERY submit from the inputs as they are then (rev 4 M4), never
|
|
6
|
+
// cached, never stored in the session file: a resumed session re-decides from
|
|
7
|
+
// today's config and today's proxy. The history is NOT an input (rev 5 S-B):
|
|
8
|
+
// `assistant.skills = system`, `/chat off`, a non-TTY and a non-serving proxy
|
|
9
|
+
// always win. The REPL passes the result to the loop as `ctx.inlineSkills`.
|
|
10
|
+
// Piped stdin and one-shot never get it; reader and agent_run children never
|
|
11
|
+
// carry it (their ctx is built field by field).
|
|
12
|
+
import { servedInlineSkills } from '../models/server-config.js';
|
|
13
|
+
/** True only for `skills = inline` in the chat lane, on a TTY, where the provider can serve it. */
|
|
14
|
+
export function inlineActive({ skills, lane, interactive, served }) {
|
|
15
|
+
return interactive && served && skills === 'inline' && lane === 'chat';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Can this provider serve the inline system block? Managed: only when the
|
|
19
|
+
* proxy's served config says `inline_skills: true` (an old proxy, the kill
|
|
20
|
+
* switch or a stale cache ⇒ false). byok / ai-hub / azure / local build the
|
|
21
|
+
* system block on the laptop ⇒ true. Read at submit time: the served config
|
|
22
|
+
* loads after the config is read at startup.
|
|
23
|
+
*/
|
|
24
|
+
export function inlineServed(mode) {
|
|
25
|
+
if (mode === undefined)
|
|
26
|
+
return false;
|
|
27
|
+
return mode === 'managed' ? servedInlineSkills() : true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The `config show` comment for a stored `inline` that would not be active in
|
|
31
|
+
* an interactive session; null when there is nothing to say.
|
|
32
|
+
*/
|
|
33
|
+
export function inlineInactiveReason(i) {
|
|
34
|
+
if (i.skills !== 'inline')
|
|
35
|
+
return null;
|
|
36
|
+
if (i.lane !== 'chat')
|
|
37
|
+
return 'not active: skill routing is on';
|
|
38
|
+
if (!i.served)
|
|
39
|
+
return 'not active: this server does not serve it';
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Stage 1A, Task 1A.2 (S5) — an interactive chat-lane process: lane `chat` and
|
|
44
|
+
* a TTY, the condition applyLaneTools uses, whatever `assistant.skills` and the
|
|
45
|
+
* proxy say. In such a process the loop neutralises `<skill`, `<playbook` and
|
|
46
|
+
* `<session_context` in what the person sends (ctx.chatLaneProcess). Decided
|
|
47
|
+
* at every turn site like inlineActive.
|
|
48
|
+
*/
|
|
49
|
+
export function isChatLaneProcess(lane, interactive) {
|
|
50
|
+
return lane === 'chat' && interactive;
|
|
51
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LT-15 Feature 4 — messages typed while CSPeach worked that are still
|
|
3
|
+
* waiting when a dispatch ends (Ink REPL only; classic, headless and one-shot
|
|
4
|
+
* never queue anything).
|
|
5
|
+
*
|
|
6
|
+
* The runner holds the per-dispatch flags (plan review I4): a Ctrl+C press,
|
|
7
|
+
* a turn error, a scheduled hop. They are set during the dispatch, read ONCE
|
|
8
|
+
* by `settle` at that dispatch's end (handleSubmit's finally, after release,
|
|
9
|
+
* or a restore's end), and reset right there, so a stale flag can never touch
|
|
10
|
+
* the next hop or turn.
|
|
11
|
+
*
|
|
12
|
+
* `settle` acts on leftoverDecision:
|
|
13
|
+
* - run: on the next tick, drain the queue THERE (a message typed
|
|
14
|
+
* between the end and the tick rides along) and submit it once
|
|
15
|
+
* as the person's prose. Nothing when a dispatch is already busy
|
|
16
|
+
* (that turn reads the queue at its round 0, or its own end
|
|
17
|
+
* decides) or the session is ending (I4.1, I4.3).
|
|
18
|
+
* - to-input: ask the App to put the text back into the input box (D1, I5).
|
|
19
|
+
* - wait / report-unsent / none: touch nothing. A queued hop reads the
|
|
20
|
+
* message at its round 0 (D3); the exit reports what is left.
|
|
21
|
+
* Every message ends in exactly one place (I1.1).
|
|
22
|
+
*/
|
|
23
|
+
import { drainSteering, drainSteeringText, steeringSnapshot } from '../agent/steering-queue.js';
|
|
24
|
+
import { leftoverDecision, unsentLine } from '../ui/interrupt.js';
|
|
25
|
+
import { formatInfo } from '../renderer/notice-shape.js';
|
|
26
|
+
import { parseShortcut } from '../router/classifier.js';
|
|
27
|
+
export function createLeftoverRunner(deps) {
|
|
28
|
+
let ctrlCStop = false;
|
|
29
|
+
let errorEnd = false;
|
|
30
|
+
let dispatchScheduled = false;
|
|
31
|
+
const reset = () => {
|
|
32
|
+
ctrlCStop = false;
|
|
33
|
+
errorEnd = false;
|
|
34
|
+
dispatchScheduled = false;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
noteCtrlC() { ctrlCStop = true; },
|
|
38
|
+
noteError() { errorEnd = true; },
|
|
39
|
+
noteDispatchScheduled() { dispatchScheduled = true; },
|
|
40
|
+
reset,
|
|
41
|
+
settle(s) {
|
|
42
|
+
const decision = leftoverDecision({
|
|
43
|
+
done: deps.isDone(),
|
|
44
|
+
exitRequested: s.exitRequested,
|
|
45
|
+
forceExit: s.forceExit === true,
|
|
46
|
+
ctrlCStop,
|
|
47
|
+
braked: s.braked === true,
|
|
48
|
+
errorEnd,
|
|
49
|
+
dispatchScheduled,
|
|
50
|
+
restoreActive: s.restoreActive === true,
|
|
51
|
+
waiting: steeringSnapshot().length,
|
|
52
|
+
});
|
|
53
|
+
reset();
|
|
54
|
+
if (decision === 'run') {
|
|
55
|
+
deps.schedule(() => {
|
|
56
|
+
if (deps.isDone() || deps.isBusy())
|
|
57
|
+
return;
|
|
58
|
+
const text = drainSteeringText();
|
|
59
|
+
if (text !== null)
|
|
60
|
+
deps.submit(text);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else if (decision === 'to-input') {
|
|
64
|
+
deps.toBox();
|
|
65
|
+
}
|
|
66
|
+
return decision;
|
|
67
|
+
},
|
|
68
|
+
toBoxOrReport() {
|
|
69
|
+
if (steeringSnapshot().length === 0)
|
|
70
|
+
return;
|
|
71
|
+
deps.toBox();
|
|
72
|
+
const left = drainSteering();
|
|
73
|
+
if (left.length > 0)
|
|
74
|
+
deps.print(formatInfo(unsentLine(left.length)));
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** At exit: drain what still waits and return the one line to print, or null. */
|
|
79
|
+
export function takeUnsentReport() {
|
|
80
|
+
const left = drainSteering();
|
|
81
|
+
return left.length > 0 ? formatInfo(unsentLine(left.length)) : null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* LT-15 F4 — how a submitted line is read. A replayed waiting message is
|
|
85
|
+
* prose: never a slash command or an @-shortcut, so "/sap/bc/adt/x is slow"
|
|
86
|
+
* reaches the model as text. Anything typed at the prompt is read as today.
|
|
87
|
+
*/
|
|
88
|
+
export function parseSubmitted(input, prose) {
|
|
89
|
+
return prose ? { prompt: input } : parseShortcut(input);
|
|
90
|
+
}
|