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