@triflux/remote 10.31.0 → 10.33.0

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.
@@ -0,0 +1,594 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Triflux CTO Tray</title>
6
+ <style>
7
+ :root {
8
+ --bg-color: rgba(30, 30, 30, 0.68);
9
+ --border-color: rgba(255,255,255,0.15);
10
+ --text-main: #ffffff;
11
+ --text-muted: rgba(255,255,255,0.58);
12
+ --accent: #0A84FF;
13
+ --danger: #FF453A;
14
+ --success: #32D74B;
15
+ --warning: #FFD60A;
16
+ --hover-bg: rgba(255,255,255,0.1);
17
+ }
18
+ * { box-sizing: border-box; }
19
+ body { -webkit-font-smoothing: antialiased; margin: 0; background: transparent; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; color: var(--text-main); }
20
+ ::-webkit-scrollbar { width: 6px; }
21
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.18); border-radius: 10px; }
22
+ .mac-popover { width: 100%; height: 100vh; background: var(--bg-color); backdrop-filter: blur(22px) saturate(155%); -webkit-backdrop-filter: blur(22px) saturate(155%); border: 1px solid var(--border-color); border-radius: 14px; overflow: hidden; box-shadow: 0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.05) inset; display: flex; flex-direction: column; }
23
+ .mac-header { padding: 15px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid rgba(255,255,255,0.1); }
24
+ .title-area { display: flex; align-items: center; gap: 10px; min-width: 0; }
25
+ .mac-logo { width: 24px; height: 24px; background: linear-gradient(135deg, #0A84FF, #5E5CE6); border-radius: 6px; display: grid; place-items: center; font-weight: 700; font-size: 12px; flex: 0 0 auto; }
26
+ .mac-title { font-size: 14px; font-weight: 600; letter-spacing: -0.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
27
+ .mac-status { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); }
28
+ .status-dot, .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: 0 0 auto; }
29
+ .status-dot.on, .dot.on { background: var(--success); box-shadow: 0 0 8px var(--success); }
30
+ .status-dot.off, .dot.off { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
31
+ .dot.claude { background: #d19a66; box-shadow: 0 0 5px #d19a66; }
32
+ .dot.codex { background: #ffffff; box-shadow: 0 0 5px rgba(255,255,255,0.5); }
33
+ .dot.antigravity { background: #61afef; box-shadow: 0 0 5px #61afef; }
34
+ .mac-tabs { display: flex; padding: 8px; gap: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); }
35
+ .mac-tab { flex: 1; background: transparent; border: none; color: var(--text-muted); font-size: 12px; font-weight: 600; padding: 6px 8px; cursor: pointer; border-radius: 7px; }
36
+ .mac-tab.active { background: rgba(255,255,255,0.13); color: var(--text-main); }
37
+ .mac-content-area { overflow-y: auto; flex: 1; }
38
+ .tab-pane { display: none; }
39
+ .tab-pane.active { display: block; }
40
+ .section { padding: 12px 16px; border-bottom: 1px solid rgba(255,255,255,0.05); }
41
+ .section-title { font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
42
+ .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
43
+ .stat-box { background: rgba(0,0,0,0.22); border-radius: 8px; padding: 10px; border: 1px solid rgba(255,255,255,0.06); min-width: 0; }
44
+ .stat-label { font-size: 10px; color: var(--text-muted); margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
45
+ .stat-value { font-size: 18px; font-weight: 650; font-variant-numeric: tabular-nums; display: flex; align-items: baseline; gap: 4px; }
46
+ .stat-unit { font-size: 10px; font-weight: 400; color: var(--text-muted); }
47
+ .models-list, .server-list, .session-list { display: flex; flex-direction: column; gap: 8px; }
48
+ .model-row { display: grid; grid-template-columns: minmax(76px, auto) 1fr auto; gap: 10px; align-items: center; font-size: 12px; min-width: 0; }
49
+ .model-name { display: flex; align-items: center; gap: 8px; min-width: 0; }
50
+ .model-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
51
+ .model-icon { width: 16px; height: 16px; border-radius: 4px; background: rgba(255,255,255,0.1); display: grid; place-items: center; font-size: 10px; font-weight: 700; flex: 0 0 auto; }
52
+ .progress-bar-bg { height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; min-width: 30px; }
53
+ .progress-bar-fill { height: 100%; border-radius: 2px; }
54
+ .value-pill { font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; }
55
+ .mini-card, .mcp-card { background: rgba(0,0,0,0.18); border: 1px solid rgba(255,255,255,0.06); border-radius: 9px; padding: 9px 10px; min-width: 0; }
56
+ .mini-head, .mcp-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; min-width: 0; }
57
+ .mini-title, .mcp-name { font-size: 12px; font-weight: 650; line-height: 1.25; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58
+ .path-text { font-size: 10px; color: var(--text-muted); margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
59
+ .focus-btn { background: transparent; border: 1px solid rgba(10,132,255,0.45); color: #64b5ff; font-size: 10px; line-height: 1; padding: 4px 7px; cursor: pointer; border-radius: 6px; flex: 0 0 auto; }
60
+ .focus-btn:hover { background: var(--accent); color: #fff; }
61
+ .agent-view { display: flex; flex-direction: column; gap: 8px; }
62
+ .project-group { min-width: 0; border: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.13); border-radius: 9px; overflow: hidden; }
63
+ .project-group > summary, .cto-chat > summary, .bubble-wrap > summary { list-style: none; }
64
+ .project-group > summary::-webkit-details-marker, .cto-chat > summary::-webkit-details-marker, .bubble-wrap > summary::-webkit-details-marker { display: none; }
65
+ .project-summary { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; gap: 7px; align-items: center; padding: 8px 9px; cursor: pointer; }
66
+ .project-chevron { color: rgba(255,255,255,0.44); font-size: 11px; transform: rotate(-90deg); transition: transform 120ms ease; }
67
+ .project-group[open] .project-chevron { transform: rotate(0deg); }
68
+ .project-name { color: #b6b1e8; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 13px; font-weight: 750; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
69
+ .project-count { color: var(--text-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
70
+ .project-path { color: var(--text-muted); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 10px; padding: 0 9px 8px 32px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
71
+ .project-body { padding: 0 8px 9px; display: flex; flex-direction: column; gap: 7px; }
72
+ .cto-chat { border: 1px solid rgba(10,132,255,0.13); background: rgba(255,255,255,0.035); border-radius: 8px; overflow: hidden; }
73
+ .cto-summary { display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; gap: 7px; align-items: center; padding: 7px 8px; cursor: pointer; }
74
+ .cto-summary-title { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
75
+ .cto-summary-sub { color: var(--text-muted); font-size: 10px; margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
76
+ .chat-panel { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; padding: 8px; border-top: 1px solid rgba(255,255,255,0.05); }
77
+ .chat-column { min-width: 0; display: flex; flex-direction: column; gap: 7px; }
78
+ .chat-column-title { color: var(--text-muted); font-size: 9px; font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase; }
79
+ .chat-empty { color: var(--text-muted); border: 1px dashed rgba(255,255,255,0.10); border-radius: 8px; padding: 8px; font-size: 10px; }
80
+ .runtime-list { display: flex; flex-direction: column; gap: 7px; }
81
+ .runtime-card { display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; gap: 7px; align-items: center; padding: 7px; border-radius: 8px; background: rgba(255,255,255,0.045); border: 1px solid rgba(255,255,255,0.07); }
82
+ .runtime-main { min-width: 0; }
83
+ .runtime-title { display: flex; align-items: center; gap: 6px; min-width: 0; }
84
+ .runtime-command { color: rgba(255,255,255,0.90); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; font-weight: 750; }
85
+ .runtime-sub { color: var(--text-muted); font-size: 10px; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
86
+ .runtime-meta { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 5px; }
87
+ .chat-session-title { display: flex; align-items: center; justify-content: space-between; gap: 6px; min-width: 0; margin-bottom: 5px; }
88
+ .chat-session-id { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 10px; color: rgba(255,255,255,0.78); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
89
+ .bubble-card { min-width: 0; }
90
+ .bubble-wrap { min-width: 0; }
91
+ .bubble-preview { display: block; cursor: pointer; border-radius: 12px; padding: 8px 9px; font-size: 11px; line-height: 1.35; color: rgba(255,255,255,0.88); background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.08); max-height: 48px; overflow: hidden; }
92
+ .worker-column .bubble-preview { border-top-left-radius: 4px; }
93
+ .cto-column .bubble-preview { background: rgba(10,132,255,0.14); border-color: rgba(10,132,255,0.18); border-top-right-radius: 4px; }
94
+ .bubble-full { margin: 6px 0 0; white-space: pre-wrap; word-break: break-word; max-height: 190px; overflow: auto; padding: 8px; border-radius: 8px; background: rgba(0,0,0,0.28); color: rgba(255,255,255,0.82); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 10px; line-height: 1.35; }
95
+ .agent-line { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 5px; align-items: start; min-width: 0; padding: 3px 0; }
96
+ .agent-line.worker { padding-left: 10px; }
97
+ .agent-marker { color: rgba(255,255,255,0.46); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 13px; line-height: 20px; text-align: center; user-select: none; }
98
+ .agent-body { min-width: 0; border-radius: 7px; padding: 4px 5px; }
99
+ .agent-body:hover { background: rgba(255,255,255,0.05); }
100
+ .agent-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; align-items: center; min-width: 0; }
101
+ .agent-title { color: rgba(255,255,255,0.90); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 12px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
102
+ .agent-sub { color: var(--text-muted); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 10px; line-height: 1.35; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
103
+ .agent-meta { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 5px; }
104
+ .agent-state { color: var(--text-muted); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; white-space: nowrap; }
105
+ .agent-state.active, .agent-state.on { color: var(--success); }
106
+ .agent-state.idle { color: var(--warning); }
107
+ .agent-state.stopped, .agent-state.stale, .agent-state.error { color: var(--danger); }
108
+ .badge-row, .client-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
109
+ .chip { display: inline-flex; align-items: center; gap: 5px; min-height: 20px; border-radius: 999px; padding: 3px 7px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.07); font-size: 10px; color: rgba(255,255,255,0.76); }
110
+ .copy-chip { cursor: pointer; }
111
+ .copy-chip:hover { border-color: rgba(10,132,255,0.42); color: #8ec9ff; }
112
+ .chip.on { color: var(--success); border-color: rgba(50,215,75,0.25); background: rgba(50,215,75,0.08); }
113
+ .chip.partial { color: var(--warning); border-color: rgba(255,214,10,0.25); background: rgba(255,214,10,0.08); }
114
+ .chip.off { color: var(--danger); border-color: rgba(255,69,58,0.25); background: rgba(255,69,58,0.08); }
115
+ .agent-badge { width: 20px; height: 20px; display: inline-grid; place-items: center; flex: 0 0 auto; border-radius: 6px; font-size: 11px; font-weight: 850; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; border: 1px solid rgba(255,255,255,0.12); }
116
+ .agent-tag { justify-content: center; }
117
+ .agent-tag.claude { color: #d19a66; background: rgba(209,154,102,0.12); border-color: rgba(209,154,102,0.32); }
118
+ .agent-tag.codex { color: #ffffff; background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.32); }
119
+ .agent-tag.antigravity { color: #61afef; background: rgba(97,175,239,0.12); border-color: rgba(97,175,239,0.32); }
120
+ .muted { color: var(--text-muted); font-size: 11px; }
121
+ .status-text.on { color: var(--success); }
122
+ .status-text.partial { color: var(--warning); }
123
+ .status-text.off, .status-text.error { color: var(--danger); }
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <div class="mac-popover">
128
+ <div class="mac-header">
129
+ <div class="title-area"><div class="mac-logo">T</div><div class="mac-title">Triflux CTO</div></div>
130
+ <div class="mac-status"><span class="status-dot on"></span><span>Connected</span></div>
131
+ </div>
132
+ <div class="mac-tabs">
133
+ <button class="mac-tab active" onclick="switchTab('sessions', this)">Sessions</button>
134
+ <button class="mac-tab" onclick="switchTab('gateway', this)">Gateway MCP</button>
135
+ </div>
136
+ <div class="mac-content-area">
137
+ <div id="tab-sessions" class="tab-pane active"><div class="section muted">Loading sessions…</div></div>
138
+ <div id="tab-gateway" class="tab-pane"><div class="section muted">Loading MCP…</div></div>
139
+ </div>
140
+ </div>
141
+ <script>
142
+ const CLIENT_LABEL = { claude: 'Claude', codex: 'Codex', antigravity: 'Antigravity' };
143
+ const STATUS_LABEL = { on: 'ON', partial: 'PARTIAL', off: 'OFF' };
144
+
145
+ function switchTab(id, btn) {
146
+ document.querySelectorAll('.tab-pane').forEach(e => e.classList.remove('active'));
147
+ document.querySelectorAll('.mac-tab').forEach(e => e.classList.remove('active'));
148
+ document.getElementById('tab-' + id).classList.add('active');
149
+ btn.classList.add('active');
150
+ }
151
+
152
+ async function fetchState() {
153
+ try {
154
+ const res = await fetch('/api/tray-state');
155
+ const data = await res.json();
156
+ renderSessions(data);
157
+ renderGateway(data.mcp);
158
+ document.querySelector('.mac-status').innerHTML = '<span class="status-dot on"></span><span>Connected</span>';
159
+ } catch (e) {
160
+ console.error(e);
161
+ document.querySelector('.mac-status').innerHTML = '<span class="status-dot off"></span><span>Disconnected</span>';
162
+ document.getElementById('tab-sessions').innerHTML = '<div class="section muted">Error loading sessions…</div>';
163
+ }
164
+ }
165
+
166
+ function escapeHtml(value) {
167
+ return String(value ?? '').replace(/[&<>"']/g, ch => ({
168
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
169
+ }[ch]));
170
+ }
171
+ function shortId(value) { return String(value || '').substring(0, 8); }
172
+ function pct(count, max) { return Math.max(4, Math.min(100, max ? Math.round((count / max) * 100) : 4)); }
173
+ function dataAttr(value) { return escapeHtml(String(value ?? '')); }
174
+ function renderAgentTag(agent) {
175
+ if (!agent?.id) return '';
176
+ const initial = (agent.icon || { claude: 'C', codex: 'X', antigravity: 'A' }[agent.id] || agent.id.slice(0, 1)).toUpperCase();
177
+ return `<span class="agent-badge agent-tag ${escapeHtml(agent.id)}" title="${escapeHtml(agent.label || agent.id)}" aria-label="${escapeHtml(agent.label || agent.id)}">${escapeHtml(initial)}</span>`;
178
+ }
179
+ function renderCopyChip(label, value) {
180
+ if (!value) return '';
181
+ return `<span class="chip copy-chip" title="Copy ${escapeHtml(label)}" data-copy-text="${dataAttr(value)}">${escapeHtml(label)} ${escapeHtml(shortId(value))}</span>`;
182
+ }
183
+
184
+ function collectOpenDetailKeys(container) {
185
+ try {
186
+ return new Set([...container.querySelectorAll('details[data-open-key][open]')].map(detail => detail.dataset.openKey).filter(Boolean));
187
+ } catch (_err) {
188
+ return new Set();
189
+ }
190
+ }
191
+
192
+ function restoreOpenDetails(container, openKeys) {
193
+ if (!openKeys || openKeys.size === 0) return;
194
+ try {
195
+ for (const detail of container.querySelectorAll('details[data-open-key]')) {
196
+ if (openKeys.has(detail.dataset.openKey)) detail.open = true;
197
+ }
198
+ } catch (_err) {
199
+ // Test and legacy WebView fallback: missing querySelectorAll should not break rendering.
200
+ }
201
+ }
202
+
203
+ function renderRuntimeClients(runtime) {
204
+ const clients = Array.isArray(runtime?.summary?.clients) ? runtime.summary.clients : [];
205
+ const max = Math.max(1, ...clients.map(client => client.count || 0));
206
+ return clients.map(client => `
207
+ <div class="model-row">
208
+ <div class="model-name"><div class="model-icon" style="color:${escapeHtml(client.color)}">${escapeHtml(client.icon)}</div><span class="model-label">${escapeHtml(client.label)}</span></div>
209
+ <div class="progress-bar-bg"><div class="progress-bar-fill" style="width:${pct(client.count || 0, max)}%; background:${escapeHtml(client.color)}"></div></div>
210
+ <div class="value-pill status-text ${escapeHtml(client.status)}">${escapeHtml(client.count || 0)}</div>
211
+ </div>
212
+ `).join('');
213
+ }
214
+
215
+ function liveFallbackRows(live) {
216
+ return live.map(session => ({
217
+ sessionId: session.sessionId,
218
+ status: session.phase || 'active',
219
+ agent_id: session.agent_id || session.agent?.id || '',
220
+ agent: session.agent || null,
221
+ promptText: session.promptText || '',
222
+ prompt: session.prompt || '',
223
+ userPrompt: session.userPrompt || '',
224
+ handoffPrompt: session.handoffPrompt || '',
225
+ command: session.command || '',
226
+ source: session.source || '',
227
+ conversationId: session.conversationId || '',
228
+ focusable: session.focusable !== false,
229
+ taskSummary: session.taskSummary || session.command || (session.pid ? `PID ${session.pid}` : ''),
230
+ pid: session.pid || '',
231
+ cwd: session.cwd || session.host || 'local runtime',
232
+ elapsed: session.elapsed || '',
233
+ started_at: session.started_at || null,
234
+ }));
235
+ }
236
+
237
+ function compactText(value, fallback = '') {
238
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
239
+ const safe = text || fallback;
240
+ return safe.length > 84 ? safe.slice(0, 81) + '…' : safe;
241
+ }
242
+
243
+ function displayPath(value) {
244
+ const path = String(value || '').trim();
245
+ if (!path) return '';
246
+ return path.replace(/^\/Users\/[^/]+/, '~');
247
+ }
248
+
249
+ function projectName(value) {
250
+ const path = String(value || '').replace(/\/+$/u, '').trim();
251
+ if (!path) return 'local';
252
+ return path.split('/').filter(Boolean).pop() || path;
253
+ }
254
+
255
+ function projectPathFor(row, hub, fallback = '') {
256
+ const cwd = String(row?.cwd || row?.worktreePath || '').trim();
257
+ if (cwd && cwd !== 'local' && cwd !== 'local runtime') return cwd;
258
+ const root = String(hub?.projectRoot || fallback || '').trim();
259
+ return root || cwd || 'local';
260
+ }
261
+
262
+ function getProjectGroup(groups, projectPath) {
263
+ const key = projectPath || 'local';
264
+ if (!groups.has(key)) groups.set(key, { projectPath: key, ctos: [], workers: [] });
265
+ return groups.get(key);
266
+ }
267
+
268
+ function buildProjectGroups(activeSessions, liveRows, hub) {
269
+ const groups = new Map();
270
+ const fallbackProject = hub.projectRoot || activeSessions[0]?.cwd || activeSessions[0]?.worktreePath || 'local';
271
+ for (const session of activeSessions) {
272
+ getProjectGroup(groups, projectPathFor(session, hub, fallbackProject)).ctos.push(session);
273
+ }
274
+
275
+ const ctoIds = new Set(activeSessions.map(session => session.sessionId).filter(Boolean));
276
+ for (const worker of liveRows) {
277
+ if (ctoIds.has(worker.sessionId)) continue;
278
+ getProjectGroup(groups, projectPathFor(worker, hub, fallbackProject)).workers.push(worker);
279
+ }
280
+
281
+ if (groups.size === 0 && fallbackProject) getProjectGroup(groups, fallbackProject);
282
+ for (const group of groups.values()) {
283
+ if (group.ctos.length === 0 && group.workers.length > 0) {
284
+ group.ctos.push({
285
+ sessionId: `cto-${shortId(hub.id || group.projectPath)}`,
286
+ status: 'active',
287
+ cwd: group.projectPath,
288
+ taskSummary: 'CTO runtime overlay',
289
+ synthetic: true,
290
+ });
291
+ }
292
+ }
293
+ return [...groups.values()].sort((a, b) => a.projectPath.localeCompare(b.projectPath));
294
+ }
295
+
296
+ function explicitPromptText(row) {
297
+ return String(row?.promptText || row?.prompt || row?.userPrompt || row?.handoffPrompt || '').trim();
298
+ }
299
+
300
+ function numericPid(value) {
301
+ const text = String(value ?? '').trim();
302
+ if (!text) return '';
303
+ const pid = Number(text);
304
+ return Number.isFinite(pid) && pid > 0 ? pid : '';
305
+ }
306
+
307
+ function isRuntimeRow(row) {
308
+ return Boolean(numericPid(row?.pid)) || Boolean(row?.command);
309
+ }
310
+
311
+ function promptCandidateText(row) {
312
+ const explicit = explicitPromptText(row);
313
+ if (explicit) return explicit;
314
+ if (isRuntimeRow(row)) return '';
315
+ return String(row?.taskSummary || '').trim();
316
+ }
317
+
318
+ function equivalentText(left, right) {
319
+ return String(left || '').replace(/\s+/g, ' ').trim() === String(right || '').replace(/\s+/g, ' ').trim();
320
+ }
321
+
322
+ function bubbleText(row, fallback) {
323
+ return promptCandidateText(row) || String(row?.command || row?.sessionKind || row?.sessionId || fallback || '').trim() || fallback;
324
+ }
325
+
326
+ function isSyntheticCto(row) {
327
+ return row?.synthetic === true || row?.sessionId?.startsWith('cto-') && row?.taskSummary === 'CTO runtime overlay';
328
+ }
329
+
330
+ function looksLikeRawCommand(text) {
331
+ const value = String(text || '').trim();
332
+ return /\/node_modules\/|\/bin\/|--dangerously-|model_instructions_file=|(?:^|\s)--(?:secret|token|api-key|password|pass|key)(?:[=\s]|$)|\bAuthorization\s*:|\bBearer\s+\S+|^(?:node|npm|pnpm|yarn|python(?:3)?|bash|zsh|sh|npx|bun|curl|wget)(?:\s|$)|^claude(?:\s+agents)?(?:\s|$)|^codex(?:\s|$)|^antigravity(?:\s|$)|^(?:\/|~\/|\.\/|\.\.\/|[A-Za-z]:[\\/])\S+/u.test(value);
333
+ }
334
+
335
+ function hasPromptText(row) {
336
+ const text = promptCandidateText(row);
337
+ const command = String(row?.command || '').trim();
338
+ return Boolean(text) && !isSyntheticCto(row) && !looksLikeRawCommand(text) && (!command || !equivalentText(text, command));
339
+ }
340
+
341
+ function commandLabel(row) {
342
+ const text = String(row?.command || row?.taskSummary || '').trim();
343
+ const agentId = row?.agent_id || row?.agent?.id || '';
344
+ if (/^claude\s+agents\b/u.test(text)) return 'claude agents';
345
+ if (/^claude\b/u.test(text)) return 'claude';
346
+ if (/codex(?:\s|$)|\/codex(?:\s|$)/u.test(text) || agentId === 'codex') return 'codex';
347
+ if (/antigravity|\bagy\b/u.test(text) || agentId === 'antigravity') return 'antigravity';
348
+ return agentId || 'runtime';
349
+ }
350
+
351
+ function renderChatBubble(row, { side = 'worker' } = {}) {
352
+ const sid = row.sessionId || '';
353
+ const pid = numericPid(row.pid);
354
+ const agentId = row.agent_id || row.agent?.id || '';
355
+ const text = bubbleText(row, side === 'cto' ? 'CTO prompt unavailable' : 'Worker prompt unavailable');
356
+ const focusDisabled = row.synthetic || !sid;
357
+ const badge = side === 'cto'
358
+ ? '<span class="agent-badge">T</span>'
359
+ : `<span class="agent-marker">└</span>${renderAgentTag(row.agent)}`;
360
+ return `
361
+ <div class="bubble-card">
362
+ <div class="chat-session-title">
363
+ <span style="display:flex;align-items:center;gap:6px;min-width:0;">${badge}<span class="chat-session-id">${escapeHtml(sid || side)}</span></span>
364
+ ${focusDisabled ? '' : `<button class="focus-btn" data-focus-session="1" data-session-id="${dataAttr(sid)}" data-pid="${dataAttr(pid)}" data-agent-id="${dataAttr(agentId)}">Focus</button>`}
365
+ </div>
366
+ <details class="bubble-wrap" data-open-key="bubble:${escapeHtml(side)}:${dataAttr(sid || text.slice(0, 24))}">
367
+ <summary class="bubble-preview">${escapeHtml(compactText(text, 'prompt'))}</summary>
368
+ <pre class="bubble-full">${escapeHtml(text)}</pre>
369
+ </details>
370
+ </div>
371
+ `;
372
+ }
373
+
374
+ function renderRuntimeCard(row) {
375
+ const sid = row.sessionId || '';
376
+ const pid = numericPid(row.pid);
377
+ const agentId = row.agent_id || row.agent?.id || '';
378
+ const status = row.status || row.phase || 'active';
379
+ const focusDisabled = row.focusable === false || !sid || !pid;
380
+ return `
381
+ <div class="runtime-card">
382
+ ${renderAgentTag(row.agent)}
383
+ <div class="runtime-main">
384
+ <div class="runtime-title"><span class="runtime-command">${escapeHtml(commandLabel(row))}</span><span class="agent-state ${escapeHtml(status)}">${escapeHtml(status)}</span></div>
385
+ <div class="runtime-sub">${escapeHtml(displayPath(row.cwd || row.host || 'local runtime'))}${row.elapsed ? ` · ${escapeHtml(row.elapsed)}` : ''}</div>
386
+ <div class="runtime-meta">${pid ? `<span class="chip">PID ${escapeHtml(pid)}</span>` : ''}${renderCopyChip('ID', sid)}</div>
387
+ </div>
388
+ ${focusDisabled ? '' : `<button class="focus-btn" data-focus-session="1" data-session-id="${dataAttr(sid)}" data-pid="${dataAttr(pid)}" data-agent-id="${dataAttr(agentId)}">Focus</button>`}
389
+ </div>
390
+ `;
391
+ }
392
+
393
+ function renderCtoChat(ctoRow, workers) {
394
+ const status = ctoRow.status || ctoRow.phase || 'active';
395
+ const title = compactText(ctoRow.sessionId || ctoRow.taskSummary || 'CTO', 'CTO');
396
+ const subtitle = compactText(hasPromptText(ctoRow) ? ctoRow.taskSummary || ctoRow.sessionKind || '' : '', '');
397
+ return `
398
+ <details class="cto-chat" data-open-key="cto:${dataAttr(ctoRow.sessionId || title)}">
399
+ <summary class="cto-summary">
400
+ <div class="agent-marker">•</div>
401
+ <div style="min-width:0;">
402
+ <div class="cto-summary-title">${escapeHtml(title)}</div>
403
+ ${subtitle ? `<div class="cto-summary-sub">${escapeHtml(subtitle)}</div>` : ''}
404
+ </div>
405
+ <div class="agent-state ${escapeHtml(status)}">${escapeHtml(status)}</div>
406
+ </summary>
407
+ <div class="chat-panel">
408
+ <div class="chat-column worker-column">
409
+ <div class="chat-column-title">Workers</div>
410
+ ${workers.length ? `<div class="runtime-list">${workers.map(worker => hasPromptText(worker) ? renderChatBubble(worker, { side: 'worker' }) : renderRuntimeCard(worker)).join('')}</div>` : '<div class="chat-empty">No worker sessions detected</div>'}
411
+ </div>
412
+ <div class="chat-column cto-column">
413
+ <div class="chat-column-title">CTO</div>
414
+ ${hasPromptText(ctoRow) ? renderChatBubble(ctoRow, { side: 'cto' }) : '<div class="chat-empty">No prompt handoff events captured yet</div>'}
415
+ </div>
416
+ </div>
417
+ </details>
418
+ `;
419
+ }
420
+
421
+ function renderProjectGroup(group) {
422
+ const projectDisplayPath = displayPath(group.projectPath);
423
+ const projectLabel = projectName(group.projectPath);
424
+ const totalChildren = group.ctos.length + group.workers.length;
425
+ let html = `
426
+ <details class="project-group" data-open-key="project:${dataAttr(group.projectPath)}">
427
+ <summary class="project-summary">
428
+ <span class="project-chevron">▾</span>
429
+ <span class="project-name">${escapeHtml(projectLabel)}</span>
430
+ <span class="project-count">${escapeHtml(String(totalChildren))}</span>
431
+ </summary>
432
+ <div class="project-path">${escapeHtml(projectDisplayPath)}</div>
433
+ <div class="project-body">
434
+ `;
435
+ const ctos = group.ctos.slice(0, 6);
436
+ const workers = group.workers.slice(0, 10);
437
+ for (const [index, ctoRow] of ctos.entries()) {
438
+ html += renderCtoChat(ctoRow, index === 0 ? workers : []);
439
+ }
440
+ html += '</div></details>';
441
+ return html;
442
+ }
443
+
444
+ function renderSessions(data) {
445
+ const container = document.getElementById('tab-sessions');
446
+ const openKeys = collectOpenDetailKeys(container);
447
+ const hub = data?.hub || {};
448
+ const sessions = Array.isArray(data?.sessions) ? data.sessions : [];
449
+ const cto = data?.cto || {};
450
+ const live = Array.isArray(cto.live_sessions) ? cto.live_sessions : [];
451
+ const runtime = data?.runtime || {};
452
+ const activeSessions = sessions.filter(s => s.status !== 'stale');
453
+ const liveRows = liveFallbackRows(live);
454
+ const projectGroups = buildProjectGroups(activeSessions.slice(0, 10), liveRows, hub);
455
+ const staleCount = sessions.filter(s => s.status === 'stale').length;
456
+ let html = `
457
+ <div class="section">
458
+ <div class="stats-grid">
459
+ <div class="stat-box"><div class="stat-label">Live Sessions</div><div class="stat-value">${live.length}<span class="stat-unit">nodes</span></div></div>
460
+ <div class="stat-box"><div class="stat-label">Active Shards</div><div class="stat-value">${cto.active_shards?.length || 0}<span class="stat-unit">shards</span></div></div>
461
+ <div class="stat-box"><div class="stat-label">Runtime CLIs</div><div class="stat-value">${runtime?.summary?.total || 0}<span class="stat-unit">proc</span></div></div>
462
+ <div class="stat-box"><div class="stat-label">Hub ID</div><div class="badge-row" style="margin-top:0;">${renderCopyChip('Hub', hub.id || String(hub.pid || ''))}</div></div>
463
+ </div>
464
+ ${staleCount ? `<div class="path-text">${staleCount} stale sessions hidden from active cards</div>` : ''}
465
+ </div>
466
+ <div class="section">
467
+ <div class="section-title">Runtime Clients</div>
468
+ <div class="models-list">${renderRuntimeClients(runtime)}</div>
469
+ </div>
470
+ <div class="section">
471
+ <div class="section-title">Projects / CTO / Workers</div>
472
+ <div class="agent-view">
473
+ `;
474
+ for (const group of projectGroups) html += renderProjectGroup(group);
475
+ if (projectGroups.length === 0) html += '<div class="mini-card muted">No active sessions detected</div>';
476
+ html += '</div></div>';
477
+ container.innerHTML = html;
478
+ restoreOpenDetails(container, openKeys);
479
+ }
480
+
481
+ function renderGateway(mcp) {
482
+ const container = document.getElementById('tab-gateway');
483
+ const servers = Array.isArray(mcp?.servers) ? mcp.servers : [];
484
+ const byStatus = mcp?.summary?.by_status || {};
485
+ let html = `
486
+ <div class="section">
487
+ <div class="stats-grid">
488
+ <div class="stat-box"><div class="stat-label">MCP Servers</div><div class="stat-value">${servers.length || mcp?.server_count || 0}<span class="stat-unit">cards</span></div></div>
489
+ <div class="stat-box"><div class="stat-label">Registry Rows</div><div class="stat-value">${mcp?.summary?.total || 0}<span class="stat-unit">rows</span></div></div>
490
+ <div class="stat-box"><div class="stat-label">Present</div><div class="stat-value">${byStatus.present || 0}<span class="stat-unit">ok</span></div></div>
491
+ <div class="stat-box"><div class="stat-label">Registry</div><div class="stat-value">${mcp?.server_count || 0}<span class="stat-unit">defs</span></div></div>
492
+ </div>
493
+ <div class="path-text">${escapeHtml(mcp?.registry_path || '')}</div>
494
+ </div>
495
+ <div class="section">
496
+ <div class="section-title">MCP Cards</div>
497
+ <div class="server-list">
498
+ `;
499
+ for (const server of servers) {
500
+ html += `
501
+ <div class="mcp-card">
502
+ <div class="mcp-head">
503
+ <div class="mcp-name">${escapeHtml(server.name)}</div>
504
+ <div class="value-pill status-text ${escapeHtml(server.status)}">${escapeHtml(STATUS_LABEL[server.status] || server.status)}</div>
505
+ </div>
506
+ <div class="path-text">${escapeHtml(server.target || '')}</div>
507
+ <div class="client-chips">
508
+ ${server.clients.map(client => `
509
+ <span class="chip ${escapeHtml(client.status)}"><span class="dot ${escapeHtml(client.id)}"></span>${escapeHtml(client.label)} ${escapeHtml(STATUS_LABEL[client.status] || client.status)}${client.total > 1 ? ` · ${client.present}/${client.total}` : ''}</span>
510
+ `).join('')}
511
+ </div>
512
+ </div>
513
+ `;
514
+ }
515
+ if (servers.length === 0) html += '<div class="mcp-card muted">No MCP registry cards reported</div>';
516
+ html += `</div>${mcp?.error ? `<div class="path-text status-text error">${escapeHtml(mcp.error)}</div>` : ''}</div>`;
517
+ container.innerHTML = html;
518
+ }
519
+
520
+ async function copyText(txt, element) {
521
+ if (!txt) return;
522
+ try {
523
+ await navigator.clipboard.writeText(txt);
524
+ } catch (_err) {
525
+ const input = document.createElement('textarea');
526
+ input.value = txt;
527
+ input.style.position = 'fixed';
528
+ input.style.opacity = '0';
529
+ document.body.appendChild(input);
530
+ input.focus();
531
+ input.select();
532
+ document.execCommand('copy');
533
+ document.body.removeChild(input);
534
+ }
535
+ if (element) {
536
+ const previous = element.textContent;
537
+ element.textContent = 'Copied';
538
+ setTimeout(() => { element.textContent = previous; }, 700);
539
+ }
540
+ }
541
+ function postNativeTrayMessage(message) {
542
+ try {
543
+ window?.webkit?.messageHandlers?.tray?.postMessage(message);
544
+ } catch (_err) {
545
+ // Browser fallback: the web tray can run without a native wrapper.
546
+ }
547
+ }
548
+
549
+ async function focusSession(sid, pid, agentId, button) {
550
+ if (!sid) return;
551
+ const previous = button?.textContent || 'Focus';
552
+ if (button) {
553
+ button.disabled = true;
554
+ button.textContent = 'Focusing…';
555
+ }
556
+ try {
557
+ const res = await fetch('/api/focus-session', {
558
+ method: 'POST',
559
+ headers: { 'Content-Type': 'application/json' },
560
+ body: JSON.stringify({ sessionId: sid, pid: pid || undefined, agentId: agentId || undefined })
561
+ });
562
+ const body = await res.json().catch(() => ({}));
563
+ if (!res.ok || body?.ok === false || body?.focus?.ok === false) throw new Error(body?.error || body?.focus?.error || 'focus failed');
564
+ if (button) button.textContent = 'Focused';
565
+ postNativeTrayMessage({ type: 'focus-complete' });
566
+ } catch (err) {
567
+ console.error(err);
568
+ if (button) button.textContent = 'Failed';
569
+ } finally {
570
+ setTimeout(() => {
571
+ if (button) {
572
+ button.textContent = previous;
573
+ button.disabled = false;
574
+ }
575
+ }, 650);
576
+ }
577
+ }
578
+ document.addEventListener('click', event => {
579
+ const copyTarget = event.target.closest('[data-copy-text]');
580
+ if (copyTarget) {
581
+ copyText(copyTarget.dataset.copyText, copyTarget);
582
+ return;
583
+ }
584
+ const focusTarget = event.target.closest('[data-focus-session]');
585
+ if (focusTarget) {
586
+ focusSession(focusTarget.dataset.sessionId, Number(focusTarget.dataset.pid || 0), focusTarget.dataset.agentId || '', focusTarget);
587
+ }
588
+ });
589
+
590
+ setInterval(fetchState, 3000);
591
+ fetchState();
592
+ </script>
593
+ </body>
594
+ </html>