osdy-pi 1.11.1 → 1.11.2

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.
package/README.md CHANGED
@@ -240,12 +240,14 @@ Profile names accept ASCII letters, numbers, and hyphens, up to 63 characters, p
240
240
 
241
241
  ### See and switch the active account
242
242
 
243
- When Pi was launched through a profile, Osdy Pi shows its profile name in the editor:
243
+ Direct `pi` startup restores the last activated profile label from valid active-account metadata. A valid explicit launcher profile (`OSDY_PI_PROFILE_NAME`) takes precedence. Osdy Pi shows the profile name in the editor:
244
244
 
245
245
  - **Simple/native editor:** beside the model, for example `gpt-5.6-sol · personal · think high`.
246
246
  - **Extended/framed editor:** `personal` replaces the `Osdy-Pi` title.
247
247
  - **No managed profile:** the existing model line and `Osdy-Pi` title remain unchanged.
248
248
 
249
+ Label recovery is visual only: it does not switch credentials or change the configured default. Missing, invalid, unreadable, or stale active-account metadata keeps the existing fallback.
250
+
249
251
  `account use <name>` activates the profile in Pi's shared agent directory, saves it as active/default, then starts Pi. Inside Pi, `/osdy-account` waits for active work to finish and swaps the canonical `auth.json` in place; the next request resolves the new account without a spawn, shutdown, session handoff, or restart.
250
252
 
251
253
  To resume a specific session directly from the terminal:
@@ -12,6 +12,46 @@ export function resolveActiveProfileLabel(
12
12
  : undefined;
13
13
  }
14
14
 
