input-kanban 0.0.8 → 0.0.10
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 +7 -4
- package/LICENSE +21 -0
- package/PROJECT_GUIDE.md +124 -12
- package/README.en.md +27 -17
- package/README.md +27 -17
- package/RELEASE_NOTES.md +38 -1
- package/bin/input-kanban.js +277 -58
- package/package.json +5 -3
- package/public/index.html +101 -20
- package/src/orchestrator.js +523 -201
- package/src/scheduler.js +40 -0
- package/src/server.js +13 -6
- package/src/utils.js +7 -1
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "input-kanban",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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
|
+
"license": "MIT",
|
|
13
14
|
"files": [
|
|
14
15
|
"bin",
|
|
15
16
|
"src",
|
|
@@ -18,7 +19,8 @@
|
|
|
18
19
|
"README.en.md",
|
|
19
20
|
"RELEASE_NOTES.md",
|
|
20
21
|
"PROJECT_GUIDE.md",
|
|
21
|
-
"ENVIRONMENT.md"
|
|
22
|
+
"ENVIRONMENT.md",
|
|
23
|
+
"LICENSE"
|
|
22
24
|
],
|
|
23
25
|
"keywords": [
|
|
24
26
|
"codex",
|
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,12 @@
|
|
|
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
|
+
.workspace-filter-panel { margin: 4px 0 10px; display: flex; align-items: center; gap: 6px; }
|
|
64
|
+
.workspace-filter-select { flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 7px 9px; color: #cbd5e1; }
|
|
63
65
|
.task-text { max-height: 180px; color: #cbd5e1; }
|
|
64
66
|
.empty { color: var(--muted); padding: 18px 0; }
|
|
67
|
+
.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; }
|
|
68
|
+
.runs-load-icon:hover { opacity: 1; color: #cbd5e1; border-color: var(--line-strong); }
|
|
65
69
|
.run-list { display: flex; flex-direction: column; gap: 10px; flex: 1; min-height: 0; overflow-y: auto; padding-right: 4px; }
|
|
66
70
|
.run-list-more { width: 100%; margin-top: 4px; }
|
|
67
71
|
.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; }
|
|
@@ -119,6 +123,7 @@
|
|
|
119
123
|
.modal-backdrop.hidden { display: none; }
|
|
120
124
|
.modal-card { width: min(760px, 100%); border: 1px solid var(--line); border-radius: 14px; background: var(--panel); box-shadow: 0 18px 60px rgba(0,0,0,.38); padding: 16px; }
|
|
121
125
|
.modal-card textarea { min-height: 220px; }
|
|
126
|
+
.page-footer { padding: 0 18px 18px; color: var(--muted); text-align: center; font-size: 12px; }
|
|
122
127
|
</style>
|
|
123
128
|
</head>
|
|
124
129
|
<body>
|
|
@@ -128,21 +133,24 @@
|
|
|
128
133
|
<section>
|
|
129
134
|
<div class="toolbar">
|
|
130
135
|
<button onclick="showCreateForm()">新建任务批次</button>
|
|
131
|
-
<button class="secondary" onclick="refreshRuns()">刷新批次列表</button>
|
|
132
136
|
</div>
|
|
133
137
|
<h2>任务批次</h2>
|
|
134
|
-
<div
|
|
138
|
+
<div class="workspace-filter-panel">
|
|
139
|
+
<select id="workspaceFilterSelect" class="workspace-filter-select" onchange="setWorkspaceFilter(this.value)" title="未筛选工作区"></select>
|
|
140
|
+
<span id="runsLoadHint" class="runs-load-icon" title="批次列表尚未加载" aria-label="批次列表尚未加载">ⓘ</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div id="runs" class="run-list"><div class="empty">批次列表加载中...</div></div>
|
|
135
143
|
</section>
|
|
136
144
|
</div>
|
|
137
145
|
<div>
|
|
138
146
|
<section id="createPanel" class="hidden">
|
|
139
147
|
<h2>新建任务批次</h2>
|
|
140
148
|
<label>批次名称</label><input id="label" value="codex-task" />
|
|
141
|
-
<label
|
|
149
|
+
<label>工作区</label><input id="repo" />
|
|
142
150
|
<label>运行目录</label><input id="runsDir" readonly />
|
|
143
151
|
<label>最大并发数</label><input id="maxParallel" type="number" value="3" min="1" max="16" />
|
|
144
152
|
<label>Worker 沙箱</label><select id="workerSandbox">
|
|
145
|
-
<option value="workspace-write" selected>workspace-write
|
|
153
|
+
<option value="workspace-write" selected>workspace-write(默认,允许写当前工作区)</option>
|
|
146
154
|
<option value="read-only">read-only(只读)</option>
|
|
147
155
|
<option value="danger-full-access">danger-full-access(高风险,跳过沙箱限制)</option>
|
|
148
156
|
</select>
|
|
@@ -186,6 +194,7 @@
|
|
|
186
194
|
</section>
|
|
187
195
|
</div>
|
|
188
196
|
</main>
|
|
197
|
+
<footer id="pageFooter" class="page-footer">版本:-</footer>
|
|
189
198
|
<div id="manualCompleteModal" class="modal-backdrop hidden">
|
|
190
199
|
<div class="modal-card">
|
|
191
200
|
<h2>手动标记成功</h2>
|
|
@@ -209,9 +218,13 @@ let currentState = null;
|
|
|
209
218
|
let lastAutoRefreshAt = null;
|
|
210
219
|
let runListVisibleCount = 10;
|
|
211
220
|
let latestRuns = [];
|
|
221
|
+
let workspaceCatalogRuns = [];
|
|
212
222
|
const statusByRunId = new Map();
|
|
213
223
|
const AUTO_REFRESH_MS = 3000;
|
|
214
224
|
const RUN_LIST_PAGE_SIZE = 10;
|
|
225
|
+
const WORKSPACE_FILTER_ALL = '';
|
|
226
|
+
let currentWorkspacePath = '';
|
|
227
|
+
let selectedWorkspaceFilter = localStorage.getItem('input-kanban.workspaceFilter') || WORKSPACE_FILTER_ALL;
|
|
215
228
|
|
|
216
229
|
async function api(path, opts={}) {
|
|
217
230
|
const res = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...opts });
|
|
@@ -284,6 +297,7 @@ function basenamePath(value) {
|
|
|
284
297
|
function metaChip(label, value, { title = value, danger = false, long = false, extra = '' } = {}) {
|
|
285
298
|
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>`;
|
|
286
299
|
}
|
|
300
|
+
function gitChip() { return '<span class="meta-chip" title="Git 工作区"><span class="meta-value">Git</span></span>'; }
|
|
287
301
|
function editIcon() {
|
|
288
302
|
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>';
|
|
289
303
|
}
|
|
@@ -328,8 +342,12 @@ function runListHasTmuxMetadata(run) {
|
|
|
328
342
|
|
|
329
343
|
async function loadHealth() {
|
|
330
344
|
const h = await api('/api/health');
|
|
331
|
-
|
|
345
|
+
currentWorkspacePath = h.defaultWorkspace || h.defaultRepo || '';
|
|
346
|
+
document.getElementById('repo').value = currentWorkspacePath;
|
|
332
347
|
document.getElementById('runsDir').value = h.runsDir;
|
|
348
|
+
document.getElementById('pageFooter').textContent = h.version ? `版本:v${h.version}` : '版本:未知(请重启服务)';
|
|
349
|
+
renderWorkspaceFilterOptions();
|
|
350
|
+
updateWorkspaceFilterTitle();
|
|
333
351
|
}
|
|
334
352
|
function showCreateForm() {
|
|
335
353
|
selectedRun = null; selectedTask = null; selectedFileName = null; currentState = null;
|
|
@@ -344,7 +362,7 @@ function hideCreateForm() {
|
|
|
344
362
|
document.getElementById('filePanel').classList.remove('hidden');
|
|
345
363
|
}
|
|
346
364
|
async function createRun() {
|
|
347
|
-
const body = { label: label.value, repo: repo.value, maxParallel: maxParallel.value, workerSandbox: workerSandbox.value, taskText: taskText.value };
|
|
365
|
+
const body = { label: label.value, workspace: repo.value, repo: repo.value, maxParallel: maxParallel.value, workerSandbox: workerSandbox.value, taskText: taskText.value };
|
|
348
366
|
const r = await api('/api/runs', { method: 'POST', body: JSON.stringify(body) });
|
|
349
367
|
selectedRun = r.runId; selectedTask = null; selectedFileName = null;
|
|
350
368
|
clearFileView();
|
|
@@ -354,9 +372,28 @@ async function createRun() {
|
|
|
354
372
|
await refreshSelected();
|
|
355
373
|
}
|
|
356
374
|
async function refreshRuns() {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
375
|
+
const startedAt = performance.now();
|
|
376
|
+
const hint = document.getElementById('runsLoadHint');
|
|
377
|
+
if (hint) { hint.title = '批次列表加载中...'; hint.setAttribute('aria-label', hint.title); }
|
|
378
|
+
if (!latestRuns.length) document.getElementById('runs').innerHTML = '<div class="empty">批次列表加载中...</div>';
|
|
379
|
+
try {
|
|
380
|
+
const params = new URLSearchParams();
|
|
381
|
+
if (selectedWorkspaceFilter && selectedWorkspaceFilter !== WORKSPACE_FILTER_ALL) params.set('workspace', selectedWorkspaceFilter);
|
|
382
|
+
const [filteredData, catalogData] = await Promise.all([
|
|
383
|
+
api(`/api/runs${params.toString() ? `?${params.toString()}` : ''}`),
|
|
384
|
+
api('/api/runs?includeArchived=1')
|
|
385
|
+
]);
|
|
386
|
+
latestRuns = filteredData.runs || [];
|
|
387
|
+
workspaceCatalogRuns = catalogData.runs || [];
|
|
388
|
+
renderWorkspaceFilterOptions();
|
|
389
|
+
renderRunList();
|
|
390
|
+
updateWorkspaceFilterTitle();
|
|
391
|
+
if (hint) { hint.title = `加载 ${Math.round(performance.now() - startedAt)}ms|显示 ${latestRuns.length} 个批次`; hint.setAttribute('aria-label', hint.title); }
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error('批次列表加载失败', error);
|
|
394
|
+
if (hint) { hint.title = `批次列表加载失败|${errorDetail(error)}`; hint.setAttribute('aria-label', hint.title); }
|
|
395
|
+
if (!latestRuns.length) document.getElementById('runs').innerHTML = '<div class="empty">批次列表加载失败</div>';
|
|
396
|
+
}
|
|
360
397
|
}
|
|
361
398
|
function renderRunList() {
|
|
362
399
|
const visibleRuns = latestRuns.slice(0, runListVisibleCount);
|
|
@@ -364,7 +401,8 @@ function renderRunList() {
|
|
|
364
401
|
<div class="run-card ${selectedRun === r.runId ? 'active' : ''}" onclick="selectRun('${r.runId}')" onmouseleave="clearArchiveConfirm('${r.runId}')">
|
|
365
402
|
<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>
|
|
366
403
|
<div class="run-card-meta">
|
|
367
|
-
${metaChip('
|
|
404
|
+
${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>` })}
|
|
405
|
+
${r.git?.isGit ? gitChip() : ''}
|
|
368
406
|
${metaChip('创建', formatDateTime(r.createdAt))}
|
|
369
407
|
${metaChip('用时', runCardDurationText(r))}
|
|
370
408
|
${metaChip('进度', `${r.completed}/${r.total}`)}
|
|
@@ -381,6 +419,46 @@ function showMoreRuns() {
|
|
|
381
419
|
runListVisibleCount += RUN_LIST_PAGE_SIZE;
|
|
382
420
|
renderRunList();
|
|
383
421
|
}
|
|
422
|
+
function renderWorkspaceFilterOptions() {
|
|
423
|
+
const select = document.getElementById('workspaceFilterSelect');
|
|
424
|
+
if (!select) return;
|
|
425
|
+
const workspaces = new Map();
|
|
426
|
+
for (const run of workspaceCatalogRuns) {
|
|
427
|
+
const workspacePath = run.workspacePath || run.repo || '';
|
|
428
|
+
if (!workspacePath) continue;
|
|
429
|
+
const item = workspaces.get(workspacePath) || { label: basenamePath(workspacePath), count: 0, git: !!run.git?.isGit };
|
|
430
|
+
item.count += 1;
|
|
431
|
+
item.git = item.git || !!run.git?.isGit;
|
|
432
|
+
workspaces.set(workspacePath, item);
|
|
433
|
+
}
|
|
434
|
+
const currentValue = workspaces.has(selectedWorkspaceFilter) ? selectedWorkspaceFilter : WORKSPACE_FILTER_ALL;
|
|
435
|
+
if (currentValue !== selectedWorkspaceFilter) {
|
|
436
|
+
selectedWorkspaceFilter = currentValue;
|
|
437
|
+
localStorage.setItem('input-kanban.workspaceFilter', selectedWorkspaceFilter);
|
|
438
|
+
}
|
|
439
|
+
const options = [[WORKSPACE_FILTER_ALL, '工作区筛选'], ...[...workspaces.entries()].map(([workspacePath, item]) => {
|
|
440
|
+
const gitText = item.git ? ' · Git' : '';
|
|
441
|
+
return [workspacePath, `${item.label}${gitText} (${item.count})`];
|
|
442
|
+
})];
|
|
443
|
+
select.innerHTML = options.map(([value, label]) => `<option value="${esc(value)}">${esc(label)}</option>`).join('');
|
|
444
|
+
select.value = selectedWorkspaceFilter;
|
|
445
|
+
updateWorkspaceFilterTitle();
|
|
446
|
+
}
|
|
447
|
+
function updateWorkspaceFilterTitle() {
|
|
448
|
+
const select = document.getElementById('workspaceFilterSelect');
|
|
449
|
+
if (!select) return;
|
|
450
|
+
if (selectedWorkspaceFilter === WORKSPACE_FILTER_ALL) {
|
|
451
|
+
select.title = currentWorkspacePath ? `未筛选工作区|默认工作区:${currentWorkspacePath}` : '未筛选工作区';
|
|
452
|
+
} else {
|
|
453
|
+
select.title = `当前筛选:${selectedWorkspaceFilter}`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function setWorkspaceFilter(value) {
|
|
457
|
+
selectedWorkspaceFilter = String(value || '').trim();
|
|
458
|
+
localStorage.setItem('input-kanban.workspaceFilter', selectedWorkspaceFilter);
|
|
459
|
+
runListVisibleCount = RUN_LIST_PAGE_SIZE;
|
|
460
|
+
refreshRuns().catch(console.error);
|
|
461
|
+
}
|
|
384
462
|
async function selectRun(id) { selectedRun = id; selectedTask = null; selectedFileName = null; clearFileView(); hideCreateForm(); await refreshSelected(); }
|
|
385
463
|
async function refreshSelected({auto=false} = {}) {
|
|
386
464
|
if (!selectedRun) return;
|
|
@@ -400,15 +478,18 @@ function renderSelectedHeader() {
|
|
|
400
478
|
const sandbox = currentState.workerSandbox || 'workspace-write';
|
|
401
479
|
const chips = [
|
|
402
480
|
metaChip('Run ID', currentState.runId, { long: true }),
|
|
403
|
-
metaChip('
|
|
404
|
-
title: currentState.repo,
|
|
481
|
+
metaChip('工作区', basenamePath(currentState.workspacePath || currentState.repo), {
|
|
482
|
+
title: currentState.workspacePath || currentState.repo,
|
|
405
483
|
long: true,
|
|
406
|
-
extra: `<button class="secondary copy-btn" title="
|
|
484
|
+
extra: `<button class="secondary copy-btn" title="复制工作区地址" onclick="copyRepoPath(event)">⧉</button>`
|
|
407
485
|
}),
|
|
408
486
|
metaChip('沙箱', sandbox, { danger: sandbox === 'danger-full-access' }),
|
|
409
487
|
metaChip('开始', formatDateTime(currentState.createdAt)),
|
|
410
488
|
metaChip('用时', formatDurationMs(durationSeconds(currentState.createdAt, runDurationEnd(currentState)) * 1000))
|
|
411
489
|
];
|
|
490
|
+
if (currentState.git?.isGit || currentState.workspace?.git?.isGit) {
|
|
491
|
+
chips.push(gitChip());
|
|
492
|
+
}
|
|
412
493
|
if (currentState.runner === 'tmux') {
|
|
413
494
|
if (hasRunTmuxMetadata(currentState)) {
|
|
414
495
|
chips.push(metaChip('终端', tmuxSessionName(currentState), {
|
|
@@ -463,9 +544,9 @@ function updateAutoRefreshHint() {
|
|
|
463
544
|
const el = document.getElementById('autoRefreshHint');
|
|
464
545
|
if (!el) return;
|
|
465
546
|
if (!selectedRun || !currentState) { el.textContent = '自动刷新:未启动'; return; }
|
|
466
|
-
const active = ['planning','running','judging'].includes(currentState.status) || (currentState.tasks || []).some(t => t.status === 'running') || currentState.planner?.status === 'running' || currentState.judge?.status === 'running';
|
|
547
|
+
const active = ['planning','running','judging','planned','batches_completed','batch_blocked'].includes(currentState.status) || (currentState.tasks || []).some(t => t.status === 'running') || currentState.planner?.status === 'running' || currentState.judge?.status === 'running';
|
|
467
548
|
const last = lastAutoRefreshAt ? lastAutoRefreshAt.toLocaleTimeString() : '尚未触发';
|
|
468
|
-
el.textContent = active ?
|
|
549
|
+
el.textContent = active ? `自动模式中:每 ${AUTO_REFRESH_MS / 1000} 秒刷新并推进一次|上次刷新 ${last}` : `自动模式待命:每 ${AUTO_REFRESH_MS / 1000} 秒检查一次|上次刷新 ${last}`;
|
|
469
550
|
}
|
|
470
551
|
function taskStatusCell(t) {
|
|
471
552
|
if (t?.manualCompletion) {
|
|
@@ -623,7 +704,7 @@ function hideExecutionSummary() {
|
|
|
623
704
|
el.classList.add('hidden');
|
|
624
705
|
el.innerHTML = '';
|
|
625
706
|
}
|
|
626
|
-
async function copyRepoPath(event, repoPath = currentState?.repo || '') {
|
|
707
|
+
async function copyRepoPath(event, repoPath = currentState?.workspacePath || currentState?.repo || '') {
|
|
627
708
|
event.stopPropagation();
|
|
628
709
|
if (!repoPath) return;
|
|
629
710
|
try {
|
|
@@ -631,11 +712,11 @@ async function copyRepoPath(event, repoPath = currentState?.repo || '') {
|
|
|
631
712
|
event.currentTarget.textContent = '已复制';
|
|
632
713
|
setTimeout(() => { event.currentTarget.textContent = '⧉'; }, 900);
|
|
633
714
|
} catch {
|
|
634
|
-
prompt('
|
|
715
|
+
prompt('复制工作区地址', repoPath);
|
|
635
716
|
}
|
|
636
717
|
}
|
|
637
718
|
async function copyRunRepoPath(event, runId) {
|
|
638
|
-
const repoPath = latestRuns.find(run => run.runId === runId)?.repo || '';
|
|
719
|
+
const repoPath = latestRuns.find(run => run.runId === runId)?.workspacePath || latestRuns.find(run => run.runId === runId)?.repo || '';
|
|
639
720
|
await copyRepoPath(event, repoPath);
|
|
640
721
|
}
|
|
641
722
|
async function copyTmuxRunCommand(event) {
|