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