@wrongstack/webui 0.5.3 → 0.5.6

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.
@@ -303,6 +303,20 @@ async function startWebUI(opts = {}) {
303
303
  });
304
304
  const wssSecondary = wsHost2 === "127.0.0.1" ? new WebSocketServer({ port: wsPort2, host: "::1", verifyClient }) : null;
305
305
  const clients = /* @__PURE__ */ new Map();
306
+ const RATE_LIMIT_MESSAGES = 60;
307
+ const RATE_LIMIT_WINDOW_MS = 6e4;
308
+ const rateLimits = /* @__PURE__ */ new Map();
309
+ function checkRateLimit(ws) {
310
+ const now = Date.now();
311
+ const limit = rateLimits.get(ws);
312
+ if (!limit || now > limit.resetAt) {
313
+ rateLimits.set(ws, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
314
+ return true;
315
+ }
316
+ if (limit.count >= RATE_LIMIT_MESSAGES) return false;
317
+ limit.count++;
318
+ return true;
319
+ }
306
320
  let runLock = null;
307
321
  console.log(
308
322
  `[WebUI] WebSocket server running on ws://${wsHost2}:${wsPort2}` + (wssSecondary ? ` (and ws://[::1]:${wsPort2})` : "")
@@ -416,13 +430,23 @@ async function startWebUI(opts = {}) {
416
430
  }
417
431
  }
418
432
  const handleConnection = (ws) => {
419
- const client = { ws, sessionId: session.id };
433
+ const client = { ws, sessionId: session.id, connectedAt: Date.now() };
420
434
  clients.set(ws, client);
421
435
  console.log("[WebUI] Client connected, total:", clients.size);
422
436
  void sessionStartPayload().then((payload) => {
423
437
  send(ws, { type: "session.start", payload });
424
438
  });
425
439
  ws.on("message", async (data) => {
440
+ if (!checkRateLimit(ws)) {
441
+ send(ws, {
442
+ type: "error",
443
+ payload: {
444
+ phase: "rate_limit",
445
+ message: "Too many messages. Please wait before sending more."
446
+ }
447
+ });
448
+ return;
449
+ }
426
450
  try {
427
451
  const msg = JSON.parse(data.toString());
428
452
  await handleMessage(ws, client, msg);
@@ -432,6 +456,7 @@ async function startWebUI(opts = {}) {
432
456
  });
433
457
  ws.on("close", () => {
434
458
  clients.delete(ws);
459
+ rateLimits.delete(ws);
435
460
  console.log("[WebUI] Client disconnected, total:", clients.size);
436
461
  if (pendingConfirms.size > 0) {
437
462
  for (const [id, resolve] of pendingConfirms) {
@@ -1008,11 +1033,64 @@ async function startWebUI(opts = {}) {
1008
1033
  break;
1009
1034
  }
1010
1035
  case "todos.clear": {
1011
- context.todos.length = 0;
1036
+ context.state.replaceTodos([]);
1012
1037
  sendResult(ws, true, "Todos cleared");
1013
1038
  broadcast({ type: "todos.updated", payload: { todos: [] } });
1014
1039
  break;
1015
1040
  }
1041
+ case "plan.get": {
1042
+ const planPath = context.meta["plan.path"];
1043
+ if (typeof planPath === "string" && planPath) {
1044
+ try {
1045
+ const { loadPlan } = await import("@wrongstack/core");
1046
+ const plan = await loadPlan(planPath);
1047
+ send(ws, {
1048
+ type: "plan.updated",
1049
+ payload: { plan: plan ?? { version: 1, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), items: [] } }
1050
+ });
1051
+ } catch {
1052
+ send(ws, {
1053
+ type: "plan.updated",
1054
+ payload: { plan: { version: 1, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), items: [] } }
1055
+ });
1056
+ }
1057
+ } else {
1058
+ send(ws, {
1059
+ type: "plan.updated",
1060
+ payload: { plan: null, error: "Plan storage is not configured for this session." }
1061
+ });
1062
+ }
1063
+ break;
1064
+ }
1065
+ case "plan.template_use": {
1066
+ const { template } = msg.payload;
1067
+ const planPath = context.meta["plan.path"];
1068
+ if (typeof planPath !== "string" || !planPath) {
1069
+ sendResult(ws, false, "Plan storage is not configured for this session.");
1070
+ break;
1071
+ }
1072
+ try {
1073
+ const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem, formatPlan } = await import("@wrongstack/core");
1074
+ const tpl = getPlanTemplate(template);
1075
+ if (!tpl) {
1076
+ sendResult(ws, false, `Unknown template "${template}".`);
1077
+ break;
1078
+ }
1079
+ let plan = await loadPlan(planPath) ?? emptyPlan(session.id);
1080
+ for (const item of tpl.items) {
1081
+ ({ plan } = addPlanItem(plan, item.title, item.details));
1082
+ }
1083
+ await savePlan(planPath, plan);
1084
+ sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
1085
+ broadcast({
1086
+ type: "plan.updated",
1087
+ payload: { plan }
1088
+ });
1089
+ } catch (err) {
1090
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
1091
+ }
1092
+ break;
1093
+ }
1016
1094
  case "files.list": {
1017
1095
  const payload = msg.payload ?? {};
1018
1096
  const query = (payload.query ?? "").toLowerCase();