@cspeach/cli 1.1.17 → 1.1.19
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/README.md +3 -4
- package/dist/agent/loop.js +143 -25
- package/dist/agent/providers/local-provider.js +4 -1
- package/dist/agent/repair-partial.js +76 -19
- package/dist/agent/tool-dispatch.js +80 -22
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +25 -6
- package/dist/commands/compact.js +12 -9
- package/dist/commands/config-set.js +6 -2
- package/dist/commands/config-show.js +7 -5
- package/dist/commands/cost.js +41 -34
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +15 -3
- package/dist/cost/pricing.js +6 -5
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lock-contention.js +2 -2
- package/dist/one-shot.js +135 -40
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +96 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +152 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +238 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +42 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +36 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +795 -332
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +69 -27
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/signing-public-key.js +1 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/sap-read.js +16 -1
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +30 -12
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +23 -18
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +33 -7
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +28 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +11 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
package/dist/ui/app.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
5
|
+
import { roleProps } from './role-props.js';
|
|
6
|
+
import { IDLE_EXIT_WINDOW_MS } from './interrupt.js';
|
|
5
7
|
import { Body } from './body.js';
|
|
6
8
|
import { Footer } from './footer.js';
|
|
7
9
|
import { ApprovalModal } from './approval-modal.js';
|
|
@@ -9,63 +11,61 @@ import { CommandPalette } from './command-palette.js';
|
|
|
9
11
|
import { FilePalette } from './file-palette.js';
|
|
10
12
|
import { nextResolutionStep, applySlashPick } from './line-resolution.js';
|
|
11
13
|
import { TurnStatus } from './turn-status.js';
|
|
12
|
-
import {
|
|
14
|
+
import { FooterLine } from './footer-line.js';
|
|
15
|
+
import { ShortcutsSheet } from './widgets/shortcuts-sheet.js';
|
|
16
|
+
import { wasPasted } from '../repl/paste-marker.js';
|
|
13
17
|
import { turnStatusEmitter } from './turn-status-emitter.js';
|
|
18
|
+
import { pickSlotOccupant, liveBudget, SLOT_ROWS_ESTIMATE, PALETTE_ROWS } from './live-area.js';
|
|
19
|
+
import { Checklist } from './checklist.js';
|
|
20
|
+
import { formatChecklist, isPlanFinished, CHECKLIST_MAX_ROWS } from '../renderer/checklist-format.js';
|
|
14
21
|
import { listWorkspaceFileChoices } from '../repl/at-picker.js';
|
|
15
|
-
import { RichErrorBox } from './rich-error-box.js';
|
|
16
22
|
import { SessionTimeline, toTimelineEntry } from './session-timeline.js';
|
|
17
|
-
import { TodoPanel } from './todo-panel.js';
|
|
18
23
|
import { RewindPanel } from './rewind-panel.js';
|
|
19
24
|
import { rewindEmitter } from './rewind-emitter.js';
|
|
20
25
|
import { todoEmitter } from './todo-emitter.js';
|
|
21
26
|
import { approvalEmitter } from './approval-emitter.js';
|
|
22
|
-
import { errorEmitter } from './error-emitter.js';
|
|
23
27
|
import { SapStateContext, globalStore } from './sap-state-store.js';
|
|
24
28
|
import { coachingPickerEmitter } from './coaching-picker-emitter.js';
|
|
25
29
|
import { CoachingPicker } from './widgets/coaching-picker.js';
|
|
26
30
|
import { askQuestionEmitter } from './ask-question-emitter.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
31
|
+
import { AskCard, estimateAskCardRows } from './widgets/ask-card.js';
|
|
32
|
+
import { ConfirmCard, estimateConfirmCardRows } from './widgets/confirm-card.js';
|
|
33
|
+
import { cardSlotEmitter } from './card-slot.js';
|
|
34
|
+
import { useCardRequest } from './use-card-request.js';
|
|
35
|
+
import { FilesCard } from './widgets/files-card.js';
|
|
36
|
+
import { filesCardEmitter } from './files-card-emitter.js';
|
|
37
|
+
import { uiWidth } from '../renderer/ui-width.js';
|
|
29
38
|
import { runModeCommand } from '../repl/mode-cycle.js';
|
|
30
39
|
import { enqueueSteering } from '../agent/steering-queue.js';
|
|
40
|
+
import { steeringEchoLine, classifyMidTurnLine, midTurnSlashHint } from '../renderer/steering-echo.js';
|
|
41
|
+
// Moved to renderer/steering-echo.ts (piece 2 I1); re-exported for existing importers.
|
|
42
|
+
export { STEERING_ECHO_MAX, formatSteeringEcho } from '../renderer/steering-echo.js';
|
|
31
43
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* were queued (`…` alone when just the first line was clipped). Exported
|
|
37
|
-
* for the format-pinning test.
|
|
44
|
+
* I1 (spec §7) — slot occupants that ASK the user something. While one of
|
|
45
|
+
* these waits, the input box reads "› (answer above to continue)". The
|
|
46
|
+
* timeline, rewind and shortcuts panels also disable the box, but show no
|
|
47
|
+
* words.
|
|
38
48
|
*/
|
|
39
|
-
|
|
40
|
-
export function
|
|
41
|
-
|
|
42
|
-
const first = lines[0] ?? '';
|
|
43
|
-
const extra = lines.length - 1;
|
|
44
|
-
const clipped = first.length > STEERING_ECHO_MAX;
|
|
45
|
-
let out = clipped ? first.slice(0, STEERING_ECHO_MAX) : first;
|
|
46
|
-
if (clipped || extra > 0)
|
|
47
|
-
out += '…';
|
|
48
|
-
if (extra > 0)
|
|
49
|
-
out += ` (+${extra} line${extra === 1 ? '' : 's'})`;
|
|
50
|
-
return out;
|
|
49
|
+
const ASKING_OCCUPANTS = new Set(['ask', 'confirm', 'approval', 'coaching', 'files']);
|
|
50
|
+
export function isAskingOccupant(occupant) {
|
|
51
|
+
return ASKING_OCCUPANTS.has(occupant);
|
|
51
52
|
}
|
|
52
53
|
/**
|
|
53
|
-
* App — inline-mode Ink frame
|
|
54
|
-
*
|
|
54
|
+
* App — inline-mode Ink frame. Live area, top to bottom (CLI look piece 2,
|
|
55
|
+
* spec §2 — the Claude Code layout):
|
|
55
56
|
*
|
|
56
|
-
*
|
|
57
|
-
* <
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* <TurnStatus />
|
|
61
|
-
* <
|
|
62
|
-
* <
|
|
57
|
+
* <Body /> ← <Static> scrollback (unchanged)
|
|
58
|
+
* <Checklist /> ← live plan checklist (P2, spec §4)
|
|
59
|
+
* card slot ← ONE of approval / confirm / ask / coaching / files /
|
|
60
|
+
* rewind / timeline / shortcuts (pickSlotOccupant)
|
|
61
|
+
* <TurnStatus /> ← only while working and no card waits
|
|
62
|
+
* <Footer /> ← the input box
|
|
63
|
+
* <FooterLine /> ← system · user · transport · mode · spend, `? shortcuts`
|
|
64
|
+
* palettes ← `/` commands, `@` files — under the footer
|
|
63
65
|
*
|
|
64
|
-
*
|
|
65
|
-
* native scrollback handles all of it — mouse wheel, PgUp, search,
|
|
66
|
-
* everything just works because we render inline like a normal CLI.
|
|
66
|
+
* The terminal's native scrollback handles everything above the live area.
|
|
67
67
|
*/
|
|
68
|
-
export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger, getToolCalls, getTodos, getRewindCandidates, onRewindRestore, sapRole, sapAlias, onInterrupt }) {
|
|
68
|
+
export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger, getToolCalls, getTodos, getRewindCandidates, onRewindRestore, sapRole, sapAlias, onInterrupt, onExitRequest }) {
|
|
69
69
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
70
70
|
// Task 3 review I1(b): synchronous mirror + monotonic remount key — the same
|
|
71
71
|
// I2 treatment ask-question already has. A second approval request arriving
|
|
@@ -81,8 +81,8 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
81
81
|
// lag an arrival that happened during the await).
|
|
82
82
|
const coachingPendingRef = useRef(null);
|
|
83
83
|
// UX Wave 2 / Task 2 — discriminated union: v1 single-question requests
|
|
84
|
-
//
|
|
85
|
-
//
|
|
84
|
+
// and v2 form requests ('questions' in req) both render <AskCard>
|
|
85
|
+
// (piece 2 / Q2). All the input gates below only check
|
|
86
86
|
// truthiness, so both shapes suppress the global handlers identically.
|
|
87
87
|
const [askQuestionPending, setAskQuestionPending] = useState(null);
|
|
88
88
|
// Review fix (I2): synchronous mirror of the pending request so a second
|
|
@@ -90,8 +90,8 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
90
90
|
// the first one — otherwise the first tool call's promise leaks and
|
|
91
91
|
// hangs forever.
|
|
92
92
|
const askQuestionPendingRef = useRef(null);
|
|
93
|
-
// Review fix (I2, + T2 rider): monotonic counter keying <
|
|
94
|
-
//
|
|
93
|
+
// Review fix (I2, + T2 rider): monotonic counter keying <AskCard>
|
|
94
|
+
// (v1 and forms) so a replacing request REMOUNTS the widget instead
|
|
95
95
|
// of React reusing the instance with stale state (v1→v1 overlap had the
|
|
96
96
|
// same stale-state exposure as v2→v2). Deliberately NOT the formId /
|
|
97
97
|
// question id (collision caveat — two requests may legitimately share
|
|
@@ -104,13 +104,15 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
104
104
|
// Footer via `prefill`. Replaces the old bare-`/`-only palette flow.
|
|
105
105
|
const [lineResolution, setLineResolution] = useState(null);
|
|
106
106
|
const [prefill, setPrefill] = useState({ text: '', nonce: 0 });
|
|
107
|
-
const [errorStack, setErrorStack] = useState([]);
|
|
108
107
|
const [showTimeline, setShowTimeline] = useState(false);
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
108
|
+
// Piece 2 I2 (spec §8) — the `?` shortcuts sheet (card-slot occupant 'shortcuts').
|
|
109
|
+
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
110
|
+
// P2 (spec §4) — the live checklist. Seeded from getTodos() so --resume
|
|
111
|
+
// shows the persisted plan before any todo_set fires in this process; a
|
|
112
|
+
// plan that was already finished stays closed.
|
|
113
113
|
const [todos, setTodos] = useState(() => getTodos?.() ?? []);
|
|
114
|
+
const [planClosed, setPlanClosed] = useState(() => isPlanFinished(getTodos?.() ?? []));
|
|
115
|
+
const [checklistExpanded, setChecklistExpanded] = useState(false);
|
|
114
116
|
// Task 3 (ux-wave3) — the Esc-Esc / /rewind panel + its lazily-loaded
|
|
115
117
|
// candidate list. Candidates come from an ASYNC accessor (snapshots.list
|
|
116
118
|
// reads fs), so the open handler loads them BEFORE flipping the panel on —
|
|
@@ -120,6 +122,24 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
120
122
|
// Two-Esc chord detector: timestamp of the last lone Esc seen at idle.
|
|
121
123
|
const lastEscRef = useRef(0);
|
|
122
124
|
const [footerValue, setFooterValue] = useState('');
|
|
125
|
+
// Piece 2 F3 — terminal size for the live-area height budget (spec §2).
|
|
126
|
+
// ink-testing-library reports no rows, so 24 (the Review Focus 1 case) is the fallback.
|
|
127
|
+
const { stdout } = useStdout();
|
|
128
|
+
const columns = stdout?.columns ?? 80;
|
|
129
|
+
const rows = stdout?.rows ?? 24;
|
|
130
|
+
// P2 (F3 review carry-over) — Ink's useStdout does not re-render on a
|
|
131
|
+
// terminal resize, so rows/columns/budget would go stale. Bump a state value
|
|
132
|
+
// on 'resize' so this render reads the new size.
|
|
133
|
+
const [, setResizeTick] = useState(0);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!stdout)
|
|
136
|
+
return;
|
|
137
|
+
const onResize = () => setResizeTick((n) => n + 1);
|
|
138
|
+
stdout.on('resize', onResize);
|
|
139
|
+
return () => { stdout.off('resize', onResize); };
|
|
140
|
+
}, [stdout]);
|
|
141
|
+
// The wrapped input's WANTED height. I1 feeds it from the Footer.
|
|
142
|
+
const [inputLines, setInputLines] = useState(1);
|
|
123
143
|
// Task 7 (ux-wave1) — continuation-buffer depth mirrored up from the
|
|
124
144
|
// Footer. While a multiline draft is pending the input line is EMPTY
|
|
125
145
|
// between continuation lines, so every footerValue === '' live trigger
|
|
@@ -128,15 +148,15 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
128
148
|
// all live triggers stay dormant while the buffer is non-empty.
|
|
129
149
|
const [footerBufferCount, setFooterBufferCount] = useState(0);
|
|
130
150
|
// Wave-2 review I-1: model-driven overlays (approval / ask-question /
|
|
131
|
-
// coaching) must own input EXCLUSIVELY.
|
|
132
|
-
//
|
|
151
|
+
// coaching) must own input EXCLUSIVELY. A passive panel (the ? timeline,
|
|
152
|
+
// the rewind panel) can be up when a request arrives —
|
|
133
153
|
// leaving BOTH useInputs active. Ink delivers every keypress to every
|
|
134
154
|
// active subscriber (no stopPropagation), so one Esc would close the
|
|
135
155
|
// panel AND fire the approval modal's onCancel — silently declining a
|
|
136
156
|
// pending write; `q` typed into a free-text ask-form answer would close
|
|
137
157
|
// the panel behind it; and panel + modal stacked can overflow a 24-row
|
|
138
158
|
// terminal. Fix at the arrival site: evict the passive panels BEFORE
|
|
139
|
-
// mounting the modal. (The user can
|
|
159
|
+
// mounting the modal. (The user can press ? again after answering.)
|
|
140
160
|
useEffect(() => {
|
|
141
161
|
const onReq = (req) => {
|
|
142
162
|
// I1(b) — a replacing request must not leak the previous producer's
|
|
@@ -149,8 +169,8 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
149
169
|
}
|
|
150
170
|
pendingApprovalRef.current = req;
|
|
151
171
|
setApprovalSeq((s) => s + 1);
|
|
152
|
-
setShowTodoPanel(false); // I-1 — evict passive panels, see block comment
|
|
153
172
|
setShowTimeline(false);
|
|
173
|
+
setShowShortcuts(false);
|
|
154
174
|
setShowRewindPanel(false); // Task 3 — rewind panel joins the eviction family
|
|
155
175
|
lastEscRef.current = 0; // M3 — a mounting overlay kills any armed Esc chord
|
|
156
176
|
setPendingApproval(req);
|
|
@@ -163,8 +183,8 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
163
183
|
useEffect(() => {
|
|
164
184
|
const onRequest = (req) => {
|
|
165
185
|
coachingPendingRef.current = req; // I1(c) — synchronous mirror
|
|
166
|
-
setShowTodoPanel(false); // I-1 — same eviction as approval/ask-question
|
|
167
186
|
setShowTimeline(false);
|
|
187
|
+
setShowShortcuts(false);
|
|
168
188
|
setShowRewindPanel(false); // Task 3 — rewind panel joins the eviction family
|
|
169
189
|
lastEscRef.current = 0; // M3 — a mounting overlay kills any armed Esc chord
|
|
170
190
|
setCoachingPending(req);
|
|
@@ -190,8 +210,8 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
190
210
|
}
|
|
191
211
|
askQuestionPendingRef.current = req;
|
|
192
212
|
setAskQuestionSeq((s) => s + 1);
|
|
193
|
-
setShowTodoPanel(false); // I-1 — evict passive panels, see approval block
|
|
194
213
|
setShowTimeline(false);
|
|
214
|
+
setShowShortcuts(false);
|
|
195
215
|
setShowRewindPanel(false); // Task 3 — rewind panel joins the eviction family
|
|
196
216
|
lastEscRef.current = 0; // M3 — a mounting overlay kills any armed Esc chord
|
|
197
217
|
setAskQuestionPending(req);
|
|
@@ -199,29 +219,42 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
199
219
|
askQuestionEmitter.on('request', onRequest);
|
|
200
220
|
return () => { askQuestionEmitter.off('request', onRequest); };
|
|
201
221
|
}, []);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
const confirmSlot = useCardRequest((l) => { cardSlotEmitter.on('confirm', l); return () => { cardSlotEmitter.off('confirm', l); }; }, { value: null, cancelled: true }, () => {
|
|
223
|
+
// Same eviction the approval / ask listeners do on arrival.
|
|
224
|
+
setShowTimeline(false);
|
|
225
|
+
setShowShortcuts(false);
|
|
226
|
+
setShowRewindPanel(false);
|
|
227
|
+
lastEscRef.current = 0;
|
|
228
|
+
});
|
|
229
|
+
// Piece 2 §6.4 — the `/files` search-and-list card (occupant 'files').
|
|
230
|
+
// Same request rules as the confirm slot: a replaced or orphaned pick
|
|
231
|
+
// resolves cancelled, so /files never wedges.
|
|
232
|
+
const filesSlot = useCardRequest((l) => { filesCardEmitter.on('files', l); return () => { filesCardEmitter.off('files', l); }; }, { value: null, cancelled: true }, () => { setShowTimeline(false); setShowShortcuts(false); setShowRewindPanel(false); lastEscRef.current = 0; });
|
|
233
|
+
// P2 (spec §4) — todo_set fan-out → the live checklist. 'update' redraws in
|
|
234
|
+
// place (an empty list hides it); 'close' (turn end, plan finished) hides it
|
|
235
|
+
// until the next 'update'. Re-seed from getTodos AFTER subscribing
|
|
236
|
+
// (TurnStatus snapshot pattern): an update fired between mount and this
|
|
237
|
+
// effect would otherwise be missed.
|
|
205
238
|
useEffect(() => {
|
|
206
|
-
const onUpdate = (t) =>
|
|
239
|
+
const onUpdate = (t) => {
|
|
240
|
+
setTodos(t);
|
|
241
|
+
setPlanClosed(t.length === 0);
|
|
242
|
+
// Fix round 1 — an emptied list (/new, cleared plan) folds Ctrl+T's
|
|
243
|
+
// expand back, so the next plan opens folded.
|
|
244
|
+
if (t.length === 0)
|
|
245
|
+
setChecklistExpanded(false);
|
|
246
|
+
};
|
|
247
|
+
const onClose = () => { setPlanClosed(true); setChecklistExpanded(false); };
|
|
207
248
|
todoEmitter.on('update', onUpdate);
|
|
208
|
-
|
|
209
|
-
|
|
249
|
+
todoEmitter.on('close', onClose);
|
|
250
|
+
const seeded = getTodos?.() ?? [];
|
|
251
|
+
setTodos(seeded);
|
|
252
|
+
setPlanClosed(isPlanFinished(seeded));
|
|
253
|
+
return () => { todoEmitter.off('update', onUpdate); todoEmitter.off('close', onClose); };
|
|
210
254
|
// getTodos is stable for the life of the mount (repl.tsx passes a
|
|
211
255
|
// closure over the session object) — mount-once is intentional.
|
|
212
256
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
213
257
|
}, []);
|
|
214
|
-
useEffect(() => {
|
|
215
|
-
const onError = (err) => {
|
|
216
|
-
const id = crypto.randomUUID();
|
|
217
|
-
setErrorStack((s) => [...s, { ...err, id }]);
|
|
218
|
-
setTimeout(() => {
|
|
219
|
-
setErrorStack((s) => s.filter((e) => e.id !== id));
|
|
220
|
-
}, 5000);
|
|
221
|
-
};
|
|
222
|
-
errorEmitter.on('error', onError);
|
|
223
|
-
return () => { errorEmitter.off('error', onError); };
|
|
224
|
-
}, []);
|
|
225
258
|
// Task 3 (ux-wave3) — load candidates (async fs read) THEN show the panel, so
|
|
226
259
|
// it never renders against a stale/empty list. Shared by the Esc-Esc chord and
|
|
227
260
|
// the /rewind builtin (which signals through rewindEmitter — <App> is a
|
|
@@ -254,7 +287,7 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
254
287
|
onSubmit(step.line);
|
|
255
288
|
return;
|
|
256
289
|
case 'prefill':
|
|
257
|
-
chunkEmitter.emit('chunk',
|
|
290
|
+
chunkEmitter.emit('chunk', chalk.dim(step.hint) + '\n');
|
|
258
291
|
setPrefill((p) => ({ text: step.text, nonce: p.nonce + 1 }));
|
|
259
292
|
return;
|
|
260
293
|
case 'open-slash':
|
|
@@ -284,18 +317,31 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
284
317
|
// message at the next round boundary — clear the box, and confirm it
|
|
285
318
|
// landed. (Was: bounce the input and tell them to wait.)
|
|
286
319
|
const t = line.trim();
|
|
320
|
+
// Fix round 3 (item 5): slash commands are never steering text.
|
|
321
|
+
const kind = classifyMidTurnLine(t);
|
|
322
|
+
if (kind.kind === 'exit') {
|
|
323
|
+
setPrefill((p) => ({ text: '', nonce: p.nonce + 1 }));
|
|
324
|
+
onExitRequest?.();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (kind.kind === 'slash') {
|
|
328
|
+
// Not sent. The text stays in the input so Enter runs it after the turn.
|
|
329
|
+
chunkEmitter.emit('chunk', midTurnSlashHint(kind.command));
|
|
330
|
+
setPrefill((p) => ({ text: line, nonce: p.nonce + 1 }));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
287
333
|
if (t.length > 0) {
|
|
288
334
|
enqueueSteering(t);
|
|
289
335
|
// Fast-follow: echo a truncated receipt, not the full (possibly
|
|
290
336
|
// multiline-pasted) text — the model still gets `t` verbatim.
|
|
291
|
-
chunkEmitter.emit('chunk',
|
|
337
|
+
chunkEmitter.emit('chunk', steeringEchoLine(t));
|
|
292
338
|
}
|
|
293
339
|
setPrefill((p) => ({ text: '', nonce: p.nonce + 1 }));
|
|
294
340
|
return;
|
|
295
341
|
}
|
|
296
342
|
const files = listWorkspaceFileChoices();
|
|
297
343
|
handleStep(nextResolutionStep(line, files), line, files);
|
|
298
|
-
}, [handleStep, chunkEmitter]);
|
|
344
|
+
}, [handleStep, chunkEmitter, onExitRequest]);
|
|
299
345
|
// Palette cancel: restore the user's original line so they can edit it.
|
|
300
346
|
const cancelResolution = useCallback(() => {
|
|
301
347
|
const lr = lineResolution;
|
|
@@ -304,25 +350,25 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
304
350
|
setPrefill((p) => ({ text: lr.original, nonce: p.nonce + 1 }));
|
|
305
351
|
}
|
|
306
352
|
}, [lineResolution]);
|
|
307
|
-
//
|
|
308
|
-
//
|
|
353
|
+
// Piece 2 (spec §8) — `?` on an EMPTY input opens the shortcuts sheet in the
|
|
354
|
+
// card slot. Pasted text never triggers it (the paste guard marks paste
|
|
355
|
+
// chunks — repl/paste-marker.ts).
|
|
309
356
|
//
|
|
310
357
|
// Keystroke-leak note (Task 5, verified by probe): Ink delivers every
|
|
311
358
|
// keypress to EVERY active useInput subscriber — there is no
|
|
312
359
|
// stopPropagation — so a live-trigger character ALSO lands in the Footer's
|
|
313
|
-
// TextInput.
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
// since Task 5). Every live trigger in this handler family does the same.
|
|
360
|
+
// TextInput. The working mechanism is an explicit swallow: bump `prefill`
|
|
361
|
+
// with empty text, which the Footer applies on the next commit (footer.tsx
|
|
362
|
+
// honours empty prefills since Task 5). Every live trigger in this handler
|
|
363
|
+
// family does the same.
|
|
318
364
|
useInput((input, _key) => {
|
|
319
365
|
if (footerBufferCount > 0)
|
|
320
366
|
return; // Task 7 — mid-composition, stay dormant
|
|
321
|
-
if (input === '?' && footerValue === '') {
|
|
322
|
-
|
|
367
|
+
if (input === '?' && footerValue === '' && !wasPasted(input)) {
|
|
368
|
+
setShowShortcuts(true);
|
|
323
369
|
setPrefill((p) => ({ text: '', nonce: p.nonce + 1 })); // swallow leaked '?'
|
|
324
370
|
}
|
|
325
|
-
}, { isActive: !lineResolution && !showTimeline && !
|
|
371
|
+
}, { isActive: !lineResolution && !showTimeline && !showShortcuts && !showRewindPanel && !pendingApproval && !coachingPending && !askQuestionPending && !confirmSlot.pending && !filesSlot.pending });
|
|
326
372
|
// Task 5 (ux-wave1) — live `/` opens the command palette immediately
|
|
327
373
|
// (CC-style), no Enter needed. Fill-in semantics: because the original
|
|
328
374
|
// line is empty, a pick PREFILLS `/{command} ` instead of running (the
|
|
@@ -338,7 +384,7 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
338
384
|
return; // BUG-2b: steering, not palette (covers prep window)
|
|
339
385
|
setPrefill((p) => ({ text: '', nonce: p.nonce + 1 })); // swallow leaked '/'
|
|
340
386
|
handleSlashTrigger();
|
|
341
|
-
}, { isActive: !lineResolution && !showTimeline && !
|
|
387
|
+
}, { isActive: !lineResolution && !showTimeline && !showShortcuts && !showRewindPanel && !pendingApproval && !coachingPending && !askQuestionPending && !confirmSlot.pending && !filesSlot.pending });
|
|
342
388
|
// Task 6 (ux-wave1) — live `@` opens the file palette with APPEND
|
|
343
389
|
// semantics: the partial line survives and the picked token lands where
|
|
344
390
|
// the user was typing (pick prefills `footerValue + '@' + name + ' '`
|
|
@@ -374,7 +420,7 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
374
420
|
return; // nothing to pick — let `@` land as plain text
|
|
375
421
|
setPrefill((p) => ({ text: footerValue, nonce: p.nonce + 1 })); // swallow leaked '@', keep the line
|
|
376
422
|
setLineResolution({ mode: 'at', prefix: '', line: `${footerValue}@`, original: `${footerValue}@`, files });
|
|
377
|
-
}, { isActive: !lineResolution && !showTimeline && !
|
|
423
|
+
}, { isActive: !lineResolution && !showTimeline && !showShortcuts && !showRewindPanel && !pendingApproval && !coachingPending && !askQuestionPending && !confirmSlot.pending && !filesSlot.pending });
|
|
378
424
|
// Task 3 (ux-wave1) — Shift+Tab / Alt+M cycle the session write mode.
|
|
379
425
|
// Same gating as the `?` handler: only when the footer is empty and no
|
|
380
426
|
// overlay owns input. Windows Terminal sends backtab as CSI Z, which Ink
|
|
@@ -401,25 +447,14 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
401
447
|
if (!shiftTab && !altM)
|
|
402
448
|
return;
|
|
403
449
|
const { line, mode } = runModeCommand(undefined, globalStore.get().writeMode ?? 'approval-gated', sapRole, sapAlias);
|
|
404
|
-
chunkEmitter.emit('chunk',
|
|
450
|
+
chunkEmitter.emit('chunk', chalk.dim(line) + '\n');
|
|
405
451
|
globalStore.update({ writeMode: mode });
|
|
406
452
|
// Alt+M's 'm' also lands in the TextInput (see the leak note on the
|
|
407
453
|
// `?` handler) — swallow it. Backtab is harmless (TextInput ignores
|
|
408
454
|
// tab), but the swallow is a no-op for it anyway.
|
|
409
455
|
setPrefill((p) => ({ text: '', nonce: p.nonce + 1 }));
|
|
410
|
-
}, { isActive: !lineResolution && !showTimeline && !
|
|
411
|
-
//
|
|
412
|
-
// the Wave-1 live triggers (no overlay owns input, footer empty, no
|
|
413
|
-
// multiline draft) with ONE deliberate difference: mid-turn is ALLOWED —
|
|
414
|
-
// no turnStatusEmitter guard. Peeking at the model's task list WHILE it
|
|
415
|
-
// works is the point of the panel (the turn-status strip shows only the
|
|
416
|
-
// current item; Ctrl+T shows the whole plan). Closing is the panel's
|
|
417
|
-
// job (Esc/q/Ctrl+T in TodoPanel's useInput) — this handler is inactive
|
|
418
|
-
// while the panel is up, so the chord toggles instead of double-firing.
|
|
419
|
-
// Open re-reads getTodos(): cheap, and correct after a session resume
|
|
420
|
-
// (the emitter only fires on new todo_set calls). The leaked 't' is
|
|
421
|
-
// swallowed with an empty prefill — same mechanism as `?` / Alt+M
|
|
422
|
-
// (footerValue is '' by gate, so the empty prefill is lossless).
|
|
456
|
+
}, { isActive: !lineResolution && !showTimeline && !showShortcuts && !showRewindPanel && !pendingApproval && !coachingPending && !askQuestionPending && !confirmSlot.pending && !filesSlot.pending });
|
|
457
|
+
// P2 — Ctrl+T toggles the full checklist (spec §4).
|
|
423
458
|
useInput((input, key) => {
|
|
424
459
|
if (!(key.ctrl && (input === 't' || input === 'T')))
|
|
425
460
|
return;
|
|
@@ -427,10 +462,11 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
427
462
|
return;
|
|
428
463
|
if (footerBufferCount > 0)
|
|
429
464
|
return; // Task 7 — mid-composition, stay dormant
|
|
430
|
-
|
|
431
|
-
|
|
465
|
+
// P2 — Ctrl+T expands / folds the live checklist in place (spec §4).
|
|
466
|
+
// Mid-turn is allowed (watching the plan while it runs is the point).
|
|
467
|
+
setChecklistExpanded((e) => !e);
|
|
432
468
|
setPrefill((p) => ({ text: '', nonce: p.nonce + 1 })); // swallow leaked 't'
|
|
433
|
-
}, { isActive: !lineResolution && !showTimeline && !
|
|
469
|
+
}, { isActive: !lineResolution && !showTimeline && !showShortcuts && !showRewindPanel && !pendingApproval && !coachingPending && !askQuestionPending && !confirmSlot.pending && !filesSlot.pending });
|
|
434
470
|
// Task 3 (ux-wave3) — Esc-Esc opens the rewind panel. Two Esc within 500ms
|
|
435
471
|
// at IDLE ONLY. The isActive gate is the full five-gate family (no overlay
|
|
436
472
|
// owns input, footer empty, no multiline draft) so a single Esc keeps its
|
|
@@ -457,15 +493,35 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
457
493
|
lastEscRef.current = now;
|
|
458
494
|
}
|
|
459
495
|
}, {
|
|
460
|
-
isActive: !lineResolution && !showTimeline && !
|
|
461
|
-
&& !pendingApproval && !coachingPending && !askQuestionPending
|
|
496
|
+
isActive: !lineResolution && !showTimeline && !showShortcuts && !showRewindPanel
|
|
497
|
+
&& !pendingApproval && !coachingPending && !askQuestionPending && !confirmSlot.pending && !filesSlot.pending
|
|
462
498
|
&& footerValue === '' && footerBufferCount === 0,
|
|
463
499
|
});
|
|
464
|
-
// Ctrl+C handler — abort in-flight turn or
|
|
500
|
+
// Ctrl+C handler — abort in-flight turn, or arm / confirm the idle exit.
|
|
501
|
+
// Fix round 3 (item 4): while a card, sheet or palette is open, Ctrl+C
|
|
502
|
+
// belongs to it (each cancels itself like Esc) and never reaches the REPL —
|
|
503
|
+
// it used to exit the whole CLI from the skill picker.
|
|
504
|
+
const overlayOpen = !!pendingApproval || !!coachingPending || !!askQuestionPending
|
|
505
|
+
|| confirmSlot.pending !== null || filesSlot.pending !== null
|
|
506
|
+
|| !!lineResolution || showTimeline || showShortcuts || showRewindPanel;
|
|
507
|
+
const [exitHint, setExitHint] = useState(false);
|
|
508
|
+
const exitHintTimer = useRef(null);
|
|
509
|
+
useEffect(() => () => { if (exitHintTimer.current)
|
|
510
|
+
clearTimeout(exitHintTimer.current); }, []);
|
|
465
511
|
useInput((input, key) => {
|
|
466
|
-
if (key.ctrl && input === 'c')
|
|
467
|
-
|
|
468
|
-
|
|
512
|
+
if (!(key.ctrl && input === 'c'))
|
|
513
|
+
return;
|
|
514
|
+
const r = onInterrupt?.();
|
|
515
|
+
if (exitHintTimer.current)
|
|
516
|
+
clearTimeout(exitHintTimer.current);
|
|
517
|
+
if (r === 'armed') {
|
|
518
|
+
setExitHint(true);
|
|
519
|
+
exitHintTimer.current = setTimeout(() => setExitHint(false), IDLE_EXIT_WINDOW_MS);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
setExitHint(false);
|
|
523
|
+
}
|
|
524
|
+
}, { isActive: !overlayOpen });
|
|
469
525
|
// I1(b) — clear ONLY when this request still owns the slot: a replacing
|
|
470
526
|
// request (resolved-as-superseded in onReq) may already be pending, and the
|
|
471
527
|
// old modal's resolve callback must not evict it.
|
|
@@ -490,40 +546,134 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
490
546
|
.filter((c) => c.completed === true)
|
|
491
547
|
.map(toTimelineEntry)
|
|
492
548
|
: [];
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
549
|
+
// Spec §2 / §1.6 — exactly ONE card in the slot.
|
|
550
|
+
// piece 2 / Q2 — clear the slot only while this request is still the
|
|
551
|
+
// pending one; a replacing request (I2) may already own it.
|
|
552
|
+
const clearAskIfCurrent = (req) => {
|
|
553
|
+
if (askQuestionPendingRef.current === req) {
|
|
554
|
+
askQuestionPendingRef.current = null;
|
|
555
|
+
setAskQuestionPending(null);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const occupant = pickSlotOccupant({
|
|
559
|
+
approval: !!pendingApproval,
|
|
560
|
+
confirm: confirmSlot.pending !== null,
|
|
561
|
+
ask: !!askQuestionPending,
|
|
562
|
+
coaching: !!coachingPending,
|
|
563
|
+
files: filesSlot.pending !== null,
|
|
564
|
+
rewind: showRewindPanel,
|
|
565
|
+
timeline: showTimeline,
|
|
566
|
+
shortcuts: showShortcuts,
|
|
567
|
+
});
|
|
568
|
+
// I1 (spec §7) — "(answer above to continue)" only while the occupant asks.
|
|
569
|
+
const waitingForCard = isAskingOccupant(occupant);
|
|
570
|
+
// Spec §2 height cap: the live area never exceeds rows − 2. Each occupant
|
|
571
|
+
// contributes a fixed estimate. Q2 (ask) and C1 (confirm) replace theirs
|
|
572
|
+
// with a real estimator right below this line. P2 replaces `checklistRows: 0`.
|
|
573
|
+
let cardRows = occupant === null ? 0 : SLOT_ROWS_ESTIMATE[occupant];
|
|
574
|
+
// Q2 — the ask card's full height (descriptions included, so shedding them
|
|
575
|
+
// cannot oscillate; spec §2 fold order: descriptions drop before options).
|
|
576
|
+
if (occupant === 'ask' && askQuestionPending)
|
|
577
|
+
cardRows = estimateAskCardRows(askQuestionPending, true, uiWidth(columns));
|
|
578
|
+
// C1 — the confirm card's real height (facts + choices) for the spec §2 cap.
|
|
579
|
+
if (occupant === 'confirm' && confirmSlot.pending)
|
|
580
|
+
cardRows = estimateConfirmCardRows(confirmSlot.pending);
|
|
581
|
+
const paletteRows = lineResolution ? PALETTE_ROWS : 0;
|
|
582
|
+
// P2 — the checklist's WANTED height (header + rows); liveBudget folds it.
|
|
583
|
+
const showChecklist = todos.length > 0 && !planClosed;
|
|
584
|
+
const checklistWidth = uiWidth(columns);
|
|
585
|
+
const checklistDesiredRows = showChecklist
|
|
586
|
+
? formatChecklist(todos, {
|
|
587
|
+
width: checklistWidth,
|
|
588
|
+
maxRows: checklistExpanded ? todos.length : CHECKLIST_MAX_ROWS,
|
|
589
|
+
expanded: checklistExpanded,
|
|
590
|
+
foldedToOne: false,
|
|
591
|
+
}).length
|
|
592
|
+
: 0;
|
|
593
|
+
const budget = liveBudget({ rows, checklistRows: checklistDesiredRows, cardRows, inputLines, paletteRows });
|
|
594
|
+
let slot = null;
|
|
595
|
+
if (occupant === 'approval' && pendingApproval) {
|
|
596
|
+
slot = (_jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(ApprovalModal, { change: pendingApproval.change, effectiveRisk: pendingApproval.effectiveRisk, transport: pendingApproval.transport, onApply: () => {
|
|
597
|
+
pendingApproval.resolve({ approved: true });
|
|
598
|
+
clearApproval(pendingApproval);
|
|
599
|
+
}, onSkip: () => {
|
|
600
|
+
pendingApproval.resolve({ approved: false, reason: 'skipped' });
|
|
601
|
+
clearApproval(pendingApproval);
|
|
602
|
+
}, onDiff: () => { }, onCancel: () => {
|
|
603
|
+
pendingApproval.resolve({ approved: false, reason: '__cancel_turn__' });
|
|
604
|
+
clearApproval(pendingApproval);
|
|
605
|
+
}, onInterruptTurn: () => { onInterrupt?.(); } }, approvalSeq) }));
|
|
606
|
+
}
|
|
607
|
+
else if (occupant === 'confirm' && confirmSlot.pending) {
|
|
608
|
+
const req = confirmSlot.pending;
|
|
609
|
+
slot = (_jsx(ConfirmCard, { request: req, width: uiWidth(columns), onResolve: (r) => confirmSlot.finish(req, r), onInterruptTurn: () => { onInterrupt?.(); } }, confirmSlot.seq));
|
|
610
|
+
}
|
|
611
|
+
else if (occupant === 'ask' && askQuestionPending) {
|
|
612
|
+
const req = askQuestionPending;
|
|
613
|
+
slot = (_jsx(AskCard, { request: req, width: uiWidth(columns), showDescriptions: budget.showDescriptions, onResolveV1: (result) => {
|
|
614
|
+
if (!('questions' in req))
|
|
615
|
+
req.resolve(result);
|
|
616
|
+
clearAskIfCurrent(req);
|
|
617
|
+
}, onResolveForm: (result) => {
|
|
618
|
+
if ('questions' in req)
|
|
619
|
+
req.resolve(result);
|
|
620
|
+
clearAskIfCurrent(req);
|
|
621
|
+
}, onInterruptTurn: () => { onInterrupt?.(); } }, askQuestionSeq));
|
|
622
|
+
}
|
|
623
|
+
else if (occupant === 'coaching' && coachingPending) {
|
|
624
|
+
const req = coachingPending;
|
|
625
|
+
slot = (_jsx(CoachingPicker, { params: req.params, width: uiWidth(columns), onResolve: (result) => {
|
|
626
|
+
req.resolve(result);
|
|
627
|
+
if (coachingPendingRef.current === req) {
|
|
628
|
+
coachingPendingRef.current = null; // I1(c) — keep the mirror true
|
|
629
|
+
}
|
|
630
|
+
setCoachingPending(null);
|
|
631
|
+
} }));
|
|
632
|
+
}
|
|
633
|
+
else if (occupant === 'files' && filesSlot.pending) {
|
|
634
|
+
const req = filesSlot.pending;
|
|
635
|
+
slot = (_jsx(FilesCard, { request: req, width: uiWidth(columns), onResolve: (r) => filesSlot.finish(req, r) }, filesSlot.seq));
|
|
636
|
+
}
|
|
637
|
+
else if (occupant === 'rewind') {
|
|
638
|
+
slot = (_jsx(Box, { marginTop: 1, children: _jsx(RewindPanel, { width: uiWidth(columns), candidates: rewindCandidates,
|
|
639
|
+
// Panel confirms, then fires the gated executor (repl.tsx assembles
|
|
640
|
+
// deps + bridges approval to the Ink modal) and closes itself.
|
|
641
|
+
onRestore: (c) => { onRewindRestore?.(c); }, onClose: () => {
|
|
642
|
+
setShowRewindPanel(false);
|
|
643
|
+
// P2 — /rewind restores the checklist from session.todos (spec §4).
|
|
644
|
+
const list = getTodos?.() ?? [];
|
|
645
|
+
setTodos(list);
|
|
646
|
+
setPlanClosed(isPlanFinished(list));
|
|
647
|
+
} }) }));
|
|
648
|
+
}
|
|
649
|
+
else if (occupant === 'timeline') {
|
|
650
|
+
slot = (_jsx(Box, { marginTop: 1, children: _jsx(SessionTimeline, { width: uiWidth(columns), entries: timelineEntries, onClose: () => setShowTimeline(false) }) }));
|
|
651
|
+
}
|
|
652
|
+
else if (occupant === 'shortcuts') {
|
|
653
|
+
slot = _jsx(ShortcutsSheet, { width: uiWidth(columns), onClose: () => setShowShortcuts(false) });
|
|
654
|
+
}
|
|
655
|
+
return (_jsx(SapStateContext.Provider, { value: globalStore, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Body, { chunkEmitter: chunkEmitter }), showChecklist && (_jsx(Checklist, { todos: todos, width: checklistWidth, maxRows: Math.max(1, budget.checklistRows - 1), expanded: checklistExpanded,
|
|
656
|
+
// liveBudget folds the checklist to 1 row whenever a card is open (occupant !== null).
|
|
657
|
+
foldedToOne: budget.checklistRows <= 1 })), slot, occupant === null && _jsx(TurnStatus, {}), _jsx(Footer, { onSubmit: beginSubmit, onSlashTrigger: handleSlashTrigger, onHistoryTrigger: handleHistoryTrigger, prefill: prefill, onValueChange: setFooterValue, onBufferChange: setFooterBufferCount, maxLines: budget.inputMaxLines, onInputLinesChange: setInputLines,
|
|
658
|
+
// Spec §7: "(answer above to continue)" only while a card that asks
|
|
659
|
+
// waits — not for the timeline / rewind / shortcuts panels, nor an
|
|
660
|
+
// open `/` / `@` palette (those are not slot occupants).
|
|
661
|
+
waitingForCard: waitingForCard,
|
|
514
662
|
// Task 7 — multiline announcements (draft discarded) land as
|
|
515
663
|
// dim scrollback lines, house style for Footer-adjacent notices.
|
|
516
|
-
onNotice: (t) => chunkEmitter.emit('chunk',
|
|
664
|
+
onNotice: (t) => chunkEmitter.emit('chunk', chalk.dim(t) + '\n'),
|
|
517
665
|
// 2026-05-17 — disable Footer input while any modal owns
|
|
518
666
|
// input, so keystrokes don't double-fire (modal answer +
|
|
519
667
|
// spurious Footer submission with same text).
|
|
520
668
|
disabled: !!coachingPending
|
|
521
669
|
|| !!askQuestionPending
|
|
670
|
+
|| confirmSlot.pending !== null
|
|
671
|
+
|| filesSlot.pending !== null
|
|
522
672
|
|| !!pendingApproval
|
|
523
673
|
|| !!lineResolution
|
|
524
674
|
|| showTimeline
|
|
525
|
-
||
|
|
526
|
-
|| showRewindPanel }), lineResolution?.mode === 'slash' && (_jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(CommandPalette, { initialQuery: lineResolution.prefix, onSelect: (name) => {
|
|
675
|
+
|| showShortcuts
|
|
676
|
+
|| showRewindPanel }), exitHint ? _jsx(Text, { ...roleProps('dim'), children: "Press Ctrl+C again to exit" }) : _jsx(FooterLine, {}), lineResolution?.mode === 'slash' && (_jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(CommandPalette, { initialQuery: lineResolution.prefix, onSelect: (name) => {
|
|
527
677
|
const lr = lineResolution;
|
|
528
678
|
setLineResolution(null);
|
|
529
679
|
// Task 5 — empty original ⇔ palette opened by the live `/`
|
|
@@ -537,17 +687,5 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
537
687
|
// (e.g. add `--scope ZMM_VENDOR_CHECK` after `--from @<file>`).
|
|
538
688
|
const resolved = lr.line.replace(/@\S*$/, `@${filename}`);
|
|
539
689
|
setPrefill((p) => ({ text: `${resolved} `, nonce: p.nonce + 1 }));
|
|
540
|
-
}, onCancel: cancelResolution }) }))
|
|
541
|
-
// Panel confirms, then fires the gated executor (repl.tsx assembles
|
|
542
|
-
// deps + bridges approval to the Ink modal) and closes itself.
|
|
543
|
-
onRestore: (c) => { onRewindRestore?.(c); }, onClose: () => setShowRewindPanel(false) }) })), pendingApproval && (_jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(ApprovalModal, { change: pendingApproval.change, effectiveRisk: pendingApproval.effectiveRisk, transport: pendingApproval.transport, onApply: () => {
|
|
544
|
-
pendingApproval.resolve({ approved: true });
|
|
545
|
-
clearApproval(pendingApproval);
|
|
546
|
-
}, onSkip: () => {
|
|
547
|
-
pendingApproval.resolve({ approved: false, reason: 'skipped' });
|
|
548
|
-
clearApproval(pendingApproval);
|
|
549
|
-
}, onDiff: () => { }, onCancel: () => {
|
|
550
|
-
pendingApproval.resolve({ approved: false, reason: '__cancel_turn__' });
|
|
551
|
-
clearApproval(pendingApproval);
|
|
552
|
-
} }, approvalSeq) }))] }) }));
|
|
690
|
+
}, onCancel: cancelResolution }) }))] }) }));
|
|
553
691
|
}
|