openclacky 1.3.6 → 1.3.8
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 +54 -0
- data/lib/clacky/agent/hook_manager.rb +10 -1
- data/lib/clacky/agent/system_prompt_builder.rb +1 -1
- data/lib/clacky/agent.rb +17 -3
- data/lib/clacky/agent_profile.rb +124 -53
- data/lib/clacky/billing/billing_store.rb +3 -2
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
- data/lib/clacky/default_extensions/coding/ext.yml +19 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -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 +33 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1288 -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/agents/general/avatar.png +0 -0
- data/lib/clacky/default_extensions/general/ext.yml +19 -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} +4 -5
- 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 -90
- 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 +489 -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 +439 -146
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/shell_hook_loader.rb +266 -22
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/utils/workspace_rules.rb +2 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1034 -675
- 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 +432 -89
- 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 +390 -0
- data/lib/clacky/web/features/new-session/store.js +199 -0
- data/lib/clacky/web/features/new-session/view.js +619 -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 +48 -12
- 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 +152 -106
- data/lib/clacky/web/index.html +172 -190
- data/lib/clacky/web/sessions.js +67 -272
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +88 -48
- data/lib/clacky/web/theme.js +5 -0
- data/lib/clacky/web/ws-dispatcher.js +12 -2
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +52 -21
- 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,23 @@
|
|
|
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 }. The render
|
|
12
|
+
// signature is always `render(container, ctx, runtime)`:
|
|
13
|
+
// - `container` is a host-owned DOM element — append your UI into it,
|
|
14
|
+
// or return a DOM node / HTML string and the host appends it for you.
|
|
15
|
+
// - returning a function registers it as a teardown callback.
|
|
16
|
+
// - returning null is treated as an error (almost always a wrong
|
|
17
|
+
// signature, e.g. `(ctx) => ...`), surfaced as a crashed placeholder.
|
|
18
|
+
// On per-session slots (session.aside/banner/composer), providing
|
|
19
|
+
// `create(ctx)` gives you one runtime per sessionId, isolated across
|
|
20
|
+
// sessions and disposed only when the session leaves the sidebar.
|
|
10
21
|
//
|
|
11
22
|
// Safety guarantees enforced here (the "constitution"):
|
|
12
23
|
// - Every extension callback is wrapped in try/catch. A throwing extension is
|
|
@@ -23,9 +34,9 @@
|
|
|
23
34
|
window.Clacky = window.Clacky || {};
|
|
24
35
|
|
|
25
36
|
Clacky.ext = (() => {
|
|
26
|
-
// Pure mode: detect ?pure=true once. When on, all registration is a no-op
|
|
27
|
-
// the host must not
|
|
28
|
-
//
|
|
37
|
+
// Pure mode: detect ?pure=true once. When on, all registration is a no-op
|
|
38
|
+
// and the host must not inject any extension scripts (handled server-side).
|
|
39
|
+
// This is the ultimate escape hatch back to a clean official UI.
|
|
29
40
|
const PURE = (() => {
|
|
30
41
|
try {
|
|
31
42
|
return new URLSearchParams(window.location.search).get("pure") === "true";
|
|
@@ -37,14 +48,47 @@ Clacky.ext = (() => {
|
|
|
37
48
|
const _dataSources = {}; // name => fn
|
|
38
49
|
const _subscribers = {}; // event => [handler]
|
|
39
50
|
const _slotRenderers = {}; // slot => [{ fn, extId, agents, panel, order, tab }]
|
|
51
|
+
const _workspaces = {}; // id => { id, title, render, extId }
|
|
40
52
|
let _currentExtId = null; // set while an extension file is loading
|
|
41
53
|
|
|
42
54
|
// Slots that render as a tabbed container instead of a vertical stack: each
|
|
43
55
|
// renderer becomes one tab (chrome drawn by the host), and only the active
|
|
44
56
|
// tab's body is shown. Renderers in these slots must carry opts.tab.
|
|
45
57
|
const TABBED_SLOTS = { "session.aside": true };
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
|
|
59
|
+
// Slots that follow the current session's agent scope. A mount into any
|
|
60
|
+
// of these, made from inside a panel file, is confined to that panel's
|
|
61
|
+
// agents (via _currentPanel). Every other slot — sidebar, header, main
|
|
62
|
+
// workspace — is treated as global chrome: mounts there always show,
|
|
63
|
+
// regardless of which panel file happened to be loading. This is what
|
|
64
|
+
// separates "session UI" from "app chrome" and it is the ONLY place
|
|
65
|
+
// that distinction lives.
|
|
66
|
+
const SESSION_SCOPED_SLOTS = {
|
|
67
|
+
"session.banner": true,
|
|
68
|
+
"session.composer": true,
|
|
69
|
+
"session.aside": true,
|
|
70
|
+
"settings.tabs": true,
|
|
71
|
+
"settings.body": true,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Named slots the host renders. Extensions mounting into any other name
|
|
75
|
+
// will silently do nothing — we warn once per bad name to catch typos
|
|
76
|
+
// like "sidebar-nav.top" or "session.aisde" during development.
|
|
77
|
+
const KNOWN_SLOTS = {
|
|
78
|
+
"header.left": true,
|
|
79
|
+
"header.right": true,
|
|
80
|
+
"sidebar.nav.top": true,
|
|
81
|
+
"sidebar.nav": true,
|
|
82
|
+
"sidebar.nav.bottom": true,
|
|
83
|
+
"sidebar.footer": true,
|
|
84
|
+
"main.workspace": true,
|
|
85
|
+
"session.banner": true,
|
|
86
|
+
"session.composer": true,
|
|
87
|
+
"session.aside": true,
|
|
88
|
+
"settings.tabs": true,
|
|
89
|
+
"settings.body": true,
|
|
90
|
+
};
|
|
91
|
+
const _warnedUnknownSlots = {};
|
|
48
92
|
|
|
49
93
|
// Per-extension/panel agent scoping declared at load time:
|
|
50
94
|
// _extAgents[extId] = ["coding", ...] (from <script data-agent=...>, may be multiple)
|
|
@@ -52,6 +96,7 @@ Clacky.ext = (() => {
|
|
|
52
96
|
// Resolved by the host before the extension scripts run (see loader markers).
|
|
53
97
|
const _extAgents = {};
|
|
54
98
|
const _panelAgents = {};
|
|
99
|
+
const _agentContributions = {}; // agent id => { panels:[{id,title,title_zh}], skills:[...] }
|
|
55
100
|
let _currentPanel = null; // set while an official panel file is loading
|
|
56
101
|
|
|
57
102
|
// Current session context, kept in sync by the host on every session switch.
|
|
@@ -72,15 +117,88 @@ Clacky.ext = (() => {
|
|
|
72
117
|
};
|
|
73
118
|
}
|
|
74
119
|
|
|
75
|
-
//
|
|
76
|
-
|
|
120
|
+
// Warn once per unknown slot so a typo is loud but the log stays finite.
|
|
121
|
+
function _warnUnknownSlot(slot, label) {
|
|
122
|
+
if (KNOWN_SLOTS[slot] || _warnedUnknownSlots[slot]) return;
|
|
123
|
+
_warnedUnknownSlots[slot] = true;
|
|
124
|
+
const known = Object.keys(KNOWN_SLOTS).join(", ");
|
|
125
|
+
console.warn(
|
|
126
|
+
`[Clacky.ext] unknown slot ${JSON.stringify(slot)} in ${label}. ` +
|
|
127
|
+
`Nothing will render. Known slots: ${known}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Slots whose renderer represents "the current session's copy" of some UI
|
|
132
|
+
// (a tabbed panel body, a session banner, a session-composer button). Mounts
|
|
133
|
+
// to these slots may declare `create(ctx)` and get one runtime per session,
|
|
134
|
+
// isolated from every other session and torn down only when the session
|
|
135
|
+
// itself leaves the sidebar.
|
|
136
|
+
const PER_SESSION_SLOTS = {
|
|
137
|
+
"session.aside": true,
|
|
138
|
+
"session.banner": true,
|
|
139
|
+
"session.composer": true,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// sessionId => Map<mountKey, runtime>. Populated lazily on first render.
|
|
143
|
+
const _sessionRuntimes = new Map();
|
|
144
|
+
// sessionId => Map<mountKey, teardownFn|null>. Cleared before every rerender.
|
|
145
|
+
const _viewTeardowns = new Map();
|
|
146
|
+
|
|
147
|
+
// Deterministic key for a renderer within a session: same slot + extId + tab
|
|
148
|
+
// id always resolves to the same runtime for a given session.
|
|
149
|
+
function _mountKey(slot, entry) {
|
|
150
|
+
const tabId = entry.tab && entry.tab.id ? entry.tab.id : "_";
|
|
151
|
+
return `${slot}::${entry.extId || "?"}::${tabId}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Normalize a mount registration. Accepts either a plain function (render
|
|
155
|
+
// only) or an object { create?, render }. Anything else is rejected with a
|
|
156
|
+
// console warning to catch typos early.
|
|
157
|
+
function _register(slot, spec, opts, extId, extra) {
|
|
158
|
+
let create = null;
|
|
159
|
+
let render;
|
|
160
|
+
if (typeof spec === "function") {
|
|
161
|
+
render = spec;
|
|
162
|
+
} else if (spec && typeof spec === "object" && typeof spec.render === "function") {
|
|
163
|
+
render = spec.render;
|
|
164
|
+
if (typeof spec.create === "function") create = spec.create;
|
|
165
|
+
} else {
|
|
166
|
+
console.warn(
|
|
167
|
+
`[Clacky.ext] ui.mount(${JSON.stringify(slot)}) needs a render function ` +
|
|
168
|
+
`or { create?, render } object. Got: ${typeof spec}`
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const isBuiltin = !!(extra && extra.builtin);
|
|
174
|
+
const explicit = opts && Array.isArray(opts.agents) ? opts.agents : null;
|
|
175
|
+
const panel = (!isBuiltin && SESSION_SCOPED_SLOTS[slot]) ? _currentPanel : null;
|
|
176
|
+
const scoped = isBuiltin ? null : (explicit || _extAgents[extId] || null);
|
|
177
|
+
const order = opts && Number.isFinite(opts.order) ? opts.order : 100;
|
|
178
|
+
const tab = (opts && opts.tab) || null;
|
|
179
|
+
const workspace = (opts && typeof opts.workspace === "string") ? opts.workspace : null;
|
|
180
|
+
|
|
181
|
+
const label = `ui.mount(${slot})`;
|
|
182
|
+
(_slotRenderers[slot] ||= []).push({
|
|
183
|
+
render: _guard(render, label, extId),
|
|
184
|
+
create: create ? _guard(create, `${label}#create`, extId) : null,
|
|
185
|
+
extId,
|
|
186
|
+
agents: scoped,
|
|
187
|
+
panel,
|
|
188
|
+
order,
|
|
189
|
+
tab,
|
|
190
|
+
workspace,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
77
195
|
// extension's <script src> and _extEnd right after. In pure mode these are
|
|
78
196
|
// no-ops (and the host does not emit extension scripts at all).
|
|
79
197
|
//
|
|
80
|
-
// `agents` scopes a single-agent extension (from agents/<name>/webui/):
|
|
81
|
-
// of agent profile names it should appear for. `panel` marks
|
|
82
|
-
//
|
|
83
|
-
//
|
|
198
|
+
// `agents` scopes a single-agent extension (from user agents/<name>/webui/):
|
|
199
|
+
// a list of agent profile names it should appear for. `panel` marks a panel
|
|
200
|
+
// whose agent scope is resolved separately via registerPanelAgents. Either
|
|
201
|
+
// may be omitted for a global extension.
|
|
84
202
|
function _extBegin(extId, agents, panel) {
|
|
85
203
|
if (PURE) return;
|
|
86
204
|
_currentExtId = extId;
|
|
@@ -100,6 +218,11 @@ Clacky.ext = (() => {
|
|
|
100
218
|
Object.assign(_panelAgents, map);
|
|
101
219
|
}
|
|
102
220
|
|
|
221
|
+
function registerAgentContributions(map) {
|
|
222
|
+
if (PURE || !map) return;
|
|
223
|
+
Object.assign(_agentContributions, map);
|
|
224
|
+
}
|
|
225
|
+
|
|
103
226
|
const api = {
|
|
104
227
|
// Register a named data source. Host/extensions can later resolve it.
|
|
105
228
|
register(name, fn) {
|
|
@@ -137,50 +260,246 @@ Clacky.ext = (() => {
|
|
|
137
260
|
const ui = {
|
|
138
261
|
// Register a renderer for a named slot. renderFn(ctx) -> Node | string | null.
|
|
139
262
|
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
263
|
+
// Scope rules (automatic — you never spell them out):
|
|
264
|
+
// • Mounts to session/settings slots inherit the current panel's agent
|
|
265
|
+
// scope, so a `session.aside` tab written next to a designer panel
|
|
266
|
+
// only shows when the user is on the designer agent.
|
|
267
|
+
// • Mounts to every other slot (sidebar.*, header.*, main.workspace)
|
|
268
|
+
// are global app chrome — they show regardless of panel/agent.
|
|
269
|
+
//
|
|
270
|
+
// opts.agents — restrict to these agent profile names, overriding the
|
|
271
|
+
// defaults above. Rarely needed.
|
|
272
|
+
// Mount UI into a named slot. `spec` is either:
|
|
273
|
+
// • a function (ctx) => Node|string|null — plain render, no state
|
|
274
|
+
// • an object { create?, render } — render + optional
|
|
275
|
+
// per-session runtime bound to `create`
|
|
276
|
+
//
|
|
277
|
+
// If the slot is per-session (session.aside etc.) AND `create` is present,
|
|
278
|
+
// the host allocates one runtime per sessionId — `create(ctx)` runs the
|
|
279
|
+
// first time this session shows the tab, `render(container, ctx, runtime)`
|
|
280
|
+
// runs every time the tab becomes visible with a fresh empty container,
|
|
281
|
+
// and `runtime.dispose()` runs when the session leaves the sidebar. State
|
|
282
|
+
// set up in `create` (media recorders, timers, subscriptions, buffers) is
|
|
283
|
+
// therefore isolated per session and survives tab teardown.
|
|
284
|
+
//
|
|
285
|
+
// For non-per-session slots or specs without `create`, the mount is a
|
|
286
|
+
// pure global renderer — one call per (re)render.
|
|
287
|
+
//
|
|
288
|
+
// opts.tab — { id, label, badge? } required for tabbed slots.
|
|
289
|
+
// opts.order — vertical sort weight (lower first). Default 100.
|
|
290
|
+
// opts.workspace — id of a registerWorkspace() workspace this mount opens;
|
|
291
|
+
// the host stamps it on the mount so the Router highlights this item
|
|
292
|
+
// while that workspace is active. Use on sidebar.nav.* items.
|
|
293
|
+
// opts.agents — override the automatic agent scope.
|
|
294
|
+
mount(slot, spec, opts) {
|
|
295
|
+
if (PURE || spec == null) return;
|
|
296
|
+
_warnUnknownSlot(slot, `ui.mount(${_currentExtId || "?"})`);
|
|
297
|
+
_register(slot, spec, opts, _currentExtId);
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// Host-owned mount: identical to `mount` but bypasses PURE so official
|
|
301
|
+
// built-in tabs keep working in safe mode, and defaults to no agent scope.
|
|
302
|
+
mountBuiltin(slot, spec, opts) {
|
|
303
|
+
if (spec == null) return;
|
|
304
|
+
_warnUnknownSlot(slot, "ui.mountBuiltin");
|
|
305
|
+
_register(slot, spec, opts, "host", { builtin: true });
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// Register a full-page workspace under this extension. When opened it
|
|
309
|
+
// takes over the main content area (host hides its own panels first) and
|
|
310
|
+
// gets its own URL hash `#ext/<id>` so back/forward + reload work.
|
|
311
|
+
//
|
|
312
|
+
// Clacky.ext.ui.registerWorkspace("my-console", {
|
|
313
|
+
// title: "My Console",
|
|
314
|
+
// render(container) { container.textContent = "hello"; },
|
|
315
|
+
// });
|
|
316
|
+
//
|
|
317
|
+
// A workspace is opened with `Clacky.ext.ui.openWorkspace(id)` — typically
|
|
318
|
+
// wired to a `sidebar.nav` menu item mounted from the same extension.
|
|
319
|
+
// render(container, ctx) is called every time the workspace is shown; the
|
|
320
|
+
// container is cleared beforehand so the render function can be dumb.
|
|
321
|
+
registerWorkspace(id, def) {
|
|
322
|
+
if (PURE || !id || !def || typeof def.render !== "function") return;
|
|
323
|
+
_workspaces[id] = {
|
|
324
|
+
id,
|
|
325
|
+
title: def.title || id,
|
|
326
|
+
render: _guard(def.render, `ui.registerWorkspace(${id})`, _currentExtId),
|
|
156
327
|
extId: _currentExtId,
|
|
157
|
-
|
|
158
|
-
panel, // official-panel id, or null
|
|
159
|
-
order,
|
|
160
|
-
tab, // { id, label, badge? } for tabbed slots, else null
|
|
161
|
-
});
|
|
328
|
+
};
|
|
162
329
|
},
|
|
163
330
|
|
|
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
|
-
});
|
|
331
|
+
// Open a registered workspace. Emits a `clacky:ext:navigate` event that
|
|
332
|
+
// the host router listens for; ext.js has no compile-time dependency on
|
|
333
|
+
// the router. No-op if the id was never registered.
|
|
334
|
+
openWorkspace(id) {
|
|
335
|
+
if (PURE || !_workspaces[id]) return;
|
|
336
|
+
document.dispatchEvent(new CustomEvent("clacky:ext:navigate", {
|
|
337
|
+
detail: { view: "ext-workspace", params: { id } },
|
|
338
|
+
}));
|
|
180
339
|
},
|
|
181
340
|
};
|
|
182
341
|
|
|
183
|
-
//
|
|
342
|
+
// Return the per-session runtime for a mount, allocating it via `create` on
|
|
343
|
+
// first use. Global mounts (or per-session slots without `create`) return
|
|
344
|
+
// null — the render function is expected to work stateless.
|
|
345
|
+
function _getRuntime(slot, entry, ctx) {
|
|
346
|
+
if (!entry.create) return null;
|
|
347
|
+
if (!PER_SESSION_SLOTS[slot]) return null;
|
|
348
|
+
const sessionId = ctx && ctx.sessionId;
|
|
349
|
+
if (!sessionId) return null;
|
|
350
|
+
|
|
351
|
+
let bySession = _sessionRuntimes.get(sessionId);
|
|
352
|
+
if (!bySession) {
|
|
353
|
+
bySession = new Map();
|
|
354
|
+
_sessionRuntimes.set(sessionId, bySession);
|
|
355
|
+
}
|
|
356
|
+
const key = _mountKey(slot, entry);
|
|
357
|
+
let runtime = bySession.get(key);
|
|
358
|
+
if (!runtime) {
|
|
359
|
+
runtime = entry.create(ctx);
|
|
360
|
+
if (runtime === undefined) return null; // create threw (guarded)
|
|
361
|
+
bySession.set(key, runtime || {});
|
|
362
|
+
}
|
|
363
|
+
return runtime;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Run any view-level teardown collected on the previous render pass for the
|
|
367
|
+
// current session, so old event listeners / RAF loops don't leak when a tab
|
|
368
|
+
// is re-rendered or the session switches away.
|
|
369
|
+
function _runViewTeardowns(sessionId) {
|
|
370
|
+
if (!sessionId) return;
|
|
371
|
+
const map = _viewTeardowns.get(sessionId);
|
|
372
|
+
if (!map) return;
|
|
373
|
+
map.forEach((fn) => { if (typeof fn === "function") { try { fn(); } catch (_e) {} } });
|
|
374
|
+
map.clear();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function _rememberViewTeardown(sessionId, key, teardown) {
|
|
378
|
+
if (!sessionId || typeof teardown !== "function") return;
|
|
379
|
+
let map = _viewTeardowns.get(sessionId);
|
|
380
|
+
if (!map) { map = new Map(); _viewTeardowns.set(sessionId, map); }
|
|
381
|
+
map.set(key, teardown);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Invoke a renderer's render function. Handles the (container, ctx, runtime)
|
|
385
|
+
// signature used everywhere; if the render returns a function it is stored
|
|
386
|
+
// as the view teardown, run before the next render or on session exit.
|
|
387
|
+
function _invokeRender(slot, entry, container, ctx) {
|
|
388
|
+
const runtime = _getRuntime(slot, entry, ctx);
|
|
389
|
+
let node, teardown, crashed = false, reason = null;
|
|
390
|
+
try {
|
|
391
|
+
const out = entry.render(container, ctx, runtime);
|
|
392
|
+
if (out === undefined || out === null) {
|
|
393
|
+
// undefined → container mutated in-place; null → renderer opted out of
|
|
394
|
+
// this context (e.g. `if (!ctx.sessionId) return null` on the new-session
|
|
395
|
+
// page). Both render nothing; neither is an error.
|
|
396
|
+
} else if (typeof out === "function") {
|
|
397
|
+
teardown = out;
|
|
398
|
+
} else {
|
|
399
|
+
node = out;
|
|
400
|
+
}
|
|
401
|
+
} catch (err) {
|
|
402
|
+
reason = String((err && err.stack) || err);
|
|
403
|
+
console.error(`[Clacky.ext] extension "${entry.extId || "?"}" threw in render for slot ${JSON.stringify(slot)}:`, err);
|
|
404
|
+
crashed = true;
|
|
405
|
+
}
|
|
406
|
+
if (crashed) return { crashed: true, reason, extId: entry.extId };
|
|
407
|
+
if (ctx && ctx.sessionId && teardown) {
|
|
408
|
+
_rememberViewTeardown(ctx.sessionId, _mountKey(slot, entry), teardown);
|
|
409
|
+
}
|
|
410
|
+
return { node };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Build a styled, self-contained crashed-panel placeholder: a short headline
|
|
414
|
+
// plus a collapsible <details> exposing the actual reason and a nudge to the
|
|
415
|
+
// console. Never throws; the reason is rendered as plain text.
|
|
416
|
+
function _crashedPlaceholder(extId, reason) {
|
|
417
|
+
_injectCrashStyle();
|
|
418
|
+
const box = document.createElement("div");
|
|
419
|
+
box.className = "ext-slot-crashed";
|
|
420
|
+
box.setAttribute("data-ext-status", "crashed");
|
|
421
|
+
|
|
422
|
+
const head = document.createElement("div");
|
|
423
|
+
head.className = "ext-crashed-head";
|
|
424
|
+
const icon = document.createElement("span");
|
|
425
|
+
icon.className = "ext-crashed-icon";
|
|
426
|
+
icon.textContent = "!";
|
|
427
|
+
const title = document.createElement("span");
|
|
428
|
+
title.className = "ext-crashed-title";
|
|
429
|
+
title.textContent = extId ? `扩展「${extId}」渲染失败` : "扩展渲染失败";
|
|
430
|
+
head.appendChild(icon);
|
|
431
|
+
head.appendChild(title);
|
|
432
|
+
box.appendChild(head);
|
|
433
|
+
|
|
434
|
+
const hint = document.createElement("div");
|
|
435
|
+
hint.className = "ext-crashed-hint";
|
|
436
|
+
hint.textContent = "面板未能加载。展开下方详情,或打开浏览器控制台 (console) 查看完整错误。";
|
|
437
|
+
box.appendChild(hint);
|
|
438
|
+
|
|
439
|
+
if (reason) {
|
|
440
|
+
const details = document.createElement("details");
|
|
441
|
+
details.className = "ext-crashed-details";
|
|
442
|
+
const summary = document.createElement("summary");
|
|
443
|
+
summary.textContent = "错误详情";
|
|
444
|
+
const pre = document.createElement("pre");
|
|
445
|
+
pre.className = "ext-crashed-reason";
|
|
446
|
+
pre.textContent = reason;
|
|
447
|
+
details.appendChild(summary);
|
|
448
|
+
details.appendChild(pre);
|
|
449
|
+
box.appendChild(details);
|
|
450
|
+
}
|
|
451
|
+
return box;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let _crashStyleInjected = false;
|
|
455
|
+
function _injectCrashStyle() {
|
|
456
|
+
if (_crashStyleInjected) return;
|
|
457
|
+
_crashStyleInjected = true;
|
|
458
|
+
const style = document.createElement("style");
|
|
459
|
+
style.id = "ext-crashed-style";
|
|
460
|
+
style.textContent = `
|
|
461
|
+
.ext-slot-crashed {
|
|
462
|
+
margin: 12px;
|
|
463
|
+
padding: 14px 16px;
|
|
464
|
+
border: 1px solid var(--color-error-border, #fecaca);
|
|
465
|
+
background: var(--color-error-bg, #fef2f2);
|
|
466
|
+
border-radius: var(--radius-md, 8px);
|
|
467
|
+
color: var(--color-text-secondary, #56585e);
|
|
468
|
+
font-size: 13px;
|
|
469
|
+
line-height: 1.5;
|
|
470
|
+
}
|
|
471
|
+
.ext-crashed-head { display: flex; align-items: center; gap: 8px; }
|
|
472
|
+
.ext-crashed-icon {
|
|
473
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
474
|
+
width: 18px; height: 18px; flex: none;
|
|
475
|
+
border-radius: 50%;
|
|
476
|
+
background: var(--color-error, #ef4444);
|
|
477
|
+
color: #fff; font-size: 12px; font-weight: 700; line-height: 1;
|
|
478
|
+
}
|
|
479
|
+
.ext-crashed-title { font-weight: 600; color: var(--color-text-primary, #1a1b1e); }
|
|
480
|
+
.ext-crashed-hint { margin-top: 6px; color: var(--color-text-tertiary, #8a8d94); }
|
|
481
|
+
.ext-crashed-details { margin-top: 10px; }
|
|
482
|
+
.ext-crashed-details > summary {
|
|
483
|
+
cursor: pointer; user-select: none;
|
|
484
|
+
color: var(--color-error, #ef4444); font-weight: 500;
|
|
485
|
+
}
|
|
486
|
+
.ext-crashed-reason {
|
|
487
|
+
margin: 8px 0 0;
|
|
488
|
+
padding: 10px;
|
|
489
|
+
max-height: 220px; overflow: auto;
|
|
490
|
+
background: var(--color-bg-primary, #fbfbfa);
|
|
491
|
+
border: 1px solid var(--color-border-primary, #e8e8e4);
|
|
492
|
+
border-radius: var(--radius-sm, 6px);
|
|
493
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
494
|
+
font-size: 12px; line-height: 1.45;
|
|
495
|
+
white-space: pre-wrap; word-break: break-word;
|
|
496
|
+
color: var(--color-text-secondary, #56585e);
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
(document.head || document.documentElement).appendChild(style);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
184
503
|
// null agents AND null panel => global (always visible). Otherwise the
|
|
185
504
|
// current profile must be in the renderer's agent list, or in the set of
|
|
186
505
|
// agents that reference its panel.
|
|
@@ -191,6 +510,13 @@ Clacky.ext = (() => {
|
|
|
191
510
|
return false;
|
|
192
511
|
}
|
|
193
512
|
|
|
513
|
+
// What a third-party agent's extension contributes (panels + skills), from
|
|
514
|
+
// ext.yml — so the new-session page can advertise it before a session exists.
|
|
515
|
+
// Returns { panels:[{id,title,title_zh}], skills:[...] } or null for built-ins.
|
|
516
|
+
function contributionsForAgent(agentId) {
|
|
517
|
+
return (agentId && _agentContributions[agentId]) || null;
|
|
518
|
+
}
|
|
519
|
+
|
|
194
520
|
// Render every extension registered for `slot` into `container`, scoped to the
|
|
195
521
|
// current agent profile. Called by the host's view layer wherever it exposes a
|
|
196
522
|
// slot, and re-called on session switch (the container is cleared first so a
|
|
@@ -215,34 +541,25 @@ Clacky.ext = (() => {
|
|
|
215
541
|
const ordered = renderers.slice().sort((a, b) => a.order - b.order);
|
|
216
542
|
ordered.forEach((entry) => {
|
|
217
543
|
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
|
-
|
|
544
|
+
const wrap = document.createElement("div");
|
|
545
|
+
if (entry.extId) wrap.setAttribute("data-ext-id", entry.extId);
|
|
546
|
+
// Nav mounts that open a workspace declare it via opts.workspace; the
|
|
547
|
+
// host stamps it so the Router alone owns the active highlight.
|
|
548
|
+
if (entry.workspace) wrap.setAttribute("data-ext-workspace", entry.workspace);
|
|
549
|
+
const { node, crashed, reason, extId } = _invokeRender(slot, entry, wrap, renderCtx);
|
|
228
550
|
if (crashed) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
ph.setAttribute("data-ext-id", extId || "");
|
|
232
|
-
ph.className = "ext-slot-crashed";
|
|
233
|
-
ph.textContent = "Extension failed to render.";
|
|
234
|
-
container.appendChild(ph);
|
|
551
|
+
wrap.appendChild(_crashedPlaceholder(extId || entry.extId, reason));
|
|
552
|
+
container.appendChild(wrap);
|
|
235
553
|
return;
|
|
236
554
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
555
|
+
if (node != null) {
|
|
556
|
+
if (typeof node === "string") wrap.innerHTML = node;
|
|
557
|
+
else wrap.appendChild(node);
|
|
558
|
+
}
|
|
559
|
+
// If render mutated `wrap` in-place (returned nothing) or produced a
|
|
560
|
+
// node, either way we only append when there's something to show.
|
|
561
|
+
if (wrap.childNodes.length > 0 || (node != null && typeof node === "string")) {
|
|
243
562
|
container.appendChild(wrap);
|
|
244
|
-
} else {
|
|
245
|
-
container.appendChild(node);
|
|
246
563
|
}
|
|
247
564
|
});
|
|
248
565
|
}
|
|
@@ -269,17 +586,16 @@ Clacky.ext = (() => {
|
|
|
269
586
|
const bodies = document.createElement("div");
|
|
270
587
|
bodies.className = "aside-bodies";
|
|
271
588
|
|
|
272
|
-
//
|
|
589
|
+
// Always default to the first (highest-priority) tab. Tabs may register in
|
|
590
|
+
// several async passes; whichever pass runs last simply lands on tab #1.
|
|
273
591
|
const ids = renderers.map((e) => e.tab.id);
|
|
274
|
-
|
|
275
|
-
if (!active || !ids.includes(active)) active = ids[0];
|
|
592
|
+
const active = ids[0];
|
|
276
593
|
|
|
277
594
|
const tabBtns = {};
|
|
278
595
|
const bodyEls = {};
|
|
279
596
|
const rendered = {};
|
|
280
597
|
|
|
281
598
|
function activate(id) {
|
|
282
|
-
_activeTab[slot] = id;
|
|
283
599
|
Object.keys(tabBtns).forEach((k) => tabBtns[k].classList.toggle("active", k === id));
|
|
284
600
|
Object.keys(bodyEls).forEach((k) => bodyEls[k].classList.toggle("active", k === id));
|
|
285
601
|
if (!rendered[id]) {
|
|
@@ -289,16 +605,9 @@ Clacky.ext = (() => {
|
|
|
289
605
|
const localCtx = Object.assign({}, renderCtx, {
|
|
290
606
|
setBadge: (n) => _setTabBadge(tabBtns[id], n),
|
|
291
607
|
});
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
node = entry.fn(localCtx);
|
|
295
|
-
if (node === undefined) crashed = true;
|
|
296
|
-
} catch (_e) { crashed = true; }
|
|
608
|
+
const { node, crashed, reason, extId } = _invokeRender(slot, entry, body, localCtx);
|
|
297
609
|
if (crashed) {
|
|
298
|
-
|
|
299
|
-
ph.className = "ext-slot-crashed";
|
|
300
|
-
ph.textContent = "Extension failed to render.";
|
|
301
|
-
body.appendChild(ph);
|
|
610
|
+
body.appendChild(_crashedPlaceholder(extId || entry.extId, reason));
|
|
302
611
|
} else if (node != null) {
|
|
303
612
|
if (typeof node === "string") body.innerHTML = node;
|
|
304
613
|
else body.appendChild(node);
|
|
@@ -355,11 +664,38 @@ Clacky.ext = (() => {
|
|
|
355
664
|
|
|
356
665
|
// Update the current session context (host calls this on every session
|
|
357
666
|
// switch) and re-render all slots so panels match the new agent profile.
|
|
667
|
+
// Runs the previous session's view-level teardowns first (event listeners /
|
|
668
|
+
// RAF loops attached to now-dead DOM), but leaves per-session runtimes
|
|
669
|
+
// alive — the meeting recorder for session A keeps running while you're
|
|
670
|
+
// looking at B, and its captions are still there when you switch back.
|
|
358
671
|
function setContext(next) {
|
|
672
|
+
const prevSessionId = context.sessionId;
|
|
359
673
|
Object.assign(context, next || {});
|
|
674
|
+
if (prevSessionId && prevSessionId !== context.sessionId) {
|
|
675
|
+
_runViewTeardowns(prevSessionId);
|
|
676
|
+
}
|
|
360
677
|
refreshSlots();
|
|
361
678
|
}
|
|
362
679
|
|
|
680
|
+
// Called by the host when a session leaves the sidebar for good (deleted,
|
|
681
|
+
// archived, or the page is unloading). Runs every runtime's `dispose()` for
|
|
682
|
+
// that session so timers, media recorders, and network handles are released.
|
|
683
|
+
function notifySessionRemoved(sessionId) {
|
|
684
|
+
if (!sessionId) return;
|
|
685
|
+
_runViewTeardowns(sessionId);
|
|
686
|
+
_viewTeardowns.delete(sessionId);
|
|
687
|
+
const bySession = _sessionRuntimes.get(sessionId);
|
|
688
|
+
if (!bySession) return;
|
|
689
|
+
bySession.forEach((runtime) => {
|
|
690
|
+
if (runtime && typeof runtime.dispose === "function") {
|
|
691
|
+
try { runtime.dispose(); } catch (err) {
|
|
692
|
+
console.error("[Clacky.ext] runtime.dispose failed:", err);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
_sessionRuntimes.delete(sessionId);
|
|
697
|
+
}
|
|
698
|
+
|
|
363
699
|
// Re-render every named slot present in the DOM against the current context.
|
|
364
700
|
// Idempotent: each slot's container is cleared before re-rendering.
|
|
365
701
|
function refreshSlots() {
|
|
@@ -373,14 +709,21 @@ Clacky.ext = (() => {
|
|
|
373
709
|
get pure() { return PURE; },
|
|
374
710
|
context,
|
|
375
711
|
setContext,
|
|
712
|
+
notifySessionRemoved,
|
|
376
713
|
refreshSlots,
|
|
377
714
|
registerPanelAgents,
|
|
715
|
+
registerAgentContributions,
|
|
716
|
+
contributionsForAgent,
|
|
378
717
|
api,
|
|
379
718
|
ui,
|
|
380
719
|
subscribe,
|
|
381
720
|
emit,
|
|
382
721
|
renderSlot,
|
|
383
722
|
slots,
|
|
723
|
+
// Host-only: look up a registered workspace by id. Returns
|
|
724
|
+
// { id, title, render, extId } or undefined. Used by the Router when
|
|
725
|
+
// handling the `ext-workspace` view — extensions never call this.
|
|
726
|
+
_getWorkspace(id) { return _workspaces[id]; },
|
|
384
727
|
_extBegin, // used by the loader; not part of the public extension API
|
|
385
728
|
_extEnd, // used by the loader; not part of the public extension API
|
|
386
729
|
};
|