openclacky 1.5.4 → 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/CHANGELOG.md +31 -0
- data/lib/clacky/agent/message_compressor_helper.rb +4 -1
- data/lib/clacky/agent/session_serializer.rb +1 -0
- data/lib/clacky/agent.rb +1 -1
- data/lib/clacky/agent_config.rb +4 -2
- data/lib/clacky/agent_profile.rb +25 -3
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +42 -33
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +4 -2
- 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_skills/new/SKILL.md +17 -12
- data/lib/clacky/extension/api_extension.rb +2 -0
- 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 +8 -0
- data/lib/clacky/providers.rb +61 -0
- data/lib/clacky/server/http_server.rb +154 -15
- data/lib/clacky/server/web_ui_controller.rb +12 -2
- data/lib/clacky/session_manager.rb +1 -0
- data/lib/clacky/tools/terminal.rb +30 -3
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +301 -171
- data/lib/clacky/web/app.js +16 -2
- 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 +337 -140
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/i18n.js +29 -11
- data/lib/clacky/web/index.html +36 -64
- data/lib/clacky/web/sessions.js +2 -1
- data/lib/clacky/web/utils.js +1 -0
- data/lib/clacky/web/ws-dispatcher.js +39 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5803d9775474800f6f61d6771837ebe90597080e0f068a9dff49d5d44fdc90d1
|
|
4
|
+
data.tar.gz: f111f2b083635876cd2e4477e5a64da14245684c5c18a5f5f23860541ac9def0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0cc2a8397b5165b941a5aec4f1fc72770d47fdee4b24b72d862b12154d6a9b8114563b2e977d64e43a39c86d278fb5bbb97b045357c0a00c2f818f6466dbbf8
|
|
7
|
+
data.tar.gz: '07913be97ae9966164649f88a8e8abc60346c80038aec65886c51d9552e75eaf1050e3a9cc6fc6e28d1137d00d9fabcf735a8ac195bc3c5554890a6a8c0a073d'
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [1.5.5] - 2026-08-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Ollama (Cloud) provider preset — connect to self-hosted Ollama instances with one click
|
|
11
|
+
- Extension WebSocket bridge — extensions can now subscribe to session events via `Clacky.ext` bus
|
|
12
|
+
- Hidden field support for extension agent profiles
|
|
13
|
+
- Max reasoning effort level beyond `xhigh` (supports `max` tier)
|
|
14
|
+
- Smart terminal output truncation — keeps head and tail of long output instead of cutting at the end
|
|
15
|
+
- Time machine panel now attached to all views
|
|
16
|
+
- Persist sort and installed filter selection across sessions in extensions panel
|
|
17
|
+
- New extension button added to extensions management page header
|
|
18
|
+
- Auto cleanup of trash sessions after 7 days
|
|
19
|
+
|
|
20
|
+
### Improved
|
|
21
|
+
- Extensions panel redesigned with system/market tabs and toggle switch
|
|
22
|
+
- New-session page: replaced advanced modal with inline context bar chips; agents refresh on open with localized badges, ordered by most-recently-used
|
|
23
|
+
- Overflow tooltip shown on truncated skill descriptions
|
|
24
|
+
- Compression: idle and threshold switches are now separate controls
|
|
25
|
+
- Extension install is now async with progress polling
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Ollama base URL corrected from `/api` to root domain
|
|
29
|
+
- Extension WebSocket bridge included `hidden` field in verifier AGENT_KEYS
|
|
30
|
+
- User message files render immediately in Web UI; local disk images proxied correctly
|
|
31
|
+
- Sidebar visibility persisted across page reloads
|
|
32
|
+
- Brand logo injected server-side to eliminate flash on load
|
|
33
|
+
- Extension uninstall 404 resolved by normalizing slug field
|
|
34
|
+
- Extension homepage link color variable corrected
|
|
35
|
+
- `request_user_feedback` skill uses structured JSON example to avoid literal `\n` in output
|
|
36
|
+
- `create_session` in extensions now broadcasts session update correctly
|
|
37
|
+
|
|
7
38
|
## [1.5.4] - 2026-08-01
|
|
8
39
|
|
|
9
40
|
### Added
|
|
@@ -23,6 +23,7 @@ module Clacky
|
|
|
23
23
|
Clacky::Logger.info(
|
|
24
24
|
"Idle compression skipped",
|
|
25
25
|
enable_compression: @config.enable_compression,
|
|
26
|
+
enable_idle_compression: @config.enable_idle_compression,
|
|
26
27
|
previous_total_tokens: @previous_total_tokens,
|
|
27
28
|
history_size: @history.size,
|
|
28
29
|
idle_threshold: IDLE_COMPRESSION_THRESHOLD,
|
|
@@ -129,8 +130,10 @@ module Clacky
|
|
|
129
130
|
# (force: true) is still allowed as a safety net for long-running subagents.
|
|
130
131
|
return nil if @is_subagent && !force
|
|
131
132
|
|
|
132
|
-
# Check if compression is enabled
|
|
133
|
+
# Check if compression is enabled (master switch)
|
|
133
134
|
return nil unless @config.enable_compression
|
|
135
|
+
# Idle compression has its own sub-switch under the master
|
|
136
|
+
return nil if force && !@config.enable_idle_compression
|
|
134
137
|
|
|
135
138
|
# Use the larger of: API-reported tokens from last response, or current
|
|
136
139
|
# estimated history size. The estimate guards the case where a single
|
|
@@ -224,6 +224,7 @@ module Clacky
|
|
|
224
224
|
# (falling back to the current default if the model no longer exists).
|
|
225
225
|
permission_mode: @config.permission_mode.to_s,
|
|
226
226
|
enable_compression: @config.enable_compression,
|
|
227
|
+
enable_idle_compression: @config.enable_idle_compression,
|
|
227
228
|
enable_prompt_caching: @config.enable_prompt_caching,
|
|
228
229
|
max_tokens: @config.max_tokens,
|
|
229
230
|
verbose: @config.verbose,
|
data/lib/clacky/agent.rb
CHANGED
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -159,7 +159,7 @@ module Clacky
|
|
|
159
159
|
DEFAULT_MESSAGE_COUNT_THRESHOLD = 200
|
|
160
160
|
|
|
161
161
|
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
162
|
-
:enable_compression, :enable_prompt_caching,
|
|
162
|
+
:enable_compression, :enable_idle_compression, :enable_prompt_caching,
|
|
163
163
|
:compression_threshold, :message_count_threshold,
|
|
164
164
|
:models, :current_model_index, :current_model_id,
|
|
165
165
|
:memory_update_enabled, :skill_evolution,
|
|
@@ -173,6 +173,7 @@ module Clacky
|
|
|
173
173
|
@max_tokens = options[:max_tokens] || 16384
|
|
174
174
|
@verbose = options[:verbose] || false
|
|
175
175
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
176
|
+
@enable_idle_compression = options[:enable_idle_compression].nil? ? true : options[:enable_idle_compression]
|
|
176
177
|
# Enable prompt caching by default for cost savings
|
|
177
178
|
@enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
|
|
178
179
|
# Token threshold that triggers proactive history compression. Local models
|
|
@@ -411,7 +412,7 @@ module Clacky
|
|
|
411
412
|
# Settings keys that are persisted to config.yml.
|
|
412
413
|
# These map directly to AgentConfig accessors.
|
|
413
414
|
CONFIG_SETTINGS_KEYS = %w[
|
|
414
|
-
enable_compression enable_prompt_caching
|
|
415
|
+
enable_compression enable_idle_compression enable_prompt_caching
|
|
415
416
|
compression_threshold message_count_threshold
|
|
416
417
|
memory_update_enabled
|
|
417
418
|
skill_evolution max_running_agents max_idle_agents
|
|
@@ -428,6 +429,7 @@ module Clacky
|
|
|
428
429
|
end
|
|
429
430
|
settings = {
|
|
430
431
|
"enable_compression" => @enable_compression,
|
|
432
|
+
"enable_idle_compression" => @enable_idle_compression,
|
|
431
433
|
"enable_prompt_caching" => @enable_prompt_caching,
|
|
432
434
|
"compression_threshold" => @compression_threshold,
|
|
433
435
|
"message_count_threshold" => @message_count_threshold,
|
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)
|
|
@@ -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",
|
|
@@ -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
|
|
@@ -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: ["*"]
|
|
@@ -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.
|
|
@@ -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],
|
|
@@ -18,6 +18,14 @@ module Clacky
|
|
|
18
18
|
MANIFEST = "ext.yml"
|
|
19
19
|
MAX_ZIP_SIZE = 50 * 1024 * 1024
|
|
20
20
|
|
|
21
|
+
# Network timeouts for the download step. `read_timeout` is an *idle*
|
|
22
|
+
# timeout (resets on every chunk received), so a stalled connection with
|
|
23
|
+
# no new bytes flowing for this long is treated as dead — not a "download
|
|
24
|
+
# still going, just slow" situation, which large files on a slow link
|
|
25
|
+
# would otherwise trigger.
|
|
26
|
+
OPEN_TIMEOUT = 15 # seconds to establish the connection
|
|
27
|
+
READ_TIMEOUT = 60 # seconds of silence before giving up mid-download
|
|
28
|
+
|
|
21
29
|
# Platform metadata that leaks in from the developer's OS; never ship it.
|
|
22
30
|
SYSTEM_METADATA = [".DS_Store", "__MACOSX", "Thumbs.db", "desktop.ini"].freeze
|
|
23
31
|
|
|
@@ -56,12 +64,19 @@ module Clacky
|
|
|
56
64
|
# local zip path or an http(s) URL. Validates the archive (single top
|
|
57
65
|
# container with an ext.yml, no path traversal), extracts it, then runs
|
|
58
66
|
# verify on the resolved installed layer.
|
|
59
|
-
|
|
67
|
+
# on_progress is an optional callable that receives a stage hash, e.g.:
|
|
68
|
+
# { stage: "downloading", progress: 45 }
|
|
69
|
+
# { stage: "extracting" }
|
|
70
|
+
# { stage: "verifying" }
|
|
71
|
+
# Caller can use this for progress reporting without coupling to internals.
|
|
72
|
+
def install(source, installed_dir: Clacky::ExtensionLoader::INSTALLED_DIR, force: false, on_progress: nil)
|
|
60
73
|
Dir.mktmpdir("clacky-ext-install") do |tmp|
|
|
61
|
-
|
|
74
|
+
on_progress&.call({ stage: "downloading", progress: 0 })
|
|
75
|
+
zip_path = local_zip_for(source, tmp, on_progress: on_progress)
|
|
76
|
+
|
|
77
|
+
on_progress&.call({ stage: "extracting" })
|
|
62
78
|
extract_root = File.join(tmp, "unpacked")
|
|
63
79
|
FileUtils.mkdir_p(extract_root)
|
|
64
|
-
|
|
65
80
|
extract_zip(zip_path, extract_root)
|
|
66
81
|
|
|
67
82
|
ext_id, container_src = locate_container(extract_root)
|
|
@@ -74,6 +89,7 @@ module Clacky
|
|
|
74
89
|
FileUtils.mkdir_p(target)
|
|
75
90
|
FileUtils.cp_r(Dir.glob("#{container_src}/*"), target)
|
|
76
91
|
|
|
92
|
+
on_progress&.call({ stage: "verifying" })
|
|
77
93
|
Clacky::ExtensionLoader.invalidate_cache!
|
|
78
94
|
units = verify_installed(ext_id, installed_dir)
|
|
79
95
|
|
|
@@ -149,11 +165,11 @@ module Clacky
|
|
|
149
165
|
rel.split(File::SEPARATOR).any? { |seg| SYSTEM_METADATA.include?(seg) }
|
|
150
166
|
end
|
|
151
167
|
|
|
152
|
-
private def local_zip_for(source, tmp)
|
|
168
|
+
private def local_zip_for(source, tmp, on_progress: nil)
|
|
153
169
|
src = source.to_s
|
|
154
170
|
if src.match?(%r{\Ahttps?://})
|
|
155
171
|
dest = File.join(tmp, "download.zip")
|
|
156
|
-
download(src, dest)
|
|
172
|
+
download(src, dest, on_progress: on_progress)
|
|
157
173
|
dest
|
|
158
174
|
else
|
|
159
175
|
path = File.expand_path(src)
|
|
@@ -162,19 +178,48 @@ module Clacky
|
|
|
162
178
|
end
|
|
163
179
|
end
|
|
164
180
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
# Download url → dest file.
|
|
182
|
+
# IMPORTANT: OpenURI's block form (`URI.open(url) { |io| ... }`) buffers
|
|
183
|
+
# the *entire* response into a StringIO/Tempfile via Net::HTTP's
|
|
184
|
+
# read_body before ever yielding `io` — so reading `io` in chunks after
|
|
185
|
+
# the fact reports progress only after the download has already
|
|
186
|
+
# finished. To get progress callbacks that fire while bytes are still
|
|
187
|
+
# arriving on the socket, we hook OpenURI's own `progress_proc` /
|
|
188
|
+
# `content_length_proc`, which run inside read_body as each chunk lands.
|
|
189
|
+
# Respects whatever proxy env vars the user has configured (some CDN
|
|
190
|
+
# domains are only reachable at usable speed through a proxy).
|
|
191
|
+
private def download(url, dest, on_progress: nil)
|
|
192
|
+
content_length = nil
|
|
193
|
+
|
|
194
|
+
content_length_proc = lambda do |len|
|
|
195
|
+
content_length = len if len&.positive?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
progress_proc = lambda do |size|
|
|
199
|
+
raise Error, "download exceeds #{MAX_ZIP_SIZE} bytes" if size > MAX_ZIP_SIZE
|
|
200
|
+
next unless on_progress
|
|
201
|
+
|
|
202
|
+
pct = if content_length&.positive?
|
|
203
|
+
[(size * 100 / content_length).floor, 99].min
|
|
204
|
+
else
|
|
205
|
+
(90 - 90 * Math.exp(-size.to_f / (1024 * 1024))).floor
|
|
206
|
+
end
|
|
207
|
+
on_progress.call({ stage: "downloading", progress: pct })
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
URI.open(
|
|
211
|
+
url, "rb",
|
|
212
|
+
open_timeout: OPEN_TIMEOUT,
|
|
213
|
+
read_timeout: READ_TIMEOUT,
|
|
214
|
+
content_length_proc: content_length_proc,
|
|
215
|
+
progress_proc: progress_proc
|
|
216
|
+
) do |io| # rubocop:disable Security/Open
|
|
217
|
+
File.open(dest, "wb") { |out| out.write(io.read) }
|
|
175
218
|
end
|
|
176
219
|
rescue OpenURI::HTTPError, SocketError => e
|
|
177
220
|
raise Error, "failed to download #{url}: #{e.message}"
|
|
221
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
222
|
+
raise Error, "download timed out: #{e.message}"
|
|
178
223
|
end
|
|
179
224
|
|
|
180
225
|
private def extract_zip(zip_path, dest_root)
|
|
@@ -22,7 +22,7 @@ module Clacky
|
|
|
22
22
|
PANEL_KEYS = %w[id title title_zh description description_zh view order attach entry_points].freeze
|
|
23
23
|
API_KEYS = %w[id handler].freeze
|
|
24
24
|
SKILL_KEYS = %w[id dir protected].freeze
|
|
25
|
-
AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills avatar].freeze
|
|
25
|
+
AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills avatar hidden].freeze
|
|
26
26
|
CHANNEL_KEYS = %w[id platform adapter].freeze
|
|
27
27
|
PATCH_KEYS = %w[target file fingerprint on_mismatch].freeze
|
|
28
28
|
HOOK_KEYS = %w[event file].freeze
|
|
@@ -114,7 +114,7 @@ module Clacky
|
|
|
114
114
|
private_class_method def self.normalized_effort(effort)
|
|
115
115
|
return nil if effort.nil? || effort.to_s.empty?
|
|
116
116
|
s = effort.to_s
|
|
117
|
-
%w[low medium high].include?(s) ? s : nil
|
|
117
|
+
%w[low medium high xhigh max].include?(s) ? s : nil
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Merge consecutive tool_result user messages into a single user message.
|
|
@@ -91,7 +91,7 @@ module Clacky
|
|
|
91
91
|
|
|
92
92
|
private_class_method def self.additional_fields_for_effort(effort)
|
|
93
93
|
return nil if effort.nil? || effort.to_s.empty?
|
|
94
|
-
return nil unless %w[low medium high].include?(effort.to_s)
|
|
94
|
+
return nil unless %w[low medium high xhigh max].include?(effort.to_s)
|
|
95
95
|
{
|
|
96
96
|
thinking: { type: "adaptive" },
|
|
97
97
|
output_config: { effort: effort.to_s }
|
|
@@ -279,6 +279,14 @@ module Clacky
|
|
|
279
279
|
elsif !effort_str.empty?
|
|
280
280
|
body[:thinking] = { type: "adaptive" }
|
|
281
281
|
end
|
|
282
|
+
elsif model.to_s.match?(/deepseek/i)
|
|
283
|
+
# DeepSeek V4 reasoning_effort only accepts low/high/max.
|
|
284
|
+
# xhigh is not recognized — map it to max.
|
|
285
|
+
effort = case effort_str
|
|
286
|
+
when "xhigh" then "max"
|
|
287
|
+
else effort_str
|
|
288
|
+
end
|
|
289
|
+
body[:reasoning_effort] = effort unless effort.empty?
|
|
282
290
|
elsif reasoning_effort && !effort_str.empty?
|
|
283
291
|
body[:reasoning_effort] = effort_str
|
|
284
292
|
end
|