osdy-pi 0.1.8 → 1.2.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 (39) hide show
  1. package/README.md +209 -161
  2. package/bin/osdy-pi.mjs +127 -0
  3. package/extensions/osdy-pi/account-profiles.test.ts +517 -0
  4. package/extensions/osdy-pi/account-profiles.ts +581 -0
  5. package/extensions/osdy-pi/animation.ts +47 -5
  6. package/extensions/osdy-pi/constants.ts +282 -103
  7. package/extensions/osdy-pi/diff-panel.ts +78 -24
  8. package/extensions/osdy-pi/editor-settings.test.ts +163 -0
  9. package/extensions/osdy-pi/editor-settings.ts +112 -0
  10. package/extensions/osdy-pi/metrics.ts +8 -135
  11. package/extensions/osdy-pi/plugin-events.ts +26 -0
  12. package/extensions/osdy-pi/profile-label.ts +29 -0
  13. package/extensions/osdy-pi/runtime-helpers.test.ts +26 -0
  14. package/extensions/osdy-pi/runtime-helpers.ts +87 -44
  15. package/extensions/osdy-pi/runtime.test.ts +62 -0
  16. package/extensions/osdy-pi/runtime.ts +256 -64
  17. package/extensions/osdy-pi/types.ts +33 -10
  18. package/extensions/osdy-pi/ui.test.ts +51 -0
  19. package/extensions/osdy-pi/ui.ts +298 -114
  20. package/extensions/osdy-pi/utils.ts +43 -1
  21. package/extensions/osdy-pi/working-tree.ts +25 -6
  22. package/package.json +22 -5
  23. package/scripts/osdy-pi-account-profiles.mjs +501 -0
  24. package/themes/osdy-pi-catppuccin-frappe.json +94 -0
  25. package/themes/osdy-pi-catppuccin-latte.json +94 -0
  26. package/themes/osdy-pi-catppuccin-macchiato.json +94 -0
  27. package/themes/osdy-pi-catppuccin-mocha.json +94 -0
  28. package/themes/osdy-pi-dark.json +0 -6
  29. package/themes/osdy-pi-dracula.json +83 -0
  30. package/themes/osdy-pi-kanagawa-dragon.json +91 -0
  31. package/themes/osdy-pi-kanagawa-lotus.json +92 -0
  32. package/themes/osdy-pi-kanagawa-wave.json +91 -0
  33. package/themes/osdy-pi-lucent-orange.json +80 -0
  34. package/themes/osdy-pi-matrix.json +83 -0
  35. package/themes/osdy-pi-new.json +91 -0
  36. package/themes/osdy-pi-sexy.json +84 -0
  37. package/themes/osdy-pi-tokyo-night.json +83 -0
  38. package/themes/osdy-pi-light.json +0 -84
  39. package/themes/osdy-pi-simple.json +0 -83
@@ -6,14 +6,26 @@ import { createAudioEventRouter } from "./audio-event-router.js";
6
6
  import { registerAudioNotificationFlags } from "./audio-notification-config.js";
7
7
  import { createAudioNotificationService } from "./audio-notification-service.js";
8
8
  import { createAudioPlaybackAdapter } from "./audio-playback.js";
9
+ import { subscribeQuestionPromptAudioNotification } from "./plugin-events.js";
9
10
  import { createAudioSoundSettingsStore } from "./audio-sound-settings.js";
10
- import { applyOsdyPi, disableOsdyPi, notifyStatus } from "./runtime-helpers.js";
11
+ import { createEditorSettingsStore } from "./editor-settings.js";
12
+ import { registerAccountProfilesCommand } from "./account-profiles.js";
13
+ import {
14
+ applyOsdyPi,
15
+ createResponsiveCoordinator,
16
+ disableOsdyPi,
17
+ notifyStatus,
18
+ reconcileResponsiveUi,
19
+ } from "./runtime-helpers.js";
11
20
  import { runSoundSetupWizard } from "./sound-setup-wizard.js";
