@wrongstack/webui 0.8.5 → 0.9.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.
- package/dist/assets/{index-DjDDhHNu.js → index-B5qzSV8A.js} +25 -25
- package/dist/assets/index-BTevO8Vz.css +1 -0
- package/dist/index.css +117 -0
- package/dist/index.css.map +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +206 -20
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.js +206 -20
- package/dist/server/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/assets/index-aTQFIbqW.css +0 -1
package/dist/server/index.js
CHANGED
|
@@ -135,22 +135,36 @@ function patchConfig(config, updates) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// src/server/autophase-ws-handler.ts
|
|
138
|
+
import { spawnSync } from "child_process";
|
|
138
139
|
import {
|
|
139
140
|
AutoPhasePlanner,
|
|
140
141
|
PhaseGraphBuilder,
|
|
141
142
|
PhaseOrchestrator,
|
|
142
|
-
PhaseStore
|
|
143
|
+
PhaseStore,
|
|
144
|
+
WorktreeManager
|
|
143
145
|
} from "@wrongstack/core";
|
|
146
|
+
function isGitRepo(cwd) {
|
|
147
|
+
try {
|
|
148
|
+
const r = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8" });
|
|
149
|
+
return r.status === 0 && r.stdout.trim() === "true";
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
144
154
|
var AutoPhaseWebSocketHandler = class {
|
|
145
|
-
constructor(agent, context, logger, storeDir) {
|
|
155
|
+
constructor(agent, context, logger, storeDir, events, projectRoot) {
|
|
146
156
|
this.agent = agent;
|
|
147
157
|
this.context = context;
|
|
148
158
|
this.logger = logger;
|
|
159
|
+
this.events = events;
|
|
160
|
+
this.projectRoot = projectRoot;
|
|
149
161
|
this.store = new PhaseStore({ baseDir: storeDir });
|
|
150
162
|
}
|
|
151
163
|
agent;
|
|
152
164
|
context;
|
|
153
165
|
logger;
|
|
166
|
+
events;
|
|
167
|
+
projectRoot;
|
|
154
168
|
orchestrator = null;
|
|
155
169
|
graph = null;
|
|
156
170
|
store;
|
|
@@ -158,6 +172,8 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
158
172
|
broadcastInterval = null;
|
|
159
173
|
/** Aborts in-flight task agents when the run is stopped. */
|
|
160
174
|
abort = null;
|
|
175
|
+
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
176
|
+
worktrees = null;
|
|
161
177
|
addClient(ws) {
|
|
162
178
|
const client = { ws, id: crypto.randomUUID() };
|
|
163
179
|
this.clients.add(client);
|
|
@@ -245,12 +261,15 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
245
261
|
this.graph = graph;
|
|
246
262
|
this.abort = new AbortController();
|
|
247
263
|
await this.store.save(graph);
|
|
264
|
+
if (!this.worktrees && this.events && this.projectRoot && process.env["WRONGSTACK_AUTOPHASE_WORKTREES"] !== "0" && isGitRepo(this.projectRoot)) {
|
|
265
|
+
this.worktrees = new WorktreeManager({ projectRoot: this.projectRoot, events: this.events });
|
|
266
|
+
}
|
|
248
267
|
this.orchestrator = new PhaseOrchestrator({
|
|
249
268
|
graph,
|
|
250
269
|
ctx: {
|
|
251
|
-
executeTask: async (task, phaseId) => {
|
|
270
|
+
executeTask: async (task, phaseId, env) => {
|
|
252
271
|
this.logger.info(`[AutoPhase] [${phaseId}] Executing: ${task.title}`);
|
|
253
|
-
const result = await this.executeTaskWithAgent(task, phaseId);
|
|
272
|
+
const result = await this.executeTaskWithAgent(task, phaseId, env);
|
|
254
273
|
this.logger.info(`[AutoPhase] [${phaseId}] Completed: ${task.title}`);
|
|
255
274
|
return result;
|
|
256
275
|
},
|
|
@@ -265,10 +284,13 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
265
284
|
this.broadcastState();
|
|
266
285
|
}
|
|
267
286
|
},
|
|
287
|
+
worktrees: this.worktrees ?? void 0,
|
|
268
288
|
autonomous,
|
|
289
|
+
// Must stay 1: phase tasks run on the single shared context whose cwd we
|
|
290
|
+
// swap per phase, so parallel phases would race on context.cwd.
|
|
269
291
|
maxConcurrentPhases: 1,
|
|
270
292
|
// Sequential within a phase: each todo is a full-tool agent editing the
|
|
271
|
-
//
|
|
293
|
+
// phase worktree, so running two at once risks concurrent writes.
|
|
272
294
|
maxConcurrentTasks: 1
|
|
273
295
|
});
|
|
274
296
|
this.startBroadcast();
|
|
@@ -320,7 +342,7 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
320
342
|
}
|
|
321
343
|
return this.defaultPhases();
|
|
322
344
|
}
|
|
323
|
-
async executeTaskWithAgent(task, phaseId) {
|
|
345
|
+
async executeTaskWithAgent(task, phaseId, env) {
|
|
324
346
|
const prompt = `Execute task: ${task.title}
|
|
325
347
|
|
|
326
348
|
Description: ${task.description}
|
|
@@ -328,8 +350,13 @@ Phase: ${phaseId}
|
|
|
328
350
|
Priority: ${task.priority}
|
|
329
351
|
Type: ${task.type}`;
|
|
330
352
|
const signal = this.abort?.signal ?? new AbortController().signal;
|
|
331
|
-
const
|
|
332
|
-
|
|
353
|
+
const prevCwd = this.context.cwd;
|
|
354
|
+
if (env?.cwd) this.context.cwd = env.cwd;
|
|
355
|
+
try {
|
|
356
|
+
return await this.agent.run(prompt, { signal });
|
|
357
|
+
} finally {
|
|
358
|
+
this.context.cwd = prevCwd;
|
|
359
|
+
}
|
|
333
360
|
}
|
|
334
361
|
async handleTaskStatusChange(taskId, status) {
|
|
335
362
|
if (!this.graph) return;
|
|
@@ -436,8 +463,145 @@ Type: ${task.type}`;
|
|
|
436
463
|
}
|
|
437
464
|
};
|
|
438
465
|
|
|
466
|
+
// src/server/worktree-ws-handler.ts
|
|
467
|
+
var MAX_ACTIVITY = 6;
|
|
468
|
+
var WorktreeWebSocketHandler = class {
|
|
469
|
+
constructor(events, logger) {
|
|
470
|
+
this.events = events;
|
|
471
|
+
this.logger = logger;
|
|
472
|
+
this.subscribe();
|
|
473
|
+
}
|
|
474
|
+
events;
|
|
475
|
+
logger;
|
|
476
|
+
clients = /* @__PURE__ */ new Set();
|
|
477
|
+
handles = /* @__PURE__ */ new Map();
|
|
478
|
+
baseBranch = "";
|
|
479
|
+
broadcastInterval = null;
|
|
480
|
+
offs = [];
|
|
481
|
+
addClient(ws) {
|
|
482
|
+
this.clients.add(ws);
|
|
483
|
+
ws.on("close", () => this.clients.delete(ws));
|
|
484
|
+
ws.on("error", () => this.clients.delete(ws));
|
|
485
|
+
this.send(ws, this.stateMessage());
|
|
486
|
+
}
|
|
487
|
+
dispose() {
|
|
488
|
+
for (const off of this.offs) off();
|
|
489
|
+
this.offs.length = 0;
|
|
490
|
+
this.stopBroadcast();
|
|
491
|
+
}
|
|
492
|
+
// ── internals ───────────────────────────────────────────────────────────
|
|
493
|
+
subscribe() {
|
|
494
|
+
const on = this.events.on.bind(this.events);
|
|
495
|
+
this.offs.push(
|
|
496
|
+
on("worktree.allocated", (p) => {
|
|
497
|
+
const e = p;
|
|
498
|
+
this.baseBranch = e.baseBranch || this.baseBranch;
|
|
499
|
+
this.upsert(e.handleId, {
|
|
500
|
+
handleId: e.handleId,
|
|
501
|
+
ownerId: e.ownerId,
|
|
502
|
+
ownerLabel: e.ownerLabel,
|
|
503
|
+
branch: e.branch,
|
|
504
|
+
baseBranch: e.baseBranch,
|
|
505
|
+
status: "active",
|
|
506
|
+
insertions: 0,
|
|
507
|
+
deletions: 0,
|
|
508
|
+
files: 0,
|
|
509
|
+
allocatedAt: Date.now(),
|
|
510
|
+
lastEventAt: Date.now(),
|
|
511
|
+
recentActivity: []
|
|
512
|
+
});
|
|
513
|
+
this.activity(e.handleId, "allocated", `branch ${e.branch}`);
|
|
514
|
+
this.ensureBroadcast();
|
|
515
|
+
}),
|
|
516
|
+
on("worktree.committed", (p) => {
|
|
517
|
+
const e = p;
|
|
518
|
+
this.patch(e.handleId, { status: "committing", insertions: e.insertions, deletions: e.deletions, files: e.files });
|
|
519
|
+
if (e.committed) this.activity(e.handleId, "committed", `+${e.insertions}/-${e.deletions} (${e.files}f)`);
|
|
520
|
+
this.broadcastState();
|
|
521
|
+
}),
|
|
522
|
+
on("worktree.merged", (p) => {
|
|
523
|
+
const e = p;
|
|
524
|
+
this.patch(e.handleId, { status: "merged" });
|
|
525
|
+
this.activity(e.handleId, "merged", `\u2192 ${e.baseBranch}`);
|
|
526
|
+
this.broadcastState();
|
|
527
|
+
}),
|
|
528
|
+
on("worktree.conflict", (p) => {
|
|
529
|
+
const e = p;
|
|
530
|
+
this.patch(e.handleId, { status: "needs-review", conflictFiles: e.conflictFiles });
|
|
531
|
+
this.activity(e.handleId, "conflict", e.conflictFiles.join(", "));
|
|
532
|
+
this.broadcastState();
|
|
533
|
+
}),
|
|
534
|
+
on("worktree.failed", (p) => {
|
|
535
|
+
const e = p;
|
|
536
|
+
this.patch(e.handleId, { status: "failed" });
|
|
537
|
+
this.activity(e.handleId, "failed", e.error);
|
|
538
|
+
this.broadcastState();
|
|
539
|
+
}),
|
|
540
|
+
on("worktree.released", (p) => {
|
|
541
|
+
const e = p;
|
|
542
|
+
if (!e.kept) this.handles.delete(e.handleId);
|
|
543
|
+
this.activity(e.handleId, "released", e.kept ? "kept for review" : "removed");
|
|
544
|
+
if (this.handles.size === 0) this.stopBroadcast();
|
|
545
|
+
else this.broadcastState();
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
upsert(id, view) {
|
|
550
|
+
this.handles.set(id, view);
|
|
551
|
+
}
|
|
552
|
+
patch(id, patch) {
|
|
553
|
+
const cur = this.handles.get(id);
|
|
554
|
+
if (!cur) return;
|
|
555
|
+
this.handles.set(id, { ...cur, ...patch, lastEventAt: Date.now() });
|
|
556
|
+
}
|
|
557
|
+
activity(id, kind, text) {
|
|
558
|
+
const cur = this.handles.get(id);
|
|
559
|
+
if (cur) {
|
|
560
|
+
const recentActivity = [...cur.recentActivity, { kind, text, at: Date.now() }].slice(-MAX_ACTIVITY);
|
|
561
|
+
this.handles.set(id, { ...cur, recentActivity });
|
|
562
|
+
}
|
|
563
|
+
this.broadcast({ type: "worktree.event", payload: { kind, handleId: id, text, at: Date.now() } });
|
|
564
|
+
}
|
|
565
|
+
stateMessage() {
|
|
566
|
+
return {
|
|
567
|
+
type: "worktree.state",
|
|
568
|
+
payload: { worktrees: [...this.handles.values()], baseBranch: this.baseBranch }
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
broadcastState() {
|
|
572
|
+
this.broadcast(this.stateMessage());
|
|
573
|
+
}
|
|
574
|
+
ensureBroadcast() {
|
|
575
|
+
this.broadcast(this.stateMessage());
|
|
576
|
+
if (this.broadcastInterval) return;
|
|
577
|
+
this.broadcastInterval = setInterval(() => this.broadcast(this.stateMessage()), 2e3);
|
|
578
|
+
}
|
|
579
|
+
stopBroadcast() {
|
|
580
|
+
this.broadcast(this.stateMessage());
|
|
581
|
+
if (this.broadcastInterval) {
|
|
582
|
+
clearInterval(this.broadcastInterval);
|
|
583
|
+
this.broadcastInterval = null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
broadcast(msg) {
|
|
587
|
+
const data = JSON.stringify(msg);
|
|
588
|
+
for (const ws of this.clients) {
|
|
589
|
+
try {
|
|
590
|
+
if (ws.readyState === 1) ws.send(data);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
this.logger.debug?.(`worktree broadcast failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
send(ws, msg) {
|
|
597
|
+
try {
|
|
598
|
+
if (ws.readyState === 1) ws.send(JSON.stringify(msg));
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
439
604
|
// src/server/index.ts
|
|
440
|
-
var HTML_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'";
|
|
441
605
|
async function startWebUI(opts = {}) {
|
|
442
606
|
const wsPort = opts.wsPort ?? 3457;
|
|
443
607
|
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
@@ -447,7 +611,7 @@ async function startWebUI(opts = {}) {
|
|
|
447
611
|
let config = baseConfig;
|
|
448
612
|
let configWriteLock = Promise.resolve();
|
|
449
613
|
console.log("[WebUI] Config loaded:", config.provider ?? "(none)", "/", config.model ?? "(none)");
|
|
450
|
-
if (!config.provider && config.providers && Object.keys(config.providers).length > 0) {
|
|
614
|
+
if (!config.provider && config.providers && typeof config.providers === "object" && config.providers !== null && !Array.isArray(config.providers) && Object.keys(config.providers).length > 0) {
|
|
451
615
|
const firstKey = Object.keys(config.providers)[0];
|
|
452
616
|
config = patchConfig(config, { provider: firstKey });
|
|
453
617
|
console.log("[WebUI] No active provider \u2014 auto-selected:", firstKey);
|
|
@@ -639,7 +803,15 @@ async function startWebUI(opts = {}) {
|
|
|
639
803
|
toolExecutor
|
|
640
804
|
});
|
|
641
805
|
console.log("[WebUI] Agent initialized");
|
|
642
|
-
const autoPhaseHandler = new AutoPhaseWebSocketHandler(
|
|
806
|
+
const autoPhaseHandler = new AutoPhaseWebSocketHandler(
|
|
807
|
+
agent,
|
|
808
|
+
context,
|
|
809
|
+
logger,
|
|
810
|
+
wpaths.projectAutophase,
|
|
811
|
+
events,
|
|
812
|
+
projectRoot
|
|
813
|
+
);
|
|
814
|
+
const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
|
|
643
815
|
async function sessionStartPayload() {
|
|
644
816
|
let maxContext = 0;
|
|
645
817
|
let inputCost = 0;
|
|
@@ -730,11 +902,12 @@ async function startWebUI(opts = {}) {
|
|
|
730
902
|
const RATE_LIMIT_MESSAGES = 60;
|
|
731
903
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
732
904
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
733
|
-
function checkRateLimit(ws) {
|
|
905
|
+
function checkRateLimit(ws, client) {
|
|
734
906
|
const now = Date.now();
|
|
735
|
-
const
|
|
907
|
+
const key = client.sessionId ?? String(ws);
|
|
908
|
+
const limit = rateLimits.get(key);
|
|
736
909
|
if (!limit || now > limit.resetAt) {
|
|
737
|
-
rateLimits.set(
|
|
910
|
+
rateLimits.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
738
911
|
return true;
|
|
739
912
|
}
|
|
740
913
|
if (limit.count >= RATE_LIMIT_MESSAGES) return false;
|
|
@@ -861,8 +1034,9 @@ async function startWebUI(opts = {}) {
|
|
|
861
1034
|
send(ws, { type: "session.start", payload });
|
|
862
1035
|
});
|
|
863
1036
|
autoPhaseHandler.addClient(ws);
|
|
1037
|
+
worktreeHandler.addClient(ws);
|
|
864
1038
|
ws.on("message", async (data) => {
|
|
865
|
-
if (!checkRateLimit(ws)) {
|
|
1039
|
+
if (!checkRateLimit(ws, client)) {
|
|
866
1040
|
send(ws, {
|
|
867
1041
|
type: "error",
|
|
868
1042
|
payload: {
|
|
@@ -873,15 +1047,24 @@ async function startWebUI(opts = {}) {
|
|
|
873
1047
|
return;
|
|
874
1048
|
}
|
|
875
1049
|
try {
|
|
876
|
-
const
|
|
877
|
-
|
|
1050
|
+
const rawObj = JSON.parse(data.toString());
|
|
1051
|
+
if (typeof rawObj === "object" && rawObj !== null) {
|
|
1052
|
+
const obj = rawObj;
|
|
1053
|
+
if ("__proto__" in obj || "constructor" in obj || "prototype" in obj) {
|
|
1054
|
+
send(ws, { type: "error", payload: { phase: "parse", message: "Invalid message object" } });
|
|
1055
|
+
} else {
|
|
1056
|
+
await handleMessage(ws, client, rawObj);
|
|
1057
|
+
}
|
|
1058
|
+
} else {
|
|
1059
|
+
await handleMessage(ws, client, rawObj);
|
|
1060
|
+
}
|
|
878
1061
|
} catch (err) {
|
|
879
1062
|
console.error("[WebUI] Failed to parse message", err);
|
|
880
1063
|
}
|
|
881
1064
|
});
|
|
882
1065
|
ws.on("close", () => {
|
|
883
1066
|
clients.delete(ws);
|
|
884
|
-
rateLimits.delete(ws);
|
|
1067
|
+
rateLimits.delete(String(ws));
|
|
885
1068
|
console.log("[WebUI] Client disconnected, total:", clients.size);
|
|
886
1069
|
if (pendingConfirms.size > 0) {
|
|
887
1070
|
for (const [id, resolve2] of pendingConfirms) {
|
|
@@ -1875,7 +2058,10 @@ async function startWebUI(opts = {}) {
|
|
|
1875
2058
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1876
2059
|
if (ext === ".html") {
|
|
1877
2060
|
res.setHeader("Cache-Control", "no-cache");
|
|
1878
|
-
res.setHeader(
|
|
2061
|
+
res.setHeader(
|
|
2062
|
+
"Content-Security-Policy",
|
|
2063
|
+
`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort} wss://127.0.0.1:${wsPort} ws://[::1]:${wsPort} wss://[::1]:${wsPort}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`
|
|
2064
|
+
);
|
|
1879
2065
|
}
|
|
1880
2066
|
const fileContent = await fs2.readFile(resolvedPath);
|
|
1881
2067
|
res.writeHead(200);
|
|
@@ -1891,7 +2077,7 @@ async function startWebUI(opts = {}) {
|
|
|
1891
2077
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
1892
2078
|
// SPA fallback previously shipped no CSP — apply the same policy as
|
|
1893
2079
|
// the direct .html branch so deep-linked routes aren't unprotected.
|
|
1894
|
-
"Content-Security-Policy":
|
|
2080
|
+
"Content-Security-Policy": `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort} wss://127.0.0.1:${wsPort} ws://[::1]:${wsPort} wss://[::1]:${wsPort}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`
|
|
1895
2081
|
});
|
|
1896
2082
|
res.end(fileContent);
|
|
1897
2083
|
} catch {
|