one-code-extension 0.1.4 → 0.3.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 (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +200 -172
  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 +126 -0
  8. package/docs/guide/configuration.md +171 -0
  9. package/docs/guide/differences-from-claude-code.md +150 -0
  10. package/docs/guide/doctor.md +86 -0
  11. package/docs/guide/hooks.md +109 -0
  12. package/docs/guide/installation.md +124 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
  14. package/docs/guide/providers-and-models.md +194 -0
  15. package/docs/guide/reference.md +224 -0
  16. package/docs/guide/sessions-and-context.md +190 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +176 -0
  18. package/docs/guide/subagents-and-workflows.md +193 -0
  19. package/docs/guide/tasks-and-background-work.md +111 -0
  20. package/docs/guide/terminal-interface.md +155 -0
  21. package/docs/guide/tools.md +175 -0
  22. package/docs/guide/troubleshooting.md +175 -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,21 @@
1
+ /**
2
+ * The model-facing skills listing (pure). The state decides visibility: "on"
3
+ * carries name + description, "name-only" just the name (saving context
4
+ * tokens), "user-only"/"off" are hidden so the model won't auto-trigger them.
5
+ * The description goes in whole, newlines included, the way Claude Code lists
6
+ * it: a skill written with a YAML block description puts its "Use when…"
7
+ * trigger on the later lines, and a first-line-only listing dropped exactly
8
+ * the sentence that tells the model when to reach for the skill.
9
+ */
10
+
11
+ import { type SkillState, skillListingVisibility } from "../lib/skill-overrides.ts";
12
+
13
+ export function skillListingText(skills: ReadonlyArray<{ name: string; description?: string; state: SkillState }>): string {
14
+ const lines = skills.flatMap((skill) => {
15
+ const visibility = skillListingVisibility(skill.state);
16
+ if (visibility === "hidden") return [];
17
+ if (visibility === "name") return [`- ${skill.name}`];
18
+ return [`- ${skill.name}${skill.description?.trim() ? `: ${skill.description.trim()}` : ""}`];
19
+ });
20
+ return lines.length === 0 ? "(no skills available)" : lines.join("\n");
21
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Raw-byte key decoding for the /skills panel (pure).
3
+ *
4
+ * Same control-byte map as the /plugins panel; kept local so the skill
5
+ * extension carries no dependency on another extension's internals. Printable
6
+ * input is a `text` intent the state layer routes (into the search box, or as a
7
+ * single-letter action when search is inactive). Skill names never contain
8
+ * spaces, so Space is always the cycle action, never typed.
9
+ */
10
+
11
+ export type SkillsKey =
12
+ | { kind: "up" | "down" | "enter" | "space" | "back" | "backspace" | "close" }
13
+ | { kind: "text"; text: string };
14
+
15
+ export function decodeSkillsKey(data: string): SkillsKey | undefined {
16
+ switch (data) {
17
+ case "\x1b[A":
18
+ case "\x1bOA":
19
+ return { kind: "up" };
20
+ case "\x1b[B":
21
+ case "\x1bOB":
22
+ return { kind: "down" };
23
+ case "\r":
24
+ case "\n":
25
+ return { kind: "enter" };
26
+ case " ":
27
+ return { kind: "space" };
28
+ case "\x1b":
29
+ return { kind: "back" };
30
+ case "\x7f":
31
+ case "\b":
32
+ return { kind: "backspace" };
33
+ case "\x03":
34
+ return { kind: "close" };
35
+ default:
36
+ break;
37
+ }
38
+ if (data.startsWith("\x1b")) return undefined;
39
+ const text = [...data].filter((ch) => ch >= " " && ch !== "\x7f").join("");
40
+ return text ? { kind: "text", text } : undefined;
41
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * /skills 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. Mirrors Claude
6
+ * Code's Skills menu: one row per skill, a state glyph, token estimate, and
7
+ * scope, with plugin skills shown locked (managed via /plugins). Only width-1
8
+ * glyphs are used — cutPlainText counts code points, so a wide emoji would
9
+ * under-measure and risk pi's overwide-line crash.
10
+ */
11
+
12
+ import { cutPlainText, padPlainText, panelTopRule, searchBoxLines, visibleWidth } from "../../lib/tui-render.ts";
13
+ import type { SkillState } from "../../lib/skill-overrides.ts";
14
+ import { clampSkillsCursor, type SkillsPanelState, type SkillsRow, visibleRows } from "./state.ts";
15
+
16
+ export interface SkillsPaint {
17
+ fg: (color: string, text: string) => string;
18
+ bold: (text: string) => string;
19
+ }
20
+
21
+ export interface SkillsRenderInput {
22
+ state: SkillsPanelState;
23
+ rows: SkillsRow[];
24
+ width: number;
25
+ height: number;
26
+ notice?: string;
27
+ }
28
+
29
+ const GLYPH: Record<SkillState, string> = { on: "✔", "name-only": "●", "user-only": "○", off: "✘" };
30
+ const LABEL: Record<SkillState, string> = { on: "on", "name-only": "name-only", "user-only": "user-only", off: "off" };
31
+ const COLOR: Record<SkillState, string> = { on: "success", "name-only": "accent", "user-only": "warning", off: "error" };
32
+ /** Width of the state column, sized to the longest label ("● name-only"). */
33
+ const STATE_WIDTH = 12;
34
+
35
+ function rowLine(row: SkillsRow, isCursor: boolean, paint: SkillsPaint, width: number): string {
36
+ const marker = isCursor ? "❯ " : " ";
37
+ const suffix = row.locked ? ` · ${row.pluginName ?? "plugin"} · locked` : "";
38
+ const meta = ` · ${row.scope} · ~${row.tokens} tok${suffix}`;
39
+ const label = padPlainText(`${GLYPH[row.state]} ${LABEL[row.state]}`, STATE_WIDTH);
40
+ const plainHead = `${marker}${label}${row.name}`;
41
+ // One flat highlight for the cursor row, like the /plugins panel.
42
+ if (isCursor) return paint.fg("accent", cutPlainText(plainHead + meta, width - 1));
43
+ const color = row.locked ? "dim" : COLOR[row.state];
44
+ // Cut the meta on the visible-length budget left after the head, then paint.
45
+ const remaining = Math.max(0, width - 1 - visibleWidth(plainHead));
46
+ return `${marker}${paint.fg(color, label)}${row.name}${paint.fg("dim", cutPlainText(meta, remaining))}`;
47
+ }
48
+
49
+ /** Slide a window over the rows so the cursor stays visible within `budget` lines. */
50
+ function window<T>(items: T[], cursor: number, budget: number): { slice: T[]; start: number; more: number } {
51
+ if (items.length <= budget) return { slice: items, start: 0, more: 0 };
52
+ let start = Math.min(cursor, items.length - budget);
53
+ if (cursor < start) start = cursor;
54
+ if (cursor >= start + budget) start = cursor - budget + 1;
55
+ start = Math.max(0, start);
56
+ return { slice: items.slice(start, start + budget), start, more: items.length - (start + budget) };
57
+ }
58
+
59
+ export function renderSkillsPanel(input: SkillsRenderInput, paint: SkillsPaint): string[] {
60
+ const { state, rows, width, height } = input;
61
+ const visible = visibleRows(rows, state);
62
+ clampSkillsCursor(state, visible);
63
+
64
+ const out: string[] = [panelTopRule(paint.fg, width)];
65
+ out.push(` ${paint.bold("Skills")}`);
66
+ const sortNote = state.sort === "state" ? "sorted by state" : "sorted by name";
67
+ out.push(paint.fg("dim", cutPlainText(` ${rows.length} skill${rows.length === 1 ? "" : "s"} · ${sortNote} · enter/space to cycle, / to search, t to sort, Esc to close`, width - 1)));
68
+ out.push("");
69
+ out.push(...searchBoxLines(state.search, "Search skills…", paint.fg, width));
70
+ out.push("");
71
+
72
+ // Reserve: header(2) + blank + search + blank already pushed = out.length;
73
+ // leave room for the "more" line, notice, blank, and footer note.
74
+ const footerReserve = 3 + (input.notice ? 1 : 0);
75
+ const budget = Math.max(1, height - out.length - footerReserve);
76
+
77
+ if (visible.length === 0) {
78
+ out.push(paint.fg("dim", state.search ? " No skills match your search." : " No skills found."));
79
+ } else {
80
+ const win = window(visible, state.cursor, budget);
81
+ for (let i = 0; i < win.slice.length; i++) {
82
+ out.push(rowLine(win.slice[i], win.start + i === state.cursor, paint, width));
83
+ }
84
+ if (win.more > 0) out.push(paint.fg("dim", ` ↓ ${win.more} more`));
85
+ }
86
+
87
+ if (input.notice) out.push(paint.fg("warning", cutPlainText(` ${input.notice}`, width - 1)));
88
+ out.push("");
89
+ out.push(paint.fg("dim", cutPlainText(" Plugin skills are managed via /plugins", width - 1)));
90
+ return out;
91
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * /skills panel state machine (pure — no pi, no fs).
3
+ *
4
+ * The wiring (skill/index.ts) builds the row list from disk each repaint and
5
+ * owns persistence; this module owns cursor/search/sort and turns keys into
6
+ * effects. Filtering and sorting are pure functions over the row list so the
7
+ * renderer and the key handler agree on what "the selected row" is.
8
+ */
9
+
10
+ import type { SkillScope, SkillState } from "../../lib/skill-overrides.ts";
11
+ import type { SkillsKey } from "./keys.ts";
12
+
13
+ export interface SkillsRow {
14
+ /** Override-store key, `<scope>:<name>`. */
15
+ key: string;
16
+ name: string;
17
+ scope: SkillScope;
18
+ tokens: number;
19
+ /** Current state; for locked (plugin) rows this is the effective on/off. */
20
+ state: SkillState;
21
+ /** Plugin/MCP skills can't be cycled here — managed via /plugins. */
22
+ locked: boolean;
23
+ pluginName?: string;
24
+ }
25
+
26
+ export type SkillsSort = "name" | "state";
27
+
28
+ export interface SkillsPanelState {
29
+ cursor: number;
30
+ search: string;
31
+ searching: boolean;
32
+ sort: SkillsSort;
33
+ }
34
+
35
+ export type SkillsEffect = { kind: "close" } | { kind: "cycle"; row: SkillsRow } | { kind: "locked"; row: SkillsRow };
36
+
37
+ export function initialSkillsState(): SkillsPanelState {
38
+ return { cursor: 0, search: "", searching: false, sort: "name" };
39
+ }
40
+
41
+ /** State-sort order groups by availability, most-available first, locked last. */
42
+ const STATE_ORDER: Record<SkillState, number> = { on: 0, "name-only": 1, "user-only": 2, off: 3 };
43
+
44
+ export function visibleRows(rows: SkillsRow[], state: SkillsPanelState): SkillsRow[] {
45
+ const query = state.search.trim().toLowerCase();
46
+ const filtered = query ? rows.filter((row) => row.name.toLowerCase().includes(query)) : rows.slice();
47
+ const rank = (row: SkillsRow) => (row.locked ? 4 : STATE_ORDER[row.state]);
48
+ filtered.sort((a, b) => {
49
+ if (state.sort === "state") {
50
+ const diff = rank(a) - rank(b);
51
+ if (diff !== 0) return diff;
52
+ }
53
+ return a.name.localeCompare(b.name);
54
+ });
55
+ return filtered;
56
+ }
57
+
58
+ export function clampSkillsCursor(state: SkillsPanelState, visible: SkillsRow[]): void {
59
+ if (visible.length === 0) {
60
+ state.cursor = 0;
61
+ return;
62
+ }
63
+ state.cursor = Math.max(0, Math.min(state.cursor, visible.length - 1));
64
+ }
65
+
66
+ /**
67
+ * Fold a key into the state, returning an effect the wiring must act on. The
68
+ * caller passes the already-filtered/sorted rows so "the selected row" matches
69
+ * what the renderer drew.
70
+ */
71
+ export function applySkillsKey(state: SkillsPanelState, key: SkillsKey, visible: SkillsRow[]): SkillsEffect | undefined {
72
+ clampSkillsCursor(state, visible);
73
+ switch (key.kind) {
74
+ case "close":
75
+ return { kind: "close" };
76
+ case "up":
77
+ state.cursor = Math.max(0, state.cursor - 1);
78
+ return;
79
+ case "down":
80
+ state.cursor = Math.min(Math.max(0, visible.length - 1), state.cursor + 1);
81
+ return;
82
+ case "space":
83
+ case "enter": {
84
+ const row = visible[state.cursor];
85
+ if (!row) return;
86
+ return row.locked ? { kind: "locked", row } : { kind: "cycle", row };
87
+ }
88
+ case "backspace":
89
+ if (state.searching) state.search = state.search.slice(0, -1);
90
+ return;
91
+ case "back":
92
+ // Esc leaves search first, then closes the panel.
93
+ if (state.searching || state.search) {
94
+ state.searching = false;
95
+ state.search = "";
96
+ return;
97
+ }
98
+ return { kind: "close" };
99
+ case "text": {
100
+ if (!state.searching) {
101
+ if (key.text === "/") {
102
+ state.searching = true;
103
+ return;
104
+ }
105
+ if (key.text === "t") {
106
+ state.sort = state.sort === "name" ? "state" : "name";
107
+ return;
108
+ }
109
+ return;
110
+ }
111
+ state.search += key.text;
112
+ return;
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * spinner extension — Claude Code's live-activity chrome around the prompt:
3
+ *
4
+ * - The working line: pi's spinner becomes CC's animated asterisk (✳ frame
5
+ * cycle) with a random gerund verb per turn and live stats — "Hyperspacing…
6
+ * (10s · ↓ 448 tokens)" — updated on a 1s ticker while the model streams.
7
+ * Tokens are CC's estimate (streamed characters / 4).
8
+ *
9
+ * Context usage is shown by the footer extension, not here (the two once
10
+ * duplicated an above-input token counter).
11
+ *
12
+ * Matched to a frame capture of CC 2.1.233. CC's glimmer animation and
13
+ * rotating tip lines are deliberately not replicated (pi renders the working
14
+ * message as plain text, and tips carry CC's own frequency machinery).
15
+ */
16
+
17
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
18
+ import { safeThemePaint } from "../lib/tui-render.ts";
19
+ import { composeWorkingMessage, messageChars, spinnerFrames } from "./line.ts";
20
+ import { pickVerb } from "./verbs.ts";
21
+
22
+ const FRAME_INTERVAL_MS = 120;
23
+ const TICK_MS = 1000;
24
+
25
+ export default function spinnerExtension(pi: ExtensionAPI) {
26
+ let lastCtx: ExtensionContext | undefined;
27
+ let verb = pickVerb();
28
+ let turnStartedAt = 0;
29
+ /** Characters from finished assistant messages this turn. */
30
+ let settledChars = 0;
31
+ /**
32
+ * The streaming message, summed lazily on the 1s ticker — message_update
33
+ * fires per token carrying the whole message-so-far, so counting there
34
+ * would be O(n²) over a long response.
35
+ */
36
+ let streamingMessage: unknown;
37
+ let ticker: ReturnType<typeof setInterval> | undefined;
38
+
39
+ const updateWorkingMessage = () => {
40
+ const ctx = lastCtx;
41
+ if (!ctx?.hasUI || !turnStartedAt) return;
42
+ ctx.ui.setWorkingMessage(
43
+ composeWorkingMessage({
44
+ verb,
45
+ elapsedMs: Date.now() - turnStartedAt,
46
+ responseChars: settledChars + messageChars(streamingMessage),
47
+ }),
48
+ );
49
+ };
50
+
51
+ const stopTicker = () => {
52
+ if (ticker) clearInterval(ticker);
53
+ ticker = undefined;
54
+ };
55
+
56
+ pi.on("session_start", (_event, ctx) => {
57
+ lastCtx = ctx;
58
+ if (!ctx.hasUI) return;
59
+ const paint = safeThemePaint(ctx.ui.theme);
60
+ ctx.ui.setWorkingIndicator({
61
+ frames: spinnerFrames(process.platform, process.env.TERM).map((frame) => paint("accent", frame)),
62
+ intervalMs: FRAME_INTERVAL_MS,
63
+ });
64
+ });
65
+
66
+ pi.on("agent_start", (_event, ctx) => {
67
+ lastCtx = ctx;
68
+ // A turn can hold several runs (provider retry, auto-compaction, queued
69
+ // follow-up — findings §3): a later run joins the open span, keeping the
70
+ // first run's start and verb, so the elapsed shown is the time the user
71
+ // has actually waited (the live counterpart of turn-duration's TurnSpan).
72
+ if (!turnStartedAt) {
73
+ verb = pickVerb();
74
+ turnStartedAt = Date.now();
75
+ settledChars = 0;
76
+ }
77
+ streamingMessage = undefined;
78
+ updateWorkingMessage();
79
+ if (!ticker) {
80
+ ticker = setInterval(updateWorkingMessage, TICK_MS);
81
+ ticker.unref?.();
82
+ }
83
+ });
84
+
85
+ pi.on("message_update", (event) => {
86
+ if (event.message.role !== "assistant") return;
87
+ streamingMessage = event.message;
88
+ });
89
+
90
+ pi.on("message_end", (event) => {
91
+ if (event.message.role === "assistant") {
92
+ settledChars += messageChars(event.message);
93
+ streamingMessage = undefined;
94
+ }
95
+ });
96
+
97
+ // Settle, not agent_end: `agent_end` fires per run, and stopping there made
98
+ // the counter restart from zero on a provider retry. The ticker runs through
99
+ // the backoff so the elapsed keeps counting, and stops (for any outcome,
100
+ // unlike the gated duration line) only when pi will not continue by itself.
101
+ pi.on("agent_settled", (_event, ctx) => {
102
+ stopTicker();
103
+ turnStartedAt = 0;
104
+ if (ctx.hasUI) ctx.ui.setWorkingMessage(); // restore pi's default for non-turn work
105
+ });
106
+
107
+ pi.on("session_shutdown", () => {
108
+ stopTicker();
109
+ });
110
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Pure pieces of the Claude Code working line ("✳ Hyperspacing… (10s · ↓ 448
3
+ * tokens)") — formats matched to a frame capture of CC 2.1.233 plus its
4
+ * reconstructed Spinner source.
5
+ */
6
+
7
+ import { formatDuration } from "../lib/tui-render.ts";
8
+ import { formatTokenCount } from "../subagents/usage.ts";
9
+
10
+ /**
11
+ * CC's spinner glyph frames (Spinner/utils.ts), mirrored forward-then-back.
12
+ * Ghostty gets `*` for ✽ (renders offset there); non-mac gets `*` for ✳.
13
+ */
14
+ export function spinnerFrames(platform: string, term: string | undefined): string[] {
15
+ const chars =
16
+ term === "xterm-ghostty"
17
+ ? ["·", "✢", "✳", "✶", "✻", "*"]
18
+ : platform === "darwin"
19
+ ? ["·", "✢", "✳", "✶", "✻", "✽"]
20
+ : ["·", "✢", "*", "✶", "✻", "✽"];
21
+ return [...chars, ...[...chars].reverse()];
22
+ }
23
+
24
+ /** CC estimates streamed tokens as response characters / 4. */
25
+ export function estimateTokens(responseChars: number): number {
26
+ return Math.round(responseChars / 4);
27
+ }
28
+
29
+ /**
30
+ * `Verb… (elapsed · ↓ N tokens)` — the token part appears once anything has
31
+ * streamed, formatted compactly the way CC's formatNumber does (448, 1.3k).
32
+ */
33
+ export function composeWorkingMessage(input: { verb: string; elapsedMs: number; responseChars: number }): string {
34
+ const parts = [formatDuration(0, input.elapsedMs)];
35
+ const tokens = estimateTokens(input.responseChars);
36
+ if (tokens > 0) parts.push(`↓ ${formatTokenCount(tokens)} tokens`);
37
+ return `${input.verb}… (${parts.join(" · ")})`;
38
+ }
39
+
40
+ /** Character count of an assistant message's streamed text + thinking blocks. */
41
+ export function messageChars(message: unknown): number {
42
+ const content = (message as { content?: unknown })?.content;
43
+ if (!Array.isArray(content)) return 0;
44
+ let total = 0;
45
+ for (const block of content) {
46
+ const b = block as { type?: string; text?: string; thinking?: string };
47
+ if (typeof b?.text === "string") total += b.text.length;
48
+ if (typeof b?.thinking === "string") total += b.thinking.length;
49
+ }
50
+ return total;
51
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Claude Code's spinner verbs, verbatim from the reconstructed source
3
+ * (src/constants/spinnerVerbs.ts) — the whimsical gerund shown while the
4
+ * model works ("✳ Hyperspacing…"). One is picked at random per turn.
5
+ */
6
+
7
+ export const SPINNER_VERBS: readonly string[] = [
8
+ "Accomplishing",
9
+ "Actioning",
10
+ "Actualizing",
11
+ "Architecting",
12
+ "Baking",
13
+ "Beaming",
14
+ "Beboppin'",
15
+ "Befuddling",
16
+ "Billowing",
17
+ "Blanching",
18
+ "Bloviating",
19
+ "Boogieing",
20
+ "Boondoggling",
21
+ "Booping",
22
+ "Bootstrapping",
23
+ "Brewing",
24
+ "Bunning",
25
+ "Burrowing",
26
+ "Calculating",
27
+ "Canoodling",
28
+ "Caramelizing",
29
+ "Cascading",
30
+ "Catapulting",
31
+ "Cerebrating",
32
+ "Channeling",
33
+ "Channelling",
34
+ "Choreographing",
35
+ "Churning",
36
+ "Clauding",
37
+ "Coalescing",
38
+ "Cogitating",
39
+ "Combobulating",
40
+ "Composing",
41
+ "Computing",
42
+ "Concocting",
43
+ "Considering",
44
+ "Contemplating",
45
+ "Cooking",
46
+ "Crafting",
47
+ "Creating",
48
+ "Crunching",
49
+ "Crystallizing",
50
+ "Cultivating",
51
+ "Deciphering",
52
+ "Deliberating",
53
+ "Determining",
54
+ "Dilly-dallying",
55
+ "Discombobulating",
56
+ "Doing",
57
+ "Doodling",
58
+ "Drizzling",
59
+ "Ebbing",
60
+ "Effecting",
61
+ "Elucidating",
62
+ "Embellishing",
63
+ "Enchanting",
64
+ "Envisioning",
65
+ "Evaporating",
66
+ "Fermenting",
67
+ "Fiddle-faddling",
68
+ "Finagling",
69
+ "Flambéing",
70
+ "Flibbertigibbeting",
71
+ "Flowing",
72
+ "Flummoxing",
73
+ "Fluttering",
74
+ "Forging",
75
+ "Forming",
76
+ "Frolicking",
77
+ "Frosting",
78
+ "Gallivanting",
79
+ "Galloping",
80
+ "Garnishing",
81
+ "Generating",
82
+ "Gesticulating",
83
+ "Germinating",
84
+ "Gitifying",
85
+ "Grooving",
86
+ "Gusting",
87
+ "Harmonizing",
88
+ "Hashing",
89
+ "Hatching",
90
+ "Herding",
91
+ "Honking",
92
+ "Hullaballooing",
93
+ "Hyperspacing",
94
+ "Ideating",
95
+ "Imagining",
96
+ "Improvising",
97
+ "Incubating",
98
+ "Inferring",
99
+ "Infusing",
100
+ "Ionizing",
101
+ "Jitterbugging",
102
+ "Julienning",
103
+ "Kneading",
104
+ "Leavening",
105
+ "Levitating",
106
+ "Lollygagging",
107
+ "Manifesting",
108
+ "Marinating",
109
+ "Meandering",
110
+ "Metamorphosing",
111
+ "Misting",
112
+ "Moonwalking",
113
+ "Moseying",
114
+ "Mulling",
115
+ "Mustering",
116
+ "Musing",
117
+ "Nebulizing",
118
+ "Nesting",
119
+ "Newspapering",
120
+ "Noodling",
121
+ "Nucleating",
122
+ "Orbiting",
123
+ "Orchestrating",
124
+ "Osmosing",
125
+ "Perambulating",
126
+ "Percolating",
127
+ "Perusing",
128
+ "Philosophising",
129
+ "Photosynthesizing",
130
+ "Pollinating",
131
+ "Pondering",
132
+ "Pontificating",
133
+ "Pouncing",
134
+ "Precipitating",
135
+ "Prestidigitating",
136
+ "Processing",
137
+ "Proofing",
138
+ "Propagating",
139
+ "Puttering",
140
+ "Puzzling",
141
+ "Quantumizing",
142
+ "Razzle-dazzling",
143
+ "Razzmatazzing",
144
+ "Recombobulating",
145
+ "Reticulating",
146
+ "Roosting",
147
+ "Ruminating",
148
+ "Sautéing",
149
+ "Scampering",
150
+ "Schlepping",
151
+ "Scurrying",
152
+ "Seasoning",
153
+ "Shenaniganing",
154
+ "Shimmying",
155
+ "Simmering",
156
+ "Skedaddling",
157
+ "Sketching",
158
+ "Slithering",
159
+ "Smooshing",
160
+ "Sock-hopping",
161
+ "Spelunking",
162
+ "Spinning",
163
+ "Sprouting",
164
+ "Stewing",
165
+ "Sublimating",
166
+ "Swirling",
167
+ "Swooping",
168
+ "Symbioting",
169
+ "Synthesizing",
170
+ "Tempering",
171
+ "Thinking",
172
+ "Thundering",
173
+ "Tinkering",
174
+ "Tomfoolering",
175
+ "Topsy-turvying",
176
+ "Transfiguring",
177
+ "Transmuting",
178
+ "Twisting",
179
+ "Undulating",
180
+ "Unfurling",
181
+ "Unravelling",
182
+ "Vibing",
183
+ "Waddling",
184
+ "Wandering",
185
+ "Warping",
186
+ "Whatchamacalliting",
187
+ "Whirlpooling",
188
+ "Whirring",
189
+ "Whisking",
190
+ "Wibbling",
191
+ "Working",
192
+ "Wrangling",
193
+ "Zesting",
194
+ "Zigzagging",
195
+ ];
196
+
197
+ /** Random verb for one turn; `random` is injectable for tests. */
198
+ export function pickVerb(random: () => number = Math.random): string {
199
+ const index = Math.min(SPINNER_VERBS.length - 1, Math.floor(random() * SPINNER_VERBS.length));
200
+ return SPINNER_VERBS[index];
201
+ }