tickmarkr 2.6.4 → 2.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +36 -0
- package/dist/adapters/codex-commit-check.js +205 -0
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +12 -6
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +1 -1
- package/dist/adapters/model-lints.js +4 -0
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +10 -0
- package/dist/cli/commands/approve.js +10 -4
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +17 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +363 -106
- package/dist/cli/commands/init.js +10 -3
- package/dist/cli/commands/plan.js +76 -4
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +65 -15
- package/dist/config/fleet-overlay.d.ts +16 -1
- package/dist/config/fleet-overlay.js +230 -28
- package/dist/drivers/orca.d.ts +34 -2
- package/dist/drivers/orca.js +206 -32
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +5 -3
- package/dist/gates/run-gates.js +112 -56
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +1 -1
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/daemon.js +326 -130
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +6 -2
- package/dist/run/journal.js +22 -7
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +24 -1
- package/dist/tui/cockpit/live-store.js +99 -8
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +37 -10
- package/dist/tui/ink/fleet-app.js +65 -21
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +32 -2
- package/skills/tickmarkr-loop/SKILL.md +46 -2
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
|
@@ -136,13 +136,16 @@ export function boardFrame(input, width) {
|
|
|
136
136
|
const unknown = buckets.filter(([, v]) => v === undefined).map(([k]) => k);
|
|
137
137
|
const parked = buckets.filter(([, v]) => v !== undefined && v.length > 0).map(([k, v]) => `${k} ${v.join(",")}`);
|
|
138
138
|
const tally = `${doneIds.size} merged · ${(s.buckets.human ?? []).length} parked · ${(s.buckets.blocked ?? []).length} dep-blocked`;
|
|
139
|
-
|
|
139
|
+
// I2: the same CURRENT owed-check authority as the lifecycle — debt must be known and empty.
|
|
140
|
+
const owed = s.debt === 0 ? undefined : s.debt === "unknown" ? "outstanding unknown" : `outstanding ${s.debt} (${s.outstanding.join(", ")})`;
|
|
141
|
+
const tipOk = s.currentTip === "passed" || s.currentTip === "not required";
|
|
142
|
+
const green = !unknown.length && !parked.length && tipOk && !owed;
|
|
140
143
|
const now = green
|
|
141
144
|
? `${pass("RUN ENDED — GREEN")} ${mute(`tip ${s.currentTip} · ${tally}`)}`
|
|
142
145
|
: s.currentTip === "failed"
|
|
143
146
|
? `${fail("RUN ENDED — TIP VERIFY FAILED")} ${mute(tally)}`
|
|
144
|
-
: parked.length || unknown.length
|
|
145
|
-
? `${fail("RUN ENDED — NOT GREEN")} ${mute(`${[...parked, ...unknown.map((k) => `${k} bucket unknown`)].join(" · ")} · ${tally}`)}`
|
|
147
|
+
: parked.length || unknown.length || tipOk
|
|
148
|
+
? `${fail("RUN ENDED — NOT GREEN")} ${mute(`${[...parked, ...unknown.map((k) => `${k} bucket unknown`), ...(owed ? [owed] : [])].join(" · ")} · ${tally}`)}`
|
|
146
149
|
: `${warn("RUN ENDED — TIP NOT VERIFIED")} ${mute(`tip ${s.currentTip} · ${tally}`)}`;
|
|
147
150
|
top.push(` ${accent("▌")} ${bold("NOW")} ${now}`);
|
|
148
151
|
top.push("");
|
|
@@ -152,8 +155,11 @@ export function boardFrame(input, width) {
|
|
|
152
155
|
top.push("");
|
|
153
156
|
// ── aligned gate header: each label truncated to the cell it owns (prototype :382-394) ──
|
|
154
157
|
const elastic = Math.max(30, W - BOARD_FIXED - BOARD_NOTE_RESERVE);
|
|
155
|
-
const
|
|
156
|
-
const
|
|
158
|
+
const compactLabel = Math.min(46, Math.max(18, Math.round(elastic * 0.62)));
|
|
159
|
+
const stacked = boardBand(W) === "stacked";
|
|
160
|
+
const LBL = stacked ? Math.max(0, W - 4 - 5 - BOARD_AREA_W - BOARD_DEPS_W)
|
|
161
|
+
: compactLabel;
|
|
162
|
+
const CHAN = Math.min(26, Math.max(12, elastic - compactLabel));
|
|
157
163
|
const gateHead = BOARD_GATES.map((g) => faint(pad(g.slice(0, BOARD_CELL - 1), BOARD_CELL))).join("");
|
|
158
164
|
const gateHeader = {
|
|
159
165
|
head: ` ${pad("", 5)}${pad(faint("area"), BOARD_AREA_W)}${pad(faint("deps"), BOARD_DEPS_W)}${pad(faint("task"), LBL)}`,
|
|
@@ -200,7 +206,7 @@ export function boardFrame(input, width) {
|
|
|
200
206
|
? depText
|
|
201
207
|
: depIds.length > 1 ? `${depIds[0]}+${depIds.length - 1}` : `${depIds[0].slice(0, BOARD_DEPS_W - 3)}…`;
|
|
202
208
|
const deps = pad(faint(depLabel), BOARD_DEPS_W);
|
|
203
|
-
const titleText = sliceCells(g.title ?? "", LBL - 2).head;
|
|
209
|
+
const titleText = sliceCells(g.title ?? "", stacked ? LBL : LBL - 2).head;
|
|
204
210
|
const title = hot ? body(pad(titleText, LBL)) : faint(pad(titleText, LBL));
|
|
205
211
|
const note = [
|
|
206
212
|
parks >= 3 ? park(`◍ parked ×${parks}`) : "",
|
|
@@ -259,6 +259,10 @@ export declare const SHELL_BINDINGS: readonly [{
|
|
|
259
259
|
readonly key: "5";
|
|
260
260
|
readonly label: "Evidence";
|
|
261
261
|
readonly action: "evidence";
|
|
262
|
+
}, {
|
|
263
|
+
readonly key: "7";
|
|
264
|
+
readonly label: "Log";
|
|
265
|
+
readonly action: "log";
|
|
262
266
|
}, {
|
|
263
267
|
readonly key: "Tab";
|
|
264
268
|
readonly label: "Focus";
|
|
@@ -322,6 +326,10 @@ export declare function shellBindings(context: {
|
|
|
322
326
|
readonly key: "5";
|
|
323
327
|
readonly label: "Evidence";
|
|
324
328
|
readonly action: "evidence";
|
|
329
|
+
} | {
|
|
330
|
+
readonly key: "7";
|
|
331
|
+
readonly label: "Log";
|
|
332
|
+
readonly action: "log";
|
|
325
333
|
} | {
|
|
326
334
|
readonly key: "Tab";
|
|
327
335
|
readonly label: "Focus";
|
package/dist/tui/cockpit/keys.js
CHANGED
|
@@ -637,6 +637,7 @@ export const SHELL_BINDINGS = [
|
|
|
637
637
|
{ key: "1", label: "Home", action: "home" },
|
|
638
638
|
{ key: "4", label: "Run", action: "run" },
|
|
639
639
|
{ key: "5", label: "Evidence", action: "evidence" },
|
|
640
|
+
{ key: "7", label: "Log", action: "log" },
|
|
640
641
|
{ key: "Tab", label: "Focus", action: "focus" },
|
|
641
642
|
{ key: "Enter", label: "Open", action: "open" },
|
|
642
643
|
{ key: "a", label: "Actions", action: "actions" },
|
|
@@ -658,7 +659,9 @@ export function shellBindings(context) {
|
|
|
658
659
|
return context.canResize === true;
|
|
659
660
|
if (binding.action === "export")
|
|
660
661
|
return context.view === "evidence";
|
|
661
|
-
if (binding.action === "
|
|
662
|
+
if (binding.action === "follow")
|
|
663
|
+
return context.view === "log" || context.view === "evidence" && !context.evidenceSection;
|
|
664
|
+
if (binding.action === "filter")
|
|
662
665
|
return context.view === "evidence" && !context.evidenceSection;
|
|
663
666
|
return true;
|
|
664
667
|
});
|
|
@@ -306,8 +306,8 @@ export declare function planFrame(size: MeasuredSize, view?: FrameView, state?:
|
|
|
306
306
|
export declare function resolveCockpitLayout(columns: number, rows: number): CockpitLayout;
|
|
307
307
|
/**
|
|
308
308
|
* FINAL §3.1: one geometry for paint, focus and pointer hit testing. A ZERO shortcut budget is the
|
|
309
|
-
*
|
|
310
|
-
*
|
|
309
|
+
* explicit compact mount: it plans no rail and no shortcuts at any width. Daemon-owned and manual
|
|
310
|
+
* boards both use the default budget; an explicit zero is honoured, never floored back to 20.
|
|
311
311
|
*/
|
|
312
312
|
export declare function planShell(columns: number, rows: number, shortcutColumns?: number): {
|
|
313
313
|
columns: number;
|
|
@@ -391,8 +391,8 @@ export function resolveCockpitLayout(columns, rows) {
|
|
|
391
391
|
}
|
|
392
392
|
/**
|
|
393
393
|
* FINAL §3.1: one geometry for paint, focus and pointer hit testing. A ZERO shortcut budget is the
|
|
394
|
-
*
|
|
395
|
-
*
|
|
394
|
+
* explicit compact mount: it plans no rail and no shortcuts at any width. Daemon-owned and manual
|
|
395
|
+
* boards both use the default budget; an explicit zero is honoured, never floored back to 20.
|
|
396
396
|
*/
|
|
397
397
|
export function planShell(columns, rows, shortcutColumns = 22) {
|
|
398
398
|
columns = Math.max(0, Math.floor(columns));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { renderMarkdownRecord } from "../../cli/commands/report.js";
|
|
5
5
|
import { loadConfig } from "../../config/config.js";
|
|
@@ -7,13 +7,14 @@ import { estimateCosts } from "../../report/cost.js";
|
|
|
7
7
|
import { PerformanceObserver, performance } from "node:perf_hooks";
|
|
8
8
|
import { render, useInput, useStdin } from "ink";
|
|
9
9
|
import { useLayoutEffect, useSyncExternalStore } from "react";
|
|
10
|
-
import { createLiveStore } from "./live-store.js";
|
|
10
|
+
import { createLiveStore, journalBasis, readObservedJournal } from "./live-store.js";
|
|
11
11
|
import { HomeView, deriveHomeView, selectNeedsYouTarget } from "./home-view.js";
|
|
12
12
|
import { RunView, applyRunViewKey, initialRunViewSession, runGateCells, evidenceLookup } from "./run-view.js";
|
|
13
13
|
import { renderBoardLines, stripBoardAnsi } from "./board.js";
|
|
14
14
|
import { EvidenceView, deriveEvidenceView, defaultExportPath, planEvidenceExport, writeEvidenceExport } from "./evidence-view.js";
|
|
15
15
|
import { ShellGridFlow, ShellJournalSelection } from "./components.js";
|
|
16
16
|
import { CockpitShell, initialShellState } from "./shell.js";
|
|
17
|
+
import { LogView, readLogPage, moveLogEnd } from "./log-view.js";
|
|
17
18
|
import { planShell } from "./layout.js";
|
|
18
19
|
import { shellBindings } from "./keys.js";
|
|
19
20
|
import { createPointerReportReader, POINTER_TRACKING_ON, POINTER_TRACKING_OFF } from "./pointer.js";
|
|
@@ -21,7 +22,7 @@ import { deriveRunDecisions, previewDecision, executeDecision, withDecisionPrevi
|
|
|
21
22
|
import { approvalRunOwner } from "../../cli/commands/approve.js";
|
|
22
23
|
import { GLYPHS } from "../../brand.js";
|
|
23
24
|
import { effectiveEvents, Journal, parseJournalText, physicalLine } from "../../run/journal.js";
|
|
24
|
-
import { observeNamedRun
|
|
25
|
+
import { observeNamedRun } from "../../run/supervision.js";
|
|
25
26
|
import { formatOwnedName } from "../../drivers/types.js";
|
|
26
27
|
import { readOperatorState } from "../../run/operator-state.js";
|
|
27
28
|
import { cellWidth, wrapCells } from "./width.js";
|
|
@@ -59,6 +60,10 @@ export function borrowRuntimeTimeline() {
|
|
|
59
60
|
}
|
|
60
61
|
export const SHELL_TERMINAL_ENTER = "\x1b[?1049h\x1b[22;0t\x1b]0;tickmarkr cockpit\x07" + POINTER_TRACKING_ON;
|
|
61
62
|
export const SHELL_TERMINAL_RESTORE = POINTER_TRACKING_OFF + "\x1b[23;0t\x1b[?25h\x1b[?1049l";
|
|
63
|
+
/** Rows not bound to the store's observation: only the store's own observation is served, with debt unknown
|
|
64
|
+
* (the fold's formula then gives PARTIAL for a headline that was green — green implies ended and unapproved). */
|
|
65
|
+
const unbound = (snap) => ({ ...snap, owed: undefined, operator: { ...snap.operator, debt: "unknown", outstanding: [],
|
|
66
|
+
green: false, ...(snap.operator.green ? { lifecycle: "PARTIAL", label: "PARTIAL" } : {}) } });
|
|
62
67
|
/**
|
|
63
68
|
* OBS-1178: the store's incremental fold reads every task-approved row as a release. A refused or unsound
|
|
64
69
|
* decision released nothing, so whenever the rows the store has read hold one — whatever state the raw
|
|
@@ -70,36 +75,48 @@ export const SHELL_TERMINAL_RESTORE = POINTER_TRACKING_OFF + "\x1b[23;0t\x1b[?25
|
|
|
70
75
|
*/
|
|
71
76
|
export function decidedLiveStore(store) {
|
|
72
77
|
let key;
|
|
73
|
-
let
|
|
78
|
+
let events;
|
|
79
|
+
// I2: the refold is keyed by the store's owed authority too, so a debt the store re-folded (an artifact
|
|
80
|
+
// removed or altered under unchanged journal bytes) is never served from an earlier observation's refold.
|
|
81
|
+
let folded;
|
|
74
82
|
const decided = (snap) => {
|
|
75
83
|
const { journal, graph } = snap;
|
|
76
|
-
const observed = `${journal
|
|
84
|
+
const observed = `${journalBasis(journal)}:${graph.identity}:${graph.status}:${snap.lock.state}`;
|
|
77
85
|
if (observed !== key) {
|
|
78
86
|
key = observed;
|
|
79
|
-
|
|
87
|
+
events = undefined;
|
|
88
|
+
folded = undefined;
|
|
80
89
|
let bytes;
|
|
90
|
+
// I2 (D-857): the refold reads the very journal version the store observed — the one its owed authority
|
|
91
|
+
// and basis name. A changed or failed read is never cached (un-keyed, re-read next observation) and
|
|
92
|
+
// decides nothing meanwhile: debt unknown, never green.
|
|
81
93
|
try {
|
|
82
|
-
bytes =
|
|
94
|
+
bytes = readObservedJournal(journal);
|
|
83
95
|
}
|
|
84
96
|
catch {
|
|
85
|
-
|
|
97
|
+
key = undefined;
|
|
98
|
+
return unbound(snap);
|
|
86
99
|
}
|
|
87
100
|
if (bytes.includes("task-approved")) {
|
|
88
101
|
const rows = parseJournalText(bytes.subarray(0, bytes.lastIndexOf(10) + 1).toString("utf8"))
|
|
89
102
|
.filter(e => e !== null && typeof e === "object" && typeof e.event === "string" && typeof e.data === "object" && e.data !== null);
|
|
90
103
|
const kept = effectiveEvents(rows);
|
|
91
104
|
if (kept.length !== rows.length) {
|
|
92
|
-
|
|
105
|
+
events = kept.map((event, i) => {
|
|
93
106
|
const line = physicalLine(kept, i);
|
|
94
107
|
return { source: journal.source, line, id: `${journal.source}#L${line}`, generation: journal.generation, event };
|
|
95
108
|
});
|
|
96
|
-
operator = readOperatorState({ events, graph: graph.status === "readable" ? graph.value : undefined,
|
|
97
|
-
readable: journal.status === "readable" && journal.backlogBytes === 0 });
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
|
-
if (!
|
|
112
|
+
if (!events)
|
|
102
113
|
return snap;
|
|
114
|
+
if (!folded || folded.owed !== snap.owed) {
|
|
115
|
+
folded = { owed: snap.owed, operator: readOperatorState({ events, graph: graph.status === "readable" ? graph.value : undefined,
|
|
116
|
+
readable: journal.status === "readable" && journal.backlogBytes === 0, daemonDead: snap.lock.state === "dead",
|
|
117
|
+
owed: snap.owed, basis: journalBasis(journal) }) };
|
|
118
|
+
}
|
|
119
|
+
const operator = folded.operator;
|
|
103
120
|
const { sequence, observedAt, graphAvailability } = snap.operator;
|
|
104
121
|
return { ...snap, operator: { ...operator, sequence, observedAt, graphAvailability } };
|
|
105
122
|
};
|
|
@@ -147,10 +164,8 @@ export async function runConsolidatedCockpit(options) {
|
|
|
147
164
|
const source = decidedLiveStore(store);
|
|
148
165
|
if (options.observeRun !== false)
|
|
149
166
|
observation = observeNamedRun(cwd, runId, options.environment);
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
const railless = Boolean((options.environment ?? process.env)[WATCH_OWNER_ENV]);
|
|
153
|
-
let state = { ...initialShellState(), view: options.initialView ?? "home", ...(railless ? { shortcutColumns: 0 } : {}) };
|
|
167
|
+
// Daemon-placed and manual boards share the same rails and navigation.
|
|
168
|
+
let state = { ...initialShellState(), view: options.initialView ?? "home" };
|
|
154
169
|
let runSession = initialRunViewSession();
|
|
155
170
|
if (options.initialParks) {
|
|
156
171
|
runSession = { ...runSession, selection: Math.max(0, source.snapshot().operator.tasks.findIndex(t => t.state === "human" || t.state === "blocked")) };
|
|
@@ -202,6 +217,9 @@ export async function runConsolidatedCockpit(options) {
|
|
|
202
217
|
snap.journal.generation, snap.journal.offset, snap.journal.status, snap.journal.malformedCount, snap.journal.backlogBytes, snap.journal.pending, snap.journal.error,
|
|
203
218
|
snap.graph.identity, snap.graph.status, snap.config.identity, snap.config.status, snap.cache.identity, snap.cache.status,
|
|
204
219
|
snap.lock.value, snap.lock.status, snap.lock.state, snap.lock.alive,
|
|
220
|
+
// I2: owed-check debt moves under unchanged journal bytes (an artifact removed or altered) and
|
|
221
|
+
// decides the drawn lifecycle and board headline.
|
|
222
|
+
snap.operator.lifecycle, snap.operator.debt, snap.operator.outstanding,
|
|
205
223
|
// D-383: supervision tier state is not drawn by any view; an ARMED→STALE transition earns no
|
|
206
224
|
// frame. Its UNREADABLE case already surfaces through snap.errors above.
|
|
207
225
|
]);
|
|
@@ -418,7 +436,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
418
436
|
}
|
|
419
437
|
else {
|
|
420
438
|
const action = shellBindings(state).find(b => b.key === text)?.action;
|
|
421
|
-
if (action === "home" || action === "run" || action === "evidence")
|
|
439
|
+
if (action === "home" || action === "run" || action === "evidence" || action === "log")
|
|
422
440
|
state = { ...state, view: action, scroll: 0, evidenceSection: action === state.view ? state.evidenceSection : 0 };
|
|
423
441
|
else if (action === "widen" || action === "narrow")
|
|
424
442
|
state = { ...state, shortcutColumns: planShell(geometry().columns, geometry().rows, geometry().shortcuts + (action === "widen" ? 1 : -1)).shortcuts };
|
|
@@ -432,6 +450,13 @@ export async function runConsolidatedCockpit(options) {
|
|
|
432
450
|
}
|
|
433
451
|
else if (state.view === "evidence" && (k.leftArrow || k.rightArrow))
|
|
434
452
|
state = { ...state, evidenceSection: ((state.evidenceSection ?? 0) + (k.rightArrow ? 1 : 4)) % 5 };
|
|
453
|
+
else if (state.view === "log" && (k.pageDown || k.pageUp || k.upArrow || k.downArrow || action === "follow")) {
|
|
454
|
+
const snap = source.snapshot();
|
|
455
|
+
const sourceLog = { journal: snap.journal, page: source.page, runId, columns: geometry().bodyColumns };
|
|
456
|
+
const delta = (k.pageDown || k.downArrow ? 1 : -1) * (k.pageDown || k.pageUp ? geometry().bodyRows : 1);
|
|
457
|
+
state = { ...state, logEnd: action === "follow" ? state.logEnd === undefined ? snap.journal.lines : undefined
|
|
458
|
+
: moveLogEnd(sourceLog, state.logEnd, delta), scroll: 0 };
|
|
459
|
+
}
|
|
435
460
|
else if ((k.pageDown || k.pageUp) && state.view !== "run")
|
|
436
461
|
state = { ...state, scroll: Math.max(0, state.scroll + (k.pageDown ? 1 : -1) * geometry().bodyRows) };
|
|
437
462
|
else if (k.return || action === "open") {
|
|
@@ -471,7 +496,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
471
496
|
else if (text === "a")
|
|
472
497
|
state = { ...state, overlay: ["Existing CLI actions", "tickmarkr fleet", "tickmarkr plan", "tickmarkr doctor"], overlayOffset: 0 };
|
|
473
498
|
}
|
|
474
|
-
if (!terminal && state.view !== "run" && !state.overlay && !state.help && state.editor === undefined) {
|
|
499
|
+
if (!terminal && state.view !== "run" && state.view !== "log" && !state.overlay && !state.help && state.editor === undefined) {
|
|
475
500
|
const bytes = k.upArrow ? "\x1b[A" : k.downArrow ? "\x1b[B" : k.rightArrow ? "\x1b[C" : k.leftArrow ? "\x1b[D" : k.pageUp ? "\x1b[5~" : k.pageDown ? "\x1b[6~" : text === "f" ? "f" : undefined;
|
|
476
501
|
if (bytes)
|
|
477
502
|
forwardLeaf(bytes);
|
|
@@ -700,7 +725,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
700
725
|
const usedTasks = new Set();
|
|
701
726
|
for (const row of plan.paintedRows) {
|
|
702
727
|
if (row.row < plan.bodyRow && (row.text.startsWith("1 Home") || row.text.startsWith("1H"))) {
|
|
703
|
-
for (const binding of bindings.filter(binding => ["home", "run", "evidence"].includes(binding.action))) {
|
|
728
|
+
for (const binding of bindings.filter(binding => ["home", "run", "evidence", "log"].includes(binding.action))) {
|
|
704
729
|
const caption = plan.columns < 60 ? `${binding.key}${binding.label[0]}` : `${binding.key} ${binding.label}`;
|
|
705
730
|
const index = row.text.indexOf(caption);
|
|
706
731
|
if (index >= 0)
|
|
@@ -758,7 +783,8 @@ export async function runConsolidatedCockpit(options) {
|
|
|
758
783
|
committedTargets = targets;
|
|
759
784
|
}, children: state.view === "home" ? _jsx(ShellGridFlow, { width: p.bodyColumns, columns: p.columns < 120 ? 2 : 3, rowHeight: p.rows < 20 ? 1 : p.columns < 120 ? 2 : 3, children: _jsx(HomeView, { width: p.bodyColumns, model: homeModel, focused: focused, onOpenPark: id => { runSession = { ...runSession, selection: Math.max(0, snap.operator.tasks.findIndex(t => t.id === id)) }; state = { ...state, view: "run", scroll: 0 }; followSelection(); publish(); }, onOpenDiagnostic: id => { state = { ...state, overlay: [id], overlayOffset: 0 }; publish(); }, onOpenEvidence: openEvidence, onSelectNeedsYou: reportHomeNeedsYouSelection }, String(homeModel.needsYou.length > 0)) }) :
|
|
760
785
|
state.view === "run" ? _jsx(RunView, { snapshot: snap.operator, rows: snap.journal.history, page: source.page, graph: snap.graph.value, decisions: visibleDecisions, session: runSession, columns: p.bodyColumns, run: approvalRunOwner(cwd, runId), now: options.now }) :
|
|
761
|
-
|
|
786
|
+
state.view === "log" ? _jsx(LogView, { page: readLogPage({ journal: snap.journal, page: source.page, runId, columns: p.bodyColumns }, p.bodyRows, state.logEnd) }) :
|
|
787
|
+
_jsx(ShellJournalSelection.Provider, { value: reportEvidenceSelection, children: _jsx(EvidenceView, { width: p.bodyColumns, model: { ...model, journal: evidenceRows }, focusEvidence: selectedEvidence, focused: focused, onExport: beginExport, onSelect: id => { selectedEvidence = id; openDetail(); publish(); } }, `${state.query}:${evidenceNavigation}`) }) });
|
|
762
788
|
}
|
|
763
789
|
app = render(_jsx(App, {}), { stdin, stdout: output, debug: options.debug, exitOnCtrlC: false, patchConsole: false });
|
|
764
790
|
void app.waitUntilExit().then(() => stop(), stop);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JournalEvent } from "../../run/journal.js";
|
|
2
|
-
import { type OperatorRecord } from "../../run/operator-state.js";
|
|
2
|
+
import { type OperatorRecord, type OwedAuthority } from "../../run/operator-state.js";
|
|
3
3
|
export declare const OBSERVATION_INTERVAL_MS = 1000;
|
|
4
4
|
/** Stat-to-fstat observations one poll spends on a journal a writer keeps appending to. */
|
|
5
5
|
export declare const APPEND_RACE_OBSERVATIONS = 3;
|
|
@@ -27,9 +27,12 @@ export interface JournalLine extends Omit<OperatorRecord, "event"> {
|
|
|
27
27
|
endOffset: number;
|
|
28
28
|
}
|
|
29
29
|
export interface TailSnapshot {
|
|
30
|
+
/** `digest` (I2): sha256 of every byte the tail consumed, [0, offset) — the bytes its fold was built from. */
|
|
30
31
|
source: string;
|
|
31
32
|
generation: number;
|
|
32
33
|
identity?: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
digest: string;
|
|
33
36
|
offset: number;
|
|
34
37
|
lines: number;
|
|
35
38
|
history: readonly JournalLine[];
|
|
@@ -64,11 +67,17 @@ export declare class JournalTail {
|
|
|
64
67
|
private lastSuccessfulReadAt?;
|
|
65
68
|
private failure?;
|
|
66
69
|
private stale;
|
|
70
|
+
private hash;
|
|
71
|
+
private invalid;
|
|
67
72
|
constructor(source: string, hooks?: {
|
|
68
73
|
record?: (record: OperatorRecord) => void;
|
|
69
74
|
reset?: () => void;
|
|
70
75
|
});
|
|
71
76
|
private reset;
|
|
77
|
+
/** I2: the consumed prefix was found rewritten (an in-place rewrite then growth reads as an append), so the
|
|
78
|
+
* next poll treats the file as replaced and refolds it from its first byte. */
|
|
79
|
+
invalidate(): void;
|
|
80
|
+
get invalidated(): boolean;
|
|
72
81
|
/** A same-inode append between stat and fstat is retried; exhaustion keeps the last good snapshot, pending. */
|
|
73
82
|
poll(now?: number): TailSnapshot;
|
|
74
83
|
/** One stat-to-fstat observation. False when an append raced it; nothing was consumed or reset. */
|
|
@@ -86,6 +95,18 @@ export interface CachedSource<T = unknown> {
|
|
|
86
95
|
error?: string;
|
|
87
96
|
observedAt: number;
|
|
88
97
|
}
|
|
98
|
+
/** I2: the journal basis an owed-check fold read — the observed file version, its generation and the bytes consumed. */
|
|
99
|
+
export declare const journalBasis: (journal: TailSnapshot) => string;
|
|
100
|
+
/** I2: the bytes the tail consumed are no longer the file's prefix — rewritten in place, then grown. */
|
|
101
|
+
export declare class JournalRewritten extends Error {
|
|
102
|
+
constructor();
|
|
103
|
+
}
|
|
104
|
+
/** I2 (D-857): the ONE way any reader beside the tail gets `journal`'s bytes — every consumed byte of the very
|
|
105
|
+
* file version the tail observed (an in-place rewrite, even an equal-length one, changes it), stamped before
|
|
106
|
+
* and after the read, and proved by digest to be the very bytes the tail's fold consumed (a rewrite followed by
|
|
107
|
+
* growth keeps the tail appending). Any change throws, so no second read is ever combined with this
|
|
108
|
+
* observation's lifecycle, owed or basis. */
|
|
109
|
+
export declare function readObservedJournal(journal: TailSnapshot): Buffer;
|
|
89
110
|
export interface LiveStoreOptions {
|
|
90
111
|
cwd: string;
|
|
91
112
|
runId: string;
|
|
@@ -102,6 +123,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
102
123
|
delayed: boolean;
|
|
103
124
|
freshness: string;
|
|
104
125
|
operator: import("../../run/operator-state.js").OperatorSnapshot;
|
|
126
|
+
owed: OwedAuthority | undefined;
|
|
105
127
|
journal: TailSnapshot;
|
|
106
128
|
graph: CachedSource<{
|
|
107
129
|
version: 1;
|
|
@@ -204,6 +226,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
204
226
|
delayed: boolean;
|
|
205
227
|
freshness: string;
|
|
206
228
|
operator: import("../../run/operator-state.js").OperatorSnapshot;
|
|
229
|
+
owed: OwedAuthority | undefined;
|
|
207
230
|
journal: TailSnapshot;
|
|
208
231
|
graph: CachedSource<{
|
|
209
232
|
version: 1;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { graphPath, stateDirName } from "../../graph/graph.js";
|
|
4
5
|
import { validateGraph } from "../../graph/schema.js";
|
|
5
|
-
import { parseRunId } from "../../run/journal.js";
|
|
6
|
+
import { foldOwedChecks, parseJournalText, parseRunId } from "../../run/journal.js";
|
|
6
7
|
import { isPidLive, STALE_MS } from "../../run/lock.js";
|
|
7
8
|
import { readTierLiveness, SUPERVISION_TIERS } from "../../run/supervision.js";
|
|
8
9
|
import { OperatorStateFold } from "../../run/operator-state.js";
|
|
@@ -14,6 +15,12 @@ export const STORE_LIMITS = { graphBytes: 16 * 1024 * 1024, history: 256, histor
|
|
|
14
15
|
const errorText = (e) => e instanceof Error ? e.message : String(e);
|
|
15
16
|
const fileIdentity = (st) => `${st.dev}:${st.ino}`;
|
|
16
17
|
const stamp = (st) => `${fileIdentity(st)}:${st.size}:${st.mtimeNs}:${st.ctimeNs}`;
|
|
18
|
+
/** The one record shape the tail accepts; the complete-journal (owed) read validates against it too. */
|
|
19
|
+
const isJournalRecord = (e) => {
|
|
20
|
+
const r = e;
|
|
21
|
+
return !!r && typeof r === "object" && typeof r.ts === "string" && Number.isFinite(Date.parse(r.ts)) && typeof r.event === "string"
|
|
22
|
+
&& !!r.data && typeof r.data === "object" && !Array.isArray(r.data) && (r.taskId === undefined || typeof r.taskId === "string");
|
|
23
|
+
};
|
|
17
24
|
function decodeLine(bytes, source, line, offset, endOffset, generation, oversized = false) {
|
|
18
25
|
const base = { source, line, id: `${source}#L${line}`, offset, endOffset, generation };
|
|
19
26
|
let raw = "";
|
|
@@ -24,7 +31,7 @@ function decodeLine(bytes, source, line, offset, endOffset, generation, oversize
|
|
|
24
31
|
if (!raw.trim())
|
|
25
32
|
return { ...base, raw };
|
|
26
33
|
const e = JSON.parse(raw);
|
|
27
|
-
if (!
|
|
34
|
+
if (!isJournalRecord(e))
|
|
28
35
|
throw new Error("invalid journal record");
|
|
29
36
|
return { ...base, raw, event: e };
|
|
30
37
|
}
|
|
@@ -51,6 +58,8 @@ export class JournalTail {
|
|
|
51
58
|
lastSuccessfulReadAt;
|
|
52
59
|
failure;
|
|
53
60
|
stale = false;
|
|
61
|
+
hash = createHash("sha256");
|
|
62
|
+
invalid = false;
|
|
54
63
|
constructor(source, hooks = {}) {
|
|
55
64
|
this.source = source;
|
|
56
65
|
this.hooks = hooks;
|
|
@@ -66,8 +75,14 @@ export class JournalTail {
|
|
|
66
75
|
this.errors = [];
|
|
67
76
|
this.malformedCount = 0;
|
|
68
77
|
this.generation++;
|
|
78
|
+
this.hash = createHash("sha256");
|
|
79
|
+
this.invalid = false;
|
|
69
80
|
this.hooks.reset?.();
|
|
70
81
|
}
|
|
82
|
+
/** I2: the consumed prefix was found rewritten (an in-place rewrite then growth reads as an append), so the
|
|
83
|
+
* next poll treats the file as replaced and refolds it from its first byte. */
|
|
84
|
+
invalidate() { this.invalid = true; }
|
|
85
|
+
get invalidated() { return this.invalid; }
|
|
71
86
|
/** A same-inode append between stat and fstat is retried; exhaustion keeps the last good snapshot, pending. */
|
|
72
87
|
poll(now = Date.now()) {
|
|
73
88
|
try {
|
|
@@ -94,7 +109,7 @@ export class JournalTail {
|
|
|
94
109
|
const st = statSync(this.source, { bigint: true });
|
|
95
110
|
if (!st.isFile())
|
|
96
111
|
throw new Error("journal source is not a regular file");
|
|
97
|
-
const replaced = !this.st || fileIdentity(st) !== fileIdentity(this.st) || st.size < this.st.size || (st.size === this.st.size && stamp(st) !== stamp(this.st));
|
|
112
|
+
const replaced = this.invalid || !this.st || fileIdentity(st) !== fileIdentity(this.st) || st.size < this.st.size || (st.size === this.st.size && stamp(st) !== stamp(this.st));
|
|
98
113
|
if (Number(st.size) > (replaced ? 0 : this.offset)) {
|
|
99
114
|
fd = openSync(this.source, "r");
|
|
100
115
|
const opened = fstatSync(fd, { bigint: true });
|
|
@@ -115,6 +130,7 @@ export class JournalTail {
|
|
|
115
130
|
throw new Error("journal truncated during read");
|
|
116
131
|
this.bytesRead += count;
|
|
117
132
|
remaining -= count;
|
|
133
|
+
this.hash.update(buffer.subarray(0, count));
|
|
118
134
|
let begin = 0;
|
|
119
135
|
for (let index = 0; index < count; index++)
|
|
120
136
|
if (buffer[index] === 10) {
|
|
@@ -158,7 +174,7 @@ export class JournalTail {
|
|
|
158
174
|
}
|
|
159
175
|
snapshot() {
|
|
160
176
|
return {
|
|
161
|
-
source: this.source, generation: this.generation, identity: this.st && fileIdentity(this.st), offset: this.offset, lines: this.line,
|
|
177
|
+
source: this.source, generation: this.generation, identity: this.st && fileIdentity(this.st), version: this.st && stamp(this.st), digest: this.hash.copy().digest("hex"), offset: this.offset, lines: this.line,
|
|
162
178
|
history: this.history.slice(), errors: this.errors.slice(), malformedCount: this.malformedCount,
|
|
163
179
|
pending: this.carryBytes ? { line: this.line + 1, bytes: this.carryBytes } : undefined,
|
|
164
180
|
backlogBytes: Math.max(0, Number(this.st?.size ?? 0) - this.offset),
|
|
@@ -262,6 +278,48 @@ class JsonSource {
|
|
|
262
278
|
}
|
|
263
279
|
}
|
|
264
280
|
}
|
|
281
|
+
/** I2: the journal basis an owed-check fold read — the observed file version, its generation and the bytes consumed. */
|
|
282
|
+
export const journalBasis = (journal) => `${journal.source}:${journal.generation}:${journal.version}:${journal.offset}`;
|
|
283
|
+
const unknownOwed = (reason) => ({ known: false, debt: "unknown", outstanding: [], acceptedRisk: [], discharged: [], unknown: [{ reason }] });
|
|
284
|
+
/** I2: the bytes the tail consumed are no longer the file's prefix — rewritten in place, then grown. */
|
|
285
|
+
export class JournalRewritten extends Error {
|
|
286
|
+
constructor() { super("journal prefix rewritten since the tail consumed it"); }
|
|
287
|
+
}
|
|
288
|
+
/** I2 (D-857): the ONE way any reader beside the tail gets `journal`'s bytes — every consumed byte of the very
|
|
289
|
+
* file version the tail observed (an in-place rewrite, even an equal-length one, changes it), stamped before
|
|
290
|
+
* and after the read, and proved by digest to be the very bytes the tail's fold consumed (a rewrite followed by
|
|
291
|
+
* growth keeps the tail appending). Any change throws, so no second read is ever combined with this
|
|
292
|
+
* observation's lifecycle, owed or basis. */
|
|
293
|
+
export function readObservedJournal(journal) {
|
|
294
|
+
const fd = openSync(journal.source, "r");
|
|
295
|
+
try {
|
|
296
|
+
const observed = () => { if (stamp(fstatSync(fd, { bigint: true })) !== journal.version)
|
|
297
|
+
throw new Error("journal changed since its observation"); };
|
|
298
|
+
observed();
|
|
299
|
+
const bytes = Buffer.alloc(journal.offset);
|
|
300
|
+
for (let length = 0; length < bytes.length;) {
|
|
301
|
+
const n = readSync(fd, bytes, length, bytes.length - length, length);
|
|
302
|
+
if (!n)
|
|
303
|
+
throw new Error("journal truncated since its observation");
|
|
304
|
+
length += n;
|
|
305
|
+
}
|
|
306
|
+
observed();
|
|
307
|
+
if (createHash("sha256").update(bytes).digest("hex") !== journal.digest)
|
|
308
|
+
throw new JournalRewritten();
|
|
309
|
+
return bytes;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
closeSync(fd);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** The complete journal behind `journal`'s basis: its observed bytes, every non-blank line a valid record. */
|
|
316
|
+
function readLedger(journal) {
|
|
317
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(readObservedJournal(journal));
|
|
318
|
+
const events = parseJournalText(text);
|
|
319
|
+
if (events.length !== text.split("\n").filter(line => line.trim()).length || !events.every(isJournalRecord))
|
|
320
|
+
throw new Error("complete journal holds an invalid line");
|
|
321
|
+
return events;
|
|
322
|
+
}
|
|
265
323
|
/** C1 mount/C6 reader boundary. No timers or presence writes: the mount owns scheduling and disarm. */
|
|
266
324
|
export function createLiveStore(options) {
|
|
267
325
|
const now = options.now ?? Date.now;
|
|
@@ -285,10 +343,41 @@ export function createLiveStore(options) {
|
|
|
285
343
|
let disposed = false;
|
|
286
344
|
let queued;
|
|
287
345
|
const metrics = [];
|
|
346
|
+
// I2: the complete journal is re-read only when its basis moves; its owed fold re-reads every discharge
|
|
347
|
+
// artifact on every observation, and only the immutable git re-proof is reused through this store's memo.
|
|
348
|
+
// ponytail: an ended run's whole parsed journal stays resident; fold a debt-only projection if that measures large.
|
|
349
|
+
const owedMemo = new Set();
|
|
350
|
+
let ledger;
|
|
351
|
+
const owedAuthority = (journal) => {
|
|
352
|
+
const basis = journalBasis(journal);
|
|
353
|
+
if (ledger?.basis !== basis) {
|
|
354
|
+
// Never cache a failure: only a successful read is reused; a failed one is unknown debt now and re-read next observation.
|
|
355
|
+
try {
|
|
356
|
+
ledger = { basis, events: readLedger(journal) };
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
ledger = undefined;
|
|
360
|
+
if (e instanceof JournalRewritten)
|
|
361
|
+
tail.invalidate();
|
|
362
|
+
return { basis, fold: unknownOwed(`complete journal unreadable: ${errorText(e)}`) };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { basis, fold: foldOwedChecks(ledger.events, options.cwd, owedMemo) };
|
|
366
|
+
};
|
|
288
367
|
function observe() {
|
|
289
368
|
const observedAt = now();
|
|
290
369
|
const delayed = lastObservation !== undefined && observedAt - lastObservation > 2 * OBSERVATION_INTERVAL_MS;
|
|
291
|
-
const
|
|
370
|
+
const isReadable = (j) => j.status === "readable" && j.backlogBytes === 0;
|
|
371
|
+
// Debt decides only an ended run's headline; a running or unreadable journal never pays for the fold.
|
|
372
|
+
const owedOf = (j) => isReadable(j) && fold.ended ? owedAuthority(j) : undefined;
|
|
373
|
+
let journal = tail.poll(observedAt);
|
|
374
|
+
let owed = owedOf(journal);
|
|
375
|
+
// I2: the owed read found the tail's consumed prefix rewritten, so its fold (lifecycle) is stale. Refold the
|
|
376
|
+
// whole file now: lifecycle and debt then come from the same bytes. A repeat rewrite is unknown debt.
|
|
377
|
+
if (tail.invalidated) {
|
|
378
|
+
journal = tail.poll(observedAt);
|
|
379
|
+
owed = owedOf(journal);
|
|
380
|
+
}
|
|
292
381
|
const graphReading = graph.read(observedAt);
|
|
293
382
|
const configReading = config.read(observedAt);
|
|
294
383
|
const cacheReading = cache.read(observedAt);
|
|
@@ -301,11 +390,13 @@ export function createLiveStore(options) {
|
|
|
301
390
|
if (metrics.length > STORE_LIMITS.metrics)
|
|
302
391
|
metrics.shift();
|
|
303
392
|
lastObservation = observedAt;
|
|
304
|
-
const readable = journal
|
|
393
|
+
const readable = isReadable(journal);
|
|
305
394
|
return {
|
|
306
395
|
sequence: ++sequence, observedAt, delayed, freshness: errors.length ? "failed" : delayed ? "delayed" : "fresh",
|
|
307
|
-
|
|
308
|
-
|
|
396
|
+
// I1: only this run's own lock naming a provably-dead (ESRCH) holder settles unfinished cells; an alive,
|
|
397
|
+
// EPERM, foreign, unreadable or absent lock is not proof the work stopped.
|
|
398
|
+
operator: fold.snapshot({ graph: graphReading.status === "readable" ? graphReading.value : undefined, sequence, observedAt, readable, graphAvailability: { status: graphReading.status, error: graphReading.error }, daemonDead: lockState === "dead", owed, basis: journalBasis(journal) }),
|
|
399
|
+
owed, journal, graph: graphReading, config: configReading, cache: cacheReading,
|
|
309
400
|
lock: { ...owner, state: lockState, alive, expired: owner.identity ? observedAt - Number(owner.identity.split(":")[3]) / 1e6 > STALE_MS : undefined },
|
|
310
401
|
supervision, errors, actionsEnabled: readable && !errors.length && !delayed,
|
|
311
402
|
viewport, inputSequence, metrics: metrics.slice(),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JournalEvent } from "../../run/journal.js";
|
|
2
|
+
import { type JournalLine, type TailSnapshot } from "./live-store.js";
|
|
3
|
+
/** The board uses the daemon's TTY narration, including its suppression and sanitization. */
|
|
4
|
+
export declare const renderLogLines: (events: readonly JournalEvent[], runId: string, columns: number) => string[];
|
|
5
|
+
export interface LogRow {
|
|
6
|
+
line: number;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LogPage {
|
|
10
|
+
rows: readonly LogRow[];
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
type PageReader = (firstLine: number, count?: number, generation?: number) => readonly JournalLine[];
|
|
14
|
+
interface LogSource {
|
|
15
|
+
journal: TailSnapshot;
|
|
16
|
+
page: PageReader;
|
|
17
|
+
runId: string;
|
|
18
|
+
columns: number;
|
|
19
|
+
}
|
|
20
|
+
/** Demand-page backwards to fill the viewport, including events outside the store's retained tail.
|
|
21
|
+
* Only the requested visible rows survive; suppressed events never consume viewport rows. */
|
|
22
|
+
export declare function readLogPage(source: LogSource, count: number, end?: number): LogPage;
|
|
23
|
+
/** Move by visible narration rows. Undefined means follow the journal tail again. */
|
|
24
|
+
export declare function moveLogEnd(source: LogSource, end: number | undefined, delta: number): number | undefined;
|
|
25
|
+
export declare function LogView({ page }: {
|
|
26
|
+
page: LogPage;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
28
|
+
export {};
|