15
+ function isCallable(value: unknown): value is (...args: unknown[]) => unknown {
16
+ return typeof value === "function";
17
+ }
18
+
19
+ async function loadAccountProfiles(): Promise<unknown> {
20
+ const moduleUrl = new URL(
21
+ "../../scripts/osdy-pi-account-profiles.mjs",
22
+ import.meta.url,
23
+ ).href;
24
+ return import(moduleUrl);
25
+ }
26
+
27
+ /** Read visual identity only; never activate credentials or consult defaults. */
28
+ export async function readLastActiveProfileLabel(
29
+ env: NodeJS.ProcessEnv = process.env,
30
+ loadModule: () => Promise<unknown> = loadAccountProfiles,
31
+ ): Promise<string | undefined> {
32
+ try {
33
+ const profiles = await loadModule();
34
+ if (
35
+ typeof profiles !== "object" || profiles === null ||
36
+ !("getSharedAgentDir" in profiles) ||
37
+ !isCallable(profiles.getSharedAgentDir) ||
38
+ !("readActiveAccount" in profiles) ||
39
+ !isCallable(profiles.readActiveAccount)
40
+ ) return undefined;
41
+ const directory: unknown = profiles.getSharedAgentDir(env);
42
+ if (typeof directory !== "string") return undefined;
43
+ const active: unknown = await profiles.readActiveAccount(directory);
44
+ if (
45
+ typeof active !== "object" || active === null ||
46
+ !("status" in active) || active.status !== "valid" ||
47
+ !("profile" in active) || typeof active.profile !== "string"
48
+ ) return undefined;
49
+ return resolveActiveProfileLabel({ OSDY_PI_PROFILE_NAME: active.profile });
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
15
55
  export function formatModelMetadata(
16
56
  model: string,
17
57
  thinkingLevel: string,
@@ -41,6 +41,8 @@ registerHooks({
41
41
  },
42
42
  });
43
43
 
44
+ const profileLabels = await import("./profile-label.js");
45
+
44
46
  const { PLUGIN_EVENTS, subscribeQuestionPromptAudioNotification } =
45
47
  await import("./plugin-events.js");
46
48
  const {
@@ -211,6 +213,115 @@ void test("TODO command persists on/off in a disposable custom profile before su
211
213
  }
212
214
  });
213
215
 
216
+ void test("startup label reader validates the dynamic module and active metadata only", async () => {
217
+ const env = { OSDY_PI_SHARED_AGENT_DIR: "/unused/shared" };
218
+ let reads = 0;
219
+ for (const [record, expected] of [
220
+ [{ status: "valid", profile: "work" }, "work"],
221
+ [{ status: "unset" }, undefined],
222
+ [{ status: "invalid" }, undefined],
223
+ [{ status: "valid", profile: "default" }, undefined],
224
+ [{ status: "valid", profile: "../bad" }, undefined],
225
+ [null, undefined],
226
+ ] as const) {
227
+ assert.equal(await profileLabels.readLastActiveProfileLabel(env, () => Promise.resolve({
228
+ getSharedAgentDir: (received: NodeJS.ProcessEnv) => {
229
+ assert.equal(received, env);
230
+ return "/unused/shared";
231
+ },
232
+ readActiveAccount: (directory: string) => {
233
+ assert.equal(directory, "/unused/shared");
234
+ reads++;
235
+ return Promise.resolve(record);
236
+ },
237
+ })), expected);
238
+ }
239
+ assert.equal(reads, 6);
240
+ for (const module of [null, {}, { getSharedAgentDir: "bad" }, {
241
+ getSharedAgentDir: () => 42, readActiveAccount: () => { throw new Error("unexpected read"); },
242
+ }, {
243
+ getSharedAgentDir: () => "/unused/shared", readActiveAccount: () => { throw new Error("read denied"); },
244
+ }]) assert.equal(await profileLabels.readLastActiveProfileLabel(env, () => Promise.resolve(module)), undefined);
245
+ assert.equal(await profileLabels.readLastActiveProfileLabel(env, () => Promise.reject(new Error("import failed"))), undefined);
246
+ });
247
+
248
+ void test("real session_start restores labels before editor mounting and rejects stale results", async () => {
249
+ const previousDir = process.env.PI_CODING_AGENT_DIR;
250
+ const previousName = process.env.OSDY_PI_PROFILE_NAME;
251
+ // This nonexistent package-local directory prevents reads of personal settings.
252
+ process.env.PI_CODING_AGENT_DIR = fileURLToPath(new URL("./.startup-test-missing", import.meta.url));
253
+ try {
254
+ for (const scenario of ["restore", "invalid-env", "explicit", "unset", "invalid", "deleted", "failure", "replace", "shutdown", "switch"] as const) {
255
+ delete process.env.OSDY_PI_PROFILE_NAME;
256
+ if (scenario === "explicit") process.env.OSDY_PI_PROFILE_NAME = "launcher";
257
+ if (scenario === "invalid-env") process.env.OSDY_PI_PROFILE_NAME = "../invalid";
258
+ const handlers = new Map<string, Array<(event: unknown, ctx: ExtensionContext) => Promise<void> | void>>();
259
+ const mounted: string[] = [];
260
+ let reads = 0;
261
+ let complete: (value: string | undefined) => void = () => {};
262
+ let entered: () => void = () => {};
263
+ const started = new Promise<void>((resolve) => { entered = resolve; });
264
+ const pending = new Promise<string | undefined>((resolve) => { complete = resolve; });
265
+ const pi = {
266
+ on: (name: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) => {
267
+ const registered = handlers.get(name) ?? [];
268
+ registered.push(handler);
269
+ handlers.set(name, registered);
270
+ },
271
+ registerCommand: () => {}, registerFlag: () => {}, registerMessageRenderer: () => {}, registerEntryRenderer: () => {},
272
+ events: { on: () => () => {} },
273
+ exec: () => Promise.resolve({ code: 1, stdout: "", stderr: "unavailable" }),
274
+ } as unknown as ExtensionAPI;
275
+ const ctx = {
276
+ hasUI: true, cwd: process.env.PI_CODING_AGENT_DIR,
277
+ modelRegistry: { getProviderAuth: () => Promise.resolve(undefined) },
278
+ ui: {
279
+ getEditorComponent: () => undefined,
280
+ setEditorComponent: () => {
281
+ const label = profileLabels.resolveActiveProfileLabel();
282
+ mounted.push(`${profileLabels.resolveEditorTitleLabel(label)}|${profileLabels.formatModelMetadata("test-model", "high", label)}`);
283
+ },
284
+ setHeader: () => {}, setFooter: () => {}, setWidget: () => {}, setWorkingVisible: () => {},
285
+ },
286
+ } as unknown as ExtensionContext;
287
+ await registerOsdyPi(pi, { readActiveProfile: () => {
288
+ reads++; entered();
289
+ return scenario === "failure" ? Promise.reject(new Error("read failed")) : pending;
290
+ } });
291
+ // Role-marker resets register first; the runtime lifecycle registers second.
292
+ const start = handlers.get("session_start")?.[1];
293
+ const shutdown = handlers.get("session_shutdown")?.[1];
294
+ assert.ok(start); assert.ok(shutdown);
295
+ try {
296
+ const startup = start({}, ctx);
297
+ if (scenario !== "explicit") {
298
+ await Promise.race([started, Promise.resolve(startup)]);
299
+ assert.equal(reads, 1, "session_start must read the last active identity");
300
+ }
301
+ if (scenario === "switch") process.env.OSDY_PI_PROFILE_NAME = "new-selection";
302
+ if (scenario === "shutdown") await shutdown({}, ctx);
303
+ let replacement: Promise<void> | void = undefined;
304
+ if (scenario === "replace") {
305
+ process.env.OSDY_PI_PROFILE_NAME = "new-session";
306
+ replacement = start({}, { ...ctx });
307
+ }
308
+ complete(["unset", "invalid", "deleted"].includes(scenario) ? undefined : "last-active");
309
+ await startup;
310
+ await replacement;
311
+ const expected = scenario === "explicit" ? "launcher" : scenario === "switch" ? "new-selection" : scenario === "replace" ? "new-session" :
312
+ ["unset", "invalid", "deleted", "failure", "shutdown"].includes(scenario) ? undefined : "last-active";
313
+ assert.equal(process.env.OSDY_PI_PROFILE_NAME, expected, scenario);
314
+ assert.equal(reads, scenario === "explicit" ? 0 : 1, scenario);
315
+ assert.equal(mounted.length, scenario === "shutdown" ? 0 : 1, scenario);
316
+ if (mounted.length) assert.equal(mounted[0], `${expected ?? "Osdy-Pi"}|test-model · ${expected ? `${expected} · ` : ""}think high`, scenario);
317
+ } finally { await shutdown({}, ctx); }
318
+ }
319
+ } finally {
320
+ if (previousDir === undefined) delete process.env.PI_CODING_AGENT_DIR; else process.env.PI_CODING_AGENT_DIR = previousDir;
321
+ if (previousName === undefined) delete process.env.OSDY_PI_PROFILE_NAME; else process.env.OSDY_PI_PROFILE_NAME = previousName;
322
+ }
323
+ });
324
+
214
325
  class TestEventBus {
215
326
  event: string | undefined;
216
327
  handler: ((data: unknown) => void) | undefined;
@@ -52,7 +52,7 @@ import {
52
52
  } from "./codex-usage.js";
53
53
  import { showCodexUsagePanel } from "./codex-usage-ui.js";
54
54
  import { modelLabel } from "./metrics.js";
55
- import { resolveActiveProfileLabel } from "./profile-label.js";
55
+ import { readLastActiveProfileLabel, resolveActiveProfileLabel } from "./profile-label.js";
56
56
  import {
57
57
  createWorkingController,
58
58
  type WorkingController,
@@ -961,7 +961,10 @@ async function sdkTodoActive(pi: ExtensionAPI): Promise<boolean> {
961
961
  } catch { return false; }
962
962
  }
963
963
 
964
- export async function registerOsdyPi(pi: ExtensionAPI): Promise<void> {
964
+ export async function registerOsdyPi(
965
+ pi: ExtensionAPI,
966
+ dependencies: { readActiveProfile?: () => Promise<string | undefined> } = {},
967
+ ): Promise<void> {
965
968
  const state: OsdyState = {
966
969
  codexUsage: { kind: "idle" },
967
970
  enabled: true,
@@ -1109,6 +1112,20 @@ export async function registerOsdyPi(pi: ExtensionAPI): Promise<void> {
1109
1112
  cancelOsdyRefreshes();
1110
1113
  stopResponsive();
1111
1114
  sessionContext = ctx;
1115
+ const startupProfile = process.env.OSDY_PI_PROFILE_NAME;
1116
+ if (resolveActiveProfileLabel() === undefined) {
1117
+ try {
1118
+ const profile = await (dependencies.readActiveProfile ?? readLastActiveProfileLabel)();
1119
+ if (sessionContext !== ctx) return;
1120
+ if (
1121
+ process.env.OSDY_PI_PROFILE_NAME === startupProfile &&
1122
+ resolveActiveProfileLabel({ OSDY_PI_PROFILE_NAME: profile }) !== undefined
1123
+ ) process.env.OSDY_PI_PROFILE_NAME = profile;
1124
+ } catch {
1125
+ // Visual metadata failures must not prevent session startup.
1126
+ }
1127
+ }
1128
+ if (sessionContext !== ctx) return;
1112
1129
  const editorSettings = await editorSettingsStore.load();
1113
1130
  if (sessionContext !== ctx) return;
1114
1131
  state.fallbackEditorFactory = ctx.ui.getEditorComponent();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osdy-pi",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
4
4
  "description": "Osdy Pi package: themes, header, and custom editor for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -83,7 +83,7 @@
83
83
  "typescript": "^5.9.3",
84
84
  "typescript-eslint": "^8.46.3"
85
85
  },
86
- "releaseSource": "d1e976e64df61fc489766e9512fdf59b7948e9ca",
86
+ "releaseSource": "db95251d5b6a8a60eaf0b4e21b2c44e2afa359f9",
87
87
  "repository": {
88
88
  "type": "git",
89
89
  "url": "git+https://github.com/OsdyOrtiz/Osdy-Pi.git"