@phren/cli 0.0.1

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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,707 @@
1
+ /**
2
+ * View rendering functions for the phren interactive shell.
3
+ * Extracted from shell.ts to keep the orchestrator under 300 lines.
4
+ */
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { canonicalTaskFilePath, listProjectCards, readTasks, readFindings, readReviewQueue, readRuntimeHealth, resolveTaskFilePath, } from "./data-access.js";
8
+ import { style, badge, separator, stripAnsi, truncateLine, renderWidth, wrapSegments, lineViewport, shellHelpText, gradient, } from "./shell-render.js";
9
+ import { formatSelectableLine, viewportWithStatus, } from "./shell-view-list.js";
10
+ import { SUB_VIEWS, TAB_ICONS, } from "./shell-types.js";
11
+ import { tasksByFilter, queueByFilter, } from "./shell-palette.js";
12
+ import { listMachines, listProfiles, } from "./data-access.js";
13
+ import { readInstallPreferences } from "./init-preferences.js";
14
+ import { isProjectHookEnabled, readProjectConfig } from "./project-config.js";
15
+ import { getScopedSkills } from "./skill-registry.js";
16
+ // ── Tab bar ────────────────────────────────────────────────────────────────
17
+ function renderTabBar(state) {
18
+ const cols = renderWidth();
19
+ if (state.view === "Health") {
20
+ const label = `${TAB_ICONS.Health} Health`;
21
+ return ` ${style.boldMagenta(label)}\n${separator(cols)}`;
22
+ }
23
+ if (state.view === "Projects") {
24
+ const label = `${TAB_ICONS.Projects} Projects`;
25
+ const tabLine = ` ${style.boldMagenta(label)} `;
26
+ return `${tabLine}\n${separator(cols)}`;
27
+ }
28
+ const projectTag = state.project
29
+ ? `${style.cyan(state.project)} ${style.dim("›")}`
30
+ : "";
31
+ const tabs = SUB_VIEWS.map((v) => {
32
+ const icon = TAB_ICONS[v] || "";
33
+ const label = `${icon} ${v}`;
34
+ return v === state.view
35
+ ? ` ${style.boldMagenta(label)} `
36
+ : ` ${style.dim(label)} `;
37
+ });
38
+ const segments = projectTag ? [projectTag, ...tabs] : tabs;
39
+ const tabLine = wrapSegments(segments, cols, {
40
+ indent: " ",
41
+ maxLines: 2,
42
+ separator: style.dim("│"),
43
+ });
44
+ return `${tabLine}\n${separator(cols)}`;
45
+ }
46
+ // ── Bottom bar ─────────────────────────────────────────────────────────────
47
+ function renderBottomBar(state, navMode, inputCtx, inputBuf) {
48
+ const cols = renderWidth();
49
+ const sep = separator(cols);
50
+ const dot = style.dim(" · ");
51
+ const k = (s) => style.boldCyan(s);
52
+ const d = (s) => style.dim(s);
53
+ if (navMode === "input") {
54
+ const labels = {
55
+ filter: "filter",
56
+ command: "cmd",
57
+ add: "add task",
58
+ "learn-add": "add finding",
59
+ "skill-add": "new skill name",
60
+ "mq-edit": "edit Review Queue item",
61
+ };
62
+ const label = labels[inputCtx] || inputCtx;
63
+ return `${sep}\n ${style.boldCyan(label + " ›")} ${inputBuf}${style.cyan("█")}`;
64
+ }
65
+ const viewHints = {
66
+ Projects: [`${k("↵")} ${d("open project")}`, `${k("i")} ${d("intro mode")}`],
67
+ Tasks: [`${k("a")} ${d("add")}`, `${k("↵")} ${d("mark done")}`, `${k("d")} ${d("toggle active")}`],
68
+ Findings: [`${k("a")} ${d("add")}`, `${k("d")} ${d("remove")}`],
69
+ "Review Queue": [`${k("a")} ${d("keep")}`, `${k("d")} ${d("discard")}`, `${k("e")} ${d("edit")}`],
70
+ Skills: [`${k("t")} ${d("toggle")}`, `${k("d")} ${d("remove")}`],
71
+ Hooks: [`${k("a")} ${d("enable")}`, `${k("d")} ${d("disable")}`],
72
+ Health: [`${k("↑↓")} ${d("scroll")}`, `${k("esc")} ${d("back")}`],
73
+ };
74
+ const extra = viewHints[state.view] ?? [];
75
+ const isSubView = state.view !== "Projects" && state.view !== "Health";
76
+ const nav = isSubView
77
+ ? [`${k("←→")} ${d("tabs")}`, `${k("↑↓")} ${d("move")}`, `${k("esc")} ${d("back")}`]
78
+ : state.view === "Health"
79
+ ? []
80
+ : [`${k("↑↓")} ${d("move")}`];
81
+ const tail = [`${k("h")} ${d("health")}`, `${k("/")} ${d("filter")}`, `${k(":")} ${d("cmd")}`, `${k("?")} ${d("help")}`, `${k("q")} ${d("quit")}`];
82
+ const hints = [...nav, ...extra, ...tail];
83
+ return `${sep}\n${wrapSegments(hints, cols, {
84
+ indent: " ",
85
+ maxLines: 3,
86
+ separator: dot,
87
+ })}`;
88
+ }
89
+ // ── Content height ─────────────────────────────────────────────────────────
90
+ function countRenderedLines(block) {
91
+ return block.split("\n").length;
92
+ }
93
+ function contentHeight(tabBar, bottomBar) {
94
+ const rows = process.stdout.rows || 24;
95
+ const reserved = 1 + countRenderedLines(tabBar) + 1 + countRenderedLines(bottomBar);
96
+ return Math.max(4, rows - reserved);
97
+ }
98
+ function collectProjectDashboardEntries(ctx) {
99
+ const cards = listProjectCards(ctx.phrenPath, ctx.profile);
100
+ return cards.map((card) => {
101
+ if (card.name === "global") {
102
+ return {
103
+ ...card,
104
+ activeCount: 0,
105
+ queueCount: 0,
106
+ findingCount: 0,
107
+ reviewCount: 0,
108
+ };
109
+ }
110
+ const task = readTasks(ctx.phrenPath, card.name);
111
+ const findings = readFindings(ctx.phrenPath, card.name);
112
+ const review = readReviewQueue(ctx.phrenPath, card.name);
113
+ return {
114
+ ...card,
115
+ activeCount: task.ok ? task.data.items.Active.length : 0,
116
+ queueCount: task.ok ? task.data.items.Queue.length : 0,
117
+ findingCount: findings.ok ? findings.data.length : 0,
118
+ reviewCount: review.ok ? review.data.length : 0,
119
+ };
120
+ });
121
+ }
122
+ function renderProjectsDashboard(ctx, entries, height) {
123
+ const runtime = readRuntimeHealth(ctx.phrenPath);
124
+ const scoped = entries.filter((entry) => entry.name !== "global");
125
+ const totals = scoped.reduce((acc, entry) => {
126
+ acc.active += entry.activeCount;
127
+ acc.queue += entry.queueCount;
128
+ acc.findings += entry.findingCount;
129
+ acc.review += entry.reviewCount;
130
+ return acc;
131
+ }, { active: 0, queue: 0, findings: 0, review: 0 });
132
+ const activePreview = scoped
133
+ .filter((entry) => entry.activeCount > 0 || entry.queueCount > 0)
134
+ .slice(0, 3)
135
+ .map((entry) => `${style.bold(entry.name)} ${style.dim(`A${entry.activeCount} · Q${entry.queueCount}`)}`);
136
+ const findingsPreview = scoped
137
+ .filter((entry) => entry.findingCount > 0)
138
+ .slice(0, 3)
139
+ .map((entry) => `${style.bold(entry.name)} ${style.dim(`${entry.findingCount} fragments`)}`);
140
+ const lines = [
141
+ ` ${badge(ctx.profile || "default", style.boldBlue)} ${style.bold(String(scoped.length))} projects ${style.dim("·")} ${style.boldGreen(String(totals.active))} active ${style.dim("·")} ${style.boldYellow(String(totals.queue))} queued ${style.dim("·")} ${style.boldCyan(String(totals.findings))} fragments ${style.dim("·")} ${style.boldMagenta(String(totals.review))} review`,
142
+ ctx.state.project
143
+ ? ` ${style.green("●")} active context ${style.boldCyan(ctx.state.project)} ${style.dim("· ↵ opens selected project tasks")}`
144
+ : ` ${style.dim("No project selected yet")} ${style.dim("· ↵ sets context and opens tasks")}`,
145
+ ` ${style.dim("Sync")} ${style.dim(runtime.lastSync?.lastPushStatus || runtime.lastAutoSave?.status || "unknown")} ${style.dim("·")} ${style.dim("unsynced")} ${style.bold(String(runtime.lastSync?.unsyncedCommits ?? 0))} ${style.dim("·")} ${style.dim("intro")} ${style.cyan(ctx.state.introMode || "once-per-version")}`,
146
+ ];
147
+ if (height >= 12) {
148
+ lines.push("");
149
+ lines.push(` ${style.bold("Task pulse")} ${activePreview.length ? activePreview.join(style.dim(" · ")) : style.dim("No active tasks across this profile.")}`);
150
+ lines.push(` ${style.bold("Recent fragments")} ${findingsPreview.length ? findingsPreview.join(style.dim(" · ")) : style.dim("Nothing yet.")}`);
151
+ }
152
+ lines.push("");
153
+ lines.push(` ${style.bold("Projects")} ${style.dim("press ↵ to open a project, / to filter, :intro to tune startup")}`);
154
+ return lines;
155
+ }
156
+ function renderProjectsView(ctx, cursor, height) {
157
+ const cols = renderWidth();
158
+ const cards = collectProjectDashboardEntries(ctx);
159
+ const filtered = ctx.state.filter
160
+ ? cards.filter((c) => `${c.name} ${c.summary} ${c.docs.join(" ")}`.toLowerCase().includes(ctx.state.filter.toLowerCase()))
161
+ : cards;
162
+ if (!filtered.length) {
163
+ return [style.dim(" No projects in this profile.")];
164
+ }
165
+ const dashboardLines = renderProjectsDashboard(ctx, cards, height);
166
+ const listHeight = Math.max(4, height - dashboardLines.length);
167
+ const allLines = [];
168
+ let cursorFirstLine = 0;
169
+ let cursorLastLine = 0;
170
+ for (let absIdx = 0; absIdx < filtered.length; absIdx++) {
171
+ const card = filtered[absIdx];
172
+ const isSelected = absIdx === cursor;
173
+ if (isSelected)
174
+ cursorFirstLine = allLines.length;
175
+ const isActive = card.name === ctx.state.project;
176
+ const cursorChar = isSelected ? style.cyan("▶") : " ";
177
+ const bullet = isActive ? style.green("●") : style.dim("○");
178
+ const nameStr = isActive ? style.boldGreen(card.name) : style.bold(card.name);
179
+ const docsStr = style.dim(`[A${card.activeCount} · Q${card.queueCount} · F${card.findingCount} · R${card.reviewCount}]`);
180
+ let nameRow = ` ${cursorChar} ${bullet} ${nameStr} ${docsStr}`;
181
+ let summaryRow = ` ${style.dim(card.summary || "No summary yet.")}`;
182
+ if (isSelected) {
183
+ nameRow = formatSelectableLine(nameRow, cols, true);
184
+ summaryRow = formatSelectableLine(summaryRow, cols, true);
185
+ }
186
+ allLines.push(nameRow);
187
+ allLines.push(summaryRow);
188
+ if (isSelected)
189
+ cursorLastLine = allLines.length - 1;
190
+ }
191
+ const usableHeight = Math.max(1, listHeight - (allLines.length > listHeight ? 1 : 0));
192
+ const vp = viewportWithStatus(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll(), cursor, filtered.length);
193
+ ctx.setScroll(vp.scrollStart);
194
+ return [...dashboardLines, ...vp.lines];
195
+ }
196
+ // ── Task helpers ────────────────────────────────────────────────────────
197
+ function sectionBullet(title) {
198
+ switch (title) {
199
+ case "Active": return { bullet: style.green("●"), colorFn: style.boldGreen };
200
+ case "Queue": return { bullet: style.yellow("●"), colorFn: style.boldYellow };
201
+ case "Done": return { bullet: style.gray("●"), colorFn: style.dim };
202
+ default: return { bullet: "●", colorFn: style.bold };
203
+ }
204
+ }
205
+ function priorityIndicator(priority, isDone) {
206
+ if (isDone)
207
+ return style.dim("○");
208
+ switch (priority) {
209
+ case "high": return style.boldRed("●");
210
+ case "medium": return style.yellow("◐");
211
+ case "low": return style.dim("○");
212
+ default: return style.dim("·");
213
+ }
214
+ }
215
+ function taskStatusIcon(section, checked) {
216
+ if (checked || section === "Done")
217
+ return style.dim("✓");
218
+ if (section === "Active")
219
+ return style.magenta("◉");
220
+ return style.dim("☐");
221
+ }
222
+ const BID_RE = /<!--\s*bid:([a-z0-9]{8})\s*-->/;
223
+ function parseSubsections(taskPath, project, cache) {
224
+ if (cache?.project === project)
225
+ return { map: cache.map, cache };
226
+ const map = new Map();
227
+ try {
228
+ const raw = fs.readFileSync(taskPath, "utf8");
229
+ let currentSub = "";
230
+ let rowIdx = 0;
231
+ for (const line of raw.split("\n")) {
232
+ const subMatch = line.match(/^###\s+(.+)/);
233
+ if (subMatch) {
234
+ currentSub = subMatch[1].trim();
235
+ continue;
236
+ }
237
+ if (line.match(/^##\s/)) {
238
+ currentSub = "";
239
+ continue;
240
+ }
241
+ if (line.startsWith("- ")) {
242
+ if (currentSub) {
243
+ const bidMatch = line.match(BID_RE);
244
+ const key = bidMatch ? bidMatch[1] : `row:${rowIdx}`;
245
+ map.set(key, currentSub);
246
+ }
247
+ rowIdx++;
248
+ }
249
+ }
250
+ }
251
+ catch (err) {
252
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
253
+ process.stderr.write(`[phren] buildSubsectionMap: ${err instanceof Error ? err.message : String(err)}\n`);
254
+ }
255
+ const newCache = { project, map };
256
+ return { map, cache: newCache };
257
+ }
258
+ // ── Tasks view ─────────────────────────────────────────────────────────────
259
+ function renderTaskView(ctx, cursor, height, subsectionsCache) {
260
+ const cols = renderWidth();
261
+ const project = ctx.state.project;
262
+ if (!project) {
263
+ return { lines: [style.dim(" No project selected — navigate to Projects (← →) and press ↵")], subsectionsCache };
264
+ }
265
+ const result = readTasks(ctx.phrenPath, project);
266
+ if (!result.ok)
267
+ return { lines: [result.error], subsectionsCache };
268
+ const parsed = result.data;
269
+ const warnings = parsed.issues.length
270
+ ? [` ${style.yellow("⚠")} ${style.yellow(parsed.issues.join("; "))}`, ""]
271
+ : [];
272
+ const taskFile = resolveTaskFilePath(ctx.phrenPath, project)
273
+ ?? canonicalTaskFilePath(ctx.phrenPath, project)
274
+ ?? path.join(ctx.phrenPath, project, "tasks.md");
275
+ const subsResult = parseSubsections(taskFile, project, subsectionsCache);
276
+ const subsections = subsResult.map;
277
+ const newCache = subsResult.cache;
278
+ const active = ctx.state.filter ? tasksByFilter(parsed.items.Active, ctx.state.filter) : parsed.items.Active;
279
+ const queue = ctx.state.filter ? tasksByFilter(parsed.items.Queue, ctx.state.filter) : parsed.items.Queue;
280
+ const done = ctx.state.filter ? tasksByFilter(parsed.items.Done, ctx.state.filter) : parsed.items.Done;
281
+ const flatItems = [...active, ...queue, ...done];
282
+ if (!flatItems.length) {
283
+ const hint = ctx.state.filter ? " No items match the filter." : ` No tasks yet. Press ${style.boldCyan("a")} to add one.`;
284
+ return { lines: [...warnings, style.dim(hint)], subsectionsCache: newCache };
285
+ }
286
+ const queueStart = active.length;
287
+ const doneStart = active.length + queue.length;
288
+ const allLines = [];
289
+ let cursorFirstLine = 0;
290
+ let cursorLastLine = 0;
291
+ let lastSection = "";
292
+ let lastSub = "";
293
+ for (let absIdx = 0; absIdx < flatItems.length; absIdx++) {
294
+ const item = flatItems[absIdx];
295
+ const isSelected = absIdx === cursor;
296
+ const isDone = absIdx >= doneStart;
297
+ const section = absIdx < queueStart ? "Active" : absIdx < doneStart ? "Queue" : "Done";
298
+ if (section !== lastSection) {
299
+ lastSection = section;
300
+ lastSub = "";
301
+ const { bullet, colorFn } = sectionBullet(section);
302
+ if (allLines.length > 0)
303
+ allLines.push("");
304
+ allLines.push(` ${bullet} ${colorFn(section)}`);
305
+ }
306
+ const sub = (item.stableId ? subsections.get(item.stableId) : undefined) ?? subsections.get(`row:${absIdx}`) ?? "";
307
+ if (sub && sub !== lastSub) {
308
+ lastSub = sub;
309
+ allLines.push(` ${style.boldYellow(sub)}`);
310
+ }
311
+ if (isSelected)
312
+ cursorFirstLine = allLines.length;
313
+ const prioIcon = priorityIndicator(item.priority, isDone);
314
+ const statusIcon = taskStatusIcon(section, item.checked);
315
+ const pinTag = item.pinned ? ` ${style.boldCyan("★")}` : "";
316
+ const ghTag = item.githubIssue
317
+ ? ` ${style.dim("[")}${style.cyan(`#${item.githubIssue}`)}${style.dim("]")}`
318
+ : "";
319
+ const lineText = isDone ? style.dim(item.line) : item.line;
320
+ const idStr = style.dim(item.id.padEnd(3));
321
+ let row = ` ${prioIcon} ${statusIcon} ${idStr} ${lineText}${pinTag}${ghTag}`;
322
+ row = isSelected && !isDone
323
+ ? formatSelectableLine(row, cols, true)
324
+ : truncateLine(row, cols);
325
+ allLines.push(row);
326
+ if (item.context) {
327
+ const ctxLine = ` ${style.dimItalic("→ " + item.context)}`;
328
+ allLines.push(isSelected && !isDone ? formatSelectableLine(ctxLine, cols, true) : truncateLine(ctxLine, cols));
329
+ }
330
+ if (isSelected)
331
+ cursorLastLine = allLines.length - 1;
332
+ }
333
+ const usableHeight = Math.max(1, height - warnings.length - (allLines.length > height ? 1 : 0));
334
+ const vp = viewportWithStatus(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll(), cursor, active.length + queue.length);
335
+ ctx.setScroll(vp.scrollStart);
336
+ return { lines: [...warnings, ...vp.lines], subsectionsCache: newCache };
337
+ }
338
+ // ── Findings view ──────────────────────────────────────────────────────────
339
+ function renderFindingsView(ctx, cursor, height) {
340
+ const cols = renderWidth();
341
+ const project = ctx.state.project;
342
+ if (!project)
343
+ return [style.dim(" No project selected.")];
344
+ const result = readFindings(ctx.phrenPath, project);
345
+ if (!result.ok)
346
+ return [result.error];
347
+ const all = result.data;
348
+ const filtered = ctx.state.filter
349
+ ? all.filter((item) => `${item.id} ${item.date} ${item.text}`.toLowerCase().includes(ctx.state.filter.toLowerCase()))
350
+ : all;
351
+ if (!filtered.length) {
352
+ return [style.dim(` Nothing here yet. Press ${style.boldCyan("a")} to tell phren something.`)];
353
+ }
354
+ const allLines = [];
355
+ let cursorFirstLine = 0;
356
+ let cursorLastLine = 0;
357
+ for (let absIdx = 0; absIdx < filtered.length; absIdx++) {
358
+ const item = filtered[absIdx];
359
+ const isSelected = absIdx === cursor;
360
+ if (isSelected)
361
+ cursorFirstLine = allLines.length;
362
+ const idStr = style.dim(item.id.padEnd(4));
363
+ const dateStr = style.dim(`[${item.date}]`);
364
+ let row = ` ${idStr} ${dateStr} ${item.text}`;
365
+ row = formatSelectableLine(row, cols, isSelected);
366
+ allLines.push(row);
367
+ if (item.citation) {
368
+ const cite = ` ${style.italic(style.blue("↗ " + item.citation))}`;
369
+ allLines.push(formatSelectableLine(cite, cols, isSelected));
370
+ }
371
+ if (isSelected)
372
+ cursorLastLine = allLines.length - 1;
373
+ }
374
+ const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
375
+ const vp = viewportWithStatus(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll(), cursor, filtered.length);
376
+ ctx.setScroll(vp.scrollStart);
377
+ return vp.lines;
378
+ }
379
+ // ── Review Queue view ──────────────────────────────────────────────────────
380
+ function queueSectionBadge(section) {
381
+ switch (section.toLowerCase()) {
382
+ case "review": return badge(section, style.yellow);
383
+ case "stale": return badge(section, style.red);
384
+ case "conflicts": return badge(section, style.magenta);
385
+ default: return badge(section, style.dim);
386
+ }
387
+ }
388
+ function renderMemoryQueueView(ctx, cursor, height) {
389
+ const cols = renderWidth();
390
+ const project = ctx.state.project;
391
+ if (!project)
392
+ return [style.dim(" No project selected.")];
393
+ const result = readReviewQueue(ctx.phrenPath, project);
394
+ if (!result.ok)
395
+ return [result.error];
396
+ const filtered = ctx.state.filter
397
+ ? queueByFilter(result.data, ctx.state.filter)
398
+ : result.data;
399
+ if (!filtered.length) {
400
+ return [style.dim(" No queued memory items. Run :govern to scan for stale entries.")];
401
+ }
402
+ const allLines = [];
403
+ let cursorFirstLine = 0;
404
+ let cursorLastLine = 0;
405
+ let currentSection = "";
406
+ for (let absIdx = 0; absIdx < filtered.length; absIdx++) {
407
+ const item = filtered[absIdx];
408
+ const isSelected = absIdx === cursor;
409
+ if (item.section !== currentSection) {
410
+ currentSection = item.section;
411
+ allLines.push(` ${queueSectionBadge(currentSection)} ${style.bold(currentSection)}`);
412
+ }
413
+ if (isSelected)
414
+ cursorFirstLine = allLines.length;
415
+ const riskBadge = item.risky ? badge("risk", style.boldRed) : badge("ok", style.green);
416
+ const confStr = item.confidence !== undefined
417
+ ? ` ${style.dim("conf=")}${item.confidence >= 0.8 ? style.green(item.confidence.toFixed(2))
418
+ : item.confidence >= 0.6 ? style.yellow(item.confidence.toFixed(2))
419
+ : style.red(item.confidence.toFixed(2))}`
420
+ : "";
421
+ let metaRow = ` ${style.dim(item.id)} ${riskBadge} ${style.dim(`[${item.date}]`)}${confStr}`;
422
+ let textRow = ` ${item.text}`;
423
+ if (isSelected) {
424
+ metaRow = formatSelectableLine(metaRow, cols, true);
425
+ textRow = formatSelectableLine(textRow, cols, true);
426
+ }
427
+ else {
428
+ metaRow = truncateLine(metaRow, cols);
429
+ textRow = truncateLine(textRow, cols);
430
+ }
431
+ allLines.push(metaRow);
432
+ allLines.push(textRow);
433
+ if (isSelected)
434
+ cursorLastLine = allLines.length - 1;
435
+ }
436
+ const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
437
+ const vp = viewportWithStatus(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll(), cursor, filtered.length);
438
+ ctx.setScroll(vp.scrollStart);
439
+ return vp.lines;
440
+ }
441
+ export function getProjectSkills(phrenPath, project) {
442
+ return getScopedSkills(phrenPath, "", project).map((skill) => ({
443
+ name: skill.name,
444
+ path: skill.path,
445
+ enabled: skill.enabled,
446
+ }));
447
+ }
448
+ /** Max lines of skill content to show inline when selected. */
449
+ const SKILL_PREVIEW_LINES = 20;
450
+ function readSkillBody(skillPath) {
451
+ try {
452
+ const raw = fs.readFileSync(skillPath, "utf8");
453
+ // Strip YAML frontmatter (--- ... ---)
454
+ const stripped = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
455
+ const fmMatch = stripped.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
456
+ const body = fmMatch ? fmMatch[1] : stripped;
457
+ // Strip leading title (# ...) and blank lines
458
+ const lines = body.split("\n");
459
+ let start = 0;
460
+ while (start < lines.length && (lines[start].trim() === "" || lines[start].startsWith("# ")))
461
+ start++;
462
+ return lines.slice(start, start + SKILL_PREVIEW_LINES);
463
+ }
464
+ catch {
465
+ return ["(could not read skill file)"];
466
+ }
467
+ }
468
+ function renderSkillsView(ctx, cursor, height) {
469
+ const cols = renderWidth();
470
+ const project = ctx.state.project;
471
+ if (!project)
472
+ return [style.dim(" No project selected.")];
473
+ const skills = getProjectSkills(ctx.phrenPath, project);
474
+ const filtered = ctx.state.filter
475
+ ? skills.filter((s) => s.name.toLowerCase().includes(ctx.state.filter.toLowerCase()))
476
+ : skills;
477
+ if (!filtered.length) {
478
+ return [style.dim(` No skills for ${project}. Use "phren skills add ${project} <path>" to add one.`)];
479
+ }
480
+ const allLines = [];
481
+ let cursorFirstLine = 0;
482
+ let cursorLastLine = 0;
483
+ for (let i = 0; i < filtered.length; i++) {
484
+ const s = filtered[i];
485
+ const isSelected = i === cursor;
486
+ if (isSelected)
487
+ cursorFirstLine = allLines.length;
488
+ const isSymlink = (() => { try {
489
+ return fs.lstatSync(s.path).isSymbolicLink();
490
+ }
491
+ catch {
492
+ return false;
493
+ } })();
494
+ const linkTag = isSymlink ? style.dim(" →") : "";
495
+ const status = s.enabled ? style.boldGreen("enabled ") : style.dim("disabled");
496
+ let row = ` ${style.dim((i + 1).toString().padEnd(3))} ${status} ${style.bold(s.name)}${linkTag}`;
497
+ row = formatSelectableLine(row, cols, isSelected);
498
+ allLines.push(row);
499
+ // Show inline content preview for the selected skill
500
+ if (isSelected) {
501
+ const bodyLines = readSkillBody(s.path);
502
+ if (bodyLines.length > 0) {
503
+ allLines.push("");
504
+ for (const line of bodyLines) {
505
+ allLines.push(truncateLine(` ${style.dim(line)}`, cols));
506
+ }
507
+ allLines.push("");
508
+ }
509
+ }
510
+ if (isSelected)
511
+ cursorLastLine = allLines.length - 1;
512
+ }
513
+ const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
514
+ const vp = viewportWithStatus(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll(), cursor, filtered.length);
515
+ ctx.setScroll(vp.scrollStart);
516
+ return vp.lines;
517
+ }
518
+ const LIFECYCLE_HOOKS = [
519
+ { event: "UserPromptSubmit", description: "inject context before each prompt" },
520
+ { event: "Stop", description: "phren saves fragments after each response" },
521
+ { event: "SessionStart", description: "git pull at session start" },
522
+ ];
523
+ export function getHookEntries(phrenPath, project) {
524
+ const prefs = readInstallPreferences(phrenPath);
525
+ const hooksEnabled = prefs.hooksEnabled !== false;
526
+ const projectConfig = project ? readProjectConfig(phrenPath, project) : undefined;
527
+ return LIFECYCLE_HOOKS.map((h) => ({
528
+ ...h,
529
+ enabled: hooksEnabled && isProjectHookEnabled(phrenPath, project, h.event, projectConfig),
530
+ }));
531
+ }
532
+ function renderHooksView(ctx, cursor, height) {
533
+ const cols = renderWidth();
534
+ const entries = getHookEntries(ctx.phrenPath, ctx.state.project);
535
+ const allEnabled = entries.every((e) => e.enabled);
536
+ const allLines = [];
537
+ let cursorFirstLine = 0;
538
+ let cursorLastLine = 0;
539
+ for (let i = 0; i < entries.length; i++) {
540
+ const e = entries[i];
541
+ const isSelected = i === cursor;
542
+ if (isSelected)
543
+ cursorFirstLine = allLines.length;
544
+ const statusBadge = e.enabled ? style.boldGreen("active ") : style.dim("inactive");
545
+ let nameRow = ` ${style.dim((i + 1).toString().padEnd(3))} ${statusBadge} ${style.bold(e.event)}`;
546
+ let descRow = ` ${style.dim(e.description)}`;
547
+ if (isSelected) {
548
+ nameRow = formatSelectableLine(nameRow, cols, true);
549
+ descRow = formatSelectableLine(descRow, cols, true);
550
+ }
551
+ else {
552
+ nameRow = truncateLine(nameRow, cols);
553
+ descRow = truncateLine(descRow, cols);
554
+ }
555
+ allLines.push(nameRow);
556
+ allLines.push(descRow);
557
+ if (isSelected)
558
+ cursorLastLine = allLines.length - 1;
559
+ }
560
+ allLines.push("");
561
+ allLines.push(style.dim(` hooks: ${allEnabled ? style.boldGreen("ON") : style.boldRed("OFF")} · ${style.dim("a = enable all · d = disable all")}`));
562
+ const usableHeight = Math.max(1, height - (allLines.length > height ? 1 : 0));
563
+ const vp = viewportWithStatus(allLines, cursorFirstLine, cursorLastLine, usableHeight, ctx.currentScroll(), cursor, entries.length);
564
+ ctx.setScroll(vp.scrollStart);
565
+ return vp.lines;
566
+ }
567
+ export { writeInstallPreferences } from "./init-preferences.js";
568
+ // ── Machines/Profiles view ─────────────────────────────────────────────────
569
+ function renderMachinesView(phrenPath) {
570
+ const machines = listMachines(phrenPath);
571
+ const profiles = listProfiles(phrenPath);
572
+ const lines = [];
573
+ lines.push(style.bold(" Machines"));
574
+ if (!machines.ok) {
575
+ lines.push(` ${style.dim(machines.error)}`);
576
+ }
577
+ else {
578
+ const entries = Object.entries(machines.data);
579
+ if (!entries.length)
580
+ lines.push(` ${style.dim("(none)")}`);
581
+ for (const [machine, prof] of entries) {
582
+ lines.push(` ${style.bold(machine)} ${style.dim("→")} ${style.cyan(prof)}`);
583
+ }
584
+ }
585
+ lines.push("", style.bold(" Profiles"));
586
+ if (!profiles.ok) {
587
+ lines.push(` ${style.dim(profiles.error)}`);
588
+ }
589
+ else {
590
+ if (!profiles.data.length)
591
+ lines.push(` ${style.dim("(none)")}`);
592
+ for (const prof of profiles.data) {
593
+ lines.push(` ${style.cyan(prof.name)}: ${prof.projects.join(", ") || style.dim("(no projects)")}`);
594
+ }
595
+ }
596
+ lines.push("", ` ${style.dim(":machine map <hostname> <profile>")}`, ` ${style.dim(":profile add-project|remove-project <profile> <project>")}`);
597
+ return lines;
598
+ }
599
+ // ── Health view ────────────────────────────────────────────────────────────
600
+ function renderHealthView(phrenPath, doctor, cursor, height, currentScroll, setScroll) {
601
+ const runtime = readRuntimeHealth(phrenPath);
602
+ const allLines = [];
603
+ const statusIcon = doctor.ok ? style.green("✓") : style.red("✗");
604
+ const statusLabel = doctor.ok ? style.boldGreen("healthy") : style.boldRed("issues found");
605
+ allLines.push(` ${statusIcon} ${style.bold("phren")} ${statusLabel}`);
606
+ if (doctor.machine)
607
+ allLines.push(` ${style.dim("machine:")} ${style.bold(doctor.machine)}`);
608
+ if (doctor.profile)
609
+ allLines.push(` ${style.dim("profile:")} ${style.cyan(doctor.profile)}`);
610
+ allLines.push("", ` ${style.bold("Checks")}`);
611
+ for (const check of doctor.checks) {
612
+ const icon = check.ok ? style.green("✓") : style.red("✗");
613
+ const status = check.ok ? style.dim("ok") : style.boldRed("fail");
614
+ allLines.push(` ${icon} ${status} ${check.name}: ${check.detail}`);
615
+ }
616
+ allLines.push("", ` ${style.bold("Runtime")}`);
617
+ allLines.push(` ${style.dim("last hook: ")} ${style.dim(runtime.lastPromptAt || "n/a")}`);
618
+ allLines.push(` ${style.dim("last auto-save: ")} ${style.dim(runtime.lastAutoSave?.at || "n/a")} ${style.dim(runtime.lastAutoSave?.status || "")}`);
619
+ allLines.push(` ${style.dim("last governance: ")} ${style.dim(runtime.lastGovernance?.at || "n/a")} ${style.dim(runtime.lastGovernance?.status || "")}`);
620
+ allLines.push(` ${style.dim("last pull: ")} ${style.dim(runtime.lastSync?.lastPullAt || "n/a")} ${style.dim(runtime.lastSync?.lastPullStatus || "")}`);
621
+ allLines.push(` ${style.dim("last push: ")} ${style.dim(runtime.lastSync?.lastPushAt || "n/a")} ${style.dim(runtime.lastSync?.lastPushStatus || "")}`);
622
+ allLines.push(` ${style.dim("unsynced: ")} ${style.bold(String(runtime.lastSync?.unsyncedCommits ?? 0))} ${style.dim("commit(s)")}`);
623
+ if (!doctor.ok) {
624
+ allLines.push("", ` ${style.boldYellow("→")} ${style.bold(":run fix")} ${style.dim("to auto-heal")} ${style.dim(":relink :rerun hooks :update")}`);
625
+ }
626
+ else {
627
+ allLines.push("", ` ${style.dim(":run fix :relink :rerun hooks :update")}`);
628
+ }
629
+ const lineCount = allLines.length;
630
+ if (allLines.length <= height)
631
+ return { lines: allLines, lineCount };
632
+ const cols = renderWidth();
633
+ const clampedCursor = Math.max(0, Math.min(cursor, allLines.length - 1));
634
+ allLines[clampedCursor] = formatSelectableLine(allLines[clampedCursor], cols, true);
635
+ const vp = lineViewport(allLines, clampedCursor, clampedCursor, height - 1, currentScroll);
636
+ setScroll(vp.scrollStart);
637
+ const pct = allLines.length <= 1 ? 100 : Math.round((clampedCursor / (allLines.length - 1)) * 100);
638
+ vp.lines.push(style.dim(` ━━━${clampedCursor + 1}/${allLines.length} ${pct}%`));
639
+ return { lines: vp.lines, lineCount };
640
+ }
641
+ // ── Main render ────────────────────────────────────────────────────────────
642
+ export async function renderShell(ctx, navMode, inputCtx, inputBuf, showHelp, message, doctorSnapshot, subsectionsCache, setHealthLineCount, setSubsectionsCache) {
643
+ const projectLabel = ctx.state.project
644
+ ? ` ${style.dim("·")} ${style.cyan(ctx.state.project)}`
645
+ : "";
646
+ const filterLabel = ctx.state.filter
647
+ ? ` ${style.dim("·")} ${style.yellow("/" + ctx.state.filter)}`
648
+ : "";
649
+ const header = ` ${gradient("◆ phren")}${projectLabel}${filterLabel}`;
650
+ const tabBar = renderTabBar(ctx.state);
651
+ const bottomBar = renderBottomBar(ctx.state, navMode, inputCtx, inputBuf);
652
+ const cursor = ctx.currentCursor();
653
+ const height = contentHeight(tabBar, bottomBar);
654
+ let contentLines;
655
+ if (showHelp) {
656
+ contentLines = shellHelpText().split("\n");
657
+ }
658
+ else {
659
+ switch (ctx.state.view) {
660
+ case "Projects":
661
+ contentLines = renderProjectsView(ctx, cursor, height);
662
+ break;
663
+ case "Tasks": {
664
+ const result = renderTaskView(ctx, cursor, height, subsectionsCache);
665
+ contentLines = result.lines;
666
+ setSubsectionsCache(result.subsectionsCache);
667
+ break;
668
+ }
669
+ case "Findings":
670
+ contentLines = renderFindingsView(ctx, cursor, height);
671
+ break;
672
+ case "Review Queue":
673
+ contentLines = renderMemoryQueueView(ctx, cursor, height);
674
+ break;
675
+ case "Skills":
676
+ contentLines = renderSkillsView(ctx, cursor, height);
677
+ break;
678
+ case "Hooks":
679
+ contentLines = renderHooksView(ctx, cursor, height);
680
+ break;
681
+ case "Machines/Profiles":
682
+ contentLines = renderMachinesView(ctx.phrenPath);
683
+ break;
684
+ case "Health": {
685
+ const doctor = await doctorSnapshot();
686
+ const result = renderHealthView(ctx.phrenPath, doctor, cursor, height, ctx.currentScroll(), ctx.setScroll);
687
+ contentLines = result.lines;
688
+ setHealthLineCount(result.lineCount);
689
+ break;
690
+ }
691
+ default:
692
+ contentLines = [" Unknown view."];
693
+ }
694
+ }
695
+ const displayed = contentLines.slice(0, height);
696
+ while (displayed.length < height)
697
+ displayed.push("");
698
+ const msgLine = ` ${style.dimItalic(stripAnsi(message).trimStart() ? message : "")}`;
699
+ const cols = renderWidth();
700
+ const parts = [header, tabBar, ...displayed, msgLine, bottomBar];
701
+ return parts.map(line => {
702
+ if (line.includes("\n")) {
703
+ return line.split("\n").map(sub => truncateLine(sub, cols) + "\x1b[K").join("\n");
704
+ }
705
+ return truncateLine(line, cols) + "\x1b[K";
706
+ }).join("\n");
707
+ }