agim-cli 1.1.2 → 1.1.4
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/CHANGELOG.md +104 -0
- package/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/dist/core/a2a.d.ts +19 -0
- package/dist/core/a2a.d.ts.map +1 -1
- package/dist/core/a2a.js +55 -4
- package/dist/core/a2a.js.map +1 -1
- package/dist/core/approval-bus.d.ts.map +1 -1
- package/dist/core/approval-bus.js +19 -0
- package/dist/core/approval-bus.js.map +1 -1
- package/dist/core/artifacts.d.ts +86 -0
- package/dist/core/artifacts.d.ts.map +1 -0
- package/dist/core/artifacts.js +306 -0
- package/dist/core/artifacts.js.map +1 -0
- package/dist/core/commands/a2a.d.ts.map +1 -1
- package/dist/core/commands/a2a.js +19 -5
- package/dist/core/commands/a2a.js.map +1 -1
- package/dist/core/job-board.d.ts +5 -0
- package/dist/core/job-board.d.ts.map +1 -1
- package/dist/core/job-board.js +32 -0
- package/dist/core/job-board.js.map +1 -1
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +27 -2
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/mcp-approval-server.js +58 -6
- package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -1
- package/dist/web/public/memos.html +71 -15
- package/dist/web/public/reminders.html +89 -15
- package/dist/web/public/tasks.html +555 -8
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +275 -4
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -149,6 +149,85 @@
|
|
|
149
149
|
jobsBatchRun: 'Run selected',
|
|
150
150
|
jobsBatchEmpty: 'No jobs selected.',
|
|
151
151
|
jobsBatchResult: 'Batch result: {ok} ok, {fail} failed',
|
|
152
|
+
// v1.1.2 / v1.1.3 — new tabs + columns
|
|
153
|
+
tabsOutbox: 'Outbox',
|
|
154
|
+
tabsA2A: 'A2A',
|
|
155
|
+
kindCol: 'Kind',
|
|
156
|
+
parentCol: 'Parent',
|
|
157
|
+
depthCol: 'Depth',
|
|
158
|
+
filterAllKinds: 'All kinds',
|
|
159
|
+
filterKindJob: 'job (explicit /job)',
|
|
160
|
+
filterKindInline: 'inline (auto-tracked)',
|
|
161
|
+
outboxEmpty: 'No outbox rows.',
|
|
162
|
+
outboxStatusPending: '⏳ Pending',
|
|
163
|
+
outboxStatusDelivered: '✅ Delivered',
|
|
164
|
+
outboxStatusGivingUp: '💀 Giving up',
|
|
165
|
+
outboxRetry: 'Retry',
|
|
166
|
+
outboxColPlatform: 'Platform',
|
|
167
|
+
outboxColKind: 'Kind',
|
|
168
|
+
outboxColPri: 'Pri',
|
|
169
|
+
outboxColAttempts: 'Attempts',
|
|
170
|
+
outboxColPayload: 'Payload',
|
|
171
|
+
outboxColError: 'Error',
|
|
172
|
+
outboxColCreated: 'Created',
|
|
173
|
+
a2aEmpty: 'No A2A calls yet.',
|
|
174
|
+
a2aRecent: 'Recent A2A calls',
|
|
175
|
+
a2aCallerCallee: 'Caller→Callee',
|
|
176
|
+
a2aTreeRoot: 'Call tree (root #{id})',
|
|
177
|
+
a2aTreeInputJob: 'job id for tree…',
|
|
178
|
+
a2aBtnTree: 'Tree',
|
|
179
|
+
a2aBtnViewParent: 'View parent tree',
|
|
180
|
+
a2aStatTotal: 'Total',
|
|
181
|
+
a2aStat24h: '24h',
|
|
182
|
+
a2aStatMaxDepth: 'Max depth',
|
|
183
|
+
a2aStatTop: 'Top:',
|
|
184
|
+
modalParent: 'Parent',
|
|
185
|
+
modalDepth: 'Depth',
|
|
186
|
+
modalThread: 'Thread',
|
|
187
|
+
modalReplaced: 'Replaced by',
|
|
188
|
+
modalLastOutbox: 'Last outbox',
|
|
189
|
+
modalArtifacts: 'Artifacts',
|
|
190
|
+
modalArtifactsInputs: 'Inputs:',
|
|
191
|
+
modalArtifactsOutputs: 'Outputs:',
|
|
192
|
+
modalDelivered: 'Delivered',
|
|
193
|
+
helpClose: 'Close',
|
|
194
|
+
// Help tooltips (concept glossary). Used by the (?) icons next to
|
|
195
|
+
// jargon. Keys are short ids; values are { title, body } in the
|
|
196
|
+
// 'help' namespace below.
|
|
197
|
+
help: {
|
|
198
|
+
outbox: {
|
|
199
|
+
title: 'Outbox · 投递队列',
|
|
200
|
+
body: 'Every outbound IM message is first written to a SQLite outbox, then a background worker drains it with exponential backoff. If the messenger glitches or the user is briefly offline, the row stays "pending" and gets retried (1s → 5s → 30s → 5min → 30min → 2h). After 6 failed attempts the row transitions to "giving_up" and stops retrying until you click Retry. Visible in: /tasks Outbox tab, IM command /outbox status.',
|
|
201
|
+
},
|
|
202
|
+
inline: {
|
|
203
|
+
title: 'Inline job · 内联任务',
|
|
204
|
+
body: 'An inline job is the row agim auto-creates for every inbound IM message that triggers an agent. It tracks the full lifecycle (pending → running → completed → delivered) so a crash or restart never loses work. Distinct from kind=job rows which come from explicit /job create. Retention: 24h (vs 30d for kind=job).',
|
|
205
|
+
},
|
|
206
|
+
a2a: {
|
|
207
|
+
title: 'A2A · Agent-to-Agent',
|
|
208
|
+
body: 'When the active agent calls another agent via mcp__imhub__call_agent (e.g. claude saying "用 codex 跑 git status"), agim spawns the callee as a fresh inline job whose parent_id points back at the caller. Guardrails enforced by agim: depth ceiling (default 3), self-call ban, workspace whitelist, shared per-user budget. The tree view shows the full caller→callee chain.',
|
|
209
|
+
},
|
|
210
|
+
artifacts: {
|
|
211
|
+
title: 'Artifacts · 共享文件',
|
|
212
|
+
body: 'A2A Layer 2 lets agents exchange files instead of cramming everything into prompts. Each A2A inline job gets a dedicated ~/.agim/artifacts/<jobId>/_agim-{input,output}/ directory. Caller drops files via `inputs[]`; callee writes results to _agim-output/; caller reads them through this UI or with the agent\'s native Read tool. Retention follows the inline job (24h).',
|
|
213
|
+
},
|
|
214
|
+
callDepth: {
|
|
215
|
+
title: 'Call depth · 调用深度',
|
|
216
|
+
body: 'How deep this row sits in an A2A chain. 0 = user-originated message. +1 per nested mcp__imhub__call_agent invocation. Configurable max via IMHUB_A2A_MAX_DEPTH (default 3) — exceeding it rejects the call before any tokens are spent.',
|
|
217
|
+
},
|
|
218
|
+
parent: {
|
|
219
|
+
title: 'Parent · 父任务',
|
|
220
|
+
body: 'For A2A callees, the inline-job id of the row that fired mcp__imhub__call_agent. NULL on user-originated rows. Click to open the parent\'s detail view.',
|
|
221
|
+
},
|
|
222
|
+
replacedBy: {
|
|
223
|
+
title: 'Replaced by · 已被替换为',
|
|
224
|
+
body: 'For interrupted inline jobs the user retried via "1 重发" after a restart, this points at the new replacement row that ran instead. The old row stays for audit purposes.',
|
|
225
|
+
},
|
|
226
|
+
givingUp: {
|
|
227
|
+
title: 'Giving up · 已放弃',
|
|
228
|
+
body: 'An outbox row enters this state after 6 failed delivery attempts (cumulative ~3h of backoff). It will no longer be retried automatically. Click "Retry" to put it back into the pending queue from scratch.',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
152
231
|
},
|
|
153
232
|
zh: {
|
|
154
233
|
title: 'Agim — 任务',
|
|
@@ -276,6 +355,82 @@
|
|
|
276
355
|
jobsBatchRun: '批量运行',
|
|
277
356
|
jobsBatchEmpty: '未选择任何任务。',
|
|
278
357
|
jobsBatchResult: '批量结果:成功 {ok},失败 {fail}',
|
|
358
|
+
// v1.1.2 / v1.1.3 — 新 Tab 与列
|
|
359
|
+
tabsOutbox: '投递队列',
|
|
360
|
+
tabsA2A: 'A2A 调用',
|
|
361
|
+
kindCol: '类型',
|
|
362
|
+
parentCol: '父任务',
|
|
363
|
+
depthCol: '深度',
|
|
364
|
+
filterAllKinds: '全部类型',
|
|
365
|
+
filterKindJob: 'job(显式 /job 创建)',
|
|
366
|
+
filterKindInline: 'inline(自动跟踪)',
|
|
367
|
+
outboxEmpty: '投递队列为空。',
|
|
368
|
+
outboxStatusPending: '⏳ 待发',
|
|
369
|
+
outboxStatusDelivered: '✅ 已送达',
|
|
370
|
+
outboxStatusGivingUp: '💀 已放弃',
|
|
371
|
+
outboxRetry: '重试',
|
|
372
|
+
outboxColPlatform: '平台',
|
|
373
|
+
outboxColKind: '类型',
|
|
374
|
+
outboxColPri: '优先级',
|
|
375
|
+
outboxColAttempts: '尝试次数',
|
|
376
|
+
outboxColPayload: '消息内容',
|
|
377
|
+
outboxColError: '错误',
|
|
378
|
+
outboxColCreated: '创建时间',
|
|
379
|
+
a2aEmpty: '暂无 A2A 调用记录。',
|
|
380
|
+
a2aRecent: '最近 A2A 调用',
|
|
381
|
+
a2aCallerCallee: '调用方→被调方',
|
|
382
|
+
a2aTreeRoot: '调用链树(根 #{id})',
|
|
383
|
+
a2aTreeInputJob: '输入 job id 看调用链…',
|
|
384
|
+
a2aBtnTree: '查看树',
|
|
385
|
+
a2aBtnViewParent: '查看父任务调用链',
|
|
386
|
+
a2aStatTotal: '总数',
|
|
387
|
+
a2aStat24h: '24小时',
|
|
388
|
+
a2aStatMaxDepth: '最大深度',
|
|
389
|
+
a2aStatTop: '热门:',
|
|
390
|
+
modalParent: '父任务',
|
|
391
|
+
modalDepth: '深度',
|
|
392
|
+
modalThread: '会话',
|
|
393
|
+
modalReplaced: '替换为',
|
|
394
|
+
modalLastOutbox: '最近投递',
|
|
395
|
+
modalArtifacts: '文件附件',
|
|
396
|
+
modalArtifactsInputs: '输入:',
|
|
397
|
+
modalArtifactsOutputs: '输出:',
|
|
398
|
+
modalDelivered: '送达时间',
|
|
399
|
+
helpClose: '关闭',
|
|
400
|
+
help: {
|
|
401
|
+
outbox: {
|
|
402
|
+
title: 'Outbox · 投递队列',
|
|
403
|
+
body: '每条要发到 IM 的消息都先写入 SQLite 投递队列,后台 worker 按指数退避节奏拉走真发:失败时 1s→5s→30s→5min→30min→2h 重试,连续 6 次失败转为「已放弃」直到你手动点重试。这样 IM 抖动 / 短暂断网都不会丢消息。可在 IM 内用 /outbox status 查询。',
|
|
404
|
+
},
|
|
405
|
+
inline: {
|
|
406
|
+
title: 'Inline 任务 · 自动跟踪',
|
|
407
|
+
body: '每条进入 agim 触发 Agent 的 IM 消息,会自动建一条 inline job 跟踪完整生命周期(待执行 → 运行中 → 已完成 → 已送达)。agim 崩溃 / 重启时绝不丢任务。区别于 kind=job 行(来自显式 /job create)。inline 保留 24 小时,job 保留 30 天。',
|
|
408
|
+
},
|
|
409
|
+
a2a: {
|
|
410
|
+
title: 'A2A · Agent 互调',
|
|
411
|
+
body: '当前 Agent 可以通过 mcp__imhub__call_agent 工具调用另一个 Agent(比如 claude 说"用 codex 跑 git status")。agim 会为被调 Agent 建一条新 inline job,parent_id 指向调用方。护栏由 agim 强制:调用深度上限(默认 3)、禁止自调、工作区白名单、按人共享预算。调用树视图展示完整调用链。',
|
|
412
|
+
},
|
|
413
|
+
artifacts: {
|
|
414
|
+
title: 'Artifacts · 共享文件',
|
|
415
|
+
body: 'A2A Layer 2 让 Agent 之间传文件而非把内容塞进 prompt。每条 A2A 任务有独立工作目录 ~/.agim/artifacts/<jobId>/_agim-{input,output}/。调用方通过 inputs[] 放文件,被调方写到 _agim-output/,调用方在这里点击下载或用 Read 工具读。保留期跟 inline 任务一致(24 小时)。',
|
|
416
|
+
},
|
|
417
|
+
callDepth: {
|
|
418
|
+
title: '调用深度',
|
|
419
|
+
body: '该任务在 A2A 链中的层数。0 = 用户发起的消息,每多一层 mcp__imhub__call_agent 嵌套 +1。可由 IMHUB_A2A_MAX_DEPTH 配置最大值(默认 3),超过则在花费 token 前就拒绝调用。',
|
|
420
|
+
},
|
|
421
|
+
parent: {
|
|
422
|
+
title: '父任务',
|
|
423
|
+
body: 'A2A 被调方任务的 parent_id 指向触发此次调用的任务行。用户原发的任务为空。点击可打开父任务详情。',
|
|
424
|
+
},
|
|
425
|
+
replacedBy: {
|
|
426
|
+
title: '已被替换为',
|
|
427
|
+
body: '服务重启后被中断、用户回复"1 重发"重新发起的任务,其 replaced_by 字段指向新行 id。旧行保留用于审计。',
|
|
428
|
+
},
|
|
429
|
+
givingUp: {
|
|
430
|
+
title: '已放弃',
|
|
431
|
+
body: '投递队列中连续 6 次失败(累计退避约 3 小时)的行进入此状态,不再自动重试。点击"重试"会将其重新入队、退避计数清零。',
|
|
432
|
+
},
|
|
433
|
+
},
|
|
279
434
|
},
|
|
280
435
|
};
|
|
281
436
|
window.__t = T[window.__lang];
|
|
@@ -419,8 +574,45 @@
|
|
|
419
574
|
.pill.pending { background: rgba(255, 193, 7, 0.18); color: #b89000; }
|
|
420
575
|
.pill.running { background: rgba(23, 162, 184, 0.18); color: #0d8898; }
|
|
421
576
|
.pill.completed { background: rgba(40, 167, 69, 0.18); color: #1e7c34; }
|
|
577
|
+
.pill.delivered { background: rgba(40, 167, 69, 0.28); color: #155724; font-weight: 500; }
|
|
422
578
|
.pill.failed { background: rgba(220, 53, 69, 0.18); color: #b32433; }
|
|
423
579
|
.pill.cancelled { background: rgba(108, 117, 125, 0.18); color: #5a6268; }
|
|
580
|
+
.pill.interrupted { background: rgba(255, 152, 0, 0.22); color: #b35900; }
|
|
581
|
+
.pill.replaced { background: rgba(94, 53, 177, 0.18); color: #4527a0; }
|
|
582
|
+
.pill.abandoned { background: rgba(96, 125, 139, 0.18); color: #455a64; }
|
|
583
|
+
.pill.kind-inline { background: rgba(63, 81, 181, 0.12); color: #3949ab; font-size: 11px; }
|
|
584
|
+
.pill.kind-job { background: rgba(141, 110, 99, 0.18); color: #5d4037; font-size: 11px; }
|
|
585
|
+
/* Mobile-friendly: wide tables (Jobs / Outbox / A2A / Audit) get a
|
|
586
|
+
horizontal scrollbar inside their pane instead of overflowing the
|
|
587
|
+
viewport. WeChat/Telegram in-app WebView is the common entry. */
|
|
588
|
+
#jobs-list, #outbox-list, #a2a-list, #audit-list, #subtasks-list, #bg-list, #schedules-list {
|
|
589
|
+
overflow-x: auto;
|
|
590
|
+
-webkit-overflow-scrolling: touch;
|
|
591
|
+
}
|
|
592
|
+
#jobs-list table, #outbox-list table, #a2a-list table, #audit-list table {
|
|
593
|
+
min-width: 720px; /* allow horizontal scroll instead of squishing cells */
|
|
594
|
+
}
|
|
595
|
+
/* Tab bar should also scroll horizontally on narrow screens rather
|
|
596
|
+
than wrapping (10 tabs don't fit on a phone otherwise). */
|
|
597
|
+
.tabs {
|
|
598
|
+
overflow-x: auto;
|
|
599
|
+
-webkit-overflow-scrolling: touch;
|
|
600
|
+
white-space: nowrap;
|
|
601
|
+
flex-wrap: nowrap;
|
|
602
|
+
}
|
|
603
|
+
.tabs .tab { flex-shrink: 0; }
|
|
604
|
+
/* Help-tooltip button — small (?) next to jargon, opens centered modal */
|
|
605
|
+
.help-btn {
|
|
606
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
607
|
+
width: 18px; height: 18px; padding: 0; border-radius: 50%;
|
|
608
|
+
font-size: 11px; font-weight: 700; line-height: 1;
|
|
609
|
+
background: var(--surface-2, rgba(0,0,0,0.06)); color: var(--text-muted, #666);
|
|
610
|
+
border: 1px solid var(--border, rgba(0,0,0,0.12));
|
|
611
|
+
cursor: pointer; transition: all .15s;
|
|
612
|
+
}
|
|
613
|
+
.help-btn:hover { background: var(--primary, #3b82f6); color: #fff; border-color: var(--primary, #3b82f6); }
|
|
614
|
+
.help-modal h2 { margin: 0 0 .5em; font-size: 18px; }
|
|
615
|
+
.help-modal p { line-height: 1.7; color: var(--text, #333); white-space: pre-wrap; }
|
|
424
616
|
.row-actions button {
|
|
425
617
|
font-size: 12px;
|
|
426
618
|
padding: 3px 8px;
|
|
@@ -555,9 +747,13 @@
|
|
|
555
747
|
<header>
|
|
556
748
|
<h1 id="page-title"></h1>
|
|
557
749
|
<button id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
|
|
750
|
+
<select id="langSelect" title="Language / 语言" style="margin-right:8px">
|
|
751
|
+
<option value="en">EN</option>
|
|
752
|
+
<option value="zh">中文</option>
|
|
753
|
+
</select>
|
|
558
754
|
<a href="/">↩ <span id="lbl-chat"></span></a>
|
|
559
|
-
<a href="/reminders">Reminders</a>
|
|
560
|
-
<a href="/memos">Memos</a>
|
|
755
|
+
<a href="/reminders" id="lnk-reminders">Reminders</a>
|
|
756
|
+
<a href="/memos" id="lnk-memos">Memos</a>
|
|
561
757
|
<a href="/settings" aria-label="Settings"><span id="lbl-settings"></span></a>
|
|
562
758
|
</header>
|
|
563
759
|
<main>
|
|
@@ -570,6 +766,8 @@
|
|
|
570
766
|
<button type="button" class="tab" data-tab="health" id="tab-health"></button>
|
|
571
767
|
<button type="button" class="tab" data-tab="files" id="tab-files"></button>
|
|
572
768
|
<button type="button" class="tab" data-tab="audit" id="tab-audit"></button>
|
|
769
|
+
<button type="button" class="tab" data-tab="outbox" id="tab-outbox">Outbox</button>
|
|
770
|
+
<button type="button" class="tab" data-tab="a2a" id="tab-a2a">A2A</button>
|
|
573
771
|
</div>
|
|
574
772
|
|
|
575
773
|
<section id="jobs-pane">
|
|
@@ -582,6 +780,15 @@
|
|
|
582
780
|
<option value="completed"></option>
|
|
583
781
|
<option value="failed"></option>
|
|
584
782
|
<option value="cancelled"></option>
|
|
783
|
+
<option value="delivered">delivered</option>
|
|
784
|
+
<option value="interrupted">interrupted</option>
|
|
785
|
+
<option value="replaced">replaced</option>
|
|
786
|
+
<option value="abandoned">abandoned</option>
|
|
787
|
+
</select>
|
|
788
|
+
<select id="filter-kind" title="kind filter" data-i18n-attr="title:kindCol">
|
|
789
|
+
<option value="" data-i18n="filterAllKinds">All kinds</option>
|
|
790
|
+
<option value="job" data-i18n="filterKindJob">job (explicit /job)</option>
|
|
791
|
+
<option value="inline" data-i18n="filterKindInline">inline (auto-tracked)</option>
|
|
585
792
|
</select>
|
|
586
793
|
<select id="jobs-agent-filter" data-agent-filter>
|
|
587
794
|
<option value="">All agents</option>
|
|
@@ -689,6 +896,47 @@
|
|
|
689
896
|
<div class="stats" id="audit-stats"></div>
|
|
690
897
|
<div id="audit-list"></div>
|
|
691
898
|
</section>
|
|
899
|
+
|
|
900
|
+
<!-- Outbox tab (v1.1.2) — persistent IM delivery queue -->
|
|
901
|
+
<section id="outbox-pane" hidden>
|
|
902
|
+
<h2 style="margin-top:0;display:flex;align-items:center;gap:.5em">
|
|
903
|
+
<span id="hdr-outbox">Outbox</span>
|
|
904
|
+
<button type="button" class="help-btn" data-help="outbox" title="?">?</button>
|
|
905
|
+
</h2>
|
|
906
|
+
<div class="toolbar">
|
|
907
|
+
<select id="outbox-status-filter">
|
|
908
|
+
<option value="" data-i18n="filterAll">All</option>
|
|
909
|
+
<option value="pending" data-i18n="outboxStatusPending">⏳ Pending</option>
|
|
910
|
+
<option value="delivered" data-i18n="outboxStatusDelivered">✅ Delivered</option>
|
|
911
|
+
<option value="giving_up" data-i18n="outboxStatusGivingUp">💀 Giving up</option>
|
|
912
|
+
</select>
|
|
913
|
+
<select id="outbox-limit">
|
|
914
|
+
<option value="20">20</option>
|
|
915
|
+
<option value="50" selected>50</option>
|
|
916
|
+
<option value="200">200</option>
|
|
917
|
+
</select>
|
|
918
|
+
<button type="button" id="btn-outbox-refresh" data-i18n="refresh">Refresh</button>
|
|
919
|
+
</div>
|
|
920
|
+
<div class="stats" id="outbox-stats"></div>
|
|
921
|
+
<div id="outbox-list"></div>
|
|
922
|
+
</section>
|
|
923
|
+
|
|
924
|
+
<!-- A2A tab (v1.1.3) — agent-to-agent call observability + artifacts viewer -->
|
|
925
|
+
<section id="a2a-pane" hidden>
|
|
926
|
+
<h2 style="margin-top:0;display:flex;align-items:center;gap:.5em">
|
|
927
|
+
<span id="hdr-a2a">A2A</span>
|
|
928
|
+
<button type="button" class="help-btn" data-help="a2a" title="?">?</button>
|
|
929
|
+
<button type="button" class="help-btn" data-help="artifacts" title="?" style="margin-left:-4px" id="hdr-a2a-art-help">📎?</button>
|
|
930
|
+
</h2>
|
|
931
|
+
<div class="toolbar">
|
|
932
|
+
<input type="number" id="a2a-tree-id" min="1" style="width:160px" data-i18n-attr="placeholder:a2aTreeInputJob">
|
|
933
|
+
<button type="button" id="btn-a2a-tree" data-i18n="a2aBtnTree">Tree</button>
|
|
934
|
+
<button type="button" id="btn-a2a-refresh" data-i18n="refresh">Refresh</button>
|
|
935
|
+
</div>
|
|
936
|
+
<div class="stats" id="a2a-stats"></div>
|
|
937
|
+
<div id="a2a-list"></div>
|
|
938
|
+
<div id="a2a-tree"></div>
|
|
939
|
+
</section>
|
|
692
940
|
</main>
|
|
693
941
|
|
|
694
942
|
<div class="modal-bg" id="modal-bg">
|
|
@@ -716,6 +964,67 @@
|
|
|
716
964
|
document.getElementById('tab-approvals').textContent = T.tabsApprovals;
|
|
717
965
|
document.getElementById('tab-health').textContent = T.tabsHealth;
|
|
718
966
|
document.getElementById('tab-files').textContent = T.tabsFiles;
|
|
967
|
+
document.getElementById('tab-outbox').textContent = T.tabsOutbox;
|
|
968
|
+
document.getElementById('tab-a2a').textContent = T.tabsA2A;
|
|
969
|
+
document.getElementById('hdr-outbox').textContent = T.tabsOutbox;
|
|
970
|
+
document.getElementById('hdr-a2a').textContent = T.tabsA2A;
|
|
971
|
+
|
|
972
|
+
// data-i18n / data-i18n-attr — sweep static markers so placeholders +
|
|
973
|
+
// option labels pick up the active language. Same shape as _app.js's
|
|
974
|
+
// applyI18n but inline here (tasks.html runs before _app.js applies
|
|
975
|
+
// its sweep to the rest of the doc).
|
|
976
|
+
(function applyStaticI18n() {
|
|
977
|
+
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
|
978
|
+
const k = el.getAttribute('data-i18n');
|
|
979
|
+
if (k && T[k] != null) el.textContent = T[k];
|
|
980
|
+
});
|
|
981
|
+
document.querySelectorAll('[data-i18n-attr]').forEach((el) => {
|
|
982
|
+
const spec = el.getAttribute('data-i18n-attr') || '';
|
|
983
|
+
for (const pair of spec.split(';')) {
|
|
984
|
+
const [attr, key] = pair.split(':').map(s => s && s.trim());
|
|
985
|
+
if (attr && key && T[key] != null) el.setAttribute(attr, T[key]);
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
})();
|
|
989
|
+
|
|
990
|
+
// Help-tooltip system — every (?) button opens a centered modal whose
|
|
991
|
+
// body comes from T.help[<key>]. Reuses the existing modal-bg overlay
|
|
992
|
+
// so a single Escape press dismisses everything.
|
|
993
|
+
document.addEventListener('click', (ev) => {
|
|
994
|
+
const btn = ev.target.closest && ev.target.closest('.help-btn');
|
|
995
|
+
if (!btn) return;
|
|
996
|
+
ev.preventDefault();
|
|
997
|
+
const key = btn.getAttribute('data-help');
|
|
998
|
+
const def = (T.help && T.help[key]) || null;
|
|
999
|
+
if (!def) { console.warn('no help def for', key); return; }
|
|
1000
|
+
const m = document.getElementById('modal');
|
|
1001
|
+
m.classList.add('help-modal');
|
|
1002
|
+
m.innerHTML = `
|
|
1003
|
+
<h2>${def.title}</h2>
|
|
1004
|
+
<p>${def.body}</p>
|
|
1005
|
+
<div class="modal-actions">
|
|
1006
|
+
<button type="button" id="help-modal-close">${T.helpClose}</button>
|
|
1007
|
+
</div>
|
|
1008
|
+
`;
|
|
1009
|
+
document.getElementById('modal-bg').classList.add('show');
|
|
1010
|
+
document.getElementById('help-modal-close').onclick = () => {
|
|
1011
|
+
document.getElementById('modal-bg').classList.remove('show');
|
|
1012
|
+
m.classList.remove('help-modal');
|
|
1013
|
+
};
|
|
1014
|
+
});
|
|
1015
|
+
// Language switcher: persist + reload so the IIFE in <head> reads the
|
|
1016
|
+
// new preference and rebuilds T.
|
|
1017
|
+
(function setupLangSwitcher() {
|
|
1018
|
+
const sel = document.getElementById('langSelect');
|
|
1019
|
+
if (!sel) return;
|
|
1020
|
+
sel.value = window.__lang;
|
|
1021
|
+
sel.addEventListener('change', () => {
|
|
1022
|
+
const newLang = sel.value;
|
|
1023
|
+
if (newLang === window.__lang) return;
|
|
1024
|
+
localStorage.setItem('im-hub-lang', newLang);
|
|
1025
|
+
window.location.reload();
|
|
1026
|
+
});
|
|
1027
|
+
})();
|
|
719
1028
|
document.getElementById('btn-bg-refresh').textContent = T.refresh;
|
|
720
1029
|
document.getElementById('btn-sub-refresh').textContent = T.refresh;
|
|
721
1030
|
document.getElementById('btn-approvals-refresh').textContent = T.refresh;
|
|
@@ -754,6 +1063,8 @@
|
|
|
754
1063
|
document.getElementById('approvals-pane').hidden = tab !== 'approvals';
|
|
755
1064
|
document.getElementById('health-pane').hidden = tab !== 'health';
|
|
756
1065
|
document.getElementById('files-pane').hidden = tab !== 'files';
|
|
1066
|
+
document.getElementById('outbox-pane').hidden = tab !== 'outbox';
|
|
1067
|
+
document.getElementById('a2a-pane').hidden = tab !== 'a2a';
|
|
757
1068
|
// Lazy-load on first activation; auto-refresh hooks below kick in too.
|
|
758
1069
|
if (tab === 'schedules') loadSchedules();
|
|
759
1070
|
if (tab === 'background') { ensureBgRootsLoaded().then(loadBgjobs); }
|
|
@@ -762,6 +1073,8 @@
|
|
|
762
1073
|
if (tab === 'approvals') loadApprovals();
|
|
763
1074
|
if (tab === 'health') loadHealth();
|
|
764
1075
|
if (tab === 'files') ensureFilesAgentLoaded().then(() => loadFiles(filesPath));
|
|
1076
|
+
if (tab === 'outbox') loadOutbox();
|
|
1077
|
+
if (tab === 'a2a') loadA2A();
|
|
765
1078
|
// Pause/resume auto-refresh so hidden tabs don't poll.
|
|
766
1079
|
setupBgAutoRefresh();
|
|
767
1080
|
setupApprovalsAutoRefresh();
|
|
@@ -779,7 +1092,15 @@
|
|
|
779
1092
|
|
|
780
1093
|
function fmtTime(s) {
|
|
781
1094
|
if (!s) return '-';
|
|
782
|
-
|
|
1095
|
+
// SQLite datetime('now') produces "2026-05-15 14:02:50" — space-separated
|
|
1096
|
+
// and no zone. Safari + iOS WebView refuse that string (Invalid Date);
|
|
1097
|
+
// Chrome happens to parse it. Normalize to RFC3339 by swapping space for
|
|
1098
|
+
// T and forcing UTC. Also accept already-ISO strings unchanged.
|
|
1099
|
+
let iso = String(s).trim();
|
|
1100
|
+
if (iso && !iso.includes('T')) iso = iso.replace(' ', 'T');
|
|
1101
|
+
if (iso && !/[Z+]/.test(iso.slice(-6))) iso = iso + 'Z';
|
|
1102
|
+
const d = new Date(iso);
|
|
1103
|
+
if (Number.isNaN(d.getTime())) return s; // fall back to raw if still bad
|
|
783
1104
|
return d.toLocaleString();
|
|
784
1105
|
}
|
|
785
1106
|
|
|
@@ -826,7 +1147,10 @@
|
|
|
826
1147
|
<tr>
|
|
827
1148
|
<th><input type="checkbox" id="job-select-all" ${allSelected ? 'checked' : ''} title="${T.jobsSelectAll}"></th>
|
|
828
1149
|
<th>#</th>
|
|
1150
|
+
<th>${T.kindCol} <button type="button" class="help-btn" data-help="inline" title="?">?</button></th>
|
|
829
1151
|
<th>${T.agent}</th>
|
|
1152
|
+
<th>${T.parentCol} <button type="button" class="help-btn" data-help="parent" title="?">?</button></th>
|
|
1153
|
+
<th>${T.depthCol} <button type="button" class="help-btn" data-help="callDepth" title="?">?</button></th>
|
|
830
1154
|
<th>${T.prompt}</th>
|
|
831
1155
|
<th>${T.status}</th>
|
|
832
1156
|
<th>${T.created}</th>
|
|
@@ -834,11 +1158,21 @@
|
|
|
834
1158
|
</tr>
|
|
835
1159
|
</thead>
|
|
836
1160
|
<tbody>
|
|
837
|
-
${jobs.map(j =>
|
|
1161
|
+
${jobs.map(j => {
|
|
1162
|
+
const kind = j.kind || 'job';
|
|
1163
|
+
const kindPill = `<span class="pill kind-${kind}">${esc(kind)}</span>`;
|
|
1164
|
+
const parentLink = (j.parent_id != null && j.parent_id > 0)
|
|
1165
|
+
? `<a href="#" data-act="parent" data-pid="${j.parent_id}" title="View parent">#${j.parent_id}</a>` : '';
|
|
1166
|
+
const depth = (typeof j.call_depth === 'number' && j.call_depth > 0)
|
|
1167
|
+
? `<span style="font-size:11px;opacity:0.8">↳${j.call_depth}</span>` : '';
|
|
1168
|
+
return `
|
|
838
1169
|
<tr data-id="${j.id}">
|
|
839
1170
|
<td><input type="checkbox" data-sel="${j.id}" ${jobSelection.has(j.id) ? 'checked' : ''}></td>
|
|
840
1171
|
<td>#${j.id}</td>
|
|
1172
|
+
<td>${kindPill}</td>
|
|
841
1173
|
<td><code>${esc(j.agent)}</code></td>
|
|
1174
|
+
<td>${parentLink}</td>
|
|
1175
|
+
<td>${depth}</td>
|
|
842
1176
|
<td class="truncate">${esc(j.prompt)}</td>
|
|
843
1177
|
<td>${statusPill(j.status)}</td>
|
|
844
1178
|
<td>${fmtTime(j.created_at)}</td>
|
|
@@ -849,8 +1183,8 @@
|
|
|
849
1183
|
${j.status === 'pending' || j.status === 'running'
|
|
850
1184
|
? `<button type="button" data-act="cancel" class="danger">${T.cancel}</button>` : ''}
|
|
851
1185
|
</td>
|
|
852
|
-
</tr
|
|
853
|
-
|
|
1186
|
+
</tr>`;
|
|
1187
|
+
}).join('')}
|
|
854
1188
|
</tbody>
|
|
855
1189
|
</table>
|
|
856
1190
|
`;
|
|
@@ -859,6 +1193,11 @@
|
|
|
859
1193
|
tr.querySelector('[data-act="view"]')?.addEventListener('click', () => showJob(id));
|
|
860
1194
|
tr.querySelector('[data-act="run"]')?.addEventListener('click', () => runJob(id));
|
|
861
1195
|
tr.querySelector('[data-act="cancel"]')?.addEventListener('click', () => cancelJob(id));
|
|
1196
|
+
tr.querySelector('[data-act="parent"]')?.addEventListener('click', (e) => {
|
|
1197
|
+
e.preventDefault();
|
|
1198
|
+
const pid = parseInt(e.currentTarget.getAttribute('data-pid'), 10);
|
|
1199
|
+
if (pid > 0) showJob(pid);
|
|
1200
|
+
});
|
|
862
1201
|
tr.querySelector('[data-sel]')?.addEventListener('change', (e) => {
|
|
863
1202
|
if (e.target.checked) jobSelection.add(id);
|
|
864
1203
|
else jobSelection.delete(id);
|
|
@@ -914,9 +1253,11 @@
|
|
|
914
1253
|
async function loadJobs() {
|
|
915
1254
|
const status = document.getElementById('filter-status').value;
|
|
916
1255
|
const agent = document.getElementById('jobs-agent-filter').value;
|
|
1256
|
+
const kind = document.getElementById('filter-kind')?.value || '';
|
|
917
1257
|
const qs = new URLSearchParams({ limit: '100' });
|
|
918
1258
|
if (status) qs.set('status', status);
|
|
919
1259
|
if (agent) qs.set('agent', agent);
|
|
1260
|
+
if (kind) qs.set('kind', kind);
|
|
920
1261
|
try {
|
|
921
1262
|
const { jobs, stats } = await api(`/api/jobs?${qs.toString()}`);
|
|
922
1263
|
renderStats(stats);
|
|
@@ -926,18 +1267,46 @@
|
|
|
926
1267
|
`<div class="empty">${T.error}: ${esc(e.message)}</div>`;
|
|
927
1268
|
}
|
|
928
1269
|
}
|
|
1270
|
+
document.getElementById('filter-kind')?.addEventListener('change', loadJobs);
|
|
929
1271
|
|
|
930
1272
|
async function showJob(id) {
|
|
931
1273
|
const { job } = await api(`/api/jobs/${id}`);
|
|
1274
|
+
// Fetch artifacts in parallel — non-fatal if 404 / empty.
|
|
1275
|
+
let arts = { inputs: [], outputs: [], totalBytes: 0 };
|
|
1276
|
+
try {
|
|
1277
|
+
const r = await fetch(`/api/artifacts/${id}`);
|
|
1278
|
+
if (r.ok) arts = await r.json();
|
|
1279
|
+
} catch { /* ignore — modal still renders */ }
|
|
932
1280
|
const m = document.getElementById('modal');
|
|
1281
|
+
const kind = job.kind || 'job';
|
|
1282
|
+
const kindPill = `<span class="pill kind-${kind}">${esc(kind)}</span>`;
|
|
1283
|
+
const parentLine = (job.parent_id != null && job.parent_id > 0)
|
|
1284
|
+
? `<p><strong>${T.modalParent}:</strong> <a href="#" data-act="modal-parent" data-pid="${job.parent_id}">#${job.parent_id}</a> · <strong>${T.modalDepth}:</strong> ${job.call_depth || 0}</p>` : '';
|
|
1285
|
+
const threadLine = job.thread_key
|
|
1286
|
+
? `<p><strong>${T.modalThread}:</strong> <code>${esc(job.thread_key)}</code></p>` : '';
|
|
1287
|
+
const replacedLine = (job.replaced_by != null && job.replaced_by > 0)
|
|
1288
|
+
? `<p><strong>${T.modalReplaced}:</strong> <a href="#" data-act="modal-parent" data-pid="${job.replaced_by}">#${job.replaced_by}</a></p>` : '';
|
|
1289
|
+
const outboxLine = (job.last_outbox_id != null && job.last_outbox_id > 0)
|
|
1290
|
+
? `<p><strong>${T.modalLastOutbox}:</strong> #${job.last_outbox_id}</p>` : '';
|
|
1291
|
+
const fmt = (n) => n < 1024 ? `${n} B` : n < 1024*1024 ? `${(n/1024).toFixed(1)} KB` : `${(n/1024/1024).toFixed(1)} MB`;
|
|
1292
|
+
const artsBlock = (arts.outputs.length > 0 || arts.inputs.length > 0) ? `
|
|
1293
|
+
<h3>📎 ${T.modalArtifacts} (${fmt(arts.totalBytes || 0)}) <button type="button" class="help-btn" data-help="artifacts" title="?">?</button></h3>
|
|
1294
|
+
${arts.inputs.length > 0 ? `<p style="font-size:13px;color:var(--muted)">${T.modalArtifactsInputs}</p><ul>${arts.inputs.map(f => `<li><a href="/api/artifacts/${id}/file/${encodeURIComponent(f.name)}" target="_blank">${esc(f.name)}</a> (${fmt(f.bytes)})</li>`).join('')}</ul>` : ''}
|
|
1295
|
+
${arts.outputs.length > 0 ? `<p style="font-size:13px;color:var(--muted)">${T.modalArtifactsOutputs}</p><ul>${arts.outputs.map(f => `<li><a href="/api/artifacts/${id}/file/${encodeURIComponent(f.name)}" target="_blank">${esc(f.name)}</a> (${fmt(f.bytes)})</li>`).join('')}</ul>` : ''}
|
|
1296
|
+
` : '';
|
|
933
1297
|
m.innerHTML = `
|
|
934
|
-
<h2>${T.details} #${job.id}</h2>
|
|
1298
|
+
<h2>${T.details} #${job.id} ${kindPill}</h2>
|
|
935
1299
|
<p><strong>${T.agent}:</strong> <code>${esc(job.agent)}</code> · <strong>${T.status}:</strong> ${statusPill(job.status)}</p>
|
|
936
|
-
|
|
1300
|
+
${parentLine}
|
|
1301
|
+
${threadLine}
|
|
1302
|
+
${replacedLine}
|
|
1303
|
+
${outboxLine}
|
|
1304
|
+
<p><strong>${T.created}:</strong> ${fmtTime(job.created_at)}${job.completed_at ? ` · <strong>${T.completed}:</strong> ${fmtTime(job.completed_at)}` : ''}${job.delivered_at ? ` · <strong>${T.modalDelivered}:</strong> ${fmtTime(job.delivered_at)}` : ''}</p>
|
|
937
1305
|
<h3>${T.prompt}</h3>
|
|
938
1306
|
<pre>${esc(job.prompt)}</pre>
|
|
939
1307
|
${job.result ? `<h3>${T.result}</h3><pre>${esc(job.result)}</pre>` : ''}
|
|
940
1308
|
${job.error ? `<h3>${T.error}</h3><pre>${esc(job.error)}</pre>` : ''}
|
|
1309
|
+
${artsBlock}
|
|
941
1310
|
<div class="modal-actions">
|
|
942
1311
|
<button type="button" id="modal-close">${T.close}</button>
|
|
943
1312
|
</div>
|
|
@@ -945,6 +1314,13 @@
|
|
|
945
1314
|
document.getElementById('modal-bg').classList.add('show');
|
|
946
1315
|
document.getElementById('modal-close').onclick = () =>
|
|
947
1316
|
document.getElementById('modal-bg').classList.remove('show');
|
|
1317
|
+
m.querySelectorAll('[data-act="modal-parent"]').forEach((a) => {
|
|
1318
|
+
a.addEventListener('click', (e) => {
|
|
1319
|
+
e.preventDefault();
|
|
1320
|
+
const pid = parseInt(e.currentTarget.getAttribute('data-pid'), 10);
|
|
1321
|
+
if (pid > 0) showJob(pid);
|
|
1322
|
+
});
|
|
1323
|
+
});
|
|
948
1324
|
}
|
|
949
1325
|
|
|
950
1326
|
async function runJob(id) {
|
|
@@ -1817,6 +2193,177 @@
|
|
|
1817
2193
|
|
|
1818
2194
|
setupSSE();
|
|
1819
2195
|
|
|
2196
|
+
// ============================================
|
|
2197
|
+
// Outbox tab (v1.1.2)
|
|
2198
|
+
// ============================================
|
|
2199
|
+
|
|
2200
|
+
function escapeHtml(s) {
|
|
2201
|
+
return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
async function loadOutbox() {
|
|
2205
|
+
const statusEl = document.getElementById('outbox-status-filter');
|
|
2206
|
+
const limitEl = document.getElementById('outbox-limit');
|
|
2207
|
+
const statusFilter = statusEl ? statusEl.value : '';
|
|
2208
|
+
const limit = limitEl ? limitEl.value : '50';
|
|
2209
|
+
try {
|
|
2210
|
+
const statsResp = await fetch('/api/outbox/stats');
|
|
2211
|
+
const stats = await statsResp.json();
|
|
2212
|
+
const statsEl = document.getElementById('outbox-stats');
|
|
2213
|
+
if (statsEl) {
|
|
2214
|
+
statsEl.innerHTML =
|
|
2215
|
+
`<span class="stat">${T.outboxStatusPending} <b>${stats.pending ?? 0}</b></span>` +
|
|
2216
|
+
`<span class="stat">${T.outboxStatusDelivered} <b>${stats.delivered ?? 0}</b></span>` +
|
|
2217
|
+
`<span class="stat">${T.outboxStatusGivingUp} <b>${stats.giving_up ?? 0}</b></span> ` +
|
|
2218
|
+
`<button type="button" class="help-btn" data-help="givingUp" title="?">?</button>`;
|
|
2219
|
+
}
|
|
2220
|
+
const qs = new URLSearchParams({ limit });
|
|
2221
|
+
if (statusFilter) qs.set('status', statusFilter);
|
|
2222
|
+
const listResp = await fetch('/api/outbox?' + qs);
|
|
2223
|
+
const { rows = [] } = await listResp.json();
|
|
2224
|
+
const listEl = document.getElementById('outbox-list');
|
|
2225
|
+
if (!rows.length) { listEl.innerHTML = `<p style="color:var(--text-muted)">${T.outboxEmpty}</p>`; return; }
|
|
2226
|
+
const html = ['<table class="data-table"><thead><tr>',
|
|
2227
|
+
`<th>#</th><th>${T.status}</th><th>${T.outboxColPlatform}</th><th>${T.outboxColKind}</th><th>${T.outboxColPri}</th>`,
|
|
2228
|
+
`<th>${T.outboxColAttempts}</th><th>${T.outboxColPayload}</th><th>${T.outboxColError}</th><th>${T.outboxColCreated}</th><th></th>`,
|
|
2229
|
+
'</tr></thead><tbody>'];
|
|
2230
|
+
for (const r of rows) {
|
|
2231
|
+
const icon = r.status === 'delivered' ? '✅' : r.status === 'giving_up' ? '💀' : '⏳';
|
|
2232
|
+
const preview = (r.payload || '').slice(0, 60).replace(/\n/g, ' ');
|
|
2233
|
+
const errCell = r.last_error ? `<span title="${escapeHtml(r.last_error)}">${escapeHtml(r.last_error.slice(0, 40))}…</span>` : '';
|
|
2234
|
+
const ts = fmtTime(r.created_at);
|
|
2235
|
+
const retryBtn = r.status === 'giving_up'
|
|
2236
|
+
? `<button class="btn-link" data-retry="${r.id}">${T.outboxRetry}</button>` : '';
|
|
2237
|
+
html.push(`<tr>
|
|
2238
|
+
<td>#${r.id}</td>
|
|
2239
|
+
<td>${icon} ${escapeHtml(r.status)}</td>
|
|
2240
|
+
<td>${escapeHtml(r.platform)}</td>
|
|
2241
|
+
<td>${escapeHtml(r.kind)}</td>
|
|
2242
|
+
<td>${escapeHtml(r.priority)}</td>
|
|
2243
|
+
<td>${r.attempts}</td>
|
|
2244
|
+
<td title="${escapeHtml(r.payload || '')}">${escapeHtml(preview)}…</td>
|
|
2245
|
+
<td>${errCell}</td>
|
|
2246
|
+
<td>${ts}</td>
|
|
2247
|
+
<td>${retryBtn}</td>
|
|
2248
|
+
</tr>`);
|
|
2249
|
+
}
|
|
2250
|
+
html.push('</tbody></table>');
|
|
2251
|
+
listEl.innerHTML = html.join('');
|
|
2252
|
+
listEl.querySelectorAll('[data-retry]').forEach((btn) => {
|
|
2253
|
+
btn.addEventListener('click', async () => {
|
|
2254
|
+
const id = btn.getAttribute('data-retry');
|
|
2255
|
+
const r = await fetch(`/api/outbox/${id}/retry`, { method: 'POST' });
|
|
2256
|
+
const j = await r.json();
|
|
2257
|
+
if (j.ok) { loadOutbox(); }
|
|
2258
|
+
else alert('Retry failed: ' + (j.error || 'unknown'));
|
|
2259
|
+
});
|
|
2260
|
+
});
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
document.getElementById('outbox-list').innerHTML = `<p style="color:#d00">Failed: ${escapeHtml(String(err))}</p>`;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
document.getElementById('btn-outbox-refresh')?.addEventListener('click', loadOutbox);
|
|
2266
|
+
document.getElementById('outbox-status-filter')?.addEventListener('change', loadOutbox);
|
|
2267
|
+
document.getElementById('outbox-limit')?.addEventListener('change', loadOutbox);
|
|
2268
|
+
|
|
2269
|
+
// ============================================
|
|
2270
|
+
// A2A tab (v1.1.3)
|
|
2271
|
+
// ============================================
|
|
2272
|
+
|
|
2273
|
+
async function loadA2A() {
|
|
2274
|
+
try {
|
|
2275
|
+
const [statsResp, recentResp] = await Promise.all([
|
|
2276
|
+
fetch('/api/a2a/stats'),
|
|
2277
|
+
fetch('/api/a2a/recent?limit=20'),
|
|
2278
|
+
]);
|
|
2279
|
+
const stats = await statsResp.json();
|
|
2280
|
+
const recent = (await recentResp.json()).rows || [];
|
|
2281
|
+
const statsEl = document.getElementById('a2a-stats');
|
|
2282
|
+
if (statsEl) {
|
|
2283
|
+
const byStatus = (stats.byStatus || []).map(s => `${s.status}:${s.n}`).join(' · ');
|
|
2284
|
+
const byAgent = (stats.byAgent || []).map(s => `${s.agent} <b>${s.n}</b>`).join(' / ');
|
|
2285
|
+
statsEl.innerHTML =
|
|
2286
|
+
`<span class="stat">${T.a2aStatTotal} <b>${stats.total ?? 0}</b></span>` +
|
|
2287
|
+
`<span class="stat">${T.a2aStat24h} <b>${stats.recent24h ?? 0}</b></span>` +
|
|
2288
|
+
`<span class="stat">${T.a2aStatMaxDepth} <b>${stats.maxDepth ?? 0}</b></span>` +
|
|
2289
|
+
(byStatus ? `<span class="stat">${byStatus}</span>` : '') +
|
|
2290
|
+
(byAgent ? `<span class="stat">${T.a2aStatTop} ${byAgent}</span>` : '');
|
|
2291
|
+
}
|
|
2292
|
+
const listEl = document.getElementById('a2a-list');
|
|
2293
|
+
if (!recent.length) { listEl.innerHTML = `<p style="color:var(--text-muted)">${T.a2aEmpty}</p>`; return; }
|
|
2294
|
+
const html = [`<h3 style="margin-top:1em">${T.a2aRecent}</h3>`,
|
|
2295
|
+
'<table class="data-table"><thead><tr>',
|
|
2296
|
+
`<th>#</th><th>${T.a2aCallerCallee}</th><th>${T.depthCol}</th><th>${T.status}</th>`,
|
|
2297
|
+
`<th>${T.prompt}</th><th>${T.created}</th><th>${T.modalDelivered}</th><th></th>`,
|
|
2298
|
+
'</tr></thead><tbody>'];
|
|
2299
|
+
for (const r of recent) {
|
|
2300
|
+
const icon = r.status === 'delivered' || r.status === 'completed' ? '✅'
|
|
2301
|
+
: r.status === 'failed' ? '❌'
|
|
2302
|
+
: r.status === 'running' ? '🔄' : '⏳';
|
|
2303
|
+
const preview = (r.prompt || '').slice(0, 50).replace(/\n/g, ' ');
|
|
2304
|
+
const started = fmtTime(r.started_at);
|
|
2305
|
+
const done = fmtTime(r.completed_at || r.delivered_at);
|
|
2306
|
+
html.push(`<tr>
|
|
2307
|
+
<td>#${r.id}</td>
|
|
2308
|
+
<td>←#${r.parent_id ?? '?'} → <b>${escapeHtml(r.agent)}</b></td>
|
|
2309
|
+
<td>${r.call_depth}</td>
|
|
2310
|
+
<td>${icon} ${escapeHtml(r.status)}</td>
|
|
2311
|
+
<td title="${escapeHtml(r.prompt || '')}">${escapeHtml(preview)}…</td>
|
|
2312
|
+
<td>${started}</td>
|
|
2313
|
+
<td>${done}</td>
|
|
2314
|
+
<td><button class="btn-link" data-tree="${r.parent_id}">${T.a2aBtnViewParent}</button></td>
|
|
2315
|
+
</tr>`);
|
|
2316
|
+
}
|
|
2317
|
+
html.push('</tbody></table>');
|
|
2318
|
+
listEl.innerHTML = html.join('');
|
|
2319
|
+
listEl.querySelectorAll('[data-tree]').forEach((btn) => {
|
|
2320
|
+
btn.addEventListener('click', () => {
|
|
2321
|
+
const id = btn.getAttribute('data-tree');
|
|
2322
|
+
if (!id || id === 'null') return;
|
|
2323
|
+
document.getElementById('a2a-tree-id').value = id;
|
|
2324
|
+
loadA2ATree(id);
|
|
2325
|
+
});
|
|
2326
|
+
});
|
|
2327
|
+
} catch (err) {
|
|
2328
|
+
document.getElementById('a2a-list').innerHTML = `<p style="color:#d00">Failed: ${escapeHtml(String(err))}</p>`;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
async function loadA2ATree(rootId) {
|
|
2333
|
+
const treeEl = document.getElementById('a2a-tree');
|
|
2334
|
+
if (!rootId) return;
|
|
2335
|
+
treeEl.innerHTML = `<p style="color:var(--text-muted)">${T.loading}</p>`;
|
|
2336
|
+
try {
|
|
2337
|
+
const resp = await fetch(`/api/a2a/tree/${rootId}`);
|
|
2338
|
+
const j = await resp.json();
|
|
2339
|
+
if (!resp.ok) { treeEl.innerHTML = `<p style="color:#d00">${escapeHtml(j.error || 'failed')}</p>`; return; }
|
|
2340
|
+
const lines = [`<h3 style="margin-top:1em">🌳 ${T.a2aTreeRoot.replace('{id}', String(rootId))}</h3>`];
|
|
2341
|
+
function render(node, depth) {
|
|
2342
|
+
const indent = ' '.repeat(depth * 4);
|
|
2343
|
+
const icon = node.status === 'delivered' || node.status === 'completed' ? '✅'
|
|
2344
|
+
: node.status === 'failed' ? '❌'
|
|
2345
|
+
: node.status === 'running' ? '🔄' : '⏳';
|
|
2346
|
+
const preview = (node.prompt || '').slice(0, 70).replace(/\n/g, ' ');
|
|
2347
|
+
lines.push(`<div>${indent}${icon} <b>#${node.id}</b> ${escapeHtml(node.agent)} [${escapeHtml(node.status)}] ${escapeHtml(preview)}…</div>`);
|
|
2348
|
+
for (const f of (node.outputs || [])) {
|
|
2349
|
+
const fmt = (n) => n < 1024 ? `${n} B` : n < 1024*1024 ? `${(n/1024).toFixed(1)} KB` : `${(n/1024/1024).toFixed(1)} MB`;
|
|
2350
|
+
lines.push(`<div>${indent} 📎 <a href="/api/artifacts/${node.id}/file/${encodeURIComponent(f.name)}" target="_blank">${escapeHtml(f.name)}</a> (${fmt(f.bytes)})</div>`);
|
|
2351
|
+
}
|
|
2352
|
+
for (const c of (node.children || [])) render(c, depth + 1);
|
|
2353
|
+
}
|
|
2354
|
+
render(j.tree, 0);
|
|
2355
|
+
treeEl.innerHTML = `<div style="font-family:monospace;font-size:13px;line-height:1.6">${lines.join('')}</div>`;
|
|
2356
|
+
} catch (err) {
|
|
2357
|
+
treeEl.innerHTML = `<p style="color:#d00">Tree failed: ${escapeHtml(String(err))}</p>`;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
document.getElementById('btn-a2a-refresh')?.addEventListener('click', loadA2A);
|
|
2362
|
+
document.getElementById('btn-a2a-tree')?.addEventListener('click', () => {
|
|
2363
|
+
const id = document.getElementById('a2a-tree-id').value;
|
|
2364
|
+
if (id) loadA2ATree(id);
|
|
2365
|
+
});
|
|
2366
|
+
|
|
1820
2367
|
// Initial load
|
|
1821
2368
|
loadJobs();
|
|
1822
2369
|
</script>
|