@wrongstack/webui 0.1.9 → 0.2.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.
@@ -3,7 +3,6 @@
3
3
  import * as fs from "fs/promises";
4
4
  import * as os from "os";
5
5
  import * as path from "path";
6
- import { WebSocketServer, WebSocket } from "ws";
7
6
  import {
8
7
  Agent,
9
8
  AutoCompactionMiddleware,
@@ -14,6 +13,7 @@ import {
14
13
  DefaultErrorHandler,
15
14
  DefaultLogger,
16
15
  DefaultMemoryStore,
16
+ DefaultModeStore,
17
17
  DefaultModelsRegistry,
18
18
  DefaultPathResolver,
19
19
  DefaultPermissionPolicy,
@@ -22,25 +22,22 @@ import {
22
22
  DefaultSecretVault,
23
23
  DefaultSessionStore,
24
24
  DefaultSkillLoader,
25
- DefaultModeStore,
26
25
  DefaultSystemPromptBuilder,
27
26
  DefaultTokenCounter,
28
27
  EventBus,
29
28
  HybridCompactor,
30
29
  ProviderRegistry,
31
- ToolRegistry,
32
30
  TOKENS,
33
- createDefaultPipelines,
31
+ ToolRegistry,
34
32
  atomicWrite,
33
+ createDefaultPipelines,
35
34
  migratePlaintextSecrets,
36
35
  resolveWstackPaths
37
36
  } from "@wrongstack/core";
38
- import {
39
- buildProviderFactoriesFromRegistry,
40
- makeProviderFromConfig
41
- } from "@wrongstack/providers";
37
+ import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
38
+ import { forgetTool, rememberTool } from "@wrongstack/tools";
42
39
  import { builtinTools } from "@wrongstack/tools/builtin";
43
- import { rememberTool, forgetTool } from "@wrongstack/tools";
40
+ import { WebSocket, WebSocketServer } from "ws";
44
41
  async function bootConfig() {
45
42
  const cwd = process.cwd();
46
43
  const pathResolver = new DefaultPathResolver(cwd);
@@ -99,11 +96,14 @@ async function startWebUI(opts = {}) {
99
96
  container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
100
97
  container.bind(TOKENS.ErrorHandler, () => new DefaultErrorHandler());
101
98
  container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
102
- container.bind(TOKENS.PermissionPolicy, () => new DefaultPermissionPolicy({
103
- trustFile: wpaths.projectTrust,
104
- yolo: false,
105
- promptDelegate: void 0
106
- }));
99
+ container.bind(
100
+ TOKENS.PermissionPolicy,
101
+ () => new DefaultPermissionPolicy({
102
+ trustFile: wpaths.projectTrust,
103
+ yolo: false,
104
+ promptDelegate: void 0
105
+ })
106
+ );
107
107
  const providerRegistry = new ProviderRegistry();
108
108
  try {
109
109
  const factories = await buildProviderFactoriesFromRegistry({
@@ -254,10 +254,24 @@ async function startWebUI(opts = {}) {
254
254
  mode: modeId
255
255
  };
256
256
  }
257
- const wssPrimary = new WebSocketServer({ port: wsPort2, host: wsHost2 });
258
- const wssSecondary = wsHost2 === "127.0.0.1" ? new WebSocketServer({ port: wsPort2, host: "::1" }) : null;
257
+ const verifyClient = (info) => {
258
+ const origin = info.origin;
259
+ if (!origin) return true;
260
+ try {
261
+ const { hostname } = new URL(origin);
262
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
263
+ } catch {
264
+ return false;
265
+ }
266
+ };
267
+ const wssPrimary = new WebSocketServer({
268
+ port: wsPort2,
269
+ host: wsHost2,
270
+ verifyClient
271
+ });
272
+ const wssSecondary = wsHost2 === "127.0.0.1" ? new WebSocketServer({ port: wsPort2, host: "::1", verifyClient }) : null;
259
273
  const clients = /* @__PURE__ */ new Map();
260
- let abortController = null;
274
+ let runLock = null;
261
275
  console.log(
262
276
  `[WebUI] WebSocket server running on ws://${wsHost2}:${wsPort2}` + (wssSecondary ? ` (and ws://[::1]:${wsPort2})` : "")
263
277
  );
@@ -271,6 +285,9 @@ async function startWebUI(opts = {}) {
271
285
  events.on("provider.text_delta", (e) => {
272
286
  broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
273
287
  });
288
+ events.on("provider.thinking_delta", (e) => {
289
+ broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
290
+ });
274
291
  events.on("tool.started", (e) => {
275
292
  broadcast({
276
293
  type: "tool.started",
@@ -392,10 +409,20 @@ async function startWebUI(opts = {}) {
392
409
  switch (msg.type) {
393
410
  case "user_message": {
394
411
  const content = msg.payload.content;
395
- abortController?.abort();
396
- abortController = new AbortController();
412
+ if (runLock) {
413
+ send(ws, {
414
+ type: "error",
415
+ payload: {
416
+ phase: "user_message",
417
+ message: "Agent is already processing a request. Wait for the current run to finish."
418
+ }
419
+ });
420
+ break;
421
+ }
422
+ runLock = new AbortController();
423
+ const thisRun = runLock;
397
424
  try {
398
- const result = await agent.run(content, { signal: abortController.signal });
425
+ const result = await agent.run(content, { signal: thisRun.signal });
399
426
  send(ws, {
400
427
  type: "run.result",
401
428
  payload: {
@@ -418,12 +445,14 @@ async function startWebUI(opts = {}) {
418
445
  }
419
446
  });
420
447
  } finally {
421
- abortController = null;
448
+ if (runLock === thisRun) {
449
+ runLock = null;
450
+ }
422
451
  }
423
452
  break;
424
453
  }
425
454
  case "abort":
426
- abortController?.abort();
455
+ runLock?.abort();
427
456
  broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
428
457
  break;
429
458
  case "ping":
@@ -469,10 +498,7 @@ async function startWebUI(opts = {}) {
469
498
  return String(c);
470
499
  }
471
500
  };
472
- const sysTokens = context.systemPrompt.reduce(
473
- (acc, b) => acc + estimate(b.text ?? ""),
474
- 0
475
- );
501
+ const sysTokens = context.systemPrompt.reduce((acc, b) => acc + estimate(b.text ?? ""), 0);
476
502
  const tools = toolRegistry.list();
477
503
  const toolBreakdown = tools.map((t) => {
478
504
  const schema = t.inputSchema ?? {};
@@ -512,7 +538,11 @@ async function startWebUI(opts = {}) {
512
538
  total,
513
539
  systemPrompt: sysTokens,
514
540
  tools: { total: toolTokens, count: tools.length, breakdown: toolBreakdown },
515
- messages: { total: msgTokens, count: context.messages.length, breakdown: messageBreakdown }
541
+ messages: {
542
+ total: msgTokens,
543
+ count: context.messages.length,
544
+ breakdown: messageBreakdown
545
+ }
516
546
  }
517
547
  });
518
548
  break;
@@ -787,7 +817,11 @@ async function startWebUI(opts = {}) {
787
817
  const { text, scope } = msg.payload;
788
818
  try {
789
819
  const removed = await memoryStore.forget(text, scope ?? "project-memory");
790
- sendResult(ws, removed > 0, removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries");
820
+ sendResult(
821
+ ws,
822
+ removed > 0,
823
+ removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
824
+ );
791
825
  } catch (err) {
792
826
  sendResult(ws, false, err instanceof Error ? err.message : String(err));
793
827
  }
@@ -820,7 +854,11 @@ async function startWebUI(opts = {}) {
820
854
  } catch (err) {
821
855
  send(ws, {
822
856
  type: "skills.list",
823
- payload: { skills: [], enabled: true, error: err instanceof Error ? err.message : String(err) }
857
+ payload: {
858
+ skills: [],
859
+ enabled: true,
860
+ error: err instanceof Error ? err.message : String(err)
861
+ }
824
862
  });
825
863
  }
826
864
  break;
@@ -895,7 +933,8 @@ async function startWebUI(opts = {}) {
895
933
  for (const e of entries) {
896
934
  if (results.length >= 600) return;
897
935
  if (e.name.startsWith(".") && e.name !== ".wrongstack" && e.name !== ".env.example") {
898
- if (e.name !== ".gitignore" && e.name !== ".eslintrc" && e.name !== ".prettierrc") continue;
936
+ if (e.name !== ".gitignore" && e.name !== ".eslintrc" && e.name !== ".prettierrc")
937
+ continue;
899
938
  }
900
939
  const childRel = rel ? `${rel}/${e.name}` : e.name;
901
940
  if (e.isDirectory()) {
@@ -949,7 +988,11 @@ async function startWebUI(opts = {}) {
949
988
  } catch (err) {
950
989
  send(ws, {
951
990
  type: "modes.list",
952
- payload: { modes: [], activeId: "default", error: err instanceof Error ? err.message : String(err) }
991
+ payload: {
992
+ modes: [],
993
+ activeId: "default",
994
+ error: err instanceof Error ? err.message : String(err)
995
+ }
953
996
  });
954
997
  }
955
998
  break;
@@ -965,6 +1008,22 @@ async function startWebUI(opts = {}) {
965
1008
  await modeStore.setActiveMode(id);
966
1009
  }
967
1010
  modeId = id;
1011
+ const modePrompt2 = id === "default" ? "" : (await modeStore.getMode(id))?.prompt ?? "";
1012
+ const freshBuilder = new DefaultSystemPromptBuilder({
1013
+ memoryStore,
1014
+ skillLoader,
1015
+ modeStore,
1016
+ modeId: id,
1017
+ modePrompt: modePrompt2,
1018
+ modelCapabilities
1019
+ });
1020
+ context.systemPrompt = await freshBuilder.build({
1021
+ cwd: projectRoot,
1022
+ projectRoot,
1023
+ tools: toolRegistry.list(),
1024
+ provider: config.provider,
1025
+ model: config.model
1026
+ });
968
1027
  sendResult(ws, true, `Switched to mode "${id}"`);
969
1028
  broadcast({
970
1029
  type: "session.start",
@@ -1126,7 +1185,9 @@ async function startWebUI(opts = {}) {
1126
1185
  baseUrl: payload.baseUrl
1127
1186
  };
1128
1187
  if (payload.apiKey) {
1129
- newProv.apiKeys = [{ label: "default", apiKey: payload.apiKey, createdAt: (/* @__PURE__ */ new Date()).toISOString() }];
1188
+ newProv.apiKeys = [
1189
+ { label: "default", apiKey: payload.apiKey, createdAt: (/* @__PURE__ */ new Date()).toISOString() }
1190
+ ];
1130
1191
  newProv.activeKey = "default";
1131
1192
  }
1132
1193
  providers[payload.id] = newProv;
@@ -1153,7 +1214,11 @@ async function startWebUI(opts = {}) {
1153
1214
  const shutdown = async () => {
1154
1215
  console.log("[WebUI] Shutting down...");
1155
1216
  try {
1156
- await session.append({ type: "session_end", ts: (/* @__PURE__ */ new Date()).toISOString(), usage: tokenCounter.total() });
1217
+ await session.append({
1218
+ type: "session_end",
1219
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1220
+ usage: tokenCounter.total()
1221
+ });
1157
1222
  await session.close();
1158
1223
  } catch (e) {
1159
1224
  console.warn("[WebUI] Error closing session:", e);