@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.
Files changed (267) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/dist/activation-probe.js +1 -0
  5. package/dist/activation-prompt-utils.d.ts +1 -1
  6. package/dist/activation-reentry.d.ts +20 -0
  7. package/dist/activation-reentry.js +110 -1
  8. package/dist/activation-routing.d.ts +2 -1
  9. package/dist/activation-routing.js +100 -11
  10. package/dist/activation-workflow-prompts.d.ts +4 -0
  11. package/dist/activation-workflow-prompts.js +30 -0
  12. package/dist/activation.d.ts +1 -1
  13. package/dist/activation.js +44 -5
  14. package/dist/adhd-next.d.ts +51 -0
  15. package/dist/adhd-next.js +150 -0
  16. package/dist/cli/managed-skill-assets.d.ts +1 -1
  17. package/dist/cli/managed-skill-assets.js +7 -1
  18. package/dist/cli/native-skill-integrity.js +1 -1
  19. package/dist/cli.js +7 -1
  20. package/dist/commands.d.ts +28 -1
  21. package/dist/commands.js +48 -1
  22. package/dist/features.d.ts +80 -1
  23. package/dist/features.js +105 -1
  24. package/dist/ledger.js +12 -1
  25. package/dist/secret-shapes.js +5 -5
  26. package/dist/skills.d.ts +22 -1
  27. package/dist/skills.js +36 -0
  28. package/dist/start-work-typed.d.ts +34 -0
  29. package/dist/start-work-typed.js +156 -0
  30. package/dist/state.d.ts +2 -0
  31. package/dist/state.js +2 -0
  32. package/dist/tools.js +22 -2
  33. package/dist/tui/mission-band.d.ts +20 -1
  34. package/dist/tui/mission-band.js +73 -16
  35. package/dist/tui/mission-looks.d.ts +38 -0
  36. package/dist/tui/mission-looks.js +73 -0
  37. package/dist/tui/mission-model.d.ts +2 -0
  38. package/dist/tui/mission-model.js +3 -1
  39. package/dist/tui/mission-rainbow.d.ts +33 -0
  40. package/dist/tui/mission-rainbow.js +90 -0
  41. package/dist/tui/mission-source.d.ts +48 -0
  42. package/dist/tui/mission-source.js +184 -2
  43. package/dist/tui/mission-text.d.ts +2 -0
  44. package/dist/tui/mission-text.js +9 -1
  45. package/dist/tui/mission-threads.d.ts +50 -0
  46. package/dist/tui/mission-threads.js +119 -0
  47. package/dist/tui.js +84 -6
  48. package/dist/uiux-visual-catalog.js +8 -1
  49. package/dist/v2/session-hooks.js +38 -5
  50. package/dist/workflow-stack.d.ts +53 -0
  51. package/dist/workflow-stack.js +196 -0
  52. package/docs/assets/readme/badge-version.svg +1 -1
  53. package/docs/assets/skills/lit-schematic.webp +0 -0
  54. package/docs/reference-Ko-KR.md +30 -2
  55. package/docs/reference.md +34 -2
  56. package/package.json +1 -1
  57. package/skills/frontend-ui-ux/SKILL.md +3 -2
  58. package/skills/frontend-ui-ux/kit/components.css +414 -0
  59. package/skills/frontend-ui-ux/kit/fonts.json +22 -0
  60. package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  61. package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  62. package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  63. package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  64. package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  65. package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  66. package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  67. package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  68. package/skills/frontend-ui-ux/kit/ui.js +226 -0
  69. package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
  70. package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
  71. package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
  72. package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
  73. package/skills/frontend-ui-ux/references/house-styles.md +236 -0
  74. package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
  75. package/skills/frontend-ui-ux/references/production.md +3 -1
  76. package/skills/frontend-ui-ux/references/slop-register.md +4 -4
  77. package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  78. package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  79. package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
  80. package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
  81. package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
  82. package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
  83. package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
  84. package/skills/lit-adhd/SKILL.md +208 -0
  85. package/skills/lit-adhd/references/brief-format.md +43 -0
  86. package/skills/lit-adhd/references/card-examples.md +82 -0
  87. package/skills/lit-adhd/references/status-taxonomy.md +29 -0
  88. package/skills/lit-adhd/scripts/rewind.mjs +835 -0
  89. package/skills/lit-diagram-drawer/SKILL.md +4 -3
  90. package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  91. package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  92. package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  93. package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  94. package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  95. package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
  96. package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
  97. package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
  98. package/skills/lit-docx/SKILL.md +2 -1
  99. package/skills/lit-handoff/SKILL.md +2 -0
  100. package/skills/lit-hijack/SKILL.md +422 -0
  101. package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
  102. package/skills/lit-hijack/references/clean-room-build.md +111 -0
  103. package/skills/lit-hijack/references/distil-and-licence.md +100 -0
  104. package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
  105. package/skills/lit-hijack/references/parity-and-report.md +110 -0
  106. package/skills/lit-hijack/scripts/hijack.mjs +993 -0
  107. package/skills/lit-plan/SKILL.md +2 -0
  108. package/skills/lit-pptx/SKILL.md +27 -11
  109. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  110. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  111. package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  112. package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
  113. package/skills/lit-pptx/references/density-and-fill.md +9 -3
  114. package/skills/lit-pptx/references/direction-step.md +21 -6
  115. package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
  116. package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
  117. package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
  118. package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
  119. package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
  120. package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  121. package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  122. package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
  123. package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
  124. package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  125. package/skills/lit-pptx/references/layout-families.md +65 -47
  126. package/skills/lit-pptx/references/title-treatments.md +83 -134
  127. package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
  128. package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
  129. package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
  130. package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
  131. package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
  132. package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
  133. package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
  134. package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
  135. package/skills/lit-pptx/references/tonalities/night.md +1 -1
  136. package/skills/lit-pptx/references/tonalities/paper.md +2 -2
  137. package/skills/lit-pptx/references/tonalities/riso.md +137 -0
  138. package/skills/lit-pptx/references/tonalities/signal.md +3 -3
  139. package/skills/lit-pptx/references/tonalities/studio.md +7 -6
  140. package/skills/lit-pptx/run.mjs +2 -1
  141. package/skills/lit-pptx/scripts/compile-deck.js +1 -1
  142. package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
  143. package/skills/lit-pptx/scripts/craft_extras.py +56 -3
  144. package/skills/lit-pptx/scripts/deck_output.py +75 -90
  145. package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
  146. package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
  147. package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
  148. package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  149. package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
  150. package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  151. package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
  152. package/skills/lit-pptx/scripts/qa_deck.py +10 -11
  153. package/skills/lit-pptx/scripts/render_pages.py +118 -0
  154. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
  155. package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  156. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
  157. package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  158. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
  159. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
  160. package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  161. package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  162. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
  163. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
  164. package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  165. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
  166. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
  167. package/skills/lit-recap/SKILL.md +4 -0
  168. package/skills/lit-schematic/SKILL.md +118 -0
  169. package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  170. package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  171. package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  172. package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  173. package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  174. package/skills/lit-schematic/references/isometric-craft.md +101 -0
  175. package/skills/lit-schematic/references/journal-figures.md +72 -0
  176. package/skills/lit-schematic/references/kit-api.md +134 -0
  177. package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  178. package/skills/lit-schematic/references/pid-drafting.md +103 -0
  179. package/skills/lit-schematic/references/pid-symbols.md +163 -0
  180. package/skills/lit-schematic/references/verification.md +100 -0
  181. package/skills/lit-schematic/scripts/doctor.mjs +57 -0
  182. package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  183. package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
  184. package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  185. package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  186. package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  187. package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  188. package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  189. package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  190. package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  191. package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  192. package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  193. package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  194. package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  195. package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  196. package/skills/lit-schematic/scripts/motion.mjs +129 -0
  197. package/skills/lit-schematic/scripts/qa.mjs +92 -0
  198. package/skills/lit-schematic/scripts/render.mjs +233 -0
  199. package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  200. package/skills/lit-scientific-visualization/SKILL.md +27 -4
  201. package/skills/lit-typographic-motion/SKILL.md +19 -17
  202. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
  203. package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
  204. package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
  205. package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
  206. package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
  207. package/skills/lit-typographic-motion/engine/board.mjs +138 -0
  208. package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
  209. package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
  210. package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
  211. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
  212. package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
  213. package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
  214. package/skills/lit-typographic-motion/engine/house.mjs +42 -0
  215. package/skills/lit-typographic-motion/engine/look.mjs +65 -8
  216. package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
  217. package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
  218. package/skills/lit-typographic-motion/engine/score.mjs +594 -0
  219. package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
  220. package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
  221. package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
  222. package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
  223. package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
  224. package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
  225. package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
  226. package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
  227. package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
  228. package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
  229. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
  230. package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
  231. package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
  232. package/skills/lit-typographic-motion/engine/text.mjs +1 -1
  233. package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
  234. package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  235. package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  236. package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  237. package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  238. package/skills/lit-typographic-motion/gate.mjs +18 -2
  239. package/skills/lit-typographic-motion/probe.mjs +13 -1
  240. package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
  241. package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
  242. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
  243. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
  244. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
  245. package/skills/lit-typographic-motion/references/cover.md +56 -13
  246. package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
  247. package/skills/lit-typographic-motion/references/depth.md +112 -0
  248. package/skills/lit-typographic-motion/references/runtime.md +21 -6
  249. package/skills/lit-typographic-motion/references/sound.md +76 -0
  250. package/skills/lit-typographic-motion/references/stage.md +50 -7
  251. package/skills/lit-typographic-motion/references/studio.md +162 -0
  252. package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
  253. package/skills/lit-typographic-motion/references/treatment.md +108 -9
  254. package/skills/lit-typographic-motion/render.mjs +60 -3
  255. package/skills/lit-typographic-motion/styles/paper.json +185 -0
  256. package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
  257. package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
  258. package/skills/lit-typographic-motion/styles/tidal.json +187 -0
  259. package/skills/litresearch/SKILL.md +2 -0
  260. package/skills/managed-skill-manifest.json +517 -88
  261. package/skills/readme-studio/SKILL.md +1 -1
  262. package/skills/review-work/SKILL.md +2 -0
  263. package/skills/start-work/SKILL.md +8 -0
  264. package/skills/workflow-loop/SKILL.md +4 -0
  265. package/tools/check-pack-payload.mjs +4 -2
  266. package/tools/payload-substance-parity.json +4 -3
  267. 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;
@@ -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 next = buildMissionView(readMissionInput(source, sessionID, ledger, now, { work: readWork(root, sessionID), research: readLatestResearchRun(root) }));
112
- const card = readCardInput(source, sessionID);
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
- if (!viewChanged && JSON.stringify(entry.card) === JSON.stringify(card))
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 and offline design intelligence.",
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",