@litfamily/litopencode 1.0.31 → 1.0.32
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 +24 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/activation-probe.js +1 -0
- package/dist/activation-prompt-utils.d.ts +1 -1
- package/dist/activation-reentry.d.ts +20 -0
- package/dist/activation-reentry.js +110 -1
- package/dist/activation-routing.d.ts +2 -1
- package/dist/activation-routing.js +100 -11
- package/dist/activation-workflow-prompts.d.ts +4 -0
- package/dist/activation-workflow-prompts.js +30 -0
- package/dist/activation.d.ts +1 -1
- package/dist/activation.js +44 -5
- package/dist/adhd-next.d.ts +51 -0
- package/dist/adhd-next.js +150 -0
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +7 -1
- package/dist/cli/native-skill-integrity.js +1 -1
- package/dist/cli.js +7 -1
- package/dist/commands.d.ts +28 -1
- package/dist/commands.js +48 -1
- package/dist/features.d.ts +80 -1
- package/dist/features.js +105 -1
- package/dist/ledger.js +12 -1
- package/dist/secret-shapes.js +5 -5
- package/dist/skills.d.ts +22 -1
- package/dist/skills.js +36 -0
- package/dist/start-work-typed.d.ts +34 -0
- package/dist/start-work-typed.js +156 -0
- package/dist/state.d.ts +2 -0
- package/dist/state.js +2 -0
- package/dist/tools.js +22 -2
- package/dist/tui/mission-band.d.ts +20 -1
- package/dist/tui/mission-band.js +73 -16
- package/dist/tui/mission-looks.d.ts +38 -0
- package/dist/tui/mission-looks.js +73 -0
- package/dist/tui/mission-model.d.ts +2 -0
- package/dist/tui/mission-model.js +3 -1
- package/dist/tui/mission-rainbow.d.ts +33 -0
- package/dist/tui/mission-rainbow.js +90 -0
- package/dist/tui/mission-source.d.ts +48 -0
- package/dist/tui/mission-source.js +184 -2
- package/dist/tui/mission-text.d.ts +2 -0
- package/dist/tui/mission-text.js +9 -1
- package/dist/tui/mission-threads.d.ts +50 -0
- package/dist/tui/mission-threads.js +119 -0
- package/dist/tui.js +84 -6
- package/dist/uiux-visual-catalog.js +8 -1
- package/dist/v2/session-hooks.js +38 -5
- package/dist/workflow-stack.d.ts +53 -0
- package/dist/workflow-stack.js +196 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/skills/lit-schematic.webp +0 -0
- package/docs/reference-Ko-KR.md +30 -2
- package/docs/reference.md +34 -2
- package/package.json +1 -1
- package/skills/frontend-ui-ux/SKILL.md +3 -2
- package/skills/frontend-ui-ux/kit/components.css +414 -0
- package/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
- package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
- package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
- package/skills/frontend-ui-ux/references/house-styles.md +236 -0
- package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
- package/skills/frontend-ui-ux/references/production.md +3 -1
- package/skills/frontend-ui-ux/references/slop-register.md +4 -4
- package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
- package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
- package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
- package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
- package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
- package/skills/lit-adhd/SKILL.md +208 -0
- package/skills/lit-adhd/references/brief-format.md +43 -0
- package/skills/lit-adhd/references/card-examples.md +82 -0
- package/skills/lit-adhd/references/status-taxonomy.md +29 -0
- package/skills/lit-adhd/scripts/rewind.mjs +835 -0
- package/skills/lit-diagram-drawer/SKILL.md +4 -3
- package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
- package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
- package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
- package/skills/lit-docx/SKILL.md +2 -1
- package/skills/lit-handoff/SKILL.md +2 -0
- package/skills/lit-hijack/SKILL.md +422 -0
- package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
- package/skills/lit-hijack/references/clean-room-build.md +111 -0
- package/skills/lit-hijack/references/distil-and-licence.md +100 -0
- package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
- package/skills/lit-hijack/references/parity-and-report.md +110 -0
- package/skills/lit-hijack/scripts/hijack.mjs +993 -0
- package/skills/lit-plan/SKILL.md +2 -0
- package/skills/lit-pptx/SKILL.md +27 -11
- package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
- package/skills/lit-pptx/references/density-and-fill.md +9 -3
- package/skills/lit-pptx/references/direction-step.md +21 -6
- package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
- package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
- package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
- package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
- package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
- package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/skills/lit-pptx/references/layout-families.md +65 -47
- package/skills/lit-pptx/references/title-treatments.md +83 -134
- package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
- package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
- package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
- package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
- package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
- package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
- package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
- package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
- package/skills/lit-pptx/references/tonalities/night.md +1 -1
- package/skills/lit-pptx/references/tonalities/paper.md +2 -2
- package/skills/lit-pptx/references/tonalities/riso.md +137 -0
- package/skills/lit-pptx/references/tonalities/signal.md +3 -3
- package/skills/lit-pptx/references/tonalities/studio.md +7 -6
- package/skills/lit-pptx/run.mjs +2 -1
- package/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
- package/skills/lit-pptx/scripts/craft_extras.py +56 -3
- package/skills/lit-pptx/scripts/deck_output.py +75 -90
- package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
- package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
- package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
- package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/skills/lit-pptx/scripts/qa_deck.py +10 -11
- package/skills/lit-pptx/scripts/render_pages.py +118 -0
- package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/skills/lit-recap/SKILL.md +4 -0
- package/skills/lit-schematic/SKILL.md +118 -0
- package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/skills/lit-schematic/references/journal-figures.md +72 -0
- package/skills/lit-schematic/references/kit-api.md +134 -0
- package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/skills/lit-schematic/references/verification.md +100 -0
- package/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
- package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/skills/lit-schematic/scripts/render.mjs +233 -0
- package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/skills/lit-scientific-visualization/SKILL.md +27 -4
- package/skills/lit-typographic-motion/SKILL.md +19 -17
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
- package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
- package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
- package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
- package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
- package/skills/lit-typographic-motion/engine/board.mjs +138 -0
- package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
- package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
- package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
- package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
- package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
- package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
- package/skills/lit-typographic-motion/engine/house.mjs +42 -0
- package/skills/lit-typographic-motion/engine/look.mjs +65 -8
- package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
- package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
- package/skills/lit-typographic-motion/engine/score.mjs +594 -0
- package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
- package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
- package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
- package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
- package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
- package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
- package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
- package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
- package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
- package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
- package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
- package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
- package/skills/lit-typographic-motion/engine/text.mjs +1 -1
- package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
- package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/skills/lit-typographic-motion/gate.mjs +18 -2
- package/skills/lit-typographic-motion/probe.mjs +13 -1
- package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
- package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
- package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
- package/skills/lit-typographic-motion/references/cover.md +56 -13
- package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
- package/skills/lit-typographic-motion/references/depth.md +112 -0
- package/skills/lit-typographic-motion/references/runtime.md +21 -6
- package/skills/lit-typographic-motion/references/sound.md +76 -0
- package/skills/lit-typographic-motion/references/stage.md +50 -7
- package/skills/lit-typographic-motion/references/studio.md +162 -0
- package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
- package/skills/lit-typographic-motion/references/treatment.md +108 -9
- package/skills/lit-typographic-motion/render.mjs +60 -3
- package/skills/lit-typographic-motion/styles/paper.json +185 -0
- package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
- package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
- package/skills/lit-typographic-motion/styles/tidal.json +187 -0
- package/skills/litresearch/SKILL.md +2 -0
- package/skills/managed-skill-manifest.json +517 -88
- package/skills/readme-studio/SKILL.md +1 -1
- package/skills/review-work/SKILL.md +2 -0
- package/skills/start-work/SKILL.md +8 -0
- package/skills/workflow-loop/SKILL.md +4 -0
- package/tools/check-pack-payload.mjs +4 -2
- package/tools/payload-substance-parity.json +4 -3
- package/tools/qa-cover-recipes.mjs +4 -3
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { planChecklist, readLitMark, readWorkState } from "./mission-model.js";
|
|
7
|
+
import { isLaneResult, isReviewLane } from "./mission-looks.js";
|
|
7
8
|
import { readSkillUse, skillChips } from "./mission-skills.js";
|
|
9
|
+
import { isRealCount } from "./mission-threads.js";
|
|
10
|
+
import { readSiblingStacksSync, readStackSync } from "../workflow-stack.js";
|
|
8
11
|
const ledgerByteLimit = 1024 * 1024;
|
|
9
12
|
/** The same root the server plugin uses: the worktree unless it is the filesystem root, else the directory. */
|
|
10
13
|
export function missionProjectRoot(paths) {
|
|
@@ -67,7 +70,7 @@ export function createWorkReader() {
|
|
|
67
70
|
return undefined;
|
|
68
71
|
const inside = work.planPath !== "" && path.isAbsolute(work.planPath) && !path.relative(path.resolve(root), work.planPath).startsWith("..");
|
|
69
72
|
const plan = inside ? readPlan(work.planPath) : undefined;
|
|
70
|
-
return { status: work.status, iterations: work.iterations, ...(plan === undefined ? {} : { plan }) };
|
|
73
|
+
return { status: work.status, iterations: work.iterations, ...(plan === undefined ? {} : { plan }), ...(work.boundary === true ? { boundary: true } : {}) };
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
function lstatOrUndefined(file) {
|
|
@@ -235,6 +238,35 @@ export function readMissionInput(state, sessionID, ledger, now, records = {}) {
|
|
|
235
238
|
now
|
|
236
239
|
};
|
|
237
240
|
}
|
|
241
|
+
/** A completed `review-work` call with action `lane`: one lane's reported result. */
|
|
242
|
+
export function readLaneReport(part) {
|
|
243
|
+
if (!isRecord(part) || part.type !== "tool" || part.tool !== "review-work" || !isRecord(part.state) || part.state.status !== "completed")
|
|
244
|
+
return undefined;
|
|
245
|
+
const input = isRecord(part.state.input) ? part.state.input : {};
|
|
246
|
+
if (input.action !== "lane" || !isReviewLane(input.lane) || !isLaneResult(input.result))
|
|
247
|
+
return undefined;
|
|
248
|
+
return { lane: input.lane, result: input.result };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* True when the reply's first line of prose opens with `BLOCKED:`. The ignition mark comes first on the
|
|
252
|
+
* host (a fenced LIT block from experimental.text.complete, and the model's `LIT IGNITED` line), so those
|
|
253
|
+
* lines and blank ones are passed over; anything else ends the search.
|
|
254
|
+
*/
|
|
255
|
+
export function opensBlocked(parts) {
|
|
256
|
+
const text = parts.map((part) => (isRecord(part) && part.type === "text" && typeof part.text === "string" ? part.text : "")).join("\n");
|
|
257
|
+
let fenced = false;
|
|
258
|
+
for (const line of text.split("\n")) {
|
|
259
|
+
const trimmed = line.trim();
|
|
260
|
+
if (trimmed.startsWith("```")) {
|
|
261
|
+
fenced = !fenced;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (fenced || trimmed === "" || trimmed.includes("LIT IGNITED"))
|
|
265
|
+
continue;
|
|
266
|
+
return /^BLOCKED:/u.test(trimmed);
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
238
270
|
/**
|
|
239
271
|
* What the band card shows beside the mission: the session's skill chips, whether a turn is running,
|
|
240
272
|
* context use and git branch. OpenCode keeps each session's last `session.status` ({ type: "busy" },
|
|
@@ -244,23 +276,173 @@ export function readCardInput(state, sessionID) {
|
|
|
244
276
|
const messages = state.session.messages(sessionID);
|
|
245
277
|
const uses = [];
|
|
246
278
|
let since;
|
|
279
|
+
// A new review start clears the lanes, so the border shows only this review's reports.
|
|
280
|
+
let lanes;
|
|
247
281
|
for (const message of messages) {
|
|
248
282
|
const at = num(message.time?.created);
|
|
249
283
|
if (message.role === "user")
|
|
250
284
|
since = at;
|
|
251
285
|
for (const part of state.part(message.id)) {
|
|
286
|
+
const report = readLaneReport(part);
|
|
287
|
+
if (report !== undefined) {
|
|
288
|
+
lanes = { ...lanes, [report.lane]: report.result };
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
252
291
|
const use = readSkillUse(part, at);
|
|
253
292
|
if (use !== undefined)
|
|
254
293
|
uses.push(use);
|
|
294
|
+
const action = isRecord(part) && isRecord(part.state) && isRecord(part.state.input) ? part.state.input.action : undefined;
|
|
295
|
+
if (use?.name === "review-work" && action !== "status" && action !== "lane")
|
|
296
|
+
lanes = {};
|
|
255
297
|
}
|
|
256
298
|
}
|
|
257
299
|
const status = state.session.status(sessionID)?.type;
|
|
258
300
|
const busy = status === "busy" || status === "retry";
|
|
259
301
|
const branch = state.vcs?.branch?.trim();
|
|
302
|
+
const last = messages.at(-1);
|
|
303
|
+
const asked = state.session.permission(sessionID).length > 0 || state.session.question(sessionID).length > 0;
|
|
260
304
|
return {
|
|
261
305
|
chips: skillChips(uses, { since, busy }),
|
|
262
306
|
busy,
|
|
263
307
|
contextPercent: contextPercent(state, messages),
|
|
264
|
-
...(branch === undefined || branch === "" ? {} : { branch })
|
|
308
|
+
...(branch === undefined || branch === "" ? {} : { branch }),
|
|
309
|
+
...(lanes === undefined ? {} : { lanes }),
|
|
310
|
+
...(asked ? { asked } : {}),
|
|
311
|
+
...(last?.role === "user" ? { userSpokeLast: true } : {}),
|
|
312
|
+
...(last?.role === "assistant" && opensBlocked(state.part(last.id)) ? { blockedReply: true } : {})
|
|
265
313
|
};
|
|
266
314
|
}
|
|
315
|
+
export const earlierWorkWindowMs = 7 * 24 * 60 * 60_000;
|
|
316
|
+
const siblingFreshMs = 15 * 60_000;
|
|
317
|
+
const siblingMax = 6;
|
|
318
|
+
const taskLingerMs = 30_000;
|
|
319
|
+
const recentAssistantMessages = 3;
|
|
320
|
+
function isRecord(value) {
|
|
321
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
322
|
+
}
|
|
323
|
+
function countsOf(done, total) {
|
|
324
|
+
return isRealCount(done, total) ? { done, total } : {};
|
|
325
|
+
}
|
|
326
|
+
function taskSources(state, sessionID, now) {
|
|
327
|
+
const sources = [];
|
|
328
|
+
const children = new Set();
|
|
329
|
+
const assistants = state.session.messages(sessionID).filter((message) => message.role === "assistant").slice(-recentAssistantMessages);
|
|
330
|
+
for (const message of assistants) {
|
|
331
|
+
for (const part of state.part(message.id)) {
|
|
332
|
+
if (part.type !== "tool" || part.tool !== "task" || !isRecord(part.state))
|
|
333
|
+
continue;
|
|
334
|
+
const toolState = part.state;
|
|
335
|
+
const status = toolState.status;
|
|
336
|
+
const time = isRecord(toolState.time) ? toolState.time : {};
|
|
337
|
+
const start = typeof time.start === "number" ? time.start : undefined;
|
|
338
|
+
const end = typeof time.end === "number" ? time.end : undefined;
|
|
339
|
+
if (status !== "running" && status !== "completed" && status !== "error")
|
|
340
|
+
continue;
|
|
341
|
+
if (status !== "running" && (end === undefined || now - end > taskLingerMs))
|
|
342
|
+
continue;
|
|
343
|
+
const input = isRecord(toolState.input) ? toolState.input : {};
|
|
344
|
+
const metadata = isRecord(toolState.metadata) ? toolState.metadata : {};
|
|
345
|
+
const child = typeof metadata.sessionId === "string" ? metadata.sessionId : undefined;
|
|
346
|
+
if (child !== undefined)
|
|
347
|
+
children.add(child);
|
|
348
|
+
const todos = child === undefined ? [] : state.session.todo(child).filter((todo) => todo.status !== "cancelled");
|
|
349
|
+
const done = todos.filter((todo) => todo.status === "completed").length;
|
|
350
|
+
const name = typeof input.description === "string" && input.description.trim() !== "" ? input.description.trim() : typeof input.subagent_type === "string" ? input.subagent_type : "subagent";
|
|
351
|
+
// A running task last moved when its own session did; its start time alone would read a long, busy run as idle.
|
|
352
|
+
const moved = child === undefined ? undefined : lastActivity(state, child);
|
|
353
|
+
sources.push({
|
|
354
|
+
kind: "task",
|
|
355
|
+
name,
|
|
356
|
+
status: status === "running" ? "working" : status === "completed" ? "done" : "failed",
|
|
357
|
+
...countsOf(done, todos.length),
|
|
358
|
+
...(start === undefined ? {} : { startedAt: start }),
|
|
359
|
+
touchedAt: end ?? (start === undefined && moved === undefined ? now : Math.max(start ?? 0, moved ?? 0))
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { sources, children };
|
|
364
|
+
}
|
|
365
|
+
/** The latest time any message of a session was created or completed, if it has one. */
|
|
366
|
+
function lastActivity(state, sessionID) {
|
|
367
|
+
let latest;
|
|
368
|
+
for (const message of state.session.messages(sessionID)) {
|
|
369
|
+
for (const at of [message.time?.created, message.time?.completed]) {
|
|
370
|
+
if (typeof at === "number" && Number.isFinite(at) && (latest === undefined || at > latest))
|
|
371
|
+
latest = at;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return latest;
|
|
375
|
+
}
|
|
376
|
+
function stackSources(stack) {
|
|
377
|
+
return (stack?.frames ?? []).map((frame) => ({
|
|
378
|
+
kind: "stack",
|
|
379
|
+
name: frame.skill,
|
|
380
|
+
status: frame.status === "active" ? "working" : "paused",
|
|
381
|
+
...countsOf(frame.done, frame.total),
|
|
382
|
+
...(frame.phase === undefined ? {} : { phase: frame.phase }),
|
|
383
|
+
touchedAt: frame.at
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
function siblingSource(stack) {
|
|
387
|
+
const top = stack.frames.find((frame) => frame.status === "active") ?? stack.frames[stack.frames.length - 1];
|
|
388
|
+
return {
|
|
389
|
+
kind: "session",
|
|
390
|
+
name: `${top.skill} · ${stack.sessionID.slice(-4)}`,
|
|
391
|
+
status: top.status === "active" ? "working" : "paused",
|
|
392
|
+
...countsOf(top.done, top.total),
|
|
393
|
+
...(top.phase === undefined ? {} : { phase: top.phase }),
|
|
394
|
+
touchedAt: stack.updatedAt
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The lanes beside this session's own mission: its workflow stack frames, its running `task` calls and
|
|
399
|
+
* the stacks of other sessions in the workspace that moved in the last fifteen minutes. Only counts
|
|
400
|
+
* that are real totals are carried; the input of a tool call is read for its description alone.
|
|
401
|
+
*/
|
|
402
|
+
export function readThreadSources(state, sessionID, root, now) {
|
|
403
|
+
const tasks = taskSources(state, sessionID, now);
|
|
404
|
+
if (root === undefined)
|
|
405
|
+
return tasks.sources;
|
|
406
|
+
const siblings = readSiblingStacksSync(root, sessionID, { now, freshMs: siblingFreshMs, max: siblingMax }).filter((stack) => !tasks.children.has(stack.sessionID));
|
|
407
|
+
return [...stackSources(readStackSync(root, sessionID)), ...tasks.sources, ...siblings.map(siblingSource)];
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Whether earlier work exists for the band's idle hint and the one suggestion: another session here
|
|
411
|
+
* touched its workflow stack within `withinMs`.
|
|
412
|
+
*/
|
|
413
|
+
/** When the session's last message and its last user prompt were created, from the reactive store. */
|
|
414
|
+
export function messageTimes(state, sessionID) {
|
|
415
|
+
let lastAt;
|
|
416
|
+
let lastPromptAt;
|
|
417
|
+
for (const message of state.session.messages(sessionID)) {
|
|
418
|
+
const created = message.time?.created;
|
|
419
|
+
if (typeof created !== "number")
|
|
420
|
+
continue;
|
|
421
|
+
if (lastAt === undefined || created > lastAt)
|
|
422
|
+
lastAt = created;
|
|
423
|
+
if (message.role === "user" && (lastPromptAt === undefined || created > lastPromptAt))
|
|
424
|
+
lastPromptAt = created;
|
|
425
|
+
}
|
|
426
|
+
return { ...(lastAt === undefined ? {} : { lastAt }), ...(lastPromptAt === undefined ? {} : { lastPromptAt }) };
|
|
427
|
+
}
|
|
428
|
+
/** The way back in is offered after this long without a message in the session. */
|
|
429
|
+
export const adhdBreakMs = 30 * 60_000;
|
|
430
|
+
/** The idle card names lit adhd in a fresh session or after a break, only when earlier work exists. */
|
|
431
|
+
export function wayBackDue(lastActivityAt, now, earlier) {
|
|
432
|
+
return earlier && (lastActivityAt === undefined || now - lastActivityAt >= adhdBreakMs);
|
|
433
|
+
}
|
|
434
|
+
function localDay(at) {
|
|
435
|
+
const date = new Date(at);
|
|
436
|
+
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
|
437
|
+
}
|
|
438
|
+
/** At most one start suggestion per workspace per local day; records the day when it answers true. */
|
|
439
|
+
export function adhdSuggestDue(days, root, now) {
|
|
440
|
+
const day = localDay(now);
|
|
441
|
+
if (days.get(root) === day)
|
|
442
|
+
return false;
|
|
443
|
+
days.set(root, day);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
export function hasEarlierWork(root, sessionID, now, withinMs) {
|
|
447
|
+
return root !== undefined && readSiblingStacksSync(root, sessionID, { now, freshMs: withinMs, max: 1 }).length > 0;
|
|
448
|
+
}
|
|
@@ -53,6 +53,8 @@ export declare function chipBudget(input: {
|
|
|
53
53
|
readonly child: boolean;
|
|
54
54
|
}): number;
|
|
55
55
|
export declare function cellWidth(value: string): number;
|
|
56
|
+
/** Record text without terminal escape sequences or control characters; tabs and newlines become spaces later. */
|
|
57
|
+
export declare function stripControls(value: string): string;
|
|
56
58
|
/** Cuts `value` to at most `max` cells, ending in an ellipsis when anything was dropped. */
|
|
57
59
|
export declare function truncateCells(value: string, max: number): string;
|
|
58
60
|
export declare function padCells(value: string, width: number): string;
|
package/dist/tui/mission-text.js
CHANGED
|
@@ -120,9 +120,17 @@ export function cellWidth(value) {
|
|
|
120
120
|
width += charWidth(char);
|
|
121
121
|
return width;
|
|
122
122
|
}
|
|
123
|
+
// Escape and control sequences (CSI, OSC, C1) from plan rows, todos, branches or commit text would act on
|
|
124
|
+
// the terminal instead of being drawn, so they are removed before any record text reaches a cell.
|
|
125
|
+
const terminalSequence = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b[@-Z\\-_]|\u009b[0-?]*[ -/]*[@-~]|\u009d[^\u0007\u009c]*[\u0007\u009c]?/gu;
|
|
126
|
+
const controlCharacter = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
|
|
127
|
+
/** Record text without terminal escape sequences or control characters; tabs and newlines become spaces later. */
|
|
128
|
+
export function stripControls(value) {
|
|
129
|
+
return value.replace(terminalSequence, "").replace(controlCharacter, "");
|
|
130
|
+
}
|
|
123
131
|
/** Cuts `value` to at most `max` cells, ending in an ellipsis when anything was dropped. */
|
|
124
132
|
export function truncateCells(value, max) {
|
|
125
|
-
const clean = value.replace(/\s+/gu, " ").trim();
|
|
133
|
+
const clean = stripControls(value).replace(/\s+/gu, " ").trim();
|
|
126
134
|
if (cellWidth(clean) <= max)
|
|
127
135
|
return clean;
|
|
128
136
|
if (max <= 0)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Line, type MissionPalette } from "./mission-text.ts";
|
|
2
|
+
export type ThreadStatus = "working" | "paused" | "done" | "failed" | "idle";
|
|
3
|
+
export type ThreadSource = {
|
|
4
|
+
readonly kind: "stack" | "task" | "session";
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly status: ThreadStatus;
|
|
7
|
+
/** Both counts or neither. */
|
|
8
|
+
readonly done?: number;
|
|
9
|
+
readonly total?: number;
|
|
10
|
+
/** A word for what is known, such as the stage a frame is on. */
|
|
11
|
+
readonly phase?: string;
|
|
12
|
+
/** When the lane started, for the elapsed time of a task. */
|
|
13
|
+
readonly startedAt?: number;
|
|
14
|
+
/** When the lane last moved; an old one reads as idle. */
|
|
15
|
+
readonly touchedAt?: number;
|
|
16
|
+
};
|
|
17
|
+
export type ThreadRow = {
|
|
18
|
+
readonly key: string;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly status: ThreadStatus;
|
|
21
|
+
readonly done?: number;
|
|
22
|
+
readonly total?: number;
|
|
23
|
+
readonly facts: string;
|
|
24
|
+
};
|
|
25
|
+
export declare const threadRowCap = 4;
|
|
26
|
+
/** A lane untouched for this long is shown as idle rather than working. */
|
|
27
|
+
export declare const idleAfterMs: number;
|
|
28
|
+
export declare function isRealCount(done: unknown, total: unknown): done is number;
|
|
29
|
+
/**
|
|
30
|
+
* The rows for the sources, or undefined when fewer than two lanes exist: one lane is the session's own
|
|
31
|
+
* work, which the four-row card already shows. A source untouched for five minutes reads as idle.
|
|
32
|
+
*/
|
|
33
|
+
export declare function threadsOf(sources: readonly ThreadSource[], now: number): ThreadRow[] | undefined;
|
|
34
|
+
/** How many card rows the threads take: the fork line, the shown lanes and the `+N more` line. */
|
|
35
|
+
export declare function threadLineCount(rows: readonly ThreadRow[] | undefined): number;
|
|
36
|
+
/** `[▉▉▉░░░░░] 3/10` for a real total, an empty string otherwise. */
|
|
37
|
+
export declare function threadGauge(done: number | undefined, total: number | undefined, cells: number): string;
|
|
38
|
+
/** An indeterminate pulse: five cells that scan while motion is on, one steady frame otherwise. */
|
|
39
|
+
export declare function pulseText(now: number, motion: boolean): string;
|
|
40
|
+
export type ThreadDrawOptions = {
|
|
41
|
+
readonly now: number;
|
|
42
|
+
readonly motion: boolean;
|
|
43
|
+
/** A colour per gauge index while the rainbow plays; the gauge keeps the palette colour otherwise. */
|
|
44
|
+
readonly gaugeColor?: (index: number) => string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The lines under the skill row: `┬ 3 lanes in parallel`, then one `├─`/`└─` line per lane (four at most;
|
|
48
|
+
* `+N more` names the rest). Each line is fitted to `width` by the caller.
|
|
49
|
+
*/
|
|
50
|
+
export declare function threadLines(rows: readonly ThreadRow[], palette: MissionPalette, width: number, options: ThreadDrawOptions): Line[];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Thread rows for the band: the lanes of work that run beside the session's own mission. A lane is a
|
|
2
|
+
// frame on this session's workflow stack (a paused spine under an interjected command), a `task` tool
|
|
3
|
+
// call that runs a subagent, or another session in the same workspace whose stack moved lately. The
|
|
4
|
+
// module is pure: sources go in, rows and drawn lines come out.
|
|
5
|
+
//
|
|
6
|
+
// A gauge needs a real total. Without one a row says what is known in words (how long it has run, which
|
|
7
|
+
// step it is on) and, while it is working, plays a small pulse; it never shows a question mark.
|
|
8
|
+
import { cellWidth, lineWidth, truncateCells } from "./mission-text.js";
|
|
9
|
+
export const threadRowCap = 4;
|
|
10
|
+
/** A lane untouched for this long is shown as idle rather than working. */
|
|
11
|
+
export const idleAfterMs = 5 * 60_000;
|
|
12
|
+
const pulseFrames = ["▓▒░░░", "░▓▒░░", "░░▓▒░", "░░░▓▒", "░░▒▓░", "░▒▓░░"];
|
|
13
|
+
const steadyPulse = "░▒▓▒░";
|
|
14
|
+
const pulseMs = 250;
|
|
15
|
+
const statusWordCells = 7;
|
|
16
|
+
export function isRealCount(done, total) {
|
|
17
|
+
return Number.isInteger(done) && Number.isInteger(total) && total > 0 && done >= 0 && done <= total;
|
|
18
|
+
}
|
|
19
|
+
function minutes(from, now) {
|
|
20
|
+
if (from === undefined || !Number.isFinite(from) || from > now)
|
|
21
|
+
return undefined;
|
|
22
|
+
const total = Math.floor((now - from) / 60_000);
|
|
23
|
+
return total < 1 ? "<1m" : total < 60 ? `${total}m` : `${Math.floor(total / 60)}h${String(total % 60).padStart(2, "0")}`;
|
|
24
|
+
}
|
|
25
|
+
function factsFor(source, status, now) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
if (source.phase !== undefined && source.phase !== "")
|
|
28
|
+
parts.push(source.phase);
|
|
29
|
+
const elapsed = minutes(source.startedAt ?? (status === "working" ? source.touchedAt : undefined), now);
|
|
30
|
+
if (elapsed !== undefined && status === "working")
|
|
31
|
+
parts.push(elapsed);
|
|
32
|
+
if (parts.length === 0)
|
|
33
|
+
parts.push(status === "done" ? "finished" : status === "failed" ? "stopped" : status === "paused" ? "held" : status === "idle" ? "quiet" : "started");
|
|
34
|
+
return parts.join(" · ");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The rows for the sources, or undefined when fewer than two lanes exist: one lane is the session's own
|
|
38
|
+
* work, which the four-row card already shows. A source untouched for five minutes reads as idle.
|
|
39
|
+
*/
|
|
40
|
+
export function threadsOf(sources, now) {
|
|
41
|
+
if (sources.length < 2)
|
|
42
|
+
return undefined;
|
|
43
|
+
return sources.map((source, index) => {
|
|
44
|
+
const status = source.status === "working" && source.touchedAt !== undefined && now - source.touchedAt > idleAfterMs ? "idle" : source.status;
|
|
45
|
+
return {
|
|
46
|
+
key: `${source.kind}:${index}:${source.name}`,
|
|
47
|
+
name: source.name,
|
|
48
|
+
status,
|
|
49
|
+
...(isRealCount(source.done, source.total) ? { done: source.done, total: source.total } : {}),
|
|
50
|
+
facts: factsFor(source, status, now)
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** How many card rows the threads take: the fork line, the shown lanes and the `+N more` line. */
|
|
55
|
+
export function threadLineCount(rows) {
|
|
56
|
+
if (rows === undefined || rows.length === 0)
|
|
57
|
+
return 0;
|
|
58
|
+
return 1 + Math.min(rows.length, threadRowCap) + (rows.length > threadRowCap ? 1 : 0);
|
|
59
|
+
}
|
|
60
|
+
/** `[▉▉▉░░░░░] 3/10` for a real total, an empty string otherwise. */
|
|
61
|
+
export function threadGauge(done, total, cells) {
|
|
62
|
+
if (!isRealCount(done, total) || cells < 1)
|
|
63
|
+
return "";
|
|
64
|
+
const full = Math.round((done / total) * cells);
|
|
65
|
+
return `[${"▉".repeat(full)}${"░".repeat(cells - full)}] ${done}/${total}`;
|
|
66
|
+
}
|
|
67
|
+
/** An indeterminate pulse: five cells that scan while motion is on, one steady frame otherwise. */
|
|
68
|
+
export function pulseText(now, motion) {
|
|
69
|
+
return motion ? pulseFrames[Math.floor(now / pulseMs) % pulseFrames.length] : steadyPulse;
|
|
70
|
+
}
|
|
71
|
+
const statusWords = { working: "working", paused: "paused", done: "done", failed: "failed", idle: "idle" };
|
|
72
|
+
function statusColor(status, palette) {
|
|
73
|
+
if (status === "working")
|
|
74
|
+
return palette.info;
|
|
75
|
+
if (status === "done")
|
|
76
|
+
return palette.success;
|
|
77
|
+
if (status === "failed")
|
|
78
|
+
return palette.error;
|
|
79
|
+
if (status === "paused")
|
|
80
|
+
return palette.warning;
|
|
81
|
+
return palette.muted;
|
|
82
|
+
}
|
|
83
|
+
function rowLine(row, index, last, palette, width, options) {
|
|
84
|
+
const branch = { text: last ? "└─ " : "├─ ", fg: palette.borderSubtle };
|
|
85
|
+
const word = { text: statusWords[row.status].padEnd(statusWordCells), fg: statusColor(row.status, palette), bold: row.status === "working" };
|
|
86
|
+
const gap = { text: " ", fg: palette.muted };
|
|
87
|
+
const gaugeColor = options.gaugeColor?.(index) ?? palette.accent;
|
|
88
|
+
const real = threadGauge(row.done, row.total, 8);
|
|
89
|
+
const trailing = (room) => {
|
|
90
|
+
if (real !== "" && cellWidth(real) <= room)
|
|
91
|
+
return [{ text: real, fg: gaugeColor }];
|
|
92
|
+
const facts = truncateCells(row.facts, Math.max(0, room - (row.status === "working" ? 6 : 0)));
|
|
93
|
+
const pulse = row.status === "working" ? [{ text: ` ${pulseText(options.now, options.motion)}`, fg: gaugeColor }] : [];
|
|
94
|
+
return [{ text: facts, fg: palette.muted }, ...pulse];
|
|
95
|
+
};
|
|
96
|
+
const fixed = lineWidth([branch, gap, word, gap]);
|
|
97
|
+
const room = Math.max(0, width - fixed);
|
|
98
|
+
const nameRoom = Math.max(6, Math.min(24, Math.floor(room / 2)));
|
|
99
|
+
const name = { text: truncateCells(row.name, nameRoom), fg: row.status === "idle" || row.status === "done" ? palette.muted : palette.text };
|
|
100
|
+
const pad = { text: " ".repeat(Math.max(0, Math.min(24, nameRoom) - cellWidth(name.text))), fg: palette.muted };
|
|
101
|
+
const left = [branch, name, pad, gap, word, gap];
|
|
102
|
+
return [...left, ...trailing(Math.max(0, width - lineWidth(left)))];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The lines under the skill row: `┬ 3 lanes in parallel`, then one `├─`/`└─` line per lane (four at most;
|
|
106
|
+
* `+N more` names the rest). Each line is fitted to `width` by the caller.
|
|
107
|
+
*/
|
|
108
|
+
export function threadLines(rows, palette, width, options) {
|
|
109
|
+
if (rows.length === 0)
|
|
110
|
+
return [];
|
|
111
|
+
const shown = rows.slice(0, threadRowCap);
|
|
112
|
+
const more = rows.length - shown.length;
|
|
113
|
+
const fork = [{ text: "┬ ", fg: palette.borderSubtle }, { text: `${rows.length} lanes in parallel`, fg: palette.muted }];
|
|
114
|
+
const lines = [fork];
|
|
115
|
+
shown.forEach((row, index) => lines.push(rowLine(row, index, index === shown.length - 1 && more === 0, palette, width, options)));
|
|
116
|
+
if (more > 0)
|
|
117
|
+
lines.push([{ text: "└─ ", fg: palette.borderSubtle }, { text: `+${more} more`, fg: palette.muted }]);
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
package/dist/tui.js
CHANGED
|
@@ -4,9 +4,12 @@ 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, createWorkReader, missionProjectRoot, readCardInput, readLatestResearchRun, readMissionInput } from "./tui/mission-source.js";
|
|
7
|
+
import { adhdSuggestDue, createLedgerReader, createWorkReader, earlierWorkWindowMs, hasEarlierWork, messageTimes, missionProjectRoot, wayBackDue, readCardInput, readLatestResearchRun, readMissionInput, readThreadSources } from "./tui/mission-source.js";
|
|
8
8
|
import { chipShimmer, idleChipMotion, planChipMotion } from "./tui/mission-skills.js";
|
|
9
9
|
import { bandCard, readBandOptions, readNotifyOptions, statusCard } from "./tui/mission-band.js";
|
|
10
|
+
import { activeSkillLook, turnGlow, waitingOnYou } from "./tui/mission-looks.js";
|
|
11
|
+
import { rainbowActive, rainbowAllowed, rainbowAt, rainbowShows, rainbowTickMs } from "./tui/mission-rainbow.js";
|
|
12
|
+
import { threadLineCount, threadsOf } from "./tui/mission-threads.js";
|
|
10
13
|
export const litOpenCodeTuiPluginId = "litopencode";
|
|
11
14
|
const homeLineWidth = 69;
|
|
12
15
|
const refreshDelayMs = 80;
|
|
@@ -50,6 +53,10 @@ const tui = async (api, options) => {
|
|
|
50
53
|
let refreshTimer;
|
|
51
54
|
let pollTimer;
|
|
52
55
|
let liveTimer;
|
|
56
|
+
let bandTimer;
|
|
57
|
+
const suggested = new Set();
|
|
58
|
+
const suggestedDays = new Map();
|
|
59
|
+
const colorAllowed = rainbowAllowed(motionEnabled, process.env.NO_COLOR);
|
|
53
60
|
const state = () => api.state;
|
|
54
61
|
const palette = () => paletteFromTheme(api.theme.current, api.theme.mode());
|
|
55
62
|
// OpenCode keeps the sidebar choice in its KV store under "sidebar" ("auto" or "hide"); a child
|
|
@@ -64,6 +71,30 @@ const tui = async (api, options) => {
|
|
|
64
71
|
const session = api.state.session.get(sessionID);
|
|
65
72
|
return typeof session?.parentID === "string" && session.parentID !== "";
|
|
66
73
|
}
|
|
74
|
+
// lit-adhd plays the rainbow and a working lane without a real total plays a pulse; both step at 4 Hz
|
|
75
|
+
// and only while they are on screen, and neither runs under reduced motion or NO_COLOR.
|
|
76
|
+
function rainbowOn(entry, now) {
|
|
77
|
+
return colorAllowed && rainbowShows(rainbowActive(entry.view, entry.card.chips, entry.card.busy), entry.rainbowSeen, entry.lastPromptAt, now);
|
|
78
|
+
}
|
|
79
|
+
function bandStepping(entry) {
|
|
80
|
+
if (!colorAllowed)
|
|
81
|
+
return false;
|
|
82
|
+
if (entry.waiting || rainbowOn(entry, Date.now()))
|
|
83
|
+
return true;
|
|
84
|
+
return entry.card.threads?.some((row) => row.status === "working" && row.done === undefined) === true;
|
|
85
|
+
}
|
|
86
|
+
function syncBandTimer() {
|
|
87
|
+
const needed = [...sessions.values()].some(bandStepping);
|
|
88
|
+
if (needed && bandTimer === undefined)
|
|
89
|
+
bandTimer = setInterval(() => {
|
|
90
|
+
setClock(Date.now());
|
|
91
|
+
syncBandTimer();
|
|
92
|
+
}, rainbowTickMs);
|
|
93
|
+
if (!needed && bandTimer !== undefined) {
|
|
94
|
+
clearInterval(bandTimer);
|
|
95
|
+
bandTimer = undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
67
98
|
function anyMoving(now) {
|
|
68
99
|
for (const entry of sessions.values())
|
|
69
100
|
if (sampleMotion(entry.motion, now).moving || chipShimmer(entry.chipMotion, now) !== undefined)
|
|
@@ -103,16 +134,48 @@ const tui = async (api, options) => {
|
|
|
103
134
|
.notify({ title: "🔥 LIT complete", message: notice.message, notification: notify.desktop, sound: notify.sound ? { name: "done" } : false })
|
|
104
135
|
.catch(() => undefined);
|
|
105
136
|
}
|
|
137
|
+
// The card keeps the tallest lane count it reached while lanes are live or a turn runs, and drops back to
|
|
138
|
+
// four rows only once everything has settled, so the prompt does not jump between repaints.
|
|
139
|
+
function holdRows(held, rows, busy) {
|
|
140
|
+
if (rows > 0)
|
|
141
|
+
return Math.max(held, rows);
|
|
142
|
+
return busy ? held : 0;
|
|
143
|
+
}
|
|
144
|
+
// One toast per session, for a fresh root session in a workspace that has earlier work to pick up.
|
|
145
|
+
function suggestAdhd(sessionID) {
|
|
146
|
+
if (suggested.has(sessionID) || isChildSession(sessionID))
|
|
147
|
+
return;
|
|
148
|
+
suggested.add(sessionID);
|
|
149
|
+
const source = state();
|
|
150
|
+
const created = api.state.session.get(sessionID)?.time?.created;
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
if (source.session.messages(sessionID).length > 0 || typeof created !== "number" || now - created > 2 * 60_000)
|
|
153
|
+
return;
|
|
154
|
+
const root = missionProjectRoot(source.path);
|
|
155
|
+
if (root === undefined || !hasEarlierWork(root, sessionID, now, earlierWorkWindowMs) || !adhdSuggestDue(suggestedDays, root, now))
|
|
156
|
+
return;
|
|
157
|
+
api.ui.toast({ variant: "info", title: "🔥 LIT", message: "Earlier work here. Say lit adhd to pick up where you left off.", duration: toastMs });
|
|
158
|
+
}
|
|
106
159
|
function refresh(sessionID) {
|
|
107
160
|
const now = Date.now();
|
|
108
161
|
const source = state();
|
|
109
162
|
const root = missionProjectRoot(source.path);
|
|
110
163
|
const ledger = readLedger(root);
|
|
111
|
-
const
|
|
112
|
-
const
|
|
164
|
+
const work = readWork(root, sessionID);
|
|
165
|
+
const next = buildMissionView(readMissionInput(source, sessionID, ledger, now, { work, research: readLatestResearchRun(root) }));
|
|
166
|
+
const threads = threadsOf(readThreadSources(source, sessionID, root, now), now);
|
|
167
|
+
const times = messageTimes(source, sessionID);
|
|
168
|
+
const card = {
|
|
169
|
+
...readCardInput(source, sessionID),
|
|
170
|
+
...(threads === undefined ? {} : { threads }),
|
|
171
|
+
...(wayBackDue(times.lastAt, now, hasEarlierWork(root, sessionID, now, earlierWorkWindowMs)) ? { earlier: true } : {})
|
|
172
|
+
};
|
|
113
173
|
const entry = sessions.get(sessionID);
|
|
114
174
|
const viewChanged = entry === undefined || JSON.stringify(entry.view) !== JSON.stringify(next);
|
|
115
|
-
|
|
175
|
+
const rainbowSeen = rainbowActive(next, card.chips, card.busy) ? now : entry?.rainbowSeen;
|
|
176
|
+
const waiting = waitingOnYou(next, card.busy, { asked: card.asked === true, userSpokeLast: card.userSpokeLast === true, blockedReply: card.blockedReply === true, boundary: work?.boundary === true });
|
|
177
|
+
const promptMoved = entry?.lastPromptAt !== times.lastPromptAt;
|
|
178
|
+
if (!viewChanged && !promptMoved && JSON.stringify(entry.card) === JSON.stringify(card) && entry.rainbowSeen === rainbowSeen && entry.waiting === waiting)
|
|
116
179
|
return;
|
|
117
180
|
if (entry !== undefined && viewChanged) {
|
|
118
181
|
const notices = missionNotices(entry.view, next);
|
|
@@ -127,12 +190,17 @@ const tui = async (api, options) => {
|
|
|
127
190
|
view: next,
|
|
128
191
|
card,
|
|
129
192
|
motion: viewChanged ? planMotion(entry?.view, next, now, entry?.motion ?? idleMotion, motionEnabled) : entry.motion,
|
|
130
|
-
chipMotion: planChipMotion(entry?.card.chips, card.chips, now, entry?.chipMotion ?? idleChipMotion, motionEnabled)
|
|
193
|
+
chipMotion: planChipMotion(entry?.card.chips, card.chips, now, entry?.chipMotion ?? idleChipMotion, motionEnabled),
|
|
194
|
+
reserve: holdRows(entry?.reserve ?? 0, threadLineCount(threads), card.busy),
|
|
195
|
+
...(rainbowSeen === undefined ? {} : { rainbowSeen }),
|
|
196
|
+
...(times.lastPromptAt === undefined ? {} : { lastPromptAt: times.lastPromptAt }),
|
|
197
|
+
waiting
|
|
131
198
|
});
|
|
132
199
|
setClock(now);
|
|
133
200
|
setRevision(revision() + 1);
|
|
134
201
|
ensureFrames();
|
|
135
202
|
syncLiveTick();
|
|
203
|
+
syncBandTimer();
|
|
136
204
|
}
|
|
137
205
|
function refreshTracked() {
|
|
138
206
|
refreshTimer = undefined;
|
|
@@ -143,7 +211,7 @@ const tui = async (api, options) => {
|
|
|
143
211
|
// Ledger writes from a terminal or another session fire no TUI event, so an active mission
|
|
144
212
|
// re-reads goals.json (one lstat while unchanged) every two seconds. Idle sessions do not poll.
|
|
145
213
|
function syncPoll() {
|
|
146
|
-
const active = [...sessions.values()].some((entry) => entry.view.active);
|
|
214
|
+
const active = [...sessions.values()].some((entry) => entry.view.active || entry.card.threads !== undefined);
|
|
147
215
|
if (active && pollTimer === undefined)
|
|
148
216
|
pollTimer = setInterval(refreshTracked, ledgerPollMs);
|
|
149
217
|
if (!active && pollTimer !== undefined) {
|
|
@@ -164,6 +232,7 @@ const tui = async (api, options) => {
|
|
|
164
232
|
queueMicrotask(() => {
|
|
165
233
|
refresh(sessionID);
|
|
166
234
|
syncPoll();
|
|
235
|
+
suggestAdhd(sessionID);
|
|
167
236
|
});
|
|
168
237
|
}
|
|
169
238
|
// Opening a session loads its messages and parts over HTTP, which fires no bus event. Reading the
|
|
@@ -209,6 +278,8 @@ const tui = async (api, options) => {
|
|
|
209
278
|
clearInterval(pollTimer);
|
|
210
279
|
if (liveTimer !== undefined)
|
|
211
280
|
clearInterval(liveTimer);
|
|
281
|
+
if (bandTimer !== undefined)
|
|
282
|
+
clearInterval(bandTimer);
|
|
212
283
|
sessions.clear();
|
|
213
284
|
});
|
|
214
285
|
api.slots.register({
|
|
@@ -322,6 +393,13 @@ const tui = async (api, options) => {
|
|
|
322
393
|
frame: snapshot.frame,
|
|
323
394
|
chips: snapshot.card.chips,
|
|
324
395
|
busy: snapshot.card.busy,
|
|
396
|
+
motion: motionEnabled,
|
|
397
|
+
reserve: snapshot.reserve,
|
|
398
|
+
...(snapshot.card.threads === undefined ? {} : { threads: snapshot.card.threads }),
|
|
399
|
+
...(snapshot.card.earlier === true ? { earlier: true } : {}),
|
|
400
|
+
...(snapshot.waiting ? { turn: { glow: turnGlow(snapshot.now, colorAllowed) } } : {}),
|
|
401
|
+
...(rainbowOn(snapshot, snapshot.now) ? { rainbow: rainbowAt(snapshot.now) } : {}),
|
|
402
|
+
...(snapshot.card.lanes !== undefined && activeSkillLook(snapshot.view, snapshot.card.chips, snapshot.card.busy) === "review" ? { lanes: snapshot.card.lanes } : {}),
|
|
325
403
|
...(shimmer === undefined ? {} : { shimmer }),
|
|
326
404
|
figures: { contextPercent: snapshot.card.contextPercent, ...(snapshot.card.branch === undefined ? {} : { branch: snapshot.card.branch }), inLane: snapshot.inLane },
|
|
327
405
|
...(last === undefined ? {} : { last })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const frontendUiUxFeature = {
|
|
2
2
|
id: "frontend-ui-ux",
|
|
3
3
|
title: "Frontend UI/UX",
|
|
4
|
-
summary: "Builds and inspects authorized interfaces using an evolving beta2 contract
|
|
4
|
+
summary: "Builds and inspects authorized interfaces using an evolving beta2 contract, offline design intelligence and a locked house style with a rich default.",
|
|
5
5
|
bindings: [
|
|
6
6
|
{
|
|
7
7
|
kind: "config",
|
|
@@ -15,6 +15,12 @@ export const frontendUiUxFeature = {
|
|
|
15
15
|
surface: "OpenCode native skill frontend-ui-ux",
|
|
16
16
|
description: "Ships focused contract references plus an exact verified 167-file canonical design library. The canonical route is inert and separate from the normalized retrieval dataset."
|
|
17
17
|
},
|
|
18
|
+
{
|
|
19
|
+
kind: "config",
|
|
20
|
+
id: "skills/frontend-ui-ux/kit",
|
|
21
|
+
surface: "OpenCode native skill frontend-ui-ux",
|
|
22
|
+
description: "Ships five house styles, the token-only component sheet and three layout starters; scripts/design-lock.mjs writes the design lock, tokens and fonts into a new project, and the probe checks the rendered page against it (DS-001 to DS-008)."
|
|
23
|
+
},
|
|
18
24
|
{
|
|
19
25
|
kind: "cli",
|
|
20
26
|
id: "litopencode install",
|
|
@@ -29,6 +35,7 @@ export const frontendUiUxFeature = {
|
|
|
29
35
|
}
|
|
30
36
|
],
|
|
31
37
|
verification: [
|
|
38
|
+
"node --test test/frontend-design-lock.test.mjs",
|
|
32
39
|
"node --test --test-name-pattern='uiux\\.' test/runtime-skills.test.mjs test/packed-artifact.test.mjs",
|
|
33
40
|
"node --test --test-name-pattern='integration\\.installed-nested-assets' test/cli-install-surface.test.mjs",
|
|
34
41
|
"npm run check:managed-skill-manifest",
|