agim-cli 1.2.2 → 1.2.18
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 +22 -0
- package/README.md +18 -5
- package/README.zh-CN.md +20 -7
- package/dist/cli.js +42 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/commands/builtin.d.ts.map +1 -1
- package/dist/core/commands/builtin.js +2 -0
- package/dist/core/commands/builtin.js.map +1 -1
- package/dist/core/commands/router.js +1 -1
- package/dist/core/commands/router.js.map +1 -1
- package/dist/core/commands/web.d.ts +3 -0
- package/dist/core/commands/web.d.ts.map +1 -0
- package/dist/core/commands/web.js +28 -0
- package/dist/core/commands/web.js.map +1 -0
- package/dist/core/intent.d.ts +11 -2
- package/dist/core/intent.d.ts.map +1 -1
- package/dist/core/intent.js +26 -4
- package/dist/core/intent.js.map +1 -1
- package/dist/core/memory.js.map +1 -1
- package/dist/core/render-router.d.ts.map +1 -1
- package/dist/core/render-router.js +3 -2
- package/dist/core/render-router.js.map +1 -1
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +8 -1
- package/dist/core/router.js.map +1 -1
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -1
- package/dist/plugins/agents/acp/acp-client.js +7 -0
- package/dist/plugins/agents/acp/acp-client.js.map +1 -1
- package/dist/plugins/agents/acp/discovery.d.ts.map +1 -1
- package/dist/plugins/agents/acp/discovery.js +4 -0
- package/dist/plugins/agents/acp/discovery.js.map +1 -1
- package/dist/plugins/agents/acp/url-guard.d.ts +44 -0
- package/dist/plugins/agents/acp/url-guard.d.ts.map +1 -0
- package/dist/plugins/agents/acp/url-guard.js +109 -0
- package/dist/plugins/agents/acp/url-guard.js.map +1 -0
- package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -1
- package/dist/plugins/agents/opencode/serve-manager.js +129 -4
- package/dist/plugins/agents/opencode/serve-manager.js.map +1 -1
- package/dist/web/env-mask.d.ts +21 -0
- package/dist/web/env-mask.d.ts.map +1 -0
- package/dist/web/env-mask.js +44 -0
- package/dist/web/env-mask.js.map +1 -0
- package/dist/web/public/assets/a2a-Dk2fSs33.js +7 -0
- package/dist/web/public/assets/a2a-Dk2fSs33.js.map +1 -0
- package/dist/web/public/assets/activity-eiIPshcV.js +7 -0
- package/dist/web/public/assets/activity-eiIPshcV.js.map +1 -0
- package/dist/web/public/assets/admins-DlbQYdW_.js +12 -0
- package/dist/web/public/assets/admins-DlbQYdW_.js.map +1 -0
- package/dist/web/public/assets/agents-BMI1WbZj.js +12 -0
- package/dist/web/public/assets/agents-BMI1WbZj.js.map +1 -0
- package/dist/web/public/assets/approvals-DlXS_sKD.js +10 -0
- package/dist/web/public/assets/approvals-DlXS_sKD.js.map +1 -0
- package/dist/web/public/assets/audit-C8I8xC_6.js +2 -0
- package/dist/web/public/assets/audit-C8I8xC_6.js.map +1 -0
- package/dist/web/public/assets/bgjobs-PFYinH7D.js +7 -0
- package/dist/web/public/assets/bgjobs-PFYinH7D.js.map +1 -0
- package/dist/web/public/assets/brain-DEEJttEL.js +7 -0
- package/dist/web/public/assets/brain-DEEJttEL.js.map +1 -0
- package/dist/web/public/assets/briefcase-BlMy8gI6.js +7 -0
- package/dist/web/public/assets/briefcase-BlMy8gI6.js.map +1 -0
- package/dist/web/public/assets/browser-ponyfill-BOcGq8h9.js +3 -0
- package/dist/web/public/assets/browser-ponyfill-BOcGq8h9.js.map +1 -0
- package/dist/web/public/assets/chevron-right-DmABPvoA.js +7 -0
- package/dist/web/public/assets/chevron-right-DmABPvoA.js.map +1 -0
- package/dist/web/public/assets/circle-check-C0Qpg1vL.js +7 -0
- package/dist/web/public/assets/circle-check-C0Qpg1vL.js.map +1 -0
- package/dist/web/public/assets/circle-check-big-C8LG3beV.js +7 -0
- package/dist/web/public/assets/circle-check-big-C8LG3beV.js.map +1 -0
- package/dist/web/public/assets/circle-x-D_cRHcHK.js +7 -0
- package/dist/web/public/assets/circle-x-D_cRHcHK.js.map +1 -0
- package/dist/web/public/assets/confirm-dialog-Baz_xFle.js +2 -0
- package/dist/web/public/assets/confirm-dialog-Baz_xFle.js.map +1 -0
- package/dist/web/public/assets/data-table--I_ktDF4.js +17 -0
- package/dist/web/public/assets/data-table--I_ktDF4.js.map +1 -0
- package/dist/web/public/assets/dialog-DZpoEskO.js +6 -0
- package/dist/web/public/assets/dialog-DZpoEskO.js.map +1 -0
- package/dist/web/public/assets/download-DbFGHwZ5.js +7 -0
- package/dist/web/public/assets/download-DbFGHwZ5.js.map +1 -0
- package/dist/web/public/assets/email-BB1Hq8eE.js +7 -0
- package/dist/web/public/assets/email-BB1Hq8eE.js.map +1 -0
- package/dist/web/public/assets/empty-state-DXNa90pP.js +2 -0
- package/dist/web/public/assets/empty-state-DXNa90pP.js.map +1 -0
- package/dist/web/public/assets/env-Bqrb9XkC.js +2 -0
- package/dist/web/public/assets/env-Bqrb9XkC.js.map +1 -0
- package/dist/web/public/assets/external-link-nhnJN0qg.js +7 -0
- package/dist/web/public/assets/external-link-nhnJN0qg.js.map +1 -0
- package/dist/web/public/assets/eye-IKkn_oUo.js +12 -0
- package/dist/web/public/assets/eye-IKkn_oUo.js.map +1 -0
- package/dist/web/public/assets/facts-C7Qy9vTw.js +2 -0
- package/dist/web/public/assets/facts-C7Qy9vTw.js.map +1 -0
- package/dist/web/public/assets/health-CMRdeNEW.js +2 -0
- package/dist/web/public/assets/health-CMRdeNEW.js.map +1 -0
- package/dist/web/public/assets/hot-Bh5Nrc7i.js +17 -0
- package/dist/web/public/assets/hot-Bh5Nrc7i.js.map +1 -0
- package/dist/web/public/assets/index-CpGWCLE5.js +166 -0
- package/dist/web/public/assets/index-CpGWCLE5.js.map +1 -0
- package/dist/web/public/assets/index-GpceOxum.css +1 -0
- package/dist/web/public/assets/installed-FYLkPij2.js +7 -0
- package/dist/web/public/assets/installed-FYLkPij2.js.map +1 -0
- package/dist/web/public/assets/jobs-BmqLUzHp.js +2 -0
- package/dist/web/public/assets/jobs-BmqLUzHp.js.map +1 -0
- package/dist/web/public/assets/layout-9Gp_myEd.js +2 -0
- package/dist/web/public/assets/layout-9Gp_myEd.js.map +1 -0
- package/dist/web/public/assets/layout-BZaHqf69.js +2 -0
- package/dist/web/public/assets/layout-BZaHqf69.js.map +1 -0
- package/dist/web/public/assets/layout-CXsUyEpG.js +2 -0
- package/dist/web/public/assets/layout-CXsUyEpG.js.map +1 -0
- package/dist/web/public/assets/layout-DFxtpNut.js +2 -0
- package/dist/web/public/assets/layout-DFxtpNut.js.map +1 -0
- package/dist/web/public/assets/layout-d8qxPKQk.js +2 -0
- package/dist/web/public/assets/layout-d8qxPKQk.js.map +1 -0
- package/dist/web/public/assets/loader-circle-JaKY-xMt.js +7 -0
- package/dist/web/public/assets/loader-circle-JaKY-xMt.js.map +1 -0
- package/dist/web/public/assets/map-pin-hFFSWZ3B.js +7 -0
- package/dist/web/public/assets/map-pin-hFFSWZ3B.js.map +1 -0
- package/dist/web/public/assets/memos-EhjMUvVZ.js +12 -0
- package/dist/web/public/assets/memos-EhjMUvVZ.js.map +1 -0
- package/dist/web/public/assets/messengers-BRV1IVGX.js +7 -0
- package/dist/web/public/assets/messengers-BRV1IVGX.js.map +1 -0
- package/dist/web/public/assets/network-DtCI2ZUU.js +7 -0
- package/dist/web/public/assets/network-DtCI2ZUU.js.map +1 -0
- package/dist/web/public/assets/outbox-CxUbMp6o.js +7 -0
- package/dist/web/public/assets/outbox-CxUbMp6o.js.map +1 -0
- package/dist/web/public/assets/pagination-CkZY8YNa.js +17 -0
- package/dist/web/public/assets/pagination-CkZY8YNa.js.map +1 -0
- package/dist/web/public/assets/persona-B6TFMSnI.js +2 -0
- package/dist/web/public/assets/persona-B6TFMSnI.js.map +1 -0
- package/dist/web/public/assets/play-BxRcWaH5.js +7 -0
- package/dist/web/public/assets/play-BxRcWaH5.js.map +1 -0
- package/dist/web/public/assets/policy-ndE1Y8zD.js +2 -0
- package/dist/web/public/assets/policy-ndE1Y8zD.js.map +1 -0
- package/dist/web/public/assets/react-C9F3QeMB.js +33 -0
- package/dist/web/public/assets/react-C9F3QeMB.js.map +1 -0
- package/dist/web/public/assets/refresh-ccw-Bx817_KW.js +7 -0
- package/dist/web/public/assets/refresh-ccw-Bx817_KW.js.map +1 -0
- package/dist/web/public/assets/reminders-XynkGQc5.js +17 -0
- package/dist/web/public/assets/reminders-XynkGQc5.js.map +1 -0
- package/dist/web/public/assets/save-CqMcATrh.js +7 -0
- package/dist/web/public/assets/save-CqMcATrh.js.map +1 -0
- package/dist/web/public/assets/schedules-VM02w_Om.js +7 -0
- package/dist/web/public/assets/schedules-VM02w_Om.js.map +1 -0
- package/dist/web/public/assets/search-Ba-e1t1P.js +7 -0
- package/dist/web/public/assets/search-Ba-e1t1P.js.map +1 -0
- package/dist/web/public/assets/service-C-wnwJ-b.js +7 -0
- package/dist/web/public/assets/service-C-wnwJ-b.js.map +1 -0
- package/dist/web/public/assets/status-badge-CsdJ6k8Q.js +2 -0
- package/dist/web/public/assets/status-badge-CsdJ6k8Q.js.map +1 -0
- package/dist/web/public/assets/subtasks-mGRKpF0G.js +7 -0
- package/dist/web/public/assets/subtasks-mGRKpF0G.js.map +1 -0
- package/dist/web/public/assets/table-vmLMgj6_.js +2 -0
- package/dist/web/public/assets/table-vmLMgj6_.js.map +1 -0
- package/dist/web/public/assets/topn-nu66Fotx.js +7 -0
- package/dist/web/public/assets/topn-nu66Fotx.js.map +1 -0
- package/dist/web/public/assets/trash-2-ZIitN_U3.js +7 -0
- package/dist/web/public/assets/trash-2-ZIitN_U3.js.map +1 -0
- package/dist/web/public/assets/use-event-stream-BGeFcayX.js +2 -0
- package/dist/web/public/assets/use-event-stream-BGeFcayX.js.map +1 -0
- package/dist/web/public/assets/use-memory-DgEqHEca.js +2 -0
- package/dist/web/public/assets/use-memory-DgEqHEca.js.map +1 -0
- package/dist/web/public/assets/use-observability-CQev_A8e.js +2 -0
- package/dist/web/public/assets/use-observability-CQev_A8e.js.map +1 -0
- package/dist/web/public/assets/use-settings-CU-UcrVD.js +2 -0
- package/dist/web/public/assets/use-settings-CU-UcrVD.js.map +1 -0
- package/dist/web/public/assets/use-skills-Dr77CXLA.js +2 -0
- package/dist/web/public/assets/use-skills-Dr77CXLA.js.map +1 -0
- package/dist/web/public/assets/use-workspace-PNv9Z4de.js +2 -0
- package/dist/web/public/assets/use-workspace-PNv9Z4de.js.map +1 -0
- package/dist/web/public/assets/useQuery-BTyugXYV.js +2 -0
- package/dist/web/public/assets/useQuery-BTyugXYV.js.map +1 -0
- package/dist/web/public/assets/vector-w-Ea3pg6.js +2 -0
- package/dist/web/public/assets/vector-w-Ea3pg6.js.map +1 -0
- package/dist/web/public/assets/viewer-DKA7QP9U.js +12 -0
- package/dist/web/public/assets/viewer-DKA7QP9U.js.map +1 -0
- package/dist/web/public/assets/workspace-DVLZca7t.js +17 -0
- package/dist/web/public/assets/workspace-DVLZca7t.js.map +1 -0
- package/dist/web/public/assets/workspaces-DYZsMmY-.js +7 -0
- package/dist/web/public/assets/workspaces-DYZsMmY-.js.map +1 -0
- package/dist/web/public/assets/x-Ru3rHT82.js +7 -0
- package/dist/web/public/assets/x-Ru3rHT82.js.map +1 -0
- package/dist/web/public/favicon.svg +4 -0
- package/dist/web/public/index.html +37 -928
- package/dist/web/public/manifest.webmanifest +19 -0
- package/dist/web/public/tasks.html +307 -5
- package/dist/web/public/vendor/chart.umd.min.js +20 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +640 -60
- package/dist/web/server.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,936 +1,45 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
|
-
<meta charset="UTF-8"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<!-- viewport-fit=cover so iOS safe-area-inset-* actually has values to
|
|
6
|
+
expose; initial-scale=1 + maximum-scale=1 keeps mobile chrome from
|
|
7
|
+
auto-zooming on focused inputs (we control text size ourselves). -->
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1" />
|
|
9
|
+
<meta name="theme-color" content="#f7f8fa" media="(prefers-color-scheme: light)" />
|
|
10
|
+
<meta name="theme-color" content="#0e0f12" media="(prefers-color-scheme: dark)" />
|
|
11
|
+
<meta name="description" content="agim — multi-agent gateway console" />
|
|
12
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
13
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
14
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
15
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
16
|
+
<title>Agim</title>
|
|
17
|
+
<!--
|
|
18
|
+
Synchronous theme bootstrap. Reads localStorage 'agim-theme' BEFORE
|
|
19
|
+
the React bundle parses + renders, so the first paint already has
|
|
20
|
+
the right `data-theme` attribute on <html>. Without this, the page
|
|
21
|
+
flashes light-themed for ~80ms on dark-mode setups (the JS render
|
|
22
|
+
happens after first paint).
|
|
23
|
+
|
|
24
|
+
Mirrors the same key + state machine that agim-cli's old _app.js
|
|
25
|
+
used — keeps user preference continuous across the v1 → v2
|
|
26
|
+
upgrade.
|
|
27
|
+
-->
|
|
10
28
|
<script>
|
|
11
|
-
|
|
12
|
-
const LANGS = { en: 'English', zh: '中文' };
|
|
13
|
-
const savedLang = localStorage.getItem('im-hub-lang');
|
|
14
|
-
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
|
|
15
|
-
window.__lang = savedLang && LANGS[savedLang] ? savedLang : browserLang;
|
|
16
|
-
document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
|
|
17
|
-
|
|
18
|
-
const T = {
|
|
19
|
-
en: {
|
|
20
|
-
title: 'Agim — Agent Chat',
|
|
21
|
-
connecting: 'Connecting...',
|
|
22
|
-
connected: 'Connected',
|
|
23
|
-
disconnected: 'Disconnected',
|
|
24
|
-
connError: 'Connection error',
|
|
25
|
-
loadingAgents: 'Loading agents...',
|
|
26
|
-
newChat: 'New Chat',
|
|
27
|
-
welcomeTitle: 'Agim Agent Chat',
|
|
28
|
-
welcomeDesc: 'Select an agent and start chatting. Your locally installed AI coding agents are ready.',
|
|
29
|
-
inputPlaceholder: 'Type a message... (Enter to send, Shift+Enter for newline)',
|
|
30
|
-
send: 'Send',
|
|
31
|
-
you: 'You',
|
|
32
|
-
assistant: 'Assistant',
|
|
33
|
-
approvalTitle: 'Tool approval request',
|
|
34
|
-
approvalToolLabel: 'Tool',
|
|
35
|
-
approvalInputLabel: 'Input',
|
|
36
|
-
approvalAllow: 'Allow',
|
|
37
|
-
approvalDeny: 'Deny',
|
|
38
|
-
approvalAllowAll: 'Allow + auto for similar',
|
|
39
|
-
approvalAutoAllowing: 'Auto-allowing in {s}s — click Deny to reject',
|
|
40
|
-
approvalAllowed: '✅ Allowed',
|
|
41
|
-
approvalAllowedPinned: '✅ Allowed (auto-allow rule pinned)',
|
|
42
|
-
approvalDenied: '❌ Denied',
|
|
43
|
-
approvalDeniedRevoked: '❌ Denied (auto-allow rule revoked)',
|
|
44
|
-
approvalExpired: '⏱ Timed out (auto-denied)',
|
|
45
|
-
},
|
|
46
|
-
zh: {
|
|
47
|
-
title: 'Agim — Agent 对话',
|
|
48
|
-
connecting: '连接中...',
|
|
49
|
-
connected: '已连接',
|
|
50
|
-
disconnected: '已断开',
|
|
51
|
-
connError: '连接错误',
|
|
52
|
-
loadingAgents: '加载 Agent...',
|
|
53
|
-
newChat: '新对话',
|
|
54
|
-
welcomeTitle: 'Agim Agent 对话',
|
|
55
|
-
welcomeDesc: '选择一个 Agent 开始对话,本地安装的 AI 编程助手已就绪。',
|
|
56
|
-
inputPlaceholder: '输入消息...(Enter 发送,Shift+Enter 换行)',
|
|
57
|
-
send: '发送',
|
|
58
|
-
you: '你',
|
|
59
|
-
assistant: '助手',
|
|
60
|
-
approvalTitle: '工具调用审批',
|
|
61
|
-
approvalToolLabel: '工具',
|
|
62
|
-
approvalInputLabel: '入参',
|
|
63
|
-
approvalAllow: '批准',
|
|
64
|
-
approvalDeny: '拒绝',
|
|
65
|
-
approvalAllowAll: '批准并自动放行后续同类',
|
|
66
|
-
approvalAutoAllowing: '将在 {s}s 内自动放行 — 点击拒绝可阻止',
|
|
67
|
-
approvalAllowed: '✅ 已批准',
|
|
68
|
-
approvalAllowedPinned: '✅ 已批准(已启用自动放行)',
|
|
69
|
-
approvalDenied: '❌ 已拒绝',
|
|
70
|
-
approvalDeniedRevoked: '❌ 已拒绝(已撤销自动放行)',
|
|
71
|
-
approvalExpired: '⏱ 已超时(自动拒绝)',
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
function t(key) { return T[window.__lang][key] || T.en[key] || key; }
|
|
75
|
-
document.addEventListener('DOMContentLoaded', () => { document.title = t('title'); });
|
|
76
|
-
</script>
|
|
77
|
-
<style>
|
|
78
|
-
/* Three-state theming. `:root` defaults to light; explicit
|
|
79
|
-
data-theme="dark" forces dark; `prefers-color-scheme: dark` only
|
|
80
|
-
applies when the attribute is absent (mode === 'system'). */
|
|
81
|
-
:root {
|
|
82
|
-
color-scheme: light dark;
|
|
83
|
-
/* light defaults */
|
|
84
|
-
--bg: #f8f9fb;
|
|
85
|
-
--surface: #ffffff;
|
|
86
|
-
--surface2: #f1f3f6;
|
|
87
|
-
--border: #e1e4e8;
|
|
88
|
-
--text: #1a1f2e;
|
|
89
|
-
--text-dim: #6b7280;
|
|
90
|
-
--accent: #6366f1;
|
|
91
|
-
--accent-dim: #818cf8;
|
|
92
|
-
--user-bg: #eef2ff;
|
|
93
|
-
--assistant-bg: #ffffff;
|
|
94
|
-
--code-bg: #f1f5f9;
|
|
95
|
-
--green: #16a34a;
|
|
96
|
-
--red: #dc2626;
|
|
97
|
-
--yellow: #ca8a04;
|
|
98
|
-
}
|
|
99
|
-
:root[data-theme="dark"] {
|
|
100
|
-
--bg: #0a0a0a;
|
|
101
|
-
--surface: #141414;
|
|
102
|
-
--surface2: #1e1e1e;
|
|
103
|
-
--border: #2a2a2a;
|
|
104
|
-
--text: #e5e5e5;
|
|
105
|
-
--text-dim: #888;
|
|
106
|
-
--accent: #6366f1;
|
|
107
|
-
--accent-dim: #4f46e5;
|
|
108
|
-
--user-bg: #1a1a2e;
|
|
109
|
-
--assistant-bg: #1e1e1e;
|
|
110
|
-
--code-bg: #0d0d0d;
|
|
111
|
-
--green: #22c55e;
|
|
112
|
-
--red: #ef4444;
|
|
113
|
-
--yellow: #eab308;
|
|
114
|
-
}
|
|
115
|
-
@media (prefers-color-scheme: dark) {
|
|
116
|
-
:root:not([data-theme]) {
|
|
117
|
-
--bg: #0a0a0a;
|
|
118
|
-
--surface: #141414;
|
|
119
|
-
--surface2: #1e1e1e;
|
|
120
|
-
--border: #2a2a2a;
|
|
121
|
-
--text: #e5e5e5;
|
|
122
|
-
--text-dim: #888;
|
|
123
|
-
--accent-dim: #4f46e5;
|
|
124
|
-
--user-bg: #1a1a2e;
|
|
125
|
-
--assistant-bg: #1e1e1e;
|
|
126
|
-
--code-bg: #0d0d0d;
|
|
127
|
-
--green: #22c55e;
|
|
128
|
-
--red: #ef4444;
|
|
129
|
-
--yellow: #eab308;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
134
|
-
|
|
135
|
-
body {
|
|
136
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
137
|
-
background: var(--bg);
|
|
138
|
-
color: var(--text);
|
|
139
|
-
height: 100vh;
|
|
140
|
-
display: flex;
|
|
141
|
-
flex-direction: column;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/* Header */
|
|
145
|
-
.header {
|
|
146
|
-
display: flex;
|
|
147
|
-
align-items: center;
|
|
148
|
-
justify-content: space-between;
|
|
149
|
-
padding: 12px 20px;
|
|
150
|
-
border-bottom: 1px solid var(--border);
|
|
151
|
-
background: var(--surface);
|
|
152
|
-
}
|
|
153
|
-
.header-left {
|
|
154
|
-
display: flex;
|
|
155
|
-
align-items: center;
|
|
156
|
-
gap: 12px;
|
|
157
|
-
}
|
|
158
|
-
.logo {
|
|
159
|
-
font-weight: 700;
|
|
160
|
-
font-size: 16px;
|
|
161
|
-
color: var(--accent);
|
|
162
|
-
}
|
|
163
|
-
.status-dot {
|
|
164
|
-
width: 8px;
|
|
165
|
-
height: 8px;
|
|
166
|
-
border-radius: 50%;
|
|
167
|
-
background: var(--yellow);
|
|
168
|
-
display: inline-block;
|
|
169
|
-
}
|
|
170
|
-
.status-dot.connected { background: var(--green); }
|
|
171
|
-
.status-dot.disconnected { background: var(--red); }
|
|
172
|
-
.status-text { font-size: 12px; color: var(--text-dim); }
|
|
173
|
-
.header-right { display: flex; align-items: center; gap: 10px; }
|
|
174
|
-
|
|
175
|
-
/* Agent selector */
|
|
176
|
-
.agent-select {
|
|
177
|
-
background: var(--surface2);
|
|
178
|
-
color: var(--text);
|
|
179
|
-
border: 1px solid var(--border);
|
|
180
|
-
border-radius: 6px;
|
|
181
|
-
padding: 6px 10px;
|
|
182
|
-
font-size: 13px;
|
|
183
|
-
cursor: pointer;
|
|
184
|
-
outline: none;
|
|
185
|
-
}
|
|
186
|
-
.agent-select:focus { border-color: var(--accent); }
|
|
187
|
-
|
|
188
|
-
.btn {
|
|
189
|
-
background: var(--surface2);
|
|
190
|
-
color: var(--text-dim);
|
|
191
|
-
border: 1px solid var(--border);
|
|
192
|
-
border-radius: 6px;
|
|
193
|
-
padding: 6px 10px;
|
|
194
|
-
font-size: 12px;
|
|
195
|
-
cursor: pointer;
|
|
196
|
-
}
|
|
197
|
-
.btn:hover { color: var(--text); border-color: var(--text-dim); }
|
|
198
|
-
|
|
199
|
-
/* Language selector */
|
|
200
|
-
.lang-select {
|
|
201
|
-
background: var(--surface2);
|
|
202
|
-
color: var(--text-dim);
|
|
203
|
-
border: 1px solid var(--border);
|
|
204
|
-
border-radius: 6px;
|
|
205
|
-
padding: 4px 6px;
|
|
206
|
-
font-size: 12px;
|
|
207
|
-
cursor: pointer;
|
|
208
|
-
outline: none;
|
|
209
|
-
}
|
|
210
|
-
.lang-select:hover { color: var(--text); }
|
|
211
|
-
|
|
212
|
-
/* Messages */
|
|
213
|
-
.messages {
|
|
214
|
-
flex: 1;
|
|
215
|
-
overflow-y: auto;
|
|
216
|
-
padding: 20px;
|
|
217
|
-
display: flex;
|
|
218
|
-
flex-direction: column;
|
|
219
|
-
gap: 16px;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
.message {
|
|
223
|
-
max-width: 85%;
|
|
224
|
-
padding: 12px 16px;
|
|
225
|
-
border-radius: 12px;
|
|
226
|
-
font-size: 14px;
|
|
227
|
-
line-height: 1.6;
|
|
228
|
-
white-space: pre-wrap;
|
|
229
|
-
word-break: break-word;
|
|
230
|
-
}
|
|
231
|
-
.message.user {
|
|
232
|
-
align-self: flex-end;
|
|
233
|
-
background: var(--user-bg);
|
|
234
|
-
border-bottom-right-radius: 4px;
|
|
235
|
-
}
|
|
236
|
-
.message.assistant {
|
|
237
|
-
align-self: flex-start;
|
|
238
|
-
background: var(--assistant-bg);
|
|
239
|
-
border-bottom-left-radius: 4px;
|
|
240
|
-
}
|
|
241
|
-
.message.assistant code {
|
|
242
|
-
background: var(--code-bg);
|
|
243
|
-
padding: 2px 6px;
|
|
244
|
-
border-radius: 4px;
|
|
245
|
-
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
246
|
-
font-size: 13px;
|
|
247
|
-
}
|
|
248
|
-
.message.assistant pre {
|
|
249
|
-
background: var(--code-bg);
|
|
250
|
-
padding: 12px 16px;
|
|
251
|
-
border-radius: 8px;
|
|
252
|
-
overflow-x: auto;
|
|
253
|
-
margin: 8px 0;
|
|
254
|
-
}
|
|
255
|
-
.message.assistant pre code {
|
|
256
|
-
background: none;
|
|
257
|
-
padding: 0;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.message-label {
|
|
261
|
-
font-size: 11px;
|
|
262
|
-
color: var(--text-dim);
|
|
263
|
-
margin-bottom: 4px;
|
|
264
|
-
font-weight: 600;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/* Typing indicator */
|
|
268
|
-
.typing {
|
|
269
|
-
align-self: flex-start;
|
|
270
|
-
padding: 12px 16px;
|
|
271
|
-
color: var(--text-dim);
|
|
272
|
-
font-size: 13px;
|
|
273
|
-
}
|
|
274
|
-
.typing span {
|
|
275
|
-
animation: blink 1.4s infinite;
|
|
276
|
-
}
|
|
277
|
-
.typing span:nth-child(2) { animation-delay: 0.2s; }
|
|
278
|
-
.typing span:nth-child(3) { animation-delay: 0.4s; }
|
|
279
|
-
@keyframes blink {
|
|
280
|
-
0%, 60%, 100% { opacity: 0.2; }
|
|
281
|
-
30% { opacity: 1; }
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/* Welcome screen */
|
|
285
|
-
.welcome {
|
|
286
|
-
flex: 1;
|
|
287
|
-
display: flex;
|
|
288
|
-
flex-direction: column;
|
|
289
|
-
align-items: center;
|
|
290
|
-
justify-content: center;
|
|
291
|
-
gap: 16px;
|
|
292
|
-
color: var(--text-dim);
|
|
293
|
-
}
|
|
294
|
-
.welcome h2 { color: var(--text); font-size: 20px; }
|
|
295
|
-
.welcome p { font-size: 14px; max-width: 400px; text-align: center; }
|
|
296
|
-
|
|
297
|
-
/* Input area */
|
|
298
|
-
.input-area {
|
|
299
|
-
padding: 16px 20px;
|
|
300
|
-
border-top: 1px solid var(--border);
|
|
301
|
-
background: var(--surface);
|
|
302
|
-
}
|
|
303
|
-
.input-wrapper {
|
|
304
|
-
display: flex;
|
|
305
|
-
gap: 8px;
|
|
306
|
-
align-items: flex-end;
|
|
307
|
-
}
|
|
308
|
-
.input-wrapper textarea {
|
|
309
|
-
flex: 1;
|
|
310
|
-
background: var(--surface2);
|
|
311
|
-
color: var(--text);
|
|
312
|
-
border: 1px solid var(--border);
|
|
313
|
-
border-radius: 10px;
|
|
314
|
-
padding: 10px 14px;
|
|
315
|
-
font-size: 14px;
|
|
316
|
-
font-family: inherit;
|
|
317
|
-
resize: none;
|
|
318
|
-
outline: none;
|
|
319
|
-
max-height: 150px;
|
|
320
|
-
min-height: 42px;
|
|
321
|
-
}
|
|
322
|
-
.input-wrapper textarea:focus { border-color: var(--accent); }
|
|
323
|
-
.input-wrapper textarea::placeholder { color: var(--text-dim); }
|
|
324
|
-
|
|
325
|
-
.send-btn {
|
|
326
|
-
background: var(--accent);
|
|
327
|
-
color: white;
|
|
328
|
-
border: none;
|
|
329
|
-
border-radius: 10px;
|
|
330
|
-
padding: 10px 16px;
|
|
331
|
-
font-size: 14px;
|
|
332
|
-
cursor: pointer;
|
|
333
|
-
white-space: nowrap;
|
|
334
|
-
}
|
|
335
|
-
.send-btn:hover { background: var(--accent-dim); }
|
|
336
|
-
.send-btn:disabled {
|
|
337
|
-
background: var(--surface2);
|
|
338
|
-
color: var(--text-dim);
|
|
339
|
-
cursor: not-allowed;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/* Scrollbar */
|
|
343
|
-
.messages::-webkit-scrollbar { width: 6px; }
|
|
344
|
-
.messages::-webkit-scrollbar-track { background: transparent; }
|
|
345
|
-
.messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
346
|
-
|
|
347
|
-
/* In-chat HITL approval card. Mirrors the IM card's structure (Telegram
|
|
348
|
-
inline-keyboard variant) so the cross-platform UX is consistent. */
|
|
349
|
-
.approval-card {
|
|
350
|
-
border: 1px solid var(--accent);
|
|
351
|
-
background: var(--surface);
|
|
352
|
-
border-radius: 10px;
|
|
353
|
-
padding: 14px 16px;
|
|
354
|
-
margin: 8px 0;
|
|
355
|
-
max-width: 720px;
|
|
356
|
-
box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
|
357
|
-
}
|
|
358
|
-
.approval-card.auto-allow { border-color: var(--yellow); }
|
|
359
|
-
.approval-card.resolved { opacity: .85; }
|
|
360
|
-
.approval-card .ac-title {
|
|
361
|
-
font-weight: 600;
|
|
362
|
-
font-size: 14px;
|
|
363
|
-
margin-bottom: 8px;
|
|
364
|
-
display: flex;
|
|
365
|
-
align-items: center;
|
|
366
|
-
gap: 6px;
|
|
367
|
-
}
|
|
368
|
-
.approval-card .ac-row { font-size: 13px; margin: 4px 0; color: var(--text-dim); }
|
|
369
|
-
.approval-card .ac-row b { color: var(--text); margin-right: 6px; }
|
|
370
|
-
.approval-card .ac-input {
|
|
371
|
-
background: var(--code-bg);
|
|
372
|
-
border-radius: 6px;
|
|
373
|
-
padding: 8px 10px;
|
|
374
|
-
font: 12px/1.5 'SF Mono', Menlo, Consolas, monospace;
|
|
375
|
-
white-space: pre-wrap;
|
|
376
|
-
word-break: break-word;
|
|
377
|
-
max-height: 180px;
|
|
378
|
-
overflow: auto;
|
|
379
|
-
margin: 6px 0 10px;
|
|
380
|
-
color: var(--text);
|
|
381
|
-
}
|
|
382
|
-
.approval-card .ac-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
|
383
|
-
.approval-card .ac-btn {
|
|
384
|
-
border: 1px solid var(--border);
|
|
385
|
-
background: var(--surface2);
|
|
386
|
-
color: var(--text);
|
|
387
|
-
padding: 6px 14px;
|
|
388
|
-
border-radius: 6px;
|
|
389
|
-
cursor: pointer;
|
|
390
|
-
font-size: 13px;
|
|
391
|
-
}
|
|
392
|
-
.approval-card .ac-btn:hover { border-color: var(--accent); }
|
|
393
|
-
.approval-card .ac-btn.allow { background: var(--green); border-color: var(--green); color: #fff; }
|
|
394
|
-
.approval-card .ac-btn.deny { background: var(--red); border-color: var(--red); color: #fff; }
|
|
395
|
-
.approval-card .ac-btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
396
|
-
.approval-card .ac-grace {
|
|
397
|
-
color: var(--yellow);
|
|
398
|
-
font-size: 13px;
|
|
399
|
-
margin-bottom: 8px;
|
|
400
|
-
font-weight: 500;
|
|
401
|
-
}
|
|
402
|
-
.approval-card .ac-outcome {
|
|
403
|
-
font-size: 13px;
|
|
404
|
-
margin-top: 8px;
|
|
405
|
-
padding-top: 8px;
|
|
406
|
-
border-top: 1px solid var(--border);
|
|
407
|
-
color: var(--text-dim);
|
|
408
|
-
}
|
|
409
|
-
</style>
|
|
410
|
-
</head>
|
|
411
|
-
<body>
|
|
412
|
-
<div class="header">
|
|
413
|
-
<div class="header-left">
|
|
414
|
-
<span class="logo">Agim</span>
|
|
415
|
-
<span class="status-dot" id="statusDot"></span>
|
|
416
|
-
<span class="status-text" id="statusText"></span>
|
|
417
|
-
</div>
|
|
418
|
-
<div class="header-right">
|
|
419
|
-
<select class="lang-select" id="langSelect">
|
|
420
|
-
<option value="en">EN</option>
|
|
421
|
-
<option value="zh">中文</option>
|
|
422
|
-
</select>
|
|
423
|
-
<select class="agent-select" id="agentSelect" disabled>
|
|
424
|
-
<option value=""></option>
|
|
425
|
-
</select>
|
|
426
|
-
<button type="button" class="btn" id="newChatBtn"></button>
|
|
427
|
-
<button class="btn" id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
|
|
428
|
-
<a class="btn" href="/tasks" title="Tasks" style="text-decoration:none;font-size:16px;line-height:1">☰</a>
|
|
429
|
-
<a class="btn" href="/reminders" title="Reminders" style="text-decoration:none;font-size:14px;line-height:1">🔔</a>
|
|
430
|
-
<a class="btn" href="/memos" title="Memos" style="text-decoration:none;font-size:14px;line-height:1">📋</a>
|
|
431
|
-
<a class="btn" href="/settings" title="Settings" style="text-decoration:none;font-size:16px;line-height:1">⚙</a>
|
|
432
|
-
</div>
|
|
433
|
-
</div>
|
|
434
|
-
|
|
435
|
-
<div class="messages" id="messages">
|
|
436
|
-
<div class="welcome" id="welcome">
|
|
437
|
-
<h2 id="welcomeTitle"></h2>
|
|
438
|
-
<p id="welcomeDesc"></p>
|
|
439
|
-
</div>
|
|
440
|
-
</div>
|
|
441
|
-
|
|
442
|
-
<div class="input-area">
|
|
443
|
-
<div class="input-wrapper">
|
|
444
|
-
<textarea
|
|
445
|
-
id="input"
|
|
446
|
-
rows="1"
|
|
447
|
-
disabled
|
|
448
|
-
></textarea>
|
|
449
|
-
<button type="button" class="send-btn" id="sendBtn" disabled></button>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
|
|
453
|
-
<script>
|
|
454
|
-
// Apply i18n to static elements
|
|
455
|
-
function applyLang() {
|
|
456
|
-
document.title = t('title');
|
|
457
|
-
document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
|
|
458
|
-
statusText.textContent = statusText.dataset.state ? t(statusText.dataset.state) : t('connecting');
|
|
459
|
-
const opt = agentSelect.querySelector('option[value=""]');
|
|
460
|
-
if (opt) opt.textContent = t('loadingAgents');
|
|
461
|
-
newChatBtn.textContent = t('newChat');
|
|
462
|
-
inputEl.placeholder = t('inputPlaceholder');
|
|
463
|
-
sendBtn.textContent = t('send');
|
|
464
|
-
const wt = document.getElementById('welcomeTitle');
|
|
465
|
-
const wd = document.getElementById('welcomeDesc');
|
|
466
|
-
if (wt) wt.textContent = t('welcomeTitle');
|
|
467
|
-
if (wd) wd.textContent = t('welcomeDesc');
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// State
|
|
471
|
-
let ws = null;
|
|
472
|
-
let currentAgent = '';
|
|
473
|
-
let isStreaming = false;
|
|
474
|
-
let streamingEl = null;
|
|
475
|
-
let streamingText = '';
|
|
476
|
-
let _reconnectTimer = null;
|
|
477
|
-
|
|
478
|
-
// DOM
|
|
479
|
-
const messagesEl = document.getElementById('messages');
|
|
480
|
-
const welcomeEl = document.getElementById('welcome');
|
|
481
|
-
const inputEl = document.getElementById('input');
|
|
482
|
-
const sendBtn = document.getElementById('sendBtn');
|
|
483
|
-
const agentSelect = document.getElementById('agentSelect');
|
|
484
|
-
const statusDot = document.getElementById('statusDot');
|
|
485
|
-
const statusText = document.getElementById('statusText');
|
|
486
|
-
const newChatBtn = document.getElementById('newChatBtn');
|
|
487
|
-
const langSelect = document.getElementById('langSelect');
|
|
488
|
-
|
|
489
|
-
// Theme toggle (light / dark / system). _app.js applied the theme
|
|
490
|
-
// synchronously in <head>; here we wire the button so clicks cycle
|
|
491
|
-
// through the three modes and the icon/label re-renders.
|
|
492
|
-
if (window.imhub) imhub.theme.bindToggle(document.getElementById('theme-toggle'));
|
|
493
|
-
|
|
494
|
-
// Language selector
|
|
495
|
-
langSelect.value = window.__lang;
|
|
496
|
-
langSelect.addEventListener('change', () => {
|
|
497
|
-
window.__lang = langSelect.value;
|
|
498
|
-
localStorage.setItem('im-hub-lang', window.__lang);
|
|
499
|
-
applyLang();
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// Connect
|
|
503
|
-
function connect() {
|
|
504
|
-
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
505
|
-
ws = new WebSocket(`${protocol}//${location.host}/`);
|
|
506
|
-
|
|
507
|
-
ws.onopen = () => {
|
|
508
|
-
setStatus('connected', t('connected'));
|
|
509
|
-
inputEl.disabled = false;
|
|
510
|
-
sendBtn.disabled = false;
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
ws.onmessage = (e) => {
|
|
514
|
-
const msg = JSON.parse(e.data);
|
|
515
|
-
handleMessage(msg);
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
ws.onclose = () => {
|
|
519
|
-
setStatus('disconnected', t('disconnected'));
|
|
520
|
-
inputEl.disabled = true;
|
|
521
|
-
sendBtn.disabled = true;
|
|
522
|
-
_reconnectTimer = setTimeout(connect, 3000);
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
ws.onerror = () => {
|
|
526
|
-
setStatus('disconnected', t('connError'));
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Handle server messages
|
|
531
|
-
function handleMessage(msg) {
|
|
532
|
-
switch (msg.type) {
|
|
533
|
-
case 'init':
|
|
534
|
-
agentSelect.innerHTML = '';
|
|
535
|
-
msg.agents.forEach(name => {
|
|
536
|
-
const opt = document.createElement('option');
|
|
537
|
-
opt.value = name;
|
|
538
|
-
opt.textContent = name;
|
|
539
|
-
if (name === msg.defaultAgent) opt.selected = true;
|
|
540
|
-
agentSelect.appendChild(opt);
|
|
541
|
-
});
|
|
542
|
-
agentSelect.disabled = false;
|
|
543
|
-
currentAgent = msg.defaultAgent;
|
|
544
|
-
break;
|
|
545
|
-
|
|
546
|
-
case 'history':
|
|
547
|
-
hideWelcome();
|
|
548
|
-
if (msg.agent) {
|
|
549
|
-
currentAgent = msg.agent;
|
|
550
|
-
agentSelect.value = msg.agent;
|
|
551
|
-
}
|
|
552
|
-
msg.messages.forEach(m => {
|
|
553
|
-
addMessage(m.role, m.content, false);
|
|
554
|
-
});
|
|
555
|
-
scrollToBottom();
|
|
556
|
-
break;
|
|
557
|
-
|
|
558
|
-
case 'agents':
|
|
559
|
-
agentSelect.innerHTML = '';
|
|
560
|
-
msg.agents.forEach(name => {
|
|
561
|
-
const opt = document.createElement('option');
|
|
562
|
-
opt.value = name;
|
|
563
|
-
opt.textContent = name;
|
|
564
|
-
if (name === currentAgent) opt.selected = true;
|
|
565
|
-
agentSelect.appendChild(opt);
|
|
566
|
-
});
|
|
567
|
-
agentSelect.disabled = false;
|
|
568
|
-
break;
|
|
569
|
-
|
|
570
|
-
case 'chunk':
|
|
571
|
-
if (!isStreaming) {
|
|
572
|
-
hideWelcome();
|
|
573
|
-
startStreaming();
|
|
574
|
-
}
|
|
575
|
-
streamingText += msg.text;
|
|
576
|
-
updateStreamingEl();
|
|
577
|
-
scrollToBottom();
|
|
578
|
-
break;
|
|
579
|
-
|
|
580
|
-
case 'done':
|
|
581
|
-
if (isStreaming) {
|
|
582
|
-
finishStreaming();
|
|
583
|
-
} else if (msg.text) {
|
|
584
|
-
hideWelcome();
|
|
585
|
-
addMessage('assistant', msg.text);
|
|
586
|
-
scrollToBottom();
|
|
587
|
-
}
|
|
588
|
-
break;
|
|
589
|
-
|
|
590
|
-
case 'error':
|
|
591
|
-
if (isStreaming) finishStreaming();
|
|
592
|
-
addMessage('assistant', `\u274c ${msg.message}`);
|
|
593
|
-
scrollToBottom();
|
|
594
|
-
break;
|
|
595
|
-
|
|
596
|
-
case 'agent-switched':
|
|
597
|
-
currentAgent = msg.agent;
|
|
598
|
-
agentSelect.value = msg.agent;
|
|
599
|
-
break;
|
|
600
|
-
|
|
601
|
-
// HITL approval messages relayed by the synthetic 'web' messenger
|
|
602
|
-
// (registered on the server in startWebServer). approval-router
|
|
603
|
-
// calls our messenger.sendApprovalCard / sendMessage / editApprovalCard
|
|
604
|
-
// — those become the three WS message types below.
|
|
605
|
-
case 'approval-text':
|
|
606
|
-
// Fallback / receipt path. Render as a system message.
|
|
607
|
-
hideWelcome();
|
|
608
|
-
addSystemMessage(msg.text);
|
|
609
|
-
scrollToBottom();
|
|
610
|
-
break;
|
|
611
|
-
case 'approval-card':
|
|
612
|
-
hideWelcome();
|
|
613
|
-
renderApprovalCard(msg.messageId, msg.prompt);
|
|
614
|
-
scrollToBottom();
|
|
615
|
-
break;
|
|
616
|
-
case 'approval-card-edit':
|
|
617
|
-
editApprovalCard(msg.messageId, msg.outcome);
|
|
618
|
-
break;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Send message
|
|
623
|
-
function sendMessage() {
|
|
624
|
-
const text = inputEl.value.trim();
|
|
625
|
-
if (!text || !ws || ws.readyState !== ws.OPEN) return;
|
|
626
|
-
|
|
627
|
-
hideWelcome();
|
|
628
|
-
addMessage('user', text);
|
|
629
|
-
|
|
630
|
-
ws.send(JSON.stringify({
|
|
631
|
-
type: 'message',
|
|
632
|
-
text,
|
|
633
|
-
agent: currentAgent,
|
|
634
|
-
}));
|
|
635
|
-
|
|
636
|
-
inputEl.value = '';
|
|
637
|
-
autoResize();
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Streaming helpers
|
|
641
|
-
function startStreaming() {
|
|
642
|
-
isStreaming = true;
|
|
643
|
-
streamingText = '';
|
|
644
|
-
|
|
645
|
-
const wrapper = document.createElement('div');
|
|
646
|
-
wrapper.innerHTML = `<div class="message-label">${t('assistant')}</div>`;
|
|
647
|
-
streamingEl = document.createElement('div');
|
|
648
|
-
streamingEl.className = 'message assistant';
|
|
649
|
-
wrapper.appendChild(streamingEl);
|
|
650
|
-
messagesEl.appendChild(wrapper);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function updateStreamingEl() {
|
|
654
|
-
if (!streamingEl) return;
|
|
655
|
-
streamingEl.textContent = streamingText;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function finishStreaming() {
|
|
659
|
-
isStreaming = false;
|
|
660
|
-
if (streamingEl) {
|
|
661
|
-
streamingEl.innerHTML = renderMarkdown(streamingText);
|
|
662
|
-
}
|
|
663
|
-
streamingEl = null;
|
|
664
|
-
saveHistory();
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Message helpers
|
|
668
|
-
/**
|
|
669
|
-
* System / receipt-style message bubble. Used for `approval-text` so the
|
|
670
|
-
* "✅ Allowed" / "⏱ Timed out" notices the bus emits show up in the
|
|
671
|
-
* conversation flow without a "user / assistant" label.
|
|
672
|
-
*/
|
|
673
|
-
function addSystemMessage(text) {
|
|
674
|
-
hideWelcome();
|
|
675
|
-
const wrapper = document.createElement('div');
|
|
676
|
-
wrapper.style.cssText = 'margin:6px 0; color:var(--text-dim); font-size:13px;';
|
|
677
|
-
wrapper.textContent = text;
|
|
678
|
-
messagesEl.appendChild(wrapper);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/** Active approval cards keyed by server-issued messageId, so the
|
|
682
|
-
* `approval-card-edit` handler can find the same DOM node and
|
|
683
|
-
* collapse it to an outcome stamp. */
|
|
684
|
-
const approvalCards = new Map();
|
|
685
|
-
|
|
686
|
-
function renderApprovalCard(messageId, prompt) {
|
|
687
|
-
const isAuto = prompt.mode === 'auto-allow';
|
|
688
|
-
const card = document.createElement('div');
|
|
689
|
-
card.className = `approval-card${isAuto ? ' auto-allow' : ''}`;
|
|
690
|
-
card.dataset.cardId = messageId;
|
|
691
|
-
card.innerHTML = `
|
|
692
|
-
<div class="ac-title">🔐 <span data-slot="title"></span></div>
|
|
693
|
-
${isAuto ? `<div class="ac-grace" data-slot="grace"></div>` : ''}
|
|
694
|
-
<div class="ac-row"><b data-slot="tool-label"></b><code data-slot="tool"></code></div>
|
|
695
|
-
<div class="ac-row"><b data-slot="input-label"></b></div>
|
|
696
|
-
<pre class="ac-input" data-slot="input"></pre>
|
|
697
|
-
<div class="ac-actions" data-slot="actions"></div>
|
|
698
|
-
`;
|
|
699
|
-
card.querySelector('[data-slot="title"]').textContent = t('approvalTitle');
|
|
700
|
-
card.querySelector('[data-slot="tool-label"]').textContent = `${t('approvalToolLabel')}:`;
|
|
701
|
-
card.querySelector('[data-slot="tool"]').textContent = prompt.toolName;
|
|
702
|
-
card.querySelector('[data-slot="input-label"]').textContent = `${t('approvalInputLabel')}:`;
|
|
703
|
-
card.querySelector('[data-slot="input"]').textContent = prompt.inputJson;
|
|
704
|
-
|
|
705
|
-
const send = (choice) => {
|
|
706
|
-
// Soft disable: dim and ignore further clicks for 800 ms while the
|
|
707
|
-
// server round-trips. We deliberately do NOT hard-disable — if the
|
|
708
|
-
// server returns an error (e.g. 'approval handler not bound'), the
|
|
709
|
-
// card stays usable so the user can retry once they fix it. The
|
|
710
|
-
// success path replaces the buttons via `approval-card-edit` anyway.
|
|
711
|
-
card.querySelectorAll('button').forEach((b) => {
|
|
712
|
-
b.style.opacity = '0.6';
|
|
713
|
-
b.style.pointerEvents = 'none';
|
|
714
|
-
});
|
|
715
|
-
setTimeout(() => {
|
|
716
|
-
card.querySelectorAll('button').forEach((b) => {
|
|
717
|
-
b.style.opacity = '';
|
|
718
|
-
b.style.pointerEvents = '';
|
|
719
|
-
});
|
|
720
|
-
}, 800);
|
|
721
|
-
const payload = { type: 'approval-action', messageId, data: `apv:${prompt.reqId}:${choice}` };
|
|
722
|
-
try { console.debug?.('[approval] click', payload); } catch {}
|
|
723
|
-
if (ws.readyState !== WebSocket.OPEN) {
|
|
724
|
-
imhub?.showError?.('WebSocket not connected — click ignored');
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
ws.send(JSON.stringify(payload));
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
const actions = card.querySelector('[data-slot="actions"]');
|
|
731
|
-
if (isAuto) {
|
|
732
|
-
// Auto-allow grace mode: only Deny is meaningful (Allow happens
|
|
733
|
-
// automatically on grace expiry).
|
|
734
|
-
const denyBtn = document.createElement('button');
|
|
735
|
-
denyBtn.className = 'ac-btn deny';
|
|
736
|
-
denyBtn.textContent = t('approvalDeny');
|
|
737
|
-
denyBtn.onclick = () => send('n');
|
|
738
|
-
actions.appendChild(denyBtn);
|
|
739
|
-
|
|
740
|
-
// Live countdown.
|
|
741
|
-
let s = Number(prompt.graceSeconds) || 5;
|
|
742
|
-
const grace = card.querySelector('[data-slot="grace"]');
|
|
743
|
-
const refreshGrace = () => { grace.textContent = t('approvalAutoAllowing').replace('{s}', String(s)); };
|
|
744
|
-
refreshGrace();
|
|
745
|
-
const tick = setInterval(() => {
|
|
746
|
-
s = Math.max(0, s - 1);
|
|
747
|
-
refreshGrace();
|
|
748
|
-
if (s <= 0) clearInterval(tick);
|
|
749
|
-
}, 1000);
|
|
750
|
-
card._graceTimer = tick;
|
|
751
|
-
} else {
|
|
752
|
-
const allowBtn = document.createElement('button');
|
|
753
|
-
allowBtn.className = 'ac-btn allow';
|
|
754
|
-
allowBtn.textContent = t('approvalAllow');
|
|
755
|
-
allowBtn.onclick = () => send('y');
|
|
756
|
-
|
|
757
|
-
const denyBtn = document.createElement('button');
|
|
758
|
-
denyBtn.className = 'ac-btn deny';
|
|
759
|
-
denyBtn.textContent = t('approvalDeny');
|
|
760
|
-
denyBtn.onclick = () => send('n');
|
|
761
|
-
|
|
762
|
-
const allowAllBtn = document.createElement('button');
|
|
763
|
-
allowAllBtn.className = 'ac-btn';
|
|
764
|
-
allowAllBtn.textContent = t('approvalAllowAll');
|
|
765
|
-
allowAllBtn.onclick = () => send('a');
|
|
766
|
-
|
|
767
|
-
actions.appendChild(allowBtn);
|
|
768
|
-
actions.appendChild(denyBtn);
|
|
769
|
-
actions.appendChild(allowAllBtn);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
messagesEl.appendChild(card);
|
|
773
|
-
approvalCards.set(messageId, card);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function editApprovalCard(messageId, outcome) {
|
|
777
|
-
const card = approvalCards.get(messageId);
|
|
778
|
-
if (!card) return;
|
|
779
|
-
card.classList.add('resolved');
|
|
780
|
-
// Stop any countdown if this was an auto-allow card.
|
|
781
|
-
if (card._graceTimer) { clearInterval(card._graceTimer); card._graceTimer = null; }
|
|
782
|
-
const grace = card.querySelector('[data-slot="grace"]');
|
|
783
|
-
if (grace) grace.remove();
|
|
784
|
-
// Disable any remaining live buttons.
|
|
785
|
-
card.querySelectorAll('.ac-actions button').forEach(b => { b.disabled = true; });
|
|
786
|
-
const actions = card.querySelector('[data-slot="actions"]');
|
|
787
|
-
if (actions) actions.remove();
|
|
788
|
-
const stamp = document.createElement('div');
|
|
789
|
-
stamp.className = 'ac-outcome';
|
|
790
|
-
const KEY = {
|
|
791
|
-
allowed: 'approvalAllowed',
|
|
792
|
-
'allowed-pinned': 'approvalAllowedPinned',
|
|
793
|
-
denied: 'approvalDenied',
|
|
794
|
-
'denied-revoked': 'approvalDeniedRevoked',
|
|
795
|
-
expired: 'approvalExpired',
|
|
796
|
-
};
|
|
797
|
-
const text = t(KEY[outcome.decision] || 'approvalAllowed');
|
|
798
|
-
const by = outcome.byUserDisplay ? ` · ${outcome.byUserDisplay}` : '';
|
|
799
|
-
stamp.textContent = `${text}${by}`;
|
|
800
|
-
card.appendChild(stamp);
|
|
801
|
-
// Keep the card around for visual continuity, but free the map slot
|
|
802
|
-
// so a same-id replay doesn't double-edit it.
|
|
803
|
-
approvalCards.delete(messageId);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function addMessage(role, content, save = true) {
|
|
807
|
-
hideWelcome();
|
|
808
|
-
|
|
809
|
-
const wrapper = document.createElement('div');
|
|
810
|
-
const label = document.createElement('div');
|
|
811
|
-
label.className = 'message-label';
|
|
812
|
-
label.textContent = role === 'user' ? t('you') : t('assistant');
|
|
813
|
-
|
|
814
|
-
const el = document.createElement('div');
|
|
815
|
-
el.className = `message ${role}`;
|
|
816
|
-
|
|
817
|
-
if (role === 'assistant') {
|
|
818
|
-
el.innerHTML = renderMarkdown(content);
|
|
819
|
-
} else {
|
|
820
|
-
el.textContent = content;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
wrapper.appendChild(label);
|
|
824
|
-
wrapper.appendChild(el);
|
|
825
|
-
messagesEl.appendChild(wrapper);
|
|
826
|
-
|
|
827
|
-
if (save) saveHistory();
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Basic markdown rendering
|
|
831
|
-
function renderMarkdown(text) {
|
|
832
|
-
if (!text) return '';
|
|
833
|
-
let html = escapeHtml(text);
|
|
834
|
-
|
|
835
|
-
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, _lang, code) => {
|
|
836
|
-
return `<pre><code>${code.trim()}</code></pre>`;
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
840
|
-
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
841
|
-
html = html.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
|
|
842
|
-
|
|
843
|
-
return html;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function escapeHtml(text) {
|
|
847
|
-
return text
|
|
848
|
-
.replace(/&/g, '&')
|
|
849
|
-
.replace(/</g, '<')
|
|
850
|
-
.replace(/>/g, '>');
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function hideWelcome() {
|
|
854
|
-
if (welcomeEl) welcomeEl.remove();
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function scrollToBottom() {
|
|
858
|
-
requestAnimationFrame(() => {
|
|
859
|
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// Status
|
|
864
|
-
function setStatus(state, text) {
|
|
865
|
-
statusDot.className = `status-dot ${state}`;
|
|
866
|
-
statusText.textContent = text;
|
|
867
|
-
statusText.dataset.state = state === 'connected' ? 'connected' : state === 'disconnected' ? 'disconnected' : 'connecting';
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// LocalStorage persistence
|
|
871
|
-
function saveHistory() {
|
|
872
|
-
const msgs = [];
|
|
873
|
-
messagesEl.querySelectorAll('.message').forEach(el => {
|
|
874
|
-
const role = el.classList.contains('user') ? 'user' : 'assistant';
|
|
875
|
-
msgs.push({ role, content: el.textContent });
|
|
876
|
-
});
|
|
29
|
+
(function () {
|
|
877
30
|
try {
|
|
878
|
-
localStorage.
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
function loadHistory() {
|
|
883
|
-
try {
|
|
884
|
-
const saved = localStorage.getItem('im-hub-history');
|
|
885
|
-
if (saved) {
|
|
886
|
-
const msgs = JSON.parse(saved);
|
|
887
|
-
if (msgs.length > 0) {
|
|
888
|
-
hideWelcome();
|
|
889
|
-
msgs.forEach(m => { addMessage(m.role, m.content, false); });
|
|
890
|
-
scrollToBottom();
|
|
891
|
-
}
|
|
31
|
+
var m = localStorage.getItem('agim-theme')
|
|
32
|
+
if (m === 'light' || m === 'dark') {
|
|
33
|
+
document.documentElement.setAttribute('data-theme', m)
|
|
892
34
|
}
|
|
893
|
-
} catch {}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Auto-resize textarea
|
|
897
|
-
function autoResize() {
|
|
898
|
-
inputEl.style.height = 'auto';
|
|
899
|
-
inputEl.style.height = `${Math.min(inputEl.scrollHeight, 150)}px`;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// Event listeners
|
|
903
|
-
inputEl.addEventListener('input', autoResize);
|
|
904
|
-
|
|
905
|
-
inputEl.addEventListener('keydown', (e) => {
|
|
906
|
-
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
|
907
|
-
e.preventDefault();
|
|
908
|
-
sendMessage();
|
|
909
|
-
}
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
sendBtn.addEventListener('click', sendMessage);
|
|
913
|
-
|
|
914
|
-
agentSelect.addEventListener('change', () => {
|
|
915
|
-
const agent = agentSelect.value;
|
|
916
|
-
if (agent && agent !== currentAgent && ws?.readyState === WebSocket.OPEN) {
|
|
917
|
-
currentAgent = agent;
|
|
918
|
-
ws.send(JSON.stringify({ type: 'switch-agent', agent }));
|
|
919
|
-
}
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
newChatBtn.addEventListener('click', () => {
|
|
923
|
-
messagesEl.innerHTML = '';
|
|
924
|
-
localStorage.removeItem('im-hub-history');
|
|
925
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
926
|
-
ws.send(JSON.stringify({ type: 'message', text: '/new' }));
|
|
927
|
-
}
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
// Init
|
|
931
|
-
applyLang();
|
|
932
|
-
loadHistory();
|
|
933
|
-
connect();
|
|
35
|
+
} catch (e) { /* private-mode storage exceptions: noop */ }
|
|
36
|
+
})()
|
|
934
37
|
</script>
|
|
38
|
+
<script type="module" crossorigin src="/assets/index-CpGWCLE5.js"></script>
|
|
39
|
+
<link rel="modulepreload" crossorigin href="/assets/react-C9F3QeMB.js">
|
|
40
|
+
<link rel="stylesheet" crossorigin href="/assets/index-GpceOxum.css">
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div id="root"></div>
|
|
935
44
|
</body>
|
|
936
45
|
</html>
|