@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
package/dist/ui/footer.js
CHANGED
|
@@ -9,7 +9,7 @@ import Fuse from 'fuse.js';
|
|
|
9
9
|
// Task 8 (ux-wave1) — persistence lives in the shared writer.
|
|
10
10
|
import { appendHistoryEntry, readHistoryEntries, flattenHistoryEntry } from '../repl/history.js';
|
|
11
11
|
import { frameWidth, truncateToWidth } from '../renderer/ui-width.js';
|
|
12
|
-
import {
|
|
12
|
+
import { cardBorderStyle } from './ascii-chars.js';
|
|
13
13
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
14
14
|
import { roleProps, selectedProps } from './role-props.js';
|
|
15
15
|
import { countInputRows, expandPasteChips, nextPasteChip, INPUT_MAX_LINES } from './input-wrap.js';
|
|
@@ -25,6 +25,22 @@ function appendHistory(line, history) {
|
|
|
25
25
|
if (appendHistoryEntry(entry))
|
|
26
26
|
history.push(entry);
|
|
27
27
|
}
|
|
28
|
+
/** LT-15 final review I1 — the second Esc must come within this to clear returned text. */
|
|
29
|
+
export const RETURNED_ESC_WINDOW_MS = 2000;
|
|
30
|
+
/** LT-15 final review I1 — the dim hint at the first Esc on returned held lines. */
|
|
31
|
+
export const RETURNED_ESC_HINT = 'Esc again to clear the returned message';
|
|
32
|
+
/**
|
|
33
|
+
* LT-15 (review I3) — where returned text lands in the box. The input row
|
|
34
|
+
* shows a newline as a space, so a multiline message goes into the held
|
|
35
|
+
* lines (shown as real lines, sent joined with '\n' on Enter) and only its
|
|
36
|
+
* last line becomes the editable value. A draft already in the box stays in
|
|
37
|
+
* front of it, never overwritten: its value line becomes a held line too.
|
|
38
|
+
*/
|
|
39
|
+
export function placeInBox(held, value, text) {
|
|
40
|
+
const lines = text.split(/\r?\n/);
|
|
41
|
+
const last = lines.pop() ?? '';
|
|
42
|
+
return { held: [...held, ...(value !== '' ? [value] : []), ...lines], value: last };
|
|
43
|
+
}
|
|
28
44
|
/** I1 (spec §7) — the prompt glyph; the brand stays on the start screen. */
|
|
29
45
|
export const PROMPT_GLYPH = '›';
|
|
30
46
|
const PROMPT = `${PROMPT_GLYPH} `;
|
|
@@ -38,8 +54,12 @@ const BOX_CHROME = 4;
|
|
|
38
54
|
export function inputBoxWidth(columns) {
|
|
39
55
|
return frameWidth(columns);
|
|
40
56
|
}
|
|
41
|
-
export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChange, prefill, disabled, onNotice, onBufferChange, maxLines, onInputLinesChange, waitingForCard, }) {
|
|
57
|
+
export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChange, prefill, disabled, onNotice, onBufferChange, maxLines, onInputLinesChange, waitingForCard, onWithdraw, boxRef, onFuzzyChange, }) {
|
|
42
58
|
const [value, setValue] = useState('');
|
|
59
|
+
// LT-15 (review I3) — the value as of the last render, for putInBox (called
|
|
60
|
+
// from outside the input handlers, so it must not read a stale closure).
|
|
61
|
+
const valueNowRef = useRef(value);
|
|
62
|
+
valueNowRef.current = value;
|
|
43
63
|
const { stdout } = useStdout();
|
|
44
64
|
const [, bumpOnResize] = useState(0);
|
|
45
65
|
useEffect(() => {
|
|
@@ -72,9 +92,24 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
72
92
|
const fuse = React.useMemo(() => new Fuse(history, { threshold: 0.4 }), [history]);
|
|
73
93
|
useEffect(() => { onValueChange?.(value); }, [value, onValueChange]);
|
|
74
94
|
useEffect(() => { onBufferChange?.(buffer.length); }, [buffer.length, onBufferChange]);
|
|
95
|
+
useEffect(() => { onFuzzyChange?.(fuzzyMode); }, [fuzzyMode, onFuzzyChange]);
|
|
75
96
|
const desiredRows = countInputRows(buffer, value, wrapWidth);
|
|
76
97
|
useEffect(() => { onInputLinesChange?.(desiredRows); }, [desiredRows, onInputLinesChange]);
|
|
98
|
+
// LT-15 final review I1 — held lines that came from putInBox and were not
|
|
99
|
+
// edited since. The first Esc on them only shows a hint; a second Esc
|
|
100
|
+
// within RETURNED_ESC_WINDOW_MS discards as today. Any edit (a key, a
|
|
101
|
+
// paste, a submit) makes them an ordinary draft again.
|
|
102
|
+
const putHeldFreshRef = useRef(false);
|
|
103
|
+
const escArmedAtRef = useRef(0);
|
|
104
|
+
// Set while the Footer handles an Esc; cleared after the current input
|
|
105
|
+
// burst (a microtask runs only once the synchronous drain is done).
|
|
106
|
+
const escOwnedRef = useRef(false);
|
|
107
|
+
const handleChange = useCallback((v) => {
|
|
108
|
+
putHeldFreshRef.current = false;
|
|
109
|
+
setValue(v);
|
|
110
|
+
}, []);
|
|
77
111
|
const handlePasteLines = useCallback((lines) => {
|
|
112
|
+
putHeldFreshRef.current = false;
|
|
78
113
|
updateBuffer([...bufferRef.current, ...lines]);
|
|
79
114
|
}, [updateBuffer]);
|
|
80
115
|
const handleLargePaste = useCallback((text) => {
|
|
@@ -95,12 +130,73 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
95
130
|
setFuzzyIdx(0);
|
|
96
131
|
}
|
|
97
132
|
}, [fuzzyQuery, fuzzyMode, fuse]);
|
|
133
|
+
// LT-15 Feature 2 / review I3 — text put back into the box.
|
|
134
|
+
// `boxPutPendingRef`: set by putInBox, cleared by the first commit after
|
|
135
|
+
// it (that render already holds the put's state updates, and the input
|
|
136
|
+
// handlers are re-bound with the new value by then). A second ↑ in the same
|
|
137
|
+
// input burst still sees the old (empty) value in its closure; without this
|
|
138
|
+
// it would take a second message and overwrite the first one in the box.
|
|
139
|
+
// The token is captured during render, so an older commit's effect can
|
|
140
|
+
// never clear a newer put, and nothing can leave it set for good.
|
|
141
|
+
// `skipHistRef`: the history copy of the text just put back (the Footer
|
|
142
|
+
// stored it in history when it was first sent). The next ↑ skips it, so ↑
|
|
143
|
+
// does not show the same text again (controller ruling I2.4).
|
|
144
|
+
const boxPutPendingRef = useRef(null);
|
|
145
|
+
const skipHistRef = useRef(null);
|
|
146
|
+
const putPendingAtRender = boxPutPendingRef.current;
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (putPendingAtRender !== null && boxPutPendingRef.current === putPendingAtRender)
|
|
149
|
+
boxPutPendingRef.current = null;
|
|
150
|
+
});
|
|
151
|
+
// Fix round 1 (C1) — the field's synchronous handle (null while the box is
|
|
152
|
+
// disabled or Ctrl+R search shows; the field then mounts with the state).
|
|
153
|
+
const inputHandleRef = useRef(null);
|
|
154
|
+
const putInBox = useCallback((text) => {
|
|
155
|
+
// LT-15 Task 4 — the field's live text when it has one (a key typed just
|
|
156
|
+
// before an outside put must stay in front), else the last render's value.
|
|
157
|
+
const next = placeInBox(bufferRef.current, inputHandleRef.current?.getValueNow() ?? valueNowRef.current, text);
|
|
158
|
+
updateBuffer(next.held);
|
|
159
|
+
inputHandleRef.current?.setValueNow(next.value);
|
|
160
|
+
setValue(next.value);
|
|
161
|
+
valueNowRef.current = next.value;
|
|
162
|
+
setHistIdx(-1);
|
|
163
|
+
skipHistRef.current = flattenHistoryEntry(text);
|
|
164
|
+
boxPutPendingRef.current = {};
|
|
165
|
+
putHeldFreshRef.current = next.held.length > 0;
|
|
166
|
+
escArmedAtRef.current = 0;
|
|
167
|
+
}, [updateBuffer]);
|
|
168
|
+
const ownsEscape = useCallback(() => bufferRef.current.length > 0 || escOwnedRef.current, []);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!boxRef)
|
|
171
|
+
return undefined;
|
|
172
|
+
boxRef.current = { putInBox, ownsEscape };
|
|
173
|
+
return () => { if (boxRef.current?.putInBox === putInBox)
|
|
174
|
+
boxRef.current = null; };
|
|
175
|
+
}, [boxRef, putInBox, ownsEscape]);
|
|
98
176
|
// I1 — history recall, called by the TextInput at the first / last visual row.
|
|
99
|
-
const historyUp = useCallback(() => {
|
|
100
|
-
|
|
177
|
+
const historyUp = useCallback((currentValue) => {
|
|
178
|
+
// LT-15 — text was just put in the box and has not rendered yet: this ↑
|
|
179
|
+
// belongs to the same burst. Do nothing rather than overwrite it.
|
|
180
|
+
if (boxPutPendingRef.current !== null)
|
|
181
|
+
return;
|
|
182
|
+
// LT-15 Feature 2 — an EMPTY box takes back the last waiting message.
|
|
183
|
+
// A miss (nothing waits, no turn, the loop just read it) falls through.
|
|
184
|
+
// Fix round 1 (I1) — "empty" is the field's CURRENT text (its synchronous
|
|
185
|
+
// mirror), not this render's value: a key typed in the same burst counts.
|
|
186
|
+
const now = currentValue ?? value;
|
|
187
|
+
if (now === '' && histIdx === -1 && bufferRef.current.length === 0 && onWithdraw?.())
|
|
188
|
+
return;
|
|
189
|
+
const skip = skipHistRef.current;
|
|
190
|
+
const skipOne = histIdx === -1 && skip !== null && history[history.length - 1] === skip;
|
|
191
|
+
// Fix round 1 (M3) — the copy is the ONLY entry: nothing older to show.
|
|
192
|
+
// Leave the box (and the skip) as they are rather than show the copy.
|
|
193
|
+
if (skipOne && history.length === 1)
|
|
194
|
+
return;
|
|
195
|
+
skipHistRef.current = null;
|
|
196
|
+
const next = Math.min(histIdx + (skipOne ? 2 : 1), history.length - 1);
|
|
101
197
|
setHistIdx(next);
|
|
102
198
|
setValue(history[history.length - 1 - next] ?? '');
|
|
103
|
-
}, [histIdx, history]);
|
|
199
|
+
}, [histIdx, history, value, onWithdraw]);
|
|
104
200
|
const historyDown = useCallback(() => {
|
|
105
201
|
// Not browsing history: ↓ on the last row leaves the draft alone.
|
|
106
202
|
if (histIdx === -1)
|
|
@@ -144,6 +240,29 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
144
240
|
}
|
|
145
241
|
// Task 7 — Esc discards the multiline draft.
|
|
146
242
|
if (key.escape && bufferRef.current.length > 0) {
|
|
243
|
+
escOwnedRef.current = true;
|
|
244
|
+
queueMicrotask(() => { escOwnedRef.current = false; });
|
|
245
|
+
// LT-15 final review I1 — returned text, not edited since: the first
|
|
246
|
+
// Esc only warns; the second within the window discards.
|
|
247
|
+
const returned = putHeldFreshRef.current;
|
|
248
|
+
if (returned) {
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
if (now - escArmedAtRef.current > RETURNED_ESC_WINDOW_MS) {
|
|
251
|
+
escArmedAtRef.current = now;
|
|
252
|
+
onNotice?.(RETURNED_ESC_HINT);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
putHeldFreshRef.current = false;
|
|
256
|
+
escArmedAtRef.current = 0;
|
|
257
|
+
// Live check 2026-10-26 — the returned message's last line sits in
|
|
258
|
+
// the field, not in the held lines: clear it too, or the discard
|
|
259
|
+
// leaves part of the message behind. Typed drafts keep their line.
|
|
260
|
+
inputHandleRef.current?.setValueNow('');
|
|
261
|
+
setValue('');
|
|
262
|
+
valueNowRef.current = '';
|
|
263
|
+
}
|
|
264
|
+
// Its history copy is no longer in the box: ↑ may show it again.
|
|
265
|
+
skipHistRef.current = null;
|
|
147
266
|
updateBuffer([]);
|
|
148
267
|
// Chips that lived only in the discarded lines go with them; a chip
|
|
149
268
|
// still in the input keeps its text.
|
|
@@ -157,6 +276,8 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
157
276
|
// ↑/↓ belong to the TextInput now (visual rows first, then history).
|
|
158
277
|
}, { isActive: !disabled });
|
|
159
278
|
const handleSubmit = useCallback((line) => {
|
|
279
|
+
putHeldFreshRef.current = false; // LT-15 final review I1 — sent or continued: no longer "returned"
|
|
280
|
+
skipHistRef.current = null; // LT-15 I2.4 — the skip lasts until the next submit
|
|
160
281
|
// Task 7 — trailing `\` continues the message (chips stay folded until the final submit).
|
|
161
282
|
if (line.endsWith('\\')) {
|
|
162
283
|
updateBuffer([...bufferRef.current, line.slice(0, -1)]);
|
|
@@ -200,7 +321,7 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
200
321
|
onNotice?.('no image on the clipboard');
|
|
201
322
|
return chip;
|
|
202
323
|
}, [onNotice]);
|
|
203
|
-
const borderStyle =
|
|
324
|
+
const borderStyle = cardBorderStyle();
|
|
204
325
|
const borderColor = roleProps('frame').color;
|
|
205
326
|
if (fuzzyMode) {
|
|
206
327
|
const g = getGlyphs();
|
|
@@ -216,5 +337,5 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
216
337
|
if (disabled) {
|
|
217
338
|
return (_jsx(Box, { borderStyle: borderStyle, borderColor: borderColor, paddingX: 1, width: boxWidth, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...roleProps('sap', { bold: true }), children: waitingForCard ? PROMPT : PROMPT_GLYPH }), waitingForCard ? _jsx(Text, { ...roleProps('dim'), children: "(answer above to continue)" }) : null] }) }));
|
|
218
339
|
}
|
|
219
|
-
return (_jsx(Box, { flexDirection: "column", borderStyle: borderStyle, borderColor: borderColor, paddingX: 1, width: boxWidth, children: _jsx(TextInput, { value: value, onChange:
|
|
340
|
+
return (_jsx(Box, { flexDirection: "column", borderStyle: borderStyle, borderColor: borderColor, paddingX: 1, width: boxWidth, children: _jsx(TextInput, { value: value, onChange: handleChange, onSubmit: handleSubmit, onPasteLines: handlePasteLines, onPasteImage: handlePasteImage, onLargePaste: handleLargePaste, wrapWidth: wrapWidth, maxLines: Math.max(1, Math.min(INPUT_MAX_LINES, maxLines ?? INPUT_MAX_LINES)), heldLines: buffer, prompt: PROMPT, handleRef: inputHandleRef, onHistoryUp: historyUp, onHistoryDown: historyDown }) }));
|
|
220
341
|
}
|
package/dist/ui/interrupt.js
CHANGED
|
@@ -95,9 +95,43 @@ export function createPendingTurnCancel() {
|
|
|
95
95
|
export function autoDispatchDecision(s, cmd) {
|
|
96
96
|
if (s.done)
|
|
97
97
|
return { kind: 'skip' };
|
|
98
|
-
if (s.exitRequested || s.cancelPending) {
|
|
98
|
+
if (s.exitRequested || s.cancelPending || (s.stopped === true && s.chatFallback !== true)) {
|
|
99
99
|
const what = queuedWorkLabel(cmd).replace(/^the /, '');
|
|
100
100
|
return { kind: 'not-started', line: `${what.charAt(0).toUpperCase()}${what.slice(1)} not started · run it when ready: ${cmd}` };
|
|
101
101
|
}
|
|
102
102
|
return { kind: 'go' };
|
|
103
103
|
}
|
|
104
|
+
export function leftoverDecision(s) {
|
|
105
|
+
if (s.waiting <= 0)
|
|
106
|
+
return 'none';
|
|
107
|
+
if (s.done || s.exitRequested || s.forceExit)
|
|
108
|
+
return 'report-unsent';
|
|
109
|
+
if (s.ctrlCStop || s.errorEnd)
|
|
110
|
+
return 'to-input';
|
|
111
|
+
if (s.dispatchScheduled || s.restoreActive)
|
|
112
|
+
return 'wait';
|
|
113
|
+
return 'run';
|
|
114
|
+
}
|
|
115
|
+
/** LT-15 Feature 4 — the one line at exit for messages that never ran. */
|
|
116
|
+
export function unsentLine(n) {
|
|
117
|
+
return `${n} waiting message${n === 1 ? '' : 's'} not sent`;
|
|
118
|
+
}
|
|
119
|
+
export function decideEscape(s) {
|
|
120
|
+
if (s.overlayOpen || s.bufferCount > 0 || s.fuzzyOpen)
|
|
121
|
+
return 'none';
|
|
122
|
+
if (s.alreadyStopped || s.restoreActive)
|
|
123
|
+
return 'none';
|
|
124
|
+
if (s.turnActive)
|
|
125
|
+
return 'brake';
|
|
126
|
+
if (s.busy && s.phase === 'post' && s.canChain === true)
|
|
127
|
+
return 'cancel-pending';
|
|
128
|
+
return 'none';
|
|
129
|
+
}
|
|
130
|
+
/** The one line at the Esc press (D2: the short form when nothing waits). */
|
|
131
|
+
export function brakeLine(waiting) {
|
|
132
|
+
return waiting ? 'Stopping at the next safe point · your message goes next' : 'Stopping at the next safe point';
|
|
133
|
+
}
|
|
134
|
+
/** The one dim line a braked turn prints in place of the turn-error block. */
|
|
135
|
+
export function brakeEndLine(waiting) {
|
|
136
|
+
return waiting ? 'Stopped · your work so far is kept' : 'Stopped · your work so far is kept · type continue to pick up';
|
|
137
|
+
}
|
package/dist/ui/live-area.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Pure layout decisions for the Ink live area (CLI look piece 2, spec §2).
|
|
4
4
|
// Order, top to bottom: Body (<Static>) → checklist → card slot → status
|
|
5
|
-
// strip → input → footer → palettes. Ink 5 clears and
|
|
6
|
-
// screen once the live region reaches the terminal height
|
|
7
|
-
// `outputHeight >= stdout.rows`), so the live area is capped at
|
|
5
|
+
// strip → queue row (LT-15) → input → footer → palettes. Ink 5 clears and
|
|
6
|
+
// redraws the WHOLE screen once the live region reaches the terminal height
|
|
7
|
+
// (ink.js `outputHeight >= stdout.rows`), so the live area is capped at
|
|
8
|
+
// rows − 2.
|
|
8
9
|
/** Precedence: model-driven blocking cards first, then user-opened panels. */
|
|
9
10
|
const PRECEDENCE = ['approval', 'confirm', 'ask', 'coaching', 'files', 'rewind', 'timeline', 'shortcuts'];
|
|
10
11
|
/** The ONE card shown in the slot (spec §1.6: one card at a time), or null. */
|
|
@@ -57,10 +58,13 @@ export const PALETTE_ROWS = 13;
|
|
|
57
58
|
* even one input line fits beside the full card.
|
|
58
59
|
* The status row is always counted (conservative: it is hidden while a card
|
|
59
60
|
* is open, so the real frame is one row shorter).
|
|
61
|
+
* LT-15 Feature 1 — `queueRows` is the "N waiting" row's height as shown
|
|
62
|
+
* (0 when it is hidden). It is fixed like the status row; absent means 0.
|
|
60
63
|
*/
|
|
61
64
|
export function liveBudget(args) {
|
|
62
65
|
const cap = Math.max(0, args.rows - 2);
|
|
63
|
-
const fixed = STATUS_ROWS + INPUT_BORDER_ROWS + FOOTER_ROWS + Math.max(0, args.paletteRows)
|
|
66
|
+
const fixed = STATUS_ROWS + INPUT_BORDER_ROWS + FOOTER_ROWS + Math.max(0, args.paletteRows)
|
|
67
|
+
+ Math.max(0, args.queueRows ?? 0);
|
|
64
68
|
const wanted = Math.max(0, args.checklistRows);
|
|
65
69
|
const card = Math.max(0, args.cardRows);
|
|
66
70
|
const checklistRows = card > 0
|
|
@@ -70,6 +74,19 @@ export function liveBudget(args) {
|
|
|
70
74
|
const inputMaxLines = Math.max(1, Math.min(Math.max(1, args.inputLines), INPUT_MAX_LINES, remaining));
|
|
71
75
|
return { checklistRows, inputMaxLines, showDescriptions: remaining >= 1 };
|
|
72
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* LT-15 Task 2 fix round 1 (review I-2): the rows the "N waiting" row may
|
|
79
|
+
* take so the live area stays ≤ rows − 2 — the cap less the status strip,
|
|
80
|
+
* the input border, the footer, an open palette, the checklist's one folded
|
|
81
|
+
* line (when there is a checklist) and one input line. Never below 1: the
|
|
82
|
+
* row then folds to one line (formatQueueRow). The row is hidden while a
|
|
83
|
+
* card is open, so no card term.
|
|
84
|
+
*/
|
|
85
|
+
export function queueRowBudget(args) {
|
|
86
|
+
const cap = Math.max(0, args.rows - 2);
|
|
87
|
+
const fixed = STATUS_ROWS + INPUT_BORDER_ROWS + FOOTER_ROWS + Math.max(0, args.paletteRows);
|
|
88
|
+
return Math.max(1, cap - fixed - Math.min(Math.max(0, args.checklistRows), 1) - 1);
|
|
89
|
+
}
|
|
73
90
|
/**
|
|
74
91
|
* Fix round 1 (review I1): the rows a card may take so the whole live area
|
|
75
92
|
* stays ≤ rows − 2 with one input line beside it — the cap less the status
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
/**
|
|
3
|
+
* LT-15 Task 4 (owner D1, plan review I3/I5) — repl.tsx asks the mounted
|
|
4
|
+
* <App> to put every waiting message back into the input box: a turn end
|
|
5
|
+
* after Ctrl+C or an error, and a session switch. <App> owns the box
|
|
6
|
+
* (returnQueueToBox), repl.tsx owns the turn, so they talk through this
|
|
7
|
+
* singleton — the same pattern as rewindEmitter. In classic mode <App> is
|
|
8
|
+
* never mounted and nothing listens.
|
|
9
|
+
*/
|
|
10
|
+
export const queueBoxEmitter = new EventEmitter();
|
|
11
|
+
/** True when a mounted <App> took the request (it may still find nothing to put). */
|
|
12
|
+
export function requestQueueToBox() {
|
|
13
|
+
return queueBoxEmitter.emit('return');
|
|
14
|
+
}
|
package/dist/ui/rewind-panel.js
CHANGED
|
@@ -7,6 +7,7 @@ import { roleProps, selectedProps } from './role-props.js';
|
|
|
7
7
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
8
8
|
import { uiWidth, truncateToWidth } from '../renderer/ui-width.js';
|
|
9
9
|
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
10
|
+
import { cardBorderStyle } from './ascii-chars.js';
|
|
10
11
|
/**
|
|
11
12
|
* RewindPanel — UX Wave 3 / Task 3; a card-slot occupant since CLI look
|
|
12
13
|
* piece 2 (§10). The Esc-Esc (and /rewind) card that turns "undo for your
|
|
@@ -120,7 +121,7 @@ export function RewindPanel({ candidates, onRestore, onClose, width = uiWidth()
|
|
|
120
121
|
const below = Math.max(0, candidates.length - windowStop);
|
|
121
122
|
const scrolls = windowStart > 0 || below > 0;
|
|
122
123
|
const g = getGlyphs();
|
|
123
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
124
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: cardBorderStyle(), borderColor: roleProps('frame').color, paddingX: 2, width: width, children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: truncateToWidth('Rewind · undo a write from this session', inner) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [empty && _jsx(Text, { ...roleProps('dim'), children: "No restorable writes this session" }), !empty && windowStart > 0 && _jsxs(Text, { ...roleProps('dim'), children: [" \u2191 ", windowStart, " more"] }), !empty && visible.map((c, i) => {
|
|
124
125
|
const idx = windowStart + i;
|
|
125
126
|
const sel = idx === cursor;
|
|
126
127
|
const row = truncateToWidth(`${sel ? `${g.cursor} ` : ' '}${rewindRowLabel(c)}`, inner);
|
|
@@ -8,6 +8,7 @@ import { uiWidth, truncateToWidth } from '../renderer/ui-width.js';
|
|
|
8
8
|
import { toolLabel } from '../renderer/tool-labels.js';
|
|
9
9
|
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
10
10
|
import { hhmm } from '../rewind/format.js';
|
|
11
|
+
import { cardBorderStyle } from './ascii-chars.js';
|
|
11
12
|
// v1.1: add 'skipped' (approval rejection logs) and 'rolled_back' (snapshot restore).
|
|
12
13
|
// Both need session-schema bump (schema_version 2) — deferred.
|
|
13
14
|
const MUTATING_TOOLS = new Set([
|
|
@@ -64,7 +65,7 @@ export function SessionTimeline({ entries, onClose, width = uiWidth() }) {
|
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: cardBorderStyle(), borderColor: roleProps('frame').color, paddingX: 2, width: width, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Write history ", _jsx(Text, { ...roleProps('dim'), children: maxOffset > 0 ? '· ↑↓ scroll · Esc to close' : '· Esc to close' })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [writes.length === 0 && _jsx(Text, { ...roleProps('dim'), children: "No writes this session" }), start > 0 && _jsxs(Text, { ...roleProps('dim'), children: [" \u2191 ", start, " more"] }), visible.map((e, i) => {
|
|
68
69
|
const time = `${hhmm(e.timestamp)} `;
|
|
69
70
|
const glyph = e.isError ? g.failed : g.done;
|
|
70
71
|
const verb = toolLabel(e.toolName, {}).verb;
|
package/dist/ui/text-input.js
CHANGED
|
@@ -88,7 +88,7 @@ export function computeDisplayWindow(valueLength, cursorOffset, budget, cursorVi
|
|
|
88
88
|
const end = Math.min(valueLength, start + budget);
|
|
89
89
|
return { start, end };
|
|
90
90
|
}
|
|
91
|
-
export function PasteSafeTextInput({ value: originalValue, placeholder = '', focus = true, mask, showCursor = true, onChange, onSubmit, onPasteLines, maxDisplayWidth, onPasteImage, wrapWidth, maxLines, heldLines, prompt, onHistoryUp, onHistoryDown, onLargePaste, }) {
|
|
91
|
+
export function PasteSafeTextInput({ value: originalValue, placeholder = '', focus = true, mask, showCursor = true, onChange, onSubmit, onPasteLines, maxDisplayWidth, onPasteImage, wrapWidth, maxLines, heldLines, prompt, onHistoryUp, onHistoryDown, onLargePaste, handleRef, }) {
|
|
92
92
|
const [cursorOffset, setCursorOffset] = useState((originalValue || '').length);
|
|
93
93
|
// The paste-safety core: synchronous mirrors of value + cursor. The
|
|
94
94
|
// input handler reads/writes THESE, never the (possibly stale) props.
|
|
@@ -102,6 +102,9 @@ export function PasteSafeTextInput({ value: originalValue, placeholder = '', foc
|
|
|
102
102
|
// the repro as words landing out of order). An echo — even a stale
|
|
103
103
|
// one — must never overwrite the mirror; only EXTERNAL pushes may.
|
|
104
104
|
const pendingEchoes = useRef([]);
|
|
105
|
+
// LT-15 Task 4 — the value prop as of the last render (for getValueNow).
|
|
106
|
+
const propValueRef = useRef(originalValue);
|
|
107
|
+
propValueRef.current = originalValue;
|
|
105
108
|
// Re-sync the mirrors on EXTERNAL value pushes only (parent setValue('')
|
|
106
109
|
// after submit, palette prefill). Echoes of our own onChange — current
|
|
107
110
|
// or stale — are recognised via pendingEchoes and skipped: the mirror
|
|
@@ -119,6 +122,32 @@ export function PasteSafeTextInput({ value: originalValue, placeholder = '', foc
|
|
|
119
122
|
setCursorOffset(originalValue.length);
|
|
120
123
|
}
|
|
121
124
|
}, [originalValue]);
|
|
125
|
+
// LT-15 (fix round 1, C1) — the synchronous put. The value is recorded as
|
|
126
|
+
// our own pending echo: when the parent's matching `value` prop commits,
|
|
127
|
+
// the re-sync effect above skips it (it is not an external push), so a
|
|
128
|
+
// keystroke typed on top of it in the meantime is never rewound.
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!handleRef)
|
|
131
|
+
return undefined;
|
|
132
|
+
const handle = {
|
|
133
|
+
setValueNow: (next) => {
|
|
134
|
+
valueRef.current = next;
|
|
135
|
+
cursorRef.current = next.length;
|
|
136
|
+
// LT-15 Task 4 — added, not replaced: an older echo of our own typing
|
|
137
|
+
// that commits late must still be recognised, or it would rewind the put.
|
|
138
|
+
pendingEchoes.current.push(next);
|
|
139
|
+
setCursorOffset(next.length);
|
|
140
|
+
},
|
|
141
|
+
// While the field has typing the parent has not echoed back yet, its
|
|
142
|
+
// mirror is the truth (a key typed just before an outside put). Otherwise
|
|
143
|
+
// the last rendered prop is: right after Enter the parent has already
|
|
144
|
+
// cleared the box, but the mirror only hears it in a later effect.
|
|
145
|
+
getValueNow: () => (pendingEchoes.current.length > 0 ? valueRef.current : propValueRef.current) ?? '',
|
|
146
|
+
};
|
|
147
|
+
handleRef.current = handle;
|
|
148
|
+
return () => { if (handleRef.current === handle)
|
|
149
|
+
handleRef.current = null; };
|
|
150
|
+
}, [handleRef]);
|
|
122
151
|
// Clamp rendered cursor when the value shrinks under it (vendored
|
|
123
152
|
// semantic from ink-text-input's first effect).
|
|
124
153
|
useEffect(() => {
|
|
@@ -179,7 +208,7 @@ export function PasteSafeTextInput({ value: originalValue, placeholder = '', foc
|
|
|
179
208
|
if (w.cursorLine > 0)
|
|
180
209
|
moveCursor(indexAt(cur, wrapWidth, w.cursorLine - 1, w.cursorCol));
|
|
181
210
|
else
|
|
182
|
-
onHistoryUp?.();
|
|
211
|
+
onHistoryUp?.(cur);
|
|
183
212
|
}
|
|
184
213
|
else if (w.cursorLine < w.lines.length - 1) {
|
|
185
214
|
moveCursor(indexAt(cur, wrapWidth, w.cursorLine + 1, w.cursorCol));
|