pi-ui-extend 0.1.21 → 0.1.24
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 +1 -10
- package/bin/pix.mjs +11 -154
- package/dist/app/app.d.ts +1 -0
- package/dist/app/app.js +34 -9
- package/dist/app/cli/startup-info.d.ts +0 -1
- package/dist/app/cli/startup-info.js +0 -3
- package/dist/app/commands/command-session-actions.js +3 -0
- package/dist/app/popup/popup-menu-controller.js +7 -1
- package/dist/app/rendering/conversation-entry-renderer.js +29 -40
- package/dist/app/rendering/render-text.d.ts +6 -0
- package/dist/app/rendering/render-text.js +9 -0
- package/dist/app/rendering/tab-line-renderer.js +1 -5
- package/dist/app/rendering/tool-block-renderer.js +7 -1
- package/dist/app/screen/mouse-controller.js +14 -6
- package/dist/app/session/session-event-controller.js +5 -4
- package/dist/app/session/session-lifecycle-controller.js +0 -4
- package/dist/app/session/tabs-controller.d.ts +5 -1
- package/dist/app/session/tabs-controller.js +111 -23
- package/dist/app/types.d.ts +5 -0
- package/dist/app/workspace/workspace-actions-controller.d.ts +3 -0
- package/dist/app/workspace/workspace-actions-controller.js +71 -16
- package/dist/app/workspace/workspace-undo.js +41 -6
- package/dist/markdown-format.d.ts +4 -0
- package/dist/markdown-format.js +6 -1
- package/dist/theme.js +18 -18
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
- package/external/pi-tools-suite/src/telegram-mirror/README.md +81 -46
- package/external/pi-tools-suite/src/telegram-mirror/bot.ts +81 -10
- package/external/pi-tools-suite/src/telegram-mirror/events.ts +6 -38
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +246 -40
- package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +20 -0
- package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +247 -17
- package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +75 -78
- package/external/pi-tools-suite/src/todo/index.ts +7 -6
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +1 -1
- package/external/pi-tools-suite/src/web-search/index.ts +139 -2
- package/package.json +7 -7
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
1
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
4
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "@earendil-works/pi-coding-agent";
|
|
3
5
|
import { Type } from "typebox";
|
|
4
6
|
|
|
5
7
|
import { WEB_SEARCH_TOOL_DESCRIPTIONS } from "../tool-descriptions";
|
|
6
8
|
|
|
9
|
+
let spawnImpl: typeof spawn = spawn;
|
|
10
|
+
|
|
7
11
|
interface SearchResult {
|
|
8
12
|
title: string;
|
|
9
13
|
url: string;
|
|
@@ -22,11 +26,21 @@ interface FetchResponse {
|
|
|
22
26
|
|
|
23
27
|
type Operation = "Search" | "Fetch";
|
|
24
28
|
|
|
29
|
+
class OllamaEndpointUnavailableError extends Error {
|
|
30
|
+
constructor(message: string, readonly status: number) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "OllamaEndpointUnavailableError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
const DEFAULT_OLLAMA_HOST = "http://localhost:11434";
|
|
26
37
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
27
38
|
const MAX_REQUEST_TIMEOUT_MS = 120_000;
|
|
28
39
|
const REQUEST_TIMEOUT_ENV = "PI_WEB_SEARCH_TIMEOUT_MS";
|
|
40
|
+
const OLLAMA_STARTUP_TIMEOUT_ENV = "PI_WEB_SEARCH_OLLAMA_STARTUP_TIMEOUT_MS";
|
|
41
|
+
const DEFAULT_OLLAMA_STARTUP_TIMEOUT_MS = 30_000;
|
|
29
42
|
const MAX_ERROR_BODY_CHARS = 1_200;
|
|
43
|
+
const STARTED_OLLAMA_PROCESSES = new Set<string>();
|
|
30
44
|
|
|
31
45
|
function normalizeOllamaHost(host: string | undefined): string {
|
|
32
46
|
const trimmed = host?.trim();
|
|
@@ -55,6 +69,92 @@ function resolveRequestTimeoutMs(timeoutMs: number | undefined): number {
|
|
|
55
69
|
return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
56
70
|
}
|
|
57
71
|
|
|
72
|
+
function resolveOllamaStartupTimeoutMs(timeoutMs: number): number {
|
|
73
|
+
const envTimeout = process.env[OLLAMA_STARTUP_TIMEOUT_ENV]?.trim();
|
|
74
|
+
if (envTimeout) return parseTimeoutMs(envTimeout, OLLAMA_STARTUP_TIMEOUT_ENV);
|
|
75
|
+
|
|
76
|
+
return Math.min(timeoutMs, DEFAULT_OLLAMA_STARTUP_TIMEOUT_MS);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isLoopbackHost(host: string): boolean {
|
|
80
|
+
try {
|
|
81
|
+
const { hostname } = new URL(host);
|
|
82
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
91
|
+
const timeout = setTimeout(() => {
|
|
92
|
+
cleanup();
|
|
93
|
+
resolve();
|
|
94
|
+
}, ms);
|
|
95
|
+
const abort = () => {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
cleanup();
|
|
98
|
+
reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (signal?.aborted) abort();
|
|
102
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function startOllama(host: string): void {
|
|
107
|
+
if (!isLoopbackHost(host) || STARTED_OLLAMA_PROCESSES.has(host)) return;
|
|
108
|
+
|
|
109
|
+
const child = spawnImpl("ollama", ["serve"], {
|
|
110
|
+
detached: true,
|
|
111
|
+
stdio: "ignore",
|
|
112
|
+
env: { ...process.env, OLLAMA_HOST: host },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
STARTED_OLLAMA_PROCESSES.add(host);
|
|
116
|
+
child.on("error", () => STARTED_OLLAMA_PROCESSES.delete(host));
|
|
117
|
+
child.unref();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function __setSpawnForTests(nextSpawn: typeof spawn | undefined): void {
|
|
121
|
+
spawnImpl = nextSpawn ?? spawn;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function waitForOllama(host: string, timeoutMs: number, signal: AbortSignal | undefined): Promise<void> {
|
|
125
|
+
const deadline = Date.now() + timeoutMs;
|
|
126
|
+
let lastError: unknown;
|
|
127
|
+
|
|
128
|
+
while (Date.now() < deadline) {
|
|
129
|
+
const remainingMs = deadline - Date.now();
|
|
130
|
+
const requestSignal = createRequestSignal(signal, Math.max(1, Math.min(1_000, remainingMs)));
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(`${host}/api/tags`, { signal: requestSignal.signal });
|
|
134
|
+
if (response.ok) return;
|
|
135
|
+
lastError = new Error(`HTTP ${response.status}`);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (requestSignal.timedOut()) lastError = error;
|
|
138
|
+
else if (isAbortError(error) && signal?.aborted) throw error;
|
|
139
|
+
else lastError = error;
|
|
140
|
+
} finally {
|
|
141
|
+
requestSignal.cleanup();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await sleep(Math.min(250, Math.max(1, deadline - Date.now())), signal);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const details = collectErrorText(lastError);
|
|
148
|
+
throw new Error(`Started Ollama for ${host}, but it did not become ready within ${timeoutMs}ms.${details ? ` ${details}` : ""}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function ensureOllamaRunning(host: string, timeoutMs: number, signal: AbortSignal | undefined): Promise<void> {
|
|
152
|
+
if (!isLoopbackHost(host)) return;
|
|
153
|
+
|
|
154
|
+
startOllama(host);
|
|
155
|
+
await waitForOllama(host, resolveOllamaStartupTimeoutMs(timeoutMs), signal);
|
|
156
|
+
}
|
|
157
|
+
|
|
58
158
|
function createRequestSignal(parentSignal: AbortSignal | undefined, timeoutMs: number) {
|
|
59
159
|
const controller = new AbortController();
|
|
60
160
|
let timedOut = false;
|
|
@@ -145,9 +245,10 @@ function createHttpError(response: Response, operation: Operation, host: string,
|
|
|
145
245
|
}
|
|
146
246
|
|
|
147
247
|
if (response.status === 404 || response.status === 405) {
|
|
148
|
-
return new
|
|
248
|
+
return new OllamaEndpointUnavailableError(
|
|
149
249
|
`Ollama ${apiName} endpoint is not available at ${host} (HTTP ${response.status}). ` +
|
|
150
250
|
`Update Ollama and make sure experimental web ${operationNoun(operation)} is enabled.${withBody}`,
|
|
251
|
+
response.status,
|
|
151
252
|
);
|
|
152
253
|
}
|
|
153
254
|
|
|
@@ -158,6 +259,31 @@ function createHttpError(response: Response, operation: Operation, host: string,
|
|
|
158
259
|
return new Error(`Ollama ${apiName} API at ${host} returned HTTP ${response.status}.${withBody || ` ${response.statusText}`}`);
|
|
159
260
|
}
|
|
160
261
|
|
|
262
|
+
function isEndpointUnavailable(error: unknown): boolean {
|
|
263
|
+
return error instanceof OllamaEndpointUnavailableError;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function waitForEndpointReady<T>(request: () => Promise<T>, host: string, operation: Operation, timeoutMs: number, signal: AbortSignal | undefined): Promise<T> {
|
|
267
|
+
const startupTimeoutMs = resolveOllamaStartupTimeoutMs(timeoutMs);
|
|
268
|
+
const deadline = Date.now() + startupTimeoutMs;
|
|
269
|
+
let lastError: unknown;
|
|
270
|
+
|
|
271
|
+
while (Date.now() < deadline) {
|
|
272
|
+
try {
|
|
273
|
+
return await request();
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (!isEndpointUnavailable(error)) throw error;
|
|
276
|
+
lastError = error;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await sleep(Math.min(250, Math.max(1, deadline - Date.now())), signal);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
throw lastError instanceof Error
|
|
283
|
+
? lastError
|
|
284
|
+
: new Error(`Ollama ${endpointName(operation)} endpoint at ${host} did not become ready within ${startupTimeoutMs}ms.`);
|
|
285
|
+
}
|
|
286
|
+
|
|
161
287
|
async function readJsonResponse<T>(response: Response, operation: Operation, host: string): Promise<T> {
|
|
162
288
|
const body = await response.text().catch(() => "");
|
|
163
289
|
|
|
@@ -207,7 +333,7 @@ function normalizeOllamaError(error: unknown, operation: Operation, host: string
|
|
|
207
333
|
return error instanceof Error ? error : new Error(String(error));
|
|
208
334
|
}
|
|
209
335
|
|
|
210
|
-
async function postOllamaJson<T>(host: string, endpoint: "web_search" | "web_fetch", body: Record<string, unknown>, operation: Operation, signal: AbortSignal | undefined, timeoutMs: number): Promise<T> {
|
|
336
|
+
async function postOllamaJson<T>(host: string, endpoint: "web_search" | "web_fetch", body: Record<string, unknown>, operation: Operation, signal: AbortSignal | undefined, timeoutMs: number, retryEndpointUnavailable = true): Promise<T> {
|
|
211
337
|
const requestSignal = createRequestSignal(signal, timeoutMs);
|
|
212
338
|
|
|
213
339
|
try {
|
|
@@ -220,6 +346,17 @@ async function postOllamaJson<T>(host: string, endpoint: "web_search" | "web_fet
|
|
|
220
346
|
|
|
221
347
|
return await readJsonResponse<T>(response, operation, host);
|
|
222
348
|
} catch (error) {
|
|
349
|
+
if (isConnectionRefused(error) && isLoopbackHost(host)) {
|
|
350
|
+
requestSignal.cleanup();
|
|
351
|
+
await ensureOllamaRunning(host, timeoutMs, signal);
|
|
352
|
+
return waitForEndpointReady(() => postOllamaJson<T>(host, endpoint, body, operation, signal, timeoutMs, false), host, operation, timeoutMs, signal);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (retryEndpointUnavailable && isEndpointUnavailable(error) && isLoopbackHost(host)) {
|
|
356
|
+
requestSignal.cleanup();
|
|
357
|
+
return waitForEndpointReady(() => postOllamaJson<T>(host, endpoint, body, operation, signal, timeoutMs, false), host, operation, timeoutMs, signal);
|
|
358
|
+
}
|
|
359
|
+
|
|
223
360
|
throw normalizeOllamaError(error, operation, host, timeoutMs, requestSignal.timedOut(), signal);
|
|
224
361
|
} finally {
|
|
225
362
|
requestSignal.cleanup();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
"prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@earendil-works/pi-tui": "0.
|
|
65
|
-
"@earendil-works/pi-ai": "0.
|
|
66
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
67
|
-
"@mariozechner/pi-ai": "npm:@earendil-works/pi-ai@0.
|
|
68
|
-
"@mariozechner/pi-coding-agent": "npm:@earendil-works/pi-coding-agent@0.
|
|
69
|
-
"@mariozechner/pi-tui": "npm:@earendil-works/pi-tui@0.
|
|
64
|
+
"@earendil-works/pi-tui": "0.79.1",
|
|
65
|
+
"@earendil-works/pi-ai": "0.79.1",
|
|
66
|
+
"@earendil-works/pi-coding-agent": "0.79.1",
|
|
67
|
+
"@mariozechner/pi-ai": "npm:@earendil-works/pi-ai@0.79.1",
|
|
68
|
+
"@mariozechner/pi-coding-agent": "npm:@earendil-works/pi-coding-agent@0.79.1",
|
|
69
|
+
"@mariozechner/pi-tui": "npm:@earendil-works/pi-tui@0.79.1",
|
|
70
70
|
"@mariozechner/clipboard": "^0.3.9",
|
|
71
71
|
"jsonc-parser": "3.3.1",
|
|
72
72
|
"typebox": "1.1.38",
|