12
- import type {
13
- HeaderVariant,
14
- OsdyState,
15
- WorkingTreeState,
16
- WorkingWidgetState,
21
+ import {
22
+ DEFAULT_EDITOR_MODE,
23
+ EDITOR_MODES,
24
+ type EditorMode,
25
+ type HeaderVariant,
26
+ type OsdyState,
27
+ type WorkingTreeState,
28
+ type WorkingWidgetState,
17
29
  } from "./types.js";
18
30
  import { WORKING_TREE_WIDGET_KEY, WORKING_WIDGET_KEY } from "./constants.js";
19
31
  import {
@@ -34,11 +46,15 @@ function scheduleOsdyRefresh(
34
46
  state: OsdyState,
35
47
  workingState: WorkingWidgetState,
36
48
  workingTreeState: WorkingTreeState,
49
+ pendingRefreshes: Set<ReturnType<typeof setTimeout>>,
50
+ isCurrentSession: () => boolean,
37
51
  ): void {
38
- setTimeout(() => {
39
- if (state.enabled)
52
+ const timeout = setTimeout(() => {
53
+ pendingRefreshes.delete(timeout);
54
+ if (state.enabled && isCurrentSession())
40
55
  applyOsdyPi(pi, ctx, state, workingState, workingTreeState);
41
56
  }, delayMs);
57
+ pendingRefreshes.add(timeout);
42
58
  }
43
59
 
44
60
  function claimOsdyVisualLayer(
@@ -47,6 +63,8 @@ function claimOsdyVisualLayer(
47
63
  state: OsdyState,
48
64
  workingState: WorkingWidgetState,
49
65
  workingTreeState: WorkingTreeState,
66
+ pendingRefreshes: Set<ReturnType<typeof setTimeout>>,
67
+ isCurrentSession: () => boolean,
50
68
  ): void {
51
69
  if (!state.enabled) return;
52
70
  applyOsdyPi(pi, ctx, state, workingState, workingTreeState);
@@ -58,37 +76,12 @@ function claimOsdyVisualLayer(
58
76
  state,
59
77
  workingState,
60
78
  workingTreeState,
79
+ pendingRefreshes,
80
+ isCurrentSession,
61
81
  );
62
82
  }
63
83
  }
64
84
 
65
- function loadSessionMetadata(
66
- pi: ExtensionAPI,
67
- ctx: ExtensionContext,
68
- state: OsdyState,
69
- ): void {
70
- void pi
71
- .exec("git", ["branch", "--show-current"], { cwd: ctx.cwd })
72
- .then((result) => {
73
- state.gitLabel = result.stdout.trim() || "detached";
74
- })
75
- .catch(() => {
76
- state.gitLabel = "no-git";
77
- });
78
- void pi
79
- .exec("find", [ctx.cwd, "-name", "AGENTS.md", "-o", "-name", "AGENTS.MD"], {
80
- cwd: ctx.cwd,
81
- })
82
- .then((result) => {
83
- state.agentsLabel = String(
84
- result.stdout.split("\n").filter(Boolean).length,
85
- );
86
- })
87
- .catch(() => {
88
- state.agentsLabel = "0";
89
- });
90
- }
91
-
92
85
  function parseCommandArgs(args: string): string[] {
93
86
  return args.trim().split(/\s+/).filter(Boolean);
94
87
  }
@@ -119,8 +112,11 @@ function disableOsdyPiCommand(
119
112
  state: OsdyState,
120
113
  workingTreeState: WorkingTreeState,
121
114
  controller: WorkingController,
115
+ stopResponsive: () => void,
122
116
  ): void {
117
+ stopResponsive();
123
118
  state.enabled = false;
119
+ workingTreeState.visible = false;
124
120
  controller.stopWorking();
125
121
  clearWorkingTree(workingTreeState);
126
122
  disableOsdyPi(ctx, state);
@@ -135,8 +131,7 @@ function setHeaderVariant(
135
131
  variant: HeaderVariant,
136
132
  ): void {
137
133
  state.headerVariant = variant;
138
- if (state.enabled)
139
- applyOsdyPi(pi, ctx, state, workingState, workingTreeState);
134
+ if (state.enabled) applyOsdyPi(pi, ctx, state, workingState, workingTreeState);
140
135
  ctx.ui.notify(`osdy-pi style: ${variant}`, "info");
141
136
  }
142
137
 
@@ -150,6 +145,7 @@ function getOsdyCommandCompletions(prefix: string) {
150
145
  "status",
151
146
  "sound",
152
147
  "working-tree",
148
+ "editor",
153
149
  "diff",
154
150
  ...HEADER_VARIANTS,
155
151
  ].map((value) => ({ value, label: value }));
@@ -167,6 +163,17 @@ function getOsdyCommandCompletions(prefix: string) {
167
163
  "working-tree position bottom",
168
164
  ].map((value) => ({ value, label: value }));
169
165
  }
166
+ if (trimmed === "editor") {
167
+ return [
168
+ "editor auto",
169
+ "editor extended",
170
+ "editor simple",
171
+ "editor on",
172
+ "editor off",
173
+ "editor toggle",
174
+ "editor status",
175
+ ].map((value) => ({ value, label: value }));
176
+ }
170
177
  if (parts.length === 1) {
171
178
  const valuePrefix = parts[0] ?? "";
172
179
  return [
@@ -175,6 +182,7 @@ function getOsdyCommandCompletions(prefix: string) {
175
182
  "status",
176
183
  "sound",
177
184
  "working-tree",
185
+ "editor",
178
186
  "diff",
179
187
  ...HEADER_VARIANTS,
180
188
  ]
@@ -187,6 +195,15 @@ function getOsdyCommandCompletions(prefix: string) {
187
195
  .filter((value) => value.startsWith(valuePrefix))
188
196
  .map((value) => ({ value: `sound ${value}`, label: `sound ${value}` }));
189
197
  }
198
+ if (parts[0] === "editor" && parts.length === 2) {
199
+ const valuePrefix = parts[1] ?? "";
200
+ return ["auto", "extended", "simple", "on", "off", "toggle", "status"]
201
+ .filter((value) => value.startsWith(valuePrefix))
202
+ .map((value) => ({
203
+ value: `editor ${value}`,
204
+ label: `editor ${value}`,
205
+ }));
206
+ }
190
207
  if (parts[0] === "working-tree" && parts.length === 2) {
191
208
  const valuePrefix = parts[1] ?? "";
192
209
  return ["on", "off", "toggle", "status", "position"]
@@ -260,6 +277,101 @@ function handleWorkingTreePositionCommand(
260
277
  );
261
278
  }
262
279
 
280
+ async function setEditorMode(
281
+ mode: EditorMode,
282
+ pi: ExtensionAPI,
283
+ ctx: ExtensionContext,
284
+ state: OsdyState,
285
+ workingTreeState: WorkingTreeState,
286
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
287
+ ): Promise<void> {
288
+ state.editorMode = mode;
289
+ 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
+ }
303
+ }
304
+
305
+ async function handleEditorCommand(
306
+ action: string | undefined,
307
+ rest: string[],
308
+ pi: ExtensionAPI,
309
+ ctx: ExtensionContext,
310
+ state: OsdyState,
311
+ workingTreeState: WorkingTreeState,
312
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
313
+ ): Promise<void> {
314
+ if (rest.length > 0) {
315
+ ctx.ui.notify(
316
+ "Usage: /osdy-pi editor auto | extended | simple | on | off | toggle | status",
317
+ "warning",
318
+ );
319
+ return;
320
+ }
321
+ const modeForAction: Record<string, EditorMode> = {
322
+ auto: EDITOR_MODES.AUTO,
323
+ extended: EDITOR_MODES.EXTENDED,
324
+ simple: EDITOR_MODES.SIMPLE,
325
+ on: EDITOR_MODES.EXTENDED,
326
+ off: EDITOR_MODES.SIMPLE,
327
+ };
328
+ const mode = action ? modeForAction[action] : undefined;
329
+ if (mode) {
330
+ await setEditorMode(mode, pi, ctx, state, workingTreeState, settingsStore);
331
+ return;
332
+ }
333
+ if (action === "toggle") {
334
+ await setEditorMode(
335
+ state.editorMode === EDITOR_MODES.EXTENDED
336
+ ? EDITOR_MODES.SIMPLE
337
+ : EDITOR_MODES.EXTENDED,
338
+ pi,
339
+ ctx,
340
+ state,
341
+ workingTreeState,
342
+ settingsStore,
343
+ );
344
+ return;
345
+ }
346
+ if (action === "status" || action === undefined) {
347
+ ctx.ui.notify(
348
+ `osdy-pi editor mode ${state.editorMode}, effective ${state.editorEffective ? "extended" : "simple/native"}`,
349
+ "info",
350
+ );
351
+ return;
352
+ }
353
+ ctx.ui.notify(
354
+ "Usage: /osdy-pi editor auto | extended | simple | on | off | toggle | status",
355
+ "warning",
356
+ );
357
+ }
358
+
359
+ async function saveWorkingTreePreference(
360
+ state: OsdyState,
361
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
362
+ ): 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
+ }
373
+ }
374
+
263
375
  async function handleWorkingTreeCommand(
264
376
  action: string | undefined,
265
377
  rest: string[],
@@ -268,6 +380,7 @@ async function handleWorkingTreeCommand(
268
380
  state: OsdyState,
269
381
  workingState: WorkingWidgetState,
270
382
  workingTreeState: WorkingTreeState,
383
+ settingsStore: ReturnType<typeof createEditorSettingsStore>,
271
384
  ): Promise<void> {
272
385
  if (action === "position") {
273
386
  handleWorkingTreePositionCommand(
@@ -283,15 +396,29 @@ async function handleWorkingTreeCommand(
283
396
  if (action === "on") {
284
397
  state.workingTreeEnabled = true;
285
398
  workingTreeState.enabled = true;
399
+ reconcileResponsiveUi(pi, ctx, state, workingTreeState);
400
+ const saved = await saveWorkingTreePreference(state, settingsStore);
286
401
  await refreshWorkingTree(pi, ctx, workingTreeState);
287
- ctx.ui.notify("osdy-pi working tree enabled", "info");
402
+ ctx.ui.notify(
403
+ saved
404
+ ? "osdy-pi working tree enabled"
405
+ : "osdy-pi working tree enabled but could not be saved",
406
+ saved ? "info" : "warning",
407
+ );
288
408
  return;
289
409
  }
290
410
  if (action === "off") {
291
411
  state.workingTreeEnabled = false;
292
412
  workingTreeState.enabled = false;
413
+ reconcileResponsiveUi(pi, ctx, state, workingTreeState);
293
414
  clearWorkingTree(workingTreeState);
294
- ctx.ui.notify("osdy-pi working tree disabled", "info");
415
+ const saved = await saveWorkingTreePreference(state, settingsStore);
416
+ ctx.ui.notify(
417
+ saved
418
+ ? "osdy-pi working tree disabled"
419
+ : "osdy-pi working tree disabled but could not be saved",
420
+ saved ? "info" : "warning",
421
+ );
295
422
  return;
296
423
  }
297
424
  if (action === "toggle") {
@@ -304,6 +431,7 @@ async function handleWorkingTreeCommand(
304
431
  state,
305
432
  workingState,
306
433
  workingTreeState,
434
+ settingsStore,
307
435
  );
308
436
  return;
309
437
  }
@@ -360,10 +488,13 @@ function registerCommand(
360
488
  workingTreeState: WorkingTreeState,
361
489
  controller: WorkingController,
362
490
  settingsStore: ReturnType<typeof createAudioSoundSettingsStore>,
491
+ editorSettingsStore: ReturnType<typeof createEditorSettingsStore>,
492
+ startResponsive: () => void,
493
+ stopResponsive: () => void,
363
494
  ): void {
364
495
  pi.registerCommand("osdy-pi", {
365
496
  description:
366
- "Manage the Osdy Pi experience: enable, disable, status, style, sound setup, working tree, or diff panel.",
497
+ "Manage the Osdy Pi experience: enable, disable, status, editor, style, sound setup, working tree, or diff panel.",
367
498
  getArgumentCompletions: getOsdyCommandCompletions,
368
499
  handler: async (args, ctx) => {
369
500
  const [action = "status", ...rest] = parseCommandArgs(args);
@@ -376,10 +507,17 @@ function registerCommand(
376
507
  workingTreeState,
377
508
  controller,
378
509
  );
510
+ startResponsive();
379
511
  return;
380
512
  }
381
513
  if (action === "disable") {
382
- disableOsdyPiCommand(ctx, state, workingTreeState, controller);
514
+ disableOsdyPiCommand(
515
+ ctx,
516
+ state,
517
+ workingTreeState,
518
+ controller,
519
+ stopResponsive,
520
+ );
383
521
  return;
384
522
  }
385
523
  if (action === "status") {
@@ -390,6 +528,18 @@ function registerCommand(
390
528
  await handleSoundCommand(rest, ctx, settingsStore);
391
529
  return;
392
530
  }
531
+ if (action === "editor") {
532
+ await handleEditorCommand(
533
+ rest[0],
534
+ rest.slice(1),
535
+ pi,
536
+ ctx,
537
+ state,
538
+ workingTreeState,
539
+ editorSettingsStore,
540
+ );
541
+ return;
542
+ }
393
543
  if (action === "working-tree") {
394
544
  await handleWorkingTreeCommand(
395
545
  rest[0],
@@ -399,6 +549,7 @@ function registerCommand(
399
549
  state,
400
550
  workingState,
401
551
  workingTreeState,
552
+ editorSettingsStore,
402
553
  );
403
554
  return;
404
555
  }
@@ -407,18 +558,11 @@ function registerCommand(
407
558
  return;
408
559
  }
409
560
  if (isHeaderVariant(action)) {
410
- setHeaderVariant(
411
- pi,
412
- ctx,
413
- state,
414
- workingState,
415
- workingTreeState,
416
- action,
417
- );
561
+ setHeaderVariant(pi, ctx, state, workingState, workingTreeState, action);
418
562
  return;
419
563
  }
420
564
  ctx.ui.notify(
421
- "Usage: /osdy-pi enable | disable | status | sound setup | working-tree ... | diff | osdy-theme | classic",
565
+ "Usage: /osdy-pi enable | disable | status | editor auto|extended|simple|on|off|toggle|status | sound setup | working-tree ... | diff | osdy-theme | classic",
422
566
  "warning",
423
567
  );
424
568
  },
@@ -428,14 +572,7 @@ function registerCommand(
428
572
  pi.registerCommand(`osdy-pi-${variant}`, {
429
573
  description: `Switch Osdy Pi header to ${variant}.`,
430
574
  handler: (_args, ctx) => {
431
- setHeaderVariant(
432
- pi,
433
- ctx,
434
- state,
435
- workingState,
436
- workingTreeState,
437
- variant,
438
- );
575
+ setHeaderVariant(pi, ctx, state, workingState, workingTreeState, variant);
439
576
  return Promise.resolve();
440
577
  },
441
578
  });
@@ -445,9 +582,11 @@ function registerCommand(
445
582
  export function registerOsdyPi(pi: ExtensionAPI): void {
446
583
  const state: OsdyState = {
447
584
  enabled: true,
585
+ editorEffective: false,
586
+ editorMode: DEFAULT_EDITOR_MODE,
448
587
  headerVariant: "osdy-theme",
449
- gitLabel: "-",
450
- agentsLabel: "-",
588
+ smallMode: false,
589
+ tui: undefined,
451
590
  workingTreeEnabled: true,
452
591
  workingTreePlacement: "aboveEditor",
453
592
  };
@@ -461,13 +600,16 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
461
600
  const workingTreeState: WorkingTreeState = {
462
601
  enabled: true,
463
602
  loading: false,
603
+ visible: true,
464
604
  snapshot: null,
465
605
  error: undefined,
466
606
  tui: undefined,
467
607
  };
468
608
  const controller = createWorkingController(state, workingState);
469
609
  const settingsStore = createAudioSoundSettingsStore();
610
+ const editorSettingsStore = createEditorSettingsStore();
470
611
  registerAudioNotificationFlags(pi);
612
+ registerAccountProfilesCommand(pi);
471
613
  const audioRouter = createAudioEventRouter(
472
614
  createAudioNotificationService(
473
615
  pi,
@@ -475,6 +617,23 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
475
617
  settingsStore,
476
618
  ),
477
619
  );
620
+ let responsiveCoordinator:
621
+ | ReturnType<typeof createResponsiveCoordinator>
622
+ | undefined;
623
+ let sessionContext: ExtensionContext | undefined;
624
+ const pendingOsdyRefreshes = new Set<ReturnType<typeof setTimeout>>();
625
+ const cancelOsdyRefreshes = (): void => {
626
+ for (const timeout of pendingOsdyRefreshes) clearTimeout(timeout);
627
+ pendingOsdyRefreshes.clear();
628
+ };
629
+ const startResponsive = (): void => responsiveCoordinator?.start();
630
+ const stopResponsive = (): void => responsiveCoordinator?.stop();
631
+
632
+ subscribeQuestionPromptAudioNotification(
633
+ pi,
634
+ { getCurrentSessionContext: () => sessionContext },
635
+ audioRouter,
636
+ );
478
637
 
479
638
  pi.on("agent_start", () => {
480
639
  controller.onAgentStart();
@@ -490,17 +649,47 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
490
649
  pi.on("tool_execution_end", (event, ctx) => {
491
650
  controller.onToolEnd();
492
651
  audioRouter.onToolExecutionEnd(event.isError === true, ctx);
493
- if (event.isError !== true && shouldRefreshWorkingTree(event.toolName)) {
652
+ if (
653
+ state.workingTreeEnabled &&
654
+ event.isError !== true &&
655
+ shouldRefreshWorkingTree(event.toolName)
656
+ ) {
494
657
  void refreshWorkingTree(pi, ctx, workingTreeState);
495
658
  }
496
659
  });
497
660
  pi.on("session_shutdown", () => {
661
+ cancelOsdyRefreshes();
498
662
  controller.onShutdown();
663
+ stopResponsive();
664
+ if (sessionContext) disableOsdyPi(sessionContext, state);
499
665
  workingTreeState.tui = undefined;
666
+ sessionContext = undefined;
500
667
  });
501
- pi.on("session_start", (_event, ctx) => {
502
- loadSessionMetadata(pi, ctx, state);
503
- claimOsdyVisualLayer(pi, ctx, state, workingState, workingTreeState);
668
+ pi.on("session_start", async (_event, ctx) => {
669
+ cancelOsdyRefreshes();
670
+ stopResponsive();
671
+ sessionContext = ctx;
672
+ const editorSettings = await editorSettingsStore.load();
673
+ state.editorMode = editorSettings.editorMode;
674
+ state.workingTreeEnabled = editorSettings.workingTreeEnabled;
675
+ workingTreeState.enabled = editorSettings.workingTreeEnabled;
676
+ if (sessionContext !== ctx) return;
677
+ claimOsdyVisualLayer(
678
+ pi,
679
+ ctx,
680
+ state,
681
+ workingState,
682
+ workingTreeState,
683
+ pendingOsdyRefreshes,
684
+ () => sessionContext === ctx,
685
+ );
686
+ responsiveCoordinator = createResponsiveCoordinator(
687
+ pi,
688
+ ctx,
689
+ state,
690
+ workingTreeState,
691
+ );
692
+ responsiveCoordinator.start();
504
693
  if (state.workingTreeEnabled) {
505
694
  void refreshWorkingTree(pi, ctx, workingTreeState);
506
695
  }
@@ -513,5 +702,8 @@ export function registerOsdyPi(pi: ExtensionAPI): void {
513
702
  workingTreeState,
514
703
  controller,
515
704
  settingsStore,
705
+ editorSettingsStore,
706
+ startResponsive,
707
+ stopResponsive,
516
708
  );
517
709
  }
@@ -2,16 +2,45 @@ import type { TUI } from "@earendil-works/pi-tui";
2
2
 
3
3
  export type AnimationMode = "off" | "intro" | "continuous";
4
4
 
5
+ export const EDITOR_MODES = {
6
+ AUTO: "auto",
7
+ EXTENDED: "extended",
8
+ SIMPLE: "simple",
9
+ } as const;
10
+
11
+ export type EditorMode = (typeof EDITOR_MODES)[keyof typeof EDITOR_MODES];
12
+
13
+ export const DEFAULT_EDITOR_MODE = EDITOR_MODES.AUTO;
14
+
15
+ export interface GlobalEditorSettings {
16
+ version: number;
17
+ editorMode: EditorMode;
18
+ workingTreeEnabled: boolean;
19
+ }
20
+
21
+ export function resolveEffectiveEditorMode(
22
+ mode: EditorMode,
23
+ smallMode: boolean,
24
+ ): EditorMode {
25
+ if (smallMode || mode === EDITOR_MODES.SIMPLE) return EDITOR_MODES.SIMPLE;
26
+ return EDITOR_MODES.EXTENDED;
27
+ }
28
+
29
+ export function shouldShowFooterMetadata(editorEffective: boolean): boolean {
30
+ return !editorEffective;
31
+ }
32
+
5
33
  export type HeaderVariant = "osdy-theme" | "classic";
6
34
 
7
35
  export type WorkingTreePlacement = "aboveEditor" | "belowEditor";
8
36
 
9
37
  export type OsdyState = {
10
38
  enabled: boolean;
39
+ editorEffective: boolean;
40
+ editorMode: EditorMode;
11
41
  headerVariant: HeaderVariant;
12
- previousThemeName?: string;
13
- gitLabel: string;
14
- agentsLabel: string;
42
+ smallMode: boolean;
43
+ tui: TUI | undefined;
15
44
  workingTreeEnabled: boolean;
16
45
  workingTreePlacement: WorkingTreePlacement;
17
46
  };
@@ -46,6 +75,7 @@ export type WorkingTreeSnapshot = {
46
75
  export type WorkingTreeState = {
47
76
  enabled: boolean;
48
77
  loading: boolean;
78
+ visible: boolean;
49
79
  snapshot: WorkingTreeSnapshot | null;
50
80
  error: string | undefined;
51
81
  tui: TUI | undefined;
@@ -72,13 +102,6 @@ export type AssistantSessionEntry = {
72
102
  };
73
103
  };
74
104
 
75
- export type HeaderMetaRow = {
76
- leftLabel: string;
77
- leftValue: string;
78
- rightLabel: string;
79
- rightValue: string;
80
- };
81
-
82
105
  export type SimpleTheme = {
83
106
  fg(name: string, text: string): string;
84
107
  };
@@ -0,0 +1,51 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import test from "node:test";
4
+ // @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
5
+ import * as profileLabel from "./profile-label.ts";
6
+ // @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
7
+ import { shouldShowFooterMetadata } from "./types.ts";
8
+
9
+ void test("footer metadata is shown only when the native editor is effective", () => {
10
+ assert.equal(shouldShowFooterMetadata(false), true);
11
+ assert.equal(shouldShowFooterMetadata(true), false);
12
+ });
13
+
14
+ void test("maximum thinking uses the existing extra-high theme color", () => {
15
+ const source = readFileSync(new URL("./ui.ts", import.meta.url), "utf8");
16
+ assert.match(source, /max:\s*"thinkingXhigh"/);
17
+ });
18
+
19
+ void test("places a managed profile beside the model in simple mode and preserves its fallback", () => {
20
+ assert.equal(
21
+ profileLabel.resolveActiveProfileLabel({ OSDY_PI_PROFILE_NAME: "work" }),
22
+ "work",
23
+ );
24
+ assert.equal(profileLabel.resolveActiveProfileLabel({}), undefined);
25
+ assert.equal(
26
+ profileLabel.formatModelMetadata("gpt-5", "high", "work"),
27
+ "gpt-5 · work · think high",
28
+ );
29
+ assert.equal(
30
+ profileLabel.formatModelMetadata(
31
+ "gpt-5",
32
+ "high",
33
+ profileLabel.resolveActiveProfileLabel({
34
+ OSDY_PI_PROFILE_NAME: "untrusted/profile",
35
+ }),
36
+ ),
37
+ "gpt-5 · think high",
38
+ );
39
+ });
40
+
41
+ void test("replaces the extended editor Osdy-Pi title with a managed profile or preserves its fallback", () => {
42
+ assert.equal(profileLabel.resolveEditorTitleLabel("work"), "work");
43
+ assert.equal(
44
+ profileLabel.resolveEditorTitleLabel(
45
+ profileLabel.resolveActiveProfileLabel({
46
+ OSDY_PI_PROFILE_NAME: "untrusted/profile",
47
+ }),
48
+ ),
49
+ "Osdy-Pi",
50
+ );
51
+ });