osdy-pi 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +160 -30
  2. package/bin/osdy-pi.mjs +29 -6
  3. package/bin/osdy.mjs +43 -0
  4. package/extensions/osdy-pi/account-profiles.test.ts +111 -97
  5. package/extensions/osdy-pi/account-profiles.ts +91 -162
  6. package/extensions/osdy-pi/animation.test.ts +84 -0
  7. package/extensions/osdy-pi/animation.ts +17 -8
  8. package/extensions/osdy-pi/codex-usage-ui.test.ts +1145 -0
  9. package/extensions/osdy-pi/codex-usage-ui.ts +728 -0
  10. package/extensions/osdy-pi/codex-usage.test.ts +276 -0
  11. package/extensions/osdy-pi/codex-usage.ts +276 -0
  12. package/extensions/osdy-pi/constants.ts +134 -34
  13. package/extensions/osdy-pi/context-usage-ui.test.ts +168 -0
  14. package/extensions/osdy-pi/context-usage-ui.ts +60 -0
  15. package/extensions/osdy-pi/editor-settings.test.ts +121 -23
  16. package/extensions/osdy-pi/editor-settings.ts +39 -3
  17. package/extensions/osdy-pi/metrics.test.ts +95 -0
  18. package/extensions/osdy-pi/metrics.ts +39 -1
  19. package/extensions/osdy-pi/profile-label.ts +2 -2
  20. package/extensions/osdy-pi/runtime-helpers.test.ts +220 -1
  21. package/extensions/osdy-pi/runtime-helpers.ts +42 -19
  22. package/extensions/osdy-pi/runtime.test.ts +355 -7
  23. package/extensions/osdy-pi/runtime.ts +344 -66
  24. package/extensions/osdy-pi/types.ts +26 -1
  25. package/extensions/osdy-pi/ui.test.ts +128 -0
  26. package/extensions/osdy-pi/ui.ts +57 -40
  27. package/package.json +5 -2
  28. package/scripts/osdy-pi-account-profiles.mjs +521 -76
  29. package/scripts/osdy-pi-gentle-coexistence.mjs +200 -0
  30. package/scripts/osdy-pi-profile-setup.mjs +175 -0
  31. package/themes/osdy-pi-catppuccin-frappe.json +6 -2
  32. package/themes/osdy-pi-catppuccin-latte.json +6 -2
  33. package/themes/osdy-pi-catppuccin-macchiato.json +6 -2
  34. package/themes/osdy-pi-catppuccin-mocha.json +6 -2
  35. package/themes/osdy-pi-dark.json +6 -2
  36. package/themes/osdy-pi-dracula.json +6 -2
  37. package/themes/osdy-pi-kanagawa-dragon.json +6 -2
  38. package/themes/osdy-pi-kanagawa-lotus.json +6 -2
  39. package/themes/osdy-pi-kanagawa-wave.json +6 -2
  40. package/themes/osdy-pi-lucent-orange.json +6 -2
  41. package/themes/osdy-pi-matrix.json +6 -2
  42. package/themes/osdy-pi-new.json +6 -2
  43. package/themes/osdy-pi-sexy.json +6 -2
  44. package/themes/osdy-pi-tokyo-night.json +6 -2
@@ -12,17 +12,22 @@ import { createEditorSettingsStore } from "./editor-settings.js";
12
12
  import { registerAccountProfilesCommand } from "./account-profiles.js";
13
13
  import {
14
14
  applyOsdyPi,
15
+ clearGentleShellChangesWidget,
15
16
  createResponsiveCoordinator,
16
17
  disableOsdyPi,
17
18
  notifyStatus,
18
19
  reconcileResponsiveUi,
20
+ syncWorkingTreeWidget,
19
21
  } from "./runtime-helpers.js";
20
22
  import { runSoundSetupWizard } from "./sound-setup-wizard.js";
