openclacky 1.5.3 → 1.5.5
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/.dockerignore +13 -0
- data/CHANGELOG.md +57 -0
- data/Dockerfile +21 -1
- data/README.md +26 -7
- data/README_CN.md +26 -7
- data/README_JA.md +26 -7
- data/lib/clacky/agent/message_compressor.rb +4 -29
- data/lib/clacky/agent/message_compressor_helper.rb +17 -16
- data/lib/clacky/agent/session_serializer.rb +7 -0
- data/lib/clacky/agent.rb +14 -2
- data/lib/clacky/agent_config.rb +10 -5
- data/lib/clacky/agent_profile.rb +25 -3
- data/lib/clacky/brand_config.rb +32 -12
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +15 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +1 -5
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +43 -34
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +9 -3
- data/lib/clacky/default_extensions/git/ext.yml +3 -0
- data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
- data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
- data/lib/clacky/default_parsers/xlsx_parser.py +66 -0
- data/lib/clacky/default_skills/new/SKILL.md +17 -12
- data/lib/clacky/extension/api_extension.rb +5 -2
- data/lib/clacky/extension/loader.rb +1 -0
- data/lib/clacky/extension/packager.rb +60 -15
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/message_format/anthropic.rb +1 -1
- data/lib/clacky/message_format/bedrock.rb +1 -1
- data/lib/clacky/message_format/open_ai.rb +34 -3
- data/lib/clacky/providers.rb +77 -8
- data/lib/clacky/server/http_server.rb +495 -39
- data/lib/clacky/server/project_manager.rb +150 -0
- data/lib/clacky/server/server_master.rb +7 -0
- data/lib/clacky/server/session_registry.rb +14 -1
- data/lib/clacky/server/web_ui_controller.rb +12 -2
- data/lib/clacky/session_manager.rb +6 -4
- data/lib/clacky/tools/file_reader.rb +2 -1
- data/lib/clacky/tools/terminal.rb +30 -3
- data/lib/clacky/utils/model_pricing.rb +69 -18
- data/lib/clacky/utils/parser_manager.rb +115 -7
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1216 -318
- data/lib/clacky/web/app.js +27 -4
- data/lib/clacky/web/components/notify.js +139 -22
- data/lib/clacky/web/core/aside.js +21 -0
- data/lib/clacky/web/features/brand/store.js +3 -1
- data/lib/clacky/web/features/brand/view.js +29 -5
- data/lib/clacky/web/features/extensions/store.js +214 -9
- data/lib/clacky/web/features/extensions/view.js +156 -11
- data/lib/clacky/web/features/new-session/store.js +25 -3
- data/lib/clacky/web/features/new-session/view.js +339 -142
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/features/trash/store.js +23 -9
- data/lib/clacky/web/features/trash/view.js +51 -0
- data/lib/clacky/web/i18n.js +124 -15
- data/lib/clacky/web/index.html +105 -79
- data/lib/clacky/web/projects.js +1143 -0
- data/lib/clacky/web/sessions.js +320 -538
- data/lib/clacky/web/settings.js +138 -5
- data/lib/clacky/web/utils.js +1 -0
- data/lib/clacky/web/ws-dispatcher.js +98 -4
- data/scripts/build/src/install_system_deps.sh.cc +11 -3
- data/scripts/install_system_deps.sh +11 -3
- metadata +5 -2
- data/lib/clacky/default_parsers/xlsx_parser.rb +0 -121
data/lib/clacky/agent_profile.rb
CHANGED
|
@@ -53,8 +53,11 @@ module Clacky
|
|
|
53
53
|
|
|
54
54
|
# List all available agent profiles across user + extension layers.
|
|
55
55
|
# Precedence on id collision: user override → extension unit.
|
|
56
|
+
# @param recency [Hash<String, Integer>] agent id => epoch of last use,
|
|
57
|
+
# used to order third-party extension agents by most-recently-used
|
|
58
|
+
# instead of their static `order` field.
|
|
56
59
|
# @return [Array<Hash>] each: { id:, title:, title_zh:, description:, description_zh:, source:, order:, layer:, author:, avatar: }
|
|
57
|
-
def self.all
|
|
60
|
+
def self.all(recency: {})
|
|
58
61
|
out = {}
|
|
59
62
|
|
|
60
63
|
add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author, avatar|
|
|
@@ -76,6 +79,7 @@ module Clacky
|
|
|
76
79
|
ext_result = ExtensionLoader.last_result || ExtensionLoader.load_all
|
|
77
80
|
ext_result&.agents&.each do |unit|
|
|
78
81
|
spec = unit.spec || {}
|
|
82
|
+
next if spec["hidden"]
|
|
79
83
|
title = spec["title"].to_s
|
|
80
84
|
title = unit.id if title.empty?
|
|
81
85
|
avatar = spec["avatar_abs"].to_s.empty? ? nil : "/agent_avatar/#{unit.id}"
|
|
@@ -102,8 +106,26 @@ module Clacky
|
|
|
102
106
|
)
|
|
103
107
|
end
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
# Builtin agents always come first, then user overrides, then third-party
|
|
110
|
+
# extension agents — so the "start new session" grid keeps the official
|
|
111
|
+
# agents grouped ahead of anything the user installed from the market.
|
|
112
|
+
# Within the third-party group, agents are ordered by most-recently-used
|
|
113
|
+
# first (via `recency`), falling back to their static `order` field for
|
|
114
|
+
# agents that have never been used yet.
|
|
115
|
+
layer_rank = lambda do |a|
|
|
116
|
+
next 0 if a[:layer] == "builtin"
|
|
117
|
+
next 1 if a[:source] == "user"
|
|
118
|
+
|
|
119
|
+
2
|
|
120
|
+
end
|
|
121
|
+
out.values.sort_by do |a|
|
|
122
|
+
rank = layer_rank.call(a)
|
|
123
|
+
if rank == 2
|
|
124
|
+
[rank, -(recency[a[:id]] || 0), a[:order] || 999, a[:id]]
|
|
125
|
+
else
|
|
126
|
+
[rank, a[:order] || 999, a[:id]]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
107
129
|
end
|
|
108
130
|
|
|
109
131
|
private_class_method def self.read_profile_yml(path)
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -383,16 +383,16 @@ module Clacky
|
|
|
383
383
|
|
|
384
384
|
# Returns true when a public distribution refresh is due.
|
|
385
385
|
#
|
|
386
|
-
# Refresh is needed
|
|
387
|
-
#
|
|
388
|
-
# distribution data via #heartbeat
|
|
386
|
+
# Refresh is needed for unactivated branded installs and for private
|
|
387
|
+
# platform sources that have not supplied their deployment brand yet.
|
|
388
|
+
# Activated licenses already get fresh distribution data via #heartbeat!.
|
|
389
389
|
#
|
|
390
390
|
# Rate limit: once every HEARTBEAT_INTERVAL (24h), measured from the last
|
|
391
391
|
# SUCCESSFUL refresh. A failed refresh does not advance the clock so we'll
|
|
392
392
|
# keep trying on subsequent startups / status polls.
|
|
393
393
|
def distribution_refresh_due?
|
|
394
|
-
return false unless branded?
|
|
395
394
|
return false if activated?
|
|
395
|
+
return false unless branded? || private_platform_source?
|
|
396
396
|
return true if @distribution_last_refreshed_at.nil?
|
|
397
397
|
|
|
398
398
|
elapsed = Time.now.utc - @distribution_last_refreshed_at
|
|
@@ -400,7 +400,8 @@ module Clacky
|
|
|
400
400
|
end
|
|
401
401
|
|
|
402
402
|
# Refresh public brand assets (logo, theme, homepage_url, support_*) for
|
|
403
|
-
# installs
|
|
403
|
+
# unactivated installs. Package installs use an exact package lookup;
|
|
404
|
+
# private platform sources without a local brand use the source default.
|
|
404
405
|
#
|
|
405
406
|
# Motivation: `install.sh --brand-name=X --command=X` only writes
|
|
406
407
|
# product_name + package_name to brand.yml. The rest of the distribution
|
|
@@ -409,32 +410,47 @@ module Clacky
|
|
|
409
410
|
# anonymous public lookup endpoint.
|
|
410
411
|
#
|
|
411
412
|
# Behaviour:
|
|
412
|
-
# * No-op
|
|
413
|
-
#
|
|
413
|
+
# * No-op when already activated, or when neither a package_name nor a
|
|
414
|
+
# private platform source is available.
|
|
414
415
|
# * On success: apply_distribution + save + stamp
|
|
415
416
|
# @distribution_last_refreshed_at.
|
|
417
|
+
# * When a block is provided, discard the response unless it still
|
|
418
|
+
# confirms that the platform source is current.
|
|
416
419
|
# * On failure: log and return without touching the timestamp (so we
|
|
417
420
|
# retry on next trigger).
|
|
418
421
|
#
|
|
419
422
|
# Returns { success: Boolean, message: String }.
|
|
420
423
|
def refresh_distribution!
|
|
421
|
-
|
|
424
|
+
source_brand_lookup = private_platform_source? &&
|
|
425
|
+
(@package_name.nil? || @package_name.strip.empty?)
|
|
426
|
+
unless branded? || source_brand_lookup
|
|
422
427
|
return { success: false, message: "Not branded" }
|
|
423
428
|
end
|
|
424
429
|
if activated?
|
|
425
430
|
return { success: false, message: "License activated — use heartbeat! instead" }
|
|
426
431
|
end
|
|
427
|
-
if @package_name.nil? || @package_name.strip.empty?
|
|
432
|
+
if !source_brand_lookup && (@package_name.nil? || @package_name.strip.empty?)
|
|
428
433
|
return { success: false, message: "package_name not configured" }
|
|
429
434
|
end
|
|
430
435
|
|
|
431
|
-
|
|
432
|
-
|
|
436
|
+
if source_brand_lookup
|
|
437
|
+
path = "/api/v1/distributions/lookup"
|
|
438
|
+
else
|
|
439
|
+
encoded_pkg = URI.encode_www_form_component(@package_name.strip)
|
|
440
|
+
path = "/api/v1/distributions/lookup?package_name=#{encoded_pkg}"
|
|
441
|
+
end
|
|
433
442
|
|
|
434
|
-
Clacky::Logger.info(
|
|
443
|
+
Clacky::Logger.info(
|
|
444
|
+
"[Brand] refresh_distribution! fetching — package_name=#{@package_name || "source-default"}"
|
|
445
|
+
)
|
|
435
446
|
response = platform_client.get(path)
|
|
436
447
|
|
|
437
448
|
if response[:success] && response[:data].is_a?(Hash) && response[:data]["distribution"].is_a?(Hash)
|
|
449
|
+
if block_given? && !yield
|
|
450
|
+
Clacky::Logger.info("[Brand] refresh_distribution! discarded — platform source changed")
|
|
451
|
+
return { success: false, message: "Platform source changed during refresh" }
|
|
452
|
+
end
|
|
453
|
+
|
|
438
454
|
apply_distribution(response[:data]["distribution"])
|
|
439
455
|
@distribution_last_refreshed_at = Time.now.utc
|
|
440
456
|
save
|
|
@@ -1762,6 +1778,10 @@ module Clacky
|
|
|
1762
1778
|
@homepage_url = dist["homepage_url"] if dist.key?("homepage_url")
|
|
1763
1779
|
end
|
|
1764
1780
|
|
|
1781
|
+
private def private_platform_source?
|
|
1782
|
+
!ENV["CLACKY_LICENSE_SERVER"].to_s.strip.empty?
|
|
1783
|
+
end
|
|
1784
|
+
|
|
1765
1785
|
# Download a remote URL to a local file path.
|
|
1766
1786
|
#
|
|
1767
1787
|
# Deprecated: this method now delegates to
|
|
@@ -143,3 +143,18 @@ main product, and apply every rule below whenever you propose or write code:
|
|
|
143
143
|
Ruby and carry supply-chain risk.
|
|
144
144
|
- After editing `view.js`, `handler.rb`, or a `SKILL.md`, tell the user to reload the
|
|
145
145
|
WebUI page — hot reload is per-request, no restart needed.
|
|
146
|
+
- After you finish editing extension files, call this once at the very end to trigger
|
|
147
|
+
a reload button in the UI (if it doesn't appear, the user can manually refresh the browser):
|
|
148
|
+
```
|
|
149
|
+
curl -s --noproxy '*' -X POST "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/ui/show_ext_refresh" \
|
|
150
|
+
-H "Content-Type: application/json" \
|
|
151
|
+
-d "{\"session_id\": \"${CLACKY_SESSION_ID}\"}"
|
|
152
|
+
```
|
|
153
|
+
(`--noproxy '*'` prevents shell proxy env vars from silently intercepting this loopback call.)
|
|
154
|
+
- If the extension contributes a `session.aside` panel, also call this right after the above
|
|
155
|
+
to open the panel automatically:
|
|
156
|
+
```
|
|
157
|
+
curl -s --noproxy '*' -X POST "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/ui/open_aside" \
|
|
158
|
+
-H "Content-Type: application/json" \
|
|
159
|
+
-d "{\"session_id\": \"${CLACKY_SESSION_ID}\"}"
|
|
160
|
+
```
|
|
@@ -201,11 +201,7 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
201
201
|
error!("extension not found: #{ext_id}", status: 404) unless container
|
|
202
202
|
|
|
203
203
|
readme_path = File.join(container[:dir], "README.md")
|
|
204
|
-
|
|
205
|
-
File.delete(readme_path) if File.exist?(readme_path)
|
|
206
|
-
else
|
|
207
|
-
File.write(readme_path, readme, encoding: "utf-8")
|
|
208
|
-
end
|
|
204
|
+
File.write(readme_path, readme, encoding: "utf-8")
|
|
209
205
|
end
|
|
210
206
|
|
|
211
207
|
json(ok: true, ext_id: ext_id, saved_to: published ? "platform" : "local")
|
|
@@ -19,8 +19,8 @@ contributes:
|
|
|
19
19
|
- id: ext-developer
|
|
20
20
|
title: Extension Developer
|
|
21
21
|
title_zh: 扩展开发
|
|
22
|
-
description: AI expert that helps you build
|
|
23
|
-
description_zh:
|
|
22
|
+
description: AI expert that helps you build custom AI agent workflows
|
|
23
|
+
description_zh: 帮你开发自定义 AI Agent 工作流的专家
|
|
24
24
|
order: 5
|
|
25
25
|
prompt: agents/ext-developer/system_prompt.md
|
|
26
26
|
avatar: agents/ext-developer/avatar.png
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"extlist.newExt.label": "Create a new extension",
|
|
147
147
|
"extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
|
|
148
148
|
"extlist.newExt.btn": "Create New Extension",
|
|
149
|
-
"extlist.newExt.seed": "I want to build a new
|
|
149
|
+
"extlist.newExt.seed": "I want to build a new extension. Help me get started: ask me what the extension should do, then scaffold it and build it out step by step.",
|
|
150
150
|
"unit.panel": "panel",
|
|
151
151
|
"unit.panels": "panels",
|
|
152
152
|
"unit.agent": "agent",
|
|
@@ -331,7 +331,7 @@
|
|
|
331
331
|
"extlist.newExt.label": "新建扩展",
|
|
332
332
|
"extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
|
|
333
333
|
"extlist.newExt.btn": "新建扩展",
|
|
334
|
-
"extlist.newExt.seed": "
|
|
334
|
+
"extlist.newExt.seed": "我想开发一个新的扩展。请引导我开始:先问清楚这个扩展要做什么,然后帮我搭好骨架并一步步开发出来。",
|
|
335
335
|
"unit.panel": "个面板",
|
|
336
336
|
"unit.panels": "个面板",
|
|
337
337
|
"unit.agent": "个 Agent",
|
|
@@ -1539,7 +1539,7 @@
|
|
|
1539
1539
|
let data = {};
|
|
1540
1540
|
try { data = await res.json(); } catch (_e) {}
|
|
1541
1541
|
if (!res.ok) throw new Error(data?.error || `Error ${res.status}`);
|
|
1542
|
-
|
|
1542
|
+
location.reload();
|
|
1543
1543
|
} catch (e) {
|
|
1544
1544
|
alert(t("err.generic", { msg: e.message }));
|
|
1545
1545
|
btn.disabled = false;
|
|
@@ -1567,28 +1567,29 @@
|
|
|
1567
1567
|
return box;
|
|
1568
1568
|
}
|
|
1569
1569
|
|
|
1570
|
-
function rebuild() {
|
|
1570
|
+
function rebuild(headerActions) {
|
|
1571
1571
|
if (!container) return;
|
|
1572
1572
|
container.innerHTML = "";
|
|
1573
1573
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1574
|
+
if (headerActions) {
|
|
1575
|
+
headerActions.innerHTML = "";
|
|
1576
|
+
headerActions.appendChild(el("button", {
|
|
1577
|
+
class: "studio-btn studio-btn-primary",
|
|
1578
|
+
text: t("extlist.newExt.btn"),
|
|
1579
|
+
onclick: () => createExtension(t("extlist.newExt.seed")),
|
|
1580
|
+
}));
|
|
1581
|
+
}
|
|
1581
1582
|
|
|
1582
1583
|
container.appendChild(section("extlist.section.cloud", "extlist.section.cloudHint", cloud, cloudCard, "extlist.cloud.empty"));
|
|
1583
1584
|
container.appendChild(section("extlist.section.local", "extlist.section.localHint", store.extensions, localCard, "extlist.local.empty"));
|
|
1584
1585
|
}
|
|
1585
1586
|
|
|
1586
1587
|
return {
|
|
1587
|
-
async attach(root) {
|
|
1588
|
+
async attach(root, headerActions) {
|
|
1588
1589
|
container = el("div", { class: "studio-panel" });
|
|
1589
1590
|
root.appendChild(container);
|
|
1590
1591
|
container.innerHTML = _skeletonHtml();
|
|
1591
|
-
try { await reload(); rebuild(); }
|
|
1592
|
+
try { await reload(); rebuild(headerActions); }
|
|
1592
1593
|
catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
|
|
1593
1594
|
},
|
|
1594
1595
|
};
|
|
@@ -1723,24 +1724,27 @@
|
|
|
1723
1724
|
return box;
|
|
1724
1725
|
}
|
|
1725
1726
|
|
|
1726
|
-
function rebuild() {
|
|
1727
|
+
function rebuild(headerActions) {
|
|
1727
1728
|
if (!container) return;
|
|
1728
1729
|
container.innerHTML = "";
|
|
1729
1730
|
|
|
1730
|
-
if (
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1731
|
+
if (headerActions) {
|
|
1732
|
+
headerActions.innerHTML = "";
|
|
1733
|
+
const newBtn = el("button", {
|
|
1734
|
+
class: "studio-btn studio-btn-primary",
|
|
1735
|
+
text: t("skills.newSkill.btn"),
|
|
1736
|
+
onclick: () => createSkill(null),
|
|
1737
|
+
});
|
|
1738
|
+
newBtn.disabled = locked;
|
|
1739
|
+
newBtn.title = locked ? t("skills.locked") : "";
|
|
1740
|
+
headerActions.appendChild(newBtn);
|
|
1735
1741
|
}
|
|
1736
1742
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
newBox.appendChild(newBar);
|
|
1743
|
-
container.appendChild(newBox);
|
|
1743
|
+
if (locked) {
|
|
1744
|
+
const lockHint = el("p", { class: "studio-empty", text: t("skills.locked") });
|
|
1745
|
+
lockHint.style.marginBottom = "12px";
|
|
1746
|
+
container.appendChild(lockHint);
|
|
1747
|
+
}
|
|
1744
1748
|
|
|
1745
1749
|
const cloudEmptyKey = locked ? "skills.cloud.locked" : "skills.cloud.empty";
|
|
1746
1750
|
container.appendChild(section("skills.section.cloud", "skills.section.cloudHint", cloud, cloudCard, cloudEmptyKey));
|
|
@@ -1748,11 +1752,11 @@
|
|
|
1748
1752
|
}
|
|
1749
1753
|
|
|
1750
1754
|
return {
|
|
1751
|
-
async attach(root) {
|
|
1755
|
+
async attach(root, headerActions) {
|
|
1752
1756
|
container = el("div", { class: "studio-panel" });
|
|
1753
1757
|
root.appendChild(container);
|
|
1754
1758
|
container.innerHTML = _skeletonHtml();
|
|
1755
|
-
try { await reload(); rebuild(); }
|
|
1759
|
+
try { await reload(); rebuild(headerActions); }
|
|
1756
1760
|
catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
|
|
1757
1761
|
},
|
|
1758
1762
|
};
|
|
@@ -1768,6 +1772,8 @@
|
|
|
1768
1772
|
|
|
1769
1773
|
const header = el("div", { class: "studio-page-head" });
|
|
1770
1774
|
header.appendChild(el("h2", { class: "studio-page-title", text: t("ws.title") }));
|
|
1775
|
+
const headerActions = el("div", { class: "studio-page-head-actions" });
|
|
1776
|
+
header.appendChild(headerActions);
|
|
1771
1777
|
page.appendChild(header);
|
|
1772
1778
|
|
|
1773
1779
|
const tabsBar = el("div", { class: "studio-tabs" });
|
|
@@ -1785,8 +1791,9 @@
|
|
|
1785
1791
|
active = id;
|
|
1786
1792
|
Array.from(tabsBar.children).forEach((b) => b.classList.toggle("studio-tab-active", b.dataset.tab === id));
|
|
1787
1793
|
body.innerHTML = "";
|
|
1794
|
+
headerActions.innerHTML = "";
|
|
1788
1795
|
const tab = tabs.find((t2) => t2.id === id);
|
|
1789
|
-
if (tab) tab.build(body);
|
|
1796
|
+
if (tab) tab.build(body, headerActions);
|
|
1790
1797
|
}
|
|
1791
1798
|
|
|
1792
1799
|
tabs.forEach((tab) => {
|
|
@@ -1799,12 +1806,12 @@
|
|
|
1799
1806
|
select("extensions");
|
|
1800
1807
|
}
|
|
1801
1808
|
|
|
1802
|
-
function buildExtensionsTab(root) {
|
|
1803
|
-
createExtensionsPanel().attach(root);
|
|
1809
|
+
function buildExtensionsTab(root, headerActions) {
|
|
1810
|
+
createExtensionsPanel().attach(root, headerActions);
|
|
1804
1811
|
}
|
|
1805
1812
|
|
|
1806
|
-
function buildSkillsTab(root) {
|
|
1807
|
-
createSkillsPanel().attach(root);
|
|
1813
|
+
function buildSkillsTab(root, headerActions) {
|
|
1814
|
+
createSkillsPanel().attach(root, headerActions);
|
|
1808
1815
|
}
|
|
1809
1816
|
|
|
1810
1817
|
function navRow(labelKey, onClick) {
|
|
@@ -1904,8 +1911,10 @@
|
|
|
1904
1911
|
.studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 0; font-size: 12px; border-bottom: 1px solid var(--color-border-primary); }
|
|
1905
1912
|
.studio-published-name { display: flex; align-items: center; gap: 6px; color: var(--color-text-primary); }
|
|
1906
1913
|
.studio-page { width: 100%; }
|
|
1907
|
-
.studio-page-head { margin-bottom: 20px; }
|
|
1914
|
+
.studio-page-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
|
1915
|
+
.studio-page-head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
1908
1916
|
.studio-page-title { margin: 0; font-size: 22px; font-weight: 600; color: var(--color-text-primary); }
|
|
1917
|
+
.studio-btn-sm { padding: 5px 12px; font-size: 12px; }
|
|
1909
1918
|
.studio-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--color-border-primary); margin-bottom: 24px; }
|
|
1910
1919
|
.studio-tab { padding: 8px 16px; border: none; background: transparent; color: var(--color-text-tertiary); cursor: pointer; font-size: 14px; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
|
1911
1920
|
.studio-tab:hover { color: var(--color-text-primary); }
|
|
@@ -24,7 +24,7 @@ Seven `contributes:` types (use one, several, or all):
|
|
|
24
24
|
- `panels` — WebUI panels (a `view.js`, no build step, no React, no iframe)
|
|
25
25
|
- `api` — one backend file `api/handler.rb`, mounted at `/api/ext/<id>/`
|
|
26
26
|
- `skills` — a `SKILL.md` under `skills/<id>/` (prompt-only capability)
|
|
27
|
-
- `agents` — a `system_prompt.md`; can reference `panels: [id]` and `skills: [id]`
|
|
27
|
+
- `agents` — a `system_prompt.md`; can reference `panels: [id]` and `skills: [id]`; `hidden: true` hides from the new-session picker
|
|
28
28
|
- `channels` — an IM adapter
|
|
29
29
|
- `patches` — monkey-patch a real class (advanced, supply-chain risk)
|
|
30
30
|
- `hooks` — lifecycle hooks like `before_tool_use` (advanced)
|
|
@@ -92,10 +92,12 @@ exactly three capabilities:
|
|
|
92
92
|
|
|
93
93
|
```js
|
|
94
94
|
Clacky.ext.ui.mount(slot, spec, opts) // inject UI into a named slot
|
|
95
|
-
Clacky.ext.subscribe(event, handler) // observe
|
|
95
|
+
Clacky.ext.subscribe(event, handler) // observe store events + live session events (read-only)
|
|
96
96
|
Clacky.ext.api.register(name, fn) // expose a named data source; api.resolve(name)
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
**`subscribe(event, handler)`** - two event families: (1) host **store** events (`skills:changed`, `tasks:changed`, `profile:changed`, …) and (2) **live session events** mirrored from the WebSocket stream — conversation (`session:assistant-message`, `session:tool-call`, `session:tool-result`), status/errors (`session:error`, `session:warning`, `session:update`), lifecycle (`session:renamed`, `session:deleted`, …). Payload is `{ sessionId, ...wsFields }`. Full list in the "Core Events" section of the extend-webui docs. Handlers are read-only.
|
|
100
|
+
|
|
99
101
|
**`ui.mount(slot, spec, opts)`** — `spec` is either `(container, ctx, runtime) => …` or
|
|
100
102
|
`{ create?, render }`. The render function:
|
|
101
103
|
- gets a host-owned `container` DOM element — append into it, **or** return a Node / HTML
|
|
@@ -204,6 +206,10 @@ Response helpers: `json` / `text(str)` / `send_data(bytes, content_type:, filena
|
|
|
204
206
|
- Drive sessions from the backend: `create_session(prompt:, profile:, …)`,
|
|
205
207
|
`submit_task(session_id, prompt)`, `dispatch_to_session(session_id, prompt)` (runs a
|
|
206
208
|
side task on a fork and returns its reply without touching the conversation).
|
|
209
|
+
- `create_session(hidden: true)` creates a session the openclacky session list hides
|
|
210
|
+
and the 200-session cleanup skips - use it for dedicated extension sessions you
|
|
211
|
+
manage yourself. Still reachable by `session_id` via `submit_task` /
|
|
212
|
+
`dispatch_to_session` / `registry.with_session`; forking resets `hidden` to false.
|
|
207
213
|
- Public (no-auth) endpoints: call `public_endpoint("/path")` in the class **and** set
|
|
208
214
|
`public: true` at ext.yml top level — both are required.
|
|
209
215
|
|
|
@@ -294,7 +300,7 @@ contributes:
|
|
|
294
300
|
classes; swap the body for the real UI, POST to your own route:
|
|
295
301
|
|
|
296
302
|
```js
|
|
297
|
-
Clacky.ext.ui.mount("session.aside", function (ctx) {
|
|
303
|
+
Clacky.ext.ui.mount("session.aside", function (container, ctx) {
|
|
298
304
|
var el = document.createElement("div");
|
|
299
305
|
el.style.padding = "16px";
|
|
300
306
|
var input = document.createElement("input");
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
id: git
|
|
2
2
|
name: Git
|
|
3
|
+
display_name: Git
|
|
4
|
+
display_name_zh: Git
|
|
3
5
|
description: Changes panel — a friendly view of what the AI changed in the working tree
|
|
6
|
+
description_zh: 变更面板 — 直观查看 AI 在工作区的每一处修改
|
|
4
7
|
version: "0.1.0"
|
|
5
8
|
author: OpenClacky
|
|
6
9
|
homepage: https://www.openclacky.com
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
id: time_machine
|
|
2
2
|
name: Time Machine
|
|
3
|
+
display_name: Time Machine
|
|
4
|
+
display_name_zh: 时间机器
|
|
3
5
|
description: Vertical timeline of session tasks with per-task diff and one-click restore
|
|
6
|
+
description_zh: 会话任务时间轴,支持按任务查看差异并一键还原
|
|
4
7
|
version: "0.1.0"
|
|
5
8
|
author: OpenClacky
|
|
6
9
|
homepage: https://www.openclacky.com
|
|
@@ -9,3 +12,4 @@ contributes:
|
|
|
9
12
|
panels:
|
|
10
13
|
- id: time_machine
|
|
11
14
|
view: panels/time_machine/view.js
|
|
15
|
+
attach: ["*"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""xlsx_parser.py — extract an XLSX/XLS workbook to Markdown tables.
|
|
3
|
+
|
|
4
|
+
Usage: python3 xlsx_parser.py <file_path>
|
|
5
|
+
Output: stdout — Markdown tables (UTF-8), one section per non-empty sheet
|
|
6
|
+
stderr — error messages
|
|
7
|
+
exit 0 success / 1 failure / 2 dependency missing
|
|
8
|
+
|
|
9
|
+
Uses openpyxl in read_only mode: a streaming reader that keeps memory
|
|
10
|
+
constant regardless of sheet size, so multi-megabyte worksheets parse in
|
|
11
|
+
seconds instead of exhausting RAM/CPU like a full DOM parse would.
|
|
12
|
+
"""
|
|
13
|
+
# VERSION: 2
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_table(rows):
|
|
18
|
+
width = max(len(r) for r in rows)
|
|
19
|
+
lines = []
|
|
20
|
+
for i, r in enumerate(rows):
|
|
21
|
+
padded = r + [""] * (width - len(r))
|
|
22
|
+
lines.append("| " + " | ".join(padded) + " |")
|
|
23
|
+
if i == 0:
|
|
24
|
+
lines.append("|" + " --- |" * width)
|
|
25
|
+
return "\n".join(lines)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
if len(sys.argv) < 2:
|
|
30
|
+
sys.stderr.write("Usage: xlsx_parser.py <file_path>\n")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
path = sys.argv[1]
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from openpyxl import load_workbook
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
sys.stderr.write(f"openpyxl not installed: {e}\n")
|
|
38
|
+
sys.stderr.write("Install with: pip3 install --user openpyxl\n")
|
|
39
|
+
sys.exit(2)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
wb = load_workbook(path, read_only=True, data_only=True)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
sys.stderr.write(f"Failed to open workbook: {e}\n")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
sections = []
|
|
48
|
+
for ws in wb.worksheets:
|
|
49
|
+
rows = []
|
|
50
|
+
for row in ws.iter_rows(values_only=True):
|
|
51
|
+
cells = ["" if v is None else str(v) for v in row]
|
|
52
|
+
if any(c != "" for c in cells):
|
|
53
|
+
rows.append(cells)
|
|
54
|
+
if rows:
|
|
55
|
+
sections.append(f"### {ws.title}\n\n{build_table(rows)}")
|
|
56
|
+
wb.close()
|
|
57
|
+
|
|
58
|
+
if not sections:
|
|
59
|
+
sys.stderr.write("Spreadsheet appears to be empty\n")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
sys.stdout.write("\n\n".join(sections))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|
|
@@ -17,20 +17,25 @@ When user wants to create a new Rails project:
|
|
|
17
17
|
## Process Steps
|
|
18
18
|
|
|
19
19
|
### 0. Ask Project Type and Requirement
|
|
20
|
-
Before doing anything,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
Before doing anything, call `request_user_feedback` with a structured prompt. Use the following as a guide:
|
|
21
|
+
|
|
22
|
+
zh:
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"question": "请选择项目类型,并用一句话描述你想做什么:",
|
|
26
|
+
"context": "开始新项目前需要知道项目类型和需求,请先完成填写。",
|
|
27
|
+
"options": ["⚡ Demo — 无数据库,AI 自由发挥,快速原型", "🏗️ Production — 真实应用,可部署,完整 Rails 配置"]
|
|
28
|
+
}
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
en:
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"question": "Choose a project type and describe what you want to build in one sentence:",
|
|
35
|
+
"context": "Tell us what kind of project to create so we can get started.",
|
|
36
|
+
"options": ["⚡ Demo — no database, AI builds freely, quick prototype", "🏗️ Production — real app, ready to deploy, full Rails setup"]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
34
39
|
|
|
35
40
|
**Based on user's choice:**
|
|
36
41
|
- If **Demo**: do NOT follow the Rails setup steps below. Instead, freely build a simple HTML/CSS/JS (or React) prototype directly in the working directory based on their requirement. Use your creativity.
|
|
@@ -263,7 +263,7 @@ module Clacky
|
|
|
263
263
|
# submitted immediately (the session starts running); display_message
|
|
264
264
|
# controls the user-facing bubble shown in place of the raw prompt.
|
|
265
265
|
def create_session(name: nil, prompt: nil, working_dir: nil, profile: "general",
|
|
266
|
-
source: :manual, display_message: nil)
|
|
266
|
+
source: :manual, display_message: nil, hidden: false)
|
|
267
267
|
error!("server not ready", status: 503) unless @http_server
|
|
268
268
|
|
|
269
269
|
session_id = @http_server.send(
|
|
@@ -271,11 +271,14 @@ module Clacky
|
|
|
271
271
|
name: name,
|
|
272
272
|
working_dir: working_dir,
|
|
273
273
|
profile: profile,
|
|
274
|
-
source: source
|
|
274
|
+
source: source,
|
|
275
|
+
hidden: hidden
|
|
275
276
|
)
|
|
276
277
|
|
|
277
278
|
submit_task(session_id, prompt, display_message: display_message) if prompt && !prompt.strip.empty?
|
|
278
279
|
|
|
280
|
+
@http_server.send(:broadcast_session_update, session_id)
|
|
281
|
+
|
|
279
282
|
session_id
|
|
280
283
|
end
|
|
281
284
|
|
|
@@ -422,6 +422,7 @@ module Clacky
|
|
|
422
422
|
"description" => spec["description"].to_s,
|
|
423
423
|
"description_zh" => spec["description_zh"].to_s,
|
|
424
424
|
"order" => spec["order"],
|
|
425
|
+
"hidden" => !!spec["hidden"],
|
|
425
426
|
"author" => container[:author],
|
|
426
427
|
"homepage" => container[:homepage],
|
|
427
428
|
"license" => container[:license],
|