linersh 0.5.7 → 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.
Files changed (98) hide show
  1. package/README.md +65 -165
  2. package/bin/liner.js +82 -44
  3. package/cli-update-docs/ABOUT.md +16 -16
  4. package/cli-update-docs/AI-HANDOFF.md +62 -204
  5. package/cli-update-docs/CURATION.md +48 -19
  6. package/cli-update-docs/JTBD-MASTER-PROMPT-ABOUT.md +9 -12
  7. package/cli-update-docs/JTBD-MASTER-PROMPT.md +1 -1
  8. package/cli-update-docs/LINER-MASTER.md +77 -63
  9. package/cli-update-docs/LINER-MD-PRD.md +531 -0
  10. package/cli-update-docs/MIXTAPE-FORMAT.md +76 -44
  11. package/cli-update-docs/PLATFORM.md +47 -46
  12. package/cli-update-docs/README.md +6 -6
  13. package/cli-update-docs/SKILL.md +72 -37
  14. package/cli-update-docs/TUI-BACKLOG.md +205 -26
  15. package/cli-update-docs/quality-check-tests.md +256 -8
  16. package/cli-update-docs/source-finding-tactics.md +140 -3
  17. package/cli-update-docs/source-quality-hierarchy.md +83 -0
  18. package/cli-update-docs/synthesis-guidance.md +19 -5
  19. package/dist/agents/artifact-validation.js +440 -0
  20. package/dist/agents/artifact-validation.js.map +1 -0
  21. package/dist/agents/candidate-longlist.js +100 -0
  22. package/dist/agents/candidate-longlist.js.map +1 -0
  23. package/dist/agents/detect.js +3 -2
  24. package/dist/agents/detect.js.map +1 -1
  25. package/dist/agents/evaluation-assembly.js +270 -0
  26. package/dist/agents/evaluation-assembly.js.map +1 -0
  27. package/dist/agents/headless-runner.js +227 -0
  28. package/dist/agents/headless-runner.js.map +1 -0
  29. package/dist/agents/jtbd-clarify.js +42 -41
  30. package/dist/agents/jtbd-clarify.js.map +1 -1
  31. package/dist/agents/models.js +6 -7
  32. package/dist/agents/models.js.map +1 -1
  33. package/dist/agents/prompts.js +220 -20
  34. package/dist/agents/prompts.js.map +1 -1
  35. package/dist/agents/runner.js +462 -21
  36. package/dist/agents/runner.js.map +1 -1
  37. package/dist/phases.js +13 -13
  38. package/dist/phases.js.map +1 -1
  39. package/dist/source-inbox.js +297 -0
  40. package/dist/source-inbox.js.map +1 -0
  41. package/dist/version.js +3 -9
  42. package/dist/version.js.map +1 -1
  43. package/dist/yaml-io.js +42 -11
  44. package/dist/yaml-io.js.map +1 -1
  45. package/package.json +26 -23
  46. package/cli-update-docs/JOB-STORY-MIGRATION.md +0 -99
  47. package/dist/App.js +0 -220
  48. package/dist/App.js.map +0 -1
  49. package/dist/colors.js +0 -132
  50. package/dist/colors.js.map +0 -1
  51. package/dist/components/Header.js +0 -15
  52. package/dist/components/Header.js.map +0 -1
  53. package/dist/components/KeyHints.js +0 -17
  54. package/dist/components/KeyHints.js.map +0 -1
  55. package/dist/components/LabeledBox.js +0 -53
  56. package/dist/components/LabeledBox.js.map +0 -1
  57. package/dist/components/NextSteps.js +0 -18
  58. package/dist/components/NextSteps.js.map +0 -1
  59. package/dist/index.js +0 -17
  60. package/dist/index.js.map +0 -1
  61. package/dist/screens/AgentSetup.js +0 -224
  62. package/dist/screens/AgentSetup.js.map +0 -1
  63. package/dist/screens/BootSplash.js +0 -163
  64. package/dist/screens/BootSplash.js.map +0 -1
  65. package/dist/screens/CompileView.js +0 -567
  66. package/dist/screens/CompileView.js.map +0 -1
  67. package/dist/screens/FramingConfirmReview.js +0 -197
  68. package/dist/screens/FramingConfirmReview.js.map +0 -1
  69. package/dist/screens/Gate1Review.js +0 -147
  70. package/dist/screens/Gate1Review.js.map +0 -1
  71. package/dist/screens/Gate2Review.js +0 -162
  72. package/dist/screens/Gate2Review.js.map +0 -1
  73. package/dist/screens/ImportPrompt.js +0 -173
  74. package/dist/screens/ImportPrompt.js.map +0 -1
  75. package/dist/screens/JsSetup.js +0 -77
  76. package/dist/screens/JsSetup.js.map +0 -1
  77. package/dist/screens/LocalFilePicker.js +0 -139
  78. package/dist/screens/LocalFilePicker.js.map +0 -1
  79. package/dist/screens/NewMixtapeWizard.js +0 -662
  80. package/dist/screens/NewMixtapeWizard.js.map +0 -1
  81. package/dist/screens/PhaseRunner.js +0 -1203
  82. package/dist/screens/PhaseRunner.js.map +0 -1
  83. package/dist/screens/ProcessManifest.js +0 -160
  84. package/dist/screens/ProcessManifest.js.map +0 -1
  85. package/dist/screens/ProjectHub.js +0 -557
  86. package/dist/screens/ProjectHub.js.map +0 -1
  87. package/dist/screens/SourceModal.js +0 -269
  88. package/dist/screens/SourceModal.js.map +0 -1
  89. package/dist/screens/TapeBrowser.js +0 -312
  90. package/dist/screens/TapeBrowser.js.map +0 -1
  91. package/dist/screens/TapeDraftReview.js +0 -444
  92. package/dist/screens/TapeDraftReview.js.map +0 -1
  93. package/dist/screens/TapeEditor.js +0 -310
  94. package/dist/screens/TapeEditor.js.map +0 -1
  95. package/dist/screens/cassette-art.js +0 -72
  96. package/dist/screens/cassette-art.js.map +0 -1
  97. package/dist/store.js +0 -10
  98. 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