@wrongstack/webui 0.3.3 → 0.3.7

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.
@@ -25,8 +25,7 @@ import {
25
25
  resolveContextWindowPolicy
26
26
  } from "@wrongstack/core";
27
27
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
28
- import { forgetTool, rememberTool } from "@wrongstack/tools";
29
- import { builtinToolsPack } from "@wrongstack/tools/pack";
28
+ import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
30
29
  import { WebSocket, WebSocketServer } from "ws";
31
30
  import { randomBytes } from "crypto";
32
31
  import { createDefaultContainer } from "@wrongstack/runtime";
@@ -82,6 +81,7 @@ async function startWebUI(opts = {}) {
82
81
  const boot = await bootConfig();
83
82
  const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
84
83
  let config = baseConfig;
84
+ let configWriteLock = Promise.resolve();
85
85
  console.log("[WebUI] Config loaded:", config.provider, "/", config.model);
86
86
  const modelsRegistry = new DefaultModelsRegistry({
87
87
  cacheFile: wpaths.modelsCache,
@@ -290,6 +290,7 @@ async function startWebUI(opts = {}) {
290
290
  console.log(
291
291
  `[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
292
292
  );
293
+ const pendingConfirms = /* @__PURE__ */ new Map();
293
294
  function setupEvents() {
294
295
  events.on("iteration.started", (e) => {
295
296
  broadcast({
@@ -361,6 +362,19 @@ async function startWebUI(opts = {}) {
361
362
  }
362
363
  });
363
364
  });
365
+ events.on("tool.confirm_needed", (e) => {
366
+ const id = e.toolUseId ?? `confirm_${Date.now()}`;
367
+ pendingConfirms.set(id, e.resolve);
368
+ broadcast({
369
+ type: "tool.confirm_needed",
370
+ payload: {
371
+ id,
372
+ toolName: e.tool?.name ?? "unknown",
373
+ input: e.input,
374
+ suggestedPattern: e.suggestedPattern
375
+ }
376
+ });
377
+ });
364
378
  events.on("error", (e) => {
365
379
  broadcast({
366
380
  type: "error",
@@ -402,6 +416,12 @@ async function startWebUI(opts = {}) {
402
416
  ws.on("close", () => {
403
417
  clients.delete(ws);
404
418
  console.log("[WebUI] Client disconnected, total:", clients.size);
419
+ if (pendingConfirms.size > 0) {
420
+ for (const [id, resolve] of pendingConfirms) {
421
+ resolve("no");
422
+ pendingConfirms.delete(id);
423
+ }
424
+ }
405
425
  });
406
426
  ws.on("error", (err) => {
407
427
  console.warn("[WebUI] Client socket error:", err.message);
@@ -476,6 +496,15 @@ async function startWebUI(opts = {}) {
476
496
  }
477
497
  break;
478
498
  }
499
+ case "tool.confirm_result": {
500
+ const { id, decision } = msg.payload;
501
+ const resolve = pendingConfirms.get(id);
502
+ if (resolve) {
503
+ pendingConfirms.delete(id);
504
+ resolve(decision);
505
+ }
506
+ break;
507
+ }
479
508
  case "abort":
480
509
  runLock?.abort();
481
510
  broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
@@ -709,11 +738,14 @@ async function startWebUI(opts = {}) {
709
738
  const newProv = providerRegistry.has(newProvider) ? providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig(newProvider, providerCfg);
710
739
  context.provider = newProv;
711
740
  try {
712
- const raw = await fs2.readFile(globalConfigPath, "utf8");
713
- const parsed = JSON.parse(raw);
714
- parsed.provider = newProvider;
715
- parsed.model = newModel;
716
- await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2));
741
+ configWriteLock = configWriteLock.then(async () => {
742
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
743
+ const parsed = JSON.parse(raw);
744
+ parsed.provider = newProvider;
745
+ parsed.model = newModel;
746
+ await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2));
747
+ });
748
+ await configWriteLock;
717
749
  } catch (err) {
718
750
  console.warn("[WebUI] Failed to save config:", err);
719
751
  }
@@ -760,11 +792,11 @@ async function startWebUI(opts = {}) {
760
792
  await handleProviderRemove(ws, providerId);
761
793
  break;
762
794
  }
763
- case "providers.saved": {
795
+ case "providers.list": {
764
796
  try {
765
797
  const providers = await loadSavedProviders();
766
798
  send(ws, {
767
- type: "providers.saved",
799
+ type: "providers.list",
768
800
  payload: {
769
801
  providers: Object.entries(providers).map(([id, cfg]) => ({
770
802
  id,
@@ -780,7 +812,7 @@ async function startWebUI(opts = {}) {
780
812
  }
781
813
  });
782
814
  } catch {
783
- send(ws, { type: "providers.saved", payload: { providers: [] } });
815
+ send(ws, { type: "providers.list", payload: { providers: [] } });
784
816
  }
785
817
  break;
786
818
  }
@@ -1156,15 +1188,18 @@ async function startWebUI(opts = {}) {
1156
1188
  }
1157
1189
  }
1158
1190
  async function saveProviders(providers) {
1159
- let parsed;
1160
- try {
1161
- const raw = await fs2.readFile(globalConfigPath, "utf8");
1162
- parsed = JSON.parse(raw);
1163
- } catch {
1164
- parsed = {};
1165
- }
1166
- parsed["providers"] = providers;
1167
- await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2), { mode: 384 });
1191
+ configWriteLock = configWriteLock.then(async () => {
1192
+ let parsed;
1193
+ try {
1194
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
1195
+ parsed = JSON.parse(raw);
1196
+ } catch {
1197
+ parsed = {};
1198
+ }
1199
+ parsed["providers"] = providers;
1200
+ await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2), { mode: 384 });
1201
+ });
1202
+ await configWriteLock;
1168
1203
  }
1169
1204
  function normalizeKeys(cfg) {
1170
1205
  if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {