osdy-pi 0.1.9 → 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 +6 -14
- 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,163 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
createEditorSettingsStore,
|
|
5
|
+
normalizeEditorSettings,
|
|
6
|
+
type EditorSettingsFileSystem,
|
|
7
|
+
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
8
|
+
} from "./editor-settings.ts";
|
|
9
|
+
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
10
|
+
import { EDITOR_MODES } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
class MemoryEditorSettingsFileSystem implements EditorSettingsFileSystem {
|
|
13
|
+
content: string | undefined;
|
|
14
|
+
mkdirPaths: string[] = [];
|
|
15
|
+
writePaths: string[] = [];
|
|
16
|
+
renameCalls: Array<[string, string]> = [];
|
|
17
|
+
|
|
18
|
+
mkdir(directory: string): Promise<void> {
|
|
19
|
+
this.mkdirPaths.push(directory);
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
readFile(): Promise<string> {
|
|
24
|
+
return this.content === undefined
|
|
25
|
+
? Promise.reject(new Error("missing settings"))
|
|
26
|
+
: Promise.resolve(this.content);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
rename(source: string, destination: string): Promise<void> {
|
|
30
|
+
this.renameCalls.push([source, destination]);
|
|
31
|
+
this.content = this.pendingContent;
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
writeFile(filePath: string, content: string): Promise<void> {
|
|
36
|
+
this.writePaths.push(filePath);
|
|
37
|
+
this.pendingContent = content;
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private pendingContent = "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void test("editor settings normalize only supported versioned modes", () => {
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
normalizeEditorSettings({ version: 1, editorMode: "simple" }),
|
|
47
|
+
{
|
|
48
|
+
version: 1,
|
|
49
|
+
editorMode: EDITOR_MODES.SIMPLE,
|
|
50
|
+
workingTreeEnabled: true,
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
assert.deepEqual(
|
|
54
|
+
normalizeEditorSettings({ version: 1, editorMode: "invalid" }),
|
|
55
|
+
{
|
|
56
|
+
version: 1,
|
|
57
|
+
editorMode: EDITOR_MODES.AUTO,
|
|
58
|
+
workingTreeEnabled: true,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
assert.deepEqual(
|
|
62
|
+
normalizeEditorSettings({ version: 2, editorMode: "extended" }),
|
|
63
|
+
{
|
|
64
|
+
version: 1,
|
|
65
|
+
editorMode: EDITOR_MODES.AUTO,
|
|
66
|
+
workingTreeEnabled: true,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
assert.deepEqual(normalizeEditorSettings(null), {
|
|
70
|
+
version: 1,
|
|
71
|
+
editorMode: EDITOR_MODES.AUTO,
|
|
72
|
+
workingTreeEnabled: true,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
void test("working-tree visibility defaults safely and persists globally", async () => {
|
|
77
|
+
assert.equal(
|
|
78
|
+
normalizeEditorSettings({
|
|
79
|
+
version: 1,
|
|
80
|
+
editorMode: "auto",
|
|
81
|
+
workingTreeEnabled: false,
|
|
82
|
+
}).workingTreeEnabled,
|
|
83
|
+
false,
|
|
84
|
+
);
|
|
85
|
+
assert.equal(
|
|
86
|
+
normalizeEditorSettings({
|
|
87
|
+
version: 1,
|
|
88
|
+
editorMode: "auto",
|
|
89
|
+
workingTreeEnabled: "disabled",
|
|
90
|
+
}).workingTreeEnabled,
|
|
91
|
+
true,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const fileSystem = new MemoryEditorSettingsFileSystem();
|
|
95
|
+
const firstStore = createEditorSettingsStore(
|
|
96
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
97
|
+
fileSystem,
|
|
98
|
+
);
|
|
99
|
+
await firstStore.save({
|
|
100
|
+
version: 1,
|
|
101
|
+
editorMode: EDITOR_MODES.AUTO,
|
|
102
|
+
workingTreeEnabled: false,
|
|
103
|
+
});
|
|
104
|
+
const reloadedStore = createEditorSettingsStore(
|
|
105
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
106
|
+
fileSystem,
|
|
107
|
+
);
|
|
108
|
+
assert.equal((await reloadedStore.load()).workingTreeEnabled, false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
void test("editor settings load safely and save atomically", async () => {
|
|
112
|
+
const fileSystem = new MemoryEditorSettingsFileSystem();
|
|
113
|
+
const store = createEditorSettingsStore(
|
|
114
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
115
|
+
fileSystem,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
assert.equal((await store.load()).editorMode, EDITOR_MODES.AUTO);
|
|
119
|
+
assert.equal((await store.load()).workingTreeEnabled, true);
|
|
120
|
+
fileSystem.content = JSON.stringify({ version: 1, editorMode: "extended" });
|
|
121
|
+
assert.equal((await store.load()).editorMode, EDITOR_MODES.EXTENDED);
|
|
122
|
+
assert.equal((await store.load()).workingTreeEnabled, true);
|
|
123
|
+
fileSystem.content = "{ invalid JSON";
|
|
124
|
+
assert.equal((await store.load()).workingTreeEnabled, true);
|
|
125
|
+
|
|
126
|
+
await store.save({
|
|
127
|
+
version: 1,
|
|
128
|
+
editorMode: EDITOR_MODES.SIMPLE,
|
|
129
|
+
workingTreeEnabled: true,
|
|
130
|
+
});
|
|
131
|
+
assert.deepEqual(fileSystem.mkdirPaths, ["/agent/extensions/osdy-pi"]);
|
|
132
|
+
assert.deepEqual(fileSystem.writePaths, [
|
|
133
|
+
"/agent/extensions/osdy-pi/settings.json.tmp",
|
|
134
|
+
]);
|
|
135
|
+
assert.deepEqual(fileSystem.renameCalls, [
|
|
136
|
+
[
|
|
137
|
+
"/agent/extensions/osdy-pi/settings.json.tmp",
|
|
138
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
139
|
+
],
|
|
140
|
+
]);
|
|
141
|
+
assert.equal((await store.load()).editorMode, EDITOR_MODES.SIMPLE);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
void test("new editor settings stores load their own persisted mode", async () => {
|
|
145
|
+
const fileSystem = new MemoryEditorSettingsFileSystem();
|
|
146
|
+
fileSystem.content = JSON.stringify({ version: 1, editorMode: "auto" });
|
|
147
|
+
const firstStore = createEditorSettingsStore(
|
|
148
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
149
|
+
fileSystem,
|
|
150
|
+
);
|
|
151
|
+
assert.equal((await firstStore.load()).editorMode, EDITOR_MODES.AUTO);
|
|
152
|
+
|
|
153
|
+
await firstStore.save({
|
|
154
|
+
version: 1,
|
|
155
|
+
editorMode: EDITOR_MODES.EXTENDED,
|
|
156
|
+
workingTreeEnabled: true,
|
|
157
|
+
});
|
|
158
|
+
const reloadedStore = createEditorSettingsStore(
|
|
159
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
160
|
+
fileSystem,
|
|
161
|
+
);
|
|
162
|
+
assert.equal((await reloadedStore.load()).editorMode, EDITOR_MODES.EXTENDED);
|
|
163
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { EditorMode, GlobalEditorSettings } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_EDITOR_MODES = ["auto", "extended", "simple"] as const satisfies readonly EditorMode[];
|
|
7
|
+
const DEFAULT_EDITOR_MODE: EditorMode = "auto";
|
|
8
|
+
const EDITOR_SETTINGS_VERSION = 1;
|
|
9
|
+
const EDITOR_SETTINGS_FILE_NAME = "settings.json";
|
|
10
|
+
const EDITOR_SETTINGS_RELATIVE_DIR = path.join("extensions", "osdy-pi");
|
|
11
|
+
|
|
12
|
+
export interface EditorSettingsFileSystem {
|
|
13
|
+
mkdir(directory: string): Promise<void>;
|
|
14
|
+
readFile(filePath: string): Promise<string>;
|
|
15
|
+
rename(source: string, destination: string): Promise<void>;
|
|
16
|
+
writeFile(filePath: string, content: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getAgentDir(): string {
|
|
20
|
+
const configuredDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
21
|
+
if (configuredDir) return configuredDir;
|
|
22
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createDefaultEditorSettings(): GlobalEditorSettings {
|
|
26
|
+
return {
|
|
27
|
+
version: EDITOR_SETTINGS_VERSION,
|
|
28
|
+
editorMode: DEFAULT_EDITOR_MODE,
|
|
29
|
+
workingTreeEnabled: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isEditorMode(value: unknown): value is EditorMode {
|
|
34
|
+
return typeof value === "string" && SUPPORTED_EDITOR_MODES.some((mode) => mode === value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeEditorSettings(value: unknown): GlobalEditorSettings {
|
|
38
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
39
|
+
return createDefaultEditorSettings();
|
|
40
|
+
}
|
|
41
|
+
const recordValue = value as Record<string, unknown>;
|
|
42
|
+
if (recordValue.version !== EDITOR_SETTINGS_VERSION) {
|
|
43
|
+
return createDefaultEditorSettings();
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
version: EDITOR_SETTINGS_VERSION,
|
|
47
|
+
editorMode: isEditorMode(recordValue.editorMode)
|
|
48
|
+
? recordValue.editorMode
|
|
49
|
+
: DEFAULT_EDITOR_MODE,
|
|
50
|
+
workingTreeEnabled:
|
|
51
|
+
typeof recordValue.workingTreeEnabled === "boolean"
|
|
52
|
+
? recordValue.workingTreeEnabled
|
|
53
|
+
: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nodeEditorSettingsFileSystem: EditorSettingsFileSystem = {
|
|
58
|
+
async mkdir(directory): Promise<void> {
|
|
59
|
+
await mkdir(directory, { recursive: true });
|
|
60
|
+
},
|
|
61
|
+
readFile: (filePath) => readFile(filePath, "utf8"),
|
|
62
|
+
rename,
|
|
63
|
+
writeFile: (filePath, content) => writeFile(filePath, content, "utf8"),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface EditorSettingsStore {
|
|
67
|
+
readonly path: string;
|
|
68
|
+
load(): Promise<GlobalEditorSettings>;
|
|
69
|
+
save(settings: GlobalEditorSettings): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class FileEditorSettingsStore implements EditorSettingsStore {
|
|
73
|
+
readonly path: string;
|
|
74
|
+
private readonly fileSystem: EditorSettingsFileSystem;
|
|
75
|
+
|
|
76
|
+
constructor(filePath: string, fileSystem: EditorSettingsFileSystem) {
|
|
77
|
+
this.path = filePath;
|
|
78
|
+
this.fileSystem = fileSystem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async load(): Promise<GlobalEditorSettings> {
|
|
82
|
+
try {
|
|
83
|
+
const content = await this.fileSystem.readFile(this.path);
|
|
84
|
+
return normalizeEditorSettings(JSON.parse(content) as unknown);
|
|
85
|
+
} catch {
|
|
86
|
+
return createDefaultEditorSettings();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async save(settings: GlobalEditorSettings): Promise<void> {
|
|
91
|
+
const parentDir = path.dirname(this.path);
|
|
92
|
+
const tempPath = `${this.path}.tmp`;
|
|
93
|
+
const normalizedSettings = normalizeEditorSettings(settings);
|
|
94
|
+
await this.fileSystem.mkdir(parentDir);
|
|
95
|
+
await this.fileSystem.writeFile(
|
|
96
|
+
tempPath,
|
|
97
|
+
`${JSON.stringify(normalizedSettings, null, 2)}\n`,
|
|
98
|
+
);
|
|
99
|
+
await this.fileSystem.rename(tempPath, this.path);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createEditorSettingsStore(
|
|
104
|
+
filePath = path.join(
|
|
105
|
+
getAgentDir(),
|
|
106
|
+
EDITOR_SETTINGS_RELATIVE_DIR,
|
|
107
|
+
EDITOR_SETTINGS_FILE_NAME,
|
|
108
|
+
),
|
|
109
|
+
fileSystem = nodeEditorSettingsFileSystem,
|
|
110
|
+
): EditorSettingsStore {
|
|
111
|
+
return new FileEditorSettingsStore(filePath, fileSystem);
|
|
112
|
+
}
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
VERSION,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
7
|
-
import { formatPath, shortNumber, truncateMiddle } from "./format.js";
|
|
8
|
-
import type {
|
|
9
|
-
AssistantSessionEntry,
|
|
10
|
-
HeaderMetaRow,
|
|
11
|
-
OsdyState,
|
|
12
|
-
SimpleTheme,
|
|
13
|
-
} from "./types.js";
|
|
14
|
-
import { centerVisible, fitCenterVisible } from "./utils.js";
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { shortNumber } from "./format.js";
|
|
3
|
+
import type { AssistantSessionEntry } from "./types.js";
|
|
15
4
|
|
|
16
5
|
function isAssistantSessionEntry(
|
|
17
6
|
entry: unknown,
|
|
@@ -86,125 +75,9 @@ function mergeUsageTotals(
|
|
|
86
75
|
}
|
|
87
76
|
|
|
88
77
|
function collectUsageTotals(ctx: ExtensionContext): UsageTotals {
|
|
89
|
-
return ctx.sessionManager
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
: totals;
|
|
95
|
-
}, createUsageTotals());
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function getToolSources(pi: ExtensionAPI) {
|
|
99
|
-
return pi.getAllTools().map((tool) => ({
|
|
100
|
-
name: tool.name.toLowerCase(),
|
|
101
|
-
origin: tool.sourceInfo?.origin?.toLowerCase() ?? "",
|
|
102
|
-
source: tool.sourceInfo?.source?.toLowerCase() ?? "",
|
|
103
|
-
path: tool.sourceInfo?.path?.toLowerCase() ?? "",
|
|
104
|
-
}));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function isMcpTool(tool: ReturnType<typeof getToolSources>[number]): boolean {
|
|
108
|
-
return tool.name.includes("mcp") || tool.source.includes("mcp");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function getToolServerKey(
|
|
112
|
-
tool: ReturnType<typeof getToolSources>[number],
|
|
113
|
-
): string {
|
|
114
|
-
return tool.source || tool.path || tool.name;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function getMcpServerCount(pi: ExtensionAPI): number {
|
|
118
|
-
const serverKeys = new Set(
|
|
119
|
-
getToolSources(pi).filter(isMcpTool).map(getToolServerKey),
|
|
120
|
-
);
|
|
121
|
-
return serverKeys.size;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function countToolsByOrigin(pi: ExtensionAPI, origin: string): number {
|
|
125
|
-
return getToolSources(pi).filter((tool) => tool.origin === origin).length;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function countToolsBySource(pi: ExtensionAPI, source: string): number {
|
|
129
|
-
return getToolSources(pi).filter((tool) => tool.source.includes(source))
|
|
130
|
-
.length;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function getPathWidth(width: number): number {
|
|
134
|
-
return width < 110 ? 34 : 52;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function renderHeaderMetadata(
|
|
138
|
-
pi: ExtensionAPI,
|
|
139
|
-
ctx: ExtensionContext,
|
|
140
|
-
state: OsdyState,
|
|
141
|
-
width: number,
|
|
142
|
-
): HeaderMetaRow[] {
|
|
143
|
-
const allTools = pi.getAllTools().length;
|
|
144
|
-
const git = state.gitLabel === "no-git" ? "Not a git repo" : state.gitLabel;
|
|
145
|
-
const path = truncateMiddle(formatPath(ctx.cwd), getPathWidth(width));
|
|
146
|
-
const mcpServerCount = getMcpServerCount(pi);
|
|
147
|
-
const pluginCount = countToolsByOrigin(pi, "package");
|
|
148
|
-
const extensionCount = countToolsBySource(pi, "extension");
|
|
149
|
-
return [
|
|
150
|
-
{
|
|
151
|
-
leftLabel: "GIT:",
|
|
152
|
-
leftValue: git,
|
|
153
|
-
rightLabel: "PATH:",
|
|
154
|
-
rightValue: path,
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
leftLabel: "MCP:",
|
|
158
|
-
leftValue: `${mcpServerCount} servers`,
|
|
159
|
-
rightLabel: "PLUGINS:",
|
|
160
|
-
rightValue: `${pluginCount} package`,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
leftLabel: "AGENTS:",
|
|
164
|
-
leftValue: `${state.agentsLabel} loaded`,
|
|
165
|
-
rightLabel: "EXTENSIONS:",
|
|
166
|
-
rightValue: `${extensionCount} active`,
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
leftLabel: "VER:",
|
|
170
|
-
leftValue: VERSION,
|
|
171
|
-
rightLabel: "TOOLS:",
|
|
172
|
-
rightValue: `${allTools} customs`,
|
|
173
|
-
},
|
|
174
|
-
];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function renderMetaRows(
|
|
178
|
-
rows: HeaderMetaRow[],
|
|
179
|
-
width: number,
|
|
180
|
-
theme: SimpleTheme,
|
|
181
|
-
): string[] {
|
|
182
|
-
const labelWidth = 11;
|
|
183
|
-
const leftValueWidth = 32;
|
|
184
|
-
const rightValueWidth = 52;
|
|
185
|
-
const gapWidth = 10;
|
|
186
|
-
const blockWidth =
|
|
187
|
-
labelWidth +
|
|
188
|
-
1 +
|
|
189
|
-
leftValueWidth +
|
|
190
|
-
gapWidth +
|
|
191
|
-
labelWidth +
|
|
192
|
-
1 +
|
|
193
|
-
rightValueWidth;
|
|
194
|
-
return rows.map((row) => {
|
|
195
|
-
const leftLabel = theme.fg("mdLink", row.leftLabel.padEnd(labelWidth));
|
|
196
|
-
const leftValue = theme.fg(
|
|
197
|
-
"accent",
|
|
198
|
-
truncateToWidth(row.leftValue, leftValueWidth).padEnd(leftValueWidth),
|
|
199
|
-
);
|
|
200
|
-
const rightLabel = theme.fg("mdLink", row.rightLabel.padEnd(labelWidth));
|
|
201
|
-
const rightValue = theme.fg(
|
|
202
|
-
"accent",
|
|
203
|
-
truncateToWidth(row.rightValue, rightValueWidth).padEnd(rightValueWidth),
|
|
204
|
-
);
|
|
205
|
-
const rowText = `${leftLabel} ${leftValue}${" ".repeat(gapWidth)}${rightLabel} ${rightValue}`;
|
|
206
|
-
return width >= blockWidth
|
|
207
|
-
? centerVisible(rowText, width)
|
|
208
|
-
: fitCenterVisible(rowText, width);
|
|
209
|
-
});
|
|
78
|
+
return ctx.sessionManager.getEntries().reduce<UsageTotals>((totals, entry) => {
|
|
79
|
+
return isAssistantSessionEntry(entry)
|
|
80
|
+
? mergeUsageTotals(totals, entry)
|
|
81
|
+
: totals;
|
|
82
|
+
}, createUsageTotals());
|
|
210
83
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
export const PLUGIN_EVENTS = {
|
|
6
|
+
QUESTION_PROMPT: "rpiv:ask-user:prompt",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export interface SessionContextProvider {
|
|
10
|
+
getCurrentSessionContext(): ExtensionContext | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QuestionAudioNotificationRouter {
|
|
14
|
+
onQuestionRequested(ctx: ExtensionContext): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function subscribeQuestionPromptAudioNotification(
|
|
18
|
+
api: Pick<ExtensionAPI, "events">,
|
|
19
|
+
sessionContextProvider: SessionContextProvider,
|
|
20
|
+
audioRouter: QuestionAudioNotificationRouter,
|
|
21
|
+
): void {
|
|
22
|
+
api.events.on(PLUGIN_EVENTS.QUESTION_PROMPT, () => {
|
|
23
|
+
const ctx = sessionContextProvider.getCurrentSessionContext();
|
|
24
|
+
if (ctx) audioRouter.onQuestionRequested(ctx);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const PROFILE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62})$/;
|
|
2
|
+
const RESERVED_PROFILE_NAMES = new Set(["default", "profiles", "auth.json"]);
|
|
3
|
+
|
|
4
|
+
export function resolveActiveProfileLabel(
|
|
5
|
+
env: { OSDY_PI_PROFILE_NAME?: string | undefined } = process.env,
|
|
6
|
+
): string | undefined {
|
|
7
|
+
const profileName = env.OSDY_PI_PROFILE_NAME;
|
|
8
|
+
return typeof profileName === "string" &&
|
|
9
|
+
PROFILE_NAME.test(profileName) &&
|
|
10
|
+
!RESERVED_PROFILE_NAMES.has(profileName)
|
|
11
|
+
? profileName
|
|
12
|
+
: undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatModelMetadata(
|
|
16
|
+
model: string,
|
|
17
|
+
thinkingLevel: string,
|
|
18
|
+
profileName: string | undefined,
|
|
19
|
+
): string {
|
|
20
|
+
return profileName === undefined
|
|
21
|
+
? `${model} · think ${thinkingLevel}`
|
|
22
|
+
: `${model} · ${profileName} · think ${thinkingLevel}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveEditorTitleLabel(
|
|
26
|
+
profileName: string | undefined,
|
|
27
|
+
): string {
|
|
28
|
+
return profileName ?? "Osdy-Pi";
|
|
29
|
+
}
|
|
@@ -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
|
+
});
|