claudeboard 2.3.0 → 2.4.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/agents/claude-api.js +1 -1
- package/agents/qa.js +160 -139
- package/bin/cli.js +11 -1
- package/dashboard/index.html +23 -9
- package/package.json +1 -1
package/agents/claude-api.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const MODEL = "claude-sonnet-4-20250514";
|
|
7
|
-
const MAX_TOKENS =
|
|
7
|
+
const MAX_TOKENS = 8096; // Max output tokens — input context window is 200k, no limits there
|
|
8
8
|
|
|
9
9
|
function getHeaders() {
|
|
10
10
|
const key = process.env.ANTHROPIC_API_KEY;
|
package/agents/qa.js
CHANGED
|
@@ -1,133 +1,103 @@
|
|
|
1
1
|
import { callClaude, callClaudeJSON, callClaudeWithImage } from "./claude-api.js";
|
|
2
|
-
import { addLog
|
|
3
|
-
import { readFile, listFiles
|
|
4
|
-
import { runCommand
|
|
2
|
+
import { addLog } from "./board-client.js";
|
|
3
|
+
import { readFile, listFiles } from "../tools/filesystem.js";
|
|
4
|
+
import { runCommand } from "../tools/terminal.js";
|
|
5
5
|
import { screenshotExpoWeb } from "../tools/screenshot.js";
|
|
6
|
-
import {
|
|
6
|
+
import { readErrorLogs } from "../tools/supabase-reader.js";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
|
|
10
|
-
const SYSTEM_QA = `You are a senior QA engineer for
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
You receive:
|
|
14
|
-
- The original PRD description of a feature
|
|
15
|
-
- Console logs from the app
|
|
16
|
-
- A screenshot of the current state
|
|
17
|
-
- TypeScript/build errors if any
|
|
18
|
-
|
|
19
|
-
You must determine:
|
|
20
|
-
1. Does the feature work as described in the PRD?
|
|
21
|
-
2. Does the UI look polished and correct?
|
|
22
|
-
3. Are there any errors in the logs?
|
|
23
|
-
4. What bugs or issues need to be fixed?
|
|
24
|
-
|
|
25
|
-
Be specific about what's wrong and what needs to change.`;
|
|
10
|
+
const SYSTEM_QA = `You are a senior QA engineer for React Native / Expo apps.
|
|
11
|
+
Verify that implemented features work correctly. Be specific about actual issues.
|
|
12
|
+
If Expo is not running, evaluate purely on code quality and completeness.`;
|
|
26
13
|
|
|
27
14
|
export async function runQAAgent(task, devResult, projectPath, prdContent, expoPort = 8081) {
|
|
28
15
|
console.log(` 🔍 QA checking: ${task.title}`);
|
|
29
|
-
await addLog(task.id, "QA
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
16
|
+
await addLog(task.id, "QA starting", "progress");
|
|
17
|
+
|
|
18
|
+
// ── 1. TypeScript check ────────────────────────────────────────────────────
|
|
19
|
+
const tsResult = await runCommand("npx tsc --noEmit 2>&1", projectPath, 60000);
|
|
20
|
+
const tsErrors = tsResult.stdout.includes("error TS") ? tsResult.stdout : null;
|
|
21
|
+
if (tsErrors) await addLog(task.id, `TypeScript errors detected`, "error");
|
|
22
|
+
|
|
23
|
+
// ── 2. Check for truncated/incomplete files ────────────────────────────────
|
|
24
|
+
const truncationIssues = await checkForTruncatedFiles(task, projectPath);
|
|
25
|
+
if (truncationIssues.length > 0) {
|
|
26
|
+
const msg = `Incomplete files: ${truncationIssues.join(", ")}`;
|
|
27
|
+
await addLog(task.id, msg, "error");
|
|
28
|
+
return {
|
|
29
|
+
passed: false,
|
|
30
|
+
issues: truncationIssues,
|
|
31
|
+
fixInstructions: `These files are truncated/incomplete:\n${truncationIssues.join("\n")}\n\nRe-implement them completely from scratch.`,
|
|
32
|
+
screenshotPath: null,
|
|
33
|
+
};
|
|
43
34
|
}
|
|
44
35
|
|
|
45
|
-
//
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
// ── 3. Screenshot (only if Expo is running) ────────────────────────────────
|
|
37
|
+
let visualVerdict = null;
|
|
38
|
+
const expoRunning = await isPortOpen(expoPort);
|
|
39
|
+
|
|
40
|
+
if (expoRunning) {
|
|
41
|
+
await addLog(task.id, "Taking screenshot...", "progress");
|
|
42
|
+
const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
|
|
43
|
+
const screenshot = await screenshotExpoWeb(expoPort, screenshotDir);
|
|
44
|
+
if (screenshot.success && screenshot.base64) {
|
|
45
|
+
const visionResult = await callClaudeWithImage(
|
|
46
|
+
SYSTEM_QA,
|
|
47
|
+
`Task: ${task.title}\nExpected: ${task.description}\n\nDoes the UI look correct? Any visual bugs?`,
|
|
48
|
+
screenshot.base64
|
|
49
|
+
);
|
|
50
|
+
visualVerdict = visionResult.text;
|
|
51
|
+
await addLog(task.id, `Visual: ${visualVerdict}`, "progress");
|
|
52
|
+
} else {
|
|
53
|
+
await addLog(task.id, `Screenshot failed: ${screenshot.error}`, "progress");
|
|
52
54
|
}
|
|
53
|
-
} catch {}
|
|
54
|
-
|
|
55
|
-
// 3. Read Supabase error logs if project uses Supabase
|
|
56
|
-
let supabaseLogs = [];
|
|
57
|
-
try {
|
|
58
|
-
supabaseLogs = await readErrorLogs("logs", 10);
|
|
59
|
-
} catch {}
|
|
60
|
-
|
|
61
|
-
// 4. Take screenshot
|
|
62
|
-
await addLog(task.id, "Taking screenshot of Expo Web...", "progress");
|
|
63
|
-
const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
|
|
64
|
-
const screenshot = await screenshotExpoWeb(expoPort, screenshotDir);
|
|
65
|
-
|
|
66
|
-
if (screenshot.success) {
|
|
67
|
-
qaReport.screenshotPath = screenshot.imagePath;
|
|
68
|
-
await addLog(task.id, `Screenshot captured: ${path.basename(screenshot.imagePath)}`, "progress");
|
|
69
55
|
} else {
|
|
70
|
-
await addLog(task.id,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 5. Visual QA via Claude Vision
|
|
74
|
-
let visualVerdict = null;
|
|
75
|
-
if (screenshot.success && screenshot.base64) {
|
|
76
|
-
const visualPrompt = `
|
|
77
|
-
This is a screenshot of a React Native / Expo app screen.
|
|
78
|
-
|
|
79
|
-
Task that was just implemented: ${task.title}
|
|
80
|
-
Expected behavior from PRD: ${task.description}
|
|
81
|
-
|
|
82
|
-
Evaluate:
|
|
83
|
-
1. Does the screen look implemented? (not blank, not error screen)
|
|
84
|
-
2. Does the UI look polished? (proper layout, readable text, no obvious visual bugs)
|
|
85
|
-
3. Does it appear to match what was described in the task?
|
|
86
|
-
4. Any visual issues? (overlapping elements, cut off text, missing components)
|
|
87
|
-
|
|
88
|
-
Be concise and specific.`;
|
|
89
|
-
|
|
90
|
-
const visionResult = await callClaudeWithImage(SYSTEM_QA, visualPrompt, screenshot.base64);
|
|
91
|
-
visualVerdict = visionResult.text;
|
|
92
|
-
await addLog(task.id, `Visual QA: ${visualVerdict.slice(0, 200)}`, "progress");
|
|
56
|
+
await addLog(task.id, "Expo not running — code-only QA", "progress");
|
|
93
57
|
}
|
|
94
58
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const rel = path.relative(projectPath, f).toLowerCase();
|
|
99
|
-
return task.title.toLowerCase().split(" ").some((w) => w.length > 4 && rel.includes(w));
|
|
100
|
-
})
|
|
101
|
-
.slice(0, 5);
|
|
102
|
-
|
|
59
|
+
// ── 4. Read ALL relevant code — no artificial limits ──────────────────────
|
|
60
|
+
// Claude Code already wrote these files — we read them fully for QA review
|
|
61
|
+
const relevantFiles = getRelevantFiles(task, projectPath);
|
|
103
62
|
let codeContext = "";
|
|
104
63
|
for (const f of relevantFiles) {
|
|
105
64
|
const content = readFile(f);
|
|
106
65
|
if (content) {
|
|
107
|
-
|
|
66
|
+
// No slice — read the complete file
|
|
67
|
+
codeContext += `\n### ${path.relative(projectPath, f)}\n${content}\n`;
|
|
108
68
|
}
|
|
109
69
|
}
|
|
110
70
|
|
|
111
|
-
|
|
71
|
+
// ── 5. App logs (full) ─────────────────────────────────────────────────────
|
|
72
|
+
let appLogs = "";
|
|
73
|
+
try {
|
|
74
|
+
const logFile = path.join(projectPath, ".claudeboard-logs.txt");
|
|
75
|
+
if (fs.existsSync(logFile)) appLogs = fs.readFileSync(logFile, "utf8");
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
// ── 6. Supabase errors ─────────────────────────────────────────────────────
|
|
79
|
+
let supabaseLogs = [];
|
|
80
|
+
try { supabaseLogs = await readErrorLogs("logs", 20); } catch {}
|
|
81
|
+
|
|
82
|
+
// ── 7. Functional verdict ─────────────────────────────────────────────────
|
|
83
|
+
const verdict = await callClaudeJSON(
|
|
112
84
|
SYSTEM_QA,
|
|
113
|
-
`
|
|
114
|
-
Task: ${task.title}
|
|
85
|
+
`Task: ${task.title}
|
|
115
86
|
Description: ${task.description}
|
|
116
87
|
|
|
117
|
-
|
|
88
|
+
Implemented code (complete files):
|
|
118
89
|
${codeContext || "Could not read relevant files"}
|
|
119
90
|
|
|
120
|
-
|
|
121
|
-
${appLogs || "
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
${supabaseLogs.length > 0 ? JSON.stringify(supabaseLogs) : "None"}
|
|
91
|
+
TypeScript: ${tsErrors ? `ERRORS:\n${tsErrors}` : "Clean — no errors"}
|
|
92
|
+
App logs: ${appLogs || "None"}
|
|
93
|
+
Supabase errors: ${supabaseLogs.length > 0 ? JSON.stringify(supabaseLogs, null, 2) : "None"}
|
|
94
|
+
Visual QA: ${visualVerdict || "No screenshot (Expo not running — evaluate code only)"}
|
|
125
95
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
96
|
+
EVALUATION RULES:
|
|
97
|
+
- PASS if: code is complete, TypeScript is clean, implementation matches description
|
|
98
|
+
- FAIL only if: code is incomplete/truncated, has TS errors, or implementation is clearly wrong
|
|
99
|
+
- Do NOT fail just because Expo isn't running
|
|
100
|
+
- Do NOT fail for minor style preferences
|
|
131
101
|
|
|
132
102
|
Respond with JSON:
|
|
133
103
|
{
|
|
@@ -135,74 +105,125 @@ Respond with JSON:
|
|
|
135
105
|
"confidence": 0-100,
|
|
136
106
|
"issues": ["specific issue 1", "specific issue 2"],
|
|
137
107
|
"summary": "One sentence verdict",
|
|
138
|
-
"fixInstructions": "
|
|
139
|
-
}
|
|
140
|
-
{ maxTokens: 2000 }
|
|
108
|
+
"fixInstructions": "Specific fix instructions if failed, null if passed"
|
|
109
|
+
}`
|
|
141
110
|
);
|
|
142
111
|
|
|
143
|
-
qaReport
|
|
144
|
-
|
|
145
|
-
|
|
112
|
+
const qaReport = {
|
|
113
|
+
passed: verdict.passed,
|
|
114
|
+
issues: verdict.issues || [],
|
|
115
|
+
fixInstructions: verdict.fixInstructions,
|
|
116
|
+
screenshotPath: null,
|
|
117
|
+
};
|
|
146
118
|
|
|
147
119
|
if (qaReport.passed) {
|
|
148
|
-
await addLog(task.id, `✓ QA passed (${
|
|
120
|
+
await addLog(task.id, `✓ QA passed (${verdict.confidence}%): ${verdict.summary}`, "complete");
|
|
149
121
|
console.log(` ✓ QA passed: ${task.title}`);
|
|
150
122
|
} else {
|
|
151
|
-
await addLog(
|
|
152
|
-
|
|
153
|
-
`✗ QA failed: ${functionalVerdict.summary}. Issues: ${qaReport.issues.slice(0, 2).join("; ")}`,
|
|
154
|
-
"error"
|
|
155
|
-
);
|
|
156
|
-
console.log(` ✗ QA failed: ${task.title}`);
|
|
157
|
-
console.log(` Issues: ${qaReport.issues.join(", ")}`);
|
|
123
|
+
await addLog(task.id, `✗ QA failed: ${verdict.summary}`, "error");
|
|
124
|
+
console.log(` ✗ QA failed: ${task.title} — ${verdict.issues?.slice(0,2).join(", ")}`);
|
|
158
125
|
}
|
|
159
126
|
|
|
160
127
|
return qaReport;
|
|
161
128
|
}
|
|
162
129
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
130
|
+
// ── Detect truncated/incomplete files ──────────────────────────────────────────
|
|
131
|
+
async function checkForTruncatedFiles(task, projectPath) {
|
|
132
|
+
const issues = [];
|
|
133
|
+
const keywords = task.title.toLowerCase().split(" ").filter(w => w.length > 4);
|
|
134
|
+
|
|
135
|
+
const files = listFiles(projectPath, [".ts", ".tsx"])
|
|
136
|
+
.filter(f => {
|
|
137
|
+
const rel = path.relative(projectPath, f).toLowerCase();
|
|
138
|
+
if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
|
|
139
|
+
return keywords.some(kw => rel.includes(kw));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
for (const f of files) {
|
|
143
|
+
const content = readFile(f);
|
|
144
|
+
if (!content) continue;
|
|
145
|
+
const rel = path.relative(projectPath, f);
|
|
146
|
+
const lastLines = content.split("\n").slice(-5).join("\n").trim();
|
|
147
|
+
const openBraces = (content.match(/\{/g) || []).length;
|
|
148
|
+
const closeBraces = (content.match(/\}/g) || []).length;
|
|
149
|
+
|
|
150
|
+
const truncated =
|
|
151
|
+
lastLines.endsWith("{") || lastLines.endsWith("(") || lastLines.endsWith(",") ||
|
|
152
|
+
lastLines.match(/^(import|\/\/) *$/) ||
|
|
153
|
+
openBraces > closeBraces + 3 ||
|
|
154
|
+
(content.length < 80 && (rel.includes("store") || rel.includes("hook") || rel.includes("screen")));
|
|
155
|
+
|
|
156
|
+
if (truncated) {
|
|
157
|
+
issues.push(`${rel} (ends: "${lastLines.slice(-80)}")`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return issues;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Full app QA ────────────────────────────────────────────────────────────────
|
|
167
164
|
export async function runFullAppQA(projectPath, prdContent, expoPort = 8081) {
|
|
168
165
|
console.log("\n 🔍 Running full app QA...");
|
|
169
166
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
if (!await isPortOpen(expoPort)) {
|
|
168
|
+
console.log(" ⚠️ Expo not running — skipping visual QA");
|
|
169
|
+
return { passed: true, routes: [], screenshotsCaptures: 0, issues: [] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const appDir = path.join(projectPath, "app");
|
|
173
|
+
if (!fs.existsSync(appDir)) return { passed: true, routes: [], screenshotsCaptures: 0, issues: [] };
|
|
174
|
+
|
|
175
|
+
const routeFiles = listFiles(appDir, [".tsx", ".ts"])
|
|
176
|
+
.filter(f => !f.includes("_layout") && !f.includes("_error"))
|
|
177
|
+
.map(f => "/" + path.relative(appDir, f).replace(/\.(tsx|ts)$/, "").replace(/index$/, ""));
|
|
177
178
|
|
|
178
179
|
const screenshotDir = path.join(projectPath, ".claudeboard-screenshots", "full-qa");
|
|
179
180
|
const screenshots = [];
|
|
180
181
|
|
|
181
|
-
for (const route of routeFiles
|
|
182
|
+
for (const route of routeFiles) {
|
|
182
183
|
const shot = await screenshotExpoWeb(expoPort, screenshotDir, route);
|
|
183
184
|
if (shot.success) screenshots.push({ route, ...shot });
|
|
184
|
-
await new Promise(
|
|
185
|
+
await new Promise(r => setTimeout(r, 800));
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
const report = {
|
|
189
|
-
passed: screenshots.length > 0,
|
|
190
|
-
routes: routeFiles,
|
|
191
|
-
screenshotsCaptures: screenshots.length,
|
|
192
|
-
issues: [],
|
|
193
|
-
};
|
|
188
|
+
const report = { passed: true, routes: routeFiles, screenshotsCaptures: screenshots.length, issues: [] };
|
|
194
189
|
|
|
195
190
|
for (const shot of screenshots) {
|
|
196
191
|
if (!shot.base64) continue;
|
|
197
192
|
const verdict = await callClaudeWithImage(
|
|
198
193
|
SYSTEM_QA,
|
|
199
|
-
`Route: ${shot.route}\n\
|
|
194
|
+
`Route: ${shot.route}\n\nFull PRD:\n${prdContent}\n\nAny obvious issues?`,
|
|
200
195
|
shot.base64
|
|
201
196
|
);
|
|
202
197
|
if (verdict.text.toLowerCase().includes("issue") || verdict.text.toLowerCase().includes("problem")) {
|
|
203
|
-
report.issues.push({ route: shot.route, note: verdict.text
|
|
198
|
+
report.issues.push({ route: shot.route, note: verdict.text });
|
|
204
199
|
}
|
|
205
200
|
}
|
|
206
201
|
|
|
207
202
|
return report;
|
|
208
203
|
}
|
|
204
|
+
|
|
205
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
206
|
+
function getRelevantFiles(task, projectPath) {
|
|
207
|
+
const keywords = task.title.toLowerCase().split(" ").filter(w => w.length > 4);
|
|
208
|
+
return listFiles(projectPath, [".ts", ".tsx"])
|
|
209
|
+
.filter(f => {
|
|
210
|
+
const rel = path.relative(projectPath, f).toLowerCase();
|
|
211
|
+
if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
|
|
212
|
+
return keywords.some(kw => rel.includes(kw));
|
|
213
|
+
});
|
|
214
|
+
// No .slice() — return all matching files
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function isPortOpen(port) {
|
|
218
|
+
try {
|
|
219
|
+
const { default: net } = await import("net");
|
|
220
|
+
return new Promise(resolve => {
|
|
221
|
+
const sock = new net.Socket();
|
|
222
|
+
sock.setTimeout(800);
|
|
223
|
+
sock.once("connect", () => { sock.destroy(); resolve(true); });
|
|
224
|
+
sock.once("error", () => resolve(false));
|
|
225
|
+
sock.once("timeout", () => resolve(false));
|
|
226
|
+
sock.connect(port, "127.0.0.1");
|
|
227
|
+
});
|
|
228
|
+
} catch { return false; }
|
|
229
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -166,11 +166,21 @@ program
|
|
|
166
166
|
process.exit(1);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
const resolvedProject = path.resolve(opts.project);
|
|
170
|
+
|
|
171
|
+
// Persist the project path so `claudeboard start` uses the right directory for Expo
|
|
172
|
+
const configPath = path.join(process.cwd(), ".claudeboard.json");
|
|
173
|
+
try {
|
|
174
|
+
const saved = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
175
|
+
saved.projectDir = resolvedProject;
|
|
176
|
+
fs.writeFileSync(configPath, JSON.stringify(saved, null, 2));
|
|
177
|
+
} catch {}
|
|
178
|
+
|
|
169
179
|
const { runOrchestrator } = await import("../agents/orchestrator.js");
|
|
170
180
|
|
|
171
181
|
await runOrchestrator({
|
|
172
182
|
prdPath: path.resolve(opts.prd),
|
|
173
|
-
projectPath:
|
|
183
|
+
projectPath: resolvedProject,
|
|
174
184
|
supabaseUrl: config.supabaseUrl,
|
|
175
185
|
supabaseKey: config.supabaseKey,
|
|
176
186
|
projectName: config.projectName,
|
package/dashboard/index.html
CHANGED
|
@@ -929,7 +929,14 @@
|
|
|
929
929
|
<button class="stab" id="tab-detail" onclick="switchTab('detail')">Detail</button>
|
|
930
930
|
</div>
|
|
931
931
|
<div class="sidebar-body" id="sidebarBody">
|
|
932
|
-
|
|
932
|
+
<!-- Activity pane — always in DOM, shown/hidden -->
|
|
933
|
+
<div id="activityPane">
|
|
934
|
+
<div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>
|
|
935
|
+
</div>
|
|
936
|
+
<!-- Detail pane — always in DOM, shown/hidden -->
|
|
937
|
+
<div id="detailPane" style="display:none">
|
|
938
|
+
<div class="detail-empty">Click any task card<br>to see its details.</div>
|
|
939
|
+
</div>
|
|
933
940
|
</div>
|
|
934
941
|
</div>
|
|
935
942
|
|
|
@@ -1102,6 +1109,7 @@ function connectWS() {
|
|
|
1102
1109
|
if (event === 'log') {
|
|
1103
1110
|
board.logs.unshift(data);
|
|
1104
1111
|
if (activeTab === 'activity') renderLogs();
|
|
1112
|
+
// If on detail tab, don't touch activityPane — it'll refresh next time they switch
|
|
1105
1113
|
}
|
|
1106
1114
|
if (event === 'expo_status') {
|
|
1107
1115
|
setExpoStatus(data.status, data.url);
|
|
@@ -1265,10 +1273,6 @@ async function onDrop(e, status) {
|
|
|
1265
1273
|
|
|
1266
1274
|
// ── CARD SELECT ───────────────────────────────────────────────────────────────
|
|
1267
1275
|
async function selectCard(id) {
|
|
1268
|
-
activeTab = 'detail';
|
|
1269
|
-
document.getElementById('tab-activity').className = 'stab';
|
|
1270
|
-
document.getElementById('tab-detail').className = 'stab active';
|
|
1271
|
-
|
|
1272
1276
|
const task = allTasks().find(t => t.id === id);
|
|
1273
1277
|
if (!task) return;
|
|
1274
1278
|
selectedTask = task;
|
|
@@ -1276,8 +1280,8 @@ async function selectCard(id) {
|
|
|
1276
1280
|
const res = await fetch(`/api/tasks/${id}/logs`);
|
|
1277
1281
|
const { logs } = await res.json();
|
|
1278
1282
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1283
|
+
// Write into the dedicated detail pane (never touches activityPane)
|
|
1284
|
+
document.getElementById('detailPane').innerHTML = `
|
|
1281
1285
|
<div>
|
|
1282
1286
|
<div class="detail-title">${esc(task.title)}</div>
|
|
1283
1287
|
<div class="detail-tags">
|
|
@@ -1295,16 +1299,23 @@ async function selectCard(id) {
|
|
|
1295
1299
|
}).join('')
|
|
1296
1300
|
}
|
|
1297
1301
|
</div>`;
|
|
1302
|
+
|
|
1303
|
+
// Switch to detail tab
|
|
1304
|
+
activeTab = 'detail';
|
|
1305
|
+
document.getElementById('tab-activity').className = 'stab';
|
|
1306
|
+
document.getElementById('tab-detail').className = 'stab active';
|
|
1307
|
+
document.getElementById('activityPane').style.display = 'none';
|
|
1308
|
+
document.getElementById('detailPane').style.display = 'block';
|
|
1298
1309
|
}
|
|
1299
1310
|
|
|
1300
1311
|
// ── LOGS ──────────────────────────────────────────────────────────────────────
|
|
1301
1312
|
function renderLogs() {
|
|
1302
|
-
|
|
1313
|
+
// Only update the activity pane — never overwrites detail pane
|
|
1314
|
+
const el = document.getElementById('activityPane');
|
|
1303
1315
|
if (!board.logs?.length) {
|
|
1304
1316
|
el.innerHTML = `<div class="detail-empty">Waiting for activity...<br><br>Agents will log<br>their work here.</div>`;
|
|
1305
1317
|
return;
|
|
1306
1318
|
}
|
|
1307
|
-
|
|
1308
1319
|
const icons = { start: '▶', complete: '✓', error: '✕', progress: '·', info: '·' };
|
|
1309
1320
|
el.innerHTML = board.logs.map(l => {
|
|
1310
1321
|
const t = new Date(l.created_at).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
@@ -1325,6 +1336,9 @@ function switchTab(tab) {
|
|
|
1325
1336
|
activeTab = tab;
|
|
1326
1337
|
document.getElementById('tab-activity').className = 'stab' + (tab === 'activity' ? ' active' : '');
|
|
1327
1338
|
document.getElementById('tab-detail').className = 'stab' + (tab === 'detail' ? ' active' : '');
|
|
1339
|
+
// Show/hide panes — never overwrite the other pane's content
|
|
1340
|
+
document.getElementById('activityPane').style.display = tab === 'activity' ? 'block' : 'none';
|
|
1341
|
+
document.getElementById('detailPane').style.display = tab === 'detail' ? 'block' : 'none';
|
|
1328
1342
|
if (tab === 'activity') renderLogs();
|
|
1329
1343
|
}
|
|
1330
1344
|
|