openclacky 1.3.6 → 1.3.7
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/clacky/agent.rb +8 -1
- data/lib/clacky/agent_profile.rb +121 -53
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/ext.yml +18 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
- data/lib/clacky/default_extensions/general/ext.yml +18 -0
- data/lib/clacky/default_extensions/git/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
- data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
- data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
- data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
- data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
- data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
- data/lib/clacky/extension/api_loader.rb +136 -0
- data/lib/clacky/extension/cli_commands.rb +226 -0
- data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
- data/lib/clacky/extension/hook_loader.rb +77 -0
- data/lib/clacky/extension/loader.rb +483 -0
- data/lib/clacky/extension/packager.rb +226 -0
- data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
- data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
- data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
- data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
- data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
- data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
- data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
- data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
- data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
- data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
- data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
- data/lib/clacky/extension/scaffold.rb +55 -0
- data/lib/clacky/extension/verifier.rb +196 -0
- data/lib/clacky/identity.rb +70 -0
- data/lib/clacky/locales/en.rb +19 -1
- data/lib/clacky/locales/zh.rb +19 -1
- data/lib/clacky/platform_http_client.rb +18 -15
- data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
- data/lib/clacky/server/channel.rb +6 -0
- data/lib/clacky/server/http_server.rb +313 -144
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +748 -573
- data/lib/clacky/web/app.js +63 -15
- data/lib/clacky/web/auth.js +2 -0
- data/lib/clacky/web/components/notify.js +2 -0
- data/lib/clacky/web/components/sidebar.js +1 -5
- data/lib/clacky/web/core/ext.js +316 -79
- data/lib/clacky/web/features/backup/store.js +1 -0
- data/lib/clacky/web/features/backup/view.js +0 -1
- data/lib/clacky/web/features/brand/view.js +13 -5
- data/lib/clacky/web/features/extensions/store.js +184 -0
- data/lib/clacky/web/features/extensions/view.js +389 -0
- data/lib/clacky/web/features/new-session/store.js +186 -0
- data/lib/clacky/web/features/new-session/view.js +319 -0
- data/lib/clacky/web/features/share/view.js +63 -14
- data/lib/clacky/web/features/skills/store.js +3 -1
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +24 -6
- data/lib/clacky/web/features/trash/view.js +46 -9
- data/lib/clacky/web/features/workspace/store.js +2 -0
- data/lib/clacky/web/features/workspace/view.js +0 -1
- data/lib/clacky/web/i18n.js +118 -106
- data/lib/clacky/web/index.html +138 -188
- data/lib/clacky/web/sessions.js +64 -269
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +2 -0
- data/lib/clacky/web/theme.js +2 -0
- data/lib/clacky/web/ws-dispatcher.js +1 -0
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +55 -24
- data/benchmark/runner.rb +0 -441
- data/lib/clacky/api_extension_loader.rb +0 -168
- data/lib/clacky/default_agents/SOUL.md +0 -3
- data/lib/clacky/default_agents/USER.md +0 -1
- data/lib/clacky/default_agents/coding/profile.yml +0 -5
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_agents/general/profile.yml +0 -2
- data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
- data/lib/clacky/web/features/creator/store.js +0 -81
- data/lib/clacky/web/features/creator/view.js +0 -380
- /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
data/lib/clacky/web/core/ext.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// ── Clacky.ext — WebUI extension registry ─────────────────────────────────
|
|
2
2
|
//
|
|
3
|
-
// The single, controlled entry point through which
|
|
4
|
-
//
|
|
3
|
+
// The single, controlled entry point through which extensions declared
|
|
4
|
+
// in an ext.yml container hook into the WebUI.
|
|
5
5
|
//
|
|
6
6
|
// Three capabilities (the whole contract an extension author must learn):
|
|
7
7
|
// Clacky.ext.api.register(name, fn) — register a data source
|
|
8
8
|
// Clacky.ext.subscribe(event, handler) — listen to store events (read-only)
|
|
9
|
-
// Clacky.ext.ui.mount(slot,
|
|
9
|
+
// Clacky.ext.ui.mount(slot, spec, opts) — inject UI into a named slot
|
|
10
|
+
//
|
|
11
|
+
// `spec` is either a render function or { create?, render }. On per-session
|
|
12
|
+
// slots (session.aside/banner/composer), providing `create(ctx)` gives you
|
|
13
|
+
// one runtime per sessionId, isolated across sessions and disposed only
|
|
14
|
+
// when the session leaves the sidebar.
|
|
10
15
|
//
|
|
11
16
|
// Safety guarantees enforced here (the "constitution"):
|
|
12
17
|
// - Every extension callback is wrapped in try/catch. A throwing extension is
|
|
@@ -23,9 +28,9 @@
|
|
|
23
28
|
window.Clacky = window.Clacky || {};
|
|
24
29
|
|
|
25
30
|
Clacky.ext = (() => {
|
|
26
|
-
// Pure mode: detect ?pure=true once. When on, all registration is a no-op
|
|
27
|
-
// the host must not
|
|
28
|
-
//
|
|
31
|
+
// Pure mode: detect ?pure=true once. When on, all registration is a no-op
|
|
32
|
+
// and the host must not inject any extension scripts (handled server-side).
|
|
33
|
+
// This is the ultimate escape hatch back to a clean official UI.
|
|
29
34
|
const PURE = (() => {
|
|
30
35
|
try {
|
|
31
36
|
return new URLSearchParams(window.location.search).get("pure") === "true";
|
|
@@ -37,6 +42,7 @@ Clacky.ext = (() => {
|
|
|
37
42
|
const _dataSources = {}; // name => fn
|
|
38
43
|
const _subscribers = {}; // event => [handler]
|
|
39
44
|
const _slotRenderers = {}; // slot => [{ fn, extId, agents, panel, order, tab }]
|
|
45
|
+
const _workspaces = {}; // id => { id, title, render, extId }
|
|
40
46
|
let _currentExtId = null; // set while an extension file is loading
|
|
41
47
|
|
|
42
48
|
// Slots that render as a tabbed container instead of a vertical stack: each
|
|
@@ -46,6 +52,40 @@ Clacky.ext = (() => {
|
|
|
46
52
|
// slot => active tab id (remembered across re-renders within a page load).
|
|
47
53
|
const _activeTab = {};
|
|
48
54
|
|
|
55
|
+
// Slots that follow the current session's agent scope. A mount into any
|
|
56
|
+
// of these, made from inside a panel file, is confined to that panel's
|
|
57
|
+
// agents (via _currentPanel). Every other slot — sidebar, header, main
|
|
58
|
+
// workspace — is treated as global chrome: mounts there always show,
|
|
59
|
+
// regardless of which panel file happened to be loading. This is what
|
|
60
|
+
// separates "session UI" from "app chrome" and it is the ONLY place
|
|
61
|
+
// that distinction lives.
|
|
62
|
+
const SESSION_SCOPED_SLOTS = {
|
|
63
|
+
"session.banner": true,
|
|
64
|
+
"session.composer": true,
|
|
65
|
+
"session.aside": true,
|
|
66
|
+
"settings.tabs": true,
|
|
67
|
+
"settings.body": true,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Named slots the host renders. Extensions mounting into any other name
|
|
71
|
+
// will silently do nothing — we warn once per bad name to catch typos
|
|
72
|
+
// like "sidebar-nav.top" or "session.aisde" during development.
|
|
73
|
+
const KNOWN_SLOTS = {
|
|
74
|
+
"header.left": true,
|
|
75
|
+
"header.right": true,
|
|
76
|
+
"sidebar.nav.top": true,
|
|
77
|
+
"sidebar.nav": true,
|
|
78
|
+
"sidebar.nav.bottom": true,
|
|
79
|
+
"sidebar.footer": true,
|
|
80
|
+
"main.workspace": true,
|
|
81
|
+
"session.banner": true,
|
|
82
|
+
"session.composer": true,
|
|
83
|
+
"session.aside": true,
|
|
84
|
+
"settings.tabs": true,
|
|
85
|
+
"settings.body": true,
|
|
86
|
+
};
|
|
87
|
+
const _warnedUnknownSlots = {};
|
|
88
|
+
|
|
49
89
|
// Per-extension/panel agent scoping declared at load time:
|
|
50
90
|
// _extAgents[extId] = ["coding", ...] (from <script data-agent=...>, may be multiple)
|
|
51
91
|
// _panelAgents[panel] = ["coding", ...] (agents whose profile.yml references the panel)
|
|
@@ -72,15 +112,88 @@ Clacky.ext = (() => {
|
|
|
72
112
|
};
|
|
73
113
|
}
|
|
74
114
|
|
|
75
|
-
//
|
|
76
|
-
|
|
115
|
+
// Warn once per unknown slot so a typo is loud but the log stays finite.
|
|
116
|
+
function _warnUnknownSlot(slot, label) {
|
|
117
|
+
if (KNOWN_SLOTS[slot] || _warnedUnknownSlots[slot]) return;
|
|
118
|
+
_warnedUnknownSlots[slot] = true;
|
|
119
|
+
const known = Object.keys(KNOWN_SLOTS).join(", ");
|
|
120
|
+
console.warn(
|
|
121
|
+
`[Clacky.ext] unknown slot ${JSON.stringify(slot)} in ${label}. ` +
|
|
122
|
+
`Nothing will render. Known slots: ${known}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Slots whose renderer represents "the current session's copy" of some UI
|
|
127
|
+
// (a tabbed panel body, a session banner, a session-composer button). Mounts
|
|
128
|
+
// to these slots may declare `create(ctx)` and get one runtime per session,
|
|
129
|
+
// isolated from every other session and torn down only when the session
|
|
130
|
+
// itself leaves the sidebar.
|
|
131
|
+
const PER_SESSION_SLOTS = {
|
|
132
|
+
"session.aside": true,
|
|
133
|
+
"session.banner": true,
|
|
134
|
+
"session.composer": true,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// sessionId => Map<mountKey, runtime>. Populated lazily on first render.
|
|
138
|
+
const _sessionRuntimes = new Map();
|
|
139
|
+
// sessionId => Map<mountKey, teardownFn|null>. Cleared before every rerender.
|
|
140
|
+
const _viewTeardowns = new Map();
|
|
141
|
+
|
|
142
|
+
// Deterministic key for a renderer within a session: same slot + extId + tab
|
|
143
|
+
// id always resolves to the same runtime for a given session.
|
|
144
|
+
function _mountKey(slot, entry) {
|
|
145
|
+
const tabId = entry.tab && entry.tab.id ? entry.tab.id : "_";
|
|
146
|
+
return `${slot}::${entry.extId || "?"}::${tabId}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Normalize a mount registration. Accepts either a plain function (render
|
|
150
|
+
// only) or an object { create?, render }. Anything else is rejected with a
|
|
151
|
+
// console warning to catch typos early.
|
|
152
|
+
function _register(slot, spec, opts, extId, extra) {
|
|
153
|
+
let create = null;
|
|
154
|
+
let render;
|
|
155
|
+
if (typeof spec === "function") {
|
|
156
|
+
render = spec;
|
|
157
|
+
} else if (spec && typeof spec === "object" && typeof spec.render === "function") {
|
|
158
|
+
render = spec.render;
|
|
159
|
+
if (typeof spec.create === "function") create = spec.create;
|
|
160
|
+
} else {
|
|
161
|
+
console.warn(
|
|
162
|
+
`[Clacky.ext] ui.mount(${JSON.stringify(slot)}) needs a render function ` +
|
|
163
|
+
`or { create?, render } object. Got: ${typeof spec}`
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const isBuiltin = !!(extra && extra.builtin);
|
|
169
|
+
const explicit = opts && Array.isArray(opts.agents) ? opts.agents : null;
|
|
170
|
+
const panel = (!isBuiltin && SESSION_SCOPED_SLOTS[slot]) ? _currentPanel : null;
|
|
171
|
+
const scoped = isBuiltin ? null : (explicit || _extAgents[extId] || null);
|
|
172
|
+
const order = opts && Number.isFinite(opts.order) ? opts.order : 100;
|
|
173
|
+
const tab = (opts && opts.tab) || null;
|
|
174
|
+
const workspace = (opts && typeof opts.workspace === "string") ? opts.workspace : null;
|
|
175
|
+
|
|
176
|
+
const label = `ui.mount(${slot})`;
|
|
177
|
+
(_slotRenderers[slot] ||= []).push({
|
|
178
|
+
render: _guard(render, label, extId),
|
|
179
|
+
create: create ? _guard(create, `${label}#create`, extId) : null,
|
|
180
|
+
extId,
|
|
181
|
+
agents: scoped,
|
|
182
|
+
panel,
|
|
183
|
+
order,
|
|
184
|
+
tab,
|
|
185
|
+
workspace,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
77
190
|
// extension's <script src> and _extEnd right after. In pure mode these are
|
|
78
191
|
// no-ops (and the host does not emit extension scripts at all).
|
|
79
192
|
//
|
|
80
|
-
// `agents` scopes a single-agent extension (from agents/<name>/webui/):
|
|
81
|
-
// of agent profile names it should appear for. `panel` marks
|
|
82
|
-
//
|
|
83
|
-
//
|
|
193
|
+
// `agents` scopes a single-agent extension (from user agents/<name>/webui/):
|
|
194
|
+
// a list of agent profile names it should appear for. `panel` marks a panel
|
|
195
|
+
// whose agent scope is resolved separately via registerPanelAgents. Either
|
|
196
|
+
// may be omitted for a global extension.
|
|
84
197
|
function _extBegin(extId, agents, panel) {
|
|
85
198
|
if (PURE) return;
|
|
86
199
|
_currentExtId = extId;
|
|
@@ -137,50 +250,153 @@ Clacky.ext = (() => {
|
|
|
137
250
|
const ui = {
|
|
138
251
|
// Register a renderer for a named slot. renderFn(ctx) -> Node | string | null.
|
|
139
252
|
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
253
|
+
// Scope rules (automatic — you never spell them out):
|
|
254
|
+
// • Mounts to session/settings slots inherit the current panel's agent
|
|
255
|
+
// scope, so a `session.aside` tab written next to a designer panel
|
|
256
|
+
// only shows when the user is on the designer agent.
|
|
257
|
+
// • Mounts to every other slot (sidebar.*, header.*, main.workspace)
|
|
258
|
+
// are global app chrome — they show regardless of panel/agent.
|
|
259
|
+
//
|
|
260
|
+
// opts.agents — restrict to these agent profile names, overriding the
|
|
261
|
+
// defaults above. Rarely needed.
|
|
262
|
+
// Mount UI into a named slot. `spec` is either:
|
|
263
|
+
// • a function (ctx) => Node|string|null — plain render, no state
|
|
264
|
+
// • an object { create?, render } — render + optional
|
|
265
|
+
// per-session runtime bound to `create`
|
|
266
|
+
//
|
|
267
|
+
// If the slot is per-session (session.aside etc.) AND `create` is present,
|
|
268
|
+
// the host allocates one runtime per sessionId — `create(ctx)` runs the
|
|
269
|
+
// first time this session shows the tab, `render(container, ctx, runtime)`
|
|
270
|
+
// runs every time the tab becomes visible with a fresh empty container,
|
|
271
|
+
// and `runtime.dispose()` runs when the session leaves the sidebar. State
|
|
272
|
+
// set up in `create` (media recorders, timers, subscriptions, buffers) is
|
|
273
|
+
// therefore isolated per session and survives tab teardown.
|
|
274
|
+
//
|
|
275
|
+
// For non-per-session slots or specs without `create`, the mount is a
|
|
276
|
+
// pure global renderer — one call per (re)render.
|
|
277
|
+
//
|
|
278
|
+
// opts.tab — { id, label, badge? } required for tabbed slots.
|
|
279
|
+
// opts.order — vertical sort weight (lower first). Default 100.
|
|
280
|
+
// opts.workspace — id of a registerWorkspace() workspace this mount opens;
|
|
281
|
+
// the host stamps it on the mount so the Router highlights this item
|
|
282
|
+
// while that workspace is active. Use on sidebar.nav.* items.
|
|
283
|
+
// opts.agents — override the automatic agent scope.
|
|
284
|
+
mount(slot, spec, opts) {
|
|
285
|
+
if (PURE || spec == null) return;
|
|
286
|
+
_warnUnknownSlot(slot, `ui.mount(${_currentExtId || "?"})`);
|
|
287
|
+
_register(slot, spec, opts, _currentExtId);
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// Host-owned mount: identical to `mount` but bypasses PURE so official
|
|
291
|
+
// built-in tabs keep working in safe mode, and defaults to no agent scope.
|
|
292
|
+
mountBuiltin(slot, spec, opts) {
|
|
293
|
+
if (spec == null) return;
|
|
294
|
+
_warnUnknownSlot(slot, "ui.mountBuiltin");
|
|
295
|
+
_register(slot, spec, opts, "host", { builtin: true });
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// Register a full-page workspace under this extension. When opened it
|
|
299
|
+
// takes over the main content area (host hides its own panels first) and
|
|
300
|
+
// gets its own URL hash `#ext/<id>` so back/forward + reload work.
|
|
301
|
+
//
|
|
302
|
+
// Clacky.ext.ui.registerWorkspace("my-console", {
|
|
303
|
+
// title: "My Console",
|
|
304
|
+
// render(container) { container.textContent = "hello"; },
|
|
305
|
+
// });
|
|
306
|
+
//
|
|
307
|
+
// A workspace is opened with `Clacky.ext.ui.openWorkspace(id)` — typically
|
|
308
|
+
// wired to a `sidebar.nav` menu item mounted from the same extension.
|
|
309
|
+
// render(container, ctx) is called every time the workspace is shown; the
|
|
310
|
+
// container is cleared beforehand so the render function can be dumb.
|
|
311
|
+
registerWorkspace(id, def) {
|
|
312
|
+
if (PURE || !id || !def || typeof def.render !== "function") return;
|
|
313
|
+
_workspaces[id] = {
|
|
314
|
+
id,
|
|
315
|
+
title: def.title || id,
|
|
316
|
+
render: _guard(def.render, `ui.registerWorkspace(${id})`, _currentExtId),
|
|
156
317
|
extId: _currentExtId,
|
|
157
|
-
|
|
158
|
-
panel, // official-panel id, or null
|
|
159
|
-
order,
|
|
160
|
-
tab, // { id, label, badge? } for tabbed slots, else null
|
|
161
|
-
});
|
|
318
|
+
};
|
|
162
319
|
},
|
|
163
320
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
(_slotRenderers[slot] ||= []).push({
|
|
173
|
-
fn: _guard(renderFn, `ui.mountBuiltin(${slot})`, "host"),
|
|
174
|
-
extId: "host",
|
|
175
|
-
agents: null,
|
|
176
|
-
panel: null,
|
|
177
|
-
order,
|
|
178
|
-
tab,
|
|
179
|
-
});
|
|
321
|
+
// Open a registered workspace. Emits a `clacky:ext:navigate` event that
|
|
322
|
+
// the host router listens for; ext.js has no compile-time dependency on
|
|
323
|
+
// the router. No-op if the id was never registered.
|
|
324
|
+
openWorkspace(id) {
|
|
325
|
+
if (PURE || !_workspaces[id]) return;
|
|
326
|
+
document.dispatchEvent(new CustomEvent("clacky:ext:navigate", {
|
|
327
|
+
detail: { view: "ext-workspace", params: { id } },
|
|
328
|
+
}));
|
|
180
329
|
},
|
|
181
330
|
};
|
|
182
331
|
|
|
183
|
-
//
|
|
332
|
+
// Return the per-session runtime for a mount, allocating it via `create` on
|
|
333
|
+
// first use. Global mounts (or per-session slots without `create`) return
|
|
334
|
+
// null — the render function is expected to work stateless.
|
|
335
|
+
function _getRuntime(slot, entry, ctx) {
|
|
336
|
+
if (!entry.create) return null;
|
|
337
|
+
if (!PER_SESSION_SLOTS[slot]) return null;
|
|
338
|
+
const sessionId = ctx && ctx.sessionId;
|
|
339
|
+
if (!sessionId) return null;
|
|
340
|
+
|
|
341
|
+
let bySession = _sessionRuntimes.get(sessionId);
|
|
342
|
+
if (!bySession) {
|
|
343
|
+
bySession = new Map();
|
|
344
|
+
_sessionRuntimes.set(sessionId, bySession);
|
|
345
|
+
}
|
|
346
|
+
const key = _mountKey(slot, entry);
|
|
347
|
+
let runtime = bySession.get(key);
|
|
348
|
+
if (!runtime) {
|
|
349
|
+
runtime = entry.create(ctx);
|
|
350
|
+
if (runtime === undefined) return null; // create threw (guarded)
|
|
351
|
+
bySession.set(key, runtime || {});
|
|
352
|
+
}
|
|
353
|
+
return runtime;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Run any view-level teardown collected on the previous render pass for the
|
|
357
|
+
// current session, so old event listeners / RAF loops don't leak when a tab
|
|
358
|
+
// is re-rendered or the session switches away.
|
|
359
|
+
function _runViewTeardowns(sessionId) {
|
|
360
|
+
if (!sessionId) return;
|
|
361
|
+
const map = _viewTeardowns.get(sessionId);
|
|
362
|
+
if (!map) return;
|
|
363
|
+
map.forEach((fn) => { if (typeof fn === "function") { try { fn(); } catch (_e) {} } });
|
|
364
|
+
map.clear();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function _rememberViewTeardown(sessionId, key, teardown) {
|
|
368
|
+
if (!sessionId || typeof teardown !== "function") return;
|
|
369
|
+
let map = _viewTeardowns.get(sessionId);
|
|
370
|
+
if (!map) { map = new Map(); _viewTeardowns.set(sessionId, map); }
|
|
371
|
+
map.set(key, teardown);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Invoke a renderer's render function. Handles the (container, ctx, runtime)
|
|
375
|
+
// signature used everywhere; if the render returns a function it is stored
|
|
376
|
+
// as the view teardown, run before the next render or on session exit.
|
|
377
|
+
function _invokeRender(slot, entry, container, ctx) {
|
|
378
|
+
const runtime = _getRuntime(slot, entry, ctx);
|
|
379
|
+
let node, teardown, crashed = false;
|
|
380
|
+
try {
|
|
381
|
+
const out = entry.render(container, ctx, runtime);
|
|
382
|
+
if (out === undefined) {
|
|
383
|
+
crashed = false; // container mutated in-place is fine
|
|
384
|
+
} else if (typeof out === "function") {
|
|
385
|
+
teardown = out;
|
|
386
|
+
} else {
|
|
387
|
+
node = out;
|
|
388
|
+
}
|
|
389
|
+
} catch (_e) {
|
|
390
|
+
crashed = true;
|
|
391
|
+
}
|
|
392
|
+
if (crashed) return { crashed: true };
|
|
393
|
+
if (ctx && ctx.sessionId && teardown) {
|
|
394
|
+
_rememberViewTeardown(ctx.sessionId, _mountKey(slot, entry), teardown);
|
|
395
|
+
}
|
|
396
|
+
return { node };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
184
400
|
// null agents AND null panel => global (always visible). Otherwise the
|
|
185
401
|
// current profile must be in the renderer's agent list, or in the set of
|
|
186
402
|
// agents that reference its panel.
|
|
@@ -215,34 +431,27 @@ Clacky.ext = (() => {
|
|
|
215
431
|
const ordered = renderers.slice().sort((a, b) => a.order - b.order);
|
|
216
432
|
ordered.forEach((entry) => {
|
|
217
433
|
if (!_visibleFor(entry, profile)) return;
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
} catch (_e) {
|
|
225
|
-
crashed = true; // defensive: should already be guarded
|
|
226
|
-
}
|
|
227
|
-
|
|
434
|
+
const wrap = document.createElement("div");
|
|
435
|
+
if (entry.extId) wrap.setAttribute("data-ext-id", entry.extId);
|
|
436
|
+
// Nav mounts that open a workspace declare it via opts.workspace; the
|
|
437
|
+
// host stamps it so the Router alone owns the active highlight.
|
|
438
|
+
if (entry.workspace) wrap.setAttribute("data-ext-workspace", entry.workspace);
|
|
439
|
+
const { node, crashed } = _invokeRender(slot, entry, wrap, renderCtx);
|
|
228
440
|
if (crashed) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
ph.textContent = "Extension failed to render.";
|
|
234
|
-
container.appendChild(ph);
|
|
441
|
+
wrap.setAttribute("data-ext-status", "crashed");
|
|
442
|
+
wrap.className = "ext-slot-crashed";
|
|
443
|
+
wrap.textContent = "Extension failed to render.";
|
|
444
|
+
container.appendChild(wrap);
|
|
235
445
|
return;
|
|
236
446
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
447
|
+
if (node != null) {
|
|
448
|
+
if (typeof node === "string") wrap.innerHTML = node;
|
|
449
|
+
else wrap.appendChild(node);
|
|
450
|
+
}
|
|
451
|
+
// If render mutated `wrap` in-place (returned nothing) or produced a
|
|
452
|
+
// node, either way we only append when there's something to show.
|
|
453
|
+
if (wrap.childNodes.length > 0 || (node != null && typeof node === "string")) {
|
|
243
454
|
container.appendChild(wrap);
|
|
244
|
-
} else {
|
|
245
|
-
container.appendChild(node);
|
|
246
455
|
}
|
|
247
456
|
});
|
|
248
457
|
}
|
|
@@ -289,11 +498,7 @@ Clacky.ext = (() => {
|
|
|
289
498
|
const localCtx = Object.assign({}, renderCtx, {
|
|
290
499
|
setBadge: (n) => _setTabBadge(tabBtns[id], n),
|
|
291
500
|
});
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
node = entry.fn(localCtx);
|
|
295
|
-
if (node === undefined) crashed = true;
|
|
296
|
-
} catch (_e) { crashed = true; }
|
|
501
|
+
const { node, crashed } = _invokeRender(slot, entry, body, localCtx);
|
|
297
502
|
if (crashed) {
|
|
298
503
|
const ph = document.createElement("div");
|
|
299
504
|
ph.className = "ext-slot-crashed";
|
|
@@ -355,11 +560,38 @@ Clacky.ext = (() => {
|
|
|
355
560
|
|
|
356
561
|
// Update the current session context (host calls this on every session
|
|
357
562
|
// switch) and re-render all slots so panels match the new agent profile.
|
|
563
|
+
// Runs the previous session's view-level teardowns first (event listeners /
|
|
564
|
+
// RAF loops attached to now-dead DOM), but leaves per-session runtimes
|
|
565
|
+
// alive — the meeting recorder for session A keeps running while you're
|
|
566
|
+
// looking at B, and its captions are still there when you switch back.
|
|
358
567
|
function setContext(next) {
|
|
568
|
+
const prevSessionId = context.sessionId;
|
|
359
569
|
Object.assign(context, next || {});
|
|
570
|
+
if (prevSessionId && prevSessionId !== context.sessionId) {
|
|
571
|
+
_runViewTeardowns(prevSessionId);
|
|
572
|
+
}
|
|
360
573
|
refreshSlots();
|
|
361
574
|
}
|
|
362
575
|
|
|
576
|
+
// Called by the host when a session leaves the sidebar for good (deleted,
|
|
577
|
+
// archived, or the page is unloading). Runs every runtime's `dispose()` for
|
|
578
|
+
// that session so timers, media recorders, and network handles are released.
|
|
579
|
+
function notifySessionRemoved(sessionId) {
|
|
580
|
+
if (!sessionId) return;
|
|
581
|
+
_runViewTeardowns(sessionId);
|
|
582
|
+
_viewTeardowns.delete(sessionId);
|
|
583
|
+
const bySession = _sessionRuntimes.get(sessionId);
|
|
584
|
+
if (!bySession) return;
|
|
585
|
+
bySession.forEach((runtime) => {
|
|
586
|
+
if (runtime && typeof runtime.dispose === "function") {
|
|
587
|
+
try { runtime.dispose(); } catch (err) {
|
|
588
|
+
console.error("[Clacky.ext] runtime.dispose failed:", err);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
_sessionRuntimes.delete(sessionId);
|
|
593
|
+
}
|
|
594
|
+
|
|
363
595
|
// Re-render every named slot present in the DOM against the current context.
|
|
364
596
|
// Idempotent: each slot's container is cleared before re-rendering.
|
|
365
597
|
function refreshSlots() {
|
|
@@ -373,6 +605,7 @@ Clacky.ext = (() => {
|
|
|
373
605
|
get pure() { return PURE; },
|
|
374
606
|
context,
|
|
375
607
|
setContext,
|
|
608
|
+
notifySessionRemoved,
|
|
376
609
|
refreshSlots,
|
|
377
610
|
registerPanelAgents,
|
|
378
611
|
api,
|
|
@@ -381,6 +614,10 @@ Clacky.ext = (() => {
|
|
|
381
614
|
emit,
|
|
382
615
|
renderSlot,
|
|
383
616
|
slots,
|
|
617
|
+
// Host-only: look up a registered workspace by id. Returns
|
|
618
|
+
// { id, title, render, extId } or undefined. Used by the Router when
|
|
619
|
+
// handling the `ext-workspace` view — extensions never call this.
|
|
620
|
+
_getWorkspace(id) { return _workspaces[id]; },
|
|
384
621
|
_extBegin, // used by the loader; not part of the public extension API
|
|
385
622
|
_extEnd, // used by the loader; not part of the public extension API
|
|
386
623
|
};
|
|
@@ -94,7 +94,7 @@ const BrandView = (() => {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function _goToLicenseInput() {
|
|
97
|
-
Router.navigate("settings");
|
|
97
|
+
window.Clacky.Router.navigate("settings");
|
|
98
98
|
if (typeof Settings !== "undefined") Settings.open();
|
|
99
99
|
setTimeout(() => {
|
|
100
100
|
const generalTabBtn = document.querySelector('#settings-tabs .settings-tab[data-tab="general"]');
|
|
@@ -297,12 +297,20 @@ const BrandView = (() => {
|
|
|
297
297
|
span.textContent = message;
|
|
298
298
|
if (i18nKey) span.setAttribute("data-i18n", i18nKey);
|
|
299
299
|
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
const renewBtn = document.createElement("button");
|
|
301
|
+
renewBtn.className = "brand-warning-renew-btn";
|
|
302
|
+
renewBtn.textContent = I18n.t("brand.warning.renewBtn");
|
|
303
|
+
renewBtn.setAttribute("data-i18n", "brand.warning.renewBtn");
|
|
304
|
+
renewBtn.onclick = () => { bar.remove(); _goToLicenseInput(); };
|
|
305
|
+
|
|
306
|
+
const closeBtn = document.createElement("button");
|
|
307
|
+
closeBtn.className = "brand-warning-close-btn";
|
|
308
|
+
closeBtn.innerHTML = "✕";
|
|
309
|
+
closeBtn.onclick = () => bar.remove();
|
|
303
310
|
|
|
304
311
|
bar.appendChild(span);
|
|
305
|
-
bar.appendChild(
|
|
312
|
+
bar.appendChild(renewBtn);
|
|
313
|
+
bar.appendChild(closeBtn);
|
|
306
314
|
document.getElementById("main").prepend(bar);
|
|
307
315
|
}
|
|
308
316
|
|