@litfamily/litopencode 1.0.21 → 1.0.22
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/CHANGELOG.md +8 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/tui/mission-band.d.ts +11 -0
- package/dist/tui/mission-band.js +44 -16
- package/dist/tui/mission-model.d.ts +69 -3
- package/dist/tui/mission-model.js +155 -29
- package/dist/tui/mission-motion.d.ts +10 -1
- package/dist/tui/mission-motion.js +17 -7
- package/dist/tui/mission-source.d.ts +30 -5
- package/dist/tui/mission-source.js +159 -11
- package/dist/tui/mission-text.d.ts +34 -9
- package/dist/tui/mission-text.js +169 -74
- package/dist/tui.js +37 -5
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/reference-Ko-KR.md +70 -19
- package/docs/reference.md +81 -22
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Motion for the mission chip and panel. Everything here is a pure function of a clock value, so the
|
|
2
2
|
// TUI only runs a frame timer while one of these tracks is still moving and stays static otherwise.
|
|
3
|
-
import { missionRowKeys } from "./mission-model.js";
|
|
3
|
+
import { currentStepIndex, missionRowKeys } from "./mission-model.js";
|
|
4
4
|
export const motionTiming = Object.freeze({
|
|
5
5
|
stagger: 60,
|
|
6
6
|
entry: 160,
|
|
@@ -96,6 +96,8 @@ function settleTime(state) {
|
|
|
96
96
|
end = Math.max(end, state.barDelay + motionTiming.fill);
|
|
97
97
|
if (state.celebrate)
|
|
98
98
|
end = Math.max(end, motionTiming.complete);
|
|
99
|
+
if (state.stageFrom !== undefined)
|
|
100
|
+
end = Math.max(end, motionTiming.shimmer);
|
|
99
101
|
return end;
|
|
100
102
|
}
|
|
101
103
|
/**
|
|
@@ -110,8 +112,12 @@ export function planMotion(prev, next, now, state, enabled = true) {
|
|
|
110
112
|
const current = sampleMotion(state, now);
|
|
111
113
|
const appearing = prev === undefined || !prev.active;
|
|
112
114
|
const before = new Set(appearing ? [] : missionRowKeys(prev));
|
|
113
|
-
|
|
115
|
+
// The live step changes with every tool call, so its row appears in place instead of sliding in.
|
|
116
|
+
const entering = missionRowKeys(next).filter((key) => !before.has(key) && key !== "live");
|
|
114
117
|
const goalArrived = !appearing && prev?.goal === undefined && next.goal !== undefined;
|
|
118
|
+
const from = appearing || prev === undefined ? -1 : currentStepIndex(prev);
|
|
119
|
+
const to = currentStepIndex(next);
|
|
120
|
+
const celebrate = !appearing && prev?.complete !== true && next.complete;
|
|
115
121
|
return {
|
|
116
122
|
changeAt: now,
|
|
117
123
|
entering,
|
|
@@ -121,12 +127,14 @@ export function planMotion(prev, next, now, state, enabled = true) {
|
|
|
121
127
|
barDelay: goalArrived || appearing ? motionTiming.fillDelayOnGoal : 0,
|
|
122
128
|
warnFrom: appearing ? nextWarn : current.warn,
|
|
123
129
|
warnTo: nextWarn,
|
|
124
|
-
celebrate
|
|
130
|
+
celebrate,
|
|
131
|
+
...(from >= 0 && to > from && prev?.family === next.family ? { stageFrom: from } : {}),
|
|
132
|
+
...(celebrate && from >= 0 ? { celebrateFrom: from } : {})
|
|
125
133
|
};
|
|
126
134
|
}
|
|
127
135
|
export function sampleMotion(state, now) {
|
|
128
136
|
if (state.changeAt === undefined) {
|
|
129
|
-
return { t: Number.POSITIVE_INFINITY, entering: [], chipEnter: false, barRatio: state.barTo, warn: state.warnTo, moving: false };
|
|
137
|
+
return { t: Number.POSITIVE_INFINITY, entering: [], chipEnter: false, barRatio: state.barTo, warn: state.warnTo, now, moving: false };
|
|
130
138
|
}
|
|
131
139
|
const t = now - state.changeAt;
|
|
132
140
|
const moving = t < settleTime(state);
|
|
@@ -138,10 +146,12 @@ export function sampleMotion(state, now) {
|
|
|
138
146
|
chipEnter: moving && state.chipEnter,
|
|
139
147
|
barRatio: clamp01(state.barFrom + (state.barTo - state.barFrom) * fill),
|
|
140
148
|
warn: clamp01(state.warnFrom + (state.warnTo - state.warnFrom) * fade),
|
|
141
|
-
...(moving && state.celebrate ? { celebrate: t } : {}),
|
|
149
|
+
...(moving && state.celebrate ? { celebrate: t, ...(state.celebrateFrom === undefined ? {} : { celebrateFrom: state.celebrateFrom }) } : {}),
|
|
150
|
+
...(moving && state.stageFrom !== undefined ? { stageFrom: state.stageFrom } : {}),
|
|
151
|
+
now,
|
|
142
152
|
moving
|
|
143
153
|
};
|
|
144
154
|
}
|
|
145
|
-
export function restingFrameFor(view) {
|
|
146
|
-
return { t: Number.POSITIVE_INFINITY, entering: [], chipEnter: false, barRatio: ratio(view), warn: warnTarget(view), moving: false };
|
|
155
|
+
export function restingFrameFor(view, now) {
|
|
156
|
+
return { t: Number.POSITIVE_INFINITY, entering: [], chipEnter: false, barRatio: ratio(view), warn: warnTarget(view), ...(now === undefined ? {} : { now }), moving: false };
|
|
147
157
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MissionInput, type TodoInput } from "./mission-model.ts";
|
|
1
|
+
import { type LiveStep, type MissionInput, type ResearchInput, type TodoInput, type WorkInput } from "./mission-model.ts";
|
|
2
2
|
import { type SkillChip } from "./mission-skills.ts";
|
|
3
3
|
type MessageLike = {
|
|
4
4
|
readonly id: string;
|
|
@@ -47,17 +47,42 @@ export type MissionStateSource = {
|
|
|
47
47
|
};
|
|
48
48
|
part(messageID: string): ReadonlyArray<{
|
|
49
49
|
readonly type?: string;
|
|
50
|
+
readonly tool?: unknown;
|
|
51
|
+
readonly state?: unknown;
|
|
50
52
|
readonly metadata?: unknown;
|
|
51
53
|
}>;
|
|
52
54
|
};
|
|
53
55
|
/** The same root the server plugin uses: the worktree unless it is the filesystem root, else the directory. */
|
|
54
56
|
export declare function missionProjectRoot(paths: MissionStateSource["path"]): string | undefined;
|
|
57
|
+
/** Reads goals.json under `root`, refusing symbolic links and oversized files. */
|
|
58
|
+
export declare function createLedgerReader(): (root: string | undefined) => unknown;
|
|
55
59
|
/**
|
|
56
|
-
* Reads
|
|
57
|
-
*
|
|
60
|
+
* Reads this session's bounded work from work-schema-3.json and the checklist of the plan it names.
|
|
61
|
+
* The plan must be a regular file inside `root`; anything else leaves the checklist out.
|
|
58
62
|
*/
|
|
59
|
-
export declare function
|
|
60
|
-
|
|
63
|
+
export declare function createWorkReader(): (root: string | undefined, sessionID: string) => WorkInput | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* What a litresearch run folder says so far. The skill keeps a root-owned journal.md, an append-only
|
|
66
|
+
* claim-graph.jsonl and a final report.md; the stage is read from them: a report is synthesis, a
|
|
67
|
+
* claim with a gate result is verify, a journal whose last heading names EXPAND is expand, claims are
|
|
68
|
+
* a wave (numbered by the highest `wave` field or `## Wave` heading), and nothing yet is decompose.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readResearchRun(journal: string, claimGraph: string, reported: boolean, touchedAt: number): ResearchInput;
|
|
71
|
+
/**
|
|
72
|
+
* The most recently written run under .litopencode/litgoal/lit-loop/research/, or undefined when
|
|
73
|
+
* there is none. Symbolic links are skipped and only the three known files are read.
|
|
74
|
+
*/
|
|
75
|
+
export declare function readLatestResearchRun(root: string | undefined): ResearchInput | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* The tool the session's latest assistant message is running: its name and `time.start` from the
|
|
78
|
+
* running tool part. The input is never read.
|
|
79
|
+
*/
|
|
80
|
+
export declare function readLiveStep(state: MissionStateSource, messages: ReadonlyArray<MessageLike>): LiveStep | undefined;
|
|
81
|
+
export type MissionRecords = {
|
|
82
|
+
readonly work?: WorkInput;
|
|
83
|
+
readonly research?: ResearchInput;
|
|
84
|
+
};
|
|
85
|
+
export declare function readMissionInput(state: MissionStateSource, sessionID: string, ledger: unknown, now: number, records?: MissionRecords): MissionInput;
|
|
61
86
|
export type CardInput = {
|
|
62
87
|
readonly chips: readonly SkillChip[];
|
|
63
88
|
readonly busy: boolean;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// Gathers the mission input from the OpenCode TUI state and the project's
|
|
1
|
+
// Gathers the mission input from the OpenCode TUI state and the project's own records: the evidence
|
|
2
|
+
// ledger, the bounded work record with its plan checklist, and litresearch run folders. The state
|
|
2
3
|
// shape is the slice of `api.state` the plugin reads, so tests can pass a plain object.
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import { readLitMark } from "./mission-model.js";
|
|
6
|
+
import { planChecklist, readLitMark, readWorkState } from "./mission-model.js";
|
|
6
7
|
import { readSkillUse, skillChips } from "./mission-skills.js";
|
|
7
8
|
const ledgerByteLimit = 1024 * 1024;
|
|
8
9
|
/** The same root the server plugin uses: the worktree unless it is the filesystem root, else the directory. */
|
|
@@ -14,15 +15,12 @@ export function missionProjectRoot(paths) {
|
|
|
14
15
|
return directory === undefined || directory === "" ? undefined : directory;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* A reader for one regular file that refuses symbolic links and files over 1 MiB, cached on mtime and
|
|
19
|
+
* size so a call on every refresh costs one lstat.
|
|
19
20
|
*/
|
|
20
|
-
|
|
21
|
+
function createFileReader(parse) {
|
|
21
22
|
let cache;
|
|
22
|
-
return (
|
|
23
|
-
if (root === undefined)
|
|
24
|
-
return undefined;
|
|
25
|
-
const file = path.join(root, ".litopencode", "litgoal", "lit-loop", "goals.json");
|
|
23
|
+
return (file) => {
|
|
26
24
|
let stat;
|
|
27
25
|
try {
|
|
28
26
|
stat = fs.lstatSync(file);
|
|
@@ -37,7 +35,7 @@ export function createLedgerReader() {
|
|
|
37
35
|
return cache.value;
|
|
38
36
|
let value;
|
|
39
37
|
try {
|
|
40
|
-
value =
|
|
38
|
+
value = parse(fs.readFileSync(file, "utf8"));
|
|
41
39
|
}
|
|
42
40
|
catch {
|
|
43
41
|
value = undefined;
|
|
@@ -46,6 +44,131 @@ export function createLedgerReader() {
|
|
|
46
44
|
return value;
|
|
47
45
|
};
|
|
48
46
|
}
|
|
47
|
+
function litLoopDir(root) {
|
|
48
|
+
return path.join(root, ".litopencode", "litgoal", "lit-loop");
|
|
49
|
+
}
|
|
50
|
+
/** Reads goals.json under `root`, refusing symbolic links and oversized files. */
|
|
51
|
+
export function createLedgerReader() {
|
|
52
|
+
const read = createFileReader((text) => JSON.parse(text));
|
|
53
|
+
return (root) => (root === undefined ? undefined : read(path.join(litLoopDir(root), "goals.json")));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reads this session's bounded work from work-schema-3.json and the checklist of the plan it names.
|
|
57
|
+
* The plan must be a regular file inside `root`; anything else leaves the checklist out.
|
|
58
|
+
*/
|
|
59
|
+
export function createWorkReader() {
|
|
60
|
+
const readState = createFileReader((text) => JSON.parse(text));
|
|
61
|
+
const readPlan = createFileReader(planChecklist);
|
|
62
|
+
return (root, sessionID) => {
|
|
63
|
+
if (root === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
const work = readWorkState(readState(path.join(litLoopDir(root), "work-schema-3.json")), sessionID);
|
|
66
|
+
if (work === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
const inside = work.planPath !== "" && path.isAbsolute(work.planPath) && !path.relative(path.resolve(root), work.planPath).startsWith("..");
|
|
69
|
+
const plan = inside ? readPlan(work.planPath) : undefined;
|
|
70
|
+
return { status: work.status, iterations: work.iterations, ...(plan === undefined ? {} : { plan }) };
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function lstatOrUndefined(file) {
|
|
74
|
+
try {
|
|
75
|
+
return fs.lstatSync(file);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function readSmall(file, stat) {
|
|
82
|
+
if (stat === undefined || !stat.isFile() || stat.size > ledgerByteLimit)
|
|
83
|
+
return "";
|
|
84
|
+
try {
|
|
85
|
+
return fs.readFileSync(file, "utf8");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const verifiedWords = new Set(["verified", "abstained", "unresolved", "refuted"]);
|
|
92
|
+
/**
|
|
93
|
+
* What a litresearch run folder says so far. The skill keeps a root-owned journal.md, an append-only
|
|
94
|
+
* claim-graph.jsonl and a final report.md; the stage is read from them: a report is synthesis, a
|
|
95
|
+
* claim with a gate result is verify, a journal whose last heading names EXPAND is expand, claims are
|
|
96
|
+
* a wave (numbered by the highest `wave` field or `## Wave` heading), and nothing yet is decompose.
|
|
97
|
+
*/
|
|
98
|
+
export function readResearchRun(journal, claimGraph, reported, touchedAt) {
|
|
99
|
+
let claims = 0;
|
|
100
|
+
let verified = 0;
|
|
101
|
+
let gated = false;
|
|
102
|
+
let waveField = 0;
|
|
103
|
+
for (const line of claimGraph.split(/\r?\n/u)) {
|
|
104
|
+
if (line.trim() === "")
|
|
105
|
+
continue;
|
|
106
|
+
let row;
|
|
107
|
+
try {
|
|
108
|
+
row = JSON.parse(line);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (typeof row !== "object" || row === null)
|
|
114
|
+
continue;
|
|
115
|
+
claims += 1;
|
|
116
|
+
const record = row;
|
|
117
|
+
if (typeof record.wave === "number" && Number.isFinite(record.wave))
|
|
118
|
+
waveField = Math.max(waveField, Math.floor(record.wave));
|
|
119
|
+
const verdict = [record.gate, record.status, record.verdict].find((value) => typeof value === "string" && verifiedWords.has(value.toLowerCase()));
|
|
120
|
+
if (verdict !== undefined) {
|
|
121
|
+
gated = true;
|
|
122
|
+
if (String(verdict).toLowerCase() === "verified")
|
|
123
|
+
verified += 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const headings = journal.split(/\r?\n/u).filter((line) => /^#{1,6}\s/u.test(line)).map((line) => line.replace(/^#+\s*/u, "").toLowerCase());
|
|
127
|
+
const waveHeadings = headings.filter((heading) => /^wave\b/u.test(heading)).length;
|
|
128
|
+
const last = headings.at(-1) ?? "";
|
|
129
|
+
const waves = Math.max(waveField, waveHeadings, claims > 0 ? 1 : 0);
|
|
130
|
+
const stage = reported || /^synth/u.test(last)
|
|
131
|
+
? "synthesis"
|
|
132
|
+
: gated || /^verif/u.test(last)
|
|
133
|
+
? "verify"
|
|
134
|
+
: /^expand\b/u.test(last)
|
|
135
|
+
? "expand"
|
|
136
|
+
: waves > 0 ? "wave" : "decompose";
|
|
137
|
+
return { stage, waves, claims, verified, reported, touchedAt };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The most recently written run under .litopencode/litgoal/lit-loop/research/, or undefined when
|
|
141
|
+
* there is none. Symbolic links are skipped and only the three known files are read.
|
|
142
|
+
*/
|
|
143
|
+
export function readLatestResearchRun(root) {
|
|
144
|
+
if (root === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
const base = path.join(litLoopDir(root), "research");
|
|
147
|
+
const baseStat = lstatOrUndefined(base);
|
|
148
|
+
if (baseStat === undefined || !baseStat.isDirectory())
|
|
149
|
+
return undefined;
|
|
150
|
+
let latest;
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (!entry.isDirectory())
|
|
160
|
+
continue;
|
|
161
|
+
const dir = path.join(base, entry.name);
|
|
162
|
+
const files = Object.fromEntries(["journal.md", "claim-graph.jsonl", "report.md"].map((name) => [name, lstatOrUndefined(path.join(dir, name))]));
|
|
163
|
+
const touchedAt = Math.max(lstatOrUndefined(dir)?.mtimeMs ?? 0, ...Object.values(files).map((stat) => stat?.mtimeMs ?? 0));
|
|
164
|
+
if (latest === undefined || touchedAt > latest.touchedAt)
|
|
165
|
+
latest = { dir, touchedAt, files };
|
|
166
|
+
}
|
|
167
|
+
if (latest === undefined)
|
|
168
|
+
return undefined;
|
|
169
|
+
const report = latest.files["report.md"];
|
|
170
|
+
return readResearchRun(readSmall(path.join(latest.dir, "journal.md"), latest.files["journal.md"]), readSmall(path.join(latest.dir, "claim-graph.jsonl"), latest.files["claim-graph.jsonl"]), report !== undefined && report.isFile() && report.size > 0, latest.touchedAt);
|
|
171
|
+
}
|
|
49
172
|
function num(value) {
|
|
50
173
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
51
174
|
}
|
|
@@ -57,7 +180,28 @@ function contextPercent(state, messages) {
|
|
|
57
180
|
const limit = state.provider.find((provider) => provider.id === last.providerID)?.models[String(last.modelID)]?.limit?.context;
|
|
58
181
|
return typeof limit === "number" && limit > 0 ? (tokens / limit) * 100 : null;
|
|
59
182
|
}
|
|
60
|
-
|
|
183
|
+
/**
|
|
184
|
+
* The tool the session's latest assistant message is running: its name and `time.start` from the
|
|
185
|
+
* running tool part. The input is never read.
|
|
186
|
+
*/
|
|
187
|
+
export function readLiveStep(state, messages) {
|
|
188
|
+
const last = [...messages].reverse().find((message) => message.role === "assistant");
|
|
189
|
+
if (last === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
let live;
|
|
192
|
+
for (const part of state.part(last.id)) {
|
|
193
|
+
if (part.type !== "tool" || typeof part.tool !== "string" || typeof part.state !== "object" || part.state === null)
|
|
194
|
+
continue;
|
|
195
|
+
const toolState = part.state;
|
|
196
|
+
const start = toolState.time?.start;
|
|
197
|
+
if (toolState.status !== "running" || typeof start !== "number" || !Number.isFinite(start))
|
|
198
|
+
continue;
|
|
199
|
+
if (live === undefined || start >= live.start)
|
|
200
|
+
live = { tool: part.tool, start };
|
|
201
|
+
}
|
|
202
|
+
return live;
|
|
203
|
+
}
|
|
204
|
+
export function readMissionInput(state, sessionID, ledger, now, records = {}) {
|
|
61
205
|
const messages = state.session.messages(sessionID);
|
|
62
206
|
const marks = [];
|
|
63
207
|
let agent;
|
|
@@ -74,6 +218,7 @@ export function readMissionInput(state, sessionID, ledger, now) {
|
|
|
74
218
|
}
|
|
75
219
|
}
|
|
76
220
|
const status = state.session.status(sessionID);
|
|
221
|
+
const live = readLiveStep(state, messages);
|
|
77
222
|
return {
|
|
78
223
|
marks,
|
|
79
224
|
...(agent === undefined ? {} : { agent }),
|
|
@@ -84,6 +229,9 @@ export function readMissionInput(state, sessionID, ledger, now) {
|
|
|
84
229
|
pendingPermissions: state.session.permission(sessionID).length,
|
|
85
230
|
pendingQuestions: state.session.question(sessionID).length,
|
|
86
231
|
contextPercent: contextPercent(state, messages),
|
|
232
|
+
...(records.work === undefined ? {} : { work: records.work }),
|
|
233
|
+
...(records.research === undefined ? {} : { research: records.research }),
|
|
234
|
+
...(live === undefined ? {} : { live }),
|
|
87
235
|
now
|
|
88
236
|
};
|
|
89
237
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MotionFrame } from "./mission-motion.ts";
|
|
2
|
-
import { type
|
|
2
|
+
import { type MissionView } from "./mission-model.ts";
|
|
3
3
|
export type Segment = {
|
|
4
4
|
readonly text: string;
|
|
5
5
|
readonly fg: string;
|
|
@@ -9,6 +9,7 @@ export type Segment = {
|
|
|
9
9
|
export type Line = readonly Segment[];
|
|
10
10
|
export type MissionPalette = {
|
|
11
11
|
readonly primary: string;
|
|
12
|
+
readonly secondary: string;
|
|
12
13
|
readonly accent: string;
|
|
13
14
|
readonly info: string;
|
|
14
15
|
readonly warning: string;
|
|
@@ -61,19 +62,43 @@ export declare function shimmerAt(frame: MotionFrame): number;
|
|
|
61
62
|
* primary-to-accent gradient. The empty track is a thin muted rule, never a dither.
|
|
62
63
|
*/
|
|
63
64
|
export declare function progressBar(ratio: number, cells: number, palette: MissionPalette, sweep?: number): Segment[];
|
|
64
|
-
|
|
65
|
+
/** The stage colour ramp: the theme's primary through secondary to accent, so every theme keeps its own feel. */
|
|
66
|
+
export declare function stageColor(index: number, count: number, palette: MissionPalette): string;
|
|
67
|
+
/** A light pass over the running stage: once when the map moves forward, and during the completion moment. */
|
|
68
|
+
export declare function stageFlash(frame: MotionFrame): number;
|
|
69
|
+
/**
|
|
70
|
+
* The colour of the connector into stage `index`. It carries the stage ramp once the stage before it is
|
|
71
|
+
* behind the map, and fills in turn, 60 ms apart, when the map has just moved forward.
|
|
72
|
+
*/
|
|
73
|
+
export declare function linkColor(view: MissionView, index: number, frame: MotionFrame, palette: MissionPalette, idle: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* One stage of the breadcrumb. Behind the map a stage is its name in its ramp colour with `✓` (or `–`
|
|
76
|
+
* when skipped); ahead it is a muted name; the running stage is a filled capital pill followed by an
|
|
77
|
+
* optional `bar`-cell bar and the count.
|
|
78
|
+
*/
|
|
79
|
+
export declare function crumb(view: MissionView, index: number, frame: MotionFrame, palette: MissionPalette, bar: number, flash?: number): Segment[];
|
|
80
|
+
/**
|
|
81
|
+
* The live step: the running tool's name and how long it has run, `⠹ bash · 0:48`. The spinner moves
|
|
82
|
+
* with the elapsed second, so it changes only when the time does. Nothing of the tool's input is shown.
|
|
83
|
+
*/
|
|
84
|
+
export declare function liveSegments(view: MissionView, frame: MotionFrame, palette: MissionPalette, room: number): Segment[];
|
|
65
85
|
export type ChipTier = "full" | "medium" | "tight";
|
|
66
86
|
export type ChipGroup = readonly Segment[];
|
|
67
|
-
/**
|
|
68
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The chip's groups for a tier. The full tier is the brand and the breadcrumb around the running stage
|
|
89
|
+
* (the one before, the running one with a seven-cell bar, the one after); medium keeps the running
|
|
90
|
+
* stage and the next with a five-cell bar; tight keeps only the count.
|
|
91
|
+
*/
|
|
92
|
+
export declare function chipGroups(view: MissionView, frame: MotionFrame, palette: MissionPalette, tier: ChipTier): ChipGroup[];
|
|
69
93
|
export declare function groupsWidth(groups: readonly ChipGroup[]): number;
|
|
70
94
|
export declare function fade(segment: Segment, alpha: number, palette: MissionPalette): Segment;
|
|
71
95
|
export declare const chipSparkWidth = 7;
|
|
72
96
|
export declare function chipSparkCells(t: number, palette: MissionPalette): Segment[] | undefined;
|
|
73
97
|
export declare function chipTier(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number): ChipTier;
|
|
74
98
|
/**
|
|
75
|
-
* The
|
|
76
|
-
*
|
|
99
|
+
* The map chip on the right of the prompt's agent/model row: `🔥 LIT plan ✓ › WORK ▰▰▰▱▱▱▱ 3/7 › review`,
|
|
100
|
+
* then the live step when the full chip leaves room for it, shedding labels before values as `budget`
|
|
101
|
+
* shrinks. Idle sessions render nothing.
|
|
77
102
|
*/
|
|
78
103
|
export declare function chipLine(view: MissionView, frame: MotionFrame, palette: MissionPalette, budget: number): Line;
|
|
79
104
|
/** The header status: a glyph and words, or the COMPLETE badge that pops in and settles to green text. */
|
|
@@ -91,9 +116,9 @@ export type MissionPanel = {
|
|
|
91
116
|
};
|
|
92
117
|
/**
|
|
93
118
|
* The sidebar mission panel: a rounded box whose border carries the state (accent while working,
|
|
94
|
-
* warning on a warning, success when complete), a tiny LIT header, the goal title, the
|
|
95
|
-
* the bar, the
|
|
96
|
-
* `panelInnerWidth` cells.
|
|
119
|
+
* warning on a warning, success when complete), a tiny LIT header, the goal title, the vertical stage
|
|
120
|
+
* map with its connectors, the bar, the detail line, the live step, criteria, warnings and the
|
|
121
|
+
* evidence timeline. Rows are at most `panelInnerWidth` cells.
|
|
97
122
|
*/
|
|
98
123
|
export declare function missionPanel(view: MissionView, frame: MotionFrame, palette: MissionPalette): MissionPanel | undefined;
|
|
99
124
|
export type LastMission = {
|