input-kanban 0.0.9 → 0.0.12
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/ENVIRONMENT.md +8 -4
- package/PROJECT_GUIDE.md +11 -9
- package/README.en.md +19 -13
- package/README.md +19 -13
- package/RELEASE_NOTES.md +63 -0
- package/bin/input-kanban.js +43 -40
- package/package.json +2 -2
- package/public/index.html +126 -93
- package/src/orchestrator.js +167 -28
- package/src/scheduler.js +40 -0
- package/src/server.js +8 -5
- package/src/utils.js +4 -2
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "input-kanban",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"input-kanban": "bin/input-kanban.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node bin/input-kanban.js",
|
|
10
|
-
"check": "node --check bin/input-kanban.js && node --check bin/input-kanban-format-events.js && node --check bin/input-kanban-timestamp-events.js && node --check bin/input-kanban-tmux-overview.js && node --check src/server.js && node --check src/orchestrator.js && node --check src/appServerClient.js && node --check src/utils.js && node --check src/eventFormatter.js && node --check src/runners/index.js && node --check src/runners/headlessRunner.js && node --check src/runners/tmuxRunner.js && node --check src/runners/tmuxUtils.js && node --check src/tmux.js && node --test"
|
|
10
|
+
"check": "node --check bin/input-kanban.js && node --check bin/input-kanban-format-events.js && node --check bin/input-kanban-timestamp-events.js && node --check bin/input-kanban-tmux-overview.js && node --check src/server.js && node --check src/scheduler.js && node --check src/orchestrator.js && node --check src/appServerClient.js && node --check src/utils.js && node --check src/eventFormatter.js && node --check src/runners/index.js && node --check src/runners/headlessRunner.js && node --check src/runners/tmuxRunner.js && node --check src/runners/tmuxUtils.js && node --check src/tmux.js && node --test"
|
|
11
11
|
},
|
|
12
12
|
"description": "A local Codex orchestration kanban dashboard",
|
|
13
13
|
"license": "MIT",
|
package/public/index.html
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
.sidebar { position: sticky; top: 18px; height: calc(100vh - 36px); }
|
|
23
23
|
.sidebar section { height: 100%; display: flex; flex-direction: column; box-sizing: border-box; }
|
|
24
24
|
section { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 16px; box-shadow: 0 8px 24px rgba(0,0,0,.18); }
|
|
25
|
-
textarea, input { width: 100%; box-sizing: border-box; background: #020617; color: var(--text); border: 1px solid #475569; border-radius: 9px; padding: 9px 10px; outline: none; }
|
|
26
|
-
textarea:focus, input:focus { border-color: #60a5fa; box-shadow: 0 0 0 2px rgba(37,99,235,.25); }
|
|
25
|
+
textarea, input, select { width: 100%; box-sizing: border-box; background: #020617; color: var(--text); border: 1px solid #475569; border-radius: 9px; padding: 9px 10px; outline: none; }
|
|
26
|
+
textarea:focus, input:focus, select:focus { border-color: #60a5fa; box-shadow: 0 0 0 2px rgba(37,99,235,.25); }
|
|
27
27
|
textarea { min-height: 240px; }
|
|
28
28
|
label { display: block; margin-top: 10px; color: #cbd5e1; font-weight: 700; }
|
|
29
29
|
button { background: var(--blue); color: white; border: 0; border-radius: 9px; padding: 8px 11px; margin: 4px 4px 4px 0; cursor: pointer; font-weight: 700; }
|
|
@@ -60,8 +60,14 @@
|
|
|
60
60
|
.muted { color: var(--muted); font-size: 12px; }
|
|
61
61
|
.hidden { display: none; }
|
|
62
62
|
.toolbar { margin: 8px 0 12px; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
|
|
63
|
+
.section-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 10px 0 8px; }
|
|
64
|
+
.section-header h2 { margin: 0; }
|
|
65
|
+
.workspace-filter-panel { margin: 4px 0 10px; display: flex; align-items: center; gap: 6px; }
|
|
66
|
+
.workspace-filter-select { flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 7px 9px; color: #cbd5e1; }
|
|
63
67
|
.task-text { max-height: 180px; color: #cbd5e1; }
|
|
64
68
|
.empty { color: var(--muted); padding: 18px 0; }
|
|
69
|
+
.info-icon, .runs-load-icon { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); font-size: 10px; cursor: help; opacity: .72; }
|
|
70
|
+
.info-icon:hover, .runs-load-icon:hover { opacity: 1; color: #cbd5e1; border-color: var(--line-strong); }
|
|
65
71
|
.run-list { display: flex; flex-direction: column; gap: 10px; flex: 1; min-height: 0; overflow-y: auto; padding-right: 4px; }
|
|
66
72
|
.run-list-more { width: 100%; margin-top: 4px; }
|
|
67
73
|
.run-card { border: 1px solid var(--line); border-radius: 12px; padding: 12px; background: var(--panel-2); cursor: pointer; transition: border-color .15s, transform .15s, background .15s; }
|
|
@@ -127,27 +133,30 @@
|
|
|
127
133
|
<main>
|
|
128
134
|
<div class="sidebar">
|
|
129
135
|
<section>
|
|
130
|
-
<div class="
|
|
131
|
-
<
|
|
132
|
-
<button class="secondary" onclick="
|
|
136
|
+
<div class="section-header">
|
|
137
|
+
<h2>任务批次</h2>
|
|
138
|
+
<button class="secondary" onclick="showCreateForm()">新建</button>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="workspace-filter-panel">
|
|
141
|
+
<select id="workspaceFilterSelect" class="workspace-filter-select" onchange="setWorkspaceFilter(this.value)" title="未筛选工作区"></select>
|
|
142
|
+
<span id="runsLoadHint" class="runs-load-icon" title="批次列表尚未加载" aria-label="批次列表尚未加载">ⓘ</span>
|
|
133
143
|
</div>
|
|
134
|
-
<
|
|
135
|
-
<div id="runs" class="run-list"></div>
|
|
144
|
+
<div id="runs" class="run-list"><div class="empty">批次列表加载中...</div></div>
|
|
136
145
|
</section>
|
|
137
146
|
</div>
|
|
138
147
|
<div>
|
|
139
148
|
<section id="createPanel" class="hidden">
|
|
140
149
|
<h2>新建任务批次</h2>
|
|
141
150
|
<label>批次名称</label><input id="label" value="codex-task" />
|
|
142
|
-
<label
|
|
151
|
+
<label>工作区</label><input id="repo" />
|
|
143
152
|
<label>运行目录</label><input id="runsDir" readonly />
|
|
144
153
|
<label>最大并发数</label><input id="maxParallel" type="number" value="3" min="1" max="16" />
|
|
145
154
|
<label>Worker 沙箱</label><select id="workerSandbox">
|
|
146
|
-
<option value="workspace-write" selected>workspace-write
|
|
155
|
+
<option value="workspace-write" selected>workspace-write(默认,允许写当前工作区)</option>
|
|
147
156
|
<option value="read-only">read-only(只读)</option>
|
|
148
157
|
<option value="danger-full-access">danger-full-access(高风险,跳过沙箱限制)</option>
|
|
149
158
|
</select>
|
|
150
|
-
<div class="muted">仅影响 worker;任务拆分和汇总验收仍保持 read-only。</div>
|
|
159
|
+
<div class="muted">仅影响 worker;任务拆分和汇总验收仍保持 read-only。若执行过程提示 Permission denied / sandbox denied,这通常不是任务本身失败,而是当前沙箱能力不足;在可信工作区可改用 danger-full-access。DNS / 网络失败则通常需要检查代理、VPN 或本地 evidence。</div>
|
|
151
160
|
<label>任务说明</label><textarea id="taskText" placeholder="粘贴任务说明"></textarea>
|
|
152
161
|
<div class="toolbar">
|
|
153
162
|
<button onclick="createRun()">创建批次</button>
|
|
@@ -176,7 +185,10 @@
|
|
|
176
185
|
</section>
|
|
177
186
|
|
|
178
187
|
<section id="filePanel" class="log-panel">
|
|
179
|
-
<
|
|
188
|
+
<div class="section-header">
|
|
189
|
+
<h2>任务详情</h2>
|
|
190
|
+
<span class="info-icon" title="若执行过程提示 Permission denied / sandbox denied,这通常不是任务本身失败,而是当前 worker 沙箱能力不足;在可信工作区可改用 danger-full-access。DNS / 网络失败则通常需要检查代理、VPN 或本地 evidence。" aria-label="任务详情权限与网络提示">ⓘ</span>
|
|
191
|
+
</div>
|
|
180
192
|
<div id="fileTitle" class="muted">点击任务后查看详情</div>
|
|
181
193
|
<div id="fileTabs" class="toolbar file-tabs"></div>
|
|
182
194
|
<div id="executionSummary" class="execution-summary hidden"></div>
|
|
@@ -207,18 +219,19 @@ let selectedTask = null;
|
|
|
207
219
|
let selectedFileName = null;
|
|
208
220
|
let manualCompleteTaskId = null;
|
|
209
221
|
let pendingArchiveRunId = null;
|
|
210
|
-
const autoDispatchingRuns = new Set();
|
|
211
|
-
const autoJudgingRuns = new Set();
|
|
212
|
-
const autoRetryingRuns = new Set();
|
|
213
|
-
const autoRetrySkippedRuns = new Set();
|
|
214
222
|
let currentState = null;
|
|
215
223
|
let lastAutoRefreshAt = null;
|
|
216
224
|
let runListVisibleCount = 10;
|
|
217
225
|
let latestRuns = [];
|
|
226
|
+
let workspaceCatalogRuns = [];
|
|
218
227
|
const statusByRunId = new Map();
|
|
219
228
|
const AUTO_REFRESH_MS = 3000;
|
|
220
|
-
const AUTO_MAX_RETRIES = 1;
|
|
221
229
|
const RUN_LIST_PAGE_SIZE = 10;
|
|
230
|
+
const WORKSPACE_FILTER_ALL = '';
|
|
231
|
+
const WORKER_SANDBOX_STORAGE_KEY = 'input-kanban.workerSandbox';
|
|
232
|
+
const VALID_WORKER_SANDBOXES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
|
|
233
|
+
let currentWorkspacePath = '';
|
|
234
|
+
let selectedWorkspaceFilter = localStorage.getItem('input-kanban.workspaceFilter') || WORKSPACE_FILTER_ALL;
|
|
222
235
|
|
|
223
236
|
async function api(path, opts={}) {
|
|
224
237
|
const res = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...opts });
|
|
@@ -291,6 +304,7 @@ function basenamePath(value) {
|
|
|
291
304
|
function metaChip(label, value, { title = value, danger = false, long = false, extra = '' } = {}) {
|
|
292
305
|
return `<span class="meta-chip ${danger ? 'danger' : ''} ${long ? 'long' : ''}" title="${esc(title)}"><span class="meta-label">${esc(label)}</span><span class="meta-value">${esc(value)}</span>${extra}</span>`;
|
|
293
306
|
}
|
|
307
|
+
function gitChip() { return '<span class="meta-chip" title="Git 工作区"><span class="meta-value">Git</span></span>'; }
|
|
294
308
|
function editIcon() {
|
|
295
309
|
return '<svg class="icon-svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true"><path d="M4 16.5V20h3.5L18.1 9.4l-3.5-3.5L4 16.5Z" fill="currentColor"/><path d="m16 4.5 3.5 3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
|
296
310
|
}
|
|
@@ -335,9 +349,12 @@ function runListHasTmuxMetadata(run) {
|
|
|
335
349
|
|
|
336
350
|
async function loadHealth() {
|
|
337
351
|
const h = await api('/api/health');
|
|
338
|
-
|
|
352
|
+
currentWorkspacePath = h.defaultWorkspace || h.defaultRepo || '';
|
|
353
|
+
document.getElementById('repo').value = currentWorkspacePath;
|
|
339
354
|
document.getElementById('runsDir').value = h.runsDir;
|
|
340
355
|
document.getElementById('pageFooter').textContent = h.version ? `版本:v${h.version}` : '版本:未知(请重启服务)';
|
|
356
|
+
renderWorkspaceFilterOptions();
|
|
357
|
+
updateWorkspaceFilterTitle();
|
|
341
358
|
}
|
|
342
359
|
function showCreateForm() {
|
|
343
360
|
selectedRun = null; selectedTask = null; selectedFileName = null; currentState = null;
|
|
@@ -351,8 +368,21 @@ function hideCreateForm() {
|
|
|
351
368
|
document.getElementById('detailPanel').classList.remove('hidden');
|
|
352
369
|
document.getElementById('filePanel').classList.remove('hidden');
|
|
353
370
|
}
|
|
371
|
+
function saveWorkerSandboxPreference() {
|
|
372
|
+
const select = document.getElementById('workerSandbox');
|
|
373
|
+
const value = select?.value || '';
|
|
374
|
+
if (VALID_WORKER_SANDBOXES.has(value)) localStorage.setItem(WORKER_SANDBOX_STORAGE_KEY, value);
|
|
375
|
+
}
|
|
376
|
+
function initializeWorkerSandboxPreference() {
|
|
377
|
+
const select = document.getElementById('workerSandbox');
|
|
378
|
+
if (!select) return;
|
|
379
|
+
const saved = localStorage.getItem(WORKER_SANDBOX_STORAGE_KEY);
|
|
380
|
+
if (VALID_WORKER_SANDBOXES.has(saved)) select.value = saved;
|
|
381
|
+
select.addEventListener('change', saveWorkerSandboxPreference);
|
|
382
|
+
}
|
|
354
383
|
async function createRun() {
|
|
355
|
-
|
|
384
|
+
saveWorkerSandboxPreference();
|
|
385
|
+
const body = { label: label.value, workspace: repo.value, repo: repo.value, maxParallel: maxParallel.value, workerSandbox: workerSandbox.value, taskText: taskText.value };
|
|
356
386
|
const r = await api('/api/runs', { method: 'POST', body: JSON.stringify(body) });
|
|
357
387
|
selectedRun = r.runId; selectedTask = null; selectedFileName = null;
|
|
358
388
|
clearFileView();
|
|
@@ -362,10 +392,28 @@ async function createRun() {
|
|
|
362
392
|
await refreshSelected();
|
|
363
393
|
}
|
|
364
394
|
async function refreshRuns() {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
395
|
+
const startedAt = performance.now();
|
|
396
|
+
const hint = document.getElementById('runsLoadHint');
|
|
397
|
+
if (hint) { hint.title = '批次列表加载中...'; hint.setAttribute('aria-label', hint.title); }
|
|
398
|
+
if (!latestRuns.length) document.getElementById('runs').innerHTML = '<div class="empty">批次列表加载中...</div>';
|
|
399
|
+
try {
|
|
400
|
+
const params = new URLSearchParams();
|
|
401
|
+
if (selectedWorkspaceFilter && selectedWorkspaceFilter !== WORKSPACE_FILTER_ALL) params.set('workspace', selectedWorkspaceFilter);
|
|
402
|
+
const [filteredData, catalogData] = await Promise.all([
|
|
403
|
+
api(`/api/runs${params.toString() ? `?${params.toString()}` : ''}`),
|
|
404
|
+
api('/api/runs?includeArchived=1')
|
|
405
|
+
]);
|
|
406
|
+
latestRuns = filteredData.runs || [];
|
|
407
|
+
workspaceCatalogRuns = catalogData.runs || [];
|
|
408
|
+
renderWorkspaceFilterOptions();
|
|
409
|
+
renderRunList();
|
|
410
|
+
updateWorkspaceFilterTitle();
|
|
411
|
+
if (hint) { hint.title = `加载 ${Math.round(performance.now() - startedAt)}ms|显示 ${latestRuns.length} 个批次`; hint.setAttribute('aria-label', hint.title); }
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error('批次列表加载失败', error);
|
|
414
|
+
if (hint) { hint.title = `批次列表加载失败|${errorDetail(error)}`; hint.setAttribute('aria-label', hint.title); }
|
|
415
|
+
if (!latestRuns.length) document.getElementById('runs').innerHTML = '<div class="empty">批次列表加载失败</div>';
|
|
416
|
+
}
|
|
369
417
|
}
|
|
370
418
|
function renderRunList() {
|
|
371
419
|
const visibleRuns = latestRuns.slice(0, runListVisibleCount);
|
|
@@ -373,7 +421,8 @@ function renderRunList() {
|
|
|
373
421
|
<div class="run-card ${selectedRun === r.runId ? 'active' : ''}" onclick="selectRun('${r.runId}')" onmouseleave="clearArchiveConfirm('${r.runId}')">
|
|
374
422
|
<div class="run-card-title"><span class="run-card-name-wrap"><span class="run-card-name" title="${esc(r.label)}">${esc(r.label)}</span><span class="run-card-title-actions"><button class="secondary copy-btn rename-btn" title="修改任务批次名称" onclick="renameRunLabel(event, '${r.runId}')">${editIcon()}</button><button class="secondary copy-btn rename-btn ${pendingArchiveRunId === r.runId ? 'archive-confirm-btn' : ''}" title="${pendingArchiveRunId === r.runId ? '再次点击确认归档' : '归档任务批次(运行中请先停止)'}" onclick="archiveRunFromCard(event, '${r.runId}')">${pendingArchiveRunId === r.runId ? '确认' : archiveIcon()}</button></span></span><span>${pill(r.status)}</span></div>
|
|
375
423
|
<div class="run-card-meta">
|
|
376
|
-
${metaChip('
|
|
424
|
+
${metaChip('工作区', basenamePath(r.workspacePath || r.repo), { title: r.workspacePath || r.repo, long: true, extra: `<button class="secondary copy-btn" title="复制工作区地址" onclick="copyRunRepoPath(event, '${r.runId}')">⧉</button>` })}
|
|
425
|
+
${r.git?.isGit ? gitChip() : ''}
|
|
377
426
|
${metaChip('创建', formatDateTime(r.createdAt))}
|
|
378
427
|
${metaChip('用时', runCardDurationText(r))}
|
|
379
428
|
${metaChip('进度', `${r.completed}/${r.total}`)}
|
|
@@ -390,8 +439,48 @@ function showMoreRuns() {
|
|
|
390
439
|
runListVisibleCount += RUN_LIST_PAGE_SIZE;
|
|
391
440
|
renderRunList();
|
|
392
441
|
}
|
|
442
|
+
function renderWorkspaceFilterOptions() {
|
|
443
|
+
const select = document.getElementById('workspaceFilterSelect');
|
|
444
|
+
if (!select) return;
|
|
445
|
+
const workspaces = new Map();
|
|
446
|
+
for (const run of workspaceCatalogRuns) {
|
|
447
|
+
const workspacePath = run.workspacePath || run.repo || '';
|
|
448
|
+
if (!workspacePath) continue;
|
|
449
|
+
const item = workspaces.get(workspacePath) || { label: basenamePath(workspacePath), count: 0, git: !!run.git?.isGit };
|
|
450
|
+
item.count += 1;
|
|
451
|
+
item.git = item.git || !!run.git?.isGit;
|
|
452
|
+
workspaces.set(workspacePath, item);
|
|
453
|
+
}
|
|
454
|
+
const currentValue = workspaces.has(selectedWorkspaceFilter) ? selectedWorkspaceFilter : WORKSPACE_FILTER_ALL;
|
|
455
|
+
if (currentValue !== selectedWorkspaceFilter) {
|
|
456
|
+
selectedWorkspaceFilter = currentValue;
|
|
457
|
+
localStorage.setItem('input-kanban.workspaceFilter', selectedWorkspaceFilter);
|
|
458
|
+
}
|
|
459
|
+
const options = [[WORKSPACE_FILTER_ALL, '工作区筛选'], ...[...workspaces.entries()].map(([workspacePath, item]) => {
|
|
460
|
+
const gitText = item.git ? ' · Git' : '';
|
|
461
|
+
return [workspacePath, `${item.label}${gitText} (${item.count})`];
|
|
462
|
+
})];
|
|
463
|
+
select.innerHTML = options.map(([value, label]) => `<option value="${esc(value)}">${esc(label)}</option>`).join('');
|
|
464
|
+
select.value = selectedWorkspaceFilter;
|
|
465
|
+
updateWorkspaceFilterTitle();
|
|
466
|
+
}
|
|
467
|
+
function updateWorkspaceFilterTitle() {
|
|
468
|
+
const select = document.getElementById('workspaceFilterSelect');
|
|
469
|
+
if (!select) return;
|
|
470
|
+
if (selectedWorkspaceFilter === WORKSPACE_FILTER_ALL) {
|
|
471
|
+
select.title = currentWorkspacePath ? `未筛选工作区|默认工作区:${currentWorkspacePath}` : '未筛选工作区';
|
|
472
|
+
} else {
|
|
473
|
+
select.title = `当前筛选:${selectedWorkspaceFilter}`;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function setWorkspaceFilter(value) {
|
|
477
|
+
selectedWorkspaceFilter = String(value || '').trim();
|
|
478
|
+
localStorage.setItem('input-kanban.workspaceFilter', selectedWorkspaceFilter);
|
|
479
|
+
runListVisibleCount = RUN_LIST_PAGE_SIZE;
|
|
480
|
+
refreshRuns().catch(console.error);
|
|
481
|
+
}
|
|
393
482
|
async function selectRun(id) { selectedRun = id; selectedTask = null; selectedFileName = null; clearFileView(); hideCreateForm(); await refreshSelected(); }
|
|
394
|
-
async function refreshSelected({auto=false
|
|
483
|
+
async function refreshSelected({auto=false} = {}) {
|
|
395
484
|
if (!selectedRun) return;
|
|
396
485
|
currentState = await api(`/api/runs/${selectedRun}/status`);
|
|
397
486
|
statusByRunId.set(selectedRun, currentState);
|
|
@@ -403,22 +492,24 @@ async function refreshSelected({auto=false, skipAutoAdvance=false} = {}) {
|
|
|
403
492
|
await loadTaskDescription();
|
|
404
493
|
renderTasks(); await refreshRuns();
|
|
405
494
|
if (selectedTask && selectedFileName) await loadFile(selectedFileName, { preserveScroll: true });
|
|
406
|
-
if (!skipAutoAdvance) await maybeAutoAdvanceSelectedRun();
|
|
407
495
|
}
|
|
408
496
|
function renderSelectedHeader() {
|
|
409
497
|
if (!currentState) return '<div class="muted">未选择任务批次</div>';
|
|
410
498
|
const sandbox = currentState.workerSandbox || 'workspace-write';
|
|
411
499
|
const chips = [
|
|
412
500
|
metaChip('Run ID', currentState.runId, { long: true }),
|
|
413
|
-
metaChip('
|
|
414
|
-
title: currentState.repo,
|
|
501
|
+
metaChip('工作区', basenamePath(currentState.workspacePath || currentState.repo), {
|
|
502
|
+
title: currentState.workspacePath || currentState.repo,
|
|
415
503
|
long: true,
|
|
416
|
-
extra: `<button class="secondary copy-btn" title="
|
|
504
|
+
extra: `<button class="secondary copy-btn" title="复制工作区地址" onclick="copyRepoPath(event)">⧉</button>`
|
|
417
505
|
}),
|
|
418
506
|
metaChip('沙箱', sandbox, { danger: sandbox === 'danger-full-access' }),
|
|
419
507
|
metaChip('开始', formatDateTime(currentState.createdAt)),
|
|
420
508
|
metaChip('用时', formatDurationMs(durationSeconds(currentState.createdAt, runDurationEnd(currentState)) * 1000))
|
|
421
509
|
];
|
|
510
|
+
if (currentState.git?.isGit || currentState.workspace?.git?.isGit) {
|
|
511
|
+
chips.push(gitChip());
|
|
512
|
+
}
|
|
422
513
|
if (currentState.runner === 'tmux') {
|
|
423
514
|
if (hasRunTmuxMetadata(currentState)) {
|
|
424
515
|
chips.push(metaChip('终端', tmuxSessionName(currentState), {
|
|
@@ -585,6 +676,7 @@ async function loadFile(name, { preserveScroll = false } = {}) {
|
|
|
585
676
|
selectedFileName = name;
|
|
586
677
|
const pre = document.getElementById('fileContent');
|
|
587
678
|
const previousScrollTop = pre.scrollTop;
|
|
679
|
+
const wasAtBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 24;
|
|
588
680
|
let text;
|
|
589
681
|
const selected = taskById(selectedTask);
|
|
590
682
|
if (name === 'result.json' && selected?.manualCompletion?.hasManualResult) {
|
|
@@ -594,7 +686,8 @@ async function loadFile(name, { preserveScroll = false } = {}) {
|
|
|
594
686
|
text = await api(`/api/runs/${selectedRun}/tasks/${selectedTask}/file?name=${encodeURIComponent(name)}`);
|
|
595
687
|
}
|
|
596
688
|
pre.textContent = text;
|
|
597
|
-
if (preserveScroll) pre.scrollTop =
|
|
689
|
+
if (name === 'events.pretty' && (!preserveScroll || wasAtBottom)) pre.scrollTop = pre.scrollHeight;
|
|
690
|
+
else if (preserveScroll) pre.scrollTop = previousScrollTop;
|
|
598
691
|
else pre.scrollTop = 0;
|
|
599
692
|
if (name === 'events.pretty') await renderExecutionSummary();
|
|
600
693
|
else hideExecutionSummary();
|
|
@@ -633,7 +726,7 @@ function hideExecutionSummary() {
|
|
|
633
726
|
el.classList.add('hidden');
|
|
634
727
|
el.innerHTML = '';
|
|
635
728
|
}
|
|
636
|
-
async function copyRepoPath(event, repoPath = currentState?.repo || '') {
|
|
729
|
+
async function copyRepoPath(event, repoPath = currentState?.workspacePath || currentState?.repo || '') {
|
|
637
730
|
event.stopPropagation();
|
|
638
731
|
if (!repoPath) return;
|
|
639
732
|
try {
|
|
@@ -641,11 +734,11 @@ async function copyRepoPath(event, repoPath = currentState?.repo || '') {
|
|
|
641
734
|
event.currentTarget.textContent = '已复制';
|
|
642
735
|
setTimeout(() => { event.currentTarget.textContent = '⧉'; }, 900);
|
|
643
736
|
} catch {
|
|
644
|
-
prompt('
|
|
737
|
+
prompt('复制工作区地址', repoPath);
|
|
645
738
|
}
|
|
646
739
|
}
|
|
647
740
|
async function copyRunRepoPath(event, runId) {
|
|
648
|
-
const repoPath = latestRuns.find(run => run.runId === runId)?.repo || '';
|
|
741
|
+
const repoPath = latestRuns.find(run => run.runId === runId)?.workspacePath || latestRuns.find(run => run.runId === runId)?.repo || '';
|
|
649
742
|
await copyRepoPath(event, repoPath);
|
|
650
743
|
}
|
|
651
744
|
async function copyTmuxRunCommand(event) {
|
|
@@ -812,67 +905,6 @@ async function renameRunLabel(event, runId = selectedRun) {
|
|
|
812
905
|
else await refreshRuns();
|
|
813
906
|
});
|
|
814
907
|
}
|
|
815
|
-
async function maybeAutoAdvanceRunSummaries(runs) {
|
|
816
|
-
for (const run of runs || []) {
|
|
817
|
-
if (!run || run.runId === selectedRun) continue;
|
|
818
|
-
if (run.status !== 'batch_blocked') autoRetrySkippedRuns.delete(run.runId);
|
|
819
|
-
if (run.status === 'planned') await autoDispatchRun(run.runId, false);
|
|
820
|
-
else if (run.status === 'batches_completed') await autoJudgeRun(run.runId, false);
|
|
821
|
-
else if (run.status === 'batch_blocked') await autoRetryRun(run.runId, false);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
async function autoDispatchRun(runId, refreshSelectedAfter = true) {
|
|
825
|
-
if (!runId || autoDispatchingRuns.has(runId)) return false;
|
|
826
|
-
autoDispatchingRuns.add(runId);
|
|
827
|
-
try {
|
|
828
|
-
await api(`/api/runs/${runId}/dispatch`, {method:'POST'});
|
|
829
|
-
if (refreshSelectedAfter && selectedRun === runId) await refreshSelected({auto:true, skipAutoAdvance:true});
|
|
830
|
-
return true;
|
|
831
|
-
} catch (error) {
|
|
832
|
-
console.error('自动派发失败', error);
|
|
833
|
-
return false;
|
|
834
|
-
} finally {
|
|
835
|
-
autoDispatchingRuns.delete(runId);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
async function autoJudgeRun(runId, refreshSelectedAfter = true) {
|
|
839
|
-
if (!runId || autoJudgingRuns.has(runId)) return false;
|
|
840
|
-
autoJudgingRuns.add(runId);
|
|
841
|
-
try {
|
|
842
|
-
await api(`/api/runs/${runId}/judge`, {method:'POST'});
|
|
843
|
-
if (refreshSelectedAfter && selectedRun === runId) await refreshSelected({auto:true, skipAutoAdvance:true});
|
|
844
|
-
return true;
|
|
845
|
-
} catch (error) {
|
|
846
|
-
console.error('自动验收失败', error);
|
|
847
|
-
return false;
|
|
848
|
-
} finally {
|
|
849
|
-
autoJudgingRuns.delete(runId);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
async function autoRetryRun(runId, refreshSelectedAfter = true) {
|
|
853
|
-
if (!runId || autoRetryingRuns.has(runId) || autoRetrySkippedRuns.has(runId)) return false;
|
|
854
|
-
autoRetryingRuns.add(runId);
|
|
855
|
-
try {
|
|
856
|
-
await api(`/api/runs/${runId}/retry`, {method:'POST', body: JSON.stringify({ reason: 'auto retry from dashboard', maxRetries: AUTO_MAX_RETRIES, auto: true })});
|
|
857
|
-
if (refreshSelectedAfter && selectedRun === runId) await refreshSelected({auto:true, skipAutoAdvance:true});
|
|
858
|
-
return true;
|
|
859
|
-
} catch (error) {
|
|
860
|
-
autoRetrySkippedRuns.add(runId);
|
|
861
|
-
console.error('自动重试失败', error);
|
|
862
|
-
return false;
|
|
863
|
-
} finally {
|
|
864
|
-
autoRetryingRuns.delete(runId);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
async function maybeAutoAdvanceSelectedRun() {
|
|
868
|
-
const runId = selectedRun;
|
|
869
|
-
const state = currentState;
|
|
870
|
-
if (!runId || !state) return;
|
|
871
|
-
if (state.status !== 'batch_blocked') autoRetrySkippedRuns.delete(runId);
|
|
872
|
-
if (state.status === 'planned') await autoDispatchRun(runId);
|
|
873
|
-
else if (state.status === 'batches_completed' && state.judge?.status !== 'running' && state.judge?.status !== 'completed') await autoJudgeRun(runId);
|
|
874
|
-
else if (state.status === 'batch_blocked') await autoRetryRun(runId);
|
|
875
|
-
}
|
|
876
908
|
async function planRun() { if (selectedRun) await runAction(async () => { await api(`/api/runs/${selectedRun}/plan`, {method:'POST'}); await refreshSelected(); }); }
|
|
877
909
|
async function dispatchRun() { if (selectedRun) await runAction(async () => { await api(`/api/runs/${selectedRun}/dispatch`, {method:'POST'}); await refreshSelected(); }); }
|
|
878
910
|
async function judgeRun() { if (selectedRun) await runAction(async () => { await api(`/api/runs/${selectedRun}/judge`, {method:'POST'}); await refreshSelected(); }); }
|
|
@@ -964,6 +996,7 @@ async function submitManualComplete() {
|
|
|
964
996
|
await loadFile('result.json');
|
|
965
997
|
}
|
|
966
998
|
|
|
999
|
+
initializeWorkerSandboxPreference();
|
|
967
1000
|
loadHealth().then(refreshRuns);
|
|
968
1001
|
setInterval(() => { if (selectedRun) refreshSelected({auto:true}).catch(console.error); else refreshRuns().catch(console.error); }, AUTO_REFRESH_MS);
|
|
969
1002
|
</script>
|