@turri-dev/cli 0.0.0-stage → 0.1.2-next.48dacfc

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 (315) hide show
  1. package/.pi/themes/turri-dark.json +80 -0
  2. package/.pi/themes/turri-light.json +82 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -2
  5. package/assets/jellyfish/dark-00.png +0 -0
  6. package/assets/jellyfish/dark-01.png +0 -0
  7. package/assets/jellyfish/dark-02.png +0 -0
  8. package/assets/jellyfish/dark-03.png +0 -0
  9. package/assets/jellyfish/dark-04.png +0 -0
  10. package/assets/jellyfish/dark-05.png +0 -0
  11. package/assets/jellyfish/dark-06.png +0 -0
  12. package/assets/jellyfish/dark-07.png +0 -0
  13. package/assets/jellyfish/dark-08.png +0 -0
  14. package/assets/jellyfish/dark-09.png +0 -0
  15. package/assets/jellyfish/dark-10.png +0 -0
  16. package/assets/jellyfish/dark-11.png +0 -0
  17. package/assets/jellyfish/dark-12.png +0 -0
  18. package/assets/jellyfish/dark-13.png +0 -0
  19. package/assets/jellyfish/dark-14.png +0 -0
  20. package/assets/jellyfish/fallback.txt +252 -0
  21. package/assets/jellyfish/light-00.png +0 -0
  22. package/assets/jellyfish/light-01.png +0 -0
  23. package/assets/jellyfish/light-02.png +0 -0
  24. package/assets/jellyfish/light-03.png +0 -0
  25. package/assets/jellyfish/light-04.png +0 -0
  26. package/assets/jellyfish/light-05.png +0 -0
  27. package/assets/jellyfish/light-06.png +0 -0
  28. package/assets/jellyfish/light-07.png +0 -0
  29. package/assets/jellyfish/light-08.png +0 -0
  30. package/assets/jellyfish/light-09.png +0 -0
  31. package/assets/jellyfish/light-10.png +0 -0
  32. package/assets/jellyfish/light-11.png +0 -0
  33. package/assets/jellyfish/light-12.png +0 -0
  34. package/assets/jellyfish/light-13.png +0 -0
  35. package/assets/jellyfish/light-14.png +0 -0
  36. package/bin/turri.mjs +29 -0
  37. package/dist/ask.js +149 -0
  38. package/dist/brand.js +50 -0
  39. package/dist/btw.js +174 -0
  40. package/dist/chat-models.js +37 -0
  41. package/dist/chat-skills.js +18 -0
  42. package/dist/cli.js +178 -0
  43. package/dist/config-command.js +197 -0
  44. package/dist/config-edit.js +367 -0
  45. package/dist/config.js +489 -0
  46. package/dist/conform-cli.js +29 -0
  47. package/dist/cultivate-cli.js +110 -0
  48. package/dist/drift-cli.js +25 -0
  49. package/dist/environment.js +70 -0
  50. package/dist/extension.js +2372 -0
  51. package/dist/genome.js +301 -0
  52. package/dist/graph.js +246 -0
  53. package/dist/identity.js +35 -0
  54. package/dist/init-config.js +511 -0
  55. package/dist/judge.js +62 -0
  56. package/dist/lineage-cli.js +13 -0
  57. package/dist/loop-cli.js +132 -0
  58. package/dist/loop-tools.js +225 -0
  59. package/dist/mature-tools.js +193 -0
  60. package/dist/model-check.js +117 -0
  61. package/dist/module-report.js +9 -0
  62. package/dist/pi-launch.js +31 -0
  63. package/dist/pi.js +813 -0
  64. package/dist/plain/check.js +76 -0
  65. package/dist/plain/concepts.js +213 -0
  66. package/dist/plain/errors.js +43 -0
  67. package/dist/plain/files.js +57 -0
  68. package/dist/plain/highlight.js +195 -0
  69. package/dist/plain/index.js +15 -0
  70. package/dist/plain/liquid.js +129 -0
  71. package/dist/plain/markdown.js +208 -0
  72. package/dist/plain/parser.js +752 -0
  73. package/dist/plain/plain.tmLanguage.json +124 -0
  74. package/dist/plain/spec.js +291 -0
  75. package/dist/plain/stdlib.js +44 -0
  76. package/dist/question-dialog.js +125 -0
  77. package/dist/questioning.js +109 -0
  78. package/dist/quiet.js +125 -0
  79. package/dist/regenerate/activity.js +187 -0
  80. package/dist/regenerate/agent-env.js +190 -0
  81. package/dist/regenerate/agent-shell.js +114 -0
  82. package/dist/regenerate/arbiter.js +133 -0
  83. package/dist/regenerate/audit.js +203 -0
  84. package/dist/regenerate/cases.js +83 -0
  85. package/dist/regenerate/change-view.js +102 -0
  86. package/dist/regenerate/choose-conformance.js +117 -0
  87. package/dist/regenerate/chunk-plan.js +625 -0
  88. package/dist/regenerate/codebase.js +75 -0
  89. package/dist/regenerate/command-check.js +312 -0
  90. package/dist/regenerate/compaction.js +20 -0
  91. package/dist/regenerate/confine.js +310 -0
  92. package/dist/regenerate/conform.js +133 -0
  93. package/dist/regenerate/conformance-agents.js +955 -0
  94. package/dist/regenerate/conformance.js +936 -0
  95. package/dist/regenerate/conversation-writes.js +100 -0
  96. package/dist/regenerate/credentials.js +88 -0
  97. package/dist/regenerate/debug-log.js +228 -0
  98. package/dist/regenerate/debugger.js +90 -0
  99. package/dist/regenerate/delivery.js +19 -0
  100. package/dist/regenerate/diagnose-job.js +40 -0
  101. package/dist/regenerate/diagnose.js +81 -0
  102. package/dist/regenerate/diff-view.js +240 -0
  103. package/dist/regenerate/drift.js +194 -0
  104. package/dist/regenerate/engine.js +118 -0
  105. package/dist/regenerate/environment-error.js +220 -0
  106. package/dist/regenerate/job-log.js +102 -0
  107. package/dist/regenerate/job-panel.js +336 -0
  108. package/dist/regenerate/jobs-tree.js +300 -0
  109. package/dist/regenerate/jobs.js +427 -0
  110. package/dist/regenerate/lanes.js +32 -0
  111. package/dist/regenerate/lineage.js +384 -0
  112. package/dist/regenerate/loop.js +1435 -0
  113. package/dist/regenerate/masking.js +95 -0
  114. package/dist/regenerate/notes.js +397 -0
  115. package/dist/regenerate/os-sandbox.js +243 -0
  116. package/dist/regenerate/paths.js +165 -0
  117. package/dist/regenerate/plain-palette.js +83 -0
  118. package/dist/regenerate/prompt.js +704 -0
  119. package/dist/regenerate/records.js +294 -0
  120. package/dist/regenerate/regenerate.js +598 -0
  121. package/dist/regenerate/regression.js +181 -0
  122. package/dist/regenerate/release.js +262 -0
  123. package/dist/regenerate/requires.js +241 -0
  124. package/dist/regenerate/resources.js +164 -0
  125. package/dist/regenerate/self-check.js +111 -0
  126. package/dist/regenerate/services.js +155 -0
  127. package/dist/regenerate/shell.js +320 -0
  128. package/dist/regenerate/side-panel.js +278 -0
  129. package/dist/regenerate/spec-map-view.js +574 -0
  130. package/dist/regenerate/spec-map.js +561 -0
  131. package/dist/regenerate/spec-steps.js +527 -0
  132. package/dist/regenerate/state.js +10 -0
  133. package/dist/regenerate/steer.js +91 -0
  134. package/dist/regenerate/steering.js +119 -0
  135. package/dist/regenerate/stop-report.js +161 -0
  136. package/dist/regenerate/submit-work.js +71 -0
  137. package/dist/regenerate/test-kinds.js +124 -0
  138. package/dist/regenerate/test-scripts.js +695 -0
  139. package/dist/regenerate/testing.js +406 -0
  140. package/dist/regenerate/transcript-view.js +1 -0
  141. package/dist/regenerate/unit-gate.js +20 -0
  142. package/dist/regenerate/unit-review.js +347 -0
  143. package/dist/regenerate/web.js +256 -0
  144. package/dist/regenerate/workspace.js +99 -0
  145. package/dist/regenerate-cli.js +68 -0
  146. package/dist/release-agent.js +114 -0
  147. package/dist/release-cli.js +131 -0
  148. package/dist/release-tools.js +148 -0
  149. package/dist/review/live.js +165 -0
  150. package/dist/rules.js +47 -0
  151. package/dist/self-invocation.js +110 -0
  152. package/dist/setup.js +245 -0
  153. package/dist/skill-progress.js +78 -0
  154. package/dist/skill.js +149 -0
  155. package/dist/skills.js +74 -0
  156. package/dist/spec-diff-cli.js +37 -0
  157. package/dist/spec-tools.js +155 -0
  158. package/dist/stack.js +158 -0
  159. package/dist/stacks.js +97 -0
  160. package/dist/stdlib.js +100 -0
  161. package/dist/terminal-images.js +46 -0
  162. package/dist/tools.js +242 -0
  163. package/dist/validate-cli.js +52 -0
  164. package/docs/README.md +58 -0
  165. package/docs/authoring-plain.md +245 -0
  166. package/docs/config.md +353 -0
  167. package/docs/deploy.md +109 -0
  168. package/docs/development.md +88 -0
  169. package/docs/headless.md +148 -0
  170. package/docs/images/how-turri-works.svg +50 -0
  171. package/docs/images/turri-banner.png +0 -0
  172. package/docs/installation.md +119 -0
  173. package/docs/parser.md +108 -0
  174. package/docs/regenerating.md +1116 -0
  175. package/docs/sandbox.md +233 -0
  176. package/docs/skills-and-gates.md +254 -0
  177. package/docs/theming.md +76 -0
  178. package/docs/using.md +454 -0
  179. package/genome/rules/bullet-continuation.md +69 -0
  180. package/genome/rules/concise-specs-examples.md +431 -0
  181. package/genome/rules/concise-specs-integration-examples.md +270 -0
  182. package/genome/rules/concise-specs.md +80 -0
  183. package/genome/rules/definitions.md +107 -0
  184. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
  185. package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
  186. package/genome/rules/exported-concepts.md +39 -0
  187. package/genome/rules/func-specs.md +129 -0
  188. package/genome/rules/impl-reqs.md +68 -0
  189. package/genome/rules/import-modules.md +48 -0
  190. package/genome/rules/integration-embedded-testing.md +276 -0
  191. package/genome/rules/integration-embedded.md +171 -0
  192. package/genome/rules/integration-standalone.md +135 -0
  193. package/genome/rules/integrations.md +137 -0
  194. package/genome/rules/linked-resources.md +69 -0
  195. package/genome/rules/module-structure.md +105 -0
  196. package/genome/rules/project-model.md +88 -0
  197. package/genome/rules/required-concepts.md +44 -0
  198. package/genome/rules/requires-modules.md +80 -0
  199. package/genome/rules/stdlib.md +73 -0
  200. package/genome/rules/test-reqs.md +57 -0
  201. package/genome/skills/add-acceptance-test/SKILL.md +100 -0
  202. package/genome/skills/add-concept/SKILL.md +71 -0
  203. package/genome/skills/add-functional-spec/SKILL.md +84 -0
  204. package/genome/skills/add-functional-specs/SKILL.md +118 -0
  205. package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
  206. package/genome/skills/add-resource/SKILL.md +105 -0
  207. package/genome/skills/add-template/SKILL.md +64 -0
  208. package/genome/skills/add-test-requirement/SKILL.md +73 -0
  209. package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
  210. package/genome/skills/analyze-func-specs/SKILL.md +123 -0
  211. package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
  212. package/genome/skills/break-down-func-spec/SKILL.md +205 -0
  213. package/genome/skills/brood/SKILL.md +229 -0
  214. package/genome/skills/brood/references/intent.md +35 -0
  215. package/genome/skills/brood/references/product.md +101 -0
  216. package/genome/skills/brood/references/stack.md +61 -0
  217. package/genome/skills/brood/references/testing.md +56 -0
  218. package/genome/skills/bud/SKILL.md +148 -0
  219. package/genome/skills/bud/references/checklist.md +44 -0
  220. package/genome/skills/bud/references/intent.md +30 -0
  221. package/genome/skills/check-plain-env/SKILL.md +285 -0
  222. package/genome/skills/consolidate-concepts/SKILL.md +192 -0
  223. package/genome/skills/create-import-module/SKILL.md +104 -0
  224. package/genome/skills/create-requires-module/SKILL.md +109 -0
  225. package/genome/skills/debug-specs/SKILL.md +188 -0
  226. package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
  227. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
  228. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
  229. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
  230. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
  231. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
  232. package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
  233. package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
  234. package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
  235. package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
  236. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
  237. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
  238. package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
  239. package/genome/skills/init-plain-project/SKILL.md +117 -0
  240. package/genome/skills/init-plain-project/references/checklist.md +30 -0
  241. package/genome/skills/mature/SKILL.md +126 -0
  242. package/genome/skills/refactor-module/SKILL.md +196 -0
  243. package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
  244. package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
  245. package/genome/skills/spawn/SKILL.md +163 -0
  246. package/package.json +68 -4
  247. package/stdlib/backend/common.plain +25 -0
  248. package/stdlib/backend/conformance/common.plain +12 -0
  249. package/stdlib/backend/conformance/go-gin.plain +9 -0
  250. package/stdlib/backend/conformance/go-nethttp.plain +9 -0
  251. package/stdlib/backend/conformance/java-springboot.plain +9 -0
  252. package/stdlib/backend/conformance/python-django.plain +9 -0
  253. package/stdlib/backend/conformance/python-fastapi.plain +9 -0
  254. package/stdlib/backend/conformance/python-flask.plain +9 -0
  255. package/stdlib/backend/conformance/rust-axum.plain +9 -0
  256. package/stdlib/backend/conformance/typescript-express.plain +9 -0
  257. package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
  258. package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
  259. package/stdlib/backend/go-gin.plain +18 -0
  260. package/stdlib/backend/go-nethttp.plain +18 -0
  261. package/stdlib/backend/java-springboot.plain +19 -0
  262. package/stdlib/backend/python-django.plain +19 -0
  263. package/stdlib/backend/python-fastapi.plain +23 -0
  264. package/stdlib/backend/python-flask.plain +23 -0
  265. package/stdlib/backend/rust-axum.plain +17 -0
  266. package/stdlib/backend/typescript-express.plain +19 -0
  267. package/stdlib/backend/typescript-fastify.plain +19 -0
  268. package/stdlib/backend/typescript-nestjs.plain +19 -0
  269. package/stdlib/cli/common.plain +17 -0
  270. package/stdlib/cli/conformance/common.plain +11 -0
  271. package/stdlib/cli/conformance/go-cobra.plain +9 -0
  272. package/stdlib/cli/conformance/go-flag.plain +9 -0
  273. package/stdlib/cli/conformance/java-picocli.plain +9 -0
  274. package/stdlib/cli/conformance/python-argparse.plain +9 -0
  275. package/stdlib/cli/conformance/python-click.plain +9 -0
  276. package/stdlib/cli/conformance/python-typer.plain +9 -0
  277. package/stdlib/cli/conformance/rust-clap.plain +9 -0
  278. package/stdlib/cli/conformance/typescript-commander.plain +9 -0
  279. package/stdlib/cli/go-cobra.plain +14 -0
  280. package/stdlib/cli/go-flag.plain +14 -0
  281. package/stdlib/cli/java-picocli.plain +15 -0
  282. package/stdlib/cli/python-argparse.plain +18 -0
  283. package/stdlib/cli/python-click.plain +16 -0
  284. package/stdlib/cli/python-typer.plain +16 -0
  285. package/stdlib/cli/rust-clap.plain +14 -0
  286. package/stdlib/cli/typescript-commander.plain +17 -0
  287. package/stdlib/frontend/angular.plain +17 -0
  288. package/stdlib/frontend/common.plain +17 -0
  289. package/stdlib/frontend/conformance/angular.plain +5 -0
  290. package/stdlib/frontend/conformance/common.plain +11 -0
  291. package/stdlib/frontend/conformance/react-vite.plain +5 -0
  292. package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
  293. package/stdlib/frontend/conformance/vue-vite.plain +5 -0
  294. package/stdlib/frontend/react-vite.plain +16 -0
  295. package/stdlib/frontend/sveltekit-spa.plain +16 -0
  296. package/stdlib/frontend/vue-vite.plain +16 -0
  297. package/stdlib/lang/go.plain +13 -0
  298. package/stdlib/lang/java.plain +12 -0
  299. package/stdlib/lang/python.plain +18 -0
  300. package/stdlib/lang/rust.plain +12 -0
  301. package/stdlib/lang/typescript.plain +13 -0
  302. package/stdlib/library/common.plain +15 -0
  303. package/stdlib/library/conformance/common.plain +9 -0
  304. package/stdlib/library/conformance/go.plain +9 -0
  305. package/stdlib/library/conformance/java-gradle.plain +9 -0
  306. package/stdlib/library/conformance/java-maven.plain +9 -0
  307. package/stdlib/library/conformance/python.plain +9 -0
  308. package/stdlib/library/conformance/rust.plain +9 -0
  309. package/stdlib/library/conformance/typescript.plain +9 -0
  310. package/stdlib/library/go.plain +12 -0
  311. package/stdlib/library/java-gradle.plain +13 -0
  312. package/stdlib/library/java-maven.plain +13 -0
  313. package/stdlib/library/python.plain +13 -0
  314. package/stdlib/library/rust.plain +12 -0
  315. package/stdlib/library/typescript.plain +15 -0
