@ashx-j/lunr 0.2.8 → 0.2.9

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 (127) hide show
  1. package/dist/builtin-extensions/ashxj-spinners.d.ts +7 -0
  2. package/dist/builtin-extensions/ashxj-spinners.d.ts.map +1 -1
  3. package/dist/builtin-extensions/ashxj-spinners.js +40 -3
  4. package/dist/builtin-extensions/ashxj-spinners.js.map +1 -1
  5. package/dist/builtin-extensions/ashxj-tui.d.ts +4 -0
  6. package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
  7. package/dist/builtin-extensions/ashxj-tui.js +44 -0
  8. package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
  9. package/dist/builtin-extensions/lunr-behavior.d.ts +2 -1
  10. package/dist/builtin-extensions/lunr-behavior.d.ts.map +1 -1
  11. package/dist/builtin-extensions/lunr-behavior.js +26 -14
  12. package/dist/builtin-extensions/lunr-behavior.js.map +1 -1
  13. package/dist/builtin-extensions/lunr-todos.d.ts +4 -7
  14. package/dist/builtin-extensions/lunr-todos.d.ts.map +1 -1
  15. package/dist/builtin-extensions/lunr-todos.js +18 -10
  16. package/dist/builtin-extensions/lunr-todos.js.map +1 -1
  17. package/dist/builtin-extensions/pi-subagents/src/agents/agent-management.d.ts.map +1 -1
  18. package/dist/builtin-extensions/pi-subagents/src/agents/agent-management.js +3 -3
  19. package/dist/builtin-extensions/pi-subagents/src/agents/agent-management.js.map +1 -1
  20. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts.map +1 -1
  21. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js +2 -2
  22. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js.map +1 -1
  23. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts +2 -2
  24. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts.map +1 -1
  25. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js +3 -4
  26. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
  27. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  28. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +6 -18
  29. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js.map +1 -1
  30. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts.map +1 -1
  31. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js +13 -5
  32. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js.map +1 -1
  33. package/dist/builtin-extensions/pi-subagents/src/shared/lunr-child-context.d.ts +21 -0
  34. package/dist/builtin-extensions/pi-subagents/src/shared/lunr-child-context.d.ts.map +1 -0
  35. package/dist/builtin-extensions/pi-subagents/src/shared/lunr-child-context.js +22 -0
  36. package/dist/builtin-extensions/pi-subagents/src/shared/lunr-child-context.js.map +1 -0
  37. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.d.ts.map +1 -1
  38. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.js +2 -1
  39. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.js.map +1 -1
  40. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts.map +1 -1
  41. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js +10 -18
  42. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js.map +1 -1
  43. package/dist/builtin-extensions/pi-web-access/render-search-error.d.ts.map +1 -1
  44. package/dist/builtin-extensions/pi-web-access/render-search-error.js +1 -1
  45. package/dist/builtin-extensions/pi-web-access/render-search-error.js.map +1 -1
  46. package/dist/catalog/models.json +1 -1
  47. package/dist/catalog/providers/openrouter.json +1 -1
  48. package/dist/catalog/providers/vercel-ai-gateway.json +1 -1
  49. package/dist/catalog/publication.json +3 -3
  50. package/dist/cli/update-cli.d.ts +16 -0
  51. package/dist/cli/update-cli.d.ts.map +1 -0
  52. package/dist/cli/update-cli.js +83 -0
  53. package/dist/cli/update-cli.js.map +1 -0
  54. package/dist/cli.d.ts.map +1 -1
  55. package/dist/cli.js +6 -0
  56. package/dist/cli.js.map +1 -1
  57. package/dist/core/behavior-preset.d.ts +60 -0
  58. package/dist/core/behavior-preset.d.ts.map +1 -0
  59. package/dist/core/behavior-preset.js +257 -0
  60. package/dist/core/behavior-preset.js.map +1 -0
  61. package/dist/core/customize.d.ts +3 -0
  62. package/dist/core/customize.d.ts.map +1 -1
  63. package/dist/core/customize.js +9 -0
  64. package/dist/core/customize.js.map +1 -1
  65. package/dist/core/footer-data-provider.d.ts +12 -1
  66. package/dist/core/footer-data-provider.d.ts.map +1 -1
  67. package/dist/core/footer-data-provider.js +70 -5
  68. package/dist/core/footer-data-provider.js.map +1 -1
  69. package/dist/core/keybindings.d.ts +2 -2
  70. package/dist/core/keybindings.d.ts.map +1 -1
  71. package/dist/core/keybindings.js +1 -1
  72. package/dist/core/keybindings.js.map +1 -1
  73. package/dist/core/model-tiers.d.ts +9 -0
  74. package/dist/core/model-tiers.d.ts.map +1 -1
  75. package/dist/core/model-tiers.js +16 -0
  76. package/dist/core/model-tiers.js.map +1 -1
  77. package/dist/core/settings-manager.d.ts +23 -0
  78. package/dist/core/settings-manager.d.ts.map +1 -1
  79. package/dist/core/settings-manager.js +66 -0
  80. package/dist/core/settings-manager.js.map +1 -1
  81. package/dist/core/tools/bash.d.ts.map +1 -1
  82. package/dist/core/tools/bash.js +1 -3
  83. package/dist/core/tools/bash.js.map +1 -1
  84. package/dist/core/update-check.d.ts +32 -0
  85. package/dist/core/update-check.d.ts.map +1 -0
  86. package/dist/core/update-check.js +112 -0
  87. package/dist/core/update-check.js.map +1 -0
  88. package/dist/core/usage-service.d.ts +24 -1
  89. package/dist/core/usage-service.d.ts.map +1 -1
  90. package/dist/core/usage-service.js +91 -14
  91. package/dist/core/usage-service.js.map +1 -1
  92. package/dist/gateway/agent-bridge.d.ts.map +1 -1
  93. package/dist/gateway/agent-bridge.js +5 -1
  94. package/dist/gateway/agent-bridge.js.map +1 -1
  95. package/dist/gateway/cron.d.ts.map +1 -1
  96. package/dist/gateway/cron.js +5 -1
  97. package/dist/gateway/cron.js.map +1 -1
  98. package/dist/main.d.ts.map +1 -1
  99. package/dist/main.js +13 -1
  100. package/dist/main.js.map +1 -1
  101. package/dist/modes/interactive/components/assistant-message.d.ts +4 -0
  102. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  103. package/dist/modes/interactive/components/assistant-message.js +68 -10
  104. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  105. package/dist/modes/interactive/components/settings-selector.d.ts +13 -0
  106. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/settings-selector.js +157 -10
  108. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  109. package/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  110. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/tool-execution.js +9 -0
  112. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  113. package/dist/modes/interactive/interactive-mode.d.ts +4 -1
  114. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  115. package/dist/modes/interactive/interactive-mode.js +82 -3
  116. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  117. package/docs/keybindings.md +1 -1
  118. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  119. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  120. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  121. package/examples/extensions/gondolin/package-lock.json +2 -2
  122. package/examples/extensions/gondolin/package.json +1 -1
  123. package/examples/extensions/sandbox/package-lock.json +2 -2
  124. package/examples/extensions/sandbox/package.json +1 -1
  125. package/examples/extensions/with-deps/package-lock.json +2 -2
  126. package/examples/extensions/with-deps/package.json +1 -1
  127. package/package.json +4 -4
@@ -1,3 +1,10 @@
1
1
  import type { ExtensionAPI } from "@ashx-j/lunr";
2
+ /** True for parent-session tools that mean a child agent is in flight or being waited on. */
3
+ export declare function isSubagentToolName(name: string | undefined): boolean;
4
+ /**
5
+ * Working-indicator message: spinner is rendered separately (Loader puts it first).
6
+ * While any subagent has run this parent turn: `Orchestrating… kaomoji`.
7
+ */
8
+ export declare function composeWorkingMessage(kaomoji: string, orchestrating: boolean): string;
2
9
  export default function (pi: ExtensionAPI): void;
3
10
  //# sourceMappingURL=ashxj-spinners.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ashxj-spinners.d.ts","sourceRoot":"","sources":["../../src/builtin-extensions/ashxj-spinners.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AAgItF,MAAM,CAAC,OAAO,WAAW,EAAE,EAAE,YAAY,GAAG,IAAI,CA0K/C","sourcesContent":["import type { ExtensionAPI, ExtensionContext } from \"@ashx-j/lunr\";\nimport { spinners } from \"unicode-animations\";\n\n// Spinners come from the `unicode-animations` package; see\n// https://www.npmjs.com/package/unicode-animations for the full catalog.\n// On each state entry we pick a random spinner from that state's pool, and\n// we re-roll the kaomoji on a timer while the state is active (see\n// KAOMOJI_REROLL_MS), so the working-indicator row keeps varying instead of\n// freezing on one (spinner, kaomoji) pair for the whole state.\ntype SpinnerName = keyof typeof spinners;\n\ntype State = \"idle\" | \"startup\" | \"thinking\" | \"searching\" | \"editing\" | \"toolRunning\" | \"complete\" | \"error\";\n\n// `thinking` and `toolRunning` share one combined pool for both spinners and\n// kaomoji, so the two states are visually interchangeable. The combined\n// spinner pool is the union of the former thinking spinners (rain, breathe,\n// checkerboard, sparkle) and the former toolRunning spinners (braille,\n// snake). The combined kaomoji pool is the union of both former kaomoji\n// sets plus the new toolRunning entries below (6 spinners x 14 kaomoji).\nconst THINKING_TOOL_SPINNER_POOL: readonly SpinnerName[] = [\n \"rain\",\n \"breathe\",\n \"checkerboard\",\n \"sparkle\",\n \"braille\",\n \"snake\",\n];\n\nconst THINKING_TOOL_KAOMOJI_POOL: readonly string[] = [\n // formerly `thinking`\n \"(ᵕ—ᴗ—)\", // determined\n \"(˵ ¬ᴗ¬˵)\", // clever\n \"(⸝⸝ᵕᴗᵕ⸝⸝)\", // admiring gaze\n \"⸜(。˃ ᵕ ˂ )⸝♡\", // blushing happy\n \"(˶>⩊<˶)\", // cheerful\n \"૮⸝⸝> ̫ <⸝⸝ ა\", // pleading with worry\n \"(˶ > ₃ < ˶)\", // licking lips\n // formerly `toolRunning`\n \"(๑´>᎑<)~*\", // reaching for hug, eager\n // new\n \"₍^. .^₎⟆\",\n \"(ㅅ´ ˘ `)\",\n \"(„• ֊ •„)\",\n \"(˶˃ ᵕ ˂˶)\",\n \"⡞⠳⣄⣀⣠⠞⢷ ֹ۪\",\n \"(˶•̀ ᎑-˶)\",\n];\n\nconst SPINNER_POOL_FOR_STATE: Record<State, readonly SpinnerName[]> = {\n idle: [],\n startup: [],\n thinking: THINKING_TOOL_SPINNER_POOL,\n searching: [\"diagswipe\", \"scan\", \"cascade\"],\n editing: [\"columns\", \"fillsweep\"],\n toolRunning: THINKING_TOOL_SPINNER_POOL,\n complete: [],\n error: [\"pulse\"],\n};\n\nconst KAOMOJI_FOR_STATE: Record<State, readonly string[]> = {\n // idle, startup, complete are silent: no kaomoji.\n idle: [],\n startup: [],\n thinking: THINKING_TOOL_KAOMOJI_POOL,\n searching: [\n \"(⸝⸝⩌ ⤙ ⩌⸝⸝)\", // confused\n \"(╹ -╹)?\", // questioning\n ],\n editing: [\n \"(ᵕ • ᴗ •)\", // content\n \"(˶ᵔ ᵕ ᵔ˶) ‹𝟹\", // warm with small heart\n \"(๑ᵔ⤙ᵔ๑)\", // gleeful\n ],\n toolRunning: THINKING_TOOL_KAOMOJI_POOL,\n complete: [],\n error: [\n \"(,,>﹏<,,)\", // crying\n \"(⸝⸝ ♡﹏♡⸝⸝)\", // apologetic crying\n \"\\\"૮₍ ˶•⤙•˶ ₎ა\", // pleading paws\n \"(╥﹏╥)\", // heavy crying\n \"(˶°ㅁ°)!!\", // shocked\n \"(,,•᷄﹏‎•᷅,,)\", // distressed\n ],\n};\n\nconst SEARCH_RE = /(search|find|grep|list|ls|fuzzy|query)/i;\nconst EDIT_RE = /(^|[_\\-\\s])(write|edit|patch|create|delete|remove|replace|insert|update|apply_?patch|apply_?edit|str_?replace|str_?replace_?editor|create_?file|delete_?file|write_?file|edit_?file|insert_?content|append_?file|modify_?file)([_\\-\\s]|$)/i;\n\nconst TRANSIENT_MS = 3000;\n// Minimum time a visible \"active\" state must persist before being replaced.\n// Prevents flicker (e.g. error -> thinking clobbering the error before the\n// user can see it). The TTL only gates active->active transitions:\n// - Active -> active: TTL applies (within 3s, the new state is rejected)\n// - Active -> silent (idle/startup/complete): TTL bypassed (end-of-turn)\n// - Active -> error: TTL bypassed (error always wins)\n// - Silent -> active: TTL bypassed (silent states aren't in STATES_WITH_TTL)\n// Auto-revert timers bypass this by calling doApplyState directly.\nconst STATE_TTL_MS = 3000;\nconst STATES_WITH_TTL: ReadonlySet<State> = new Set<State>([\n \"thinking\", \"searching\", \"editing\", \"toolRunning\", \"error\",\n]);\nconst SILENT_STATES: ReadonlySet<State> = new Set<State>([\n \"idle\", \"startup\", \"complete\",\n]);\n\n// How often the kaomoji is re-rolled while an active state is showing. Set\n// long (30 s) so it only kicks in for genuinely long tasks; short states\n// finish before the first re-roll and show a single kaomoji. The spinner is\n// NOT re-rolled on this timer (restarting its animation would stutter); the\n// spinner only changes on state transitions.\nconst KAOMOJI_REROLL_MS = 30000;\n\nfunction pick<T>(arr: readonly T[]): T {\n return arr[Math.floor(Math.random() * arr.length)]!;\n}\n\n// Like pick, but never returns `current` (so a re-roll always produces a\n// visible change). Pools of length <= 1 can't vary, so they return the\n// sole element unchanged.\nfunction pickOther<T>(arr: readonly T[], current: T): T {\n if (arr.length <= 1) return arr[0]!;\n let v: T;\n do {\n v = pick(arr);\n } while (v === current);\n return v;\n}\n\nexport default function (pi: ExtensionAPI): void {\n let state: State = \"idle\";\n let stateEnteredAt = 0;\n let currentKaomoji = \"\";\n let transientTimer: ReturnType<typeof setTimeout> | undefined;\n let rerollTimer: ReturnType<typeof setInterval> | undefined;\n\n function clearTransient(): void {\n if (transientTimer !== undefined) {\n clearTimeout(transientTimer);\n transientTimer = undefined;\n }\n }\n\n function clearReroll(): void {\n if (rerollTimer !== undefined) {\n clearInterval(rerollTimer);\n rerollTimer = undefined;\n }\n }\n\n function doApplyState(next: State, ctx: ExtensionContext): void {\n state = next;\n stateEnteredAt = Date.now();\n\n const spinnerPool = SPINNER_POOL_FOR_STATE[next];\n if (spinnerPool.length > 0) {\n const spinnerName = pick(spinnerPool);\n const def = spinners[spinnerName];\n const frames = def.frames.map(f => ctx.ui.theme.fg(\"accent\", f));\n ctx.ui.setWorkingIndicator({ frames, intervalMs: def.interval });\n } else {\n ctx.ui.setWorkingIndicator({ frames: [] });\n }\n\n const kaomojiPool = KAOMOJI_FOR_STATE[next];\n currentKaomoji = kaomojiPool.length > 0 ? pick(kaomojiPool) : \"\";\n ctx.ui.setWorkingMessage(currentKaomoji);\n\n clearReroll();\n clearTransient();\n\n // Re-roll the kaomoji periodically while an active state is showing, so\n // a long thinking / toolRunning / error / searching / editing stretch\n // cycles through kaomoji instead of freezing on one. The spinner stays\n // put for the whole state (see KAOMOJI_REROLL_MS). Pools of length <= 1\n // can't vary, so skip them; silent states have empty pools and are also\n // skipped.\n if (kaomojiPool.length > 1) {\n rerollTimer = setInterval(() => rerollKaomoji(ctx), KAOMOJI_REROLL_MS);\n }\n\n if (next === \"complete\" || next === \"startup\") {\n transientTimer = setTimeout(() => doApplyState(\"idle\", ctx), TRANSIENT_MS);\n }\n }\n\n function rerollKaomoji(ctx: ExtensionContext): void {\n const pool = KAOMOJI_FOR_STATE[state];\n if (pool.length <= 1) return;\n currentKaomoji = pickOther(pool, currentKaomoji);\n ctx.ui.setWorkingMessage(currentKaomoji);\n }\n\n function applyState(next: State, ctx: ExtensionContext, options: { force?: boolean } = {}): void {\n if (ctx.mode !== \"tui\") return;\n // Enforce minimum TTL on event-driven transitions, with three exceptions:\n // - `error` always wins (so tool_execution_end can always set error).\n // - Silent states (idle, startup, complete) bypass TTL — they're\n // end-of-turn markers, not active states that need minimum display.\n // Without this, agent_end within 3s of turn_start would be rejected,\n // leaving the spinner stuck in the active state.\n // - `force: true` bypasses TTL — used for tool_execution_start so the\n // spinner syncs with the tool bubble the moment it appears.\n // Auto-revert timers bypass this by calling doApplyState directly.\n if (\n !options.force &&\n !SILENT_STATES.has(next) &&\n next !== \"error\" &&\n STATES_WITH_TTL.has(state) &&\n Date.now() - stateEnteredAt < STATE_TTL_MS\n ) {\n return;\n }\n doApplyState(next, ctx);\n }\n\n pi.on(\"session_start\", (event, ctx) => {\n if (ctx.mode !== \"tui\") return;\n ctx.ui.setTitle(\"ashxj\");\n doApplyState(event.reason === \"startup\" ? \"startup\" : \"idle\", ctx);\n });\n\n pi.on(\"turn_start\", (_event, ctx) => {\n applyState(\"thinking\", ctx);\n });\n\n pi.on(\"tool_execution_start\", (event, ctx) => {\n // Hook on tool_execution_start (not tool_call) so the spinner activates\n // at the same moment the tool bubble appears in the UI. tool_call fires\n // earlier (when the LLM decides to call the tool, before execution), and\n // the bubble only appears once the tool actually starts — so hooking on\n // tool_call creates a visual desync between spinner and bubble.\n // Bonus: tools blocked at tool_call never run, so tool_execution_start\n // won't fire and the spinner won't activate for blocked tools.\n // `force: true` bypasses the 3s TTL so the spinner syncs with the\n // bubble immediately, even if the previous active state is still\n // within its minimum display window.\n if (state === \"error\") return;\n const name = event.toolName;\n const force = true;\n if (SEARCH_RE.test(name)) {\n applyState(\"searching\", ctx, { force });\n } else if (EDIT_RE.test(name)) {\n applyState(\"editing\", ctx, { force });\n } else {\n applyState(\"toolRunning\", ctx, { force });\n }\n });\n\n pi.on(\"tool_execution_end\", (event, ctx) => {\n if (event.isError) {\n applyState(\"error\", ctx);\n }\n });\n\n pi.on(\"turn_end\", (_event, ctx) => {\n // A turn that ran a tool ends with the spinner still on the tool state\n // (searching/editing/toolRunning). The next turn_start would revert to\n // thinking, but it's TTL-gated and gets rejected for fast tools (<3s),\n // which leaves the spinner stuck on the tool state for the whole next\n // turn. Revert to thinking here, right after the turn's tools finish.\n // turn_end fires once per turn after all its (concurrent) tools\n // complete, so this is safe for parallel tool calls. Only force out of\n // tool states, never out of error (error has priority and is handled\n // by agent_end). No-op on turns that had no tools (state is already\n // thinking).\n if (state === \"searching\" || state === \"editing\" || state === \"toolRunning\") {\n applyState(\"thinking\", ctx, { force: true });\n }\n });\n\n pi.on(\"agent_end\", (event, ctx) => {\n const last = event.messages.at(-1);\n if (!last || last.role !== \"assistant\") {\n applyState(\"idle\", ctx);\n return;\n }\n const stopReason = (last as { stopReason?: string }).stopReason;\n if (stopReason === \"stop\" || stopReason === \"length\") {\n applyState(\"complete\", ctx);\n } else if (stopReason === \"error\" || stopReason === \"aborted\") {\n applyState(\"error\", ctx);\n } else if (stopReason === \"toolUse\") {\n } else {\n applyState(\"idle\", ctx);\n }\n });\n\n pi.on(\"session_shutdown\", (_event, ctx) => {\n clearTransient();\n clearReroll();\n if (ctx.mode === \"tui\") {\n ctx.ui.setWorkingIndicator({ frames: [] });\n ctx.ui.setWorkingMessage(\"\");\n }\n state = \"idle\";\n stateEnteredAt = 0;\n currentKaomoji = \"\";\n });\n}"]}
