@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
@@ -51,7 +51,7 @@ import {
51
51
  import { CustomMessageComponent } from "./custom-message";
52
52
  import { EvalExecutionComponent } from "./eval-execution";
53
53
  import { type LateDiagnosticsFile, LateDiagnosticsMessageComponent } from "./late-diagnostics-message";
54
- import { ReadToolGroupComponent, readArgsCollapseIntoGroup } from "./read-tool-group";
54
+ import { groupedReadUsageCallIds, ReadToolGroupComponent, readArgsCollapseIntoGroup } from "./read-tool-group";
55
55
  import { SkillMessageComponent } from "./skill-message";
56
56
  import { ToolExecutionComponent } from "./tool-execution";
57
57
  import { TranscriptContainer } from "./transcript-container";
@@ -86,6 +86,7 @@ export class ChatTranscriptBuilder {
86
86
  #pendingUsageDuration: number | undefined;
87
87
  #pendingUsageTtft: number | undefined;
88
88
  #pendingUsageTimestamp: number | undefined;
89
+ #pendingReadUsageCallIds: string[] | undefined;
89
90
  #lastAssistantUsage: Usage | undefined;
90
91
  #waitingPoll: ToolExecutionComponent | null = null;
91
92
  #todoSnapshot: ToolExecutionComponent | null = null;
@@ -135,6 +136,7 @@ export class ChatTranscriptBuilder {
135
136
  this.#pendingUsageDuration = undefined;
136
137
  this.#pendingUsageTtft = undefined;
137
138
  this.#pendingUsageTimestamp = undefined;
139
+ this.#pendingReadUsageCallIds = undefined;
138
140
  this.#lastAssistantUsage = undefined;
139
141
  this.#waitingPoll = null;
140
142
  this.#todoSnapshot = null;
@@ -194,30 +196,46 @@ export class ChatTranscriptBuilder {
194
196
  return this.#readGroup;
195
197
  }
196
198
 
197
- // The per-turn token-usage row must land below the turn's tool blocks, but
198
- // normal `read` calls only materialize their group in #appendToolResult. Defer
199
- // the row: stash it on the assistant message and flush once the turn's tools
200
- // are placed, sealing the read run so the row sits under it.
199
+ // Defer per-turn metrics until the turn's tool results have materialized.
200
+ // Read-only invisible turns attach the metrics to their shared compact
201
+ // group; every other turn keeps the standalone row below its tool blocks.
201
202
  #flushPendingUsage(): void {
202
203
  if (!this.#pendingUsage) return;
203
- this.#readGroup?.seal();
204
- this.#readGroup = null;
205
- this.container.addChild(
206
- createUsageRowBlock(
204
+ const usageAttached =
205
+ this.#pendingReadUsageCallIds !== undefined &&
206
+ (this.#readGroup?.attachUsage(
207
+ this.#pendingReadUsageCallIds,
207
208
  this.#pendingUsage,
208
209
  this.#pendingUsageDuration,
209
210
  this.#pendingUsageTtft,
210
211
  this.#pendingUsageTimestamp,
211
- ),
212
- );
212
+ ) ??
213
+ false);
214
+ if (!usageAttached) {
215
+ this.#readGroup?.seal();
216
+ this.#readGroup = null;
217
+ this.container.addChild(
218
+ createUsageRowBlock(
219
+ this.#pendingUsage,
220
+ this.#pendingUsageDuration,
221
+ this.#pendingUsageTtft,
222
+ this.#pendingUsageTimestamp,
223
+ ),
224
+ );
225
+ }
213
226
  this.#pendingUsage = undefined;
214
227
  this.#pendingUsageDuration = undefined;
215
228
  this.#pendingUsageTtft = undefined;
216
229
  this.#pendingUsageTimestamp = undefined;
230
+ this.#pendingReadUsageCallIds = undefined;
217
231
  }
218
232
 
219
233
  #appendChatMessage(message: AgentMessage): void {
220
234
  if (message.role !== "toolResult") this.#flushPendingUsage();
235
+ if (message.role !== "assistant" && message.role !== "toolResult") {
236
+ this.#readGroup?.seal();
237
+ this.#readGroup = null;
238
+ }
221
239
  switch (message.role) {
222
240
  case "assistant":
223
241
  this.#appendAssistantMessage(message);
@@ -403,6 +421,7 @@ export class ChatTranscriptBuilder {
403
421
  this.#pendingUsageDuration = message.duration;
404
422
  this.#pendingUsageTtft = message.ttft;
405
423
  this.#pendingUsageTimestamp = message.timestamp;
424
+ this.#pendingReadUsageCallIds = this.#pendingUsage ? groupedReadUsageCallIds(message) : undefined;
406
425
  }
407
426
 
408
427
  #appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {
@@ -0,0 +1,369 @@
1
+ import {
2
+ type Component,
3
+ getSegmenter,
4
+ matchesKey,
5
+ type OverlayHandle,
6
+ type OverlayOptions,
7
+ truncateToWidth,
8
+ visibleWidth,
9
+ } from "@oh-my-pi/pi-tui";
10
+ import { type ThemeColor, theme } from "../theme/theme";
11
+
12
+ const FRAME_INTERVAL_MS = 85;
13
+ const FRAME_COUNT = 34;
14
+
15
+ const FIREWORK_THEME_COLORS = {
16
+ cyan: "mdLink",
17
+ dim: "dim",
18
+ gold: "warning",
19
+ green: "success",
20
+ pink: "accent",
21
+ violet: "thinkingXhigh",
22
+ white: "text",
23
+ } as const satisfies Record<string, ThemeColor>;
24
+
25
+ type FireworkColor = keyof typeof FIREWORK_THEME_COLORS;
26
+
27
+ /** The active Codex account fields retained between status refreshes. */
28
+ export interface CodexResetUsageSnapshot {
29
+ /** When this usage report was observed, if supplied by the provider. */
30
+ observedAt?: number;
31
+ /** Weekly usage, its quota identity, and its previously scheduled reset deadline. */
32
+ sevenDay?: { percent: number; resetsAt?: number; tier?: string; plan?: string };
33
+ savedResets?: number;
34
+ }
35
+
36
+ /** A detected Codex quota event that can trigger the fireworks presentation. */
37
+ export type CodexResetFireworksEvent =
38
+ | { kind: "unscheduled-weekly-reset" }
39
+ | { kind: "saved-reset-banked"; added: number; available: number };
40
+
41
+ interface CanvasCell {
42
+ glyph: string;
43
+ color: FireworkColor;
44
+ priority: number;
45
+ }
46
+
47
+ interface FireworkBurst {
48
+ x: number;
49
+ y: number;
50
+ start: number;
51
+ color: FireworkColor;
52
+ }
53
+
54
+ interface ActiveFireworks {
55
+ component: CodexResetFireworksComponent;
56
+ overlay: OverlayHandle;
57
+ }
58
+
59
+ interface CodexResetFireworksHost {
60
+ ui: {
61
+ showOverlay(component: Component, options?: OverlayOptions): OverlayHandle;
62
+ setFocus(component: Component): void;
63
+ requestRender(): void;
64
+ readonly terminal: { readonly rows: number };
65
+ };
66
+ }
67
+
68
+ const BURSTS: readonly FireworkBurst[] = [
69
+ { x: 0.17, y: 0.46, start: 5, color: "pink" },
70
+ { x: 0.48, y: 0.2, start: 9, color: "cyan" },
71
+ { x: 0.78, y: 0.42, start: 13, color: "gold" },
72
+ { x: 0.31, y: 0.24, start: 17, color: "violet" },
73
+ { x: 0.65, y: 0.28, start: 21, color: "green" },
74
+ { x: 0.88, y: 0.18, start: 25, color: "pink" },
75
+ ];
76
+
77
+ /**
78
+ * Compare consecutive reports for one Codex account. A saved-reset grant takes
79
+ * precedence when both changes arrive in the same report. A verified decrease,
80
+ * or a prior positive balance becoming unavailable, suppresses the weekly event
81
+ * because the user may have redeemed a credit. Other weekly usage drops are
82
+ * celebrated only before the previously scheduled reset deadline.
83
+ */
84
+ export function detectCodexResetFireworks(
85
+ previous: CodexResetUsageSnapshot,
86
+ current: CodexResetUsageSnapshot,
87
+ ): CodexResetFireworksEvent | undefined {
88
+ const previousSavedResets = previous.savedResets;
89
+ const currentSavedResets = current.savedResets;
90
+ if (previousSavedResets !== undefined) {
91
+ if (currentSavedResets === undefined) {
92
+ if (previousSavedResets > 0) return undefined;
93
+ } else {
94
+ if (currentSavedResets > previousSavedResets) {
95
+ return {
96
+ kind: "saved-reset-banked",
97
+ added: currentSavedResets - previousSavedResets,
98
+ available: currentSavedResets,
99
+ };
100
+ }
101
+ if (currentSavedResets < previousSavedResets) return undefined;
102
+ }
103
+ }
104
+
105
+ if (!previous.sevenDay || !current.sevenDay) return undefined;
106
+ if (previous.sevenDay.tier !== current.sevenDay.tier || previous.sevenDay.plan !== current.sevenDay.plan) {
107
+ return undefined;
108
+ }
109
+ const previousWeeklyPercent = Math.round(Math.max(0, Math.min(100, previous.sevenDay.percent)));
110
+ const currentWeeklyPercent = Math.round(Math.max(0, Math.min(100, current.sevenDay.percent)));
111
+ if (previousWeeklyPercent === 0 || currentWeeklyPercent >= previousWeeklyPercent) return undefined;
112
+
113
+ const scheduledResetAt = previous.sevenDay.resetsAt;
114
+ if (
115
+ scheduledResetAt === undefined ||
116
+ !Number.isFinite(scheduledResetAt) ||
117
+ typeof current.observedAt !== "number" ||
118
+ !Number.isFinite(current.observedAt) ||
119
+ current.observedAt >= scheduledResetAt
120
+ ) {
121
+ return undefined;
122
+ }
123
+ return { kind: "unscheduled-weekly-reset" };
124
+ }
125
+
126
+ function setCell(
127
+ canvas: Array<Array<CanvasCell | undefined>>,
128
+ x: number,
129
+ y: number,
130
+ glyph: string,
131
+ color: FireworkColor,
132
+ priority: number,
133
+ ): void {
134
+ const row = canvas[y];
135
+ if (!row || x < 0 || x >= row.length) return;
136
+ const current = row[x];
137
+ if (!current || priority >= current.priority) row[x] = { glyph, color, priority };
138
+ }
139
+
140
+ function drawText(
141
+ canvas: Array<Array<CanvasCell | undefined>>,
142
+ x: number,
143
+ y: number,
144
+ text: string,
145
+ color: FireworkColor,
146
+ priority: number,
147
+ ): void {
148
+ let column = x;
149
+ for (const { segment } of getSegmenter().segment(text)) {
150
+ const width = visibleWidth(segment);
151
+ if (width <= 0) continue;
152
+ setCell(canvas, column, y, segment, color, priority);
153
+ for (let continuation = 1; continuation < width; continuation++) {
154
+ setCell(canvas, column + continuation, y, "", color, priority);
155
+ }
156
+ column += width;
157
+ }
158
+ }
159
+
160
+ function drawBanner(
161
+ canvas: Array<Array<CanvasCell | undefined>>,
162
+ left: number,
163
+ artWidth: number,
164
+ height: number,
165
+ event: CodexResetFireworksEvent,
166
+ ): void {
167
+ if (height < 3 || artWidth < 8) return;
168
+ const panelWidth = Math.min(62, artWidth);
169
+ const panelLeft = left + Math.floor((artWidth - panelWidth) / 2);
170
+ const top = height - 3;
171
+ const innerWidth = panelWidth - 2;
172
+ const titleText =
173
+ event.kind === "unscheduled-weekly-reset" ? " O P E N A I R E S E T " : " S A V E D R E S E T ";
174
+ const subtitleText =
175
+ event.kind === "unscheduled-weekly-reset"
176
+ ? "Weekly usage cleared early · ESC to return"
177
+ : event.added === 1
178
+ ? `New reset banked · ${event.available} available · ESC to return`
179
+ : `${event.added} resets banked · ${event.available} available · ESC to return`;
180
+ const title = truncateToWidth(titleText, innerWidth, "");
181
+ const subtitle = truncateToWidth(subtitleText, innerWidth, "");
182
+ const titleOffset = Math.floor((innerWidth - visibleWidth(title)) / 2);
183
+ const subtitleOffset = Math.floor((innerWidth - visibleWidth(subtitle)) / 2);
184
+
185
+ drawText(canvas, panelLeft, top, `╭${"─".repeat(innerWidth)}╮`, "violet", 20);
186
+ drawText(canvas, panelLeft + 1 + titleOffset, top, title, "gold", 21);
187
+ drawText(canvas, panelLeft, top + 1, `│${" ".repeat(innerWidth)}│`, "violet", 20);
188
+ drawText(canvas, panelLeft + 1 + subtitleOffset, top + 1, subtitle, "cyan", 21);
189
+ drawText(canvas, panelLeft, top + 2, `╰${"─".repeat(innerWidth)}╯`, "violet", 20);
190
+ }
191
+
192
+ function drawStars(
193
+ canvas: Array<Array<CanvasCell | undefined>>,
194
+ left: number,
195
+ artWidth: number,
196
+ skyHeight: number,
197
+ frame: number,
198
+ ): void {
199
+ if (skyHeight <= 0) return;
200
+ const count = Math.min(26, Math.max(5, Math.floor(artWidth / 3)));
201
+ for (let index = 0; index < count; index++) {
202
+ const x = left + ((index * 37 + 11) % artWidth);
203
+ const y = (index * 7 + 2) % skyHeight;
204
+ const bright = (index + Math.floor(frame / 3)) % 5 === 0;
205
+ setCell(canvas, x, y, bright ? "+" : ".", bright ? "white" : "dim", bright ? 2 : 1);
206
+ }
207
+ }
208
+
209
+ function drawBurst(
210
+ canvas: Array<Array<CanvasCell | undefined>>,
211
+ burst: FireworkBurst,
212
+ left: number,
213
+ artWidth: number,
214
+ skyHeight: number,
215
+ frame: number,
216
+ ): void {
217
+ if (skyHeight <= 1) return;
218
+ const centerX = left + Math.round((artWidth - 1) * burst.x);
219
+ const centerY = Math.max(0, Math.min(skyHeight - 2, Math.round((skyHeight - 1) * burst.y)));
220
+ const age = frame - burst.start;
221
+
222
+ if (age >= -6 && age < 0) {
223
+ const progress = (age + 6) / 6;
224
+ const y = skyHeight - 1 - Math.round(progress * (skyHeight - 1 - centerY));
225
+ setCell(canvas, centerX, y, "^", "white", 8);
226
+ setCell(canvas, centerX, y + 1, "|", burst.color, 7);
227
+ setCell(canvas, centerX, y + 2, ".", "gold", 6);
228
+ return;
229
+ }
230
+ if (age < 0 || age > 8) return;
231
+
232
+ const radius = age === 0 ? 0 : 0.8 + age * 0.92;
233
+ const gravity = Math.floor((age * age) / 22);
234
+ const glyphs = ["@", "*", "*", "+", "o", "o", ".", ".", "."] as const;
235
+ const particleColor: FireworkColor = age <= 5 ? burst.color : age <= 7 ? "gold" : "dim";
236
+
237
+ for (let particle = 0; particle < 20; particle++) {
238
+ const angle = (particle / 20) * Math.PI * 2 + burst.start * 0.17;
239
+ const x = centerX + Math.round(Math.cos(angle) * radius * 1.75);
240
+ const y = centerY + Math.round(Math.sin(angle) * radius * 0.58 + gravity);
241
+ setCell(canvas, x, y, glyphs[age], particleColor, 10);
242
+ if (age >= 2 && age <= 6) {
243
+ const trailRadius = Math.max(0, radius - 1.4);
244
+ const trailX = centerX + Math.round(Math.cos(angle) * trailRadius * 1.75);
245
+ const trailY = centerY + Math.round(Math.sin(angle) * trailRadius * 0.58 + gravity);
246
+ setCell(canvas, trailX, trailY, ".", "dim", 5);
247
+ }
248
+ }
249
+ if (age <= 2) setCell(canvas, centerX, centerY, age === 0 ? "@" : "+", "white", 12);
250
+ }
251
+
252
+ function renderCanvas(canvas: Array<Array<CanvasCell | undefined>>): string[] {
253
+ return canvas.map(row => {
254
+ let output = "";
255
+ let run = "";
256
+ let runColor: FireworkColor | undefined;
257
+ for (const cell of row) {
258
+ if (cell && cell.color !== runColor) {
259
+ if (run) output += runColor ? theme.fg(FIREWORK_THEME_COLORS[runColor], run) : run;
260
+ run = "";
261
+ runColor = cell.color;
262
+ }
263
+ run += cell?.glyph ?? " ";
264
+ }
265
+ if (run) output += runColor ? theme.fg(FIREWORK_THEME_COLORS[runColor], run) : run;
266
+ return output;
267
+ });
268
+ }
269
+
270
+ /** Render one deterministic animation frame for the top-third overlay. */
271
+ function renderCodexResetFireworks(
272
+ width: number,
273
+ height: number,
274
+ frame: number,
275
+ event: CodexResetFireworksEvent,
276
+ ): string[] {
277
+ const safeWidth = Math.max(1, Math.floor(width));
278
+ const safeHeight = Math.max(1, Math.floor(height));
279
+ const artWidth = Math.min(96, safeWidth);
280
+ const left = Math.floor((safeWidth - artWidth) / 2);
281
+ const skyHeight = Math.max(0, safeHeight - 3);
282
+ const canvas = Array.from({ length: safeHeight }, () => new Array<CanvasCell | undefined>(safeWidth));
283
+
284
+ drawStars(canvas, left, artWidth, skyHeight, frame);
285
+ for (const burst of BURSTS) drawBurst(canvas, burst, left, artWidth, skyHeight, frame);
286
+ drawBanner(canvas, left, artWidth, safeHeight, event);
287
+ return renderCanvas(canvas);
288
+ }
289
+
290
+ class CodexResetFireworksComponent implements Component {
291
+ #timer: NodeJS.Timeout | undefined;
292
+ #done = Promise.withResolvers<void>();
293
+ #frame = 0;
294
+ #disposed = false;
295
+
296
+ constructor(
297
+ readonly host: CodexResetFireworksHost,
298
+ readonly event: CodexResetFireworksEvent,
299
+ ) {}
300
+
301
+ run(): Promise<void> {
302
+ this.#timer ??= setInterval(() => {
303
+ if (this.#disposed) return;
304
+ this.#frame = (this.#frame + 1) % FRAME_COUNT;
305
+ this.host.ui.requestRender();
306
+ }, FRAME_INTERVAL_MS);
307
+ this.host.ui.requestRender();
308
+ return this.#done.promise;
309
+ }
310
+
311
+ dispose(): void {
312
+ if (this.#disposed) return;
313
+ this.#disposed = true;
314
+ if (this.#timer) {
315
+ clearInterval(this.#timer);
316
+ this.#timer = undefined;
317
+ }
318
+ this.#done.resolve();
319
+ }
320
+
321
+ handleInput(data: string): void {
322
+ if (matchesKey(data, "escape") || matchesKey(data, "esc")) this.#done.resolve();
323
+ }
324
+
325
+ render(width: number): readonly string[] {
326
+ const height = Math.max(1, Math.floor(this.host.ui.terminal.rows * 0.33));
327
+ return renderCodexResetFireworks(width, height, this.#frame, this.event);
328
+ }
329
+ }
330
+
331
+ /** Owns the at-most-one modal celebration lifecycle for an interactive session. */
332
+ export class CodexResetFireworksController {
333
+ #active: ActiveFireworks | undefined;
334
+
335
+ constructor(private readonly host: CodexResetFireworksHost) {}
336
+
337
+ /** Present a celebration unless another one already owns the modal overlay. */
338
+ show(event: CodexResetFireworksEvent): boolean {
339
+ if (this.#active) return false;
340
+ const component = new CodexResetFireworksComponent(this.host, event);
341
+ const overlay = this.host.ui.showOverlay(component, {
342
+ anchor: "top-center",
343
+ width: "100%",
344
+ maxHeight: "33%",
345
+ margin: 0,
346
+ });
347
+ this.#active = { component, overlay };
348
+ this.host.ui.setFocus(component);
349
+ void component.run().then(() => this.#finish(component));
350
+ return true;
351
+ }
352
+
353
+ /** Stop the active celebration and release its overlay, if present. */
354
+ dispose(): void {
355
+ const active = this.#active;
356
+ if (!active) return;
357
+ this.#active = undefined;
358
+ active.component.dispose();
359
+ active.overlay.hide();
360
+ }
361
+
362
+ #finish(component: CodexResetFireworksComponent): void {
363
+ const active = this.#active;
364
+ if (!active || active.component !== component) return;
365
+ this.#active = undefined;
366
+ component.dispose();
367
+ active.overlay.hide();
368
+ }
369
+ }