@empir3/empir3-bridge 0.3.21
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 +1531 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +464 -0
- package/SECURITY.md +130 -0
- package/assets/accuracy-lab.html +2639 -0
- package/assets/api-clis-real.jpg +0 -0
- package/assets/bridge-console-hero.jpg +0 -0
- package/assets/browser-privacy.svg +151 -0
- package/assets/demo-orchestration.svg +74 -0
- package/assets/desktop-select-region.jpg +0 -0
- package/assets/in-page-chat.gif +0 -0
- package/assets/orchestration-hero.svg +126 -0
- package/assets/social-preview.png +0 -0
- package/assets/zara-accent.png +0 -0
- package/build/bootstrap.js +548 -0
- package/build/build.js +680 -0
- package/build/payload-entry.js +649 -0
- package/build/payload-signing-pub.json +7 -0
- package/docs/AGENT_GUIDE.md +259 -0
- package/docs/RELEASE.md +106 -0
- package/docs/SAFETY.md +112 -0
- package/docs/TESTING.md +181 -0
- package/installer/server.js +231 -0
- package/installer/ui/app.js +278 -0
- package/installer/ui/index.html +24 -0
- package/installer/ui/styles.css +146 -0
- package/package.json +95 -0
- package/scripts/bootstrap-e2e.mjs +650 -0
- package/scripts/certify-bridge.mjs +636 -0
- package/scripts/check-companion-surface.mjs +118 -0
- package/scripts/extract-welcome.mjs +64 -0
- package/scripts/gh-route-handler-check.mjs +57 -0
- package/scripts/gh-wire-test.mjs +107 -0
- package/scripts/publish-downloads.mjs +180 -0
- package/scripts/smoke-all-tools.mjs +509 -0
- package/scripts/smoke-live-bridge.mjs +696 -0
- package/scripts/splice-welcome.mjs +63 -0
- package/scripts/welcome-body.txt +2733 -0
- package/src/anthropic-client.ts +192 -0
- package/src/bootstrap-exe.ts +69 -0
- package/src/bridge.ts +2444 -0
- package/src/chat.ts +345 -0
- package/src/cli-runner.ts +239 -0
- package/src/cli.ts +649 -0
- package/src/config.ts +199 -0
- package/src/desktop-overlay.ps1 +121 -0
- package/src/executable-resolver.ts +330 -0
- package/src/handlers/agy-imagegen.ts +179 -0
- package/src/handlers/github-cli.ts +399 -0
- package/src/handlers/higgsfield-cli.ts +783 -0
- package/src/launch.js +337 -0
- package/src/mcp-server.ts +1265 -0
- package/src/pair-claim.ts +218 -0
- package/src/payload-daemon.ts +168 -0
- package/src/server.ts +21036 -0
- package/src/tool-defaults.ts +230 -0
- package/src/update-check.js +136 -0
- package/tray/build.py +76 -0
- package/tray/requirements.txt +2 -0
- package/tray/tray.py +1843 -0
|
@@ -0,0 +1,2733 @@
|
|
|
1
|
+
const api = apiBase || '';
|
|
2
|
+
const signedIn = hasBridgeAuth();
|
|
3
|
+
|
|
4
|
+
// Map each tool to the global safety category it requires (mirrors
|
|
5
|
+
// requiredBridgePermission() in this file). Drives the per-row "NEEDS X"
|
|
6
|
+
// warning chip when a tool is enabled but its required global category
|
|
7
|
+
// is off — saves users from chasing silent denials.
|
|
8
|
+
const toolsEnriched = TOOL_META.map(t => ({
|
|
9
|
+
n: t.name,
|
|
10
|
+
g: t.group,
|
|
11
|
+
d: t.defaultEnabled,
|
|
12
|
+
r: TOOL_PERMISSION_REQUIREMENTS[t.name] || null,
|
|
13
|
+
b: t.blurb,
|
|
14
|
+
}));
|
|
15
|
+
const toolsJson = JSON.stringify(toolsEnriched);
|
|
16
|
+
const toolCount = TOOL_META.length;
|
|
17
|
+
|
|
18
|
+
return `<!doctype html>
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="utf-8">
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
23
|
+
<title>empir3 Bridge — Console</title>
|
|
24
|
+
<style>
|
|
25
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap');
|
|
26
|
+
* { box-sizing: border-box; }
|
|
27
|
+
:root {
|
|
28
|
+
--ink:#18181b;--ink-2:#27272a;--ink-3:#3f3f46;
|
|
29
|
+
--paper:#ffffff;--bg:#f4f4f5;--surface:#fafafa;--surface-2:#e4e4e7;
|
|
30
|
+
--rail:#18181b;--rail-2:#27272a;
|
|
31
|
+
--rail-line:rgba(255,255,255,0.08);
|
|
32
|
+
--rail-text:#d4d4d8;--rail-text-dim:#71717a;--rail-text-bright:#fafafa;
|
|
33
|
+
--line:rgba(0,0,0,0.10);--line-strong:rgba(0,0,0,0.18);--rule:rgba(0,0,0,0.06);
|
|
34
|
+
--muted:#52525b;--soft:#71717a;--faint:#a1a1aa;
|
|
35
|
+
--accent:#6b4ef0;--accent-soft:rgba(107,78,240,0.10);
|
|
36
|
+
--good:#059669;--good-soft:rgba(5,150,105,0.10);
|
|
37
|
+
--warn:#d97706;--warn-soft:rgba(217,119,6,0.10);
|
|
38
|
+
--bad:#dc2626;--bad-soft:rgba(220,38,38,0.10);
|
|
39
|
+
}
|
|
40
|
+
body[data-theme="dark"] {
|
|
41
|
+
--ink:#fafafa;--ink-2:#e4e4e7;--ink-3:#d4d4d8;
|
|
42
|
+
--paper:#18181b;--bg:#09090b;--surface:#1f1f23;--surface-2:#27272a;
|
|
43
|
+
--rail:#050507;--rail-2:#131316;--rail-line:rgba(255,255,255,0.06);
|
|
44
|
+
--rail-text:#a1a1aa;--rail-text-dim:#52525b;--rail-text-bright:#fafafa;
|
|
45
|
+
--line:rgba(255,255,255,0.08);--line-strong:rgba(255,255,255,0.16);--rule:rgba(255,255,255,0.05);
|
|
46
|
+
--muted:#a1a1aa;--soft:#71717a;--faint:#52525b;
|
|
47
|
+
--accent:#8c6bff;--accent-soft:rgba(140,107,255,0.14);
|
|
48
|
+
--good:#10b981;--good-soft:rgba(16,185,129,0.14);
|
|
49
|
+
--warn:#f59e0b;--warn-soft:rgba(245,158,11,0.14);
|
|
50
|
+
--bad:#ef4444;--bad-soft:rgba(239,68,68,0.14);
|
|
51
|
+
}
|
|
52
|
+
body[data-theme="dark"] pre.snippet { background:#050507; color:#fafafa; }
|
|
53
|
+
body[data-theme="dark"] .signin { background:var(--accent); border-color:var(--accent); color:#fff; }
|
|
54
|
+
body[data-theme="dark"] .signin:hover { background:#7a55ff; border-color:#7a55ff; }
|
|
55
|
+
body[data-theme="dark"] .signin .brand-inline .three,
|
|
56
|
+
body[data-theme="dark"] .btn.primary .brand-inline .three { color:#fff; }
|
|
57
|
+
body[data-theme="dark"] .btn.primary { background:var(--accent); border-color:var(--accent); color:#fff; }
|
|
58
|
+
body[data-theme="dark"] .btn.primary:hover { background:#7a55ff; border-color:#7a55ff; }
|
|
59
|
+
body[data-theme="dark"] .tag.solid-ink { background:var(--accent); color:#fff; border-color:var(--accent); }
|
|
60
|
+
body[data-theme="dark"] .perm-toolbar .filter-group button.on { background:var(--accent); color:#fff; }
|
|
61
|
+
body[data-theme="dark"] .acct .av { background:linear-gradient(135deg,#8c6bff,#b8a3ff); }
|
|
62
|
+
body[data-theme="dark"] .rail-foot .led { box-shadow:0 0 6px rgba(16,185,129,0.5); }
|
|
63
|
+
body[data-theme="dark"] .tele-cell .led { box-shadow:0 0 5px rgba(16,185,129,0.45); }
|
|
64
|
+
|
|
65
|
+
html, body { margin:0; height:100vh; overflow:hidden; }
|
|
66
|
+
body {
|
|
67
|
+
background:var(--bg); color:var(--ink);
|
|
68
|
+
font-family:'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
69
|
+
font-size:14.5px; line-height:1.5;
|
|
70
|
+
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
|
71
|
+
font-feature-settings:'cv11','ss01','ss03';
|
|
72
|
+
}
|
|
73
|
+
code, pre.snippet {
|
|
74
|
+
font-family:'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
75
|
+
font-variant-numeric:tabular-nums;
|
|
76
|
+
}
|
|
77
|
+
.mono, .num { font-feature-settings:'cv11','ss01'; font-variant-numeric:tabular-nums; letter-spacing:-0.005em; }
|
|
78
|
+
|
|
79
|
+
.app { display:grid; grid-template-columns:224px 1fr; height:100vh; }
|
|
80
|
+
aside.rail {
|
|
81
|
+
background:var(--rail); color:var(--rail-text);
|
|
82
|
+
display:grid; grid-template-rows:auto 1fr auto;
|
|
83
|
+
border-right:1px solid var(--rail-line); overflow:hidden;
|
|
84
|
+
}
|
|
85
|
+
.rail-head { padding:18px 20px 16px; border-bottom:1px solid var(--rail-line); }
|
|
86
|
+
.rail-brand { display:flex; align-items:baseline; gap:0; font-weight:900; font-size:19px; letter-spacing:-0.01em; color:var(--rail-text-bright); line-height:1; }
|
|
87
|
+
.rail-brand .three { color:var(--accent); }
|
|
88
|
+
.rail-brand .sub { margin-left:8px; font-weight:700; font-size:14px; color:var(--rail-text-dim); }
|
|
89
|
+
.rail-tag { margin-top:8px; font-variant-numeric:tabular-nums; font-size:10px; color:var(--rail-text-dim); letter-spacing:0.04em; }
|
|
90
|
+
.rail-tag .live { display:inline-block; width:6px; height:6px; background:var(--good); border-radius:1px; margin-right:6px; box-shadow:0 0 8px rgba(15,139,90,0.7); }
|
|
91
|
+
|
|
92
|
+
.rail-nav { padding:14px 10px; overflow-y:auto; }
|
|
93
|
+
.rail-section-label { padding:14px 12px 6px; font-size:11px; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:var(--rail-text-dim); }
|
|
94
|
+
.rail-nav a { display:flex; align-items:center; gap:11px; padding:9px 12px; margin:1px 0; color:var(--rail-text); text-decoration:none; font-weight:500; font-size:14px; border-radius:5px; cursor:pointer; position:relative; }
|
|
95
|
+
.rail-nav a:hover { background:var(--rail-2); color:var(--rail-text-bright); }
|
|
96
|
+
.rail-nav a.active { background:var(--rail-2); color:var(--rail-text-bright); font-weight:700; }
|
|
97
|
+
.rail-nav a.active::before { content:''; position:absolute; left:0; top:6px; bottom:6px; width:3px; background:var(--accent); border-radius:0 2px 2px 0; }
|
|
98
|
+
.rail-nav a .ico { width:16px; height:16px; color:var(--rail-text-dim); flex-shrink:0; }
|
|
99
|
+
.rail-nav a.active .ico { color:var(--accent); }
|
|
100
|
+
.rail-nav a .count { margin-left:auto; font-variant-numeric:tabular-nums; font-size:10.5px; color:var(--rail-text-dim); background:rgba(255,255,255,0.06); padding:1px 6px; border-radius:3px; }
|
|
101
|
+
.rail-nav a.danger { color:#e88a85; }
|
|
102
|
+
.rail-nav a.danger .ico { color:#c66662; }
|
|
103
|
+
|
|
104
|
+
.rail-foot { padding:12px 16px 14px; border-top:1px solid var(--rail-line); display:grid; gap:8px; }
|
|
105
|
+
.rail-foot .row { display:flex; align-items:center; gap:8px; font-variant-numeric:tabular-nums; font-size:10.5px; color:var(--rail-text-dim); }
|
|
106
|
+
.rail-foot .row .k { color:var(--rail-text); }
|
|
107
|
+
.rail-foot .led { width:7px; height:7px; border-radius:1px; background:var(--good); box-shadow:0 0 6px rgba(15,139,90,0.6); }
|
|
108
|
+
.rail-foot .led.warn { background:var(--warn); box-shadow:0 0 6px rgba(182,107,0,0.6); }
|
|
109
|
+
|
|
110
|
+
main.console { display:grid; grid-template-rows:auto auto 1fr; overflow:hidden; background:var(--bg); }
|
|
111
|
+
.topbar { display:flex; align-items:center; gap:14px; padding:12px 22px; background:var(--paper); border-bottom:1px solid var(--line); height:52px; }
|
|
112
|
+
.crumbs { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--soft); }
|
|
113
|
+
.crumbs .here { color:var(--ink); font-weight:700; }
|
|
114
|
+
.crumbs .sep { color:var(--faint); }
|
|
115
|
+
.topbar .spacer { flex:1; }
|
|
116
|
+
.topbar .search { width:280px; position:relative; }
|
|
117
|
+
.topbar .search input { width:100%; padding:7px 10px 7px 30px; border:1px solid var(--line); background:var(--surface); color:var(--ink); font:inherit; font-size:12.5px; border-radius:4px; }
|
|
118
|
+
.topbar .search svg { position:absolute; left:9px; top:50%; transform:translateY(-50%); width:13px; height:13px; color:var(--soft); }
|
|
119
|
+
.topbar kbd { position:absolute; right:8px; top:50%; transform:translateY(-50%); font-variant-numeric:tabular-nums; font-size:10px; color:var(--soft); border:1px solid var(--line); padding:1px 5px; border-radius:3px; background:var(--paper); }
|
|
120
|
+
.theme-toggle { appearance:none; width:32px; height:32px; display:inline-flex; align-items:center; justify-content:center; border:1px solid var(--line); background:var(--surface); color:var(--muted); border-radius:5px; cursor:pointer; transition:color 100ms ease, border-color 100ms ease, background 100ms ease; }
|
|
121
|
+
.theme-toggle:hover { color:var(--ink); border-color:var(--line-strong); }
|
|
122
|
+
.theme-toggle svg { width:15px; height:15px; }
|
|
123
|
+
body[data-theme="light"] .theme-toggle .moon { display:none; }
|
|
124
|
+
body[data-theme="dark"] .theme-toggle .sun { display:none; }
|
|
125
|
+
.signin { display:inline-flex; align-items:center; gap:9px; padding:8px 14px; border:1px solid var(--ink); background:var(--ink); color:var(--bg); font:inherit; font-weight:700; font-size:12.5px; cursor:pointer; border-radius:5px; transition:background 100ms ease; }
|
|
126
|
+
.signin:hover { background:var(--ink-2); }
|
|
127
|
+
.signin svg { width:12px; height:12px; }
|
|
128
|
+
.signin .brand-inline { color:inherit; }
|
|
129
|
+
.brand-inline { display:inline-flex; align-items:baseline; font-weight:700; color:inherit; }
|
|
130
|
+
.brand-inline .three { color:var(--accent); }
|
|
131
|
+
.signin .brand-inline .three { color:#b8a3ff; }
|
|
132
|
+
.acct { display:inline-flex; align-items:center; gap:9px; padding:5px 10px 5px 6px; border:1px solid var(--line-strong); background:var(--surface); border-radius:5px; font:inherit; font-weight:600; font-size:12.5px; cursor:pointer; color:var(--ink); }
|
|
133
|
+
.acct:hover { border-color:var(--accent); }
|
|
134
|
+
.acct .av { width:22px; height:22px; background:linear-gradient(135deg, var(--accent), #8c6bff); color:#fff; font-weight:800; font-size:10.5px; display:inline-flex; align-items:center; justify-content:center; border-radius:4px; }
|
|
135
|
+
.acct .em { color:var(--muted); font-size:12px; }
|
|
136
|
+
.acct .car { color:var(--soft); font-size:9px; }
|
|
137
|
+
|
|
138
|
+
.tele-strip { display:flex; gap:0; background:var(--surface); border-bottom:1px solid var(--line); padding:0; overflow-x:auto; }
|
|
139
|
+
.tele-cell { flex:1; min-width:0; padding:12px 22px; border-right:1px solid var(--rule); display:grid; gap:3px; }
|
|
140
|
+
.tele-cell:last-child { border-right:none; }
|
|
141
|
+
.tele-cell .lbl { font-size:11px; font-weight:700; letter-spacing:0.12em; text-transform:uppercase; color:var(--soft); display:flex; align-items:center; gap:7px; }
|
|
142
|
+
.tele-cell .led { width:7px; height:7px; border-radius:1px; background:var(--good); box-shadow:0 0 5px rgba(15,139,90,0.5); }
|
|
143
|
+
.tele-cell .led.warn { background:var(--warn); box-shadow:0 0 5px rgba(182,107,0,0.5); }
|
|
144
|
+
.tele-cell .led.bad { background:var(--bad); box-shadow:0 0 5px rgba(178,36,31,0.5); }
|
|
145
|
+
.tele-cell .led.idle { background:var(--soft); box-shadow:none; }
|
|
146
|
+
.tele-cell .val { font-variant-numeric:tabular-nums; font-size:15px; font-weight:600; color:var(--ink); line-height:1.15; letter-spacing:-0.01em; }
|
|
147
|
+
.tele-cell .sub { font-variant-numeric:tabular-nums; font-size:11.5px; color:var(--muted); line-height:1.4; }
|
|
148
|
+
.safety-shortcuts { display:inline-flex; align-items:center; gap:4px; }
|
|
149
|
+
.safety-shortcut { appearance:none; border:1px solid transparent; background:transparent; color:var(--muted); font:inherit; font-weight:800; line-height:1; padding:2px 3px; border-radius:3px; cursor:pointer; min-width:18px; }
|
|
150
|
+
.safety-shortcut:hover { border-color:var(--accent); color:var(--ink); }
|
|
151
|
+
.safety-shortcut.on { color:var(--good); }
|
|
152
|
+
.safety-shortcut.off { color:var(--muted); opacity:0.55; }
|
|
153
|
+
.safety-divider { color:var(--muted); font-weight:600; }
|
|
154
|
+
|
|
155
|
+
.pane-scroll { overflow-y:auto; padding:24px 22px 36px; }
|
|
156
|
+
.pane { max-width:1100px; }
|
|
157
|
+
.pane[data-pane] { display:none; }
|
|
158
|
+
.pane.active { display:block; }
|
|
159
|
+
|
|
160
|
+
h1.pane-title { margin:0 0 4px; font-size:22px; font-weight:800; letter-spacing:-0.005em; }
|
|
161
|
+
p.pane-lede { margin:0 0 22px; color:var(--muted); font-size:14.5px; line-height:1.6; max-width:68ch; }
|
|
162
|
+
|
|
163
|
+
.block { background:var(--paper); border:1px solid var(--line); border-radius:6px; margin-bottom:18px; }
|
|
164
|
+
.block-head { padding:12px 16px; border-bottom:1px solid var(--rule); display:flex; align-items:center; gap:12px; background:var(--surface); border-radius:6px 6px 0 0; }
|
|
165
|
+
.block-head .ix { font-variant-numeric:tabular-nums; font-size:11.5px; font-weight:600; color:var(--soft); letter-spacing:0; }
|
|
166
|
+
.block-head .title { font-size:13.5px; font-weight:700; letter-spacing:0.04em; color:var(--ink); }
|
|
167
|
+
.block-head .spacer { flex:1; }
|
|
168
|
+
.block-head .sub { font-size:12.5px; color:var(--muted); }
|
|
169
|
+
.block-body { padding:14px 16px; }
|
|
170
|
+
|
|
171
|
+
.tag { display:inline-flex; align-items:center; gap:5px; padding:3px 7px; border-radius:3px; font-variant-numeric:tabular-nums; font-size:11px; font-weight:600; letter-spacing:0; background:var(--surface-2); color:var(--muted); border:1px solid var(--line); white-space:nowrap; }
|
|
172
|
+
.tag.good { background:var(--good-soft); color:var(--good); border-color:rgba(15,139,90,0.22); }
|
|
173
|
+
.tag.warn { background:var(--warn-soft); color:var(--warn); border-color:rgba(182,107,0,0.22); }
|
|
174
|
+
.tag.bad { background:var(--bad-soft); color:var(--bad); border-color:rgba(178,36,31,0.22); }
|
|
175
|
+
.tag.accent { background:var(--accent-soft); color:var(--accent); border-color:rgba(107,78,240,0.22); }
|
|
176
|
+
.tag.solid-ink { background:var(--ink); color:var(--bg); border-color:var(--ink); }
|
|
177
|
+
|
|
178
|
+
.btn { appearance:none; display:inline-flex; align-items:center; justify-content:center; gap:7px; padding:7px 12px; border:1px solid var(--line-strong); background:var(--paper); color:var(--ink); font:inherit; font-weight:600; font-size:12.5px; cursor:pointer; border-radius:4px; min-height:32px; transition:border-color 100ms ease, background 100ms ease; }
|
|
179
|
+
.btn:hover { border-color:var(--accent); }
|
|
180
|
+
.btn.primary { background:var(--ink); color:var(--bg); border-color:var(--ink); }
|
|
181
|
+
.btn.primary:hover { background:var(--ink-2); border-color:var(--ink-2); }
|
|
182
|
+
.btn.danger { color:var(--bad); border-color:rgba(178,36,31,0.35); }
|
|
183
|
+
.btn.danger:hover { background:var(--bad-soft); border-color:var(--bad); }
|
|
184
|
+
.btn.small { padding:4px 8px; min-height:26px; font-size:11.5px; }
|
|
185
|
+
.btn.ghost { background:transparent; }
|
|
186
|
+
.btn:disabled { opacity:0.4; cursor:not-allowed; }
|
|
187
|
+
.btn svg { width:12px; height:12px; }
|
|
188
|
+
|
|
189
|
+
.sw { position:relative; display:inline-block; width:32px; height:18px; flex-shrink:0; }
|
|
190
|
+
.sw input { opacity:0; width:0; height:0; }
|
|
191
|
+
.sw .s { position:absolute; inset:0; background:rgba(0,0,0,0.22); border-radius:3px; transition:background 140ms ease; cursor:pointer; }
|
|
192
|
+
.sw .s::before { content:''; position:absolute; width:14px; height:14px; left:2px; top:2px; background:#fff; border-radius:2px; box-shadow:0 1px 3px rgba(0,0,0,0.3); transition:transform 140ms cubic-bezier(.4,1.4,.6,1); }
|
|
193
|
+
.sw input:checked + .s { background:var(--good); }
|
|
194
|
+
.sw input:checked + .s::before { transform:translateX(14px); }
|
|
195
|
+
|
|
196
|
+
.safety-bar { display:grid; grid-template-columns:repeat(3, 1fr); gap:1px; background:var(--line); margin:0; }
|
|
197
|
+
.safety-cell { background:var(--paper); padding:14px 16px; display:grid; grid-template-columns:auto 1fr auto; gap:12px; align-items:center; }
|
|
198
|
+
.safety-cell .ico { width:28px; height:28px; border-radius:4px; background:var(--bad-soft); color:var(--bad); display:inline-flex; align-items:center; justify-content:center; }
|
|
199
|
+
.safety-cell.on .ico { background:var(--good-soft); color:var(--good); }
|
|
200
|
+
.safety-cell .ico svg { width:14px; height:14px; }
|
|
201
|
+
.safety-cell .meta .name { font-weight:700; font-size:13px; }
|
|
202
|
+
.safety-cell .meta .sub { font-size:11px; color:var(--muted); margin-top:1px; }
|
|
203
|
+
|
|
204
|
+
.perm-toolbar { display:flex; align-items:center; gap:10px; padding:10px 16px; border-bottom:1px solid var(--rule); background:var(--surface); flex-wrap:wrap; }
|
|
205
|
+
.perm-toolbar .filter-group { display:inline-flex; background:var(--paper); border:1px solid var(--line); border-radius:4px; overflow:hidden; }
|
|
206
|
+
.perm-toolbar .filter-group button { appearance:none; background:transparent; border:none; border-right:1px solid var(--line); padding:6px 11px; font:inherit; font-size:12.5px; font-weight:600; letter-spacing:0; color:var(--muted); cursor:pointer; }
|
|
207
|
+
.perm-toolbar .filter-group button:last-child { border-right:none; }
|
|
208
|
+
.perm-toolbar .filter-group button.on { background:var(--ink); color:var(--bg); }
|
|
209
|
+
.perm-toolbar .filter-group button:hover:not(.on) { background:var(--surface-2); color:var(--ink); }
|
|
210
|
+
.perm-toolbar .count-readout { margin-left:auto; font-variant-numeric:tabular-nums; font-size:12.5px; color:var(--soft); }
|
|
211
|
+
.perm-toolbar .count-readout .num { color:var(--ink); font-weight:700; }
|
|
212
|
+
.perm-toolbar .scope-note, .perm-bulkbar .scope-note { display:inline-flex; align-items:center; gap:8px; padding:6px 11px; border:1px solid var(--line); background:var(--accent-soft); color:var(--ink); border-radius:4px; font-size:12.5px; line-height:1.4; }
|
|
213
|
+
.perm-toolbar .scope-note svg, .perm-bulkbar .scope-note svg { color:var(--accent); }
|
|
214
|
+
|
|
215
|
+
.perm-bulkbar { display:flex; align-items:center; gap:12px; padding:10px 16px; border-bottom:1px solid var(--rule); background:var(--paper); flex-wrap:wrap; }
|
|
216
|
+
.perm-bulkbar .bulk-actions { display:inline-flex; gap:6px; }
|
|
217
|
+
|
|
218
|
+
.perm-table .dep-warn { display:inline-flex; align-items:center; gap:5px; padding:2px 7px; border-radius:3px; font-size:11px; font-weight:600; background:var(--warn-soft); color:var(--warn); border:1px solid color-mix(in srgb, var(--warn) 28%, var(--line)); margin-left:8px; cursor:help; }
|
|
219
|
+
.perm-table .dep-warn svg { width:11px; height:11px; }
|
|
220
|
+
|
|
221
|
+
.perm-table { width:100%; border-collapse:collapse; font-size:13px; }
|
|
222
|
+
.perm-table thead th { text-align:left; padding:10px 16px; font-size:11px; font-weight:700; letter-spacing:0.10em; text-transform:uppercase; color:var(--soft); background:var(--surface); border-bottom:1px solid var(--line); position:sticky; top:0; }
|
|
223
|
+
.perm-table tbody tr { border-bottom:1px solid var(--rule); transition:background 80ms ease; }
|
|
224
|
+
.perm-table tbody tr:hover { background:var(--surface); }
|
|
225
|
+
.perm-table tbody tr.group-header { background:var(--surface-2); cursor:pointer; }
|
|
226
|
+
.perm-table tbody tr.group-header:hover { background:var(--surface-2); }
|
|
227
|
+
.perm-table tbody tr.group-header td { padding:9px 16px; font-size:11.5px; font-weight:700; letter-spacing:0.10em; text-transform:uppercase; color:var(--ink-3); border-bottom:1px solid var(--line); }
|
|
228
|
+
.perm-table tbody tr.group-header td .count { font-variant-numeric:tabular-nums; font-size:11.5px; font-weight:500; letter-spacing:0; text-transform:none; color:var(--soft); margin-left:10px; }
|
|
229
|
+
.perm-table tbody tr.group-header .bulk { float:right; display:inline-flex; gap:4px; text-transform:none; letter-spacing:0; }
|
|
230
|
+
.perm-table tbody tr.group-header .bulk button { appearance:none; border:1px solid var(--line-strong); background:var(--paper); color:var(--muted); font:inherit; font-variant-numeric:tabular-nums; font-size:11.5px; font-weight:500; padding:3px 9px; border-radius:3px; cursor:pointer; }
|
|
231
|
+
.perm-table tbody tr.group-header .bulk button:hover { color:var(--ink); border-color:var(--accent); }
|
|
232
|
+
.perm-table td { padding:9px 16px; vertical-align:top; }
|
|
233
|
+
.perm-table td.col-tg { width:44px; padding-top:11px; }
|
|
234
|
+
.perm-table td.col-nm { font-variant-numeric:tabular-nums; font-size:13px; font-weight:500; color:var(--ink); white-space:nowrap; width:230px; }
|
|
235
|
+
.perm-table td.col-tag { width:110px; padding-top:11px; }
|
|
236
|
+
.perm-table td.col-blurb { color:var(--muted); font-size:13.5px; line-height:1.5; }
|
|
237
|
+
.perm-table tr.disabled td.col-nm { color:var(--faint); }
|
|
238
|
+
.perm-table tr.eval td.col-nm { color:var(--bad); }
|
|
239
|
+
|
|
240
|
+
.kv { display:grid; grid-template-columns:130px 1fr; gap:4px 16px; font-size:13px; }
|
|
241
|
+
.kv .k { color:var(--soft); font-weight:600; font-size:12px; letter-spacing:0.04em; text-transform:uppercase; align-self:center; }
|
|
242
|
+
.kv .v { color:var(--ink); font-weight:500; }
|
|
243
|
+
.kv .v.mono { font-variant-numeric:tabular-nums; letter-spacing:-0.005em; font-weight:500; font-size:13.5px; word-break:break-all; }
|
|
244
|
+
.kv .v + .k { margin-top:8px; }
|
|
245
|
+
.kv .k + .v { padding:4px 0; }
|
|
246
|
+
|
|
247
|
+
.log-table { width:100%; border-collapse:collapse; margin-top:10px; font-size:12.5px; }
|
|
248
|
+
.log-table td { padding:5px 10px; border-bottom:1px solid var(--rule); vertical-align:middle; white-space:nowrap; }
|
|
249
|
+
.log-table tr:last-child td { border-bottom:none; }
|
|
250
|
+
.log-table td.ts { color:var(--soft); font-size:12px; width:70px; }
|
|
251
|
+
.log-table td.nm { color:var(--ink); font-weight:500; }
|
|
252
|
+
.log-table td.st { color:var(--good); font-weight:600; width:40px; }
|
|
253
|
+
.log-table td.st.err { color:var(--bad); }
|
|
254
|
+
.log-table td.ms { color:var(--muted); text-align:right; width:64px; }
|
|
255
|
+
.log-table td.dt { color:var(--soft); font-size:12px; overflow:hidden; text-overflow:ellipsis; max-width:280px; }
|
|
256
|
+
|
|
257
|
+
pre.snippet { margin:0; padding:12px 14px; background:var(--ink); color:var(--bg); border-radius:5px; font-size:12px; line-height:1.55; overflow:auto; white-space:pre; max-height:240px; }
|
|
258
|
+
|
|
259
|
+
.backdrop { position:fixed; inset:0; background:rgba(0,0,0,0.30); z-index:50; display:none; }
|
|
260
|
+
.backdrop.open { display:block; }
|
|
261
|
+
.drawer { position:fixed; top:0; right:0; bottom:0; width:400px; max-width:92vw; background:var(--paper); border-left:1px solid var(--line-strong); z-index:60; display:none; grid-template-rows:auto auto 1fr; box-shadow:-16px 0 40px rgba(0,0,0,0.18); }
|
|
262
|
+
.drawer.open { display:grid; }
|
|
263
|
+
.drawer-head { display:flex; align-items:center; gap:10px; padding:14px 18px; border-bottom:1px solid var(--line); background:var(--surface); }
|
|
264
|
+
.drawer-head .title { font-size:15px; font-weight:700; letter-spacing:-0.005em; color:var(--ink); }
|
|
265
|
+
.drawer > .sub { margin:0; padding:12px 18px 14px; background:var(--surface); border-bottom:1px solid var(--line); color:var(--muted); font-size:13px; line-height:1.5; }
|
|
266
|
+
.drawer-head .spacer { flex:1; }
|
|
267
|
+
.drawer-head .x { appearance:none; border:none; background:none; width:28px; height:28px; color:var(--soft); cursor:pointer; border-radius:4px; }
|
|
268
|
+
.drawer-head .x:hover { background:var(--surface-2); color:var(--ink); }
|
|
269
|
+
.drawer-body { padding:18px; display:grid; gap:14px; overflow-y:auto; align-content:start; }
|
|
270
|
+
.drawer-body label { display:grid; gap:6px; font-size:12.5px; color:var(--ink); font-weight:600; }
|
|
271
|
+
.drawer-body input, .drawer-body select { width:100%; padding:9px 11px; border:1px solid var(--line-strong); background:var(--paper); color:var(--ink); font:inherit; font-size:13.5px; font-weight:400; border-radius:4px; height:36px; line-height:1.2; }
|
|
272
|
+
.drawer-body input::placeholder { color:var(--faint); }
|
|
273
|
+
.drawer-body input:focus, .drawer-body select:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); }
|
|
274
|
+
.drawer-body .btn.primary, .drawer-body .btn { min-height:36px; padding:8px 14px; }
|
|
275
|
+
.drawer-body .divider { display:flex; align-items:center; gap:10px; color:var(--soft); font-size:11.5px; font-weight:500; margin:2px 0; }
|
|
276
|
+
.drawer-body .divider::before, .drawer-body .divider::after { content:''; flex:1; height:1px; background:var(--rule); }
|
|
277
|
+
.drawer-body .fields { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
|
|
278
|
+
.drawer-body .hint { margin:-4px 0 0; font-size:11.5px; color:var(--soft); line-height:1.4; }
|
|
279
|
+
.drawer-body .status { margin:0; font-size:12px; color:var(--muted); min-height:14px; }
|
|
280
|
+
.drawer-body .status.ok { color:var(--good); }
|
|
281
|
+
.drawer-body .status.err { color:var(--bad); }
|
|
282
|
+
|
|
283
|
+
body[data-view="signedout"] .when-signedin { display:none !important; }
|
|
284
|
+
body[data-view="signedin"] .when-signedout { display:none !important; }
|
|
285
|
+
|
|
286
|
+
.ov-grid { display:grid; grid-template-columns:repeat(12, 1fr); gap:14px; }
|
|
287
|
+
.ov-tile { grid-column:span 6; background:var(--paper); border:1px solid var(--line); border-radius:6px; padding:16px; }
|
|
288
|
+
.ov-tile.s4 { grid-column:span 4; }
|
|
289
|
+
.ov-tile.s8 { grid-column:span 8; }
|
|
290
|
+
.ov-tile.s12 { grid-column:span 12; }
|
|
291
|
+
.ov-tile h3 { margin:0 0 4px; font-size:12px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:var(--soft); }
|
|
292
|
+
.ov-tile .big { font-variant-numeric:tabular-nums; font-size:24px; font-weight:500; color:var(--ink); line-height:1.1; margin:6px 0 4px; letter-spacing:-0.01em; }
|
|
293
|
+
.ov-tile .big.good { color:var(--good); }
|
|
294
|
+
.ov-tile .big.warn { color:var(--warn); }
|
|
295
|
+
.ov-tile .big.bad { color:var(--bad); }
|
|
296
|
+
.ov-tile p { margin:6px 0 0; color:var(--muted); font-size:13.5px; line-height:1.55; }
|
|
297
|
+
.ov-tile .actions { margin-top:12px; display:flex; gap:6px; flex-wrap:wrap; }
|
|
298
|
+
|
|
299
|
+
.stub { border:1px dashed var(--line-strong); background:var(--surface); border-radius:6px; padding:32px 24px; color:var(--muted); font-size:13px; text-align:center; }
|
|
300
|
+
|
|
301
|
+
/* Generic status text under blocks/buttons */
|
|
302
|
+
.status { min-height:16px; font-size:12.5px; color:var(--muted); margin:0; }
|
|
303
|
+
.status.ok { color:var(--good); }
|
|
304
|
+
.status.err { color:var(--bad); }
|
|
305
|
+
.status.info { color:var(--accent); }
|
|
306
|
+
.desktop-metrics { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:1px; background:var(--line); border:1px solid var(--line); border-radius:4px; overflow:hidden; margin-bottom:14px; }
|
|
307
|
+
.desktop-metric { background:var(--paper); padding:12px; min-width:0; }
|
|
308
|
+
.desktop-metric.wide { grid-column:span 2; }
|
|
309
|
+
.desktop-metric .label { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; color:var(--soft); margin-bottom:6px; }
|
|
310
|
+
.desktop-metric .value { font-size:20px; line-height:1.1; font-weight:800; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
311
|
+
.desktop-metric .value.small { font-size:13px; line-height:1.35; font-weight:600; white-space:normal; word-break:break-all; }
|
|
312
|
+
.desktop-metric .value.good { color:var(--good); }
|
|
313
|
+
.desktop-metric .value.warn { color:var(--warn); }
|
|
314
|
+
.desktop-metric .value.bad { color:var(--bad); }
|
|
315
|
+
.desktop-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
|
|
316
|
+
.desktop-actions input, .desktop-actions select { min-height:32px; padding:6px 9px; border:1px solid var(--line-strong); border-radius:4px; background:var(--paper); color:var(--ink); font:inherit; font-size:12.5px; }
|
|
317
|
+
.desktop-actions input { min-width:190px; }
|
|
318
|
+
.desktop-actions select { min-width:210px; max-width:100%; }
|
|
319
|
+
.desktop-output { display:none; margin:12px 0 0; padding:10px; min-height:64px; max-height:220px; overflow:auto; border:1px solid var(--line); border-radius:4px; background:var(--surface); color:var(--ink); font:12px/1.45 var(--mono); white-space:pre-wrap; }
|
|
320
|
+
.setup-checklist { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:8px; margin-bottom:14px; }
|
|
321
|
+
.setup-item { display:grid; grid-template-columns:16px 1fr; gap:9px; padding:10px; border:1px solid var(--line); border-radius:4px; background:var(--surface); min-width:0; }
|
|
322
|
+
.setup-dot { width:10px; height:10px; border-radius:50%; margin-top:3px; background:var(--faint); box-shadow:0 0 0 3px rgba(0,0,0,0.04); }
|
|
323
|
+
.setup-item.done .setup-dot { background:var(--good); }
|
|
324
|
+
.setup-item.warn .setup-dot { background:var(--warn); }
|
|
325
|
+
.setup-item strong { display:block; font-size:12.5px; color:var(--ink); }
|
|
326
|
+
.setup-item small { display:block; margin-top:2px; color:var(--muted); line-height:1.35; overflow-wrap:anywhere; }
|
|
327
|
+
|
|
328
|
+
/* Sidebar overlay toggle — shown only below the breakpoint. Clicking
|
|
329
|
+
pops the rail back in as a floating overlay so the user can pick a
|
|
330
|
+
section, then it auto-dismisses. Click outside also closes. */
|
|
331
|
+
.rail-toggle { display:none; align-items:center; justify-content:center; width:34px; height:34px; padding:0; border:1px solid var(--surface-2); background:var(--surface); color:var(--ink); border-radius:6px; cursor:pointer; flex-shrink:0; margin-right:10px; }
|
|
332
|
+
.rail-toggle:hover { background:var(--surface-2); }
|
|
333
|
+
.rail-toggle svg { width:16px; height:16px; }
|
|
334
|
+
.rail-scrim { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.45); z-index:80; }
|
|
335
|
+
|
|
336
|
+
@media (max-width: 880px) {
|
|
337
|
+
.app { grid-template-columns:1fr; }
|
|
338
|
+
aside.rail { display:none; }
|
|
339
|
+
.rail-toggle { display:inline-flex; }
|
|
340
|
+
body.rail-open aside.rail { display:grid; position:fixed; left:0; top:0; bottom:0; width:240px; z-index:90; box-shadow:0 0 24px rgba(0,0,0,0.45); }
|
|
341
|
+
body.rail-open .rail-scrim { display:block; }
|
|
342
|
+
}
|
|
343
|
+
@media (max-width: 720px) {
|
|
344
|
+
.topbar { height:auto; min-height:52px; padding:8px 10px; gap:8px; flex-wrap:wrap; }
|
|
345
|
+
.topbar .spacer { display:none; }
|
|
346
|
+
.crumbs { flex:1 1 auto; min-width:0; gap:6px; }
|
|
347
|
+
.crumbs > span:first-child { max-width:86px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
348
|
+
.crumbs .here { max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
349
|
+
.topbar .search { order:2; width:100%; flex:1 0 100%; }
|
|
350
|
+
.topbar kbd { display:none; }
|
|
351
|
+
.signin { padding:7px 10px; font-size:12px; white-space:nowrap; }
|
|
352
|
+
.theme-toggle { width:34px; height:34px; flex-shrink:0; }
|
|
353
|
+
.tele-strip { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); overflow-x:visible; }
|
|
354
|
+
.tele-cell { min-width:0; padding:10px 12px; border-right:1px solid var(--rule); border-bottom:1px solid var(--rule); }
|
|
355
|
+
.tele-cell:nth-child(2n) { border-right:none; }
|
|
356
|
+
.tele-cell:last-child { grid-column:1 / -1; border-right:none; }
|
|
357
|
+
.tele-cell .sub { overflow-wrap:anywhere; }
|
|
358
|
+
.pane-scroll { padding:20px 14px 30px; overflow-x:hidden; }
|
|
359
|
+
.pane { max-width:100%; min-width:0; }
|
|
360
|
+
h1.pane-title { font-size:21px; }
|
|
361
|
+
p.pane-lede { font-size:14px; max-width:100%; }
|
|
362
|
+
.ov-grid { grid-template-columns:1fr; gap:12px; }
|
|
363
|
+
.ov-tile, .ov-tile.s4, .ov-tile.s8, .ov-tile.s12 { grid-column:1; min-width:0; }
|
|
364
|
+
.ov-tile { padding:14px; overflow:hidden; }
|
|
365
|
+
.log-table { width:100%; table-layout:fixed; }
|
|
366
|
+
.log-table td { padding:5px 6px; white-space:normal; overflow:hidden; text-overflow:ellipsis; }
|
|
367
|
+
.log-table td.ts { width:62px; }
|
|
368
|
+
.log-table td.st { width:38px; }
|
|
369
|
+
.log-table td.ms, .log-table td.dt { display:none; }
|
|
370
|
+
.block { overflow-x:auto; }
|
|
371
|
+
.block-head { align-items:flex-start; flex-wrap:wrap; }
|
|
372
|
+
.block-head .sub { flex-basis:100%; }
|
|
373
|
+
.safety-bar { grid-template-columns:1fr; }
|
|
374
|
+
.safety-cell { grid-template-columns:auto 1fr auto; padding:12px 14px; }
|
|
375
|
+
.perm-toolbar, .perm-bulkbar { padding:10px 12px; align-items:flex-start; }
|
|
376
|
+
.perm-toolbar .filter-group { max-width:100%; overflow-x:auto; }
|
|
377
|
+
.perm-toolbar .filter-group button { white-space:nowrap; }
|
|
378
|
+
.perm-toolbar .count-readout { width:100%; margin-left:0; }
|
|
379
|
+
.perm-table { min-width:680px; }
|
|
380
|
+
.perm-table td.col-nm { white-space:normal; overflow-wrap:anywhere; }
|
|
381
|
+
.kv { grid-template-columns:1fr; gap:2px; }
|
|
382
|
+
.desktop-metrics { grid-template-columns:repeat(2, minmax(0, 1fr)); }
|
|
383
|
+
.desktop-metric.wide { grid-column:1 / -1; }
|
|
384
|
+
.setup-checklist { grid-template-columns:1fr; }
|
|
385
|
+
.drawer { width:100vw; max-width:100vw; }
|
|
386
|
+
.drawer-body { padding:16px; }
|
|
387
|
+
.drawer-body .fields { grid-template-columns:1fr; }
|
|
388
|
+
}
|
|
389
|
+
</style>
|
|
390
|
+
</head>
|
|
391
|
+
<body data-view="${signedIn ? 'signedin' : 'signedout'}" data-theme="light">
|
|
392
|
+
|
|
393
|
+
<div class="app">
|
|
394
|
+
|
|
395
|
+
<aside class="rail">
|
|
396
|
+
<div class="rail-head">
|
|
397
|
+
<div class="rail-brand"><span>empir<span class="three">3</span></span><span class="sub">Bridge</span></div>
|
|
398
|
+
<div class="rail-tag"><span class="live"></span>BRIDGE ${BRIDGE_VERSION} · LIVE</div>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<nav class="rail-nav">
|
|
402
|
+
<div class="rail-section-label">Console</div>
|
|
403
|
+
<a data-nav="overview" class="active">
|
|
404
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="5" height="6" rx="0.6"/><rect x="9" y="2" width="5" height="4" rx="0.6"/><rect x="2" y="10" width="5" height="4" rx="0.6"/><rect x="9" y="8" width="5" height="6" rx="0.6"/></svg>
|
|
405
|
+
Overview
|
|
406
|
+
</a>
|
|
407
|
+
<a data-nav="permissions">
|
|
408
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1L2 4v4c0 3.5 2.5 6.5 6 7 3.5-.5 6-3.5 6-7V4L8 1z"/><path d="M5.5 8l2 2 3-3.5"/></svg>
|
|
409
|
+
Permissions
|
|
410
|
+
<span class="count" id="sidebarPermCount">${toolCount}</span>
|
|
411
|
+
</a>
|
|
412
|
+
<a data-nav="mcp">
|
|
413
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4l-3 4 3 4M12 4l3 4-3 4M10 2L6 14"/></svg>
|
|
414
|
+
MCP Connection
|
|
415
|
+
</a>
|
|
416
|
+
<a data-nav="clis">
|
|
417
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h12v10H2z"/><path d="M4 6l2 2-2 2M8 10h4"/></svg>
|
|
418
|
+
API & CLIs
|
|
419
|
+
</a>
|
|
420
|
+
<a data-nav="agent">
|
|
421
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="12" height="12" rx="1"/><path d="M6 6h4v4H6z"/></svg>
|
|
422
|
+
Desktop Tools
|
|
423
|
+
</a>
|
|
424
|
+
<a data-nav="account">
|
|
425
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="6" r="3"/><path d="M2 14c0-3 2.5-5 6-5s6 2 6 5"/></svg>
|
|
426
|
+
empir3 Account
|
|
427
|
+
</a>
|
|
428
|
+
|
|
429
|
+
<div class="rail-section-label">System</div>
|
|
430
|
+
<a data-nav="daemon">
|
|
431
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2.4"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.2 3.2l1.4 1.4M11.4 11.4l1.4 1.4M3.2 12.8l1.4-1.4M11.4 4.6l1.4-1.4"/></svg>
|
|
432
|
+
Daemon
|
|
433
|
+
</a>
|
|
434
|
+
<a data-nav="updates">
|
|
435
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8a6 6 0 0 1 10-4.5L14 5M14 8a6 6 0 0 1-10 4.5L2 11M11 5h3V2M5 11H2v3"/></svg>
|
|
436
|
+
Updates
|
|
437
|
+
</a>
|
|
438
|
+
<a data-nav="logs">
|
|
439
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2h7l3 3v9H3V2zM10 2v3h3M5 8h6M5 11h6"/></svg>
|
|
440
|
+
Activity Log
|
|
441
|
+
</a>
|
|
442
|
+
<a data-nav="lifecycle" class="danger">
|
|
443
|
+
<svg class="ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1L1 14h14L8 1zM8 6v4M8 12v.5"/></svg>
|
|
444
|
+
Tray Lifecycle
|
|
445
|
+
</a>
|
|
446
|
+
</nav>
|
|
447
|
+
|
|
448
|
+
<div class="rail-foot">
|
|
449
|
+
<div class="row"><span class="led" id="footDaemonLed"></span><span class="k">daemon</span><span style="margin-left:auto" id="footDaemonUptime">—</span></div>
|
|
450
|
+
<div class="row"><span class="led" id="footMcpLed"></span><span class="k">mcp</span><span style="margin-left:auto" id="footMcpCount">—</span></div>
|
|
451
|
+
<div class="row"><span class="led${signedIn ? '' : ' warn'}" id="footRelayLed"></span><span class="k">relay</span><span style="margin-left:auto" id="footRelayState">${signedIn ? 'connected' : 'unpaired'}</span></div>
|
|
452
|
+
</div>
|
|
453
|
+
</aside>
|
|
454
|
+
|
|
455
|
+
<div class="rail-scrim" id="railScrim"></div>
|
|
456
|
+
|
|
457
|
+
<main class="console">
|
|
458
|
+
|
|
459
|
+
<div class="topbar">
|
|
460
|
+
<button class="rail-toggle" type="button" id="railToggle" title="Show navigation" aria-label="Show navigation">
|
|
461
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M2 8h12M2 12h12"/></svg>
|
|
462
|
+
</button>
|
|
463
|
+
<div class="crumbs">
|
|
464
|
+
<span>empir<span style="color:var(--accent); font-weight:700;">3</span> Bridge</span>
|
|
465
|
+
<span class="sep">/</span>
|
|
466
|
+
<span class="here" id="crumbHere">Overview</span>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="spacer"></div>
|
|
469
|
+
<div class="search">
|
|
470
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="7" cy="7" r="5"/><path d="M14 14l-3-3"/></svg>
|
|
471
|
+
<input placeholder="Search tools, settings…" id="globalSearch" />
|
|
472
|
+
<kbd>⌘K</kbd>
|
|
473
|
+
</div>
|
|
474
|
+
<button class="theme-toggle" type="button" id="themeToggle" title="Toggle theme" aria-label="Toggle theme">
|
|
475
|
+
<svg class="sun" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="3"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.2 3.2l1.4 1.4M11.4 11.4l1.4 1.4M3.2 12.8l1.4-1.4M11.4 4.6l1.4-1.4"/></svg>
|
|
476
|
+
<svg class="moon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.5 9.5A5.5 5.5 0 0 1 6.5 2.5a6 6 0 1 0 7 7z"/></svg>
|
|
477
|
+
</button>
|
|
478
|
+
<button class="signin when-signedout" type="button" id="openSignIn">
|
|
479
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11l3-3-3-3M13 8H6M9 14H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h5"/></svg>
|
|
480
|
+
Sign in with <span class="brand-inline">empir<span class="three">3</span></span>
|
|
481
|
+
</button>
|
|
482
|
+
<button class="acct when-signedin" type="button" id="openAccount">
|
|
483
|
+
<span class="av" id="acctAvatar">··</span><span class="em" id="acctEmail">loading…</span><span class="car">▾</span>
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<div class="tele-strip">
|
|
488
|
+
<div class="tele-cell">
|
|
489
|
+
<div class="lbl"><span class="led" id="teleDaemonLed"></span>Daemon</div>
|
|
490
|
+
<div class="val" id="teleDaemonVal">RUNNING</div>
|
|
491
|
+
<div class="sub" id="teleDaemonSub">PID ${process.pid} · uptime —</div>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="tele-cell">
|
|
494
|
+
<div class="lbl"><span class="led" id="teleMcpLed"></span>MCP</div>
|
|
495
|
+
<div class="val" id="teleMcpVal">READY</div>
|
|
496
|
+
<div class="sub" id="teleMcpSub">stdio · — calls / — errors</div>
|
|
497
|
+
</div>
|
|
498
|
+
<div class="tele-cell">
|
|
499
|
+
<div class="lbl"><span class="led idle" id="teleAgentLed"></span>Agent</div>
|
|
500
|
+
<div class="val" id="teleAgentVal">IDLE</div>
|
|
501
|
+
<div class="sub" id="teleAgentSub">no focus region</div>
|
|
502
|
+
</div>
|
|
503
|
+
<div class="tele-cell">
|
|
504
|
+
<div class="lbl"><span class="led${signedIn ? '' : ' warn'}" id="teleRelayLed"></span>Relay</div>
|
|
505
|
+
<div class="val" id="teleRelayVal">—</div>
|
|
506
|
+
<div class="sub" id="teleRelaySub">—</div>
|
|
507
|
+
</div>
|
|
508
|
+
<div class="tele-cell">
|
|
509
|
+
<div class="lbl"><span class="led" id="teleSafetyLed"></span>Safety</div>
|
|
510
|
+
<div class="val" id="teleSafetyVal">— / — / —</div>
|
|
511
|
+
<div class="sub" id="teleSafetySub">—</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<div class="pane-scroll">
|
|
516
|
+
|
|
517
|
+
<section class="pane active" data-pane="overview">
|
|
518
|
+
<h1 class="pane-title">Overview</h1>
|
|
519
|
+
<p class="pane-lede">The <span class="brand-inline">empir<span class="three">3</span></span> Bridge is the local Chrome bridge for Claude Code, Claude Desktop, OpenAI, and any other MCP client. Everything runs on this PC.</p>
|
|
520
|
+
|
|
521
|
+
<div class="ov-grid">
|
|
522
|
+
|
|
523
|
+
<div class="ov-tile s4">
|
|
524
|
+
<h3>Daemon</h3>
|
|
525
|
+
<div class="big" id="ovDaemonUptime">—</div>
|
|
526
|
+
<p>Process <span class="mono">empir3-bridge</span> · wrapper <span class="mono">:${PORT}</span> · <span id="ovDaemonPid">PID ${process.pid}</span></p>
|
|
527
|
+
<div class="actions">
|
|
528
|
+
<button class="btn small" id="ovOpenBridge" type="button">Open bridge window</button>
|
|
529
|
+
<button class="btn small ghost" id="ovReconnect" type="button">Reconnect</button>
|
|
530
|
+
</div>
|
|
531
|
+
<p class="status" id="ovDaemonStatus"></p>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div class="ov-tile s4">
|
|
535
|
+
<h3>MCP Calls (last 80)</h3>
|
|
536
|
+
<div class="big" id="ovMcpCalls">— <span style="color:var(--soft); font-size:14px;">/ — err</span></div>
|
|
537
|
+
<p id="ovMcpLast">Loading recent activity…</p>
|
|
538
|
+
<div class="actions">
|
|
539
|
+
<button class="btn small" data-goto="logs" type="button">View call log</button>
|
|
540
|
+
<button class="btn small ghost" data-goto="mcp" type="button">Show config</button>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<div class="ov-tile s4">
|
|
545
|
+
<h3>Permissions</h3>
|
|
546
|
+
<div class="big" id="ovPermSummary">— / ${toolCount}</div>
|
|
547
|
+
<p id="ovPermTags"><span class="tag good">—</span></p>
|
|
548
|
+
<div class="actions">
|
|
549
|
+
<button class="btn small" data-goto="permissions" type="button">Manage permissions →</button>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<div class="ov-tile s8">
|
|
554
|
+
<h3>Recent activity</h3>
|
|
555
|
+
<table class="log-table" id="ovLogTable">
|
|
556
|
+
<tbody id="ovLogRows"><tr><td class="dt" style="text-align:center;">No activity yet.</td></tr></tbody>
|
|
557
|
+
</table>
|
|
558
|
+
</div>
|
|
559
|
+
|
|
560
|
+
<div class="ov-tile s4">
|
|
561
|
+
<h3>Relay</h3>
|
|
562
|
+
<div class="big when-signedout" style="color: var(--warn)">UNPAIRED</div>
|
|
563
|
+
<div class="big good when-signedin" id="ovRelayStatus">CONNECTED</div>
|
|
564
|
+
<p class="when-signedout">Sign in with <span class="brand-inline">empir<span class="three">3</span></span> to relay browser tools to your agents.</p>
|
|
565
|
+
<p class="when-signedin"><span class="mono" id="ovRelayUser">—</span> · <span class="mono" id="ovRelayServer">—</span></p>
|
|
566
|
+
<div class="actions">
|
|
567
|
+
<button class="btn small primary when-signedout" type="button" onclick="openDrawer()">Sign in</button>
|
|
568
|
+
<button class="btn small when-signedin" data-goto="account" type="button">Manage account</button>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
</div>
|
|
573
|
+
</section>
|
|
574
|
+
|
|
575
|
+
<section class="pane" data-pane="permissions">
|
|
576
|
+
<h1 class="pane-title">Permissions</h1>
|
|
577
|
+
<p class="pane-lede">Every bridge tool can be allowed or blocked at the PC level. Disabled tools never appear in the MCP client's inventory — same model as Anthropic computer-use, macOS Accessibility, or Chrome extension permissions: explicit consent, no AI judgment.</p>
|
|
578
|
+
|
|
579
|
+
<div class="block">
|
|
580
|
+
<div class="block-head">
|
|
581
|
+
<span class="ix">00</span>
|
|
582
|
+
<span class="title">Global Safety Override</span>
|
|
583
|
+
<span class="spacer"></span>
|
|
584
|
+
<span class="sub">The final PC-level switch. Disabling a category blocks every tool in it regardless of fine-tune.</span>
|
|
585
|
+
</div>
|
|
586
|
+
<div class="safety-bar">
|
|
587
|
+
<label class="safety-cell">
|
|
588
|
+
<span class="ico"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2"/></svg></span>
|
|
589
|
+
<div class="meta"><div class="name">Read</div><div class="sub">Screenshots, page text, snapshots, status</div></div>
|
|
590
|
+
<span class="sw"><input type="checkbox" data-safety="read"><span class="s"></span></span>
|
|
591
|
+
</label>
|
|
592
|
+
<label class="safety-cell">
|
|
593
|
+
<span class="ico"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z"/></svg></span>
|
|
594
|
+
<div class="meta"><div class="name">Write</div><div class="sub">Overlay chat, recordings, safety lockdown</div></div>
|
|
595
|
+
<span class="sw"><input type="checkbox" data-safety="write"><span class="s"></span></span>
|
|
596
|
+
</label>
|
|
597
|
+
<label class="safety-cell">
|
|
598
|
+
<span class="ico"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2l9 6-9 6V2z"/></svg></span>
|
|
599
|
+
<div class="meta"><div class="name">Execute</div><div class="sub">Click, type, navigate, drag, run JS</div></div>
|
|
600
|
+
<span class="sw"><input type="checkbox" data-safety="execute"><span class="s"></span></span>
|
|
601
|
+
</label>
|
|
602
|
+
</div>
|
|
603
|
+
<p class="status" id="safetyStatus" style="padding:8px 16px 12px;"></p>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<div class="block">
|
|
607
|
+
<div class="block-head">
|
|
608
|
+
<span class="ix">01</span>
|
|
609
|
+
<span class="title">Tool-by-tool Permissions</span>
|
|
610
|
+
<span class="spacer"></span>
|
|
611
|
+
<span class="sub">${toolCount} tools across 7 groups</span>
|
|
612
|
+
</div>
|
|
613
|
+
<div class="perm-bulkbar">
|
|
614
|
+
<div class="bulk-actions">
|
|
615
|
+
<button class="btn small" type="button" data-bulk="enable-all">Enable all</button>
|
|
616
|
+
<button class="btn small" type="button" data-bulk="default">Restore defaults</button>
|
|
617
|
+
<button class="btn small" type="button" data-bulk="disable-all">Disable all</button>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="scope-note">
|
|
620
|
+
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><circle cx="8" cy="8" r="6.5"/><path d="M8 4.5v4M8 11v.5"/></svg>
|
|
621
|
+
<span>Disabling a tool blocks it everywhere — MCP, local chat, and <span class="brand-inline">empir<span class="three">3</span></span> relay.</span>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
<div class="perm-toolbar">
|
|
625
|
+
<div class="filter-group" role="tablist">
|
|
626
|
+
<button class="on" data-filter="all">All</button>
|
|
627
|
+
<button data-filter="advisor">Advisor</button>
|
|
628
|
+
<button data-filter="read">Read</button>
|
|
629
|
+
<button data-filter="navigate">Navigate</button>
|
|
630
|
+
<button data-filter="interact">Interact</button>
|
|
631
|
+
<button data-filter="desktop">Desktop</button>
|
|
632
|
+
<button data-filter="eval">Eval</button>
|
|
633
|
+
<button data-filter="recordings">Recordings</button>
|
|
634
|
+
<button data-filter="higgsfield">Higgsfield</button>
|
|
635
|
+
</div>
|
|
636
|
+
<div class="count-readout"><span class="num" id="enabledCount">—</span> / <span class="num" id="visibleToolCount">${toolCount}</span> enabled</div>
|
|
637
|
+
</div>
|
|
638
|
+
<div style="overflow-x:auto;">
|
|
639
|
+
<table class="perm-table">
|
|
640
|
+
<thead>
|
|
641
|
+
<tr><th>On</th><th>Tool</th><th>Group</th><th>What it does</th></tr>
|
|
642
|
+
</thead>
|
|
643
|
+
<tbody id="permRows"></tbody>
|
|
644
|
+
</table>
|
|
645
|
+
</div>
|
|
646
|
+
<p class="status" id="permStatus" style="padding:8px 16px 12px;"></p>
|
|
647
|
+
</div>
|
|
648
|
+
</section>
|
|
649
|
+
|
|
650
|
+
<section class="pane" data-pane="mcp">
|
|
651
|
+
<h1 class="pane-title">MCP Connection</h1>
|
|
652
|
+
<p class="pane-lede">Add the <span class="brand-inline">empir<span class="three">3</span></span> Bridge as a stdio MCP server in Claude Code, Claude Desktop, OpenAI, or any other client.</p>
|
|
653
|
+
|
|
654
|
+
<div class="block">
|
|
655
|
+
<div class="block-head">
|
|
656
|
+
<span class="ix">01</span>
|
|
657
|
+
<span class="title">stdio Config</span>
|
|
658
|
+
<span class="spacer"></span>
|
|
659
|
+
<span class="tag good">READY</span>
|
|
660
|
+
</div>
|
|
661
|
+
<div class="block-body">
|
|
662
|
+
<div style="display:flex; gap:8px; margin-bottom:12px;">
|
|
663
|
+
<button class="btn primary" id="mcpShowConfig" type="button">Show config</button>
|
|
664
|
+
<button class="btn" id="mcpCopyConfig" type="button">Copy snippet</button>
|
|
665
|
+
</div>
|
|
666
|
+
<pre class="snippet" id="mcpSnippet">Click "Show config" to generate the snippet for this install.</pre>
|
|
667
|
+
<ol id="mcpSteps" style="margin:14px 0 0; padding-left:20px; color:var(--muted); line-height:1.7; font-size:13px;">
|
|
668
|
+
<li>Save the config as <code style="background:var(--surface-2); padding:1px 6px; border-radius:3px;">.mcp.json</code> in your project root.</li>
|
|
669
|
+
<li>Restart your MCP client from that folder.</li>
|
|
670
|
+
<li>Ask the client to use the <span class="brand-inline">empir<span class="three">3</span></span> Bridge browser or desktop tools.</li>
|
|
671
|
+
</ol>
|
|
672
|
+
<p class="status" id="mcpStatus" style="margin-top:10px;"></p>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
</section>
|
|
676
|
+
|
|
677
|
+
<section class="pane" data-pane="clis">
|
|
678
|
+
<h1 class="pane-title">API & CLIs</h1>
|
|
679
|
+
<p class="pane-lede">Local CLIs the bridge knows about, the API keys it can hold, and which inference CLIs you've opted-in to lend to <span class="brand-inline">empir<span class="three">3</span></span> team agents.</p>
|
|
680
|
+
|
|
681
|
+
<div class="block">
|
|
682
|
+
<div class="block-head">
|
|
683
|
+
<span class="ix">01</span>
|
|
684
|
+
<span class="title">Installed CLIs</span>
|
|
685
|
+
<span class="spacer"></span>
|
|
686
|
+
<span class="tag" id="cliInstalledTag">— / —</span>
|
|
687
|
+
</div>
|
|
688
|
+
<div class="block-body">
|
|
689
|
+
<div style="overflow-x:auto;">
|
|
690
|
+
<table class="perm-table" style="font-size:13px;">
|
|
691
|
+
<thead>
|
|
692
|
+
<tr>
|
|
693
|
+
<th style="width:24%;">CLI</th>
|
|
694
|
+
<th style="width:20%;">Install</th>
|
|
695
|
+
<th style="width:18%;">Auth</th>
|
|
696
|
+
<th style="width:20%;" title="For inference CLIs: lend to empir3 team agents. For handler CLIs: gate whether the bridge advertises the tools at all.">Lend / Tools</th>
|
|
697
|
+
<th style="width:18%;">Action</th>
|
|
698
|
+
</tr>
|
|
699
|
+
</thead>
|
|
700
|
+
<tbody id="cliRows">
|
|
701
|
+
<tr><td colspan="5" class="dt" style="text-align:center; color:var(--soft);">Loading…</td></tr>
|
|
702
|
+
</tbody>
|
|
703
|
+
</table>
|
|
704
|
+
</div>
|
|
705
|
+
<div style="display:flex; gap:8px; margin-top:14px; flex-wrap:wrap;">
|
|
706
|
+
<button class="btn" type="button" id="rescanClisBtn">↻ Re-scan</button>
|
|
707
|
+
<button class="btn" type="button" id="addCustomProviderBtn">+ Add custom provider</button>
|
|
708
|
+
<span style="font-size:11.5px; color:var(--soft); align-self:center;">Re-scan after installing a CLI · custom = OpenAI-compatible endpoint (Ollama, LM Studio, OpenRouter, vLLM, etc)</span>
|
|
709
|
+
</div>
|
|
710
|
+
<p class="status" id="cliStatus" style="margin-top:10px;"></p>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<!-- Add Custom Provider modal -->
|
|
715
|
+
<div id="providerModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.55); z-index:100; align-items:center; justify-content:center;">
|
|
716
|
+
<div style="background:var(--paper); color:var(--ink); border:1px solid var(--surface-2); border-radius:8px; max-width:640px; width:92%; max-height:90vh; overflow:auto; padding:24px;">
|
|
717
|
+
<div style="display:flex; align-items:center; margin-bottom:14px;">
|
|
718
|
+
<h2 style="margin:0; font-size:18px; font-weight:700;">Add custom provider</h2>
|
|
719
|
+
<button class="btn small ghost" type="button" id="providerModalClose" style="margin-left:auto;">✕</button>
|
|
720
|
+
</div>
|
|
721
|
+
<p style="margin:0 0 14px; color:var(--muted); font-size:13px;">Paste a JSON definition for an OpenAI-compatible provider. Must include <code>slug</code>, <code>name</code>, <code>apiBaseUrl</code>. <code>models</code> auto-populates from <code>/v1/models</code> if omitted; <code>apiKey</code> is optional (Ollama doesn't need one, OpenRouter does).</p>
|
|
722
|
+
<textarea id="providerModalJson" spellcheck="false" style="width:100%; min-height:240px; padding:12px; font-family:var(--mono); font-size:13px; background:var(--surface); border:1px solid var(--surface-2); border-radius:4px; color:var(--ink); resize:vertical;" placeholder='{
|
|
723
|
+
"slug": "ollama-local",
|
|
724
|
+
"name": "Ollama (local)",
|
|
725
|
+
"apiBaseUrl": "http://localhost:11434/v1"
|
|
726
|
+
}'></textarea>
|
|
727
|
+
<div style="display:flex; gap:8px; margin-top:12px;">
|
|
728
|
+
<button class="btn primary" type="button" id="providerModalSave">Add provider</button>
|
|
729
|
+
<button class="btn ghost" type="button" id="providerModalCancel">Cancel</button>
|
|
730
|
+
<button class="btn small ghost" type="button" id="providerModalExample" style="margin-left:auto;">Insert example</button>
|
|
731
|
+
</div>
|
|
732
|
+
<p class="status" id="providerModalStatus" style="margin-top:10px;"></p>
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
|
|
736
|
+
<div class="block">
|
|
737
|
+
<div class="block-head">
|
|
738
|
+
<span class="ix">02</span>
|
|
739
|
+
<span class="title">API Keys</span>
|
|
740
|
+
<span class="spacer"></span>
|
|
741
|
+
<span class="tag" id="apiKeysTag">— set</span>
|
|
742
|
+
</div>
|
|
743
|
+
<div class="block-body">
|
|
744
|
+
<p style="margin:0 0 14px; color:var(--muted); font-size:13px;">Keys stay on this PC in <code style="background:var(--surface-2); padding:1px 6px; border-radius:3px;">~/.empir3-bridge/config.json</code> (chmod 600). Used when an MCP client or empir3 agent picks the direct-API route instead of a local CLI. Leave blank to keep the existing value — submitting an empty field never clobbers a saved key.</p>
|
|
745
|
+
<div class="kv" style="grid-template-columns: 140px 1fr; gap:10px 14px;">
|
|
746
|
+
<div class="k">Anthropic</div><div class="v"><input type="password" id="apiKeyAnthropic" placeholder="sk-ant-…" autocomplete="off" style="width:100%; padding:7px 10px; font-family:var(--mono); background:var(--surface); border:1px solid var(--surface-2); border-radius:4px; color:var(--ink);"></div>
|
|
747
|
+
<div class="k">OpenAI</div><div class="v"><input type="password" id="apiKeyOpenai" placeholder="sk-…" autocomplete="off" style="width:100%; padding:7px 10px; font-family:var(--mono); background:var(--surface); border:1px solid var(--surface-2); border-radius:4px; color:var(--ink);"></div>
|
|
748
|
+
<div class="k">Google</div><div class="v"><input type="password" id="apiKeyGoogle" placeholder="AIza…" autocomplete="off" style="width:100%; padding:7px 10px; font-family:var(--mono); background:var(--surface); border:1px solid var(--surface-2); border-radius:4px; color:var(--ink);"></div>
|
|
749
|
+
<div class="k">xAI (Grok)</div><div class="v"><input type="password" id="apiKeyXai" placeholder="xai-…" autocomplete="off" style="width:100%; padding:7px 10px; font-family:var(--mono); background:var(--surface); border:1px solid var(--surface-2); border-radius:4px; color:var(--ink);"></div>
|
|
750
|
+
</div>
|
|
751
|
+
<div style="display:flex; gap:8px; margin-top:14px;">
|
|
752
|
+
<button class="btn primary" id="apiKeysSave" type="button">Save keys</button>
|
|
753
|
+
<button class="btn" id="apiKeysReveal" type="button">Show/hide</button>
|
|
754
|
+
</div>
|
|
755
|
+
<p class="status" id="apiKeysStatus" style="margin-top:10px;"></p>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</section>
|
|
759
|
+
|
|
760
|
+
<section class="pane" data-pane="agent">
|
|
761
|
+
<h1 class="pane-title">Desktop Tools</h1>
|
|
762
|
+
<p class="pane-lede">Open the safe desktop-test harness or the Accuracy Lab click-precision stress test, inspect the controlled browser window, and scope desktop actions before clicking real apps.</p>
|
|
763
|
+
|
|
764
|
+
<div class="block">
|
|
765
|
+
<div class="block-head">
|
|
766
|
+
<span class="ix">01</span>
|
|
767
|
+
<span class="title">Testing Tools And Calibration</span>
|
|
768
|
+
<span class="spacer"></span>
|
|
769
|
+
<span class="tag warn" id="setupTag">CHECK REQUIRED</span>
|
|
770
|
+
</div>
|
|
771
|
+
<div class="block-body">
|
|
772
|
+
<div class="setup-checklist">
|
|
773
|
+
<div class="setup-item" id="setupItemOverlay"><span class="setup-dot"></span><div><strong>Overlay chat injected</strong><small id="setupOverlayText">Checking overlay bubble.</small></div></div>
|
|
774
|
+
<div class="setup-item" id="setupItemMonitors"><span class="setup-dot"></span><div><strong>Monitors detected</strong><small id="setupMonitorText">Checking display map.</small></div></div>
|
|
775
|
+
<div class="setup-item" id="setupItemCalibration"><span class="setup-dot"></span><div><strong>Click calibration saved</strong><small id="setupCalibrationText">Checking persisted calibration.</small></div></div>
|
|
776
|
+
<div class="setup-item" id="setupItemRecordings"><span class="setup-dot"></span><div><strong>Recording tools ready</strong><small id="setupRecordingText">Checking record/playback surface.</small></div></div>
|
|
777
|
+
</div>
|
|
778
|
+
<div class="desktop-actions">
|
|
779
|
+
<button class="btn primary" id="setupInjectOverlay" type="button">Inject overlay chat</button>
|
|
780
|
+
<button class="btn" id="setupDetectMonitors" type="button">Detect monitors</button>
|
|
781
|
+
<button class="btn" id="setupCalibratePrimary" type="button">Calibrate primary</button>
|
|
782
|
+
<button class="btn" id="setupSaveComplete" type="button">Save setup complete</button>
|
|
783
|
+
</div>
|
|
784
|
+
<p class="status" id="setupStatus" style="margin-top:8px;"></p>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
<div class="block">
|
|
789
|
+
<div class="block-head">
|
|
790
|
+
<span class="ix">02</span>
|
|
791
|
+
<span class="title">Bridge Test Harness</span>
|
|
792
|
+
<span class="spacer"></span>
|
|
793
|
+
<span class="tag" id="dtBridgeTag">CHECKING</span>
|
|
794
|
+
</div>
|
|
795
|
+
<div class="block-body">
|
|
796
|
+
<div class="desktop-metrics">
|
|
797
|
+
<div class="desktop-metric">
|
|
798
|
+
<div class="label">Bridge</div>
|
|
799
|
+
<div class="value" id="dtBridgeStatus">Checking</div>
|
|
800
|
+
</div>
|
|
801
|
+
<div class="desktop-metric">
|
|
802
|
+
<div class="label">Messages</div>
|
|
803
|
+
<div class="value" id="dtMessageCount">0</div>
|
|
804
|
+
</div>
|
|
805
|
+
<div class="desktop-metric">
|
|
806
|
+
<div class="label">Overlay</div>
|
|
807
|
+
<div class="value" id="dtOverlayStatus">-</div>
|
|
808
|
+
</div>
|
|
809
|
+
<div class="desktop-metric">
|
|
810
|
+
<div class="label">Safety</div>
|
|
811
|
+
<div class="value" id="dtSafetyStatus">-</div>
|
|
812
|
+
</div>
|
|
813
|
+
<div class="desktop-metric wide">
|
|
814
|
+
<div class="label">Current URL</div>
|
|
815
|
+
<div class="value small mono" id="dtCurrentUrl">-</div>
|
|
816
|
+
</div>
|
|
817
|
+
<div class="desktop-metric wide">
|
|
818
|
+
<div class="label">Control Detail</div>
|
|
819
|
+
<div class="value small" id="dtSafetyDetail">read - write - execute</div>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
<div class="desktop-actions">
|
|
823
|
+
<button class="btn primary" id="dtOpenDesktopTest" type="button">Open desktop test</button>
|
|
824
|
+
<button class="btn primary" id="dtOpenAccuracyLab" type="button">Open Accuracy Lab</button>
|
|
825
|
+
<button class="btn" id="dtBrowserScreenshot" type="button">Screenshot</button>
|
|
826
|
+
<button class="btn" id="dtBrowserRefresh" type="button">Refresh</button>
|
|
827
|
+
<button class="btn" id="dtBrowserSnapshot" type="button">Snapshot</button>
|
|
828
|
+
<button class="btn" id="dtInjectOverlay" type="button">Inject overlay</button>
|
|
829
|
+
<button class="btn" id="dtOpenToolbar" type="button">Open floating toolbar</button>
|
|
830
|
+
<button class="btn danger" id="dtRevokeControl" type="button">Revoke write control</button>
|
|
831
|
+
</div>
|
|
832
|
+
<pre class="desktop-output" id="dtCommandOutput"></pre>
|
|
833
|
+
<p class="status" id="desktopToolsStatus" style="margin-top:10px;"></p>
|
|
834
|
+
</div>
|
|
835
|
+
</div>
|
|
836
|
+
|
|
837
|
+
<div class="block">
|
|
838
|
+
<div class="block-head">
|
|
839
|
+
<span class="ix">03</span>
|
|
840
|
+
<span class="title">Recording And Playback</span>
|
|
841
|
+
<span class="spacer"></span>
|
|
842
|
+
<span class="tag" id="recordingTag">IDLE</span>
|
|
843
|
+
</div>
|
|
844
|
+
<div class="block-body">
|
|
845
|
+
<div class="desktop-metrics">
|
|
846
|
+
<div class="desktop-metric">
|
|
847
|
+
<div class="label">Recorder</div>
|
|
848
|
+
<div class="value" id="recState">Idle</div>
|
|
849
|
+
</div>
|
|
850
|
+
<div class="desktop-metric">
|
|
851
|
+
<div class="label">Actions</div>
|
|
852
|
+
<div class="value" id="recActionCount">0</div>
|
|
853
|
+
</div>
|
|
854
|
+
<div class="desktop-metric">
|
|
855
|
+
<div class="label">Saved</div>
|
|
856
|
+
<div class="value" id="recSavedCount">0</div>
|
|
857
|
+
</div>
|
|
858
|
+
<div class="desktop-metric">
|
|
859
|
+
<div class="label">Playback</div>
|
|
860
|
+
<div class="value" id="recPlayState">Ready</div>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
<div class="desktop-actions">
|
|
864
|
+
<button class="btn primary" id="recStart" type="button">Start recording</button>
|
|
865
|
+
<button class="btn" id="recStop" type="button">Stop and save</button>
|
|
866
|
+
<input id="recName" type="text" placeholder="Recording name">
|
|
867
|
+
<button class="btn" id="recRefresh" type="button">Pull recordings</button>
|
|
868
|
+
<select id="recSelect" aria-label="Saved recording"></select>
|
|
869
|
+
<button class="btn" id="recLoad" type="button">Pull up recording</button>
|
|
870
|
+
<button class="btn" id="recPlay" type="button">Play recording</button>
|
|
871
|
+
</div>
|
|
872
|
+
<pre class="desktop-output" id="recPreview"></pre>
|
|
873
|
+
<p class="status" id="recStatus" style="margin-top:8px;"></p>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
<div class="block">
|
|
878
|
+
<div class="block-head">
|
|
879
|
+
<span class="ix">04</span>
|
|
880
|
+
<span class="title">Focus Region</span>
|
|
881
|
+
<span class="spacer"></span>
|
|
882
|
+
<span class="tag" id="focusTag">NONE LOCKED</span>
|
|
883
|
+
</div>
|
|
884
|
+
<div class="block-body">
|
|
885
|
+
<div class="kv">
|
|
886
|
+
<div class="k">Status</div><div class="v" id="focusStatusText">No region — agent sees full virtual screen</div>
|
|
887
|
+
<div class="k">TTL</div><div class="v mono" id="focusTtl">—</div>
|
|
888
|
+
<div class="k">Bounds</div><div class="v mono" id="focusBounds">—</div>
|
|
889
|
+
</div>
|
|
890
|
+
<div style="display:flex; gap:8px; margin-top:14px; flex-wrap:wrap;">
|
|
891
|
+
<button class="btn primary" id="agtSelectRegion" type="button">Select region…</button>
|
|
892
|
+
<button class="btn" id="agtReleaseFocus" type="button">Release focus</button>
|
|
893
|
+
<button class="btn" id="agtFocusGrid" type="button" disabled>Show focus grid</button>
|
|
894
|
+
</div>
|
|
895
|
+
<p class="status" id="agentStatus" style="margin-top:8px;"></p>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
<div class="block">
|
|
900
|
+
<div class="block-head">
|
|
901
|
+
<span class="ix">05</span>
|
|
902
|
+
<span class="title">Click Calibration</span>
|
|
903
|
+
<span class="spacer"></span>
|
|
904
|
+
<span class="tag" id="calibrationTag">UNCALIBRATED</span>
|
|
905
|
+
</div>
|
|
906
|
+
<div class="block-body">
|
|
907
|
+
<div class="kv">
|
|
908
|
+
<div class="k">Offset X</div><div class="v mono" id="calOffsetX">—</div>
|
|
909
|
+
<div class="k">Offset Y</div><div class="v mono" id="calOffsetY">—</div>
|
|
910
|
+
<div class="k">Last run</div><div class="v mono" id="calLastRun">—</div>
|
|
911
|
+
<div class="k">Monitors</div><div class="v mono" id="calMonitorSummary">-</div>
|
|
912
|
+
</div>
|
|
913
|
+
<div class="desktop-actions" style="margin-top:14px;">
|
|
914
|
+
<button class="btn" id="agtDetectMonitors" type="button">Detect monitors</button>
|
|
915
|
+
<select id="agtCalMonitor" aria-label="Calibration monitor"><option value="primary">Primary monitor</option><option value="all">All monitors</option></select>
|
|
916
|
+
<button class="btn" id="agtCalibrate" type="button">Calibrate selected</button>
|
|
917
|
+
<button class="btn" id="agtCalibrateAll" type="button">Calibrate all</button>
|
|
918
|
+
</div>
|
|
919
|
+
<p class="status" id="calStatus" style="margin-top:8px;"></p>
|
|
920
|
+
</div>
|
|
921
|
+
</div>
|
|
922
|
+
</section>
|
|
923
|
+
|
|
924
|
+
<section class="pane" data-pane="account">
|
|
925
|
+
<h1 class="pane-title">empir3 Account</h1>
|
|
926
|
+
<p class="pane-lede">Pair this bridge with an <span class="brand-inline">empir<span class="three">3</span></span> account so agents can drive it remotely. The token stays on this PC.</p>
|
|
927
|
+
|
|
928
|
+
<div class="block when-signedout">
|
|
929
|
+
<div class="block-head">
|
|
930
|
+
<span class="ix">01</span>
|
|
931
|
+
<span class="title">Pairing</span>
|
|
932
|
+
<span class="spacer"></span>
|
|
933
|
+
<span class="tag warn">NOT PAIRED</span>
|
|
934
|
+
</div>
|
|
935
|
+
<div class="block-body">
|
|
936
|
+
<p style="margin:0 0 14px; color:var(--muted); font-size:13px;">Two ways to pair: use the empir3 account already in your browser, or sign in directly to store a token on this PC that survives browser logouts.</p>
|
|
937
|
+
<div style="display:flex; gap:8px;">
|
|
938
|
+
<button class="btn primary" type="button" onclick="openDrawer()">Sign in with <span class="brand-inline">empir<span class="three">3</span></span></button>
|
|
939
|
+
</div>
|
|
940
|
+
</div>
|
|
941
|
+
</div>
|
|
942
|
+
|
|
943
|
+
<div class="block when-signedin">
|
|
944
|
+
<div class="block-head">
|
|
945
|
+
<span class="ix">01</span>
|
|
946
|
+
<span class="title">Paired Account</span>
|
|
947
|
+
<span class="spacer"></span>
|
|
948
|
+
<span class="tag good" id="acctConnTag">CONNECTED</span>
|
|
949
|
+
</div>
|
|
950
|
+
<div class="block-body">
|
|
951
|
+
<div class="kv">
|
|
952
|
+
<div class="k">Account</div><div class="v" id="acctPaneEmail">—</div>
|
|
953
|
+
<div class="k">Server</div><div class="v mono" id="acctPaneServer">—</div>
|
|
954
|
+
<div class="k">Mode</div><div class="v" id="acctPaneMode">—</div>
|
|
955
|
+
<div class="k">Device</div><div class="v mono" id="acctPaneDevice">—</div>
|
|
956
|
+
</div>
|
|
957
|
+
<div style="display:flex; gap:8px; margin-top:14px;">
|
|
958
|
+
<button class="btn danger" id="acctSignOut" type="button">Sign out</button>
|
|
959
|
+
</div>
|
|
960
|
+
<p class="status" id="acctStatus" style="margin-top:8px;"></p>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
</section>
|
|
964
|
+
|
|
965
|
+
<section class="pane" data-pane="daemon">
|
|
966
|
+
<h1 class="pane-title">Daemon</h1>
|
|
967
|
+
<p class="pane-lede">The <span class="brand-inline">empir<span class="three">3</span></span> Bridge process and the Chrome window it drives.</p>
|
|
968
|
+
|
|
969
|
+
<div class="block">
|
|
970
|
+
<div class="block-head">
|
|
971
|
+
<span class="ix">01</span>
|
|
972
|
+
<span class="title">Process</span>
|
|
973
|
+
<span class="spacer"></span>
|
|
974
|
+
<span class="tag good" id="daemonHealthTag">HEALTHY</span>
|
|
975
|
+
</div>
|
|
976
|
+
<div class="block-body">
|
|
977
|
+
<div class="kv">
|
|
978
|
+
<div class="k">PID</div><div class="v mono" id="daemonPid">${process.pid}</div>
|
|
979
|
+
<div class="k">Uptime</div><div class="v mono" id="daemonUptime">—</div>
|
|
980
|
+
<div class="k">Bridge URL</div><div class="v mono" id="daemonBridgeUrl">http://localhost:${PORT}</div>
|
|
981
|
+
<div class="k">Version</div><div class="v mono">${BRIDGE_VERSION}</div>
|
|
982
|
+
</div>
|
|
983
|
+
<div style="display:flex; gap:8px; margin-top:14px; flex-wrap:wrap;">
|
|
984
|
+
<button class="btn" id="daemonOpenBridge" type="button">Open bridge window</button>
|
|
985
|
+
<button class="btn" id="daemonReconnect" type="button">Reconnect daemon</button>
|
|
986
|
+
<button class="btn ghost" id="daemonToggleLog" type="button">Show log</button>
|
|
987
|
+
</div>
|
|
988
|
+
<pre class="snippet" id="daemonLog" style="display:none; max-height:260px; margin-top:12px;"></pre>
|
|
989
|
+
<p class="status" id="daemonStatus" style="margin-top:8px;"></p>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
|
|
993
|
+
<div class="block">
|
|
994
|
+
<div class="block-head">
|
|
995
|
+
<span class="ix">02</span>
|
|
996
|
+
<span class="title">Identity</span>
|
|
997
|
+
<span class="spacer"></span>
|
|
998
|
+
<span class="tag" id="identityTag">—</span>
|
|
999
|
+
</div>
|
|
1000
|
+
<div class="block-body">
|
|
1001
|
+
<p style="margin:0 0 14px; color:var(--muted); font-size:13px;">How this PC is labeled in <span class="brand-inline">empir<span class="three">3</span></span> agents and where bridge-side file ops (project sync, file pulls/pushes) are scoped.</p>
|
|
1002
|
+
<div class="kv" style="grid-template-columns: 150px 1fr; gap:10px 14px;">
|
|
1003
|
+
<div class="k">Device name</div><div class="v"><input type="text" id="deviceNameInput" placeholder="MSI" autocomplete="off" style="width:100%; padding:7px 10px; font-family:var(--mono); background:var(--surface); border:1px solid var(--surface-2); border-radius:4px; color:var(--ink);"></div>
|
|
1004
|
+
<div class="k">Home directory</div><div class="v"><input type="text" id="homeDirInput" placeholder="C:\\Users\\you\\Documents\\Empir3" autocomplete="off" style="width:100%; padding:7px 10px; font-family:var(--mono); background:var(--surface); border:1px solid var(--surface-2); border-radius:4px; color:var(--ink);"></div>
|
|
1005
|
+
</div>
|
|
1006
|
+
<div style="display:flex; gap:8px; margin-top:14px;">
|
|
1007
|
+
<button class="btn primary" id="identitySave" type="button">Save identity</button>
|
|
1008
|
+
<button class="btn ghost" id="identityReset" type="button">Reset to current</button>
|
|
1009
|
+
</div>
|
|
1010
|
+
<p class="status" id="identityStatus" style="margin-top:10px;"></p>
|
|
1011
|
+
</div>
|
|
1012
|
+
</div>
|
|
1013
|
+
</section>
|
|
1014
|
+
|
|
1015
|
+
<section class="pane" data-pane="updates">
|
|
1016
|
+
<h1 class="pane-title">Updates</h1>
|
|
1017
|
+
<p class="pane-lede">Bridge payloads ship as auto-updateable packages. You can hold a version or auto-apply.</p>
|
|
1018
|
+
|
|
1019
|
+
<div class="block">
|
|
1020
|
+
<div class="block-head">
|
|
1021
|
+
<span class="ix">01</span>
|
|
1022
|
+
<span class="title">Version</span>
|
|
1023
|
+
<span class="spacer"></span>
|
|
1024
|
+
<span class="tag" id="updateTag">—</span>
|
|
1025
|
+
</div>
|
|
1026
|
+
<div class="block-body">
|
|
1027
|
+
<div class="kv">
|
|
1028
|
+
<div class="k">Installed</div><div class="v mono">${BRIDGE_VERSION}</div>
|
|
1029
|
+
<div class="k">Available</div><div class="v mono" id="updateAvailable">—</div>
|
|
1030
|
+
<div class="k">Last check</div><div class="v mono" id="updateLastCheck">—</div>
|
|
1031
|
+
</div>
|
|
1032
|
+
<div style="display:flex; gap:8px; margin-top:14px;">
|
|
1033
|
+
<button class="btn" id="updateCheck" type="button">Check now</button>
|
|
1034
|
+
<button class="btn primary" id="updateApply" type="button" style="display:none;">Apply update (restarts tray)</button>
|
|
1035
|
+
</div>
|
|
1036
|
+
<p class="status" id="updateStatus" style="margin-top:8px;"></p>
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
|
|
1040
|
+
<div class="block">
|
|
1041
|
+
<div class="block-head">
|
|
1042
|
+
<span class="ix">02</span>
|
|
1043
|
+
<span class="title">Policy</span>
|
|
1044
|
+
</div>
|
|
1045
|
+
<div class="block-body">
|
|
1046
|
+
<label style="display:flex; align-items:center; gap:10px; padding:10px 12px; border:1px solid var(--line); border-radius:5px; background:var(--surface); cursor:pointer;">
|
|
1047
|
+
<span class="sw"><input type="checkbox" id="autoUpdateToggle"><span class="s"></span></span>
|
|
1048
|
+
<span style="font-size:13px;"><strong>Auto-update</strong> — apply new bridge payloads automatically as they ship.</span>
|
|
1049
|
+
</label>
|
|
1050
|
+
<p class="status" id="policyStatus" style="margin-top:8px;"></p>
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
</section>
|
|
1054
|
+
|
|
1055
|
+
<section class="pane" data-pane="logs">
|
|
1056
|
+
<h1 class="pane-title">Activity Log</h1>
|
|
1057
|
+
<p class="pane-lede">Recent tool calls captured by the action log.</p>
|
|
1058
|
+
<div class="block">
|
|
1059
|
+
<div class="block-head">
|
|
1060
|
+
<span class="ix">01</span>
|
|
1061
|
+
<span class="title">Action Log</span>
|
|
1062
|
+
<span class="spacer"></span>
|
|
1063
|
+
<button class="btn small ghost" id="logsRefresh" type="button">Refresh</button>
|
|
1064
|
+
</div>
|
|
1065
|
+
<div class="block-body">
|
|
1066
|
+
<table class="log-table" id="logsTable">
|
|
1067
|
+
<tbody id="logsRows"><tr><td class="dt" style="text-align:center;">Loading…</td></tr></tbody>
|
|
1068
|
+
</table>
|
|
1069
|
+
</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
</section>
|
|
1072
|
+
|
|
1073
|
+
<section class="pane" data-pane="lifecycle">
|
|
1074
|
+
<h1 class="pane-title" style="color: var(--bad);">Tray Lifecycle</h1>
|
|
1075
|
+
<p class="pane-lede">These restart or remove the running tray. Each click asks for confirmation. Uninstall is irreversible.</p>
|
|
1076
|
+
|
|
1077
|
+
<div class="block" style="border-color: rgba(178,36,31,0.28);">
|
|
1078
|
+
<div class="block-head" style="background: var(--bad-soft); border-bottom-color: rgba(178,36,31,0.18);">
|
|
1079
|
+
<span class="ix" style="color:var(--bad);">!!</span>
|
|
1080
|
+
<span class="title" style="color:var(--bad);">Destructive Actions</span>
|
|
1081
|
+
<span class="spacer"></span>
|
|
1082
|
+
<span class="tag bad">CONFIRMATION REQUIRED</span>
|
|
1083
|
+
</div>
|
|
1084
|
+
<div class="block-body">
|
|
1085
|
+
<div style="display:grid; gap:10px;">
|
|
1086
|
+
<div style="display:grid; grid-template-columns: 1fr auto; gap:14px; align-items:center; padding:12px; border:1px solid var(--line); border-radius:5px; background:var(--surface);">
|
|
1087
|
+
<div>
|
|
1088
|
+
<div style="font-weight:700; font-size:13px;">Restart tray</div>
|
|
1089
|
+
<div style="font-size:11.5px; color:var(--muted); margin-top:1px;">The bridge daemon will restart with it. Active MCP sessions reconnect automatically.</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
<button class="btn danger" id="lifeRestart" type="button">Restart tray</button>
|
|
1092
|
+
</div>
|
|
1093
|
+
<div style="display:grid; grid-template-columns: 1fr auto; gap:14px; align-items:center; padding:12px; border:1px solid var(--line); border-radius:5px; background:var(--surface);">
|
|
1094
|
+
<div>
|
|
1095
|
+
<div style="font-weight:700; font-size:13px;">Quit Empir3</div>
|
|
1096
|
+
<div style="font-size:11.5px; color:var(--muted); margin-top:1px;">Tray icon disappears, bridge daemon stops. MCP clients lose connection.</div>
|
|
1097
|
+
</div>
|
|
1098
|
+
<button class="btn danger" id="lifeQuit" type="button">Quit Empir3</button>
|
|
1099
|
+
</div>
|
|
1100
|
+
<div style="display:grid; grid-template-columns: 1fr auto; gap:14px; align-items:center; padding:12px; border:1px solid var(--bad); border-radius:5px; background:var(--bad-soft);">
|
|
1101
|
+
<div>
|
|
1102
|
+
<div style="font-weight:700; font-size:13px; color:var(--bad);">Uninstall Empir3</div>
|
|
1103
|
+
<div style="font-size:11.5px; color:var(--muted); margin-top:1px;">Wipes Chrome profile, auth, settings, autostart entry, Start Menu shortcut, and cached payloads. Irreversible.</div>
|
|
1104
|
+
</div>
|
|
1105
|
+
<button class="btn danger" id="lifeUninstall" type="button">Uninstall…</button>
|
|
1106
|
+
</div>
|
|
1107
|
+
</div>
|
|
1108
|
+
<p class="status" id="lifeStatus" style="margin-top:10px;"></p>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
</section>
|
|
1112
|
+
|
|
1113
|
+
</div>
|
|
1114
|
+
</main>
|
|
1115
|
+
</div>
|
|
1116
|
+
|
|
1117
|
+
<div class="backdrop" id="backdrop"></div>
|
|
1118
|
+
<aside class="drawer" id="drawer">
|
|
1119
|
+
<div class="drawer-head">
|
|
1120
|
+
<span class="title">Sign in with <span class="brand-inline">empir<span class="three">3</span></span></span>
|
|
1121
|
+
<span class="spacer"></span>
|
|
1122
|
+
<button class="x" type="button" onclick="closeDrawer()" aria-label="Close">
|
|
1123
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
|
1124
|
+
</button>
|
|
1125
|
+
</div>
|
|
1126
|
+
<p class="sub">Pair this bridge with your <span class="brand-inline">empir<span class="three">3</span></span> account. The token stays on this PC.</p>
|
|
1127
|
+
<div class="drawer-body">
|
|
1128
|
+
<label>Server
|
|
1129
|
+
<select id="serverPreset">
|
|
1130
|
+
<option value="production">Production — app.empir3.com</option>
|
|
1131
|
+
<option value="local-dev">Local dev — localhost:3005</option>
|
|
1132
|
+
<option value="custom">Custom…</option>
|
|
1133
|
+
</select>
|
|
1134
|
+
</label>
|
|
1135
|
+
<label id="customServerLabel" style="display:none;">Custom URL <input id="serverUrl" type="url" value="${EMPIR3_SERVER}"></label>
|
|
1136
|
+
<button class="btn primary" id="pairEmpir3" type="button" style="width:100%;">Use browser <span class="brand-inline">empir<span class="three">3</span></span> login</button>
|
|
1137
|
+
<p class="hint">Reuses the <span class="brand-inline">empir<span class="three">3</span></span> account you're already signed into in your browser.</p>
|
|
1138
|
+
<div class="divider">or sign in directly</div>
|
|
1139
|
+
<form id="loginForm" style="display:grid; gap:10px;">
|
|
1140
|
+
<div class="fields">
|
|
1141
|
+
<label>Email<input type="email" id="loginEmail" autocomplete="username" placeholder="you@example.com"></label>
|
|
1142
|
+
<label>Password<input type="password" id="loginPassword" autocomplete="current-password" placeholder="••••••••"></label>
|
|
1143
|
+
</div>
|
|
1144
|
+
<button class="btn" type="submit" style="width:100%;">Sign in and store on this bridge</button>
|
|
1145
|
+
</form>
|
|
1146
|
+
<p class="hint">Stores a long-lived bridge token locally, independent of your browser session.</p>
|
|
1147
|
+
<p class="status" id="drawerStatus"></p>
|
|
1148
|
+
</div>
|
|
1149
|
+
</aside>
|
|
1150
|
+
|
|
1151
|
+
<script>
|
|
1152
|
+
// ────── Constants from server ──────
|
|
1153
|
+
const API = ${JSON.stringify(api)};
|
|
1154
|
+
const TOOLS = ${toolsJson};
|
|
1155
|
+
TOOLS.forEach(function(t){ t.on = t.d; });
|
|
1156
|
+
const PROD_SERVER = ${JSON.stringify(DEFAULT_EMPIR3_SERVER)};
|
|
1157
|
+
const DEV_SERVER = ${JSON.stringify(LOCAL_DEV_EMPIR3_SERVER)};
|
|
1158
|
+
let SAFETY = { read: true, write: false, execute: false };
|
|
1159
|
+
|
|
1160
|
+
const GROUP_ORDER = ['advisor','read','navigate','interact','desktop','eval','recordings','higgsfield'];
|
|
1161
|
+
const GROUP_LABEL = { advisor:'Advisor', read:'Read', navigate:'Navigate', interact:'Interact', desktop:'Desktop', eval:'JavaScript (Eval)', recordings:'Recordings & Chat', higgsfield:'Higgsfield CLI' };
|
|
1162
|
+
const GROUP_TAG = { advisor:'good', read:'good', navigate:'accent', interact:'warn', desktop:'warn', eval:'bad', recordings:'', higgsfield:'accent' };
|
|
1163
|
+
|
|
1164
|
+
const $ = function(id){ return document.getElementById(id); };
|
|
1165
|
+
function setText(id, v) { var el = $(id); if (el) el.textContent = v == null ? '—' : String(v); }
|
|
1166
|
+
function setStatus(id, msg, tone) {
|
|
1167
|
+
var el = $(id); if (!el) return;
|
|
1168
|
+
el.classList.remove('ok','err','info');
|
|
1169
|
+
if (tone) el.classList.add(tone);
|
|
1170
|
+
el.textContent = msg || '';
|
|
1171
|
+
}
|
|
1172
|
+
function setMetricTone(id, tone) {
|
|
1173
|
+
var el = $(id); if (!el) return;
|
|
1174
|
+
el.classList.remove('good','warn','bad');
|
|
1175
|
+
if (tone) el.classList.add(tone);
|
|
1176
|
+
}
|
|
1177
|
+
function escapeAttr(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
1178
|
+
function escapeHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
1179
|
+
|
|
1180
|
+
async function api(path, opts) {
|
|
1181
|
+
var r = await fetch(API + path, opts);
|
|
1182
|
+
var j = {}; try { j = await r.json(); } catch (_) {}
|
|
1183
|
+
if (!r.ok && !j.ok) throw new Error(j.error || ('http ' + r.status));
|
|
1184
|
+
return j;
|
|
1185
|
+
}
|
|
1186
|
+
// Single-flight per path + abort timeout. The dashboard runs ~6 polling
|
|
1187
|
+
// intervals (status/actions/focus/recording/cli/settings); if the daemon is
|
|
1188
|
+
// momentarily slow, fixed-clock intervals would re-fire before the prior fetch
|
|
1189
|
+
// returns and pile up concurrent requests — which loads the daemon's shared
|
|
1190
|
+
// event loop further and feeds a slow-loop⇄pile-up spiral. Collapsing
|
|
1191
|
+
// same-path GETs to one in-flight request (and aborting a stuck one) prevents
|
|
1192
|
+
// that pile-up so the daemon's trivial handlers stay responsive.
|
|
1193
|
+
var __getJsonInflight = {};
|
|
1194
|
+
async function getJson(path) {
|
|
1195
|
+
if (__getJsonInflight[path]) return __getJsonInflight[path];
|
|
1196
|
+
var ctrl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1197
|
+
var timer = ctrl ? setTimeout(function(){ try { ctrl.abort(); } catch (e) {} }, 8000) : null;
|
|
1198
|
+
var p = fetch(API + path, ctrl ? { signal: ctrl.signal } : undefined)
|
|
1199
|
+
.then(function(r){ return r.json(); })
|
|
1200
|
+
.finally(function(){ if (timer) clearTimeout(timer); delete __getJsonInflight[path]; });
|
|
1201
|
+
__getJsonInflight[path] = p;
|
|
1202
|
+
return p;
|
|
1203
|
+
}
|
|
1204
|
+
async function postJson(path, body) {
|
|
1205
|
+
return api(path, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body || {}) });
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// ────── Permissions table ──────
|
|
1209
|
+
// Map a tool group to its handler-family key in
|
|
1210
|
+
// settings.handlers.<key>.enabled. Only handler families (Higgsfield,
|
|
1211
|
+
// future Replicate/Runway/Suno) are gated this way — the core browser /
|
|
1212
|
+
// desktop groups have no family layer above them. Returns null for the
|
|
1213
|
+
// groups that bypass the family gate entirely.
|
|
1214
|
+
function familyHandlerKey(grp) {
|
|
1215
|
+
if (grp === 'higgsfield') return 'higgsfield';
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
function warnSvg() {
|
|
1219
|
+
return '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1L1 14h14L8 1zM8 6v4M8 12v.5"/></svg>';
|
|
1220
|
+
}
|
|
1221
|
+
function dependencyWarning(tool) {
|
|
1222
|
+
if (!tool.r || !tool.on) return '';
|
|
1223
|
+
if (SAFETY[tool.r]) return '';
|
|
1224
|
+
var attrTitle = tool.r.toUpperCase() + ' is OFF on the global safety override — this tool will be blocked until you enable ' + tool.r + '.';
|
|
1225
|
+
return ' <span class="dep-warn" title="' + escapeAttr(attrTitle) + '">' + warnSvg() + 'NEEDS ' + tool.r.toUpperCase() + '</span>';
|
|
1226
|
+
}
|
|
1227
|
+
function activeFilter() {
|
|
1228
|
+
var btn = document.querySelector('.perm-toolbar .filter-group button.on');
|
|
1229
|
+
return btn ? btn.dataset.filter : 'all';
|
|
1230
|
+
}
|
|
1231
|
+
var currentSearch = '';
|
|
1232
|
+
function renderTable(filter) {
|
|
1233
|
+
filter = filter || activeFilter();
|
|
1234
|
+
var tbody = $('permRows'); if (!tbody) return;
|
|
1235
|
+
tbody.innerHTML = '';
|
|
1236
|
+
var q = currentSearch.trim().toLowerCase();
|
|
1237
|
+
for (var gi = 0; gi < GROUP_ORDER.length; gi++) {
|
|
1238
|
+
var grp = GROUP_ORDER[gi];
|
|
1239
|
+
var rows = TOOLS.filter(function(t){
|
|
1240
|
+
if (t.hidden) return false;
|
|
1241
|
+
if (t.g !== grp) return false;
|
|
1242
|
+
if (filter !== 'all' && filter !== grp) return false;
|
|
1243
|
+
if (q && t.n.toLowerCase().indexOf(q) === -1 && (t.b||'').toLowerCase().indexOf(q) === -1) return false;
|
|
1244
|
+
return true;
|
|
1245
|
+
});
|
|
1246
|
+
if (!rows.length) continue;
|
|
1247
|
+
var onCount = rows.filter(function(r){ return r.on; }).length;
|
|
1248
|
+
var hdr = document.createElement('tr');
|
|
1249
|
+
hdr.className = 'group-header';
|
|
1250
|
+
hdr.innerHTML =
|
|
1251
|
+
'<td colspan="4">' +
|
|
1252
|
+
'<span>' + escapeHtml(GROUP_LABEL[grp]) + '</span>' +
|
|
1253
|
+
'<span class="count">' + onCount + ' / ' + rows.length + '</span>' +
|
|
1254
|
+
'<span class="bulk">' +
|
|
1255
|
+
'<button data-grp="' + grp + '" data-act="enable">enable all</button>' +
|
|
1256
|
+
'<button data-grp="' + grp + '" data-act="default">defaults</button>' +
|
|
1257
|
+
'<button data-grp="' + grp + '" data-act="disable">disable all</button>' +
|
|
1258
|
+
'</span>' +
|
|
1259
|
+
'</td>';
|
|
1260
|
+
tbody.appendChild(hdr);
|
|
1261
|
+
// Handler-family gate banner — when a family group is shown but its
|
|
1262
|
+
// tray/handler gate is off, every row below is effectively dead. Say
|
|
1263
|
+
// so plainly with a jump back to the API & CLIs pane.
|
|
1264
|
+
var familyGated = familyHandlerKey(grp);
|
|
1265
|
+
if (familyGated) {
|
|
1266
|
+
var fEnabled = !!(CLI_STATE && CLI_STATE.bridge && CLI_STATE.bridge.handlers && CLI_STATE.bridge.handlers[familyGated] && CLI_STATE.bridge.handlers[familyGated].enabled);
|
|
1267
|
+
if (!fEnabled) {
|
|
1268
|
+
var banner = document.createElement('tr');
|
|
1269
|
+
banner.innerHTML = '<td colspan="4" style="padding:10px 14px; background:rgba(243,156,18,0.08); border-left:3px solid var(--warn,#f39c12); color:var(--muted); font-size:12.5px;">' +
|
|
1270
|
+
'⚠ Family gate is OFF — these tools won\\'t appear in any MCP client until you enable <strong>' + escapeHtml(GROUP_LABEL[grp]) + '</strong> on the ' +
|
|
1271
|
+
'<a href="#" data-goto="clis" style="color:var(--accent); text-decoration:underline;">API & CLIs</a> page.' +
|
|
1272
|
+
'</td>';
|
|
1273
|
+
tbody.appendChild(banner);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
for (var i = 0; i < rows.length; i++) {
|
|
1277
|
+
var t = rows[i];
|
|
1278
|
+
var tr = document.createElement('tr');
|
|
1279
|
+
if (!t.on) tr.classList.add('disabled');
|
|
1280
|
+
if (t.g === 'eval') tr.classList.add('eval');
|
|
1281
|
+
var dangerTag = t.g === 'eval' ? ' <span class="tag bad" style="margin-left:6px;">DANGER</span>' : '';
|
|
1282
|
+
tr.innerHTML =
|
|
1283
|
+
'<td class="col-tg"><label class="sw"><input type="checkbox" data-tool="' + t.n + '"' + (t.on?' checked':'') + '><span class="s"></span></label></td>' +
|
|
1284
|
+
'<td class="col-nm">' + escapeHtml(t.n) + dangerTag + dependencyWarning(t) + '</td>' +
|
|
1285
|
+
'<td class="col-tag"><span class="tag ' + (GROUP_TAG[t.g] || '') + '">' + t.g.toUpperCase() + '</span></td>' +
|
|
1286
|
+
'<td class="col-blurb">' + escapeHtml(t.b) + '</td>';
|
|
1287
|
+
tbody.appendChild(tr);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
var visibleTools = TOOLS.filter(function(t){ return !t.hidden; });
|
|
1291
|
+
var enabled = visibleTools.filter(function(t){ return t.on; }).length;
|
|
1292
|
+
var blockedBySafety = visibleTools.filter(function(t){ return t.on && t.r && !SAFETY[t.r]; }).length;
|
|
1293
|
+
var effective = enabled - blockedBySafety;
|
|
1294
|
+
setText('enabledCount', enabled);
|
|
1295
|
+
setText('visibleToolCount', visibleTools.length);
|
|
1296
|
+
setText('sidebarPermCount', visibleTools.length);
|
|
1297
|
+
setText('ovPermSummary', effective + ' / ' + visibleTools.length);
|
|
1298
|
+
var ovTags = $('ovPermTags');
|
|
1299
|
+
if (ovTags) {
|
|
1300
|
+
var tags = '<span class="tag good">' + effective + ' READY</span>';
|
|
1301
|
+
if (blockedBySafety) tags += ' <span class="tag warn" style="margin-left:4px;">' + blockedBySafety + ' NEED SAFETY</span>';
|
|
1302
|
+
var disabled = visibleTools.length - enabled;
|
|
1303
|
+
if (disabled) tags += ' <span class="tag" style="margin-left:4px;">' + disabled + ' BLOCKED</span>';
|
|
1304
|
+
ovTags.innerHTML = tags;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
document.querySelectorAll('.perm-toolbar .filter-group button').forEach(function(btn){
|
|
1308
|
+
btn.addEventListener('click', function(){
|
|
1309
|
+
document.querySelectorAll('.perm-toolbar .filter-group button').forEach(function(b){ b.classList.remove('on'); });
|
|
1310
|
+
btn.classList.add('on');
|
|
1311
|
+
renderTable(btn.dataset.filter);
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
// Global ⌘K / Ctrl+K search — filters the permissions table by tool name
|
|
1315
|
+
// or blurb substring as the user types, and auto-switches to the
|
|
1316
|
+
// Permissions pane when something is queried.
|
|
1317
|
+
var gs = $('globalSearch');
|
|
1318
|
+
if (gs) {
|
|
1319
|
+
gs.addEventListener('input', function(){
|
|
1320
|
+
currentSearch = gs.value || '';
|
|
1321
|
+
if (currentSearch.trim() && typeof goto === 'function') {
|
|
1322
|
+
var permPane = document.querySelector('[data-pane="permissions"]');
|
|
1323
|
+
if (permPane && !permPane.classList.contains('active')) goto('permissions');
|
|
1324
|
+
}
|
|
1325
|
+
renderTable();
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
document.addEventListener('keydown', function(e){
|
|
1329
|
+
var isModK = (e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K');
|
|
1330
|
+
if (isModK && gs) {
|
|
1331
|
+
e.preventDefault();
|
|
1332
|
+
gs.focus();
|
|
1333
|
+
gs.select();
|
|
1334
|
+
} else if (e.key === 'Escape' && document.activeElement === gs && gs.value) {
|
|
1335
|
+
gs.value = '';
|
|
1336
|
+
currentSearch = '';
|
|
1337
|
+
renderTable();
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
// Save a single tool's enabled state to /api/settings/state
|
|
1341
|
+
async function saveToolPatch(name, on) {
|
|
1342
|
+
markLocalMutate();
|
|
1343
|
+
setStatus('permStatus', 'Saving ' + name + '…', 'info');
|
|
1344
|
+
try {
|
|
1345
|
+
var patch = {}; patch[name] = on;
|
|
1346
|
+
var r = await postJson('/api/settings/state', { chat: { enabledTools: patch } });
|
|
1347
|
+
// server returns full state; sync our model
|
|
1348
|
+
var et = (r && r.chat && r.chat.enabledTools) || {};
|
|
1349
|
+
TOOLS.forEach(function(t){
|
|
1350
|
+
// A tool absent from server's enabledTools means it's family-gated
|
|
1351
|
+
// off (e.g. custom_llm when no custom providers exist). Treat as
|
|
1352
|
+
// hidden — don't render the row, don't count in N / N.
|
|
1353
|
+
t.hidden = !(t.n in et);
|
|
1354
|
+
if (et[t.n] !== undefined) t.on = !!et[t.n];
|
|
1355
|
+
});
|
|
1356
|
+
renderTable();
|
|
1357
|
+
// Per-tool toggle on Permissions affects the "N / N tools" readout
|
|
1358
|
+
// in the API & CLIs row — keep them in sync without a full reload.
|
|
1359
|
+
if (typeof renderCliRows === 'function' && CLI_STATE) renderCliRows();
|
|
1360
|
+
setStatus('permStatus', 'Saved ' + name + ' = ' + (on?'on':'off') + '.', 'ok');
|
|
1361
|
+
} catch (e) {
|
|
1362
|
+
setStatus('permStatus', 'Failed to save: ' + e.message, 'err');
|
|
1363
|
+
// revert UI by reloading
|
|
1364
|
+
loadPermissionState();
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
async function saveBulkPatch(patchMap) {
|
|
1368
|
+
markLocalMutate();
|
|
1369
|
+
setStatus('permStatus', 'Saving ' + Object.keys(patchMap).length + ' tools…', 'info');
|
|
1370
|
+
try {
|
|
1371
|
+
var r = await postJson('/api/settings/state', { chat: { enabledTools: patchMap } });
|
|
1372
|
+
var et = (r && r.chat && r.chat.enabledTools) || {};
|
|
1373
|
+
TOOLS.forEach(function(t){
|
|
1374
|
+
// A tool absent from server's enabledTools means it's family-gated
|
|
1375
|
+
// off (e.g. custom_llm when no custom providers exist). Treat as
|
|
1376
|
+
// hidden — don't render the row, don't count in N / N.
|
|
1377
|
+
t.hidden = !(t.n in et);
|
|
1378
|
+
if (et[t.n] !== undefined) t.on = !!et[t.n];
|
|
1379
|
+
});
|
|
1380
|
+
renderTable();
|
|
1381
|
+
// Per-tool toggle on Permissions affects the "N / N tools" readout
|
|
1382
|
+
// in the API & CLIs row — keep them in sync without a full reload.
|
|
1383
|
+
if (typeof renderCliRows === 'function' && CLI_STATE) renderCliRows();
|
|
1384
|
+
setStatus('permStatus', 'Saved.', 'ok');
|
|
1385
|
+
} catch (e) {
|
|
1386
|
+
setStatus('permStatus', 'Failed to save: ' + e.message, 'err');
|
|
1387
|
+
loadPermissionState();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
document.addEventListener('change', function(e){
|
|
1391
|
+
if (e.target.matches && e.target.matches('input[data-tool]')) {
|
|
1392
|
+
var name = e.target.dataset.tool;
|
|
1393
|
+
var t = TOOLS.find(function(x){ return x.n === name; });
|
|
1394
|
+
if (t) { t.on = e.target.checked; renderTable(); saveToolPatch(name, t.on); }
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
document.addEventListener('change', function(e){
|
|
1398
|
+
if (e.target.matches && e.target.matches('input[data-safety]')) {
|
|
1399
|
+
var key = e.target.dataset.safety;
|
|
1400
|
+
var on = !!e.target.checked;
|
|
1401
|
+
SAFETY[key] = on;
|
|
1402
|
+
var row = e.target.closest('.safety-cell');
|
|
1403
|
+
if (row) row.classList.toggle('on', on);
|
|
1404
|
+
paintSafety(SAFETY);
|
|
1405
|
+
renderTable();
|
|
1406
|
+
saveSafety();
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
document.addEventListener('click', function(e){
|
|
1410
|
+
var b = e.target.closest && e.target.closest('button[data-safety-toggle]');
|
|
1411
|
+
if (!b) return;
|
|
1412
|
+
var key = b.dataset.safetyToggle;
|
|
1413
|
+
if (!(key in SAFETY)) return;
|
|
1414
|
+
SAFETY[key] = !SAFETY[key];
|
|
1415
|
+
paintSafety(SAFETY);
|
|
1416
|
+
renderTable();
|
|
1417
|
+
saveSafety();
|
|
1418
|
+
});
|
|
1419
|
+
document.addEventListener('click', function(e){
|
|
1420
|
+
var b = e.target.closest && e.target.closest('button[data-grp]');
|
|
1421
|
+
if (!b) return;
|
|
1422
|
+
var act = b.dataset.act;
|
|
1423
|
+
var patch = {};
|
|
1424
|
+
TOOLS.filter(function(t){ return t.g === b.dataset.grp; }).forEach(function(t){
|
|
1425
|
+
if (act === 'enable') t.on = true;
|
|
1426
|
+
else if (act === 'disable') t.on = false;
|
|
1427
|
+
else if (act === 'default') t.on = t.d;
|
|
1428
|
+
patch[t.n] = t.on;
|
|
1429
|
+
});
|
|
1430
|
+
renderTable();
|
|
1431
|
+
saveBulkPatch(patch);
|
|
1432
|
+
});
|
|
1433
|
+
document.addEventListener('click', function(e){
|
|
1434
|
+
var b = e.target.closest && e.target.closest('button[data-bulk]');
|
|
1435
|
+
if (!b) return;
|
|
1436
|
+
var act = b.dataset.bulk;
|
|
1437
|
+
var patch = {};
|
|
1438
|
+
TOOLS.forEach(function(t){
|
|
1439
|
+
if (act === 'enable-all') t.on = true;
|
|
1440
|
+
else if (act === 'disable-all') t.on = false;
|
|
1441
|
+
else if (act === 'default') t.on = t.d;
|
|
1442
|
+
patch[t.n] = t.on;
|
|
1443
|
+
});
|
|
1444
|
+
renderTable();
|
|
1445
|
+
saveBulkPatch(patch);
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
// ────── Global Safety persistence ──────
|
|
1449
|
+
function paintSafety(s) {
|
|
1450
|
+
if (!s) return;
|
|
1451
|
+
SAFETY = { read: !!s.read, write: !!s.write, execute: !!s.execute };
|
|
1452
|
+
document.querySelectorAll('input[data-safety]').forEach(function(input){
|
|
1453
|
+
var k = input.dataset.safety;
|
|
1454
|
+
input.checked = !!SAFETY[k];
|
|
1455
|
+
var row = input.closest('.safety-cell');
|
|
1456
|
+
if (row) row.classList.toggle('on', !!SAFETY[k]);
|
|
1457
|
+
});
|
|
1458
|
+
var v = (SAFETY.read?'R':'·') + ' / ' + (SAFETY.write?'W':'·') + ' / ' + (SAFETY.execute?'E':'·');
|
|
1459
|
+
var detail = 'read ' + (SAFETY.read?'on':'off') + ' · write ' + (SAFETY.write?'on':'off') + ' · exec ' + (SAFETY.execute?'on':'off');
|
|
1460
|
+
var topSafety = $('teleSafetyVal');
|
|
1461
|
+
if (topSafety) {
|
|
1462
|
+
topSafety.innerHTML = '<span class="safety-shortcuts">' +
|
|
1463
|
+
'<button class="safety-shortcut ' + (SAFETY.read ? 'on' : 'off') + '" type="button" data-safety-toggle="read" aria-pressed="' + (SAFETY.read ? 'true' : 'false') + '" title="Toggle read permission">R</button>' +
|
|
1464
|
+
'<span class="safety-divider">/</span>' +
|
|
1465
|
+
'<button class="safety-shortcut ' + (SAFETY.write ? 'on' : 'off') + '" type="button" data-safety-toggle="write" aria-pressed="' + (SAFETY.write ? 'true' : 'false') + '" title="Toggle write permission">W</button>' +
|
|
1466
|
+
'<span class="safety-divider">/</span>' +
|
|
1467
|
+
'<button class="safety-shortcut ' + (SAFETY.execute ? 'on' : 'off') + '" type="button" data-safety-toggle="execute" aria-pressed="' + (SAFETY.execute ? 'true' : 'false') + '" title="Toggle execute permission">E</button>' +
|
|
1468
|
+
'</span>';
|
|
1469
|
+
}
|
|
1470
|
+
setText('teleSafetySub', detail + ' · click R/W/E to toggle');
|
|
1471
|
+
setText('dtSafetyStatus', v);
|
|
1472
|
+
setText('dtSafetyDetail', detail);
|
|
1473
|
+
setMetricTone('dtSafetyStatus', (!SAFETY.read && !SAFETY.write && !SAFETY.execute) ? 'bad' : ((SAFETY.write || SAFETY.execute) ? 'warn' : 'good'));
|
|
1474
|
+
var led = $('teleSafetyLed');
|
|
1475
|
+
if (led) {
|
|
1476
|
+
led.classList.remove('warn','bad','idle');
|
|
1477
|
+
if (!SAFETY.read && !SAFETY.write && !SAFETY.execute) led.classList.add('bad');
|
|
1478
|
+
else if (SAFETY.read && !SAFETY.write && !SAFETY.execute) {/* default */}
|
|
1479
|
+
else led.classList.remove('warn','bad','idle');
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async function saveSafety() {
|
|
1483
|
+
markLocalMutate();
|
|
1484
|
+
setStatus('safetyStatus', 'Saving safety override…', 'info');
|
|
1485
|
+
try {
|
|
1486
|
+
var r = await postJson('/api/settings/state', { bridge: { globalSafety: SAFETY } });
|
|
1487
|
+
paintSafety(r && r.bridge && r.bridge.globalSafety);
|
|
1488
|
+
renderTable();
|
|
1489
|
+
setStatus('safetyStatus', 'Saved.', 'ok');
|
|
1490
|
+
} catch (e) {
|
|
1491
|
+
setStatus('safetyStatus', 'Failed: ' + e.message, 'err');
|
|
1492
|
+
loadPermissionState();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// ────── Initial settings load ──────
|
|
1497
|
+
async function loadPermissionState() {
|
|
1498
|
+
try {
|
|
1499
|
+
var s = await getJson('/api/settings/state');
|
|
1500
|
+
var et = (s.chat && s.chat.enabledTools) || {};
|
|
1501
|
+
TOOLS.forEach(function(t){
|
|
1502
|
+
// A tool absent from server's enabledTools means it's family-gated
|
|
1503
|
+
// off (e.g. custom_llm when no custom providers exist). Treat as
|
|
1504
|
+
// hidden — don't render the row, don't count in N / N.
|
|
1505
|
+
t.hidden = !(t.n in et);
|
|
1506
|
+
if (et[t.n] !== undefined) t.on = !!et[t.n];
|
|
1507
|
+
});
|
|
1508
|
+
paintSafety((s.bridge && s.bridge.globalSafety) || { read:true, write:false, execute:false });
|
|
1509
|
+
renderTable();
|
|
1510
|
+
// auto-update toggle
|
|
1511
|
+
if ($('autoUpdateToggle')) $('autoUpdateToggle').checked = !!(s.bridge && s.bridge.autoUpdate !== false);
|
|
1512
|
+
} catch (e) {
|
|
1513
|
+
setStatus('permStatus', 'Could not load settings: ' + e.message, 'err');
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Track most recent local mutation so the background refresh doesn't clobber
|
|
1518
|
+
// an in-flight save (POST → 200 → UI updates → background poll fires before
|
|
1519
|
+
// user sees the result would briefly revert state).
|
|
1520
|
+
var lastLocalMutate = 0;
|
|
1521
|
+
function markLocalMutate() { lastLocalMutate = Date.now(); }
|
|
1522
|
+
async function refreshSettingsIfQuiet() {
|
|
1523
|
+
if (Date.now() - lastLocalMutate < 2500) return; // user is actively editing
|
|
1524
|
+
try { await loadPermissionState(); } catch (_) {}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// ────── Live status / telemetry ──────
|
|
1528
|
+
function formatUptime(ms) {
|
|
1529
|
+
if (!ms || ms < 0) return '—';
|
|
1530
|
+
var s = Math.floor(ms / 1000);
|
|
1531
|
+
var h = Math.floor(s / 3600); s -= h*3600;
|
|
1532
|
+
var m = Math.floor(s / 60); s -= m*60;
|
|
1533
|
+
function pad(n){ return n < 10 ? '0' + n : '' + n; }
|
|
1534
|
+
return pad(h) + ':' + pad(m) + ':' + pad(s);
|
|
1535
|
+
}
|
|
1536
|
+
function timeAgo(ts) {
|
|
1537
|
+
if (!ts) return '—';
|
|
1538
|
+
var d = (Date.now() - Date.parse(ts)) / 1000;
|
|
1539
|
+
if (d < 60) return Math.floor(d) + 's ago';
|
|
1540
|
+
if (d < 3600) return Math.floor(d/60) + 'm ago';
|
|
1541
|
+
if (d < 86400) return Math.floor(d/3600) + 'h ago';
|
|
1542
|
+
return Math.floor(d/86400) + 'd ago';
|
|
1543
|
+
}
|
|
1544
|
+
async function refreshStatus() {
|
|
1545
|
+
try {
|
|
1546
|
+
var s = await getJson('/api/status');
|
|
1547
|
+
var rs = await getJson('/api/relay-status');
|
|
1548
|
+
var uptimeMs = rs.uptimeMs || 0;
|
|
1549
|
+
// Daemon telemetry. The daemon answered with a valid status payload, so it
|
|
1550
|
+
// is alive BY DEFINITION here. Daemon/MCP liveness must NOT be gated on
|
|
1551
|
+
// s.running (= cdpConnected, the Chrome/CDP browser link) — closing Chrome
|
|
1552
|
+
// leaves the daemon perfectly healthy. The browser/CDP state is surfaced
|
|
1553
|
+
// separately by the Desktop-tools indicator (dtBridge*) below. (getJson
|
|
1554
|
+
// doesn't check res.ok, so validate the payload shape, not just "no throw".)
|
|
1555
|
+
var daemonAlive = !!(s && s.engine === 'empir3-bridge');
|
|
1556
|
+
setText('teleDaemonVal', daemonAlive ? 'RUNNING' : 'OFFLINE');
|
|
1557
|
+
setText('teleDaemonSub', (s.pid ? 'PID ' + s.pid + ' · ' : '') + 'uptime ' + formatUptime(uptimeMs));
|
|
1558
|
+
var dled = $('teleDaemonLed'); if (dled){ dled.classList.remove('warn','bad','idle'); if (!daemonAlive) dled.classList.add('bad'); }
|
|
1559
|
+
// Desktop tools harness telemetry
|
|
1560
|
+
setText('dtBridgeStatus', s.running ? 'Connected' : 'Disconnected');
|
|
1561
|
+
setMetricTone('dtBridgeStatus', s.running ? 'good' : 'bad');
|
|
1562
|
+
var dtTag = $('dtBridgeTag');
|
|
1563
|
+
if (dtTag) { dtTag.textContent = s.running ? 'CONNECTED' : 'OFFLINE'; dtTag.className = 'tag ' + (s.running ? 'good' : 'bad'); }
|
|
1564
|
+
setText('dtCurrentUrl', s.currentUrl || '—');
|
|
1565
|
+
setText('dtMessageCount', s.messageCount || 0);
|
|
1566
|
+
setMetricTone('dtMessageCount', (s.messageCount || 0) > 0 ? 'warn' : '');
|
|
1567
|
+
setText('dtOverlayStatus', s.overlayInjected ? 'Active' : 'Not injected');
|
|
1568
|
+
setMetricTone('dtOverlayStatus', s.overlayInjected ? 'good' : 'warn');
|
|
1569
|
+
// Overview daemon tile
|
|
1570
|
+
setText('ovDaemonUptime', formatUptime(uptimeMs));
|
|
1571
|
+
setText('ovDaemonPid', s.pid ? 'PID ' + s.pid : 'PID —');
|
|
1572
|
+
// Daemon pane
|
|
1573
|
+
setText('daemonPid', s.pid || '—');
|
|
1574
|
+
setText('daemonUptime', formatUptime(uptimeMs));
|
|
1575
|
+
setText('daemonBridgeUrl', s.bridgeUrl || ('http://localhost:${PORT}'));
|
|
1576
|
+
var healthTag = $('daemonHealthTag');
|
|
1577
|
+
if (healthTag) {
|
|
1578
|
+
healthTag.classList.remove('good','warn','bad');
|
|
1579
|
+
if (daemonAlive) { healthTag.classList.add('good'); healthTag.textContent = 'HEALTHY'; }
|
|
1580
|
+
else { healthTag.classList.add('bad'); healthTag.textContent = 'OFFLINE'; }
|
|
1581
|
+
}
|
|
1582
|
+
// Rail foot
|
|
1583
|
+
setText('footDaemonUptime', formatUptime(uptimeMs));
|
|
1584
|
+
var fdLed = $('footDaemonLed'); if (fdLed){ fdLed.classList.remove('warn','bad'); if (!daemonAlive) fdLed.classList.add('bad'); }
|
|
1585
|
+
var fmLed = $('footMcpLed'); if (fmLed){ fmLed.classList.remove('warn','bad'); if (!daemonAlive) fmLed.classList.add('bad'); }
|
|
1586
|
+
// MCP cell — the stdio MCP server IS the daemon, so MCP is READY whenever
|
|
1587
|
+
// the daemon is reachable (NOT gated on the browser/CDP link). Calls are
|
|
1588
|
+
// counted from /api/actions.
|
|
1589
|
+
setText('teleMcpVal', daemonAlive ? 'READY' : 'OFFLINE');
|
|
1590
|
+
var mled = $('teleMcpLed'); if (mled){ mled.classList.remove('warn','bad'); if (!daemonAlive) mled.classList.add('bad'); }
|
|
1591
|
+
// Relay
|
|
1592
|
+
var rConn = !!(rs.relay && rs.relay.connected);
|
|
1593
|
+
var rPaired = !!rs.hasAuth;
|
|
1594
|
+
var rRejected = !!(rs.relay && rs.relay.authRejected);
|
|
1595
|
+
var rNeedAuth = rPaired && rRejected;
|
|
1596
|
+
var who = (rs.authUser && rs.authUser.email) || '';
|
|
1597
|
+
var relayLabel = rPaired ? (rNeedAuth ? 'SIGN IN NEEDED' : (rConn ? 'CONNECTED' : 'PAIRED')) : 'UNPAIRED';
|
|
1598
|
+
setText('teleRelayVal', relayLabel);
|
|
1599
|
+
setText('teleRelaySub', rPaired ? (rNeedAuth ? ((who || 'stored bridge account') + ' - sign in again') : (who + (rConn ? '' : ' · awaiting'))) : 'no empir3 account on this PC');
|
|
1600
|
+
var rled = $('teleRelayLed'); if (rled){ rled.classList.remove('warn','bad'); if (rNeedAuth) rled.classList.add('bad'); else if (!rPaired) rled.classList.add('warn'); }
|
|
1601
|
+
var frled = $('footRelayLed'); if (frled){ frled.classList.remove('warn','bad'); if (rNeedAuth) frled.classList.add('bad'); else if (!rPaired) frled.classList.add('warn'); }
|
|
1602
|
+
setText('footRelayState', rNeedAuth ? 'sign in needed' : (rPaired ? (who.split('@')[0] || 'paired') : 'unpaired'));
|
|
1603
|
+
var ovRelayStatus = $('ovRelayStatus');
|
|
1604
|
+
if (ovRelayStatus) {
|
|
1605
|
+
ovRelayStatus.textContent = relayLabel;
|
|
1606
|
+
ovRelayStatus.classList.remove('good','warn','bad');
|
|
1607
|
+
ovRelayStatus.classList.add(rNeedAuth ? 'bad' : (rPaired ? 'good' : 'warn'));
|
|
1608
|
+
}
|
|
1609
|
+
// Account chip + pane
|
|
1610
|
+
if (rPaired && who) {
|
|
1611
|
+
setText('acctEmail', who);
|
|
1612
|
+
var initials = (who.split('@')[0] || '').slice(0,2).toUpperCase();
|
|
1613
|
+
setText('acctAvatar', initials || '··');
|
|
1614
|
+
setText('acctPaneEmail', who);
|
|
1615
|
+
setText('acctPaneServer', (rs.serverUrl || '').replace(/^https?:\\/\\//,''));
|
|
1616
|
+
setText('acctPaneMode', rs.mode + (rNeedAuth ? ' · sign in needed' : (rConn ? ' · connected' : ' · awaiting relay')));
|
|
1617
|
+
setText('acctPaneDevice', (rs.relay && rs.relay.deviceName) || '—');
|
|
1618
|
+
var acctTag = $('acctConnTag');
|
|
1619
|
+
if (acctTag) {
|
|
1620
|
+
acctTag.className = 'tag ' + (rNeedAuth ? 'bad' : (rConn ? 'good' : 'warn'));
|
|
1621
|
+
acctTag.textContent = rNeedAuth ? 'SIGN IN NEEDED' : (rConn ? 'CONNECTED' : 'PAIRED');
|
|
1622
|
+
}
|
|
1623
|
+
if (rNeedAuth) setStatus('acctStatus', 'Empir3 rejected the stored bridge token. Sign in again to pair this PC.', 'err');
|
|
1624
|
+
else setStatus('acctStatus', '', 'info');
|
|
1625
|
+
setText('ovRelayUser', who);
|
|
1626
|
+
setText('ovRelayServer', (rs.serverUrl || '').replace(/^https?:\\/\\//,''));
|
|
1627
|
+
document.body.dataset.view = 'signedin';
|
|
1628
|
+
} else {
|
|
1629
|
+
document.body.dataset.view = 'signedout';
|
|
1630
|
+
}
|
|
1631
|
+
} catch (e) {
|
|
1632
|
+
// Daemon unreachable — reset the ENTIRE daemon family to OFFLINE, not just
|
|
1633
|
+
// the daemon pill. Leaving MCP / health / foot LEDs / uptime at their last
|
|
1634
|
+
// good value would paint a half-green console over a dead daemon.
|
|
1635
|
+
setText('teleDaemonVal','OFFLINE');
|
|
1636
|
+
var dled2 = $('teleDaemonLed'); if (dled2){ dled2.classList.remove('warn','idle'); dled2.classList.add('bad'); }
|
|
1637
|
+
setText('teleMcpVal','OFFLINE');
|
|
1638
|
+
var mled2 = $('teleMcpLed'); if (mled2){ mled2.classList.remove('warn'); mled2.classList.add('bad'); }
|
|
1639
|
+
var healthTag2 = $('daemonHealthTag');
|
|
1640
|
+
if (healthTag2) { healthTag2.classList.remove('good','warn'); healthTag2.classList.add('bad'); healthTag2.textContent = 'OFFLINE'; }
|
|
1641
|
+
var fdLed2 = $('footDaemonLed'); if (fdLed2){ fdLed2.classList.remove('warn'); fdLed2.classList.add('bad'); }
|
|
1642
|
+
var fmLed2 = $('footMcpLed'); if (fmLed2){ fmLed2.classList.remove('warn'); fmLed2.classList.add('bad'); }
|
|
1643
|
+
setText('footDaemonUptime','—');
|
|
1644
|
+
setText('daemonUptime','—');
|
|
1645
|
+
setText('ovDaemonUptime','—');
|
|
1646
|
+
setText('dtBridgeStatus','Disconnected');
|
|
1647
|
+
setMetricTone('dtBridgeStatus','bad');
|
|
1648
|
+
var dtTag2 = $('dtBridgeTag'); if (dtTag2) { dtTag2.textContent = 'OFFLINE'; dtTag2.className = 'tag bad'; }
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
async function refreshActions() {
|
|
1652
|
+
try {
|
|
1653
|
+
var rows = await getJson('/api/actions');
|
|
1654
|
+
if (!Array.isArray(rows)) return;
|
|
1655
|
+
var calls = rows.length, errs = rows.filter(function(r){ return r.ok === false; }).length;
|
|
1656
|
+
setText('ovMcpCalls', calls + ' '); // suffix kept by overview HTML structure
|
|
1657
|
+
var ovCalls = $('ovMcpCalls');
|
|
1658
|
+
if (ovCalls) ovCalls.innerHTML = calls + ' <span style="color:var(--soft); font-size:14px;">/ ' + errs + ' err</span>';
|
|
1659
|
+
setText('teleMcpSub', 'stdio · ' + calls + ' calls / ' + errs + ' errors');
|
|
1660
|
+
setText('footMcpCount', calls + ' calls');
|
|
1661
|
+
// Overview recent activity (top 5)
|
|
1662
|
+
var ovRows = $('ovLogRows');
|
|
1663
|
+
if (ovRows) {
|
|
1664
|
+
if (!rows.length) ovRows.innerHTML = '<tr><td class="dt" style="text-align:center;">No activity yet.</td></tr>';
|
|
1665
|
+
else ovRows.innerHTML = rows.slice(-5).reverse().map(rowHtml).join('');
|
|
1666
|
+
var last = rows[rows.length-1];
|
|
1667
|
+
if (last) setText('ovMcpLast', 'Last call ' + last.type + ' · ' + timeAgo(last.timestamp));
|
|
1668
|
+
}
|
|
1669
|
+
// Full log pane
|
|
1670
|
+
var logsRows = $('logsRows');
|
|
1671
|
+
if (logsRows) {
|
|
1672
|
+
if (!rows.length) logsRows.innerHTML = '<tr><td class="dt" style="text-align:center;">No activity yet.</td></tr>';
|
|
1673
|
+
else logsRows.innerHTML = rows.slice().reverse().map(rowHtml).join('');
|
|
1674
|
+
}
|
|
1675
|
+
} catch (e) {}
|
|
1676
|
+
}
|
|
1677
|
+
function rowHtml(r) {
|
|
1678
|
+
var t = (r.timestamp || '').slice(11,19);
|
|
1679
|
+
var ok = r.ok !== false;
|
|
1680
|
+
var status = ok ? '200' : 'err';
|
|
1681
|
+
var stClass = ok ? 'st' : 'st err';
|
|
1682
|
+
var ms = (r.elapsedMs || 0) + ' ms';
|
|
1683
|
+
var detail = r.error ? r.error : summarizeReceiptInput(r);
|
|
1684
|
+
return '<tr><td class="ts mono">' + escapeHtml(t) + '</td><td class="nm">' + escapeHtml(r.type || '') + '</td><td class="' + stClass + '">' + status + '</td><td class="ms mono">' + escapeHtml(ms) + '</td><td class="dt">' + escapeHtml(detail) + '</td></tr>';
|
|
1685
|
+
}
|
|
1686
|
+
function summarizeReceiptInput(r) {
|
|
1687
|
+
var i = r.input || {};
|
|
1688
|
+
// Pick the field most useful per tool family. summarizeCommand() in
|
|
1689
|
+
// server.ts copies through these keys verbatim, so they're stable.
|
|
1690
|
+
if (i.url) return String(i.url);
|
|
1691
|
+
if (i.ref) return 'ref:' + i.ref;
|
|
1692
|
+
if (i.selector) return i.selector;
|
|
1693
|
+
if (typeof i.x === 'number' && typeof i.y === 'number') return '(' + i.x + ',' + i.y + ')';
|
|
1694
|
+
if (i.monitor) return 'monitor:' + i.monitor;
|
|
1695
|
+
if (i.recording) return 'rec:' + i.recording;
|
|
1696
|
+
if (i.filter) return 'filter:' + i.filter;
|
|
1697
|
+
if (typeof i.textLength === 'number') return 'text·' + i.textLength + 'ch';
|
|
1698
|
+
if (typeof i.messageLength === 'number') return 'msg·' + i.messageLength + 'ch';
|
|
1699
|
+
if (i.format) return 'fmt:' + i.format;
|
|
1700
|
+
return r.source || '';
|
|
1701
|
+
}
|
|
1702
|
+
async function refreshFocus() {
|
|
1703
|
+
try {
|
|
1704
|
+
var j = await getJson('/api/desktop/focus');
|
|
1705
|
+
var has = !!j.active;
|
|
1706
|
+
var tag = $('focusTag'); if (tag) { tag.textContent = has ? 'LOCKED' : 'NONE LOCKED'; tag.className = 'tag ' + (has ? 'good' : ''); }
|
|
1707
|
+
setText('focusStatusText', has ? 'Region locked — agent scoped to it.' : 'No region — agent sees full virtual screen');
|
|
1708
|
+
setText('focusTtl', has && j.ttlMs ? Math.round(j.ttlMs/1000) + 's' : '—');
|
|
1709
|
+
setText('focusBounds', has && j.region ? (j.region.width + '×' + j.region.height + ' @ (' + j.region.x + ',' + j.region.y + ')') : '—');
|
|
1710
|
+
var rel = $('agtReleaseFocus'); if (rel) rel.disabled = false;
|
|
1711
|
+
var grid = $('agtFocusGrid'); if (grid) grid.disabled = !has;
|
|
1712
|
+
// Sync the focus-grid button label with the daemon's authoritative
|
|
1713
|
+
// state. Without this the boot label always reads "Show focus grid"
|
|
1714
|
+
// even when another channel (CLI, second tab) already toggled it on.
|
|
1715
|
+
var serverGrid = !!(j.grid && j.grid.enabled);
|
|
1716
|
+
if (serverGrid !== focusGridOn) {
|
|
1717
|
+
focusGridOn = serverGrid;
|
|
1718
|
+
if (grid) grid.textContent = focusGridOn ? 'Hide focus grid' : 'Show focus grid';
|
|
1719
|
+
}
|
|
1720
|
+
var aled = $('teleAgentLed'); if (aled){ aled.classList.remove('warn','bad','idle'); if (has) aled.classList.remove('idle'); else aled.classList.add('idle'); }
|
|
1721
|
+
setText('teleAgentVal', has ? 'FOCUSED' : 'IDLE');
|
|
1722
|
+
setText('teleAgentSub', has ? (j.region.width + '×' + j.region.height + ' locked') : 'no focus region');
|
|
1723
|
+
} catch (e) {}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// ────── MCP config ──────
|
|
1727
|
+
var mcpText = '';
|
|
1728
|
+
$('mcpShowConfig').addEventListener('click', async function(){
|
|
1729
|
+
setStatus('mcpStatus', 'Generating MCP config…', 'info');
|
|
1730
|
+
try {
|
|
1731
|
+
var j = await postJson('/api/install/claude-code', {});
|
|
1732
|
+
mcpText = JSON.stringify(j.snippet, null, 2);
|
|
1733
|
+
$('mcpSnippet').textContent = mcpText;
|
|
1734
|
+
if (Array.isArray(j.instructions)) {
|
|
1735
|
+
$('mcpSteps').innerHTML = j.instructions.map(function(s){ return '<li>' + escapeHtml(s) + '</li>'; }).join('');
|
|
1736
|
+
}
|
|
1737
|
+
setStatus('mcpStatus', 'MCP config ready.', 'ok');
|
|
1738
|
+
} catch (e) { setStatus('mcpStatus', 'Could not generate config: ' + e.message, 'err'); }
|
|
1739
|
+
});
|
|
1740
|
+
$('mcpCopyConfig').addEventListener('click', async function(){
|
|
1741
|
+
if (!mcpText) mcpText = $('mcpSnippet').textContent;
|
|
1742
|
+
try { await navigator.clipboard.writeText(mcpText); setStatus('mcpStatus','Copied.','ok'); }
|
|
1743
|
+
catch (e) {
|
|
1744
|
+
// navigator.clipboard.writeText rejects in headless contexts and any
|
|
1745
|
+
// browser tab without focus. Pre-select the snippet so ⌘C / Ctrl+C
|
|
1746
|
+
// grabs it without the user having to click into the <pre> manually.
|
|
1747
|
+
try {
|
|
1748
|
+
var pre = $('mcpSnippet');
|
|
1749
|
+
var range = document.createRange();
|
|
1750
|
+
range.selectNodeContents(pre);
|
|
1751
|
+
var sel = window.getSelection();
|
|
1752
|
+
sel.removeAllRanges();
|
|
1753
|
+
sel.addRange(range);
|
|
1754
|
+
setStatus('mcpStatus','Snippet selected — press \u2318C / Ctrl+C to copy.','info');
|
|
1755
|
+
} catch (_e) {
|
|
1756
|
+
setStatus('mcpStatus','Select the snippet and copy manually.','err');
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
// ────── Daemon pane ──────
|
|
1762
|
+
async function onOpenBridge() {
|
|
1763
|
+
try { await postJson('/api/command', { action:'desktop:browse:show', params:{} }); setStatus('daemonStatus','Bridge window requested.','ok'); }
|
|
1764
|
+
catch (e) { setStatus('daemonStatus','Could not open: ' + e.message,'err'); }
|
|
1765
|
+
}
|
|
1766
|
+
$('daemonOpenBridge').addEventListener('click', onOpenBridge);
|
|
1767
|
+
$('ovOpenBridge').addEventListener('click', onOpenBridge);
|
|
1768
|
+
async function onReconnect() {
|
|
1769
|
+
setStatus('daemonStatus','Reconnecting daemon… tray will respawn it.','info');
|
|
1770
|
+
try { await fetch(API + '/api/shutdown', { method:'POST' }); setTimeout(refreshStatus, 2500); }
|
|
1771
|
+
catch (e) { setStatus('daemonStatus','Reconnect failed: ' + e.message,'err'); }
|
|
1772
|
+
}
|
|
1773
|
+
$('daemonReconnect').addEventListener('click', onReconnect);
|
|
1774
|
+
$('ovReconnect').addEventListener('click', onReconnect);
|
|
1775
|
+
$('daemonToggleLog').addEventListener('click', async function(){
|
|
1776
|
+
var pane = $('daemonLog');
|
|
1777
|
+
if (pane.style.display === 'none') {
|
|
1778
|
+
try {
|
|
1779
|
+
var j = await getJson('/api/log/tail?lines=200');
|
|
1780
|
+
var lines = (j.lines || []).filter(function(l){ return l && l.trim(); });
|
|
1781
|
+
pane.textContent = lines.length ? lines.join('\\n') : '(no log lines yet — ' + (j.path || 'bridge.log') + ')';
|
|
1782
|
+
pane.style.display = 'block';
|
|
1783
|
+
$('daemonToggleLog').textContent = 'Hide log';
|
|
1784
|
+
} catch (e) {
|
|
1785
|
+
pane.textContent = 'Could not load log: ' + e.message;
|
|
1786
|
+
pane.style.display = 'block';
|
|
1787
|
+
$('daemonToggleLog').textContent = 'Hide log';
|
|
1788
|
+
}
|
|
1789
|
+
} else {
|
|
1790
|
+
pane.style.display = 'none';
|
|
1791
|
+
$('daemonToggleLog').textContent = 'Show log';
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// ────── Identity (Device name + Home directory) ──────
|
|
1796
|
+
// Mirrors the old /settings page fields that didn't make it into the
|
|
1797
|
+
// welcome console rewrite. Both live in bridge-settings.json and feed
|
|
1798
|
+
// empir3-agent labelling + the project-sync scope.
|
|
1799
|
+
function hydrateIdentity() {
|
|
1800
|
+
if (!CLI_STATE || !CLI_STATE.bridge) return;
|
|
1801
|
+
var dn = $('deviceNameInput'); if (dn && !dn.matches(':focus')) dn.value = CLI_STATE.bridge.deviceName || '';
|
|
1802
|
+
var hd = $('homeDirInput'); if (hd && !hd.matches(':focus')) hd.value = CLI_STATE.bridge.homeDirectory || '';
|
|
1803
|
+
setText('identityTag', CLI_STATE.bridge.deviceName || 'unnamed');
|
|
1804
|
+
}
|
|
1805
|
+
var identitySave = $('identitySave');
|
|
1806
|
+
if (identitySave) {
|
|
1807
|
+
identitySave.addEventListener('click', async function(){
|
|
1808
|
+
var dn = ($('deviceNameInput').value || '').trim();
|
|
1809
|
+
var hd = ($('homeDirInput').value || '').trim();
|
|
1810
|
+
if (!dn && !hd) { setStatus('identityStatus','Nothing to save.','info'); return; }
|
|
1811
|
+
var patch = {};
|
|
1812
|
+
if (dn) patch.deviceName = dn;
|
|
1813
|
+
if (hd) patch.homeDirectory = hd;
|
|
1814
|
+
setStatus('identityStatus','Saving identity…','info');
|
|
1815
|
+
markLocalMutate();
|
|
1816
|
+
try {
|
|
1817
|
+
await postJson('/api/settings/state', { bridge: patch });
|
|
1818
|
+
setStatus('identityStatus','Saved.','ok');
|
|
1819
|
+
await loadCliState();
|
|
1820
|
+
} catch (e) {
|
|
1821
|
+
setStatus('identityStatus','Failed: ' + e.message,'err');
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
var identityReset = $('identityReset');
|
|
1826
|
+
if (identityReset) {
|
|
1827
|
+
identityReset.addEventListener('click', function(){
|
|
1828
|
+
if (CLI_STATE && CLI_STATE.bridge) {
|
|
1829
|
+
$('deviceNameInput').value = CLI_STATE.bridge.deviceName || '';
|
|
1830
|
+
$('homeDirInput').value = CLI_STATE.bridge.homeDirectory || '';
|
|
1831
|
+
setStatus('identityStatus','Reset to saved values.','info');
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// ────── Desktop tools pane ──────
|
|
1837
|
+
function showDesktopOutput(label, value) {
|
|
1838
|
+
var out = $('dtCommandOutput');
|
|
1839
|
+
if (!out) return;
|
|
1840
|
+
out.style.display = 'block';
|
|
1841
|
+
var text = typeof value === 'string' ? value : JSON.stringify(value, function(k, v) {
|
|
1842
|
+
if (typeof v === 'string' && v.length > 1200) return v.slice(0, 1200) + '…';
|
|
1843
|
+
return v;
|
|
1844
|
+
}, 2);
|
|
1845
|
+
out.textContent = label + '\\n' + text;
|
|
1846
|
+
}
|
|
1847
|
+
async function runDesktopCommand(label, cmd, options) {
|
|
1848
|
+
options = options || {};
|
|
1849
|
+
setStatus('desktopToolsStatus', label + '…', 'info');
|
|
1850
|
+
try {
|
|
1851
|
+
var j = await postJson('/api/command', cmd);
|
|
1852
|
+
var result = j && (j.result || j);
|
|
1853
|
+
if (options.show !== false) showDesktopOutput(label, result);
|
|
1854
|
+
setStatus('desktopToolsStatus', options.done || (label + ' complete.'), 'ok');
|
|
1855
|
+
refreshStatus();
|
|
1856
|
+
refreshActions();
|
|
1857
|
+
return result;
|
|
1858
|
+
} catch (e) {
|
|
1859
|
+
setStatus('desktopToolsStatus', label + ' failed: ' + e.message, 'err');
|
|
1860
|
+
throw e;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
var DESKTOP_MONITORS = [];
|
|
1864
|
+
function setSetupItem(key, done, detail) {
|
|
1865
|
+
var item = $('setupItem' + key);
|
|
1866
|
+
if (item) {
|
|
1867
|
+
item.classList.toggle('done', !!done);
|
|
1868
|
+
item.classList.toggle('warn', !done);
|
|
1869
|
+
}
|
|
1870
|
+
var text = $('setup' + key + 'Text');
|
|
1871
|
+
if (text) text.textContent = detail;
|
|
1872
|
+
}
|
|
1873
|
+
async function refreshSetupStatus() {
|
|
1874
|
+
try {
|
|
1875
|
+
var j = await postJson('/api/command', { type:'bridge_setup_status' });
|
|
1876
|
+
var s = j && (j.result || j);
|
|
1877
|
+
var cur = (s && s.current) || {};
|
|
1878
|
+
var saved = (s && s.saved) || {};
|
|
1879
|
+
setSetupItem('Overlay', !!cur.overlay, cur.overlay ? 'Chat bubble/overlay is available.' : 'Overlay is not connected yet.');
|
|
1880
|
+
setSetupItem('Monitors', !!cur.monitors, (s.monitors && s.monitors.count ? (s.monitors.count + ' monitor(s): ' + (s.monitors.ids || []).join(', ')) : 'No monitors detected yet.'));
|
|
1881
|
+
setSetupItem('Calibration', !!cur.calibration, cur.calibration ? 'Saved click calibration found.' : 'Run calibration before first agent use.');
|
|
1882
|
+
setSetupItem('Recordings', !!cur.recordings, 'Record/playback endpoints are available.');
|
|
1883
|
+
var tag = $('setupTag');
|
|
1884
|
+
if (tag) {
|
|
1885
|
+
tag.className = 'tag ' + (saved.completed ? 'good' : (s.completeNow ? 'warn' : 'bad'));
|
|
1886
|
+
tag.textContent = saved.completed ? 'SAVED' : (s.completeNow ? 'READY TO SAVE' : 'CHECK REQUIRED');
|
|
1887
|
+
}
|
|
1888
|
+
setStatus('setupStatus', saved.completed ? ('Saved ' + (saved.completedAt || '').slice(0, 16).replace('T',' ')) : 'Complete the checks, then save setup.', saved.completed ? 'ok' : 'info');
|
|
1889
|
+
return s;
|
|
1890
|
+
} catch (e) {
|
|
1891
|
+
setStatus('setupStatus', 'Setup status failed: ' + e.message, 'err');
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
async function refreshMonitors(showStatus) {
|
|
1896
|
+
try {
|
|
1897
|
+
if (showStatus) setStatus('calStatus', 'Detecting monitors...', 'info');
|
|
1898
|
+
var j = await postJson('/api/command', { type:'desktop_monitors' });
|
|
1899
|
+
var result = j && (j.result || j);
|
|
1900
|
+
DESKTOP_MONITORS = Array.isArray(result.monitors) ? result.monitors : [];
|
|
1901
|
+
var sel = $('agtCalMonitor');
|
|
1902
|
+
if (sel) {
|
|
1903
|
+
var current = sel.value || 'primary';
|
|
1904
|
+
sel.innerHTML = '<option value="primary">Primary monitor</option><option value="all">All monitors</option>';
|
|
1905
|
+
DESKTOP_MONITORS.forEach(function(m){
|
|
1906
|
+
var b = m.bounds || {};
|
|
1907
|
+
var opt = document.createElement('option');
|
|
1908
|
+
opt.value = m.id;
|
|
1909
|
+
opt.textContent = m.id + (m.primary ? ' (primary)' : '') + ' - ' + (b.width || '?') + 'x' + (b.height || '?') + ' @ ' + (b.x || 0) + ',' + (b.y || 0);
|
|
1910
|
+
sel.appendChild(opt);
|
|
1911
|
+
});
|
|
1912
|
+
if ([].slice.call(sel.options).some(function(o){ return o.value === current; })) sel.value = current;
|
|
1913
|
+
}
|
|
1914
|
+
setText('calMonitorSummary', DESKTOP_MONITORS.length ? DESKTOP_MONITORS.map(function(m){ return m.id + (m.primary ? '*' : ''); }).join(', ') : '-');
|
|
1915
|
+
if (showStatus) setStatus('calStatus', DESKTOP_MONITORS.length + ' monitor(s) detected.', 'ok');
|
|
1916
|
+
refreshSetupStatus();
|
|
1917
|
+
return DESKTOP_MONITORS;
|
|
1918
|
+
} catch (e) {
|
|
1919
|
+
if (showStatus) setStatus('calStatus', 'Monitor detection failed: ' + e.message, 'err');
|
|
1920
|
+
return [];
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
async function refreshRecordingStatus() {
|
|
1924
|
+
try {
|
|
1925
|
+
var s = await getJson('/api/recording-status');
|
|
1926
|
+
setText('recState', s.recording ? 'Recording' : 'Idle');
|
|
1927
|
+
setText('recActionCount', s.actionCount || 0);
|
|
1928
|
+
setText('recPlayState', s.playing ? 'Playing' : 'Ready');
|
|
1929
|
+
var tag = $('recordingTag');
|
|
1930
|
+
if (tag) { tag.textContent = s.recording ? 'RECORDING' : (s.playing ? 'PLAYING' : 'IDLE'); tag.className = 'tag ' + (s.recording ? 'bad' : (s.playing ? 'warn' : '')); }
|
|
1931
|
+
} catch (_) {}
|
|
1932
|
+
}
|
|
1933
|
+
async function refreshRecordings(showStatus) {
|
|
1934
|
+
try {
|
|
1935
|
+
var rows = await getJson('/api/recordings');
|
|
1936
|
+
var sel = $('recSelect');
|
|
1937
|
+
if (sel) {
|
|
1938
|
+
var current = sel.value;
|
|
1939
|
+
sel.innerHTML = '';
|
|
1940
|
+
if (!rows.length) {
|
|
1941
|
+
var empty = document.createElement('option'); empty.value = ''; empty.textContent = 'No recordings saved'; sel.appendChild(empty);
|
|
1942
|
+
} else {
|
|
1943
|
+
rows.forEach(function(r){
|
|
1944
|
+
var opt = document.createElement('option');
|
|
1945
|
+
opt.value = r.name;
|
|
1946
|
+
opt.textContent = r.name + ' - ' + (r.actionCount || 0) + ' action(s)';
|
|
1947
|
+
sel.appendChild(opt);
|
|
1948
|
+
});
|
|
1949
|
+
if ([].slice.call(sel.options).some(function(o){ return o.value === current; })) sel.value = current;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
setText('recSavedCount', rows.length || 0);
|
|
1953
|
+
if (showStatus) setStatus('recStatus', rows.length + ' recording(s) loaded.', 'ok');
|
|
1954
|
+
refreshSetupStatus();
|
|
1955
|
+
return rows;
|
|
1956
|
+
} catch (e) {
|
|
1957
|
+
if (showStatus) setStatus('recStatus', 'Could not load recordings: ' + e.message, 'err');
|
|
1958
|
+
return [];
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
async function loadSelectedRecording() {
|
|
1962
|
+
var sel = $('recSelect');
|
|
1963
|
+
var name = sel && sel.value;
|
|
1964
|
+
if (!name) { setStatus('recStatus', 'Select a recording first.', 'err'); return; }
|
|
1965
|
+
try {
|
|
1966
|
+
var rec = await getJson('/api/recordings/' + encodeURIComponent(name));
|
|
1967
|
+
var preview = {
|
|
1968
|
+
name: rec.name,
|
|
1969
|
+
startUrl: rec.startUrl,
|
|
1970
|
+
recorded: rec.recorded,
|
|
1971
|
+
actionCount: Array.isArray(rec.actions) ? rec.actions.length : 0,
|
|
1972
|
+
variables: rec.variables || [],
|
|
1973
|
+
actions: (rec.actions || []).slice(0, 12),
|
|
1974
|
+
};
|
|
1975
|
+
var out = $('recPreview');
|
|
1976
|
+
if (out) { out.style.display = 'block'; out.textContent = JSON.stringify(preview, null, 2); }
|
|
1977
|
+
setStatus('recStatus', 'Recording loaded.', 'ok');
|
|
1978
|
+
} catch (e) {
|
|
1979
|
+
setStatus('recStatus', 'Load failed: ' + e.message, 'err');
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
$('setupInjectOverlay').addEventListener('click', async function(){
|
|
1983
|
+
setStatus('setupStatus', 'Injecting overlay chat...', 'info');
|
|
1984
|
+
try {
|
|
1985
|
+
await postJson('/api/command', { type:'bridge_overlay_reinject', reason:'setup' });
|
|
1986
|
+
setStatus('setupStatus', 'Overlay chat injected.', 'ok');
|
|
1987
|
+
refreshStatus(); refreshSetupStatus();
|
|
1988
|
+
} catch (e) { setStatus('setupStatus', 'Overlay injection failed: ' + e.message, 'err'); }
|
|
1989
|
+
});
|
|
1990
|
+
$('setupDetectMonitors').addEventListener('click', function(){ refreshMonitors(true); });
|
|
1991
|
+
$('setupCalibratePrimary').addEventListener('click', async function(){
|
|
1992
|
+
setStatus('setupStatus', 'Calibration started on primary monitor.', 'info');
|
|
1993
|
+
try {
|
|
1994
|
+
await postJson('/api/command', { type:'desktop_calibrate_pointer', monitor:'primary', area:'monitor' });
|
|
1995
|
+
setStatus('setupStatus', 'Primary monitor calibration saved.', 'ok');
|
|
1996
|
+
refreshCalibration(); refreshSetupStatus();
|
|
1997
|
+
} catch (e) { setStatus('setupStatus', 'Calibration failed: ' + e.message, 'err'); }
|
|
1998
|
+
});
|
|
1999
|
+
$('setupSaveComplete').addEventListener('click', async function(){
|
|
2000
|
+
setStatus('setupStatus', 'Saving setup...', 'info');
|
|
2001
|
+
try {
|
|
2002
|
+
var s = await postJson('/api/command', { type:'bridge_setup_status' });
|
|
2003
|
+
var status = s && (s.result || s);
|
|
2004
|
+
await postJson('/api/settings/state', { bridge: { desktopSetup: { completed:true, checklist:(status && status.current) || {}, snapshot:status || {} } } });
|
|
2005
|
+
setStatus('setupStatus', 'Setup saved for MCP and empir3 agents.', 'ok');
|
|
2006
|
+
refreshSetupStatus();
|
|
2007
|
+
} catch (e) { setStatus('setupStatus', 'Save failed: ' + e.message, 'err'); }
|
|
2008
|
+
});
|
|
2009
|
+
$('dtOpenDesktopTest').addEventListener('click', function(){
|
|
2010
|
+
runDesktopCommand('Opening desktop test', { type:'navigate', url: location.origin + '/desktop-test' }, { show:false, done:'Desktop test opened.' });
|
|
2011
|
+
});
|
|
2012
|
+
$('dtOpenAccuracyLab').addEventListener('click', function(){
|
|
2013
|
+
window.open(location.origin + '/accuracy-lab', '_blank');
|
|
2014
|
+
setStatus('desktopToolsStatus', 'Accuracy Lab opened in a new tab.', 'ok');
|
|
2015
|
+
});
|
|
2016
|
+
$('dtBrowserScreenshot').addEventListener('click', function(){
|
|
2017
|
+
runDesktopCommand('Browser screenshot', { type:'screenshot' });
|
|
2018
|
+
});
|
|
2019
|
+
$('dtBrowserRefresh').addEventListener('click', function(){
|
|
2020
|
+
runDesktopCommand('Browser refresh', { type:'refresh' }, { show:false, done:'Refresh sent.' });
|
|
2021
|
+
});
|
|
2022
|
+
$('dtBrowserSnapshot').addEventListener('click', function(){
|
|
2023
|
+
runDesktopCommand('Interactive snapshot', { type:'snapshot', filter:'interactive', format:'compact' });
|
|
2024
|
+
});
|
|
2025
|
+
$('dtInjectOverlay').addEventListener('click', async function(){
|
|
2026
|
+
try { await runDesktopCommand('Inject overlay', { type:'bridge_overlay_reinject', reason:'desktop-tools' }, { done:'Overlay injected and verified.' }); refreshSetupStatus(); }
|
|
2027
|
+
catch (_) {}
|
|
2028
|
+
});
|
|
2029
|
+
$('dtOpenToolbar').addEventListener('click', async function(){
|
|
2030
|
+
try { await runDesktopCommand('Open floating toolbar', { type:'desktop_toolbar', action:'show' }, { show:false, done:'Floating toolbar opened.' }); }
|
|
2031
|
+
catch (_) {}
|
|
2032
|
+
});
|
|
2033
|
+
$('dtRevokeControl').addEventListener('click', async function(){
|
|
2034
|
+
if (!confirm('Disable browser interact, desktop, eval, and recording tools?')) return;
|
|
2035
|
+
setStatus('desktopToolsStatus','Revoking write control…','info');
|
|
2036
|
+
try {
|
|
2037
|
+
var r = await fetch(API + '/api/safety/lockdown', { method:'POST' });
|
|
2038
|
+
if (!r.ok) throw new Error(await r.text());
|
|
2039
|
+
await loadPermissionState();
|
|
2040
|
+
await refreshStatus();
|
|
2041
|
+
setStatus('desktopToolsStatus','Write control revoked.','ok');
|
|
2042
|
+
} catch (e) {
|
|
2043
|
+
setStatus('desktopToolsStatus','Revoke failed: ' + e.message,'err');
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
$('recStart').addEventListener('click', async function(){
|
|
2048
|
+
setStatus('recStatus', 'Starting recording...', 'info');
|
|
2049
|
+
try {
|
|
2050
|
+
var j = await postJson('/api/command', { type:'record_start' });
|
|
2051
|
+
showDesktopOutput('Recording start', j && (j.result || j));
|
|
2052
|
+
setStatus('recStatus', 'Recording started.', 'ok');
|
|
2053
|
+
refreshRecordingStatus(); refreshSetupStatus();
|
|
2054
|
+
} catch (e) { setStatus('recStatus', 'Start failed: ' + e.message, 'err'); }
|
|
2055
|
+
});
|
|
2056
|
+
$('recStop').addEventListener('click', async function(){
|
|
2057
|
+
var name = ($('recName').value || '').trim() || ('recording-' + new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'));
|
|
2058
|
+
setStatus('recStatus', 'Stopping recording...', 'info');
|
|
2059
|
+
try {
|
|
2060
|
+
var j = await postJson('/api/command', { type:'record_stop', text:name });
|
|
2061
|
+
var result = j && (j.result || j);
|
|
2062
|
+
showDesktopOutput('Recording stop', result);
|
|
2063
|
+
setStatus('recStatus', 'Saved ' + (result.saved || name) + '.', 'ok');
|
|
2064
|
+
$('recName').value = '';
|
|
2065
|
+
refreshRecordingStatus(); refreshRecordings(); refreshSetupStatus();
|
|
2066
|
+
} catch (e) { setStatus('recStatus', 'Stop failed: ' + e.message, 'err'); }
|
|
2067
|
+
});
|
|
2068
|
+
$('recRefresh').addEventListener('click', function(){ refreshRecordings(true); });
|
|
2069
|
+
$('recLoad').addEventListener('click', loadSelectedRecording);
|
|
2070
|
+
$('recPlay').addEventListener('click', async function(){
|
|
2071
|
+
var sel = $('recSelect');
|
|
2072
|
+
var name = sel && sel.value;
|
|
2073
|
+
if (!name) { setStatus('recStatus', 'Select a recording first.', 'err'); return; }
|
|
2074
|
+
setStatus('recStatus', 'Playing recording...', 'info');
|
|
2075
|
+
try {
|
|
2076
|
+
var j = await postJson('/api/command', { type:'play', recording:name, speed:1, variables:{} });
|
|
2077
|
+
var result = j && (j.result || j);
|
|
2078
|
+
var out = $('recPreview');
|
|
2079
|
+
if (out) { out.style.display = 'block'; out.textContent = JSON.stringify(result, null, 2); }
|
|
2080
|
+
setStatus('recStatus', 'Playback complete.', 'ok');
|
|
2081
|
+
refreshRecordingStatus();
|
|
2082
|
+
} catch (e) { setStatus('recStatus', 'Playback failed: ' + e.message, 'err'); refreshRecordingStatus(); }
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
$('agtSelectRegion').addEventListener('click', async function(){
|
|
2086
|
+
setStatus('agentStatus','Drag a region on screen — bridge waits up to 2 minutes.','info');
|
|
2087
|
+
try {
|
|
2088
|
+
var j = await postJson('/api/command', { type:'desktop_select_region', timeoutMs:120000 });
|
|
2089
|
+
var region = j && j.result && j.result.region;
|
|
2090
|
+
if (region) setStatus('agentStatus','Region set: ' + region.width + '×' + region.height + ' at (' + region.x + ',' + region.y + ').','ok');
|
|
2091
|
+
else if (j && j.result && j.result.cancelled) setStatus('agentStatus','Region selection cancelled.','info');
|
|
2092
|
+
else setStatus('agentStatus','Region selection finished.','ok');
|
|
2093
|
+
refreshFocus(); refreshSetupStatus();
|
|
2094
|
+
} catch (e) { setStatus('agentStatus','Select region failed: ' + e.message,'err'); }
|
|
2095
|
+
});
|
|
2096
|
+
$('agtReleaseFocus').addEventListener('click', async function(){
|
|
2097
|
+
setStatus('agentStatus','Releasing focus…','info');
|
|
2098
|
+
try { await postJson('/api/command', { type:'desktop_release_focus' }); setStatus('agentStatus','Agent focus and screen artifacts released.','ok'); refreshFocus(); refreshSetupStatus(); }
|
|
2099
|
+
catch (e) { setStatus('agentStatus','Release failed: ' + e.message,'err'); }
|
|
2100
|
+
});
|
|
2101
|
+
var focusGridOn = false;
|
|
2102
|
+
$('agtFocusGrid').addEventListener('click', async function(){
|
|
2103
|
+
try {
|
|
2104
|
+
var want = !focusGridOn;
|
|
2105
|
+
var j = await postJson('/api/command', { type:'desktop_focus_grid', action: want ? 'show' : 'hide' });
|
|
2106
|
+
focusGridOn = !!(j && j.result && j.result.enabled);
|
|
2107
|
+
$('agtFocusGrid').textContent = focusGridOn ? 'Hide focus grid' : 'Show focus grid';
|
|
2108
|
+
setStatus('agentStatus','Focus grid ' + (focusGridOn ? 'visible.' : 'hidden.'),'ok');
|
|
2109
|
+
} catch (e) { setStatus('agentStatus','Focus grid toggle failed: ' + e.message,'err'); }
|
|
2110
|
+
});
|
|
2111
|
+
$('agtDetectMonitors').addEventListener('click', function(){ refreshMonitors(true); });
|
|
2112
|
+
$('agtCalibrate').addEventListener('click', async function(){
|
|
2113
|
+
var sel = $('agtCalMonitor');
|
|
2114
|
+
var mon = (sel && sel.value) || 'primary';
|
|
2115
|
+
var area = mon === 'all' ? 'all' : 'monitor';
|
|
2116
|
+
setStatus('calStatus','Calibration started for ' + mon + '.','info');
|
|
2117
|
+
try {
|
|
2118
|
+
var j = await postJson('/api/command', { type:'desktop_calibrate_pointer', monitor:mon, area:area });
|
|
2119
|
+
if (j && j.result && j.result.cancelled) { setStatus('calStatus','Calibration cancelled.','info'); return; }
|
|
2120
|
+
setStatus('calStatus','Calibration saved.','ok');
|
|
2121
|
+
refreshCalibration(); refreshSetupStatus();
|
|
2122
|
+
} catch (e) { setStatus('calStatus','Calibration failed: ' + e.message,'err'); }
|
|
2123
|
+
});
|
|
2124
|
+
$('agtCalibrateAll').addEventListener('click', async function(){
|
|
2125
|
+
setStatus('calStatus','Calibration started for all monitors.','info');
|
|
2126
|
+
try {
|
|
2127
|
+
await postJson('/api/command', { type:'desktop_calibrate_pointer', monitor:'all', area:'all' });
|
|
2128
|
+
setStatus('calStatus','All monitor calibration saved.','ok');
|
|
2129
|
+
refreshCalibration(); refreshSetupStatus();
|
|
2130
|
+
} catch (e) { setStatus('calStatus','Calibration failed: ' + e.message,'err'); }
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
// ────── Updates pane ──────
|
|
2134
|
+
$('updateCheck').addEventListener('click', async function(){
|
|
2135
|
+
setStatus('updateStatus','Checking for updates…','info');
|
|
2136
|
+
try {
|
|
2137
|
+
var j = await getJson('/api/updates/check');
|
|
2138
|
+
var local = j.local || '${BRIDGE_VERSION}';
|
|
2139
|
+
var remote = j.remote || '(unknown)';
|
|
2140
|
+
setText('updateAvailable', remote);
|
|
2141
|
+
setText('updateLastCheck', new Date().toLocaleTimeString());
|
|
2142
|
+
var tag = $('updateTag'); if (tag){ tag.textContent = j.newer ? 'UPDATE AVAILABLE' : 'CURRENT'; tag.className = 'tag ' + (j.newer ? 'warn' : 'good'); }
|
|
2143
|
+
if (j.newer) { $('updateApply').style.display = 'inline-flex'; setStatus('updateStatus','Update available: v' + remote + ' (you are on v' + local + ').','ok'); }
|
|
2144
|
+
else { $('updateApply').style.display = 'none'; setStatus('updateStatus','You are on the latest version.','ok'); }
|
|
2145
|
+
} catch (e) { setStatus('updateStatus','Update check failed: ' + e.message,'err'); }
|
|
2146
|
+
});
|
|
2147
|
+
$('updateApply').addEventListener('click', async function(){
|
|
2148
|
+
if (!confirm('Apply update? The tray and bridge daemon will restart.')) return;
|
|
2149
|
+
setStatus('updateStatus','Queuing update…','info');
|
|
2150
|
+
try { await postJson('/api/tray/enqueue', { type:'tray_apply_update' }); setStatus('updateStatus','Update queued — tray will restart shortly.','ok'); }
|
|
2151
|
+
catch (e) { setStatus('updateStatus','Could not queue update: ' + e.message,'err'); }
|
|
2152
|
+
});
|
|
2153
|
+
$('autoUpdateToggle').addEventListener('change', async function(e){
|
|
2154
|
+
setStatus('policyStatus','Saving…','info');
|
|
2155
|
+
try { var r = await postJson('/api/settings/state', { bridge: { autoUpdate: !!e.target.checked } }); setStatus('policyStatus','Auto-update ' + ((r && r.bridge && r.bridge.autoUpdate !== false) ? 'on' : 'off') + '.','ok'); }
|
|
2156
|
+
catch (e2) { setStatus('policyStatus','Could not save: ' + e2.message,'err'); }
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
// ────── Lifecycle pane ──────
|
|
2160
|
+
async function enqueueTray(type, confirmMsg) {
|
|
2161
|
+
if (confirmMsg && !confirm(confirmMsg)) return;
|
|
2162
|
+
setStatus('lifeStatus','Queuing ' + type + '…','info');
|
|
2163
|
+
try { await postJson('/api/tray/enqueue', { type: type }); setStatus('lifeStatus',type + ' queued.','ok'); }
|
|
2164
|
+
catch (e) { setStatus('lifeStatus','Failed: ' + e.message,'err'); }
|
|
2165
|
+
}
|
|
2166
|
+
$('lifeRestart').addEventListener('click', function(){ enqueueTray('tray_restart_tray','Restart the Empir3 tray? The bridge daemon will restart with it.'); });
|
|
2167
|
+
$('lifeQuit').addEventListener('click', function(){ enqueueTray('tray_quit','Quit Empir3? The tray icon will disappear and the bridge daemon will stop.'); });
|
|
2168
|
+
$('lifeUninstall').addEventListener('click', function(){ enqueueTray('tray_uninstall','Uninstall Empir3?\\n\\nThis wipes the bridge Chrome profile, auth, settings, autostart entry, Start Menu shortcut, and cached payloads. Irreversible.'); });
|
|
2169
|
+
|
|
2170
|
+
// ────── Account / sign-in drawer ──────
|
|
2171
|
+
function syncServerUi(serverUrl) {
|
|
2172
|
+
var norm = (serverUrl || PROD_SERVER).replace(/\\/+$/,'');
|
|
2173
|
+
if (norm === PROD_SERVER) $('serverPreset').value = 'production';
|
|
2174
|
+
else if (norm === DEV_SERVER) $('serverPreset').value = 'local-dev';
|
|
2175
|
+
else $('serverPreset').value = 'custom';
|
|
2176
|
+
if ($('serverUrl')) $('serverUrl').value = norm;
|
|
2177
|
+
$('customServerLabel').style.display = $('serverPreset').value === 'custom' ? 'grid' : 'none';
|
|
2178
|
+
}
|
|
2179
|
+
function selectedServer() {
|
|
2180
|
+
var p = $('serverPreset').value;
|
|
2181
|
+
if (p === 'production') return PROD_SERVER;
|
|
2182
|
+
if (p === 'local-dev') return DEV_SERVER;
|
|
2183
|
+
return ($('serverUrl').value || PROD_SERVER).trim();
|
|
2184
|
+
}
|
|
2185
|
+
$('serverPreset').addEventListener('change', function(){
|
|
2186
|
+
var p = $('serverPreset').value;
|
|
2187
|
+
if (p === 'production') $('serverUrl').value = PROD_SERVER;
|
|
2188
|
+
else if (p === 'local-dev') $('serverUrl').value = DEV_SERVER;
|
|
2189
|
+
$('customServerLabel').style.display = p === 'custom' ? 'grid' : 'none';
|
|
2190
|
+
});
|
|
2191
|
+
$('pairEmpir3').addEventListener('click', async function(){
|
|
2192
|
+
setStatus('drawerStatus','Starting browser-based empir3 pairing…','info');
|
|
2193
|
+
try {
|
|
2194
|
+
var j = await postJson('/api/install/empir3-pair', { serverUrl: selectedServer() });
|
|
2195
|
+
if (!j.ok || !j.redirectUrl) throw new Error(j.error || 'pairing failed');
|
|
2196
|
+
setStatus('drawerStatus','Opening empir3…','ok');
|
|
2197
|
+
setTimeout(function(){ location.href = j.redirectUrl; }, 400);
|
|
2198
|
+
} catch (e) { setStatus('drawerStatus','Could not start pairing: ' + e.message,'err'); }
|
|
2199
|
+
});
|
|
2200
|
+
$('loginForm').addEventListener('submit', async function(e){
|
|
2201
|
+
e.preventDefault();
|
|
2202
|
+
setStatus('drawerStatus','Signing in…','info');
|
|
2203
|
+
try {
|
|
2204
|
+
var j = await postJson('/api/install/empir3-login', { email: $('loginEmail').value, password: $('loginPassword').value, serverUrl: selectedServer() });
|
|
2205
|
+
if (!j.ok) throw new Error(j.error || 'login failed');
|
|
2206
|
+
setStatus('drawerStatus','Signed in. Reloading…','ok');
|
|
2207
|
+
setTimeout(function(){ location.reload(); }, 700);
|
|
2208
|
+
} catch (e2) { setStatus('drawerStatus','Could not sign in: ' + e2.message,'err'); }
|
|
2209
|
+
});
|
|
2210
|
+
$('acctSignOut').addEventListener('click', async function(){
|
|
2211
|
+
if (!confirm('Sign out the stored empir3 account from this bridge?')) return;
|
|
2212
|
+
setStatus('acctStatus','Signing out…','info');
|
|
2213
|
+
try { var j = await postJson('/api/install/sign-out', {}); if (!j.ok) throw new Error(j.error||'sign out failed'); setStatus('acctStatus','Signed out. Reloading…','ok'); setTimeout(function(){ location.reload(); }, 600); }
|
|
2214
|
+
catch (e) { setStatus('acctStatus','Could not sign out: ' + e.message,'err'); }
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
// ────── Drawer + nav + theme ──────
|
|
2218
|
+
var drawer = $('drawer'), backdrop = $('backdrop');
|
|
2219
|
+
window.openDrawer = function(){ drawer.classList.add('open'); backdrop.classList.add('open'); };
|
|
2220
|
+
window.closeDrawer = function(){ drawer.classList.remove('open'); backdrop.classList.remove('open'); };
|
|
2221
|
+
$('openSignIn').addEventListener('click', openDrawer);
|
|
2222
|
+
$('openAccount').addEventListener('click', function(){ goto('account'); });
|
|
2223
|
+
backdrop.addEventListener('click', closeDrawer);
|
|
2224
|
+
|
|
2225
|
+
var PANE_LABELS = { overview:'Overview', permissions:'Permissions', mcp:'MCP Connection', clis:'API & CLIs', agent:'Desktop Tools', account:'empir3 Account', daemon:'Daemon', updates:'Updates', logs:'Activity Log', lifecycle:'Tray Lifecycle' };
|
|
2226
|
+
function closeRail() { document.body.classList.remove('rail-open'); }
|
|
2227
|
+
function goto(name) {
|
|
2228
|
+
document.querySelectorAll('.rail-nav a').forEach(function(a){ a.classList.toggle('active', a.dataset.nav === name); });
|
|
2229
|
+
document.querySelectorAll('.pane').forEach(function(p){ p.classList.toggle('active', p.dataset.pane === name); });
|
|
2230
|
+
setText('crumbHere', PANE_LABELS[name] || name);
|
|
2231
|
+
document.querySelector('.pane-scroll').scrollTop = 0;
|
|
2232
|
+
if (name === 'logs') refreshActions();
|
|
2233
|
+
closeRail();
|
|
2234
|
+
}
|
|
2235
|
+
document.querySelectorAll('[data-nav]').forEach(function(a){ a.addEventListener('click', function(e){ e.preventDefault(); goto(a.dataset.nav); }); });
|
|
2236
|
+
document.querySelectorAll('[data-goto]').forEach(function(b){ b.addEventListener('click', function(e){ e.preventDefault(); goto(b.dataset.goto); }); });
|
|
2237
|
+
var railToggleBtn = $('railToggle');
|
|
2238
|
+
if (railToggleBtn) railToggleBtn.addEventListener('click', function(){ document.body.classList.toggle('rail-open'); });
|
|
2239
|
+
var railScrim = $('railScrim');
|
|
2240
|
+
if (railScrim) railScrim.addEventListener('click', closeRail);
|
|
2241
|
+
document.addEventListener('keydown', function(e){ if (e.key === 'Escape' && document.body.classList.contains('rail-open')) closeRail(); });
|
|
2242
|
+
|
|
2243
|
+
// Theme persistence
|
|
2244
|
+
try { var th = localStorage.getItem('empir3-bridge-theme'); if (th === 'dark' || th === 'light') document.body.dataset.theme = th; } catch (_) {}
|
|
2245
|
+
$('themeToggle').addEventListener('click', function(){
|
|
2246
|
+
var next = document.body.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
2247
|
+
document.body.dataset.theme = next;
|
|
2248
|
+
try { localStorage.setItem('empir3-bridge-theme', next); } catch (_) {}
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
// Logs refresh button
|
|
2252
|
+
$('logsRefresh').addEventListener('click', refreshActions);
|
|
2253
|
+
|
|
2254
|
+
// ────── Calibration initial load (real fetch on boot) ──────
|
|
2255
|
+
async function refreshCalibration() {
|
|
2256
|
+
try {
|
|
2257
|
+
var j = await postJson('/api/command', { type: 'desktop_calibration_status' });
|
|
2258
|
+
var result = j && (j.result || j);
|
|
2259
|
+
var cal = result && result.calibration;
|
|
2260
|
+
var monitorIds = (result && result.monitors) || (cal && cal.monitors ? Object.keys(cal.monitors) : []);
|
|
2261
|
+
var hasCal = !!(result && result.applied);
|
|
2262
|
+
var tag = $('calibrationTag');
|
|
2263
|
+
if (hasCal) {
|
|
2264
|
+
if (cal && cal.version === 2 && cal.monitors) {
|
|
2265
|
+
var rows = Object.keys(cal.monitors).map(function(id){ var m = cal.monitors[id] || {}; return id + (m.residualPx != null ? ' (' + m.residualPx + 'px)' : ''); });
|
|
2266
|
+
setText('calOffsetX', 'per-monitor');
|
|
2267
|
+
setText('calOffsetY', 'affine fit');
|
|
2268
|
+
setText('calMonitorSummary', rows.join(', '));
|
|
2269
|
+
if (cal.capturedAt) setText('calLastRun', String(cal.capturedAt).slice(0,16).replace('T',' '));
|
|
2270
|
+
} else {
|
|
2271
|
+
var dx = cal && (cal.offsetX|0), dy = cal && (cal.offsetY|0);
|
|
2272
|
+
setText('calOffsetX', (dx>=0?'+':'') + dx + ' px');
|
|
2273
|
+
setText('calOffsetY', (dy>=0?'+':'') + dy + ' px');
|
|
2274
|
+
setText('calMonitorSummary', monitorIds.length ? monitorIds.join(', ') : 'legacy');
|
|
2275
|
+
if (cal && cal.updatedAt) setText('calLastRun', String(cal.updatedAt).slice(0,16).replace('T',' '));
|
|
2276
|
+
}
|
|
2277
|
+
if (tag) { tag.textContent = 'CALIBRATED'; tag.className = 'tag good'; }
|
|
2278
|
+
} else {
|
|
2279
|
+
setText('calOffsetX', '—'); setText('calOffsetY', '—'); setText('calLastRun', '—');
|
|
2280
|
+
if (!DESKTOP_MONITORS.length) setText('calMonitorSummary', '—');
|
|
2281
|
+
if (tag) { tag.textContent = 'UNCALIBRATED'; tag.className = 'tag'; }
|
|
2282
|
+
}
|
|
2283
|
+
refreshSetupStatus();
|
|
2284
|
+
} catch (_) { /* read-only call; ignore if denied */ }
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// ────── API & CLIs pane ──────
|
|
2288
|
+
// The row schema is single-source-of-truth — adding a provider here +
|
|
2289
|
+
// its probe in buildSettingsState() is the whole front-end change.
|
|
2290
|
+
// The 'lendable' flag controls whether the row gets a "Lend to empir3"
|
|
2291
|
+
// toggle vs the higgsfield handler-family toggle.
|
|
2292
|
+
var CLI_ROWS = [
|
|
2293
|
+
{ id:'claude', label:'Claude Code', vendor:'Anthropic', lendable:true, settingsKey:'lendClaudeMax', keyField:'apiKeyAnthropic', keyVendor:'anthropic' },
|
|
2294
|
+
{ id:'codex', label:'OpenAI Codex', vendor:'OpenAI', lendable:true, settingsKey:'lendOpenAiCodex', keyField:'apiKeyOpenai', keyVendor:'openai' },
|
|
2295
|
+
{ id:'gemini', label:'Gemini CLI', vendor:'Google', lendable:true, settingsKey:'lendGoogleGemini', keyField:'apiKeyGoogle', keyVendor:'google' },
|
|
2296
|
+
{ id:'grok', label:'Grok Build CLI', vendor:'xAI', lendable:true, settingsKey:'lendXaiGrok', keyField:'apiKeyXai', keyVendor:'xai' },
|
|
2297
|
+
{ id:'agy', label:'Antigravity', vendor:'Google', lendable:true, settingsKey:'lendGoogleAntigravity' },
|
|
2298
|
+
{ id:'higgsfield', label:'Higgsfield CLI', vendor:'Higgsfield',lendable:false, handlerKey:'higgsfield' },
|
|
2299
|
+
{ id:'github', label:'GitHub CLI', vendor:'GitHub', lendable:true, settingsKey:'lendGitHubCli', ghScopes:true },
|
|
2300
|
+
];
|
|
2301
|
+
// GitHub CLI lend scopes — fine-grained per-capability gates shown under
|
|
2302
|
+
// the GitHub row when the master lend is on. Mirrors the backend
|
|
2303
|
+
// defaultGhScopes() baseline. label/hint are UI-only.
|
|
2304
|
+
var GH_SCOPE_DEFS = [
|
|
2305
|
+
{ key:'read', label:'Read', hint:'list / view / status / search / api GET' },
|
|
2306
|
+
{ key:'pr', label:'Pull requests', hint:'create / edit / merge / review / comment' },
|
|
2307
|
+
{ key:'issue', label:'Issues', hint:'create / edit / close / comment' },
|
|
2308
|
+
{ key:'repo', label:'Repos', hint:'create / edit / fork / rename (not delete)' },
|
|
2309
|
+
{ key:'release', label:'Releases', hint:'create / edit / upload / download' },
|
|
2310
|
+
{ key:'workflow', label:'Workflows', hint:'run / cancel / rerun — spends CI' },
|
|
2311
|
+
{ key:'admin', label:'Admin', hint:'secrets, repo delete, org, SSH/GPG keys' },
|
|
2312
|
+
{ key:'api_write', label:'Raw API write', hint:'gh api with POST / PATCH / DELETE' },
|
|
2313
|
+
];
|
|
2314
|
+
var CLI_STATE = null; // last full settings-state response
|
|
2315
|
+
|
|
2316
|
+
function renderCliRows() {
|
|
2317
|
+
var tbody = $('cliRows'); if (!tbody) return;
|
|
2318
|
+
if (!CLI_STATE) { tbody.innerHTML = '<tr><td colspan="5" class="dt" style="text-align:center; color:var(--soft);">Loading…</td></tr>'; return; }
|
|
2319
|
+
var providers = (CLI_STATE.providers || {});
|
|
2320
|
+
var bridge = (CLI_STATE.bridge || {});
|
|
2321
|
+
var handlers = bridge.handlers || {};
|
|
2322
|
+
var installedCount = 0;
|
|
2323
|
+
var html = '';
|
|
2324
|
+
for (var i = 0; i < CLI_ROWS.length; i++) {
|
|
2325
|
+
var row = CLI_ROWS[i];
|
|
2326
|
+
var p = providers[row.id] || {};
|
|
2327
|
+
var installed = !!p.available;
|
|
2328
|
+
if (installed) installedCount++;
|
|
2329
|
+
var authed = !!p.authenticated;
|
|
2330
|
+
var installTag = installed
|
|
2331
|
+
? '<span class="tag good">' + escapeHtml(p.version || 'installed') + '</span>'
|
|
2332
|
+
: '<span class="tag bad">NOT INSTALLED</span>';
|
|
2333
|
+
var authTag = installed
|
|
2334
|
+
? (authed
|
|
2335
|
+
? '<span class="tag good">AUTHED' + (p.auth_via && p.auth_via !== 'creds_file' && p.auth_via !== 'auth_file' ? ' · ' + escapeHtml(String(p.auth_via).toUpperCase()) : '') + '</span>'
|
|
2336
|
+
: '<span class="tag warn">NEEDS AUTH</span>')
|
|
2337
|
+
: '<span class="tag" style="color:var(--soft);">—</span>';
|
|
2338
|
+
var toggleCell;
|
|
2339
|
+
if (row.lendable) {
|
|
2340
|
+
var checked = !!bridge[row.settingsKey];
|
|
2341
|
+
toggleCell = '<label class="sw"><input type="checkbox" data-cli-toggle="' + row.id + '" data-settings-key="' + row.settingsKey + '"' + (checked?' checked':'') + (installed?'':' disabled') + '><span class="s"></span></label> <span style="font-size:11.5px; color:var(--soft); margin-left:6px;">' + (installed ? (checked ? 'lending' : 'not lent') : 'install first') + '</span>';
|
|
2342
|
+
} else if (row.handlerKey) {
|
|
2343
|
+
var hEnabled = !!(handlers[row.handlerKey] && handlers[row.handlerKey].enabled);
|
|
2344
|
+
// Count per-tool toggles for this family on the Permissions page —
|
|
2345
|
+
// gives users a "3 / 3 tools on" readout so they can see the family
|
|
2346
|
+
// gate and per-tool layers stay in sync.
|
|
2347
|
+
var familyTools = (typeof TOOLS !== 'undefined' && TOOLS) ? TOOLS.filter(function(t){ return t.g === row.handlerKey; }) : [];
|
|
2348
|
+
var familyOn = familyTools.filter(function(t){ return t.on; }).length;
|
|
2349
|
+
var summary;
|
|
2350
|
+
if (!hEnabled) {
|
|
2351
|
+
summary = 'tools disabled <span style="opacity:0.7;">(' + familyOn + '/' + familyTools.length + ' configured)</span>';
|
|
2352
|
+
} else {
|
|
2353
|
+
summary = familyOn + ' / ' + familyTools.length + ' tools · <a href="#" data-goto="permissions" data-perm-filter="' + row.handlerKey + '" style="color:var(--accent); text-decoration:underline;">configure</a>';
|
|
2354
|
+
}
|
|
2355
|
+
toggleCell = '<label class="sw"><input type="checkbox" data-cli-toggle="' + row.id + '" data-handler-key="' + row.handlerKey + '"' + (hEnabled?' checked':'') + '><span class="s"></span></label> <span style="font-size:11.5px; color:var(--soft); margin-left:6px;">' + summary + '</span>';
|
|
2356
|
+
} else {
|
|
2357
|
+
toggleCell = '<span style="color:var(--soft);">—</span>';
|
|
2358
|
+
}
|
|
2359
|
+
var authBtn = installed
|
|
2360
|
+
? '<button class="btn small" type="button" data-cli-auth="' + row.id + '">' + (authed ? 'Re-auth' : 'Authenticate') + '</button>'
|
|
2361
|
+
: (p.install
|
|
2362
|
+
? '<button class="btn small" type="button" data-cli-install="' + row.id + '">Install</button>'
|
|
2363
|
+
: '<span style="font-size:11.5px; color:var(--soft);">install first</span>');
|
|
2364
|
+
html += '<tr>' +
|
|
2365
|
+
'<td class="col-nm"><div style="font-weight:600;">' + escapeHtml(row.label) + '</div><div style="font-size:11.5px; color:var(--soft);">' + escapeHtml(row.vendor) + (p.path ? ' · ' + escapeHtml(shortPath(p.path)) : '') + '</div></td>' +
|
|
2366
|
+
'<td>' + installTag + '</td>' +
|
|
2367
|
+
'<td>' + authTag + '</td>' +
|
|
2368
|
+
'<td>' + toggleCell + '</td>' +
|
|
2369
|
+
'<td>' + authBtn + '</td>' +
|
|
2370
|
+
'</tr>';
|
|
2371
|
+
// NOT INSTALLED: full-width helper row with the install command (copy),
|
|
2372
|
+
// a Get-it link to the official page, and any caveat note. The "Install"
|
|
2373
|
+
// button in the Action column runs the same command in a console.
|
|
2374
|
+
if (!installed && p.install) {
|
|
2375
|
+
html += '<tr class="cli-install-row"><td colspan="5" style="padding:2px 14px 14px 14px;">' +
|
|
2376
|
+
'<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">' +
|
|
2377
|
+
'<span style="font-size:11.5px; color:var(--soft);">Get it:</span>' +
|
|
2378
|
+
'<code style="font-size:12px; background:var(--card2,rgba(255,255,255,0.04)); border:1px solid var(--line); border-radius:6px; padding:4px 8px; user-select:all;">' + escapeHtml(p.install.command) + '</code>' +
|
|
2379
|
+
'<button class="btn small ghost" type="button" data-install-copy="' + escapeAttr(p.install.command) + '">Copy</button>' +
|
|
2380
|
+
'<a href="' + escapeAttr(p.install.docsUrl) + '" target="_blank" rel="noopener noreferrer" style="font-size:12px; color:var(--accent); text-decoration:underline;">Official page ↗</a>' +
|
|
2381
|
+
'<span style="font-size:11px; color:var(--soft);">or just tell your agent to install it</span>' +
|
|
2382
|
+
'</div>' +
|
|
2383
|
+
(p.install.note ? '<div style="font-size:11px; color:var(--soft); margin-top:6px;">' + escapeHtml(p.install.note) + '</div>' : '') +
|
|
2384
|
+
'</td></tr>';
|
|
2385
|
+
}
|
|
2386
|
+
// GitHub CLI: when the master lend is on, render the fine-grained
|
|
2387
|
+
// scope matrix as a full-width sub-row directly beneath it.
|
|
2388
|
+
if (row.ghScopes && !!bridge.lendGitHubCli) {
|
|
2389
|
+
var sc = bridge.githubScopes || {};
|
|
2390
|
+
var acct = (p.account ? ' · acting as <b>' + escapeHtml(p.account) + '</b>' : '');
|
|
2391
|
+
var cells = '';
|
|
2392
|
+
for (var gi = 0; gi < GH_SCOPE_DEFS.length; gi++) {
|
|
2393
|
+
var sd = GH_SCOPE_DEFS[gi];
|
|
2394
|
+
var on = !!sc[sd.key];
|
|
2395
|
+
cells += '<label class="gh-scope" title="' + escapeAttr(sd.hint) + '" style="display:flex; align-items:flex-start; gap:7px; padding:7px 9px; border:1px solid var(--line); border-radius:8px; background:var(--card2,rgba(255,255,255,0.02));">' +
|
|
2396
|
+
'<input type="checkbox" data-gh-scope="' + sd.key + '"' + (on?' checked':'') + (installed?'':' disabled') + ' style="margin-top:2px;">' +
|
|
2397
|
+
'<span><span style="font-weight:600; font-size:12.5px;">' + escapeHtml(sd.label) + '</span><br><span style="font-size:11px; color:var(--soft);">' + escapeHtml(sd.hint) + '</span></span>' +
|
|
2398
|
+
'</label>';
|
|
2399
|
+
}
|
|
2400
|
+
html += '<tr class="gh-scope-row"><td colspan="5" style="padding:4px 14px 14px 14px;">' +
|
|
2401
|
+
'<div style="font-size:11.5px; color:var(--soft); margin:0 0 8px 2px;">Scopes a remote / team agent may use with your GitHub login' + acct + '. Token exfil, de-auth, aliases & extensions are always blocked.</div>' +
|
|
2402
|
+
'<div style="display:grid; grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); gap:8px;">' + cells + '</div>' +
|
|
2403
|
+
'</td></tr>';
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
// Append custom OpenAI-compatible providers (dynamic — added by the
|
|
2407
|
+
// user via the "+ Add custom provider" modal). Renders as the same
|
|
2408
|
+
// row shape so the table stays consistent. Action column gets a
|
|
2409
|
+
// Remove button instead of Auth since custom providers don't have a
|
|
2410
|
+
// CLI auth flow — they hold their API key (if any) inside the
|
|
2411
|
+
// provider definition itself.
|
|
2412
|
+
var customs = (CLI_STATE.customProviders || []);
|
|
2413
|
+
for (var ci = 0; ci < customs.length; ci++) {
|
|
2414
|
+
var cp = customs[ci];
|
|
2415
|
+
var avail = !!cp.available;
|
|
2416
|
+
if (avail) installedCount++;
|
|
2417
|
+
var statusTag = avail
|
|
2418
|
+
? (cp.authError
|
|
2419
|
+
? '<span class="tag warn">AUTH ERROR</span>'
|
|
2420
|
+
: '<span class="tag good">ONLINE</span>')
|
|
2421
|
+
: '<span class="tag bad">OFFLINE</span>';
|
|
2422
|
+
var keyTag = cp.apiKeySet
|
|
2423
|
+
? '<span class="tag good">KEY SET</span>'
|
|
2424
|
+
: (avail ? '<span class="tag" style="color:var(--soft);">NO KEY</span>' : '<span class="tag" style="color:var(--soft);">—</span>');
|
|
2425
|
+
var modelCount = (cp.models || []).length;
|
|
2426
|
+
var modelInfo = modelCount > 0 ? modelCount + ' model' + (modelCount === 1 ? '' : 's') : (avail ? '0 models' : '—');
|
|
2427
|
+
var lendCell = '<label class="sw"><input type="checkbox" data-custom-lend="' + escapeAttr(cp.slug) + '"' + (cp.lend?' checked':'') + (avail?'':' disabled') + '><span class="s"></span></label> <span style="font-size:11.5px; color:var(--soft); margin-left:6px;">' + (avail ? (cp.lend ? 'lending (v2)' : 'not lent') : 'offline') + '</span>';
|
|
2428
|
+
html += '<tr>' +
|
|
2429
|
+
'<td class="col-nm"><div style="font-weight:600;">' + escapeHtml(cp.name) + '</div><div style="font-size:11.5px; color:var(--soft);">custom · ' + escapeHtml(cp.slug) + ' · ' + escapeHtml(shortPath(cp.apiBaseUrl)) + '</div></td>' +
|
|
2430
|
+
'<td>' + statusTag + ' <span style="font-size:11.5px; color:var(--soft); margin-left:4px;">' + escapeHtml(modelInfo) + '</span></td>' +
|
|
2431
|
+
'<td>' + keyTag + '</td>' +
|
|
2432
|
+
'<td>' + lendCell + '</td>' +
|
|
2433
|
+
'<td><button class="btn small ghost" type="button" data-custom-remove="' + escapeAttr(cp.slug) + '">Remove</button></td>' +
|
|
2434
|
+
'</tr>';
|
|
2435
|
+
}
|
|
2436
|
+
tbody.innerHTML = html;
|
|
2437
|
+
setText('cliInstalledTag', installedCount + ' / ' + (CLI_ROWS.length + customs.length));
|
|
2438
|
+
// API key indicators
|
|
2439
|
+
var keysSet = (CLI_STATE.chat && CLI_STATE.chat.apiKeysSet) || {};
|
|
2440
|
+
var setCount = ['anthropic','openai','google','xai'].filter(function(k){ return !!keysSet[k]; }).length;
|
|
2441
|
+
setText('apiKeysTag', setCount + ' / 4 set');
|
|
2442
|
+
// Wire toggle change handlers freshly each render
|
|
2443
|
+
document.querySelectorAll('[data-cli-toggle]').forEach(function(cb){
|
|
2444
|
+
cb.addEventListener('change', function(){ onCliToggleChange(cb); });
|
|
2445
|
+
});
|
|
2446
|
+
document.querySelectorAll('[data-gh-scope]').forEach(function(cb){
|
|
2447
|
+
cb.addEventListener('change', function(){ onGhScopeChange(cb); });
|
|
2448
|
+
});
|
|
2449
|
+
document.querySelectorAll('[data-cli-auth]').forEach(function(btn){
|
|
2450
|
+
btn.addEventListener('click', function(){ onCliAuthClick(btn.dataset.cliAuth); });
|
|
2451
|
+
});
|
|
2452
|
+
document.querySelectorAll('[data-cli-install]').forEach(function(btn){
|
|
2453
|
+
btn.addEventListener('click', function(){ onCliInstallClick(btn.dataset.cliInstall); });
|
|
2454
|
+
});
|
|
2455
|
+
document.querySelectorAll('[data-install-copy]').forEach(function(btn){
|
|
2456
|
+
btn.addEventListener('click', async function(){
|
|
2457
|
+
var cmd = btn.dataset.installCopy;
|
|
2458
|
+
try { await navigator.clipboard.writeText(cmd); setStatus('cliStatus', 'Copied: ' + cmd, 'ok'); }
|
|
2459
|
+
catch (e) { setStatus('cliStatus', 'Copy this command: ' + cmd, 'info'); }
|
|
2460
|
+
});
|
|
2461
|
+
});
|
|
2462
|
+
// Wire the "configure" link inside the cell summary — jump to
|
|
2463
|
+
// Permissions and pre-select the matching family filter button.
|
|
2464
|
+
document.querySelectorAll('#cliRows [data-perm-filter]').forEach(function(a){
|
|
2465
|
+
a.addEventListener('click', function(e){
|
|
2466
|
+
e.preventDefault();
|
|
2467
|
+
var f = a.dataset.permFilter;
|
|
2468
|
+
goto('permissions');
|
|
2469
|
+
var btn = document.querySelector('.perm-toolbar .filter-group [data-filter="' + f + '"]');
|
|
2470
|
+
if (btn) btn.click();
|
|
2471
|
+
});
|
|
2472
|
+
});
|
|
2473
|
+
// Custom provider: Remove button.
|
|
2474
|
+
document.querySelectorAll('#cliRows [data-custom-remove]').forEach(function(b){
|
|
2475
|
+
b.addEventListener('click', function(){
|
|
2476
|
+
var slug = b.dataset.customRemove;
|
|
2477
|
+
if (!confirm('Remove provider "' + slug + '"?')) return;
|
|
2478
|
+
removeCustomProvider(slug);
|
|
2479
|
+
});
|
|
2480
|
+
});
|
|
2481
|
+
// Custom provider: Lend toggle (v1 flips the local flag; empir3
|
|
2482
|
+
// server-side routing is v2 and ignores this for now).
|
|
2483
|
+
document.querySelectorAll('#cliRows [data-custom-lend]').forEach(function(cb){
|
|
2484
|
+
cb.addEventListener('change', function(){
|
|
2485
|
+
toggleCustomProviderLend(cb.dataset.customLend, cb.checked);
|
|
2486
|
+
});
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
async function removeCustomProvider(slug) {
|
|
2491
|
+
setStatus('cliStatus', 'Removing ' + slug + '…', 'info');
|
|
2492
|
+
markLocalMutate();
|
|
2493
|
+
try {
|
|
2494
|
+
var r = await fetch(API + '/api/cli/providers/' + encodeURIComponent(slug), { method: 'DELETE' });
|
|
2495
|
+
var j = await r.json();
|
|
2496
|
+
if (!j.ok) throw new Error(j.error || 'remove failed');
|
|
2497
|
+
setStatus('cliStatus', slug + ' removed.', 'ok');
|
|
2498
|
+
await loadCliState();
|
|
2499
|
+
} catch (e) {
|
|
2500
|
+
setStatus('cliStatus', 'Remove failed: ' + e.message, 'err');
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
async function toggleCustomProviderLend(slug, lend) {
|
|
2505
|
+
setStatus('cliStatus', 'Saving ' + slug + '…', 'info');
|
|
2506
|
+
markLocalMutate();
|
|
2507
|
+
try {
|
|
2508
|
+
var r = await fetch(API + '/api/cli/providers/' + encodeURIComponent(slug) + '/lend', {
|
|
2509
|
+
method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ lend: lend })
|
|
2510
|
+
});
|
|
2511
|
+
var j = await r.json();
|
|
2512
|
+
if (!j.ok) throw new Error(j.error || 'failed');
|
|
2513
|
+
setStatus('cliStatus', slug + ' ' + (lend ? 'lend on' : 'lend off') + ' (empir3 routing is v2 — not active yet)', 'ok');
|
|
2514
|
+
await loadCliState();
|
|
2515
|
+
} catch (e) {
|
|
2516
|
+
setStatus('cliStatus', 'Failed: ' + e.message, 'err');
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
function shortPath(p) {
|
|
2521
|
+
if (!p) return '';
|
|
2522
|
+
var s = String(p);
|
|
2523
|
+
if (s.length <= 36) return s;
|
|
2524
|
+
return '…' + s.slice(-33);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
async function onCliToggleChange(cb) {
|
|
2528
|
+
var id = cb.dataset.cliToggle;
|
|
2529
|
+
var want = cb.checked;
|
|
2530
|
+
setStatus('cliStatus', 'Saving ' + id + '…', 'info');
|
|
2531
|
+
markLocalMutate();
|
|
2532
|
+
try {
|
|
2533
|
+
var patch;
|
|
2534
|
+
if (cb.dataset.settingsKey) {
|
|
2535
|
+
patch = {}; patch[cb.dataset.settingsKey] = want;
|
|
2536
|
+
await postJson('/api/settings/state', { bridge: patch });
|
|
2537
|
+
} else if (cb.dataset.handlerKey) {
|
|
2538
|
+
var h = {}; h[cb.dataset.handlerKey] = { enabled: want };
|
|
2539
|
+
await postJson('/api/settings/state', { bridge: { handlers: h } });
|
|
2540
|
+
}
|
|
2541
|
+
setStatus('cliStatus', id + ' ' + (want ? 'enabled.' : 'disabled.'), 'ok');
|
|
2542
|
+
await loadCliState();
|
|
2543
|
+
} catch (e) {
|
|
2544
|
+
setStatus('cliStatus', 'Failed: ' + e.message, 'err');
|
|
2545
|
+
await loadCliState();
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
async function onGhScopeChange(cb) {
|
|
2550
|
+
var scope = cb.dataset.ghScope;
|
|
2551
|
+
var want = cb.checked;
|
|
2552
|
+
setStatus('cliStatus', 'Saving GitHub ' + scope + ' scope…', 'info');
|
|
2553
|
+
markLocalMutate();
|
|
2554
|
+
try {
|
|
2555
|
+
var s = {}; s[scope] = want;
|
|
2556
|
+
await postJson('/api/settings/state', { bridge: { githubScopes: s } });
|
|
2557
|
+
setStatus('cliStatus', 'GitHub "' + scope + '" scope ' + (want ? 'enabled.' : 'disabled.'), 'ok');
|
|
2558
|
+
await loadCliState();
|
|
2559
|
+
} catch (e) {
|
|
2560
|
+
setStatus('cliStatus', 'Failed: ' + e.message, 'err');
|
|
2561
|
+
await loadCliState();
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
async function onCliAuthClick(id) {
|
|
2566
|
+
setStatus('cliStatus', 'Launching ' + id + ' auth flow…', 'info');
|
|
2567
|
+
try {
|
|
2568
|
+
var j = await postJson('/api/cli/auth', { provider: id });
|
|
2569
|
+
if (j && j.launched) {
|
|
2570
|
+
setStatus('cliStatus', id + ' auth launched in a new window — finish in your browser, then click Refresh.', 'ok');
|
|
2571
|
+
} else if (j && j.error) {
|
|
2572
|
+
setStatus('cliStatus', 'Auth failed: ' + j.error, 'err');
|
|
2573
|
+
}
|
|
2574
|
+
} catch (e) {
|
|
2575
|
+
setStatus('cliStatus', 'Auth call failed: ' + e.message, 'err');
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
async function onCliInstallClick(id) {
|
|
2580
|
+
setStatus('cliStatus', 'Launching ' + id + ' installer…', 'info');
|
|
2581
|
+
try {
|
|
2582
|
+
var j = await postJson('/api/cli/install', { provider: id });
|
|
2583
|
+
if (j && j.launched) {
|
|
2584
|
+
setStatus('cliStatus', id + ' installer opened in a new console — watch it finish, then click Re-scan.', 'ok');
|
|
2585
|
+
} else if (j && j.error) {
|
|
2586
|
+
setStatus('cliStatus', 'Install failed: ' + j.error, 'err');
|
|
2587
|
+
}
|
|
2588
|
+
} catch (e) {
|
|
2589
|
+
setStatus('cliStatus', 'Install call failed: ' + e.message, 'err');
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
async function loadCliState() {
|
|
2594
|
+
try {
|
|
2595
|
+
var s = await getJson('/api/settings/state');
|
|
2596
|
+
CLI_STATE = s;
|
|
2597
|
+
renderCliRows();
|
|
2598
|
+
// Refresh permissions table too — the family-gate banner there is
|
|
2599
|
+
// driven by CLI_STATE.bridge.handlers, so a tray/CLI-page toggle
|
|
2600
|
+
// needs to re-render the per-tool group below it.
|
|
2601
|
+
if (typeof renderTable === 'function') renderTable();
|
|
2602
|
+
// Identity inputs on the Daemon pane mirror bridge-settings.json
|
|
2603
|
+
if (typeof hydrateIdentity === 'function') hydrateIdentity();
|
|
2604
|
+
// Hydrate API-key field placeholders to reflect "set" vs "empty".
|
|
2605
|
+
var keysSet = (s.chat && s.chat.apiKeysSet) || {};
|
|
2606
|
+
var k = $('apiKeyAnthropic'); if (k) k.placeholder = keysSet.anthropic ? '•••••• (saved — leave blank to keep)' : 'sk-ant-…';
|
|
2607
|
+
var k2 = $('apiKeyOpenai'); if (k2) k2.placeholder = keysSet.openai ? '•••••• (saved — leave blank to keep)' : 'sk-…';
|
|
2608
|
+
var k3 = $('apiKeyGoogle'); if (k3) k3.placeholder = keysSet.google ? '•••••• (saved — leave blank to keep)' : 'AIza…';
|
|
2609
|
+
var k4 = $('apiKeyXai'); if (k4) k4.placeholder = keysSet.xai ? '•••••• (saved — leave blank to keep)' : 'xai-…';
|
|
2610
|
+
} catch (e) {
|
|
2611
|
+
setStatus('cliStatus', 'Could not load CLI state: ' + e.message, 'err');
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
var apiKeysSaveBtn = $('apiKeysSave');
|
|
2616
|
+
if (apiKeysSaveBtn) {
|
|
2617
|
+
apiKeysSaveBtn.addEventListener('click', async function(){
|
|
2618
|
+
var patch = { apiKeys: {} };
|
|
2619
|
+
var fields = [['apiKeyAnthropic','anthropic'],['apiKeyOpenai','openai'],['apiKeyGoogle','google'],['apiKeyXai','xai']];
|
|
2620
|
+
var any = false;
|
|
2621
|
+
for (var i = 0; i < fields.length; i++) {
|
|
2622
|
+
var el = $(fields[i][0]); if (!el) continue;
|
|
2623
|
+
var v = el.value.trim();
|
|
2624
|
+
if (v) { patch.apiKeys[fields[i][1]] = v; any = true; }
|
|
2625
|
+
}
|
|
2626
|
+
if (!any) { setStatus('apiKeysStatus', 'No new keys to save.', 'info'); return; }
|
|
2627
|
+
setStatus('apiKeysStatus', 'Saving keys…', 'info');
|
|
2628
|
+
markLocalMutate();
|
|
2629
|
+
try {
|
|
2630
|
+
await postJson('/api/settings/state', { chat: patch });
|
|
2631
|
+
// Clear the fields so a refresh doesn't reveal what was typed.
|
|
2632
|
+
fields.forEach(function(f){ var el = $(f[0]); if (el) el.value = ''; });
|
|
2633
|
+
setStatus('apiKeysStatus', 'Saved ' + Object.keys(patch.apiKeys).length + ' key(s).', 'ok');
|
|
2634
|
+
await loadCliState();
|
|
2635
|
+
} catch (e) {
|
|
2636
|
+
setStatus('apiKeysStatus', 'Failed: ' + e.message, 'err');
|
|
2637
|
+
}
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
var apiKeysRevealBtn = $('apiKeysReveal');
|
|
2641
|
+
if (apiKeysRevealBtn) {
|
|
2642
|
+
apiKeysRevealBtn.addEventListener('click', function(){
|
|
2643
|
+
var fields = ['apiKeyAnthropic','apiKeyOpenai','apiKeyGoogle','apiKeyXai'];
|
|
2644
|
+
var anyPassword = fields.some(function(id){ var el = $(id); return el && el.type === 'password'; });
|
|
2645
|
+
fields.forEach(function(id){ var el = $(id); if (el) el.type = anyPassword ? 'text' : 'password'; });
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// ────── Custom provider modal ──────
|
|
2650
|
+
function openProviderModal() {
|
|
2651
|
+
var m = $('providerModal'); if (!m) return;
|
|
2652
|
+
m.style.display = 'flex';
|
|
2653
|
+
setStatus('providerModalStatus', '', 'info');
|
|
2654
|
+
setTimeout(function(){ $('providerModalJson').focus(); }, 50);
|
|
2655
|
+
}
|
|
2656
|
+
function closeProviderModal() {
|
|
2657
|
+
var m = $('providerModal'); if (m) m.style.display = 'none';
|
|
2658
|
+
}
|
|
2659
|
+
var rescanBtn = $('rescanClisBtn');
|
|
2660
|
+
if (rescanBtn) rescanBtn.addEventListener('click', async function(){
|
|
2661
|
+
setStatus('cliStatus', 'Re-scanning installed CLIs…', 'info');
|
|
2662
|
+
// Force a fresh probe (bypass the server-side CLI-probe cache), then render.
|
|
2663
|
+
try { await getJson('/api/settings/state?fresh=1'); } catch (e) {}
|
|
2664
|
+
await loadCliState();
|
|
2665
|
+
setStatus('cliStatus', 'Re-scan complete.', 'ok');
|
|
2666
|
+
});
|
|
2667
|
+
var addBtn = $('addCustomProviderBtn');
|
|
2668
|
+
if (addBtn) addBtn.addEventListener('click', openProviderModal);
|
|
2669
|
+
var closeBtn = $('providerModalClose');
|
|
2670
|
+
if (closeBtn) closeBtn.addEventListener('click', closeProviderModal);
|
|
2671
|
+
var cancelBtn = $('providerModalCancel');
|
|
2672
|
+
if (cancelBtn) cancelBtn.addEventListener('click', closeProviderModal);
|
|
2673
|
+
var exampleBtn = $('providerModalExample');
|
|
2674
|
+
if (exampleBtn) exampleBtn.addEventListener('click', function(){
|
|
2675
|
+
$('providerModalJson').value = JSON.stringify({
|
|
2676
|
+
slug: 'ollama-local',
|
|
2677
|
+
name: 'Ollama (local)',
|
|
2678
|
+
apiBaseUrl: 'http://localhost:11434/v1'
|
|
2679
|
+
}, null, 2);
|
|
2680
|
+
});
|
|
2681
|
+
var saveBtn = $('providerModalSave');
|
|
2682
|
+
if (saveBtn) saveBtn.addEventListener('click', async function(){
|
|
2683
|
+
var raw = ($('providerModalJson').value || '').trim();
|
|
2684
|
+
if (!raw) { setStatus('providerModalStatus','Paste a JSON definition first.','err'); return; }
|
|
2685
|
+
var parsed;
|
|
2686
|
+
try { parsed = JSON.parse(raw); }
|
|
2687
|
+
catch (e) { setStatus('providerModalStatus','Invalid JSON: ' + e.message,'err'); return; }
|
|
2688
|
+
setStatus('providerModalStatus','Saving + probing endpoint…','info');
|
|
2689
|
+
markLocalMutate();
|
|
2690
|
+
try {
|
|
2691
|
+
var r = await fetch(API + '/api/cli/providers', {
|
|
2692
|
+
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(parsed)
|
|
2693
|
+
});
|
|
2694
|
+
var j = await r.json();
|
|
2695
|
+
if (!j.ok) throw new Error(j.error || ('http ' + r.status));
|
|
2696
|
+
var probed = j.provider || {};
|
|
2697
|
+
var msg = 'Added ' + (probed.name || parsed.name) + ' — ';
|
|
2698
|
+
if (probed.available) {
|
|
2699
|
+
msg += probed.authError ? 'reachable but auth error' : (probed.models?.length || 0) + ' models detected';
|
|
2700
|
+
} else {
|
|
2701
|
+
msg += 'not reachable (' + (probed.error || 'no response') + ')';
|
|
2702
|
+
}
|
|
2703
|
+
setStatus('providerModalStatus', msg, probed.available && !probed.authError ? 'ok' : 'err');
|
|
2704
|
+
await loadCliState();
|
|
2705
|
+
if (probed.available && !probed.authError) {
|
|
2706
|
+
setTimeout(closeProviderModal, 600);
|
|
2707
|
+
}
|
|
2708
|
+
} catch (e) {
|
|
2709
|
+
setStatus('providerModalStatus','Failed: ' + e.message,'err');
|
|
2710
|
+
}
|
|
2711
|
+
});
|
|
2712
|
+
|
|
2713
|
+
// ────── Boot ──────
|
|
2714
|
+
syncServerUi(${JSON.stringify(EMPIR3_SERVER)});
|
|
2715
|
+
loadPermissionState();
|
|
2716
|
+
loadCliState();
|
|
2717
|
+
refreshStatus();
|
|
2718
|
+
refreshActions();
|
|
2719
|
+
refreshFocus();
|
|
2720
|
+
refreshCalibration();
|
|
2721
|
+
refreshMonitors(false);
|
|
2722
|
+
refreshRecordings(false);
|
|
2723
|
+
refreshRecordingStatus();
|
|
2724
|
+
refreshSetupStatus();
|
|
2725
|
+
setInterval(refreshStatus, 5000);
|
|
2726
|
+
setInterval(refreshActions, 8000);
|
|
2727
|
+
setInterval(refreshFocus, 6000);
|
|
2728
|
+
setInterval(refreshRecordingStatus, 3000);
|
|
2729
|
+
setInterval(loadCliState, 15000);
|
|
2730
|
+
setInterval(refreshSettingsIfQuiet, 10000);
|
|
2731
|
+
</script>
|
|
2732
|
+
</body>
|
|
2733
|
+
</html>`;
|