1
+ {"version":3,"file":"ashxj-spinners.d.ts","sourceRoot":"","sources":["../../src/builtin-extensions/ashxj-spinners.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AAkItF,6FAA6F;AAC7F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEpE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,MAAM,CAGrF;AAED,MAAM,CAAC,OAAO,WAAW,EAAE,EAAE,YAAY,GAAG,IAAI,CAmM/C","sourcesContent":["import type { ExtensionAPI, ExtensionContext } from \"@ashx-j/lunr\";\nimport { spinners } from \"unicode-animations\";\n\n// Spinners come from the `unicode-animations` package; see\n// https://www.npmjs.com/package/unicode-animations for the full catalog.\n// On each state entry we pick a random spinner from that state's pool, and\n// we re-roll the kaomoji on a timer while the state is active (see\n// KAOMOJI_REROLL_MS), so the working-indicator row keeps varying instead of\n// freezing on one (spinner, kaomoji) pair for the whole state.\ntype SpinnerName = keyof typeof spinners;\n\ntype State = \"idle\" | \"startup\" | \"thinking\" | \"searching\" | \"editing\" | \"toolRunning\" | \"complete\" | \"error\";\n\n// `thinking` and `toolRunning` share one combined pool for both spinners and\n// kaomoji, so the two states are visually interchangeable. The combined\n// spinner pool is the union of the former thinking spinners (rain, breathe,\n// checkerboard, sparkle) and the former toolRunning spinners (braille,\n// snake). The combined kaomoji pool is the union of both former kaomoji\n// sets plus the new toolRunning entries below (6 spinners x 14 kaomoji).\nconst THINKING_TOOL_SPINNER_POOL: readonly SpinnerName[] = [\n \"rain\",\n \"breathe\",\n \"checkerboard\",\n \"sparkle\",\n \"braille\",\n \"snake\",\n];\n\nconst THINKING_TOOL_KAOMOJI_POOL: readonly string[] = [\n // formerly `thinking`\n \"(ᵕ—ᴗ—)\", // determined\n \"(˵ ¬ᴗ¬˵)\", // clever\n \"(⸝⸝ᵕᴗᵕ⸝⸝)\", // admiring gaze\n \"⸜(。˃ ᵕ ˂ )⸝♡\", // blushing happy\n \"(˶>⩊<˶)\", // cheerful\n \"૮⸝⸝> ̫ <⸝⸝ ა\", // pleading with worry\n \"(˶ > ₃ < ˶)\", // licking lips\n // formerly `toolRunning`\n \"(๑´>᎑<)~*\", // reaching for hug, eager\n // new\n \"₍^. .^₎⟆\",\n \"(ㅅ´ ˘ `)\",\n \"(„• ֊ •„)\",\n \"(˶˃ ᵕ ˂˶)\",\n \"⡞⠳⣄⣀⣠⠞⢷ ֹ۪\",\n \"(˶•̀ ᎑-˶)\",\n];\n\nconst SPINNER_POOL_FOR_STATE: Record<State, readonly SpinnerName[]> = {\n idle: [],\n startup: [],\n thinking: THINKING_TOOL_SPINNER_POOL,\n searching: [\"diagswipe\", \"scan\", \"cascade\"],\n editing: [\"columns\", \"fillsweep\"],\n toolRunning: THINKING_TOOL_SPINNER_POOL,\n complete: [],\n error: [\"pulse\"],\n};\n\nconst KAOMOJI_FOR_STATE: Record<State, readonly string[]> = {\n // idle, startup, complete are silent: no kaomoji.\n idle: [],\n startup: [],\n thinking: THINKING_TOOL_KAOMOJI_POOL,\n searching: [\n \"(⸝⸝⩌ ⤙ ⩌⸝⸝)\", // confused\n \"(╹ -╹)?\", // questioning\n ],\n editing: [\n \"(ᵕ • ᴗ •)\", // content\n \"(˶ᵔ ᵕ ᵔ˶) ‹𝟹\", // warm with small heart\n \"(๑ᵔ⤙ᵔ๑)\", // gleeful\n ],\n toolRunning: THINKING_TOOL_KAOMOJI_POOL,\n complete: [],\n error: [\n \"(,,>﹏<,,)\", // crying\n \"(⸝⸝ ♡﹏♡⸝⸝)\", // apologetic crying\n \"\\\"૮₍ ˶•⤙•˶ ₎ა\", // pleading paws\n \"(╥﹏╥)\", // heavy crying\n \"(˶°ㅁ°)!!\", // shocked\n \"(,,•᷄﹏‎•᷅,,)\", // distressed\n ],\n};\n\nconst SEARCH_RE = /(search|find|grep|list|ls|fuzzy|query)/i;\nconst EDIT_RE = /(^|[_\\-\\s])(write|edit|patch|create|delete|remove|replace|insert|update|apply_?patch|apply_?edit|str_?replace|str_?replace_?editor|create_?file|delete_?file|write_?file|edit_?file|insert_?content|append_?file|modify_?file)([_\\-\\s]|$)/i;\n\nconst TRANSIENT_MS = 3000;\n// Minimum time a visible \"active\" state must persist before being replaced.\n// Prevents flicker (e.g. error -> thinking clobbering the error before the\n// user can see it). The TTL only gates active->active transitions:\n// - Active -> active: TTL applies (within 3s, the new state is rejected)\n// - Active -> silent (idle/startup/complete): TTL bypassed (end-of-turn)\n// - Active -> error: TTL bypassed (error always wins)\n// - Silent -> active: TTL bypassed (silent states aren't in STATES_WITH_TTL)\n// Auto-revert timers bypass this by calling doApplyState directly.\nconst STATE_TTL_MS = 3000;\nconst STATES_WITH_TTL: ReadonlySet<State> = new Set<State>([\n \"thinking\", \"searching\", \"editing\", \"toolRunning\", \"error\",\n]);\nconst SILENT_STATES: ReadonlySet<State> = new Set<State>([\n \"idle\", \"startup\", \"complete\",\n]);\n\n// How often the kaomoji is re-rolled while an active state is showing. Set\n// long (30 s) so it only kicks in for genuinely long tasks; short states\n// finish before the first re-roll and show a single kaomoji. The spinner is\n// NOT re-rolled on this timer (restarting its animation would stutter); the\n// spinner only changes on state transitions.\nconst KAOMOJI_REROLL_MS = 30000;\n\nfunction pick<T>(arr: readonly T[]): T {\n return arr[Math.floor(Math.random() * arr.length)]!;\n}\n\n// Like pick, but never returns `current` (so a re-roll always produces a\n// visible change). Pools of length <= 1 can't vary, so they return the\n// sole element unchanged.\nfunction pickOther<T>(arr: readonly T[], current: T): T {\n if (arr.length <= 1) return arr[0]!;\n let v: T;\n do {\n v = pick(arr);\n } while (v === current);\n return v;\n}\n\nconst SUBAGENT_TOOL_NAMES = new Set([\"subagent\", \"subagent_wait\"]);\n\n/** True for parent-session tools that mean a child agent is in flight or being waited on. */\nexport function isSubagentToolName(name: string | undefined): boolean {\n return typeof name === \"string\" && SUBAGENT_TOOL_NAMES.has(name);\n}\n\n/**\n * Working-indicator message: spinner is rendered separately (Loader puts it first).\n * While any subagent has run this parent turn: `Orchestrating… kaomoji`.\n */\nexport function composeWorkingMessage(kaomoji: string, orchestrating: boolean): string {\n if (!orchestrating) return kaomoji;\n return kaomoji ? `Orchestrating… ${kaomoji}` : \"Orchestrating…\";\n}\n\nexport default function (pi: ExtensionAPI): void {\n let state: State = \"idle\";\n let stateEnteredAt = 0;\n let currentKaomoji = \"\";\n let liveSubagent = 0;\n let seenSubagentThisTurn = false;\n let transientTimer: ReturnType<typeof setTimeout> | undefined;\n let rerollTimer: ReturnType<typeof setInterval> | undefined;\n\n function orchestrating(): boolean {\n return liveSubagent > 0 || seenSubagentThisTurn;\n }\n\n function publishWorkingMessage(ctx: ExtensionContext): void {\n ctx.ui.setWorkingMessage(composeWorkingMessage(currentKaomoji, orchestrating()));\n }\n\n function clearTransient(): void {\n if (transientTimer !== undefined) {\n clearTimeout(transientTimer);\n transientTimer = undefined;\n }\n }\n\n function clearReroll(): void {\n if (rerollTimer !== undefined) {\n clearInterval(rerollTimer);\n rerollTimer = undefined;\n }\n }\n\n function doApplyState(next: State, ctx: ExtensionContext): void {\n state = next;\n stateEnteredAt = Date.now();\n\n const spinnerPool = SPINNER_POOL_FOR_STATE[next];\n if (spinnerPool.length > 0) {\n const spinnerName = pick(spinnerPool);\n const def = spinners[spinnerName];\n const frames = def.frames.map(f => ctx.ui.theme.fg(\"accent\", f));\n ctx.ui.setWorkingIndicator({ frames, intervalMs: def.interval });\n } else {\n ctx.ui.setWorkingIndicator({ frames: [] });\n }\n\n const kaomojiPool = KAOMOJI_FOR_STATE[next];\n currentKaomoji = kaomojiPool.length > 0 ? pick(kaomojiPool) : \"\";\n publishWorkingMessage(ctx);\n\n clearReroll();\n clearTransient();\n\n // Re-roll the kaomoji periodically while an active state is showing, so\n // a long thinking / toolRunning / error / searching / editing stretch\n // cycles through kaomoji instead of freezing on one. The spinner stays\n // put for the whole state (see KAOMOJI_REROLL_MS). Pools of length <= 1\n // can't vary, so skip them; silent states have empty pools and are also\n // skipped.\n if (kaomojiPool.length > 1) {\n rerollTimer = setInterval(() => rerollKaomoji(ctx), KAOMOJI_REROLL_MS);\n }\n\n if (next === \"complete\" || next === \"startup\") {\n transientTimer = setTimeout(() => doApplyState(\"idle\", ctx), TRANSIENT_MS);\n }\n }\n\n function rerollKaomoji(ctx: ExtensionContext): void {\n const pool = KAOMOJI_FOR_STATE[state];\n if (pool.length <= 1) return;\n currentKaomoji = pickOther(pool, currentKaomoji);\n publishWorkingMessage(ctx);\n }\n\n function applyState(next: State, ctx: ExtensionContext, options: { force?: boolean } = {}): void {\n if (ctx.mode !== \"tui\") return;\n // Enforce minimum TTL on event-driven transitions, with three exceptions:\n // - `error` always wins (so tool_execution_end can always set error).\n // - Silent states (idle, startup, complete) bypass TTL — they're\n // end-of-turn markers, not active states that need minimum display.\n // Without this, agent_end within 3s of turn_start would be rejected,\n // leaving the spinner stuck in the active state.\n // - `force: true` bypasses TTL — used for tool_execution_start so the\n // spinner syncs with the tool bubble the moment it appears.\n // Auto-revert timers bypass this by calling doApplyState directly.\n if (\n !options.force &&\n !SILENT_STATES.has(next) &&\n next !== \"error\" &&\n STATES_WITH_TTL.has(state) &&\n Date.now() - stateEnteredAt < STATE_TTL_MS\n ) {\n return;\n }\n doApplyState(next, ctx);\n }\n\n pi.on(\"session_start\", (event, ctx) => {\n if (ctx.mode !== \"tui\") return;\n // lunr: do not ctx.ui.setTitle here — InteractiveMode owns the OSC title\n // (`lunr - [session -] cwd`). Overwriting it with a brand string made\n // Windows Terminal tabs say \"ashxj\" (or left them as \"node\" until this ran).\n doApplyState(event.reason === \"startup\" ? \"startup\" : \"idle\", ctx);\n });\n\n pi.on(\"turn_start\", (_event, ctx) => {\n applyState(\"thinking\", ctx);\n });\n\n pi.on(\"tool_execution_start\", (event, ctx) => {\n // Hook on tool_execution_start (not tool_call) so the spinner activates\n // at the same moment the tool bubble appears in the UI. tool_call fires\n // earlier (when the LLM decides to call the tool, before execution), and\n // the bubble only appears once the tool actually starts — so hooking on\n // tool_call creates a visual desync between spinner and bubble.\n // Bonus: tools blocked at tool_call never run, so tool_execution_start\n // won't fire and the spinner won't activate for blocked tools.\n // `force: true` bypasses the 3s TTL so the spinner syncs with the\n // bubble immediately, even if the previous active state is still\n // within its minimum display window.\n if (state === \"error\") return;\n const name = event.toolName;\n // lunr: any subagent / subagent_wait this turn prefixes the working row\n // with \"Orchestrating…\" (spinner is already first via Loader).\n if (isSubagentToolName(name)) {\n liveSubagent += 1;\n seenSubagentThisTurn = true;\n }\n const force = true;\n if (SEARCH_RE.test(name)) {\n applyState(\"searching\", ctx, { force });\n } else if (EDIT_RE.test(name)) {\n applyState(\"editing\", ctx, { force });\n } else {\n applyState(\"toolRunning\", ctx, { force });\n }\n });\n\n pi.on(\"tool_execution_end\", (event, ctx) => {\n if (isSubagentToolName(event.toolName) && liveSubagent > 0) {\n liveSubagent -= 1;\n }\n if (event.isError) {\n applyState(\"error\", ctx);\n }\n });\n\n pi.on(\"turn_end\", (_event, ctx) => {\n // A turn that ran a tool ends with the spinner still on the tool state\n // (searching/editing/toolRunning). The next turn_start would revert to\n // thinking, but it's TTL-gated and gets rejected for fast tools (<3s),\n // which leaves the spinner stuck on the tool state for the whole next\n // turn. Revert to thinking here, right after the turn's tools finish.\n // turn_end fires once per turn after all its (concurrent) tools\n // complete, so this is safe for parallel tool calls. Only force out of\n // tool states, never out of error (error has priority and is handled\n // by agent_end). No-op on turns that had no tools (state is already\n // thinking).\n if (state === \"searching\" || state === \"editing\" || state === \"toolRunning\") {\n applyState(\"thinking\", ctx, { force: true });\n }\n });\n\n pi.on(\"agent_end\", (event, ctx) => {\n liveSubagent = 0;\n seenSubagentThisTurn = false;\n const last = event.messages.at(-1);\n if (!last || last.role !== \"assistant\") {\n applyState(\"idle\", ctx);\n return;\n }\n const stopReason = (last as { stopReason?: string }).stopReason;\n if (stopReason === \"stop\" || stopReason === \"length\") {\n applyState(\"complete\", ctx);\n } else if (stopReason === \"error\" || stopReason === \"aborted\") {\n applyState(\"error\", ctx);\n } else if (stopReason === \"toolUse\") {\n } else {\n applyState(\"idle\", ctx);\n }\n });\n\n pi.on(\"session_shutdown\", (_event, ctx) => {\n clearTransient();\n clearReroll();\n if (ctx.mode === \"tui\") {\n ctx.ui.setWorkingIndicator({ frames: [] });\n ctx.ui.setWorkingMessage(\"\");\n }\n state = \"idle\";\n stateEnteredAt = 0;\n currentKaomoji = \"\";\n liveSubagent = 0;\n seenSubagentThisTurn = false;\n });\n}"]}
@@ -106,12 +106,34 @@ function pickOther(arr, current) {
106
106
  } while (v === current);
107
107
  return v;
108
108
  }
