@inerrata-corporation/errata 2.0.21-dev.3975 → 2.0.21-dev.3996
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/errata.mjs +342 -128
- package/package.json +1 -1
- package/pass-worker.mjs +297 -91
- package/resources/claude-mods/errata-minimap/.claude-plugin/plugin.json +2 -2
- package/resources/claude-mods/errata-minimap/hooks/bookcase.ts +421 -0
- package/resources/claude-mods/errata-minimap/hooks/chains.ts +416 -0
- package/resources/claude-mods/errata-minimap/hooks/graph.ts +349 -0
- package/resources/claude-mods/errata-minimap/hooks/minimap.ts +39 -351
- package/resources/claude-mods/errata-minimap/hooks/register.tsx +346 -149
- package/resources/claude-mods/errata-minimap/hooks/tags.ts +129 -0
- package/resources/claude-mods/errata-minimap/types/index.d.ts +42 -8
- package/resources/context-plugin/.codex-plugin/plugin.json +1 -1
- package/resources/context-plugin/SHA256SUMS +1 -1
|
@@ -1,44 +1,41 @@
|
|
|
1
1
|
import { atom, read, update } from 'claude-code'
|
|
2
2
|
import type { EngineInterface, Register, Timer } from 'claude-code'
|
|
3
3
|
|
|
4
|
-
import type { Burst, MapGraph, Verdict } from '../types'
|
|
4
|
+
import type { Burst, ChainFacts, MapGraph, Verdict } from '../types'
|
|
5
|
+
import { BINDING_GLYPH, BINDING_NAME, bindingOf, fitTitle, focusBook, PALETTE, rasterizeLibrary } from './bookcase'
|
|
6
|
+
import type { Binding, LibraryFrame, LibraryOptions, Room } from './bookcase'
|
|
7
|
+
import { bookHolding, callNumber, catalogue, enrichTargets, parseNeighbors } from './chains'
|
|
8
|
+
import type { Catalog } from './chains'
|
|
9
|
+
import { colorOf, crossEdges, DARK_BG, glyphOf, layoutRadial, LIGHT_BG, rasterizeGraph, SCOPE_COLOR, SCOPE_ORDER, VERDICT_COLOR } from './graph'
|
|
10
|
+
import type { RasterOptions } from './graph'
|
|
5
11
|
import {
|
|
6
|
-
colorOf,
|
|
7
|
-
DARK_BG,
|
|
8
|
-
FULL,
|
|
9
|
-
LIGHT_BG,
|
|
10
12
|
displayName,
|
|
11
|
-
glyphOf,
|
|
12
|
-
hex,
|
|
13
13
|
JUDGMENT_STYLE,
|
|
14
14
|
layoutBurst,
|
|
15
|
-
parseJudgments,
|
|
16
|
-
resolveJudgmentRef,
|
|
17
|
-
neighborIds,
|
|
18
15
|
parseAgentBurst,
|
|
19
16
|
parseAgentSearch,
|
|
20
17
|
parseFileRecall,
|
|
18
|
+
parseJudgments,
|
|
21
19
|
parsePromptPrime,
|
|
22
|
-
rasterize,
|
|
23
|
-
SCOPE_COLOR,
|
|
24
|
-
SCOPE_ORDER,
|
|
25
|
-
seedNode,
|
|
26
20
|
relPath,
|
|
21
|
+
resolveJudgmentRef,
|
|
22
|
+
seedNode,
|
|
27
23
|
shortName,
|
|
28
24
|
statusLine,
|
|
29
25
|
textsOf,
|
|
30
26
|
} from './minimap'
|
|
31
|
-
import type {
|
|
27
|
+
import type { HandleTable, JudgmentKind } from './minimap'
|
|
28
|
+
import { hasTags, TAG_MODES, tidyTags } from './tags'
|
|
29
|
+
import type { TagMode } from './tags'
|
|
32
30
|
|
|
33
31
|
const PANE = 'errata-minimap'
|
|
34
32
|
const ERRATA = 'errata'
|
|
35
|
-
const ENRICH_MAX = 8
|
|
36
|
-
const TOP_ITEMS = 3
|
|
37
33
|
|
|
38
34
|
const focus = atom({ plugin: 'errata-minimap', key: 'focus' } as const, '')
|
|
39
35
|
const burst = atom({ plugin: 'errata-minimap', key: 'burst' } as const, null as Burst | null)
|
|
40
|
-
const graph = atom({ plugin: 'errata-minimap', key: 'graph' } as const, { title: '', nodes: [],
|
|
36
|
+
const graph = atom({ plugin: 'errata-minimap', key: 'graph' } as const, { title: '', nodes: [], at: 0 } as MapGraph)
|
|
41
37
|
const verdicts = atom({ plugin: 'errata-minimap', key: 'verdicts' } as const, [] as Verdict[])
|
|
38
|
+
const facts = atom({ plugin: 'errata-minimap', key: 'facts' } as const, {} as Record<string, ChainFacts | null>)
|
|
42
39
|
|
|
43
40
|
const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
|
|
44
41
|
const BURST_FILE = /\.errata\/g\/burst\/([^/]+)/
|
|
@@ -46,60 +43,115 @@ const BURST_FILE = /\.errata\/g\/burst\/([^/]+)/
|
|
|
46
43
|
const ORIGIN_LABEL = { agent: 'agent burst', 'daemon-file': 'file recall', 'daemon-prompt': 'prompt prime' } as const
|
|
47
44
|
|
|
48
45
|
// Module-level on purpose: a reload starts by redrawing whatever burst the session holds.
|
|
49
|
-
const pending = { isRefreshing: false, drawnAt: -1 }
|
|
46
|
+
const pending = { isRefreshing: false, drawnAt: -1, cataloguedAt: -1, askedFor: -1, asked: 0 }
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `errata_neighbors` calls per burst: each completes and places one chain (its
|
|
50
|
+
* cause/fix beyond the burst, package, local community, patterns, anchor). Run
|
|
51
|
+
* after the first draw, a few per tick, so the shelves fill in without ever
|
|
52
|
+
* holding the first picture back.
|
|
53
|
+
*/
|
|
54
|
+
const ENRICH_PER_BURST = 10
|
|
55
|
+
const ENRICH_PER_TICK = 4
|
|
56
|
+
/** Facts kept across bursts (the same chains recur); past this the cache keeps only the current burst's. */
|
|
57
|
+
const FACTS_KEPT = 300
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
60
|
* Whether the pane is drawn decides the UI. Drawn — docked beside a fullscreen
|
|
53
|
-
* transcript, or inline above the prompt — it is the full
|
|
54
|
-
* on a narrow terminal, behind another tab or closed by hand,
|
|
55
|
-
* into the status line. The engine does the seating: an unasked pane
|
|
56
|
-
* below 144 columns and takes its place once the terminal widens. The
|
|
57
|
-
* never closed here — closing an inline one dropped that request, and
|
|
58
|
-
* fullscreen every seat is inline, so the map stayed collapsed for the
|
|
61
|
+
* transcript, or inline above the prompt — it is the full library; waiting
|
|
62
|
+
* undrawn on a narrow terminal, behind another tab or closed by hand, it
|
|
63
|
+
* collapses into the status line. The engine does the seating: an unasked pane
|
|
64
|
+
* waits below 144 columns and takes its place once the terminal widens. The
|
|
65
|
+
* pane is never closed here — closing an inline one dropped that request, and
|
|
66
|
+
* outside fullscreen every seat is inline, so the map stayed collapsed for the
|
|
67
|
+
* session.
|
|
59
68
|
*/
|
|
60
69
|
const view = { isShown: false, status: undefined as string | undefined }
|
|
61
70
|
|
|
62
|
-
/**
|
|
71
|
+
/** How the chat draws inline tags; kept in the plugin's store between sessions. */
|
|
72
|
+
const tags = { mode: 'margin' as TagMode }
|
|
73
|
+
const TAG_MODE_KEY = 'tagMode'
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What the pane draws: the library (default) or the original radial graph.
|
|
77
|
+
* Same burst, same enrichment — the graph's cross edges come from the facts
|
|
78
|
+
* the library fetched. Kept in the plugin's store between sessions.
|
|
79
|
+
*/
|
|
80
|
+
type PaneView = 'books' | 'graph'
|
|
81
|
+
const PANE_VIEWS: readonly PaneView[] = ['books', 'graph']
|
|
82
|
+
const pane = { view: 'books' as PaneView }
|
|
83
|
+
const PANE_VIEW_KEY = 'paneView'
|
|
84
|
+
|
|
85
|
+
/** Rows an inline pane asks for: room for a tier of bookcases and the card without burying the transcript. */
|
|
63
86
|
const INLINE_ROWS = 20
|
|
64
87
|
|
|
65
|
-
const GROW_MS =
|
|
66
|
-
const TYPE_MS =
|
|
88
|
+
const GROW_MS = 900
|
|
89
|
+
const TYPE_MS = 500
|
|
67
90
|
const FRAME_MS = 33
|
|
68
91
|
const HOLD_MS = 700
|
|
69
92
|
const PULSE_MS = 180
|
|
70
93
|
|
|
71
94
|
/**
|
|
72
|
-
* The reveal and the highlight run on blits, not render passes: a new
|
|
73
|
-
*
|
|
74
|
-
* bursts off
|
|
75
|
-
* the
|
|
95
|
+
* The reveal and the highlight run on blits, not render passes: a new burst's
|
|
96
|
+
* books rise onto their shelves, then the catalog card inks in; a book the
|
|
97
|
+
* agent bursts off flashes gilt until the fresh results replace the shelves; a
|
|
98
|
+
* verdict turns the card to its book and flashes its stamp. Only while drawn —
|
|
99
|
+
* the status line has nothing to animate.
|
|
76
100
|
*/
|
|
77
101
|
const MARK_MS = 4000
|
|
78
|
-
/** How long a verdict stays
|
|
102
|
+
/** How long a verdict stays stamped on its book's plank, and on the status line. */
|
|
79
103
|
const VERDICT_SHOWN_MS = 10 * 60 * 1000
|
|
80
104
|
const VERDICT_STATUS_MS = 60 * 1000
|
|
81
105
|
const VERDICTS_KEPT = 6
|
|
82
106
|
|
|
107
|
+
const FULL: LibraryFrame = { grow: 1, type: 1 }
|
|
108
|
+
|
|
83
109
|
const motion = {
|
|
84
|
-
/** Priors the agent just judged → their verdict,
|
|
110
|
+
/** Priors the agent just judged → their verdict, flashing until `markUntil`. */
|
|
85
111
|
marks: {} as Record<string, JudgmentKind>,
|
|
86
112
|
markUntil: 0,
|
|
87
113
|
isMarkOn: true,
|
|
88
114
|
graphAt: -1,
|
|
89
115
|
start: 0,
|
|
90
|
-
frame: FULL
|
|
116
|
+
frame: FULL,
|
|
91
117
|
highlight: undefined as string | undefined,
|
|
92
118
|
highlightStart: 0,
|
|
93
119
|
isPulseOn: true,
|
|
94
120
|
raster: undefined as { columns: number; rows: number } | undefined,
|
|
95
|
-
|
|
121
|
+
room: 'stacks' as Room,
|
|
96
122
|
timer: undefined as Timer | undefined,
|
|
123
|
+
/** The node whose book the card reads: the latest one judged on the shelves; unset = the seed's book. */
|
|
124
|
+
focus: undefined as string | undefined,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function catalogOf($: EngineInterface, map: MapGraph): Promise<Catalog> {
|
|
128
|
+
return catalogue(map, await read($, facts))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function stamps($: EngineInterface): Promise<Record<string, JudgmentKind>> {
|
|
132
|
+
const now = await $.clock.now()
|
|
133
|
+
const out: Record<string, JudgmentKind> = {}
|
|
134
|
+
for (const v of (await read($, verdicts)).filter(v => now - v.at < VERDICT_SHOWN_MS).reverse()) out[v.id] = v.kind
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function libraryOptions($: EngineInterface, map: MapGraph): Promise<LibraryOptions> {
|
|
139
|
+
return {
|
|
140
|
+
room: motion.room,
|
|
141
|
+
frame: motion.graphAt === map.at ? motion.frame : FULL,
|
|
142
|
+
focus: motion.focus,
|
|
143
|
+
highlight: motion.highlight,
|
|
144
|
+
isPulseOn: motion.isPulseOn,
|
|
145
|
+
marks: motion.marks,
|
|
146
|
+
isMarkOn: motion.isMarkOn,
|
|
147
|
+
stamps: await stamps($),
|
|
148
|
+
}
|
|
97
149
|
}
|
|
98
150
|
|
|
99
|
-
function
|
|
151
|
+
function graphOptions(map: MapGraph): RasterOptions {
|
|
100
152
|
return {
|
|
101
153
|
frame: motion.graphAt === map.at ? motion.frame : FULL,
|
|
102
|
-
bg: motion.
|
|
154
|
+
bg: motion.room === 'reading' ? LIGHT_BG : DARK_BG,
|
|
103
155
|
highlight: motion.highlight,
|
|
104
156
|
isPulseOn: motion.isPulseOn,
|
|
105
157
|
marks: motion.marks,
|
|
@@ -107,6 +159,18 @@ function rasterOptions(map: MapGraph): RasterOptions {
|
|
|
107
159
|
}
|
|
108
160
|
}
|
|
109
161
|
|
|
162
|
+
/** The pane's cells for the current view. */
|
|
163
|
+
async function paneCells($: EngineInterface, map: MapGraph, columns: number, rows: number): Promise<string> {
|
|
164
|
+
if (pane.view === 'graph') return rasterizeGraph(layoutRadial(map, crossEdges(map, await read($, facts))), columns, rows, graphOptions(map))
|
|
165
|
+
return rasterizeLibrary(await catalogOf($, map), columns, rows, await libraryOptions($, map))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Whether the current view shows this node (a book holding it, or a node on the graph). */
|
|
169
|
+
async function isOnView($: EngineInterface, map: MapGraph, id: string): Promise<boolean> {
|
|
170
|
+
if (pane.view === 'graph') return map.nodes.some(n => !n.isSeed && n.id === id)
|
|
171
|
+
return !!bookHolding(await catalogOf($, map), id)
|
|
172
|
+
}
|
|
173
|
+
|
|
110
174
|
async function animate($: EngineInterface): Promise<void> {
|
|
111
175
|
const now = await $.clock.now()
|
|
112
176
|
const t = now - motion.start
|
|
@@ -119,9 +183,10 @@ async function animate($: EngineInterface): Promise<void> {
|
|
|
119
183
|
if (isMarking) motion.isMarkOn = Math.floor((now - (motion.markUntil - MARK_MS)) / PULSE_MS) % 2 === 0
|
|
120
184
|
else motion.marks = {}
|
|
121
185
|
const map = await read($, graph)
|
|
122
|
-
if (motion.raster && map.nodes.length >
|
|
186
|
+
if (motion.raster && map.nodes.length > 1) {
|
|
123
187
|
const { columns, rows } = motion.raster
|
|
124
|
-
|
|
188
|
+
const cells = await paneCells($, map, columns, rows)
|
|
189
|
+
await $.ui.blit({ requestId: PANE, key: 'map', columns, rows, cells }).catch(() => undefined)
|
|
125
190
|
}
|
|
126
191
|
if (!isRevealing && !motion.highlight && !isMarking) {
|
|
127
192
|
motion.timer?.cancel()
|
|
@@ -133,10 +198,10 @@ function startMotion($: EngineInterface): void {
|
|
|
133
198
|
motion.timer ??= $.clock.every(FRAME_MS, () => void animate($))
|
|
134
199
|
}
|
|
135
200
|
|
|
136
|
-
/**
|
|
201
|
+
/** Flashes the book the agent is about to burst off, if the shelves hold it. */
|
|
137
202
|
async function highlightSeed($: EngineInterface, id: string): Promise<boolean> {
|
|
138
203
|
const map = await read($, graph)
|
|
139
|
-
if (!view.isShown || !
|
|
204
|
+
if (!view.isShown || !(await isOnView($, map, id))) return false
|
|
140
205
|
motion.highlight = id
|
|
141
206
|
motion.highlightStart = await $.clock.now()
|
|
142
207
|
motion.isPulseOn = true
|
|
@@ -144,7 +209,7 @@ async function highlightSeed($: EngineInterface, id: string): Promise<boolean> {
|
|
|
144
209
|
return true
|
|
145
210
|
}
|
|
146
211
|
|
|
147
|
-
/** Holds the highlight at least HOLD_MS, so the target reads before the
|
|
212
|
+
/** Holds the highlight at least HOLD_MS, so the target reads before the shelves move on. */
|
|
148
213
|
async function holdHighlight($: EngineInterface): Promise<void> {
|
|
149
214
|
if (!motion.highlight) return
|
|
150
215
|
const left = HOLD_MS - ((await $.clock.now()) - motion.highlightStart)
|
|
@@ -153,23 +218,33 @@ async function holdHighlight($: EngineInterface): Promise<void> {
|
|
|
153
218
|
|
|
154
219
|
/**
|
|
155
220
|
* The agent's reply judged some priors (cite / confirm / fix / refute tags):
|
|
156
|
-
* record each,
|
|
157
|
-
* that resolve to no node — `[id]` in prose
|
|
221
|
+
* record each, flash the stamp of the ones on the shelves, and turn the card
|
|
222
|
+
* to the first book judged. Refs that resolve to no node — `[id]` in prose
|
|
223
|
+
* ABOUT the protocol — are ignored.
|
|
158
224
|
*/
|
|
159
225
|
async function recordJudgments($: EngineInterface, text: string): Promise<void> {
|
|
160
226
|
const judgments = parseJudgments(text)
|
|
161
227
|
if (judgments.length === 0) return
|
|
162
228
|
const handles = await primingHandles($)
|
|
163
229
|
const map = await read($, graph)
|
|
230
|
+
const catalog = await catalogOf($, map)
|
|
164
231
|
const now = await $.clock.now()
|
|
165
232
|
const fresh: Verdict[] = []
|
|
233
|
+
let judged: string | undefined
|
|
166
234
|
for (const j of judgments) {
|
|
167
235
|
const id = resolveJudgmentRef(j.ref, handles)
|
|
168
236
|
if (!id) continue
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
237
|
+
// A part the graph completed (not in the burst) is on the shelves too: judge it there.
|
|
238
|
+
const book = bookHolding(catalog, id)
|
|
239
|
+
const part = book && [book.cause, book.problem, book.fix].find(p => p?.id === id)
|
|
240
|
+
const node = map.nodes.find(n => n.id === id)
|
|
241
|
+
fresh.push({ id, kind: j.kind, name: part?.name ?? (node ? displayName(node.label, node.name) : j.ref), at: now })
|
|
242
|
+
if (book || (pane.view === 'graph' && (await isOnView($, map, id)))) {
|
|
243
|
+
motion.marks[id] = j.kind
|
|
244
|
+
judged ??= id
|
|
245
|
+
}
|
|
172
246
|
}
|
|
247
|
+
if (judged) motion.focus = judged
|
|
173
248
|
if (fresh.length === 0) return
|
|
174
249
|
await update($, verdicts, list => [...fresh, ...list.filter(v => !fresh.some(f => f.id === v.id))].slice(0, VERDICTS_KEPT))
|
|
175
250
|
if (Object.keys(motion.marks).length > 0 && view.isShown) {
|
|
@@ -179,9 +254,10 @@ async function recordJudgments($: EngineInterface, text: string): Promise<void>
|
|
|
179
254
|
}
|
|
180
255
|
}
|
|
181
256
|
|
|
182
|
-
|
|
257
|
+
/** A light terminal theme reads in the reading room; a dark one in the stacks. */
|
|
258
|
+
async function themeRoom($: EngineInterface): Promise<Room> {
|
|
183
259
|
const row = (await $.config.list().catch(() => [])).find(r => r.key === 'theme')
|
|
184
|
-
return String(row?.value ?? '').includes('light') ?
|
|
260
|
+
return String(row?.value ?? '').includes('light') ? 'reading' : 'stacks'
|
|
185
261
|
}
|
|
186
262
|
|
|
187
263
|
async function tick($: EngineInterface): Promise<void> {
|
|
@@ -207,15 +283,12 @@ async function syncLayout($: EngineInterface): Promise<void> {
|
|
|
207
283
|
}
|
|
208
284
|
}
|
|
209
285
|
|
|
210
|
-
/**
|
|
211
|
-
* Draws a burst the moment it lands and starts its reveal. Enrichment waits:
|
|
212
|
-
* the first cut walked up to 12 neighbor calls before drawing anything, so an
|
|
213
|
-
* MCP burst showed up seconds after the agent had already read it.
|
|
214
|
-
*/
|
|
286
|
+
/** Shelves a burst the moment it lands and starts its reveal; the seed's name and the chains' facts are looked up on later ticks. */
|
|
215
287
|
async function draw($: EngineInterface, b: Burst): Promise<void> {
|
|
216
288
|
const laid = layoutBurst(b)
|
|
217
|
-
// Set the reveal up before the graph lands, so its first render is frame 0, not a flash of the
|
|
289
|
+
// Set the reveal up before the graph lands, so its first render is frame 0, not a flash of the full shelves.
|
|
218
290
|
motion.highlight = undefined
|
|
291
|
+
motion.focus = undefined
|
|
219
292
|
if (view.isShown) {
|
|
220
293
|
motion.graphAt = laid.at
|
|
221
294
|
motion.start = await $.clock.now()
|
|
@@ -226,7 +299,7 @@ async function draw($: EngineInterface, b: Burst): Promise<void> {
|
|
|
226
299
|
pending.drawnAt = b.at
|
|
227
300
|
}
|
|
228
301
|
|
|
229
|
-
/** Catches up a burst not drawn yet,
|
|
302
|
+
/** Catches up a burst not drawn yet, names its seed (a burst names it by id), then catalogues its chains. */
|
|
230
303
|
async function refresh($: EngineInterface): Promise<void> {
|
|
231
304
|
if (pending.isRefreshing) return
|
|
232
305
|
const current = await read($, burst)
|
|
@@ -234,21 +307,16 @@ async function refresh($: EngineInterface): Promise<void> {
|
|
|
234
307
|
pending.isRefreshing = true
|
|
235
308
|
try {
|
|
236
309
|
if (current.at !== pending.drawnAt) await draw($, current)
|
|
237
|
-
if (current.isEnriched)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (named !== current) {
|
|
310
|
+
if (!current.isEnriched) {
|
|
311
|
+
const named = { ...((await resolveSeed($, current)) ?? current), isEnriched: true }
|
|
312
|
+
// A newer burst landed while we asked: it is drawn already; drop this one.
|
|
241
313
|
if ((await read($, burst))?.at !== current.at) return
|
|
242
314
|
await update($, burst, () => named)
|
|
315
|
+
// Same `at`: the header renames in place, the reveal keeps running.
|
|
243
316
|
await update($, graph, () => layoutBurst(named))
|
|
317
|
+
return
|
|
244
318
|
}
|
|
245
|
-
|
|
246
|
-
// A newer burst landed while we walked: it is drawn already; drop this one.
|
|
247
|
-
const latest = await read($, burst)
|
|
248
|
-
if (latest && latest.at !== current.at) return
|
|
249
|
-
await update($, burst, () => enriched)
|
|
250
|
-
// Same `at`, same positions: the cross edges appear in place, the reveal keeps running.
|
|
251
|
-
await update($, graph, () => layoutBurst(enriched))
|
|
319
|
+
await enrich($, current.at)
|
|
252
320
|
} catch (err) {
|
|
253
321
|
await update($, graph, g => ({ ...g, note: `minimap: ${String(err).slice(0, 80)}` }))
|
|
254
322
|
} finally {
|
|
@@ -256,24 +324,35 @@ async function refresh($: EngineInterface): Promise<void> {
|
|
|
256
324
|
}
|
|
257
325
|
}
|
|
258
326
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
)
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Completes and places a few more chains: one `errata_neighbors` call per
|
|
329
|
+
* chain, strongest first, at most ENRICH_PER_BURST for one burst. A lone fix's
|
|
330
|
+
* answer names its problem, which becomes a target of its own next time round
|
|
331
|
+
* (the problem is where the chain's package lives).
|
|
332
|
+
*/
|
|
333
|
+
async function enrich($: EngineInterface, at: number): Promise<void> {
|
|
334
|
+
if (pending.cataloguedAt === at) return
|
|
335
|
+
const map = await read($, graph)
|
|
336
|
+
const known = await read($, facts)
|
|
337
|
+
const catalog = catalogue(map, known)
|
|
338
|
+
if (pending.askedFor !== at) Object.assign(pending, { askedFor: at, asked: 0 })
|
|
339
|
+
const budget = Math.min(ENRICH_PER_TICK, ENRICH_PER_BURST - pending.asked)
|
|
340
|
+
const targets = budget > 0 ? enrichTargets(catalog, known, budget) : []
|
|
341
|
+
if (targets.length === 0) {
|
|
342
|
+
pending.cataloguedAt = at
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
const heads = new Set(catalog.books.map(b => b.problem?.id ?? b.key))
|
|
346
|
+
for (const id of targets) {
|
|
347
|
+
pending.asked++
|
|
348
|
+
const res = await $.mcp.call(ERRATA, 'errata_neighbors', { nodeId: id, limit: 40 }).catch(() => undefined)
|
|
349
|
+
const answer = res && !res.isError ? parseNeighbors(textsOf(res.content)) : undefined
|
|
350
|
+
if ((await read($, burst))?.at !== at) return
|
|
351
|
+
await update($, facts, cache => {
|
|
352
|
+
const next = { ...cache, [id]: answer ?? null }
|
|
353
|
+
return Object.keys(next).length <= FACTS_KEPT ? next : Object.fromEntries(Object.entries(next).filter(([k]) => heads.has(k)))
|
|
354
|
+
})
|
|
275
355
|
}
|
|
276
|
-
return out
|
|
277
356
|
}
|
|
278
357
|
|
|
279
358
|
/** A burst names its seed by id (`file_fb8c…`): ask the graph what that node is. */
|
|
@@ -284,7 +363,7 @@ async function resolveSeed($: EngineInterface, b: Burst | null): Promise<Burst |
|
|
|
284
363
|
return node ? { ...b, seed: { id: b.seed.id, label: node.label, name: node.name } } : b
|
|
285
364
|
}
|
|
286
365
|
|
|
287
|
-
/** Sets the burst the
|
|
366
|
+
/** Sets the burst the shelves show and draws it now; the next tick names its seed. */
|
|
288
367
|
async function show($: EngineInterface, next: Burst | null): Promise<void> {
|
|
289
368
|
if (!next) return
|
|
290
369
|
await update($, burst, () => next)
|
|
@@ -297,18 +376,32 @@ async function primingHandles($: EngineInterface): Promise<HandleTable> {
|
|
|
297
376
|
return JSON.parse(String(text)) as HandleTable
|
|
298
377
|
}
|
|
299
378
|
|
|
379
|
+
function isTagMode(value: unknown): value is TagMode {
|
|
380
|
+
return TAG_MODES.includes(value as TagMode)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function isPaneView(value: unknown): value is PaneView {
|
|
384
|
+
return PANE_VIEWS.includes(value as PaneView)
|
|
385
|
+
}
|
|
386
|
+
|
|
300
387
|
export const register: Register = on => {
|
|
301
388
|
on('session.start', async ($, e, next) => {
|
|
302
|
-
await $.command.register({ name: 'minimap', description: 'Open the errata
|
|
389
|
+
await $.command.register({ name: 'minimap', description: 'Open the errata library: the causal chains of the latest burst, in bookcases by technology' })
|
|
390
|
+
await $.command.register({ name: 'errata-tags', description: 'How the chat draws errata inline tags: margin (marks), quiet (hidden) or raw' })
|
|
391
|
+
await $.command.register({ name: 'errata-view', description: 'Switch the errata pane between the library (books) and the graph' })
|
|
303
392
|
void $.ui.open({ id: PANE, title: 'errata', rows: INLINE_ROWS })
|
|
304
|
-
motion.
|
|
393
|
+
motion.room = await themeRoom($)
|
|
394
|
+
const saved = await $.store.get(TAG_MODE_KEY).catch(() => undefined)
|
|
395
|
+
if (isTagMode(saved)) tags.mode = saved
|
|
396
|
+
const savedView = await $.store.get(PANE_VIEW_KEY).catch(() => undefined)
|
|
397
|
+
if (isPaneView(savedView)) pane.view = savedView
|
|
305
398
|
$.clock.every(1500, () => void tick($))
|
|
306
399
|
return next(e)
|
|
307
400
|
})
|
|
308
401
|
|
|
309
402
|
on('config.set', { key: 'theme' }, async ($, e, next) => {
|
|
310
403
|
const done = await next(e)
|
|
311
|
-
motion.
|
|
404
|
+
motion.room = await themeRoom($)
|
|
312
405
|
return done
|
|
313
406
|
})
|
|
314
407
|
|
|
@@ -316,28 +409,56 @@ export const register: Register = on => {
|
|
|
316
409
|
const opened = await $.ui.open({ id: PANE, title: 'errata', columns: 56, rows: INLINE_ROWS })
|
|
317
410
|
pending.drawnAt = -1
|
|
318
411
|
void refresh($)
|
|
319
|
-
return { text: opened.isPlaced ? 'errata
|
|
412
|
+
return { text: opened.isPlaced ? 'errata library opened.' : `the library waits: ${opened.reason}` }
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// `/errata-tags` cycles margin → quiet → raw; `/errata-tags quiet` sets one. Display only: capture reads the stored reply.
|
|
416
|
+
on('command.run', { command: 'errata-tags' }, async ($, e) => {
|
|
417
|
+
const asked = e.args.trim().toLowerCase()
|
|
418
|
+
const nextMode = isTagMode(asked) ? asked : TAG_MODES[(TAG_MODES.indexOf(tags.mode) + 1) % TAG_MODES.length]!
|
|
419
|
+
tags.mode = nextMode
|
|
420
|
+
await $.store.set(TAG_MODE_KEY, nextMode)
|
|
421
|
+
$.ui.invalidate('ui.render')
|
|
422
|
+
const said = { margin: 'tags drawn as margin marks', quiet: 'tags hidden', raw: 'tags drawn as written' }[nextMode]
|
|
423
|
+
return { text: `errata: ${said} (${nextMode}). Capture is unchanged; it reads the stored reply.` }
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// `/errata-view` flips books ↔ graph; `/errata-view graph` sets one. The burst stays; only the drawing changes.
|
|
427
|
+
on('command.run', { command: 'errata-view' }, async ($, e) => {
|
|
428
|
+
const asked = e.args.trim().toLowerCase()
|
|
429
|
+
const nextView = isPaneView(asked) ? asked : pane.view === 'books' ? 'graph' : 'books'
|
|
430
|
+
pane.view = nextView
|
|
431
|
+
motion.marks = {}
|
|
432
|
+
motion.focus = undefined
|
|
433
|
+
await $.store.set(PANE_VIEW_KEY, nextView)
|
|
434
|
+
$.ui.invalidate('ui.render')
|
|
435
|
+
return { text: nextView === 'books' ? 'errata pane: the library (books).' : 'errata pane: the graph.' }
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// The agent's inline tags, tidied in the chat. The stored reply keeps them: the daemon harvests from the transcript.
|
|
439
|
+
on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
|
|
440
|
+
if (tags.mode === 'raw' || !hasTags(e.props.text)) return next(e)
|
|
441
|
+
return next({ ...e, props: { ...e.props, text: tidyTags(e.props.text, tags.mode) } })
|
|
320
442
|
})
|
|
321
443
|
|
|
322
444
|
// The agent bursts through MCP: the result IS the neighborhood that entered its context.
|
|
323
445
|
on('tool.call', { tool: 'mcp__errata__errata_burst' }, async ($, e, next) => {
|
|
324
446
|
const input = e as unknown as Record<string, unknown>
|
|
325
447
|
const seed = String(input['nodeId'] ?? input['qname'] ?? '')
|
|
326
|
-
// Bursting off a prior the
|
|
448
|
+
// Bursting off a prior the shelves already hold: pull it out while the burst runs.
|
|
327
449
|
await highlightSeed($, seed)
|
|
328
450
|
const ran = await next(e)
|
|
329
451
|
if (ran.deny !== undefined || ran.isError) {
|
|
330
452
|
motion.highlight = undefined
|
|
331
453
|
return ran
|
|
332
454
|
}
|
|
333
|
-
// No MCP call before the draw: the seed's name is looked up on the next tick (refresh).
|
|
334
455
|
const fresh = parseAgentBurst(textsOf([ran.text, ran.result]), seed)
|
|
335
456
|
await holdHighlight($)
|
|
336
457
|
await show($, fresh)
|
|
337
458
|
return ran
|
|
338
459
|
})
|
|
339
460
|
|
|
340
|
-
// A search is a neighborhood too: the hits that entered context,
|
|
461
|
+
// A search is a neighborhood too: the hits that entered context, catalogued under the query.
|
|
341
462
|
on('tool.call', { tool: 'mcp__errata__errata_search' }, async ($, e, next) => {
|
|
342
463
|
const ran = await next(e)
|
|
343
464
|
if (ran.deny !== undefined || ran.isError) return ran
|
|
@@ -400,85 +521,161 @@ export const register: Register = on => {
|
|
|
400
521
|
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
401
522
|
const { Box, Text } = $.ui.resolve(e)
|
|
402
523
|
const width = Math.max(24, e.props.bodyColumns)
|
|
403
|
-
// Docked, the
|
|
524
|
+
// Docked, the library takes the screen's height; inline it sits above the prompt and keeps to its row budget.
|
|
404
525
|
const screenRows = e.viewport?.rows ?? 30
|
|
405
|
-
const height = e.props.placement === 'inline' ? Math.max(
|
|
526
|
+
const height = e.props.placement === 'inline' ? Math.max(12, Math.min(INLINE_ROWS, screenRows - 10)) : Math.max(12, screenRows - 4)
|
|
406
527
|
const map = await read($, graph)
|
|
528
|
+
const hex = (color: number) => `#${color.toString(16).padStart(6, '0')}`
|
|
529
|
+
|
|
530
|
+
if (pane.view === 'graph') {
|
|
531
|
+
const delivered = map.nodes.filter(n => !n.isSeed).sort((a, b) => b.score - a.score)
|
|
532
|
+
const counts = new Map<string, number>()
|
|
533
|
+
for (const n of delivered) counts.set(n.label, (counts.get(n.label) ?? 0) + 1)
|
|
534
|
+
const legend = [...counts].sort((a, b) => b[1] - a[1])
|
|
535
|
+
const legendRows = Math.ceil(legend.reduce((w, [l, c]) => w + l.length + String(c).length + 6, 0) / width)
|
|
536
|
+
const tiers = SCOPE_ORDER.filter(s => delivered.some(n => n.scope === s))
|
|
537
|
+
const now = await $.clock.now()
|
|
538
|
+
const recent = (await read($, verdicts)).filter(v => now - v.at < VERDICT_SHOWN_MS).slice(0, 3)
|
|
539
|
+
const topItems = Math.min(3, delivered.length)
|
|
540
|
+
const mapRows = Math.max(5, height - legendRows - (tiers.length > 0 ? 1 : 0) - topItems - recent.length - 2)
|
|
541
|
+
// Blits must match the mounted size: remember what this pass mounts.
|
|
542
|
+
motion.raster = { columns: width, rows: mapRows }
|
|
543
|
+
const cells = await paneCells($, map, width, mapRows)
|
|
544
|
+
return (
|
|
545
|
+
<Box flexDirection="column" width={width}>
|
|
546
|
+
<Text bold wrap="truncate-end">
|
|
547
|
+
{map.origin ? ORIGIN_LABEL[map.origin] : 'burst'}
|
|
548
|
+
<Text dimColor> · {delivered.length} in context · </Text>
|
|
549
|
+
<Text color="#ffd75f">◉ </Text>
|
|
550
|
+
<Text>{shortName(map.title, Math.max(8, width - 30))}</Text>
|
|
551
|
+
<Text dimColor> {map.seedLabel ?? ''}</Text>
|
|
552
|
+
</Text>
|
|
553
|
+
{delivered.length > 0 && e.surface === 'terminal' ? (
|
|
554
|
+
(() => {
|
|
555
|
+
const { Raster } = $.ui.resolve(e)
|
|
556
|
+
return <Raster key="map" columns={width} rows={mapRows} cells={cells} />
|
|
557
|
+
})()
|
|
558
|
+
) : (
|
|
559
|
+
<Box height={mapRows}>
|
|
560
|
+
<Text dimColor>{map.note ?? (delivered.length === 0 ? 'no burst yet — touch a file or burst a seed' : 'the graph draws in the terminal')}</Text>
|
|
561
|
+
</Box>
|
|
562
|
+
)}
|
|
563
|
+
{tiers.length > 0 && (
|
|
564
|
+
<Text wrap="truncate-end">
|
|
565
|
+
{tiers.map(s => (
|
|
566
|
+
<Text>
|
|
567
|
+
<Text color={hex(SCOPE_COLOR[s])}>━━</Text>
|
|
568
|
+
<Text dimColor> {s} </Text>
|
|
569
|
+
</Text>
|
|
570
|
+
))}
|
|
571
|
+
</Text>
|
|
572
|
+
)}
|
|
573
|
+
{legend.length > 0 && (
|
|
574
|
+
<Text wrap="wrap">
|
|
575
|
+
{legend.map(([label, count]) => (
|
|
576
|
+
<Text>
|
|
577
|
+
<Text color={hex(colorOf(label))}>{String.fromCodePoint(glyphOf(label))}</Text>
|
|
578
|
+
<Text dimColor> {count} {label} </Text>
|
|
579
|
+
</Text>
|
|
580
|
+
))}
|
|
581
|
+
</Text>
|
|
582
|
+
)}
|
|
583
|
+
{recent.map(v => (
|
|
584
|
+
<Text wrap="truncate-end">
|
|
585
|
+
<Text color={hex(VERDICT_COLOR[v.kind])} bold>
|
|
586
|
+
{JUDGMENT_STYLE[v.kind].glyph} {JUDGMENT_STYLE[v.kind].verb}{' '}
|
|
587
|
+
</Text>
|
|
588
|
+
<Text>{v.name}</Text>
|
|
589
|
+
</Text>
|
|
590
|
+
))}
|
|
591
|
+
{delivered.slice(0, topItems).map(n => (
|
|
592
|
+
<Text wrap="truncate-end">
|
|
593
|
+
<Text color={hex(colorOf(n.label))}>{String.fromCodePoint(glyphOf(n.label))} </Text>
|
|
594
|
+
<Text dimColor>{displayName(n.label, n.name)}</Text>
|
|
595
|
+
</Text>
|
|
596
|
+
))}
|
|
597
|
+
</Box>
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const catalog = await catalogOf($, map)
|
|
602
|
+
const palette = PALETTE[motion.room]
|
|
407
603
|
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
const
|
|
604
|
+
const books = catalog.books
|
|
605
|
+
const bound = new Set(books.flatMap(b => b.memberIds))
|
|
606
|
+
const others = map.nodes.filter(n => !n.isSeed && !bound.has(n.id)).length
|
|
607
|
+
// The spines carry no words: a one-line key says what their bands mean.
|
|
608
|
+
const kinds = new Set<Binding>()
|
|
609
|
+
for (const b of books) for (const part of [b.cause, b.problem, b.fix]) if (part) kinds.add(bindingOf(part.label))
|
|
610
|
+
const legend = (['cause', 'problem', 'claim', 'fix'] as Binding[]).filter(b => kinds.has(b))
|
|
414
611
|
const now = await $.clock.now()
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
const mapRows = Math.max(5, height - footRows - 2)
|
|
612
|
+
// A verdict on a prior not on the shelves has no plank to be stamped on: it gets a line.
|
|
613
|
+
const unshelved = (await read($, verdicts)).filter(v => now - v.at < VERDICT_SHOWN_MS && !bookHolding(catalog, v.id)).slice(0, 2)
|
|
614
|
+
const libraryRows = Math.max(8, height - 1 - (legend.length > 0 ? 1 : 0) - unshelved.length)
|
|
419
615
|
// Blits must match the mounted size: remember what this pass mounts.
|
|
420
|
-
motion.raster = { columns: width, rows:
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
616
|
+
motion.raster = { columns: width, rows: libraryRows }
|
|
617
|
+
const options = await libraryOptions($, map)
|
|
618
|
+
const focus = focusBook(catalog, options.focus)
|
|
619
|
+
const empty = others > 0 ? `no causal chains in this burst — ${others} other ${others === 1 ? 'hit' : 'hits'}` : 'the shelves are empty — touch a file or burst a seed'
|
|
620
|
+
|
|
621
|
+
const library =
|
|
622
|
+
books.length === 0 ? (
|
|
623
|
+
<Box height={libraryRows}>
|
|
624
|
+
<Text dimColor>{map.note ?? empty}</Text>
|
|
625
|
+
</Box>
|
|
626
|
+
) : e.surface === 'terminal' ? (
|
|
627
|
+
(() => {
|
|
628
|
+
const { Raster } = $.ui.resolve(e)
|
|
629
|
+
return <Raster key="map" columns={width} rows={libraryRows} cells={rasterizeLibrary(catalog, width, libraryRows, options)} />
|
|
630
|
+
})()
|
|
631
|
+
) : (
|
|
632
|
+
// No cell grid off the terminal: the catalog as a reading list, the book in focus first.
|
|
633
|
+
<Box flexDirection="column" height={libraryRows}>
|
|
634
|
+
{[...(focus ? [focus] : []), ...books.filter(b => b !== focus)].slice(0, libraryRows).map(b => {
|
|
635
|
+
const head = (b.problem ?? b.fix ?? b.cause)!
|
|
636
|
+
return (
|
|
637
|
+
<Text wrap="truncate-end">
|
|
638
|
+
<Text color={hex(palette.binding[bindingOf(head.label)])}>{BINDING_GLYPH[bindingOf(head.label)]} </Text>
|
|
639
|
+
<Text dimColor>{callNumber(catalog, b)} </Text>
|
|
640
|
+
<Text>{fitTitle(head.name, width - 12)}</Text>
|
|
432
641
|
</Text>
|
|
433
|
-
|
|
434
|
-
)
|
|
642
|
+
)
|
|
643
|
+
})}
|
|
644
|
+
</Box>
|
|
645
|
+
)
|
|
435
646
|
|
|
436
647
|
return (
|
|
437
648
|
<Box flexDirection="column" width={width}>
|
|
438
649
|
<Text bold wrap="truncate-end">
|
|
439
|
-
{
|
|
440
|
-
<Text
|
|
441
|
-
<Text
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
{minimap}
|
|
446
|
-
{tiers.length > 0 && (
|
|
447
|
-
<Text wrap="truncate-end">
|
|
448
|
-
{tiers.map(s => (
|
|
449
|
-
<Text>
|
|
450
|
-
<Text color={hex(SCOPE_COLOR[s])}>━━</Text>
|
|
451
|
-
<Text dimColor> {s} </Text>
|
|
452
|
-
</Text>
|
|
453
|
-
))}
|
|
650
|
+
<Text color={hex(palette.gilt)}>◉ </Text>
|
|
651
|
+
<Text>{shortName(map.title || 'errata library', Math.max(8, width - 44))}</Text>
|
|
652
|
+
<Text dimColor>
|
|
653
|
+
{' '}
|
|
654
|
+
{map.seedLabel ?? ''} · {map.origin ? ORIGIN_LABEL[map.origin] : 'burst'} · {books.length} {books.length === 1 ? 'chain' : 'chains'} ·{' '}
|
|
655
|
+
{catalog.cases.length} {catalog.cases.length === 1 ? 'case' : 'cases'}
|
|
454
656
|
</Text>
|
|
455
|
-
|
|
657
|
+
</Text>
|
|
658
|
+
{library}
|
|
456
659
|
{legend.length > 0 && (
|
|
457
|
-
<Text wrap="
|
|
458
|
-
{legend.map(
|
|
660
|
+
<Text wrap="truncate-end">
|
|
661
|
+
{legend.map(b => (
|
|
459
662
|
<Text>
|
|
460
|
-
<Text color={hex(
|
|
461
|
-
<Text dimColor> {
|
|
663
|
+
<Text color={hex(palette.binding[b])}>█</Text>
|
|
664
|
+
<Text dimColor> {BINDING_NAME[b]} </Text>
|
|
462
665
|
</Text>
|
|
463
666
|
))}
|
|
667
|
+
<Text dimColor>· spines read top to bottom · the card reads the marked book</Text>
|
|
464
668
|
</Text>
|
|
465
669
|
)}
|
|
466
|
-
{
|
|
670
|
+
{unshelved.map(v => (
|
|
467
671
|
<Text wrap="truncate-end">
|
|
468
|
-
<Text color={hex(
|
|
672
|
+
<Text color={hex(palette.verdict[v.kind])} bold>
|
|
469
673
|
{JUDGMENT_STYLE[v.kind].glyph} {JUDGMENT_STYLE[v.kind].verb}{' '}
|
|
470
674
|
</Text>
|
|
471
|
-
<Text>{v.name}</Text>
|
|
472
|
-
</Text>
|
|
473
|
-
))}
|
|
474
|
-
{delivered.slice(0, TOP_ITEMS).map(n => (
|
|
475
|
-
<Text wrap="truncate-end">
|
|
476
|
-
<Text color={hex(colorOf(n.label))}>{String.fromCodePoint(glyphOf(n.label))} </Text>
|
|
477
|
-
<Text dimColor>{displayName(n.label, n.name)}</Text>
|
|
675
|
+
<Text>{shortName(v.name, width - 14)}</Text>
|
|
478
676
|
</Text>
|
|
479
677
|
))}
|
|
480
678
|
</Box>
|
|
481
679
|
)
|
|
482
680
|
})
|
|
483
681
|
}
|
|
484
|
-
|