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.
Files changed (37) hide show
  1. package/README.md +1 -10
  2. package/bin/pix.mjs +11 -154
  3. package/dist/app/app.d.ts +1 -0
  4. package/dist/app/app.js +34 -9
  5. package/dist/app/cli/startup-info.d.ts +0 -1
  6. package/dist/app/cli/startup-info.js +0 -3
  7. package/dist/app/commands/command-session-actions.js +3 -0
  8. package/dist/app/popup/popup-menu-controller.js +7 -1
  9. package/dist/app/rendering/conversation-entry-renderer.js +29 -40
  10. package/dist/app/rendering/render-text.d.ts +6 -0
  11. package/dist/app/rendering/render-text.js +9 -0
  12. package/dist/app/rendering/tab-line-renderer.js +1 -5
  13. package/dist/app/rendering/tool-block-renderer.js +7 -1
  14. package/dist/app/screen/mouse-controller.js +14 -6
  15. package/dist/app/session/session-event-controller.js +5 -4
  16. package/dist/app/session/session-lifecycle-controller.js +0 -4
  17. package/dist/app/session/tabs-controller.d.ts +5 -1
  18. package/dist/app/session/tabs-controller.js +111 -23
  19. package/dist/app/types.d.ts +5 -0
  20. package/dist/app/workspace/workspace-actions-controller.d.ts +3 -0
  21. package/dist/app/workspace/workspace-actions-controller.js +71 -16
  22. package/dist/app/workspace/workspace-undo.js +41 -6
  23. package/dist/markdown-format.d.ts +4 -0
  24. package/dist/markdown-format.js +6 -1
  25. package/dist/theme.js +18 -18
  26. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  27. package/external/pi-tools-suite/src/telegram-mirror/README.md +81 -46
  28. package/external/pi-tools-suite/src/telegram-mirror/bot.ts +81 -10
  29. package/external/pi-tools-suite/src/telegram-mirror/events.ts +6 -38
  30. package/external/pi-tools-suite/src/telegram-mirror/index.ts +246 -40
  31. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +20 -0
  32. package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +247 -17
  33. package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +75 -78
  34. package/external/pi-tools-suite/src/todo/index.ts +7 -6
  35. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +1 -1
  36. package/external/pi-tools-suite/src/web-search/index.ts +139 -2
  37. 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 Error(
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.21",
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.77.0",
65
- "@earendil-works/pi-ai": "0.77.0",
66
- "@earendil-works/pi-coding-agent": "0.77.0",
67
- "@mariozechner/pi-ai": "npm:@earendil-works/pi-ai@0.77.0",
68
- "@mariozechner/pi-coding-agent": "npm:@earendil-works/pi-coding-agent@0.77.0",
69
- "@mariozechner/pi-tui": "npm:@earendil-works/pi-tui@0.77.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",