open-multi-agent-kit 0.91.0 → 0.93.0

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 (168) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts +29 -0
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +89 -21
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/command-safety.d.ts.map +1 -1
  8. package/dist/core/command-safety.js +0 -50
  9. package/dist/core/command-safety.js.map +1 -1
  10. package/dist/core/compaction/branch-summarization.d.ts +5 -1
  11. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  12. package/dist/core/compaction/branch-summarization.js +5 -7
  13. package/dist/core/compaction/branch-summarization.js.map +1 -1
  14. package/dist/core/compaction/compaction.d.ts +12 -3
  15. package/dist/core/compaction/compaction.d.ts.map +1 -1
  16. package/dist/core/compaction/compaction.js +28 -15
  17. package/dist/core/compaction/compaction.js.map +1 -1
  18. package/dist/core/compaction/transaction.d.ts.map +1 -1
  19. package/dist/core/compaction/transaction.js +7 -3
  20. package/dist/core/compaction/transaction.js.map +1 -1
  21. package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
  22. package/dist/core/context-budget-headroom-candidates.js +21 -11
  23. package/dist/core/context-budget-headroom-candidates.js.map +1 -1
  24. package/dist/core/context-budget-system-prompt-items.d.ts +1 -0
  25. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  26. package/dist/core/context-budget-system-prompt-items.js +7 -8
  27. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  28. package/dist/core/extensions/builtin/command-safety-gate.d.ts +8 -0
  29. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  30. package/dist/core/extensions/builtin/command-safety-gate.js +23 -0
  31. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  32. package/dist/core/extensions/runner.d.ts.map +1 -1
  33. package/dist/core/extensions/runner.js +4 -0
  34. package/dist/core/extensions/runner.js.map +1 -1
  35. package/dist/core/extensions/types.d.ts +2 -0
  36. package/dist/core/extensions/types.d.ts.map +1 -1
  37. package/dist/core/extensions/types.js.map +1 -1
  38. package/dist/core/keybindings.d.ts +5 -0
  39. package/dist/core/keybindings.d.ts.map +1 -1
  40. package/dist/core/keybindings.js +5 -0
  41. package/dist/core/keybindings.js.map +1 -1
  42. package/dist/core/loadouts.d.ts.map +1 -1
  43. package/dist/core/loadouts.js +27 -48
  44. package/dist/core/loadouts.js.map +1 -1
  45. package/dist/core/model-resolver.d.ts +41 -3
  46. package/dist/core/model-resolver.d.ts.map +1 -1
  47. package/dist/core/model-resolver.js +17 -9
  48. package/dist/core/model-resolver.js.map +1 -1
  49. package/dist/core/provider-resilience.d.ts.map +1 -1
  50. package/dist/core/provider-resilience.js +3 -1
  51. package/dist/core/provider-resilience.js.map +1 -1
  52. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  53. package/dist/core/reasoning-router-v4-weights.js +9 -3
  54. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  55. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  56. package/dist/core/reasoning-router-v4.js +94 -24
  57. package/dist/core/reasoning-router-v4.js.map +1 -1
  58. package/dist/core/resource-loader.d.ts.map +1 -1
  59. package/dist/core/resource-loader.js +11 -5
  60. package/dist/core/resource-loader.js.map +1 -1
  61. package/dist/core/session-file-compaction.d.ts.map +1 -1
  62. package/dist/core/session-file-compaction.js +39 -5
  63. package/dist/core/session-file-compaction.js.map +1 -1
  64. package/dist/core/session-termination.d.ts.map +1 -1
  65. package/dist/core/session-termination.js +1 -1
  66. package/dist/core/session-termination.js.map +1 -1
  67. package/dist/core/settings-manager.d.ts +3 -0
  68. package/dist/core/settings-manager.d.ts.map +1 -1
  69. package/dist/core/settings-manager.js +8 -0
  70. package/dist/core/settings-manager.js.map +1 -1
  71. package/dist/core/system-prompt.d.ts.map +1 -1
  72. package/dist/core/system-prompt.js +36 -31
  73. package/dist/core/system-prompt.js.map +1 -1
  74. package/dist/core/tools/bash.d.ts +2 -0
  75. package/dist/core/tools/bash.d.ts.map +1 -1
  76. package/dist/core/tools/bash.js +38 -11
  77. package/dist/core/tools/bash.js.map +1 -1
  78. package/dist/main.d.ts.map +1 -1
  79. package/dist/main.js +112 -11
  80. package/dist/main.js.map +1 -1
  81. package/dist/modes/interactive/components/control-panel-box.d.ts +27 -0
  82. package/dist/modes/interactive/components/control-panel-box.d.ts.map +1 -1
  83. package/dist/modes/interactive/components/control-panel-box.js +60 -8
  84. package/dist/modes/interactive/components/control-panel-box.js.map +1 -1
  85. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  86. package/dist/modes/interactive/components/control-panel-layout.js +100 -45
  87. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  88. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +9 -0
  89. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/control-panel-runtime-status.js +10 -8
  91. package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
  92. package/dist/modes/interactive/components/control-panel-sparkles.d.ts +5 -0
  93. package/dist/modes/interactive/components/control-panel-sparkles.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/control-panel-sparkles.js +14 -9
  95. package/dist/modes/interactive/components/control-panel-sparkles.js.map +1 -1
  96. package/dist/modes/interactive/components/footer.d.ts +10 -0
  97. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  98. package/dist/modes/interactive/components/footer.js +3 -3
  99. package/dist/modes/interactive/components/footer.js.map +1 -1
  100. package/dist/modes/interactive/components/status-sidebar.d.ts +49 -0
  101. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -0
  102. package/dist/modes/interactive/components/status-sidebar.js +253 -0
  103. package/dist/modes/interactive/components/status-sidebar.js.map +1 -0
  104. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  105. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/dist/modes/interactive/interactive-mode.js +94 -20
  107. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/dist/modes/interactive/theme/omk-neon-control.json +13 -13
  109. package/dist/modes/print-mode.d.ts.map +1 -1
  110. package/dist/modes/print-mode.js +9 -0
  111. package/dist/modes/print-mode.js.map +1 -1
  112. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  113. package/dist/modes/rpc/rpc-mode.js +1 -0
  114. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  115. package/dist/utils/clipboard.d.ts.map +1 -1
  116. package/dist/utils/clipboard.js +19 -8
  117. package/dist/utils/clipboard.js.map +1 -1
  118. package/docs/adaptorch-preview-spec.md +174 -0
  119. package/docs/adaptorch-preview.md +1 -1
  120. package/docs/compaction.md +20 -13
  121. package/docs/correctness-wall.md +13 -5
  122. package/docs/custom-provider.md +8 -8
  123. package/docs/development.md +2 -2
  124. package/docs/docs.json +4 -0
  125. package/docs/environment-variables.md +90 -0
  126. package/docs/extensions.md +8 -8
  127. package/docs/index.md +1 -0
  128. package/docs/json.md +4 -4
  129. package/docs/providers.md +1 -1
  130. package/docs/rpc.md +5 -2
  131. package/docs/session-format.md +5 -5
  132. package/docs/settings.md +1 -0
  133. package/docs/skills.md +2 -0
  134. package/docs/themes.md +2 -2
  135. package/docs/tui.md +4 -4
  136. package/docs/usage.md +4 -0
  137. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  138. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  139. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  140. package/examples/extensions/gondolin/package-lock.json +2 -2
  141. package/examples/extensions/gondolin/package.json +1 -1
  142. package/examples/extensions/sandbox/package-lock.json +2 -2
  143. package/examples/extensions/sandbox/package.json +1 -1
  144. package/examples/extensions/subagent/README.md +35 -10
  145. package/examples/extensions/subagent/adaptive-agent-runtime.test.ts +234 -0
  146. package/examples/extensions/subagent/adaptive-agent-runtime.ts +240 -0
  147. package/examples/extensions/subagent/adaptive-result.ts +112 -0
  148. package/examples/extensions/subagent/agents.test.ts +86 -0
  149. package/examples/extensions/subagent/agents.ts +18 -4
  150. package/examples/extensions/subagent/capabilities.ts +15 -1
  151. package/examples/extensions/subagent/checkpoint-runtime.test.ts +125 -0
  152. package/examples/extensions/subagent/checkpoint-runtime.ts +198 -0
  153. package/examples/extensions/subagent/deadline-budget.test.ts +135 -0
  154. package/examples/extensions/subagent/deadline-budget.ts +242 -0
  155. package/examples/extensions/subagent/deadline-profile-store.test.ts +72 -0
  156. package/examples/extensions/subagent/deadline-profile-store.ts +172 -0
  157. package/examples/extensions/subagent/domain-profiles.ts +272 -0
  158. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +13 -0
  159. package/examples/extensions/subagent/fixtures/hanging-process-tree.mjs +40 -0
  160. package/examples/extensions/subagent/index.ts +253 -137
  161. package/examples/extensions/subagent/managed-process.test.ts +178 -0
  162. package/examples/extensions/subagent/managed-process.ts +236 -0
  163. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +112 -0
  164. package/examples/extensions/subagent/subagent-runtime-types.ts +76 -0
  165. package/examples/extensions/with-deps/package-lock.json +2 -2
  166. package/examples/extensions/with-deps/package.json +1 -1
  167. package/npm-shrinkwrap.json +21 -21
  168. package/package.json +10 -7
@@ -1,11 +1,38 @@
1
1
  import { type ThemeColor } from "../theme/theme.ts";
2
2
  export declare function composeColumns(leftLines: string[], leftWidth: number, rightLines: string[], rightWidth: number, gapWidth: number, width: number): string[];
3
+ /**
4
+ * Section rule for the control rail.
5
+ *
6
+ * Editorial treatment: the label sits flush left behind a single hairline tick
7
+ * and the rule runs out to the right edge, instead of a centered bold caption.
8
+ * Frames stay hairline so the accent colour is reserved for live values.
9
+ */
3
10
  export declare function sidebarRule(width: number, label: string, borderColor?: ThemeColor): string;
11
+ /** Width of the right-aligned label gutter shared by every control-rail row. */
12
+ export declare const RAIL_LABEL_COLUMN = 8;
13
+ /**
14
+ * Right-aligned `label:` gutter cell.
15
+ *
16
+ * Labels are right-aligned so every value starts on the same column while the
17
+ * rendered text keeps exactly one space after the colon (`route: active`),
18
+ * which downstream reference-fidelity assertions depend on.
19
+ */
20
+ export declare function labelCell(label: string, column?: number): string;
4
21
  export declare function boxTop(width: number, label: string, borderColor?: ThemeColor): string;
5
22
  export declare function boxBottom(width: number, borderColor?: ThemeColor): string;
6
23
  export declare function boxBlankLine(width: number): string;
24
+ /** Hairline rule inside a framed panel, inset from both frame edges. */
25
+ export declare function boxRuleLine(width: number, inset?: number): string;
7
26
  export declare function boxCenteredLine(width: number, text: string, color?: ThemeColor): string;
8
27
  export declare function boxTextLine(width: number, text: string, color?: ThemeColor): string;
28
+ /**
29
+ * Pad a framed column with blank body rows so two side-by-side panels close on
30
+ * the same row. The leading and trailing frame lines are preserved.
31
+ *
32
+ * `"center"` splits the filler above and below the body, which turns leftover
33
+ * height into deliberate negative space instead of a hollow bottom half.
34
+ */
35
+ export declare function padBoxColumn(lines: string[], targetHeight: number, width: number, distribution?: "bottom" | "center"): string[];
9
36
  export declare function centerLine(width: number, text: string, color?: ThemeColor): string;
10
37
  export declare function centerText(width: number, text: string): string;
11
38
  export declare function divider(width: number, label: string, color: ThemeColor): string;