109
+ const SUBAGENT_TOOL_NAMES = new Set(["subagent", "subagent_wait"]);
110
+ /** True for parent-session tools that mean a child agent is in flight or being waited on. */
111
+ export function isSubagentToolName(name) {
112
+ return typeof name === "string" && SUBAGENT_TOOL_NAMES.has(name);
113
+ }
114
+ /**
115
+ * Working-indicator message: spinner is rendered separately (Loader puts it first).
116
+ * While any subagent has run this parent turn: `Orchestrating… kaomoji`.
117
+ */
118
+ export function composeWorkingMessage(kaomoji, orchestrating) {
119
+ if (!orchestrating)
120
+ return kaomoji;
121
+ return kaomoji ? `Orchestrating… ${kaomoji}` : "Orchestrating…";
122
+ }
109
123
  export default function (pi) {
110
124
  let state = "idle";
111
125
  let stateEnteredAt = 0;
112
126
  let currentKaomoji = "";
127
+ let liveSubagent = 0;
128
+ let seenSubagentThisTurn = false;
113
129
  let transientTimer;
114
130
  let rerollTimer;
131
+ function orchestrating() {
132
+ return liveSubagent > 0 || seenSubagentThisTurn;
133
+ }
134
+ function publishWorkingMessage(ctx) {
135
+ ctx.ui.setWorkingMessage(composeWorkingMessage(currentKaomoji, orchestrating()));
136
+ }
115
137
  function clearTransient() {
116
138
  if (transientTimer !== undefined) {
117
139
  clearTimeout(transientTimer);
@@ -139,7 +161,7 @@ export default function (pi) {
139
161
  }
140
162
  const kaomojiPool = KAOMOJI_FOR_STATE[next];
141
163
  currentKaomoji = kaomojiPool.length > 0 ? pick(kaomojiPool) : "";
142
- ctx.ui.setWorkingMessage(currentKaomoji);
164
+ publishWorkingMessage(ctx);
143
165
  clearReroll();
144
166
  clearTransient();
145
167
  // Re-roll the kaomoji periodically while an active state is showing, so
@@ -160,7 +182,7 @@ export default function (pi) {
160
182
  if (pool.length <= 1)
161
183
  return;
162
184
  currentKaomoji = pickOther(pool, currentKaomoji);
163
- ctx.ui.setWorkingMessage(currentKaomoji);
185
+ publishWorkingMessage(ctx);
164
186
  }
165
187
  function applyState(next, ctx, options = {}) {
166
188
  if (ctx.mode !== "tui")
@@ -186,7 +208,9 @@ export default function (pi) {
186
208
  pi.on("session_start", (event, ctx) => {
187
209
  if (ctx.mode !== "tui")
188
210
  return;
189
- ctx.ui.setTitle("ashxj");
211
+ // lunr: do not ctx.ui.setTitle here — InteractiveMode owns the OSC title
212
+ // (`lunr - [session -] cwd`). Overwriting it with a brand string made
213
+ // Windows Terminal tabs say "ashxj" (or left them as "node" until this ran).
190
214
  doApplyState(event.reason === "startup" ? "startup" : "idle", ctx);
191
215
  });
192
216
  pi.on("turn_start", (_event, ctx) => {
@@ -206,6 +230,12 @@ export default function (pi) {
206
230
  if (state === "error")
207
231
  return;
208
232
  const name = event.toolName;
233
+ // lunr: any subagent / subagent_wait this turn prefixes the working row
234
+ // with "Orchestrating…" (spinner is already first via Loader).
235
+ if (isSubagentToolName(name)) {
236
+ liveSubagent += 1;
237
+ seenSubagentThisTurn = true;
238
+ }
209
239
  const force = true;
210
240
  if (SEARCH_RE.test(name)) {
211
241
  applyState("searching", ctx, { force });
@@ -218,6 +248,9 @@ export default function (pi) {
218
248
  }
219
249
  });
220
250
  pi.on("tool_execution_end", (event, ctx) => {
251
+ if (isSubagentToolName(event.toolName) && liveSubagent > 0) {
252
+ liveSubagent -= 1;
253
+ }
221
254
  if (event.isError) {
222
255
  applyState("error", ctx);
223
256
  }
@@ -238,6 +271,8 @@ export default function (pi) {
238
271
  }
239
272
  });
240
273
  pi.on("agent_end", (event, ctx) => {
274
+ liveSubagent = 0;
275
+ seenSubagentThisTurn = false;
241
276
  const last = event.messages.at(-1);
242
277
  if (!last || last.role !== "assistant") {
243
278
  applyState("idle", ctx);
@@ -266,6 +301,8 @@ export default function (pi) {
266
301
  state = "idle";
267
302
  stateEnteredAt = 0;
268
303
  currentKaomoji = "";
304
+ liveSubagent = 0;
305
+ seenSubagentThisTurn = false;
269
306
  });
270
307
  }
271
308
  //# sourceMappingURL=ashxj-spinners.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ashxj-spinners.js","sourceRoot":"","sources":["../../src/builtin-extensions/ashxj-spinners.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ExtensionAPI, ExtensionContext } from \"@ashx-j/lunr\";\nimport { spinners } from \"unicode-animations\";\n\n// Spinners come from the `unicode-animations` package; see\n// https://www.npmjs.com/package/unicode-animations for the full catalog.\n// On each state entry we pick a random spinner from that state's pool, and\n// we re-roll the kaomoji on a timer while the state is active (see\n// KAOMOJI_REROLL_MS), so the working-indicator row keeps varying instead of\n// freezing on one (spinner, kaomoji) pair for the whole state.\ntype SpinnerName = keyof typeof spinners;\n\ntype State = \"idle\" | \"startup\" | \"thinking\" | \"searching\" | \"editing\" | \"toolRunning\" | \"complete\" | \"error\";\n\n// `thinking` and `toolRunning` share one combined pool for both spinners and\n// kaomoji, so the two states are visually interchangeable. The combined\n// spinner pool is the union of the former thinking spinners (rain, breathe,\n// checkerboard, sparkle) and the former toolRunning spinners (braille,\n// snake). The combined kaomoji pool is the union of both former kaomoji\n// sets plus the new toolRunning entries below (6 spinners x 14 kaomoji).\nconst THINKING_TOOL_SPINNER_POOL: readonly SpinnerName[] = [\n \"rain\",\n \"breathe\",\n \"checkerboard\",\n \"sparkle\",\n \"braille\",\n \"snake\",\n];\n\nconst THINKING_TOOL_KAOMOJI_POOL: readonly string[] = [\n // formerly `thinking`\n \"(ᵕ—ᴗ—)\", // determined\n \"(˵ ¬ᴗ¬˵)\", // clever\n \"(⸝⸝ᵕᴗᵕ⸝⸝)\", // admiring gaze\n \"⸜(。˃ ᵕ ˂ )⸝♡\", // blushing happy\n \"(˶>⩊<˶)\", // cheerful\n \"૮⸝⸝> ̫ <⸝⸝ ა\", // pleading with worry\n \"(˶ > ₃ < ˶)\", // licking lips\n // formerly `toolRunning`\n \"(๑´>᎑<)~*\", // reaching for hug, eager\n // new\n \"₍^. .^₎⟆\",\n \"(ㅅ´ ˘ `)\",\n \"(„• ֊ •„)\",\n \"(˶˃ ᵕ ˂˶)\",\n \"⡞⠳⣄⣀⣠⠞⢷ ֹ۪\",\n \"(˶•̀ ᎑-˶)\",\n];\n\nconst SPINNER_POOL_FOR_STATE: Record<State, readonly SpinnerName[]> = {\n idle: [],\n startup: [],\n thinking: THINKING_TOOL_SPINNER_POOL,\n searching: [\"diagswipe\", \"scan\", \"cascade\"],\n editing: [\"columns\", \"fillsweep\"],\n toolRunning: THINKING_TOOL_SPINNER_POOL,\n complete: [],\n error: [\"pulse\"],\n};\n\nconst KAOMOJI_FOR_STATE: Record<State, readonly string[]> = {\n // idle, startup, complete are silent: no kaomoji.\n idle: [],\n startup: [],\n thinking: THINKING_TOOL_KAOMOJI_POOL,\n searching: [\n \"(⸝⸝⩌ ⤙ ⩌⸝⸝)\", // confused\n \"(╹ -╹)?\", // questioning\n ],\n editing: [\n \"(ᵕ • ᴗ •)\", // content\n \"(˶ᵔ ᵕ ᵔ˶) ‹𝟹\", // warm with small heart\n \"(๑ᵔ⤙ᵔ๑)\", // gleeful\n ],\n toolRunning: THINKING_TOOL_KAOMOJI_POOL,\n complete: [],\n error: [\n \"(,,>﹏<,,)\", // crying\n \"(⸝⸝ ♡﹏♡⸝⸝)\", // apologetic crying\n \"\\\"૮₍ ˶•⤙•˶ ₎ა\", // pleading paws\n \"(╥﹏╥)\", // heavy crying\n \"(˶°ㅁ°)!!\", // shocked\n \"(,,•᷄﹏‎•᷅,,)\", // distressed\n ],\n};\n\nconst SEARCH_RE = /(search|find|grep|list|ls|fuzzy|query)/i;\nconst EDIT_RE = /(^|[_\\-\\s])(write|edit|patch|create|delete|remove|replace|insert|update|apply_?patch|apply_?edit|str_?replace|str_?replace_?editor|create_?file|delete_?file|write_?file|edit_?file|insert_?content|append_?file|modify_?file)([_\\-\\s]|$)/i;\n\nconst TRANSIENT_MS = 3000;\n// Minimum time a visible \"active\" state must persist before being replaced.\n// Prevents flicker (e.g. error -> thinking clobbering the error before the\n// user can see it). The TTL only gates active->active transitions:\n// - Active -> active: TTL applies (within 3s, the new state is rejected)\n// - Active -> silent (idle/startup/complete): TTL bypassed (end-of-turn)\n// - Active -> error: TTL bypassed (error always wins)\n// - Silent -> active: TTL bypassed (silent states aren't in STATES_WITH_TTL)\n// Auto-revert timers bypass this by calling doApplyState directly.\nconst STATE_TTL_MS = 3000;\nconst STATES_WITH_TTL: ReadonlySet<State> = new Set<State>([\n \"thinking\", \"searching\", \"editing\", \"toolRunning\", \"error\",\n]);\nconst SILENT_STATES: ReadonlySet<State> = new Set<State>([\n \"idle\", \"startup\", \"complete\",\n]);\n\n// How often the kaomoji is re-rolled while an active state is showing. Set\n// long (30 s) so it only kicks in for genuinely long tasks; short states\n// finish before the first re-roll and show a single kaomoji. The spinner is\n// NOT re-rolled on this timer (restarting its animation would stutter); the\n// spinner only changes on state transitions.\nconst KAOMOJI_REROLL_MS = 30000;\n\nfunction pick<T>(arr: readonly T[]): T {\n return arr[Math.floor(Math.random() * arr.length)]!;\n}\n\n// Like pick, but never returns `current` (so a re-roll always produces a\n// visible change). Pools of length <= 1 can't vary, so they return the\n// sole element unchanged.\nfunction pickOther<T>(arr: readonly T[], current: T): T {\n if (arr.length <= 1) return arr[0]!;\n let v: T;\n do {\n v = pick(arr);\n } while (v === current);\n return v;\n}\n\nexport default function (pi: ExtensionAPI): void {\n let state: State = \"idle\";\n let stateEnteredAt = 0;\n let currentKaomoji = \"\";\n let transientTimer: ReturnType<typeof setTimeout> | undefined;\n let rerollTimer: ReturnType<typeof setInterval> | undefined;\n\n function clearTransient(): void {\n if (transientTimer !== undefined) {\n clearTimeout(transientTimer);\n transientTimer = undefined;\n }\n }\n\n function clearReroll(): void {\n if (rerollTimer !== undefined) {\n clearInterval(rerollTimer);\n rerollTimer = undefined;\n }\n }\n\n function doApplyState(next: State, ctx: ExtensionContext): void {\n state = next;\n stateEnteredAt = Date.now();\n\n const spinnerPool = SPINNER_POOL_FOR_STATE[next];\n if (spinnerPool.length > 0) {\n const spinnerName = pick(spinnerPool);\n const def = spinners[spinnerName];\n const frames = def.frames.map(f => ctx.ui.theme.fg(\"accent\", f));\n ctx.ui.setWorkingIndicator({ frames, intervalMs: def.interval });\n } else {\n ctx.ui.setWorkingIndicator({ frames: [] });\n }\n\n const kaomojiPool = KAOMOJI_FOR_STATE[next];\n currentKaomoji = kaomojiPool.length > 0 ? pick(kaomojiPool) : \"\";\n ctx.ui.setWorkingMessage(currentKaomoji);\n\n clearReroll();\n clearTransient();\n\n // Re-roll the kaomoji periodically while an active state is showing, so\n // a long thinking / toolRunning / error / searching / editing stretch\n // cycles through kaomoji instead of freezing on one. The spinner stays\n // put for the whole state (see KAOMOJI_REROLL_MS). Pools of length <= 1\n // can't vary, so skip them; silent states have empty pools and are also\n // skipped.\n if (kaomojiPool.length > 1) {\n rerollTimer = setInterval(() => rerollKaomoji(ctx), KAOMOJI_REROLL_MS);\n }\n\n if (next === \"complete\" || next === \"startup\") {\n transientTimer = setTimeout(() => doApplyState(\"idle\", ctx), TRANSIENT_MS);\n }\n }\n\n function rerollKaomoji(ctx: ExtensionContext): void {\n const pool = KAOMOJI_FOR_STATE[state];\n if (pool.length <= 1) return;\n currentKaomoji = pickOther(pool, currentKaomoji);\n ctx.ui.setWorkingMessage(currentKaomoji);\n }\n\n function applyState(next: State, ctx: ExtensionContext, options: { force?: boolean } = {}): void {\n if (ctx.mode !== \"tui\") return;\n // Enforce minimum TTL on event-driven transitions, with three exceptions:\n // - `error` always wins (so tool_execution_end can always set error).\n // - Silent states (idle, startup, complete) bypass TTL — they're\n // end-of-turn markers, not active states that need minimum display.\n // Without this, agent_end within 3s of turn_start would be rejected,\n // leaving the spinner stuck in the active state.\n // - `force: true` bypasses TTL — used for tool_execution_start so the\n // spinner syncs with the tool bubble the moment it appears.\n // Auto-revert timers bypass this by calling doApplyState directly.\n if (\n !options.force &&\n !SILENT_STATES.has(next) &&\n next !== \"error\" &&\n STATES_WITH_TTL.has(state) &&\n Date.now() - stateEnteredAt < STATE_TTL_MS\n ) {\n return;\n }\n doApplyState(next, ctx);\n }\n\n pi.on(\"session_start\", (event, ctx) => {\n if (ctx.mode !== \"tui\") return;\n ctx.ui.setTitle(\"ashxj\");\n doApplyState(event.reason === \"startup\" ? \"startup\" : \"idle\", ctx);\n });\n\n pi.on(\"turn_start\", (_event, ctx) => {\n applyState(\"thinking\", ctx);\n });\n\n pi.on(\"tool_execution_start\", (event, ctx) => {\n // Hook on tool_execution_start (not tool_call) so the spinner activates\n // at the same moment the tool bubble appears in the UI. tool_call fires\n // earlier (when the LLM decides to call the tool, before execution), and\n // the bubble only appears once the tool actually starts — so hooking on\n // tool_call creates a visual desync between spinner and bubble.\n // Bonus: tools blocked at tool_call never run, so tool_execution_start\n // won't fire and the spinner won't activate for blocked tools.\n // `force: true` bypasses the 3s TTL so the spinner syncs with the\n // bubble immediately, even if the previous active state is still\n // within its minimum display window.\n if (state === \"error\") return;\n const name = event.toolName;\n const force = true;\n if (SEARCH_RE.test(name)) {\n applyState(\"searching\", ctx, { force });\n } else if (EDIT_RE.test(name)) {\n applyState(\"editing\", ctx, { force });\n } else {\n applyState(\"toolRunning\", ctx, { force });\n }\n });\n\n pi.on(\"tool_execution_end\", (event, ctx) => {\n if (event.isError) {\n applyState(\"error\", ctx);\n }\n });\n\n pi.on(\"turn_end\", (_event, ctx) => {\n // A turn that ran a tool ends with the spinner still on the tool state\n // (searching/editing/toolRunning). The next turn_start would revert to\n // thinking, but it's TTL-gated and gets rejected for fast tools (<3s),\n // which leaves the spinner stuck on the tool state for the whole next\n // turn. Revert to thinking here, right after the turn's tools finish.\n // turn_end fires once per turn after all its (concurrent) tools\n // complete, so this is safe for parallel tool calls. Only force out of\n // tool states, never out of error (error has priority and is handled\n // by agent_end). No-op on turns that had no tools (state is already\n // thinking).\n if (state === \"searching\" || state === \"editing\" || state === \"toolRunning\") {\n applyState(\"thinking\", ctx, { force: true });\n }\n });\n\n pi.on(\"agent_end\", (event, ctx) => {\n const last = event.messages.at(-1);\n if (!last || last.role !== \"assistant\") {\n applyState(\"idle\", ctx);\n return;\n }\n const stopReason = (last as { stopReason?: string }).stopReason;\n if (stopReason === \"stop\" || stopReason === \"length\") {\n applyState(\"complete\", ctx);\n } else if (stopReason === \"error\" || stopReason === \"aborted\") {\n applyState(\"error\", ctx);\n } else if (stopReason === \"toolUse\") {\n } else {\n applyState(\"idle\", ctx);\n }\n });\n\n pi.on(\"session_shutdown\", (_event, ctx) => {\n clearTransient();\n clearReroll();\n if (ctx.mode === \"tui\") {\n ctx.ui.setWorkingIndicator({ frames: [] });\n ctx.ui.setWorkingMessage(\"\");\n }\n state = \"idle\";\n stateEnteredAt = 0;\n currentKaomoji = \"\";\n });\n}"]}
1
+ {"version":3,"file":"ashxj-spinners.js","sourceRoot":"","sources":["../../src/builtin-extensions/ashxj-spinners.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ExtensionAPI, ExtensionContext } from \"@ashx-j/lunr\";\nimport { spinners } from \"unicode-animations\";\n\n// Spinners come from the `unicode-animations` package; see\n// https://www.npmjs.com/package/unicode-animations for the full catalog.\n// On each state entry we pick a random spinner from that state's pool, and\n// we re-roll the kaomoji on a timer while the state is active (see\n// KAOMOJI_REROLL_MS), so the working-indicator row keeps varying instead of\n// freezing on one (spinner, kaomoji) pair for the whole state.\ntype SpinnerName = keyof typeof spinners;\n\ntype State = \"idle\" | \"startup\" | \"thinking\" | \"searching\" | \"editing\" | \"toolRunning\" | \"complete\" | \"error\";\n\n// `thinking` and `toolRunning` share one combined pool for both spinners and\n// kaomoji, so the two states are visually interchangeable. The combined\n// spinner pool is the union of the former thinking spinners (rain, breathe,\n// checkerboard, sparkle) and the former toolRunning spinners (braille,\n// snake). The combined kaomoji pool is the union of both former kaomoji\n// sets plus the new toolRunning entries below (6 spinners x 14 kaomoji).\nconst THINKING_TOOL_SPINNER_POOL: readonly SpinnerName[] = [\n \"rain\",\n \"breathe\",\n \"checkerboard\",\n \"sparkle\",\n \"braille\",\n \"snake\",\n];\n\nconst THINKING_TOOL_KAOMOJI_POOL: readonly string[] = [\n // formerly `thinking`\n \"(ᵕ—ᴗ—)\", // determined\n \"(˵ ¬ᴗ¬˵)\", // clever\n \"(⸝⸝ᵕᴗᵕ⸝⸝)\", // admiring gaze\n \"⸜(。˃ ᵕ ˂ )⸝♡\", // blushing happy\n \"(˶>⩊<˶)\", // cheerful\n \"૮⸝⸝> ̫ <⸝⸝ ა\", // pleading with worry\n \"(˶ > ₃ < ˶)\", // licking lips\n // formerly `toolRunning`\n \"(๑´>᎑<)~*\", // reaching for hug, eager\n // new\n \"₍^. .^₎⟆\",\n \"(ㅅ´ ˘ `)\",\n \"(„• ֊ •„)\",\n \"(˶˃ ᵕ ˂˶)\",\n \"⡞⠳⣄⣀⣠⠞⢷ ֹ۪\",\n \"(˶•̀ ᎑-˶)\",\n];\n\nconst SPINNER_POOL_FOR_STATE: Record<State, readonly SpinnerName[]> = {\n idle: [],\n startup: [],\n thinking: THINKING_TOOL_SPINNER_POOL,\n searching: [\"diagswipe\", \"scan\", \"cascade\"],\n editing: [\"columns\", \"fillsweep\"],\n toolRunning: THINKING_TOOL_SPINNER_POOL,\n complete: [],\n error: [\"pulse\"],\n};\n\nconst KAOMOJI_FOR_STATE: Record<State, readonly string[]> = {\n // idle, startup, complete are silent: no kaomoji.\n idle: [],\n startup: [],\n thinking: THINKING_TOOL_KAOMOJI_POOL,\n searching: [\n \"(⸝⸝⩌ ⤙ ⩌⸝⸝)\", // confused\n \"(╹ -╹)?\", // questioning\n ],\n editing: [\n \"(ᵕ • ᴗ •)\", // content\n \"(˶ᵔ ᵕ ᵔ˶) ‹𝟹\", // warm with small heart\n \"(๑ᵔ⤙ᵔ๑)\", // gleeful\n ],\n toolRunning: THINKING_TOOL_KAOMOJI_POOL,\n complete: [],\n error: [\n \"(,,>﹏<,,)\", // crying\n \"(⸝⸝ ♡﹏♡⸝⸝)\", // apologetic crying\n \"\\\"૮₍ ˶•⤙•˶ ₎ა\", // pleading paws\n \"(╥﹏╥)\", // heavy crying\n \"(˶°ㅁ°)!!\", // shocked\n \"(,,•᷄﹏‎•᷅,,)\", // distressed\n ],\n};\n\nconst SEARCH_RE = /(search|find|grep|list|ls|fuzzy|query)/i;\nconst EDIT_RE = /(^|[_\\-\\s])(write|edit|patch|create|delete|remove|replace|insert|update|apply_?patch|apply_?edit|str_?replace|str_?replace_?editor|create_?file|delete_?file|write_?file|edit_?file|insert_?content|append_?file|modify_?file)([_\\-\\s]|$)/i;\n\nconst TRANSIENT_MS = 3000;\n// Minimum time a visible \"active\" state must persist before being replaced.\n// Prevents flicker (e.g. error -> thinking clobbering the error before the\n// user can see it). The TTL only gates active->active transitions:\n// - Active -> active: TTL applies (within 3s, the new state is rejected)\n// - Active -> silent (idle/startup/complete): TTL bypassed (end-of-turn)\n// - Active -> error: TTL bypassed (error always wins)\n// - Silent -> active: TTL bypassed (silent states aren't in STATES_WITH_TTL)\n// Auto-revert timers bypass this by calling doApplyState directly.\nconst STATE_TTL_MS = 3000;\nconst STATES_WITH_TTL: ReadonlySet<State> = new Set<State>([\n \"thinking\", \"searching\", \"editing\", \"toolRunning\", \"error\",\n]);\nconst SILENT_STATES: ReadonlySet<State> = new Set<State>([\n \"idle\", \"startup\", \"complete\",\n]);\n\n// How often the kaomoji is re-rolled while an active state is showing. Set\n// long (30 s) so it only kicks in for genuinely long tasks; short states\n// finish before the first re-roll and show a single kaomoji. The spinner is\n// NOT re-rolled on this timer (restarting its animation would stutter); the\n// spinner only changes on state transitions.\nconst KAOMOJI_REROLL_MS = 30000;\n\nfunction pick<T>(arr: readonly T[]): T {\n return arr[Math.floor(Math.random() * arr.length)]!;\n}\n\n// Like pick, but never returns `current` (so a re-roll always produces a\n// visible change). Pools of length <= 1 can't vary, so they return the\n// sole element unchanged.\nfunction pickOther<T>(arr: readonly T[], current: T): T {\n if (arr.length <= 1) return arr[0]!;\n let v: T;\n do {\n v = pick(arr);\n } while (v === current);\n return v;\n}\n\nconst SUBAGENT_TOOL_NAMES = new Set([\"subagent\", \"subagent_wait\"]);\n\n/** True for parent-session tools that mean a child agent is in flight or being waited on. */\nexport function isSubagentToolName(name: string | undefined): boolean {\n return typeof name === \"string\" && SUBAGENT_TOOL_NAMES.has(name);\n}\n\n/**\n * Working-indicator message: spinner is rendered separately (Loader puts it first).\n * While any subagent has run this parent turn: `Orchestrating… kaomoji`.\n */\nexport function composeWorkingMessage(kaomoji: string, orchestrating: boolean): string {\n if (!orchestrating) return kaomoji;\n return kaomoji ? `Orchestrating… ${kaomoji}` : \"Orchestrating…\";\n}\n\nexport default function (pi: ExtensionAPI): void {\n let state: State = \"idle\";\n let stateEnteredAt = 0;\n let currentKaomoji = \"\";\n let liveSubagent = 0;\n let seenSubagentThisTurn = false;\n let transientTimer: ReturnType<typeof setTimeout> | undefined;\n let rerollTimer: ReturnType<typeof setInterval> | undefined;\n\n function orchestrating(): boolean {\n return liveSubagent > 0 || seenSubagentThisTurn;\n }\n\n function publishWorkingMessage(ctx: ExtensionContext): void {\n ctx.ui.setWorkingMessage(composeWorkingMessage(currentKaomoji, orchestrating()));\n }\n\n function clearTransient(): void {\n if (transientTimer !== undefined) {\n clearTimeout(transientTimer);\n transientTimer = undefined;\n }\n }\n\n function clearReroll(): void {\n if (rerollTimer !== undefined) {\n clearInterval(rerollTimer);\n rerollTimer = undefined;\n }\n }\n\n function doApplyState(next: State, ctx: ExtensionContext): void {\n state = next;\n stateEnteredAt = Date.now();\n\n const spinnerPool = SPINNER_POOL_FOR_STATE[next];\n if (spinnerPool.length > 0) {\n const spinnerName = pick(spinnerPool);\n const def = spinners[spinnerName];\n const frames = def.frames.map(f => ctx.ui.theme.fg(\"accent\", f));\n ctx.ui.setWorkingIndicator({ frames, intervalMs: def.interval });\n } else {\n ctx.ui.setWorkingIndicator({ frames: [] });\n }\n\n const kaomojiPool = KAOMOJI_FOR_STATE[next];\n currentKaomoji = kaomojiPool.length > 0 ? pick(kaomojiPool) : \"\";\n publishWorkingMessage(ctx);\n\n clearReroll();\n clearTransient();\n\n // Re-roll the kaomoji periodically while an active state is showing, so\n // a long thinking / toolRunning / error / searching / editing stretch\n // cycles through kaomoji instead of freezing on one. The spinner stays\n // put for the whole state (see KAOMOJI_REROLL_MS). Pools of length <= 1\n // can't vary, so skip them; silent states have empty pools and are also\n // skipped.\n if (kaomojiPool.length > 1) {\n rerollTimer = setInterval(() => rerollKaomoji(ctx), KAOMOJI_REROLL_MS);\n }\n\n if (next === \"complete\" || next === \"startup\") {\n transientTimer = setTimeout(() => doApplyState(\"idle\", ctx), TRANSIENT_MS);\n }\n }\n\n function rerollKaomoji(ctx: ExtensionContext): void {\n const pool = KAOMOJI_FOR_STATE[state];\n if (pool.length <= 1) return;\n currentKaomoji = pickOther(pool, currentKaomoji);\n publishWorkingMessage(ctx);\n }\n\n function applyState(next: State, ctx: ExtensionContext, options: { force?: boolean } = {}): void {\n if (ctx.mode !== \"tui\") return;\n // Enforce minimum TTL on event-driven transitions, with three exceptions:\n // - `error` always wins (so tool_execution_end can always set error).\n // - Silent states (idle, startup, complete) bypass TTL — they're\n // end-of-turn markers, not active states that need minimum display.\n // Without this, agent_end within 3s of turn_start would be rejected,\n // leaving the spinner stuck in the active state.\n // - `force: true` bypasses TTL — used for tool_execution_start so the\n // spinner syncs with the tool bubble the moment it appears.\n // Auto-revert timers bypass this by calling doApplyState directly.\n if (\n !options.force &&\n !SILENT_STATES.has(next) &&\n next !== \"error\" &&\n STATES_WITH_TTL.has(state) &&\n Date.now() - stateEnteredAt < STATE_TTL_MS\n ) {\n return;\n }\n doApplyState(next, ctx);\n }\n\n pi.on(\"session_start\", (event, ctx) => {\n if (ctx.mode !== \"tui\") return;\n // lunr: do not ctx.ui.setTitle here — InteractiveMode owns the OSC title\n // (`lunr - [session -] cwd`). Overwriting it with a brand string made\n // Windows Terminal tabs say \"ashxj\" (or left them as \"node\" until this ran).\n doApplyState(event.reason === \"startup\" ? \"startup\" : \"idle\", ctx);\n });\n\n pi.on(\"turn_start\", (_event, ctx) => {\n applyState(\"thinking\", ctx);\n });\n\n pi.on(\"tool_execution_start\", (event, ctx) => {\n // Hook on tool_execution_start (not tool_call) so the spinner activates\n // at the same moment the tool bubble appears in the UI. tool_call fires\n // earlier (when the LLM decides to call the tool, before execution), and\n // the bubble only appears once the tool actually starts — so hooking on\n // tool_call creates a visual desync between spinner and bubble.\n // Bonus: tools blocked at tool_call never run, so tool_execution_start\n // won't fire and the spinner won't activate for blocked tools.\n // `force: true` bypasses the 3s TTL so the spinner syncs with the\n // bubble immediately, even if the previous active state is still\n // within its minimum display window.\n if (state === \"error\") return;\n const name = event.toolName;\n // lunr: any subagent / subagent_wait this turn prefixes the working row\n // with \"Orchestrating…\" (spinner is already first via Loader).\n if (isSubagentToolName(name)) {\n liveSubagent += 1;\n seenSubagentThisTurn = true;\n }\n const force = true;\n if (SEARCH_RE.test(name)) {\n applyState(\"searching\", ctx, { force });\n } else if (EDIT_RE.test(name)) {\n applyState(\"editing\", ctx, { force });\n } else {\n applyState(\"toolRunning\", ctx, { force });\n }\n });\n\n pi.on(\"tool_execution_end\", (event, ctx) => {\n if (isSubagentToolName(event.toolName) && liveSubagent > 0) {\n liveSubagent -= 1;\n }\n if (event.isError) {\n applyState(\"error\", ctx);\n }\n });\n\n pi.on(\"turn_end\", (_event, ctx) => {\n // A turn that ran a tool ends with the spinner still on the tool state\n // (searching/editing/toolRunning). The next turn_start would revert to\n // thinking, but it's TTL-gated and gets rejected for fast tools (<3s),\n // which leaves the spinner stuck on the tool state for the whole next\n // turn. Revert to thinking here, right after the turn's tools finish.\n // turn_end fires once per turn after all its (concurrent) tools\n // complete, so this is safe for parallel tool calls. Only force out of\n // tool states, never out of error (error has priority and is handled\n // by agent_end). No-op on turns that had no tools (state is already\n // thinking).\n if (state === \"searching\" || state === \"editing\" || state === \"toolRunning\") {\n applyState(\"thinking\", ctx, { force: true });\n }\n });\n\n pi.on(\"agent_end\", (event, ctx) => {\n liveSubagent = 0;\n seenSubagentThisTurn = false;\n const last = event.messages.at(-1);\n if (!last || last.role !== \"assistant\") {\n applyState(\"idle\", ctx);\n return;\n }\n const stopReason = (last as { stopReason?: string }).stopReason;\n if (stopReason === \"stop\" || stopReason === \"length\") {\n applyState(\"complete\", ctx);\n } else if (stopReason === \"error\" || stopReason === \"aborted\") {\n applyState(\"error\", ctx);\n } else if (stopReason === \"toolUse\") {\n } else {\n applyState(\"idle\", ctx);\n }\n });\n\n pi.on(\"session_shutdown\", (_event, ctx) => {\n clearTransient();\n clearReroll();\n if (ctx.mode === \"tui\") {\n ctx.ui.setWorkingIndicator({ frames: [] });\n ctx.ui.setWorkingMessage(\"\");\n }\n state = \"idle\";\n stateEnteredAt = 0;\n currentKaomoji = \"\";\n liveSubagent = 0;\n seenSubagentThisTurn = false;\n });\n}"]}
@@ -57,6 +57,10 @@ interface KeybindingsLike {
57
57
  /** Trimmed view of pi's `ReadonlyFooterDataProvider`. */
58
58
  interface ReadonlyFooterDataProvider {
59
59
  getGitBranch(): string | null;
60
+ getGitDiffstat?(): {
61
+ added: number;
62
+ removed: number;
63
+ } | null;
60
64
  getExtensionStatuses(): ReadonlyMap<string, string>;
61
65
  getAvailableProviderCount(): number;
62
66
  onBranchChange(callback: () => void): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"ashxj-tui.d.ts","sourceRoot":"","sources":["../../src/builtin-extensions/ashxj-tui.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ashxj-tui — a custom slim chatbox for pi.\n *\n * Replaces pi-zentui's prompt box + statusline with:\n * - A rounded prompt box that auto-expands as the typed message wraps to\n * multiple rows (the sides extend; height grows with content).\n * - A right-aligned chip on the box's BOTTOM border:\n * model · provider · effort (e.g. `glm-5.2 · Ollama Cloud · xhigh`)\n * - A slim stats line (extension statuses · context% · ↑↓; each segment\n * toggle-gated via the lunr customize bridge) BELOW the box, not wrapped\n * around the input.\n * - No session-mode indicator anywhere (pi has no such concept).\n *\n * Loaded by pi via jiti — no build step, plain TypeScript.\n *\n * NOTE on type imports (see `ashxj-thinking`/`simple-memory` for the convention):\n * This file declares structural types inline rather than importing them from\n * `@ashx-j/lunr` / `@ashx-j/lunr-tui`.\n * pi-coding-agent's `index.d.ts` re-exports from internal `.d.ts` files that\n * contain subpath imports like `@ashx-j/lunr-ai/compat`, which do not\n * resolve under `tsc` with a file argument. We keep the typecheck\n * self-contained with inline types and verify via the included `tsconfig.json`\n * (`moduleResolution: \"Bundler\"` + `skipLibCheck: true`).\n *\n * The ONE exception is `CustomEditor`, imported as a RUNTIME VALUE directly\n * from its defining module (`modes/interactive/components/custom-editor.ts`).\n * We must `extends` it to inherit pi's app keybindings (escape, ctrl+d, model\n * switching, extension shortcuts) — wrapping would lose those. It is imported\n * from the concrete module, NOT the package barrel, because the barrel\n * re-exports main.ts which imports this file — a circular import that crashes\n * under vite-node (`Class extends value undefined`).\n */\n\nimport { CustomEditor } from \"../modes/interactive/components/custom-editor.js\";\n\n// lunr: customize bridge — read the prompt-symbol toggle (bridgeless → no symbol).\nconst PROMPT_SYMBOL_BRIDGE = Symbol.for(\"@lunr/customize\");\nconst PERMISSION_MODE_BRIDGE = Symbol.for(\"@lunr/permission-mode\");\ninterface CustomizeBridgeForPrompt {\n\tgetPromptSymbol(): boolean;\n\t// lunr: footer element toggles (added to the same bridge).\n\tgetFooterMcp?(): boolean;\n\tgetFooterLsp?(): boolean;\n\tgetFooterContext?(): boolean;\n\tgetFooterTokens?(): boolean;\n\tgetFooterStatuses?(): boolean;\n}\ninterface PermissionModeBridgeForFooter {\n\tgetMode(): string | undefined;\n}\nfunction lunrCustomizeBridge(): CustomizeBridgeForPrompt | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[PROMPT_SYMBOL_BRIDGE] as CustomizeBridgeForPrompt | undefined;\n}\nfunction lunrPermissionModeBridge(): PermissionModeBridgeForFooter | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[PERMISSION_MODE_BRIDGE] as PermissionModeBridgeForFooter | undefined;\n}\nfunction lunrPromptSymbolEnabled(): boolean {\n\treturn lunrCustomizeBridge()?.getPromptSymbol() ?? false;\n}\n// lunr: footer toggles, read at render time. Bridgeless / missing-getter fallback\n// matches the settings-manager defaults: MCP on, LSP off, everything else on.\nfunction lunrFooterToggles(): { mcp: boolean; lsp: boolean; context: boolean; tokens: boolean; statuses: boolean } {\n\tconst bridge = lunrCustomizeBridge();\n\treturn {\n\t\tmcp: bridge?.getFooterMcp?.() ?? true,\n\t\tlsp: bridge?.getFooterLsp?.() ?? false,\n\t\tcontext: bridge?.getFooterContext?.() ?? true,\n\t\ttokens: bridge?.getFooterTokens?.() ?? true,\n\t\tstatuses: bridge?.getFooterStatuses?.() ?? true,\n\t};\n}\n// lunr: permission mode for the footer safety indicator (always shown).\nfunction lunrPermissionMode(): string | undefined {\n\treturn lunrPermissionModeBridge()?.getMode();\n}\n// lunr: prompt glyph comes from the theme tokens `glyphs.promptMoon` /\n// `glyphs.promptArrow` (defaults below match moon.json). An empty token hides\n// that part; both empty = no glyph at all.\nfunction lunrPromptGlyph(theme: Theme | undefined): string {\n\tconst moon = theme?.glyph?.(\"promptMoon\") ?? \"☾\";\n\tconst arrow = theme?.glyph?.(\"promptArrow\") ?? \">\";\n\tconst parts = [moon, arrow].filter((s) => s !== \"\");\n\treturn parts.length > 0 ? `${parts.join(\" \")} ` : \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Minimal structural types (inline — see file header)\n// ---------------------------------------------------------------------------\n\ntype ThinkingLevel = \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\n/** Theme token for the chatbox thinking-level border / effort chip. */\nexport function thinkingThemeToken(level: ThinkingLevel): string {\n\tswitch (level) {\n\t\tcase \"off\":\n\t\t\treturn \"thinkingOff\";\n\t\tcase \"minimal\":\n\t\t\treturn \"thinkingMinimal\";\n\t\tcase \"low\":\n\t\t\treturn \"thinkingLow\";\n\t\tcase \"medium\":\n\t\t\treturn \"thinkingMedium\";\n\t\tcase \"high\":\n\t\t\treturn \"thinkingHigh\";\n\t\tcase \"xhigh\":\n\t\t\treturn \"thinkingXhigh\";\n\t\tcase \"max\":\n\t\t\treturn \"thinkingMax\";\n\t\tdefault:\n\t\t\treturn \"thinkingOff\";\n\t}\n}\n\n/** Chip effort label — identity, locked so xhigh/max are never mapped down to high. */\nexport function formatChipEffort(level: ThinkingLevel): string {\n\treturn level;\n}\n\n/** Trimmed view of pi's `Theme` — only the methods we use. */\ninterface Theme {\n\tfg(token: string, text: string): string;\n\t/** lunr: prompt glyph tokens (optional for forward/backward compat). */\n\tglyph?(name: \"promptMoon\" | \"promptArrow\"): string;\n}\n\n/** Trimmed view of `pi-tui`'s `TUI` — only the method we use. */\ninterface TUIHandle {\n\trequestRender(force?: boolean): void;\n}\n\n/** Trimmed view of `pi-tui`'s `EditorTheme`. */\ninterface EditorThemeLike {\n\tborderColor: (str: string) => string;\n\tselectList: unknown;\n}\n\n/** Opaque keybindings manager. */\ninterface KeybindingsLike {\n\treadonly _?: unknown;\n}\n\n/** Trimmed view of an autocomplete list (the editor's private `autocompleteList`). */\ninterface AutocompleteListLike {\n\trender(width: number): string[];\n}\n\n/** Trimmed view of pi's `ReadonlyFooterDataProvider`. */\ninterface ReadonlyFooterDataProvider {\n\tgetGitBranch(): string | null;\n\tgetExtensionStatuses(): ReadonlyMap<string, string>;\n\tgetAvailableProviderCount(): number;\n\tonBranchChange(callback: () => void): () => void;\n}\n\n/** Trimmed view of the `getContextUsage` return value. */\ninterface ContextUsage {\n\ttokens: number | null;\n\tcontextWindow: number;\n\tpercent: number | null;\n}\n\n/** Trimmed view of an assistant message's usage — the bits we render. */\ninterface AssistantUsage {\n\tinput: number;\n\toutput: number;\n\tcost: { total: number };\n}\n\n/** Trimmed view of an assistant message. */\ninterface AssistantMessageLike {\n\trole: \"assistant\";\n\tusage: AssistantUsage;\n}\n\n/** Trimmed view of a session entry — only what we iterate over. */\ntype SessionEntry =\n\t| { type: \"message\"; message: AssistantMessageLike }\n\t| { type: string; message?: unknown };\n\n/** Trimmed view of a `ReadonlySessionManager`. */\ninterface ReadonlySessionManagerLike {\n\tgetEntries(): readonly SessionEntry[];\n\tgetBranch?(): readonly SessionEntry[];\n}\n\n/** Trimmed view of a `Model`. */\ninterface ModelLike {\n\tid?: string;\n\tprovider?: string;\n\tcontextWindow?: number;\n\treasoning?: boolean;\n\tthinkingLevelMap?: Record<string, string | null>;\n}\n\ninterface ExtensionUIContextLike {\n\tsetEditorComponent(factory: EditorFactoryLike | undefined): void;\n\tsetFooter(factory: FooterFactoryLike | undefined): void;\n\treadonly theme?: Theme;\n}\n\n/** Trimmed view of `ExtensionContext`. */\ninterface ExtensionContextLike {\n\tmode: \"tui\" | \"rpc\" | \"json\" | \"print\";\n\thasUI: boolean;\n\tmodel: ModelLike | undefined;\n\tsessionManager: ReadonlySessionManagerLike;\n\tgetContextUsage(): ContextUsage | undefined;\n\tui: ExtensionUIContextLike;\n}\n\n/** Trimmed view of `ExtensionAPI`. */\ninterface ExtensionAPI {\n\tgetThinkingLevel(): ThinkingLevel;\n\ton(event: string, handler: (event: unknown, ctx: ExtensionContextLike) => void): void;\n}\n\n/** `setEditorComponent` factory shape. */\ntype EditorFactoryLike = (\n\ttui: TUIHandle,\n\ttheme: EditorThemeLike,\n\tkeybindings: KeybindingsLike,\n) => EditorComponentLike;\n\ninterface EditorComponentLike {\n\trender(width: number): string[];\n\tgetText(): string;\n\tsetText(text: string): void;\n\tinvalidate(): void;\n\thandleInput?(data: string): void;\n}\n\n/** `setFooter` factory shape. */\ntype FooterFactoryLike = (\n\ttui: TUIHandle,\n\ttheme: Theme,\n\tfooterData: ReadonlyFooterDataProvider,\n) => FooterComponentLike;\n\ninterface FooterComponentLike {\n\trender(width: number): string[];\n\tdispose?(): void;\n\tinvalidate?(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Display-width helpers (ANSI-aware; CJK-aware)\n//\n// The editor (`Editor.render`) word-wraps to the given width using grapheme\n// width, so each returned line is ≤ that many visual columns. To right-pad a\n// line for our box we need a width model that matches closely enough: skip ALL\n// escape sequences (SGR color, OSC/DCS/APC/PM/SOS string sequences such as\n// pi-tui's `CURSOR_MARKER` `ESC _pi:c BEL`, and two-char escapes), count common\n// East-Asian wide ranges as 2, combining marks and variation selectors as 0.\n// Handling the string sequences as 0-width is what keeps the right `│` rail\n// aligned on the focused (cursor) line — the base editor embeds the marker and\n// the TUI strips it at flush time, so we must not count it here.\n// ---------------------------------------------------------------------------\n\nfunction skipAnsi(str: string, pos: number): number {\n\tif (str.charCodeAt(pos) !== 0x1b) return 0;\n\tconst next = str.charCodeAt(pos + 1);\n\t// CSI: ESC [ ... <final 0x40-0x7e>\n\tif (next === 0x5b /* [ */) {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tconst c = str.charCodeAt(j);\n\t\t\tif (c >= 0x40 && c <= 0x7e) return j + 1 - pos;\n\t\t\tj++;\n\t\t}\n\t\treturn str.length - pos; // unterminated; consume the rest\n\t}\n\t// String sequences — OSC (]), DCS (P), APC (_), PM (^), SOS (X) — terminated\n\t// by BEL (0x07) or ST (ESC \\). pi-tui's CURSOR_MARKER `ESC _pi:c BEL` is APC;\n\t// OSC color queries are OSC. All are zero visual width.\n\tif (\n\t\tnext === 0x5d /* ] */ ||\n\t\tnext === 0x50 /* P */ ||\n\t\tnext === 0x5f /* _ */ ||\n\t\tnext === 0x5e /* ^ */ ||\n\t\tnext === 0x58 /* X */\n\t) {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tconst c = str.charCodeAt(j);\n\t\t\tif (c === 0x07 /* BEL */) return j + 1 - pos;\n\t\t\tif (c === 0x1b && str.charCodeAt(j + 1) === 0x5c /* \\ */) return j + 2 - pos;\n\t\t\tj++;\n\t\t}\n\t\treturn str.length - pos; // unterminated; consume the rest\n\t}\n\t// Any other ESC sequence: consume ESC + one final byte (e.g. ESC c, ESC \\,\n\t// ESC ( B). A lone trailing ESC with no following byte is 0 width too.\n\treturn Number.isNaN(next) ? 1 : 2;\n}\n\nfunction isWide(cp: number): boolean {\n\treturn (\n\t\t(cp >= 0x1100 && cp <= 0x115f) ||\n\t\t(cp >= 0x2e80 && cp <= 0x303e) ||\n\t\t(cp >= 0x3041 && cp <= 0x33ff) ||\n\t\t(cp >= 0x3400 && cp <= 0x4dbf) ||\n\t\t(cp >= 0x4e00 && cp <= 0x9fff) ||\n\t\t(cp >= 0xa000 && cp <= 0xa4cf) ||\n\t\t(cp >= 0xac00 && cp <= 0xd7a3) ||\n\t\t(cp >= 0xf900 && cp <= 0xfaff) ||\n\t\t(cp >= 0xfe30 && cp <= 0xfe4f) ||\n\t\t(cp >= 0xff00 && cp <= 0xff60) ||\n\t\t(cp >= 0xffe0 && cp <= 0xffe6) ||\n\t\t(cp >= 0x1f300 && cp <= 0x1faff) ||\n\t\t(cp >= 0x20000 && cp <= 0x3fffd)\n\t);\n}\n\nfunction charWidth(cp: number): number {\n\tif (cp >= 0x20 && cp < 0x7f) return 1;\n\tif ((cp >= 0x0300 && cp <= 0x036f) || (cp >= 0xfe00 && cp <= 0xfe0f) || cp === 0x200d) return 0;\n\treturn isWide(cp) ? 2 : 1;\n}\n\n/** Visible width of a string, ignoring SGR color codes. */\nfunction displayWidth(str: string): number {\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < str.length) {\n\t\tconst skip = skipAnsi(str, i);\n\t\tif (skip > 0) {\n\t\t\ti += skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst cp = str.codePointAt(i) ?? 0;\n\t\tw += charWidth(cp);\n\t\ti += cp > 0xffff ? 2 : 1;\n\t}\n\treturn w;\n}\n\n/** Right-pad a (possibly colored) line to `width` visible columns. */\nfunction padRight(line: string, width: number): string {\n\tconst w = displayWidth(line);\n\tif (w >= width) return line;\n\treturn line + \" \".repeat(width - w);\n}\n\n/** Truncate a plain (non-ANSI) string to `maxWidth` visible columns. */\nfunction truncatePlain(text: string, maxWidth: number): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (displayWidth(text) <= maxWidth) return text;\n\tlet out = \"\";\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst code = text.codePointAt(i) ?? 0;\n\t\tconst cw = charWidth(code);\n\t\tif (w + cw > maxWidth) break;\n\t\tout += String.fromCodePoint(code);\n\t\tw += cw;\n\t\ti += code > 0xffff ? 2 : 1;\n\t}\n\treturn out;\n}\n\n/** Truncate a (possibly ANSI-colored) string to `maxWidth` visible columns,\n * re-emitting an SGR reset at the cut so styling doesn't bleed. */\nfunction truncateToWidth(text: string, maxWidth: number, ellipsis = \"\"): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (displayWidth(text) <= maxWidth) return text;\n\tconst target = Math.max(0, maxWidth - displayWidth(ellipsis));\n\tlet out = \"\";\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < text.length && w < target) {\n\t\tconst skip = skipAnsi(text, i);\n\t\tif (skip > 0) {\n\t\t\tout += text.slice(i, i + skip);\n\t\t\ti += skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst code = text.codePointAt(i) ?? 0;\n\t\tconst cw = charWidth(code);\n\t\tif (w + cw > target) break;\n\t\tout += String.fromCodePoint(code);\n\t\tw += cw;\n\t\ti += code > 0xffff ? 2 : 1;\n\t}\n\tout += \"\\x1b[0m\";\n\treturn out + ellipsis;\n}\n\n/** Clamp each rendered line to `width` visible columns (fallback path). */\nfunction clampLines(lines: string[], width: number): string[] {\n\treturn lines.map((l) => (displayWidth(l) > width ? truncateToWidth(l, width, \"\") : l));\n}\n\n/** Apply a theme `fg` token safely; falls back to plain text. */\nfunction color(theme: Theme | undefined, token: string, text: string): string {\n\ttry {\n\t\treturn theme?.fg?.(token, text) ?? text;\n\t} catch {\n\t\treturn text;\n\t}\n}\n\n// lunr: strip ANSI escape sequences so footer statuses can be re-colored uniformly.\nfunction stripAnsi(s: string): string {\n\treturn s.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Stats formatting\n// ---------------------------------------------------------------------------\n\n/** Compact token-count formatter: `999`, `1.2k`, `15k`, `1.2M`, `15M`.\n * Matches pi's FooterComponent / the captured zentui examples\n * (`↑9.0M ↓112k`, `20%/1.0M`). */\nfunction formatCount(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/** Sum input/output tokens across assistant messages in the session. */\nfunction getUsageTotals(ctx: ExtensionContextLike): { input: number; output: number } {\n\tlet input = 0;\n\tlet output = 0;\n\tconst entries = ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch?.() ?? [];\n\tfor (const entry of entries as readonly SessionEntry[]) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst m = entry.message as AssistantMessageLike | undefined;\n\t\tif (!m || m.role !== \"assistant\") continue;\n\t\tconst u = m.usage;\n\t\tif (!u) continue;\n\t\tinput += u.input ?? 0;\n\t\toutput += u.output ?? 0;\n\t}\n\treturn { input, output };\n}\n\n// ---------------------------------------------------------------------------\n// Provider display label\n//\n// `ctx.model.provider` is a ProviderId string (e.g. `\"ollama-cloud\"`), NOT the\n// human label. `BUILT_IN_PROVIDER_DISPLAY_NAMES` exists in pi core but is not\n// re-exported to extensions, so we derive the label (mirrors zentui's\n// `formatProviderLabel`, with an explicit `ollama-cloud` entry).\n// ---------------------------------------------------------------------------\n\nfunction formatProviderLabel(provider: string | undefined): string {\n\tif (!provider) return \"Unknown\";\n\tconst known: Record<string, string> = {\n\t\tanthropic: \"Anthropic\",\n\t\tgemini: \"Google\",\n\t\tgoogle: \"Google\",\n\t\tollama: \"Ollama\",\n\t\t\"ollama-cloud\": \"Ollama Cloud\",\n\t\topenai: \"OpenAI\",\n\t\t\"openai-codex\": \"OpenAI\",\n\t\tmistral: \"Mistral\",\n\t};\n\treturn (\n\t\tknown[provider] ??\n\t\tprovider.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase())\n\t);\n}\n\n// ---------------------------------------------------------------------------\n// The chip: `model · provider · effort`\n// ---------------------------------------------------------------------------\n\nfunction colorEffort(theme: Theme | undefined, level: ThinkingLevel, text: string): string {\n\tconst token = thinkingThemeToken(level);\n\tconst painted = color(theme, token, text);\n\tif (token === \"thinkingMax\" && painted === text) {\n\t\treturn color(theme, \"thinkingXhigh\", text);\n\t}\n\treturn painted;\n}\n\nfunction buildChip(ctx: ExtensionContextLike, pi: ExtensionAPI): { left: string; effort: string; level: ThinkingLevel } {\n\tconst modelId = ctx.model?.id ?? \"no-model\";\n\tconst providerLabel = formatProviderLabel(ctx.model?.provider);\n\tconst level = pi.getThinkingLevel();\n\tconst effort = formatChipEffort(level);\n\treturn { left: `${modelId} \\u00b7 ${providerLabel} \\u00b7 `, effort, level };\n}\n\n// ---------------------------------------------------------------------------\n// The prompt box — `ChatboxEditor extends CustomEditor`\n// ---------------------------------------------------------------------------\n\nclass ChatboxEditor extends CustomEditor {\n\tprivate readonly ctx: ExtensionContextLike;\n\tprivate readonly pi: ExtensionAPI;\n\n\tconstructor(\n\t\ttui: TUIHandle,\n\t\ttheme: EditorThemeLike,\n\t\tkeybindings: KeybindingsLike,\n\t\tctx: ExtensionContextLike,\n\t\tpi: ExtensionAPI,\n\t) {\n\t\t// CustomEditor ctor: (tui, theme, keybindings, options?: EditorOptions).\n\t\t// Casts keep the inline types from leaking into the real base signature.\n\t\tsuper(tui as unknown as never, theme as unknown as never, keybindings as unknown as never, {\n\t\t\tpaddingX: 0,\n\t\t});\n\t\tthis.ctx = ctx;\n\t\tthis.pi = pi;\n\t\t// lunr: do not clobber borderColor — InteractiveMode.updateEditorBorderColor()\n\t\t// owns it (thinkingOff…thinkingMax, or bashMode).\n\t}\n\n\tprivate color(token: string, text: string): string {\n\t\treturn color(this.ctx.ui?.theme, token, text);\n\t}\n\n\toverride render(width: number): string[] {\n\t\t// Too narrow to draw a box — defer to the base editor, clamped.\n\t\tif (width <= 4) {\n\t\t\treturn clampLines(super.render(width), width);\n\t\t}\n\n\t\t// Rails: `│ ` (left) + ` │` (right) => 4 columns of chrome.\n\t\tconst promptSymbol = lunrPromptSymbolEnabled();\n\t\tconst glyph = lunrPromptGlyph(this.ctx.ui?.theme);\n\t\tconst glyphW = promptSymbol ? displayWidth(glyph) : 0;\n\t\tconst innerWidth = Math.max(1, width - 4 - glyphW);\n\t\tconst base = super.render(innerWidth);\n\n\t\t// The base editor appends the autocomplete menu lines (if any) to the END\n\t\t// of its render output. Split them off so the body sits inside the box and\n\t\t// the autocomplete renders BELOW the bottom border (preserves slash\n\t\t// commands). Mirrors zentui's `renderPolishedFrame`.\n\t\tconst showing = this.isShowingAutocomplete();\n\t\tconst acList = (this as unknown as { autocompleteList?: AutocompleteListLike }).autocompleteList;\n\t\tlet acCount = 0;\n\t\tif (showing && acList && typeof acList.render === \"function\") {\n\t\t\ttry {\n\t\t\t\tacCount = acList.render(innerWidth).length;\n\t\t\t} catch {\n\t\t\t\tacCount = 0;\n\t\t\t}\n\t\t}\n\n\t\t// `super.render` returns [base top ─ border, ...wrapped text lines, base\n\t\t// bottom ─ border, ...autocomplete lines] — the base editor draws its OWN\n\t\t// straight borders and (when active) appends the autocomplete menu. Split the\n\t\t// autocomplete off the end, then STRIP the base's first/last lines (its own\n\t\t// borders) so we don't render them a second time inside our rounded frame.\n\t\t// Mirrors zentui's `editorFrame.slice(1, -1)` (renderPolishedFrame).\n\t\tlet frame = base;\n\t\tlet acLines: string[] = [];\n\t\tif (acCount > 0 && acCount < frame.length) {\n\t\t\tacLines = frame.slice(frame.length - acCount);\n\t\t\tframe = frame.slice(0, frame.length - acCount);\n\t\t}\n\t\tconst inner = frame.length >= 2 ? frame.slice(1, frame.length - 1) : frame;\n\t\tconst body = inner.length > 0 ? inner : [\"\"];\n\n\t\tconst border = (s: string): string => this.borderColor(s);\n\n\t\t// Top border: ╭─…─╮\n\t\tconst top = border(\"\\u256d\" + \"\\u2500\".repeat(width - 2) + \"\\u256e\");\n\n\t\t// Body: │ <padded line> │ (auto-grows with the number of wrapped lines)\n\t\t// lunr: prefix the first body line with the dim theme prompt glyph (☾ > by\n\t\t// default); a same-width blank gutter keeps subsequent lines aligned.\n\t\tconst gutter = glyphW > 0 ? \" \".repeat(glyphW) : \"\";\n\t\tconst bodyLines = body.map((ln: string, i: number) => {\n\t\t\tconst prefix = i === 0 && glyphW > 0 ? this.color(\"dim\", glyph) : gutter;\n\t\t\treturn border(\"\\u2502 \") + prefix + padRight(ln, innerWidth) + border(\" \\u2502\");\n\t\t});\n\n\t\t// Bottom border with the right-aligned chip: ╰─…─ <chip> ─╯\n\t\tconst bottom = this.renderBottomBorder(width);\n\n\t\treturn [top, ...bodyLines, bottom, ...acLines];\n\t}\n\n\tprivate renderBottomBorder(width: number): string {\n\t\tconst border = (s: string): string => this.borderColor(s);\n\t\tconst parts = buildChip(this.ctx, this.pi);\n\t\tconst plain = parts.left + parts.effort;\n\t\tconst overhead = 4;\n\t\tconst maxChip = Math.max(0, width - overhead);\n\t\tconst truncated = displayWidth(plain) > maxChip;\n\t\tconst chipPlain = truncated ? truncatePlain(plain, maxChip) : plain;\n\t\tconst chipW = displayWidth(chipPlain);\n\t\tconst dashTotal = Math.max(0, width - overhead - chipW);\n\t\t// Right-align the chip: a single trailing dash before the corner, the\n\t\t// rest lead (right-aligns the chip near the corner).\n\t\tconst rightDashes = Math.min(dashTotal, 1);\n\t\tconst leftDashes = dashTotal - rightDashes;\n\n\t\tconst left = \"\\u2570\" + \"\\u2500\".repeat(leftDashes) + \" \";\n\t\tconst right = \" \" + \"\\u2500\".repeat(rightDashes) + \"\\u256f\";\n\t\tconst theme = this.ctx.ui?.theme;\n\t\t// Truncated chips may have lost the effort token — paint all white.\n\t\t// Otherwise model/provider stay white and the effort uses the thinking token.\n\t\tconst chip = truncated\n\t\t\t? this.color(\"white\", chipPlain)\n\t\t\t: this.color(\"white\", parts.left) + colorEffort(theme, parts.level, parts.effort);\n\t\treturn border(left) + chip + border(right);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// The stats line (footer) — slim, single line, BELOW the box\n// ---------------------------------------------------------------------------\n\nfunction renderStatsLine(\n\twidth: number,\n\tctx: ExtensionContextLike,\n\ttheme: Theme,\n\tfooterData: ReadonlyFooterDataProvider,\n): string[] {\n\tconst sep = color(theme, \"accent2\", \" | \"); // lunr: theme-polish — separator uses subtle accent2 blue (was bright-black plain fallback)\n\tconst parts: string[] = [];\n\n\t// lunr: mode zone — permission mode + agent-mode statuses (plan/goal/swarm/research)\n\t// render as ONE segment (dim middot-separated), not scattered across piped sections.\n\tconst modeZone: string[] = [];\n\n\t// lunr: permission mode safety indicator — always shown (not toggle-gated).\n\tconst mode = lunrPermissionMode();\n\tif (mode === \"yolo\" || mode === \"auto\") modeZone.push(color(theme, \"warning\", mode));\n\telse if (mode === \"manual\" || mode === \"plan\") modeZone.push(color(theme, \"white\", mode)); // lunr: theme-polish — manual/plan read white (was dim)\n\n\t// lunr: footer element toggles from the customize bridge (read at render time).\n\tconst footerToggles = lunrFooterToggles();\n\n\t// 1) Extension statuses published by other extensions (rendered as-is,\n\t// already styled): LSP (`pi-lsp-extension`), MCP (`pi-mcp-adapter`),\n\t// throughput/tokens (`pi-tps-was-taken`). These are NOT on `ctx` directly.\n\tconst statuses = footerData.getExtensionStatuses?.();\n\tif (statuses && statuses.size > 0) {\n\t\t// lunr: status segments are toggle-gated via the customize bridge:\n\t\t// footerStatuses gates plan/goal/swarm/research/tps, footerMcp gates\n\t\t// mcp/mcp-auth, footerLsp gates lsp. Publishers keep calling\n\t\t// ctx.ui.setStatus harmlessly when their segment is hidden.\n\t\t// lunr: plan/goal/swarm/research join the mode zone; tps/mcp/lsp stay piped.\n\t\tconst modeKeys: string[] = footerToggles.statuses ? [\"plan\", \"goal\", \"swarm\", \"research\"] : [];\n\t\tfor (const key of modeKeys) {\n\t\t\tconst v = statuses.get(key);\n\t\t\tif (v) modeZone.push(color(theme, \"white\", stripAnsi(v)));\n\t\t}\n\t\tconst pipedKeys: string[] = [];\n\t\tif (footerToggles.statuses) pipedKeys.push(\"tps\");\n\t\tif (footerToggles.mcp) pipedKeys.push(\"mcp\", \"mcp-auth\");\n\t\tif (footerToggles.lsp) pipedKeys.push(\"lsp\");\n\t\tfor (const key of pipedKeys) {\n\t\t\tconst v = statuses.get(key);\n\t\t\t// lunr: unify footer status colors (white, was dim) so the whole stats line reads as one tone.\n\t\t\tif (v) parts.push(color(theme, \"white\", stripAnsi(v))); // lunr: theme-polish — status segments white (was dim)\n\t\t}\n\t}\n\tif (modeZone.length > 0) {\n\t\tparts.unshift(modeZone.join(color(theme, \"dim\", \" · \")));\n\t}\n\n\t// 2) Context usage: `pct/window` (lunr: gated on footerContext).\n\tif (footerToggles.context) {\n\t\tconst usage = ctx.getContextUsage();\n\t\tconst win = ctx.model?.contextWindow ?? usage?.contextWindow ?? 0;\n\t\tconst pct = usage?.percent == null ? \"?\" : `${Math.round(usage.percent)}%`;\n\t\tconst ctxSeg = `${pct}/${formatCount(win)}`;\n\t\tconst pv = usage?.percent ?? 0;\n\t\tif (pv > 90) parts.push(color(theme, \"error\", ctxSeg));\n\t\telse if (pv > 70) parts.push(color(theme, \"warning\", ctxSeg));\n\t\telse parts.push(color(theme, \"white\", ctxSeg)); // lunr: theme-polish — normal context % white (was dim)\n\t}\n\n\t// 3) Token totals: ↑in ↓out (lunr: gated on footerTokens).\n\tif (footerToggles.tokens) {\n\t\tconst totals = getUsageTotals(ctx);\n\t\tparts.push(color(theme, \"white\", `\\u2191${formatCount(totals.input)} \\u2193${formatCount(totals.output)}`)); // lunr: theme-polish — token totals white (was dim)\n\t}\n\n\t// lunr: cost/usage counter segment removed entirely (both the $x.xxx dollar\n\t// counter for API-key providers and the plan-usage limit bar for OAuth/subscription\n\t// providers). Segments 1-3 (statuses, context, tokens) remain.\n\n\tlet line = parts.join(sep);\n\tif (displayWidth(line) > width) {\n\t\tline = truncateToWidth(line, width, \"\");\n\t}\n\treturn [line];\n}\n\n// ---------------------------------------------------------------------------\n// Install / teardown\n// ---------------------------------------------------------------------------\n\nfunction installEditor(\n\tctx: ExtensionContextLike,\n\tpi: ExtensionAPI,\n\tsetRequestor: (r: () => void) => void,\n): void {\n\tif (typeof ctx.ui.setEditorComponent !== \"function\") return;\n\tconst factory: EditorFactoryLike = (tui, _theme, _keybindings) => {\n\t\tsetRequestor(() => {\n\t\t\ttry {\n\t\t\t\ttui.requestRender();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t});\n\t\treturn new ChatboxEditor(tui, _theme, _keybindings, ctx, pi);\n\t};\n\tctx.ui.setEditorComponent(factory);\n}\n\nfunction installFooter(\n\tctx: ExtensionContextLike,\n\tsetRequestor: (r: () => void) => void,\n): void {\n\tif (typeof ctx.ui.setFooter !== \"function\") return;\n\tlet disposed = false;\n\tconst factory: FooterFactoryLike = (tui, _theme, _footerData) => {\n\t\tsetRequestor(() => {\n\t\t\ttry {\n\t\t\t\ttui.requestRender();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t});\n\t\treturn {\n\t\t\tinvalidate(): void {\n\t\t\t\t// render is pure\n\t\t\t},\n\t\t\tdispose(): void {\n\t\t\t\tdisposed = true;\n\t\t\t},\n\t\t\trender(width: number): string[] {\n\t\t\t\tif (disposed) return [];\n\t\t\t\treturn renderStatsLine(width, ctx, _theme, _footerData);\n\t\t\t},\n\t\t};\n\t};\n\tctx.ui.setFooter(factory);\n}\n\n// ---------------------------------------------------------------------------\n// Extension entry point\n// ---------------------------------------------------------------------------\n\nexport default function (pi: ExtensionAPI): void {\n\tlet renderRequestor: (() => void) | undefined = undefined;\n\tconst requestRender = (): void => {\n\t\ttry {\n\t\t\trenderRequestor?.();\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t};\n\n\tpi.on(\"session_start\", (_event, ctx) => {\n\t\tif (!ctx.hasUI || ctx.mode !== \"tui\") return;\n\t\tinstallEditor(ctx, pi, (r) => {\n\t\t\trenderRequestor = r;\n\t\t});\n\t\tinstallFooter(ctx, (r) => {\n\t\t\tif (!renderRequestor) renderRequestor = r;\n\t\t});\n\t});\n\n\tpi.on(\"session_shutdown\", (_event, ctx) => {\n\t\ttry {\n\t\t\tif (typeof ctx.ui?.setEditorComponent === \"function\") ctx.ui.setEditorComponent(undefined);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t\ttry {\n\t\t\tif (typeof ctx.ui?.setFooter === \"function\") ctx.ui.setFooter(undefined);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t\trenderRequestor = undefined;\n\t});\n\n\t// Re-render triggers: model/provider + effort (chip), and token/cost/context\n\t// (stats line). Event names match those zentui wires.\n\tconst reRenderEvents = [\n\t\t\"model_select\",\n\t\t\"thinking_level_select\",\n\t\t\"agent_end\",\n\t\t\"message_end\",\n\t\t\"tool_execution_end\",\n\t\t\"session_compact\",\n\t];\n\tfor (const ev of reRenderEvents) {\n\t\tpi.on(ev, () => requestRender());\n\t}\n}"]}
1
+ {"version":3,"file":"ashxj-tui.d.ts","sourceRoot":"","sources":["../../src/builtin-extensions/ashxj-tui.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ashxj-tui — a custom slim chatbox for pi.\n *\n * Replaces pi-zentui's prompt box + statusline with:\n * - A rounded prompt box that auto-expands as the typed message wraps to\n * multiple rows (the sides extend; height grows with content).\n * - A right-aligned chip on the box's BOTTOM border:\n * model · provider · effort (e.g. `glm-5.2 · Ollama Cloud · xhigh`)\n * - A slim stats line (extension statuses · context% · ↑↓; each segment\n * toggle-gated via the lunr customize bridge) BELOW the box, not wrapped\n * around the input.\n * - No session-mode indicator anywhere (pi has no such concept).\n *\n * Loaded by pi via jiti — no build step, plain TypeScript.\n *\n * NOTE on type imports (see `ashxj-thinking`/`simple-memory` for the convention):\n * This file declares structural types inline rather than importing them from\n * `@ashx-j/lunr` / `@ashx-j/lunr-tui`.\n * pi-coding-agent's `index.d.ts` re-exports from internal `.d.ts` files that\n * contain subpath imports like `@ashx-j/lunr-ai/compat`, which do not\n * resolve under `tsc` with a file argument. We keep the typecheck\n * self-contained with inline types and verify via the included `tsconfig.json`\n * (`moduleResolution: \"Bundler\"` + `skipLibCheck: true`).\n *\n * The ONE exception is `CustomEditor`, imported as a RUNTIME VALUE directly\n * from its defining module (`modes/interactive/components/custom-editor.ts`).\n * We must `extends` it to inherit pi's app keybindings (escape, ctrl+d, model\n * switching, extension shortcuts) — wrapping would lose those. It is imported\n * from the concrete module, NOT the package barrel, because the barrel\n * re-exports main.ts which imports this file — a circular import that crashes\n * under vite-node (`Class extends value undefined`).\n */\n\nimport { CustomEditor } from \"../modes/interactive/components/custom-editor.js\";\n\n// lunr: customize bridge — read the prompt-symbol toggle (bridgeless → no symbol).\nconst PROMPT_SYMBOL_BRIDGE = Symbol.for(\"@lunr/customize\");\nconst PERMISSION_MODE_BRIDGE = Symbol.for(\"@lunr/permission-mode\");\ninterface CustomizeBridgeForPrompt {\n\tgetPromptSymbol(): boolean;\n\t// lunr: footer element toggles (added to the same bridge).\n\tgetFooterMcp?(): boolean;\n\tgetFooterLsp?(): boolean;\n\tgetFooterContext?(): boolean;\n\tgetFooterTokens?(): boolean;\n\tgetFooterStatuses?(): boolean;\n\tgetFooterGit?(): boolean;\n\tgetFooterPlan?(): boolean;\n\tgetFooterPlanBar?(): boolean;\n}\ninterface PermissionModeBridgeForFooter {\n\tgetMode(): string | undefined;\n}\nfunction lunrCustomizeBridge(): CustomizeBridgeForPrompt | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[PROMPT_SYMBOL_BRIDGE] as CustomizeBridgeForPrompt | undefined;\n}\nfunction lunrPermissionModeBridge(): PermissionModeBridgeForFooter | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[PERMISSION_MODE_BRIDGE] as PermissionModeBridgeForFooter | undefined;\n}\nfunction lunrPromptSymbolEnabled(): boolean {\n\treturn lunrCustomizeBridge()?.getPromptSymbol() ?? false;\n}\n// lunr: footer toggles, read at render time. Bridgeless / missing-getter fallback\n// matches the settings-manager defaults: MCP on, LSP off, everything else on.\nfunction lunrFooterToggles(): {\n\tmcp: boolean;\n\tlsp: boolean;\n\tcontext: boolean;\n\ttokens: boolean;\n\tstatuses: boolean;\n\tgit: boolean;\n\tplan: boolean;\n\tplanBar: boolean;\n} {\n\tconst bridge = lunrCustomizeBridge();\n\treturn {\n\t\tmcp: bridge?.getFooterMcp?.() ?? true,\n\t\tlsp: bridge?.getFooterLsp?.() ?? false,\n\t\tcontext: bridge?.getFooterContext?.() ?? true,\n\t\ttokens: bridge?.getFooterTokens?.() ?? true,\n\t\tstatuses: bridge?.getFooterStatuses?.() ?? true,\n\t\tgit: bridge?.getFooterGit?.() ?? true,\n\t\tplan: bridge?.getFooterPlan?.() ?? true,\n\t\tplanBar: bridge?.getFooterPlanBar?.() ?? true,\n\t};\n}\n\nconst USAGE_SERVICE_BRIDGE = Symbol.for(\"@lunr/usage-service\");\ninterface UsageServiceBridgeForFooter {\n\tpickForFooter?(providerId: string): { label: string; usedPercent: number } | undefined;\n\tprefetch?(providerId: string): void;\n}\nfunction lunrUsageBridge(): UsageServiceBridgeForFooter | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE] as UsageServiceBridgeForFooter | undefined;\n}\n\nfunction compactPlanBar(percent: number, theme: Theme): string {\n\tconst cells = 8;\n\tconst clamped = Math.max(0, Math.min(100, percent));\n\tconst filled = Math.round((clamped / 100) * cells);\n\tconst token = clamped > 90 ? \"error\" : clamped > 70 ? \"warning\" : \"success\";\n\tconst fill = filled > 0 ? color(theme, token, \"█\".repeat(filled)) : \"\";\n\tconst rest = filled < cells ? color(theme, \"dim\", \"░\".repeat(cells - filled)) : \"\";\n\treturn fill + rest;\n}\n// lunr: permission mode for the footer safety indicator (always shown).\nfunction lunrPermissionMode(): string | undefined {\n\treturn lunrPermissionModeBridge()?.getMode();\n}\n// lunr: prompt glyph comes from the theme tokens `glyphs.promptMoon` /\n// `glyphs.promptArrow` (defaults below match moon.json). An empty token hides\n// that part; both empty = no glyph at all.\nfunction lunrPromptGlyph(theme: Theme | undefined): string {\n\tconst moon = theme?.glyph?.(\"promptMoon\") ?? \"☾\";\n\tconst arrow = theme?.glyph?.(\"promptArrow\") ?? \">\";\n\tconst parts = [moon, arrow].filter((s) => s !== \"\");\n\treturn parts.length > 0 ? `${parts.join(\" \")} ` : \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Minimal structural types (inline — see file header)\n// ---------------------------------------------------------------------------\n\ntype ThinkingLevel = \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\n/** Theme token for the chatbox thinking-level border / effort chip. */\nexport function thinkingThemeToken(level: ThinkingLevel): string {\n\tswitch (level) {\n\t\tcase \"off\":\n\t\t\treturn \"thinkingOff\";\n\t\tcase \"minimal\":\n\t\t\treturn \"thinkingMinimal\";\n\t\tcase \"low\":\n\t\t\treturn \"thinkingLow\";\n\t\tcase \"medium\":\n\t\t\treturn \"thinkingMedium\";\n\t\tcase \"high\":\n\t\t\treturn \"thinkingHigh\";\n\t\tcase \"xhigh\":\n\t\t\treturn \"thinkingXhigh\";\n\t\tcase \"max\":\n\t\t\treturn \"thinkingMax\";\n\t\tdefault:\n\t\t\treturn \"thinkingOff\";\n\t}\n}\n\n/** Chip effort label — identity, locked so xhigh/max are never mapped down to high. */\nexport function formatChipEffort(level: ThinkingLevel): string {\n\treturn level;\n}\n\n/** Trimmed view of pi's `Theme` — only the methods we use. */\ninterface Theme {\n\tfg(token: string, text: string): string;\n\t/** lunr: prompt glyph tokens (optional for forward/backward compat). */\n\tglyph?(name: \"promptMoon\" | \"promptArrow\"): string;\n}\n\n/** Trimmed view of `pi-tui`'s `TUI` — only the method we use. */\ninterface TUIHandle {\n\trequestRender(force?: boolean): void;\n}\n\n/** Trimmed view of `pi-tui`'s `EditorTheme`. */\ninterface EditorThemeLike {\n\tborderColor: (str: string) => string;\n\tselectList: unknown;\n}\n\n/** Opaque keybindings manager. */\ninterface KeybindingsLike {\n\treadonly _?: unknown;\n}\n\n/** Trimmed view of an autocomplete list (the editor's private `autocompleteList`). */\ninterface AutocompleteListLike {\n\trender(width: number): string[];\n}\n\n/** Trimmed view of pi's `ReadonlyFooterDataProvider`. */\ninterface ReadonlyFooterDataProvider {\n\tgetGitBranch(): string | null;\n\tgetGitDiffstat?(): { added: number; removed: number } | null;\n\tgetExtensionStatuses(): ReadonlyMap<string, string>;\n\tgetAvailableProviderCount(): number;\n\tonBranchChange(callback: () => void): () => void;\n}\n\n/** Trimmed view of the `getContextUsage` return value. */\ninterface ContextUsage {\n\ttokens: number | null;\n\tcontextWindow: number;\n\tpercent: number | null;\n}\n\n/** Trimmed view of an assistant message's usage — the bits we render. */\ninterface AssistantUsage {\n\tinput: number;\n\toutput: number;\n\tcost: { total: number };\n}\n\n/** Trimmed view of an assistant message. */\ninterface AssistantMessageLike {\n\trole: \"assistant\";\n\tusage: AssistantUsage;\n}\n\n/** Trimmed view of a session entry — only what we iterate over. */\ntype SessionEntry =\n\t| { type: \"message\"; message: AssistantMessageLike }\n\t| { type: string; message?: unknown };\n\n/** Trimmed view of a `ReadonlySessionManager`. */\ninterface ReadonlySessionManagerLike {\n\tgetEntries(): readonly SessionEntry[];\n\tgetBranch?(): readonly SessionEntry[];\n}\n\n/** Trimmed view of a `Model`. */\ninterface ModelLike {\n\tid?: string;\n\tprovider?: string;\n\tcontextWindow?: number;\n\treasoning?: boolean;\n\tthinkingLevelMap?: Record<string, string | null>;\n}\n\ninterface ExtensionUIContextLike {\n\tsetEditorComponent(factory: EditorFactoryLike | undefined): void;\n\tsetFooter(factory: FooterFactoryLike | undefined): void;\n\treadonly theme?: Theme;\n}\n\n/** Trimmed view of `ExtensionContext`. */\ninterface ExtensionContextLike {\n\tmode: \"tui\" | \"rpc\" | \"json\" | \"print\";\n\thasUI: boolean;\n\tmodel: ModelLike | undefined;\n\tsessionManager: ReadonlySessionManagerLike;\n\tgetContextUsage(): ContextUsage | undefined;\n\tui: ExtensionUIContextLike;\n}\n\n/** Trimmed view of `ExtensionAPI`. */\ninterface ExtensionAPI {\n\tgetThinkingLevel(): ThinkingLevel;\n\ton(event: string, handler: (event: unknown, ctx: ExtensionContextLike) => void): void;\n}\n\n/** `setEditorComponent` factory shape. */\ntype EditorFactoryLike = (\n\ttui: TUIHandle,\n\ttheme: EditorThemeLike,\n\tkeybindings: KeybindingsLike,\n) => EditorComponentLike;\n\ninterface EditorComponentLike {\n\trender(width: number): string[];\n\tgetText(): string;\n\tsetText(text: string): void;\n\tinvalidate(): void;\n\thandleInput?(data: string): void;\n}\n\n/** `setFooter` factory shape. */\ntype FooterFactoryLike = (\n\ttui: TUIHandle,\n\ttheme: Theme,\n\tfooterData: ReadonlyFooterDataProvider,\n) => FooterComponentLike;\n\ninterface FooterComponentLike {\n\trender(width: number): string[];\n\tdispose?(): void;\n\tinvalidate?(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Display-width helpers (ANSI-aware; CJK-aware)\n//\n// The editor (`Editor.render`) word-wraps to the given width using grapheme\n// width, so each returned line is ≤ that many visual columns. To right-pad a\n// line for our box we need a width model that matches closely enough: skip ALL\n// escape sequences (SGR color, OSC/DCS/APC/PM/SOS string sequences such as\n// pi-tui's `CURSOR_MARKER` `ESC _pi:c BEL`, and two-char escapes), count common\n// East-Asian wide ranges as 2, combining marks and variation selectors as 0.\n// Handling the string sequences as 0-width is what keeps the right `│` rail\n// aligned on the focused (cursor) line — the base editor embeds the marker and\n// the TUI strips it at flush time, so we must not count it here.\n// ---------------------------------------------------------------------------\n\nfunction skipAnsi(str: string, pos: number): number {\n\tif (str.charCodeAt(pos) !== 0x1b) return 0;\n\tconst next = str.charCodeAt(pos + 1);\n\t// CSI: ESC [ ... <final 0x40-0x7e>\n\tif (next === 0x5b /* [ */) {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tconst c = str.charCodeAt(j);\n\t\t\tif (c >= 0x40 && c <= 0x7e) return j + 1 - pos;\n\t\t\tj++;\n\t\t}\n\t\treturn str.length - pos; // unterminated; consume the rest\n\t}\n\t// String sequences — OSC (]), DCS (P), APC (_), PM (^), SOS (X) — terminated\n\t// by BEL (0x07) or ST (ESC \\). pi-tui's CURSOR_MARKER `ESC _pi:c BEL` is APC;\n\t// OSC color queries are OSC. All are zero visual width.\n\tif (\n\t\tnext === 0x5d /* ] */ ||\n\t\tnext === 0x50 /* P */ ||\n\t\tnext === 0x5f /* _ */ ||\n\t\tnext === 0x5e /* ^ */ ||\n\t\tnext === 0x58 /* X */\n\t) {\n\t\tlet j = pos + 2;\n\t\twhile (j < str.length) {\n\t\t\tconst c = str.charCodeAt(j);\n\t\t\tif (c === 0x07 /* BEL */) return j + 1 - pos;\n\t\t\tif (c === 0x1b && str.charCodeAt(j + 1) === 0x5c /* \\ */) return j + 2 - pos;\n\t\t\tj++;\n\t\t}\n\t\treturn str.length - pos; // unterminated; consume the rest\n\t}\n\t// Any other ESC sequence: consume ESC + one final byte (e.g. ESC c, ESC \\,\n\t// ESC ( B). A lone trailing ESC with no following byte is 0 width too.\n\treturn Number.isNaN(next) ? 1 : 2;\n}\n\nfunction isWide(cp: number): boolean {\n\treturn (\n\t\t(cp >= 0x1100 && cp <= 0x115f) ||\n\t\t(cp >= 0x2e80 && cp <= 0x303e) ||\n\t\t(cp >= 0x3041 && cp <= 0x33ff) ||\n\t\t(cp >= 0x3400 && cp <= 0x4dbf) ||\n\t\t(cp >= 0x4e00 && cp <= 0x9fff) ||\n\t\t(cp >= 0xa000 && cp <= 0xa4cf) ||\n\t\t(cp >= 0xac00 && cp <= 0xd7a3) ||\n\t\t(cp >= 0xf900 && cp <= 0xfaff) ||\n\t\t(cp >= 0xfe30 && cp <= 0xfe4f) ||\n\t\t(cp >= 0xff00 && cp <= 0xff60) ||\n\t\t(cp >= 0xffe0 && cp <= 0xffe6) ||\n\t\t(cp >= 0x1f300 && cp <= 0x1faff) ||\n\t\t(cp >= 0x20000 && cp <= 0x3fffd)\n\t);\n}\n\nfunction charWidth(cp: number): number {\n\tif (cp >= 0x20 && cp < 0x7f) return 1;\n\tif ((cp >= 0x0300 && cp <= 0x036f) || (cp >= 0xfe00 && cp <= 0xfe0f) || cp === 0x200d) return 0;\n\treturn isWide(cp) ? 2 : 1;\n}\n\n/** Visible width of a string, ignoring SGR color codes. */\nfunction displayWidth(str: string): number {\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < str.length) {\n\t\tconst skip = skipAnsi(str, i);\n\t\tif (skip > 0) {\n\t\t\ti += skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst cp = str.codePointAt(i) ?? 0;\n\t\tw += charWidth(cp);\n\t\ti += cp > 0xffff ? 2 : 1;\n\t}\n\treturn w;\n}\n\n/** Right-pad a (possibly colored) line to `width` visible columns. */\nfunction padRight(line: string, width: number): string {\n\tconst w = displayWidth(line);\n\tif (w >= width) return line;\n\treturn line + \" \".repeat(width - w);\n}\n\n/** Truncate a plain (non-ANSI) string to `maxWidth` visible columns. */\nfunction truncatePlain(text: string, maxWidth: number): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (displayWidth(text) <= maxWidth) return text;\n\tlet out = \"\";\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst code = text.codePointAt(i) ?? 0;\n\t\tconst cw = charWidth(code);\n\t\tif (w + cw > maxWidth) break;\n\t\tout += String.fromCodePoint(code);\n\t\tw += cw;\n\t\ti += code > 0xffff ? 2 : 1;\n\t}\n\treturn out;\n}\n\n/** Truncate a (possibly ANSI-colored) string to `maxWidth` visible columns,\n * re-emitting an SGR reset at the cut so styling doesn't bleed. */\nfunction truncateToWidth(text: string, maxWidth: number, ellipsis = \"\"): string {\n\tif (maxWidth <= 0) return \"\";\n\tif (displayWidth(text) <= maxWidth) return text;\n\tconst target = Math.max(0, maxWidth - displayWidth(ellipsis));\n\tlet out = \"\";\n\tlet w = 0;\n\tlet i = 0;\n\twhile (i < text.length && w < target) {\n\t\tconst skip = skipAnsi(text, i);\n\t\tif (skip > 0) {\n\t\t\tout += text.slice(i, i + skip);\n\t\t\ti += skip;\n\t\t\tcontinue;\n\t\t}\n\t\tconst code = text.codePointAt(i) ?? 0;\n\t\tconst cw = charWidth(code);\n\t\tif (w + cw > target) break;\n\t\tout += String.fromCodePoint(code);\n\t\tw += cw;\n\t\ti += code > 0xffff ? 2 : 1;\n\t}\n\tout += \"\\x1b[0m\";\n\treturn out + ellipsis;\n}\n\n/** Clamp each rendered line to `width` visible columns (fallback path). */\nfunction clampLines(lines: string[], width: number): string[] {\n\treturn lines.map((l) => (displayWidth(l) > width ? truncateToWidth(l, width, \"\") : l));\n}\n\n/** Apply a theme `fg` token safely; falls back to plain text. */\nfunction color(theme: Theme | undefined, token: string, text: string): string {\n\ttry {\n\t\treturn theme?.fg?.(token, text) ?? text;\n\t} catch {\n\t\treturn text;\n\t}\n}\n\n// lunr: strip ANSI escape sequences so footer statuses can be re-colored uniformly.\nfunction stripAnsi(s: string): string {\n\treturn s.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Stats formatting\n// ---------------------------------------------------------------------------\n\n/** Compact token-count formatter: `999`, `1.2k`, `15k`, `1.2M`, `15M`.\n * Matches pi's FooterComponent / the captured zentui examples\n * (`↑9.0M ↓112k`, `20%/1.0M`). */\nfunction formatCount(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/** Sum input/output tokens across assistant messages in the session. */\nfunction getUsageTotals(ctx: ExtensionContextLike): { input: number; output: number } {\n\tlet input = 0;\n\tlet output = 0;\n\tconst entries = ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch?.() ?? [];\n\tfor (const entry of entries as readonly SessionEntry[]) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst m = entry.message as AssistantMessageLike | undefined;\n\t\tif (!m || m.role !== \"assistant\") continue;\n\t\tconst u = m.usage;\n\t\tif (!u) continue;\n\t\tinput += u.input ?? 0;\n\t\toutput += u.output ?? 0;\n\t}\n\treturn { input, output };\n}\n\n// ---------------------------------------------------------------------------\n// Provider display label\n//\n// `ctx.model.provider` is a ProviderId string (e.g. `\"ollama-cloud\"`), NOT the\n// human label. `BUILT_IN_PROVIDER_DISPLAY_NAMES` exists in pi core but is not\n// re-exported to extensions, so we derive the label (mirrors zentui's\n// `formatProviderLabel`, with an explicit `ollama-cloud` entry).\n// ---------------------------------------------------------------------------\n\nfunction formatProviderLabel(provider: string | undefined): string {\n\tif (!provider) return \"Unknown\";\n\tconst known: Record<string, string> = {\n\t\tanthropic: \"Anthropic\",\n\t\tgemini: \"Google\",\n\t\tgoogle: \"Google\",\n\t\tollama: \"Ollama\",\n\t\t\"ollama-cloud\": \"Ollama Cloud\",\n\t\topenai: \"OpenAI\",\n\t\t\"openai-codex\": \"OpenAI\",\n\t\tmistral: \"Mistral\",\n\t};\n\treturn (\n\t\tknown[provider] ??\n\t\tprovider.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase())\n\t);\n}\n\n// ---------------------------------------------------------------------------\n// The chip: `model · provider · effort`\n// ---------------------------------------------------------------------------\n\nfunction colorEffort(theme: Theme | undefined, level: ThinkingLevel, text: string): string {\n\tconst token = thinkingThemeToken(level);\n\tconst painted = color(theme, token, text);\n\tif (token === \"thinkingMax\" && painted === text) {\n\t\treturn color(theme, \"thinkingXhigh\", text);\n\t}\n\treturn painted;\n}\n\nfunction buildChip(ctx: ExtensionContextLike, pi: ExtensionAPI): { left: string; effort: string; level: ThinkingLevel } {\n\tconst modelId = ctx.model?.id ?? \"no-model\";\n\tconst providerLabel = formatProviderLabel(ctx.model?.provider);\n\tconst level = pi.getThinkingLevel();\n\tconst effort = formatChipEffort(level);\n\treturn { left: `${modelId} \\u00b7 ${providerLabel} \\u00b7 `, effort, level };\n}\n\n// ---------------------------------------------------------------------------\n// The prompt box — `ChatboxEditor extends CustomEditor`\n// ---------------------------------------------------------------------------\n\nclass ChatboxEditor extends CustomEditor {\n\tprivate readonly ctx: ExtensionContextLike;\n\tprivate readonly pi: ExtensionAPI;\n\n\tconstructor(\n\t\ttui: TUIHandle,\n\t\ttheme: EditorThemeLike,\n\t\tkeybindings: KeybindingsLike,\n\t\tctx: ExtensionContextLike,\n\t\tpi: ExtensionAPI,\n\t) {\n\t\t// CustomEditor ctor: (tui, theme, keybindings, options?: EditorOptions).\n\t\t// Casts keep the inline types from leaking into the real base signature.\n\t\tsuper(tui as unknown as never, theme as unknown as never, keybindings as unknown as never, {\n\t\t\tpaddingX: 0,\n\t\t});\n\t\tthis.ctx = ctx;\n\t\tthis.pi = pi;\n\t\t// lunr: do not clobber borderColor — InteractiveMode.updateEditorBorderColor()\n\t\t// owns it (thinkingOff…thinkingMax, or bashMode).\n\t}\n\n\tprivate color(token: string, text: string): string {\n\t\treturn color(this.ctx.ui?.theme, token, text);\n\t}\n\n\toverride render(width: number): string[] {\n\t\t// Too narrow to draw a box — defer to the base editor, clamped.\n\t\tif (width <= 4) {\n\t\t\treturn clampLines(super.render(width), width);\n\t\t}\n\n\t\t// Rails: `│ ` (left) + ` │` (right) => 4 columns of chrome.\n\t\tconst promptSymbol = lunrPromptSymbolEnabled();\n\t\tconst glyph = lunrPromptGlyph(this.ctx.ui?.theme);\n\t\tconst glyphW = promptSymbol ? displayWidth(glyph) : 0;\n\t\tconst innerWidth = Math.max(1, width - 4 - glyphW);\n\t\tconst base = super.render(innerWidth);\n\n\t\t// The base editor appends the autocomplete menu lines (if any) to the END\n\t\t// of its render output. Split them off so the body sits inside the box and\n\t\t// the autocomplete renders BELOW the bottom border (preserves slash\n\t\t// commands). Mirrors zentui's `renderPolishedFrame`.\n\t\tconst showing = this.isShowingAutocomplete();\n\t\tconst acList = (this as unknown as { autocompleteList?: AutocompleteListLike }).autocompleteList;\n\t\tlet acCount = 0;\n\t\tif (showing && acList && typeof acList.render === \"function\") {\n\t\t\ttry {\n\t\t\t\tacCount = acList.render(innerWidth).length;\n\t\t\t} catch {\n\t\t\t\tacCount = 0;\n\t\t\t}\n\t\t}\n\n\t\t// `super.render` returns [base top ─ border, ...wrapped text lines, base\n\t\t// bottom ─ border, ...autocomplete lines] — the base editor draws its OWN\n\t\t// straight borders and (when active) appends the autocomplete menu. Split the\n\t\t// autocomplete off the end, then STRIP the base's first/last lines (its own\n\t\t// borders) so we don't render them a second time inside our rounded frame.\n\t\t// Mirrors zentui's `editorFrame.slice(1, -1)` (renderPolishedFrame).\n\t\tlet frame = base;\n\t\tlet acLines: string[] = [];\n\t\tif (acCount > 0 && acCount < frame.length) {\n\t\t\tacLines = frame.slice(frame.length - acCount);\n\t\t\tframe = frame.slice(0, frame.length - acCount);\n\t\t}\n\t\tconst inner = frame.length >= 2 ? frame.slice(1, frame.length - 1) : frame;\n\t\tconst body = inner.length > 0 ? inner : [\"\"];\n\n\t\tconst border = (s: string): string => this.borderColor(s);\n\n\t\t// Top border: ╭─…─╮\n\t\tconst top = border(\"\\u256d\" + \"\\u2500\".repeat(width - 2) + \"\\u256e\");\n\n\t\t// Body: │ <padded line> │ (auto-grows with the number of wrapped lines)\n\t\t// lunr: prefix the first body line with the dim theme prompt glyph (☾ > by\n\t\t// default); a same-width blank gutter keeps subsequent lines aligned.\n\t\tconst gutter = glyphW > 0 ? \" \".repeat(glyphW) : \"\";\n\t\tconst bodyLines = body.map((ln: string, i: number) => {\n\t\t\tconst prefix = i === 0 && glyphW > 0 ? this.color(\"dim\", glyph) : gutter;\n\t\t\treturn border(\"\\u2502 \") + prefix + padRight(ln, innerWidth) + border(\" \\u2502\");\n\t\t});\n\n\t\t// Bottom border with the right-aligned chip: ╰─…─ <chip> ─╯\n\t\tconst bottom = this.renderBottomBorder(width);\n\n\t\treturn [top, ...bodyLines, bottom, ...acLines];\n\t}\n\n\tprivate renderBottomBorder(width: number): string {\n\t\tconst border = (s: string): string => this.borderColor(s);\n\t\tconst parts = buildChip(this.ctx, this.pi);\n\t\tconst plain = parts.left + parts.effort;\n\t\tconst overhead = 4;\n\t\tconst maxChip = Math.max(0, width - overhead);\n\t\tconst truncated = displayWidth(plain) > maxChip;\n\t\tconst chipPlain = truncated ? truncatePlain(plain, maxChip) : plain;\n\t\tconst chipW = displayWidth(chipPlain);\n\t\tconst dashTotal = Math.max(0, width - overhead - chipW);\n\t\t// Right-align the chip: a single trailing dash before the corner, the\n\t\t// rest lead (right-aligns the chip near the corner).\n\t\tconst rightDashes = Math.min(dashTotal, 1);\n\t\tconst leftDashes = dashTotal - rightDashes;\n\n\t\tconst left = \"\\u2570\" + \"\\u2500\".repeat(leftDashes) + \" \";\n\t\tconst right = \" \" + \"\\u2500\".repeat(rightDashes) + \"\\u256f\";\n\t\tconst theme = this.ctx.ui?.theme;\n\t\t// Truncated chips may have lost the effort token — paint all white.\n\t\t// Otherwise model/provider stay white and the effort uses the thinking token.\n\t\tconst chip = truncated\n\t\t\t? this.color(\"white\", chipPlain)\n\t\t\t: this.color(\"white\", parts.left) + colorEffort(theme, parts.level, parts.effort);\n\t\treturn border(left) + chip + border(right);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// The stats line (footer) — slim, single line, BELOW the box\n// ---------------------------------------------------------------------------\n\nfunction renderStatsLine(\n\twidth: number,\n\tctx: ExtensionContextLike,\n\ttheme: Theme,\n\tfooterData: ReadonlyFooterDataProvider,\n): string[] {\n\tconst sep = color(theme, \"accent2\", \" | \"); // lunr: theme-polish — separator uses subtle accent2 blue (was bright-black plain fallback)\n\tconst parts: string[] = [];\n\n\t// lunr: mode zone — permission mode + agent-mode statuses (plan/goal/swarm/research)\n\t// render as ONE segment (dim middot-separated), not scattered across piped sections.\n\tconst modeZone: string[] = [];\n\n\t// lunr: permission mode safety indicator — always shown (not toggle-gated).\n\tconst mode = lunrPermissionMode();\n\tif (mode === \"yolo\" || mode === \"auto\") modeZone.push(color(theme, \"warning\", mode));\n\telse if (mode === \"manual\" || mode === \"plan\") modeZone.push(color(theme, \"white\", mode)); // lunr: theme-polish — manual/plan read white (was dim)\n\n\t// lunr: footer element toggles from the customize bridge (read at render time).\n\tconst footerToggles = lunrFooterToggles();\n\n\t// 1) Extension statuses published by other extensions (rendered as-is,\n\t// already styled): LSP (`pi-lsp-extension`), MCP (`pi-mcp-adapter`),\n\t// throughput/tokens (`pi-tps-was-taken`). These are NOT on `ctx` directly.\n\tconst statuses = footerData.getExtensionStatuses?.();\n\tif (statuses && statuses.size > 0) {\n\t\t// lunr: status segments are toggle-gated via the customize bridge:\n\t\t// footerStatuses gates plan/goal/swarm/research/tps, footerMcp gates\n\t\t// mcp/mcp-auth, footerLsp gates lsp. Publishers keep calling\n\t\t// ctx.ui.setStatus harmlessly when their segment is hidden.\n\t\t// lunr: plan/goal/swarm/research join the mode zone; tps/mcp/lsp stay piped.\n\t\tconst modeKeys: string[] = footerToggles.statuses ? [\"plan\", \"goal\", \"swarm\", \"research\"] : [];\n\t\tfor (const key of modeKeys) {\n\t\t\tconst v = statuses.get(key);\n\t\t\tif (v) modeZone.push(color(theme, \"white\", stripAnsi(v)));\n\t\t}\n\t\tconst pipedKeys: string[] = [];\n\t\tif (footerToggles.statuses) pipedKeys.push(\"tps\");\n\t\tif (footerToggles.mcp) pipedKeys.push(\"mcp\", \"mcp-auth\");\n\t\tif (footerToggles.lsp) pipedKeys.push(\"lsp\");\n\t\tfor (const key of pipedKeys) {\n\t\t\tconst v = statuses.get(key);\n\t\t\t// lunr: unify footer status colors (white, was dim) so the whole stats line reads as one tone.\n\t\t\tif (v) parts.push(color(theme, \"white\", stripAnsi(v))); // lunr: theme-polish — status segments white (was dim)\n\t\t}\n\t}\n\tif (modeZone.length > 0) {\n\t\tparts.unshift(modeZone.join(color(theme, \"dim\", \" · \")));\n\t}\n\n\t// lunr: git branch + working-tree +/- vs HEAD (gated on footerGit).\n\tif (footerToggles.git) {\n\t\tconst branch = footerData.getGitBranch?.();\n\t\tif (branch) {\n\t\t\tconst diff = footerData.getGitDiffstat?.();\n\t\t\tlet gitSeg = color(theme, \"white\", branch);\n\t\t\tif (diff && (diff.added !== 0 || diff.removed !== 0)) {\n\t\t\t\tgitSeg +=\n\t\t\t\t\t` ${color(theme, \"success\", `+${diff.added}`)} ${color(theme, \"error\", `-${diff.removed}`)}`;\n\t\t\t}\n\t\t\tparts.push(gitSeg);\n\t\t}\n\t}\n\n\t// lunr: live subscription window bar (gated on footerPlan). Hidden with no plan.\n\tif (footerToggles.plan) {\n\t\tconst provider = ctx.model?.provider;\n\t\tif (provider) {\n\t\t\tconst usage = lunrUsageBridge();\n\t\t\tusage?.prefetch?.(provider);\n\t\t\tconst seg = usage?.pickForFooter?.(provider);\n\t\t\tif (seg) {\n\t\t\t\tconst pct = `${Math.round(seg.usedPercent)}%`;\n\t\t\t\tconst barColor = seg.usedPercent > 90 ? \"error\" : seg.usedPercent > 70 ? \"warning\" : \"success\";\n\t\t\t\tconst bar = footerToggles.planBar ? `${compactPlanBar(seg.usedPercent, theme)} ` : \"\";\n\t\t\t\tparts.push(color(theme, \"white\", seg.label) + \" \" + bar + color(theme, barColor, pct));\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2) Context usage: `pct/window` (lunr: gated on footerContext).\n\tif (footerToggles.context) {\n\t\tconst usage = ctx.getContextUsage();\n\t\tconst win = ctx.model?.contextWindow ?? usage?.contextWindow ?? 0;\n\t\tconst pct = usage?.percent == null ? \"?\" : `${Math.round(usage.percent)}%`;\n\t\tconst ctxSeg = `${pct}/${formatCount(win)}`;\n\t\tconst pv = usage?.percent ?? 0;\n\t\tif (pv > 90) parts.push(color(theme, \"error\", ctxSeg));\n\t\telse if (pv > 70) parts.push(color(theme, \"warning\", ctxSeg));\n\t\telse parts.push(color(theme, \"white\", ctxSeg)); // lunr: theme-polish — normal context % white (was dim)\n\t}\n\n\t// 3) Token totals: ↑in ↓out (lunr: gated on footerTokens).\n\tif (footerToggles.tokens) {\n\t\tconst totals = getUsageTotals(ctx);\n\t\tparts.push(color(theme, \"white\", `\\u2191${formatCount(totals.input)} \\u2193${formatCount(totals.output)}`)); // lunr: theme-polish — token totals white (was dim)\n\t}\n\n\t// lunr: cost/usage counter segment removed entirely (both the $x.xxx dollar\n\t// counter for API-key providers and the plan-usage limit bar for OAuth/subscription\n\t// providers). Segments 1-3 (statuses, context, tokens) remain.\n\n\tlet line = parts.join(sep);\n\tif (displayWidth(line) > width) {\n\t\tline = truncateToWidth(line, width, \"\");\n\t}\n\treturn [line];\n}\n\n// ---------------------------------------------------------------------------\n// Install / teardown\n// ---------------------------------------------------------------------------\n\nfunction installEditor(\n\tctx: ExtensionContextLike,\n\tpi: ExtensionAPI,\n\tsetRequestor: (r: () => void) => void,\n): void {\n\tif (typeof ctx.ui.setEditorComponent !== \"function\") return;\n\tconst factory: EditorFactoryLike = (tui, _theme, _keybindings) => {\n\t\tsetRequestor(() => {\n\t\t\ttry {\n\t\t\t\ttui.requestRender();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t});\n\t\treturn new ChatboxEditor(tui, _theme, _keybindings, ctx, pi);\n\t};\n\tctx.ui.setEditorComponent(factory);\n}\n\nfunction installFooter(\n\tctx: ExtensionContextLike,\n\tsetRequestor: (r: () => void) => void,\n): void {\n\tif (typeof ctx.ui.setFooter !== \"function\") return;\n\tlet disposed = false;\n\tconst factory: FooterFactoryLike = (tui, _theme, _footerData) => {\n\t\tsetRequestor(() => {\n\t\t\ttry {\n\t\t\t\ttui.requestRender();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t});\n\t\treturn {\n\t\t\tinvalidate(): void {\n\t\t\t\t// render is pure\n\t\t\t},\n\t\t\tdispose(): void {\n\t\t\t\tdisposed = true;\n\t\t\t},\n\t\t\trender(width: number): string[] {\n\t\t\t\tif (disposed) return [];\n\t\t\t\treturn renderStatsLine(width, ctx, _theme, _footerData);\n\t\t\t},\n\t\t};\n\t};\n\tctx.ui.setFooter(factory);\n}\n\n// ---------------------------------------------------------------------------\n// Extension entry point\n// ---------------------------------------------------------------------------\n\nexport default function (pi: ExtensionAPI): void {\n\tlet renderRequestor: (() => void) | undefined = undefined;\n\tconst requestRender = (): void => {\n\t\ttry {\n\t\t\trenderRequestor?.();\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t};\n\n\tpi.on(\"session_start\", (_event, ctx) => {\n\t\tif (!ctx.hasUI || ctx.mode !== \"tui\") return;\n\t\tinstallEditor(ctx, pi, (r) => {\n\t\t\trenderRequestor = r;\n\t\t});\n\t\tinstallFooter(ctx, (r) => {\n\t\t\tif (!renderRequestor) renderRequestor = r;\n\t\t});\n\t});\n\n\tpi.on(\"session_shutdown\", (_event, ctx) => {\n\t\ttry {\n\t\t\tif (typeof ctx.ui?.setEditorComponent === \"function\") ctx.ui.setEditorComponent(undefined);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t\ttry {\n\t\t\tif (typeof ctx.ui?.setFooter === \"function\") ctx.ui.setFooter(undefined);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t\trenderRequestor = undefined;\n\t});\n\n\t// Re-render triggers: model/provider + effort (chip), and token/cost/context\n\t// (stats line). Event names match those zentui wires.\n\tconst reRenderEvents = [\n\t\t\"model_select\",\n\t\t\"thinking_level_select\",\n\t\t\"agent_end\",\n\t\t\"message_end\",\n\t\t\"tool_execution_end\",\n\t\t\"session_compact\",\n\t];\n\tfor (const ev of reRenderEvents) {\n\t\tpi.on(ev, () => requestRender());\n\t}\n}"]}
@@ -53,8 +53,24 @@ function lunrFooterToggles() {
53
53
  context: bridge?.getFooterContext?.() ?? true,
54
54
  tokens: bridge?.getFooterTokens?.() ?? true,
55
55
  statuses: bridge?.getFooterStatuses?.() ?? true,
56
+ git: bridge?.getFooterGit?.() ?? true,
57
+ plan: bridge?.getFooterPlan?.() ?? true,
58
+ planBar: bridge?.getFooterPlanBar?.() ?? true,
56
59
  };
57
60
  }
61
+ const USAGE_SERVICE_BRIDGE = Symbol.for("@lunr/usage-service");
62
+ function lunrUsageBridge() {
63
+ return globalThis[USAGE_SERVICE_BRIDGE];
64
+ }
65
+ function compactPlanBar(percent, theme) {
66
+ const cells = 8;
67
+ const clamped = Math.max(0, Math.min(100, percent));
68
+ const filled = Math.round((clamped / 100) * cells);
69
+ const token = clamped > 90 ? "error" : clamped > 70 ? "warning" : "success";
70
+ const fill = filled > 0 ? color(theme, token, "█".repeat(filled)) : "";
71
+ const rest = filled < cells ? color(theme, "dim", "░".repeat(cells - filled)) : "";
72
+ return fill + rest;
73
+ }
58
74
  // lunr: permission mode for the footer safety indicator (always shown).
59
75
  function lunrPermissionMode() {
60
76
  return lunrPermissionModeBridge()?.getMode();
@@ -483,6 +499,34 @@ function renderStatsLine(width, ctx, theme, footerData) {
483
499
  if (modeZone.length > 0) {
484
500
  parts.unshift(modeZone.join(color(theme, "dim", " · ")));
485
501
  }
502
+ // lunr: git branch + working-tree +/- vs HEAD (gated on footerGit).
503
+ if (footerToggles.git) {
504
+ const branch = footerData.getGitBranch?.();
505
+ if (branch) {
506
+ const diff = footerData.getGitDiffstat?.();
507
+ let gitSeg = color(theme, "white", branch);
508
+ if (diff && (diff.added !== 0 || diff.removed !== 0)) {
509
+ gitSeg +=
510
+ ` ${color(theme, "success", `+${diff.added}`)} ${color(theme, "error", `-${diff.removed}`)}`;
511
+ }
512
+ parts.push(gitSeg);
513
+ }
514
+ }
515
+ // lunr: live subscription window bar (gated on footerPlan). Hidden with no plan.
516
+ if (footerToggles.plan) {
517
+ const provider = ctx.model?.provider;
518
+ if (provider) {
519
+ const usage = lunrUsageBridge();
520
+ usage?.prefetch?.(provider);
521
+ const seg = usage?.pickForFooter?.(provider);
522
+ if (seg) {
523
+ const pct = `${Math.round(seg.usedPercent)}%`;
524
+ const barColor = seg.usedPercent > 90 ? "error" : seg.usedPercent > 70 ? "warning" : "success";
525
+ const bar = footerToggles.planBar ? `${compactPlanBar(seg.usedPercent, theme)} ` : "";
526
+ parts.push(color(theme, "white", seg.label) + " " + bar + color(theme, barColor, pct));
527
+ }
528
+ }
529
+ }
486
530
  // 2) Context usage: `pct/window` (lunr: gated on footerContext).
487
531
  if (footerToggles.context) {
488
532
  const usage = ctx.getContextUsage();