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