21
23
  import {
22
24
  DEFAULT_EDITOR_MODE,
23
25
  EDITOR_MODES,
26
+ HEADER_VARIANT_CHOICES,
24
27
  type EditorMode,
25
28
  type HeaderVariant,
29
+ MASCOT_CHOICES,
30
+ type MascotChoice,
26
31
  type OsdyState,
27
32
  type WorkingTreeState,
28
33
  type WorkingWidgetState,
@@ -34,6 +39,15 @@ import {
34
39
  shouldRefreshWorkingTree,
35
40
  } from "./working-tree.js";
36
41
  import { showWorkingTreeDiffPanel } from "./diff-panel.js";
42
+ import {
43
+ extractCodexAccountId,
44
+ fetchCodexUsage,
45
+ type CodexUsageAuth,
46
+ type CodexUsageSnapshot,
47
+ } from "./codex-usage.js";
48
+ import { showCodexUsagePanel } from "./codex-usage-ui.js";
49
+ import { modelLabel } from "./metrics.js";
50
+ import { resolveActiveProfileLabel } from "./profile-label.js";
37
51
  import {
38
52
  createWorkingController,
39
53
  type WorkingController,
@@ -86,10 +100,35 @@ function parseCommandArgs(args: string): string[] {
86
100
  return args.trim().split(/\s+/).filter(Boolean);
87
101
  }
88
102
 
89
- const HEADER_VARIANTS = ["osdy-theme", "classic"] as const;
103
+ function isMascotChoice(value: string): value is MascotChoice {
104
+ return MASCOT_CHOICES.includes(value as MascotChoice);
105
+ }
106
+
107
+ function mascotLabel(mascot: MascotChoice): string {
108
+ return mascot === "bts" ? "Bts" : mascot;
109
+ }
90
110
 
91
111
  function isHeaderVariant(value: string): value is HeaderVariant {
92
- return HEADER_VARIANTS.includes(value as HeaderVariant);
112
+ return HEADER_VARIANT_CHOICES.includes(value as HeaderVariant);
113
+ }
114
+
115
+ async function saveVisualSettings(
116
+ state: OsdyState,
117
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
118
+ ): Promise<boolean> {
119
+ try {
120
+ await settingsStore.save({
121
+ version: 1,
122
+ enabled: state.enabled,
123
+ editorMode: state.editorMode,
124
+ workingTreeEnabled: state.workingTreeEnabled,
125
+ headerVariant: state.headerVariant,
126
+ mascot: state.mascot,
127
+ });
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
93
132
  }
94
133
 
95
134
  async function enableOsdyPi(
@@ -99,55 +138,124 @@ async function enableOsdyPi(
99
138
  workingState: WorkingWidgetState,
100
139
  workingTreeState: WorkingTreeState,
101
140
  controller: WorkingController,
141
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
102
142
  ): Promise<void> {
103
143
  state.enabled = true;
104
- applyOsdyPi(pi, ctx, state, workingState, workingTreeState, true);
144
+ applyOsdyPi(pi, ctx, state, workingState, workingTreeState);
105
145
  controller.refreshWorking();
146
+ const saved = await saveVisualSettings(state, settingsStore);
106
147
  if (state.workingTreeEnabled)
107
148
  await refreshWorkingTree(pi, ctx, workingTreeState);
149
+ ctx.ui.notify(
150
+ saved ? "osdy-pi enabled" : "osdy-pi enabled but could not be saved",
151
+ saved ? "info" : "warning",
152
+ );
108
153
  }
109
154
 
110
- function disableOsdyPiCommand(
155
+ async function disableOsdyPiCommand(
111
156
  ctx: ExtensionContext,
112
157
  state: OsdyState,
113
158
  workingTreeState: WorkingTreeState,
114
159
  controller: WorkingController,
115
160
  stopResponsive: () => void,
116
- ): void {
161
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
162
+ ): Promise<void> {
117
163
  stopResponsive();
118
164
  state.enabled = false;
119
165
  workingTreeState.visible = false;
120
166
  controller.stopWorking();
121
167
  clearWorkingTree(workingTreeState);
122
168
  disableOsdyPi(ctx, state);
169
+ const saved = await saveVisualSettings(state, settingsStore);
170
+ ctx.ui.notify(
171
+ saved ? "osdy-pi disabled" : "osdy-pi disabled but could not be saved",
172
+ saved ? "info" : "warning",
173
+ );
123
174
  }
124
175
 
125
- function setHeaderVariant(
176
+ async function handleMascotCommand(
177
+ action: string | undefined,
178
+ rest: string[],
126
179
  pi: ExtensionAPI,
127
180
  ctx: ExtensionContext,
128
181
  state: OsdyState,
129
182
  workingState: WorkingWidgetState,
130
183
  workingTreeState: WorkingTreeState,
131
- variant: HeaderVariant,
132
- ): void {
133
- state.headerVariant = variant;
184
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
185
+ ): Promise<void> {
186
+ const mascotUsage = `Usage: /osdy-pi mascot ${MASCOT_CHOICES.join(" | ")} | status`;
187
+ if (rest.length > 0) {
188
+ ctx.ui.notify(mascotUsage, "warning");
189
+ return;
190
+ }
191
+ if (action === "status" || action === undefined) {
192
+ ctx.ui.notify(`osdy-pi mascot: ${mascotLabel(state.mascot)}`, "info");
193
+ return;
194
+ }
195
+ if (!isMascotChoice(action)) {
196
+ ctx.ui.notify(mascotUsage, "warning");
197
+ return;
198
+ }
199
+ state.mascot = action;
200
+ if (state.enabled) applyOsdyPi(pi, ctx, state, workingState, workingTreeState);
201
+ const saved = await saveVisualSettings(state, settingsStore);
202
+ ctx.ui.notify(
203
+ saved
204
+ ? `osdy-pi mascot: ${mascotLabel(action)}`
205
+ : "osdy-pi mascot changed but could not be saved",
206
+ saved ? "info" : "warning",
207
+ );
208
+ }
209
+
210
+ async function handleHeaderCommand(
211
+ action: string | undefined,
212
+ rest: string[],
213
+ pi: ExtensionAPI,
214
+ ctx: ExtensionContext,
215
+ state: OsdyState,
216
+ workingState: WorkingWidgetState,
217
+ workingTreeState: WorkingTreeState,
218
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
219
+ ): Promise<void> {
220
+ const headerUsage = `Usage: /osdy-pi header ${HEADER_VARIANT_CHOICES.join(" | ")} | status`;
221
+ if (
222
+ rest.length > 0 ||
223
+ (action !== undefined && action !== "status" && !isHeaderVariant(action))
224
+ ) {
225
+ ctx.ui.notify(headerUsage, "warning");
226
+ return;
227
+ }
228
+ if (action === "status" || action === undefined) {
229
+ ctx.ui.notify(`osdy-pi header: ${state.headerVariant}`, "info");
230
+ return;
231
+ }
232
+ state.headerVariant = action;
134
233
  if (state.enabled) applyOsdyPi(pi, ctx, state, workingState, workingTreeState);
135
- ctx.ui.notify(`osdy-pi style: ${variant}`, "info");
234
+ const saved = await saveVisualSettings(state, settingsStore);
235
+ ctx.ui.notify(
236
+ saved
237
+ ? `osdy-pi header: ${action}`
238
+ : "osdy-pi header changed but could not be saved",
239
+ saved ? "info" : "warning",
240
+ );
136
241
  }
137
242
 
138
- function getOsdyCommandCompletions(prefix: string) {
243
+ export function getOsdyCommandCompletions(prefix: string) {
139
244
  const trimmed = prefix.trim();
140
245
  const parts = parseCommandArgs(trimmed);
141
246
  if (parts.length === 0) {
142
247
  return [
143
248
  "enable",
144
249
  "disable",
250
+ "on",
251
+ "off",
145
252
  "status",
146
253
  "sound",
147
254
  "working-tree",
148
255
  "editor",
256
+ "mascot",
257
+ "header",
149
258
  "diff",
150
- ...HEADER_VARIANTS,
151
259
  ].map((value) => ({ value, label: value }));
152
260
  }
153
261
  if (trimmed === "sound") {
@@ -163,6 +271,18 @@ function getOsdyCommandCompletions(prefix: string) {
163
271
  "working-tree position bottom",
164
272
  ].map((value) => ({ value, label: value }));
165
273
  }
274
+ if (trimmed === "mascot") {
275
+ return [
276
+ ...MASCOT_CHOICES.map((choice) => `mascot ${choice}`),
277
+ "mascot status",
278
+ ].map((value) => ({ value, label: value }));
279
+ }
280
+ if (trimmed === "header") {
281
+ return [
282
+ ...HEADER_VARIANT_CHOICES.map((choice) => `header ${choice}`),
283
+ "header status",
284
+ ].map((value) => ({ value, label: value }));
285
+ }
166
286
  if (trimmed === "editor") {
167
287
  return [
168
288
  "editor auto",
@@ -179,12 +299,15 @@ function getOsdyCommandCompletions(prefix: string) {
179
299
  return [
180
300
  "enable",
181
301
  "disable",
302
+ "on",
303
+ "off",
182
304
  "status",
183
305
  "sound",
184
306
  "working-tree",
185
307
  "editor",
308
+ "mascot",
309
+ "header",
186
310
  "diff",
187
- ...HEADER_VARIANTS,
188
311
  ]
189
312
  .filter((value) => value.startsWith(valuePrefix))
190
313
  .map((value) => ({ value, label: value }));
@@ -195,6 +318,18 @@ function getOsdyCommandCompletions(prefix: string) {
195
318
  .filter((value) => value.startsWith(valuePrefix))
196
319
  .map((value) => ({ value: `sound ${value}`, label: `sound ${value}` }));
197
320
  }
321
+ if (parts[0] === "header" && parts.length === 2) {
322
+ const valuePrefix = parts[1] ?? "";
323
+ return [...HEADER_VARIANT_CHOICES, "status"]
324
+ .filter((value) => value.startsWith(valuePrefix))
325
+ .map((value) => ({ value: `header ${value}`, label: `header ${value}` }));
326
+ }
327
+ if (parts[0] === "mascot" && parts.length === 2) {
328
+ const valuePrefix = parts[1] ?? "";
329
+ return [...MASCOT_CHOICES, "status"]
330
+ .filter((value) => value.startsWith(valuePrefix))
331
+ .map((value) => ({ value: `mascot ${value}`, label: `mascot ${value}` }));
332
+ }
198
333
  if (parts[0] === "editor" && parts.length === 2) {
199
334
  const valuePrefix = parts[1] ?? "";
200
335
  return ["auto", "extended", "simple", "on", "off", "toggle", "status"]
@@ -287,19 +422,13 @@ async function setEditorMode(
287
422
  ): Promise<void> {
288
423
  state.editorMode = mode;
289
424
  if (state.enabled) reconcileResponsiveUi(pi, ctx, state, workingTreeState);
290
- try {
291
- await settingsStore.save({
292
- version: 1,
293
- editorMode: mode,
294
- workingTreeEnabled: state.workingTreeEnabled,
295
- });
296
- ctx.ui.notify(`osdy-pi editor mode: ${mode}`, "info");
297
- } catch {
298
- ctx.ui.notify(
299
- "osdy-pi editor mode changed but could not be saved",
300
- "warning",
301
- );
302
- }
425
+ const saved = await saveVisualSettings(state, settingsStore);
426
+ ctx.ui.notify(
427
+ saved
428
+ ? `osdy-pi editor mode: ${mode}`
429
+ : "osdy-pi editor mode changed but could not be saved",
430
+ saved ? "info" : "warning",
431
+ );
303
432
  }
304
433
 
305
434
  async function handleEditorCommand(
@@ -360,16 +489,7 @@ async function saveWorkingTreePreference(
360
489
  state: OsdyState,
361
490
  settingsStore: ReturnType<typeof createEditorSettingsStore>,
362
491
  ): Promise<boolean> {
363
- try {
364
- await settingsStore.save({
365
- version: 1,
366
- editorMode: state.editorMode,
367
- workingTreeEnabled: state.workingTreeEnabled,
368
- });
369
- return true;
370
- } catch {
371
- return false;
372
- }
492
+ return saveVisualSettings(state, settingsStore);
373
493
  }
374
494
 
375
495
  async function handleWorkingTreeCommand(
@@ -396,6 +516,7 @@ async function handleWorkingTreeCommand(
396
516
  if (action === "on") {
397
517
  state.workingTreeEnabled = true;
398
518
  workingTreeState.enabled = true;
519
+ syncWorkingTreeWidget(ctx, state, workingState, workingTreeState);
399
520
  reconcileResponsiveUi(pi, ctx, state, workingTreeState);
400
521
  const saved = await saveWorkingTreePreference(state, settingsStore);
401
522
  await refreshWorkingTree(pi, ctx, workingTreeState);
@@ -410,6 +531,7 @@ async function handleWorkingTreeCommand(
410
531
  if (action === "off") {
411
532
  state.workingTreeEnabled = false;
412
533
  workingTreeState.enabled = false;
534
+ syncWorkingTreeWidget(ctx, state, workingState, workingTreeState);
413
535
  reconcileResponsiveUi(pi, ctx, state, workingTreeState);
414
536
  clearWorkingTree(workingTreeState);
415
537
  const saved = await saveWorkingTreePreference(state, settingsStore);
@@ -481,6 +603,95 @@ async function openDiffCommand(
481
603
  }
482
604
  }
483
605
 
606
+ type CodexUsageRefreshContext = {
607
+ modelRegistry: {
608
+ getProviderAuth(
609
+ provider: string,
610
+ ): Promise<{ auth: { apiKey?: string } } | undefined>;
611
+ };
612
+ };
613
+
614
+ type CodexUsageRefreshState = {
615
+ codexUsage: OsdyState["codexUsage"];
616
+ tui?: { requestRender(): void } | undefined;
617
+ };
618
+
619
+ type CodexUsageFetcher = (
620
+ auth: CodexUsageAuth,
621
+ options: { signal: AbortSignal },
622
+ ) => Promise<CodexUsageSnapshot>;
623
+
624
+ export async function refreshCodexUsage(
625
+ ctx: CodexUsageRefreshContext,
626
+ state: CodexUsageRefreshState,
627
+ abort: AbortController,
628
+ fetchUsage: CodexUsageFetcher = fetchCodexUsage,
629
+ ): Promise<void> {
630
+ state.codexUsage = { kind: "loading", snapshot: undefined };
631
+ state.tui?.requestRender();
632
+ try {
633
+ const providerAuth = await ctx.modelRegistry.getProviderAuth("openai-codex");
634
+ const accessToken = providerAuth?.auth.apiKey;
635
+ if (!accessToken) throw new Error("Codex login is required");
636
+ const snapshot = await fetchUsage(
637
+ { accessToken, accountId: extractCodexAccountId(accessToken) },
638
+ { signal: abort.signal },
639
+ );
640
+ if (abort.signal.aborted) return;
641
+ state.codexUsage = { kind: "ready", snapshot };
642
+ } catch (error) {
643
+ if (abort.signal.aborted) return;
644
+ state.codexUsage = {
645
+ kind: "error",
646
+ message:
647
+ error instanceof Error ? error.message : "Codex usage is unavailable",
648
+ snapshot: undefined,
649
+ };
650
+ }
651
+ state.tui?.requestRender();
652
+ }
653
+
654
+ export function createActiveSessionRefresh<T>(
655
+ getSessionContext: () => T | undefined,
656
+ startRefresh: (context: T) => Promise<void>,
657
+ ): () => Promise<void> {
658
+ return async () => {
659
+ const activeSessionContext = getSessionContext();
660
+ if (activeSessionContext) await startRefresh(activeSessionContext);
661
+ };
662
+ }
663
+
664
+ function registerUsageCommand(
665
+ pi: ExtensionAPI,
666
+ state: OsdyState,
667
+ getSessionContext: () => ExtensionContext | undefined,
668
+ startRefresh: (ctx: ExtensionContext) => Promise<void>,
669
+ ): void {
670
+ const refreshActiveSessionUsage = createActiveSessionRefresh(
671
+ getSessionContext,
672
+ startRefresh,
673
+ );
674
+ pi.registerCommand("usage", {
675
+ description: "Show current Codex subscription usage.",
676
+ handler: async (_args, ctx) => {
677
+ if (!ctx.hasUI) {
678
+ ctx.ui.notify("Codex usage requires the interactive UI", "warning");
679
+ return;
680
+ }
681
+ await showCodexUsagePanel(
682
+ ctx,
683
+ () => state.codexUsage,
684
+ refreshActiveSessionUsage,
685
+ {
686
+ profile: resolveActiveProfileLabel(),
687
+ provider: ctx.model?.provider ?? "unknown",
688
+ model: modelLabel(ctx),
689
+ },
690
+ );
691
+ },
692
+ });
693
+ }
694
+
484
695
  function registerCommand(
485
696
  pi: ExtensionAPI,
486
697
  state: OsdyState,
@@ -494,11 +705,11 @@ function registerCommand(
494
705
  ): void {
495
706
  pi.registerCommand("osdy-pi", {
496
707
  description:
497
- "Manage the Osdy Pi experience: enable, disable, status, editor, style, sound setup, working tree, or diff panel.",
708
+ "Manage the Osdy Pi experience: enable/disable (on/off), status, mascot, editor, style, sound setup, working tree, or diff panel.",
498
709
  getArgumentCompletions: getOsdyCommandCompletions,
499
710
  handler: async (args, ctx) => {
500
711
  const [action = "status", ...rest] = parseCommandArgs(args);
501
- if (action === "enable") {
712
+ if (["enable", "on"].includes(action)) {
502
713
  await enableOsdyPi(
503
714
  pi,
504
715
  ctx,
@@ -506,17 +717,19 @@ function registerCommand(
506
717
  workingState,
507
718
  workingTreeState,
508
719
  controller,
720
+ editorSettingsStore,
509
721
  );
510
722
  startResponsive();
511
723
  return;
512
724
  }
513
- if (action === "disable") {
514
- disableOsdyPiCommand(
725
+ if (["disable", "off"].includes(action)) {
726
+ await disableOsdyPiCommand(
515
727
  ctx,
516
728
  state,
517
729
  workingTreeState,
518
730
  controller,
519
731
  stopResponsive,
732
+ editorSettingsStore,
520
733
  );
521
734
  return;
522
735
  }
@@ -528,6 +741,32 @@ function registerCommand(
528
741
  await handleSoundCommand(rest, ctx, settingsStore);
529
742
  return;
530
743
  }
744
+ if (action === "mascot") {
745
+ await handleMascotCommand(
746
+ rest[0],
747
+ rest.slice(1),
748
+ pi,
749
+ ctx,
750
+ state,
751
+ workingState,
752
+ workingTreeState,
753
+ editorSettingsStore,
754
+ );
755
+ return;
756
+ }
757
+ if (action === "header") {
758
+ await handleHeaderCommand(
759
+ rest[0],
760
+ rest.slice(1),
761
+ pi,
762
+ ctx,
763
+ state,
764
+ workingState,
765
+ workingTreeState,
766
+ editorSettingsStore,
767
+ );
768
+ return;
769
+ }
531
770
  if (action === "editor") {
532
771
  await handleEditorCommand(
533
772
  rest[0],
@@ -557,34 +796,23 @@ function registerCommand(
557
796
  await openDiffCommand(pi, ctx, state, workingState, workingTreeState);
558
797
  return;
559
798
  }
560
- if (isHeaderVariant(action)) {
561
- setHeaderVariant(pi, ctx, state, workingState, workingTreeState, action);
562
- return;
563
- }
564
799
  ctx.ui.notify(
565
- "Usage: /osdy-pi enable | disable | status | editor auto|extended|simple|on|off|toggle|status | sound setup | working-tree ... | diff | osdy-theme | classic",
800
+ `Usage: /osdy-pi enable|disable|on|off|status | mascot ${MASCOT_CHOICES.join("|")}|status | header ${HEADER_VARIANT_CHOICES.join("|")}|status | editor auto|extended|simple|on|off|toggle|status | sound setup | working-tree ... | diff`,
566
801
  "warning",
567
802
  );
568
803
  },
569
804
  });
570
-
571
- for (const variant of HEADER_VARIANTS) {
572
- pi.registerCommand(`osdy-pi-${variant}`, {
573
- description: `Switch Osdy Pi header to ${variant}.`,
574
- handler: (_args, ctx) => {
575
- setHeaderVariant(pi, ctx, state, workingState, workingTreeState, variant);
576
- return Promise.resolve();
577
- },
578
- });
579
- }
580
805
  }
581
806
 
582
807
  export function registerOsdyPi(pi: ExtensionAPI): void {
583
808
  const state: OsdyState = {
809
+ codexUsage: { kind: "idle" },
584
810
  enabled: true,
585
811
  editorEffective: false,
586
812
  editorMode: DEFAULT_EDITOR_MODE,
813
+ fallbackEditorFactory: undefined,
587
814
  headerVariant: "osdy-theme",
815
+ mascot: "current",
588
816
  smallMode: false,
589
817
  tui: undefined,
590
818
  workingTreeEnabled: true,
@@ -609,7 +837,6 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
609
837
  const settingsStore = createAudioSoundSettingsStore();
610
838
  const editorSettingsStore = createEditorSettingsStore();
611
839
  registerAudioNotificationFlags(pi);
612
- registerAccountProfilesCommand(pi);
613
840
  const audioRouter = createAudioEventRouter(
614
841
  createAudioNotificationService(
615
842
  pi,
@@ -621,12 +848,42 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
621
848
  | ReturnType<typeof createResponsiveCoordinator>
622
849
  | undefined;
623
850
  let sessionContext: ExtensionContext | undefined;
851
+ let codexUsageAbort: AbortController | undefined;
852
+ const refreshCurrentCodexUsage = async (
853
+ ctx: ExtensionContext,
854
+ ): Promise<void> => {
855
+ codexUsageAbort?.abort();
856
+ const abort = new AbortController();
857
+ codexUsageAbort = abort;
858
+ await refreshCodexUsage(ctx, state, abort);
859
+ if (codexUsageAbort === abort) codexUsageAbort = undefined;
860
+ };
861
+ registerAccountProfilesCommand(pi, {
862
+ requestRender: () => {
863
+ state.tui?.requestRender();
864
+ },
865
+ refreshUsage: async () => {
866
+ const activeSessionContext = sessionContext;
867
+ if (activeSessionContext)
868
+ await refreshCurrentCodexUsage(activeSessionContext);
869
+ },
870
+ });
624
871
  const pendingOsdyRefreshes = new Set<ReturnType<typeof setTimeout>>();
625
872
  const cancelOsdyRefreshes = (): void => {
626
873
  for (const timeout of pendingOsdyRefreshes) clearTimeout(timeout);
627
874
  pendingOsdyRefreshes.clear();
628
875
  };
629
- const startResponsive = (): void => responsiveCoordinator?.start();
876
+ const startResponsive = (): void => {
877
+ if (!responsiveCoordinator && sessionContext) {
878
+ responsiveCoordinator = createResponsiveCoordinator(
879
+ pi,
880
+ sessionContext,
881
+ state,
882
+ workingTreeState,
883
+ );
884
+ }
885
+ responsiveCoordinator?.start();
886
+ };
630
887
  const stopResponsive = (): void => responsiveCoordinator?.stop();
631
888
 
632
889
  subscribeQuestionPromptAudioNotification(
@@ -642,6 +899,17 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
642
899
  pi.on("agent_end", (_event, ctx) => {
643
900
  controller.onAgentEnd();
644
901
  audioRouter.onAgentEnd(ctx);
902
+ if (state.enabled) clearGentleShellChangesWidget(ctx);
903
+ });
904
+ pi.on("agent_settled", () => {
905
+ const activeSessionContext = sessionContext;
906
+ if (
907
+ state.enabled &&
908
+ activeSessionContext &&
909
+ activeSessionContext.model?.provider === "openai-codex"
910
+ ) {
911
+ void refreshCurrentCodexUsage(activeSessionContext);
912
+ }
645
913
  });
646
914
  pi.on("tool_execution_start", (event) =>
647
915
  controller.onToolStart(event.toolName),
@@ -649,7 +917,9 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
649
917
  pi.on("tool_execution_end", (event, ctx) => {
650
918
  controller.onToolEnd();
651
919
  audioRouter.onToolExecutionEnd(event.isError === true, ctx);
920
+ if (state.enabled) clearGentleShellChangesWidget(ctx);
652
921
  if (
922
+ state.enabled &&
653
923
  state.workingTreeEnabled &&
654
924
  event.isError !== true &&
655
925
  shouldRefreshWorkingTree(event.toolName)
@@ -658,10 +928,12 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
658
928
  }
659
929
  });
660
930
  pi.on("session_shutdown", () => {
931
+ codexUsageAbort?.abort();
932
+ codexUsageAbort = undefined;
933
+ state.codexUsage = { kind: "idle" };
661
934
  cancelOsdyRefreshes();
662
935
  controller.onShutdown();
663
936
  stopResponsive();
664
- if (sessionContext) disableOsdyPi(sessionContext, state);
665
937
  workingTreeState.tui = undefined;
666
938
  sessionContext = undefined;
667
939
  });
@@ -670,10 +942,15 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
670
942
  stopResponsive();
671
943
  sessionContext = ctx;
672
944
  const editorSettings = await editorSettingsStore.load();
945
+ if (sessionContext !== ctx) return;
946
+ state.fallbackEditorFactory = ctx.ui.getEditorComponent();
947
+ state.enabled = editorSettings.enabled;
673
948
  state.editorMode = editorSettings.editorMode;
674
949
  state.workingTreeEnabled = editorSettings.workingTreeEnabled;
950
+ state.headerVariant = editorSettings.headerVariant;
951
+ state.mascot = editorSettings.mascot;
675
952
  workingTreeState.enabled = editorSettings.workingTreeEnabled;
676
- if (sessionContext !== ctx) return;
953
+ if (!state.enabled) return;
677
954
  claimOsdyVisualLayer(
678
955
  pi,
679
956
  ctx,
@@ -683,18 +960,19 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
683
960
  pendingOsdyRefreshes,
684
961
  () => sessionContext === ctx,
685
962
  );
686
- responsiveCoordinator = createResponsiveCoordinator(
687
- pi,
688
- ctx,
689
- state,
690
- workingTreeState,
691
- );
692
- responsiveCoordinator.start();
963
+ startResponsive();
693
964
  if (state.workingTreeEnabled) {
694
965
  void refreshWorkingTree(pi, ctx, workingTreeState);
695
966
  }
967
+ void refreshCurrentCodexUsage(ctx);
696
968
  });
697
969
 
970
+ registerUsageCommand(
971
+ pi,
972
+ state,
973
+ () => sessionContext,
974
+ refreshCurrentCodexUsage,
975
+ );
698
976
  registerCommand(
699
977
  pi,
700
978
  state,