@pi-unipi/unipi 2.1.2 → 2.1.3
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/package.json +22 -22
- package/packages/ask-user/package.json +3 -3
- package/packages/autocomplete/package.json +1 -1
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/compactor/package.json +4 -4
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +3 -3
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +3 -3
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +2 -1
- package/packages/notify/events.ts +119 -81
- package/packages/notify/package.json +2 -2
- package/packages/notify/settings.ts +1 -0
- package/packages/notify/skills/configure-notify/SKILL.md +3 -1
- package/packages/notify/src/__tests__/event-bus.test.ts +3 -2
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +5 -5
- package/packages/updater/package.json +3 -3
- package/packages/utility/package.json +2 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
- package/packages/info-screen/tui/info-overlay.ts +0 -504
- package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
- package/packages/memory/tui/settings-tui.ts +0 -281
- package/packages/notify/platforms/focus-win.ts +0 -123
- package/packages/notify/platforms/focus.ts +0 -33
- package/packages/notify/platforms/gotify.ts +0 -36
- package/packages/notify/platforms/native.ts +0 -79
- package/packages/notify/platforms/node-notifier.d.ts +0 -20
- package/packages/notify/platforms/ntfy.ts +0 -48
- package/packages/notify/platforms/telegram.ts +0 -77
- package/packages/notify/tui/gotify-setup.ts +0 -528
- package/packages/notify/tui/ntfy-setup.ts +0 -652
- package/packages/notify/tui/recap-model-selector.ts +0 -301
- package/packages/notify/tui/settings-overlay.ts +0 -352
- package/packages/notify/tui/telegram-setup.ts +0 -301
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @pi-unipi/kanboard — Kanboard TUI Overlay
|
|
3
|
-
*
|
|
4
|
-
* Two tabs: Tasks list and Kanban Board.
|
|
5
|
-
* Uses pi-tui overlay API (same pattern as MCP add overlay).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
Key,
|
|
10
|
-
matchesKey,
|
|
11
|
-
truncateToWidth,
|
|
12
|
-
type TUI,
|
|
13
|
-
visibleWidth,
|
|
14
|
-
} from "@earendil-works/pi-tui";
|
|
15
|
-
import type { Theme, KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import type { ParsedDoc, ParsedItem, ItemStatus } from "../types.js";
|
|
17
|
-
import { createDefaultRegistry } from "../parser/index.js";
|
|
18
|
-
|
|
19
|
-
type Tab = "tasks" | "board";
|
|
20
|
-
|
|
21
|
-
interface KanboardState {
|
|
22
|
-
tab: Tab;
|
|
23
|
-
tasks: ParsedItem[];
|
|
24
|
-
taskIndex: number;
|
|
25
|
-
taskScroll: number;
|
|
26
|
-
/** Board columns: index into tasks array grouped by status */
|
|
27
|
-
boardColumns: {
|
|
28
|
-
todo: number[];
|
|
29
|
-
inProgress: number[];
|
|
30
|
-
done: number[];
|
|
31
|
-
};
|
|
32
|
-
boardCol: number; // 0=todo, 1=inProgress, 2=done
|
|
33
|
-
boardIndex: number[];
|
|
34
|
-
pendingG: boolean;
|
|
35
|
-
docsRoot: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Status icon */
|
|
39
|
-
function statusIcon(status: ItemStatus): string {
|
|
40
|
-
switch (status) {
|
|
41
|
-
case "done":
|
|
42
|
-
return "✓";
|
|
43
|
-
case "in-progress":
|
|
44
|
-
return "◐";
|
|
45
|
-
default:
|
|
46
|
-
return "○";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Pad string to visible width */
|
|
51
|
-
function padVisible(content: string, targetWidth: number): string {
|
|
52
|
-
const pad = Math.max(0, targetWidth - visibleWidth(content));
|
|
53
|
-
return content + " ".repeat(pad);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Render the kanboard overlay.
|
|
58
|
-
*/
|
|
59
|
-
export function renderKanboardOverlay(params?: {
|
|
60
|
-
docsRoot?: string;
|
|
61
|
-
onComplete?: () => void;
|
|
62
|
-
}) {
|
|
63
|
-
return (
|
|
64
|
-
tui: TUI,
|
|
65
|
-
theme: Theme,
|
|
66
|
-
_kb: KeybindingsManager,
|
|
67
|
-
done: (result: { viewed: boolean } | null) => void,
|
|
68
|
-
) => {
|
|
69
|
-
const state: KanboardState = {
|
|
70
|
-
tab: "tasks",
|
|
71
|
-
tasks: [],
|
|
72
|
-
taskIndex: 0,
|
|
73
|
-
taskScroll: 0,
|
|
74
|
-
boardColumns: { todo: [], inProgress: [], done: [] },
|
|
75
|
-
boardCol: 0,
|
|
76
|
-
boardIndex: [0, 0, 0],
|
|
77
|
-
pendingG: false,
|
|
78
|
-
docsRoot: params?.docsRoot ?? ".unipi/docs",
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// Load data
|
|
82
|
-
let loaded = false;
|
|
83
|
-
const ensureLoaded = async () => {
|
|
84
|
-
if (loaded) return;
|
|
85
|
-
const registry = await createDefaultRegistry();
|
|
86
|
-
const docs = registry.parseAll(state.docsRoot);
|
|
87
|
-
state.tasks = docs.flatMap((d) => d.items);
|
|
88
|
-
|
|
89
|
-
// Build board columns
|
|
90
|
-
state.boardColumns = { todo: [], inProgress: [], done: [] };
|
|
91
|
-
state.tasks.forEach((item, idx) => {
|
|
92
|
-
if (item.status === "done") state.boardColumns.done.push(idx);
|
|
93
|
-
else if (item.status === "in-progress") state.boardColumns.inProgress.push(idx);
|
|
94
|
-
else state.boardColumns.todo.push(idx);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Init board indices
|
|
98
|
-
state.boardIndex = [
|
|
99
|
-
0,
|
|
100
|
-
0,
|
|
101
|
-
0,
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
loaded = true;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const render = async () => {
|
|
108
|
-
await ensureLoaded();
|
|
109
|
-
|
|
110
|
-
const width = tui.terminal?.columns ?? 80;
|
|
111
|
-
const height = tui.terminal?.rows ?? 24;
|
|
112
|
-
const lines: string[] = [];
|
|
113
|
-
|
|
114
|
-
// Header
|
|
115
|
-
const header = " 📋 Kanboard ";
|
|
116
|
-
const tabLine =
|
|
117
|
-
" [T]asks | [B]oard ".replace(
|
|
118
|
-
state.tab === "tasks" ? "[T]" : "T",
|
|
119
|
-
state.tab === "tasks" ? "▸T" : " T",
|
|
120
|
-
).replace(
|
|
121
|
-
state.tab === "board" ? "[B]" : "B",
|
|
122
|
-
state.tab === "board" ? "▸B" : " B",
|
|
123
|
-
);
|
|
124
|
-
lines.push(truncateToWidth(header, width));
|
|
125
|
-
lines.push(truncateToWidth(tabLine, width));
|
|
126
|
-
lines.push("─".repeat(width));
|
|
127
|
-
|
|
128
|
-
if (state.tab === "tasks") {
|
|
129
|
-
renderTasksTab(lines, width, height - 5);
|
|
130
|
-
} else {
|
|
131
|
-
renderBoardTab(lines, width, height - 5);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Footer
|
|
135
|
-
lines.push("─".repeat(width));
|
|
136
|
-
const footer = " j/k: navigate Tab: switch tab q/Esc: close ";
|
|
137
|
-
lines.push(truncateToWidth(footer, width));
|
|
138
|
-
|
|
139
|
-
(tui as unknown as { setContent(lines: string[]): void }).setContent(lines);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const renderTasksTab = (lines: string[], width: number, maxLines: number) => {
|
|
143
|
-
if (state.tasks.length === 0) {
|
|
144
|
-
lines.push(truncateToWidth(" No tasks found.", width));
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Ensure index in range
|
|
149
|
-
state.taskIndex = Math.min(state.taskIndex, state.tasks.length - 1);
|
|
150
|
-
state.taskIndex = Math.max(0, state.taskIndex);
|
|
151
|
-
|
|
152
|
-
// Scroll to keep selected visible
|
|
153
|
-
if (state.taskIndex < state.taskScroll) state.taskScroll = state.taskIndex;
|
|
154
|
-
if (state.taskIndex >= state.taskScroll + maxLines) {
|
|
155
|
-
state.taskScroll = state.taskIndex - maxLines + 1;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const visible = state.tasks.slice(
|
|
159
|
-
state.taskScroll,
|
|
160
|
-
state.taskScroll + maxLines,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
for (let i = 0; i < visible.length; i++) {
|
|
164
|
-
const item = visible[i];
|
|
165
|
-
const globalIdx = state.taskScroll + i;
|
|
166
|
-
const selected = globalIdx === state.taskIndex;
|
|
167
|
-
const prefix = selected ? " ▸ " : " ";
|
|
168
|
-
const icon = statusIcon(item.status);
|
|
169
|
-
const source = `[${item.sourceFile}]`;
|
|
170
|
-
const line = `${prefix}${icon} ${item.text} ${source}`;
|
|
171
|
-
lines.push(
|
|
172
|
-
truncateToWidth(
|
|
173
|
-
selected ? `\x1b[7m${padVisible(line, width)}\x1b[0m` : line,
|
|
174
|
-
width,
|
|
175
|
-
),
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const renderBoardTab = (lines: string[], width: number, maxLines: number) => {
|
|
181
|
-
const colWidth = Math.floor(width / 3);
|
|
182
|
-
const cols: Array<{ title: string; indices: number[]; colIdx: number }> = [
|
|
183
|
-
{ title: "To Do", indices: state.boardColumns.todo, colIdx: 0 },
|
|
184
|
-
{ title: "In Progress", indices: state.boardColumns.inProgress, colIdx: 1 },
|
|
185
|
-
{ title: "Done", indices: state.boardColumns.done, colIdx: 2 },
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
// Column headers
|
|
189
|
-
let headerLine = "";
|
|
190
|
-
for (const col of cols) {
|
|
191
|
-
const isActive = col.colIdx === state.boardCol;
|
|
192
|
-
const title = isActive ? `▸ ${col.title}` : ` ${col.title}`;
|
|
193
|
-
headerLine += padVisible(` ${title} (${col.indices.length})`, colWidth);
|
|
194
|
-
}
|
|
195
|
-
lines.push(truncateToWidth(headerLine, width));
|
|
196
|
-
lines.push("─".repeat(width));
|
|
197
|
-
|
|
198
|
-
// Column items
|
|
199
|
-
const maxItems = Math.max(
|
|
200
|
-
...cols.map((c) => c.indices.length),
|
|
201
|
-
maxLines - 2,
|
|
202
|
-
);
|
|
203
|
-
for (let row = 0; row < Math.min(maxItems, maxLines - 2); row++) {
|
|
204
|
-
let rowLine = "";
|
|
205
|
-
for (const col of cols) {
|
|
206
|
-
const active = col.colIdx === state.boardCol;
|
|
207
|
-
const boardIdx = state.boardIndex[col.colIdx] ?? 0;
|
|
208
|
-
if (row < col.indices.length) {
|
|
209
|
-
const item = state.tasks[col.indices[row]];
|
|
210
|
-
const selected = active && row === boardIdx;
|
|
211
|
-
const icon = statusIcon(item.status);
|
|
212
|
-
const text = truncateToWidth(` ${icon} ${item.text}`, colWidth - 1);
|
|
213
|
-
if (selected) {
|
|
214
|
-
rowLine += `\x1b[7m${padVisible(text, colWidth)}\x1b[0m`;
|
|
215
|
-
} else {
|
|
216
|
-
rowLine += padVisible(text, colWidth);
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
rowLine += padVisible("", colWidth);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
lines.push(truncateToWidth(rowLine, width));
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const handleKey = async (key: string) => {
|
|
227
|
-
await ensureLoaded();
|
|
228
|
-
|
|
229
|
-
// Close
|
|
230
|
-
if (matchesKey(key, "q") || matchesKey(key, "escape")) {
|
|
231
|
-
done({ viewed: true });
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Tab switching
|
|
236
|
-
if (matchesKey(key, "tab") || matchesKey(key, "b")) {
|
|
237
|
-
state.tab = state.tab === "tasks" ? "board" : "tasks";
|
|
238
|
-
render();
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (matchesKey(key, "t")) {
|
|
243
|
-
state.tab = "tasks";
|
|
244
|
-
render();
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Navigation
|
|
249
|
-
if (state.tab === "tasks") {
|
|
250
|
-
if (matchesKey(key, "j") || matchesKey(key, "down")) {
|
|
251
|
-
state.taskIndex = Math.min(state.taskIndex + 1, state.tasks.length - 1);
|
|
252
|
-
} else if (matchesKey(key, "k") || matchesKey(key, "up")) {
|
|
253
|
-
state.taskIndex = Math.max(state.taskIndex - 1, 0);
|
|
254
|
-
} else if (matchesKey(key, "g")) {
|
|
255
|
-
if (state.pendingG) {
|
|
256
|
-
state.taskIndex = 0;
|
|
257
|
-
state.pendingG = false;
|
|
258
|
-
} else {
|
|
259
|
-
state.pendingG = true;
|
|
260
|
-
setTimeout(() => {
|
|
261
|
-
state.pendingG = false;
|
|
262
|
-
}, 500);
|
|
263
|
-
}
|
|
264
|
-
} else if (matchesKey(key, "shift+g")) {
|
|
265
|
-
state.taskIndex = state.tasks.length - 1;
|
|
266
|
-
}
|
|
267
|
-
} else {
|
|
268
|
-
// Board navigation
|
|
269
|
-
const cols = [
|
|
270
|
-
state.boardColumns.todo,
|
|
271
|
-
state.boardColumns.inProgress,
|
|
272
|
-
state.boardColumns.done,
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
if (matchesKey(key, "h") || matchesKey(key, "left")) {
|
|
276
|
-
state.boardCol = Math.max(0, state.boardCol - 1);
|
|
277
|
-
} else if (matchesKey(key, "l") || matchesKey(key, "right")) {
|
|
278
|
-
state.boardCol = Math.min(2, state.boardCol + 1);
|
|
279
|
-
} else if (matchesKey(key, "j") || matchesKey(key, "down")) {
|
|
280
|
-
const col = cols[state.boardCol];
|
|
281
|
-
state.boardIndex[state.boardCol] = Math.min(
|
|
282
|
-
state.boardIndex[state.boardCol] + 1,
|
|
283
|
-
col.length - 1,
|
|
284
|
-
);
|
|
285
|
-
} else if (matchesKey(key, "k") || matchesKey(key, "up")) {
|
|
286
|
-
state.boardIndex[state.boardCol] = Math.max(
|
|
287
|
-
state.boardIndex[state.boardCol] - 1,
|
|
288
|
-
0,
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
render();
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
// Start
|
|
297
|
-
render();
|
|
298
|
-
(tui as unknown as { on(event: string, handler: (data: string) => void): void }).on("key", handleKey);
|
|
299
|
-
};
|
|
300
|
-
}
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @unipi/memory — Settings TUI
|
|
3
|
-
*
|
|
4
|
-
* Interactive settings dialog for embedding configuration.
|
|
5
|
-
* Uses ctx.ui primitives (select, input, notify).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
loadEmbeddingConfig,
|
|
11
|
-
saveEmbeddingConfig,
|
|
12
|
-
setApiKey,
|
|
13
|
-
clearApiKey,
|
|
14
|
-
getApiKey,
|
|
15
|
-
isEmbeddingReady,
|
|
16
|
-
hasModelChanged,
|
|
17
|
-
markModelUsed,
|
|
18
|
-
OPENROUTER_EMBEDDING_MODELS,
|
|
19
|
-
type EmbeddingConfig,
|
|
20
|
-
} from "../settings.js";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Show memory settings dialog.
|
|
24
|
-
* Main entry point for /unipi:memory-settings command.
|
|
25
|
-
*/
|
|
26
|
-
export async function showMemorySettings(ctx: ExtensionCommandContext): Promise<void> {
|
|
27
|
-
const ui = ctx.ui;
|
|
28
|
-
let running = true;
|
|
29
|
-
|
|
30
|
-
while (running) {
|
|
31
|
-
const config = loadEmbeddingConfig();
|
|
32
|
-
const hasKey = !!getApiKey();
|
|
33
|
-
const ready = isEmbeddingReady();
|
|
34
|
-
|
|
35
|
-
// Build status lines
|
|
36
|
-
const statusLines = [
|
|
37
|
-
`Provider: ${config.provider === "none" ? "None (fuzzy-only)" : "OpenRouter"}`,
|
|
38
|
-
`Model: ${config.model || "N/A"}`,
|
|
39
|
-
`Dimensions: ${config.dimensions}`,
|
|
40
|
-
`API Key: ${hasKey ? "✓ Set" : "✗ Not set"}`,
|
|
41
|
-
`Status: ${ready ? "✓ Ready" : "⚠ Not configured"}`,
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
if (hasModelChanged() && !config.suppressMigrationWarning) {
|
|
45
|
-
statusLines.push("");
|
|
46
|
-
statusLines.push("⚠ Model changed — old embeddings incompatible.");
|
|
47
|
-
statusLines.push(" Re-embed to use vector search with new model.");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const options = [];
|
|
51
|
-
|
|
52
|
-
// API key management
|
|
53
|
-
if (hasKey) {
|
|
54
|
-
options.push({
|
|
55
|
-
label: "🔑 Update API Key",
|
|
56
|
-
value: "__update_key__",
|
|
57
|
-
description: "Update your OpenRouter API key",
|
|
58
|
-
});
|
|
59
|
-
options.push({
|
|
60
|
-
label: "🗑️ Remove API Key",
|
|
61
|
-
value: "__remove_key__",
|
|
62
|
-
description: "Remove API key and disable vector search",
|
|
63
|
-
});
|
|
64
|
-
} else {
|
|
65
|
-
options.push({
|
|
66
|
-
label: "🔑 Add API Key",
|
|
67
|
-
value: "__add_key__",
|
|
68
|
-
description: "Add OpenRouter API key to enable vector search",
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Model selection
|
|
73
|
-
options.push({
|
|
74
|
-
label: `📦 Select Model (current: ${config.model})`,
|
|
75
|
-
value: "__select_model__",
|
|
76
|
-
description: "Choose embedding model from OpenRouter",
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Dimensions
|
|
80
|
-
options.push({
|
|
81
|
-
label: `📐 Dimensions: ${config.dimensions}`,
|
|
82
|
-
value: "__dimensions__",
|
|
83
|
-
description: "Embedding dimensions (lower = faster, less storage)",
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Re-embed
|
|
87
|
-
if (ready && hasModelChanged()) {
|
|
88
|
-
options.push({
|
|
89
|
-
label: "🔄 Re-embed All Memories",
|
|
90
|
-
value: "__reembed__",
|
|
91
|
-
description: "Re-generate all embeddings with current model",
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Suppress warning
|
|
96
|
-
if (hasModelChanged() && !config.suppressMigrationWarning) {
|
|
97
|
-
options.push({
|
|
98
|
-
label: "🔕 Suppress Migration Warning",
|
|
99
|
-
value: "__suppress__",
|
|
100
|
-
description: "Hide the model change warning",
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
options.push({
|
|
105
|
-
label: "← Back",
|
|
106
|
-
value: "__exit__",
|
|
107
|
-
description: "Exit settings",
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const labels = options.map(o => `${o.label} — ${o.description}`);
|
|
111
|
-
const selected = await ui.select(
|
|
112
|
-
"🧠 Memory Settings",
|
|
113
|
-
labels,
|
|
114
|
-
);
|
|
115
|
-
// Map selected label back to value
|
|
116
|
-
const selectedOpt = options.find(o => `${o.label} — ${o.description}` === selected);
|
|
117
|
-
const selectedValue = selectedOpt?.value;
|
|
118
|
-
|
|
119
|
-
if (!selectedValue || selectedValue === "__exit__") {
|
|
120
|
-
running = false;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
switch (selectedValue) {
|
|
125
|
-
case "__add_key__":
|
|
126
|
-
case "__update_key__":
|
|
127
|
-
await handleApiKeyInput(ui);
|
|
128
|
-
break;
|
|
129
|
-
case "__remove_key__":
|
|
130
|
-
clearApiKey();
|
|
131
|
-
ui.notify("API key removed. Vector search disabled.", "info");
|
|
132
|
-
break;
|
|
133
|
-
case "__select_model__":
|
|
134
|
-
await handleModelSelection(ui);
|
|
135
|
-
break;
|
|
136
|
-
case "__dimensions__":
|
|
137
|
-
await handleDimensionsInput(ui);
|
|
138
|
-
break;
|
|
139
|
-
case "__reembed__":
|
|
140
|
-
await handleReembed(ui, ctx);
|
|
141
|
-
break;
|
|
142
|
-
case "__suppress__":
|
|
143
|
-
const cfg = loadEmbeddingConfig();
|
|
144
|
-
cfg.suppressMigrationWarning = true;
|
|
145
|
-
saveEmbeddingConfig(cfg);
|
|
146
|
-
ui.notify("Migration warning suppressed.", "info");
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Handle API key input.
|
|
154
|
-
*/
|
|
155
|
-
async function handleApiKeyInput(ui: ExtensionCommandContext["ui"]): Promise<void> {
|
|
156
|
-
const key = await ui.input(
|
|
157
|
-
"Enter your OpenRouter API key (sk-or-v1-...):",
|
|
158
|
-
"sk-or-v1-...",
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
if (key) {
|
|
162
|
-
const trimmed = key.trim();
|
|
163
|
-
if (trimmed.length === 0) {
|
|
164
|
-
ui.notify("API key cannot be empty.", "warning");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (!trimmed.startsWith("sk-or-") && !trimmed.startsWith("sk-")) {
|
|
168
|
-
ui.notify("Key should start with sk-or- or sk-.", "warning");
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
setApiKey(trimmed);
|
|
172
|
-
ui.notify("API key saved. Vector search enabled.", "info");
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Handle model selection.
|
|
178
|
-
*/
|
|
179
|
-
async function handleModelSelection(ui: ExtensionCommandContext["ui"]): Promise<void> {
|
|
180
|
-
const config = loadEmbeddingConfig();
|
|
181
|
-
|
|
182
|
-
const modelOptions = OPENROUTER_EMBEDDING_MODELS.map((m) => ({
|
|
183
|
-
label: `${m.name}${m.id === config.model ? " ✓" : ""}`,
|
|
184
|
-
value: m.id,
|
|
185
|
-
description: `${m.description} (${m.dimensions}d, ~${m.costPer1k}/1k tokens)`,
|
|
186
|
-
}));
|
|
187
|
-
|
|
188
|
-
// Add custom option
|
|
189
|
-
modelOptions.push({
|
|
190
|
-
label: "✏️ Custom Model ID",
|
|
191
|
-
value: "__custom__",
|
|
192
|
-
description: "Enter a custom OpenRouter model ID",
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const labels = modelOptions.map(o => `${o.label} — ${o.description}`);
|
|
196
|
-
const selected = await ui.select(
|
|
197
|
-
"Select Embedding Model",
|
|
198
|
-
labels,
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
if (!selected) return;
|
|
202
|
-
|
|
203
|
-
// Map label back to value
|
|
204
|
-
const selectedOpt = modelOptions.find(o => `${o.label} — ${o.description}` === selected);
|
|
205
|
-
let modelId = selectedOpt?.value;
|
|
206
|
-
|
|
207
|
-
if (!modelId) return;
|
|
208
|
-
|
|
209
|
-
if (modelId === "__custom__") {
|
|
210
|
-
const custom = await ui.input(
|
|
211
|
-
"Enter the OpenRouter model ID:",
|
|
212
|
-
"openai/text-embedding-3-small",
|
|
213
|
-
);
|
|
214
|
-
if (!custom) return;
|
|
215
|
-
modelId = custom.trim();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Find model info for dimensions
|
|
219
|
-
const modelInfo = OPENROUTER_EMBEDDING_MODELS.find((m) => m.id === modelId);
|
|
220
|
-
const dimensions = modelInfo?.dimensions ?? 384;
|
|
221
|
-
|
|
222
|
-
config.model = modelId;
|
|
223
|
-
config.dimensions = dimensions;
|
|
224
|
-
saveEmbeddingConfig(config);
|
|
225
|
-
|
|
226
|
-
ui.notify(
|
|
227
|
-
`Model set to ${modelId} (${dimensions}d).${hasModelChanged() ? " Re-embed existing memories to use new model." : ""}`,
|
|
228
|
-
"info",
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Handle dimensions input.
|
|
234
|
-
*/
|
|
235
|
-
async function handleDimensionsInput(ui: ExtensionCommandContext["ui"]): Promise<void> {
|
|
236
|
-
const config = loadEmbeddingConfig();
|
|
237
|
-
|
|
238
|
-
const dimStr = await ui.input(
|
|
239
|
-
`Enter dimensions (default: 384). Lower = faster, less storage.\nNote: openai/text-embedding-3 supports 256-3072.\nada-002 only supports 1536.`,
|
|
240
|
-
"384",
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
if (dimStr) {
|
|
244
|
-
const dims = parseInt(dimStr, 10);
|
|
245
|
-
if (isNaN(dims) || dims < 64 || dims > 3072) {
|
|
246
|
-
ui.notify("Must be a number between 64 and 3072.", "warning");
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
config.dimensions = dims;
|
|
250
|
-
saveEmbeddingConfig(config);
|
|
251
|
-
|
|
252
|
-
ui.notify(`Dimensions set to ${dims}. Re-embed existing memories to apply.`, "info");
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Handle re-embedding all memories.
|
|
258
|
-
* This is a destructive operation — warns user first.
|
|
259
|
-
*/
|
|
260
|
-
async function handleReembed(ui: ExtensionCommandContext["ui"], ctx: ExtensionCommandContext): Promise<void> {
|
|
261
|
-
const confirmOptions = [
|
|
262
|
-
{ label: "Yes, re-embed all — Proceed with re-embedding", value: "yes" },
|
|
263
|
-
{ label: "Cancel — Abort", value: "no" },
|
|
264
|
-
];
|
|
265
|
-
const confirmLabels = confirmOptions.map(o => o.label);
|
|
266
|
-
const confirm = await ui.select(
|
|
267
|
-
"Re-embed All Memories",
|
|
268
|
-
confirmLabels,
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
const confirmOpt = confirmOptions.find(o => o.label === confirm);
|
|
272
|
-
if (confirmOpt?.value !== "yes") return;
|
|
273
|
-
|
|
274
|
-
// Import here to avoid circular deps
|
|
275
|
-
const { reembedAllMemories } = await import("../embedding.js");
|
|
276
|
-
const count = await reembedAllMemories(ctx);
|
|
277
|
-
|
|
278
|
-
markModelUsed();
|
|
279
|
-
|
|
280
|
-
ui.notify(`Re-embedded ${count} memories with current model.`, "info");
|
|
281
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @pi-unipi/notify — Windows focus detection
|
|
3
|
-
*
|
|
4
|
-
* Checks whether the terminal window is the foreground (active) window
|
|
5
|
-
* by walking the WMI process tree upward from the current process PID
|
|
6
|
-
* and comparing each ancestor against the foreground window's owner PID.
|
|
7
|
-
*
|
|
8
|
-
* This approach works reliably across cmd, PowerShell, and Windows
|
|
9
|
-
* Terminal, unlike GetConsoleWindow which returns NULL in spawned
|
|
10
|
-
* child processes.
|
|
11
|
-
*
|
|
12
|
-
* Requires PowerShell (built-in on Windows 7+).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { execFile } from "child_process";
|
|
16
|
-
import { writeFileSync, rmSync, mkdtempSync } from "fs";
|
|
17
|
-
import { join } from "path";
|
|
18
|
-
import { tmpdir } from "os";
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// PowerShell script (embedded)
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
const POWERCHECK_SCRIPT = `
|
|
25
|
-
param($targetPid)
|
|
26
|
-
Add-Type @'
|
|
27
|
-
using System;
|
|
28
|
-
using System.Runtime.InteropServices;
|
|
29
|
-
public class WinAPI {
|
|
30
|
-
[DllImport("user32.dll", SetLastError = false)]
|
|
31
|
-
public static extern IntPtr GetForegroundWindow();
|
|
32
|
-
[DllImport("user32.dll", SetLastError = false)]
|
|
33
|
-
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
|
34
|
-
}
|
|
35
|
-
'@ | Out-Null
|
|
36
|
-
$fgHwnd = [WinAPI]::GetForegroundWindow()
|
|
37
|
-
[uint32]$fgPid = 0
|
|
38
|
-
[void][WinAPI]::GetWindowThreadProcessId($fgHwnd, [ref]$fgPid)
|
|
39
|
-
$curPid = $targetPid
|
|
40
|
-
$maxDepth = 20
|
|
41
|
-
while ($curPid -gt 0 -and $maxDepth-- -gt 0) {
|
|
42
|
-
if ($curPid -eq $fgPid) { Write-Host -NoNewline 'True'; exit }
|
|
43
|
-
$proc = Get-CimInstance -Class Win32_Process -Filter "ProcessId = $curPid" -ErrorAction SilentlyContinue | Select-Object -Property ParentProcessId
|
|
44
|
-
if (-not $proc) { break }
|
|
45
|
-
$curPid = $proc.ParentProcessId
|
|
46
|
-
}
|
|
47
|
-
Write-Host -NoNewline 'False'
|
|
48
|
-
`;
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Cache — avoid spawning PowerShell on every check
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
let cached: { result: boolean; time: number } | null = null;
|
|
55
|
-
const CACHE_TTL_MS = 500;
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Public API
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Returns true when the terminal window that owns the current process is
|
|
63
|
-
* the foreground (active) window on Windows.
|
|
64
|
-
*
|
|
65
|
-
* Works by:
|
|
66
|
-
* 1. Calling Win32 GetForegroundWindow + GetWindowThreadProcessId to
|
|
67
|
-
* obtain the foreground window's owning PID.
|
|
68
|
-
* 2. Walking the WMI Win32_Process parent chain upward from
|
|
69
|
-
* process.pid.
|
|
70
|
-
* 3. If any ancestor PID matches the foreground PID the terminal is
|
|
71
|
-
* considered focused.
|
|
72
|
-
*
|
|
73
|
-
* The result is cached for 500 ms to avoid spawning PowerShell on rapid
|
|
74
|
-
* consecutive checks (e.g. batch notifications).
|
|
75
|
-
*/
|
|
76
|
-
export async function isWindowFocusedOnWindows(): Promise<boolean> {
|
|
77
|
-
const now = Date.now();
|
|
78
|
-
if (cached && now - cached.time < CACHE_TTL_MS) {
|
|
79
|
-
return cached.result;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let tmpDir: string | null = null;
|
|
83
|
-
try {
|
|
84
|
-
tmpDir = mkdtempSync(join(tmpdir(), "pi-focus-"));
|
|
85
|
-
const scriptPath = join(tmpDir, "check.ps1");
|
|
86
|
-
writeFileSync(scriptPath, POWERCHECK_SCRIPT, "utf-8");
|
|
87
|
-
|
|
88
|
-
const stdout = await new Promise<string>((resolve, reject) => {
|
|
89
|
-
execFile(
|
|
90
|
-
"powershell.exe",
|
|
91
|
-
[
|
|
92
|
-
"-NoProfile",
|
|
93
|
-
"-NonInteractive",
|
|
94
|
-
"-ExecutionPolicy",
|
|
95
|
-
"Bypass",
|
|
96
|
-
"-File",
|
|
97
|
-
scriptPath,
|
|
98
|
-
String(process.pid),
|
|
99
|
-
],
|
|
100
|
-
{ timeout: 5000, encoding: "utf-8" },
|
|
101
|
-
(err, out) => {
|
|
102
|
-
if (err) reject(err);
|
|
103
|
-
else resolve(out);
|
|
104
|
-
}
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
cached = { result: stdout.trim() === "True", time: Date.now() };
|
|
109
|
-
return cached.result;
|
|
110
|
-
} catch {
|
|
111
|
-
// Detection failure → safe default: assume NOT focused (don't suppress)
|
|
112
|
-
cached = { result: false, time: Date.now() };
|
|
113
|
-
return false;
|
|
114
|
-
} finally {
|
|
115
|
-
if (tmpDir) {
|
|
116
|
-
try {
|
|
117
|
-
rmSync(tmpDir, { recursive: true });
|
|
118
|
-
} catch {
|
|
119
|
-
// Temp file cleanup is non-critical
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|