@litfamily/litopencode 1.0.31 → 1.0.33
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 +38 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/TRADEMARK.md +12 -0
- package/dist/activation-primary-prompts.js +5 -1
- 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/agents/defaults.js +1 -1
- 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 +58 -5
- package/dist/durable-plan.d.ts +5 -1
- package/dist/durable-plan.js +14 -2
- package/dist/features.d.ts +80 -1
- package/dist/features.js +105 -1
- package/dist/hooks.js +3 -0
- package/dist/ledger.js +12 -1
- package/dist/plan-scope-roots.d.ts +23 -0
- package/dist/plan-scope-roots.js +116 -0
- package/dist/restart-notice.d.ts +7 -0
- package/dist/restart-notice.js +40 -0
- package/dist/secret-shapes.js +5 -5
- package/dist/server.js +4 -0
- package/dist/session-liveness.d.ts +20 -0
- package/dist/session-liveness.js +99 -0
- 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 +167 -0
- package/dist/state.d.ts +3 -0
- package/dist/state.js +3 -0
- package/dist/tools.js +22 -2
- package/dist/tui/mission-band.d.ts +22 -1
- package/dist/tui/mission-band.js +75 -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 +49 -0
- package/dist/tui/mission-source.js +217 -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 +58 -0
- package/dist/tui/mission-threads.js +128 -0
- package/dist/tui.js +112 -7
- package/dist/uiux-visual-catalog.js +8 -1
- package/dist/v2/events.js +3 -0
- 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 +48 -3
- package/docs/reference.md +54 -3
- 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 +209 -0
- package/skills/lit-adhd/references/brief-format.md +46 -0
- package/skills/lit-adhd/references/card-examples.md +103 -0
- package/skills/lit-adhd/references/status-taxonomy.md +34 -0
- package/skills/lit-adhd/scripts/rewind.mjs +933 -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 +8 -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 +5 -2
- package/tools/payload-substance-parity.json +4 -3
- package/tools/qa-cover-recipes.mjs +4 -3
- package/tools/version-manifests.json +2 -2
package/dist/tui.js
CHANGED
|
@@ -4,15 +4,21 @@ 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";
|
|
13
|
+
import { readPackageMetadata } from "./cli/json.js";
|
|
14
|
+
import { readInstalledVersions, restartVersion } from "./restart-notice.js";
|
|
10
15
|
export const litOpenCodeTuiPluginId = "litopencode";
|
|
11
16
|
const homeLineWidth = 69;
|
|
12
17
|
const refreshDelayMs = 80;
|
|
13
18
|
const ledgerPollMs = 2000;
|
|
14
19
|
const toastMs = 4000;
|
|
15
20
|
const liveTickMs = 1000;
|
|
21
|
+
const restartCheckMs = 10 * 60_000;
|
|
16
22
|
const trackedSessionLimit = 4;
|
|
17
23
|
const refreshEvents = [
|
|
18
24
|
"message.updated",
|
|
@@ -50,6 +56,25 @@ const tui = async (api, options) => {
|
|
|
50
56
|
let refreshTimer;
|
|
51
57
|
let pollTimer;
|
|
52
58
|
let liveTimer;
|
|
59
|
+
let bandTimer;
|
|
60
|
+
const suggested = new Set();
|
|
61
|
+
const suggestedDays = new Map();
|
|
62
|
+
const colorAllowed = rainbowAllowed(motionEnabled, process.env.NO_COLOR);
|
|
63
|
+
// The version this start loaded; an install newer than it shows `restart to <v>` and one toast.
|
|
64
|
+
const loaded = await readPackageMetadata().catch(() => undefined);
|
|
65
|
+
const [restartTo, setRestartTo] = createSignal(undefined);
|
|
66
|
+
let restartTimer;
|
|
67
|
+
async function checkRestart() {
|
|
68
|
+
if (loaded === undefined)
|
|
69
|
+
return;
|
|
70
|
+
const target = restartVersion(loaded.version, await readInstalledVersions(loaded.packageRoot).catch(() => []));
|
|
71
|
+
if (target === undefined || target === restartTo())
|
|
72
|
+
return;
|
|
73
|
+
if (restartTo() === undefined) {
|
|
74
|
+
api.ui.toast({ variant: "info", title: "🔥 LIT", message: `LitOpenCode ${target} is installed. Restart OpenCode to load it.`, duration: toastMs });
|
|
75
|
+
}
|
|
76
|
+
setRestartTo(target);
|
|
77
|
+
}
|
|
53
78
|
const state = () => api.state;
|
|
54
79
|
const palette = () => paletteFromTheme(api.theme.current, api.theme.mode());
|
|
55
80
|
// OpenCode keeps the sidebar choice in its KV store under "sidebar" ("auto" or "hide"); a child
|
|
@@ -64,6 +89,30 @@ const tui = async (api, options) => {
|
|
|
64
89
|
const session = api.state.session.get(sessionID);
|
|
65
90
|
return typeof session?.parentID === "string" && session.parentID !== "";
|
|
66
91
|
}
|
|
92
|
+
// lit-adhd plays the rainbow and a working lane without a real total plays a pulse; both step at 4 Hz
|
|
93
|
+
// and only while they are on screen, and neither runs under reduced motion or NO_COLOR.
|
|
94
|
+
function rainbowOn(entry, now) {
|
|
95
|
+
return colorAllowed && rainbowShows(rainbowActive(entry.view, entry.card.chips, entry.card.busy), entry.rainbowSeen, entry.lastPromptAt, now);
|
|
96
|
+
}
|
|
97
|
+
function bandStepping(entry) {
|
|
98
|
+
if (!colorAllowed)
|
|
99
|
+
return false;
|
|
100
|
+
if (entry.waiting || rainbowOn(entry, Date.now()))
|
|
101
|
+
return true;
|
|
102
|
+
return entry.card.threads?.some((row) => row.status === "working" && row.done === undefined) === true;
|
|
103
|
+
}
|
|
104
|
+
function syncBandTimer() {
|
|
105
|
+
const needed = [...sessions.values()].some(bandStepping);
|
|
106
|
+
if (needed && bandTimer === undefined)
|
|
107
|
+
bandTimer = setInterval(() => {
|
|
108
|
+
setClock(Date.now());
|
|
109
|
+
syncBandTimer();
|
|
110
|
+
}, rainbowTickMs);
|
|
111
|
+
if (!needed && bandTimer !== undefined) {
|
|
112
|
+
clearInterval(bandTimer);
|
|
113
|
+
bandTimer = undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
67
116
|
function anyMoving(now) {
|
|
68
117
|
for (const entry of sessions.values())
|
|
69
118
|
if (sampleMotion(entry.motion, now).moving || chipShimmer(entry.chipMotion, now) !== undefined)
|
|
@@ -103,16 +152,48 @@ const tui = async (api, options) => {
|
|
|
103
152
|
.notify({ title: "🔥 LIT complete", message: notice.message, notification: notify.desktop, sound: notify.sound ? { name: "done" } : false })
|
|
104
153
|
.catch(() => undefined);
|
|
105
154
|
}
|
|
155
|
+
// The card keeps the tallest lane count it reached while lanes are live or a turn runs, and drops back to
|
|
156
|
+
// four rows only once everything has settled, so the prompt does not jump between repaints.
|
|
157
|
+
function holdRows(held, rows, busy) {
|
|
158
|
+
if (rows > 0)
|
|
159
|
+
return Math.max(held, rows);
|
|
160
|
+
return busy ? held : 0;
|
|
161
|
+
}
|
|
162
|
+
// One toast per session, for a fresh root session in a workspace that has earlier work to pick up.
|
|
163
|
+
function suggestAdhd(sessionID) {
|
|
164
|
+
if (suggested.has(sessionID) || isChildSession(sessionID))
|
|
165
|
+
return;
|
|
166
|
+
suggested.add(sessionID);
|
|
167
|
+
const source = state();
|
|
168
|
+
const created = api.state.session.get(sessionID)?.time?.created;
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
if (source.session.messages(sessionID).length > 0 || typeof created !== "number" || now - created > 2 * 60_000)
|
|
171
|
+
return;
|
|
172
|
+
const root = missionProjectRoot(source.path);
|
|
173
|
+
if (root === undefined || !hasEarlierWork(root, sessionID, now, earlierWorkWindowMs) || !adhdSuggestDue(suggestedDays, root, now))
|
|
174
|
+
return;
|
|
175
|
+
api.ui.toast({ variant: "info", title: "🔥 LIT", message: "Earlier work here. Say lit adhd to pick up where you left off.", duration: toastMs });
|
|
176
|
+
}
|
|
106
177
|
function refresh(sessionID) {
|
|
107
178
|
const now = Date.now();
|
|
108
179
|
const source = state();
|
|
109
180
|
const root = missionProjectRoot(source.path);
|
|
110
181
|
const ledger = readLedger(root);
|
|
111
|
-
const
|
|
112
|
-
const
|
|
182
|
+
const work = readWork(root, sessionID);
|
|
183
|
+
const next = buildMissionView(readMissionInput(source, sessionID, ledger, now, { work, research: readLatestResearchRun(root) }));
|
|
184
|
+
const threads = threadsOf(readThreadSources(source, sessionID, root, now), now);
|
|
185
|
+
const times = messageTimes(source, sessionID);
|
|
186
|
+
const card = {
|
|
187
|
+
...readCardInput(source, sessionID),
|
|
188
|
+
...(threads === undefined ? {} : { threads }),
|
|
189
|
+
...(wayBackDue(times.lastAt, now, hasEarlierWork(root, sessionID, now, earlierWorkWindowMs)) ? { earlier: true } : {})
|
|
190
|
+
};
|
|
113
191
|
const entry = sessions.get(sessionID);
|
|
114
192
|
const viewChanged = entry === undefined || JSON.stringify(entry.view) !== JSON.stringify(next);
|
|
115
|
-
|
|
193
|
+
const rainbowSeen = rainbowActive(next, card.chips, card.busy) ? now : entry?.rainbowSeen;
|
|
194
|
+
const waiting = waitingOnYou(next, card.busy, { asked: card.asked === true, userSpokeLast: card.userSpokeLast === true, blockedReply: card.blockedReply === true, boundary: work?.boundary === true });
|
|
195
|
+
const promptMoved = entry?.lastPromptAt !== times.lastPromptAt;
|
|
196
|
+
if (!viewChanged && !promptMoved && JSON.stringify(entry.card) === JSON.stringify(card) && entry.rainbowSeen === rainbowSeen && entry.waiting === waiting)
|
|
116
197
|
return;
|
|
117
198
|
if (entry !== undefined && viewChanged) {
|
|
118
199
|
const notices = missionNotices(entry.view, next);
|
|
@@ -127,12 +208,17 @@ const tui = async (api, options) => {
|
|
|
127
208
|
view: next,
|
|
128
209
|
card,
|
|
129
210
|
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)
|
|
211
|
+
chipMotion: planChipMotion(entry?.card.chips, card.chips, now, entry?.chipMotion ?? idleChipMotion, motionEnabled),
|
|
212
|
+
reserve: holdRows(entry?.reserve ?? 0, threadLineCount(threads), card.busy),
|
|
213
|
+
...(rainbowSeen === undefined ? {} : { rainbowSeen }),
|
|
214
|
+
...(times.lastPromptAt === undefined ? {} : { lastPromptAt: times.lastPromptAt }),
|
|
215
|
+
waiting
|
|
131
216
|
});
|
|
132
217
|
setClock(now);
|
|
133
218
|
setRevision(revision() + 1);
|
|
134
219
|
ensureFrames();
|
|
135
220
|
syncLiveTick();
|
|
221
|
+
syncBandTimer();
|
|
136
222
|
}
|
|
137
223
|
function refreshTracked() {
|
|
138
224
|
refreshTimer = undefined;
|
|
@@ -143,7 +229,7 @@ const tui = async (api, options) => {
|
|
|
143
229
|
// Ledger writes from a terminal or another session fire no TUI event, so an active mission
|
|
144
230
|
// re-reads goals.json (one lstat while unchanged) every two seconds. Idle sessions do not poll.
|
|
145
231
|
function syncPoll() {
|
|
146
|
-
const active = [...sessions.values()].some((entry) => entry.view.active);
|
|
232
|
+
const active = [...sessions.values()].some((entry) => entry.view.active || entry.card.threads !== undefined);
|
|
147
233
|
if (active && pollTimer === undefined)
|
|
148
234
|
pollTimer = setInterval(refreshTracked, ledgerPollMs);
|
|
149
235
|
if (!active && pollTimer !== undefined) {
|
|
@@ -164,6 +250,7 @@ const tui = async (api, options) => {
|
|
|
164
250
|
queueMicrotask(() => {
|
|
165
251
|
refresh(sessionID);
|
|
166
252
|
syncPoll();
|
|
253
|
+
suggestAdhd(sessionID);
|
|
167
254
|
});
|
|
168
255
|
}
|
|
169
256
|
// Opening a session loads its messages and parts over HTTP, which fires no bus event. Reading the
|
|
@@ -200,7 +287,11 @@ const tui = async (api, options) => {
|
|
|
200
287
|
for (const type of refreshEvents) {
|
|
201
288
|
api.event.on(type, scheduleRefresh);
|
|
202
289
|
}
|
|
290
|
+
void checkRestart();
|
|
291
|
+
restartTimer = setInterval(() => void checkRestart(), restartCheckMs);
|
|
203
292
|
api.lifecycle.onDispose(() => {
|
|
293
|
+
if (restartTimer !== undefined)
|
|
294
|
+
clearInterval(restartTimer);
|
|
204
295
|
if (frameTimer !== undefined)
|
|
205
296
|
clearInterval(frameTimer);
|
|
206
297
|
if (refreshTimer !== undefined)
|
|
@@ -209,6 +300,8 @@ const tui = async (api, options) => {
|
|
|
209
300
|
clearInterval(pollTimer);
|
|
210
301
|
if (liveTimer !== undefined)
|
|
211
302
|
clearInterval(liveTimer);
|
|
303
|
+
if (bandTimer !== undefined)
|
|
304
|
+
clearInterval(bandTimer);
|
|
212
305
|
sessions.clear();
|
|
213
306
|
});
|
|
214
307
|
api.slots.register({
|
|
@@ -322,8 +415,20 @@ const tui = async (api, options) => {
|
|
|
322
415
|
frame: snapshot.frame,
|
|
323
416
|
chips: snapshot.card.chips,
|
|
324
417
|
busy: snapshot.card.busy,
|
|
418
|
+
motion: motionEnabled,
|
|
419
|
+
reserve: snapshot.reserve,
|
|
420
|
+
...(snapshot.card.threads === undefined ? {} : { threads: snapshot.card.threads }),
|
|
421
|
+
...(snapshot.card.earlier === true ? { earlier: true } : {}),
|
|
422
|
+
...(snapshot.waiting ? { turn: { glow: turnGlow(snapshot.now, colorAllowed) } } : {}),
|
|
423
|
+
...(rainbowOn(snapshot, snapshot.now) ? { rainbow: rainbowAt(snapshot.now) } : {}),
|
|
424
|
+
...(snapshot.card.lanes !== undefined && activeSkillLook(snapshot.view, snapshot.card.chips, snapshot.card.busy) === "review" ? { lanes: snapshot.card.lanes } : {}),
|
|
325
425
|
...(shimmer === undefined ? {} : { shimmer }),
|
|
326
|
-
figures: {
|
|
426
|
+
figures: {
|
|
427
|
+
contextPercent: snapshot.card.contextPercent,
|
|
428
|
+
...(snapshot.card.branch === undefined ? {} : { branch: snapshot.card.branch }),
|
|
429
|
+
inLane: snapshot.inLane,
|
|
430
|
+
...(restartTo() === undefined ? {} : { restartTo: restartTo() })
|
|
431
|
+
},
|
|
327
432
|
...(last === undefined ? {} : { last })
|
|
328
433
|
}, colors, width);
|
|
329
434
|
const rows = lines.map(lineElement);
|
|
@@ -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",
|
package/dist/v2/events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAutoHandoff } from "../auto-handoff-hooks.js";
|
|
2
2
|
import { LifecycleConflictError, LifecycleSafetyError, createBoundedAuthorityLifecycle, parseFencedProgress } from "../bounded-authority.js";
|
|
3
|
+
import { markLiveness } from "../session-liveness.js";
|
|
3
4
|
function isRecord(value) {
|
|
4
5
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
6
|
}
|
|
@@ -100,6 +101,7 @@ export function createV2Events(options) {
|
|
|
100
101
|
if (step.sessionID === sessionID)
|
|
101
102
|
steps.delete(messageID);
|
|
102
103
|
options.onSessionDeleted(sessionID);
|
|
104
|
+
await markLiveness(options.projectRoot, sessionID, "ended");
|
|
103
105
|
await toHandoff("session.deleted", { info: { id: sessionID } });
|
|
104
106
|
return;
|
|
105
107
|
case "session.step.started": {
|
|
@@ -138,6 +140,7 @@ export function createV2Events(options) {
|
|
|
138
140
|
await toHandoff("session.status", { sessionID, status: data.status });
|
|
139
141
|
return;
|
|
140
142
|
case "session.idle":
|
|
143
|
+
await markLiveness(options.projectRoot, sessionID, "idle");
|
|
141
144
|
await toHandoff("session.idle", { sessionID });
|
|
142
145
|
return;
|
|
143
146
|
case "session.compaction.ended":
|
package/dist/v2/session-hooks.js
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
// than from LitOpenCode's own instructions (local knowledge, a start-work receipt, an auto-handoff setting
|
|
7
7
|
// reply) rides the latest user message instead, the channel it had on OpenCode 1.
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import {
|
|
9
|
+
import { typedPromptReply } from "../adhd-next.js";
|
|
10
|
+
import { createActivationMemory, decideReentry, recordActivation } from "../activation-reentry.js";
|
|
10
11
|
import { detectChatActivationMode, promptForChatActivationMode } from "../activation-routing.js";
|
|
11
|
-
import { startWorkPromptInjection } from "../activation-workflow-prompts.js";
|
|
12
|
+
import { adhdSessionNote, litAdhdPromptInjection, startWorkPromptInjection } from "../activation-workflow-prompts.js";
|
|
12
13
|
import { autoHandoffSettingText } from "../auto-handoff.js";
|
|
13
14
|
import { applyTrustedStartWorkDirective, parseStartWorkLifecycleDirective } from "../bounded-authority-hooks.js";
|
|
14
15
|
import { queryKnowledge } from "../knowledge.js";
|
|
16
|
+
import { startTypedWork, typedStartArgument } from "../start-work-typed.js";
|
|
15
17
|
import { createLitGoalOperations } from "../ledger.js";
|
|
16
18
|
import { applyCompactionRuleReset, applyStaticRuleInjection } from "../rules/hooks.js";
|
|
17
19
|
import { isReadOnlyWorkflowFamilyPlan } from "../workflow-families.js";
|
|
@@ -125,13 +127,42 @@ export function createV2SessionHooks(options) {
|
|
|
125
127
|
catch { // no-excuse-ok: catch -- local relevance must never block the user message.
|
|
126
128
|
// A malformed or unreadable knowledge store stays silent instead of inventing context.
|
|
127
129
|
}
|
|
130
|
+
const typed = await typedPromptReply(options.projectRoot, event.sessionID, input, activationMemory, options.config.boundedAuthority);
|
|
131
|
+
if (typed !== undefined) {
|
|
132
|
+
state.turn.push(typed.text);
|
|
133
|
+
event.metadata = { ...(event.metadata ?? {}), litopencode: { mode: typed.mode, trigger: "lit", source: "prompt", typed: typed.kind } };
|
|
134
|
+
if (typed.continued)
|
|
135
|
+
options.onActivation?.(event.sessionID, typed.mode);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// An installed alias file makes the host expand /lit-adhd (or /lit-rewind) itself, so the prompt
|
|
139
|
+
// arrives as the command body with no plugin mark; it still runs the brief and takes its look.
|
|
140
|
+
if (input.trimStart().startsWith(litAdhdPromptInjection.trim())) {
|
|
141
|
+
state.turn.push(adhdSessionNote(event.sessionID));
|
|
142
|
+
event.metadata = { ...(event.metadata ?? {}), litopencode: { mode: "lit-adhd", trigger: "lit", source: "alias" } };
|
|
143
|
+
options.onActivation?.(event.sessionID, "lit-adhd");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
128
146
|
const mode = detectChatActivationMode(input, session.agent);
|
|
129
147
|
if (mode === undefined)
|
|
130
148
|
return;
|
|
131
149
|
const trigger = mode === "lit-handoff" ? "handoff" : mode === "lit-scientific-visualization" ? mode : "lit";
|
|
132
150
|
let activation = promptForChatActivationMode(mode, input);
|
|
151
|
+
if (mode === "lit-adhd")
|
|
152
|
+
activation = `${activation}\n${adhdSessionNote(event.sessionID)}`;
|
|
133
153
|
if (mode === "start-work" && session.agent !== "lit-plan") {
|
|
134
154
|
const directive = parseStartWorkLifecycleDirective(input, { chat: true });
|
|
155
|
+
const planArgument = directive === undefined ? typedStartArgument(input) : undefined;
|
|
156
|
+
if (planArgument !== undefined) {
|
|
157
|
+
const started = await startTypedWork({
|
|
158
|
+
projectRoot: options.projectRoot,
|
|
159
|
+
sessionID: event.sessionID,
|
|
160
|
+
...(planArgument === "" ? {} : { plan: planArgument }),
|
|
161
|
+
options: options.config.boundedAuthority
|
|
162
|
+
});
|
|
163
|
+
if (started.quiet !== true)
|
|
164
|
+
state.user.push(started.text);
|
|
165
|
+
}
|
|
135
166
|
if (directive?.action === "resume") {
|
|
136
167
|
try {
|
|
137
168
|
const receipt = await applyTrustedStartWorkDirective(options.projectRoot, event.sessionID, directive, options.config.boundedAuthority);
|
|
@@ -160,15 +191,17 @@ export function createV2SessionHooks(options) {
|
|
|
160
191
|
activation = `${reentry.line}\n${activation}`;
|
|
161
192
|
else if (reentry.kind === "confirm")
|
|
162
193
|
activation = `${activation}\n${reentry.line}`;
|
|
194
|
+
if (reentry.kind !== "confirm" && mode !== "lit-handoff" && mode !== "lit-scientific-visualization") {
|
|
195
|
+
const recorded = await recordActivation({ memory: activationMemory, projectRoot: options.projectRoot, sessionID: event.sessionID, mode });
|
|
196
|
+
if (recorded.line !== undefined)
|
|
197
|
+
activation = `${activation}\n${recorded.line}`;
|
|
198
|
+
}
|
|
163
199
|
state.activation = activation;
|
|
164
200
|
event.metadata = {
|
|
165
201
|
...(event.metadata ?? {}),
|
|
166
202
|
litopencode: { mode, trigger, source: "prompt", ...(reentry.kind === "full" ? {} : { reentry: reentry.kind }) }
|
|
167
203
|
};
|
|
168
204
|
options.onActivation?.(event.sessionID, mode);
|
|
169
|
-
if (reentry.kind !== "confirm" && mode !== "lit-handoff" && mode !== "lit-scientific-visualization") {
|
|
170
|
-
activationMemory.remember(event.sessionID, mode);
|
|
171
|
-
}
|
|
172
205
|
if (mode === "lit-task" || mode === "lit-recap" || mode === "lit-brainstorm" || isReadOnlyWorkflowFamilyPlan(mode))
|
|
173
206
|
return;
|
|
174
207
|
try {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const workflowStackSchema = "litopencode.workflow-stack/v1";
|
|
2
|
+
export type FrameStatus = "active" | "paused";
|
|
3
|
+
export type StackFrame = {
|
|
4
|
+
readonly skill: string;
|
|
5
|
+
readonly status: FrameStatus;
|
|
6
|
+
readonly at: number;
|
|
7
|
+
readonly phase?: string;
|
|
8
|
+
/** Both counts or neither: an unknown total is never stored as zero. */
|
|
9
|
+
readonly done?: number;
|
|
10
|
+
readonly total?: number;
|
|
11
|
+
};
|
|
12
|
+
export type WorkflowStack = {
|
|
13
|
+
readonly schema: typeof workflowStackSchema;
|
|
14
|
+
readonly sessionID: string;
|
|
15
|
+
readonly frames: readonly StackFrame[];
|
|
16
|
+
readonly updatedAt: number;
|
|
17
|
+
};
|
|
18
|
+
export type FrameProgress = {
|
|
19
|
+
readonly phase?: string;
|
|
20
|
+
readonly done?: number;
|
|
21
|
+
readonly total?: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function isSpineSkill(skill: string): boolean;
|
|
24
|
+
export declare function emptyStack(sessionID: string): WorkflowStack;
|
|
25
|
+
/** A new spine skill ends whatever the stack held: the user moved on to a different discipline. */
|
|
26
|
+
export declare function startSpine(stack: WorkflowStack, skill: string, now: number): WorkflowStack;
|
|
27
|
+
/** Records the spine's progress without pausing it, so a later pause or a card read has real numbers. */
|
|
28
|
+
export declare function pauseSpine(stack: WorkflowStack, now: number, progress?: FrameProgress): WorkflowStack;
|
|
29
|
+
/**
|
|
30
|
+
* Starts a side skill. An active spine is paused under it, keeping its stage and counts; a second side
|
|
31
|
+
* skill replaces the first. With no spine the side skill stands alone.
|
|
32
|
+
*/
|
|
33
|
+
export declare function pushSide(stack: WorkflowStack, skill: string, now: number, progress: FrameProgress | undefined): WorkflowStack;
|
|
34
|
+
/** Closes the side frame and makes the paused spine active again at the stage it was paused in. */
|
|
35
|
+
export declare function resumeSpine(stack: WorkflowStack, now: number): {
|
|
36
|
+
readonly stack: WorkflowStack;
|
|
37
|
+
readonly resumed?: StackFrame;
|
|
38
|
+
};
|
|
39
|
+
export declare function stackFile(projectRoot: string, sessionID: string): string;
|
|
40
|
+
/** The session's stack, or undefined when the id is unsafe, the file is missing, a link, oversized or malformed. */
|
|
41
|
+
export declare function readStack(projectRoot: string, sessionID: string): Promise<WorkflowStack | undefined>;
|
|
42
|
+
export declare function writeStack(projectRoot: string, stack: WorkflowStack): Promise<void>;
|
|
43
|
+
export type SiblingOptions = {
|
|
44
|
+
readonly now: number;
|
|
45
|
+
readonly freshMs: number;
|
|
46
|
+
readonly max: number;
|
|
47
|
+
};
|
|
48
|
+
/** Other sessions' stacks that moved recently, for the thread rows. Links and the session's own file are skipped. */
|
|
49
|
+
export declare function readSiblingStacks(projectRoot: string, selfID: string, options: SiblingOptions): Promise<readonly WorkflowStack[]>;
|
|
50
|
+
/** The TUI reads on its own refresh and cannot await: the same checks as `readStack`, synchronously. */
|
|
51
|
+
export declare function readStackSync(projectRoot: string, sessionID: string): WorkflowStack | undefined;
|
|
52
|
+
/** Synchronous twin of `readSiblingStacks`. */
|
|
53
|
+
export declare function readSiblingStacksSync(projectRoot: string, selfID: string, options: SiblingOptions): readonly WorkflowStack[];
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRuntimePaths } from "./state.js";
|
|
5
|
+
// A session's workflow stack: the discipline it is running (the spine) and, while another lit command
|
|
6
|
+
// is interjected, the paused spine underneath it. An interjected command pauses the spine and never
|
|
7
|
+
// erases it, so `lit continue` returns to the same stage and the same k/n. Transitions are pure; the
|
|
8
|
+
// store keeps one small JSON file per session so the TUI band and a restarted plugin read the same
|
|
9
|
+
// record.
|
|
10
|
+
export const workflowStackSchema = "litopencode.workflow-stack/v1";
|
|
11
|
+
const spineSkills = new Set(["lit-loop", "lit-plan", "start-work", "lit-task"]);
|
|
12
|
+
const safeSessionID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
|
|
13
|
+
const stackByteLimit = 64 * 1024;
|
|
14
|
+
const frameLimit = 4;
|
|
15
|
+
export function isSpineSkill(skill) {
|
|
16
|
+
return spineSkills.has(skill);
|
|
17
|
+
}
|
|
18
|
+
export function emptyStack(sessionID) {
|
|
19
|
+
return { schema: workflowStackSchema, sessionID, frames: [], updatedAt: 0 };
|
|
20
|
+
}
|
|
21
|
+
function counts(progress) {
|
|
22
|
+
const done = progress?.done;
|
|
23
|
+
const total = progress?.total;
|
|
24
|
+
return Number.isInteger(done) && Number.isInteger(total) && total > 0 && done >= 0 && done <= total
|
|
25
|
+
? { done: done, total: total }
|
|
26
|
+
: {};
|
|
27
|
+
}
|
|
28
|
+
function frame(skill, status, at, progress) {
|
|
29
|
+
return { skill, status, at, ...(progress?.phase === undefined ? {} : { phase: progress.phase }), ...counts(progress) };
|
|
30
|
+
}
|
|
31
|
+
function withFrames(stack, frames, now) {
|
|
32
|
+
return { ...stack, frames: frames.slice(-frameLimit), updatedAt: now };
|
|
33
|
+
}
|
|
34
|
+
/** A new spine skill ends whatever the stack held: the user moved on to a different discipline. */
|
|
35
|
+
export function startSpine(stack, skill, now) {
|
|
36
|
+
return withFrames(stack, [frame(skill, "active", now)], now);
|
|
37
|
+
}
|
|
38
|
+
/** Records the spine's progress without pausing it, so a later pause or a card read has real numbers. */
|
|
39
|
+
export function pauseSpine(stack, now, progress) {
|
|
40
|
+
const [spine, ...rest] = stack.frames;
|
|
41
|
+
if (spine === undefined || !isSpineSkill(spine.skill))
|
|
42
|
+
return stack;
|
|
43
|
+
return withFrames(stack, [frame(spine.skill, "paused", spine.at, { phase: progress?.phase ?? spine.phase, done: progress?.done ?? spine.done, total: progress?.total ?? spine.total }), ...rest], now);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Starts a side skill. An active spine is paused under it, keeping its stage and counts; a second side
|
|
47
|
+
* skill replaces the first. With no spine the side skill stands alone.
|
|
48
|
+
*/
|
|
49
|
+
export function pushSide(stack, skill, now, progress) {
|
|
50
|
+
const spine = stack.frames.find((candidate) => isSpineSkill(candidate.skill));
|
|
51
|
+
if (spine === undefined)
|
|
52
|
+
return withFrames(stack, [frame(skill, "active", now)], now);
|
|
53
|
+
const paused = pauseSpine({ ...stack, frames: [spine] }, now, progress).frames[0];
|
|
54
|
+
return withFrames(stack, [paused, frame(skill, "active", now)], now);
|
|
55
|
+
}
|
|
56
|
+
/** Closes the side frame and makes the paused spine active again at the stage it was paused in. */
|
|
57
|
+
export function resumeSpine(stack, now) {
|
|
58
|
+
const spine = stack.frames.find((candidate) => isSpineSkill(candidate.skill) && candidate.status === "paused");
|
|
59
|
+
if (spine === undefined)
|
|
60
|
+
return { stack };
|
|
61
|
+
const resumed = { ...spine, status: "active" };
|
|
62
|
+
return { stack: withFrames(stack, [resumed], now), resumed };
|
|
63
|
+
}
|
|
64
|
+
export function stackFile(projectRoot, sessionID) {
|
|
65
|
+
if (!safeSessionID.test(sessionID))
|
|
66
|
+
throw new Error("workflow stack session id must match [A-Za-z0-9_-]");
|
|
67
|
+
return path.join(createRuntimePaths(projectRoot).workflowStackDir, `${sessionID}.json`);
|
|
68
|
+
}
|
|
69
|
+
function isFrame(value) {
|
|
70
|
+
if (typeof value !== "object" || value === null)
|
|
71
|
+
return false;
|
|
72
|
+
const record = value;
|
|
73
|
+
const optionalInt = (field) => field === undefined || (Number.isInteger(field) && field >= 0);
|
|
74
|
+
return typeof record.skill === "string" && (record.status === "active" || record.status === "paused") && Number.isFinite(record.at) &&
|
|
75
|
+
(record.phase === undefined || typeof record.phase === "string") && optionalInt(record.done) && optionalInt(record.total);
|
|
76
|
+
}
|
|
77
|
+
async function readRegular(file) {
|
|
78
|
+
try {
|
|
79
|
+
const stat = await fs.lstat(file);
|
|
80
|
+
if (!stat.isFile() || stat.size > stackByteLimit)
|
|
81
|
+
return undefined;
|
|
82
|
+
return await fs.readFile(file, "utf8");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function parseStack(text, sessionID) {
|
|
89
|
+
if (text === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
let value;
|
|
92
|
+
try {
|
|
93
|
+
value = JSON.parse(text);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value !== "object" || value === null)
|
|
99
|
+
return undefined;
|
|
100
|
+
const record = value;
|
|
101
|
+
if (record.schema !== workflowStackSchema || record.sessionID !== sessionID || !Array.isArray(record.frames) || !record.frames.every(isFrame) || !Number.isFinite(record.updatedAt))
|
|
102
|
+
return undefined;
|
|
103
|
+
return { schema: workflowStackSchema, sessionID, frames: record.frames.slice(-frameLimit), updatedAt: record.updatedAt };
|
|
104
|
+
}
|
|
105
|
+
/** The session's stack, or undefined when the id is unsafe, the file is missing, a link, oversized or malformed. */
|
|
106
|
+
export async function readStack(projectRoot, sessionID) {
|
|
107
|
+
if (!safeSessionID.test(sessionID))
|
|
108
|
+
return undefined;
|
|
109
|
+
return parseStack(await readRegular(stackFile(projectRoot, sessionID)), sessionID);
|
|
110
|
+
}
|
|
111
|
+
export async function writeStack(projectRoot, stack) {
|
|
112
|
+
const file = stackFile(projectRoot, stack.sessionID);
|
|
113
|
+
const dir = path.dirname(file);
|
|
114
|
+
await fs.mkdir(dir, { recursive: true });
|
|
115
|
+
const dirStat = await fs.lstat(dir);
|
|
116
|
+
if (!dirStat.isDirectory())
|
|
117
|
+
throw new Error("workflow stack directory is not a directory");
|
|
118
|
+
try {
|
|
119
|
+
if ((await fs.lstat(file)).isSymbolicLink())
|
|
120
|
+
throw new Error("workflow stack file is a link");
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error.code !== "ENOENT")
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
127
|
+
await fs.writeFile(temp, `${JSON.stringify(stack)}\n`, { mode: 0o600 });
|
|
128
|
+
await fs.rename(temp, file);
|
|
129
|
+
}
|
|
130
|
+
/** Other sessions' stacks that moved recently, for the thread rows. Links and the session's own file are skipped. */
|
|
131
|
+
export async function readSiblingStacks(projectRoot, selfID, options) {
|
|
132
|
+
const dir = createRuntimePaths(projectRoot).workflowStackDir;
|
|
133
|
+
let names;
|
|
134
|
+
try {
|
|
135
|
+
names = await fs.readdir(dir);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const found = [];
|
|
141
|
+
for (const name of names.sort()) {
|
|
142
|
+
if (!name.endsWith(".json"))
|
|
143
|
+
continue;
|
|
144
|
+
const sessionID = name.slice(0, -".json".length);
|
|
145
|
+
if (sessionID === selfID || !safeSessionID.test(sessionID))
|
|
146
|
+
continue;
|
|
147
|
+
const stack = parseStack(await readRegular(path.join(dir, name)), sessionID);
|
|
148
|
+
if (stack === undefined || stack.frames.length === 0 || options.now - stack.updatedAt > options.freshMs)
|
|
149
|
+
continue;
|
|
150
|
+
found.push(stack);
|
|
151
|
+
if (found.length >= options.max)
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
function readRegularSync(file) {
|
|
157
|
+
try {
|
|
158
|
+
const stat = lstatSync(file);
|
|
159
|
+
return stat.isFile() && stat.size <= stackByteLimit ? readFileSync(file, "utf8") : undefined;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** The TUI reads on its own refresh and cannot await: the same checks as `readStack`, synchronously. */
|
|
166
|
+
export function readStackSync(projectRoot, sessionID) {
|
|
167
|
+
if (!safeSessionID.test(sessionID))
|
|
168
|
+
return undefined;
|
|
169
|
+
return parseStack(readRegularSync(stackFile(projectRoot, sessionID)), sessionID);
|
|
170
|
+
}
|
|
171
|
+
/** Synchronous twin of `readSiblingStacks`. */
|
|
172
|
+
export function readSiblingStacksSync(projectRoot, selfID, options) {
|
|
173
|
+
const dir = createRuntimePaths(projectRoot).workflowStackDir;
|
|
174
|
+
let names;
|
|
175
|
+
try {
|
|
176
|
+
names = readdirSync(dir);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const found = [];
|
|
182
|
+
for (const name of names.sort()) {
|
|
183
|
+
if (!name.endsWith(".json"))
|
|
184
|
+
continue;
|
|
185
|
+
const sessionID = name.slice(0, -".json".length);
|
|
186
|
+
if (sessionID === selfID || !safeSessionID.test(sessionID))
|
|
187
|
+
continue;
|
|
188
|
+
const stack = parseStack(readRegularSync(path.join(dir, name)), sessionID);
|
|
189
|
+
if (stack === undefined || stack.frames.length === 0 || options.now - stack.updatedAt > options.freshMs)
|
|
190
|
+
continue;
|
|
191
|
+
found.push(stack);
|
|
192
|
+
if (found.length >= options.max)
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
return found;
|
|
196
|
+
}
|
|
@@ -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.33"><title>release: 1.0.33</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.33</text></g></svg>
|
|
Binary file
|