@backburner/cli 0.1.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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +329 -0
  3. package/dist/src/agents/antigravity-launch.js +162 -0
  4. package/dist/src/agents/antigravity.js +251 -0
  5. package/dist/src/agents/claude-launch.js +117 -0
  6. package/dist/src/agents/claude.js +271 -0
  7. package/dist/src/agents/codex-launch.js +82 -0
  8. package/dist/src/agents/codex.js +261 -0
  9. package/dist/src/agents/composite-runner.js +13 -0
  10. package/dist/src/agents/extraction.js +442 -0
  11. package/dist/src/agents/failure-feedback.js +132 -0
  12. package/dist/src/agents/gemini-launch.js +96 -0
  13. package/dist/src/agents/gemini.js +392 -0
  14. package/dist/src/agents/job-agent-assignments.js +180 -0
  15. package/dist/src/agents/lifecycle-feedback.js +147 -0
  16. package/dist/src/agents/limit-detector.js +83 -0
  17. package/dist/src/agents/opencode-launch.js +138 -0
  18. package/dist/src/agents/opencode.js +156 -0
  19. package/dist/src/agents/plan-breakdown-feedback.js +97 -0
  20. package/dist/src/agents/pool.js +168 -0
  21. package/dist/src/agents/prompt.js +462 -0
  22. package/dist/src/agents/provider-sessions.js +105 -0
  23. package/dist/src/agents/review-feedback.js +255 -0
  24. package/dist/src/agents/selection.js +113 -0
  25. package/dist/src/agents/service.js +1087 -0
  26. package/dist/src/agents/types.js +1 -0
  27. package/dist/src/agents/wave-assessment-feedback.js +69 -0
  28. package/dist/src/attention/derive.js +337 -0
  29. package/dist/src/capabilities/index.js +2 -0
  30. package/dist/src/capabilities/projection.js +183 -0
  31. package/dist/src/capabilities/types.js +1 -0
  32. package/dist/src/capabilities/validator.js +87 -0
  33. package/dist/src/cli/commands/broker-smoke.js +96 -0
  34. package/dist/src/cli/commands/broker.js +56 -0
  35. package/dist/src/cli/commands/init.js +4 -0
  36. package/dist/src/cli/commands/journal.js +43 -0
  37. package/dist/src/cli/commands/run-loop.js +56 -0
  38. package/dist/src/cli/commands/run.js +1241 -0
  39. package/dist/src/cli/commands/tui.js +187 -0
  40. package/dist/src/cli/dispatcher.js +51 -0
  41. package/dist/src/cli/errors.js +2 -0
  42. package/dist/src/cli/init.js +92 -0
  43. package/dist/src/cli/options.js +86 -0
  44. package/dist/src/cli/output.js +231 -0
  45. package/dist/src/cli/run-journal.js +342 -0
  46. package/dist/src/cli/run-types.js +1 -0
  47. package/dist/src/cli/run.js +32 -0
  48. package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
  49. package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
  50. package/dist/src/cli/runtime/provider-runtime.js +152 -0
  51. package/dist/src/cli/runtime/provider-tools.js +73 -0
  52. package/dist/src/cli/runtime/providers/antigravity.js +26 -0
  53. package/dist/src/cli/runtime/providers/claude.js +20 -0
  54. package/dist/src/cli/runtime/providers/codex.js +20 -0
  55. package/dist/src/cli/runtime/providers/gemini.js +20 -0
  56. package/dist/src/cli/runtime/providers/opencode.js +20 -0
  57. package/dist/src/cli/runtime/run-context.js +131 -0
  58. package/dist/src/cli/tui/index.js +3 -0
  59. package/dist/src/cli/tui/screen.js +388 -0
  60. package/dist/src/cli/tui/tracker.js +558 -0
  61. package/dist/src/cli/tui/types.js +1 -0
  62. package/dist/src/cli/tui/views.js +1006 -0
  63. package/dist/src/config/loader.js +136 -0
  64. package/dist/src/config/schemas.js +403 -0
  65. package/dist/src/config/types.js +1 -0
  66. package/dist/src/context/types.js +331 -0
  67. package/dist/src/domain/entities.js +1 -0
  68. package/dist/src/git/backburner-git-tool-service.js +680 -0
  69. package/dist/src/git/gitops-lifecycle.js +56 -0
  70. package/dist/src/git/merge-prep-workspace.js +357 -0
  71. package/dist/src/git/types.js +484 -0
  72. package/dist/src/github/broker-model-content.js +215 -0
  73. package/dist/src/github/broker-server.js +237 -0
  74. package/dist/src/github/broker.js +638 -0
  75. package/dist/src/github/composed.js +53 -0
  76. package/dist/src/github/gateway.js +478 -0
  77. package/dist/src/github/normalize.js +222 -0
  78. package/dist/src/github/pr-classification.js +73 -0
  79. package/dist/src/github/scoped-broker.js +35 -0
  80. package/dist/src/github/security.js +126 -0
  81. package/dist/src/github/service.js +332 -0
  82. package/dist/src/github/types.js +1 -0
  83. package/dist/src/github/utils.js +16 -0
  84. package/dist/src/journal/index.js +4 -0
  85. package/dist/src/journal/reader.js +62 -0
  86. package/dist/src/journal/renderer.js +50 -0
  87. package/dist/src/journal/service.js +72 -0
  88. package/dist/src/journal/types.js +1 -0
  89. package/dist/src/journal/writer.js +19 -0
  90. package/dist/src/mcps/bridge.js +166 -0
  91. package/dist/src/mcps/index.js +5 -0
  92. package/dist/src/mcps/reconcile.js +113 -0
  93. package/dist/src/mcps/smithery-client.js +186 -0
  94. package/dist/src/mcps/smithery.js +164 -0
  95. package/dist/src/mcps/types.js +1 -0
  96. package/dist/src/memory/json-store.js +71 -0
  97. package/dist/src/memory/prompt.js +16 -0
  98. package/dist/src/memory/scope.js +31 -0
  99. package/dist/src/memory/store.js +1 -0
  100. package/dist/src/memory/tool-service.js +142 -0
  101. package/dist/src/memory/types.js +12 -0
  102. package/dist/src/onboarding/config-writer.js +96 -0
  103. package/dist/src/onboarding/engine.js +86 -0
  104. package/dist/src/onboarding/prompt.js +102 -0
  105. package/dist/src/onboarding/services/provider-discovery.js +58 -0
  106. package/dist/src/onboarding/services/repo-discovery.js +246 -0
  107. package/dist/src/onboarding/steps/check-github-auth.js +32 -0
  108. package/dist/src/onboarding/steps/check-requirements.js +34 -0
  109. package/dist/src/onboarding/steps/configure-models.js +105 -0
  110. package/dist/src/onboarding/steps/configure-paths.js +33 -0
  111. package/dist/src/onboarding/steps/discover-providers.js +38 -0
  112. package/dist/src/onboarding/steps/discover-repos.js +39 -0
  113. package/dist/src/onboarding/steps/generate-config.js +106 -0
  114. package/dist/src/onboarding/steps/readiness-validation.js +158 -0
  115. package/dist/src/onboarding/steps/select-repos.js +76 -0
  116. package/dist/src/onboarding/steps/show-summary.js +97 -0
  117. package/dist/src/onboarding/steps/welcome.js +27 -0
  118. package/dist/src/onboarding/types.js +1 -0
  119. package/dist/src/onboarding/validators/git-access.js +63 -0
  120. package/dist/src/onboarding/validators/github-auth.js +85 -0
  121. package/dist/src/onboarding/validators/tool-checker.js +153 -0
  122. package/dist/src/onboarding/validators/workspace.js +83 -0
  123. package/dist/src/prompts/resolver.js +106 -0
  124. package/dist/src/retrospectives/candidates.js +224 -0
  125. package/dist/src/retrospectives/derive.js +169 -0
  126. package/dist/src/retrospectives/ingest.js +321 -0
  127. package/dist/src/retrospectives/management-prs.js +380 -0
  128. package/dist/src/retrospectives/proposals.js +199 -0
  129. package/dist/src/retrospectives/scopes.js +272 -0
  130. package/dist/src/retrospectives/scoring.js +171 -0
  131. package/dist/src/retrospectives/types.js +1 -0
  132. package/dist/src/state/loader.js +473 -0
  133. package/dist/src/state/types.js +15 -0
  134. package/dist/src/tasks/derivation/builders.js +70 -0
  135. package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
  136. package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
  137. package/dist/src/tasks/derivation/handlers/index.js +54 -0
  138. package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
  139. package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
  140. package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
  141. package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
  142. package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
  143. package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
  144. package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
  145. package/dist/src/tasks/derivation/handlers/shared.js +24 -0
  146. package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
  147. package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
  148. package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
  149. package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
  150. package/dist/src/tasks/derivation/runner.js +18 -0
  151. package/dist/src/tasks/derivation/types.js +1 -0
  152. package/dist/src/tasks/derive.js +810 -0
  153. package/dist/src/tasks/mcp-skip.js +10 -0
  154. package/dist/src/tasks/retry.js +10 -0
  155. package/dist/src/tasks/task.js +184 -0
  156. package/dist/src/tasks/types.js +1 -0
  157. package/dist/src/utils/command-runner.js +192 -0
  158. package/dist/src/utils/json.js +38 -0
  159. package/dist/src/utils/logger.js +43 -0
  160. package/dist/src/utils/paths.js +38 -0
  161. package/dist/src/utils/slug.js +10 -0
  162. package/dist/src/workflows/registry.js +174 -0
  163. package/dist/src/workflows/triage.js +90 -0
  164. package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
  165. package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
  166. package/dist/src/workflows/workstream-machine/events.js +1 -0
  167. package/dist/src/workflows/workstream-machine/machine.js +355 -0
  168. package/dist/src/workflows/workstream-machine/selectors.js +19 -0
  169. package/dist/src/workflows/workstream-machine/types.js +1 -0
  170. package/dist/src/workstreams/branch-sync.js +87 -0
  171. package/dist/src/workstreams/derive.js +629 -0
  172. package/dist/src/workstreams/mcp-blockers.js +420 -0
  173. package/dist/src/workstreams/packet-candidates.js +42 -0
  174. package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
  175. package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
  176. package/dist/src/workstreams/packet-plan.js +597 -0
  177. package/dist/src/workstreams/packet-projections.js +190 -0
  178. package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
  179. package/dist/src/workstreams/parent-branch-sync.js +148 -0
  180. package/dist/src/workstreams/phases.js +4 -0
  181. package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
  182. package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
  183. package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
  184. package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
  185. package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
  186. package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
  187. package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
  188. package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
  189. package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
  190. package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
  191. package/dist/src/workstreams/utils.js +15 -0
  192. package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
  193. package/dist/src/workstreams/wave-assessment/index.js +3 -0
  194. package/dist/src/workstreams/wave-assessment/session.js +84 -0
  195. package/dist/src/workstreams/wave-assessment/store.js +51 -0
  196. package/dist/src/workstreams/wave-assessment/tools.js +419 -0
  197. package/dist/src/workstreams/wave-assessment/types.js +1 -0
  198. package/dist/src/workstreams/wave-assessment/validate.js +78 -0
  199. package/dist/src/worktrees/service.js +839 -0
  200. package/dist/src/worktrees/workspace-ownership.js +34 -0
  201. package/dist/src/worktrees/workspace-recovery.js +177 -0
  202. package/package.json +61 -0
