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