@inerrata-corporation/errata 2.0.21 → 2.0.22-dev.4049
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/consolidate-worker.mjs +502 -187
- package/errata.mjs +7106 -5336
- package/generalize-worker.mjs +17489 -0
- package/install.ps1 +1 -1
- package/package.json +2 -1
- package/pass-worker.mjs +3147 -2640
- package/resources/claude-mods/errata-minimap/.claude-plugin/plugin.json +2 -2
- package/resources/claude-mods/errata-minimap/hooks/bookcase.ts +450 -0
- package/resources/claude-mods/errata-minimap/hooks/chains.ts +427 -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 +440 -148
- 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, librarySpots, 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,67 +43,138 @@ 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
|
|
87
|
+
/** Rows on a portrait terminal: the header, one tier of shelves, a short card. */
|
|
88
|
+
const COMPACT_ROWS = 11
|
|
89
|
+
/** The terminal's own colour in a raster cell (bit 24 alone). */
|
|
90
|
+
const DEFAULT_CELL = 0x01000000
|
|
64
91
|
|
|
65
|
-
const GROW_MS =
|
|
66
|
-
const TYPE_MS =
|
|
92
|
+
const GROW_MS = 900
|
|
93
|
+
const TYPE_MS = 500
|
|
67
94
|
const FRAME_MS = 33
|
|
68
95
|
const HOLD_MS = 700
|
|
69
96
|
const PULSE_MS = 180
|
|
70
97
|
|
|
71
98
|
/**
|
|
72
|
-
* The reveal and the highlight run on blits, not render passes: a new
|
|
73
|
-
*
|
|
74
|
-
* bursts off
|
|
75
|
-
* the
|
|
99
|
+
* The reveal and the highlight run on blits, not render passes: a new burst's
|
|
100
|
+
* books rise onto their shelves, then the catalog card inks in; a book the
|
|
101
|
+
* agent bursts off flashes gilt until the fresh results replace the shelves; a
|
|
102
|
+
* verdict turns the card to its book and flashes its stamp. Only while drawn —
|
|
103
|
+
* the status line has nothing to animate.
|
|
76
104
|
*/
|
|
77
105
|
const MARK_MS = 4000
|
|
78
|
-
/** How long a verdict stays
|
|
106
|
+
/** How long a verdict stays stamped on its book's plank, and on the status line. */
|
|
79
107
|
const VERDICT_SHOWN_MS = 10 * 60 * 1000
|
|
80
108
|
const VERDICT_STATUS_MS = 60 * 1000
|
|
81
109
|
const VERDICTS_KEPT = 6
|
|
82
110
|
|
|
111
|
+
const FULL: LibraryFrame = { grow: 1, type: 1 }
|
|
112
|
+
|
|
83
113
|
const motion = {
|
|
84
|
-
/** Priors the agent just judged → their verdict,
|
|
114
|
+
/** Priors the agent just judged → their verdict, flashing until `markUntil`. */
|
|
85
115
|
marks: {} as Record<string, JudgmentKind>,
|
|
86
116
|
markUntil: 0,
|
|
87
117
|
isMarkOn: true,
|
|
88
118
|
graphAt: -1,
|
|
89
119
|
start: 0,
|
|
90
|
-
frame: FULL
|
|
120
|
+
frame: FULL,
|
|
91
121
|
highlight: undefined as string | undefined,
|
|
92
122
|
highlightStart: 0,
|
|
93
123
|
isPulseOn: true,
|
|
94
124
|
raster: undefined as { columns: number; rows: number } | undefined,
|
|
95
|
-
|
|
125
|
+
room: 'stacks' as Room,
|
|
96
126
|
timer: undefined as Timer | undefined,
|
|
127
|
+
/** The node whose book the card reads: the latest one judged on the shelves; unset = the seed's book. */
|
|
128
|
+
focus: undefined as string | undefined,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function catalogOf($: EngineInterface, map: MapGraph): Promise<Catalog> {
|
|
132
|
+
return catalogue(map, await read($, facts))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function stamps($: EngineInterface): Promise<Record<string, JudgmentKind>> {
|
|
136
|
+
const now = await $.clock.now()
|
|
137
|
+
const out: Record<string, JudgmentKind> = {}
|
|
138
|
+
for (const v of (await read($, verdicts)).filter(v => now - v.at < VERDICT_SHOWN_MS).reverse()) out[v.id] = v.kind
|
|
139
|
+
return out
|
|
97
140
|
}
|
|
98
141
|
|
|
99
|
-
function
|
|
142
|
+
async function libraryOptions($: EngineInterface, map: MapGraph): Promise<LibraryOptions> {
|
|
100
143
|
return {
|
|
144
|
+
room: motion.room,
|
|
101
145
|
frame: motion.graphAt === map.at ? motion.frame : FULL,
|
|
102
|
-
|
|
146
|
+
focus: motion.focus,
|
|
103
147
|
highlight: motion.highlight,
|
|
104
148
|
isPulseOn: motion.isPulseOn,
|
|
105
149
|
marks: motion.marks,
|
|
106
150
|
isMarkOn: motion.isMarkOn,
|
|
151
|
+
stamps: await stamps($),
|
|
107
152
|
}
|
|
108
153
|
}
|
|
109
154
|
|
|
155
|
+
function graphOptions(map: MapGraph): RasterOptions {
|
|
156
|
+
return {
|
|
157
|
+
frame: motion.graphAt === map.at ? motion.frame : FULL,
|
|
158
|
+
bg: motion.room === 'reading' ? LIGHT_BG : DARK_BG,
|
|
159
|
+
highlight: motion.highlight,
|
|
160
|
+
isPulseOn: motion.isPulseOn,
|
|
161
|
+
marks: motion.marks,
|
|
162
|
+
isMarkOn: motion.isMarkOn,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The pane's cells for the current view. */
|
|
167
|
+
async function paneCells($: EngineInterface, map: MapGraph, columns: number, rows: number): Promise<string> {
|
|
168
|
+
if (pane.view === 'graph') return rasterizeGraph(layoutRadial(map, crossEdges(map, await read($, facts))), columns, rows, graphOptions(map))
|
|
169
|
+
return rasterizeLibrary(await catalogOf($, map), columns, rows, await libraryOptions($, map))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Whether the current view shows this node (a book holding it, or a node on the graph). */
|
|
173
|
+
async function isOnView($: EngineInterface, map: MapGraph, id: string): Promise<boolean> {
|
|
174
|
+
if (pane.view === 'graph') return map.nodes.some(n => !n.isSeed && n.id === id)
|
|
175
|
+
return !!bookHolding(await catalogOf($, map), id)
|
|
176
|
+
}
|
|
177
|
+
|
|
110
178
|
async function animate($: EngineInterface): Promise<void> {
|
|
111
179
|
const now = await $.clock.now()
|
|
112
180
|
const t = now - motion.start
|
|
@@ -119,13 +187,16 @@ async function animate($: EngineInterface): Promise<void> {
|
|
|
119
187
|
if (isMarking) motion.isMarkOn = Math.floor((now - (motion.markUntil - MARK_MS)) / PULSE_MS) % 2 === 0
|
|
120
188
|
else motion.marks = {}
|
|
121
189
|
const map = await read($, graph)
|
|
122
|
-
if (motion.raster && map.nodes.length >
|
|
190
|
+
if (motion.raster && map.nodes.length > 1) {
|
|
123
191
|
const { columns, rows } = motion.raster
|
|
124
|
-
|
|
192
|
+
const cells = await paneCells($, map, columns, rows)
|
|
193
|
+
await $.ui.blit({ requestId: PANE, key: 'map', columns, rows, cells }).catch(() => undefined)
|
|
125
194
|
}
|
|
126
195
|
if (!isRevealing && !motion.highlight && !isMarking) {
|
|
127
196
|
motion.timer?.cancel()
|
|
128
197
|
motion.timer = undefined
|
|
198
|
+
// At rest again: redraw once so the books' hover and click targets are laid (they stay off while blits run).
|
|
199
|
+
if (pane.view === 'books') $.ui.invalidate('ui.render')
|
|
129
200
|
}
|
|
130
201
|
}
|
|
131
202
|
|
|
@@ -133,10 +204,10 @@ function startMotion($: EngineInterface): void {
|
|
|
133
204
|
motion.timer ??= $.clock.every(FRAME_MS, () => void animate($))
|
|
134
205
|
}
|
|
135
206
|
|
|
136
|
-
/**
|
|
207
|
+
/** Flashes the book the agent is about to burst off, if the shelves hold it. */
|
|
137
208
|
async function highlightSeed($: EngineInterface, id: string): Promise<boolean> {
|
|
138
209
|
const map = await read($, graph)
|
|
139
|
-
if (!view.isShown || !
|
|
210
|
+
if (!view.isShown || !(await isOnView($, map, id))) return false
|
|
140
211
|
motion.highlight = id
|
|
141
212
|
motion.highlightStart = await $.clock.now()
|
|
142
213
|
motion.isPulseOn = true
|
|
@@ -144,7 +215,7 @@ async function highlightSeed($: EngineInterface, id: string): Promise<boolean> {
|
|
|
144
215
|
return true
|
|
145
216
|
}
|
|
146
217
|
|
|
147
|
-
/** Holds the highlight at least HOLD_MS, so the target reads before the
|
|
218
|
+
/** Holds the highlight at least HOLD_MS, so the target reads before the shelves move on. */
|
|
148
219
|
async function holdHighlight($: EngineInterface): Promise<void> {
|
|
149
220
|
if (!motion.highlight) return
|
|
150
221
|
const left = HOLD_MS - ((await $.clock.now()) - motion.highlightStart)
|
|
@@ -153,23 +224,33 @@ async function holdHighlight($: EngineInterface): Promise<void> {
|
|
|
153
224
|
|
|
154
225
|
/**
|
|
155
226
|
* The agent's reply judged some priors (cite / confirm / fix / refute tags):
|
|
156
|
-
* record each,
|
|
157
|
-
* that resolve to no node — `[id]` in prose
|
|
227
|
+
* record each, flash the stamp of the ones on the shelves, and turn the card
|
|
228
|
+
* to the first book judged. Refs that resolve to no node — `[id]` in prose
|
|
229
|
+
* ABOUT the protocol — are ignored.
|
|
158
230
|
*/
|
|
159
231
|
async function recordJudgments($: EngineInterface, text: string): Promise<void> {
|
|
160
232
|
const judgments = parseJudgments(text)
|
|
161
233
|
if (judgments.length === 0) return
|
|
162
234
|
const handles = await primingHandles($)
|
|
163
235
|
const map = await read($, graph)
|
|
236
|
+
const catalog = await catalogOf($, map)
|
|
164
237
|
const now = await $.clock.now()
|
|
165
238
|
const fresh: Verdict[] = []
|
|
239
|
+
let judged: string | undefined
|
|
166
240
|
for (const j of judgments) {
|
|
167
241
|
const id = resolveJudgmentRef(j.ref, handles)
|
|
168
242
|
if (!id) continue
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
243
|
+
// A part the graph completed (not in the burst) is on the shelves too: judge it there.
|
|
244
|
+
const book = bookHolding(catalog, id)
|
|
245
|
+
const part = book && [book.cause, book.problem, book.fix].find(p => p?.id === id)
|
|
246
|
+
const node = map.nodes.find(n => n.id === id)
|
|
247
|
+
fresh.push({ id, kind: j.kind, name: part?.name ?? (node ? displayName(node.label, node.name) : j.ref), at: now })
|
|
248
|
+
if (book || (pane.view === 'graph' && (await isOnView($, map, id)))) {
|
|
249
|
+
motion.marks[id] = j.kind
|
|
250
|
+
judged ??= id
|
|
251
|
+
}
|
|
172
252
|
}
|
|
253
|
+
if (judged) motion.focus = judged
|
|
173
254
|
if (fresh.length === 0) return
|
|
174
255
|
await update($, verdicts, list => [...fresh, ...list.filter(v => !fresh.some(f => f.id === v.id))].slice(0, VERDICTS_KEPT))
|
|
175
256
|
if (Object.keys(motion.marks).length > 0 && view.isShown) {
|
|
@@ -179,9 +260,10 @@ async function recordJudgments($: EngineInterface, text: string): Promise<void>
|
|
|
179
260
|
}
|
|
180
261
|
}
|
|
181
262
|
|
|
182
|
-
|
|
263
|
+
/** A light terminal theme reads in the reading room; a dark one in the stacks. */
|
|
264
|
+
async function themeRoom($: EngineInterface): Promise<Room> {
|
|
183
265
|
const row = (await $.config.list().catch(() => [])).find(r => r.key === 'theme')
|
|
184
|
-
return String(row?.value ?? '').includes('light') ?
|
|
266
|
+
return String(row?.value ?? '').includes('light') ? 'reading' : 'stacks'
|
|
185
267
|
}
|
|
186
268
|
|
|
187
269
|
async function tick($: EngineInterface): Promise<void> {
|
|
@@ -207,15 +289,12 @@ async function syncLayout($: EngineInterface): Promise<void> {
|
|
|
207
289
|
}
|
|
208
290
|
}
|
|
209
291
|
|
|
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
|
-
*/
|
|
292
|
+
/** 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
293
|
async function draw($: EngineInterface, b: Burst): Promise<void> {
|
|
216
294
|
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
|
|
295
|
+
// Set the reveal up before the graph lands, so its first render is frame 0, not a flash of the full shelves.
|
|
218
296
|
motion.highlight = undefined
|
|
297
|
+
motion.focus = undefined
|
|
219
298
|
if (view.isShown) {
|
|
220
299
|
motion.graphAt = laid.at
|
|
221
300
|
motion.start = await $.clock.now()
|
|
@@ -226,7 +305,7 @@ async function draw($: EngineInterface, b: Burst): Promise<void> {
|
|
|
226
305
|
pending.drawnAt = b.at
|
|
227
306
|
}
|
|
228
307
|
|
|
229
|
-
/** Catches up a burst not drawn yet,
|
|
308
|
+
/** Catches up a burst not drawn yet, names its seed (a burst names it by id), then catalogues its chains. */
|
|
230
309
|
async function refresh($: EngineInterface): Promise<void> {
|
|
231
310
|
if (pending.isRefreshing) return
|
|
232
311
|
const current = await read($, burst)
|
|
@@ -234,21 +313,16 @@ async function refresh($: EngineInterface): Promise<void> {
|
|
|
234
313
|
pending.isRefreshing = true
|
|
235
314
|
try {
|
|
236
315
|
if (current.at !== pending.drawnAt) await draw($, current)
|
|
237
|
-
if (current.isEnriched)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (named !== current) {
|
|
316
|
+
if (!current.isEnriched) {
|
|
317
|
+
const named = { ...((await resolveSeed($, current)) ?? current), isEnriched: true }
|
|
318
|
+
// A newer burst landed while we asked: it is drawn already; drop this one.
|
|
241
319
|
if ((await read($, burst))?.at !== current.at) return
|
|
242
320
|
await update($, burst, () => named)
|
|
321
|
+
// Same `at`: the header renames in place, the reveal keeps running.
|
|
243
322
|
await update($, graph, () => layoutBurst(named))
|
|
323
|
+
return
|
|
244
324
|
}
|
|
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))
|
|
325
|
+
await enrich($, current.at)
|
|
252
326
|
} catch (err) {
|
|
253
327
|
await update($, graph, g => ({ ...g, note: `minimap: ${String(err).slice(0, 80)}` }))
|
|
254
328
|
} finally {
|
|
@@ -256,24 +330,35 @@ async function refresh($: EngineInterface): Promise<void> {
|
|
|
256
330
|
}
|
|
257
331
|
}
|
|
258
332
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
)
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Completes and places a few more chains: one `errata_neighbors` call per
|
|
335
|
+
* chain, strongest first, at most ENRICH_PER_BURST for one burst. A lone fix's
|
|
336
|
+
* answer names its problem, which becomes a target of its own next time round
|
|
337
|
+
* (the problem is where the chain's package lives).
|
|
338
|
+
*/
|
|
339
|
+
async function enrich($: EngineInterface, at: number): Promise<void> {
|
|
340
|
+
if (pending.cataloguedAt === at) return
|
|
341
|
+
const map = await read($, graph)
|
|
342
|
+
const known = await read($, facts)
|
|
343
|
+
const catalog = catalogue(map, known)
|
|
344
|
+
if (pending.askedFor !== at) Object.assign(pending, { askedFor: at, asked: 0 })
|
|
345
|
+
const budget = Math.min(ENRICH_PER_TICK, ENRICH_PER_BURST - pending.asked)
|
|
346
|
+
const targets = budget > 0 ? enrichTargets(catalog, known, budget) : []
|
|
347
|
+
if (targets.length === 0) {
|
|
348
|
+
pending.cataloguedAt = at
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
const heads = new Set(catalog.books.map(b => b.problem?.id ?? b.key))
|
|
352
|
+
for (const id of targets) {
|
|
353
|
+
pending.asked++
|
|
354
|
+
const res = await $.mcp.call(ERRATA, 'errata_neighbors', { nodeId: id, limit: 40 }).catch(() => undefined)
|
|
355
|
+
const answer = res && !res.isError ? parseNeighbors(textsOf(res.content)) : undefined
|
|
356
|
+
if ((await read($, burst))?.at !== at) return
|
|
357
|
+
await update($, facts, cache => {
|
|
358
|
+
const next = { ...cache, [id]: answer ?? null }
|
|
359
|
+
return Object.keys(next).length <= FACTS_KEPT ? next : Object.fromEntries(Object.entries(next).filter(([k]) => heads.has(k)))
|
|
360
|
+
})
|
|
275
361
|
}
|
|
276
|
-
return out
|
|
277
362
|
}
|
|
278
363
|
|
|
279
364
|
/** A burst names its seed by id (`file_fb8c…`): ask the graph what that node is. */
|
|
@@ -284,7 +369,7 @@ async function resolveSeed($: EngineInterface, b: Burst | null): Promise<Burst |
|
|
|
284
369
|
return node ? { ...b, seed: { id: b.seed.id, label: node.label, name: node.name } } : b
|
|
285
370
|
}
|
|
286
371
|
|
|
287
|
-
/** Sets the burst the
|
|
372
|
+
/** Sets the burst the shelves show and draws it now; the next tick names its seed. */
|
|
288
373
|
async function show($: EngineInterface, next: Burst | null): Promise<void> {
|
|
289
374
|
if (!next) return
|
|
290
375
|
await update($, burst, () => next)
|
|
@@ -297,18 +382,37 @@ async function primingHandles($: EngineInterface): Promise<HandleTable> {
|
|
|
297
382
|
return JSON.parse(String(text)) as HandleTable
|
|
298
383
|
}
|
|
299
384
|
|
|
385
|
+
function isTagMode(value: unknown): value is TagMode {
|
|
386
|
+
return TAG_MODES.includes(value as TagMode)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** A book's hover group: its spine rows and its card light together. */
|
|
390
|
+
function scopeOf(book: { key: string }): string {
|
|
391
|
+
return `book:${book.key}`.slice(0, 64)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isPaneView(value: unknown): value is PaneView {
|
|
395
|
+
return PANE_VIEWS.includes(value as PaneView)
|
|
396
|
+
}
|
|
397
|
+
|
|
300
398
|
export const register: Register = on => {
|
|
301
399
|
on('session.start', async ($, e, next) => {
|
|
302
|
-
await $.command.register({ name: 'minimap', description: 'Open the errata
|
|
400
|
+
await $.command.register({ name: 'minimap', description: 'Open the errata library: the causal chains of the latest burst, in bookcases by technology' })
|
|
401
|
+
await $.command.register({ name: 'errata-tags', description: 'How the chat draws errata inline tags: margin (marks), quiet (hidden) or raw' })
|
|
402
|
+
await $.command.register({ name: 'errata-view', description: 'Switch the errata pane between the library (books) and the graph' })
|
|
303
403
|
void $.ui.open({ id: PANE, title: 'errata', rows: INLINE_ROWS })
|
|
304
|
-
motion.
|
|
404
|
+
motion.room = await themeRoom($)
|
|
405
|
+
const saved = await $.store.get(TAG_MODE_KEY).catch(() => undefined)
|
|
406
|
+
if (isTagMode(saved)) tags.mode = saved
|
|
407
|
+
const savedView = await $.store.get(PANE_VIEW_KEY).catch(() => undefined)
|
|
408
|
+
if (isPaneView(savedView)) pane.view = savedView
|
|
305
409
|
$.clock.every(1500, () => void tick($))
|
|
306
410
|
return next(e)
|
|
307
411
|
})
|
|
308
412
|
|
|
309
413
|
on('config.set', { key: 'theme' }, async ($, e, next) => {
|
|
310
414
|
const done = await next(e)
|
|
311
|
-
motion.
|
|
415
|
+
motion.room = await themeRoom($)
|
|
312
416
|
return done
|
|
313
417
|
})
|
|
314
418
|
|
|
@@ -316,28 +420,56 @@ export const register: Register = on => {
|
|
|
316
420
|
const opened = await $.ui.open({ id: PANE, title: 'errata', columns: 56, rows: INLINE_ROWS })
|
|
317
421
|
pending.drawnAt = -1
|
|
318
422
|
void refresh($)
|
|
319
|
-
return { text: opened.isPlaced ? 'errata
|
|
423
|
+
return { text: opened.isPlaced ? 'errata library opened.' : `the library waits: ${opened.reason}` }
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// `/errata-tags` cycles margin → quiet → raw; `/errata-tags quiet` sets one. Display only: capture reads the stored reply.
|
|
427
|
+
on('command.run', { command: 'errata-tags' }, async ($, e) => {
|
|
428
|
+
const asked = e.args.trim().toLowerCase()
|
|
429
|
+
const nextMode = isTagMode(asked) ? asked : TAG_MODES[(TAG_MODES.indexOf(tags.mode) + 1) % TAG_MODES.length]!
|
|
430
|
+
tags.mode = nextMode
|
|
431
|
+
await $.store.set(TAG_MODE_KEY, nextMode)
|
|
432
|
+
$.ui.invalidate('ui.render')
|
|
433
|
+
const said = { margin: 'tags drawn as margin marks', quiet: 'tags hidden', raw: 'tags drawn as written' }[nextMode]
|
|
434
|
+
return { text: `errata: ${said} (${nextMode}). Capture is unchanged; it reads the stored reply.` }
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
// `/errata-view` flips books ↔ graph; `/errata-view graph` sets one. The burst stays; only the drawing changes.
|
|
438
|
+
on('command.run', { command: 'errata-view' }, async ($, e) => {
|
|
439
|
+
const asked = e.args.trim().toLowerCase()
|
|
440
|
+
const nextView = isPaneView(asked) ? asked : pane.view === 'books' ? 'graph' : 'books'
|
|
441
|
+
pane.view = nextView
|
|
442
|
+
motion.marks = {}
|
|
443
|
+
motion.focus = undefined
|
|
444
|
+
await $.store.set(PANE_VIEW_KEY, nextView)
|
|
445
|
+
$.ui.invalidate('ui.render')
|
|
446
|
+
return { text: nextView === 'books' ? 'errata pane: the library (books).' : 'errata pane: the graph.' }
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// The agent's inline tags, tidied in the chat. The stored reply keeps them: the daemon harvests from the transcript.
|
|
450
|
+
on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
|
|
451
|
+
if (tags.mode === 'raw' || !hasTags(e.props.text)) return next(e)
|
|
452
|
+
return next({ ...e, props: { ...e.props, text: tidyTags(e.props.text, tags.mode) } })
|
|
320
453
|
})
|
|
321
454
|
|
|
322
455
|
// The agent bursts through MCP: the result IS the neighborhood that entered its context.
|
|
323
456
|
on('tool.call', { tool: 'mcp__errata__errata_burst' }, async ($, e, next) => {
|
|
324
457
|
const input = e as unknown as Record<string, unknown>
|
|
325
458
|
const seed = String(input['nodeId'] ?? input['qname'] ?? '')
|
|
326
|
-
// Bursting off a prior the
|
|
459
|
+
// Bursting off a prior the shelves already hold: pull it out while the burst runs.
|
|
327
460
|
await highlightSeed($, seed)
|
|
328
461
|
const ran = await next(e)
|
|
329
462
|
if (ran.deny !== undefined || ran.isError) {
|
|
330
463
|
motion.highlight = undefined
|
|
331
464
|
return ran
|
|
332
465
|
}
|
|
333
|
-
// No MCP call before the draw: the seed's name is looked up on the next tick (refresh).
|
|
334
466
|
const fresh = parseAgentBurst(textsOf([ran.text, ran.result]), seed)
|
|
335
467
|
await holdHighlight($)
|
|
336
468
|
await show($, fresh)
|
|
337
469
|
return ran
|
|
338
470
|
})
|
|
339
471
|
|
|
340
|
-
// A search is a neighborhood too: the hits that entered context,
|
|
472
|
+
// A search is a neighborhood too: the hits that entered context, catalogued under the query.
|
|
341
473
|
on('tool.call', { tool: 'mcp__errata__errata_search' }, async ($, e, next) => {
|
|
342
474
|
const ran = await next(e)
|
|
343
475
|
if (ran.deny !== undefined || ran.isError) return ran
|
|
@@ -400,85 +532,245 @@ export const register: Register = on => {
|
|
|
400
532
|
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
401
533
|
const { Box, Text } = $.ui.resolve(e)
|
|
402
534
|
const width = Math.max(24, e.props.bodyColumns)
|
|
403
|
-
// Docked, the
|
|
535
|
+
// Docked, the library takes the screen's height; inline it sits above the prompt and keeps to its row budget.
|
|
536
|
+
// A portrait terminal (taller than wide, at ~2 rows per column of width) can't dock and has the least
|
|
537
|
+
// transcript to spare, so inline there the library is compact: one tier of shelves and a short card.
|
|
404
538
|
const screenRows = e.viewport?.rows ?? 30
|
|
405
|
-
const
|
|
539
|
+
const isPortrait = e.props.placement === 'inline' && !!e.viewport && e.viewport.rows * 2 > e.viewport.columns
|
|
540
|
+
const height = isPortrait
|
|
541
|
+
? Math.max(10, Math.min(COMPACT_ROWS, screenRows - 6))
|
|
542
|
+
: e.props.placement === 'inline'
|
|
543
|
+
? Math.max(12, Math.min(INLINE_ROWS, screenRows - 10))
|
|
544
|
+
: Math.max(12, screenRows - 4)
|
|
406
545
|
const map = await read($, graph)
|
|
546
|
+
const hex = (color: number) => `#${color.toString(16).padStart(6, '0')}`
|
|
547
|
+
|
|
548
|
+
if (pane.view === 'graph') {
|
|
549
|
+
const delivered = map.nodes.filter(n => !n.isSeed).sort((a, b) => b.score - a.score)
|
|
550
|
+
const counts = new Map<string, number>()
|
|
551
|
+
for (const n of delivered) counts.set(n.label, (counts.get(n.label) ?? 0) + 1)
|
|
552
|
+
const legend = [...counts].sort((a, b) => b[1] - a[1])
|
|
553
|
+
const legendRows = Math.ceil(legend.reduce((w, [l, c]) => w + l.length + String(c).length + 6, 0) / width)
|
|
554
|
+
const tiers = SCOPE_ORDER.filter(s => delivered.some(n => n.scope === s))
|
|
555
|
+
const now = await $.clock.now()
|
|
556
|
+
const recent = (await read($, verdicts)).filter(v => now - v.at < VERDICT_SHOWN_MS).slice(0, 3)
|
|
557
|
+
const topItems = Math.min(3, delivered.length)
|
|
558
|
+
const mapRows = Math.max(5, height - legendRows - (tiers.length > 0 ? 1 : 0) - topItems - recent.length - 2)
|
|
559
|
+
// Blits must match the mounted size: remember what this pass mounts.
|
|
560
|
+
motion.raster = { columns: width, rows: mapRows }
|
|
561
|
+
const cells = await paneCells($, map, width, mapRows)
|
|
562
|
+
return (
|
|
563
|
+
<Box flexDirection="column" width={width}>
|
|
564
|
+
<Text bold wrap="truncate-end">
|
|
565
|
+
{map.origin ? ORIGIN_LABEL[map.origin] : 'burst'}
|
|
566
|
+
<Text dimColor> · {delivered.length} in context · </Text>
|
|
567
|
+
<Text color="#ffd75f">◉ </Text>
|
|
568
|
+
<Text>{shortName(map.title, Math.max(8, width - 30))}</Text>
|
|
569
|
+
<Text dimColor> {map.seedLabel ?? ''}</Text>
|
|
570
|
+
</Text>
|
|
571
|
+
{delivered.length > 0 && e.surface === 'terminal' ? (
|
|
572
|
+
(() => {
|
|
573
|
+
const { Raster } = $.ui.resolve(e)
|
|
574
|
+
return <Raster key="map" columns={width} rows={mapRows} cells={cells} />
|
|
575
|
+
})()
|
|
576
|
+
) : (
|
|
577
|
+
<Box height={mapRows}>
|
|
578
|
+
<Text dimColor>{map.note ?? (delivered.length === 0 ? 'no burst yet — touch a file or burst a seed' : 'the graph draws in the terminal')}</Text>
|
|
579
|
+
</Box>
|
|
580
|
+
)}
|
|
581
|
+
{tiers.length > 0 && (
|
|
582
|
+
<Text wrap="truncate-end">
|
|
583
|
+
{tiers.map(s => (
|
|
584
|
+
<Text>
|
|
585
|
+
<Text color={hex(SCOPE_COLOR[s])}>━━</Text>
|
|
586
|
+
<Text dimColor> {s} </Text>
|
|
587
|
+
</Text>
|
|
588
|
+
))}
|
|
589
|
+
</Text>
|
|
590
|
+
)}
|
|
591
|
+
{legend.length > 0 && (
|
|
592
|
+
<Text wrap="wrap">
|
|
593
|
+
{legend.map(([label, count]) => (
|
|
594
|
+
<Text>
|
|
595
|
+
<Text color={hex(colorOf(label))}>{String.fromCodePoint(glyphOf(label))}</Text>
|
|
596
|
+
<Text dimColor> {count} {label} </Text>
|
|
597
|
+
</Text>
|
|
598
|
+
))}
|
|
599
|
+
</Text>
|
|
600
|
+
)}
|
|
601
|
+
{recent.map(v => (
|
|
602
|
+
<Text wrap="truncate-end">
|
|
603
|
+
<Text color={hex(VERDICT_COLOR[v.kind])} bold>
|
|
604
|
+
{JUDGMENT_STYLE[v.kind].glyph} {JUDGMENT_STYLE[v.kind].verb}{' '}
|
|
605
|
+
</Text>
|
|
606
|
+
<Text>{v.name}</Text>
|
|
607
|
+
</Text>
|
|
608
|
+
))}
|
|
609
|
+
{delivered.slice(0, topItems).map(n => (
|
|
610
|
+
<Text wrap="truncate-end">
|
|
611
|
+
<Text color={hex(colorOf(n.label))}>{String.fromCodePoint(glyphOf(n.label))} </Text>
|
|
612
|
+
<Text dimColor>{displayName(n.label, n.name)}</Text>
|
|
613
|
+
</Text>
|
|
614
|
+
))}
|
|
615
|
+
</Box>
|
|
616
|
+
)
|
|
617
|
+
}
|
|
407
618
|
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
const
|
|
413
|
-
const
|
|
619
|
+
const catalog = await catalogOf($, map)
|
|
620
|
+
const palette = PALETTE[motion.room]
|
|
621
|
+
|
|
622
|
+
const books = catalog.books
|
|
623
|
+
const bound = new Set(books.flatMap(b => b.memberIds))
|
|
624
|
+
const others = map.nodes.filter(n => !n.isSeed && !bound.has(n.id)).length
|
|
625
|
+
// The spines carry no words: a one-line key says what their bands mean.
|
|
626
|
+
const kinds = new Set<Binding>()
|
|
627
|
+
for (const b of books) for (const part of [b.cause, b.problem, b.fix]) if (part) kinds.add(bindingOf(part.label))
|
|
628
|
+
const legend = (['cause', 'problem', 'claim', 'fix'] as Binding[]).filter(b => kinds.has(b))
|
|
414
629
|
const now = await $.clock.now()
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
const
|
|
630
|
+
// A verdict on a prior not on the shelves has no plank to be stamped on: it gets a line.
|
|
631
|
+
const unshelved = (await read($, verdicts)).filter(v => now - v.at < VERDICT_SHOWN_MS && !bookHolding(catalog, v.id)).slice(0, 2)
|
|
632
|
+
const showLegend = legend.length > 0 && !isPortrait
|
|
633
|
+
const libraryRows = Math.max(8, height - 1 - (showLegend ? 1 : 0) - unshelved.length)
|
|
419
634
|
// 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
|
-
|
|
432
|
-
|
|
635
|
+
motion.raster = { columns: width, rows: libraryRows }
|
|
636
|
+
const options = await libraryOptions($, map)
|
|
637
|
+
const focus = focusBook(catalog, options.focus)
|
|
638
|
+
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'
|
|
639
|
+
|
|
640
|
+
const library =
|
|
641
|
+
books.length === 0 ? (
|
|
642
|
+
<Box height={libraryRows}>
|
|
643
|
+
<Text dimColor>{map.note ?? empty}</Text>
|
|
644
|
+
</Box>
|
|
645
|
+
) : e.surface === 'terminal' ? (
|
|
646
|
+
(() => {
|
|
647
|
+
const { Raster, Button } = $.ui.resolve(e)
|
|
648
|
+
const cells = rasterizeLibrary(catalog, width, libraryRows, options)
|
|
649
|
+
// Hover and click ride on positioned replicas of each spine (same glyphs, same colours) laid
|
|
650
|
+
// over the raster, since pointer feedback attaches to elements, not cells. Laid only at rest:
|
|
651
|
+
// a reveal, a burst highlight or a flashing stamp repaints the raster by blit, which a static
|
|
652
|
+
// replica would hide.
|
|
653
|
+
const isAtRest = motion.frame.type >= 1 && !motion.highlight && Object.keys(motion.marks).length === 0
|
|
654
|
+
const spots = isAtRest ? librarySpots(catalog, width, libraryRows, options.focus) : []
|
|
655
|
+
const grid = new Uint32Array(Uint8Array.fromBase64(cells).buffer)
|
|
656
|
+
const cell = (x: number, y: number) => {
|
|
657
|
+
const i = (y * width + x) * 3
|
|
658
|
+
return { ch: String.fromCodePoint(grid[i]!), fg: grid[i + 1]!, bg: grid[i + 2]! }
|
|
659
|
+
}
|
|
660
|
+
const colourOf = (c: number) => (c === DEFAULT_CELL ? undefined : hex(c))
|
|
661
|
+
const cardWidth = Math.min(46, width - 2)
|
|
662
|
+
return (
|
|
663
|
+
<Box key="library" position="relative" width={width} height={libraryRows}>
|
|
664
|
+
<Raster key="map" columns={width} rows={libraryRows} cells={cells} />
|
|
665
|
+
{/* Every book's hit-area first… */}
|
|
666
|
+
{spots.flatMap(s => {
|
|
667
|
+
const scope = scopeOf(s.book)
|
|
668
|
+
const pin = () => {
|
|
669
|
+
motion.focus = s.book.problem?.id ?? s.book.memberIds[0]
|
|
670
|
+
$.ui.invalidate('ui.render')
|
|
671
|
+
}
|
|
672
|
+
return [...Array(s.height).keys()].map(r => (
|
|
673
|
+
<Box key={`${scope}:${r}`} position="absolute" top={s.top + r} left={s.x} hover={{ scope }}>
|
|
674
|
+
<Button key={`${scope}:press:${r}`} plain onPress={pin}>
|
|
675
|
+
{[...Array(s.width).keys()].map(dx => {
|
|
676
|
+
const c = cell(s.x + dx, s.top + r)
|
|
677
|
+
return <Text color={colourOf(c.fg)} backgroundColor={colourOf(c.bg)}>{c.ch}</Text>
|
|
678
|
+
})}
|
|
679
|
+
</Button>
|
|
680
|
+
</Box>
|
|
681
|
+
))
|
|
682
|
+
})}
|
|
683
|
+
{/* …then every card. Positioned boxes paint in tree order, so a card emitted beside its own
|
|
684
|
+
book was painted over by every book after it. Last in the tree, a card is over all of them;
|
|
685
|
+
the background makes it opaque, or the books behind would show through its blank cells. */}
|
|
686
|
+
{spots.map(s => {
|
|
687
|
+
const scope = scopeOf(s.book)
|
|
688
|
+
const parts = (['cause', 'problem', 'fix'] as const).flatMap(r => (s.book[r] ? [s.book[r]!] : []))
|
|
689
|
+
const cardHeight = parts.length + 3
|
|
690
|
+
const cardLeft = s.x + s.width + 1 + cardWidth <= width ? s.x + s.width + 1 : Math.max(0, s.x - cardWidth - 1)
|
|
691
|
+
const cardTop = Math.max(0, Math.min(s.top, libraryRows - cardHeight))
|
|
692
|
+
const paper = hex(palette.card)
|
|
693
|
+
return (
|
|
694
|
+
<Box
|
|
695
|
+
key={`${scope}:card`}
|
|
696
|
+
position="absolute"
|
|
697
|
+
top={cardTop}
|
|
698
|
+
left={cardLeft}
|
|
699
|
+
width={cardWidth}
|
|
700
|
+
display="none"
|
|
701
|
+
hover={{ scope, display: 'flex', backgroundColor: paper }}
|
|
702
|
+
flexDirection="column"
|
|
703
|
+
borderStyle="round"
|
|
704
|
+
borderColor={hex(palette.gilt)}
|
|
705
|
+
>
|
|
706
|
+
<Text bold wrap="truncate-end" color={hex(palette.binding.problem)} backgroundColor={paper}>
|
|
707
|
+
{fitTitle(`${callNumber(catalog, s.book)} ${s.book.tech ?? 'general'}`, cardWidth - 2).padEnd(cardWidth - 2)}
|
|
708
|
+
</Text>
|
|
709
|
+
{parts.map(part => (
|
|
710
|
+
<Text wrap="truncate-end" backgroundColor={paper}>
|
|
711
|
+
<Text color={hex(palette.binding[bindingOf(part.label)])}>{BINDING_GLYPH[bindingOf(part.label)]} </Text>
|
|
712
|
+
<Text color={hex(part.isDelivered ? palette.cardInk : palette.cardFaint)}>
|
|
713
|
+
{fitTitle(part.name, cardWidth - 4).padEnd(cardWidth - 4)}
|
|
714
|
+
</Text>
|
|
715
|
+
</Text>
|
|
716
|
+
))}
|
|
717
|
+
<Text color={hex(palette.cardFaint)} backgroundColor={paper} wrap="truncate-end">
|
|
718
|
+
{'click to read it on the card'.padEnd(cardWidth - 2)}
|
|
719
|
+
</Text>
|
|
720
|
+
</Box>
|
|
721
|
+
)
|
|
722
|
+
})}
|
|
433
723
|
</Box>
|
|
434
724
|
)
|
|
725
|
+
})()
|
|
726
|
+
) : (
|
|
727
|
+
// No cell grid off the terminal: the catalog as a reading list, the book in focus first.
|
|
728
|
+
<Box flexDirection="column" height={libraryRows}>
|
|
729
|
+
{[...(focus ? [focus] : []), ...books.filter(b => b !== focus)].slice(0, libraryRows).map(b => {
|
|
730
|
+
const head = (b.problem ?? b.fix ?? b.cause)!
|
|
731
|
+
return (
|
|
732
|
+
<Text wrap="truncate-end">
|
|
733
|
+
<Text color={hex(palette.binding[bindingOf(head.label)])}>{BINDING_GLYPH[bindingOf(head.label)]} </Text>
|
|
734
|
+
<Text dimColor>{callNumber(catalog, b)} </Text>
|
|
735
|
+
<Text>{fitTitle(head.name, width - 12)}</Text>
|
|
736
|
+
</Text>
|
|
737
|
+
)
|
|
738
|
+
})}
|
|
739
|
+
</Box>
|
|
740
|
+
)
|
|
435
741
|
|
|
436
742
|
return (
|
|
437
743
|
<Box flexDirection="column" width={width}>
|
|
438
744
|
<Text bold wrap="truncate-end">
|
|
439
|
-
{
|
|
440
|
-
<Text
|
|
441
|
-
<Text
|
|
442
|
-
|
|
443
|
-
|
|
745
|
+
<Text color={hex(palette.gilt)}>◉ </Text>
|
|
746
|
+
<Text>{shortName(map.title || 'errata library', Math.max(8, width - 44))}</Text>
|
|
747
|
+
<Text dimColor>
|
|
748
|
+
{' '}
|
|
749
|
+
{map.seedLabel ?? ''} · {map.origin ? ORIGIN_LABEL[map.origin] : 'burst'} · {books.length} {books.length === 1 ? 'chain' : 'chains'} ·{' '}
|
|
750
|
+
{catalog.cases.length} {catalog.cases.length === 1 ? 'case' : 'cases'}
|
|
751
|
+
</Text>
|
|
444
752
|
</Text>
|
|
445
|
-
{
|
|
446
|
-
{
|
|
753
|
+
{library}
|
|
754
|
+
{showLegend && (
|
|
447
755
|
<Text wrap="truncate-end">
|
|
448
|
-
{
|
|
756
|
+
{legend.map(b => (
|
|
449
757
|
<Text>
|
|
450
|
-
<Text color={hex(
|
|
451
|
-
<Text dimColor> {
|
|
758
|
+
<Text color={hex(palette.binding[b])}>█</Text>
|
|
759
|
+
<Text dimColor> {BINDING_NAME[b]} </Text>
|
|
452
760
|
</Text>
|
|
453
761
|
))}
|
|
762
|
+
<Text dimColor>· spines read top to bottom · the card reads the marked book</Text>
|
|
454
763
|
</Text>
|
|
455
764
|
)}
|
|
456
|
-
{
|
|
457
|
-
<Text wrap="wrap">
|
|
458
|
-
{legend.map(([label, count]) => (
|
|
459
|
-
<Text>
|
|
460
|
-
<Text color={hex(colorOf(label))}>{String.fromCodePoint(glyphOf(label))}</Text>
|
|
461
|
-
<Text dimColor> {count} {label} </Text>
|
|
462
|
-
</Text>
|
|
463
|
-
))}
|
|
464
|
-
</Text>
|
|
465
|
-
)}
|
|
466
|
-
{recent.map(v => (
|
|
765
|
+
{unshelved.map(v => (
|
|
467
766
|
<Text wrap="truncate-end">
|
|
468
|
-
<Text color={hex(
|
|
767
|
+
<Text color={hex(palette.verdict[v.kind])} bold>
|
|
469
768
|
{JUDGMENT_STYLE[v.kind].glyph} {JUDGMENT_STYLE[v.kind].verb}{' '}
|
|
470
769
|
</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>
|
|
770
|
+
<Text>{shortName(v.name, width - 14)}</Text>
|
|
478
771
|
</Text>
|
|
479
772
|
))}
|
|
480
773
|
</Box>
|
|
481
774
|
)
|
|
482
775
|
})
|
|
483
776
|
}
|
|
484
|
-
|