@symerian/symi 3.5.0 → 3.5.2
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/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +4 -4
- package/dist/bundled/session-memory/handler.js +4 -4
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{chrome-C_I81hbq.js → chrome-B7-rO4i9.js} +4 -4
- package/dist/{chrome-BKUACyeO.js → chrome-DPjznJQ-.js} +4 -4
- package/dist/control-ui/css/revert-red-theme.md +141 -0
- package/dist/control-ui/css/style.css +5843 -0
- package/dist/control-ui/css/style.css.backup-2026-03-03-162525 +3546 -0
- package/dist/control-ui/css/style.css.backup-before-red-2026-03-03-162525 +3546 -0
- package/dist/control-ui/css/style.css.backup-before-red-theme-2026-03-03-162530 +3546 -0
- package/dist/control-ui/css/style.css.pre-2row +2165 -0
- package/dist/control-ui/css/style.css.pre-brand +1776 -0
- package/dist/control-ui/css/style.css.pre-history +1974 -0
- package/dist/control-ui/css/style.css.pre-nav +2264 -0
- package/dist/control-ui/css/style.css.pre-newsession +1898 -0
- package/dist/control-ui/css/style.css.pre-queue +2195 -0
- package/dist/control-ui/css/style.css.pre-red-prompt +2524 -0
- package/dist/control-ui/css/style.css.pre-stop +2239 -0
- package/dist/control-ui/css/style.css.pre-textarea +2184 -0
- package/dist/control-ui/css/style.css.pre-watchdog +1848 -0
- package/dist/control-ui/css/style.css.red-theme +2999 -0
- package/dist/control-ui/index.html +1049 -0
- package/dist/control-ui/js/app.js +1304 -0
- package/dist/control-ui/js/app.js.pre-2row +463 -0
- package/dist/control-ui/js/app.js.pre-heartbeat-filter +595 -0
- package/dist/control-ui/js/app.js.pre-newsession +408 -0
- package/dist/control-ui/js/app.js.pre-queue +476 -0
- package/dist/control-ui/js/app.js.pre-stop +564 -0
- package/dist/control-ui/js/app.js.pre-textarea +467 -0
- package/dist/control-ui/js/app.js.pre-watchdog +293 -0
- package/dist/control-ui/js/connections.js +438 -0
- package/dist/control-ui/js/gateway.js +233 -0
- package/dist/control-ui/js/gateway.js.pre-stop +110 -0
- package/dist/control-ui/js/history.js +732 -0
- package/dist/control-ui/js/logs.js +238 -0
- package/dist/control-ui/js/menu.js +232 -0
- package/dist/control-ui/js/menu.js.pre-nav +66 -0
- package/dist/control-ui/js/metrics.js +53 -0
- package/dist/control-ui/js/models.js +138 -0
- package/dist/control-ui/js/render.js +882 -0
- package/dist/control-ui/js/render.test.js +112 -0
- package/dist/control-ui/js/scheduling.js +461 -0
- package/dist/control-ui/js/settings.js +910 -0
- package/dist/control-ui/js/slash-autocomplete.js +168 -0
- package/dist/control-ui/js/subagents.js +560 -0
- package/dist/control-ui/js/utils.js +29 -0
- package/dist/control-ui/vendor/highlight.min.js +2518 -0
- package/dist/control-ui/vendor/marked.min.js +69 -0
- package/dist/{deliver-DyO3QD8O.js → deliver-DTRkeYm3.js} +4 -4
- package/dist/{deliver-Cjyb6h4g.js → deliver-oWGJwzFf.js} +4 -4
- package/dist/extensionAPI.js +4 -4
- package/dist/llm-slug-generator.js +4 -4
- package/dist/{manager-rvtFoeFT.js → manager-CFenq_aO.js} +1 -1
- package/dist/{manager-PTSjHNVq.js → manager-CsxTf96V.js} +1 -1
- package/dist/{pi-embedded-BPuUM-gD.js → pi-embedded-Cdub5Vs9.js} +10 -10
- package/dist/{pw-ai-BFS9ezWe.js → pw-ai-BOOB8qoi.js} +1 -1
- package/dist/{pw-ai-Cx-Ko_FL.js → pw-ai-D2pEVS5n.js} +1 -1
- package/dist/{synthesis-7UL3pCpj.js → synthesis-Be9nYyDd.js} +4 -4
- package/dist/{synthesis-fD8J2vag.js → synthesis-CBIT6Vnk.js} +4 -4
- package/dist/{unified-runner-BIiKFnNF.js → unified-runner-BVvvnjXW.js} +10 -10
- package/package.json +3 -3
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// ── Connections Panel ───────────────────────────────────────────────────────
|
|
2
|
+
// Manages connection status display for Slack, Email, and Teams channels.
|
|
3
|
+
// Queries gateway for channel health and displays status with notifications.
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
(function () {
|
|
9
|
+
const CHANNELS = {
|
|
10
|
+
slack: {
|
|
11
|
+
id: "slack",
|
|
12
|
+
name: "Slack",
|
|
13
|
+
configPath: "channels.slack",
|
|
14
|
+
icon: `<svg viewBox="0 0 24 24" fill="currentColor">
|
|
15
|
+
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.124 2.521a2.528 2.528 0 0 1 2.52-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.52V8.834zm-1.271 0a2.528 2.528 0 0 1-2.521 2.521 2.528 2.528 0 0 1-2.521-2.521V2.522A2.528 2.528 0 0 1 15.166 0a2.528 2.528 0 0 1 2.521 2.522v6.312zm-2.521 10.124a2.528 2.528 0 0 1 2.521 2.52A2.528 2.528 0 0 1 15.166 24a2.528 2.528 0 0 1-2.521-2.522v-2.52h2.521zm0-1.271a2.528 2.528 0 0 1-2.521-2.521 2.528 2.528 0 0 1 2.521-2.521h6.312A2.528 2.528 0 0 1 24 15.166a2.528 2.528 0 0 1-2.522 2.521h-6.312z"/>
|
|
16
|
+
</svg>`,
|
|
17
|
+
setupInfo: "Configure Slack integration with Bot and App tokens for real-time messaging.",
|
|
18
|
+
configExample: `channels:
|
|
19
|
+
slack:
|
|
20
|
+
botToken: "xoxb-..."
|
|
21
|
+
appToken: "xapp-..."`,
|
|
22
|
+
},
|
|
23
|
+
email: {
|
|
24
|
+
id: "email",
|
|
25
|
+
name: "Email",
|
|
26
|
+
configPath: "hooks.gmail + outlook plugin",
|
|
27
|
+
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
28
|
+
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
|
29
|
+
<path d="M22 6l-10 7L2 6"/>
|
|
30
|
+
</svg>`,
|
|
31
|
+
// setupInfo/configExample unused for email — the modal renders two
|
|
32
|
+
// per-provider sections (Outlook, Gmail) instead. See openModal().
|
|
33
|
+
setupInfo: "",
|
|
34
|
+
configExample: "",
|
|
35
|
+
},
|
|
36
|
+
msteams: {
|
|
37
|
+
id: "msteams",
|
|
38
|
+
name: "Microsoft Teams",
|
|
39
|
+
configPath: "channels.msteams",
|
|
40
|
+
icon: `<svg viewBox="0 0 24 24" fill="currentColor">
|
|
41
|
+
<path d="M19.19 8.77c-.73 0-1.39.29-1.88.76V8.5a2.5 2.5 0 0 0-2.5-2.5h-1.62c.18-.46.28-.96.28-1.48a4.04 4.04 0 0 0-4.04-4.04c-2.23 0-4.04 1.81-4.04 4.04 0 .52.1 1.02.28 1.48H4.5A2.5 2.5 0 0 0 2 8.5v9a2.5 2.5 0 0 0 2.5 2.5h9a2.5 2.5 0 0 0 2.5-2.5v-.97c.49.47 1.15.76 1.88.76a2.69 2.69 0 0 0 2.69-2.69v-3.14a2.69 2.69 0 0 0-2.69-2.69h.31zM9.43 2.98a2.04 2.04 0 1 1 0 4.08 2.04 2.04 0 0 1 0-4.08zm4.57 14.52a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 .5.5v9zm5.88-4.17a.69.69 0 0 1-.69.69.69.69 0 0 1-.69-.69v-3.14c0-.38.31-.69.69-.69.38 0 .69.31.69.69v3.14z"/>
|
|
42
|
+
<circle cx="17.5" cy="5.5" r="2.5"/>
|
|
43
|
+
</svg>`,
|
|
44
|
+
setupInfo: "Connect Microsoft Teams using Bot Framework for bidirectional messaging.",
|
|
45
|
+
configExample: `channels:
|
|
46
|
+
msteams:
|
|
47
|
+
appId: "..."
|
|
48
|
+
appPassword: "..."
|
|
49
|
+
tenantId: "..."`,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const state = {
|
|
54
|
+
slack: { status: "checking", notifications: 0 },
|
|
55
|
+
email: { status: "checking", notifications: 0, gmail: null, outlook: null },
|
|
56
|
+
msteams: { status: "checking", notifications: 0 },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ── DOM Elements ─────────────────────────────────────────────────────────
|
|
60
|
+
const elements = {
|
|
61
|
+
slack: {
|
|
62
|
+
btn: document.getElementById("conn-slack"),
|
|
63
|
+
status: document.getElementById("conn-slack-status"),
|
|
64
|
+
state: document.getElementById("conn-slack-state"),
|
|
65
|
+
notif: document.getElementById("conn-slack-notif"),
|
|
66
|
+
},
|
|
67
|
+
email: {
|
|
68
|
+
btn: document.getElementById("conn-email"),
|
|
69
|
+
status: document.getElementById("conn-email-status"),
|
|
70
|
+
state: document.getElementById("conn-email-state"),
|
|
71
|
+
notif: document.getElementById("conn-email-notif"),
|
|
72
|
+
},
|
|
73
|
+
msteams: {
|
|
74
|
+
btn: document.getElementById("conn-teams"),
|
|
75
|
+
status: document.getElementById("conn-teams-status"),
|
|
76
|
+
state: document.getElementById("conn-teams-state"),
|
|
77
|
+
notif: document.getElementById("conn-teams-notif"),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ── Update UI ────────────────────────────────────────────────────────────
|
|
82
|
+
function updateChannelUI(channelId) {
|
|
83
|
+
const el = elements[channelId];
|
|
84
|
+
const s = state[channelId];
|
|
85
|
+
if (!el || !el.btn) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set data-state attribute for CSS styling
|
|
90
|
+
el.btn.setAttribute("data-state", s.status);
|
|
91
|
+
|
|
92
|
+
// Update status text
|
|
93
|
+
const statusText = {
|
|
94
|
+
checking: "Checking...",
|
|
95
|
+
connected: "Connected",
|
|
96
|
+
disconnected: "Not configured",
|
|
97
|
+
error: "Connection error",
|
|
98
|
+
pending: "Connecting...",
|
|
99
|
+
};
|
|
100
|
+
el.status.textContent = statusText[s.status] || s.status;
|
|
101
|
+
|
|
102
|
+
// Update notification badge
|
|
103
|
+
if (s.notifications > 0) {
|
|
104
|
+
el.notif.textContent = s.notifications > 99 ? "99+" : s.notifications;
|
|
105
|
+
} else {
|
|
106
|
+
el.notif.textContent = "";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Check Channel Status ─────────────────────────────────────────────────
|
|
111
|
+
async function checkChannelStatus() {
|
|
112
|
+
if (!window.gateway?.connected) {
|
|
113
|
+
// Gateway not connected, mark all as pending
|
|
114
|
+
Object.keys(state).forEach((id) => {
|
|
115
|
+
state[id].status = "pending";
|
|
116
|
+
updateChannelUI(id);
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Query gateway for channel status using channels.status RPC
|
|
123
|
+
const result = await window.gateway.rpc("channels.status", { probe: true });
|
|
124
|
+
const channels = result?.channels ?? {};
|
|
125
|
+
|
|
126
|
+
// Update Slack status
|
|
127
|
+
if (channels.slack) {
|
|
128
|
+
const isConnected = channels.slack.configured && channels.slack.connected !== false;
|
|
129
|
+
state.slack.status = isConnected ? "connected" : "disconnected";
|
|
130
|
+
state.slack.notifications = channels.slack.unread ?? 0;
|
|
131
|
+
} else {
|
|
132
|
+
state.slack.status = "disconnected";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Update Email status: connected if EITHER Outlook OR Gmail is live.
|
|
136
|
+
// Outlook is injected into channels.outlook by the gateway
|
|
137
|
+
// (src/gateway/server-methods/channels.ts).
|
|
138
|
+
const gmail = channels.gmail || channels.email || null;
|
|
139
|
+
const outlook = channels.outlook || null;
|
|
140
|
+
const gmailConnected = !!(gmail && gmail.configured && gmail.connected !== false);
|
|
141
|
+
const outlookConnected = !!(outlook && outlook.configured && outlook.connected !== false);
|
|
142
|
+
state.email.gmail = gmail;
|
|
143
|
+
state.email.outlook = outlook;
|
|
144
|
+
state.email.status = gmailConnected || outlookConnected ? "connected" : "disconnected";
|
|
145
|
+
state.email.notifications = (gmail?.unread ?? 0) + (outlook?.unread ?? 0);
|
|
146
|
+
|
|
147
|
+
// Update Teams status
|
|
148
|
+
if (channels.msteams) {
|
|
149
|
+
const isConnected = channels.msteams.configured && channels.msteams.connected !== false;
|
|
150
|
+
state.msteams.status = isConnected ? "connected" : "disconnected";
|
|
151
|
+
state.msteams.notifications = channels.msteams.unread ?? 0;
|
|
152
|
+
} else {
|
|
153
|
+
state.msteams.status = "disconnected";
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn("[connections] Failed to fetch channel status:", err);
|
|
157
|
+
// Mark all as disconnected on error
|
|
158
|
+
Object.keys(state).forEach((id) => {
|
|
159
|
+
state[id].status = "disconnected";
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Update all UI
|
|
164
|
+
Object.keys(state).forEach(updateChannelUI);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Modal Elements ────────────────────────────────────────────────────────
|
|
168
|
+
const modalOverlay = document.getElementById("conn-modal-overlay");
|
|
169
|
+
const modalIcon = document.getElementById("conn-modal-icon");
|
|
170
|
+
const modalTitle = document.getElementById("conn-modal-title");
|
|
171
|
+
const modalBody = document.getElementById("conn-modal-body");
|
|
172
|
+
const modalDocs = document.getElementById("conn-modal-docs");
|
|
173
|
+
const modalClose = document.getElementById("conn-modal-close");
|
|
174
|
+
|
|
175
|
+
// Docs viewer (in-app scrollable markdown renderer, stacks above conn modal)
|
|
176
|
+
const docsViewerOverlay = document.getElementById("docs-viewer-overlay");
|
|
177
|
+
const docsViewerBody = document.getElementById("docs-viewer-body");
|
|
178
|
+
const docsViewerTitle = document.getElementById("docs-viewer-title");
|
|
179
|
+
const docsViewerClose = document.getElementById("docs-viewer-close");
|
|
180
|
+
|
|
181
|
+
// Tracks which channel's connection modal is currently open, so the
|
|
182
|
+
// "View Documentation" button knows which docs key to fetch.
|
|
183
|
+
let currentModalChannelId = null;
|
|
184
|
+
|
|
185
|
+
const DOCS_VIEWER_TITLES = {
|
|
186
|
+
email: "Email — Outlook & Gmail",
|
|
187
|
+
slack: "Slack Setup",
|
|
188
|
+
msteams: "Microsoft Teams Setup",
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function stripFrontmatter(markdown) {
|
|
192
|
+
if (!markdown.startsWith("---\n") && !markdown.startsWith("---\r\n")) {
|
|
193
|
+
return markdown;
|
|
194
|
+
}
|
|
195
|
+
const end = markdown.indexOf("\n---", 4);
|
|
196
|
+
if (end === -1) {
|
|
197
|
+
return markdown;
|
|
198
|
+
}
|
|
199
|
+
return markdown.slice(end + 4).replace(/^\s+/, "");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function openDocsViewer(key) {
|
|
203
|
+
if (!docsViewerOverlay || !docsViewerBody) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
docsViewerTitle.textContent = DOCS_VIEWER_TITLES[key] || "Documentation";
|
|
207
|
+
docsViewerBody.innerHTML = '<p class="docs-viewer-loading">Loading documentation…</p>';
|
|
208
|
+
docsViewerOverlay.classList.add("open");
|
|
209
|
+
docsViewerOverlay.setAttribute("aria-hidden", "false");
|
|
210
|
+
// Capture phase so we beat the conn-modal's Esc handler
|
|
211
|
+
document.addEventListener("keydown", onDocsViewerKey, true);
|
|
212
|
+
|
|
213
|
+
if (!window.gateway?.rpc) {
|
|
214
|
+
docsViewerBody.innerHTML =
|
|
215
|
+
'<p class="docs-viewer-error">Gateway is not connected — cannot load documentation.</p>';
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const result = await window.gateway.rpc("docs.get", { key });
|
|
220
|
+
const markdown = result && typeof result.markdown === "string" ? result.markdown : "";
|
|
221
|
+
if (!markdown) {
|
|
222
|
+
docsViewerBody.innerHTML =
|
|
223
|
+
'<p class="docs-viewer-error">Documentation is empty or missing on this install.</p>';
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const body = stripFrontmatter(markdown);
|
|
227
|
+
const html =
|
|
228
|
+
window.marked && typeof window.marked.parse === "function"
|
|
229
|
+
? window.marked.parse(body)
|
|
230
|
+
: "<pre>" + escHtml(body) + "</pre>";
|
|
231
|
+
docsViewerBody.innerHTML = html;
|
|
232
|
+
// Make external links open in a new tab with safe rel
|
|
233
|
+
docsViewerBody.querySelectorAll('a[href^="http"]').forEach((a) => {
|
|
234
|
+
a.setAttribute("target", "_blank");
|
|
235
|
+
a.setAttribute("rel", "noopener noreferrer");
|
|
236
|
+
});
|
|
237
|
+
docsViewerBody.scrollTop = 0;
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const msg = err && err.message ? err.message : String(err);
|
|
240
|
+
docsViewerBody.innerHTML =
|
|
241
|
+
'<p class="docs-viewer-error">Failed to load documentation: ' + escHtml(msg) + "</p>";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function closeDocsViewer() {
|
|
246
|
+
if (!docsViewerOverlay) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
docsViewerOverlay.classList.remove("open");
|
|
250
|
+
docsViewerOverlay.setAttribute("aria-hidden", "true");
|
|
251
|
+
document.removeEventListener("keydown", onDocsViewerKey, true);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function onDocsViewerKey(e) {
|
|
255
|
+
if (e.key === "Escape") {
|
|
256
|
+
// Stop the conn-modal's keydown listener from also firing
|
|
257
|
+
e.stopImmediatePropagation();
|
|
258
|
+
closeDocsViewer();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function renderProviderSection(title, connected, detailLine, actionLine) {
|
|
263
|
+
const klass = connected ? "connected" : "disconnected";
|
|
264
|
+
const badge = connected ? "CONNECTED" : "NOT CONFIGURED";
|
|
265
|
+
return `
|
|
266
|
+
<div class="conn-modal-status ${klass}" style="margin-bottom:6px;">
|
|
267
|
+
<div class="conn-modal-status-dot"></div>
|
|
268
|
+
<span class="conn-modal-status-text"><strong>${escHtml(title)}</strong> — ${escHtml(detailLine)}</span>
|
|
269
|
+
<span class="conn-modal-status-badge">${badge}</span>
|
|
270
|
+
</div>
|
|
271
|
+
<p class="conn-modal-info" style="margin-top:0;margin-bottom:14px;">${actionLine}</p>
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function renderEmailModalBody(channelState) {
|
|
276
|
+
const outlook = channelState.outlook;
|
|
277
|
+
const gmail = channelState.gmail;
|
|
278
|
+
const outlookConnected = !!(outlook && outlook.configured && outlook.connected !== false);
|
|
279
|
+
const gmailConnected = !!(gmail && gmail.configured && gmail.connected !== false);
|
|
280
|
+
|
|
281
|
+
const outlookDetail = outlookConnected
|
|
282
|
+
? `Connected as ${outlook.email || "Microsoft account"}`
|
|
283
|
+
: "Microsoft 365 / outlook.com / hotmail.com";
|
|
284
|
+
const outlookAction = outlookConnected
|
|
285
|
+
? `Type <code>/outlook logout</code> in chat to disconnect.`
|
|
286
|
+
: `Type <code>/outlook login</code> in chat — a Microsoft device-code flow will start and tools become available immediately after sign-in.`;
|
|
287
|
+
|
|
288
|
+
const gmailAccount = gmail && typeof gmail.account === "string" ? gmail.account : null;
|
|
289
|
+
const gmailDetail = gmailConnected
|
|
290
|
+
? `Watching ${gmailAccount || "your Gmail inbox"}`
|
|
291
|
+
: "Gmail / Google Workspace (requires Pub/Sub setup)";
|
|
292
|
+
const gmailAction = gmailConnected
|
|
293
|
+
? `Incoming email creates <code>hook:gmail:<id></code> sessions automatically.`
|
|
294
|
+
: `Configure <code>hooks.gmail</code> in <code>symi.json</code> — see the docs link below for the full Google Cloud Pub/Sub walkthrough.`;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
renderProviderSection("Outlook 365", outlookConnected, outlookDetail, outlookAction) +
|
|
298
|
+
renderProviderSection("Gmail", gmailConnected, gmailDetail, gmailAction)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function openModal(channelId) {
|
|
303
|
+
const channel = CHANNELS[channelId];
|
|
304
|
+
const channelState = state[channelId];
|
|
305
|
+
if (!channel) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Set icon
|
|
310
|
+
modalIcon.className = "conn-modal-icon " + channelId;
|
|
311
|
+
modalIcon.innerHTML = channel.icon;
|
|
312
|
+
|
|
313
|
+
// Set title
|
|
314
|
+
modalTitle.textContent = channel.name;
|
|
315
|
+
|
|
316
|
+
// Remember which channel this modal is showing so the
|
|
317
|
+
// "View Documentation" button knows which docs key to fetch.
|
|
318
|
+
currentModalChannelId = channelId;
|
|
319
|
+
|
|
320
|
+
if (channelId === "email") {
|
|
321
|
+
modalBody.innerHTML = renderEmailModalBody(channelState);
|
|
322
|
+
} else {
|
|
323
|
+
// Generic single-provider layout for other channels
|
|
324
|
+
const isConnected = channelState.status === "connected";
|
|
325
|
+
const statusClass = isConnected ? "connected" : "disconnected";
|
|
326
|
+
const statusText = isConnected
|
|
327
|
+
? "Channel is active and receiving messages"
|
|
328
|
+
: "Channel is not configured";
|
|
329
|
+
const badgeText = isConnected ? "CONNECTED" : "NOT CONFIGURED";
|
|
330
|
+
|
|
331
|
+
modalBody.innerHTML = `
|
|
332
|
+
<div class="conn-modal-status ${statusClass}">
|
|
333
|
+
<div class="conn-modal-status-dot"></div>
|
|
334
|
+
<span class="conn-modal-status-text">${statusText}</span>
|
|
335
|
+
<span class="conn-modal-status-badge">${badgeText}</span>
|
|
336
|
+
</div>
|
|
337
|
+
<p class="conn-modal-info">${channel.setupInfo}</p>
|
|
338
|
+
<p class="conn-modal-info">Add to your <code>symi.json</code> config:</p>
|
|
339
|
+
<pre class="conn-modal-config">${escHtml(channel.configExample)}</pre>
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Show modal
|
|
344
|
+
modalOverlay.classList.add("open");
|
|
345
|
+
modalOverlay.setAttribute("aria-hidden", "false");
|
|
346
|
+
document.addEventListener("keydown", onModalKey);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function closeModal() {
|
|
350
|
+
modalOverlay.classList.remove("open");
|
|
351
|
+
modalOverlay.setAttribute("aria-hidden", "true");
|
|
352
|
+
document.removeEventListener("keydown", onModalKey);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function onModalKey(e) {
|
|
356
|
+
if (e.key === "Escape") {
|
|
357
|
+
closeModal();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Modal event listeners
|
|
362
|
+
if (modalClose) {
|
|
363
|
+
modalClose.addEventListener("click", closeModal);
|
|
364
|
+
}
|
|
365
|
+
if (modalOverlay) {
|
|
366
|
+
modalOverlay.addEventListener("click", (e) => {
|
|
367
|
+
if (e.target === modalOverlay) {
|
|
368
|
+
closeModal();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// "View Documentation" button → open in-app docs viewer.
|
|
374
|
+
// The docs-viewer overlay stacks on top of the conn-modal.
|
|
375
|
+
if (modalDocs) {
|
|
376
|
+
modalDocs.addEventListener("click", (e) => {
|
|
377
|
+
e.preventDefault();
|
|
378
|
+
if (currentModalChannelId) {
|
|
379
|
+
void openDocsViewer(currentModalChannelId);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Docs viewer event listeners
|
|
385
|
+
if (docsViewerClose) {
|
|
386
|
+
docsViewerClose.addEventListener("click", closeDocsViewer);
|
|
387
|
+
}
|
|
388
|
+
if (docsViewerOverlay) {
|
|
389
|
+
docsViewerOverlay.addEventListener("click", (e) => {
|
|
390
|
+
if (e.target === docsViewerOverlay) {
|
|
391
|
+
closeDocsViewer();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Handle Click ─────────────────────────────────────────────────────────
|
|
397
|
+
function handleChannelClick(channelId) {
|
|
398
|
+
openModal(channelId);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Initialize ───────────────────────────────────────────────────────────
|
|
402
|
+
function init() {
|
|
403
|
+
// Bind click handlers
|
|
404
|
+
Object.keys(elements).forEach((channelId) => {
|
|
405
|
+
const el = elements[channelId];
|
|
406
|
+
if (el?.btn) {
|
|
407
|
+
el.btn.addEventListener("click", () => handleChannelClick(channelId));
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Initial status check
|
|
412
|
+
void checkChannelStatus();
|
|
413
|
+
|
|
414
|
+
// Poll for status updates every 30 seconds
|
|
415
|
+
setInterval(checkChannelStatus, 30000);
|
|
416
|
+
|
|
417
|
+
// Also check when gateway connects
|
|
418
|
+
window.addEventListener("gateway:connected", checkChannelStatus);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Wait for DOM and gateway
|
|
422
|
+
if (document.readyState === "loading") {
|
|
423
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
424
|
+
} else {
|
|
425
|
+
init();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Expose for external use
|
|
429
|
+
window.connectionsPanel = {
|
|
430
|
+
refresh: checkChannelStatus,
|
|
431
|
+
setNotifications: (channelId, count) => {
|
|
432
|
+
if (state[channelId]) {
|
|
433
|
+
state[channelId].notifications = count;
|
|
434
|
+
updateChannelUI(channelId);
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
})();
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// ── Symi Glass UI Gateway Client ──────────────────────────────────────
|
|
2
|
+
// Connects to the embedded /glass-ws WebSocket bridge inside the gateway.
|
|
3
|
+
// The bridge handles auth and protocol translation server-side, keeping
|
|
4
|
+
// this client simple.
|
|
5
|
+
|
|
6
|
+
const SESSION_KEY = "agent:main:main";
|
|
7
|
+
|
|
8
|
+
// ── Utility ───────────────────────────────────────────────────────────
|
|
9
|
+
function extractText(message) {
|
|
10
|
+
if (!message) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
if (typeof message === "string") {
|
|
14
|
+
return message;
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(message)) {
|
|
17
|
+
return message
|
|
18
|
+
.filter((b) => b?.type === "text")
|
|
19
|
+
.map((b) => b.text ?? "")
|
|
20
|
+
.join("");
|
|
21
|
+
}
|
|
22
|
+
if (message?.content) {
|
|
23
|
+
return extractText(message.content);
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Gateway Client ────────────────────────────────────────────────────
|
|
29
|
+
class SymiGateway extends EventTarget {
|
|
30
|
+
constructor() {
|
|
31
|
+
super();
|
|
32
|
+
this.ws = null;
|
|
33
|
+
this.connected = false;
|
|
34
|
+
this.backoff = 800;
|
|
35
|
+
this._closed = false;
|
|
36
|
+
this._pending = new Map();
|
|
37
|
+
this._rpcSeq = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
start() {
|
|
41
|
+
this._closed = false;
|
|
42
|
+
this._connect();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
stop() {
|
|
46
|
+
this._closed = true;
|
|
47
|
+
this.ws?.close();
|
|
48
|
+
this.ws = null;
|
|
49
|
+
this.connected = false;
|
|
50
|
+
this._flushPending(new Error("gateway client stopped"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_connect() {
|
|
54
|
+
if (this._closed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
59
|
+
const wsUrl = `${protocol}//${location.host}/glass-ws`;
|
|
60
|
+
this.ws = new WebSocket(wsUrl);
|
|
61
|
+
|
|
62
|
+
this.ws.addEventListener("open", () => {
|
|
63
|
+
this.backoff = 800;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.ws.addEventListener("message", (e) => {
|
|
67
|
+
let msg;
|
|
68
|
+
try {
|
|
69
|
+
msg = JSON.parse(e.data);
|
|
70
|
+
} catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this._handleMessage(msg);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.ws.addEventListener("close", () => {
|
|
77
|
+
this.ws = null;
|
|
78
|
+
const wasConnected = this.connected;
|
|
79
|
+
this.connected = false;
|
|
80
|
+
this._flushPending(new Error("gateway closed"));
|
|
81
|
+
if (wasConnected) {
|
|
82
|
+
this.dispatchEvent(new CustomEvent("disconnect"));
|
|
83
|
+
}
|
|
84
|
+
this._scheduleReconnect();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.ws.addEventListener("error", () => {});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_scheduleReconnect() {
|
|
91
|
+
if (this._closed) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const delay = this.backoff;
|
|
95
|
+
this.backoff = Math.min(this.backoff * 1.7, 15000);
|
|
96
|
+
setTimeout(() => this._connect(), delay);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_flushPending(err) {
|
|
100
|
+
for (const [, p] of this._pending) {
|
|
101
|
+
p.reject(err);
|
|
102
|
+
}
|
|
103
|
+
this._pending.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_handleMessage(msg) {
|
|
107
|
+
const type = msg.type;
|
|
108
|
+
|
|
109
|
+
if (type === "status") {
|
|
110
|
+
const wasConnected = this.connected;
|
|
111
|
+
this.connected = msg.connected;
|
|
112
|
+
if (msg.connected && !wasConnected) {
|
|
113
|
+
this.dispatchEvent(new CustomEvent("connect"));
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (type === "history") {
|
|
119
|
+
this.dispatchEvent(
|
|
120
|
+
new CustomEvent("history", {
|
|
121
|
+
detail: {
|
|
122
|
+
messages: msg.messages ?? [],
|
|
123
|
+
total: msg.total ?? 0,
|
|
124
|
+
hasMore: msg.hasMore ?? false,
|
|
125
|
+
startIndex: msg.startIndex ?? 0,
|
|
126
|
+
error: msg.error ?? null,
|
|
127
|
+
recoverable: msg.recoverable === true,
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (type === "chat") {
|
|
135
|
+
this.dispatchEvent(
|
|
136
|
+
new CustomEvent("event", {
|
|
137
|
+
detail: { event: "chat", payload: msg.payload },
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (type === "agent") {
|
|
144
|
+
this.dispatchEvent(
|
|
145
|
+
new CustomEvent("event", {
|
|
146
|
+
detail: { event: "agent", payload: msg.payload },
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (type === "subagent") {
|
|
153
|
+
this.dispatchEvent(
|
|
154
|
+
new CustomEvent("event", {
|
|
155
|
+
detail: { event: "subagent", payload: msg.payload },
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (type === "profile") {
|
|
162
|
+
// Model profile broadcast — apply to Glass UI immediately
|
|
163
|
+
if (msg.payload?.profile) {
|
|
164
|
+
window.activeModelProfile = msg.payload.profile;
|
|
165
|
+
window.dispatchEvent(
|
|
166
|
+
new CustomEvent("symi:profile-changed", { detail: msg.payload.profile }),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (type === "rpc.response") {
|
|
173
|
+
const pending = this._pending.get(msg.id);
|
|
174
|
+
if (pending) {
|
|
175
|
+
this._pending.delete(msg.id);
|
|
176
|
+
if (msg.ok) {
|
|
177
|
+
pending.resolve(msg.payload);
|
|
178
|
+
} else {
|
|
179
|
+
pending.reject(new Error(msg.error?.message ?? "rpc failed"));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (type === "error") {
|
|
186
|
+
console.warn("[gateway] Server error:", msg.message);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Chat Methods ────────────────────────────────────────────────────
|
|
192
|
+
send(message) {
|
|
193
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
194
|
+
return Promise.reject(new Error("gateway not connected"));
|
|
195
|
+
}
|
|
196
|
+
this.ws.send(JSON.stringify({ type: "chat.send", message }));
|
|
197
|
+
return Promise.resolve();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
abort(runId) {
|
|
201
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
202
|
+
return Promise.reject(new Error("gateway not connected"));
|
|
203
|
+
}
|
|
204
|
+
const params = { type: "chat.abort" };
|
|
205
|
+
if (runId) {
|
|
206
|
+
params.runId = runId;
|
|
207
|
+
}
|
|
208
|
+
this.ws.send(JSON.stringify(params));
|
|
209
|
+
return Promise.resolve();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── RPC Helper ──────────────────────────────────────────────────────
|
|
213
|
+
rpc(method, params = {}) {
|
|
214
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
215
|
+
return Promise.reject(new Error("gateway not connected"));
|
|
216
|
+
}
|
|
217
|
+
const id = String(++this._rpcSeq);
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
this._pending.set(id, { resolve, reject });
|
|
220
|
+
this.ws.send(JSON.stringify({ type: "rpc", id, method, params }));
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
if (this._pending.has(id)) {
|
|
223
|
+
this._pending.delete(id);
|
|
224
|
+
reject(new Error("RPC timeout: " + method));
|
|
225
|
+
}
|
|
226
|
+
}, 30000);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
window.SymiGateway = SymiGateway;
|
|
232
|
+
window.SESSION_KEY = SESSION_KEY;
|
|
233
|
+
window.extractText = extractText;
|