@@ -1 +1 @@
1
- {"version":3,"file":"control-panel-box.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/control-panel-box.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAS,MAAM,mBAAmB,CAAC;AAE3D,wBAAgB,cAAc,CAC7B,SAAS,EAAE,MAAM,EAAE,EACnB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACX,MAAM,EAAE,CAOV;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,UAA2B,GAAG,MAAM,CAU1G;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,UAA2B,GAAG,MAAM,CAOrG;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,UAA2B,GAAG,MAAM,CAEzF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAEvF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAMnF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAElF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAM/E;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAMhF;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D","sourcesContent":["import { truncateToWidth, visibleWidth } from \"omk-tui\";\nimport { type ThemeColor, theme } from \"../theme/theme.ts\";\n\nexport function composeColumns(\n\tleftLines: string[],\n\tleftWidth: number,\n\trightLines: string[],\n\trightWidth: number,\n\tgapWidth: number,\n\twidth: number,\n): string[] {\n\treturn Array.from({ length: Math.max(leftLines.length, rightLines.length) }, (_, index) =>\n\t\tclipLine(\n\t\t\t`${fitLine(leftLines[index] ?? \"\", leftWidth)}${\" \".repeat(gapWidth)}${fitLine(rightLines[index] ?? \"\", rightWidth)}`,\n\t\t\twidth,\n\t\t),\n\t);\n}\n\nexport function sidebarRule(width: number, label: string, borderColor: ThemeColor = \"borderAccent\"): string {\n\tconst bodyWidth = Math.max(0, width - 2);\n\tconst labelText = ` ${label} `;\n\tconst fill = Math.max(0, bodyWidth - visibleWidth(labelText));\n\tconst left = Math.floor(fill / 2);\n\tconst right = fill - left;\n\treturn clipLine(\n\t\t`${theme.fg(borderColor, \"\\u2502\")}${theme.fg(\"borderMuted\", \"\\u2500\".repeat(left))}${theme.bold(theme.fg(\"accent\", labelText))}${theme.fg(\"borderMuted\", \"\\u2500\".repeat(right))}${theme.fg(borderColor, \"\\u2502\")}`,\n\t\twidth,\n\t);\n}\n\nexport function boxTop(width: number, label: string, borderColor: ThemeColor = \"borderAccent\"): string {\n\tconst text = ` ${label} `;\n\tconst fillWidth = Math.max(0, width - visibleWidth(\"+\") - visibleWidth(text) - visibleWidth(\"+\"));\n\treturn clipLine(\n\t\t`${theme.fg(borderColor, \"\\u256d\")}${theme.bold(theme.fg(\"accent\", text))}${theme.fg(borderColor, \"\\u2500\".repeat(fillWidth))}${theme.fg(borderColor, \"\\u256e\")}`,\n\t\twidth,\n\t);\n}\n\nexport function boxBottom(width: number, borderColor: ThemeColor = \"borderAccent\"): string {\n\treturn clipLine(theme.fg(borderColor, `\\u2570${\"\\u2500\".repeat(Math.max(0, width - 2))}\\u256f`), width);\n}\n\nexport function boxBlankLine(width: number): string {\n\treturn boxTextLine(width, \"\");\n}\n\nexport function boxCenteredLine(width: number, text: string, color?: ThemeColor): string {\n\treturn boxTextLine(width, centerText(Math.max(0, width - 4), color ? theme.fg(color, text) : text));\n}\n\nexport function boxTextLine(width: number, text: string, color?: ThemeColor): string {\n\tconst body = color ? theme.fg(color, text) : text;\n\treturn clipLine(\n\t\t`${theme.fg(\"borderAccent\", \"\\u2502 \")}${fitLine(body, Math.max(0, width - 4))}${theme.fg(\"borderAccent\", \" \\u2502\")}`,\n\t\twidth,\n\t);\n}\n\nexport function centerLine(width: number, text: string, color?: ThemeColor): string {\n\treturn clipLine(centerText(width, color ? theme.fg(color, text) : text), width);\n}\n\nexport function centerText(width: number, text: string): string {\n\tconst fitted = truncateToWidth(text, width, \"\");\n\tconst remaining = Math.max(0, width - visibleWidth(fitted));\n\treturn `${\" \".repeat(Math.floor(remaining / 2))}${fitted}${\" \".repeat(Math.ceil(remaining / 2))}`;\n}\n\nexport function divider(width: number, label: string, color: ThemeColor): string {\n\tconst fillWidth = Math.max(0, width - visibleWidth(\"+-- \") - visibleWidth(label) - visibleWidth(\" +\"));\n\treturn clipLine(\n\t\t`${theme.fg(\"border\", \"+-- \")}${theme.bold(theme.fg(color, label))}${theme.fg(\"border\", ` ${\"-\".repeat(fillWidth)}+`)}`,\n\t\twidth,\n\t);\n}\n\nexport function textLine(width: number, text: string, color?: ThemeColor): string {\n\tconst body = color ? theme.fg(color, text) : text;\n\treturn clipLine(\n\t\t`${theme.fg(\"borderMuted\", \"\\u2502 \")}${fitLine(body, Math.max(0, width - 4))}${theme.fg(\"borderMuted\", \" \\u2502\")}`,\n\t\twidth,\n\t);\n}\n\nexport function fitLine(line: string, width: number): string {\n\tconst clipped = truncateToWidth(line, width, \"\");\n\treturn `${clipped}${\" \".repeat(Math.max(0, width - visibleWidth(clipped)))}`;\n}\n\nexport function clipLine(line: string, width: number): string {\n\treturn visibleWidth(line) <= width ? line : truncateToWidth(line, width, \"\");\n}\n"]}
1
+ {"version":3,"file":"control-panel-box.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/control-panel-box.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAS,MAAM,mBAAmB,CAAC;AAE3D,wBAAgB,cAAc,CAC7B,SAAS,EAAE,MAAM,EAAE,EACnB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACX,MAAM,EAAE,CAOV;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,UAA0B,GAAG,MAAM,CAQzG;AAED,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAA0B,GAAG,MAAM,CAInF;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,UAA0B,GAAG,MAAM,CAOpG;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,UAA0B,GAAG,MAAM,CAExF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,MAAM,CAI5D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAEvF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAMnF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,YAAY,GAAE,QAAQ,GAAG,QAAmB,GAC1C,MAAM,EAAE,CAkBV;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAElF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAM/E;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAMhF;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D","sourcesContent":["import { truncateToWidth, visibleWidth } from \"omk-tui\";\nimport { type ThemeColor, theme } from \"../theme/theme.ts\";\n\nexport function composeColumns(\n\tleftLines: string[],\n\tleftWidth: number,\n\trightLines: string[],\n\trightWidth: number,\n\tgapWidth: number,\n\twidth: number,\n): string[] {\n\treturn Array.from({ length: Math.max(leftLines.length, rightLines.length) }, (_, index) =>\n\t\tclipLine(\n\t\t\t`${fitLine(leftLines[index] ?? \"\", leftWidth)}${\" \".repeat(gapWidth)}${fitLine(rightLines[index] ?? \"\", rightWidth)}`,\n\t\t\twidth,\n\t\t),\n\t);\n}\n\n/**\n * Section rule for the control rail.\n *\n * Editorial treatment: the label sits flush left behind a single hairline tick\n * and the rule runs out to the right edge, instead of a centered bold caption.\n * Frames stay hairline so the accent colour is reserved for live values.\n */\nexport function sidebarRule(width: number, label: string, borderColor: ThemeColor = \"borderMuted\"): string {\n\tconst bodyWidth = Math.max(0, width - 2);\n\tconst labelText = ` ${label} `;\n\tconst fill = Math.max(0, bodyWidth - visibleWidth(labelText) - 1);\n\treturn clipLine(\n\t\t`${theme.fg(borderColor, \"\\u2502\")}${theme.fg(\"borderMuted\", \"\\u2500\")}${theme.fg(\"accent\", labelText)}${theme.fg(\"borderMuted\", \"\\u2500\".repeat(fill))}${theme.fg(borderColor, \"\\u2502\")}`,\n\t\twidth,\n\t);\n}\n\n/** Width of the right-aligned label gutter shared by every control-rail row. */\nexport const RAIL_LABEL_COLUMN = 8;\n\n/**\n * Right-aligned `label:` gutter cell.\n *\n * Labels are right-aligned so every value starts on the same column while the\n * rendered text keeps exactly one space after the colon (`route: active`),\n * which downstream reference-fidelity assertions depend on.\n */\nexport function labelCell(label: string, column: number = RAIL_LABEL_COLUMN): string {\n\tconst text = `${label}:`;\n\tconst pad = Math.max(0, column + 1 - visibleWidth(text));\n\treturn `${\" \".repeat(pad)}${theme.fg(\"muted\", text)}`;\n}\n\nexport function boxTop(width: number, label: string, borderColor: ThemeColor = \"borderMuted\"): string {\n\tconst text = ` ${label} `;\n\tconst fillWidth = Math.max(0, width - visibleWidth(\"+\") - visibleWidth(text) - visibleWidth(\"+\"));\n\treturn clipLine(\n\t\t`${theme.fg(borderColor, \"\\u256d\")}${theme.bold(theme.fg(\"accent\", text))}${theme.fg(borderColor, \"\\u2500\".repeat(fillWidth))}${theme.fg(borderColor, \"\\u256e\")}`,\n\t\twidth,\n\t);\n}\n\nexport function boxBottom(width: number, borderColor: ThemeColor = \"borderMuted\"): string {\n\treturn clipLine(theme.fg(borderColor, `\\u2570${\"\\u2500\".repeat(Math.max(0, width - 2))}\\u256f`), width);\n}\n\nexport function boxBlankLine(width: number): string {\n\treturn boxTextLine(width, \"\");\n}\n\n/** Hairline rule inside a framed panel, inset from both frame edges. */\nexport function boxRuleLine(width: number, inset = 0): string {\n\tconst innerWidth = Math.max(0, width - 4);\n\tconst ruleWidth = Math.max(0, innerWidth - inset * 2);\n\treturn boxTextLine(width, `${\" \".repeat(inset)}${theme.fg(\"borderMuted\", \"\\u2500\".repeat(ruleWidth))}`);\n}\n\nexport function boxCenteredLine(width: number, text: string, color?: ThemeColor): string {\n\treturn boxTextLine(width, centerText(Math.max(0, width - 4), color ? theme.fg(color, text) : text));\n}\n\nexport function boxTextLine(width: number, text: string, color?: ThemeColor): string {\n\tconst body = color ? theme.fg(color, text) : text;\n\treturn clipLine(\n\t\t`${theme.fg(\"borderMuted\", \"\\u2502 \")}${fitLine(body, Math.max(0, width - 4))}${theme.fg(\"borderMuted\", \" \\u2502\")}`,\n\t\twidth,\n\t);\n}\n\n/**\n * Pad a framed column with blank body rows so two side-by-side panels close on\n * the same row. The leading and trailing frame lines are preserved.\n *\n * `\"center\"` splits the filler above and below the body, which turns leftover\n * height into deliberate negative space instead of a hollow bottom half.\n */\nexport function padBoxColumn(\n\tlines: string[],\n\ttargetHeight: number,\n\twidth: number,\n\tdistribution: \"bottom\" | \"center\" = \"bottom\",\n): string[] {\n\tif (lines.length === 0 || lines.length >= targetHeight) return lines;\n\tconst top = lines[0] as string;\n\tconst bottom = lines[lines.length - 1] as string;\n\tconst body = lines.slice(1, -1);\n\tconst missing = targetHeight - lines.length;\n\tconst blank = () => boxTextLine(width, \"\");\n\tif (distribution === \"bottom\") {\n\t\treturn [top, ...body, ...Array.from({ length: missing }, blank), bottom];\n\t}\n\tconst above = Math.floor(missing / 2);\n\treturn [\n\t\ttop,\n\t\t...Array.from({ length: above }, blank),\n\t\t...body,\n\t\t...Array.from({ length: missing - above }, blank),\n\t\tbottom,\n\t];\n}\n\nexport function centerLine(width: number, text: string, color?: ThemeColor): string {\n\treturn clipLine(centerText(width, color ? theme.fg(color, text) : text), width);\n}\n\nexport function centerText(width: number, text: string): string {\n\tconst fitted = truncateToWidth(text, width, \"\");\n\tconst remaining = Math.max(0, width - visibleWidth(fitted));\n\treturn `${\" \".repeat(Math.floor(remaining / 2))}${fitted}${\" \".repeat(Math.ceil(remaining / 2))}`;\n}\n\nexport function divider(width: number, label: string, color: ThemeColor): string {\n\tconst fillWidth = Math.max(0, width - visibleWidth(\"+-- \") - visibleWidth(label) - visibleWidth(\" +\"));\n\treturn clipLine(\n\t\t`${theme.fg(\"border\", \"+-- \")}${theme.bold(theme.fg(color, label))}${theme.fg(\"border\", ` ${\"-\".repeat(fillWidth)}+`)}`,\n\t\twidth,\n\t);\n}\n\nexport function textLine(width: number, text: string, color?: ThemeColor): string {\n\tconst body = color ? theme.fg(color, text) : text;\n\treturn clipLine(\n\t\t`${theme.fg(\"borderMuted\", \"\\u2502 \")}${fitLine(body, Math.max(0, width - 4))}${theme.fg(\"borderMuted\", \" \\u2502\")}`,\n\t\twidth,\n\t);\n}\n\nexport function fitLine(line: string, width: number): string {\n\tconst clipped = truncateToWidth(line, width, \"\");\n\treturn `${clipped}${\" \".repeat(Math.max(0, width - visibleWidth(clipped)))}`;\n}\n\nexport function clipLine(line: string, width: number): string {\n\treturn visibleWidth(line) <= width ? line : truncateToWidth(line, width, \"\");\n}\n"]}
@@ -3,31 +3,83 @@ import { theme } from "../theme/theme.js";
3
3
  export function composeColumns(leftLines, leftWidth, rightLines, rightWidth, gapWidth, width) {
4
4
  return Array.from({ length: Math.max(leftLines.length, rightLines.length) }, (_, index) => clipLine(`${fitLine(leftLines[index] ?? "", leftWidth)}${" ".repeat(gapWidth)}${fitLine(rightLines[index] ?? "", rightWidth)}`, width));
5
5
  }
