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.
- package/README.md +160 -30
- package/bin/osdy-pi.mjs +29 -6
- package/bin/osdy.mjs +43 -0
- package/extensions/osdy-pi/account-profiles.test.ts +111 -97
- package/extensions/osdy-pi/account-profiles.ts +91 -162
- package/extensions/osdy-pi/animation.test.ts +84 -0
- package/extensions/osdy-pi/animation.ts +17 -8
- package/extensions/osdy-pi/codex-usage-ui.test.ts +1145 -0
- package/extensions/osdy-pi/codex-usage-ui.ts +728 -0
- package/extensions/osdy-pi/codex-usage.test.ts +276 -0
- package/extensions/osdy-pi/codex-usage.ts +276 -0
- package/extensions/osdy-pi/constants.ts +134 -34
- package/extensions/osdy-pi/context-usage-ui.test.ts +168 -0
- package/extensions/osdy-pi/context-usage-ui.ts +60 -0
- package/extensions/osdy-pi/editor-settings.test.ts +121 -23
- package/extensions/osdy-pi/editor-settings.ts +39 -3
- package/extensions/osdy-pi/metrics.test.ts +95 -0
- package/extensions/osdy-pi/metrics.ts +39 -1
- package/extensions/osdy-pi/profile-label.ts +2 -2
- package/extensions/osdy-pi/runtime-helpers.test.ts +220 -1
- package/extensions/osdy-pi/runtime-helpers.ts +42 -19
- package/extensions/osdy-pi/runtime.test.ts +355 -7
- package/extensions/osdy-pi/runtime.ts +344 -66
- package/extensions/osdy-pi/types.ts +26 -1
- package/extensions/osdy-pi/ui.test.ts +128 -0
- package/extensions/osdy-pi/ui.ts +57 -40
- package/package.json +5 -2
- package/scripts/osdy-pi-account-profiles.mjs +521 -76
- package/scripts/osdy-pi-gentle-coexistence.mjs +200 -0
- package/scripts/osdy-pi-profile-setup.mjs +175 -0
- package/themes/osdy-pi-catppuccin-frappe.json +6 -2
- package/themes/osdy-pi-catppuccin-latte.json +6 -2
- package/themes/osdy-pi-catppuccin-macchiato.json +6 -2
- package/themes/osdy-pi-catppuccin-mocha.json +6 -2
- package/themes/osdy-pi-dark.json +6 -2
- package/themes/osdy-pi-dracula.json +6 -2
- package/themes/osdy-pi-kanagawa-dragon.json +6 -2
- package/themes/osdy-pi-kanagawa-lotus.json +6 -2
- package/themes/osdy-pi-kanagawa-wave.json +6 -2
- package/themes/osdy-pi-lucent-orange.json +6 -2
- package/themes/osdy-pi-matrix.json +6 -2
- package/themes/osdy-pi-new.json +6 -2
- package/themes/osdy-pi-sexy.json +6 -2
- package/themes/osdy-pi-tokyo-night.json +6 -2
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
|
|
4
|
+
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
5
|
+
import { HEADER_VARIANT_CHOICES, MASCOT_CHOICES } from "./types.ts";
|
|
6
|
+
import type {
|
|
7
|
+
EditorMode,
|
|
8
|
+
GlobalEditorSettings,
|
|
9
|
+
HeaderVariant,
|
|
10
|
+
MascotChoice,
|
|
11
|
+
} from "./types.js";
|
|
5
12
|
|
|
6
|
-
const SUPPORTED_EDITOR_MODES = [
|
|
13
|
+
const SUPPORTED_EDITOR_MODES = [
|
|
14
|
+
"auto",
|
|
15
|
+
"extended",
|
|
16
|
+
"simple",
|
|
17
|
+
] as const satisfies readonly EditorMode[];
|
|
7
18
|
const DEFAULT_EDITOR_MODE: EditorMode = "auto";
|
|
8
19
|
const EDITOR_SETTINGS_VERSION = 1;
|
|
9
20
|
const EDITOR_SETTINGS_FILE_NAME = "settings.json";
|
|
@@ -25,13 +36,34 @@ function getAgentDir(): string {
|
|
|
25
36
|
function createDefaultEditorSettings(): GlobalEditorSettings {
|
|
26
37
|
return {
|
|
27
38
|
version: EDITOR_SETTINGS_VERSION,
|
|
39
|
+
enabled: true,
|
|
28
40
|
editorMode: DEFAULT_EDITOR_MODE,
|
|
29
41
|
workingTreeEnabled: true,
|
|
42
|
+
headerVariant: "osdy-theme",
|
|
43
|
+
mascot: "current",
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
function isEditorMode(value: unknown): value is EditorMode {
|
|
34
|
-
return
|
|
48
|
+
return (
|
|
49
|
+
typeof value === "string" &&
|
|
50
|
+
SUPPORTED_EDITOR_MODES.some((mode) => mode === value)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeHeaderVariant(value: unknown): HeaderVariant {
|
|
55
|
+
return typeof value === "string" &&
|
|
56
|
+
HEADER_VARIANT_CHOICES.includes(value as HeaderVariant)
|
|
57
|
+
? (value as HeaderVariant)
|
|
58
|
+
: "osdy-theme";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeMascotChoice(value: unknown): MascotChoice {
|
|
62
|
+
if (value === "raccoon") return "bts";
|
|
63
|
+
return typeof value === "string" &&
|
|
64
|
+
MASCOT_CHOICES.includes(value as MascotChoice)
|
|
65
|
+
? (value as MascotChoice)
|
|
66
|
+
: "current";
|
|
35
67
|
}
|
|
36
68
|
|
|
37
69
|
export function normalizeEditorSettings(value: unknown): GlobalEditorSettings {
|
|
@@ -44,6 +76,8 @@ export function normalizeEditorSettings(value: unknown): GlobalEditorSettings {
|
|
|
44
76
|
}
|
|
45
77
|
return {
|
|
46
78
|
version: EDITOR_SETTINGS_VERSION,
|
|
79
|
+
enabled:
|
|
80
|
+
typeof recordValue.enabled === "boolean" ? recordValue.enabled : true,
|
|
47
81
|
editorMode: isEditorMode(recordValue.editorMode)
|
|
48
82
|
? recordValue.editorMode
|
|
49
83
|
: DEFAULT_EDITOR_MODE,
|
|
@@ -51,6 +85,8 @@ export function normalizeEditorSettings(value: unknown): GlobalEditorSettings {
|
|
|
51
85
|
typeof recordValue.workingTreeEnabled === "boolean"
|
|
52
86
|
? recordValue.workingTreeEnabled
|
|
53
87
|
: true,
|
|
88
|
+
headerVariant: normalizeHeaderVariant(recordValue.headerVariant),
|
|
89
|
+
mascot: normalizeMascotChoice(recordValue.mascot),
|
|
54
90
|
};
|
|
55
91
|
}
|
|
56
92
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { registerHooks } from "node:module";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
import type { ContextUsageSource } from "./metrics.ts";
|
|
8
|
+
|
|
9
|
+
registerHooks({
|
|
10
|
+
resolve(specifier, context, nextResolve) {
|
|
11
|
+
if (
|
|
12
|
+
specifier.startsWith(".") &&
|
|
13
|
+
specifier.endsWith(".js") &&
|
|
14
|
+
context.parentURL?.endsWith(".ts")
|
|
15
|
+
) {
|
|
16
|
+
const sourceUrl = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
|
|
17
|
+
if (existsSync(fileURLToPath(sourceUrl))) {
|
|
18
|
+
return { shortCircuit: true, url: sourceUrl.href };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return nextResolve(specifier, context);
|
|
22
|
+
},
|
|
23
|
+
load(url, context, nextLoad) {
|
|
24
|
+
if (url.endsWith(".ts")) {
|
|
25
|
+
return {
|
|
26
|
+
format: "module",
|
|
27
|
+
shortCircuit: true,
|
|
28
|
+
source: ts.transpileModule(readFileSync(fileURLToPath(url), "utf8"), {
|
|
29
|
+
compilerOptions: {
|
|
30
|
+
module: ts.ModuleKind.ESNext,
|
|
31
|
+
target: ts.ScriptTarget.ES2022,
|
|
32
|
+
},
|
|
33
|
+
}).outputText,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return nextLoad(url, context);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const { contextUsageData } = await import("./metrics.js");
|
|
41
|
+
|
|
42
|
+
function createContext(percent: number | undefined): ContextUsageSource {
|
|
43
|
+
return {
|
|
44
|
+
getContextUsage: () =>
|
|
45
|
+
percent === undefined ? undefined : { percent, contextWindow: 200_000 },
|
|
46
|
+
sessionManager: {
|
|
47
|
+
getEntries: () => [
|
|
48
|
+
{
|
|
49
|
+
type: "message",
|
|
50
|
+
message: {
|
|
51
|
+
role: "assistant",
|
|
52
|
+
usage: {
|
|
53
|
+
input: 1_500,
|
|
54
|
+
output: 2_500_000,
|
|
55
|
+
cacheRead: 12,
|
|
56
|
+
cacheWrite: 34,
|
|
57
|
+
cost: { total: 1.23456 },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
void test("returns presentation-neutral usage totals and context data", () => {
|
|
67
|
+
assert.deepEqual(contextUsageData(createContext(30)), {
|
|
68
|
+
input: 1_500,
|
|
69
|
+
output: 2_500_000,
|
|
70
|
+
cacheRead: 12,
|
|
71
|
+
cacheWrite: 34,
|
|
72
|
+
cost: 1.23456,
|
|
73
|
+
percent: 30,
|
|
74
|
+
contextWindow: 200_000,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
void test("uses the model window when context usage omits it", () => {
|
|
79
|
+
const context = createContext(30);
|
|
80
|
+
context.getContextUsage = () => ({ percent: 30 });
|
|
81
|
+
context.model = { contextWindow: 272_000 };
|
|
82
|
+
assert.equal(contextUsageData(context).contextWindow, 272_000);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
void test("represents unavailable context usage and window without formatting", () => {
|
|
86
|
+
assert.deepEqual(contextUsageData(createContext(undefined)), {
|
|
87
|
+
input: 1_500,
|
|
88
|
+
output: 2_500_000,
|
|
89
|
+
cacheRead: 12,
|
|
90
|
+
cacheWrite: 34,
|
|
91
|
+
cost: 1.23456,
|
|
92
|
+
percent: undefined,
|
|
93
|
+
contextWindow: undefined,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -34,6 +34,44 @@ function getCacheLabel(totals: UsageTotals): string {
|
|
|
34
34
|
: "";
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export type ContextUsageData = {
|
|
38
|
+
input: number;
|
|
39
|
+
output: number;
|
|
40
|
+
cacheRead: number;
|
|
41
|
+
cacheWrite: number;
|
|
42
|
+
cost: number;
|
|
43
|
+
percent: number | undefined;
|
|
44
|
+
contextWindow: number | undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ContextUsageSource = {
|
|
48
|
+
getContextUsage():
|
|
49
|
+
| {
|
|
50
|
+
percent: number | null;
|
|
51
|
+
contextWindow?: number | null;
|
|
52
|
+
}
|
|
53
|
+
| undefined;
|
|
54
|
+
model?:
|
|
55
|
+
| {
|
|
56
|
+
contextWindow?: number | null;
|
|
57
|
+
}
|
|
58
|
+
| undefined;
|
|
59
|
+
sessionManager: {
|
|
60
|
+
getEntries(): readonly unknown[];
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function contextUsageData(ctx: ContextUsageSource): ContextUsageData {
|
|
65
|
+
const totals = collectUsageTotals(ctx);
|
|
66
|
+
const context = ctx.getContextUsage();
|
|
67
|
+
return {
|
|
68
|
+
...totals,
|
|
69
|
+
percent: context?.percent ?? undefined,
|
|
70
|
+
contextWindow:
|
|
71
|
+
context?.contextWindow ?? ctx.model?.contextWindow ?? undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
37
75
|
export function usageLabel(ctx: ExtensionContext): string {
|
|
38
76
|
const totals = collectUsageTotals(ctx);
|
|
39
77
|
const cacheText = getCacheLabel(totals);
|
|
@@ -74,7 +112,7 @@ function mergeUsageTotals(
|
|
|
74
112
|
};
|
|
75
113
|
}
|
|
76
114
|
|
|
77
|
-
function collectUsageTotals(ctx:
|
|
115
|
+
function collectUsageTotals(ctx: ContextUsageSource): UsageTotals {
|
|
78
116
|
return ctx.sessionManager.getEntries().reduce<UsageTotals>((totals, entry) => {
|
|
79
117
|
return isAssistantSessionEntry(entry)
|
|
80
118
|
? mergeUsageTotals(totals, entry)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const PROFILE_NAME = /^[
|
|
1
|
+
const PROFILE_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,62})$/;
|
|
2
2
|
const RESERVED_PROFILE_NAMES = new Set(["default", "profiles", "auth.json"]);
|
|
3
3
|
|
|
4
4
|
export function resolveActiveProfileLabel(
|
|
@@ -7,7 +7,7 @@ export function resolveActiveProfileLabel(
|
|
|
7
7
|
const profileName = env.OSDY_PI_PROFILE_NAME;
|
|
8
8
|
return typeof profileName === "string" &&
|
|
9
9
|
PROFILE_NAME.test(profileName) &&
|
|
10
|
-
!RESERVED_PROFILE_NAMES.has(profileName)
|
|
10
|
+
!RESERVED_PROFILE_NAMES.has(profileName.toLowerCase())
|
|
11
11
|
? profileName
|
|
12
12
|
: undefined;
|
|
13
13
|
}
|
|
@@ -1,12 +1,131 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { registerHooks } from "node:module";
|
|
2
4
|
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import type {
|
|
7
|
+
ExtensionAPI,
|
|
8
|
+
ExtensionContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import ts from "typescript";
|
|
3
11
|
import {
|
|
4
12
|
DEFAULT_EDITOR_MODE,
|
|
5
13
|
EDITOR_MODES,
|
|
6
14
|
resolveEffectiveEditorMode,
|
|
7
|
-
|
|
15
|
+
type OsdyState,
|
|
16
|
+
type WorkingTreeState,
|
|
17
|
+
type WorkingWidgetState,
|
|
18
|
+
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
8
19
|
} from "./types.ts";
|
|
9
20
|
|
|
21
|
+
registerHooks({
|
|
22
|
+
resolve(specifier, context, nextResolve) {
|
|
23
|
+
if (
|
|
24
|
+
specifier.startsWith(".") &&
|
|
25
|
+
specifier.endsWith(".js") &&
|
|
26
|
+
context.parentURL?.endsWith(".ts")
|
|
27
|
+
) {
|
|
28
|
+
const sourceUrl = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
|
|
29
|
+
if (existsSync(fileURLToPath(sourceUrl))) {
|
|
30
|
+
return { shortCircuit: true, url: sourceUrl.href };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return nextResolve(specifier, context);
|
|
34
|
+
},
|
|
35
|
+
load(url, context, nextLoad) {
|
|
36
|
+
if (url.endsWith(".ts")) {
|
|
37
|
+
const source = readFileSync(fileURLToPath(url), "utf8");
|
|
38
|
+
return {
|
|
39
|
+
format: "module",
|
|
40
|
+
shortCircuit: true,
|
|
41
|
+
source: ts.transpileModule(source, {
|
|
42
|
+
compilerOptions: {
|
|
43
|
+
module: ts.ModuleKind.ESNext,
|
|
44
|
+
target: ts.ScriptTarget.ES2022,
|
|
45
|
+
},
|
|
46
|
+
}).outputText,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return nextLoad(url, context);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
disableOsdyPi,
|
|
55
|
+
mountOsdyEditor,
|
|
56
|
+
mountOsdyUi,
|
|
57
|
+
syncWorkingTreeWidget,
|
|
58
|
+
unmountOsdyEditor,
|
|
59
|
+
} = await import(
|
|
60
|
+
// @ts-expect-error The test hook maps production .js specifiers to TypeScript sources.
|
|
61
|
+
"./runtime-helpers.ts"
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const GENTLE_SHELL_CHANGES_WIDGET_KEY = "gentle-shell-changes";
|
|
65
|
+
const WORKING_TREE_WIDGET_KEY = "osdy-pi-working-tree";
|
|
66
|
+
|
|
67
|
+
type WidgetCall = {
|
|
68
|
+
key: string;
|
|
69
|
+
factory: unknown;
|
|
70
|
+
options: unknown;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function createState(overrides: Partial<OsdyState> = {}): OsdyState {
|
|
74
|
+
return {
|
|
75
|
+
codexUsage: { kind: "idle" },
|
|
76
|
+
enabled: true,
|
|
77
|
+
editorEffective: false,
|
|
78
|
+
editorMode: EDITOR_MODES.SIMPLE,
|
|
79
|
+
fallbackEditorFactory: undefined,
|
|
80
|
+
headerVariant: "osdy-theme",
|
|
81
|
+
mascot: "current",
|
|
82
|
+
smallMode: false,
|
|
83
|
+
tui: undefined,
|
|
84
|
+
workingTreeEnabled: true,
|
|
85
|
+
workingTreePlacement: "aboveEditor",
|
|
86
|
+
...overrides,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createWorkingTreeState(): WorkingTreeState {
|
|
91
|
+
return {
|
|
92
|
+
enabled: true,
|
|
93
|
+
loading: false,
|
|
94
|
+
visible: true,
|
|
95
|
+
snapshot: null,
|
|
96
|
+
error: undefined,
|
|
97
|
+
tui: undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createWorkingState(): WorkingWidgetState {
|
|
102
|
+
return {
|
|
103
|
+
active: false,
|
|
104
|
+
label: "Working...",
|
|
105
|
+
frame: 0,
|
|
106
|
+
timer: undefined,
|
|
107
|
+
tui: undefined,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createContext(widgetCalls: WidgetCall[], editorCalls: unknown[]) {
|
|
112
|
+
return {
|
|
113
|
+
hasUI: true,
|
|
114
|
+
ui: {
|
|
115
|
+
setEditorComponent(component: unknown): void {
|
|
116
|
+
editorCalls.push(component);
|
|
117
|
+
},
|
|
118
|
+
notify(): void {},
|
|
119
|
+
setFooter(): void {},
|
|
120
|
+
setHeader(): void {},
|
|
121
|
+
setWorkingVisible(): void {},
|
|
122
|
+
setWidget(key: string, factory: unknown, options?: unknown): void {
|
|
123
|
+
widgetCalls.push({ key, factory, options });
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
} as unknown as ExtensionContext;
|
|
127
|
+
}
|
|
128
|
+
|
|
10
129
|
void test("auto editor mode is the default and small terminals use the native editor", () => {
|
|
11
130
|
assert.equal(DEFAULT_EDITOR_MODE, EDITOR_MODES.AUTO);
|
|
12
131
|
assert.equal(
|
|
@@ -24,3 +143,103 @@ void test("auto editor mode is the default and small terminals use the native ed
|
|
|
24
143
|
"simple",
|
|
25
144
|
);
|
|
26
145
|
});
|
|
146
|
+
|
|
147
|
+
void test("unmountOsdyEditor removes the custom editor without restoring the fallback", () => {
|
|
148
|
+
const editorCalls: unknown[] = [];
|
|
149
|
+
const ctx = createContext([], editorCalls);
|
|
150
|
+
|
|
151
|
+
unmountOsdyEditor(ctx);
|
|
152
|
+
|
|
153
|
+
assert.deepEqual(editorCalls, [undefined]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
void test("disableOsdyPi restores the captured editor and Osdy can reclaim it", () => {
|
|
157
|
+
const editorCalls: unknown[] = [];
|
|
158
|
+
const ctx = createContext([], editorCalls);
|
|
159
|
+
const fallbackEditorFactory =
|
|
160
|
+
(() => ({})) as unknown as OsdyState["fallbackEditorFactory"];
|
|
161
|
+
const state = createState({ fallbackEditorFactory });
|
|
162
|
+
|
|
163
|
+
disableOsdyPi(ctx, state);
|
|
164
|
+
mountOsdyEditor({} as ExtensionAPI, ctx, state);
|
|
165
|
+
|
|
166
|
+
assert.equal(editorCalls[0], fallbackEditorFactory);
|
|
167
|
+
assert.equal(typeof editorCalls[1], "function");
|
|
168
|
+
assert.notEqual(editorCalls[1], fallbackEditorFactory);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
void test("syncWorkingTreeWidget unmounts a disabled widget", () => {
|
|
172
|
+
const widgetCalls: WidgetCall[] = [];
|
|
173
|
+
const ctx = createContext(widgetCalls, []);
|
|
174
|
+
|
|
175
|
+
syncWorkingTreeWidget(
|
|
176
|
+
ctx,
|
|
177
|
+
createState({ workingTreeEnabled: false }),
|
|
178
|
+
createWorkingState(),
|
|
179
|
+
createWorkingTreeState(),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
assert.deepEqual(widgetCalls, [
|
|
183
|
+
{ key: WORKING_TREE_WIDGET_KEY, factory: undefined, options: undefined },
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
void test("syncWorkingTreeWidget remounts an enabled widget", () => {
|
|
188
|
+
const widgetCalls: WidgetCall[] = [];
|
|
189
|
+
const ctx = createContext(widgetCalls, []);
|
|
190
|
+
|
|
191
|
+
syncWorkingTreeWidget(
|
|
192
|
+
ctx,
|
|
193
|
+
createState({ workingTreePlacement: "belowEditor" }),
|
|
194
|
+
createWorkingState(),
|
|
195
|
+
createWorkingTreeState(),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
assert.equal(widgetCalls.length, 1);
|
|
199
|
+
assert.equal(widgetCalls[0]?.key, WORKING_TREE_WIDGET_KEY);
|
|
200
|
+
assert.equal(typeof widgetCalls[0]?.factory, "function");
|
|
201
|
+
assert.deepEqual(widgetCalls[0]?.options, { placement: "belowEditor" });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
void test("initial UI mount clears Gentle's changed-files widget without affecting Osdy's working tree", () => {
|
|
205
|
+
const widgetCalls: WidgetCall[] = [];
|
|
206
|
+
const ctx = createContext(widgetCalls, []);
|
|
207
|
+
|
|
208
|
+
mountOsdyUi(
|
|
209
|
+
{} as ExtensionAPI,
|
|
210
|
+
ctx,
|
|
211
|
+
createState(),
|
|
212
|
+
createWorkingState(),
|
|
213
|
+
createWorkingTreeState(),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
assert.deepEqual(widgetCalls[0], {
|
|
217
|
+
key: GENTLE_SHELL_CHANGES_WIDGET_KEY,
|
|
218
|
+
factory: undefined,
|
|
219
|
+
options: undefined,
|
|
220
|
+
});
|
|
221
|
+
assert.equal(widgetCalls[1]?.key, "osdy-pi-working");
|
|
222
|
+
assert.equal(widgetCalls[2]?.key, WORKING_TREE_WIDGET_KEY);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
void test("initial UI mount leaves a persisted disabled working tree unmounted", () => {
|
|
226
|
+
const widgetCalls: WidgetCall[] = [];
|
|
227
|
+
const editorCalls: unknown[] = [];
|
|
228
|
+
const ctx = createContext(widgetCalls, editorCalls);
|
|
229
|
+
|
|
230
|
+
mountOsdyUi(
|
|
231
|
+
{} as ExtensionAPI,
|
|
232
|
+
ctx,
|
|
233
|
+
createState({ workingTreeEnabled: false }),
|
|
234
|
+
createWorkingState(),
|
|
235
|
+
createWorkingTreeState(),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const workingTreeCalls = widgetCalls.filter(
|
|
239
|
+
(call) => call.key === WORKING_TREE_WIDGET_KEY,
|
|
240
|
+
);
|
|
241
|
+
assert.deepEqual(workingTreeCalls, [
|
|
242
|
+
{ key: WORKING_TREE_WIDGET_KEY, factory: undefined, options: undefined },
|
|
243
|
+
]);
|
|
244
|
+
assert.deepEqual(editorCalls, []);
|
|
245
|
+
});
|
|
@@ -20,10 +20,18 @@ import {
|
|
|
20
20
|
import { createWorkingTreeWidgetFactory } from "./working-tree.js";
|
|
21
21
|
import { isSmallResponsiveMode } from "./utils.js";
|
|
22
22
|
|
|
23
|
+
const GENTLE_SHELL_CHANGES_WIDGET_KEY = "gentle-shell-changes";
|
|
23
24
|
const RESPONSIVE_WATCH_INTERVAL_MS = 150;
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
-
|
|
26
|
+
export function clearGentleShellChangesWidget(ctx: ExtensionContext): void {
|
|
27
|
+
ctx.ui.setWidget(GENTLE_SHELL_CHANGES_WIDGET_KEY, undefined);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function workingTreeEffective(
|
|
31
|
+
state: OsdyState,
|
|
32
|
+
): "visible" | "hidden" | "unmounted" {
|
|
33
|
+
if (!state.enabled || !state.workingTreeEnabled) return "unmounted";
|
|
34
|
+
return state.smallMode ? "hidden" : "visible";
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
function desiredSmallMode(state: OsdyState): boolean {
|
|
@@ -51,7 +59,7 @@ export function reconcileResponsiveUi(
|
|
|
51
59
|
resolveEffectiveEditorMode(state.editorMode, state.smallMode) === "extended";
|
|
52
60
|
if (state.editorEffective !== editorEffective) {
|
|
53
61
|
state.editorEffective = editorEffective;
|
|
54
|
-
if (editorEffective) mountOsdyEditor(pi, ctx);
|
|
62
|
+
if (editorEffective) mountOsdyEditor(pi, ctx, state);
|
|
55
63
|
else unmountOsdyEditor(ctx);
|
|
56
64
|
}
|
|
57
65
|
const treeVisible = state.workingTreeEnabled && !state.smallMode;
|
|
@@ -88,14 +96,39 @@ export function createResponsiveCoordinator(
|
|
|
88
96
|
};
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
export function mountOsdyEditor(
|
|
92
|
-
|
|
99
|
+
export function mountOsdyEditor(
|
|
100
|
+
pi: ExtensionAPI,
|
|
101
|
+
ctx: ExtensionContext,
|
|
102
|
+
state: OsdyState,
|
|
103
|
+
): void {
|
|
104
|
+
ctx.ui.setEditorComponent(createEditorComponent(pi, ctx, state));
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
export function unmountOsdyEditor(ctx: ExtensionContext): void {
|
|
96
108
|
ctx.ui.setEditorComponent(undefined);
|
|
97
109
|
}
|
|
98
110
|
|
|
111
|
+
export function syncWorkingTreeWidget(
|
|
112
|
+
ctx: ExtensionContext,
|
|
113
|
+
state: OsdyState,
|
|
114
|
+
workingState: WorkingWidgetState,
|
|
115
|
+
workingTreeState: WorkingTreeState,
|
|
116
|
+
): void {
|
|
117
|
+
if (!state.enabled || !state.workingTreeEnabled) {
|
|
118
|
+
ctx.ui.setWidget(WORKING_TREE_WIDGET_KEY, undefined);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
ctx.ui.setWidget(
|
|
122
|
+
WORKING_TREE_WIDGET_KEY,
|
|
123
|
+
createWorkingTreeWidgetFactory(
|
|
124
|
+
workingTreeState,
|
|
125
|
+
workingState,
|
|
126
|
+
state.workingTreePlacement,
|
|
127
|
+
),
|
|
128
|
+
{ placement: state.workingTreePlacement },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
export function mountOsdyUi(
|
|
100
133
|
pi: ExtensionAPI,
|
|
101
134
|
ctx: ExtensionContext,
|
|
@@ -109,20 +142,13 @@ export function mountOsdyUi(
|
|
|
109
142
|
return createFooterComponent(pi, ctx, state, footerData, theme);
|
|
110
143
|
});
|
|
111
144
|
ctx.ui.setWorkingVisible(false);
|
|
145
|
+
clearGentleShellChangesWidget(ctx);
|
|
112
146
|
ctx.ui.setWidget(
|
|
113
147
|
WORKING_WIDGET_KEY,
|
|
114
148
|
createWorkingWidgetFactory(workingState),
|
|
115
149
|
{ placement: "aboveEditor" },
|
|
116
150
|
);
|
|
117
|
-
ctx
|
|
118
|
-
WORKING_TREE_WIDGET_KEY,
|
|
119
|
-
createWorkingTreeWidgetFactory(
|
|
120
|
-
workingTreeState,
|
|
121
|
-
workingState,
|
|
122
|
-
state.workingTreePlacement,
|
|
123
|
-
),
|
|
124
|
-
{ placement: state.workingTreePlacement },
|
|
125
|
-
);
|
|
151
|
+
syncWorkingTreeWidget(ctx, state, workingState, workingTreeState);
|
|
126
152
|
reconcileResponsiveUi(pi, ctx, state, workingTreeState);
|
|
127
153
|
}
|
|
128
154
|
|
|
@@ -132,17 +158,15 @@ export function applyOsdyPi(
|
|
|
132
158
|
state: OsdyState,
|
|
133
159
|
workingState: WorkingWidgetState,
|
|
134
160
|
workingTreeState: WorkingTreeState,
|
|
135
|
-
notify = false,
|
|
136
161
|
): void {
|
|
137
162
|
if (!ctx.hasUI) return;
|
|
138
163
|
mountOsdyUi(pi, ctx, state, workingState, workingTreeState);
|
|
139
|
-
if (notify) ctx.ui.notify("osdy-pi enabled", "info");
|
|
140
164
|
}
|
|
141
165
|
|
|
142
166
|
export function disableOsdyPi(ctx: ExtensionContext, state: OsdyState): void {
|
|
143
167
|
if (!ctx.hasUI) return;
|
|
144
168
|
ctx.ui.setHeader(undefined);
|
|
145
|
-
ctx.ui.setEditorComponent(
|
|
169
|
+
ctx.ui.setEditorComponent(state.fallbackEditorFactory);
|
|
146
170
|
ctx.ui.setFooter(undefined);
|
|
147
171
|
ctx.ui.setWidget(WORKING_WIDGET_KEY, undefined);
|
|
148
172
|
ctx.ui.setWidget(WORKING_TREE_WIDGET_KEY, undefined);
|
|
@@ -150,12 +174,11 @@ export function disableOsdyPi(ctx: ExtensionContext, state: OsdyState): void {
|
|
|
150
174
|
state.editorEffective = false;
|
|
151
175
|
state.smallMode = false;
|
|
152
176
|
state.tui = undefined;
|
|
153
|
-
ctx.ui.notify("osdy-pi disabled", "info");
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
export function notifyStatus(ctx: ExtensionContext, state: OsdyState): void {
|
|
157
180
|
ctx.ui.notify(
|
|
158
|
-
`osdy-pi ${state.enabled ? "enabled" : "disabled"} · editor ${state.editorMode}, effective ${state.editorEffective ? "extended" : "simple/native"} · working-tree desired ${state.workingTreeEnabled ? "on" : "off"}, effective ${workingTreeEffective(state)
|
|
181
|
+
`osdy-pi ${state.enabled ? "enabled" : "disabled"} · editor ${state.editorMode}, effective ${state.editorEffective ? "extended" : "simple/native"} · working-tree desired ${state.workingTreeEnabled ? "on" : "off"}, effective ${workingTreeEffective(state)} · widget ${state.workingTreePlacement === "aboveEditor" ? "top" : "bottom"} · theme ${ctx.ui.theme.name ?? "unknown"} · style ${state.headerVariant} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
|
|
159
182
|
"info",
|
|
160
183
|
);
|
|
161
184
|
}
|