@litfamily/litopencode 1.0.20 → 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 +18 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/agents/registry.js +2 -2
- package/dist/cli/doctor.d.ts +5 -0
- package/dist/cli/doctor.js +27 -6
- package/dist/cli/install-config.d.ts +6 -0
- package/dist/cli/install-config.js +32 -2
- package/dist/cli/install.js +4 -1
- package/dist/cli/model-catalog.d.ts +6 -1
- package/dist/cli/model-catalog.js +32 -15
- package/dist/cli/model-routing.js +14 -9
- package/dist/cli/types.d.ts +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +10 -2
- package/dist/model-route-policy.d.ts +9 -1
- package/dist/model-route-policy.js +46 -47
- package/dist/tui/mission-band.d.ts +13 -0
- package/dist/tui/mission-band.js +63 -25
- 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 +36 -6
- package/dist/tui/mission-source.js +167 -13
- package/dist/tui/mission-text.d.ts +34 -9
- package/dist/tui/mission-text.js +169 -74
- package/dist/tui.js +38 -5
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/migration.md +1 -1
- package/docs/reference-Ko-KR.md +81 -26
- package/docs/reference.md +92 -28
- package/package.json +1 -1
- package/skills/lit-pptx/package-lock.json +167 -119
- package/skills/lit-pptx/package.json +1 -1
- package/skills/managed-skill-manifest.json +4 -4
- package/skills/readme-studio/templates/hyperframes/package-lock.json +169 -121
- package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/tools/check-pack-payload.mjs +2 -2
package/dist/tui/mission-text.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// (Hangul and emoji take two cells), truncation, the gradient bar, and the coloured segments the TUI
|
|
3
3
|
// renders as spans. Every frame of motion is drawn from a MotionFrame, so all of it is testable here.
|
|
4
4
|
import { ease, mixHex, rowEntryStart, seg, slideIn, motionTiming } from "./mission-motion.js";
|
|
5
|
-
import { missionRowKeys, missionStatus } from "./mission-model.js";
|
|
5
|
+
import { currentStepIndex, missionRowKeys, missionStatus, stepName } from "./mission-model.js";
|
|
6
6
|
/** Panel box width in cells, and the content width inside its border and one cell of padding per side. */
|
|
7
7
|
export const panelWidth = 36;
|
|
8
8
|
export const panelInnerWidth = panelWidth - 4;
|
|
@@ -49,6 +49,7 @@ export function paletteFromTheme(theme, mode = "dark") {
|
|
|
49
49
|
const neutral = mode === "light" ? "#f2f2f2" : "#1c1c1c";
|
|
50
50
|
return {
|
|
51
51
|
primary: pick("primary", "#ff6337"),
|
|
52
|
+
secondary: pick("secondary", "#ff2d95"),
|
|
52
53
|
accent: pick("accent", "#b48cff"),
|
|
53
54
|
info: pick("info", "#0aa6c2"),
|
|
54
55
|
warning: pick("warning", "#ffaf3c"),
|
|
@@ -182,27 +183,34 @@ export function progressBar(ratio, cells, palette, sweep = -1) {
|
|
|
182
183
|
segments.push({ text: "─".repeat(cells - used), fg: palette.borderSubtle });
|
|
183
184
|
return segments;
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
/** The stage colour ramp: the theme's primary through secondary to accent, so every theme keeps its own feel. */
|
|
187
|
+
export function stageColor(index, count, palette) {
|
|
188
|
+
return gradientAt([palette.primary, palette.secondary, palette.accent], count <= 1 ? 0 : index / (count - 1));
|
|
189
|
+
}
|
|
190
|
+
function microBar(ratio, palette, cells = 5) {
|
|
191
|
+
const filled = Math.max(0, Math.min(cells, Math.round(Math.max(0, Math.min(1, ratio)) * cells)));
|
|
187
192
|
const segments = [];
|
|
188
|
-
for (let index = 0; index <
|
|
189
|
-
segments.push(index < filled ? { text: "▰", fg: gradientAt([palette.primary, palette.accent], index /
|
|
193
|
+
for (let index = 0; index < cells; index += 1) {
|
|
194
|
+
segments.push(index < filled ? { text: "▰", fg: gradientAt([palette.primary, palette.secondary, palette.accent], cells <= 1 ? 0 : index / (cells - 1)) } : { text: "▱", fg: palette.borderSubtle });
|
|
190
195
|
}
|
|
191
196
|
return segments;
|
|
192
197
|
}
|
|
193
198
|
function countText(view) {
|
|
194
199
|
return view.progress === undefined ? "" : `${view.progress.done}/${view.progress.total}`;
|
|
195
200
|
}
|
|
196
|
-
/**
|
|
197
|
-
|
|
201
|
+
/**
|
|
202
|
+
* The step state to draw. During the completion moment the stage that was running and every stage
|
|
203
|
+
* after it flip to done one after the other, 60 ms apart.
|
|
204
|
+
*/
|
|
205
|
+
function shownState(step, index, frame) {
|
|
198
206
|
const t = frame.celebrate;
|
|
199
|
-
|
|
207
|
+
const from = frame.celebrateFrom;
|
|
208
|
+
if (t === undefined || from === undefined || step.state !== "done" || index < from)
|
|
200
209
|
return { state: step.state, doneMix: step.state === "done" ? 1 : 0 };
|
|
201
|
-
const at =
|
|
202
|
-
const mix = ease.inOutCubic(seg(t, at, motionTiming.crossfade));
|
|
210
|
+
const at = motionTiming.workDoneAt + (index - from) * (motionTiming.reviewDoneAt - motionTiming.workDoneAt);
|
|
203
211
|
if (t < at)
|
|
204
|
-
return { state:
|
|
205
|
-
return { state: "done", doneMix:
|
|
212
|
+
return { state: index === from ? "now" : "todo", doneMix: 0 };
|
|
213
|
+
return { state: "done", doneMix: ease.inOutCubic(seg(t, at, motionTiming.crossfade)) };
|
|
206
214
|
}
|
|
207
215
|
function pillStyle(state, palette, warn) {
|
|
208
216
|
if (state === "done")
|
|
@@ -212,64 +220,120 @@ function pillStyle(state, palette, warn) {
|
|
|
212
220
|
const level = state === "warn" ? Math.max(warn, 0.5) : warn;
|
|
213
221
|
return { fg: palette.onFill, bg: mixHex(palette.info, palette.warning, level), bold: true };
|
|
214
222
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
return
|
|
223
|
+
/** A light pass over the running stage: once when the map moves forward, and during the completion moment. */
|
|
224
|
+
export function stageFlash(frame) {
|
|
225
|
+
if (frame.stageFrom !== undefined && frame.t <= motionTiming.shimmer)
|
|
226
|
+
return -0.2 + 1.4 * seg(frame.t, 0, motionTiming.shimmer);
|
|
227
|
+
return shimmerAt(frame);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The colour of the connector into stage `index`. It carries the stage ramp once the stage before it is
|
|
231
|
+
* behind the map, and fills in turn, 60 ms apart, when the map has just moved forward.
|
|
232
|
+
*/
|
|
233
|
+
export function linkColor(view, index, frame, palette, idle) {
|
|
234
|
+
const before = view.steps[index - 1];
|
|
235
|
+
if (before === undefined)
|
|
236
|
+
return idle;
|
|
237
|
+
const state = shownState(before, index - 1, frame).state;
|
|
238
|
+
if (state !== "done" && state !== "skip")
|
|
239
|
+
return idle;
|
|
240
|
+
const color = stageColor(index - 0.5, view.steps.length, palette);
|
|
241
|
+
if (frame.stageFrom === undefined || index <= frame.stageFrom)
|
|
242
|
+
return color;
|
|
243
|
+
return mixHex(idle, color, ease.outCubic(seg(frame.t, (index - frame.stageFrom - 1) * motionTiming.stagger, motionTiming.entry)));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* One stage of the breadcrumb. Behind the map a stage is its name in its ramp colour with `✓` (or `–`
|
|
247
|
+
* when skipped); ahead it is a muted name; the running stage is a filled capital pill followed by an
|
|
248
|
+
* optional `bar`-cell bar and the count.
|
|
249
|
+
*/
|
|
250
|
+
export function crumb(view, index, frame, palette, bar, flash = -1) {
|
|
251
|
+
const step = view.steps[index];
|
|
252
|
+
const { state, doneMix } = shownState(step, index, frame);
|
|
253
|
+
const name = stepName(step.id, view);
|
|
254
|
+
const counted = step.id === "work" && view.progress !== undefined;
|
|
255
|
+
if (state === "done") {
|
|
256
|
+
const color = stageColor(index, view.steps.length, palette);
|
|
257
|
+
return [{ text: [name, counted ? countText(view) : "", "✓"].filter((part) => part !== "").join(" "), fg: doneMix >= 1 ? color : mixHex(index === frame.celebrateFrom ? palette.info : palette.muted, color, doneMix) }];
|
|
258
|
+
}
|
|
229
259
|
if (state === "skip")
|
|
230
|
-
return `${name}
|
|
231
|
-
if (state === "
|
|
232
|
-
return [name,
|
|
233
|
-
|
|
260
|
+
return [{ text: `${name} –`, fg: palette.muted }];
|
|
261
|
+
if (state === "todo")
|
|
262
|
+
return [{ text: name, fg: palette.muted }];
|
|
263
|
+
const style = pillStyle(state, palette, frame.warn);
|
|
264
|
+
const label = ` ${name.toUpperCase()}${state === "warn" ? " ⚑" : ""} `;
|
|
265
|
+
const chars = [...label];
|
|
266
|
+
const pill = flash < 0
|
|
267
|
+
? [{ text: label, fg: style.fg, ...(style.bg === undefined ? {} : { bg: style.bg }), bold: style.bold }]
|
|
268
|
+
: chars.map((char, position) => ({ text: char, fg: style.fg, bg: shine(style.bg ?? palette.info, palette, position / (chars.length - 1), flash, 0.3), bold: true }));
|
|
269
|
+
if (!counted)
|
|
270
|
+
return pill;
|
|
271
|
+
return [...pill, ...(bar > 0 ? [{ text: " ", fg: palette.muted }, ...microBar(frame.barRatio, palette, bar)] : []), { text: ` ${countText(view)}`, fg: palette.text, bold: true }];
|
|
272
|
+
}
|
|
273
|
+
const spinnerFrames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
|
|
274
|
+
function clockText(seconds) {
|
|
275
|
+
const minutes = Math.floor(seconds / 60);
|
|
276
|
+
const rest = String(seconds % 60).padStart(2, "0");
|
|
277
|
+
return minutes < 60 ? `${minutes}:${rest}` : `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, "0")}:${rest}`;
|
|
234
278
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
279
|
+
/**
|
|
280
|
+
* The live step: the running tool's name and how long it has run, `⠹ bash · 0:48`. The spinner moves
|
|
281
|
+
* with the elapsed second, so it changes only when the time does. Nothing of the tool's input is shown.
|
|
282
|
+
*/
|
|
283
|
+
export function liveSegments(view, frame, palette, room) {
|
|
284
|
+
if (view.live === undefined || view.complete || room < 4)
|
|
285
|
+
return [];
|
|
286
|
+
const seconds = frame.now === undefined ? undefined : Math.max(0, Math.floor((frame.now - view.live.start) / 1000));
|
|
287
|
+
const spin = [...spinnerFrames][(seconds ?? 0) % 10];
|
|
288
|
+
const time = seconds === undefined ? "" : ` · ${clockText(seconds)}`;
|
|
289
|
+
return [
|
|
290
|
+
{ text: `${spin} `, fg: palette.warning },
|
|
291
|
+
{ text: truncateCells(view.live.tool, Math.max(1, room - 2 - cellWidth(time))), fg: palette.text },
|
|
292
|
+
{ text: time, fg: palette.muted }
|
|
293
|
+
];
|
|
240
294
|
}
|
|
241
295
|
function brandPill(palette, sweep) {
|
|
242
296
|
const chars = [" ", "🔥", " ", "L", "I", "T", " "];
|
|
243
297
|
return chars.map((char, index) => ({ text: char, fg: palette.onFill, bg: shine(palette.primary, palette, index / (chars.length - 1), sweep, 0.3), bold: true }));
|
|
244
298
|
}
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
|
|
299
|
+
/**
|
|
300
|
+
* The chip's groups for a tier. The full tier is the brand and the breadcrumb around the running stage
|
|
301
|
+
* (the one before, the running one with a seven-cell bar, the one after); medium keeps the running
|
|
302
|
+
* stage and the next with a five-cell bar; tight keeps only the count.
|
|
303
|
+
*/
|
|
304
|
+
export function chipGroups(view, frame, palette, tier) {
|
|
248
305
|
const space = { text: " ", fg: palette.muted };
|
|
249
|
-
const separator = { text: " › ", fg: palette.borderActive };
|
|
250
306
|
const flame = { text: "🔥", fg: palette.text };
|
|
307
|
+
const count = view.steps.length;
|
|
308
|
+
const reached = currentStepIndex(view);
|
|
309
|
+
const current = Math.min(count - 1, Math.max(0, reached));
|
|
310
|
+
const flash = stageFlash(frame);
|
|
251
311
|
const groups = [];
|
|
312
|
+
const trail = (from, to, bar) => {
|
|
313
|
+
for (let index = Math.max(0, from); index <= Math.min(count - 1, to); index += 1) {
|
|
314
|
+
const lead = index === Math.max(0, from) ? space : { text: " › ", fg: linkColor(view, index, frame, palette, palette.borderActive) };
|
|
315
|
+
groups.push([lead, ...crumb(view, index, frame, palette, bar, index === current ? flash : -1)]);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
// A finished map keeps its last stages in view, so the completion moment can flip them in turn.
|
|
319
|
+
const anchor = reached >= count ? count - 2 : current;
|
|
252
320
|
if (tier === "full") {
|
|
253
|
-
groups.push(brandPill(palette,
|
|
254
|
-
|
|
321
|
+
groups.push(brandPill(palette, shimmerAt(frame)));
|
|
322
|
+
trail(anchor - 1, anchor + 1, 7);
|
|
255
323
|
}
|
|
256
324
|
else if (tier === "medium") {
|
|
257
325
|
groups.push([flame]);
|
|
258
|
-
|
|
259
|
-
const from = current >= 0 ? current : view.complete ? 1 : 0;
|
|
260
|
-
view.steps.slice(from, from + 2).forEach((step, index) => groups.push([index === 0 ? space : separator, stepPill(step, view, frame, palette)]));
|
|
326
|
+
trail(anchor, anchor + 1, 5);
|
|
261
327
|
}
|
|
262
328
|
else {
|
|
263
329
|
groups.push([flame]);
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const done = view.complete &&
|
|
267
|
-
const value = done ? `✓ ${
|
|
268
|
-
const style = pillStyle(done ? "done" :
|
|
330
|
+
const step = view.steps[current];
|
|
331
|
+
const shown = shownState(step, current, frame).state;
|
|
332
|
+
const done = view.complete && shown === "done";
|
|
333
|
+
const value = done ? `✓ ${countText(view)}`.trim() : step.state === "warn" ? `⚑ ${countText(view)}`.trim() : countText(view) || stepName(step.id, view).toUpperCase();
|
|
334
|
+
const style = pillStyle(done ? "done" : step.state === "warn" ? "warn" : "now", palette, frame.warn);
|
|
269
335
|
groups.push([space, { text: ` ${value} `, fg: style.fg, ...(style.bg === undefined ? {} : { bg: style.bg }), bold: style.bold }]);
|
|
270
336
|
}
|
|
271
|
-
if (withMicroBar && view.progress !== undefined)
|
|
272
|
-
groups.push([space, ...microBar(frame.barRatio, palette)]);
|
|
273
337
|
return groups;
|
|
274
338
|
}
|
|
275
339
|
export function groupsWidth(groups) {
|
|
@@ -314,14 +378,20 @@ export function chipTier(view, frame, palette, budget) {
|
|
|
314
378
|
return "tight";
|
|
315
379
|
}
|
|
316
380
|
/**
|
|
317
|
-
* The
|
|
318
|
-
*
|
|
381
|
+
* The map chip on the right of the prompt's agent/model row: `🔥 LIT plan ✓ › WORK ▰▰▰▱▱▱▱ 3/7 › review`,
|
|
382
|
+
* then the live step when the full chip leaves room for it, shedding labels before values as `budget`
|
|
383
|
+
* shrinks. Idle sessions render nothing.
|
|
319
384
|
*/
|
|
320
385
|
export function chipLine(view, frame, palette, budget) {
|
|
321
386
|
if (!view.active)
|
|
322
387
|
return [];
|
|
323
388
|
const tier = chipTier(view, frame, palette, budget);
|
|
324
389
|
const groups = chipGroups(view, frame, palette, tier);
|
|
390
|
+
if (tier === "full") {
|
|
391
|
+
const live = liveSegments(view, frame, palette, 24);
|
|
392
|
+
if (live.length > 0 && groupsWidth(groups) + 2 + lineWidth(live) <= budget)
|
|
393
|
+
groups.push([{ text: " ", fg: palette.muted }, ...live]);
|
|
394
|
+
}
|
|
325
395
|
const out = [];
|
|
326
396
|
if (frame.celebrate !== undefined && groupsWidth(groups) + chipSparkWidth <= budget)
|
|
327
397
|
out.push(...(chipSparkCells(frame.celebrate, palette) ?? []));
|
|
@@ -367,20 +437,35 @@ export function statusSegments(view, frame, palette) {
|
|
|
367
437
|
};
|
|
368
438
|
return { segments: [fade(pill, progress <= 0 ? 0 : ease.outCubic(progress), palette)], offset: Math.max(-1, slideIn(progress)) };
|
|
369
439
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
440
|
+
/**
|
|
441
|
+
* One stage of the sidebar's vertical map: `◆` behind, `◉` running in bold, `⚑` flagged, `◇` ahead,
|
|
442
|
+
* with the plan's task count or the loop's iteration on the right.
|
|
443
|
+
*/
|
|
444
|
+
function stepperRow(view, index, frame, palette, width) {
|
|
445
|
+
const step = view.steps[index];
|
|
446
|
+
const { state, doneMix } = shownState(step, index, frame);
|
|
447
|
+
const counted = step.id === "work" && view.progress !== undefined ? ` ${countText(view)}` : "";
|
|
448
|
+
const name = `${stepName(step.id, view)}${counted}`;
|
|
449
|
+
const note = step.id === "plan" && view.planTasks !== undefined ? `${view.planTasks} tasks` : step.id === "work" && view.iteration !== undefined ? `iter ${view.iteration}` : "";
|
|
450
|
+
const live = mixHex(palette.info, palette.warning, state === "warn" ? Math.max(frame.warn, 0.5) : frame.warn);
|
|
451
|
+
let head;
|
|
452
|
+
if (state === "done") {
|
|
453
|
+
const color = stageColor(index, view.steps.length, palette);
|
|
454
|
+
head = [{ text: "◆ ", fg: doneMix >= 1 ? color : mixHex(palette.info, color, doneMix) }, { text: name, fg: palette.text }];
|
|
455
|
+
}
|
|
456
|
+
else if (state === "now" || state === "warn") {
|
|
457
|
+
head = [{ text: `${state === "warn" ? "⚑" : "◉"} ${name}`, fg: live, bold: true }];
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
head = [{ text: `◇ ${name}${state === "skip" ? " –" : ""}`, fg: palette.muted }];
|
|
461
|
+
}
|
|
462
|
+
if (note === "")
|
|
463
|
+
return head;
|
|
464
|
+
const room = width - cellWidth(note) - 1;
|
|
465
|
+
const used = lineWidth(head);
|
|
466
|
+
if (used > room)
|
|
467
|
+
return head;
|
|
468
|
+
return [...head, { text: " ".repeat(width - used - cellWidth(note)), fg: palette.muted }, { text: note, fg: palette.muted }];
|
|
384
469
|
}
|
|
385
470
|
function criterionSegment(id, status, palette) {
|
|
386
471
|
if (status === "pass")
|
|
@@ -510,9 +595,9 @@ function withEmbers(rows, t, palette) {
|
|
|
510
595
|
}
|
|
511
596
|
/**
|
|
512
597
|
* The sidebar mission panel: a rounded box whose border carries the state (accent while working,
|
|
513
|
-
* warning on a warning, success when complete), a tiny LIT header, the goal title, the
|
|
514
|
-
* the bar, the
|
|
515
|
-
* `panelInnerWidth` cells.
|
|
598
|
+
* warning on a warning, success when complete), a tiny LIT header, the goal title, the vertical stage
|
|
599
|
+
* map with its connectors, the bar, the detail line, the live step, criteria, warnings and the
|
|
600
|
+
* evidence timeline. Rows are at most `panelInnerWidth` cells.
|
|
516
601
|
*/
|
|
517
602
|
export function missionPanel(view, frame, palette) {
|
|
518
603
|
if (!view.active)
|
|
@@ -528,12 +613,22 @@ export function missionPanel(view, frame, palette) {
|
|
|
528
613
|
rows.set("b1", []);
|
|
529
614
|
if (view.goal !== undefined)
|
|
530
615
|
rows.set("title", [{ text: truncateCells(view.goal.title, width), fg: palette.text, bold: true }]);
|
|
531
|
-
|
|
616
|
+
view.steps.forEach((step, index) => {
|
|
617
|
+
if (index > 0) {
|
|
618
|
+
const color = linkColor(view, index, frame, palette, palette.borderSubtle);
|
|
619
|
+
rows.set(`link:${index}`, [{ text: color === palette.borderSubtle ? "┆" : "┃", fg: color }]);
|
|
620
|
+
}
|
|
621
|
+
rows.set(`step:${step.id}`, stepperRow(view, index, frame, palette, width));
|
|
622
|
+
});
|
|
532
623
|
if (view.progress !== undefined)
|
|
533
624
|
rows.set("bar", progressBar(frame.barRatio, width, palette, sweep));
|
|
534
625
|
rows.set("b2", []);
|
|
535
|
-
|
|
536
|
-
|
|
626
|
+
// The detail line: `└ task 3 · next ▸ …`, or a research run's claim counts.
|
|
627
|
+
const lead = view.detail === undefined ? "" : truncateCells(view.detail, width - 2);
|
|
628
|
+
const gap = lead !== "" && view.next !== undefined ? " · " : "";
|
|
629
|
+
const next = view.next === undefined ? "" : truncateCells(`next ▸ ${view.next}`, Math.max(0, width - 2 - cellWidth(lead) - cellWidth(gap)));
|
|
630
|
+
rows.set("detail", [{ text: "└ ", fg: palette.muted }, { text: lead + (next === "" ? "" : gap), fg: palette.muted }, ...(next === "" ? [] : [{ text: next, fg: palette.text }])]);
|
|
631
|
+
rows.set("live", liveSegments(view, frame, palette, width));
|
|
537
632
|
if (view.goal !== undefined) {
|
|
538
633
|
const criteria = [{ text: "crit", fg: palette.muted }];
|
|
539
634
|
for (const criterion of view.goal.criteria) {
|
|
@@ -573,7 +668,7 @@ export function missionPanel(view, frame, palette) {
|
|
|
573
668
|
const enter = frame.chipEnter ? ease.outCubic(seg(frame.t, 0, motionTiming.entry)) : 1;
|
|
574
669
|
const done = view.complete ? (frame.celebrate === undefined ? 1 : ease.inOutCubic(seg(frame.celebrate, 0, motionTiming.crossfade))) : 0;
|
|
575
670
|
const border = mixHex(palette.mixBase, mixHex(mixHex(palette.accent, palette.warning, frame.warn), palette.success, done), enter);
|
|
576
|
-
const phase = view.complete ? "COMPLETE" : view.phase.toUpperCase();
|
|
671
|
+
const phase = view.complete ? "COMPLETE" : stepName(view.phase, view).toUpperCase();
|
|
577
672
|
const flagged = view.warnings.length > 0 && !view.complete ? " ⚑" : "";
|
|
578
673
|
const bottom = view.goal !== undefined
|
|
579
674
|
? ` ${view.goal.passed}/${view.goal.total} criteria${view.complete ? " ✓" : ""} `
|
package/dist/tui.js
CHANGED
|
@@ -4,14 +4,15 @@ import { jsx } from "@opentui/solid/jsx-runtime";
|
|
|
4
4
|
import { buildMissionView, lastMission, leadingNotice, missionNotices } from "./tui/mission-model.js";
|
|
5
5
|
import { idleMotion, motionTiming, planMotion, sampleMotion } from "./tui/mission-motion.js";
|
|
6
6
|
import { chipBudget, chipLine, homeLine, missionPanel, paletteFromTheme, panelWidth } from "./tui/mission-text.js";
|
|
7
|
-
import { createLedgerReader, missionProjectRoot, readCardInput, readMissionInput } from "./tui/mission-source.js";
|
|
7
|
+
import { createLedgerReader, createWorkReader, missionProjectRoot, readCardInput, readLatestResearchRun, readMissionInput } from "./tui/mission-source.js";
|
|
8
8
|
import { chipShimmer, idleChipMotion, planChipMotion } from "./tui/mission-skills.js";
|
|
9
|
-
import { bandCard, readBandOptions, statusCard } from "./tui/mission-band.js";
|
|
9
|
+
import { bandCard, readBandOptions, readNotifyOptions, statusCard } from "./tui/mission-band.js";
|
|
10
10
|
export const litOpenCodeTuiPluginId = "litopencode";
|
|
11
11
|
const homeLineWidth = 69;
|
|
12
12
|
const refreshDelayMs = 80;
|
|
13
13
|
const ledgerPollMs = 2000;
|
|
14
14
|
const toastMs = 4000;
|
|
15
|
+
const liveTickMs = 1000;
|
|
15
16
|
const trackedSessionLimit = 4;
|
|
16
17
|
const refreshEvents = [
|
|
17
18
|
"message.updated",
|
|
@@ -38,7 +39,9 @@ function lineElement(line) {
|
|
|
38
39
|
const tui = async (api, options) => {
|
|
39
40
|
const motionEnabled = options?.motion !== false;
|
|
40
41
|
const band = readBandOptions(options);
|
|
42
|
+
const notify = readNotifyOptions(options);
|
|
41
43
|
const readLedger = createLedgerReader();
|
|
44
|
+
const readWork = createWorkReader();
|
|
42
45
|
const sessions = new Map();
|
|
43
46
|
const tracked = [];
|
|
44
47
|
const [revision, setRevision] = createSignal(0);
|
|
@@ -46,6 +49,7 @@ const tui = async (api, options) => {
|
|
|
46
49
|
let frameTimer;
|
|
47
50
|
let refreshTimer;
|
|
48
51
|
let pollTimer;
|
|
52
|
+
let liveTimer;
|
|
49
53
|
const state = () => api.state;
|
|
50
54
|
const palette = () => paletteFromTheme(api.theme.current, api.theme.mode());
|
|
51
55
|
// OpenCode keeps the sidebar choice in its KV store under "sidebar" ("auto" or "hide"); a child
|
|
@@ -79,20 +83,45 @@ const tui = async (api, options) => {
|
|
|
79
83
|
}
|
|
80
84
|
}, motionTiming.frameMs);
|
|
81
85
|
}
|
|
86
|
+
// The live step's elapsed time is the only steady change: one tick a second while a tool runs, and
|
|
87
|
+
// no timer at all otherwise.
|
|
88
|
+
function syncLiveTick() {
|
|
89
|
+
const running = [...sessions.values()].some((entry) => entry.view.live !== undefined);
|
|
90
|
+
if (running && liveTimer === undefined)
|
|
91
|
+
liveTimer = setInterval(() => setClock(Date.now()), liveTickMs);
|
|
92
|
+
if (!running && liveTimer !== undefined) {
|
|
93
|
+
clearInterval(liveTimer);
|
|
94
|
+
liveTimer = undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// The finish alert is off unless tui.json opts in; OpenCode's own attention settings still decide
|
|
98
|
+
// whether a desktop notification or a sound actually plays.
|
|
99
|
+
function alertFinish(notice) {
|
|
100
|
+
if (!notify.enabled)
|
|
101
|
+
return;
|
|
102
|
+
void api.attention
|
|
103
|
+
.notify({ title: "🔥 LIT complete", message: notice.message, notification: notify.desktop, sound: notify.sound ? { name: "done" } : false })
|
|
104
|
+
.catch(() => undefined);
|
|
105
|
+
}
|
|
82
106
|
function refresh(sessionID) {
|
|
83
107
|
const now = Date.now();
|
|
84
108
|
const source = state();
|
|
85
|
-
const
|
|
86
|
-
const
|
|
109
|
+
const root = missionProjectRoot(source.path);
|
|
110
|
+
const ledger = readLedger(root);
|
|
111
|
+
const next = buildMissionView(readMissionInput(source, sessionID, ledger, now, { work: readWork(root, sessionID), research: readLatestResearchRun(root) }));
|
|
87
112
|
const card = readCardInput(source, sessionID);
|
|
88
113
|
const entry = sessions.get(sessionID);
|
|
89
114
|
const viewChanged = entry === undefined || JSON.stringify(entry.view) !== JSON.stringify(next);
|
|
90
115
|
if (!viewChanged && JSON.stringify(entry.card) === JSON.stringify(card))
|
|
91
116
|
return;
|
|
92
117
|
if (entry !== undefined && viewChanged) {
|
|
93
|
-
const
|
|
118
|
+
const notices = missionNotices(entry.view, next);
|
|
119
|
+
const notice = leadingNotice(notices);
|
|
94
120
|
if (notice !== undefined)
|
|
95
121
|
api.ui.toast({ variant: notice.variant, title: notice.title, message: notice.message, duration: toastMs });
|
|
122
|
+
const finish = notices.find((candidate) => candidate.celebrate === true);
|
|
123
|
+
if (finish !== undefined)
|
|
124
|
+
alertFinish(finish);
|
|
96
125
|
}
|
|
97
126
|
sessions.set(sessionID, {
|
|
98
127
|
view: next,
|
|
@@ -103,6 +132,7 @@ const tui = async (api, options) => {
|
|
|
103
132
|
setClock(now);
|
|
104
133
|
setRevision(revision() + 1);
|
|
105
134
|
ensureFrames();
|
|
135
|
+
syncLiveTick();
|
|
106
136
|
}
|
|
107
137
|
function refreshTracked() {
|
|
108
138
|
refreshTimer = undefined;
|
|
@@ -177,6 +207,8 @@ const tui = async (api, options) => {
|
|
|
177
207
|
clearTimeout(refreshTimer);
|
|
178
208
|
if (pollTimer !== undefined)
|
|
179
209
|
clearInterval(pollTimer);
|
|
210
|
+
if (liveTimer !== undefined)
|
|
211
|
+
clearInterval(liveTimer);
|
|
180
212
|
sessions.clear();
|
|
181
213
|
});
|
|
182
214
|
api.slots.register({
|
|
@@ -289,6 +321,7 @@ const tui = async (api, options) => {
|
|
|
289
321
|
view: snapshot.view,
|
|
290
322
|
frame: snapshot.frame,
|
|
291
323
|
chips: snapshot.card.chips,
|
|
324
|
+
busy: snapshot.card.busy,
|
|
292
325
|
...(shimmer === undefined ? {} : { shimmer }),
|
|
293
326
|
figures: { contextPercent: snapshot.card.contextPercent, ...(snapshot.card.branch === undefined ? {} : { branch: snapshot.card.branch }), inLane: snapshot.inLane },
|
|
294
327
|
...(last === undefined ? {} : { last })
|
|
@@ -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.22"><title>release: 1.0.22</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.22</text></g></svg>
|
package/docs/migration.md
CHANGED
|
@@ -23,7 +23,7 @@ This guide describes the supported migration shape for moving an OpenCode workfl
|
|
|
23
23
|
- Use `litopencode doctor --root <workspace>` to inspect `litopencode.json` route validity and effective `lit-plan` / `lit-loop` settings without writing files.
|
|
24
24
|
- Use `litopencode install --dry-run --root <workspace>` to preview the `opencode.json` plugin mutation without writing files.
|
|
25
25
|
- Route fields include top-level `permissionMode` (`safe`, `balanced`, or `yolo`) plus per-agent/per-category `provider`, `model`, `variant`, `category`, `reasoningEffort`, `textVerbosity`, `thinking`, `temperature`, `topP`, `maxTokens`, `promptAppend`, `tools`, `permission`, and `providerOptions`; generated route files use `variant` as the primary effort knob while `reasoningEffort` stays available for explicit provider-level overrides. LitOpenCode keeps `topP` as its config spelling and projects it to OpenCode's native `top_p` field; this preserves nonreasoning model sampling while Astra's native Responses capability omits unsupported sampling fields.
|
|
26
|
-
- Global route config loads first, project-local route config merges afterward, and effective per-agent fields override category fields. Fresh/reset lead routes use Astra with native `reasoningEffort`. GPT-6 Astra and Sol accept `low`, `medium`, `high`, `xhigh`, `max
|
|
26
|
+
- Global route config loads first, project-local route config merges afterward, and effective per-agent fields override category fields. Fresh/reset lead routes use Astra with native `reasoningEffort`. Each model accepts exactly the efforts OpenCode lists for it: GPT-6 Astra and GPT-6.1 Sol accept `low`, `medium`, `high`, `xhigh`, and `max`; GPT-6 Luna, GPT-6 Sol, and the GPT-5.6 rows accept `none`, `low`, `medium`, `high`, `xhigh`, and `max`. No model accepts `ultra`. A saved route set to `ultra` keeps running as `max`, `litopencode doctor` warns about it, and the next install rewrites it to `max` with a one-line notice after copying the file to `litopencode.json.litopencode-backup`. Astra has no invented `-fast` alias. No-selection installs preserve user route bytes but fail before success output when a managed route is unsafe. Other model IDs remain user-owned custom routes. Existing host entries named `momus` or `litwork-reviewer` preserve explicit routes and receive Astra/xhigh only when route fields are absent. LitOpenCode does not expose Hermes-specific per-subagent overrides.
|
|
27
27
|
- Use `litopencode install --permission-prompt`, `--permission-mode balanced`, or `--yolo` only when you explicitly want LitOpenCode to reduce OpenCode approval prompts. Default installs do not relax permissions. Balanced mode allows routine automation while keeping dangerous bash patterns on ask; YOLO mode writes `permissionMode: "yolo"` and the plugin config hook allows OpenCode `bash`, `edit`, `webfetch`, and `external_directory` globally. In both balanced and YOLO modes, `task` delegation is granted only to execution-capable primary agents; subagents and `lit-plan` stay task-denied so delegation never recurses and the planner cannot select an implementation child. Existing global string/object and per-primary task policies are preserved unless they would weaken those hard deny guards; absent global task policy is not invented, and the runtime lineage guard blocks child task calls even under an explicit global allow. `lit-plan` remains agent-level edit/bash/task `deny`.
|
|
28
28
|
- Restart OpenCode after changing `permissionMode`; OpenCode loads config at startup and running sessions keep the already-loaded permission policy.
|
|
29
29
|
- Planning routes now produce proportionate objective-achievable checklists rather than relying on a later review pass to discover missing execution detail. Each retained item carries action/output/verification, while risky or multi-stage work receives SDD-like gates only when justified. `/review-work` accepts both draft plans (read-only `PASS`/`ITERATE`/`NEEDS-CONTEXT`) and completed work (the existing five-lane review); it never implements a draft plan.
|