@litfamily/litopencode 1.0.36 β 1.0.37
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 +6 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/tui/mission-skills.js +3 -1
- package/dist/tui/mission-source-v2.d.ts +23 -0
- package/dist/tui/mission-source-v2.js +29 -4
- package/dist/tui/mission-source.d.ts +1 -0
- package/dist/tui/mission-source.js +3 -2
- package/dist/v2/tui-notices.d.ts +7 -0
- package/dist/v2/tui-notices.js +45 -0
- package/dist/v2/tui.d.ts +37 -1
- package/dist/v2/tui.js +309 -31
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/reference-Ko-KR.md +13 -5
- package/docs/reference.md +17 -7
- package/package.json +1 -1
- package/skills/lit-typographic-motion/engine/cover/render.mjs +1 -1
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +3 -2
- package/skills/lit-typographic-motion/engine/stage/gate.mjs +1 -0
- package/skills/lit-typographic-motion/engine/stage/render.mjs +1 -0
- package/skills/lit-typographic-motion/engine/stage/scan.mjs +109 -5
- package/skills/lit-typographic-motion/engine/stage/stage-kit.js +39 -1
- package/skills/lit-typographic-motion/references/cover.md +2 -1
- package/skills/lit-typographic-motion/references/runtime.md +1 -1
- package/skills/lit-typographic-motion/references/stage.md +48 -2
- package/skills/managed-skill-manifest.json +9 -9
package/dist/v2/tui.js
CHANGED
|
@@ -1,26 +1,52 @@
|
|
|
1
|
-
// LitOpenCode's OpenCode 2 TUI plugin: the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// LitOpenCode's OpenCode 2 TUI plugin: the same HUD OpenCode 1 draws, placed in the slots OpenCode 2
|
|
2
|
+
// publishes (@opencode/plugin SlotMap). The status card sits after the prompt footer, the stage chip and
|
|
3
|
+
// the ignition line (`π₯ LIT IGNITED Β· <discipline>`, the OpenCode 2 home of the OpenCode 1 completion
|
|
4
|
+
// mark) share prompt.footer.status, the home line is in home.footer and the mission panel in
|
|
5
|
+
// sidebar.content. Toasts cover the gate events, the stage moving, discipline switches, lanes, the
|
|
6
|
+
// ignition and the restart notice, with an opt-in finish alert. OpenCode 2 loads the plugin on its own
|
|
7
|
+
// for an active server plugin. The server cannot raise toasts there, so the ignition toast is inferred
|
|
8
|
+
// from the lit mark the server writes into the user message metadata. It only reads state. OpenTUI and
|
|
9
|
+
// Solid come from the host at run time; tests pass a stand-in runtime.
|
|
7
10
|
import { activationDiscipline } from "../activation-probe.js";
|
|
11
|
+
import { readPackageMetadata } from "../cli/json.js";
|
|
8
12
|
import { micro, supportsMarkGlyphs } from "../lit-mark.js";
|
|
9
|
-
import {
|
|
13
|
+
import { readInstalledVersions, restartVersion } from "../restart-notice.js";
|
|
14
|
+
import { buildMissionView, lastMission, leadingNotice, missionNotices } from "../tui/mission-model.js";
|
|
10
15
|
import { idleMotion, motionTiming, planMotion, sampleMotion } from "../tui/mission-motion.js";
|
|
11
16
|
import { latestLitMarkV2, missionStateFromV2 } from "../tui/mission-source-v2.js";
|
|
12
|
-
import { createLedgerReader, createWorkReader, missionProjectRoot, readLatestResearchRun, readMissionInput } from "../tui/mission-source.js";
|
|
13
|
-
import {
|
|
17
|
+
import { adhdSuggestDue, createLedgerReader, createWorkReader, earlierWorkWindowMs, hasEarlierWork, messageTimes, missionProjectRoot, readCardInput, readLatestResearchRun, readMissionInput, readThreadSources, wayBackDue } from "../tui/mission-source.js";
|
|
18
|
+
import { chipShimmer, idleChipMotion, planChipMotion } from "../tui/mission-skills.js";
|
|
19
|
+
import { bandCard, readBandOptions, readNotifyOptions, statusCard } from "../tui/mission-band.js";
|
|
20
|
+
import { activeSkillLook, turnGlow, waitingOnYou } from "../tui/mission-looks.js";
|
|
21
|
+
import { rainbowActive, rainbowAllowed, rainbowAt, rainbowShows, rainbowTickMs } from "../tui/mission-rainbow.js";
|
|
22
|
+
import { threadLineCount, threadsOf } from "../tui/mission-threads.js";
|
|
23
|
+
import { chipLine, cellWidth, homeLine, lineWidth, missionPanel, paletteFromTheme, panelWidth } from "../tui/mission-text.js";
|
|
24
|
+
import { v2StageNotices } from "./tui-notices.js";
|
|
14
25
|
const refreshDelayMs = 80;
|
|
15
26
|
const ledgerPollMs = 2000;
|
|
16
27
|
const toastMs = 4000;
|
|
17
28
|
const ignitionToastMs = 6000;
|
|
29
|
+
const liveTickMs = 1000;
|
|
30
|
+
const restartCheckMs = 10 * 60_000;
|
|
18
31
|
const trackedSessionLimit = 4;
|
|
32
|
+
const homeLineWidth = 69;
|
|
19
33
|
// A lit mark older than the moment a session is first drawn is history being loaded, not a new turn.
|
|
20
34
|
const historySlackMs = 2000;
|
|
35
|
+
const defaultWidth = 120;
|
|
36
|
+
// What the footer row holds besides our line: the host's spinner and hint on the left, its token, cost,
|
|
37
|
+
// goal and `ctrl+p commands` figures on the right (measured with a turn running, when they are widest).
|
|
38
|
+
const hostFooterCells = 68;
|
|
39
|
+
const sidebarCells = 42;
|
|
40
|
+
const sidebarMinWidth = 120;
|
|
21
41
|
async function hostRuntime() {
|
|
22
|
-
const [jsxRuntime, solid] = await Promise.all([import("@opentui/solid/jsx-runtime"), import("solid-js")]);
|
|
23
|
-
return {
|
|
42
|
+
const [jsxRuntime, solid, opentui] = await Promise.all([import("@opentui/solid/jsx-runtime"), import("solid-js"), import("@opentui/solid")]);
|
|
43
|
+
return {
|
|
44
|
+
jsx: jsxRuntime.jsx,
|
|
45
|
+
createSignal: solid.createSignal,
|
|
46
|
+
createEffect: solid.createEffect,
|
|
47
|
+
createRoot: solid.createRoot,
|
|
48
|
+
useTerminalDimensions: opentui.useTerminalDimensions
|
|
49
|
+
};
|
|
24
50
|
}
|
|
25
51
|
/** OpenCode 2 theme tokens under the names the mission palette reads. */
|
|
26
52
|
export function paletteFromV2Theme(theme, mode) {
|
|
@@ -42,23 +68,33 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
42
68
|
const ui = runtime ?? await hostRuntime();
|
|
43
69
|
const [settings] = context.storage.store("settings", { initial: { motion: true } });
|
|
44
70
|
const motionEnabled = settings.motion !== false;
|
|
71
|
+
const band = readBandOptions(context.options);
|
|
72
|
+
const notify = readNotifyOptions(context.options);
|
|
45
73
|
const directory = context.location?.directory ?? context.data.location.default().directory;
|
|
46
74
|
const root = missionProjectRoot({ directory });
|
|
47
75
|
const readLedger = createLedgerReader();
|
|
48
76
|
const readWork = createWorkReader();
|
|
77
|
+
const colorAllowed = rainbowAllowed(motionEnabled, process.env.NO_COLOR);
|
|
49
78
|
const sessions = new Map();
|
|
50
79
|
const ignitions = new Map();
|
|
51
80
|
const trackedAt = new Map();
|
|
52
81
|
const tracked = [];
|
|
82
|
+
const suggested = new Set();
|
|
83
|
+
const suggestedDays = new Map();
|
|
53
84
|
// Each session's store watcher lives in its own root, so it outlasts the slot render that started it
|
|
54
85
|
// and ends only on eviction or cleanup. After cleanup nothing may arm a timer again.
|
|
55
86
|
const watchers = new Map();
|
|
56
87
|
let disposed = false;
|
|
57
88
|
const [revision, setRevision] = ui.createSignal(0);
|
|
58
89
|
const [clock, setClock] = ui.createSignal(Date.now());
|
|
90
|
+
// The version this start loaded; an install newer than it shows `restart to <v>` and one toast.
|
|
91
|
+
const [restartTo, setRestartTo] = ui.createSignal(undefined);
|
|
59
92
|
let frameTimer;
|
|
60
93
|
let refreshTimer;
|
|
61
94
|
let pollTimer;
|
|
95
|
+
let liveTimer;
|
|
96
|
+
let bandTimer;
|
|
97
|
+
let restartTimer;
|
|
62
98
|
const palette = () => paletteFromV2Theme(context.theme, context.themeMode);
|
|
63
99
|
function span(segment) {
|
|
64
100
|
return ui.jsx("span", {
|
|
@@ -69,17 +105,43 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
69
105
|
function line(segments) {
|
|
70
106
|
return ui.jsx("text", { wrapMode: "none", children: segments.length === 0 ? " " : segments.map(span) });
|
|
71
107
|
}
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
function state() {
|
|
109
|
+
return missionStateFromV2(context.data, directory);
|
|
110
|
+
}
|
|
111
|
+
function parentOf(sessionID) {
|
|
112
|
+
const parent = context.data.session.get?.(sessionID)?.parentID;
|
|
113
|
+
return typeof parent === "string" && parent !== "" ? parent : undefined;
|
|
114
|
+
}
|
|
115
|
+
function width() {
|
|
116
|
+
return ui.useTerminalDimensions?.()().width ?? defaultWidth;
|
|
117
|
+
}
|
|
118
|
+
function rainbowOn(entry, now) {
|
|
119
|
+
return colorAllowed && rainbowShows(rainbowActive(entry.view, entry.card.chips, entry.card.busy), entry.rainbowSeen, entry.lastPromptAt, now);
|
|
120
|
+
}
|
|
121
|
+
function bandStepping(entry) {
|
|
122
|
+
if (!colorAllowed)
|
|
123
|
+
return false;
|
|
124
|
+
if (entry.waiting || rainbowOn(entry, Date.now()))
|
|
125
|
+
return true;
|
|
126
|
+
return entry.card.threads?.some((row) => row.status === "working" && row.done === undefined) === true;
|
|
127
|
+
}
|
|
128
|
+
function syncBandTimer() {
|
|
129
|
+
if (disposed)
|
|
130
|
+
return;
|
|
131
|
+
const needed = [...sessions.values()].some(bandStepping);
|
|
132
|
+
if (needed && bandTimer === undefined)
|
|
133
|
+
bandTimer = setInterval(() => {
|
|
134
|
+
setClock(Date.now());
|
|
135
|
+
syncBandTimer();
|
|
136
|
+
}, rainbowTickMs);
|
|
137
|
+
if (!needed && bandTimer !== undefined) {
|
|
138
|
+
clearInterval(bandTimer);
|
|
139
|
+
bandTimer = undefined;
|
|
140
|
+
}
|
|
79
141
|
}
|
|
80
142
|
function anyMoving(now) {
|
|
81
143
|
for (const entry of sessions.values())
|
|
82
|
-
if (sampleMotion(entry.motion, now).moving)
|
|
144
|
+
if (sampleMotion(entry.motion, now).moving || chipShimmer(entry.chipMotion, now) !== undefined)
|
|
83
145
|
return true;
|
|
84
146
|
return false;
|
|
85
147
|
}
|
|
@@ -95,11 +157,103 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
95
157
|
}
|
|
96
158
|
}, motionTiming.frameMs);
|
|
97
159
|
}
|
|
160
|
+
// The live step's elapsed time is the one steady change: a tick a second while a tool runs.
|
|
161
|
+
function syncLiveTick() {
|
|
162
|
+
if (disposed)
|
|
163
|
+
return;
|
|
164
|
+
const running = [...sessions.values()].some((entry) => entry.view.live !== undefined);
|
|
165
|
+
if (running && liveTimer === undefined)
|
|
166
|
+
liveTimer = setInterval(() => setClock(Date.now()), liveTickMs);
|
|
167
|
+
if (!running && liveTimer !== undefined) {
|
|
168
|
+
clearInterval(liveTimer);
|
|
169
|
+
liveTimer = undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// The card keeps the tallest lane count it reached while lanes are live or a turn runs, so the prompt does not jump.
|
|
173
|
+
function holdRows(held, rows, busy) {
|
|
174
|
+
if (rows > 0)
|
|
175
|
+
return Math.max(held, rows);
|
|
176
|
+
return busy ? held : 0;
|
|
177
|
+
}
|
|
98
178
|
function ignitionToast(sessionID, name) {
|
|
99
179
|
const label = `${ignitionLine(name)} π₯`;
|
|
100
180
|
const message = supportsMarkGlyphs() ? [...micro.map((row) => row.replace(/ +$/u, "")), label].join("\n") : label;
|
|
101
181
|
context.ui.toast.show({ title: "π₯ LIT IGNITED", message, variant: "warning", duration: ignitionToastMs, sessionID });
|
|
102
182
|
}
|
|
183
|
+
// The finish alert is off unless the plugin options opt in; OpenCode's own attention settings still
|
|
184
|
+
// decide whether a desktop notification or a sound actually plays.
|
|
185
|
+
function alertFinish(notice) {
|
|
186
|
+
if (!notify.enabled || context.attention === undefined)
|
|
187
|
+
return;
|
|
188
|
+
void context.attention
|
|
189
|
+
.notify({ title: "π₯ LIT complete", message: notice.message, notification: notify.desktop, sound: notify.sound ? { name: "done" } : false })
|
|
190
|
+
.catch(() => undefined);
|
|
191
|
+
}
|
|
192
|
+
async function checkRestart() {
|
|
193
|
+
const loaded = await readPackageMetadata().catch(() => undefined);
|
|
194
|
+
if (loaded === undefined || disposed)
|
|
195
|
+
return;
|
|
196
|
+
const target = restartVersion(loaded.version, await readInstalledVersions(loaded.packageRoot).catch(() => []));
|
|
197
|
+
if (disposed || target === undefined || target === restartTo())
|
|
198
|
+
return;
|
|
199
|
+
if (restartTo() === undefined) {
|
|
200
|
+
context.ui.toast.show({ variant: "info", title: "π₯ LIT", message: `LitOpenCode ${target} is installed. Restart OpenCode to load it.`, duration: toastMs });
|
|
201
|
+
}
|
|
202
|
+
setRestartTo(target);
|
|
203
|
+
}
|
|
204
|
+
// One toast per session, for a fresh root session in a workspace that has earlier work to pick up.
|
|
205
|
+
function suggestAdhd(sessionID, source) {
|
|
206
|
+
if (suggested.has(sessionID) || parentOf(sessionID) !== undefined)
|
|
207
|
+
return;
|
|
208
|
+
suggested.add(sessionID);
|
|
209
|
+
const created = context.data.session.get?.(sessionID)?.time?.created;
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
if (source.session.messages(sessionID).length > 0 || typeof created !== "number" || now - created > 2 * 60_000)
|
|
212
|
+
return;
|
|
213
|
+
const projectRoot = missionProjectRoot(source.path);
|
|
214
|
+
if (projectRoot === undefined || !hasEarlierWork(projectRoot, sessionID, now, earlierWorkWindowMs) || !adhdSuggestDue(suggestedDays, projectRoot, now))
|
|
215
|
+
return;
|
|
216
|
+
context.ui.toast.show({ variant: "info", title: "π₯ LIT", message: "Earlier work here. Say lit adhd to pick up where you left off.", duration: toastMs });
|
|
217
|
+
}
|
|
218
|
+
// One session's view, card and the notices from its previous entry. Pure reads; nothing is armed here.
|
|
219
|
+
function build(sessionID, entry, now) {
|
|
220
|
+
const source = state();
|
|
221
|
+
const work = readWork(root, sessionID);
|
|
222
|
+
const research = readLatestResearchRun(root);
|
|
223
|
+
const view = buildMissionView(readMissionInput(source, sessionID, readLedger(root), now, {
|
|
224
|
+
...(work === undefined ? {} : { work }),
|
|
225
|
+
...(research === undefined ? {} : { research })
|
|
226
|
+
}));
|
|
227
|
+
const threads = threadsOf(readThreadSources(source, sessionID, root, now), now);
|
|
228
|
+
const times = messageTimes(source, sessionID);
|
|
229
|
+
const card = {
|
|
230
|
+
...readCardInput(source, sessionID),
|
|
231
|
+
...(threads === undefined ? {} : { threads }),
|
|
232
|
+
...(wayBackDue(times.lastAt, now, hasEarlierWork(root, sessionID, now, earlierWorkWindowMs)) ? { earlier: true } : {})
|
|
233
|
+
};
|
|
234
|
+
const viewChanged = entry === undefined || JSON.stringify(entry.view) !== JSON.stringify(view);
|
|
235
|
+
const rainbowSeen = rainbowActive(view, card.chips, card.busy) ? now : entry?.rainbowSeen;
|
|
236
|
+
const waiting = waitingOnYou(view, card.busy, { asked: card.asked === true, userSpokeLast: card.userSpokeLast === true, blockedReply: card.blockedReply === true, boundary: work?.boundary === true });
|
|
237
|
+
const changed = entry === undefined || viewChanged || entry.lastPromptAt !== times.lastPromptAt || JSON.stringify(entry.card) !== JSON.stringify(card) || entry.rainbowSeen !== rainbowSeen || entry.waiting !== waiting;
|
|
238
|
+
const lanesChanged = entry !== undefined && JSON.stringify(entry.card.threads) !== JSON.stringify(card.threads);
|
|
239
|
+
const notices = entry !== undefined && (viewChanged || lanesChanged)
|
|
240
|
+
? [...missionNotices(entry.view, view), ...v2StageNotices(entry.view, view, entry.card.threads ?? [], card.threads ?? [])]
|
|
241
|
+
: [];
|
|
242
|
+
return {
|
|
243
|
+
changed,
|
|
244
|
+
notices,
|
|
245
|
+
next: {
|
|
246
|
+
view,
|
|
247
|
+
card,
|
|
248
|
+
motion: entry !== undefined && viewChanged ? planMotion(entry.view, view, now, entry.motion, motionEnabled) : (entry?.motion ?? idleMotion),
|
|
249
|
+
chipMotion: planChipMotion(entry?.card.chips, card.chips, now, entry?.chipMotion ?? idleChipMotion, motionEnabled),
|
|
250
|
+
reserve: holdRows(entry?.reserve ?? 0, threadLineCount(threads), card.busy),
|
|
251
|
+
...(rainbowSeen === undefined ? {} : { rainbowSeen }),
|
|
252
|
+
...(times.lastPromptAt === undefined ? {} : { lastPromptAt: times.lastPromptAt }),
|
|
253
|
+
waiting
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
103
257
|
function refresh(sessionID) {
|
|
104
258
|
if (disposed)
|
|
105
259
|
return;
|
|
@@ -110,19 +264,23 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
110
264
|
if (mark !== undefined && mark.at >= (trackedAt.get(sessionID) ?? 0) - historySlackMs)
|
|
111
265
|
ignitionToast(sessionID, mark.name);
|
|
112
266
|
}
|
|
113
|
-
const next = viewFor(sessionID, now);
|
|
114
267
|
const entry = sessions.get(sessionID);
|
|
115
|
-
|
|
268
|
+
const { next, changed, notices } = build(sessionID, entry, now);
|
|
269
|
+
if (!changed)
|
|
116
270
|
return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
271
|
+
const notice = leadingNotice(notices);
|
|
272
|
+
if (notice !== undefined)
|
|
273
|
+
context.ui.toast.show({ variant: notice.variant, title: notice.title, message: notice.message, duration: toastMs, sessionID });
|
|
274
|
+
const finish = notices.find((candidate) => candidate.celebrate === true);
|
|
275
|
+
if (finish !== undefined)
|
|
276
|
+
alertFinish(finish);
|
|
277
|
+
sessions.set(sessionID, next);
|
|
123
278
|
setClock(now);
|
|
124
279
|
setRevision(revision() + 1);
|
|
125
280
|
ensureFrames();
|
|
281
|
+
syncBandTimer();
|
|
282
|
+
syncLiveTick();
|
|
283
|
+
syncPoll();
|
|
126
284
|
}
|
|
127
285
|
function syncPoll() {
|
|
128
286
|
if (disposed)
|
|
@@ -164,8 +322,13 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
164
322
|
const now = Date.now();
|
|
165
323
|
trackedAt.set(sessionID, now);
|
|
166
324
|
ignitions.set(sessionID, latestLitMarkV2(context.data, sessionID)?.messageID);
|
|
167
|
-
|
|
168
|
-
|
|
325
|
+
suggestAdhd(sessionID, state());
|
|
326
|
+
sessions.set(sessionID, build(sessionID, undefined, now).next);
|
|
327
|
+
queueMicrotask(() => {
|
|
328
|
+
syncPoll();
|
|
329
|
+
syncLiveTick();
|
|
330
|
+
syncBandTimer();
|
|
331
|
+
});
|
|
169
332
|
// Opening a session loads its messages over HTTP, which raises no event. Reading the host's reactive
|
|
170
333
|
// store here re-runs this effect as they arrive, so a resumed session lights up too.
|
|
171
334
|
const dispose = ui.createRoot((release) => {
|
|
@@ -184,6 +347,60 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
184
347
|
if (type.startsWith("session.") || type.startsWith("permission.") || type.startsWith("form."))
|
|
185
348
|
scheduleRefresh();
|
|
186
349
|
});
|
|
350
|
+
// Reads that only a rendered slot needs. `view` follows the reactive revision and the clock.
|
|
351
|
+
function snapshot(sessionID) {
|
|
352
|
+
revision();
|
|
353
|
+
const entry = sessions.get(sessionID);
|
|
354
|
+
const now = clock();
|
|
355
|
+
return entry === undefined ? undefined : { ...entry, frame: sampleMotion(entry.motion, now), now };
|
|
356
|
+
}
|
|
357
|
+
function statusCardRows(sessionID, inLane, columns) {
|
|
358
|
+
const current = snapshot(sessionID);
|
|
359
|
+
if (current === undefined)
|
|
360
|
+
return undefined;
|
|
361
|
+
const colors = palette();
|
|
362
|
+
const cells = columns - 2;
|
|
363
|
+
const last = current.view.active ? undefined : lastMission(readLedger(root));
|
|
364
|
+
const shimmer = chipShimmer(current.chipMotion, current.now);
|
|
365
|
+
const lines = statusCard({
|
|
366
|
+
view: current.view,
|
|
367
|
+
frame: current.frame,
|
|
368
|
+
chips: current.card.chips,
|
|
369
|
+
busy: current.card.busy,
|
|
370
|
+
motion: motionEnabled,
|
|
371
|
+
reserve: current.reserve,
|
|
372
|
+
...(current.card.threads === undefined ? {} : { threads: current.card.threads }),
|
|
373
|
+
...(current.card.earlier === true ? { earlier: true } : {}),
|
|
374
|
+
...(current.waiting ? { turn: { glow: turnGlow(current.now, colorAllowed) } } : {}),
|
|
375
|
+
...(rainbowOn(current, current.now) ? { rainbow: rainbowAt(current.now) } : {}),
|
|
376
|
+
...(current.card.lanes !== undefined && activeSkillLook(current.view, current.card.chips, current.card.busy) === "review" ? { lanes: current.card.lanes } : {}),
|
|
377
|
+
...(shimmer === undefined ? {} : { shimmer }),
|
|
378
|
+
figures: {
|
|
379
|
+
contextPercent: current.card.contextPercent,
|
|
380
|
+
...(current.card.branch === undefined ? {} : { branch: current.card.branch }),
|
|
381
|
+
inLane,
|
|
382
|
+
...(restartTo() === undefined ? {} : { restartTo: restartTo() })
|
|
383
|
+
},
|
|
384
|
+
...(last === undefined ? {} : { last })
|
|
385
|
+
}, colors, cells);
|
|
386
|
+
const rows = lines.map(line);
|
|
387
|
+
const detail = band.card ? bandCard(current.view, current.frame, colors, cells) : undefined;
|
|
388
|
+
if (detail !== undefined) {
|
|
389
|
+
rows.push(ui.jsx("box", {
|
|
390
|
+
border: true,
|
|
391
|
+
borderStyle: "rounded",
|
|
392
|
+
borderColor: detail.border,
|
|
393
|
+
title: detail.title,
|
|
394
|
+
titleColor: colors.text,
|
|
395
|
+
paddingLeft: 1,
|
|
396
|
+
paddingRight: 1,
|
|
397
|
+
flexShrink: 0,
|
|
398
|
+
flexDirection: "column",
|
|
399
|
+
children: detail.rows.map(line)
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
return rows;
|
|
403
|
+
}
|
|
187
404
|
const releases = [
|
|
188
405
|
context.ui.slot({
|
|
189
406
|
append: "prompt.footer.status",
|
|
@@ -192,14 +409,25 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
192
409
|
if (sessionID === undefined)
|
|
193
410
|
return null;
|
|
194
411
|
track(sessionID);
|
|
412
|
+
const columns = width;
|
|
195
413
|
return () => {
|
|
196
414
|
revision();
|
|
197
415
|
const mark = latestLitMarkV2(context.data, sessionID);
|
|
198
416
|
if (mark === undefined)
|
|
199
417
|
return null;
|
|
418
|
+
const ignition = ignitionLine(mark.name);
|
|
419
|
+
const current = snapshot(sessionID);
|
|
420
|
+
// The sidebar setting is not readable here, so a wide terminal is taken to have it open.
|
|
421
|
+
const sidebar = parentOf(sessionID) === undefined && columns() > sidebarMinWidth ? sidebarCells : 0;
|
|
422
|
+
const budget = columns() - sidebar - hostFooterCells - cellWidth(ignition);
|
|
423
|
+
const drawn = current === undefined || !current.view.active ? [] : chipLine(current.view, current.frame, palette(), budget);
|
|
424
|
+
const chip = lineWidth(drawn) > budget ? [] : drawn;
|
|
200
425
|
return ui.jsx("box", {
|
|
201
|
-
flexShrink:
|
|
202
|
-
|
|
426
|
+
flexShrink: 1,
|
|
427
|
+
minWidth: 0,
|
|
428
|
+
overflow: "hidden",
|
|
429
|
+
flexDirection: "row",
|
|
430
|
+
children: [line([{ text: ignition, fg: palette().primary, bold: true }]), ...(chip.length === 0 ? [] : [line([{ text: " ", fg: palette().muted }]), line(chip)])]
|
|
203
431
|
});
|
|
204
432
|
};
|
|
205
433
|
}
|
|
@@ -235,8 +463,52 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
235
463
|
});
|
|
236
464
|
};
|
|
237
465
|
}
|
|
466
|
+
}),
|
|
467
|
+
context.ui.slot({
|
|
468
|
+
append: "home.footer",
|
|
469
|
+
render() {
|
|
470
|
+
const columns = width;
|
|
471
|
+
return () => {
|
|
472
|
+
revision();
|
|
473
|
+
const segments = homeLine(lastMission(readLedger(root)), palette(), Math.min(homeLineWidth, columns() - 10));
|
|
474
|
+
return segments.length === 0 ? null : ui.jsx("box", { flexDirection: "row", justifyContent: "center", flexShrink: 0, children: line(segments) });
|
|
475
|
+
};
|
|
476
|
+
}
|
|
238
477
|
})
|
|
239
478
|
];
|
|
479
|
+
// The card is drawn once for the whole app, under the session and its sidebar, on every session, lit or
|
|
480
|
+
// not, and keeps its rows when a turn starts. A subagent session shows its parent's card as a lane. The
|
|
481
|
+
// home route has no session, so it shows no card.
|
|
482
|
+
if (band.enabled) {
|
|
483
|
+
releases.push(context.ui.slot({
|
|
484
|
+
append: "app",
|
|
485
|
+
render() {
|
|
486
|
+
const columns = width;
|
|
487
|
+
return () => {
|
|
488
|
+
const route = context.ui.router?.current();
|
|
489
|
+
const sessionID = route?.type === "session" ? route.sessionID : undefined;
|
|
490
|
+
if (sessionID === undefined)
|
|
491
|
+
return null;
|
|
492
|
+
const parent = parentOf(sessionID);
|
|
493
|
+
const target = parent ?? sessionID;
|
|
494
|
+
track(target);
|
|
495
|
+
const rows = statusCardRows(target, parent !== undefined, columns());
|
|
496
|
+
if (rows === undefined)
|
|
497
|
+
return null;
|
|
498
|
+
return ui.jsx("box", {
|
|
499
|
+
flexDirection: "column",
|
|
500
|
+
flexShrink: 0,
|
|
501
|
+
paddingLeft: 1,
|
|
502
|
+
paddingRight: 1,
|
|
503
|
+
backgroundColor: palette().panel ?? "transparent",
|
|
504
|
+
children: rows
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
void checkRestart();
|
|
511
|
+
restartTimer = setInterval(() => void checkRestart(), restartCheckMs);
|
|
240
512
|
return async () => {
|
|
241
513
|
disposed = true;
|
|
242
514
|
for (const dispose of watchers.values())
|
|
@@ -246,12 +518,18 @@ export async function setupLitOpenCodeTui(context, runtime) {
|
|
|
246
518
|
unlisten();
|
|
247
519
|
for (const release of releases)
|
|
248
520
|
release();
|
|
521
|
+
if (restartTimer !== undefined)
|
|
522
|
+
clearInterval(restartTimer);
|
|
249
523
|
if (frameTimer !== undefined)
|
|
250
524
|
clearInterval(frameTimer);
|
|
251
525
|
if (refreshTimer !== undefined)
|
|
252
526
|
clearTimeout(refreshTimer);
|
|
253
527
|
if (pollTimer !== undefined)
|
|
254
528
|
clearInterval(pollTimer);
|
|
529
|
+
if (liveTimer !== undefined)
|
|
530
|
+
clearInterval(liveTimer);
|
|
531
|
+
if (bandTimer !== undefined)
|
|
532
|
+
clearInterval(bandTimer);
|
|
255
533
|
sessions.clear();
|
|
256
534
|
ignitions.clear();
|
|
257
535
|
trackedAt.clear();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.37"><title>release: 1.0.37</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.37</text></g></svg>
|
package/docs/reference-Ko-KR.md
CHANGED
|
@@ -380,10 +380,18 @@ Code Mode λ°, λͺ¨λΈμ λꡬ λͺ©λ‘μ λ°λ‘ μμΌλ―λ‘ `lit-plan`λ κ³
|
|
|
380
380
|
lit ν둬ννΈμ΄λ©΄ ν둬ννΈ μλ μ€(`prompt.footer.status`)μ `π₯ LIT IGNITED Β· <discipline>`μ΄ λμ΅λλ€.
|
|
381
381
|
OpenCode 1μμλ νμκ° μμ μ리μ κ·Έλλ‘ μμ΅λλ€.
|
|
382
382
|
- *ν μ€νΈ.* OpenCode 2μμλ μλ²κ° ν μ€νΈλ₯Ό λμΈ μ μμ΅λλ€. μλ²κ° μ¬μ©μ λ©μμ§ λ©νλ°μ΄ν°μ λ¨κΈ΄ lit νμλ₯Ό
|
|
383
|
-
TUIκ° λ³΄κ³ μ ν ν μ€νΈλ₯Ό μ§μ
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
383
|
+
TUIκ° λ³΄κ³ μ ν ν μ€νΈλ₯Ό μ§μ λμ°κ³ , OpenCode 1κ³Ό κ°μ κ΄λ¬Έ ν μ€νΈ(κ³ν μ μ₯, λͺ©ν μ€μ , κΈ°μ€ ν΅κ³Ό, λ§ν, μλ£)μ
|
|
384
|
+
λν΄ OpenCode 2μλ§ μλ μΈ κ°μ§λ₯Ό λμλλ€. μμ
λ¨κ³κ° λμ΄κ° λ(`π₯ Stage`, `work 3/7`), κ·μ¨μ΄ λ°λ λ
|
|
385
|
+
(`π₯ lit-loop β start-work`), λ μΈμ΄ μμλκ±°λ λλ λμ
λλ€. νκΊΌλ²μ μΌμ΄λ μΌμ `+N more`κ° λΆμ ν μ€νΈ
|
|
386
|
+
νλλ‘ ν©μΉ©λλ€. μ¬μμ μλ΄, `lit adhd` μ μ, μΌ λλ©΄ λμ€λ μλ£ μλ¦Όμ OpenCode 1κ³Ό κ°μ΅λλ€. μλ²λ§ μλ
|
|
387
|
+
μλ μΈμμΈκ³ μλ¦Όκ³Ό μ€ν¬ ννΈλ κ·Έκ³³μ λμ€μ§ μμ΅λλ€.
|
|
388
|
+
- *TUI.* HUDλ OpenCode 2κ° κ³΅κ°ν μ¬λ‘―μ λμ
λλ€. μν μΉ΄λλ `app` μ¬λ‘―μμ μΈμ
κ³Ό μ¬μ΄λλ° μλμ κ·Έλ €μ§κ³ ,
|
|
389
|
+
μ¬λ μΌνλ λ€ μ€μ μ μ§ν©λλ€. λ¨κ³ μΉ©μ μμ΄μ νΈΒ·λͺ¨λΈ μ€ μ€λ₯Έμͺ½μ μ¬λ‘―μ΄ μμ΄μ μ ν μ€κ³Ό ν¨κ»
|
|
390
|
+
`prompt.footer.status`μ μκ³ , ν μ€μ `home.footer`, λ―Έμ
ν¨λμ `sidebar.content`μ μμΌλ―λ‘ μ¬μ΄λλ°λ₯Ό μ΄μ΄
|
|
391
|
+
λλ©΄ 보μ
λλ€. μΉ΄λ, μΉ©, ν¨λ, ν μ€μ OpenCode 1κ³Ό κ°μ κΈμ λΉλλ₯Ό μλλ€. νλ¬κ·ΈμΈ νλͺ©μ `band`, `notify`
|
|
392
|
+
μ΅μ
μ OpenCode 2κ° λ겨 μ£Όλ μ΅μ
μμ μ½μ΅λλ€. OpenCode 2λ ν μΌ λͺ©λ‘μ λμ§ μμΌλ―λ‘ λ―Έμ
μ§νμ ledgerμ
|
|
393
|
+
μμ
κΈ°λ‘κ³Ό κ³νμμ μ½μ΅λλ€. λꡬ μ΄λ¦λ λ€λ¦
λλ€(`shell`, `subagent`, `id`λ₯Ό λ°λ `skill` λꡬ). μΉ΄λλ κ·Έ
|
|
394
|
+
μ΄λ¦μ μ½μ΅λλ€. OpenCode 2λ νλ¬κ·ΈμΈμ μκΈμ νλ μμΉλ₯Ό μ£Όμ§ μμΌλ―λ‘ κ·Έλ¦¬μ§ μμ΅λλ€.
|
|
387
395
|
- *μ
λ°μ΄νΈ.* OpenCode 2μμλ νλ¬κ·ΈμΈ μμ μλ μ
λ°μ΄νΈκ° μμ΅λλ€. `opencode plugin update`λ₯Ό μ€ννκ±°λ
|
|
388
396
|
OpenCode 2μ μ
λ°μ΄νΈ λ°©μμ λ§‘κΈ°λ©΄ λ©λλ€.
|
|
389
397
|
|
|
@@ -458,7 +466,7 @@ OpenCode 1.18μ `tui.json`μ΄λ `tui.jsonc`μ `plugin` λ°°μ΄μ μ ν TUI
|
|
|
458
466
|
- `lit-adhd`(λ³μΉ `lit-rewind`)κ° λλ λμ ν
λ리, LIT κΈμ, κ²μ΄μ§κ° κ°μ λ°κΈ°μ μμνμ λ°λΌ λλλ€.
|
|
459
467
|
1μ΄μ λ€ λ² μμ΄ μ‘°κΈμ© λ°λκ³ κΉλ°μ΄μ§ μμ΅λλ€. `motion: false`μ `NO_COLOR`μμλ κΊΌμ§λλ€. μ΄μ
|
|
460
468
|
μμ
μ΄ μλ μμ
곡κ°μ μ¬λ μΈμ
μ `lit adhd to pick up where you left off`λ₯Ό λ³΄μ¬ μ£Όκ³ , κ·Έκ³³μμ μ²μ μ°
|
|
461
|
-
μ μΈμ
μ κ°μ λ¬Έκ΅¬λ‘ ν μ€νΈλ₯Ό ν λ² λμλλ€(
|
|
469
|
+
μ μΈμ
μ κ°μ λ¬Έκ΅¬λ‘ ν μ€νΈλ₯Ό ν λ² λμλλ€(OpenCode 1κ³Ό 2 λͺ¨λ).
|
|
462
470
|
- μΉ΄λμ λͺ¨μμ ν λ²μ νλμ΄κ³ , `src/tui/mission-looks.ts`μ μμ λͺ©λ‘μμ κ³ λ¦
λλ€. lit-adhdλ₯Ό μ€ννλ
|
|
463
471
|
κΈΈμ λͺ¨λ λκ°κΈ° λͺ¨μμ΄ λ©λλ€. μ±ν
μ `lit adhd`/`lit rewind`, `/lit-adhd`/`/lit-rewind`(v2μ μ€μΉλ λ³μΉ
|
|
464
472
|
νμΌμμ μ¨ κ²λ ν¬ν¨), λ€μ΄ν°λΈ `skill` λ‘λκ° κ·Έλ μ΅λλ€. μΈμ
μ΄ μ¬μ©μλ₯Ό κΈ°λ€λ¦¬λ λμ(κΆν μμ²μ΄λ μ§λ¬Έ,
|
package/docs/reference.md
CHANGED
|
@@ -498,12 +498,22 @@ settings but does not send them yet.
|
|
|
498
498
|
TUI: the line under the prompt (`prompt.footer.status`) reads `π₯ LIT IGNITED Β· <discipline>` while the
|
|
499
499
|
latest prompt of the session is a lit prompt. On OpenCode 1 the mark stays where it was.
|
|
500
500
|
- *Toasts.* The server cannot show toasts on OpenCode 2. The TUI raises the ignition toast itself when a
|
|
501
|
-
user message arrives with the lit mark the server writes into its metadata
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
OpenCode 1 only
|
|
501
|
+
user message arrives with the lit mark the server writes into its metadata, and it raises the same gate
|
|
502
|
+
toasts as OpenCode 1 (plan saved, goal set, criterion passed, blockers, complete) plus three more that
|
|
503
|
+
only OpenCode 2 has: the workflow stage moving (`π₯ Stage`, `work 3/7`), a switch of discipline
|
|
504
|
+
(`π₯ lit-loop β start-work`) and a lane starting or finishing. A burst folds into one toast with a
|
|
505
|
+
`+N more` tail. The restart notice, the `lit adhd` suggestion and the opt-in finish alert work as on
|
|
506
|
+
OpenCode 1. The automatic-handoff notices and the skill hint, which only the server knew about, are
|
|
507
|
+
not shown there.
|
|
508
|
+
- *TUI.* The HUD is placed in the slots OpenCode 2 publishes: the status card is claimed in `app`, under
|
|
509
|
+
the session and the sidebar, and keeps its four rows idle or working; the stage chip shares
|
|
510
|
+
`prompt.footer.status` with the ignition line, because there is no slot right of the agent and model
|
|
511
|
+
row; the home line is in `home.footer`; the mission panel sits in `sidebar.content`, so it shows when
|
|
512
|
+
the sidebar is open. The card, chip, panel and home line use the same text builders as OpenCode 1. The
|
|
513
|
+
`band` and `notify` options of the plugin entry are read from the options OpenCode 2 hands over. OpenCode 2
|
|
514
|
+
keeps no todo list, so mission progress comes from the ledger, the work record and the plan. It names
|
|
515
|
+
its tools differently (`shell`, `subagent`, a `skill` tool that takes `id`), and the card reads those
|
|
516
|
+
names. OpenCode 2 gives plugins no rate-limit figures, so none are drawn.
|
|
507
517
|
- *Updates.* There is no automatic self-update inside the plugin on OpenCode 2. Run
|
|
508
518
|
`opencode plugin update`, or let OpenCode 2's own update policy handle it.
|
|
509
519
|
|
|
@@ -586,7 +596,7 @@ What it draws:
|
|
|
586
596
|
the colour wheel at one fixed lightness, four small colour steps a second with no flashing. It
|
|
587
597
|
is off under `motion: false` and `NO_COLOR`. An idle session in a workspace with earlier work
|
|
588
598
|
says `lit adhd to pick up where you left off`, and the first fresh session there raises one
|
|
589
|
-
toast with the same words (
|
|
599
|
+
toast with the same words (OpenCode 1 and 2).
|
|
590
600
|
- one look at a time, picked from a small registry in `src/tui/mission-looks.ts`. Every route that runs
|
|
591
601
|
lit-adhd wears the rewind look: `lit adhd`/`lit rewind` in chat, `/lit-adhd`/`/lit-rewind` (also from an
|
|
592
602
|
installed v2 alias file) and a native `skill` load. While the session waits on you (a permission or a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@litfamily/litopencode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"description": "Workflow plugin for OpenCode: add lit to a prompt and the work is planned, built, checked and written down so the next session can pick it up.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -108,7 +108,7 @@ function prepare(out) {
|
|
|
108
108
|
const cover = loadCoverTreatment(out);
|
|
109
109
|
const coverDir = path.join(out, "cover");
|
|
110
110
|
if (!existsSync(path.join(coverDir, "index.html"))) throw stop(EXIT.STAGE_CONTRACT_ERROR, "STAGE_CONTRACT_ERROR", `no cover page at ${path.join(coverDir, "index.html")}; write it first (references/cover.md)`);
|
|
111
|
-
const scan = scanStage(coverDir);
|
|
111
|
+
const scan = scanStage(coverDir, { clips: false });
|
|
112
112
|
const assets = checkAssets({ out, treatment: cover, rasters: scan.rasters });
|
|
113
113
|
const state = inspectWarmState();
|
|
114
114
|
if (state.blocked) throw stop(state.blocked.exit, state.blocked.name, `${state.blocked.detail}; fix: ${state.fix}`);
|
|
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { chromeFlagRungs } from "../constants.mjs";
|
|
9
|
-
import { StageError, contentTypes, insideDir } from "./scan.mjs";
|
|
9
|
+
import { StageError, clipsScript, contentTypes, insideDir } from "./scan.mjs";
|
|
10
10
|
|
|
11
11
|
export const stageHost = "lit.stage";
|
|
12
12
|
export const stageOrigin = `http://${stageHost}`;
|
|
@@ -108,7 +108,8 @@ export async function openStage(chromium, { chromePath, profile, stageDir, width
|
|
|
108
108
|
const line = String(error?.message ?? error).split("\n").find((part) => part.trim()) ?? String(error);
|
|
109
109
|
throw Object.assign(new Error(`BLOCKED_NO_CHROME (10): Chrome failed to launch headless: ${line}; fix: check that Chrome can start headless in this sandbox`), { exitCode: 10 });
|
|
110
110
|
}
|
|
111
|
-
const
|
|
111
|
+
const clips = clipsScript(stageDir);
|
|
112
|
+
const { blocked, refused, missing } = await serveStage(context, { stageDir, fonts, scripts: { "/lit/stage-kit.js": kitSource(), ...(clips ? { "/lit/clips.js": clips } : {}), ...styleFiles } });
|
|
112
113
|
await context.addInitScript({ content: `window.__litStageConfig = ${JSON.stringify({ seed: seed >>> 0, fps })};\n${clockSource()}` });
|
|
113
114
|
if (extraInit) await context.addInitScript({ content: extraInit });
|
|
114
115
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -146,6 +146,7 @@ export function formatStageReport(result, context) {
|
|
|
146
146
|
`outputs: ${out("film.mp4")} Β· ${out(manifest.previewEncoder === "gif" ? "preview.gif" : "preview.webp")} (encoder: ${manifest.previewEncoder}) Β· ${out("poster.png")} Β· ${out("reduced-motion.png")}`,
|
|
147
147
|
`format / fps / length: ${manifest.format} ${manifest.resolution.join("x")} @ ${manifest.fps} fps, ${manifest.durationSec.toFixed(2)} s (treatment ${manifest.targetDurationSec} s)`,
|
|
148
148
|
`renderer: stage ${manifest.rung === "gpu" ? `GPU rung (${manifest.renderer ?? "native ANGLE"})` : "software rung"} (flags: ${(manifest.chromeFlags ?? []).join(" ")})`,
|
|
149
|
+
...(manifest.clips?.length ? [`clips: ${manifest.clips.map((clip) => `${clip.id} ${clip.frames} frames at ${clip.fps} fps`).join(", ")} (keyed overlays, chosen from film time)`] : []),
|
|
149
150
|
...(manifest.warnings ?? []).map((note) => `notice: ${note}`),
|
|
150
151
|
`timing: master ${manifest.timing?.masterSec ?? "?"} s, per frame p50 ${manifest.timing?.frameP50 ?? "?"} ms / p95 ${manifest.timing?.frameP95 ?? "?"} ms, total ${manifest.timing?.totalSec ?? "?"} s`,
|
|
151
152
|
"",
|
|
@@ -450,6 +450,7 @@ export async function renderStage(out, { round = 1, stillsOnly = false } = {}) {
|
|
|
450
450
|
preview: preview ? { width: preview.width, height: preview.height, fps: preview.fps, bytes: preview.bytes } : null,
|
|
451
451
|
posterFrame, posterSize: [poster.width, poster.height], stillFrame, determinismFrames: samples,
|
|
452
452
|
timing: { ...master.timing, totalSec: Number(((performance.now() - began) / 1000).toFixed(1)) },
|
|
453
|
+
...(context.scan.clips.length ? { clips: context.scan.clips.map((clip) => ({ id: clip.id, frames: clip.count, fps: clip.fps, width: clip.width, height: clip.height, bytes: clip.bytes })) } : {}),
|
|
453
454
|
...(context.plate ? { plate: { blender: context.plate.blender, engine: context.plate.engine, device: context.plate.device, samples: context.plate.samples, fps: context.plate.fps, frames: context.plate.frames.length } } : {}),
|
|
454
455
|
sound, qa: { file: ".run/qa.json", samples: qaResult.samples.length, trackFrames: qaResult.trackFrames, allowedFaces: qaResult.allowedFaces },
|
|
455
456
|
treatmentSha256: sha256(readFileSync(path.join(out, "treatment.json"))), stageSha256: stageSha(context.stageDir),
|