6
- export function sidebarRule(width, label, borderColor = "borderAccent") {
6
+ /**
7
+ * Section rule for the control rail.
8
+ *
9
+ * Editorial treatment: the label sits flush left behind a single hairline tick
10
+ * and the rule runs out to the right edge, instead of a centered bold caption.
11
+ * Frames stay hairline so the accent colour is reserved for live values.
12
+ */
13
+ export function sidebarRule(width, label, borderColor = "borderMuted") {
7
14
  const bodyWidth = Math.max(0, width - 2);
8
15
  const labelText = ` ${label} `;
9
- const fill = Math.max(0, bodyWidth - visibleWidth(labelText));
10
- const left = Math.floor(fill / 2);
11
- const right = fill - left;
12
- return clipLine(`${theme.fg(borderColor, "\u2502")}${theme.fg("borderMuted", "\u2500".repeat(left))}${theme.bold(theme.fg("accent", labelText))}${theme.fg("borderMuted", "\u2500".repeat(right))}${theme.fg(borderColor, "\u2502")}`, width);
16
+ const fill = Math.max(0, bodyWidth - visibleWidth(labelText) - 1);
17
+ return clipLine(`${theme.fg(borderColor, "\u2502")}${theme.fg("borderMuted", "\u2500")}${theme.fg("accent", labelText)}${theme.fg("borderMuted", "\u2500".repeat(fill))}${theme.fg(borderColor, "\u2502")}`, width);
13
18
  }
14
- export function boxTop(width, label, borderColor = "borderAccent") {
19
+ /** Width of the right-aligned label gutter shared by every control-rail row. */
20
+ export const RAIL_LABEL_COLUMN = 8;
21
+ /**
22
+ * Right-aligned `label:` gutter cell.
23
+ *
24
+ * Labels are right-aligned so every value starts on the same column while the
25
+ * rendered text keeps exactly one space after the colon (`route: active`),
26
+ * which downstream reference-fidelity assertions depend on.
27
+ */
28
+ export function labelCell(label, column = RAIL_LABEL_COLUMN) {
29
+ const text = `${label}:`;
30
+ const pad = Math.max(0, column + 1 - visibleWidth(text));
31
+ return `${" ".repeat(pad)}${theme.fg("muted", text)}`;
32
+ }
33
+ export function boxTop(width, label, borderColor = "borderMuted") {
15
34
  const text = ` ${label} `;
16
35
  const fillWidth = Math.max(0, width - visibleWidth("+") - visibleWidth(text) - visibleWidth("+"));
17
36
  return clipLine(`${theme.fg(borderColor, "\u256d")}${theme.bold(theme.fg("accent", text))}${theme.fg(borderColor, "\u2500".repeat(fillWidth))}${theme.fg(borderColor, "\u256e")}`, width);
18
37
  }
19
- export function boxBottom(width, borderColor = "borderAccent") {
38
+ export function boxBottom(width, borderColor = "borderMuted") {
20
39
  return clipLine(theme.fg(borderColor, `\u2570${"\u2500".repeat(Math.max(0, width - 2))}\u256f`), width);
21
40
  }
22
41
  export function boxBlankLine(width) {
23
42
  return boxTextLine(width, "");
24
43
  }
44
+ /** Hairline rule inside a framed panel, inset from both frame edges. */
45
+ export function boxRuleLine(width, inset = 0) {
46
+ const innerWidth = Math.max(0, width - 4);
47
+ const ruleWidth = Math.max(0, innerWidth - inset * 2);
48
+ return boxTextLine(width, `${" ".repeat(inset)}${theme.fg("borderMuted", "\u2500".repeat(ruleWidth))}`);
49
+ }
25
50
  export function boxCenteredLine(width, text, color) {
26
51
  return boxTextLine(width, centerText(Math.max(0, width - 4), color ? theme.fg(color, text) : text));
27
52
  }
28
53
  export function boxTextLine(width, text, color) {
29
54
  const body = color ? theme.fg(color, text) : text;
30
- return clipLine(`${theme.fg("borderAccent", "\u2502 ")}${fitLine(body, Math.max(0, width - 4))}${theme.fg("borderAccent", " \u2502")}`, width);
55
+ return clipLine(`${theme.fg("borderMuted", "\u2502 ")}${fitLine(body, Math.max(0, width - 4))}${theme.fg("borderMuted", " \u2502")}`, width);
56
+ }
57
+ /**
58
+ * Pad a framed column with blank body rows so two side-by-side panels close on
59
+ * the same row. The leading and trailing frame lines are preserved.
60
+ *
61
+ * `"center"` splits the filler above and below the body, which turns leftover
62
+ * height into deliberate negative space instead of a hollow bottom half.
63
+ */
64
+ export function padBoxColumn(lines, targetHeight, width, distribution = "bottom") {
65
+ if (lines.length === 0 || lines.length >= targetHeight)
66
+ return lines;
67
+ const top = lines[0];
68
+ const bottom = lines[lines.length - 1];
69
+ const body = lines.slice(1, -1);
70
+ const missing = targetHeight - lines.length;
71
+ const blank = () => boxTextLine(width, "");
72
+ if (distribution === "bottom") {
73
+ return [top, ...body, ...Array.from({ length: missing }, blank), bottom];
74
+ }
75
+ const above = Math.floor(missing / 2);
76
+ return [
77
+ top,
78
+ ...Array.from({ length: above }, blank),
79
+ ...body,
80
+ ...Array.from({ length: missing - above }, blank),
81
+ bottom,
82
+ ];
31
83
  }
32
84
  export function centerLine(width, text, color) {
33
85
  return clipLine(centerText(width, color ? theme.fg(color, text) : text), width);
@@ -1 +1 @@
1
- {"version":3,"file":"control-panel-box.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/control-panel-box.ts"],"names":[],"mappings":"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","sourcesContent":["import { truncateToWidth, visibleWidth } from \"omk-tui\";\nimport { type ThemeColor, theme } from \"../theme/theme.ts\";\n\nexport function composeColumns(\n\tleftLines: string[],\n\tleftWidth: number,\n\trightLines: string[],\n\trightWidth: number,\n\tgapWidth: number,\n\twidth: number,\n): string[] {\n\treturn Array.from({ length: Math.max(leftLines.length, rightLines.length) }, (_, index) =>\n\t\tclipLine(\n\t\t\t`${fitLine(leftLines[index] ?? \"\", leftWidth)}${\" \".repeat(gapWidth)}${fitLine(rightLines[index] ?? \"\", rightWidth)}`,\n\t\t\twidth,\n\t\t),\n\t);\n}\n\nexport function sidebarRule(width: number, label: string, borderColor: ThemeColor = \"borderAccent\"): string {\n\tconst bodyWidth = Math.max(0, width - 2);\n\tconst labelText = ` ${label} `;\n\tconst fill = Math.max(0, bodyWidth - visibleWidth(labelText));\n\tconst left = Math.floor(fill / 2);\n\tconst right = fill - left;\n\treturn clipLine(\n\t\t`${theme.fg(borderColor, \"\\u2502\")}${theme.fg(\"borderMuted\", \"\\u2500\".repeat(left))}${theme.bold(theme.fg(\"accent\", labelText))}${theme.fg(\"borderMuted\", \"\\u2500\".repeat(right))}${theme.fg(borderColor, \"\\u2502\")}`,\n\t\twidth,\n\t);\n}\n\nexport function boxTop(width: number, label: string, borderColor: ThemeColor = \"borderAccent\"): string {\n\tconst text = ` ${label} `;\n\tconst fillWidth = Math.max(0, width - visibleWidth(\"+\") - visibleWidth(text) - visibleWidth(\"+\"));\n\treturn clipLine(\n\t\t`${theme.fg(borderColor, \"\\u256d\")}${theme.bold(theme.fg(\"accent\", text))}${theme.fg(borderColor, \"\\u2500\".repeat(fillWidth))}${theme.fg(borderColor, \"\\u256e\")}`,\n\t\twidth,\n\t);\n}\n\nexport function boxBottom(width: number, borderColor: ThemeColor = \"borderAccent\"): string {\n\treturn clipLine(theme.fg(borderColor, `\\u2570${\"\\u2500\".repeat(Math.max(0, width - 2))}\\u256f`), width);\n}\n\nexport function boxBlankLine(width: number): string {\n\treturn boxTextLine(width, \"\");\n}\n\nexport function boxCenteredLine(width: number, text: string, color?: ThemeColor): string {\n\treturn boxTextLine(width, centerText(Math.max(0, width - 4), color ? theme.fg(color, text) : text));\n}\n\nexport function boxTextLine(width: number, text: string, color?: ThemeColor): string {\n\tconst body = color ? theme.fg(color, text) : text;\n\treturn clipLine(\n\t\t`${theme.fg(\"borderAccent\", \"\\u2502 \")}${fitLine(body, Math.max(0, width - 4))}${theme.fg(\"borderAccent\", \" \\u2502\")}`,\n\t\twidth,\n\t);\n}\n\nexport function centerLine(width: number, text: string, color?: ThemeColor): string {\n\treturn clipLine(centerText(width, color ? theme.fg(color, text) : text), width);\n}\n\nexport function centerText(width: number, text: string): string {\n\tconst fitted = truncateToWidth(text, width, \"\");\n\tconst remaining = Math.max(0, width - visibleWidth(fitted));\n\treturn `${\" \".repeat(Math.floor(remaining / 2))}${fitted}${\" \".repeat(Math.ceil(remaining / 2))}`;\n}\n\nexport function divider(width: number, label: string, color: ThemeColor): string {\n\tconst fillWidth = Math.max(0, width - visibleWidth(\"+-- \") - visibleWidth(label) - visibleWidth(\" +\"));\n\treturn clipLine(\n\t\t`${theme.fg(\"border\", \"+-- \")}${theme.bold(theme.fg(color, label))}${theme.fg(\"border\", ` ${\"-\".repeat(fillWidth)}+`)}`,\n\t\twidth,\n\t);\n}\n\nexport function textLine(width: number, text: string, color?: ThemeColor): string {\n\tconst body = color ? theme.fg(color, text) : text;\n\treturn clipLine(\n\t\t`${theme.fg(\"borderMuted\", \"\\u2502 \")}${fitLine(body, Math.max(0, width - 4))}${theme.fg(\"borderMuted\", \" \\u2502\")}`,\n\t\twidth,\n\t);\n}\n\nexport function fitLine(line: string, width: number): string {\n\tconst clipped = truncateToWidth(line, width, \"\");\n\treturn `${clipped}${\" \".repeat(Math.max(0, width - visibleWidth(clipped)))}`;\n}\n\nexport function clipLine(line: string, width: number): string {\n\treturn visibleWidth(line) <= width ? line : truncateToWidth(line, width, \"\");\n}\n"]}
1
+ {"version":3,"file":"control-panel-box.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/control-panel-box.ts"],"names":[],"mappings":"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","sourcesContent":["import { truncateToWidth, visibleWidth } from \"omk-tui\";\nimport { type ThemeColor, theme } from \"../theme/theme.ts\";\n\nexport function composeColumns(\n\tleftLines: string[],\n\tleftWidth: number,\n\trightLines: string[],\n\trightWidth: number,\n\tgapWidth: number,\n\twidth: number,\n): string[] {\n\treturn Array.from({ length: Math.max(leftLines.length, rightLines.length) }, (_, index) =>\n\t\tclipLine(\n\t\t\t`${fitLine(leftLines[index] ?? \"\", leftWidth)}${\" \".repeat(gapWidth)}${fitLine(rightLines[index] ?? \"\", rightWidth)}`,\n\t\t\twidth,\n\t\t),\n\t);\n}\n\n/**\n * Section rule for the control rail.\n *\n * Editorial treatment: the label sits flush left behind a single hairline tick\n * and the rule runs out to the right edge, instead of a centered bold caption.\n * Frames stay hairline so the accent colour is reserved for live values.\n */\nexport function sidebarRule(width: number, label: string, borderColor: ThemeColor = \"borderMuted\"): string {\n\tconst bodyWidth = Math.max(0, width - 2);\n\tconst labelText = ` ${label} `;\n\tconst fill = Math.max(0, bodyWidth - visibleWidth(labelText) - 1);\n\treturn clipLine(\n\t\t`${theme.fg(borderColor, \"\\u2502\")}${theme.fg(\"borderMuted\", \"\\u2500\")}${theme.fg(\"accent\", labelText)}${theme.fg(\"borderMuted\", \"\\u2500\".repeat(fill))}${theme.fg(borderColor, \"\\u2502\")}`,\n\t\twidth,\n\t);\n}\n\n/** Width of the right-aligned label gutter shared by every control-rail row. */\nexport const RAIL_LABEL_COLUMN = 8;\n\n/**\n * Right-aligned `label:` gutter cell.\n *\n * Labels are right-aligned so every value starts on the same column while the\n * rendered text keeps exactly one space after the colon (`route: active`),\n * which downstream reference-fidelity assertions depend on.\n */\nexport function labelCell(label: string, column: number = RAIL_LABEL_COLUMN): string {\n\tconst text = `${label}:`;\n\tconst pad = Math.max(0, column + 1 - visibleWidth(text));\n\treturn `${\" \".repeat(pad)}${theme.fg(\"muted\", text)}`;\n}\n\nexport function boxTop(width: number, label: string, borderColor: ThemeColor = \"borderMuted\"): string {\n\tconst text = ` ${label} `;\n\tconst fillWidth = Math.max(0, width - visibleWidth(\"+\") - visibleWidth(text) - visibleWidth(\"+\"));\n\treturn clipLine(\n\t\t`${theme.fg(borderColor, \"\\u256d\")}${theme.bold(theme.fg(\"accent\", text))}${theme.fg(borderColor, \"\\u2500\".repeat(fillWidth))}${theme.fg(borderColor, \"\\u256e\")}`,\n\t\twidth,\n\t);\n}\n\nexport function boxBottom(width: number, borderColor: ThemeColor = \"borderMuted\"): string {\n\treturn clipLine(theme.fg(borderColor, `\\u2570${\"\\u2500\".repeat(Math.max(0, width - 2))}\\u256f`), width);\n}\n\nexport function boxBlankLine(width: number): string {\n\treturn boxTextLine(width, \"\");\n}\n\n/** Hairline rule inside a framed panel, inset from both frame edges. */\nexport function boxRuleLine(width: number, inset = 0): string {\n\tconst innerWidth = Math.max(0, width - 4);\n\tconst ruleWidth = Math.max(0, innerWidth - inset * 2);\n\treturn boxTextLine(width, `${\" \".repeat(inset)}${theme.fg(\"borderMuted\", \"\\u2500\".repeat(ruleWidth))}`);\n}\n\nexport function boxCenteredLine(width: number, text: string, color?: ThemeColor): string {\n\treturn boxTextLine(width, centerText(Math.max(0, width - 4), color ? theme.fg(color, text) : text));\n}\n\nexport function boxTextLine(width: number, text: string, color?: ThemeColor): string {\n\tconst body = color ? theme.fg(color, text) : text;\n\treturn clipLine(\n\t\t`${theme.fg(\"borderMuted\", \"\\u2502 \")}${fitLine(body, Math.max(0, width - 4))}${theme.fg(\"borderMuted\", \" \\u2502\")}`,\n\t\twidth,\n\t);\n}\n\n/**\n * Pad a framed column with blank body rows so two side-by-side panels close on\n * the same row. The leading and trailing frame lines are preserved.\n *\n * `\"center\"` splits the filler above and below the body, which turns leftover\n * height into deliberate negative space instead of a hollow bottom half.\n */\nexport function padBoxColumn(\n\tlines: string[],\n\ttargetHeight: number,\n\twidth: number,\n\tdistribution: \"bottom\" | \"center\" = \"bottom\",\n): string[] {\n\tif (lines.length === 0 || lines.length >= targetHeight) return lines;\n\tconst top = lines[0] as string;\n\tconst bottom = lines[lines.length - 1] as string;\n\tconst body = lines.slice(1, -1);\n\tconst missing = targetHeight - lines.length;\n\tconst blank = () => boxTextLine(width, \"\");\n\tif (distribution === \"bottom\") {\n\t\treturn [top, ...body, ...Array.from({ length: missing }, blank), bottom];\n\t}\n\tconst above = Math.floor(missing / 2);\n\treturn [\n\t\ttop,\n\t\t...Array.from({ length: above }, blank),\n\t\t...body,\n\t\t...Array.from({ length: missing - above }, blank),\n\t\tbottom,\n\t];\n}\n\nexport function centerLine(width: number, text: string, color?: ThemeColor): string {\n\treturn clipLine(centerText(width, color ? theme.fg(color, text) : text), width);\n}\n\nexport function centerText(width: number, text: string): string {\n\tconst fitted = truncateToWidth(text, width, \"\");\n\tconst remaining = Math.max(0, width - visibleWidth(fitted));\n\treturn `${\" \".repeat(Math.floor(remaining / 2))}${fitted}${\" \".repeat(Math.ceil(remaining / 2))}`;\n}\n\nexport function divider(width: number, label: string, color: ThemeColor): string {\n\tconst fillWidth = Math.max(0, width - visibleWidth(\"+-- \") - visibleWidth(label) - visibleWidth(\" +\"));\n\treturn clipLine(\n\t\t`${theme.fg(\"border\", \"+-- \")}${theme.bold(theme.fg(color, label))}${theme.fg(\"border\", ` ${\"-\".repeat(fillWidth)}+`)}`,\n\t\twidth,\n\t);\n}\n\nexport function textLine(width: number, text: string, color?: ThemeColor): string {\n\tconst body = color ? theme.fg(color, text) : text;\n\treturn clipLine(\n\t\t`${theme.fg(\"borderMuted\", \"\\u2502 \")}${fitLine(body, Math.max(0, width - 4))}${theme.fg(\"borderMuted\", \" \\u2502\")}`,\n\t\twidth,\n\t);\n}\n\nexport function fitLine(line: string, width: number): string {\n\tconst clipped = truncateToWidth(line, width, \"\");\n\treturn `${clipped}${\" \".repeat(Math.max(0, width - visibleWidth(clipped)))}`;\n}\n\nexport function clipLine(line: string, width: number): string {\n\treturn visibleWidth(line) <= width ? line : truncateToWidth(line, width, \"\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"control-panel-layout.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/control-panel-layout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAkB,KAAK,SAAS,EAA0B,MAAM,6BAA6B,CAAC;AAgBrG,eAAO,MAAM,uBAAuB,UAQnC,CAAC;AAEF,eAAO,MAAM,+BAA+B,MAAM,CAAC;AACnD,eAAO,MAAM,4BAA4B,QAAsC,CAAC;AAChF,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAC9C,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAKzC,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB,EAAE,MAAM,MAAM,CAAC;IAClC,oBAAoB,EAAE,MAAM,MAAM,CAAC;IACnC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IAChC,UAAU,EAAE,MAAM,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,0BAA0B,CAAC;CAClD;AAED,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,sBAAsB,CAAC;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,EAAE,EACtB,SAAS,SAAI,GACX,MAAM,EAAE,CAKV;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAGjG","sourcesContent":["import { truncateToWidth, visibleWidth } from \"omk-tui\";\nimport type { PiPackageIntakeSummary } from \"../../../core/pi-package-intake.ts\";\nimport { nextActiveTodo, type TodoState, summary as todoSummary } from \"../../../core/todo-state.ts\";\nimport { type ThemeColor, theme } from \"../theme/theme.ts\";\nimport {\n\tboxBottom,\n\tboxCenteredLine,\n\tboxTextLine,\n\tboxTop,\n\tcenterLine,\n\tcomposeColumns,\n\tdivider,\n\tfitLine,\n\tsidebarRule,\n\ttextLine,\n} from \"./control-panel-box.ts\";\nimport { SPARKLE_ROW_BOTTOM, SPARKLE_ROW_TOP, sparkleRow } from \"./control-panel-sparkles.ts\";\n\nexport const CONTROL_PANEL_ASCII_ART = [\n\t\" ███████ ██ ██ ██ ██\",\n\t\"██ ██ ███ ███ ██ ██ \",\n\t\"██ ██ ████ ████ ██ ██ \",\n\t\"██ ██ ██ ██ ██ ██ █████ \",\n\t\"██ ██ ██ ███ ██ ██ ██ \",\n\t\"██ ██ ██ █ ██ ██ ██ \",\n\t\" ███████ ██ ██ ██ ██\",\n];\nconst META_WIDTH = 31;\nexport const CONTROL_PANEL_OVERLAY_MIN_WIDTH = 112;\nexport const CONTROL_PANEL_DECK_MIN_WIDTH = CONTROL_PANEL_OVERLAY_MIN_WIDTH + 1;\nexport const CONTROL_PANEL_SIDEBAR_WIDTH = 38;\nexport const CONTROL_PANEL_GAP_WIDTH = 2;\nconst CONTEXT_METER_CELLS = 12;\nconst SPARKLINE_WIDTH = 16;\nconst SPARKLINE_GLYPHS = \"▁▂▃▄▅▆▇█\";\n\nexport interface ControlPanelContent {\n\tappName: string;\n\tversion: string;\n\tcompactInstructions: () => string;\n\texpandedInstructions: () => string;\n\tcompactOnboarding: () => string;\n\tonboarding: () => string;\n\tstatusSnapshot?: () => ControlPanelStatusSnapshot;\n}\n\nexport interface ControlPanelStatusSnapshot {\n\treadonly modelId?: string;\n\treadonly modelProvider?: string;\n\treadonly thinkingLevel?: string;\n\treadonly contextPercent?: number | null;\n\treadonly contextWindowTokens?: number;\n\treadonly contextTokens?: number | null;\n\treadonly headroomStatus?: string;\n\treadonly optimizerPolicy?: string;\n\treadonly skillCount?: number;\n\treadonly mcpCount?: number;\n\treadonly packageIntake?: PiPackageIntakeSummary;\n\treadonly cwdLabel?: string;\n\treadonly gitBranch?: string | null;\n\treadonly todoState?: TodoState;\n\treadonly runtimeState?: string;\n\treadonly routeState?: string;\n\treadonly evidenceState?: string;\n\treadonly controlState?: string;\n\treadonly dagOrchestrationState?: string;\n\treadonly ansiColorState?: string;\n\treadonly startupState?: string;\n\treadonly linkState?: string;\n\treadonly sidebarState?: string;\n}\n\nexport function renderControlPanelLayout(\n\tcontent: ControlPanelContent,\n\texpanded: boolean,\n\twidth: number,\n\tbannerFrame?: string[],\n\tsparkleMs = 0,\n): string[] {\n\tif (width <= 0) return [];\n\treturn expanded\n\t\t? renderExpanded(content, width, bannerFrame, sparkleMs)\n\t\t: renderCompact(content, width, bannerFrame, sparkleMs);\n}\n\nexport function renderControlPanelRightPane(content: ControlPanelContent, width: number): string[] {\n\tif (width <= 0) return [];\n\treturn sidebarPanel(content, width);\n}\n\nfunction renderCompact(content: ControlPanelContent, width: number, bannerFrame?: string[], sparkleMs = 0): string[] {\n\tif (width >= CONTROL_PANEL_DECK_MIN_WIDTH) {\n\t\tconst deck = renderDeck(content, width, bannerFrame, sparkleMs);\n\t\tif (deck.length > 0) return deck;\n\t}\n\treturn [\n\t\tdivider(width, \"OMK//CONTROL PANEL\", \"accent\"),\n\t\tstatusLine(content, width),\n\t\ttextLine(width, content.compactInstructions()),\n\t\ttextLine(width, content.compactOnboarding(), \"dim\"),\n\t];\n}\n\nfunction renderDeck(content: ControlPanelContent, width: number, bannerFrame?: string[], sparkleMs = 0): string[] {\n\tconst { leftWidth, sidebarWidth } = deckWidths(width);\n\tif (leftWidth < 72) return [];\n\tconst lines = composeColumns(\n\t\theroPanel(content, leftWidth, bannerFrame, sparkleMs),\n\t\tleftWidth,\n\t\tsidebarPanel(content, sidebarWidth),\n\t\tsidebarWidth,\n\t\tCONTROL_PANEL_GAP_WIDTH,\n\t\twidth,\n\t);\n\tlines.push(controlStripLine(content, width));\n\tlines.push(centerLine(width, content.compactOnboarding(), \"dim\"));\n\treturn lines;\n}\n\nfunction deckWidths(width: number): { leftWidth: number; sidebarWidth: number } {\n\tconst sidebarWidth = Math.min(CONTROL_PANEL_SIDEBAR_WIDTH, Math.max(34, Math.floor(width * 0.28)));\n\treturn { leftWidth: width - CONTROL_PANEL_GAP_WIDTH - sidebarWidth, sidebarWidth };\n}\n\nfunction heroPanel(content: ControlPanelContent, width: number, bannerFrame?: string[], sparkleMs = 0): string[] {\n\tconst snapshot = statusSnapshot(content);\n\tconst innerWidth = Math.max(0, width - 4);\n\treturn [\n\t\tboxTop(width, `omk v${content.version} · OMK://CONTROL`),\n\t\tboxCenteredLine(width, theme.bold(theme.fg(\"accent\", \"OMK\"))),\n\t\tboxCenteredLine(width, theme.fg(\"muted\", \"route · verify · loop · control\")),\n\t\tboxTextLine(width, sparkleRow(innerWidth, SPARKLE_ROW_TOP, sparkleMs)),\n\t\t...(bannerFrame ?? CONTROL_PANEL_ASCII_ART).map((line, index) => {\n\t\t\tif (bannerFrame) return boxCenteredLine(width, line);\n\t\t\tconst color: ThemeColor = index < 2 ? \"mdCode\" : index < 4 ? \"accent\" : index < 6 ? \"success\" : \"warning\";\n\t\t\treturn boxCenteredLine(width, theme.fg(color, line));\n\t\t}),\n\t\tboxTextLine(width, sparkleRow(innerWidth, SPARKLE_ROW_BOTTOM, sparkleMs)),\n\t\tboxCenteredLine(\n\t\t\twidth,\n\t\t\t`${theme.fg(\"warning\", \"●\")} ${theme.bold(\"OMK\")} / ${theme.fg(\"success\", heroModelLabel(snapshot))}`,\n\t\t),\n\t\tboxCenteredLine(\n\t\t\twidth,\n\t\t\t`${theme.fg(\"mdCode\", \"◇\")} omk-control · ${theme.fg(\"accent\", \"route\")} · ${theme.fg(\"warning\", \"verify\")} · ${theme.fg(\"success\", \"loop\")} · ${theme.fg(\"mdCode\", \"control\")}`,\n\t\t),\n\t\tboxBottom(width),\n\t];\n}\n\nfunction heroModelLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tif (!snapshot.modelId) return \"no-model\";\n\tconst think = snapshot.thinkingLevel;\n\treturn think && think !== \"off\" ? `${snapshot.modelId}:${think}` : snapshot.modelId;\n}\n\nfunction sidebarPanel(content: ControlPanelContent, width: number): string[] {\n\tconst snapshot = statusSnapshot(content);\n\tconst headroomLabel = snapshot.headroomStatus ?? snapshot.optimizerPolicy ?? \"unknown\";\n\tconst mcpCount = snapshot.mcpCount;\n\tconst skillCount = snapshot.skillCount;\n\tconst packageIntakeLabel = packageIntakeStatusLabel(snapshot.packageIntake);\n\tconst contextMeter = contextMeterLabel(snapshot);\n\tconst activitySparkline = activitySparklineLabel(snapshot);\n\treturn [\n\t\tsidebarTabs(width),\n\t\tboxCenteredLine(width, theme.bold(theme.fg(\"accent\", \"OMK://CONTROL\"))),\n\t\tboxCenteredLine(width, theme.bold(theme.fg(\"warning\", \"CYBERPUNK OPS CORE\"))),\n\t\tboxCenteredLine(width, `${theme.fg(\"mdCode\", \"MATRIX RAIN\")} // ${theme.fg(\"success\", \"NEON GRID ONLINE\")}`),\n\t\tboxCenteredLine(width, theme.fg(\"text\", \"NIGHT-CITY-MATRIX-V3\")),\n\t\tsidebarRule(width, \"STATUS\", \"accent\"),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"state:\")} ${coloredStatus(snapshot.runtimeState ?? \"unknown\")}`),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"route:\")} ${coloredStatus(snapshot.routeState ?? \"unknown\")}`),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"evidence:\")} ${coloredStatus(snapshot.evidenceState ?? \"unknown\")}`),\n\t\tsidebarRule(width, \"TODO\", \"accent\"),\n\t\t...todoSidebarLines(snapshot.todoState, width),\n\t\tsidebarRule(width, \"SESSION\", \"accent\"),\n\t\tsemanticBoxTextLine(width, \"cwd\", snapshot.cwdLabel ?? \"?\", \"end\"),\n\t\tsemanticBoxTextLine(width, \"git\", snapshot.gitBranch ?? \"?\", \"start\"),\n\t\tsidebarRule(width, \"MODEL / CTX\", \"accent\"),\n\t\tsemanticBoxTextLine(width, \"model\", modelStatusLabel(snapshot), \"end\"),\n\t\tsemanticBoxTextLine(width, \"think\", snapshot.thinkingLevel ?? \"off\", \"start\"),\n\t\tboxTextLine(\n\t\t\twidth,\n\t\t\t`${theme.fg(\"muted\", \"ctx:\")} ${theme.fg(statusColor(snapshot.contextPercent === undefined || snapshot.contextPercent === null ? \"unknown\" : \"ready\"), contextStatusLabel(snapshot))}`,\n\t\t),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"meter:\")} ${contextMeter}`),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"pulse:\")} ${activitySparkline}`),\n\t\tsidebarRule(width, \"RUNTIME / MCP / SKILLS\", \"accent\"),\n\t\tsemanticBoxTextLine(width, \"headroom\", headroomLabel, \"end\"),\n\t\tsemanticBoxTextLine(width, \"omk\", snapshot.dagOrchestrationState ?? \"unknown\", \"end\"),\n\t\tsemanticBoxTextLine(width, \"sidebar\", snapshot.sidebarState ?? \"unknown\", \"start\"),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"res:\")} MCP:${mcpCount ?? \"?\"} skills:${skillCount ?? \"?\"}`),\n\t\tsemanticBoxTextLine(width, \"pkg\", packageIntakeLabel, \"end\"),\n\t\tsidebarRule(width, \"CONTROL\", \"accent\"),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"route:\")} ${coloredStatus(snapshot.routeState ?? \"unknown\")}`),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"verify:\")} ${coloredStatus(snapshot.evidenceState ?? \"unknown\")}`),\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"control:\")} ${coloredStatus(snapshot.controlState ?? \"unknown\")}`),\n\t\tboxBottom(width, \"accent\"),\n\t];\n}\n\nfunction semanticBoxTextLine(\n\twidth: number,\n\tlabel: string,\n\tvalue: string,\n\tpreserve: \"start\" | \"middle\" | \"end\",\n): string {\n\tconst prefix = `${theme.fg(\"muted\", `${label}:`)} `;\n\tconst availableWidth = Math.max(0, width - 4 - visibleWidth(prefix));\n\treturn boxTextLine(width, `${prefix}${semanticTruncate(value, availableWidth, preserve)}`);\n}\n\nfunction semanticTruncate(value: string, maxWidth: number, preserve: \"start\" | \"middle\" | \"end\"): string {\n\tif (visibleWidth(value) <= maxWidth) return value;\n\tif (maxWidth <= 0) return \"\";\n\tif (preserve === \"start\") return truncateToWidth(value, maxWidth, \"…\");\n\tconst ellipsis = \"…\";\n\tconst targetWidth = maxWidth - visibleWidth(ellipsis);\n\tif (targetWidth <= 0) return truncateToWidth(ellipsis, maxWidth, \"\");\n\tif (preserve === \"middle\") {\n\t\tconst headWidth = Math.max(1, Math.floor(targetWidth / 2));\n\t\tconst tailWidth = Math.max(1, targetWidth - headWidth);\n\t\treturn `${takeStart(value, headWidth)}${ellipsis}${takeEnd(value, tailWidth)}`;\n\t}\n\treturn `${ellipsis}${takeEnd(value, targetWidth)}`;\n}\n\nfunction takeStart(value: string, maxWidth: number): string {\n\tlet prefix = \"\";\n\tfor (const char of Array.from(value)) {\n\t\tif (visibleWidth(prefix + char) > maxWidth) break;\n\t\tprefix += char;\n\t}\n\treturn prefix;\n}\n\nfunction takeEnd(value: string, maxWidth: number): string {\n\tlet suffix = \"\";\n\tfor (const char of Array.from(value).reverse()) {\n\t\tif (visibleWidth(char + suffix) > maxWidth) break;\n\t\tsuffix = char + suffix;\n\t}\n\treturn suffix;\n}\n\nfunction statusToken(label: string, value: string | undefined): string {\n\tconst state = value ?? \"unknown\";\n\treturn theme.fg(statusColor(state), `${label}:${state.toUpperCase()}`);\n}\n\nfunction coloredStatus(value: string): string {\n\treturn theme.fg(statusColor(value), value);\n}\n\nfunction statusColor(value: string): ThemeColor {\n\tconst normalized = value.toLowerCase();\n\tif ([\"ready\", \"on\", \"active\", \"available\", \"linked\", \"tracking\", \"pinned\"].includes(normalized)) return \"success\";\n\tif ([\"degraded\", \"limited\", \"blocked\", \"off\"].includes(normalized)) return \"warning\";\n\treturn \"muted\";\n}\n\nfunction todoSidebarLines(state: TodoState | undefined, width: number): string[] {\n\tif (!state || state.items.length === 0) {\n\t\treturn [\n\t\t\tboxTextLine(width, `${theme.fg(\"muted\", \"todo:\")} empty`),\n\t\t\tboxTextLine(width, `${theme.fg(\"muted\", \"next:\")} no active todos`),\n\t\t];\n\t}\n\tconst counts = todoSummary(state);\n\tconst next = nextActiveTodo(state);\n\treturn [\n\t\tboxTextLine(width, `${theme.fg(\"muted\", \"todo:\")} ${counts.done}/${counts.total} done`),\n\t\tsemanticBoxTextLine(width, \"next\", next?.label ?? \"complete\", \"middle\"),\n\t];\n}\n\nfunction packageIntakeStatusLabel(summary: PiPackageIntakeSummary | undefined): string {\n\tif (!summary) return \"ports:pending\";\n\tconst ready =\n\t\tsummary.acceptedNative + summary.acceptedReference + summary.acceptedMeasurement + summary.acceptedAdvisory;\n\tconst review = summary.deferred + summary.reject;\n\treturn `ports:${ready}/${summary.total} review:${review} block:${summary.hardForkBlocked}`;\n}\n\nfunction renderExpanded(content: ControlPanelContent, width: number, bannerFrame?: string[], sparkleMs = 0): string[] {\n\tif (width >= CONTROL_PANEL_DECK_MIN_WIDTH) {\n\t\tconst { leftWidth, sidebarWidth } = deckWidths(width);\n\t\tconst resourceLines = [\"\", ...content.onboarding().split(\"\\n\")];\n\t\tconst rightRail = blankSidebarRail(sidebarWidth, resourceLines.length);\n\t\tconst lines = [...renderDeck(content, width, bannerFrame, sparkleMs)];\n\t\tlines.push(...composeColumns(resourceLines, leftWidth, rightRail, sidebarWidth, CONTROL_PANEL_GAP_WIDTH, width));\n\t\treturn lines;\n\t}\n\n\tconst lines = [divider(width, \"OMK//CONTROL PANEL\", \"accent\"), statusLine(content, width)];\n\tif (width >= 32) lines.push(...brandLines(content, width, bannerFrame));\n\tlines.push(divider(width, \"SYSTEM MAP\", \"mdHeading\"));\n\tfor (const instruction of content.expandedInstructions().split(\"\\n\")) lines.push(textLine(width, instruction));\n\tlines.push(divider(width, \"STARTUP LINK\", \"success\"));\n\tfor (const onboardingLine of content.onboarding().split(\"\\n\")) lines.push(textLine(width, onboardingLine, \"dim\"));\n\tlines.push(divider(width, \"END\", \"borderMuted\"));\n\treturn lines;\n}\n\nfunction blankSidebarRail(width: number, lineCount: number): string[] {\n\treturn Array.from({ length: lineCount }, () => boxTextLine(width, \"\"));\n}\n\nfunction statusSnapshot(content: ControlPanelContent): ControlPanelStatusSnapshot {\n\treturn content.statusSnapshot?.() ?? {};\n}\n\nfunction modelStatusLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tif (!snapshot.modelId) return \"no-model\";\n\treturn snapshot.modelProvider ? `${snapshot.modelProvider}/${snapshot.modelId}` : snapshot.modelId;\n}\n\nfunction contextStatusLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tconst windowTokens = snapshot.contextWindowTokens ?? 0;\n\tconst windowLabel = windowTokens > 0 ? formatTokens(windowTokens) : \"?\";\n\treturn snapshot.contextPercent === null || snapshot.contextPercent === undefined\n\t\t? `?/${windowLabel}`\n\t\t: `${snapshot.contextPercent.toFixed(1)}%/${windowLabel}`;\n}\n\nfunction contextMeterLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tconst percent = normalizePercent(snapshot.contextPercent);\n\tif (percent === undefined) {\n\t\treturn `${theme.fg(\"borderMuted\", \"░\".repeat(CONTEXT_METER_CELLS))} ${theme.fg(\"muted\", \"??%\")}`;\n\t}\n\tconst filled = Math.max(0, Math.min(CONTEXT_METER_CELLS, Math.round((percent / 100) * CONTEXT_METER_CELLS)));\n\tconst color: ThemeColor = percent >= 85 ? \"warning\" : percent >= 65 ? \"mdCode\" : \"success\";\n\treturn `${theme.fg(color, \"█\".repeat(filled))}${theme.fg(\"borderMuted\", \"░\".repeat(CONTEXT_METER_CELLS - filled))} ${theme.fg(color, `${Math.round(percent)}%`)}`;\n}\n\nfunction activitySparklineLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tconst seed = hashSnapshot(snapshot);\n\tconst glyphs = Array.from(SPARKLINE_GLYPHS);\n\tconst parts: string[] = [];\n\tfor (let index = 0; index < SPARKLINE_WIDTH; index++) {\n\t\tconst glyph = glyphs[(seed + index * 5 + index * index) % glyphs.length] ?? \"▁\";\n\t\tconst color: ThemeColor = index % 5 === 0 ? \"accent\" : index % 3 === 0 ? \"warning\" : \"success\";\n\t\tparts.push(theme.fg(color, glyph));\n\t}\n\treturn parts.join(\"\");\n}\n\nfunction normalizePercent(value: number | null | undefined): number | undefined {\n\tif (value === null || value === undefined || !Number.isFinite(value)) return undefined;\n\tif (value < 0) return 0;\n\tif (value > 100) return 100;\n\treturn value;\n}\n\nfunction hashSnapshot(snapshot: ControlPanelStatusSnapshot): number {\n\tconst source = [\n\t\tsnapshot.modelProvider,\n\t\tsnapshot.modelId,\n\t\tsnapshot.contextPercent?.toFixed(1),\n\t\tsnapshot.contextTokens,\n\t\tsnapshot.contextWindowTokens,\n\t\tsnapshot.cwdLabel,\n\t\tsnapshot.gitBranch,\n\t].join(\"|\");\n\tlet hash = 2166136261;\n\tfor (let index = 0; index < source.length; index++) {\n\t\thash ^= source.charCodeAt(index);\n\t\thash = Math.imul(hash, 16777619);\n\t}\n\treturn hash >>> 0;\n}\n\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\nfunction sidebarTabs(width: number): string {\n\treturn boxTextLine(\n\t\twidth,\n\t\tfitLine(\n\t\t\t`${theme.bold(theme.fg(\"accent\", \"1:CONTROL\"))} ${theme.fg(\"muted\", \"2:HISTORY\")}`,\n\t\t\tMath.max(0, width - 4),\n\t\t),\n\t);\n}\n\nfunction controlStripLine(content: ControlPanelContent, width: number): string {\n\treturn centerLine(\n\t\twidth,\n\t\t`${theme.bold(theme.fg(\"accent\", \"OMK://CONTROL READ\"))} ${theme.fg(\"accent\", \"route/verify/loop/control\")} · ${content.compactInstructions()}`,\n\t);\n}\n\nfunction brandLines(content: ControlPanelContent, width: number, bannerFrame?: string[]): string[] {\n\tconst art = bannerFrame ?? CONTROL_PANEL_ASCII_ART;\n\tif (bannerFrame) return art.map((line) => textLine(width, line));\n\tconst leftWidth = Math.max(...art.map((line) => visibleWidth(line)));\n\tconst minWideWidth = visibleWidth(\"| \") + leftWidth + visibleWidth(\" | \") + META_WIDTH;\n\tif (width < minWideWidth) return art.map((line) => textLine(width, theme.fg(\"accent\", line)));\n\tconst snapshot = statusSnapshot(content);\n\treturn art.map((line, index) =>\n\t\ttextLine(\n\t\t\twidth,\n\t\t\t`${fitLine(theme.fg(\"accent\", line), leftWidth)}${theme.fg(\"borderMuted\", \" | \")}${metadataLines(snapshot)[index] ?? \"\"}`,\n\t\t),\n\t);\n}\n\nfunction metadataLines(snapshot: ControlPanelStatusSnapshot): string[] {\n\tconst rawThemeName = theme.name ?? \"live\";\n\tconst themeName = (rawThemeName.startsWith(\"omk-\") ? rawThemeName.slice(4) : rawThemeName).toUpperCase();\n\treturn [\n\t\t`${theme.fg(\"mdCode\", \"PANEL\")} ${coloredStatus(snapshot.runtimeState ?? \"unknown\")}`,\n\t\t`${theme.fg(\"mdCode\", \"THEME\")} ${theme.fg(\"accent\", themeName)}`,\n\t\t`${theme.fg(\"mdCode\", \"STARTUP\")} ${coloredStatus(snapshot.startupState ?? \"unknown\")}`,\n\t\t`${theme.fg(\"mdCode\", \"LINK\")} ${coloredStatus(snapshot.linkState ?? \"unknown\")}`,\n\t];\n}\n\nfunction statusLine(content: ControlPanelContent, width: number): string {\n\tconst snapshot = statusSnapshot(content);\n\tconst segments = [\n\t\ttheme.bold(theme.fg(\"accent\", `${content.appName.toUpperCase()} v${content.version}`)),\n\t\tstatusToken(\"CORE\", snapshot.runtimeState),\n\t\tstatusToken(\"ANSI\", snapshot.ansiColorState),\n\t\tstatusToken(\"STARTUP\", snapshot.startupState),\n\t\tstatusToken(\"LINK\", snapshot.linkState),\n\t\ttheme.fg(\"muted\", \"THEME:LIVE\"),\n\t];\n\treturn textLine(width, segments.join(theme.fg(\"borderMuted\", \" | \")));\n}\n"]}
1
+ {"version":3,"file":"control-panel-layout.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/control-panel-layout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAkB,KAAK,SAAS,EAA0B,MAAM,6BAA6B,CAAC;AAmBrG,eAAO,MAAM,uBAAuB,UAQnC,CAAC;AAEF,eAAO,MAAM,+BAA+B,MAAM,CAAC;AACnD,eAAO,MAAM,4BAA4B,QAAsC,CAAC;AAChF,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAC9C,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAKzC,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB,EAAE,MAAM,MAAM,CAAC;IAClC,oBAAoB,EAAE,MAAM,MAAM,CAAC;IACnC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IAChC,UAAU,EAAE,MAAM,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,0BAA0B,CAAC;CAClD;AAED,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,sBAAsB,CAAC;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,EAAE,EACtB,SAAS,SAAI,GACX,MAAM,EAAE,CAKV;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAGjG","sourcesContent":["import { truncateToWidth, visibleWidth } from \"omk-tui\";\nimport type { PiPackageIntakeSummary } from \"../../../core/pi-package-intake.ts\";\nimport { nextActiveTodo, type TodoState, summary as todoSummary } from \"../../../core/todo-state.ts\";\nimport { type ThemeColor, theme } from \"../theme/theme.ts\";\nimport {\n\tboxBottom,\n\tboxCenteredLine,\n\tboxRuleLine,\n\tboxTextLine,\n\tboxTop,\n\tcenterLine,\n\tcomposeColumns,\n\tdivider,\n\tfitLine,\n\tlabelCell,\n\tpadBoxColumn,\n\tsidebarRule,\n\ttextLine,\n} from \"./control-panel-box.ts\";\nimport { SPARKLE_ROW_BOTTOM, SPARKLE_ROW_TOP, sparkleRow } from \"./control-panel-sparkles.ts\";\n\nexport const CONTROL_PANEL_ASCII_ART = [\n\t\" ███████ ██ ██ ██ ██\",\n\t\"██ ██ ███ ███ ██ ██ \",\n\t\"██ ██ ████ ████ ██ ██ \",\n\t\"██ ██ ██ ██ ██ ██ █████ \",\n\t\"██ ██ ██ ███ ██ ██ ██ \",\n\t\"██ ██ ██ █ ██ ██ ██ \",\n\t\" ███████ ██ ██ ██ ██\",\n];\nconst META_WIDTH = 31;\nexport const CONTROL_PANEL_OVERLAY_MIN_WIDTH = 112;\nexport const CONTROL_PANEL_DECK_MIN_WIDTH = CONTROL_PANEL_OVERLAY_MIN_WIDTH + 1;\nexport const CONTROL_PANEL_SIDEBAR_WIDTH = 38;\nexport const CONTROL_PANEL_GAP_WIDTH = 2;\nconst CONTEXT_METER_CELLS = 12;\nconst SPARKLINE_WIDTH = 16;\nconst SPARKLINE_GLYPHS = \"▁▂▃▄▅▆▇█\";\n\nexport interface ControlPanelContent {\n\tappName: string;\n\tversion: string;\n\tcompactInstructions: () => string;\n\texpandedInstructions: () => string;\n\tcompactOnboarding: () => string;\n\tonboarding: () => string;\n\tstatusSnapshot?: () => ControlPanelStatusSnapshot;\n}\n\nexport interface ControlPanelStatusSnapshot {\n\treadonly modelId?: string;\n\treadonly modelProvider?: string;\n\treadonly thinkingLevel?: string;\n\treadonly contextPercent?: number | null;\n\treadonly contextWindowTokens?: number;\n\treadonly contextTokens?: number | null;\n\treadonly headroomStatus?: string;\n\treadonly optimizerPolicy?: string;\n\treadonly skillCount?: number;\n\treadonly mcpCount?: number;\n\treadonly packageIntake?: PiPackageIntakeSummary;\n\treadonly cwdLabel?: string;\n\treadonly gitBranch?: string | null;\n\treadonly todoState?: TodoState;\n\treadonly runtimeState?: string;\n\treadonly routeState?: string;\n\treadonly evidenceState?: string;\n\treadonly controlState?: string;\n\treadonly dagOrchestrationState?: string;\n\treadonly ansiColorState?: string;\n\treadonly startupState?: string;\n\treadonly linkState?: string;\n\treadonly sidebarState?: string;\n}\n\nexport function renderControlPanelLayout(\n\tcontent: ControlPanelContent,\n\texpanded: boolean,\n\twidth: number,\n\tbannerFrame?: string[],\n\tsparkleMs = 0,\n): string[] {\n\tif (width <= 0) return [];\n\treturn expanded\n\t\t? renderExpanded(content, width, bannerFrame, sparkleMs)\n\t\t: renderCompact(content, width, bannerFrame, sparkleMs);\n}\n\nexport function renderControlPanelRightPane(content: ControlPanelContent, width: number): string[] {\n\tif (width <= 0) return [];\n\treturn sidebarPanel(content, width);\n}\n\nfunction renderCompact(content: ControlPanelContent, width: number, bannerFrame?: string[], sparkleMs = 0): string[] {\n\tif (width >= CONTROL_PANEL_DECK_MIN_WIDTH) {\n\t\tconst deck = renderDeck(content, width, bannerFrame, sparkleMs);\n\t\tif (deck.length > 0) return deck;\n\t}\n\treturn [\n\t\tdivider(width, \"OMK//CONTROL PANEL\", \"accent\"),\n\t\tstatusLine(content, width),\n\t\ttextLine(width, content.compactInstructions()),\n\t\ttextLine(width, content.compactOnboarding(), \"dim\"),\n\t];\n}\n\nfunction renderDeck(content: ControlPanelContent, width: number, bannerFrame?: string[], sparkleMs = 0): string[] {\n\tconst { leftWidth, sidebarWidth } = deckWidths(width);\n\tif (leftWidth < 72) return [];\n\tconst hero = heroPanel(content, leftWidth, bannerFrame, sparkleMs);\n\tconst rail = sidebarPanel(content, sidebarWidth);\n\t// Both framed columns close on the same row so the deck reads as one block\n\t// instead of a short hero beside a long ragged rail.\n\tconst deckHeight = Math.max(hero.length, rail.length);\n\tconst lines = composeColumns(\n\t\tpadBoxColumn(hero, deckHeight, leftWidth, \"center\"),\n\t\tleftWidth,\n\t\tpadBoxColumn(rail, deckHeight, sidebarWidth),\n\t\tsidebarWidth,\n\t\tCONTROL_PANEL_GAP_WIDTH,\n\t\twidth,\n\t);\n\tlines.push(controlStripLine(content, width));\n\tlines.push(centerLine(width, content.compactOnboarding(), \"dim\"));\n\treturn lines;\n}\n\nfunction deckWidths(width: number): { leftWidth: number; sidebarWidth: number } {\n\tconst sidebarWidth = Math.min(CONTROL_PANEL_SIDEBAR_WIDTH, Math.max(34, Math.floor(width * 0.28)));\n\treturn { leftWidth: width - CONTROL_PANEL_GAP_WIDTH - sidebarWidth, sidebarWidth };\n}\n\nfunction heroPanel(content: ControlPanelContent, width: number, bannerFrame?: string[], sparkleMs = 0): string[] {\n\tconst snapshot = statusSnapshot(content);\n\tconst innerWidth = Math.max(0, width - 4);\n\treturn [\n\t\t// One wordmark only: the framed title chip carries the product name, so the\n\t\t// hero body is the block mark plus live state, not a second text logotype.\n\t\tboxTop(width, `omk v${content.version} · OMK://CONTROL`),\n\t\tboxTextLine(width, sparkleRow(innerWidth, SPARKLE_ROW_TOP, sparkleMs)),\n\t\t...(bannerFrame ?? CONTROL_PANEL_ASCII_ART).map((line, index, rows) => {\n\t\t\tif (bannerFrame) return boxCenteredLine(width, line);\n\t\t\t// Single-tone wordmark: the top and bottom scanlines fall back to the\n\t\t\t// dim ink so the mark reads as one lit object rather than a four-colour\n\t\t\t// stack. Accent stays the only hue in the hero.\n\t\t\tconst isEdge = index === 0 || index === rows.length - 1;\n\t\t\tconst isCore = index === Math.floor(rows.length / 2);\n\t\t\tconst painted = theme.fg(isEdge ? \"dim\" : \"accent\", line);\n\t\t\treturn boxCenteredLine(width, isCore ? theme.bold(painted) : painted);\n\t\t}),\n\t\tboxTextLine(width, sparkleRow(innerWidth, SPARKLE_ROW_BOTTOM, sparkleMs)),\n\t\tboxCenteredLine(\n\t\t\twidth,\n\t\t\t`${theme.fg(\"success\", \"●\")} ${theme.fg(\"muted\", \"MODEL\")} ${theme.fg(\"text\", heroModelLabel(snapshot))}`,\n\t\t),\n\t\tboxCenteredLine(\n\t\t\twidth,\n\t\t\t[\"route\", \"verify\", \"loop\", \"control\"]\n\t\t\t\t.map((stage) => theme.fg(\"dim\", stage))\n\t\t\t\t.join(theme.fg(\"borderMuted\", \" · \")),\n\t\t),\n\t\tboxTextLine(width, \"\"),\n\t\tboxRuleLine(width, Math.max(4, Math.floor(innerWidth * 0.18))),\n\t\tboxCenteredLine(width, heroMetaStrip(snapshot)),\n\t\tboxBottom(width),\n\t];\n}\n\n/**\n * Compact meta strip under the wordmark: the same signals the narrow layout\n * shows beside the banner, rendered as one centred row so the hero carries\n * real state instead of empty deck height.\n */\nfunction heroMetaStrip(snapshot: ControlPanelStatusSnapshot): string {\n\tconst rawThemeName = theme.name ?? \"live\";\n\tconst themeName = (rawThemeName.startsWith(\"omk-\") ? rawThemeName.slice(4) : rawThemeName).toUpperCase();\n\tconst cells: Array<[string, string]> = [\n\t\t[\"PANEL\", snapshot.runtimeState ?? \"unknown\"],\n\t\t[\"THEME\", themeName],\n\t\t[\"STARTUP\", snapshot.startupState ?? \"unknown\"],\n\t\t[\"LINK\", snapshot.linkState ?? \"unknown\"],\n\t];\n\treturn cells\n\t\t.map(([label, value]) => `${theme.fg(\"muted\", label)} ${coloredStatus(value)}`)\n\t\t.join(theme.fg(\"borderMuted\", \" · \"));\n}\n\nfunction heroModelLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tif (!snapshot.modelId) return \"no-model\";\n\tconst think = snapshot.thinkingLevel;\n\treturn think && think !== \"off\" ? `${snapshot.modelId}:${think}` : snapshot.modelId;\n}\n\n/**\n * Label gutters are aligned per section rather than across the whole rail: a\n * global gutter would indent short labels so far that long values (todo text,\n * model ids) lose characters at rail widths of 34-38 columns.\n */\nconst RAIL_COLUMNS = {\n\tstatus: 8,\n\ttodo: 4,\n\tsession: 3,\n\tmodel: 5,\n\truntime: 8,\n\tcontrol: 7,\n} as const;\n\nfunction sidebarPanel(content: ControlPanelContent, width: number): string[] {\n\tconst snapshot = statusSnapshot(content);\n\tconst headroomLabel = snapshot.headroomStatus ?? snapshot.optimizerPolicy ?? \"unknown\";\n\tconst mcpCount = snapshot.mcpCount;\n\tconst skillCount = snapshot.skillCount;\n\tconst packageIntakeLabel = packageIntakeStatusLabel(snapshot.packageIntake);\n\tconst contextMeter = contextMeterLabel(snapshot);\n\tconst activitySparkline = activitySparklineLabel(snapshot);\n\treturn [\n\t\tsidebarTabs(width),\n\t\tboxCenteredLine(width, theme.bold(theme.fg(\"accent\", \"OMK://CONTROL\"))),\n\t\tboxCenteredLine(width, theme.fg(\"dim\", \"CYBERPUNK OPS CORE\")),\n\t\tboxCenteredLine(\n\t\t\twidth,\n\t\t\t`${theme.fg(\"muted\", \"MATRIX RAIN\")}${theme.fg(\"borderMuted\", \" // \")}${theme.fg(\"muted\", \"NEON GRID ONLINE\")}`,\n\t\t),\n\t\tboxCenteredLine(width, theme.fg(\"dim\", \"NIGHT-CITY-MATRIX-V3\")),\n\t\tsidebarRule(width, \"STATUS\"),\n\t\tboxTextLine(\n\t\t\twidth,\n\t\t\t`${labelCell(\"state\", RAIL_COLUMNS.status)} ${coloredStatus(snapshot.runtimeState ?? \"unknown\")}`,\n\t\t),\n\t\tboxTextLine(\n\t\t\twidth,\n\t\t\t`${labelCell(\"route\", RAIL_COLUMNS.status)} ${coloredStatus(snapshot.routeState ?? \"unknown\")}`,\n\t\t),\n\t\tboxTextLine(\n\t\t\twidth,\n\t\t\t`${labelCell(\"evidence\", RAIL_COLUMNS.status)} ${coloredStatus(snapshot.evidenceState ?? \"unknown\")}`,\n\t\t),\n\t\tsidebarRule(width, \"TODO\"),\n\t\t...todoSidebarLines(snapshot.todoState, width),\n\t\tsidebarRule(width, \"SESSION\"),\n\t\tsemanticBoxTextLine(width, \"cwd\", snapshot.cwdLabel ?? \"?\", \"end\", RAIL_COLUMNS.session),\n\t\tsemanticBoxTextLine(width, \"git\", snapshot.gitBranch ?? \"?\", \"start\", RAIL_COLUMNS.session),\n\t\tsidebarRule(width, \"MODEL / CTX\"),\n\t\tsemanticBoxTextLine(width, \"model\", modelStatusLabel(snapshot), \"end\", RAIL_COLUMNS.model),\n\t\tsemanticBoxTextLine(width, \"think\", snapshot.thinkingLevel ?? \"off\", \"start\", RAIL_COLUMNS.model),\n\t\tboxTextLine(\n\t\t\twidth,\n\t\t\t`${labelCell(\"ctx\", RAIL_COLUMNS.model)} ${theme.fg(statusColor(snapshot.contextPercent === undefined || snapshot.contextPercent === null ? \"unknown\" : \"ready\"), contextStatusLabel(snapshot))}`,\n\t\t),\n\t\tboxTextLine(width, `${labelCell(\"meter\", RAIL_COLUMNS.model)} ${contextMeter}`),\n\t\tboxTextLine(width, `${labelCell(\"pulse\", RAIL_COLUMNS.model)} ${activitySparkline}`),\n\t\tsidebarRule(width, \"RUNTIME / MCP / SKILLS\"),\n\t\tsemanticBoxTextLine(width, \"headroom\", headroomLabel, \"end\", RAIL_COLUMNS.runtime),\n\t\tsemanticBoxTextLine(width, \"omk\", snapshot.dagOrchestrationState ?? \"unknown\", \"end\", RAIL_COLUMNS.runtime),\n\t\tsemanticBoxTextLine(width, \"sidebar\", snapshot.sidebarState ?? \"unknown\", \"start\", RAIL_COLUMNS.runtime),\n\t\tboxTextLine(\n\t\t\twidth,\n\t\t\t`${labelCell(\"res\", RAIL_COLUMNS.runtime)} MCP:${mcpCount ?? \"?\"} skills:${skillCount ?? \"?\"}`,\n\t\t),\n\t\tsemanticBoxTextLine(width, \"pkg\", packageIntakeLabel, \"end\", RAIL_COLUMNS.runtime),\n\t\t// CONTROL previously repeated STATUS's route row verbatim; the rail now\n\t\t// carries each signal exactly once.\n\t\tsidebarRule(width, \"CONTROL\"),\n\t\tboxTextLine(\n\t\t\twidth,\n\t\t\t`${labelCell(\"verify\", RAIL_COLUMNS.control)} ${coloredStatus(snapshot.evidenceState ?? \"unknown\")}`,\n\t\t),\n\t\tboxTextLine(\n\t\t\twidth,\n\t\t\t`${labelCell(\"control\", RAIL_COLUMNS.control)} ${coloredStatus(snapshot.controlState ?? \"unknown\")}`,\n\t\t),\n\t\tboxBottom(width),\n\t];\n}\n\nfunction semanticBoxTextLine(\n\twidth: number,\n\tlabel: string,\n\tvalue: string,\n\tpreserve: \"start\" | \"middle\" | \"end\",\n\tcolumn?: number,\n): string {\n\t// Alignment is a courtesy, not a cost: if the gutter indent would truncate the\n\t// value, this row drops back to a flush label so the data survives intact.\n\tconst aligned = `${labelCell(label, column)} `;\n\tconst flush = `${labelCell(label, label.length)} `;\n\tconst alignedRoom = Math.max(0, width - 4 - visibleWidth(aligned));\n\tconst prefix = visibleWidth(value) <= alignedRoom ? aligned : flush;\n\tconst availableWidth = Math.max(0, width - 4 - visibleWidth(prefix));\n\treturn boxTextLine(width, `${prefix}${semanticTruncate(value, availableWidth, preserve)}`);\n}\n\nfunction semanticTruncate(value: string, maxWidth: number, preserve: \"start\" | \"middle\" | \"end\"): string {\n\tif (visibleWidth(value) <= maxWidth) return value;\n\tif (maxWidth <= 0) return \"\";\n\tif (preserve === \"start\") return truncateToWidth(value, maxWidth, \"…\");\n\tconst ellipsis = \"…\";\n\tconst targetWidth = maxWidth - visibleWidth(ellipsis);\n\tif (targetWidth <= 0) return truncateToWidth(ellipsis, maxWidth, \"\");\n\tif (preserve === \"middle\") {\n\t\tconst headWidth = Math.max(1, Math.floor(targetWidth / 2));\n\t\tconst tailWidth = Math.max(1, targetWidth - headWidth);\n\t\treturn `${takeStart(value, headWidth)}${ellipsis}${takeEnd(value, tailWidth)}`;\n\t}\n\treturn `${ellipsis}${takeEnd(value, targetWidth)}`;\n}\n\nfunction takeStart(value: string, maxWidth: number): string {\n\tlet prefix = \"\";\n\tfor (const char of Array.from(value)) {\n\t\tif (visibleWidth(prefix + char) > maxWidth) break;\n\t\tprefix += char;\n\t}\n\treturn prefix;\n}\n\nfunction takeEnd(value: string, maxWidth: number): string {\n\tlet suffix = \"\";\n\tfor (const char of Array.from(value).reverse()) {\n\t\tif (visibleWidth(char + suffix) > maxWidth) break;\n\t\tsuffix = char + suffix;\n\t}\n\treturn suffix;\n}\n\nfunction statusToken(label: string, value: string | undefined): string {\n\tconst state = value ?? \"unknown\";\n\treturn theme.fg(statusColor(state), `${label}:${state.toUpperCase()}`);\n}\n\nfunction coloredStatus(value: string): string {\n\treturn theme.fg(statusColor(value), value);\n}\n\nfunction statusColor(value: string): ThemeColor {\n\tconst normalized = value.toLowerCase();\n\tif ([\"ready\", \"on\", \"active\", \"available\", \"linked\", \"tracking\", \"pinned\"].includes(normalized)) return \"success\";\n\tif ([\"degraded\", \"limited\", \"blocked\", \"off\"].includes(normalized)) return \"warning\";\n\treturn \"muted\";\n}\n\nfunction todoSidebarLines(state: TodoState | undefined, width: number): string[] {\n\tif (!state || state.items.length === 0) {\n\t\treturn [\n\t\t\tboxTextLine(width, `${labelCell(\"todo\", RAIL_COLUMNS.todo)} ${theme.fg(\"dim\", \"empty\")}`),\n\t\t\tboxTextLine(width, `${labelCell(\"next\", RAIL_COLUMNS.todo)} ${theme.fg(\"dim\", \"no active todos\")}`),\n\t\t];\n\t}\n\tconst counts = todoSummary(state);\n\tconst next = nextActiveTodo(state);\n\treturn [\n\t\tboxTextLine(width, `${labelCell(\"todo\", RAIL_COLUMNS.todo)} ${counts.done}/${counts.total} done`),\n\t\tsemanticBoxTextLine(width, \"next\", next?.label ?? \"complete\", \"middle\", RAIL_COLUMNS.todo),\n\t];\n}\n\nfunction packageIntakeStatusLabel(summary: PiPackageIntakeSummary | undefined): string {\n\tif (!summary) return \"ports:pending\";\n\tconst ready =\n\t\tsummary.acceptedNative + summary.acceptedReference + summary.acceptedMeasurement + summary.acceptedAdvisory;\n\tconst review = summary.deferred + summary.reject;\n\treturn `ports:${ready}/${summary.total} review:${review} block:${summary.hardForkBlocked}`;\n}\n\nfunction renderExpanded(content: ControlPanelContent, width: number, bannerFrame?: string[], sparkleMs = 0): string[] {\n\tif (width >= CONTROL_PANEL_DECK_MIN_WIDTH) {\n\t\tconst { leftWidth, sidebarWidth } = deckWidths(width);\n\t\tconst resourceLines = [\"\", ...content.onboarding().split(\"\\n\")];\n\t\tconst rightRail = blankSidebarRail(sidebarWidth, resourceLines.length);\n\t\tconst lines = [...renderDeck(content, width, bannerFrame, sparkleMs)];\n\t\tlines.push(...composeColumns(resourceLines, leftWidth, rightRail, sidebarWidth, CONTROL_PANEL_GAP_WIDTH, width));\n\t\treturn lines;\n\t}\n\n\tconst lines = [divider(width, \"OMK//CONTROL PANEL\", \"accent\"), statusLine(content, width)];\n\tif (width >= 32) lines.push(...brandLines(content, width, bannerFrame));\n\tlines.push(divider(width, \"SYSTEM MAP\", \"mdHeading\"));\n\tfor (const instruction of content.expandedInstructions().split(\"\\n\")) lines.push(textLine(width, instruction));\n\tlines.push(divider(width, \"STARTUP LINK\", \"success\"));\n\tfor (const onboardingLine of content.onboarding().split(\"\\n\")) lines.push(textLine(width, onboardingLine, \"dim\"));\n\tlines.push(divider(width, \"END\", \"borderMuted\"));\n\treturn lines;\n}\n\nfunction blankSidebarRail(width: number, lineCount: number): string[] {\n\treturn Array.from({ length: lineCount }, () => boxTextLine(width, \"\"));\n}\n\nfunction statusSnapshot(content: ControlPanelContent): ControlPanelStatusSnapshot {\n\treturn content.statusSnapshot?.() ?? {};\n}\n\nfunction modelStatusLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tif (!snapshot.modelId) return \"no-model\";\n\treturn snapshot.modelProvider ? `${snapshot.modelProvider}/${snapshot.modelId}` : snapshot.modelId;\n}\n\nfunction contextStatusLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tconst windowTokens = snapshot.contextWindowTokens ?? 0;\n\tconst windowLabel = windowTokens > 0 ? formatTokens(windowTokens) : \"?\";\n\treturn snapshot.contextPercent === null || snapshot.contextPercent === undefined\n\t\t? `?/${windowLabel}`\n\t\t: `${snapshot.contextPercent.toFixed(1)}%/${windowLabel}`;\n}\n\nfunction contextMeterLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tconst percent = normalizePercent(snapshot.contextPercent);\n\tif (percent === undefined) {\n\t\treturn `${theme.fg(\"borderMuted\", \"░\".repeat(CONTEXT_METER_CELLS))} ${theme.fg(\"muted\", \"??%\")}`;\n\t}\n\tconst filled = Math.max(0, Math.min(CONTEXT_METER_CELLS, Math.round((percent / 100) * CONTEXT_METER_CELLS)));\n\tconst color: ThemeColor = percent >= 85 ? \"warning\" : percent >= 65 ? \"mdCode\" : \"success\";\n\treturn `${theme.fg(color, \"█\".repeat(filled))}${theme.fg(\"borderMuted\", \"░\".repeat(CONTEXT_METER_CELLS - filled))} ${theme.fg(color, `${Math.round(percent)}%`)}`;\n}\n\nfunction activitySparklineLabel(snapshot: ControlPanelStatusSnapshot): string {\n\tconst seed = hashSnapshot(snapshot);\n\tconst glyphs = Array.from(SPARKLINE_GLYPHS);\n\tconst parts: string[] = [];\n\tfor (let index = 0; index < SPARKLINE_WIDTH; index++) {\n\t\tconst level = (seed + index * 5 + index * index) % glyphs.length;\n\t\tconst glyph = glyphs[level] ?? \"▁\";\n\t\t// Colour follows amplitude instead of column index, so the pulse reads as\n\t\t// one signal in the accent hue rather than a rotating three-colour pattern.\n\t\tconst color: ThemeColor = level >= glyphs.length - 2 ? \"accent\" : level >= 3 ? \"muted\" : \"dim\";\n\t\tparts.push(theme.fg(color, glyph));\n\t}\n\treturn parts.join(\"\");\n}\n\nfunction normalizePercent(value: number | null | undefined): number | undefined {\n\tif (value === null || value === undefined || !Number.isFinite(value)) return undefined;\n\tif (value < 0) return 0;\n\tif (value > 100) return 100;\n\treturn value;\n}\n\nfunction hashSnapshot(snapshot: ControlPanelStatusSnapshot): number {\n\tconst source = [\n\t\tsnapshot.modelProvider,\n\t\tsnapshot.modelId,\n\t\tsnapshot.contextPercent?.toFixed(1),\n\t\tsnapshot.contextTokens,\n\t\tsnapshot.contextWindowTokens,\n\t\tsnapshot.cwdLabel,\n\t\tsnapshot.gitBranch,\n\t].join(\"|\");\n\tlet hash = 2166136261;\n\tfor (let index = 0; index < source.length; index++) {\n\t\thash ^= source.charCodeAt(index);\n\t\thash = Math.imul(hash, 16777619);\n\t}\n\treturn hash >>> 0;\n}\n\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\nfunction sidebarTabs(width: number): string {\n\treturn boxTextLine(\n\t\twidth,\n\t\tfitLine(\n\t\t\t`${theme.bold(theme.fg(\"accent\", \"1:CONTROL\"))} ${theme.fg(\"muted\", \"2:HISTORY\")}`,\n\t\t\tMath.max(0, width - 4),\n\t\t),\n\t);\n}\n\nfunction controlStripLine(content: ControlPanelContent, width: number): string {\n\treturn centerLine(\n\t\twidth,\n\t\t`${theme.bold(theme.fg(\"accent\", \"OMK://CONTROL READ\"))} ${theme.fg(\"accent\", \"route/verify/loop/control\")} · ${content.compactInstructions()}`,\n\t);\n}\n\nfunction brandLines(content: ControlPanelContent, width: number, bannerFrame?: string[]): string[] {\n\tconst art = bannerFrame ?? CONTROL_PANEL_ASCII_ART;\n\tif (bannerFrame) return art.map((line) => textLine(width, line));\n\tconst leftWidth = Math.max(...art.map((line) => visibleWidth(line)));\n\tconst minWideWidth = visibleWidth(\"| \") + leftWidth + visibleWidth(\" | \") + META_WIDTH;\n\tif (width < minWideWidth) return art.map((line) => textLine(width, theme.fg(\"accent\", line)));\n\tconst snapshot = statusSnapshot(content);\n\treturn art.map((line, index) =>\n\t\ttextLine(\n\t\t\twidth,\n\t\t\t`${fitLine(theme.fg(\"accent\", line), leftWidth)}${theme.fg(\"borderMuted\", \" | \")}${metadataLines(snapshot)[index] ?? \"\"}`,\n\t\t),\n\t);\n}\n\nfunction metadataLines(snapshot: ControlPanelStatusSnapshot): string[] {\n\tconst rawThemeName = theme.name ?? \"live\";\n\tconst themeName = (rawThemeName.startsWith(\"omk-\") ? rawThemeName.slice(4) : rawThemeName).toUpperCase();\n\treturn [\n\t\t`${theme.fg(\"mdCode\", \"PANEL\")} ${coloredStatus(snapshot.runtimeState ?? \"unknown\")}`,\n\t\t`${theme.fg(\"mdCode\", \"THEME\")} ${theme.fg(\"accent\", themeName)}`,\n\t\t`${theme.fg(\"mdCode\", \"STARTUP\")} ${coloredStatus(snapshot.startupState ?? \"unknown\")}`,\n\t\t`${theme.fg(\"mdCode\", \"LINK\")} ${coloredStatus(snapshot.linkState ?? \"unknown\")}`,\n\t];\n}\n\nfunction statusLine(content: ControlPanelContent, width: number): string {\n\tconst snapshot = statusSnapshot(content);\n\tconst segments = [\n\t\ttheme.bold(theme.fg(\"accent\", `${content.appName.toUpperCase()} v${content.version}`)),\n\t\tstatusToken(\"CORE\", snapshot.runtimeState),\n\t\tstatusToken(\"ANSI\", snapshot.ansiColorState),\n\t\tstatusToken(\"STARTUP\", snapshot.startupState),\n\t\tstatusToken(\"LINK\", snapshot.linkState),\n\t\ttheme.fg(\"muted\", \"THEME:LIVE\"),\n\t];\n\treturn textLine(width, segments.join(theme.fg(\"borderMuted\", \" | \")));\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { truncateToWidth, visibleWidth } from "omk-tui";
2
2
  import { nextActiveTodo, summary as todoSummary } from "../../../core/todo-state.js";
3
3
  import { theme } from "../theme/theme.js";
4
- import { boxBottom, boxCenteredLine, boxTextLine, boxTop, centerLine, composeColumns, divider, fitLine, sidebarRule, textLine, } from "./control-panel-box.js";
4
+ import { boxBottom, boxCenteredLine, boxRuleLine, boxTextLine, boxTop, centerLine, composeColumns, divider, fitLine, labelCell, padBoxColumn, sidebarRule, textLine, } from "./control-panel-box.js";
5
5
  import { SPARKLE_ROW_BOTTOM, SPARKLE_ROW_TOP, sparkleRow } from "./control-panel-sparkles.js";
6
6
  export const CONTROL_PANEL_ASCII_ART = [
7
7
  " ███████ ██ ██ ██ ██",
@@ -49,7 +49,12 @@ function renderDeck(content, width, bannerFrame, sparkleMs = 0) {
49
49
  const { leftWidth, sidebarWidth } = deckWidths(width);
50
50
  if (leftWidth < 72)
51
51
  return [];
52
- const lines = composeColumns(heroPanel(content, leftWidth, bannerFrame, sparkleMs), leftWidth, sidebarPanel(content, sidebarWidth), sidebarWidth, CONTROL_PANEL_GAP_WIDTH, width);
52
+ const hero = heroPanel(content, leftWidth, bannerFrame, sparkleMs);
53
+ const rail = sidebarPanel(content, sidebarWidth);
54
+ // Both framed columns close on the same row so the deck reads as one block
55
+ // instead of a short hero beside a long ragged rail.
56
+ const deckHeight = Math.max(hero.length, rail.length);
57
+ const lines = composeColumns(padBoxColumn(hero, deckHeight, leftWidth, "center"), leftWidth, padBoxColumn(rail, deckHeight, sidebarWidth), sidebarWidth, CONTROL_PANEL_GAP_WIDTH, width);
53
58
  lines.push(controlStripLine(content, width));
54
59
  lines.push(centerLine(width, content.compactOnboarding(), "dim"));
55
60
  return lines;
@@ -62,28 +67,69 @@ function heroPanel(content, width, bannerFrame, sparkleMs = 0) {
62
67
  const snapshot = statusSnapshot(content);
63
68
  const innerWidth = Math.max(0, width - 4);
64
69
  return [
70
+ // One wordmark only: the framed title chip carries the product name, so the
71
+ // hero body is the block mark plus live state, not a second text logotype.
65
72
  boxTop(width, `omk v${content.version} · OMK://CONTROL`),
66
- boxCenteredLine(width, theme.bold(theme.fg("accent", "OMK"))),
67
- boxCenteredLine(width, theme.fg("muted", "route · verify · loop · control")),
68
73
  boxTextLine(width, sparkleRow(innerWidth, SPARKLE_ROW_TOP, sparkleMs)),
69
- ...(bannerFrame ?? CONTROL_PANEL_ASCII_ART).map((line, index) => {
74
+ ...(bannerFrame ?? CONTROL_PANEL_ASCII_ART).map((line, index, rows) => {
70
75
  if (bannerFrame)
71
76
  return boxCenteredLine(width, line);
72
- const color = index < 2 ? "mdCode" : index < 4 ? "accent" : index < 6 ? "success" : "warning";
73
- return boxCenteredLine(width, theme.fg(color, line));
77
+ // Single-tone wordmark: the top and bottom scanlines fall back to the
78
+ // dim ink so the mark reads as one lit object rather than a four-colour
79
+ // stack. Accent stays the only hue in the hero.
80
+ const isEdge = index === 0 || index === rows.length - 1;
81
+ const isCore = index === Math.floor(rows.length / 2);
82
+ const painted = theme.fg(isEdge ? "dim" : "accent", line);
83
+ return boxCenteredLine(width, isCore ? theme.bold(painted) : painted);
74
84
  }),
75
85
  boxTextLine(width, sparkleRow(innerWidth, SPARKLE_ROW_BOTTOM, sparkleMs)),
76
- boxCenteredLine(width, `${theme.fg("warning", "●")} ${theme.bold("OMK")} / ${theme.fg("success", heroModelLabel(snapshot))}`),
77
- boxCenteredLine(width, `${theme.fg("mdCode", "◇")} omk-control · ${theme.fg("accent", "route")} · ${theme.fg("warning", "verify")} · ${theme.fg("success", "loop")} · ${theme.fg("mdCode", "control")}`),
86
+ boxCenteredLine(width, `${theme.fg("success", "●")} ${theme.fg("muted", "MODEL")} ${theme.fg("text", heroModelLabel(snapshot))}`),
87
+ boxCenteredLine(width, ["route", "verify", "loop", "control"]
88
+ .map((stage) => theme.fg("dim", stage))
89
+ .join(theme.fg("borderMuted", " · "))),
90
+ boxTextLine(width, ""),
91
+ boxRuleLine(width, Math.max(4, Math.floor(innerWidth * 0.18))),
92
+ boxCenteredLine(width, heroMetaStrip(snapshot)),
78
93
  boxBottom(width),
79
94
  ];
80
95
  }
96
+ /**
97
+ * Compact meta strip under the wordmark: the same signals the narrow layout
98
+ * shows beside the banner, rendered as one centred row so the hero carries
99
+ * real state instead of empty deck height.
100
+ */
101
+ function heroMetaStrip(snapshot) {
102
+ const rawThemeName = theme.name ?? "live";
103
+ const themeName = (rawThemeName.startsWith("omk-") ? rawThemeName.slice(4) : rawThemeName).toUpperCase();
104
+ const cells = [
105
+ ["PANEL", snapshot.runtimeState ?? "unknown"],
106
+ ["THEME", themeName],
107
+ ["STARTUP", snapshot.startupState ?? "unknown"],
108
+ ["LINK", snapshot.linkState ?? "unknown"],
109
+ ];
110
+ return cells
111
+ .map(([label, value]) => `${theme.fg("muted", label)} ${coloredStatus(value)}`)
112
+ .join(theme.fg("borderMuted", " · "));
113
+ }
81
114
  function heroModelLabel(snapshot) {
82
115
  if (!snapshot.modelId)
83
116
  return "no-model";
84
117
  const think = snapshot.thinkingLevel;
85
118
  return think && think !== "off" ? `${snapshot.modelId}:${think}` : snapshot.modelId;
86
119
  }
120
+ /**
121
+ * Label gutters are aligned per section rather than across the whole rail: a
122
+ * global gutter would indent short labels so far that long values (todo text,
123
+ * model ids) lose characters at rail widths of 34-38 columns.
124
+ */
125
+ const RAIL_COLUMNS = {
126
+ status: 8,
127
+ todo: 4,
128
+ session: 3,
129
+ model: 5,
130
+ runtime: 8,
131
+ control: 7,
132
+ };
87
133
  function sidebarPanel(content, width) {
88
134
  const snapshot = statusSnapshot(content);
89
135
  const headroomLabel = snapshot.headroomStatus ?? snapshot.optimizerPolicy ?? "unknown";
@@ -95,39 +141,45 @@ function sidebarPanel(content, width) {
95
141
  return [
96
142
  sidebarTabs(width),
97
143
  boxCenteredLine(width, theme.bold(theme.fg("accent", "OMK://CONTROL"))),
98
- boxCenteredLine(width, theme.bold(theme.fg("warning", "CYBERPUNK OPS CORE"))),
99
- boxCenteredLine(width, `${theme.fg("mdCode", "MATRIX RAIN")} // ${theme.fg("success", "NEON GRID ONLINE")}`),
100
- boxCenteredLine(width, theme.fg("text", "NIGHT-CITY-MATRIX-V3")),
101
- sidebarRule(width, "STATUS", "accent"),
102
- boxTextLine(width, `${theme.fg("muted", "state:")} ${coloredStatus(snapshot.runtimeState ?? "unknown")}`),
103
- boxTextLine(width, `${theme.fg("muted", "route:")} ${coloredStatus(snapshot.routeState ?? "unknown")}`),
104
- boxTextLine(width, `${theme.fg("muted", "evidence:")} ${coloredStatus(snapshot.evidenceState ?? "unknown")}`),
105
- sidebarRule(width, "TODO", "accent"),
144
+ boxCenteredLine(width, theme.fg("dim", "CYBERPUNK OPS CORE")),
145
+ boxCenteredLine(width, `${theme.fg("muted", "MATRIX RAIN")}${theme.fg("borderMuted", " // ")}${theme.fg("muted", "NEON GRID ONLINE")}`),
146
+ boxCenteredLine(width, theme.fg("dim", "NIGHT-CITY-MATRIX-V3")),
147
+ sidebarRule(width, "STATUS"),
148
+ boxTextLine(width, `${labelCell("state", RAIL_COLUMNS.status)} ${coloredStatus(snapshot.runtimeState ?? "unknown")}`),
149
+ boxTextLine(width, `${labelCell("route", RAIL_COLUMNS.status)} ${coloredStatus(snapshot.routeState ?? "unknown")}`),
150
+ boxTextLine(width, `${labelCell("evidence", RAIL_COLUMNS.status)} ${coloredStatus(snapshot.evidenceState ?? "unknown")}`),
151
+ sidebarRule(width, "TODO"),
106
152
  ...todoSidebarLines(snapshot.todoState, width),
107
- sidebarRule(width, "SESSION", "accent"),
108
- semanticBoxTextLine(width, "cwd", snapshot.cwdLabel ?? "?", "end"),
109
- semanticBoxTextLine(width, "git", snapshot.gitBranch ?? "?", "start"),
110
- sidebarRule(width, "MODEL / CTX", "accent"),
111
- semanticBoxTextLine(width, "model", modelStatusLabel(snapshot), "end"),
112
- semanticBoxTextLine(width, "think", snapshot.thinkingLevel ?? "off", "start"),
113
- boxTextLine(width, `${theme.fg("muted", "ctx:")} ${theme.fg(statusColor(snapshot.contextPercent === undefined || snapshot.contextPercent === null ? "unknown" : "ready"), contextStatusLabel(snapshot))}`),
114
- boxTextLine(width, `${theme.fg("muted", "meter:")} ${contextMeter}`),
115
- boxTextLine(width, `${theme.fg("muted", "pulse:")} ${activitySparkline}`),
116
- sidebarRule(width, "RUNTIME / MCP / SKILLS", "accent"),
117
- semanticBoxTextLine(width, "headroom", headroomLabel, "end"),
118
- semanticBoxTextLine(width, "omk", snapshot.dagOrchestrationState ?? "unknown", "end"),
119
- semanticBoxTextLine(width, "sidebar", snapshot.sidebarState ?? "unknown", "start"),
120
- boxTextLine(width, `${theme.fg("muted", "res:")} MCP:${mcpCount ?? "?"} skills:${skillCount ?? "?"}`),
121
- semanticBoxTextLine(width, "pkg", packageIntakeLabel, "end"),
122
- sidebarRule(width, "CONTROL", "accent"),
123
- boxTextLine(width, `${theme.fg("muted", "route:")} ${coloredStatus(snapshot.routeState ?? "unknown")}`),
124
- boxTextLine(width, `${theme.fg("muted", "verify:")} ${coloredStatus(snapshot.evidenceState ?? "unknown")}`),
125
- boxTextLine(width, `${theme.fg("muted", "control:")} ${coloredStatus(snapshot.controlState ?? "unknown")}`),
126
- boxBottom(width, "accent"),
153
+ sidebarRule(width, "SESSION"),
154
+ semanticBoxTextLine(width, "cwd", snapshot.cwdLabel ?? "?", "end", RAIL_COLUMNS.session),
155
+ semanticBoxTextLine(width, "git", snapshot.gitBranch ?? "?", "start", RAIL_COLUMNS.session),
156
+ sidebarRule(width, "MODEL / CTX"),
157
+ semanticBoxTextLine(width, "model", modelStatusLabel(snapshot), "end", RAIL_COLUMNS.model),
158
+ semanticBoxTextLine(width, "think", snapshot.thinkingLevel ?? "off", "start", RAIL_COLUMNS.model),
159
+ boxTextLine(width, `${labelCell("ctx", RAIL_COLUMNS.model)} ${theme.fg(statusColor(snapshot.contextPercent === undefined || snapshot.contextPercent === null ? "unknown" : "ready"), contextStatusLabel(snapshot))}`),
160
+ boxTextLine(width, `${labelCell("meter", RAIL_COLUMNS.model)} ${contextMeter}`),
161
+ boxTextLine(width, `${labelCell("pulse", RAIL_COLUMNS.model)} ${activitySparkline}`),
162
+ sidebarRule(width, "RUNTIME / MCP / SKILLS"),
163
+ semanticBoxTextLine(width, "headroom", headroomLabel, "end", RAIL_COLUMNS.runtime),
164
+ semanticBoxTextLine(width, "omk", snapshot.dagOrchestrationState ?? "unknown", "end", RAIL_COLUMNS.runtime),
165
+ semanticBoxTextLine(width, "sidebar", snapshot.sidebarState ?? "unknown", "start", RAIL_COLUMNS.runtime),
166
+ boxTextLine(width, `${labelCell("res", RAIL_COLUMNS.runtime)} MCP:${mcpCount ?? "?"} skills:${skillCount ?? "?"}`),
167
+ semanticBoxTextLine(width, "pkg", packageIntakeLabel, "end", RAIL_COLUMNS.runtime),
168
+ // CONTROL previously repeated STATUS's route row verbatim; the rail now
169
+ // carries each signal exactly once.
170
+ sidebarRule(width, "CONTROL"),
171
+ boxTextLine(width, `${labelCell("verify", RAIL_COLUMNS.control)} ${coloredStatus(snapshot.evidenceState ?? "unknown")}`),
172
+ boxTextLine(width, `${labelCell("control", RAIL_COLUMNS.control)} ${coloredStatus(snapshot.controlState ?? "unknown")}`),
173
+ boxBottom(width),
127
174
  ];
128
175
  }
129
- function semanticBoxTextLine(width, label, value, preserve) {
130
- const prefix = `${theme.fg("muted", `${label}:`)} `;
176
+ function semanticBoxTextLine(width, label, value, preserve, column) {
177
+ // Alignment is a courtesy, not a cost: if the gutter indent would truncate the
178
+ // value, this row drops back to a flush label so the data survives intact.
179
+ const aligned = `${labelCell(label, column)} `;
180
+ const flush = `${labelCell(label, label.length)} `;
181
+ const alignedRoom = Math.max(0, width - 4 - visibleWidth(aligned));
182
+ const prefix = visibleWidth(value) <= alignedRoom ? aligned : flush;
131
183
  const availableWidth = Math.max(0, width - 4 - visibleWidth(prefix));
132
184
  return boxTextLine(width, `${prefix}${semanticTruncate(value, availableWidth, preserve)}`);
133
185
  }
@@ -185,15 +237,15 @@ function statusColor(value) {
185
237
  function todoSidebarLines(state, width) {
186
238
  if (!state || state.items.length === 0) {
187
239
  return [
188
- boxTextLine(width, `${theme.fg("muted", "todo:")} empty`),
189
- boxTextLine(width, `${theme.fg("muted", "next:")} no active todos`),
240
+ boxTextLine(width, `${labelCell("todo", RAIL_COLUMNS.todo)} ${theme.fg("dim", "empty")}`),
241
+ boxTextLine(width, `${labelCell("next", RAIL_COLUMNS.todo)} ${theme.fg("dim", "no active todos")}`),
190
242
  ];
191
243
  }
192
244
  const counts = todoSummary(state);
193
245
  const next = nextActiveTodo(state);
194
246
  return [
195
- boxTextLine(width, `${theme.fg("muted", "todo:")} ${counts.done}/${counts.total} done`),
196
- semanticBoxTextLine(width, "next", next?.label ?? "complete", "middle"),
247
+ boxTextLine(width, `${labelCell("todo", RAIL_COLUMNS.todo)} ${counts.done}/${counts.total} done`),
248
+ semanticBoxTextLine(width, "next", next?.label ?? "complete", "middle", RAIL_COLUMNS.todo),
197
249
  ];
198
250
  }
199
251
  function packageIntakeStatusLabel(summary) {
@@ -256,8 +308,11 @@ function activitySparklineLabel(snapshot) {
256
308
  const glyphs = Array.from(SPARKLINE_GLYPHS);
257
309
  const parts = [];
258
310
  for (let index = 0; index < SPARKLINE_WIDTH; index++) {
259
- const glyph = glyphs[(seed + index * 5 + index * index) % glyphs.length] ?? "▁";
260
- const color = index % 5 === 0 ? "accent" : index % 3 === 0 ? "warning" : "success";
311
+ const level = (seed + index * 5 + index * index) % glyphs.length;
312
+ const glyph = glyphs[level] ?? "";
313
+ // Colour follows amplitude instead of column index, so the pulse reads as
314
+ // one signal in the accent hue rather than a rotating three-colour pattern.
315
+ const color = level >= glyphs.length - 2 ? "accent" : level >= 3 ? "muted" : "dim";
261
316
  parts.push(theme.fg(color, glyph));
262
317
  }
263
318
  return parts.join("");