linersh 0.5.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -165
- package/bin/liner.js +82 -44
- package/cli-update-docs/ABOUT.md +16 -16
- package/cli-update-docs/AI-HANDOFF.md +62 -190
- package/cli-update-docs/CURATION.md +48 -19
- package/cli-update-docs/JTBD-MASTER-PROMPT-ABOUT.md +9 -12
- package/cli-update-docs/JTBD-MASTER-PROMPT.md +1 -1
- package/cli-update-docs/LINER-MASTER.md +77 -63
- package/cli-update-docs/LINER-MD-PRD.md +531 -0
- package/cli-update-docs/MIXTAPE-FORMAT.md +76 -44
- package/cli-update-docs/PLATFORM.md +47 -46
- package/cli-update-docs/README.md +6 -6
- package/cli-update-docs/SKILL.md +72 -37
- package/cli-update-docs/TUI-BACKLOG.md +205 -26
- package/cli-update-docs/quality-check-tests.md +256 -8
- package/cli-update-docs/source-finding-tactics.md +140 -3
- package/cli-update-docs/source-quality-hierarchy.md +83 -0
- package/cli-update-docs/synthesis-guidance.md +19 -5
- package/dist/agents/artifact-validation.js +440 -0
- package/dist/agents/artifact-validation.js.map +1 -0
- package/dist/agents/candidate-longlist.js +100 -0
- package/dist/agents/candidate-longlist.js.map +1 -0
- package/dist/agents/detect.js +3 -2
- package/dist/agents/detect.js.map +1 -1
- package/dist/agents/evaluation-assembly.js +270 -0
- package/dist/agents/evaluation-assembly.js.map +1 -0
- package/dist/agents/headless-runner.js +227 -0
- package/dist/agents/headless-runner.js.map +1 -0
- package/dist/agents/jtbd-clarify.js +42 -41
- package/dist/agents/jtbd-clarify.js.map +1 -1
- package/dist/agents/models.js +6 -7
- package/dist/agents/models.js.map +1 -1
- package/dist/agents/prompts.js +220 -20
- package/dist/agents/prompts.js.map +1 -1
- package/dist/agents/runner.js +462 -21
- package/dist/agents/runner.js.map +1 -1
- package/dist/phases.js +13 -13
- package/dist/phases.js.map +1 -1
- package/dist/source-inbox.js +297 -0
- package/dist/source-inbox.js.map +1 -0
- package/dist/version.js +3 -9
- package/dist/version.js.map +1 -1
- package/dist/yaml-io.js +42 -11
- package/dist/yaml-io.js.map +1 -1
- package/package.json +26 -23
- package/cli-update-docs/JOB-STORY-MIGRATION.md +0 -99
- package/dist/App.js +0 -220
- package/dist/App.js.map +0 -1
- package/dist/colors.js +0 -132
- package/dist/colors.js.map +0 -1
- package/dist/components/Header.js +0 -15
- package/dist/components/Header.js.map +0 -1
- package/dist/components/KeyHints.js +0 -17
- package/dist/components/KeyHints.js.map +0 -1
- package/dist/components/LabeledBox.js +0 -53
- package/dist/components/LabeledBox.js.map +0 -1
- package/dist/components/NextSteps.js +0 -18
- package/dist/components/NextSteps.js.map +0 -1
- package/dist/index.js +0 -17
- package/dist/index.js.map +0 -1
- package/dist/screens/AgentSetup.js +0 -224
- package/dist/screens/AgentSetup.js.map +0 -1
- package/dist/screens/BootSplash.js +0 -163
- package/dist/screens/BootSplash.js.map +0 -1
- package/dist/screens/CompileView.js +0 -567
- package/dist/screens/CompileView.js.map +0 -1
- package/dist/screens/FramingConfirmReview.js +0 -197
- package/dist/screens/FramingConfirmReview.js.map +0 -1
- package/dist/screens/Gate1Review.js +0 -147
- package/dist/screens/Gate1Review.js.map +0 -1
- package/dist/screens/Gate2Review.js +0 -162
- package/dist/screens/Gate2Review.js.map +0 -1
- package/dist/screens/ImportPrompt.js +0 -173
- package/dist/screens/ImportPrompt.js.map +0 -1
- package/dist/screens/JsSetup.js +0 -77
- package/dist/screens/JsSetup.js.map +0 -1
- package/dist/screens/LocalFilePicker.js +0 -139
- package/dist/screens/LocalFilePicker.js.map +0 -1
- package/dist/screens/NewMixtapeWizard.js +0 -662
- package/dist/screens/NewMixtapeWizard.js.map +0 -1
- package/dist/screens/PhaseRunner.js +0 -1203
- package/dist/screens/PhaseRunner.js.map +0 -1
- package/dist/screens/ProcessManifest.js +0 -160
- package/dist/screens/ProcessManifest.js.map +0 -1
- package/dist/screens/ProjectHub.js +0 -557
- package/dist/screens/ProjectHub.js.map +0 -1
- package/dist/screens/SourceModal.js +0 -269
- package/dist/screens/SourceModal.js.map +0 -1
- package/dist/screens/TapeBrowser.js +0 -312
- package/dist/screens/TapeBrowser.js.map +0 -1
- package/dist/screens/TapeDraftReview.js +0 -444
- package/dist/screens/TapeDraftReview.js.map +0 -1
- package/dist/screens/TapeEditor.js +0 -310
- package/dist/screens/TapeEditor.js.map +0 -1
- package/dist/screens/cassette-art.js +0 -72
- package/dist/screens/cassette-art.js.map +0 -1
- package/dist/store.js +0 -10
- package/dist/store.js.map +0 -1
|
@@ -1,1203 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Box, Text, useApp as useInkApp, useInput, useStdout } from "ink";
|
|
3
|
-
import Spinner from "ink-spinner";
|
|
4
|
-
import { Header } from "../components/Header.js";
|
|
5
|
-
import { KeyHints } from "../components/KeyHints.js";
|
|
6
|
-
import { LabeledBox } from "../components/LabeledBox.js";
|
|
7
|
-
import { MUTED, CURRENT, ERROR, HEADING, STRUCTURE, SUCCESS } from "../colors.js";
|
|
8
|
-
import { useApp } from "../store.js";
|
|
9
|
-
import { detectAgents, resolveSkillPathWithDiagnostics, } from "../agents/detect.js";
|
|
10
|
-
import { resolveConfiguredAgent } from "../config.js";
|
|
11
|
-
import { runPhaseWithAgent } from "../agents/runner.js";
|
|
12
|
-
import { computePhases, readGateState, writeGateState, } from "../phases.js";
|
|
13
|
-
import { markPhaseComplete, readProgress } from "../progress.js";
|
|
14
|
-
import { projectFolder } from "../yaml-io.js";
|
|
15
|
-
// Tool list viewport. Always shows VIEWPORT rows; arrow keys move the cursor
|
|
16
|
-
// and scroll the window when the cursor hits an edge. Replaces the previous
|
|
17
|
-
// "press t to expand / collapse" toggle — keeps the layout perfectly stable
|
|
18
|
-
// regardless of how many tool calls the agent made.
|
|
19
|
-
const TOOLS_VIEWPORT = 8;
|
|
20
|
-
const FINAL_RESPONSE_VIEWPORT = 7;
|
|
21
|
-
const FINAL_RESPONSE_INSPECT_VIEWPORT = 3;
|
|
22
|
-
export function PhaseRunner({ tape, folder, phaseId, }) {
|
|
23
|
-
const app = useApp();
|
|
24
|
-
const ink = useInkApp();
|
|
25
|
-
const { stdout } = useStdout();
|
|
26
|
-
const project = useMemo(() => projectFolder(folder), [folder]);
|
|
27
|
-
const phase = useMemo(() => computePhases(project, tape, readProgress(folder)).find((p) => p.id === phaseId) ?? null, [project, tape, phaseId, folder]);
|
|
28
|
-
const skillResolution = useMemo(() => resolveSkillPathWithDiagnostics(), []);
|
|
29
|
-
const skillPath = skillResolution.path;
|
|
30
|
-
const initial = useMemo(() => initialStage(skillPath), [skillPath]);
|
|
31
|
-
const [stage, setStage] = useState(initial.stage);
|
|
32
|
-
// Structured state derived from AgentEvent stream.
|
|
33
|
-
const [tools, setTools] = useState([]);
|
|
34
|
-
const [finalText, setFinalText] = useState("");
|
|
35
|
-
// Refs mirror state for the handle.done.then() closure.
|
|
36
|
-
const finalTextRef = useRef("");
|
|
37
|
-
const currentTextRef = useRef("");
|
|
38
|
-
const [stats, setStats] = useState({ events: 0, tokens: zeroTokens() });
|
|
39
|
-
// Index into the full `tools` array. Drives the cursor in the done-state
|
|
40
|
-
// tool list and the detail panel below it. Snaps to the last tool when the
|
|
41
|
-
// run finishes.
|
|
42
|
-
const [toolCursor, setToolCursor] = useState(0);
|
|
43
|
-
// First tool index visible in the viewport. The arrow handlers maintain
|
|
44
|
-
// the invariant that `toolCursor` is always inside [scrollTop, scrollTop +
|
|
45
|
-
// VIEWPORT) by scrolling when the cursor hits an edge.
|
|
46
|
-
const [toolScrollTop, setToolScrollTop] = useState(0);
|
|
47
|
-
// Inspect mode is a modal toggle pressed with `t`. When OFF (default), the
|
|
48
|
-
// tool list is just a static viewport showing the tail end of the call
|
|
49
|
-
// history — no cursor, no detail panel, arrow keys don't do anything.
|
|
50
|
-
// When ON, arrows scroll the cursor through the full list and a detail
|
|
51
|
-
// panel renders below showing the selected call's full input + result.
|
|
52
|
-
// Press `t` again to exit. This keeps the default done view compact and
|
|
53
|
-
// makes the inspection ritual explicit instead of "you happen to be in
|
|
54
|
-
// arrow mode all the time."
|
|
55
|
-
const [inspectMode, setInspectMode] = useState(false);
|
|
56
|
-
const [finalScrollTop, setFinalScrollTop] = useState(0);
|
|
57
|
-
const handleRef = useRef(null);
|
|
58
|
-
const renderedFinalText = finalText || currentTextRef.current;
|
|
59
|
-
const finalLineCount = renderedFinalText ? renderedFinalText.split("\n").length : 0;
|
|
60
|
-
const finalViewportLines = finalResponseViewportLines(stdout?.rows ?? 24, inspectMode);
|
|
61
|
-
const handleEvent = useCallback((event) => {
|
|
62
|
-
setStats((s) => ({ ...s, events: s.events + 1 }));
|
|
63
|
-
switch (event.kind) {
|
|
64
|
-
case "init":
|
|
65
|
-
return;
|
|
66
|
-
case "text":
|
|
67
|
-
// Track the latest assistant text for the "final response" pane on
|
|
68
|
-
// the done state. We don't render it live anymore — that was the
|
|
69
|
-
// source of the scrollback laddering bug on long runs.
|
|
70
|
-
currentTextRef.current = currentTextRef.current
|
|
71
|
-
? currentTextRef.current + "\n\n" + event.text
|
|
72
|
-
: event.text;
|
|
73
|
-
return;
|
|
74
|
-
case "tool_start":
|
|
75
|
-
setTools((prev) => [
|
|
76
|
-
...prev,
|
|
77
|
-
{
|
|
78
|
-
id: event.id,
|
|
79
|
-
name: event.name,
|
|
80
|
-
input: event.input,
|
|
81
|
-
inputPreview: summarizeInput(event.input),
|
|
82
|
-
status: "running",
|
|
83
|
-
},
|
|
84
|
-
]);
|
|
85
|
-
return;
|
|
86
|
-
case "tool_done":
|
|
87
|
-
setTools((prev) => prev.map((t) => t.id === event.id
|
|
88
|
-
? {
|
|
89
|
-
...t,
|
|
90
|
-
status: event.ok ? "ok" : "failed",
|
|
91
|
-
resultPreview: event.preview,
|
|
92
|
-
}
|
|
93
|
-
: t));
|
|
94
|
-
return;
|
|
95
|
-
case "tokens":
|
|
96
|
-
setStats((s) => ({
|
|
97
|
-
...s,
|
|
98
|
-
tokens: {
|
|
99
|
-
input: s.tokens.input + event.inputTokens,
|
|
100
|
-
output: s.tokens.output + event.outputTokens,
|
|
101
|
-
cacheRead: s.tokens.cacheRead + event.cacheReadTokens,
|
|
102
|
-
cacheCreation: s.tokens.cacheCreation + event.cacheCreationTokens,
|
|
103
|
-
},
|
|
104
|
-
}));
|
|
105
|
-
return;
|
|
106
|
-
case "summary":
|
|
107
|
-
setStats((s) => ({
|
|
108
|
-
...s,
|
|
109
|
-
cost: event.costUsd ?? s.cost,
|
|
110
|
-
durationMs: event.durationMs ?? s.durationMs,
|
|
111
|
-
}));
|
|
112
|
-
if (event.finalText) {
|
|
113
|
-
setFinalText(event.finalText);
|
|
114
|
-
finalTextRef.current = event.finalText;
|
|
115
|
-
}
|
|
116
|
-
return;
|
|
117
|
-
case "rate_limit":
|
|
118
|
-
// Surface rate limits via a notification so they don't get lost in the
|
|
119
|
-
// event stream, but don't dedicate UI space to them in the compact view.
|
|
120
|
-
app.setNotification({
|
|
121
|
-
kind: "error",
|
|
122
|
-
message: `Rate limit: ${event.status}`,
|
|
123
|
-
});
|
|
124
|
-
return;
|
|
125
|
-
case "raw":
|
|
126
|
-
// Quietly drop. Anything important we already surfaced; raw stderr
|
|
127
|
-
// junk was making the screen flicker.
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
}, [app]);
|
|
131
|
-
function launch(agent, opts = {}) {
|
|
132
|
-
if (!skillPath) {
|
|
133
|
-
setStage({ kind: "missing_skill" });
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
setStage({ kind: "running", agent });
|
|
137
|
-
setTools([]);
|
|
138
|
-
setFinalText("");
|
|
139
|
-
setFinalScrollTop(0);
|
|
140
|
-
currentTextRef.current = "";
|
|
141
|
-
finalTextRef.current = "";
|
|
142
|
-
setStats({ events: 0, tokens: zeroTokens(), startedAt: Date.now() });
|
|
143
|
-
const handle = runPhaseWithAgent({
|
|
144
|
-
agent,
|
|
145
|
-
phaseId,
|
|
146
|
-
project: folder,
|
|
147
|
-
skillPath,
|
|
148
|
-
tape,
|
|
149
|
-
resume: opts.resume,
|
|
150
|
-
onEvent: handleEvent,
|
|
151
|
-
});
|
|
152
|
-
handleRef.current = handle;
|
|
153
|
-
handle.done.then(({ code }) => {
|
|
154
|
-
// Clear the screen as we flip running → done. The running view and the
|
|
155
|
-
// done view differ in height, and on terminals that keep scrollback in
|
|
156
|
-
// the alternate buffer the old running frames otherwise linger above
|
|
157
|
-
// the done view (stacked duplicate headers). Clearing once on the
|
|
158
|
-
// transition lands the done view on a fresh canvas.
|
|
159
|
-
if (process.stdout.isTTY)
|
|
160
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
161
|
-
setStage({ kind: "done", agent, code });
|
|
162
|
-
// Surface the agent's final-line summary as a notification so it
|
|
163
|
-
// survives navigation. The full final response stays on this screen.
|
|
164
|
-
const summary = finalTextRef.current || currentTextRef.current;
|
|
165
|
-
if (summary) {
|
|
166
|
-
const oneLine = summary.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
167
|
-
if (oneLine) {
|
|
168
|
-
const truncated = oneLine.length > 140 ? oneLine.slice(0, 139) + "…" : oneLine;
|
|
169
|
-
app.setNotification({
|
|
170
|
-
kind: code === 0 ? "info" : "error",
|
|
171
|
-
message: `${phase?.label ?? phaseId}: ${truncated}`,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// On clean success, advance the progress cursor by one. Assembly is
|
|
176
|
-
// the exception — its "real" completion lands in TapeDraftReview when
|
|
177
|
-
// the curator accepts the proposed sources. Compile and the gates are
|
|
178
|
-
// their own screens and advance themselves.
|
|
179
|
-
if (code === 0 && phaseId !== "assembly") {
|
|
180
|
-
markPhaseComplete(folder, phaseId);
|
|
181
|
-
}
|
|
182
|
-
// Phase 7 special-case: a successful assembly run wrote
|
|
183
|
-
// working/07-tape-draft.yaml. Jump straight into the draft-review.
|
|
184
|
-
if (code === 0 && phaseId === "assembly") {
|
|
185
|
-
app.navigate({ kind: "tapeDraft", tape, folder });
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
function cancel() {
|
|
190
|
-
handleRef.current?.cancel();
|
|
191
|
-
}
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
if (initial.autoLaunch) {
|
|
194
|
-
launch(initial.autoLaunch);
|
|
195
|
-
}
|
|
196
|
-
return () => {
|
|
197
|
-
handleRef.current?.cancel();
|
|
198
|
-
};
|
|
199
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
200
|
-
}, []);
|
|
201
|
-
// When entering the done state, park the tool cursor on the most recent
|
|
202
|
-
// call so the detail panel below the table immediately shows something
|
|
203
|
-
// useful (the last thing the agent did before finishing). Snap the scroll
|
|
204
|
-
// window so the cursor sits at the bottom of the viewport.
|
|
205
|
-
useEffect(() => {
|
|
206
|
-
if (stage.kind === "done" && tools.length > 0) {
|
|
207
|
-
const cursor = tools.length - 1;
|
|
208
|
-
setToolCursor(cursor);
|
|
209
|
-
setToolScrollTop(Math.max(0, cursor - TOOLS_VIEWPORT + 1));
|
|
210
|
-
}
|
|
211
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
212
|
-
}, [stage.kind]);
|
|
213
|
-
function advance() {
|
|
214
|
-
if (stage.kind !== "done")
|
|
215
|
-
return;
|
|
216
|
-
const next = nextScreenFor(phaseId, tape, folder);
|
|
217
|
-
if (next)
|
|
218
|
-
app.navigate(next);
|
|
219
|
-
else
|
|
220
|
-
app.navigate({ kind: "hub", tape, folder });
|
|
221
|
-
}
|
|
222
|
-
useInput((input, key) => {
|
|
223
|
-
if (stage.kind === "picking_agent") {
|
|
224
|
-
if (key.upArrow || input === "k") {
|
|
225
|
-
setStage({ ...stage, selectedIndex: Math.max(0, stage.selectedIndex - 1) });
|
|
226
|
-
}
|
|
227
|
-
else if (key.downArrow || input === "j") {
|
|
228
|
-
setStage({
|
|
229
|
-
...stage,
|
|
230
|
-
selectedIndex: Math.min(stage.agents.length - 1, stage.selectedIndex + 1),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
else if (key.return) {
|
|
234
|
-
const agent = stage.agents[stage.selectedIndex];
|
|
235
|
-
if (agent)
|
|
236
|
-
launch(agent);
|
|
237
|
-
}
|
|
238
|
-
else if (input === "q") {
|
|
239
|
-
ink.exit();
|
|
240
|
-
}
|
|
241
|
-
else if (key.escape) {
|
|
242
|
-
app.navigate({ kind: "hub", tape, folder });
|
|
243
|
-
}
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
if (stage.kind === "missing_agent" || stage.kind === "missing_skill") {
|
|
247
|
-
if (input === "q") {
|
|
248
|
-
ink.exit();
|
|
249
|
-
}
|
|
250
|
-
else if (key.escape || input === "b") {
|
|
251
|
-
app.navigate({ kind: "hub", tape, folder });
|
|
252
|
-
}
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (stage.kind === "running") {
|
|
256
|
-
if (key.escape || input === "c")
|
|
257
|
-
cancel();
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
if (stage.kind === "done") {
|
|
261
|
-
const failed = stage.code !== 0;
|
|
262
|
-
// `t` is the modal toggle. Always available, regardless of mode.
|
|
263
|
-
if (input === "t") {
|
|
264
|
-
setInspectMode((m) => {
|
|
265
|
-
// Entering inspect mode: snap cursor + window to the last call so
|
|
266
|
-
// the detail panel renders something useful from the start.
|
|
267
|
-
if (!m && tools.length > 0) {
|
|
268
|
-
const cursor = tools.length - 1;
|
|
269
|
-
setToolCursor(cursor);
|
|
270
|
-
setToolScrollTop(Math.max(0, cursor - TOOLS_VIEWPORT + 1));
|
|
271
|
-
}
|
|
272
|
-
return !m;
|
|
273
|
-
});
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (inspectMode) {
|
|
277
|
-
// Inspect mode: arrows scroll, esc / b / t exit, everything else
|
|
278
|
-
// (enter to advance, r to re-run) is intentionally locked so the
|
|
279
|
-
// user doesn't accidentally move forward while scanning calls.
|
|
280
|
-
if (key.upArrow || input === "k") {
|
|
281
|
-
if (tools.length === 0)
|
|
282
|
-
return;
|
|
283
|
-
setToolCursor((c) => {
|
|
284
|
-
const next = Math.max(0, c - 1);
|
|
285
|
-
setToolScrollTop((s) => (next < s ? next : s));
|
|
286
|
-
return next;
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
else if (key.downArrow || input === "j") {
|
|
290
|
-
if (tools.length === 0)
|
|
291
|
-
return;
|
|
292
|
-
setToolCursor((c) => {
|
|
293
|
-
const next = Math.min(tools.length - 1, c + 1);
|
|
294
|
-
setToolScrollTop((s) => next >= s + TOOLS_VIEWPORT ? next - TOOLS_VIEWPORT + 1 : s);
|
|
295
|
-
return next;
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
else if (key.pageUp) {
|
|
299
|
-
if (tools.length === 0)
|
|
300
|
-
return;
|
|
301
|
-
setToolCursor((c) => {
|
|
302
|
-
const next = Math.max(0, c - TOOLS_VIEWPORT);
|
|
303
|
-
setToolScrollTop(Math.max(0, next));
|
|
304
|
-
return next;
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
else if (key.pageDown) {
|
|
308
|
-
if (tools.length === 0)
|
|
309
|
-
return;
|
|
310
|
-
setToolCursor((c) => {
|
|
311
|
-
const next = Math.min(tools.length - 1, c + TOOLS_VIEWPORT);
|
|
312
|
-
setToolScrollTop(Math.max(0, next - TOOLS_VIEWPORT + 1));
|
|
313
|
-
return next;
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
else if (key.escape) {
|
|
317
|
-
// Esc inside inspect mode = exit the mode (does NOT bail to hub).
|
|
318
|
-
setInspectMode(false);
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
// Default done mode: arrows/page keys scroll the final response. Normal
|
|
323
|
-
// terminal scrollback is unavailable in the alternate screen, and mouse
|
|
324
|
-
// wheels arrive as arrow keys in many terminals; make those inputs move
|
|
325
|
-
// the readable response instead of doing nothing.
|
|
326
|
-
if (key.upArrow || input === "k") {
|
|
327
|
-
setFinalScrollTop((s) => Math.max(0, Math.min(s, Math.max(0, finalLineCount - finalViewportLines)) - 1));
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (key.downArrow || input === "j") {
|
|
331
|
-
setFinalScrollTop((s) => Math.min(Math.max(0, finalLineCount - finalViewportLines), Math.min(s, Math.max(0, finalLineCount - finalViewportLines)) + 1));
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (key.pageUp) {
|
|
335
|
-
setFinalScrollTop((s) => Math.max(0, Math.min(s, Math.max(0, finalLineCount - finalViewportLines)) -
|
|
336
|
-
finalViewportLines));
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
if (key.pageDown) {
|
|
340
|
-
setFinalScrollTop((s) => Math.min(Math.max(0, finalLineCount - finalViewportLines), s + finalViewportLines));
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
// Default mode: enter advances, b/esc backs to hub, r retries on fail.
|
|
344
|
-
if (key.return) {
|
|
345
|
-
// On failure, Enter = resume (cheap continuation, no re-fetching).
|
|
346
|
-
// On success, Enter = advance to the next logical screen.
|
|
347
|
-
if (failed)
|
|
348
|
-
launch(stage.agent, { resume: true });
|
|
349
|
-
else
|
|
350
|
-
advance();
|
|
351
|
-
}
|
|
352
|
-
else if (input === "r" && failed) {
|
|
353
|
-
// Failure-only escape hatch: re-run from scratch.
|
|
354
|
-
launch(stage.agent);
|
|
355
|
-
}
|
|
356
|
-
else if (input === "q") {
|
|
357
|
-
ink.exit();
|
|
358
|
-
}
|
|
359
|
-
else if (key.escape || input === "b") {
|
|
360
|
-
app.navigate({ kind: "hub", tape, folder });
|
|
361
|
-
}
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
return (React.createElement(Box, { flexDirection: "column" },
|
|
366
|
-
React.createElement(Header, { title: `phase ${phase?.number ?? "?"} — ${phase?.label ?? phaseId}`, subtitle: subtitleFor(stage, skillPath) }),
|
|
367
|
-
stage.kind === "picking_agent" ? (React.createElement(PickingAgentView, { stage: stage })) : null,
|
|
368
|
-
stage.kind === "missing_agent" ? React.createElement(MissingAgentView, null) : null,
|
|
369
|
-
stage.kind === "missing_skill" ? (React.createElement(MissingSkillView, { resolution: skillResolution })) : null,
|
|
370
|
-
stage.kind === "running" ? (React.createElement(RunningView, { agent: stage.agent, phase: phase, phaseId: phaseId, tools: tools, stats: stats })) : null,
|
|
371
|
-
stage.kind === "done" ? (React.createElement(DoneView, { agent: stage.agent, code: stage.code, phaseId: phaseId, phase: phase, tape: tape, folder: folder, tools: tools, finalText: renderedFinalText, finalScrollTop: finalScrollTop, finalViewportLines: finalViewportLines, stats: stats, toolCursor: toolCursor, toolScrollTop: toolScrollTop, inspectMode: inspectMode })) : null,
|
|
372
|
-
app.notification ? (React.createElement(Box, { marginBottom: 1 },
|
|
373
|
-
React.createElement(Text, { color: app.notification.kind === "error" ? ERROR : SUCCESS }, app.notification.message))) : null,
|
|
374
|
-
React.createElement(KeyHints, { hints: hintsFor(stage, inspectMode) })));
|
|
375
|
-
}
|
|
376
|
-
// ---------------------------------------------------------------------------
|
|
377
|
-
// Sub-views
|
|
378
|
-
// ---------------------------------------------------------------------------
|
|
379
|
-
function PickingAgentView({ stage, }) {
|
|
380
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
381
|
-
React.createElement(Text, null, "Multiple agents available \u2014 pick one:"),
|
|
382
|
-
React.createElement(Box, { marginTop: 1, flexDirection: "column" }, stage.agents.map((a, i) => (React.createElement(Box, { key: a.id },
|
|
383
|
-
React.createElement(Text, { color: i === stage.selectedIndex ? STRUCTURE : undefined },
|
|
384
|
-
i === stage.selectedIndex ? "▶ " : " ",
|
|
385
|
-
a.name),
|
|
386
|
-
React.createElement(Text, { color: MUTED },
|
|
387
|
-
" ",
|
|
388
|
-
a.bin))))),
|
|
389
|
-
React.createElement(Box, { marginTop: 1 },
|
|
390
|
-
React.createElement(Text, { color: MUTED },
|
|
391
|
-
"Tip: pick once in the splash menu (",
|
|
392
|
-
React.createElement(Text, { color: STRUCTURE }, "[s]"),
|
|
393
|
-
" Settings) to skip this prompt forever \u2014 or set ",
|
|
394
|
-
React.createElement(Text, { color: STRUCTURE }, "LINER_AGENT=claude"),
|
|
395
|
-
" in your shell."))));
|
|
396
|
-
}
|
|
397
|
-
function MissingAgentView() {
|
|
398
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
399
|
-
React.createElement(Text, { color: ERROR }, "No agent CLI found on PATH."),
|
|
400
|
-
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
401
|
-
React.createElement(Text, null, "Liner can drive the methodology with either:"),
|
|
402
|
-
React.createElement(Text, null,
|
|
403
|
-
" - Claude Code (",
|
|
404
|
-
React.createElement(Text, { color: STRUCTURE }, "claude"),
|
|
405
|
-
") \u2014 install: ",
|
|
406
|
-
React.createElement(Text, { color: STRUCTURE }, "npm install -g @anthropic-ai/claude-code")),
|
|
407
|
-
React.createElement(Text, null,
|
|
408
|
-
" - OpenAI Codex (",
|
|
409
|
-
React.createElement(Text, { color: STRUCTURE }, "codex"),
|
|
410
|
-
") \u2014 see https://github.com/openai/codex"))));
|
|
411
|
-
}
|
|
412
|
-
function MissingSkillView({ resolution, }) {
|
|
413
|
-
const checked = resolution.searched.slice(0, 5);
|
|
414
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
415
|
-
React.createElement(Text, { color: ERROR }, "Couldn't find the curating-mixtapes skill bundle."),
|
|
416
|
-
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
417
|
-
React.createElement(Text, { color: MUTED },
|
|
418
|
-
"Set ",
|
|
419
|
-
React.createElement(Text, { color: STRUCTURE }, "LINER_SKILL_PATH"),
|
|
420
|
-
" to the directory containing SKILL.md."),
|
|
421
|
-
resolution.envPath ? (React.createElement(Text, { color: MUTED },
|
|
422
|
-
"Current ",
|
|
423
|
-
React.createElement(Text, { color: STRUCTURE }, "LINER_SKILL_PATH"),
|
|
424
|
-
": ",
|
|
425
|
-
resolution.envPath)) : null,
|
|
426
|
-
checked.length > 0 ? (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
427
|
-
React.createElement(Text, { color: MUTED }, "Checked:"),
|
|
428
|
-
checked.map((path) => (React.createElement(Text, { key: path, color: MUTED },
|
|
429
|
-
" - ",
|
|
430
|
-
path))))) : null)));
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Compact running view. Two lines + a bordered status block — fits any
|
|
434
|
-
* terminal height. The full tool list and final response wait for the done
|
|
435
|
-
* state so the live view never overflows the viewport.
|
|
436
|
-
*/
|
|
437
|
-
function RunningView({ agent, phase, phaseId, tools, stats, }) {
|
|
438
|
-
const latest = tools[tools.length - 1];
|
|
439
|
-
const liveDuration = stats.startedAt
|
|
440
|
-
? formatDuration(Date.now() - stats.startedAt)
|
|
441
|
-
: "";
|
|
442
|
-
const totalTokens = stats.tokens.input +
|
|
443
|
-
stats.tokens.output +
|
|
444
|
-
stats.tokens.cacheRead +
|
|
445
|
-
stats.tokens.cacheCreation;
|
|
446
|
-
const phaseLabel = phase
|
|
447
|
-
? `Phase ${phase.number} — ${phase.label}`
|
|
448
|
-
: `Phase ${phaseId}`;
|
|
449
|
-
return (React.createElement(Box, { marginBottom: 1 },
|
|
450
|
-
React.createElement(LabeledBox, { label: phaseLabel, color: CURRENT, paddingX: 1, paddingY: 1 },
|
|
451
|
-
React.createElement(Box, null,
|
|
452
|
-
React.createElement(Text, { color: CURRENT },
|
|
453
|
-
React.createElement(Spinner, { type: "dots" }),
|
|
454
|
-
" "),
|
|
455
|
-
React.createElement(Text, null, agent.name),
|
|
456
|
-
React.createElement(Text, { color: MUTED }, " · running")),
|
|
457
|
-
React.createElement(Box, { marginTop: 1 },
|
|
458
|
-
React.createElement(Text, { color: MUTED },
|
|
459
|
-
stats.events,
|
|
460
|
-
" event",
|
|
461
|
-
stats.events === 1 ? "" : "s",
|
|
462
|
-
" · ",
|
|
463
|
-
tools.length,
|
|
464
|
-
" tool",
|
|
465
|
-
tools.length === 1 ? "" : "s",
|
|
466
|
-
" · ",
|
|
467
|
-
formatTokens(totalTokens),
|
|
468
|
-
" tokens",
|
|
469
|
-
liveDuration ? ` · ${liveDuration}` : "")),
|
|
470
|
-
latest ? (React.createElement(Box, { marginTop: 1 },
|
|
471
|
-
React.createElement(Text, { color: MUTED }, "latest: "),
|
|
472
|
-
React.createElement(Text, { color: latest.status === "ok" ? SUCCESS : latest.status === "failed" ? ERROR : STRUCTURE },
|
|
473
|
-
latest.status === "ok" ? "✓" : latest.status === "failed" ? "✗" : "⟳",
|
|
474
|
-
" "),
|
|
475
|
-
React.createElement(Text, null, latest.name.replace(/^mcp__[^_]+__/, "")),
|
|
476
|
-
React.createElement(Text, { color: MUTED },
|
|
477
|
-
" ",
|
|
478
|
-
previewify(latest.inputPreview, 60)))) : (React.createElement(Box, { marginTop: 1 },
|
|
479
|
-
React.createElement(Text, { color: MUTED }, "warming up\u2026"))))));
|
|
480
|
-
}
|
|
481
|
-
function DoneView({ agent, code, phaseId, phase, tape, folder, tools, finalText, finalScrollTop, finalViewportLines, stats, toolCursor, toolScrollTop, inspectMode, }) {
|
|
482
|
-
// Fixed-size scroll viewport. Cursor index drives both the highlighted
|
|
483
|
-
// row in the list and the content of the detail panel below.
|
|
484
|
-
const visibleStart = Math.max(0, Math.min(tools.length, toolScrollTop));
|
|
485
|
-
const visibleEnd = Math.min(tools.length, visibleStart + TOOLS_VIEWPORT);
|
|
486
|
-
const visibleTools = tools.slice(visibleStart, visibleEnd);
|
|
487
|
-
const hiddenAbove = visibleStart;
|
|
488
|
-
const hiddenBelow = Math.max(0, tools.length - visibleEnd);
|
|
489
|
-
const clampedCursor = Math.max(0, Math.min(tools.length - 1, toolCursor));
|
|
490
|
-
const selectedTool = tools[clampedCursor];
|
|
491
|
-
const totalTokens = stats.tokens.input +
|
|
492
|
-
stats.tokens.output +
|
|
493
|
-
stats.tokens.cacheRead +
|
|
494
|
-
stats.tokens.cacheCreation;
|
|
495
|
-
const next = nextScreenFor(phaseId, tape, folder);
|
|
496
|
-
const nextLabel = nextScreenLabel(phaseId, tape, folder);
|
|
497
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
498
|
-
React.createElement(Box, { marginBottom: 1 },
|
|
499
|
-
React.createElement(Text, { color: code === 0 ? SUCCESS : ERROR, bold: true }, code === 0 ? "✓ done" : `✗ exited with code ${code}`),
|
|
500
|
-
React.createElement(Text, { color: MUTED },
|
|
501
|
-
" ",
|
|
502
|
-
agent.name,
|
|
503
|
-
" · ",
|
|
504
|
-
stats.events,
|
|
505
|
-
" events",
|
|
506
|
-
" · ",
|
|
507
|
-
tools.length,
|
|
508
|
-
" tools",
|
|
509
|
-
" · ",
|
|
510
|
-
formatTokens(totalTokens),
|
|
511
|
-
" tokens",
|
|
512
|
-
stats.durationMs ? ` · ${formatDuration(stats.durationMs)}` : "")),
|
|
513
|
-
finalText ? (React.createElement(Box, { marginBottom: 1 },
|
|
514
|
-
React.createElement(LabeledBox, { label: finalResponseLabel(finalText, finalScrollTop, finalViewportLines), color: code === 0 ? SUCCESS : ERROR },
|
|
515
|
-
React.createElement(MarkdownBody, { text: finalText, maxLines: finalViewportLines, scrollTop: finalScrollTop, color: code === 0 ? SUCCESS : ERROR })))) : null,
|
|
516
|
-
tools.length > 0 ? (React.createElement(Box, { marginBottom: 1 },
|
|
517
|
-
React.createElement(LabeledBox, { label: inspectMode
|
|
518
|
-
? `inspecting tool calls · ${clampedCursor + 1} / ${tools.length}`
|
|
519
|
-
: `tool calls · ${tools.length} total · press t to inspect`, color: inspectMode ? CURRENT : STRUCTURE, paddingX: 1 },
|
|
520
|
-
hiddenAbove > 0 ? (React.createElement(Text, { color: MUTED }, ` ↑ ${hiddenAbove} earlier call${hiddenAbove === 1 ? "" : "s"} above`)) : (React.createElement(Text, { color: MUTED }, " ")),
|
|
521
|
-
Array.from({ length: TOOLS_VIEWPORT }).map((_, idx) => {
|
|
522
|
-
const tool = visibleTools[idx];
|
|
523
|
-
if (!tool)
|
|
524
|
-
return React.createElement(Text, { key: `pad-${idx}` }, " ");
|
|
525
|
-
return (React.createElement(ToolCallRow, { key: tool.id, call: tool, active: inspectMode && visibleStart + idx === clampedCursor }));
|
|
526
|
-
}),
|
|
527
|
-
hiddenBelow > 0 ? (React.createElement(Text, { color: MUTED }, ` ↓ ${hiddenBelow} more call${hiddenBelow === 1 ? "" : "s"} below`)) : (React.createElement(Text, { color: MUTED }, " "))))) : null,
|
|
528
|
-
inspectMode ? (React.createElement(Box, { marginBottom: 1 },
|
|
529
|
-
React.createElement(Text, { color: CURRENT, bold: true }, "\u25B8 inspect mode "),
|
|
530
|
-
React.createElement(Text, { color: MUTED }, "use "),
|
|
531
|
-
React.createElement(Text, { color: STRUCTURE }, "\u2191\u2193"),
|
|
532
|
-
React.createElement(Text, { color: MUTED }, " to move, "),
|
|
533
|
-
React.createElement(Text, { color: STRUCTURE }, "pgup/pgdn"),
|
|
534
|
-
React.createElement(Text, { color: MUTED }, " to jump, "),
|
|
535
|
-
React.createElement(Text, { color: STRUCTURE }, "t"),
|
|
536
|
-
React.createElement(Text, { color: MUTED }, " or "),
|
|
537
|
-
React.createElement(Text, { color: STRUCTURE }, "esc"),
|
|
538
|
-
React.createElement(Text, { color: MUTED }, " to exit"))) : null,
|
|
539
|
-
selectedTool && inspectMode ? (React.createElement(Box, { marginBottom: 1 },
|
|
540
|
-
React.createElement(LabeledBox, { label: `detail · ${friendlyToolName(selectedTool.name)}`, color: CURRENT, paddingY: 1 },
|
|
541
|
-
React.createElement(ToolDetailBody, { call: selectedTool })))) : null,
|
|
542
|
-
React.createElement(Box, { marginBottom: 1 },
|
|
543
|
-
React.createElement(Text, { color: HEADING }, code === 0
|
|
544
|
-
? `next · ${nextLabel}${next ? "" : " — back to the hub"}`
|
|
545
|
-
: "next · enter resumes from where the agent stopped (no re-fetching)"))));
|
|
546
|
-
}
|
|
547
|
-
function ToolCallRow({ call, active = false, }) {
|
|
548
|
-
// Guard: every now and then the agent stream emits a tool_start with no
|
|
549
|
-
// name and no input. Skip those rows so they don't punch holes in the box.
|
|
550
|
-
if (!call.name && !call.inputPreview)
|
|
551
|
-
return null;
|
|
552
|
-
const glyph = call.status === "ok" ? "✓" : call.status === "failed" ? "✗" : "⟳";
|
|
553
|
-
const glyphColor = call.status === "ok" ? SUCCESS : call.status === "failed" ? ERROR : STRUCTURE;
|
|
554
|
-
const friendlyName = friendlyToolName(call.name);
|
|
555
|
-
// The "effect" string leads the row — what the tool produced, not what we
|
|
556
|
-
// sent it. For successful calls that's the result preview; for failures
|
|
557
|
-
// it's the cleaned-up error; for in-flight calls it's a status line.
|
|
558
|
-
const effect = effectFor(call);
|
|
559
|
-
// The input/path becomes a dim trailing chip — the where, not the what.
|
|
560
|
-
const inputChip = call.inputPreview ? ` [${previewify(call.inputPreview, 40)}]` : "";
|
|
561
|
-
return (React.createElement(Box, null,
|
|
562
|
-
React.createElement(Text, { color: active ? CURRENT : undefined, bold: active }, active ? "▸ " : " "),
|
|
563
|
-
React.createElement(Text, { color: glyphColor, bold: true },
|
|
564
|
-
glyph,
|
|
565
|
-
" "),
|
|
566
|
-
React.createElement(Text, { color: active ? CURRENT : STRUCTURE, bold: active }, friendlyName.padEnd(13, " ")),
|
|
567
|
-
React.createElement(Text, { color: call.status === "failed" ? ERROR : undefined }, truncate(effect, 54)),
|
|
568
|
-
inputChip ? React.createElement(Text, { color: MUTED }, inputChip) : null));
|
|
569
|
-
}
|
|
570
|
-
// Fixed counts so the detail panel always occupies the same vertical space.
|
|
571
|
-
// Switching between tool calls never causes the layout below the box to shift.
|
|
572
|
-
// Empty rows are rendered with a single space so Ink keeps them as real
|
|
573
|
-
// lines (and not collapsed whitespace).
|
|
574
|
-
const DETAIL_INPUT_LINES = 3;
|
|
575
|
-
const DETAIL_RESULT_LINES = 6;
|
|
576
|
-
/**
|
|
577
|
-
* Detail panel body for the currently-selected tool call. Shows the full
|
|
578
|
-
* input (path/URL/command/JSON object) and the un-truncated result preview.
|
|
579
|
-
* The total rendered height is constant: status row + input header + 3 input
|
|
580
|
-
* lines + result header + 6 result lines + spacers. Padding empty rows when
|
|
581
|
-
* content is shorter keeps the surrounding layout (NextSteps, KeyHints)
|
|
582
|
-
* anchored as the user moves the cursor through different tools.
|
|
583
|
-
*/
|
|
584
|
-
function ToolDetailBody({ call }) {
|
|
585
|
-
const statusColor = call.status === "ok" ? SUCCESS : call.status === "failed" ? ERROR : STRUCTURE;
|
|
586
|
-
const rawInputLines = formatInputDetail(call.name, call.input);
|
|
587
|
-
const inputLines = padLines(rawInputLines, DETAIL_INPUT_LINES);
|
|
588
|
-
const inputTruncated = rawInputLines.length > DETAIL_INPUT_LINES;
|
|
589
|
-
const resultText = call.resultPreview ? cleanResultPreview(call.resultPreview) : "";
|
|
590
|
-
const rawResultLines = resultText ? resultText.split("\n") : [];
|
|
591
|
-
const resultLines = padLines(rawResultLines, DETAIL_RESULT_LINES);
|
|
592
|
-
const resultTruncated = rawResultLines.length > DETAIL_RESULT_LINES;
|
|
593
|
-
return (React.createElement(Box, { flexDirection: "column" },
|
|
594
|
-
React.createElement(Box, null,
|
|
595
|
-
React.createElement(Text, { color: statusColor, bold: true },
|
|
596
|
-
call.status === "ok" ? "✓" : call.status === "failed" ? "✗" : "⟳",
|
|
597
|
-
" "),
|
|
598
|
-
React.createElement(Text, { bold: true }, friendlyToolName(call.name)),
|
|
599
|
-
React.createElement(Text, { color: MUTED },
|
|
600
|
-
" \u00B7 ",
|
|
601
|
-
call.status)),
|
|
602
|
-
React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
603
|
-
React.createElement(Text, { color: HEADING, bold: true },
|
|
604
|
-
"input",
|
|
605
|
-
inputTruncated ? (React.createElement(Text, { color: MUTED }, ` (showing first ${DETAIL_INPUT_LINES} of ${rawInputLines.length})`)) : null),
|
|
606
|
-
inputLines.map((line, i) => (React.createElement(Text, { key: `in-${i}`, color: MUTED }, " " + (line || " "))))),
|
|
607
|
-
React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
608
|
-
React.createElement(Text, { color: HEADING, bold: true },
|
|
609
|
-
"result",
|
|
610
|
-
resultTruncated ? (React.createElement(Text, { color: MUTED }, ` (showing first ${DETAIL_RESULT_LINES} of ${rawResultLines.length})`)) : null),
|
|
611
|
-
resultLines.map((line, i) => (React.createElement(Text, { key: `res-${i}`, color: MUTED }, " " + (line || " ")))))));
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Truncate `lines` to `target` rows, padding with empty strings when shorter.
|
|
615
|
-
* Used to keep the detail panel's render height constant regardless of how
|
|
616
|
-
* much content the selected tool produced.
|
|
617
|
-
*/
|
|
618
|
-
function padLines(lines, target) {
|
|
619
|
-
if (lines.length >= target)
|
|
620
|
-
return lines.slice(0, target);
|
|
621
|
-
return [...lines, ...new Array(target - lines.length).fill("")];
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Multi-line detail for a tool's input, formatted per tool type. Returns the
|
|
625
|
-
* full path/URL/command (no truncation) plus secondary fields where they
|
|
626
|
-
* exist. Falls back to a pretty-printed JSON dump for unknown shapes.
|
|
627
|
-
*/
|
|
628
|
-
function formatInputDetail(name, input) {
|
|
629
|
-
if (input == null)
|
|
630
|
-
return [];
|
|
631
|
-
// String inputs (e.g. a bare shell command Codex emits) still deserve to
|
|
632
|
-
// show up in the detail panel — fall back to a single line keyed by the
|
|
633
|
-
// tool's natural label rather than rendering nothing.
|
|
634
|
-
if (typeof input === "string") {
|
|
635
|
-
return input ? [`${name === "bash" ? "command" : "input"}: ${input}`] : [];
|
|
636
|
-
}
|
|
637
|
-
if (typeof input !== "object")
|
|
638
|
-
return [];
|
|
639
|
-
const o = input;
|
|
640
|
-
const tool = name.replace(/^mcp__[^_]+__/, "");
|
|
641
|
-
const lines = [];
|
|
642
|
-
const fieldOrder = [
|
|
643
|
-
["url", "url"],
|
|
644
|
-
["file_path", "path"],
|
|
645
|
-
["path", "path"],
|
|
646
|
-
["query", "query"],
|
|
647
|
-
["pattern", "pattern"],
|
|
648
|
-
["command", "command"],
|
|
649
|
-
["prompt", "prompt"],
|
|
650
|
-
["description", "description"],
|
|
651
|
-
];
|
|
652
|
-
const known = new Set();
|
|
653
|
-
for (const [field, label] of fieldOrder) {
|
|
654
|
-
const v = o[field];
|
|
655
|
-
if (typeof v === "string" && v) {
|
|
656
|
-
lines.push(`${label}: ${v}`);
|
|
657
|
-
known.add(field);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
// Anything else in the input — fall back to a one-line JSON dump per key.
|
|
661
|
-
// For TodoWrite specifically the `todos` array is the meaningful payload;
|
|
662
|
-
// render it line-by-line so the curator can read the plan.
|
|
663
|
-
if (tool === "TodoWrite" && Array.isArray(o["todos"])) {
|
|
664
|
-
lines.push("todos:");
|
|
665
|
-
for (const todo of o["todos"]) {
|
|
666
|
-
const t = (todo ?? {});
|
|
667
|
-
const status = String(t["status"] ?? "?");
|
|
668
|
-
const content = String(t["content"] ?? "");
|
|
669
|
-
const glyph = status === "completed" ? "✓" : status === "in_progress" ? "▸" : "○";
|
|
670
|
-
lines.push(` ${glyph} ${content}`);
|
|
671
|
-
}
|
|
672
|
-
known.add("todos");
|
|
673
|
-
}
|
|
674
|
-
// Any remaining fields go as one-liner key: value JSON-ish entries.
|
|
675
|
-
for (const [k, v] of Object.entries(o)) {
|
|
676
|
-
if (known.has(k))
|
|
677
|
-
continue;
|
|
678
|
-
if (v == null)
|
|
679
|
-
continue;
|
|
680
|
-
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
681
|
-
if (!s)
|
|
682
|
-
continue;
|
|
683
|
-
lines.push(`${k}: ${s.slice(0, 200)}`);
|
|
684
|
-
}
|
|
685
|
-
return lines;
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* One-line "what happened" string for a tool call. Falls back through the
|
|
689
|
-
* available signals so a row never goes empty:
|
|
690
|
-
* - result preview (post-call), with `<tool_use_error>` wrappers stripped
|
|
691
|
-
* - "fetching…" / "writing…" / etc. for in-flight calls
|
|
692
|
-
* - the raw input preview if nothing else is available
|
|
693
|
-
*/
|
|
694
|
-
function effectFor(call) {
|
|
695
|
-
if (call.status === "running") {
|
|
696
|
-
return inFlightVerb(call.name);
|
|
697
|
-
}
|
|
698
|
-
if (call.resultPreview) {
|
|
699
|
-
// First line only for the row display; the detail panel shows the full
|
|
700
|
-
// multi-line preview when the row is selected.
|
|
701
|
-
return firstLineOf(cleanResultPreview(call.resultPreview));
|
|
702
|
-
}
|
|
703
|
-
// Nothing reported — fall back to a tool-typed default so the row stays
|
|
704
|
-
// informative rather than displaying an empty string.
|
|
705
|
-
if (call.status === "ok")
|
|
706
|
-
return defaultDoneFor(call.name);
|
|
707
|
-
if (call.status === "failed")
|
|
708
|
-
return "failed without an error message";
|
|
709
|
-
return "";
|
|
710
|
-
}
|
|
711
|
-
function firstLineOf(s) {
|
|
712
|
-
const nl = s.indexOf("\n");
|
|
713
|
-
return nl < 0 ? s : s.slice(0, nl);
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Strip Claude's `<tool_use_error>...</tool_use_error>` envelope and the
|
|
717
|
-
* "File has not been read yet" preamble that surfaces frequently. Surfacing
|
|
718
|
-
* the bare message gives the curator a fighting chance at understanding what
|
|
719
|
-
* went wrong.
|
|
720
|
-
*/
|
|
721
|
-
function cleanResultPreview(s) {
|
|
722
|
-
let out = s.trim();
|
|
723
|
-
// Strip XML-style envelope.
|
|
724
|
-
const m = /<tool_use_error>\s*(.*?)\s*<\/tool_use_error>/is.exec(out);
|
|
725
|
-
if (m && m[1])
|
|
726
|
-
out = m[1];
|
|
727
|
-
// Strip common HTTP-error preambles that don't carry useful information
|
|
728
|
-
// for someone watching the stream.
|
|
729
|
-
out = out.replace(/^Error:\s+/i, "");
|
|
730
|
-
return out;
|
|
731
|
-
}
|
|
732
|
-
/** Human-friendly title for a tool name. */
|
|
733
|
-
function friendlyToolName(raw) {
|
|
734
|
-
const stripped = raw.replace(/^mcp__[^_]+__/, "");
|
|
735
|
-
switch (stripped) {
|
|
736
|
-
case "WebSearch":
|
|
737
|
-
return "Web search";
|
|
738
|
-
case "WebFetch":
|
|
739
|
-
return "Web fetch";
|
|
740
|
-
case "Read":
|
|
741
|
-
return "Read file";
|
|
742
|
-
case "Write":
|
|
743
|
-
return "Write file";
|
|
744
|
-
case "Edit":
|
|
745
|
-
return "Edit file";
|
|
746
|
-
case "Glob":
|
|
747
|
-
return "Find files";
|
|
748
|
-
case "Grep":
|
|
749
|
-
return "Search code";
|
|
750
|
-
case "TodoWrite":
|
|
751
|
-
return "Update plan";
|
|
752
|
-
case "Bash":
|
|
753
|
-
return "Shell";
|
|
754
|
-
default:
|
|
755
|
-
return stripped;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
function inFlightVerb(raw) {
|
|
759
|
-
const n = raw.replace(/^mcp__[^_]+__/, "");
|
|
760
|
-
switch (n) {
|
|
761
|
-
case "WebSearch":
|
|
762
|
-
return "searching the web…";
|
|
763
|
-
case "WebFetch":
|
|
764
|
-
return "fetching…";
|
|
765
|
-
case "Read":
|
|
766
|
-
return "reading…";
|
|
767
|
-
case "Write":
|
|
768
|
-
return "writing…";
|
|
769
|
-
case "Edit":
|
|
770
|
-
return "editing…";
|
|
771
|
-
case "Glob":
|
|
772
|
-
return "globbing…";
|
|
773
|
-
case "Grep":
|
|
774
|
-
return "searching…";
|
|
775
|
-
case "TodoWrite":
|
|
776
|
-
return "updating the plan…";
|
|
777
|
-
case "Bash":
|
|
778
|
-
return "running…";
|
|
779
|
-
default:
|
|
780
|
-
return "working…";
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
function defaultDoneFor(raw) {
|
|
784
|
-
const n = raw.replace(/^mcp__[^_]+__/, "");
|
|
785
|
-
switch (n) {
|
|
786
|
-
case "WebSearch":
|
|
787
|
-
return "search complete";
|
|
788
|
-
case "WebFetch":
|
|
789
|
-
return "fetched";
|
|
790
|
-
case "Read":
|
|
791
|
-
return "read";
|
|
792
|
-
case "Write":
|
|
793
|
-
return "written";
|
|
794
|
-
case "Edit":
|
|
795
|
-
return "edited";
|
|
796
|
-
case "Glob":
|
|
797
|
-
return "matched";
|
|
798
|
-
case "Grep":
|
|
799
|
-
return "matched";
|
|
800
|
-
case "TodoWrite":
|
|
801
|
-
return "plan updated";
|
|
802
|
-
case "Bash":
|
|
803
|
-
return "executed";
|
|
804
|
-
default:
|
|
805
|
-
return "done";
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
function initialStage(skillPath) {
|
|
809
|
-
if (!skillPath)
|
|
810
|
-
return { stage: { kind: "missing_skill" }, autoLaunch: null };
|
|
811
|
-
const agents = detectAgents();
|
|
812
|
-
if (agents.length === 0)
|
|
813
|
-
return { stage: { kind: "missing_agent" }, autoLaunch: null };
|
|
814
|
-
// Resolution order is centralized in resolveConfiguredAgent:
|
|
815
|
-
// LINER_AGENT env > ~/.liner/config.yaml > single-installed > null.
|
|
816
|
-
// If it returns a non-null agent, we auto-launch and skip the picker. If
|
|
817
|
-
// it returns null (multiple agents, no config, no env hint), we render
|
|
818
|
-
// the picker as before.
|
|
819
|
-
const resolved = resolveConfiguredAgent(agents);
|
|
820
|
-
if (resolved) {
|
|
821
|
-
return {
|
|
822
|
-
stage: {
|
|
823
|
-
kind: "picking_agent",
|
|
824
|
-
agents,
|
|
825
|
-
selectedIndex: agents.indexOf(resolved),
|
|
826
|
-
},
|
|
827
|
-
autoLaunch: resolved,
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
return {
|
|
831
|
-
stage: { kind: "picking_agent", agents, selectedIndex: 0 },
|
|
832
|
-
autoLaunch: null,
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
function subtitleFor(stage, skillPath) {
|
|
836
|
-
if (stage.kind === "running")
|
|
837
|
-
return `agent: ${stage.agent.name}`;
|
|
838
|
-
if (stage.kind === "done")
|
|
839
|
-
return `agent: ${stage.agent.name} · exit ${stage.code}`;
|
|
840
|
-
if (stage.kind === "picking_agent")
|
|
841
|
-
return skillPath ? `skill: ${skillPath}` : "";
|
|
842
|
-
return "";
|
|
843
|
-
}
|
|
844
|
-
function hintsFor(stage, inspectMode = false) {
|
|
845
|
-
if (stage.kind === "picking_agent") {
|
|
846
|
-
return [
|
|
847
|
-
{ key: "↑↓", label: "pick" },
|
|
848
|
-
{ key: "enter", label: "launch" },
|
|
849
|
-
{ key: "esc", label: "cancel" },
|
|
850
|
-
{ key: "q", label: "quit" },
|
|
851
|
-
];
|
|
852
|
-
}
|
|
853
|
-
if (stage.kind === "running") {
|
|
854
|
-
return [{ key: "esc", label: "cancel run" }];
|
|
855
|
-
}
|
|
856
|
-
if (stage.kind === "done") {
|
|
857
|
-
// Hints split by mode: when inspecting tool calls, only the scroll keys
|
|
858
|
-
// and the exit keys matter; enter/b are intentionally locked so the user
|
|
859
|
-
// doesn't accidentally leave inspect mode by advancing.
|
|
860
|
-
if (inspectMode) {
|
|
861
|
-
return [
|
|
862
|
-
{ key: "↑↓", label: "move cursor" },
|
|
863
|
-
{ key: "pgup/pgdn", label: "jump page" },
|
|
864
|
-
{ key: "t", label: "exit inspect" },
|
|
865
|
-
{ key: "esc", label: "exit inspect" },
|
|
866
|
-
];
|
|
867
|
-
}
|
|
868
|
-
const failed = stage.code !== 0;
|
|
869
|
-
return [
|
|
870
|
-
{ key: "↑↓", label: "scroll response" },
|
|
871
|
-
{ key: "pgup/pgdn", label: "jump" },
|
|
872
|
-
{ key: "enter", label: failed ? "try again" : "continue" },
|
|
873
|
-
{ key: "t", label: "inspect tool calls" },
|
|
874
|
-
{ key: "b", label: "back to hub" },
|
|
875
|
-
{ key: "q", label: "quit" },
|
|
876
|
-
];
|
|
877
|
-
}
|
|
878
|
-
return [{ key: "b", label: "back" }];
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* After a phase run completes, what's the obvious next screen? Returns null
|
|
882
|
-
* when we should just go back to the hub.
|
|
883
|
-
*
|
|
884
|
-
* Mode-aware: quick mode skips the gate review screens (per CURATION.md v2.0
|
|
885
|
-
* — "Quick mode defaults them to continue"). We auto-accept the gate state
|
|
886
|
-
* file as a side effect here so the hub's progress strip still shows the
|
|
887
|
-
* gate as ✓ complete.
|
|
888
|
-
*/
|
|
889
|
-
function nextScreenFor(phaseId, tape, folder) {
|
|
890
|
-
const isQuick = (tape.mode ?? "quick") !== "methodology";
|
|
891
|
-
const project = projectFolder(folder);
|
|
892
|
-
switch (phaseId) {
|
|
893
|
-
case "framing":
|
|
894
|
-
if (isQuick) {
|
|
895
|
-
// Quick mode skips the gate. Auto-accept gate0 + advance the cursor
|
|
896
|
-
// so the progress strip stays consistent (no skipped step).
|
|
897
|
-
const state = readGateState(project);
|
|
898
|
-
writeGateState(project, { ...state, gate0Accepted: true });
|
|
899
|
-
markPhaseComplete(folder, "gate0");
|
|
900
|
-
return { kind: "phaseRunner", tape, folder, phaseId: "candidates" };
|
|
901
|
-
}
|
|
902
|
-
return { kind: "gate0", tape, folder };
|
|
903
|
-
case "candidates":
|
|
904
|
-
if (isQuick) {
|
|
905
|
-
// Auto-accept Gate 1 — both the legacy gate-state file (so other
|
|
906
|
-
// surfaces still recognise it as accepted) and the progress cursor.
|
|
907
|
-
const state = readGateState(project);
|
|
908
|
-
writeGateState(project, { ...state, gate1Accepted: true });
|
|
909
|
-
markPhaseComplete(folder, "gate1");
|
|
910
|
-
return { kind: "phaseRunner", tape, folder, phaseId: "evaluation" };
|
|
911
|
-
}
|
|
912
|
-
return { kind: "gate1", tape, folder };
|
|
913
|
-
case "evaluation":
|
|
914
|
-
return { kind: "phaseRunner", tape, folder, phaseId: "quality" };
|
|
915
|
-
case "quality":
|
|
916
|
-
if (isQuick) {
|
|
917
|
-
const state = readGateState(project);
|
|
918
|
-
writeGateState(project, { ...state, gate2Accepted: true });
|
|
919
|
-
markPhaseComplete(folder, "gate2");
|
|
920
|
-
return { kind: "phaseRunner", tape, folder, phaseId: "synthesis" };
|
|
921
|
-
}
|
|
922
|
-
return { kind: "gate2", tape, folder };
|
|
923
|
-
case "synthesis":
|
|
924
|
-
return { kind: "phaseRunner", tape, folder, phaseId: "assembly" };
|
|
925
|
-
case "assembly":
|
|
926
|
-
// Handled in the .then() — auto-navigates to tape draft. If somehow we
|
|
927
|
-
// land on done without a draft, go back to the hub.
|
|
928
|
-
return null;
|
|
929
|
-
default:
|
|
930
|
-
return null;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
function nextScreenLabel(phaseId, tape, _folder) {
|
|
934
|
-
const isQuick = (tape.mode ?? "quick") !== "methodology";
|
|
935
|
-
switch (phaseId) {
|
|
936
|
-
case "framing":
|
|
937
|
-
return isQuick
|
|
938
|
-
? "continue to Phase 2 — Candidate discovery (framing auto-confirmed in quick mode)"
|
|
939
|
-
: "continue to Confirm framing — review the knowledge map";
|
|
940
|
-
case "candidates":
|
|
941
|
-
return isQuick
|
|
942
|
-
? "continue to Phase 3 — Evaluation (candidates auto-confirmed in quick mode)"
|
|
943
|
-
: "continue to Confirm candidates — review the long-list";
|
|
944
|
-
case "evaluation":
|
|
945
|
-
return "continue to Phase 4 — Quality checks";
|
|
946
|
-
case "quality":
|
|
947
|
-
return isQuick
|
|
948
|
-
? "continue to Phase 5 — Synthesis (evaluation auto-confirmed in quick mode)"
|
|
949
|
-
: "continue to Confirm evaluation — review keep-list + checks";
|
|
950
|
-
case "synthesis":
|
|
951
|
-
return "continue to Phase 6 — Assembly";
|
|
952
|
-
case "assembly":
|
|
953
|
-
return "review the proposed sources";
|
|
954
|
-
default:
|
|
955
|
-
return "back to the hub";
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
function summarizeInput(input) {
|
|
959
|
-
if (!input || typeof input !== "object")
|
|
960
|
-
return "";
|
|
961
|
-
const o = input;
|
|
962
|
-
if (typeof o["file_path"] === "string")
|
|
963
|
-
return o["file_path"];
|
|
964
|
-
if (typeof o["path"] === "string")
|
|
965
|
-
return o["path"];
|
|
966
|
-
if (typeof o["url"] === "string")
|
|
967
|
-
return o["url"];
|
|
968
|
-
if (typeof o["pattern"] === "string")
|
|
969
|
-
return o["pattern"];
|
|
970
|
-
if (typeof o["command"] === "string")
|
|
971
|
-
return o["command"];
|
|
972
|
-
if (typeof o["query"] === "string")
|
|
973
|
-
return o["query"];
|
|
974
|
-
try {
|
|
975
|
-
return JSON.stringify(o).slice(0, 80);
|
|
976
|
-
}
|
|
977
|
-
catch {
|
|
978
|
-
return "";
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
function formatTokens(n) {
|
|
982
|
-
if (n < 1_000)
|
|
983
|
-
return String(n);
|
|
984
|
-
if (n < 1_000_000)
|
|
985
|
-
return `${(n / 1_000).toFixed(1)}k`;
|
|
986
|
-
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
987
|
-
}
|
|
988
|
-
function formatDuration(ms) {
|
|
989
|
-
if (ms < 60_000)
|
|
990
|
-
return `${(ms / 1000).toFixed(0)}s`;
|
|
991
|
-
const minutes = Math.floor(ms / 60_000);
|
|
992
|
-
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
993
|
-
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
994
|
-
}
|
|
995
|
-
function zeroTokens() {
|
|
996
|
-
return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
997
|
-
}
|
|
998
|
-
function finalResponseViewportLines(termRows, inspectMode) {
|
|
999
|
-
if (inspectMode)
|
|
1000
|
-
return FINAL_RESPONSE_INSPECT_VIEWPORT;
|
|
1001
|
-
// Keep enough space for status, tool calls, next-action line, and key hints.
|
|
1002
|
-
// The body lines still may wrap, so this is intentionally conservative.
|
|
1003
|
-
return Math.max(3, Math.min(FINAL_RESPONSE_VIEWPORT, termRows - 34));
|
|
1004
|
-
}
|
|
1005
|
-
function finalResponseLabel(text, scrollTop, viewportLines) {
|
|
1006
|
-
const total = text.split("\n").length;
|
|
1007
|
-
const maxScroll = Math.max(0, total - viewportLines);
|
|
1008
|
-
const start = Math.min(Math.max(0, scrollTop), maxScroll);
|
|
1009
|
-
const end = Math.min(total, start + viewportLines);
|
|
1010
|
-
return total > viewportLines
|
|
1011
|
-
? `final response · lines ${start + 1}-${end} / ${total}`
|
|
1012
|
-
: "final response";
|
|
1013
|
-
}
|
|
1014
|
-
function truncate(s, n) {
|
|
1015
|
-
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
1016
|
-
}
|
|
1017
|
-
/**
|
|
1018
|
-
* Right-anchored truncation for file paths. The basename is the load-bearing
|
|
1019
|
-
* part of any path the agent reads/writes; keeping it visible is much more
|
|
1020
|
-
* useful than seeing "/Users/me/Docu…" without the actual filename.
|
|
1021
|
-
*/
|
|
1022
|
-
function truncatePath(s, n) {
|
|
1023
|
-
if (s.length <= n)
|
|
1024
|
-
return s;
|
|
1025
|
-
return "…" + s.slice(-(n - 1));
|
|
1026
|
-
}
|
|
1027
|
-
/**
|
|
1028
|
-
* Heuristic: looks like a filesystem path? Used to pick path-style vs.
|
|
1029
|
-
* plain truncation for the tool-call preview line.
|
|
1030
|
-
*/
|
|
1031
|
-
function looksLikePath(s) {
|
|
1032
|
-
return s.startsWith("/") || s.startsWith("./") || s.startsWith("../") ||
|
|
1033
|
-
/^[A-Za-z]:[\\/]/.test(s) ||
|
|
1034
|
-
(s.includes("/") && !s.includes(" ") && !s.includes("://"));
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* Truncate a value smartly: file paths keep their basename, URLs and other
|
|
1038
|
-
* strings get standard right-trim ellipsis.
|
|
1039
|
-
*/
|
|
1040
|
-
function previewify(s, n) {
|
|
1041
|
-
if (!s)
|
|
1042
|
-
return "";
|
|
1043
|
-
if (looksLikePath(s))
|
|
1044
|
-
return truncatePath(s, n);
|
|
1045
|
-
return truncate(s, n);
|
|
1046
|
-
}
|
|
1047
|
-
// ---------------------------------------------------------------------------
|
|
1048
|
-
// Markdown rendering for the agent's final response.
|
|
1049
|
-
//
|
|
1050
|
-
// The agent produces a small markdown blob with `**heading:**` lines, `-` bullets,
|
|
1051
|
-
// and inline `` `code` `` / `[bracketed paths]`. Rendering it raw shows the
|
|
1052
|
-
// asterisks as literal characters, which looks unfinished. This renderer:
|
|
1053
|
-
//
|
|
1054
|
-
// - Strips ** and renders the bold runs with weight + a heading color for
|
|
1055
|
-
// start-of-line headings (the ones ending with `:`).
|
|
1056
|
-
// - Renders `-` / `*` bullets with a cyan `•` glyph and indents the rest.
|
|
1057
|
-
// - Renders ``backtick code`` runs in cyan.
|
|
1058
|
-
// - Detects trailing `[paths/with/slashes]` and dims them so the verb-phrase
|
|
1059
|
-
// leads.
|
|
1060
|
-
//
|
|
1061
|
-
// Intentionally not a full markdown engine — just enough to make the agent's
|
|
1062
|
-
// output legible. If the format grows beyond this, swap in `marked-terminal`
|
|
1063
|
-
// or `ink-markdown`.
|
|
1064
|
-
// ---------------------------------------------------------------------------
|
|
1065
|
-
function MarkdownBody({ text, maxLines = 18, scrollTop = 0, color, }) {
|
|
1066
|
-
const lines = text.split("\n");
|
|
1067
|
-
const maxScroll = Math.max(0, lines.length - maxLines);
|
|
1068
|
-
const start = Math.min(Math.max(0, scrollTop), maxScroll);
|
|
1069
|
-
const shown = lines.slice(start, start + maxLines);
|
|
1070
|
-
const hiddenAbove = start;
|
|
1071
|
-
const hiddenBelow = Math.max(0, lines.length - (start + shown.length));
|
|
1072
|
-
return (React.createElement(Box, { flexDirection: "column" },
|
|
1073
|
-
hiddenAbove > 0 ? (React.createElement(Text, { color: MUTED },
|
|
1074
|
-
"\u2191 ",
|
|
1075
|
-
hiddenAbove,
|
|
1076
|
-
" earlier line",
|
|
1077
|
-
hiddenAbove === 1 ? "" : "s")) : null,
|
|
1078
|
-
shown.map((line, i) => (React.createElement(MarkdownLine, { key: start + i, line: line, color: color }))),
|
|
1079
|
-
hiddenBelow > 0 ? (React.createElement(Box, { marginTop: 1 },
|
|
1080
|
-
React.createElement(Text, { color: MUTED },
|
|
1081
|
-
"\u2193 ",
|
|
1082
|
-
hiddenBelow,
|
|
1083
|
-
" more line",
|
|
1084
|
-
hiddenBelow === 1 ? "" : "s",
|
|
1085
|
-
" \u00B7 use \u2191\u2193 or pgup/pgdn"))) : null));
|
|
1086
|
-
}
|
|
1087
|
-
function MarkdownLine({ line, color }) {
|
|
1088
|
-
// Empty line → one-row spacer (keep paragraphs visible).
|
|
1089
|
-
if (!line.trim()) {
|
|
1090
|
-
return React.createElement(Text, null, " ");
|
|
1091
|
-
}
|
|
1092
|
-
// Bullet line: - / * prefix. The marker stays orange (STRUCTURE); the
|
|
1093
|
-
// rest of the bullet text takes the body color.
|
|
1094
|
-
const bullet = /^(\s*)([-*])\s+(.*)$/.exec(line);
|
|
1095
|
-
if (bullet) {
|
|
1096
|
-
const [, indent, , rest] = bullet;
|
|
1097
|
-
return (React.createElement(Box, null,
|
|
1098
|
-
React.createElement(Text, null, indent),
|
|
1099
|
-
React.createElement(Text, { color: STRUCTURE }, "\u2022 "),
|
|
1100
|
-
React.createElement(InlineSegments, { text: rest ?? "", color: color })));
|
|
1101
|
-
}
|
|
1102
|
-
return (React.createElement(Box, null,
|
|
1103
|
-
React.createElement(InlineSegments, { text: line, headingDetect: true, color: color })));
|
|
1104
|
-
}
|
|
1105
|
-
/**
|
|
1106
|
-
* Tokenize a single line by `**bold**` and `` `code` `` runs.
|
|
1107
|
-
* `**` and backticks themselves are dropped. Unclosed markers fall back to
|
|
1108
|
-
* plain text so we never lose content.
|
|
1109
|
-
*/
|
|
1110
|
-
function tokenize(s) {
|
|
1111
|
-
const out = [];
|
|
1112
|
-
let i = 0;
|
|
1113
|
-
let buf = "";
|
|
1114
|
-
const flush = () => {
|
|
1115
|
-
if (buf) {
|
|
1116
|
-
out.push({ kind: "plain", text: buf });
|
|
1117
|
-
buf = "";
|
|
1118
|
-
}
|
|
1119
|
-
};
|
|
1120
|
-
while (i < s.length) {
|
|
1121
|
-
if (s[i] === "*" && s[i + 1] === "*") {
|
|
1122
|
-
const end = s.indexOf("**", i + 2);
|
|
1123
|
-
if (end > 0) {
|
|
1124
|
-
flush();
|
|
1125
|
-
out.push({ kind: "bold", text: s.slice(i + 2, end) });
|
|
1126
|
-
i = end + 2;
|
|
1127
|
-
continue;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
if (s[i] === "`") {
|
|
1131
|
-
const end = s.indexOf("`", i + 1);
|
|
1132
|
-
if (end > 0) {
|
|
1133
|
-
flush();
|
|
1134
|
-
out.push({ kind: "code", text: s.slice(i + 1, end) });
|
|
1135
|
-
i = end + 1;
|
|
1136
|
-
continue;
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
buf += s[i];
|
|
1140
|
-
i++;
|
|
1141
|
-
}
|
|
1142
|
-
flush();
|
|
1143
|
-
return out;
|
|
1144
|
-
}
|
|
1145
|
-
/**
|
|
1146
|
-
* Render the inline segments of a line. If `headingDetect` is set and the
|
|
1147
|
-
* first segment is a bold-ending-with-`:`, treat it as a section heading and
|
|
1148
|
-
* give the bold run a cyan tint — so the agent's `**Wrote:**` style ledes
|
|
1149
|
-
* carry the eye.
|
|
1150
|
-
*/
|
|
1151
|
-
function InlineSegments({ text, headingDetect = false, color, }) {
|
|
1152
|
-
const segs = tokenize(text);
|
|
1153
|
-
const firstBold = segs[0]?.kind === "bold" && segs[0].text.trim().endsWith(":");
|
|
1154
|
-
return (React.createElement(React.Fragment, null, segs.map((seg, i) => renderSeg(seg, i, headingDetect && firstBold && i === 0, color))));
|
|
1155
|
-
}
|
|
1156
|
-
function renderSeg(seg, key, isHeading, color) {
|
|
1157
|
-
if (seg.kind === "code") {
|
|
1158
|
-
// Inline code keeps the structural orange regardless of body color.
|
|
1159
|
-
return (React.createElement(Text, { key: key, color: STRUCTURE }, seg.text));
|
|
1160
|
-
}
|
|
1161
|
-
if (seg.kind === "bold") {
|
|
1162
|
-
return (React.createElement(Text, { key: key, bold: true, color: color }, seg.text));
|
|
1163
|
-
}
|
|
1164
|
-
// Plain text — dim `[bracketed paths]` so they recede.
|
|
1165
|
-
return renderPlainWithBrackets(seg.text, key, color);
|
|
1166
|
-
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Plain text, with `[paths/like/this.md]` substrings rendered dim. The
|
|
1169
|
-
* heuristic: a `[...]` run that contains at least one `/` or `.` is treated
|
|
1170
|
-
* as a path/citation. Anything else is rendered as-is.
|
|
1171
|
-
*/
|
|
1172
|
-
function renderPlainWithBrackets(text, key, color) {
|
|
1173
|
-
const parts = [];
|
|
1174
|
-
let i = 0;
|
|
1175
|
-
let cursor = 0;
|
|
1176
|
-
let k = 0;
|
|
1177
|
-
while (i < text.length) {
|
|
1178
|
-
if (text[i] === "[") {
|
|
1179
|
-
const close = text.indexOf("]", i + 1);
|
|
1180
|
-
if (close > 0) {
|
|
1181
|
-
const inside = text.slice(i + 1, close);
|
|
1182
|
-
if (inside.includes("/") || inside.includes(".")) {
|
|
1183
|
-
if (i > cursor) {
|
|
1184
|
-
parts.push(React.createElement(Text, { key: `${key}-${k++}`, color: color }, text.slice(cursor, i)));
|
|
1185
|
-
}
|
|
1186
|
-
parts.push(React.createElement(Text, { key: `${key}-${k++}`, color: MUTED },
|
|
1187
|
-
"[",
|
|
1188
|
-
inside,
|
|
1189
|
-
"]"));
|
|
1190
|
-
i = close + 1;
|
|
1191
|
-
cursor = i;
|
|
1192
|
-
continue;
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
i++;
|
|
1197
|
-
}
|
|
1198
|
-
if (cursor < text.length) {
|
|
1199
|
-
parts.push(React.createElement(Text, { key: `${key}-${k++}`, color: color }, text.slice(cursor)));
|
|
1200
|
-
}
|
|
1201
|
-
return React.createElement(React.Fragment, { key: key }, parts);
|
|
1202
|
-
}
|
|
1203
|
-
//# sourceMappingURL=PhaseRunner.js.map
|