taskplane 0.22.11 → 0.22.13
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/bin/rpc-wrapper.mjs +183 -2
- package/dashboard/public/app.js +108 -55
- package/dashboard/public/style.css +18 -0
- package/dashboard/server.cjs +29 -3
- package/extensions/task-runner.ts +179 -21
- package/extensions/taskplane/cleanup.ts +76 -7
- package/extensions/taskplane/diagnostics.ts +1 -1
- package/extensions/taskplane/engine-worker.ts +1 -1
- package/extensions/taskplane/engine.ts +14 -13
- package/extensions/taskplane/execution.ts +20 -3
- package/extensions/taskplane/extension.ts +616 -11
- package/extensions/taskplane/mailbox.ts +341 -0
- package/extensions/taskplane/merge.ts +11 -2
- package/extensions/taskplane/supervisor-primer.md +9 -3
- package/extensions/taskplane/supervisor.ts +2 -2
- package/extensions/taskplane/types.ts +95 -1
- package/extensions/taskplane/verification.ts +14 -9
- package/package.json +1 -1
- package/templates/agents/supervisor.md +1 -1
- package/templates/agents/task-worker.md +8 -14
package/bin/rpc-wrapper.mjs
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { spawn } from "node:child_process";
|
|
31
|
-
import { readFileSync, writeFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
32
|
-
import { dirname, resolve } from "node:path";
|
|
31
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, renameSync } from "node:fs";
|
|
32
|
+
import { dirname, resolve, join, basename } from "node:path";
|
|
33
33
|
import { StringDecoder } from "node:string_decoder";
|
|
34
34
|
|
|
35
35
|
// ── CLI Argument Parsing ─────────────────────────────────────────────
|
|
@@ -45,6 +45,7 @@ function parseArgs(argv) {
|
|
|
45
45
|
extensions: [],
|
|
46
46
|
passthrough: [],
|
|
47
47
|
help: false,
|
|
48
|
+
mailboxDir: null,
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
let i = 2; // skip "node" and script path
|
|
@@ -74,6 +75,9 @@ function parseArgs(argv) {
|
|
|
74
75
|
} else if (arg === "--extensions" && i + 1 < argv.length) {
|
|
75
76
|
args.extensions = argv[++i].split(",").map((e) => e.trim()).filter(Boolean);
|
|
76
77
|
i++;
|
|
78
|
+
} else if (arg === "--mailbox-dir" && i + 1 < argv.length) {
|
|
79
|
+
args.mailboxDir = argv[++i];
|
|
80
|
+
i++;
|
|
77
81
|
} else if (arg === "--") {
|
|
78
82
|
args.passthrough = argv.slice(i + 1);
|
|
79
83
|
break;
|
|
@@ -103,6 +107,7 @@ Optional:
|
|
|
103
107
|
--system-prompt-file <path> Path to system prompt file
|
|
104
108
|
--tools <t1,t2,...> Comma-separated tool names
|
|
105
109
|
--extensions <e1,e2,...> Comma-separated extension paths
|
|
110
|
+
--mailbox-dir <path> Mailbox directory for agent steering (TP-089)
|
|
106
111
|
-h, --help Show this help
|
|
107
112
|
`
|
|
108
113
|
);
|
|
@@ -487,6 +492,160 @@ function createSingleWriteGuard(writer) {
|
|
|
487
492
|
};
|
|
488
493
|
}
|
|
489
494
|
|
|
495
|
+
// ── Agent Mailbox Check (TP-089) ─────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Valid mailbox message types (must match MailboxMessageType in types.ts).
|
|
499
|
+
*/
|
|
500
|
+
const MAILBOX_MESSAGE_TYPES = new Set(["steer", "query", "abort", "info", "reply", "escalate"]);
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Check the agent's mailbox inbox for pending messages and inject them
|
|
504
|
+
* into the pi process via the `steer` RPC command.
|
|
505
|
+
*
|
|
506
|
+
* Called on every `message_end` event when `--mailbox-dir` is provided.
|
|
507
|
+
* Messages are validated (batchId, to, shape), sorted deterministically,
|
|
508
|
+
* injected via steer, and moved from inbox/ to ack/.
|
|
509
|
+
*
|
|
510
|
+
* @param {string} mailboxDir - Session mailbox directory (e.g., .pi/mailbox/{batchId}/{session})
|
|
511
|
+
* @param {object} proc - The spawned pi process (must have writable stdin)
|
|
512
|
+
* @returns {{ delivered: number, skipped: number }} Delivery stats
|
|
513
|
+
*/
|
|
514
|
+
function checkMailboxAndSteer(mailboxDir, proc) {
|
|
515
|
+
const stats = { delivered: 0, skipped: 0 };
|
|
516
|
+
|
|
517
|
+
// Derive expected values from path structure:
|
|
518
|
+
// mailboxDir = .pi/mailbox/{batchId}/{sessionName}
|
|
519
|
+
const expectedSessionName = basename(mailboxDir);
|
|
520
|
+
const expectedBatchId = basename(dirname(mailboxDir));
|
|
521
|
+
|
|
522
|
+
const inboxDir = join(mailboxDir, "inbox");
|
|
523
|
+
|
|
524
|
+
// Read inbox — ENOENT is quiet no-op (inbox may not exist yet)
|
|
525
|
+
let entries;
|
|
526
|
+
try {
|
|
527
|
+
entries = readdirSync(inboxDir);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
if (err.code === "ENOENT") return stats;
|
|
530
|
+
process.stderr.write(`\n[STEERING] WARNING: failed to read inbox: ${err.message}\n`);
|
|
531
|
+
return stats;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Filter: only *.msg.json files (excludes .msg.json.tmp temp files)
|
|
535
|
+
const msgFiles = entries.filter(f => f.endsWith(".msg.json") && !f.endsWith(".msg.json.tmp"));
|
|
536
|
+
if (msgFiles.length === 0) return stats;
|
|
537
|
+
|
|
538
|
+
// Read and validate all messages
|
|
539
|
+
const validMessages = [];
|
|
540
|
+
|
|
541
|
+
for (const filename of msgFiles) {
|
|
542
|
+
const filePath = join(inboxDir, filename);
|
|
543
|
+
let raw;
|
|
544
|
+
try {
|
|
545
|
+
raw = readFileSync(filePath, "utf-8");
|
|
546
|
+
} catch (err) {
|
|
547
|
+
process.stderr.write(`\n[STEERING] WARNING: failed to read ${filename}: ${err.message}\n`);
|
|
548
|
+
stats.skipped++;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let msg;
|
|
553
|
+
try {
|
|
554
|
+
msg = JSON.parse(raw);
|
|
555
|
+
} catch {
|
|
556
|
+
process.stderr.write(`\n[STEERING] WARNING: malformed JSON in ${filename}, skipping\n`);
|
|
557
|
+
stats.skipped++;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Validate shape
|
|
562
|
+
if (!isValidMailboxMessageShape(msg)) {
|
|
563
|
+
process.stderr.write(`\n[STEERING] WARNING: invalid message shape in ${filename}, skipping\n`);
|
|
564
|
+
stats.skipped++;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Validate batchId (derived from path, not message content)
|
|
569
|
+
if (msg.batchId !== expectedBatchId) {
|
|
570
|
+
process.stderr.write(`\n[STEERING] WARNING: batchId mismatch in ${filename} (expected ${expectedBatchId}, got ${msg.batchId}), skipping\n`);
|
|
571
|
+
stats.skipped++;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Validate to (no misdelivery)
|
|
576
|
+
if (msg.to !== expectedSessionName) {
|
|
577
|
+
process.stderr.write(`\n[STEERING] WARNING: misdelivery in ${filename} (to=${msg.to}, expected ${expectedSessionName}), skipping\n`);
|
|
578
|
+
stats.skipped++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
validMessages.push({ filename, message: msg });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Sort: primary by timestamp ascending, tie-break by filename lexical
|
|
586
|
+
validMessages.sort((a, b) => {
|
|
587
|
+
const tsDiff = a.message.timestamp - b.message.timestamp;
|
|
588
|
+
if (tsDiff !== 0) return tsDiff;
|
|
589
|
+
return a.filename.localeCompare(b.filename);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Inject each message via steer RPC command and move to ack/
|
|
593
|
+
for (const { filename, message } of validMessages) {
|
|
594
|
+
try {
|
|
595
|
+
// Precondition: stdin must be available for injection.
|
|
596
|
+
// If stdin is closed/destroyed, keep message in inbox (no false ack).
|
|
597
|
+
if (!proc.stdin || proc.stdin.destroyed) {
|
|
598
|
+
stats.skipped++;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Inject via steer RPC command
|
|
603
|
+
proc.stdin.write(JSON.stringify({ type: "steer", message: message.content }) + "\n");
|
|
604
|
+
|
|
605
|
+
// Move to ack/ (delivery proof)
|
|
606
|
+
const ackDir = join(mailboxDir, "ack");
|
|
607
|
+
try { mkdirSync(ackDir, { recursive: true }); } catch { /* exists */ }
|
|
608
|
+
try {
|
|
609
|
+
renameSync(join(inboxDir, filename), join(ackDir, filename));
|
|
610
|
+
} catch (err) {
|
|
611
|
+
// ENOENT race is harmless (another process acked it)
|
|
612
|
+
if (err.code !== "ENOENT") {
|
|
613
|
+
process.stderr.write(`\n[STEERING] WARNING: failed to ack ${filename}: ${err.message}\n`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
stats.delivered++;
|
|
618
|
+
process.stderr.write(`\n[STEERING] Delivered message ${message.id}\n`);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
process.stderr.write(`\n[STEERING] WARNING: failed to deliver ${filename}: ${err.message}\n`);
|
|
621
|
+
stats.skipped++;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return stats;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Runtime validation for mailbox message shape in rpc-wrapper.
|
|
630
|
+
* Mirrors isValidMailboxMessage() from mailbox.ts but as a standalone
|
|
631
|
+
* function (rpc-wrapper.mjs is a plain .mjs module, not TypeScript).
|
|
632
|
+
*
|
|
633
|
+
* @param {any} obj - Parsed JSON value
|
|
634
|
+
* @returns {boolean} true if valid shape
|
|
635
|
+
*/
|
|
636
|
+
function isValidMailboxMessageShape(obj) {
|
|
637
|
+
if (!obj || typeof obj !== "object") return false;
|
|
638
|
+
return (
|
|
639
|
+
typeof obj.id === "string" &&
|
|
640
|
+
typeof obj.batchId === "string" &&
|
|
641
|
+
typeof obj.from === "string" &&
|
|
642
|
+
typeof obj.to === "string" &&
|
|
643
|
+
typeof obj.timestamp === "number" && Number.isFinite(obj.timestamp) &&
|
|
644
|
+
typeof obj.type === "string" && MAILBOX_MESSAGE_TYPES.has(obj.type) &&
|
|
645
|
+
typeof obj.content === "string"
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
490
649
|
// ── Exports for Testing ──────────────────────────────────────────────
|
|
491
650
|
|
|
492
651
|
// Export pure functions so tests can import them without triggering side effects.
|
|
@@ -503,6 +662,9 @@ export {
|
|
|
503
662
|
applyEvent,
|
|
504
663
|
buildExitSummary,
|
|
505
664
|
createSingleWriteGuard,
|
|
665
|
+
checkMailboxAndSteer,
|
|
666
|
+
isValidMailboxMessageShape,
|
|
667
|
+
MAILBOX_MESSAGE_TYPES,
|
|
506
668
|
};
|
|
507
669
|
|
|
508
670
|
// ── Main ─────────────────────────────────────────────────────────────
|
|
@@ -602,6 +764,15 @@ const proc = spawn("pi", piArgs, {
|
|
|
602
764
|
const promptCmd = { type: "prompt", message: promptContent };
|
|
603
765
|
proc.stdin.write(JSON.stringify(promptCmd) + "\n");
|
|
604
766
|
|
|
767
|
+
// ── Agent Mailbox Steering Setup (TP-089) ────────────────────────────
|
|
768
|
+
// When mailbox-dir is provided, set steering mode to "all" so queued
|
|
769
|
+
// steering messages are delivered together at the next turn boundary.
|
|
770
|
+
// Must be sent after prompt but before any agent processing begins.
|
|
771
|
+
if (args.mailboxDir) {
|
|
772
|
+
proc.stdin.write(JSON.stringify({ type: "set_steering_mode", mode: "all" }) + "\n");
|
|
773
|
+
process.stderr.write(`[rpc-wrapper] mailbox enabled: ${args.mailboxDir}\n`);
|
|
774
|
+
}
|
|
775
|
+
|
|
605
776
|
// ── Stdin Lifecycle ──────────────────────────────────────────────────
|
|
606
777
|
|
|
607
778
|
/**
|
|
@@ -677,6 +848,16 @@ function handleEvent(event) {
|
|
|
677
848
|
// Falls back gracefully: older pi versions ignore the command
|
|
678
849
|
// or return a response without contextUsage — state.contextUsage stays null.
|
|
679
850
|
querySessionStats();
|
|
851
|
+
// Check mailbox for pending steering messages (TP-089).
|
|
852
|
+
// Only active when --mailbox-dir is provided (backward compatible).
|
|
853
|
+
if (args.mailboxDir) {
|
|
854
|
+
try {
|
|
855
|
+
checkMailboxAndSteer(args.mailboxDir, proc);
|
|
856
|
+
} catch (err) {
|
|
857
|
+
// Never crash on mailbox I/O errors
|
|
858
|
+
process.stderr.write(`\n[STEERING] ERROR: ${err.message}\n`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
680
861
|
break;
|
|
681
862
|
|
|
682
863
|
case "tool_execution_start":
|
package/dashboard/public/app.js
CHANGED
|
@@ -621,6 +621,58 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
621
621
|
|
|
622
622
|
// ─── Render: Merge Agents ───────────────────────────────────────────────────
|
|
623
623
|
|
|
624
|
+
/** Build full telemetry HTML for a merge agent (parity with worker stats).
|
|
625
|
+
* Shows: elapsed, tool count, context %, cost, current tool, retry/compaction badges.
|
|
626
|
+
* Returns empty string if no meaningful telemetry exists.
|
|
627
|
+
*/
|
|
628
|
+
function mergeTelemetryHtml(tel, alive) {
|
|
629
|
+
if (!tel) return '<span class="merge-no-data">—</span>';
|
|
630
|
+
const hasData = (tel.inputTokens || 0) > 0 || (tel.outputTokens || 0) > 0 ||
|
|
631
|
+
(tel.toolCalls || 0) > 0 || (tel.cost || 0) > 0;
|
|
632
|
+
if (!hasData) return '<span class="merge-no-data">—</span>';
|
|
633
|
+
|
|
634
|
+
let html = '<div class="merge-stats">';
|
|
635
|
+
|
|
636
|
+
// Elapsed time
|
|
637
|
+
if (tel.startedAt) {
|
|
638
|
+
const elapsed = Date.now() - tel.startedAt;
|
|
639
|
+
html += `<span class="worker-stat" title="Merge elapsed">⏱ ${formatDuration(elapsed)}</span>`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Tool calls
|
|
643
|
+
if (tel.toolCalls > 0) {
|
|
644
|
+
html += `<span class="worker-stat" title="Tool calls">🔧 ${tel.toolCalls}</span>`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Context %
|
|
648
|
+
if (tel.contextPct > 0) {
|
|
649
|
+
html += `<span class="worker-stat" title="Context window used">📊 ${Math.round(tel.contextPct)}%</span>`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Tokens + cost
|
|
653
|
+
const inp = (tel.inputTokens || 0) + (tel.cacheReadTokens || 0);
|
|
654
|
+
const out = tel.outputTokens || 0;
|
|
655
|
+
const cost = tel.cost || 0;
|
|
656
|
+
if (inp > 0 || out > 0) {
|
|
657
|
+
let tokenStr = `↑${formatTokens(inp)} ↓${formatTokens(out)}`;
|
|
658
|
+
if (cost > 0) tokenStr += ` ${formatCost(cost)}`;
|
|
659
|
+
html += `<span class="worker-stat" title="Tokens">🪙 ${tokenStr}</span>`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Current tool (if alive/active) or last tool (completed merges)
|
|
663
|
+
if (alive && tel.currentTool) {
|
|
664
|
+
html += `<span class="worker-stat worker-last-tool" title="Current tool">${escapeHtml(tel.currentTool)}</span>`;
|
|
665
|
+
} else if (!alive && tel.lastTool) {
|
|
666
|
+
html += `<span class="worker-stat worker-last-tool" title="Last tool">${escapeHtml(tel.lastTool)}</span>`;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Retry/compaction badges (reuse shared helper)
|
|
670
|
+
html += telemetryBadgesHtml(tel);
|
|
671
|
+
|
|
672
|
+
html += '</div>';
|
|
673
|
+
return html;
|
|
674
|
+
}
|
|
675
|
+
|
|
624
676
|
function renderMergeAgents(batch, tmuxSessions) {
|
|
625
677
|
const mergeResults = batch?.mergeResults || [];
|
|
626
678
|
const tmuxSet = new Set(tmuxSessions || []);
|
|
@@ -642,8 +694,8 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
642
694
|
mergePrefix = laneMatch[1] + "-merge";
|
|
643
695
|
}
|
|
644
696
|
}
|
|
645
|
-
// Helper: get merge session name for a
|
|
646
|
-
const getMergeSessionName = (
|
|
697
|
+
// Helper: get merge session name for a merge number
|
|
698
|
+
const getMergeSessionName = (mergeNum) => `${mergePrefix}-${mergeNum}`;
|
|
647
699
|
|
|
648
700
|
if (mergeResults.length === 0 && mergeSessions.length === 0) {
|
|
649
701
|
$mergeBody.innerHTML = '<div class="empty-state">No merge agents active</div>';
|
|
@@ -671,49 +723,64 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
671
723
|
: mr.status === "partial" ? "status-stalled"
|
|
672
724
|
: "status-failed";
|
|
673
725
|
|
|
674
|
-
//
|
|
675
|
-
// Merge sessions
|
|
676
|
-
//
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
const
|
|
680
|
-
|
|
726
|
+
// Merge session mapping: derive from lane numbers involved in this wave.
|
|
727
|
+
// Merge sessions are named by lane number (e.g., ...-merge-1), not wave index.
|
|
728
|
+
// Extract lane numbers from repoResults or from batch tasks for this wave.
|
|
729
|
+
const waveLaneNums = new Set();
|
|
730
|
+
const repoResults2 = mr.repoResults || [];
|
|
731
|
+
for (const rr of repoResults2) {
|
|
732
|
+
for (const ln of (rr.laneNumbers || [])) waveLaneNums.add(ln);
|
|
733
|
+
}
|
|
734
|
+
// Fallback: find lane numbers from tasks assigned to this wave
|
|
735
|
+
if (waveLaneNums.size === 0 && batch.wavePlan && batch.wavePlan[mr.waveIndex]) {
|
|
736
|
+
const waveTaskIds = new Set(batch.wavePlan[mr.waveIndex]);
|
|
737
|
+
for (const t of (batch.tasks || [])) {
|
|
738
|
+
if (waveTaskIds.has(t.taskId) && t.laneNumber != null) {
|
|
739
|
+
waveLaneNums.add(t.laneNumber);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// Find alive merge sessions matching the wave's lane numbers
|
|
744
|
+
let effectiveSession = null;
|
|
745
|
+
for (const ln of waveLaneNums) {
|
|
746
|
+
const candidate = getMergeSessionName(ln);
|
|
747
|
+
if (tmuxSet.has(candidate) && !shownSessions.has(candidate)) {
|
|
748
|
+
effectiveSession = candidate;
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Fallback: any unshown alive merge session
|
|
753
|
+
if (!effectiveSession) {
|
|
754
|
+
effectiveSession = mergeSessions.find(s => tmuxSet.has(s) && !shownSessions.has(s)) || null;
|
|
755
|
+
}
|
|
756
|
+
const effectiveAlive = !!effectiveSession;
|
|
757
|
+
if (effectiveSession) shownSessions.add(effectiveSession);
|
|
681
758
|
|
|
682
|
-
//
|
|
759
|
+
// Find merge telemetry: try sessions by lane number first
|
|
683
760
|
let mergeTel = null;
|
|
684
|
-
for (const
|
|
685
|
-
|
|
761
|
+
for (const ln of waveLaneNums) {
|
|
762
|
+
const candidate = getMergeSessionName(ln);
|
|
763
|
+
if (telemetry[candidate]) { mergeTel = telemetry[candidate]; break; }
|
|
686
764
|
}
|
|
765
|
+
// Fallback: effective session telemetry or any merge session
|
|
766
|
+
if (!mergeTel && effectiveSession) mergeTel = telemetry[effectiveSession] || null;
|
|
767
|
+
if (!mergeTel) mergeTel = mergeSessions.reduce((found, ms) => found || telemetry[ms] || null, null);
|
|
687
768
|
|
|
688
769
|
html += `<tr>`;
|
|
689
|
-
html += `<td
|
|
770
|
+
html += `<td class="merge-wave-cell">Wave ${mr.waveIndex + 1}</td>`;
|
|
690
771
|
html += `<td><span class="status-badge ${statusCls}">${mr.status}</span></td>`;
|
|
691
|
-
html += `<td
|
|
692
|
-
//
|
|
693
|
-
html += `<td
|
|
694
|
-
if (mergeTel) {
|
|
695
|
-
const totalTok = (mergeTel.inputTokens || 0) + (mergeTel.outputTokens || 0);
|
|
696
|
-
const cost = mergeTel.cost || 0;
|
|
697
|
-
if (totalTok > 0 || cost > 0) {
|
|
698
|
-
html += `<span style="color:var(--text-muted);">${totalTok > 0 ? totalTok.toLocaleString() + " tok" : ""}`;
|
|
699
|
-
if (cost > 0) html += ` · $${cost.toFixed(4)}`;
|
|
700
|
-
html += `</span>`;
|
|
701
|
-
} else {
|
|
702
|
-
html += '<span style="color:var(--text-faint);">—</span>';
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
html += '<span style="color:var(--text-faint);">—</span>';
|
|
706
|
-
}
|
|
707
|
-
html += `</td>`;
|
|
772
|
+
html += `<td class="merge-session-cell">${effectiveAlive ? escapeHtml(effectiveSession) : "—"}</td>`;
|
|
773
|
+
// Full telemetry cell
|
|
774
|
+
html += `<td class="merge-telemetry-cell">${mergeTelemetryHtml(mergeTel, effectiveAlive)}</td>`;
|
|
708
775
|
html += `<td>`;
|
|
709
|
-
if (
|
|
710
|
-
const cmd = `tmux attach -t ${
|
|
711
|
-
html += `<span class="tmux-cmd" data-tmux="${escapeHtml(
|
|
776
|
+
if (effectiveAlive) {
|
|
777
|
+
const cmd = `tmux attach -t ${effectiveSession}`;
|
|
778
|
+
html += `<span class="tmux-cmd" data-tmux="${escapeHtml(effectiveSession)}" onclick="copyTmuxCmd('${escapeHtml(effectiveSession)}')" title="Click to copy">${escapeHtml(cmd)}</span>`;
|
|
712
779
|
} else {
|
|
713
|
-
html += '<span
|
|
780
|
+
html += '<span class="merge-no-data">—</span>';
|
|
714
781
|
}
|
|
715
782
|
html += `</td>`;
|
|
716
|
-
html += `<td
|
|
783
|
+
html += `<td class="merge-detail-cell">${mr.failureReason ? escapeHtml(mr.failureReason) : "—"}</td>`;
|
|
717
784
|
html += `</tr>`;
|
|
718
785
|
|
|
719
786
|
// Per-repo sub-rows: show when workspace mode has repo results
|
|
@@ -732,10 +799,10 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
732
799
|
html += `<tr class="merge-repo-row">`;
|
|
733
800
|
html += `<td>${repoBadgeHtml(rr.repoId)}</td>`;
|
|
734
801
|
html += `<td><span class="status-badge ${rrStatusCls}">${rr.status}</span></td>`;
|
|
735
|
-
html += `<td
|
|
802
|
+
html += `<td class="merge-session-cell">${rrLanes}</td>`;
|
|
736
803
|
html += `<td></td>`; /* telemetry placeholder */
|
|
737
804
|
html += `<td></td>`; /* attach placeholder */
|
|
738
|
-
html += `<td
|
|
805
|
+
html += `<td class="merge-detail-cell">${rrDetail}</td>`;
|
|
739
806
|
html += `</tr>`;
|
|
740
807
|
}
|
|
741
808
|
}
|
|
@@ -748,25 +815,11 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
748
815
|
const sessTel = telemetry[sess] || null;
|
|
749
816
|
const cmd = `tmux attach -t ${sess}`;
|
|
750
817
|
html += `<tr>`;
|
|
751
|
-
html += `<td
|
|
818
|
+
html += `<td class="merge-wave-cell">—</td>`;
|
|
752
819
|
html += `<td><span class="status-badge status-running"><span class="status-dot running"></span> merging</span></td>`;
|
|
753
|
-
html += `<td
|
|
754
|
-
//
|
|
755
|
-
html += `<td
|
|
756
|
-
if (sessTel) {
|
|
757
|
-
const totalTok = (sessTel.inputTokens || 0) + (sessTel.outputTokens || 0);
|
|
758
|
-
const cost = sessTel.cost || 0;
|
|
759
|
-
if (totalTok > 0 || cost > 0) {
|
|
760
|
-
html += `<span style="color:var(--text-muted);">${totalTok > 0 ? totalTok.toLocaleString() + " tok" : ""}`;
|
|
761
|
-
if (cost > 0) html += ` · $${cost.toFixed(4)}`;
|
|
762
|
-
html += `</span>`;
|
|
763
|
-
} else {
|
|
764
|
-
html += '<span style="color:var(--text-faint);">—</span>';
|
|
765
|
-
}
|
|
766
|
-
} else {
|
|
767
|
-
html += '<span style="color:var(--text-faint);">—</span>';
|
|
768
|
-
}
|
|
769
|
-
html += `</td>`;
|
|
820
|
+
html += `<td class="merge-session-cell">${escapeHtml(sess)}</td>`;
|
|
821
|
+
// Full telemetry cell for active merge session
|
|
822
|
+
html += `<td class="merge-telemetry-cell">${mergeTelemetryHtml(sessTel, true)}</td>`;
|
|
770
823
|
html += `<td><span class="tmux-cmd" data-tmux="${escapeHtml(sess)}" onclick="copyTmuxCmd('${escapeHtml(sess)}')" title="Click to copy">${escapeHtml(cmd)}</span></td>`;
|
|
771
824
|
html += `<td>—</td>`;
|
|
772
825
|
html += `</tr>`;
|
|
@@ -712,6 +712,24 @@ body {
|
|
|
712
712
|
.merge-table tr:last-child td { border-bottom: none; }
|
|
713
713
|
.merge-table tr:hover td { background: var(--bg-surface-hover); }
|
|
714
714
|
|
|
715
|
+
.merge-wave-cell { font-family: var(--font-mono); }
|
|
716
|
+
.merge-session-cell { font-family: var(--font-mono); font-size: 0.8rem; }
|
|
717
|
+
.merge-detail-cell { font-size: 0.8rem; color: var(--text-muted); }
|
|
718
|
+
.merge-no-data { color: var(--text-faint); }
|
|
719
|
+
|
|
720
|
+
.merge-stats {
|
|
721
|
+
display: flex;
|
|
722
|
+
flex-wrap: wrap;
|
|
723
|
+
gap: 4px 8px;
|
|
724
|
+
align-items: center;
|
|
725
|
+
font-size: 0.75rem;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.merge-telemetry-cell {
|
|
729
|
+
font-size: 0.75rem;
|
|
730
|
+
min-width: 120px;
|
|
731
|
+
}
|
|
732
|
+
|
|
715
733
|
/* ─── Terminal Panel ────────────────────────────────────────────────────── */
|
|
716
734
|
|
|
717
735
|
.terminal-panel .panel-header {
|
package/dashboard/server.cjs
CHANGED
|
@@ -427,8 +427,9 @@ function loadTelemetryData(batchState) {
|
|
|
427
427
|
const fresh = {
|
|
428
428
|
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0,
|
|
429
429
|
cacheWriteTokens: 0, cost: 0, toolCalls: 0,
|
|
430
|
-
lastTool: "", retries: 0, retryActive: false,
|
|
430
|
+
lastTool: "", currentTool: "", retries: 0, retryActive: false,
|
|
431
431
|
lastRetryError: "", compactions: 0, latestTotalTokens: 0,
|
|
432
|
+
contextPct: 0, startedAt: 0,
|
|
432
433
|
};
|
|
433
434
|
telemetryAccumulators.set(prefix, fresh);
|
|
434
435
|
// Also reset tail states for ALL files of this prefix to re-read from beginning
|
|
@@ -450,8 +451,9 @@ function loadTelemetryData(batchState) {
|
|
|
450
451
|
if (ts && ts.wasReset) {
|
|
451
452
|
acc.inputTokens = 0; acc.outputTokens = 0; acc.cacheReadTokens = 0;
|
|
452
453
|
acc.cacheWriteTokens = 0; acc.cost = 0; acc.toolCalls = 0;
|
|
453
|
-
acc.lastTool = ""; acc.retries = 0; acc.retryActive = false;
|
|
454
|
+
acc.lastTool = ""; acc.currentTool = ""; acc.retries = 0; acc.retryActive = false;
|
|
454
455
|
acc.lastRetryError = ""; acc.compactions = 0; acc.latestTotalTokens = 0;
|
|
456
|
+
acc.contextPct = 0; acc.startedAt = 0;
|
|
455
457
|
ts.wasReset = false;
|
|
456
458
|
}
|
|
457
459
|
for (const event of events) {
|
|
@@ -497,7 +499,31 @@ function loadTelemetryData(batchState) {
|
|
|
497
499
|
}
|
|
498
500
|
}
|
|
499
501
|
}
|
|
500
|
-
|
|
502
|
+
const toolLabel = argPreview ? `${toolDesc} ${argPreview}` : toolDesc;
|
|
503
|
+
acc.lastTool = toolLabel;
|
|
504
|
+
acc.currentTool = toolLabel;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case "tool_execution_end": {
|
|
508
|
+
acc.currentTool = "";
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
case "agent_start": {
|
|
512
|
+
if (event.ts && !acc.startedAt) {
|
|
513
|
+
acc.startedAt = event.ts;
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case "response": {
|
|
518
|
+
// Extract context usage from get_session_stats responses
|
|
519
|
+
const ctxUsage = event.data?.contextUsage;
|
|
520
|
+
if (ctxUsage) {
|
|
521
|
+
// Support both percent (current) and percentUsed (legacy pi versions)
|
|
522
|
+
const pct = typeof ctxUsage.percent === "number" ? ctxUsage.percent
|
|
523
|
+
: typeof ctxUsage.percentUsed === "number" ? ctxUsage.percentUsed
|
|
524
|
+
: null;
|
|
525
|
+
if (pct !== null) acc.contextPct = pct;
|
|
526
|
+
}
|
|
501
527
|
break;
|
|
502
528
|
}
|
|
503
529
|
case "auto_retry_start": {
|