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
|
-
|
|
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(
|
|
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.
|
|
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": "
|
|
86
|
+
"releaseSource": "db95251d5b6a8a60eaf0b4e21b2c44e2afa359f9",
|
|
87
87
|
"repository": {
|
|
88
88
|
"type": "git",
|
|
89
89
|
"url": "git+https://github.com/OsdyOrtiz/Osdy-Pi.git"
|