@@ -0,0 +1,278 @@
1
+ import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { createJobsTreeState, jobsTreeLines, jobsTreeRows, keepCursorInView, scrollTree, treeKey } from "./jobs-tree.js";
3
+ import { columnHeadings, createSpecMapState, DISCUSS_KEY, legendLines, minimap, shownPage, specMapHeader, specMapLines, switchTab, tabBar } from "./spec-map-view.js";
4
+ export function createSidePanel(options) {
5
+ const style = options.style;
6
+ let lastBodyHeight = 10;
7
+ /** The tree: kept by the conversation, so it is as it was left when the panel opens again. */
8
+ const tree = options.tree ?? createJobsTreeState();
9
+ const map = options.mapState ?? createSpecMapState();
10
+ if (options.map === undefined)
11
+ map.view = "pulses";
12
+ let drawnTree = { rows: [], rowStarts: [], lineCount: 0 };
13
+ let drawnMap = { rows: [], rowStarts: [], lineCount: 0 };
14
+ let pages = [];
15
+ const waitingNote = () =>
16
+ // A question asked while it showed waits in the chat, unanswered: the person is told it is there.
17
+ options.waiting?.() === true ? ` ${style.warning?.("your question is waiting") ?? style.accent("your question is waiting")} - ${options.key} or esc to answer it, as you left it` : "";
18
+ /** The top of a view, where it is seen first: its title, a line of space, and the keys, in `room` - wrapped, never cut short. */
19
+ const topBlock = (title, keys, room) => [
20
+ ...title.flatMap((line) => wrapTextWithAnsi(line, room)),
21
+ "",
22
+ ...keyLines(keys, room).map(style.muted),
23
+ ];
24
+ /** The legend in a box of its own, for the panel's upper right corner. */
25
+ const legendBox = (legend) => {
26
+ if (legend.length === 0)
27
+ return [];
28
+ const inner = Math.max(...legend.map(visibleWidth));
29
+ return [
30
+ style.muted(`┌${"─".repeat(inner + 2)}┐`),
31
+ ...legend.map((entry) => `${style.muted("│")} ${entry}${" ".repeat(inner - visibleWidth(entry))} ${style.muted("│")}`),
32
+ style.muted(`└${"─".repeat(inner + 2)}┘`),
33
+ ];
34
+ };
35
+ /** The box laid over the upper right corner: what is under it cut where it starts, with no ellipsis. */
36
+ const overBox = (lines, box, width) => {
37
+ if (box.length === 0)
38
+ return lines;
39
+ const room = width - visibleWidth(box[0]) - 1;
40
+ return lines.map((line, index) => {
41
+ if (index >= box.length)
42
+ return line;
43
+ const left = truncateToWidth(line, room, "");
44
+ return `${left}${" ".repeat(Math.max(0, room - visibleWidth(left)))} ${box[index]}`;
45
+ });
46
+ };
47
+ /** The keys, as many to a line as fit, a line broken only between two keys - one too long alone wrapped. */
48
+ const keyLines = (keys, room) => {
49
+ const lines = [];
50
+ for (const key of keys) {
51
+ const last = lines.at(-1);
52
+ if (last !== undefined && visibleWidth(`${last} · ${key}`) <= room)
53
+ lines[lines.length - 1] = `${last} · ${key}`;
54
+ else
55
+ lines.push(...wrapTextWithAnsi(key, room));
56
+ }
57
+ return lines;
58
+ };
59
+ /** The rule under the top. */
60
+ const rule = (width) => style.muted("─".repeat(Math.max(1, width)));
61
+ const renderPulses = (width, rows) => {
62
+ const keys = ["↑↓ move", "enter open/close", "←→", "o/c all", "pgup pgdn scroll", ...(options.map !== undefined ? ["p regen"] : []), `esc or ${options.key} back to the chat`];
63
+ const top = topBlock([`${style.heading("pulses")}${waitingNote()}`], keys, width);
64
+ // Under the top: the rule, and a line of space - as the map has.
65
+ const bodyHeight = Math.max(3, rows - top.length - 2);
66
+ lastBodyHeight = bodyHeight;
67
+ // The tree scrolls to keep the cursor in sight, and only that far.
68
+ const tree_ = jobsTreeLines(options.jobs(), tree, width, style);
69
+ drawnTree = { rows: tree_.rows, rowStarts: tree_.rowStarts, lineCount: tree_.lines.length };
70
+ keepCursorInView(tree, tree_.rowStarts, tree_.lines.length, bodyHeight);
71
+ const visible = tree_.lines.slice(tree.top, tree.top + bodyHeight);
72
+ while (visible.length < bodyHeight)
73
+ visible.push("");
74
+ return [...top, rule(width), "", ...visible.map((line) => truncateToWidth(line, width))];
75
+ };
76
+ const renderMap = (width, rows) => {
77
+ pages = options.map();
78
+ const now = Date.now();
79
+ if (pages.length === 0) {
80
+ const top = topBlock([`${style.heading("regen")}${waitingNote()}`], ["p pulses", `esc or ${options.key} back to the chat`], width);
81
+ lastBodyHeight = Math.max(3, rows - top.length - 1);
82
+ drawnMap = { rows: [], rowStarts: [], lineCount: 0 };
83
+ const body = [style.muted(" No .plain file to map: name one to regenerate, or set module: in config.turri.yaml."), ...new Array(Math.max(0, lastBodyHeight - 1)).fill("")];
84
+ return [...top, rule(width), ...body];
85
+ }
86
+ // A chain of modules: a tab each, on a line of their own under the top line.
87
+ const tabbed = pages.length > 1;
88
+ const page = shownPage(pages, map);
89
+ const discuss = page.stuck && options.discuss !== undefined ? [`${DISCUSS_KEY} talk it through in the chat`] : [];
90
+ const keys = [...discuss, "↑↓ move", "enter open/close", ...(tabbed ? ["←→ module"] : []), "o/c all", "pgup pgdn", "p pulses", `esc or ${options.key} back to the chat`];
91
+ // The legend's box in the upper right corner, over what is under it; too narrow a panel has none.
92
+ const box = width >= 60 ? legendBox(legendLines(page, style)) : [];
93
+ const room = box.length > 0 ? width - visibleWidth(box[0]) - 1 : width;
94
+ // The top line whole - it wraps beside the box; the tabs fit themselves, counting what does not fit.
95
+ const title = [specMapHeader(page, Number.MAX_SAFE_INTEGER, style, waitingNote()), ...(tabbed ? [tabBar(pages, page, room, style, now)] : [])];
96
+ const top = topBlock(title, keys, room);
97
+ // Under the top: the rule, a line of space, and the columns' headings - two lines.
98
+ const bodyHeight = Math.max(3, rows - top.length - 4);
99
+ lastBodyHeight = bodyHeight;
100
+ // A long map has a minimap down its right edge: the content gives it two columns.
101
+ const first = specMapLines(pages, map, width, style, options.jobs(), now);
102
+ const long = first.lines.length > bodyHeight && width > 40;
103
+ const drawn = long ? specMapLines(pages, map, width - 2, style, options.jobs(), now) : first;
104
+ drawnMap = { rows: drawn.rows, rowStarts: drawn.rowStarts, lineCount: drawn.lines.length };
105
+ keepCursorInView(map, drawn.rowStarts, drawn.lines.length, bodyHeight);
106
+ const visible = drawn.lines.slice(map.top, map.top + bodyHeight).map((line) => truncateToWidth(line, long ? width - 2 : width));
107
+ while (visible.length < bodyHeight)
108
+ visible.push("");
109
+ const body = long
110
+ ? (() => {
111
+ const strip = minimap(drawn.marks, bodyHeight, map.top, style);
112
+ return visible.map((line, row) => `${line}${" ".repeat(Math.max(0, width - 2 - visibleWidth(line)))} ${strip[row]}`);
113
+ })()
114
+ : visible;
115
+ // The rule beside the box stops short of it.
116
+ return overBox([...top, rule(top.length < box.length ? room - 1 : width), "", ...columnHeadings(page, width, style), ...body], box, width);
117
+ };
118
+ /** From the map to the pulse list: onto the pulse that regenerates the item under the cursor, when one does. */
119
+ const toPulses = () => {
120
+ map.view = "pulses";
121
+ const row = drawnMap.rows[map.cursor];
122
+ const id = row?.kind === "item" ? row.item.pulse?.rowId : undefined;
123
+ if (id === undefined)
124
+ return;
125
+ const jobs = options.jobs();
126
+ const job = jobs.find((entry) => entry.id === id);
127
+ if (job === undefined)
128
+ return;
129
+ const section = job.status === "running" ? "running" : job.status === "queued" ? "queued" : "finished";
130
+ tree.open[section] = true;
131
+ const at = jobsTreeRows(jobs, tree).findIndex((entry) => entry.kind === "job" && entry.job.id === id);
132
+ if (at !== -1)
133
+ tree.cursor = at;
134
+ };
135
+ /** From the pulse list to the map: onto the first item the pulse under the cursor regenerates, on whichever page it is. */
136
+ const toMap = () => {
137
+ map.view = "map";
138
+ const row = drawnTree.rows[tree.cursor];
139
+ const id = row !== undefined && row.kind !== "section" ? row.job.id : undefined;
140
+ if (id === undefined)
141
+ return;
142
+ for (const page of options.map?.() ?? []) {
143
+ const item = page.items.find((entry) => entry.pulse?.rowId === id);
144
+ if (item === undefined)
145
+ continue;
146
+ // Inside a closed module, or a closed import block: they open, so the item can be shown.
147
+ map.reveal = { file: page.plainFile, key: item.key };
148
+ return;
149
+ }
150
+ };
151
+ /** A key in the map. */
152
+ const mapKey = (data) => {
153
+ const page = Math.max(1, lastBodyHeight - 1);
154
+ const rows = drawnMap.rows;
155
+ const row = rows[map.cursor];
156
+ const move = (to) => {
157
+ map.cursor = Math.max(0, Math.min(rows.length - 1, to));
158
+ map.cursorKey = rows[map.cursor]?.key;
159
+ };
160
+ const isOpen = (target) => (target.kind === "item" ? map.open.has(target.key) : target.kind === "group" ? false : target.open);
161
+ const toggle = (target, open) => {
162
+ // A group's heading only says what is under it.
163
+ if (target === undefined || target.kind === "group")
164
+ return;
165
+ const to = open ?? !isOpen(target);
166
+ if (target.kind === "import")
167
+ map.imports.set(target.key.replace(/^import\n/, ""), to);
168
+ else if (to)
169
+ map.open.add(target.key);
170
+ else
171
+ map.open.delete(target.key);
172
+ };
173
+ if (matchesKey(data, Key.up))
174
+ move(map.cursor - 1);
175
+ else if (matchesKey(data, Key.down))
176
+ move(map.cursor + 1);
177
+ else if (matchesKey(data, Key.enter) || data === " ")
178
+ toggle(row);
179
+ // ←→ go from one module's tab to the next.
180
+ else if (matchesKey(data, Key.right))
181
+ switchTab(pages, map, 1);
182
+ else if (matchesKey(data, Key.left))
183
+ switchTab(pages, map, -1);
184
+ else if (data === "o" || data === "c")
185
+ for (const entry of rows)
186
+ toggle(entry, data === "o");
187
+ else if (data === DISCUSS_KEY && options.discuss !== undefined) {
188
+ const shown = shownPage(pages, map);
189
+ if (shown?.stuck === true)
190
+ options.discuss(shown);
191
+ }
192
+ else if (matchesKey(data, Key.pageUp))
193
+ scrollMap(-page);
194
+ else if (matchesKey(data, Key.pageDown))
195
+ scrollMap(page);
196
+ else if (matchesKey(data, Key.home) || data === "g")
197
+ move(0);
198
+ else if (matchesKey(data, Key.end) || data === "G")
199
+ move(rows.length - 1);
200
+ };
201
+ const scrollMap = (by) => {
202
+ scrollTree(map, by, drawnMap.rowStarts, drawnMap.lineCount, lastBodyHeight);
203
+ map.cursorKey = drawnMap.rows[map.cursor]?.key;
204
+ };
205
+ return {
206
+ render(width) {
207
+ const rows = Math.max(8, options.rows());
208
+ try {
209
+ return map.view === "map" && options.map !== undefined ? renderMap(width, rows) : renderPulses(width, rows);
210
+ }
211
+ catch (error) {
212
+ // A frame that throws is never drawn - nor is anything after it: the screen would freeze. What failed is shown instead.
213
+ const top = topBlock([`${style.heading(map.view === "map" ? "regen" : "pulses")}${waitingNote()}`], [...(options.map !== undefined ? ["p the other view"] : []), `esc or ${options.key} back to the chat`], width);
214
+ const lines = (error instanceof Error ? (error.stack ?? error.message) : String(error)).split("\n").slice(0, Math.max(1, rows - top.length - 1));
215
+ return [
216
+ ...top,
217
+ rule(width),
218
+ ...lines.map((line, index) => truncateToWidth(index === 0 ? style.error(`could not be drawn: ${line}`) : style.muted(line), width)),
219
+ ...new Array(Math.max(0, rows - top.length - 1 - lines.length)).fill(""),
220
+ ];
221
+ }
222
+ },
223
+ handleInput(data) {
224
+ if (matchesKey(data, Key.escape)) {
225
+ options.back();
226
+ return;
227
+ }
228
+ if (options.map !== undefined && data === "p") {
229
+ if (map.view === "map")
230
+ toPulses();
231
+ else
232
+ toMap();
233
+ return;
234
+ }
235
+ if (map.view === "map" && options.map !== undefined) {
236
+ mapKey(data);
237
+ return;
238
+ }
239
+ const page = Math.max(1, lastBodyHeight - 1);
240
+ const key = matchesKey(data, Key.up)
241
+ ? "up"
242
+ : matchesKey(data, Key.down)
243
+ ? "down"
244
+ : matchesKey(data, Key.enter) || data === " "
245
+ ? "toggle"
246
+ : matchesKey(data, Key.right)
247
+ ? "open"
248
+ : matchesKey(data, Key.left)
249
+ ? "close"
250
+ : data === "o"
251
+ ? "openAll"
252
+ : data === "c"
253
+ ? "closeAll"
254
+ : undefined;
255
+ if (key !== undefined)
256
+ treeKey(tree, drawnTree.rows, key);
257
+ else if (matchesKey(data, Key.pageUp))
258
+ scrollTree(tree, -page, drawnTree.rowStarts, drawnTree.lineCount, lastBodyHeight);
259
+ else if (matchesKey(data, Key.pageDown))
260
+ scrollTree(tree, page, drawnTree.rowStarts, drawnTree.lineCount, lastBodyHeight);
261
+ else if (matchesKey(data, Key.home) || data === "g")
262
+ tree.cursor = 0;
263
+ else if (matchesKey(data, Key.end) || data === "G")
264
+ tree.cursor = Math.max(0, drawnTree.rows.length - 1);
265
+ },
266
+ /** The wheel scrolls the panel wherever the keyboard is. */
267
+ handleMouse(event) {
268
+ if (event.type !== "wheel" || event.wheelDelta === undefined)
269
+ return undefined;
270
+ if (map.view === "map" && options.map !== undefined)
271
+ scrollMap(event.wheelDelta * 3);
272
+ else
273
+ scrollTree(tree, event.wheelDelta * 3, drawnTree.rowStarts, drawnTree.lineCount, lastBodyHeight);
274
+ return { handled: true };
275
+ },
276
+ invalidate() { },
277
+ };
278
+ }