@pasko70/pibo 1.13.0 → 1.14.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.
Files changed (51) hide show
  1. package/dist/apps/chat/static-assets.js +3 -1
  2. package/dist/apps/chat/web-app.js +10 -0
  3. package/dist/apps/chat-ui/assets/{dist-B_8EcoB6.js → dist-1eQpSfYj.js} +1 -1
  4. package/dist/apps/chat-ui/assets/{dist-DVPXynJE.js → dist-BG1J2hGI.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-UYBI2F8E.js → dist-BIwXuAMQ.js} +1 -1
  6. package/dist/apps/{context-files-ui/assets/dist-D6pYe6d_.js → chat-ui/assets/dist-BeCHnL7P.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-CVLKu7NV.js → dist-CtsdsIpj.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-DugRsLaG.js → dist-DfdQQObC.js} +1 -1
  9. package/dist/apps/{context-files-ui/assets/dist-BFRDpzcv.js → chat-ui/assets/dist-DmqwP_tM.js} +1 -1
  10. package/dist/apps/{context-files-ui/assets/dist-B5mpygOZ.js → chat-ui/assets/dist-LWXXqiGT.js} +1 -1
  11. package/dist/apps/{context-files-ui/assets/dist-B_zXPNMb.js → chat-ui/assets/dist-hoDORUlL.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-XmG4qG-A.js → dist-ur4yXrlj.js} +1 -1
  13. package/dist/apps/chat-ui/assets/index-CoCl8FG8.css +1 -0
  14. package/dist/apps/chat-ui/assets/index-DLNTzhdh.js +237 -0
  15. package/dist/apps/chat-ui/index.html +2 -2
  16. package/dist/apps/chat-vscode-web/assets/index-R_7kgvvD.js +41 -0
  17. package/dist/apps/chat-vscode-web/index.html +1 -1
  18. package/dist/apps/{chat-ui/assets/dist-CnX234jg.js → context-files-ui/assets/dist-B7gXtTZC.js} +1 -1
  19. package/dist/apps/{chat-ui/assets/dist-CELRUR8b.js → context-files-ui/assets/dist-BD262Ouw.js} +1 -1
  20. package/dist/apps/context-files-ui/assets/{dist-Em3LWmNi.js → dist-BKuGdcnX.js} +1 -1
  21. package/dist/apps/{chat-ui/assets/dist-CVmpu9FK.js → context-files-ui/assets/dist-C-Jm45P4.js} +1 -1
  22. package/dist/apps/context-files-ui/assets/{dist-DM6yvMZv.js → dist-CHdx1Jri.js} +1 -1
  23. package/dist/apps/context-files-ui/assets/{dist-BLI6sQZG.js → dist-CULMH-fB.js} +1 -1
  24. package/dist/apps/context-files-ui/assets/{dist-CLrlJjPP.js → dist-Cjq6RWEt.js} +1 -1
  25. package/dist/apps/context-files-ui/assets/{dist-D8ppb7gk.js → dist-CntqFySz.js} +1 -1
  26. package/dist/apps/context-files-ui/assets/{dist-SEP88Pg7.js → dist-DxEGebCR.js} +1 -1
  27. package/dist/apps/{chat-ui/assets/dist-KOXty1JB.js → context-files-ui/assets/dist-kPuQud7t.js} +1 -1
  28. package/dist/apps/context-files-ui/assets/index-DZWsbuvg.js +202 -0
  29. package/dist/apps/context-files-ui/index.html +1 -1
  30. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  31. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.14.0.vsix +0 -0
  32. package/dist/cli-session/localSessionSource.js +2 -0
  33. package/dist/core/session-router.js +3 -0
  34. package/dist/gateway/client.js +8 -0
  35. package/dist/gateway/server.js +18 -2
  36. package/dist/gateway/web.js +1 -0
  37. package/dist/local/extension.js +2 -0
  38. package/dist/loops/plugin.js +34 -1
  39. package/dist/loops/service.js +85 -0
  40. package/dist/mcp/config-command.js +5 -3
  41. package/dist/plugins/context-files-store.js +287 -101
  42. package/dist/plugins/context-files.js +102 -154
  43. package/dist/shared/trace-live-reducer.js +6 -1
  44. package/dist/web/channel.js +105 -16
  45. package/dist/web/http.js +14 -2
  46. package/package.json +11 -7
  47. package/dist/apps/chat-ui/assets/index-DhFQ1th-.js +0 -237
  48. package/dist/apps/chat-ui/assets/index-hjygk9Ol.css +0 -1
  49. package/dist/apps/chat-vscode-web/assets/index-1wWonouX.js +0 -41
  50. package/dist/apps/context-files-ui/assets/index-C7X2UvWe.js +0 -202
  51. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.13.0.vsix +0 -0
