@defrex/autobuild 0.8.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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared session-view projection: protocol parts → display lines.
|
|
3
|
+
*
|
|
4
|
+
* This module is the one place the session view's VOCABULARY is decided. The
|
|
5
|
+
* terminal renderer consumes its output and so will the web app later; nothing
|
|
6
|
+
* outside this file decides what a part says. Two adapters feed one rendering
|
|
7
|
+
* core:
|
|
8
|
+
*
|
|
9
|
+
* - `projectSessionParts` renders the live/chunk path — raw `StreamPart[]`
|
|
10
|
+
* in store-sequence order, including parts that have not assembled (a tool
|
|
11
|
+
* call with input but no output yet).
|
|
12
|
+
* - `projectSessionDocument` renders the finalized `UIMessage[]` artifact
|
|
13
|
+
* that `closeStream` deposits. Note the SDK's document assembly drops
|
|
14
|
+
* `error` and `abort` chunks (they never become message parts), so those
|
|
15
|
+
* elements are only visible on the parts path.
|
|
16
|
+
*
|
|
17
|
+
* Both are pure functions of (input, width): stored parts are never altered,
|
|
18
|
+
* and no line depends on color. Reasoning is marked by a literal `~ ` prefix so
|
|
19
|
+
* it stays distinct from answer text even with the escapes stripped.
|
|
20
|
+
*/
|
|
21
|
+
import type { UIMessage } from 'ai'
|
|
22
|
+
import type { StreamPart } from '../../store/streams/types'
|
|
23
|
+
import { cellWidth, displayText, graphemes } from './cells'
|
|
24
|
+
import { wrapDisplay } from './composer'
|
|
25
|
+
|
|
26
|
+
/** Documented cap on the wrapped rows rendered for one tool output. */
|
|
27
|
+
export const SESSION_OUTPUT_MAX_ROWS = 8
|
|
28
|
+
|
|
29
|
+
/** One `data-ab-session` part's bracket identity, the header's source. */
|
|
30
|
+
export interface SessionHeaderInfo {
|
|
31
|
+
session: string
|
|
32
|
+
role: string
|
|
33
|
+
runner: string
|
|
34
|
+
model?: string
|
|
35
|
+
phase: string
|
|
36
|
+
round?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type DisplayEvent =
|
|
40
|
+
| { kind: 'header'; info: SessionHeaderInfo }
|
|
41
|
+
| { kind: 'prompt'; text: string }
|
|
42
|
+
| { kind: 'text'; text: string }
|
|
43
|
+
| { kind: 'reasoning'; text: string }
|
|
44
|
+
| {
|
|
45
|
+
kind: 'tool'
|
|
46
|
+
toolName: string
|
|
47
|
+
input?: unknown
|
|
48
|
+
output?: unknown
|
|
49
|
+
awaiting: boolean
|
|
50
|
+
errorText?: string
|
|
51
|
+
}
|
|
52
|
+
| { kind: 'step' }
|
|
53
|
+
| { kind: 'error'; text: string }
|
|
54
|
+
| { kind: 'abort'; text: string }
|
|
55
|
+
| { kind: 'truncation'; bytes: number }
|
|
56
|
+
| { kind: 'data'; name: string }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The dashboard's prose wrap. Duplicated from `render.ts`'s file-local
|
|
60
|
+
* `wrappedText` rather than exported from there: that file already imports
|
|
61
|
+
* this module's output vocabulary, so sharing the helper would create an
|
|
62
|
+
* import cycle for one narrow function. This copy keeps the same byte-stable
|
|
63
|
+
* behavior — paragraph structure preserved, a token wider than the line
|
|
64
|
+
* truncated with a `~`.
|
|
65
|
+
*/
|
|
66
|
+
function wrapProse(value: string, width: number, indent = ''): string[] {
|
|
67
|
+
if (width <= 0) return []
|
|
68
|
+
const lines: string[] = []
|
|
69
|
+
for (const paragraph of value.split(/\r?\n/)) {
|
|
70
|
+
const safe = displayText(paragraph)
|
|
71
|
+
if (safe.length === 0) {
|
|
72
|
+
lines.push(indent)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
const wrapped = wrapDisplay(safe, width, indent)
|
|
76
|
+
lines.push(...(wrapped.length > 0 ? wrapped : [indent]))
|
|
77
|
+
}
|
|
78
|
+
return lines
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Cell-honest one-line cut with a `…` continuation marker. */
|
|
82
|
+
function truncateOneLine(value: string, width: number): string {
|
|
83
|
+
const safe = displayText(value)
|
|
84
|
+
if (width <= 0) return ''
|
|
85
|
+
if (cellWidth(safe) <= width) return safe
|
|
86
|
+
let out = ''
|
|
87
|
+
let used = 0
|
|
88
|
+
const budget = Math.max(0, width - 1)
|
|
89
|
+
for (const cluster of graphemes(safe)) {
|
|
90
|
+
if (used + cluster.width > budget) break
|
|
91
|
+
out += cluster.text
|
|
92
|
+
used += cluster.width
|
|
93
|
+
}
|
|
94
|
+
return `${out}…`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function headerLine(info: SessionHeaderInfo | undefined): string {
|
|
98
|
+
if (info === undefined) return 'unknown session'
|
|
99
|
+
const tokens = [
|
|
100
|
+
info.role,
|
|
101
|
+
info.runner,
|
|
102
|
+
...(info.model !== undefined ? [info.model] : []),
|
|
103
|
+
`phase ${info.phase}`,
|
|
104
|
+
]
|
|
105
|
+
const round = info.round !== undefined ? ` (round ${info.round})` : ''
|
|
106
|
+
return `${tokens.join(' · ')}${round}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function outputLines(output: unknown, width: number): string[] {
|
|
110
|
+
const text = typeof output === 'string' ? output : JSON.stringify(output)
|
|
111
|
+
if (typeof text !== 'string' || text.length === 0) return []
|
|
112
|
+
return wrapProse(text, width)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Cap one block at `SESSION_OUTPUT_MAX_ROWS` rows, naming what is withheld. */
|
|
116
|
+
function capRows(rows: string[]): string[] {
|
|
117
|
+
if (rows.length <= SESSION_OUTPUT_MAX_ROWS) return rows
|
|
118
|
+
const withheld = rows.length - SESSION_OUTPUT_MAX_ROWS
|
|
119
|
+
return [...rows.slice(0, SESSION_OUTPUT_MAX_ROWS), `… ${withheld} more rows withheld`]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function inputSummary(input: unknown): string {
|
|
123
|
+
return typeof input === 'undefined' ? '' : JSON.stringify(input)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderEvents(events: readonly DisplayEvent[], width: number): string[] {
|
|
127
|
+
const lines: string[] = []
|
|
128
|
+
let headerSeen = false
|
|
129
|
+
let sawContent = false
|
|
130
|
+
let lastWasStep = false
|
|
131
|
+
const push = (...added: string[]): void => {
|
|
132
|
+
lines.push(...added)
|
|
133
|
+
if (added.length > 0) {
|
|
134
|
+
sawContent = true
|
|
135
|
+
lastWasStep = false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const pushProse = (text: string, width_: number): void => {
|
|
139
|
+
if (text.length === 0) return
|
|
140
|
+
const wrapped = wrapProse(text, width_)
|
|
141
|
+
if (wrapped.length === 0) return
|
|
142
|
+
push(...wrapped)
|
|
143
|
+
}
|
|
144
|
+
const pushStep = (): void => {
|
|
145
|
+
// The leading boundary would only double-separate the header, and two
|
|
146
|
+
// adjacent boundaries (finish then start) render one separator.
|
|
147
|
+
if (!sawContent || lastWasStep) return
|
|
148
|
+
lines.push('── step ──')
|
|
149
|
+
sawContent = true
|
|
150
|
+
lastWasStep = true
|
|
151
|
+
}
|
|
152
|
+
const pushMarked = (text: string, width_: number, mark: string): void => {
|
|
153
|
+
if (text.length === 0) return
|
|
154
|
+
for (const line of wrapProse(text, Math.max(1, width_ - mark.length))) {
|
|
155
|
+
push(`${mark}${line}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
switch (event.kind) {
|
|
161
|
+
case 'header': {
|
|
162
|
+
headerSeen = true
|
|
163
|
+
push(headerLine(event.info))
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
case 'prompt':
|
|
167
|
+
if (event.text.length > 0) push(...wrapProse(`Prompt: ${event.text}`, width))
|
|
168
|
+
break
|
|
169
|
+
case 'text':
|
|
170
|
+
pushProse(event.text, width)
|
|
171
|
+
break
|
|
172
|
+
case 'reasoning':
|
|
173
|
+
// Distinct from answer text WITHOUT color: a literal prefix that
|
|
174
|
+
// survives `--plain` and every pipe.
|
|
175
|
+
pushMarked(event.text, width, '~ ')
|
|
176
|
+
break
|
|
177
|
+
case 'tool': {
|
|
178
|
+
push(truncateOneLine(`${event.toolName}(${inputSummary(event.input)})`, width))
|
|
179
|
+
if (event.awaiting) push('waiting for output')
|
|
180
|
+
if (event.errorText !== undefined) push(`ERROR: ${event.errorText}`)
|
|
181
|
+
else if (event.output !== undefined) push(...capRows(outputLines(event.output, width)))
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
case 'step':
|
|
185
|
+
pushStep()
|
|
186
|
+
break
|
|
187
|
+
case 'error':
|
|
188
|
+
push(`ERROR: ${event.text}`)
|
|
189
|
+
break
|
|
190
|
+
case 'abort':
|
|
191
|
+
push(`ABORTED: ${event.text}`)
|
|
192
|
+
break
|
|
193
|
+
case 'truncation':
|
|
194
|
+
push(`… ${event.bytes} bytes truncated`)
|
|
195
|
+
break
|
|
196
|
+
case 'data':
|
|
197
|
+
push(`data ${event.name}`)
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// An absent session header part still leaves a neutral header line, so the
|
|
202
|
+
// view never opens without naming its bracket.
|
|
203
|
+
if (!headerSeen) lines.unshift('unknown session')
|
|
204
|
+
return lines
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Parts adapter (the live/chunk path) ──────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
interface OpenRun {
|
|
210
|
+
id: string
|
|
211
|
+
buffer: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function collectPartEvents(parts: readonly StreamPart[]): DisplayEvent[] {
|
|
215
|
+
// Tool outputs arrive in later chunks than their inputs; resolve them in a
|
|
216
|
+
// first pass so the tool call renders complete wherever its input landed.
|
|
217
|
+
const outputs = new Map<string, { output?: unknown; errorText?: string }>()
|
|
218
|
+
for (const part of parts) {
|
|
219
|
+
if (part.type === 'tool-output-available' && typeof part.toolCallId === 'string') {
|
|
220
|
+
outputs.set(part.toolCallId, { output: part.output })
|
|
221
|
+
}
|
|
222
|
+
if (part.type === 'tool-output-error' && typeof part.toolCallId === 'string') {
|
|
223
|
+
outputs.set(part.toolCallId, {
|
|
224
|
+
...(typeof part.errorText === 'string' ? { errorText: part.errorText } : {}),
|
|
225
|
+
...(part.output !== undefined ? { output: part.output } : {}),
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const events: DisplayEvent[] = []
|
|
231
|
+
let headerSeen = false
|
|
232
|
+
let openText: OpenRun | undefined
|
|
233
|
+
let openReasoning: OpenRun | undefined
|
|
234
|
+
const flush = (run: OpenRun | undefined, kind: 'text' | 'reasoning'): void => {
|
|
235
|
+
if (run === undefined || run.buffer.length === 0) return
|
|
236
|
+
events.push(
|
|
237
|
+
kind === 'text'
|
|
238
|
+
? { kind: 'text', text: run.buffer }
|
|
239
|
+
: { kind: 'reasoning', text: run.buffer },
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
// A still-open text/reasoning run renders at the position the stream
|
|
243
|
+
// reached: any other part flushes it, so content that started before a tool
|
|
244
|
+
// call is never painted after it.
|
|
245
|
+
const flushOpenRuns = (): void => {
|
|
246
|
+
flush(openText, 'text')
|
|
247
|
+
flush(openReasoning, 'reasoning')
|
|
248
|
+
openText = undefined
|
|
249
|
+
openReasoning = undefined
|
|
250
|
+
}
|
|
251
|
+
const open = (
|
|
252
|
+
current: OpenRun | undefined,
|
|
253
|
+
id: unknown,
|
|
254
|
+
kind: 'text' | 'reasoning',
|
|
255
|
+
): OpenRun | undefined => {
|
|
256
|
+
if (typeof id !== 'string') return current
|
|
257
|
+
if (current?.id === id) return current
|
|
258
|
+
flush(current, kind)
|
|
259
|
+
return { id, buffer: '' }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for (const part of parts) {
|
|
263
|
+
switch (part.type) {
|
|
264
|
+
case 'data-ab-session': {
|
|
265
|
+
flushOpenRuns()
|
|
266
|
+
const data = part.data
|
|
267
|
+
if (typeof data !== 'object' || data === null) {
|
|
268
|
+
if (!headerSeen) headerSeen = true
|
|
269
|
+
events.push({ kind: 'data', name: part.type })
|
|
270
|
+
break
|
|
271
|
+
}
|
|
272
|
+
const record = data as Record<string, unknown>
|
|
273
|
+
if (!headerSeen && typeof record.role === 'string' && typeof record.runner === 'string') {
|
|
274
|
+
headerSeen = true
|
|
275
|
+
events.push({
|
|
276
|
+
kind: 'header',
|
|
277
|
+
info: {
|
|
278
|
+
...(typeof record.session === 'string'
|
|
279
|
+
? { session: record.session }
|
|
280
|
+
: { session: '' }),
|
|
281
|
+
role: record.role,
|
|
282
|
+
runner: record.runner,
|
|
283
|
+
...(typeof record.model === 'string' ? { model: record.model } : {}),
|
|
284
|
+
...(typeof record.phase === 'string' ? { phase: record.phase } : { phase: '' }),
|
|
285
|
+
...(typeof record.round === 'number' ? { round: record.round } : {}),
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
} else {
|
|
289
|
+
events.push({ kind: 'data', name: part.type })
|
|
290
|
+
}
|
|
291
|
+
break
|
|
292
|
+
}
|
|
293
|
+
case 'data-ab-prompt': {
|
|
294
|
+
flushOpenRuns()
|
|
295
|
+
const data = part.data
|
|
296
|
+
const text =
|
|
297
|
+
typeof data === 'object' && data !== null
|
|
298
|
+
? (data as Record<string, unknown>).text
|
|
299
|
+
: undefined
|
|
300
|
+
events.push({ kind: 'prompt', text: typeof text === 'string' ? text : '' })
|
|
301
|
+
break
|
|
302
|
+
}
|
|
303
|
+
case 'data-ab-truncation': {
|
|
304
|
+
flushOpenRuns()
|
|
305
|
+
const data = part.data
|
|
306
|
+
const bytes =
|
|
307
|
+
typeof data === 'object' && data !== null
|
|
308
|
+
? (data as Record<string, unknown>).omittedBytes
|
|
309
|
+
: undefined
|
|
310
|
+
events.push({ kind: 'truncation', bytes: typeof bytes === 'number' ? bytes : 0 })
|
|
311
|
+
break
|
|
312
|
+
}
|
|
313
|
+
case 'text-start':
|
|
314
|
+
openText = open(openText, part.id, 'text')
|
|
315
|
+
break
|
|
316
|
+
case 'text-delta': {
|
|
317
|
+
openText = open(openText, part.id, 'text')
|
|
318
|
+
if (openText !== undefined && typeof part.delta === 'string') openText.buffer += part.delta
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
case 'text-end':
|
|
322
|
+
flush(openText, 'text')
|
|
323
|
+
openText = undefined
|
|
324
|
+
break
|
|
325
|
+
case 'reasoning-start':
|
|
326
|
+
openReasoning = open(openReasoning, part.id, 'reasoning')
|
|
327
|
+
break
|
|
328
|
+
case 'reasoning-delta': {
|
|
329
|
+
openReasoning = open(openReasoning, part.id, 'reasoning')
|
|
330
|
+
if (openReasoning !== undefined && typeof part.delta === 'string') {
|
|
331
|
+
openReasoning.buffer += part.delta
|
|
332
|
+
}
|
|
333
|
+
break
|
|
334
|
+
}
|
|
335
|
+
case 'reasoning-end':
|
|
336
|
+
flush(openReasoning, 'reasoning')
|
|
337
|
+
openReasoning = undefined
|
|
338
|
+
break
|
|
339
|
+
case 'tool-input-available': {
|
|
340
|
+
flushOpenRuns()
|
|
341
|
+
const callId = typeof part.toolCallId === 'string' ? part.toolCallId : ''
|
|
342
|
+
const resolved = outputs.get(callId) ?? {}
|
|
343
|
+
events.push({
|
|
344
|
+
kind: 'tool',
|
|
345
|
+
toolName: typeof part.toolName === 'string' ? part.toolName : 'unknown tool',
|
|
346
|
+
...(part.input !== undefined ? { input: part.input } : {}),
|
|
347
|
+
...('output' in resolved && resolved.output !== undefined
|
|
348
|
+
? { output: resolved.output }
|
|
349
|
+
: {}),
|
|
350
|
+
awaiting: resolved.output === undefined && resolved.errorText === undefined,
|
|
351
|
+
...('errorText' in resolved && resolved.errorText !== undefined
|
|
352
|
+
? { errorText: resolved.errorText }
|
|
353
|
+
: {}),
|
|
354
|
+
})
|
|
355
|
+
break
|
|
356
|
+
}
|
|
357
|
+
case 'start-step':
|
|
358
|
+
case 'finish-step':
|
|
359
|
+
flushOpenRuns()
|
|
360
|
+
events.push({ kind: 'step' })
|
|
361
|
+
break
|
|
362
|
+
case 'error':
|
|
363
|
+
flushOpenRuns()
|
|
364
|
+
events.push({
|
|
365
|
+
kind: 'error',
|
|
366
|
+
text: typeof part.errorText === 'string' ? part.errorText : String(part.errorText ?? ''),
|
|
367
|
+
})
|
|
368
|
+
break
|
|
369
|
+
case 'abort':
|
|
370
|
+
flushOpenRuns()
|
|
371
|
+
events.push({
|
|
372
|
+
kind: 'abort',
|
|
373
|
+
text: typeof part.reason === 'string' ? part.reason : String(part.reason ?? ''),
|
|
374
|
+
})
|
|
375
|
+
break
|
|
376
|
+
default: {
|
|
377
|
+
flushOpenRuns()
|
|
378
|
+
// Any other `data-*` extension renders legibly; non-data parts of
|
|
379
|
+
// unknown protocol types are ignored.
|
|
380
|
+
if (part.type.startsWith('data-')) events.push({ kind: 'data', name: part.type })
|
|
381
|
+
break
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
flush(openText, 'text')
|
|
386
|
+
flush(openReasoning, 'reasoning')
|
|
387
|
+
return events
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Project a live/chunk part sequence (store order) to display lines. */
|
|
391
|
+
export function projectSessionParts(parts: readonly StreamPart[], width: number): string[] {
|
|
392
|
+
return renderEvents(collectPartEvents(parts), width)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Document adapter (the finalized artifact path) ──────────────────────────
|
|
396
|
+
|
|
397
|
+
function collectDocumentEvents(messages: readonly UIMessage[]): DisplayEvent[] {
|
|
398
|
+
const events: DisplayEvent[] = []
|
|
399
|
+
let headerSeen = false
|
|
400
|
+
for (const message of messages) {
|
|
401
|
+
for (const part of message.parts) {
|
|
402
|
+
const type = part.type
|
|
403
|
+
if (type === 'text') {
|
|
404
|
+
if (typeof (part as { text?: unknown }).text === 'string') {
|
|
405
|
+
events.push({ kind: 'text', text: (part as { text: string }).text })
|
|
406
|
+
}
|
|
407
|
+
continue
|
|
408
|
+
}
|
|
409
|
+
if (type === 'reasoning') {
|
|
410
|
+
if (typeof (part as { text?: unknown }).text === 'string') {
|
|
411
|
+
events.push({ kind: 'reasoning', text: (part as { text: string }).text })
|
|
412
|
+
}
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
if (type === 'step-start') {
|
|
416
|
+
events.push({ kind: 'step' })
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
if (type === 'dynamic-tool') {
|
|
420
|
+
const tool = part as {
|
|
421
|
+
toolName?: unknown
|
|
422
|
+
state?: unknown
|
|
423
|
+
input?: unknown
|
|
424
|
+
output?: unknown
|
|
425
|
+
errorText?: unknown
|
|
426
|
+
}
|
|
427
|
+
events.push({
|
|
428
|
+
kind: 'tool',
|
|
429
|
+
toolName: typeof tool.toolName === 'string' ? tool.toolName : 'unknown tool',
|
|
430
|
+
...(tool.input !== undefined ? { input: tool.input } : {}),
|
|
431
|
+
...(tool.output !== undefined ? { output: tool.output } : {}),
|
|
432
|
+
awaiting: tool.state === 'input-streaming' || tool.state === 'input-available',
|
|
433
|
+
...(typeof tool.errorText === 'string' ? { errorText: tool.errorText } : {}),
|
|
434
|
+
})
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
if (type.startsWith('tool-')) {
|
|
438
|
+
const tool = part as {
|
|
439
|
+
state?: unknown
|
|
440
|
+
input?: unknown
|
|
441
|
+
output?: unknown
|
|
442
|
+
errorText?: unknown
|
|
443
|
+
}
|
|
444
|
+
events.push({
|
|
445
|
+
kind: 'tool',
|
|
446
|
+
toolName: type.slice('tool-'.length),
|
|
447
|
+
...(tool.input !== undefined ? { input: tool.input } : {}),
|
|
448
|
+
...(tool.output !== undefined ? { output: tool.output } : {}),
|
|
449
|
+
awaiting: tool.state === 'input-streaming' || tool.state === 'input-available',
|
|
450
|
+
...(typeof tool.errorText === 'string' ? { errorText: tool.errorText } : {}),
|
|
451
|
+
})
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
454
|
+
if (type.startsWith('data-')) {
|
|
455
|
+
const data = (part as { data?: unknown }).data
|
|
456
|
+
if (type === 'data-ab-prompt') {
|
|
457
|
+
const text =
|
|
458
|
+
typeof data === 'object' && data !== null
|
|
459
|
+
? (data as Record<string, unknown>).text
|
|
460
|
+
: undefined
|
|
461
|
+
events.push({ kind: 'prompt', text: typeof text === 'string' ? text : '' })
|
|
462
|
+
} else if (type === 'data-ab-session') {
|
|
463
|
+
const record =
|
|
464
|
+
typeof data === 'object' && data !== null ? (data as Record<string, unknown>) : {}
|
|
465
|
+
if (!headerSeen && typeof record.role === 'string' && typeof record.runner === 'string') {
|
|
466
|
+
headerSeen = true
|
|
467
|
+
events.push({
|
|
468
|
+
kind: 'header',
|
|
469
|
+
info: {
|
|
470
|
+
...(typeof record.session === 'string'
|
|
471
|
+
? { session: record.session }
|
|
472
|
+
: { session: '' }),
|
|
473
|
+
role: record.role,
|
|
474
|
+
runner: record.runner,
|
|
475
|
+
...(typeof record.model === 'string' ? { model: record.model } : {}),
|
|
476
|
+
...(typeof record.phase === 'string' ? { phase: record.phase } : { phase: '' }),
|
|
477
|
+
...(typeof record.round === 'number' ? { round: record.round } : {}),
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
} else {
|
|
481
|
+
events.push({ kind: 'data', name: type })
|
|
482
|
+
}
|
|
483
|
+
} else if (type === 'data-ab-truncation') {
|
|
484
|
+
const bytes =
|
|
485
|
+
typeof data === 'object' && data !== null
|
|
486
|
+
? (data as Record<string, unknown>).omittedBytes
|
|
487
|
+
: undefined
|
|
488
|
+
events.push({ kind: 'truncation', bytes: typeof bytes === 'number' ? bytes : 0 })
|
|
489
|
+
} else {
|
|
490
|
+
events.push({ kind: 'data', name: type })
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// `file`, `source-url`, `custom`, … have no session-view rendering yet.
|
|
494
|
+
// (`error`/`abort`/`finish-step` chunks never become message parts at
|
|
495
|
+
// all — the SDK's assembly drops them — so those elements are only
|
|
496
|
+
// visible on the parts path above.)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return events
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Project a finalized `UIMessage[]` document (the closed stream's artifact)
|
|
503
|
+
* to the same display lines. */
|
|
504
|
+
export function projectSessionDocument(document: readonly UIMessage[], width: number): string[] {
|
|
505
|
+
return renderEvents(collectDocumentEvents(document), width)
|
|
506
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export interface TranscriptUsage {
|
|
2
|
+
inputTokens: number
|
|
3
|
+
outputTokens: number
|
|
4
|
+
turns?: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TranscriptTurn {
|
|
8
|
+
prompt: string
|
|
9
|
+
text: string
|
|
10
|
+
usage?: TranscriptUsage
|
|
11
|
+
failure?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TranscriptPresentation =
|
|
15
|
+
| { kind: 'turns'; turns: TranscriptTurn[] }
|
|
16
|
+
| { kind: 'producer-boundary'; notice: string; turns: TranscriptTurn[] }
|
|
17
|
+
| { kind: 'raw'; text: string }
|
|
18
|
+
|
|
19
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
20
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
21
|
+
? (value as Record<string, unknown>)
|
|
22
|
+
: undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function usage(value: unknown): TranscriptUsage | undefined {
|
|
26
|
+
const item = record(value)
|
|
27
|
+
if (item === undefined) return undefined
|
|
28
|
+
if (typeof item.inputTokens !== 'number' || typeof item.outputTokens !== 'number')
|
|
29
|
+
return undefined
|
|
30
|
+
return {
|
|
31
|
+
inputTokens: item.inputTokens,
|
|
32
|
+
outputTokens: item.outputTokens,
|
|
33
|
+
...(typeof item.turns === 'number' ? { turns: item.turns } : {}),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function failure(value: unknown): string | undefined {
|
|
38
|
+
const item = record(value)
|
|
39
|
+
return item !== undefined && typeof item.message === 'string' ? item.message : undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseTurn(value: unknown, index: number): TranscriptTurn | undefined {
|
|
43
|
+
const item = record(value)
|
|
44
|
+
if (item === undefined) return undefined
|
|
45
|
+
const result = record(item.result)
|
|
46
|
+
const text = typeof item.text === 'string' ? item.text : result?.text
|
|
47
|
+
if (typeof text !== 'string') return undefined
|
|
48
|
+
const prompt =
|
|
49
|
+
typeof item.prompt === 'string'
|
|
50
|
+
? item.prompt
|
|
51
|
+
: typeof item.message === 'string'
|
|
52
|
+
? item.message
|
|
53
|
+
: index === 0
|
|
54
|
+
? '(initial skill invocation)'
|
|
55
|
+
: `(turn ${index + 1})`
|
|
56
|
+
return {
|
|
57
|
+
prompt,
|
|
58
|
+
text,
|
|
59
|
+
...(usage(item.usage ?? result?.usage) !== undefined
|
|
60
|
+
? { usage: usage(item.usage ?? result?.usage)! }
|
|
61
|
+
: {}),
|
|
62
|
+
...(failure(item.failure ?? result?.failure) !== undefined
|
|
63
|
+
? { failure: failure(item.failure ?? result?.failure)! }
|
|
64
|
+
: {}),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Parse opaque transcript bytes as a presentation heuristic, never a schema. */
|
|
69
|
+
export function parseTranscript(content: string): TranscriptPresentation {
|
|
70
|
+
let parsed: unknown
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(content)
|
|
73
|
+
} catch {
|
|
74
|
+
return { kind: 'raw', text: content }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const root = record(parsed)
|
|
78
|
+
if (root !== undefined && Array.isArray(root.turns)) {
|
|
79
|
+
const turns = root.turns.map(parseTurn)
|
|
80
|
+
if (turns.every((turn): turn is TranscriptTurn => turn !== undefined)) {
|
|
81
|
+
return { kind: 'turns', turns }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
root !== undefined &&
|
|
87
|
+
typeof root.note === 'string' &&
|
|
88
|
+
root.note.includes('producer session')
|
|
89
|
+
) {
|
|
90
|
+
const boundary = record(root.turn)
|
|
91
|
+
if (boundary !== undefined && typeof boundary.text === 'string') {
|
|
92
|
+
return {
|
|
93
|
+
kind: 'producer-boundary',
|
|
94
|
+
notice:
|
|
95
|
+
"Producer boundary record: this session was kept live for continuation; only this round's final turn was deposited.",
|
|
96
|
+
turns: [
|
|
97
|
+
{
|
|
98
|
+
prompt: '(producer round invocation)',
|
|
99
|
+
text: boundary.text,
|
|
100
|
+
...(usage(boundary.usage) !== undefined ? { usage: usage(boundary.usage)! } : {}),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { kind: 'raw', text: JSON.stringify(parsed, null, 2) }
|
|
108
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DashboardRenderer, DashboardRendererResolver } from './dashboard/render'
|
|
2
|
+
|
|
3
|
+
/** State retained on `globalThis` across Bun hot module evaluations. */
|
|
4
|
+
export interface DevCliState {
|
|
5
|
+
renderer?: DashboardRenderer
|
|
6
|
+
run?: Promise<number>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface EvaluateDevCliOpts {
|
|
10
|
+
state: DevCliState
|
|
11
|
+
renderer: DashboardRenderer
|
|
12
|
+
launch: (resolveDashboardRenderer: DashboardRendererResolver) => Promise<number>
|
|
13
|
+
/** Install the process exit/rejection path for a newly launched run. */
|
|
14
|
+
settle: (run: Promise<number>) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Apply one hot evaluation. Every evaluation publishes the newest renderer,
|
|
19
|
+
* but only the first creates and settles a CLI run. The resolver closes over
|
|
20
|
+
* persistent state—not the first renderer—so an existing DispatchLoop sees a
|
|
21
|
+
* replacement on its next paint without reconstructing any controller state.
|
|
22
|
+
*/
|
|
23
|
+
export function evaluateDevCli({
|
|
24
|
+
state,
|
|
25
|
+
renderer,
|
|
26
|
+
launch,
|
|
27
|
+
settle,
|
|
28
|
+
}: EvaluateDevCliOpts): Promise<number> {
|
|
29
|
+
state.renderer = renderer
|
|
30
|
+
if (state.run !== undefined) return state.run
|
|
31
|
+
|
|
32
|
+
const resolveDashboardRenderer = (): DashboardRenderer => {
|
|
33
|
+
const current = state.renderer
|
|
34
|
+
if (current === undefined) {
|
|
35
|
+
throw new Error('dashboard dev renderer is unavailable')
|
|
36
|
+
}
|
|
37
|
+
return current
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Defer launch by one microtask so state.run is installed before user wiring
|
|
41
|
+
// can execute. This keeps even a synchronous/re-entrant launch single-shot.
|
|
42
|
+
const run = Promise.resolve().then(() => launch(resolveDashboardRenderer))
|
|
43
|
+
state.run = run
|
|
44
|
+
settle(run)
|
|
45
|
+
return run
|
|
46
|
+
}
|