@@ -0,0 +1,388 @@
1
+ import * as readline from "node:readline";
2
+ import { StatusView, McpsView, TasksView, stripAnsi, padOrTruncateAnsi, bold, dim, cyan, blue } from "./views.js";
3
+ const SPIN_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
+ export class TuiScreen {
5
+ tracker;
6
+ onTriggerRefresh;
7
+ onQuit;
8
+ views;
9
+ viewScrollOffsets = {
10
+ status: 0,
11
+ mcps: 0,
12
+ tasks: 0
13
+ };
14
+ viewMode = "status";
15
+ inputMode = false;
16
+ inputBuffer = "";
17
+ isRefreshing = false;
18
+ helpOpen = false;
19
+ spinIndex = 0;
20
+ lastRenderOutput = "";
21
+ tickInterval = null;
22
+ spinInterval = null;
23
+ keypressHandler = null;
24
+ resizeHandler = null;
25
+ sigintHandler = null;
26
+ sigtermHandler = null;
27
+ exited = false;
28
+ lastTimeLength = 0;
29
+ constructor(tracker, onTriggerRefresh, onQuit) {
30
+ this.tracker = tracker;
31
+ this.onTriggerRefresh = onTriggerRefresh;
32
+ this.onQuit = onQuit;
33
+ this.views = {
34
+ status: new StatusView(tracker),
35
+ mcps: new McpsView(),
36
+ tasks: new TasksView()
37
+ };
38
+ }
39
+ setRefreshing(refreshing) {
40
+ if (this.isRefreshing === refreshing)
41
+ return;
42
+ this.isRefreshing = refreshing;
43
+ this.render();
44
+ }
45
+ start() {
46
+ // Switch to alternate screen buffer and hide cursor
47
+ process.stdout.write("\x1b[?1049h\x1b[?25l");
48
+ // Clear screen initially
49
+ readline.cursorTo(process.stdout, 0, 0);
50
+ readline.clearScreenDown(process.stdout);
51
+ // Track uptime and countdown ticks
52
+ this.tickInterval = setInterval(() => {
53
+ this.tracker.updateUptime();
54
+ this.render();
55
+ }, 1000);
56
+ // Spinner animation tick (110ms)
57
+ this.spinInterval = setInterval(() => {
58
+ this.spinIndex = (this.spinIndex + 1) % SPIN_CHARS.length;
59
+ if (this.tracker.getModel().status === "syncing" || this.tracker.getModel().status === "executing") {
60
+ this.render();
61
+ }
62
+ }, 110);
63
+ // Keyboard events
64
+ readline.emitKeypressEvents(process.stdin);
65
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
66
+ process.stdin.setRawMode(true);
67
+ }
68
+ this.keypressHandler = (str, key) => {
69
+ if (key && key.ctrl && key.name === "c") {
70
+ this.onQuit();
71
+ return;
72
+ }
73
+ if (this.inputMode) {
74
+ if (key && key.name === "return") {
75
+ const cmd = this.inputBuffer;
76
+ this.inputMode = false;
77
+ this.inputBuffer = "";
78
+ this.executeCommand(cmd);
79
+ this.render();
80
+ return;
81
+ }
82
+ if (key && key.name === "escape") {
83
+ this.inputMode = false;
84
+ this.inputBuffer = "";
85
+ this.render();
86
+ return;
87
+ }
88
+ if (key && key.name === "backspace") {
89
+ if (this.inputBuffer.length > 0) {
90
+ this.inputBuffer = this.inputBuffer.slice(0, -1);
91
+ }
92
+ this.render();
93
+ return;
94
+ }
95
+ // Printable chars
96
+ if (str && str.length === 1 && !key?.ctrl && !key?.meta) {
97
+ this.inputBuffer += str;
98
+ this.render();
99
+ }
100
+ return;
101
+ }
102
+ // Delegate keypresses to active view first if help is not open
103
+ if (!this.helpOpen) {
104
+ const currentView = (this.views[this.viewMode] || this.views.status);
105
+ if (currentView && typeof currentView.handleKey === "function") {
106
+ const handled = currentView.handleKey(key, str, () => this.render());
107
+ if (handled) {
108
+ return;
109
+ }
110
+ }
111
+ }
112
+ const k = (key?.name || str || "").toLowerCase();
113
+ // Scrolling handlers
114
+ if (k === "up" || k === "k") {
115
+ this.scroll(-1);
116
+ return;
117
+ }
118
+ else if (k === "down" || k === "j") {
119
+ this.scroll(1);
120
+ return;
121
+ }
122
+ else if (k === "pageup") {
123
+ this.scroll(-10);
124
+ return;
125
+ }
126
+ else if (k === "pagedown") {
127
+ this.scroll(10);
128
+ return;
129
+ }
130
+ if (str === ":" || str === "/") {
131
+ this.inputMode = true;
132
+ this.inputBuffer = str;
133
+ this.render();
134
+ return;
135
+ }
136
+ if (k === "q") {
137
+ this.onQuit();
138
+ return;
139
+ }
140
+ else if (k === "r") {
141
+ this.triggerManualRefresh();
142
+ }
143
+ else if (k === "?" || str === "?") {
144
+ this.helpOpen = !this.helpOpen;
145
+ this.render();
146
+ }
147
+ else if (k === "escape") {
148
+ if (this.helpOpen) {
149
+ this.helpOpen = false;
150
+ }
151
+ else if (this.viewMode !== "status") {
152
+ this.viewMode = "status";
153
+ }
154
+ this.render();
155
+ }
156
+ else if (k === "s") {
157
+ this.viewMode = "status";
158
+ this.helpOpen = false;
159
+ this.render();
160
+ }
161
+ else if (k === "m") {
162
+ this.viewMode = "mcps";
163
+ this.helpOpen = false;
164
+ this.render();
165
+ }
166
+ else if (k === "t") {
167
+ this.viewMode = "tasks";
168
+ this.helpOpen = false;
169
+ this.render();
170
+ }
171
+ };
172
+ process.stdin.on("keypress", this.keypressHandler);
173
+ // Handle terminal resizing
174
+ this.resizeHandler = () => {
175
+ readline.cursorTo(process.stdout, 0, 0);
176
+ readline.clearScreenDown(process.stdout);
177
+ this.lastRenderOutput = ""; // force full redraw on resize
178
+ this.render();
179
+ };
180
+ process.stdout.on("resize", this.resizeHandler);
181
+ // Register process termination handlers to restore terminal state
182
+ this.sigintHandler = () => {
183
+ this.exit();
184
+ process.exit(130);
185
+ };
186
+ this.sigtermHandler = () => {
187
+ this.exit();
188
+ process.exit(143);
189
+ };
190
+ process.once("SIGINT", this.sigintHandler);
191
+ process.once("SIGTERM", this.sigtermHandler);
192
+ this.tracker.register(() => this.render());
193
+ this.render();
194
+ }
195
+ scroll(delta) {
196
+ const currentOffset = this.viewScrollOffsets[this.viewMode] || 0;
197
+ this.viewScrollOffsets[this.viewMode] = Math.max(0, currentOffset + delta);
198
+ this.render();
199
+ }
200
+ executeCommand(cmd) {
201
+ const trimmed = cmd.trim();
202
+ if (!trimmed)
203
+ return;
204
+ const normalized = trimmed.startsWith(":") ? "/" + trimmed.slice(1) : trimmed;
205
+ if (normalized === "/status" || normalized === "/s") {
206
+ this.viewMode = "status";
207
+ this.helpOpen = false;
208
+ }
209
+ else if (normalized === "/mcps" || normalized === "/mcp" || normalized === "/m") {
210
+ this.viewMode = "mcps";
211
+ this.helpOpen = false;
212
+ }
213
+ else if (normalized === "/tasks" || normalized === "/task" || normalized === "/t") {
214
+ this.viewMode = "tasks";
215
+ this.helpOpen = false;
216
+ }
217
+ else if (normalized === "/refresh" || normalized === "/r") {
218
+ this.triggerManualRefresh();
219
+ }
220
+ else if (normalized === "/help" || normalized === "/h" || normalized === "/?") {
221
+ this.helpOpen = !this.helpOpen;
222
+ }
223
+ else if (normalized === "/quit" || normalized === "/q") {
224
+ this.onQuit();
225
+ }
226
+ }
227
+ triggerManualRefresh() {
228
+ void this.onTriggerRefresh();
229
+ }
230
+ exit() {
231
+ if (this.exited)
232
+ return;
233
+ this.exited = true;
234
+ // Restore main screen buffer and show cursor
235
+ process.stdout.write("\x1b[?1049l\x1b[?25h");
236
+ if (this.tickInterval)
237
+ clearInterval(this.tickInterval);
238
+ if (this.spinInterval)
239
+ clearInterval(this.spinInterval);
240
+ if (this.keypressHandler) {
241
+ process.stdin.off("keypress", this.keypressHandler);
242
+ }
243
+ if (this.resizeHandler) {
244
+ process.stdout.off("resize", this.resizeHandler);
245
+ }
246
+ if (this.sigintHandler) {
247
+ process.off("SIGINT", this.sigintHandler);
248
+ this.sigintHandler = null;
249
+ }
250
+ if (this.sigtermHandler) {
251
+ process.off("SIGTERM", this.sigtermHandler);
252
+ this.sigtermHandler = null;
253
+ }
254
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
255
+ process.stdin.setRawMode(false);
256
+ }
257
+ }
258
+ render() {
259
+ const model = this.tracker.getModel();
260
+ const cols = process.stdout.columns || 80;
261
+ const rows = process.stdout.rows || 24;
262
+ const width = Math.max(40, cols);
263
+ const contentWidth = width - 4;
264
+ const bodyHeight = Math.max(1, rows - 7);
265
+ const spinner = SPIN_CHARS[this.spinIndex] ?? "⠋";
266
+ // Compute view lines and scrollbar state first, so we can render corner connectors on dividers
267
+ let viewLines = [];
268
+ if (this.helpOpen) {
269
+ // HELP OVERLAY MODE
270
+ viewLines = [
271
+ "",
272
+ bold("Backburner runtime status — help"),
273
+ "",
274
+ `${bold("q")} ${dim("quit — return to your shell")}`,
275
+ `${bold("r")} ${dim("refresh now")}`,
276
+ `${bold("s")} ${dim("switch to main status view")}`,
277
+ `${bold("m")} ${dim("switch to Smithery MCP health view")}`,
278
+ `${bold("t")} ${dim("switch to running tasks view")}`,
279
+ `${bold(":")} / ${dim("open command prompt")}`,
280
+ `${bold("?")} ${dim("toggle this help")}`,
281
+ `${bold("esc")} ${dim("close help / return to status view")}`,
282
+ "",
283
+ bold("Explorer Navigation:"),
284
+ `${bold("up/down (j/k)")} ${dim("move selection cursor / scroll explorer content")}`,
285
+ `${bold("pageup/down")} ${dim("scroll the main page viewport")}`,
286
+ `${bold("enter")} ${dim("drill down / open selected item")}`,
287
+ `${bold("esc / backspace")} ${dim("go back one level in the hierarchy")}`,
288
+ `${bold("type chars")} ${dim("filter workstreams by number or title (esc to clear)")}`,
289
+ "",
290
+ `${dim("This screen is ")}${bold("read-only")}${dim(". It shows status and does not mutate state.")}`,
291
+ "",
292
+ dim("Go to the source of truth for detail:"),
293
+ `${cyan("GitHub")} ${dim("issues · plans · PRs · reviews")}`,
294
+ `${blue("logs/state")} ${dim("full history")}`,
295
+ "",
296
+ dim("press ? or esc to close"),
297
+ ""
298
+ ];
299
+ }
300
+ else {
301
+ const currentView = (this.views[this.viewMode] || this.views.status);
302
+ viewLines = currentView.render(model, this.tracker, contentWidth, bodyHeight, spinner);
303
+ }
304
+ const totalLines = viewLines.length;
305
+ const showScrollbar = totalLines > bodyHeight;
306
+ let scrollOffset = this.viewScrollOffsets[this.viewMode] || 0;
307
+ scrollOffset = Math.max(0, Math.min(scrollOffset, totalLines - bodyHeight));
308
+ this.viewScrollOffsets[this.viewMode] = scrollOffset;
309
+ const visibleLines = viewLines.slice(scrollOffset, scrollOffset + bodyHeight);
310
+ while (visibleLines.length < bodyHeight) {
311
+ visibleLines.push("");
312
+ }
313
+ // Build the rendered buffer
314
+ let lines = [];
315
+ // 1. Top border
316
+ lines.push(`┌${"─".repeat(width - 2)}┐`);
317
+ const padNum = (n) => String(n).padStart(2, "0");
318
+ const d = new Date();
319
+ const nowTime = `${padNum(d.getHours())}:${padNum(d.getMinutes())}:${padNum(d.getSeconds())}`;
320
+ const viewTitle = this.helpOpen ? "HELP" : this.viewMode.toUpperCase();
321
+ const titleLeftPlain = `› Backburner [${viewTitle}]`;
322
+ const titleLeftAnsi = `${dim("›")} Backburner [${bold(cyan(viewTitle))}]`;
323
+ const titleRightPlain = `tty · ${cols}x${rows} ${nowTime}`;
324
+ const titleRightAnsi = `tty · ${cols}x${rows} ${nowTime}`;
325
+ const spaceBetween = Math.max(0, contentWidth - titleLeftPlain.length - titleRightPlain.length);
326
+ lines.push(`│ ${titleLeftAnsi}${" ".repeat(spaceBetween)}${titleRightAnsi} │`);
327
+ // 3. Divider (with T-connector for scrollbar track)
328
+ const topDivider = showScrollbar
329
+ ? `├${"─".repeat(width - 3)}┬┤`
330
+ : `├${"─".repeat(width - 2)}┤`;
331
+ lines.push(topDivider);
332
+ // 4. Viewport body lines
333
+ for (let i = 0; i < bodyHeight; i++) {
334
+ const line = visibleLines[i] || "";
335
+ const formatted = padOrTruncateAnsi(line, contentWidth);
336
+ let rightBorder = " │";
337
+ if (showScrollbar) {
338
+ const thumbHeight = Math.max(1, Math.floor((bodyHeight / totalLines) * bodyHeight));
339
+ const maxScroll = totalLines - bodyHeight;
340
+ const thumbStart = maxScroll > 0 ? Math.round((scrollOffset / maxScroll) * (bodyHeight - thumbHeight)) : 0;
341
+ const isThumb = i >= thumbStart && i < thumbStart + thumbHeight;
342
+ rightBorder = isThumb ? ` ${bold(cyan("┃"))}` : ` ${dim("░")}`;
343
+ }
344
+ lines.push(`│ ${formatted}${rightBorder}`);
345
+ }
346
+ // 5. Divider before footer (with T-connector for scrollbar track)
347
+ const bottomDivider = showScrollbar
348
+ ? `├${"─".repeat(width - 3)}┴┤`
349
+ : `├${"─".repeat(width - 2)}┤`;
350
+ lines.push(bottomDivider);
351
+ // 6. Footer
352
+ if (this.inputMode) {
353
+ const footerLeftPlain = `Command: ${this.inputBuffer}`;
354
+ const footerLeftAnsi = `${bold("Command:")} ${this.inputBuffer}`;
355
+ lines.push(`│ ${padOrTruncateAnsi(footerLeftAnsi, contentWidth)} │`);
356
+ }
357
+ else {
358
+ const footerLeftPlain = "q quit r refresh s status m mcps t tasks ? help";
359
+ const footerLeftAnsi = `${bold("q")} ${dim("quit")} ${bold("r")} ${dim("refresh")} ${bold("s")} ${dim("status")} ${bold("m")} ${dim("mcps")} ${bold("t")} ${dim("tasks")} ${bold("?")} ${dim("help")}`;
360
+ let footerRightPlain = "read-only · headless off";
361
+ let footerRightAnsi = `${dim("read-only · headless off")}`;
362
+ if (this.isRefreshing) {
363
+ footerRightPlain = "⟳ refreshing...";
364
+ footerRightAnsi = cyan("⟳ refreshing...");
365
+ }
366
+ const space = Math.max(0, contentWidth - stripAnsi(footerLeftAnsi).length - stripAnsi(footerRightAnsi).length);
367
+ const footerLine = `${footerLeftAnsi}${" ".repeat(space)}${footerRightAnsi}`;
368
+ lines.push(`│ ${padOrTruncateAnsi(footerLine, contentWidth)} │`);
369
+ }
370
+ // 7. Bottom border
371
+ lines.push(`└${"─".repeat(width - 2)}┘`);
372
+ // Draw the whole content cleanly to screen with clock-dedup optimization
373
+ const newOutput = lines.join("\n");
374
+ const dedupOutput = newOutput.replace(nowTime, "HH:MM:SS");
375
+ if (dedupOutput === this.lastRenderOutput && nowTime.length === this.lastTimeLength) {
376
+ // Optimization: Only the clock ticked, so we only update the clock string
377
+ readline.cursorTo(process.stdout, cols - 2 - nowTime.length, 1);
378
+ process.stdout.write(nowTime);
379
+ }
380
+ else {
381
+ // Full screen redraw
382
+ readline.cursorTo(process.stdout, 0, 0);
383
+ process.stdout.write(newOutput);
384
+ this.lastRenderOutput = dedupOutput;
385
+ this.lastTimeLength = nowTime.length;
386
+ }
387
+ }
388
+ }