one-code-extension 0.1.4 → 0.3.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 (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +222 -177
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +128 -0
  8. package/docs/guide/configuration.md +173 -0
  9. package/docs/guide/differences-from-claude-code.md +152 -0
  10. package/docs/guide/doctor.md +88 -0
  11. package/docs/guide/hooks.md +111 -0
  12. package/docs/guide/installation.md +126 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
  14. package/docs/guide/providers-and-models.md +196 -0
  15. package/docs/guide/reference.md +226 -0
  16. package/docs/guide/sessions-and-context.md +192 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +178 -0
  18. package/docs/guide/subagents-and-workflows.md +195 -0
  19. package/docs/guide/tasks-and-background-work.md +113 -0
  20. package/docs/guide/terminal-interface.md +157 -0
  21. package/docs/guide/tools.md +177 -0
  22. package/docs/guide/troubleshooting.md +177 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -217
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. package/extensions/workflow/permission-gate.ts +0 -71
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Marketplace content sync: make a registered marketplace's manifest readable
3
+ * locally (clone/fetch/read per source type) and parse it into a snapshot.
4
+ *
5
+ * Network and git run only from explicit panel actions (open/refresh/add) —
6
+ * never at session start.
7
+ */
8
+
9
+ import { existsSync, statSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { readJsonFile, writeJsonAtomic } from "../../lib/atomic-write.ts";
12
+ import { fetchWithTimeout } from "../../lib/fetch-timeout.ts";
13
+ import { gitClone, gitUpdate } from "./git.ts";
14
+ import { writeKnownMarketplace } from "./registry.ts";
15
+ import {
16
+ type KnownMarketplace,
17
+ type MarketplaceManifest,
18
+ type MarketplaceSource,
19
+ parseMarketplaceManifest,
20
+ } from "./types.ts";
21
+
22
+ export interface MarketplaceSnapshot {
23
+ name: string;
24
+ known: KnownMarketplace;
25
+ manifest?: MarketplaceManifest;
26
+ errors: string[];
27
+ /** Directory plugin `./relative` sources resolve against (clone/local root). */
28
+ contentRoot?: string;
29
+ }
30
+
31
+ export function marketplaceCacheDir(root: string): string {
32
+ return join(root, "marketplaces");
33
+ }
34
+
35
+ /** Where a source's content lives under our root (clone dir or cached .json). */
36
+ export function installLocationFor(root: string, name: string, source: MarketplaceSource): string {
37
+ return source.source === "url" ? join(marketplaceCacheDir(root), `${name}.json`) : join(marketplaceCacheDir(root), name);
38
+ }
39
+
40
+ function cloneUrl(source: MarketplaceSource): string | undefined {
41
+ if (source.source === "github") return `https://github.com/${source.repo}.git`;
42
+ if (source.source === "git") return source.url;
43
+ return undefined;
44
+ }
45
+
46
+ const FETCH_TIMEOUT_MS = 10_000;
47
+
48
+ async function fetchManifestJson(url: string): Promise<unknown> {
49
+ const response = await fetchWithTimeout(url, FETCH_TIMEOUT_MS, { headers: { "user-agent": "one-code-plugins" } });
50
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
51
+ return await response.json();
52
+ }
53
+
54
+ /**
55
+ * Ensure the marketplace's content is present (clone/fetch when missing,
56
+ * update when `refresh`), then parse its manifest. Errors are reported in the
57
+ * snapshot, never thrown — offline opens degrade to whatever is cached.
58
+ */
59
+ export async function syncMarketplace(
60
+ root: string,
61
+ name: string,
62
+ known: KnownMarketplace,
63
+ options: { refresh?: boolean } = {},
64
+ ): Promise<MarketplaceSnapshot> {
65
+ const errors: string[] = [];
66
+ const { source } = known;
67
+ let manifestPath: string | undefined;
68
+ let contentRoot: string | undefined;
69
+ let synced = false;
70
+
71
+ const url = cloneUrl(source);
72
+ if (url) {
73
+ const dest = installLocationFor(root, name, source);
74
+ try {
75
+ if (!existsSync(dest)) {
76
+ await gitClone(url, dest, { ref: source.source === "github" || source.source === "git" ? source.ref : undefined });
77
+ synced = true;
78
+ } else if (options.refresh) {
79
+ try {
80
+ await gitUpdate(dest, { ref: source.source === "github" || source.source === "git" ? source.ref : undefined });
81
+ } catch {
82
+ // In-place update failed (force-push, sparse transition, corrupt
83
+ // clone): re-clone from scratch rather than serving a broken tree.
84
+ // The existing clone stays in place until the replacement clone
85
+ // succeeds — gitClone builds in a temp dir and renames over dest,
86
+ // so a failed re-clone keeps serving the stale content.
87
+ await gitClone(url, dest, { ref: source.source === "github" || source.source === "git" ? source.ref : undefined });
88
+ }
89
+ synced = true;
90
+ }
91
+ } catch (error) {
92
+ errors.push(`${name}: ${(error as Error).message}`);
93
+ }
94
+ if (existsSync(dest)) {
95
+ contentRoot = dest;
96
+ manifestPath = join(dest, ".claude-plugin", "marketplace.json");
97
+ }
98
+ } else if (source.source === "url") {
99
+ const cache = installLocationFor(root, name, source);
100
+ if (!existsSync(cache) || options.refresh) {
101
+ try {
102
+ const fetched = await fetchManifestJson(source.url);
103
+ const validated = parseMarketplaceManifest(fetched);
104
+ if (!validated.manifest) {
105
+ errors.push(...validated.errors.map((e) => `${name}: ${e}`));
106
+ } else {
107
+ writeJsonAtomic(cache, fetched);
108
+ synced = true;
109
+ }
110
+ } catch (error) {
111
+ errors.push(`${name}: could not fetch ${source.url} (${(error as Error).message})`);
112
+ }
113
+ }
114
+ if (existsSync(cache)) manifestPath = cache;
115
+ } else if (source.source === "file") {
116
+ manifestPath = source.path;
117
+ contentRoot = join(source.path, "..", "..");
118
+ } else if (source.source === "directory") {
119
+ contentRoot = source.path;
120
+ manifestPath = join(source.path, ".claude-plugin", "marketplace.json");
121
+ }
122
+
123
+ let manifest: MarketplaceManifest | undefined;
124
+ if (manifestPath && existsSync(manifestPath) && statSync(manifestPath).isFile()) {
125
+ const raw = readJsonFile<unknown>(manifestPath);
126
+ if (raw === undefined) {
127
+ errors.push(`${name}: ${manifestPath} is not valid JSON`);
128
+ } else {
129
+ const validated = parseMarketplaceManifest(raw);
130
+ manifest = validated.manifest;
131
+ errors.push(...validated.errors.map((e) => `${name}: ${e}`));
132
+ }
133
+ } else if (errors.length === 0) {
134
+ errors.push(`${name}: no marketplace.json found${manifestPath ? ` at ${manifestPath}` : ""}`);
135
+ }
136
+
137
+ // Return the post-write entry so callers can update their in-memory
138
+ // registry copy instead of re-reading known_marketplaces.json.
139
+ let updatedKnown = known;
140
+ if (synced) {
141
+ updatedKnown = { ...known, lastUpdated: new Date().toISOString() };
142
+ writeKnownMarketplace(root, name, updatedKnown);
143
+ }
144
+
145
+ return { name, known: updatedKnown, manifest, errors, contentRoot };
146
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Marketplace data shapes + hand-rolled validators (pure).
3
+ *
4
+ * The formats are Claude Code's: `known_marketplaces.json` records where each
5
+ * marketplace came from and where its content is cached; a marketplace's
6
+ * `.claude-plugin/marketplace.json` lists plugin entries whose `source` says
7
+ * how to materialize the plugin's files. Validation is per-entry — one bad
8
+ * entry never blanks the rest of the marketplace.
9
+ */
10
+
11
+ export type MarketplaceSource =
12
+ | { source: "github"; repo: string; ref?: string }
13
+ | { source: "git"; url: string; ref?: string }
14
+ | { source: "url"; url: string }
15
+ | { source: "file"; path: string }
16
+ | { source: "directory"; path: string };
17
+
18
+ export interface KnownMarketplace {
19
+ source: MarketplaceSource;
20
+ installLocation: string;
21
+ lastUpdated: string;
22
+ autoUpdate?: boolean;
23
+ }
24
+
25
+ /** How one plugin's files are obtained. A string is a path relative to the marketplace root. */
26
+ export type PluginEntrySource =
27
+ | string
28
+ | { source: "github"; repo: string; ref?: string; sha?: string }
29
+ | { source: "git-subdir"; url: string; path: string; ref?: string; sha?: string }
30
+ | { source: "url"; url: string; ref?: string; sha?: string };
31
+
32
+ export interface MarketplaceEntry {
33
+ name: string;
34
+ source: PluginEntrySource;
35
+ description?: string;
36
+ version?: string;
37
+ author?: { name?: string };
38
+ category?: string;
39
+ tags?: string[];
40
+ /** The full raw entry — the detail view shows contributions (lspServers, mcpServers, …) from it. */
41
+ raw: Record<string, unknown>;
42
+ }
43
+
44
+ export interface MarketplaceManifest {
45
+ name: string;
46
+ owner?: { name?: string };
47
+ description?: string;
48
+ plugins: MarketplaceEntry[];
49
+ }
50
+
51
+ function isRecord(value: unknown): value is Record<string, unknown> {
52
+ return value !== null && typeof value === "object" && !Array.isArray(value);
53
+ }
54
+
55
+ function validEntrySource(value: unknown): value is PluginEntrySource {
56
+ // A string source is a marketplace-relative path and must say so ("./…").
57
+ if (typeof value === "string") return value.startsWith("./");
58
+ if (!isRecord(value)) return false;
59
+ switch (value.source) {
60
+ case "github":
61
+ return typeof value.repo === "string" && value.repo.length > 0;
62
+ case "git-subdir":
63
+ return typeof value.url === "string" && typeof value.path === "string" && value.path.length > 0;
64
+ case "url":
65
+ return typeof value.url === "string" && value.url.length > 0;
66
+ default:
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /** Validate a parsed marketplace.json. Entry-level problems land in `errors`; good entries survive. */
72
+ export function parseMarketplaceManifest(raw: unknown): { manifest?: MarketplaceManifest; errors: string[] } {
73
+ const errors: string[] = [];
74
+ if (!isRecord(raw)) return { errors: ["marketplace.json must be a JSON object"] };
75
+ if (typeof raw.name !== "string" || raw.name.length === 0) {
76
+ return { errors: ['marketplace.json is missing a "name"'] };
77
+ }
78
+ if (!Array.isArray(raw.plugins)) {
79
+ return { errors: [`marketplace "${raw.name}" has no "plugins" array`] };
80
+ }
81
+
82
+ const plugins: MarketplaceEntry[] = [];
83
+ for (const [index, entry] of raw.plugins.entries()) {
84
+ if (!isRecord(entry) || typeof entry.name !== "string" || entry.name.length === 0 || entry.name.includes(" ")) {
85
+ errors.push(`plugins[${index}]: missing or invalid "name" — entry skipped`);
86
+ continue;
87
+ }
88
+ if (!validEntrySource(entry.source)) {
89
+ errors.push(`plugins[${index}] ("${entry.name}"): missing or unsupported "source" — entry skipped`);
90
+ continue;
91
+ }
92
+ plugins.push({
93
+ name: entry.name,
94
+ source: entry.source as PluginEntrySource,
95
+ description: typeof entry.description === "string" ? entry.description : undefined,
96
+ version: typeof entry.version === "string" ? entry.version : undefined,
97
+ author: isRecord(entry.author) && typeof entry.author.name === "string" ? { name: entry.author.name } : undefined,
98
+ category: typeof entry.category === "string" ? entry.category : undefined,
99
+ tags: Array.isArray(entry.tags) ? entry.tags.filter((t): t is string => typeof t === "string") : undefined,
100
+ raw: entry,
101
+ });
102
+ }
103
+
104
+ const owner = isRecord(raw.owner) && typeof raw.owner.name === "string" ? { name: raw.owner.name } : undefined;
105
+ const metadata = isRecord(raw.metadata) ? raw.metadata : undefined;
106
+ return {
107
+ manifest: {
108
+ name: raw.name,
109
+ owner,
110
+ description: typeof metadata?.description === "string" ? (metadata.description as string) : undefined,
111
+ plugins,
112
+ },
113
+ errors,
114
+ };
115
+ }
116
+
117
+ /** Validate one known_marketplaces.json value; undefined when malformed. */
118
+ export function parseKnownMarketplace(raw: unknown): KnownMarketplace | undefined {
119
+ if (!isRecord(raw) || !isRecord(raw.source) || typeof raw.installLocation !== "string") return undefined;
120
+ const source = raw.source as MarketplaceSource;
121
+ if (!["github", "git", "url", "file", "directory"].includes(source.source)) return undefined;
122
+ return {
123
+ source,
124
+ installLocation: raw.installLocation,
125
+ lastUpdated: typeof raw.lastUpdated === "string" ? raw.lastUpdated : new Date(0).toISOString(),
126
+ autoUpdate: typeof raw.autoUpdate === "boolean" ? raw.autoUpdate : undefined,
127
+ };
128
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Raw-byte key decoding for the /plugins panel (pure).
3
+ *
4
+ * Navigation bytes are always controls; any other printable input is a `text`
5
+ * intent the state layer routes (search box, Add Marketplace draft, or a
6
+ * single-letter action where the active view has no text field). Unrecognized
7
+ * escape sequences are swallowed so they never leak into a draft.
8
+ */
9
+
10
+ export type PanelKey =
11
+ | { kind: "up" | "down" | "pageUp" | "pageDown" | "nextTab" | "prevTab" | "enter" | "back" | "space" | "backspace" | "close" }
12
+ | { kind: "text"; text: string };
13
+
14
+ export function decodePanelKey(data: string): PanelKey | undefined {
15
+ switch (data) {
16
+ case "\x1b[A":
17
+ case "\x1bOA":
18
+ return { kind: "up" };
19
+ case "\x1b[B":
20
+ case "\x1bOB":
21
+ return { kind: "down" };
22
+ case "\x1b[5~":
23
+ return { kind: "pageUp" };
24
+ case "\x1b[6~":
25
+ return { kind: "pageDown" };
26
+ case "\x1b[C":
27
+ case "\x1bOC":
28
+ case "\t":
29
+ return { kind: "nextTab" };
30
+ case "\x1b[D":
31
+ case "\x1bOD":
32
+ case "\x1b[Z":
33
+ return { kind: "prevTab" };
34
+ case "\r":
35
+ case "\n":
36
+ return { kind: "enter" };
37
+ case "\x1b":
38
+ return { kind: "back" };
39
+ case " ":
40
+ return { kind: "space" };
41
+ case "\x7f":
42
+ case "\b":
43
+ return { kind: "backspace" };
44
+ case "\x03": // ctrl+c
45
+ return { kind: "close" };
46
+ default:
47
+ break;
48
+ }
49
+ if (data.startsWith("\x1b")) return undefined;
50
+ const text = [...data].filter((ch) => ch >= " " && ch !== "\x7f").join("");
51
+ return text ? { kind: "text", text } : undefined;
52
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * /plugins panel rendering (pure).
3
+ *
4
+ * Returns plain lines already cut to width; the wiring wraps them in a
5
+ * memoized component and applies a final ANSI-aware truncate (pi-tui crashes
6
+ * on overwide lines, so both layers guard).
7
+ */
8
+
9
+ import { formatInstallCount } from "../counts.ts";
10
+ import { cutPlainText, panelTopRule, type RenderBlock as Block, searchBoxLines, windowBlocks } from "../../lib/tui-render.ts";
11
+ import type { DiscoverRow, InstalledRow } from "./rows.ts";
12
+ import { clampPanelState, type PanelState, type PanelView, resolveDetail, selectableRows, TABS, type Tab } from "./state.ts";
13
+
14
+ export interface PanelPaint {
15
+ fg: (color: string, text: string) => string;
16
+ bold: (text: string) => string;
17
+ inverse: (text: string) => string;
18
+ }
19
+
20
+ /** Detail-view extras the row model doesn't carry (from the marketplace manifest). */
21
+ export interface DiscoverDetail {
22
+ version?: string;
23
+ author?: string;
24
+ category?: string;
25
+ contributions: string[];
26
+ }
27
+
28
+ export interface PanelRenderInput {
29
+ state: PanelState;
30
+ view: PanelView;
31
+ width: number;
32
+ height: number;
33
+ /** Active async operations, e.g. "cloning claude-plugins-official…". */
34
+ loading: string[];
35
+ discoverDetails: Map<string, DiscoverDetail>;
36
+ restartNeeded: boolean;
37
+ }
38
+
39
+ export const TRUST_NOTE =
40
+ "Make sure you trust a plugin before installing or enabling it: plugins can run commands, add MCP servers, and start language servers on your machine. One Code does not review plugin content — check the plugin's homepage and source.";
41
+
42
+ const TAB_TITLES: Record<Tab, string> = {
43
+ discover: "Discover",
44
+ installed: "Installed",
45
+ marketplaces: "Marketplaces",
46
+ errors: "Errors",
47
+ };
48
+
49
+ function tabBar(state: PanelState, view: PanelView, paint: PanelPaint, width: number): string {
50
+ const chips = TABS.map((tab) => {
51
+ const title = tab === "errors" && view.errors.length > 0 ? `${TAB_TITLES[tab]} (${view.errors.length})` : TAB_TITLES[tab];
52
+ return tab === state.tab ? paint.inverse(` ${title} `) : ` ${title} `;
53
+ });
54
+ return ` ${paint.bold("Plugins")} ${chips.join(" ")}`.slice(0, width * 4);
55
+ }
56
+
57
+ function discoverBlocks(input: PanelRenderInput, paint: PanelPaint): Block[] {
58
+ const { state, view, width } = input;
59
+ const cursorRow = selectableRows(state, view)[state.cursor.discover];
60
+ return view.discover.map((row) => {
61
+ const isCursor = row === cursorRow;
62
+ const marker = isCursor ? "❯" : " ";
63
+ const dot = row.busy ? "…" : row.installed ? "●" : "○";
64
+ const installs = row.installs !== undefined ? ` · ${formatInstallCount(row.installs)} installs` : "";
65
+ const head = cutPlainText(`${marker} ${dot} ${row.name} · ${row.marketplace}${installs}`, width - 1);
66
+ const painted = isCursor ? paint.fg("accent", head) : head;
67
+ const lines = [painted];
68
+ if (row.description) lines.push(paint.fg("dim", cutPlainText(` ${row.description}`, width - 1)));
69
+ return { lines, selectable: true };
70
+ });
71
+ }
72
+
73
+ function installedLine(row: Exclude<InstalledRow, { kind: "section" }>, paint: PanelPaint): string {
74
+ const on = paint.fg("success", "✔");
75
+ const off = paint.fg("error", "✘");
76
+ if (row.kind === "plugin") {
77
+ const state = row.busy ? "…" : row.enabled ? `${on} enabled` : `${off} disabled`;
78
+ const override = row.overridden ? " · One Code override" : "";
79
+ const marketplace = row.marketplace ? ` · ${row.marketplace}` : "";
80
+ return `${row.name} Plugin${marketplace} · ${state}${override}`;
81
+ }
82
+ if (row.kind === "mcp") {
83
+ const status =
84
+ row.status === "connected"
85
+ ? `${on} connected${row.toolCount !== undefined ? ` (${row.toolCount} tools)` : ""}`
86
+ : row.status === "authNeeded"
87
+ ? paint.fg("warning", `⚠ needs auth${row.detail ? ` — ${row.detail}` : ""}`)
88
+ : row.status === "connecting"
89
+ ? "… connecting"
90
+ : `${off} failed${row.detail ? ` — ${row.detail}` : ""}`;
91
+ return `${row.name} MCP · ${status}`;
92
+ }
93
+ const toggle = row.enabled ? `${on} on` : `${off} off`;
94
+ return `${row.name} Skill · ${row.scope} · ${toggle} · ~${row.tokens} tok · ${row.recency}`;
95
+ }
96
+
97
+ function installedBlocks(input: PanelRenderInput, paint: PanelPaint): Block[] {
98
+ const { state, view, width } = input;
99
+ const cursorRow = selectableRows(state, view)[state.cursor.installed];
100
+ return view.installed.map((row) => {
101
+ if (row.kind === "section") {
102
+ return { lines: ["", paint.bold(cutPlainText(` ${row.title}`, width - 1))], selectable: false };
103
+ }
104
+ const isCursor = row === cursorRow;
105
+ const marker = isCursor ? "❯ " : " ";
106
+ const line = `${marker}${installedLine(row, paint)}`;
107
+ return { lines: [isCursor ? paint.fg("accent", line) : line], selectable: true };
108
+ });
109
+ }
110
+
111
+ function marketplaceBlocks(input: PanelRenderInput, paint: PanelPaint): Block[] {
112
+ const { state, view, width } = input;
113
+ const cursorRow = selectableRows(state, view)[state.cursor.marketplaces];
114
+ return view.marketplaces.map((row) => {
115
+ const isCursor = row === cursorRow;
116
+ const marker = isCursor ? "❯" : "●";
117
+ if (row.kind === "add") {
118
+ const line = `${isCursor ? "❯" : " "} + Add Marketplace`;
119
+ return { lines: [isCursor ? paint.fg("accent", line) : paint.bold(line), ""], selectable: true };
120
+ }
121
+ const star = row.official ? paint.fg("error", " ✳ ") : " ";
122
+ const name = `${marker}${star}${paint.bold(row.name)}${row.official ? paint.fg("error", " ✳") : ""}`;
123
+ const counts = [
124
+ row.available !== undefined ? `${row.available} available` : "not synced",
125
+ `${row.installedCount} installed`,
126
+ `Updated ${row.updated.slice(0, 10)}`,
127
+ ].join(" • ");
128
+ const lines = [
129
+ isCursor ? paint.fg("accent", cutPlainText(name, width * 2)) : cutPlainText(name, width * 2),
130
+ paint.fg("dim", cutPlainText(` ${row.sourceLine}`, width - 1)),
131
+ paint.fg("dim", cutPlainText(` ${counts}`, width - 1)),
132
+ ];
133
+ if (row.error) lines.push(paint.fg("error", cutPlainText(` ${row.error}`, width - 1)));
134
+ lines.push("");
135
+ return { lines, selectable: true };
136
+ });
137
+ }
138
+
139
+ function addDialogLines(draft: string, paint: PanelPaint, width: number): string[] {
140
+ const inner = Math.max(30, Math.min(width - 4, 78));
141
+ const border = (ch: string) => `┌${ch.repeat(inner)}┐`;
142
+ const boxLine = (text: string, style?: (t: string) => string) => {
143
+ const cut = cutPlainText(text, inner - 2);
144
+ const padded = ` ${cut}${" ".repeat(Math.max(0, inner - 2 - cut.length))} `;
145
+ return `│${style ? style(padded) : padded}│`;
146
+ };
147
+ return [
148
+ border("─"),
149
+ boxLine("Add Marketplace", paint.bold),
150
+ boxLine(""),
151
+ boxLine("Enter marketplace source:"),
152
+ boxLine(draft ? draft : "…", draft ? undefined : (t) => paint.fg("dim", t)),
153
+ boxLine(""),
154
+ boxLine("Examples:", (t) => paint.fg("dim", t)),
155
+ boxLine("· owner/repo (GitHub)", (t) => paint.fg("dim", t)),
156
+ boxLine("· git@github.com:owner/repo.git (SSH)", (t) => paint.fg("dim", t)),
157
+ boxLine("· https://example.com/marketplace.json", (t) => paint.fg("dim", t)),
158
+ boxLine("· ./path/to/marketplace", (t) => paint.fg("dim", t)),
159
+ `└${"─".repeat(inner)}┘`,
160
+ ];
161
+ }
162
+
163
+ function detailLines(input: PanelRenderInput, paint: PanelPaint): { lines: string[]; footer: string } {
164
+ const { state, view, width } = input;
165
+ const ref = state.detail;
166
+ const row = ref ? resolveDetail(ref, view) : undefined;
167
+ if (!ref || !row) return { lines: [paint.fg("dim", " (no longer available)")], footer: "Esc to go back" };
168
+
169
+ const lines: string[] = [];
170
+ const push = (text: string, style?: (t: string) => string) => {
171
+ const cut = cutPlainText(text, width - 2);
172
+ lines.push(style ? ` ${style(cut)}` : ` ${cut}`);
173
+ };
174
+
175
+ if (ref.kind === "discover") {
176
+ const discover = row as DiscoverRow;
177
+ const detail = input.discoverDetails.get(discover.id);
178
+ push(`${discover.name} · ${discover.marketplace}`, paint.bold);
179
+ if (discover.description) push(discover.description);
180
+ push("");
181
+ if (detail?.version) push(`Version: ${detail.version}`, (t) => paint.fg("dim", t));
182
+ if (detail?.author) push(`Author: ${detail.author}`, (t) => paint.fg("dim", t));
183
+ if (detail?.category) push(`Category: ${detail.category}`, (t) => paint.fg("dim", t));
184
+ if (discover.installs !== undefined) push(`${formatInstallCount(discover.installs)} installs`, (t) => paint.fg("dim", t));
185
+ if (detail?.contributions.length) {
186
+ push("");
187
+ push("Provides:", paint.bold);
188
+ for (const item of detail.contributions) push(`· ${item}`);
189
+ }
190
+ push("");
191
+ for (const noteLine of wrapNote(TRUST_NOTE, width - 4)) push(noteLine, (t) => paint.fg("warning", t));
192
+ const footer = discover.busy
193
+ ? "working…"
194
+ : discover.installed
195
+ ? "u to uninstall · Esc to go back"
196
+ : "i to install · Esc to go back";
197
+ return { lines, footer };
198
+ }
199
+
200
+ if ("kind" in row && row.kind === "plugin") {
201
+ push(`${row.name} Plugin${row.marketplace ? ` · ${row.marketplace}` : ""}`, paint.bold);
202
+ push(`Origin: ${row.origin === "one-code" ? "installed by One Code" : "installed by Claude Code (read-only files)"}`);
203
+ push(`State: ${row.enabled ? "enabled" : "disabled"}${row.overridden ? " (One Code override)" : ""}`);
204
+ push("");
205
+ for (const noteLine of wrapNote(TRUST_NOTE, width - 4)) push(noteLine, (t) => paint.fg("warning", t));
206
+ const uninstall = row.origin === "one-code" ? " · u to uninstall" : "";
207
+ const overrideNote = row.origin === "claude" ? " (One Code override — Claude Code settings untouched)" : "";
208
+ return { lines, footer: `Enter/Space/e/d to toggle${overrideNote}${uninstall} · f to favorite · Esc to go back` };
209
+ }
210
+
211
+ if ("kind" in row && row.kind === "skill") {
212
+ push(`${row.name} Skill · ${row.scope}`, paint.bold);
213
+ push(`State: ${row.enabled ? "on" : "off"} · ~${row.tokens} tok · ${row.recency}`);
214
+ return { lines, footer: "Enter/Space/e/d to toggle · f to favorite · Esc to go back" };
215
+ }
216
+
217
+ if ("kind" in row && row.kind === "mcp") {
218
+ push(`${row.name} MCP server`, paint.bold);
219
+ push(`Status: ${row.status}${row.detail ? ` — ${row.detail}` : ""}`);
220
+ if (row.source) push(`Source: ${row.source}`, (t) => paint.fg("dim", t));
221
+ return { lines, footer: "Esc to go back" };
222
+ }
223
+
224
+ return { lines, footer: "Esc to go back" };
225
+ }
226
+
227
+ function wrapNote(text: string, width: number): string[] {
228
+ const words = text.split(" ");
229
+ const lines: string[] = [];
230
+ let current = "";
231
+ for (const word of words) {
232
+ if (current && current.length + word.length + 1 > Math.max(20, width)) {
233
+ lines.push(current);
234
+ current = word;
235
+ } else {
236
+ current = current ? `${current} ${word}` : word;
237
+ }
238
+ }
239
+ if (current) lines.push(current);
240
+ return lines;
241
+ }
242
+
243
+ /** Footer hints per tab; a second line keeps the Installed hints readable at 80 columns. */
244
+ const FOOTERS: Record<Tab, string[]> = {
245
+ discover: ["Type to search · Space to install/uninstall · Enter to view · ←/→ tabs · Esc to close"],
246
+ installed: ["Enter/Space to toggle · v to view · f to favorite · u to uninstall", "/ to search · ←/→ tabs · Esc to close"],
247
+ marketplaces: ["Enter to select · a to add · u to update · d to remove · Esc to close"],
248
+ errors: ["←/→ tabs · Esc to close"],
249
+ };
250
+ /** Installed search hints while the `/` box is open (Esc leaves it). */
251
+ const INSTALLED_SEARCH_FOOTER = ["Type to search · Enter/Space to toggle · Esc to leave search"];
252
+
253
+ export function renderPanel(input: PanelRenderInput, paint: PanelPaint): string[] {
254
+ const { state, view, width, height } = input;
255
+ clampPanelState(state, view);
256
+
257
+ const out: string[] = [panelTopRule(paint.fg, width), tabBar(state, view, paint, width), ""];
258
+ let footer: string[];
259
+
260
+ if (state.addDialog) {
261
+ out.push(...addDialogLines(state.addDialog.draft, paint, width));
262
+ footer = ["Enter to add · Esc to cancel"];
263
+ } else if (state.detail) {
264
+ const detail = detailLines(input, paint);
265
+ out.push(...detail.lines);
266
+ footer = [detail.footer];
267
+ } else {
268
+ let blocks: Block[];
269
+ switch (state.tab) {
270
+ case "discover": {
271
+ const total = view.discover.length;
272
+ const position = total === 0 ? 0 : state.cursor.discover + 1;
273
+ out.push(paint.bold(cutPlainText(` Discover plugins (${position}/${total})`, width - 1)), "");
274
+ out.push(...searchBoxLines(state.search.discover, "Search…", paint.fg, width), "");
275
+ blocks = discoverBlocks(input, paint);
276
+ break;
277
+ }
278
+ case "installed": {
279
+ const placeholder = state.installedSearching ? "Search…" : "Press / to search…";
280
+ out.push(...searchBoxLines(state.search.installed, placeholder, paint.fg, width));
281
+ blocks = installedBlocks(input, paint);
282
+ break;
283
+ }
284
+ case "marketplaces": {
285
+ out.push(paint.bold(" Manage marketplaces"), "");
286
+ blocks = marketplaceBlocks(input, paint);
287
+ break;
288
+ }
289
+ case "errors": {
290
+ blocks = view.errors.length
291
+ ? view.errors.map((error) => ({ lines: [cutPlainText(` ${error}`, width - 1)], selectable: true }))
292
+ : [{ lines: [paint.fg("dim", " No plugin errors")], selectable: false }];
293
+ break;
294
+ }
295
+ }
296
+ footer = state.tab === "installed" && state.installedSearching ? INSTALLED_SEARCH_FOOTER : FOOTERS[state.tab];
297
+ const budget = Math.max(3, height - out.length - 2 - footer.length);
298
+ const windowed = windowBlocks(blocks, state.cursor[state.tab], budget);
299
+ out.push(...windowed.lines);
300
+ if (windowed.more > 0) out.push(paint.fg("dim", ` ↓ more below (${windowed.more})`));
301
+ }
302
+
303
+ for (const loading of input.loading) out.push(paint.fg("dim", cutPlainText(` ${loading}`, width - 1)));
304
+ if (state.notice) out.push(paint.fg("warning", cutPlainText(` ${state.notice}`, width - 1)));
305
+ if (input.restartNeeded) {
306
+ out.push(
307
+ paint.fg(
308
+ "warning",
309
+ cutPlainText(" Some changes (MCP servers, agents, hooks) apply on the next session restart.", width - 1),
310
+ ),
311
+ );
312
+ }
313
+ out.push("", ...footer.map((line) => paint.fg("dim", cutPlainText(` ${line}`, width - 1))));
314
+ return out;
315
+ }