@@ -6,6 +6,7 @@ import { PiboWebHttpError, nodeRequestToWebRequest, responseHtml, responseJson,
6
6
  export const DEFAULT_WEB_CHANNEL_HOST = "127.0.0.1";
7
7
  export const DEFAULT_WEB_CHANNEL_PORT = 4788;
8
8
  export const WEB_CHANNEL_NAME = "web-host";
9
+ const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5_000;
9
10
  /**
10
11
  * Internal header that carries the TCP socket peer address from the web host
11
12
  * channel to the auth plugin. This is one of three independent safety layers
@@ -168,11 +169,44 @@ function createCanonicalRedirect(request, canonicalBaseURL) {
168
169
  }
169
170
  return redirect(new URL(`${url.pathname}${url.search}`, canonical.origin).toString());
170
171
  }
172
+ function isEventStreamResponse(response) {
173
+ return response.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream") === true;
174
+ }
175
+ async function waitForServerClose(closePromise, timeoutMs) {
176
+ return await new Promise((resolve, reject) => {
177
+ let settled = false;
178
+ const timeout = setTimeout(() => {
179
+ settled = true;
180
+ resolve(false);
181
+ }, timeoutMs);
182
+ closePromise.then(() => {
183
+ if (settled)
184
+ return;
185
+ settled = true;
186
+ clearTimeout(timeout);
187
+ resolve(true);
188
+ }, (error) => {
189
+ if (settled)
190
+ return;
191
+ settled = true;
192
+ clearTimeout(timeout);
193
+ reject(error);
194
+ });
195
+ });
196
+ }
171
197
  export function createWebHostChannel(options = {}) {
172
198
  const host = options.host ?? DEFAULT_WEB_CHANNEL_HOST;
173
199
  const port = options.port ?? DEFAULT_WEB_CHANNEL_PORT;
200
+ const shutdownDrainTimeoutMs = options.shutdownDrainTimeoutMs ?? DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS;
201
+ if (!Number.isFinite(shutdownDrainTimeoutMs) || shutdownDrainTimeoutMs < 0) {
202
+ throw new Error("Web channel shutdown drain timeout must be a non-negative finite number");
203
+ }
174
204
  let server;
175
205
  let context;
206
+ let stopPromise;
207
+ let shuttingDown = false;
208
+ const sockets = new Set();
209
+ const eventStreamControllers = new Map();
176
210
  const requireContext = () => {
177
211
  if (!context)
178
212
  throw new Error("Web channel is not started");
@@ -185,6 +219,20 @@ export function createWebHostChannel(options = {}) {
185
219
  }
186
220
  return auth.handleRequest(request);
187
221
  };
222
+ const sendResponse = async (nodeResponse, webResponse) => {
223
+ const eventStreamController = isEventStreamResponse(webResponse) ? new AbortController() : undefined;
224
+ if (eventStreamController)
225
+ eventStreamControllers.set(nodeResponse, eventStreamController);
226
+ try {
227
+ await sendWebResponse(nodeResponse, webResponse, { signal: eventStreamController?.signal });
228
+ }
229
+ finally {
230
+ if (eventStreamController)
231
+ eventStreamControllers.delete(nodeResponse);
232
+ if (shuttingDown)
233
+ server?.closeIdleConnections();
234
+ }
235
+ };
188
236
  const handleRequest = async (nodeRequest, nodeResponse) => {
189
237
  try {
190
238
  const baseURL = createRequestBaseURL(nodeRequest, host, port);
@@ -198,51 +246,51 @@ export function createWebHostChannel(options = {}) {
198
246
  const url = new URL(request.url);
199
247
  const canonicalRedirect = createCanonicalRedirect(request, options.canonicalBaseURL);
200
248
  if (canonicalRedirect) {
201
- await sendWebResponse(nodeResponse, canonicalRedirect);
249
+ await sendResponse(nodeResponse, canonicalRedirect);
202
250
  return;
203
251
  }
204
252
  if (url.pathname === "/health") {
205
- await sendWebResponse(nodeResponse, responseJson({
253
+ await sendResponse(nodeResponse, responseJson({
206
254
  status: "ok",
207
255
  mode: process.env.PIBO_FALLBACK_MODE === "1" ? "fallback" : "main",
208
256
  }));
209
257
  return;
210
258
  }
211
259
  if (url.pathname === "/gateway/status") {
212
- await sendWebResponse(nodeResponse, createGatewayStatusResponse(requireContext(), options));
260
+ await sendResponse(nodeResponse, createGatewayStatusResponse(requireContext(), options));
213
261
  return;
214
262
  }
215
263
  if (url.pathname.startsWith("/api/auth/")) {
216
264
  const authResponse = stripSocketPeerHeaderFromResponse(await handleAuthRequest(request));
217
- await sendWebResponse(nodeResponse, authResponse);
265
+ await sendResponse(nodeResponse, authResponse);
218
266
  return;
219
267
  }
220
268
  const ctx = requireContext();
221
269
  const simpleApiResponse = await handleSimpleAgentApiRequest(request, ctx);
222
270
  if (simpleApiResponse) {
223
- await sendWebResponse(nodeResponse, simpleApiResponse);
271
+ await sendResponse(nodeResponse, simpleApiResponse);
224
272
  return;
225
273
  }
226
274
  const apps = ctx.getWebApps();
227
275
  const app = apps.find((candidate) => matchPrefix(url.pathname, candidate.mountPath) || matchPrefix(url.pathname, candidate.apiPrefix));
228
276
  if (app) {
229
277
  const response = await app.handleRequest(request, createAppContext(ctx));
230
- await sendWebResponse(nodeResponse, response ?? notFound());
278
+ await sendResponse(nodeResponse, response ?? notFound());
231
279
  return;
232
280
  }
233
281
  if (url.pathname === "/" && apps[0]) {
234
- await sendWebResponse(nodeResponse, redirect(apps[0].mountPath));
282
+ await sendResponse(nodeResponse, redirect(apps[0].mountPath));
235
283
  return;
236
284
  }
237
285
  if (url.pathname === "/") {
238
- await sendWebResponse(nodeResponse, responseHtml("<!doctype html><title>Pibo</title><p>No web apps registered.</p>"));
286
+ await sendResponse(nodeResponse, responseHtml("<!doctype html><title>Pibo</title><p>No web apps registered.</p>"));
239
287
  return;
240
288
  }
241
- await sendWebResponse(nodeResponse, notFound());
289
+ await sendResponse(nodeResponse, notFound());
242
290
  }
243
291
  catch (error) {
244
292
  const status = error instanceof PiboAuthError || error instanceof PiboWebHttpError ? error.statusCode : 500;
245
- await sendWebResponse(nodeResponse, responseJson({ error: error instanceof Error ? error.message : String(error) }, { status }));
293
+ await sendResponse(nodeResponse, responseJson({ error: error instanceof Error ? error.message : String(error) }, { status }));
246
294
  }
247
295
  };
248
296
  return {
@@ -251,12 +299,19 @@ export function createWebHostChannel(options = {}) {
251
299
  description: "Same-origin HTTP host for pibo web apps and auth routes.",
252
300
  auth: { mode: "required" },
253
301
  async start(channelContext) {
302
+ if (stopPromise)
303
+ await stopPromise;
254
304
  if (server)
255
305
  return;
306
+ shuttingDown = false;
256
307
  context = channelContext;
257
308
  server = createServer((request, response) => {
258
309
  void handleRequest(request, response);
259
310
  });
311
+ server.on("connection", (socket) => {
312
+ sockets.add(socket);
313
+ socket.once("close", () => sockets.delete(socket));
314
+ });
260
315
  await new Promise((resolve, reject) => {
261
316
  server.once("error", reject);
262
317
  server.listen(port, host, () => {
@@ -270,12 +325,46 @@ export function createWebHostChannel(options = {}) {
270
325
  }
271
326
  },
272
327
  async stop() {
273
- context = undefined;
274
- if (server) {
275
- await new Promise((resolve, reject) => {
276
- server.close((error) => (error ? reject(error) : resolve()));
277
- });
278
- server = undefined;
328
+ if (stopPromise)
329
+ return await stopPromise;
330
+ if (!server) {
331
+ context = undefined;
332
+ return;
333
+ }
334
+ const closingServer = server;
335
+ shuttingDown = true;
336
+ stopPromise = (async () => {
337
+ try {
338
+ const closePromise = new Promise((resolve, reject) => {
339
+ closingServer.close((error) => (error ? reject(error) : resolve()));
340
+ });
341
+ const eventStreamCount = eventStreamControllers.size;
342
+ for (const controller of eventStreamControllers.values())
343
+ controller.abort();
344
+ closingServer.closeIdleConnections();
345
+ const drained = await waitForServerClose(closePromise, shutdownDrainTimeoutMs);
346
+ if (!drained) {
347
+ const connectionCount = sockets.size;
348
+ console.warn(`[web-host] graceful shutdown timed out after ${shutdownDrainTimeoutMs} ms; force-closing ${connectionCount} active connection(s)`);
349
+ for (const socket of sockets)
350
+ socket.destroy();
351
+ await closePromise;
352
+ }
353
+ else if (eventStreamCount > 0) {
354
+ console.error(`[web-host] graceful shutdown closed ${eventStreamCount} active event stream(s)`);
355
+ }
356
+ }
357
+ finally {
358
+ server = undefined;
359
+ context = undefined;
360
+ shuttingDown = false;
361
+ }
362
+ })();
363
+ try {
364
+ await stopPromise;
365
+ }
366
+ finally {
367
+ stopPromise = undefined;
279
368
  }
280
369
  },
281
370
  getAddress() {
package/dist/web/http.js CHANGED
@@ -76,7 +76,7 @@ export async function nodeRequestToWebRequest(request, baseURL) {
76
76
  body,
77
77
  });
78
78
  }
79
- export async function sendWebResponse(response, webResponse) {
79
+ export async function sendWebResponse(response, webResponse, options = {}) {
80
80
  const headers = responseHeaders(webResponse);
81
81
  const compressEncoding = preferredResponseEncoding(response.req?.headers["accept-encoding"], webResponse);
82
82
  if (compressEncoding && webResponse.body) {
@@ -108,7 +108,17 @@ export async function sendWebResponse(response, webResponse) {
108
108
  const cancel = () => {
109
109
  void reader.cancel();
110
110
  };
111
+ const abort = () => {
112
+ const socket = response.socket;
113
+ void reader.cancel();
114
+ if (!response.writableEnded)
115
+ response.end();
116
+ socket?.end();
117
+ };
111
118
  response.once("close", cancel);
119
+ options.signal?.addEventListener("abort", abort, { once: true });
120
+ if (options.signal?.aborted)
121
+ abort();
112
122
  try {
113
123
  while (true) {
114
124
  const { done, value } = await reader.read();
@@ -116,10 +126,12 @@ export async function sendWebResponse(response, webResponse) {
116
126
  break;
117
127
  response.write(Buffer.from(value));
118
128
  }
119
- response.end();
129
+ if (!response.writableEnded)
130
+ response.end();
120
131
  }
121
132
  finally {
122
133
  response.off("close", cancel);
134
+ options.signal?.removeEventListener("abort", abort);
123
135
  }
124
136
  }
125
137
  function appendServerTimingHeader(headers, value) {
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
- "imports": {
6
- "vscode": "./src/apps/chat-vscode/extension/src/vscode-shim.js"
7
- },
5
+ "workspaces": [
6
+ "packages/workflows",
7
+ "src/apps/chat-vscode/extension/vscode-shim"
8
+ ],
8
9
  "description": "Minimal TypeScript wrapper around Pi Coding Agent.",
9
10
  "files": [
10
11
  "dist",
@@ -39,16 +40,18 @@
39
40
  "vscode:webview:build": "vite build --config src/apps/chat-vscode/extension/webview/vite.config.ts",
40
41
  "vscode:extension:build": "esbuild src/apps/chat-vscode/extension/src/extension.ts --bundle --platform=node --target=node24 --format=cjs --outfile=src/apps/chat-vscode/dist/extension/extension.cjs --external:vscode --sourcemap=inline",
41
42
  "vscode:package": "node scripts/vscode-package.mjs",
43
+ "workflows:build": "npm run build --workspace @pasko70/pibo-workflows",
44
+ "workflows:typecheck": "npm run typecheck --workspace @pasko70/pibo-workflows",
42
45
  "release": "node scripts/release.mjs",
43
46
  "release:github": "node scripts/create-github-release.mjs",
44
- "build": "tsc -p tsconfig.json && npm run web-ui:build && npm run vscode:webview:build && node scripts/ensure-bin-executable.mjs",
47
+ "build": "npm run workflows:build && node --max-old-space-size=1200 node_modules/typescript/bin/tsc -p tsconfig.json && npm run web-ui:build && npm run vscode:webview:build && node scripts/ensure-bin-executable.mjs",
45
48
  "start": "node dist/bin/pibo.js",
46
- "test": "npm run build && node --test test/*.test.mjs test/chat-vscode/*.test.mjs",
49
+ "test": "npm run build && node scripts/run-test-suite.mjs",
47
50
  "check:product-vocab": "node scripts/legacy-product-vocabulary-gate.mjs",
48
51
  "validate:ink-web-derived": "node scripts/ink-cli-web-derived-parity-validate.mjs",
49
52
  "compute:limited-worker-smoke": "node scripts/compute-limited-worker-smoke.mjs",
50
53
  "validate:goal-endurance": "node scripts/goal-endurance-check.mjs",
51
- "typecheck": "tsc -p tsconfig.json --noEmit && npm run chat-ui:typecheck && npm run context-files-ui:typecheck && npm run vscode:typecheck",
54
+ "typecheck": "npm run workflows:typecheck && node --max-old-space-size=1200 node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && npm run chat-ui:typecheck && npm run context-files-ui:typecheck && npm run vscode:typecheck",
52
55
  "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
53
56
  "prepack": "npm run build"
54
57
  },
@@ -92,6 +95,7 @@
92
95
  "tsx": "^4.21.0",
93
96
  "typescript": "^6.0.3",
94
97
  "vite": "^8.0.10",
98
+ "vscode": "0.0.0-pibo-test-shim",
95
99
  "vite-tsconfig-paths": "^5.1.4"
96
100
  }
97
101
  }