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.
- package/README.md +209 -161
- package/bin/osdy-pi.mjs +127 -0
- package/extensions/osdy-pi/account-profiles.test.ts +517 -0
- package/extensions/osdy-pi/account-profiles.ts +581 -0
- package/extensions/osdy-pi/animation.ts +47 -5
- package/extensions/osdy-pi/constants.ts +282 -103
- package/extensions/osdy-pi/diff-panel.ts +78 -24
- package/extensions/osdy-pi/editor-settings.test.ts +163 -0
- package/extensions/osdy-pi/editor-settings.ts +112 -0
- package/extensions/osdy-pi/metrics.ts +8 -135
- package/extensions/osdy-pi/plugin-events.ts +26 -0
- package/extensions/osdy-pi/profile-label.ts +29 -0
- package/extensions/osdy-pi/runtime-helpers.test.ts +26 -0
- package/extensions/osdy-pi/runtime-helpers.ts +87 -44
- package/extensions/osdy-pi/runtime.test.ts +62 -0
- package/extensions/osdy-pi/runtime.ts +256 -64
- package/extensions/osdy-pi/types.ts +33 -10
- package/extensions/osdy-pi/ui.test.ts +51 -0
- package/extensions/osdy-pi/ui.ts +298 -114
- package/extensions/osdy-pi/utils.ts +43 -1
- package/extensions/osdy-pi/working-tree.ts +25 -6
- package/package.json +22 -5
- package/scripts/osdy-pi-account-profiles.mjs +501 -0
- package/themes/osdy-pi-catppuccin-frappe.json +94 -0
- package/themes/osdy-pi-catppuccin-latte.json +94 -0
- package/themes/osdy-pi-catppuccin-macchiato.json +94 -0
- package/themes/osdy-pi-catppuccin-mocha.json +94 -0
- package/themes/osdy-pi-dark.json +0 -6
- package/themes/osdy-pi-dracula.json +83 -0
- package/themes/osdy-pi-kanagawa-dragon.json +91 -0
- package/themes/osdy-pi-kanagawa-lotus.json +92 -0
- package/themes/osdy-pi-kanagawa-wave.json +91 -0
- package/themes/osdy-pi-lucent-orange.json +80 -0
- package/themes/osdy-pi-matrix.json +83 -0
- package/themes/osdy-pi-new.json +91 -0
- package/themes/osdy-pi-sexy.json +84 -0
- package/themes/osdy-pi-tokyo-night.json +83 -0
- package/themes/osdy-pi-light.json +0 -84
- package/themes/osdy-pi-simple.json +0 -83
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_EDITOR_MODE,
|
|
5
|
+
EDITOR_MODES,
|
|
6
|
+
resolveEffectiveEditorMode,
|
|
7
|
+
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
|
|
10
|
+
void test("auto editor mode is the default and small terminals use the native editor", () => {
|
|
11
|
+
assert.equal(DEFAULT_EDITOR_MODE, EDITOR_MODES.AUTO);
|
|
12
|
+
assert.equal(
|
|
13
|
+
resolveEffectiveEditorMode(DEFAULT_EDITOR_MODE, false),
|
|
14
|
+
"extended",
|
|
15
|
+
);
|
|
16
|
+
assert.equal(resolveEffectiveEditorMode(DEFAULT_EDITOR_MODE, true), "simple");
|
|
17
|
+
assert.equal(resolveEffectiveEditorMode(EDITOR_MODES.SIMPLE, false), "simple");
|
|
18
|
+
assert.equal(
|
|
19
|
+
resolveEffectiveEditorMode(EDITOR_MODES.EXTENDED, false),
|
|
20
|
+
"extended",
|
|
21
|
+
);
|
|
22
|
+
assert.equal(
|
|
23
|
+
resolveEffectiveEditorMode(EDITOR_MODES.EXTENDED, true),
|
|
24
|
+
"simple",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
@@ -3,16 +3,13 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { asciiAnimationMode } from "./animation.js";
|
|
6
|
-
import {
|
|
7
|
-
THEME_NAME,
|
|
8
|
-
WORKING_TREE_WIDGET_KEY,
|
|
9
|
-
WORKING_WIDGET_KEY,
|
|
10
|
-
} from "./constants.js";
|
|
6
|
+
import { WORKING_TREE_WIDGET_KEY, WORKING_WIDGET_KEY } from "./constants.js";
|
|
11
7
|
import { modelLabel, usageLabel } from "./metrics.js";
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
import {
|
|
9
|
+
resolveEffectiveEditorMode,
|
|
10
|
+
type OsdyState,
|
|
11
|
+
type WorkingTreeState,
|
|
12
|
+
type WorkingWidgetState,
|
|
16
13
|
} from "./types.js";
|
|
17
14
|
import {
|
|
18
15
|
createEditorComponent,
|
|
@@ -21,39 +18,82 @@ import {
|
|
|
21
18
|
createWorkingWidgetFactory,
|
|
22
19
|
} from "./ui.js";
|
|
23
20
|
import { createWorkingTreeWidgetFactory } from "./working-tree.js";
|
|
21
|
+
import { isSmallResponsiveMode } from "./utils.js";
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
const RESPONSIVE_WATCH_INTERVAL_MS = 150;
|
|
24
|
+
|
|
25
|
+
function workingTreeEffective(state: OsdyState): boolean {
|
|
26
|
+
return state.workingTreeEnabled && !state.smallMode && state.enabled;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function desiredSmallMode(state: OsdyState): boolean {
|
|
30
|
+
const tui = state.tui;
|
|
31
|
+
return tui
|
|
32
|
+
? isSmallResponsiveMode(
|
|
33
|
+
state.headerVariant,
|
|
34
|
+
tui.terminal.columns,
|
|
35
|
+
tui.terminal.rows,
|
|
36
|
+
)
|
|
37
|
+
: false;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
export function
|
|
40
|
+
export function reconcileResponsiveUi(
|
|
41
|
+
pi: ExtensionAPI,
|
|
37
42
|
ctx: ExtensionContext,
|
|
38
43
|
state: OsdyState,
|
|
44
|
+
workingTreeState: WorkingTreeState,
|
|
39
45
|
): void {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
if (!ctx.hasUI || !state.enabled) return;
|
|
47
|
+
const nextSmallMode = desiredSmallMode(state);
|
|
48
|
+
const smallModeChanged = nextSmallMode !== state.smallMode;
|
|
49
|
+
state.smallMode = nextSmallMode;
|
|
50
|
+
const editorEffective =
|
|
51
|
+
resolveEffectiveEditorMode(state.editorMode, state.smallMode) === "extended";
|
|
52
|
+
if (state.editorEffective !== editorEffective) {
|
|
53
|
+
state.editorEffective = editorEffective;
|
|
54
|
+
if (editorEffective) mountOsdyEditor(pi, ctx);
|
|
55
|
+
else unmountOsdyEditor(ctx);
|
|
43
56
|
}
|
|
57
|
+
const treeVisible = state.workingTreeEnabled && !state.smallMode;
|
|
58
|
+
if (workingTreeState.visible !== treeVisible) {
|
|
59
|
+
workingTreeState.visible = treeVisible;
|
|
60
|
+
if (!smallModeChanged) workingTreeState.tui?.requestRender();
|
|
61
|
+
}
|
|
62
|
+
if (smallModeChanged) (state.tui ?? workingTreeState.tui)?.requestRender();
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
export function createResponsiveCoordinator(
|
|
66
|
+
pi: ExtensionAPI,
|
|
67
|
+
ctx: ExtensionContext,
|
|
68
|
+
state: OsdyState,
|
|
69
|
+
workingTreeState: WorkingTreeState,
|
|
70
|
+
) {
|
|
71
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
72
|
+
return {
|
|
73
|
+
start(): void {
|
|
74
|
+
if (timer) return;
|
|
75
|
+
reconcileResponsiveUi(pi, ctx, state, workingTreeState);
|
|
76
|
+
timer = setInterval(() => {
|
|
77
|
+
if (!state.enabled) return;
|
|
78
|
+
const nextSmallMode = desiredSmallMode(state);
|
|
79
|
+
if (nextSmallMode !== state.smallMode) {
|
|
80
|
+
reconcileResponsiveUi(pi, ctx, state, workingTreeState);
|
|
81
|
+
}
|
|
82
|
+
}, RESPONSIVE_WATCH_INTERVAL_MS);
|
|
83
|
+
},
|
|
84
|
+
stop(): void {
|
|
85
|
+
if (timer) clearInterval(timer);
|
|
86
|
+
timer = undefined;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function mountOsdyEditor(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
92
|
+
ctx.ui.setEditorComponent(createEditorComponent(pi, ctx));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function unmountOsdyEditor(ctx: ExtensionContext): void {
|
|
96
|
+
ctx.ui.setEditorComponent(undefined);
|
|
57
97
|
}
|
|
58
98
|
|
|
59
99
|
export function mountOsdyUi(
|
|
@@ -64,9 +104,10 @@ export function mountOsdyUi(
|
|
|
64
104
|
workingTreeState: WorkingTreeState,
|
|
65
105
|
): void {
|
|
66
106
|
ctx.ui.setHeader(createHeaderComponent(pi, ctx, state));
|
|
67
|
-
ctx.ui.setFooter((
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
108
|
+
state.tui = tui;
|
|
109
|
+
return createFooterComponent(pi, ctx, state, footerData, theme);
|
|
110
|
+
});
|
|
70
111
|
ctx.ui.setWorkingVisible(false);
|
|
71
112
|
ctx.ui.setWidget(
|
|
72
113
|
WORKING_WIDGET_KEY,
|
|
@@ -75,10 +116,14 @@ export function mountOsdyUi(
|
|
|
75
116
|
);
|
|
76
117
|
ctx.ui.setWidget(
|
|
77
118
|
WORKING_TREE_WIDGET_KEY,
|
|
78
|
-
createWorkingTreeWidgetFactory(
|
|
119
|
+
createWorkingTreeWidgetFactory(
|
|
120
|
+
workingTreeState,
|
|
121
|
+
workingState,
|
|
122
|
+
state.workingTreePlacement,
|
|
123
|
+
),
|
|
79
124
|
{ placement: state.workingTreePlacement },
|
|
80
125
|
);
|
|
81
|
-
|
|
126
|
+
reconcileResponsiveUi(pi, ctx, state, workingTreeState);
|
|
82
127
|
}
|
|
83
128
|
|
|
84
129
|
export function applyOsdyPi(
|
|
@@ -90,8 +135,6 @@ export function applyOsdyPi(
|
|
|
90
135
|
notify = false,
|
|
91
136
|
): void {
|
|
92
137
|
if (!ctx.hasUI) return;
|
|
93
|
-
rememberPreviousTheme(ctx, state);
|
|
94
|
-
applyOsdyTheme(ctx);
|
|
95
138
|
mountOsdyUi(pi, ctx, state, workingState, workingTreeState);
|
|
96
139
|
if (notify) ctx.ui.notify("osdy-pi enabled", "info");
|
|
97
140
|
}
|
|
@@ -104,15 +147,15 @@ export function disableOsdyPi(ctx: ExtensionContext, state: OsdyState): void {
|
|
|
104
147
|
ctx.ui.setWidget(WORKING_WIDGET_KEY, undefined);
|
|
105
148
|
ctx.ui.setWidget(WORKING_TREE_WIDGET_KEY, undefined);
|
|
106
149
|
ctx.ui.setWorkingVisible(true);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
150
|
+
state.editorEffective = false;
|
|
151
|
+
state.smallMode = false;
|
|
152
|
+
state.tui = undefined;
|
|
110
153
|
ctx.ui.notify("osdy-pi disabled", "info");
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
export function notifyStatus(ctx: ExtensionContext, state: OsdyState): void {
|
|
114
157
|
ctx.ui.notify(
|
|
115
|
-
`osdy-pi ${state.enabled ? "enabled" : "disabled"} · working-tree ${state.workingTreeEnabled ? "on" : "off"} · widget ${state.workingTreePlacement === "aboveEditor" ? "top" : "bottom"} · theme ${ctx.ui.theme.name ?? "unknown"} · style ${state.headerVariant} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
|
|
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) ? "visible" : "hidden"} · widget ${state.workingTreePlacement === "aboveEditor" ? "top" : "bottom"} · theme ${ctx.ui.theme.name ?? "unknown"} · style ${state.headerVariant} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
|
|
116
159
|
"info",
|
|
117
160
|
);
|
|
118
161
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
5
|
+
import { PLUGIN_EVENTS, subscribeQuestionPromptAudioNotification } from "./plugin-events.ts";
|
|
6
|
+
|
|
7
|
+
class TestEventBus {
|
|
8
|
+
event: string | undefined;
|
|
9
|
+
handler: ((data: unknown) => void) | undefined;
|
|
10
|
+
|
|
11
|
+
on(event: string, handler: (data: unknown) => void): () => void {
|
|
12
|
+
this.event = event;
|
|
13
|
+
this.handler = handler;
|
|
14
|
+
return () => {
|
|
15
|
+
this.handler = undefined;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
emit(event: string, data: unknown): void {
|
|
20
|
+
if (event === this.event) this.handler?.(data);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class TestExtensionApi {
|
|
25
|
+
readonly events = new TestEventBus();
|
|
26
|
+
lifecycleOnCallCount = 0;
|
|
27
|
+
|
|
28
|
+
on(): void {
|
|
29
|
+
this.lifecycleOnCallCount += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class TestSessionContextProvider {
|
|
34
|
+
context: ExtensionContext | undefined;
|
|
35
|
+
|
|
36
|
+
getCurrentSessionContext(): ExtensionContext | undefined {
|
|
37
|
+
return this.context;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
void test("question prompts subscribe through the plugin event bus", () => {
|
|
42
|
+
const api = new TestExtensionApi();
|
|
43
|
+
const sessionContextProvider = new TestSessionContextProvider();
|
|
44
|
+
const notifiedContexts: ExtensionContext[] = [];
|
|
45
|
+
|
|
46
|
+
subscribeQuestionPromptAudioNotification(
|
|
47
|
+
api,
|
|
48
|
+
sessionContextProvider,
|
|
49
|
+
{ onQuestionRequested: (ctx) => notifiedContexts.push(ctx) },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
assert.equal(api.events.event, PLUGIN_EVENTS.QUESTION_PROMPT);
|
|
53
|
+
assert.equal(api.lifecycleOnCallCount, 0);
|
|
54
|
+
api.events.emit(PLUGIN_EVENTS.QUESTION_PROMPT, undefined);
|
|
55
|
+
assert.deepEqual(notifiedContexts, []);
|
|
56
|
+
|
|
57
|
+
// SAFETY: The notification contract only passes this context through unchanged.
|
|
58
|
+
const sessionContext = {} as ExtensionContext;
|
|
59
|
+
sessionContextProvider.context = sessionContext;
|
|
60
|
+
api.events.emit(PLUGIN_EVENTS.QUESTION_PROMPT, { question: "Continue?" });
|
|
61
|
+
assert.deepEqual(notifiedContexts, [sessionContext]);
|
|
62
|
+
});
|