@wrongstack/webui 0.8.5 → 0.8.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.
- 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 +183 -9
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.js +183 -9
- 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,6 +463,144 @@ 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
605
|
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
606
|
async function startWebUI(opts = {}) {
|
|
@@ -639,7 +804,15 @@ async function startWebUI(opts = {}) {
|
|
|
639
804
|
toolExecutor
|
|
640
805
|
});
|
|
641
806
|
console.log("[WebUI] Agent initialized");
|
|
642
|
-
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);
|
|
643
816
|
async function sessionStartPayload() {
|
|
644
817
|
let maxContext = 0;
|
|
645
818
|
let inputCost = 0;
|
|
@@ -861,6 +1034,7 @@ 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
1039
|
if (!checkRateLimit(ws)) {
|
|
866
1040
|
send(ws, {
|