openclacky 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/clacky/agent/session_serializer.rb +16 -6
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/server/http_server.rb +24 -1
- data/lib/clacky/utils/environment_detector.rb +16 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +46 -7
- data/lib/clacky/web/billing.js +2 -2
- data/lib/clacky/web/i18n.js +4 -0
- data/lib/clacky/web/index.html +1 -0
- data/lib/clacky/web/settings.js +8 -8
- data/lib/clacky/web/skills.js +48 -6
- data/lib/clacky/web/tasks.js +18 -10
- data/lib/clacky/web/workspace.js +104 -0
- 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: ed703cddac3ffa9887b5c500c5dfbe57c895eb5aab0e9dc3338592c9317555ec
|
|
4
|
+
data.tar.gz: 67d21c3c857e72bb729cfcecbdb3749eaaac1c76b21e1d0600081d1e4d79e880
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4fe46c92c67e5937bf24e325be47046a9ebf555afb722aaaaf108a17b2c13403c7e42c08d4b1a7abafad5c5ffb5d79d2749264c051572d157d0094dcb067ac78
|
|
7
|
+
data.tar.gz: 5fa8e909159560aa13944c0435716e7a98265504c6f61e8cfc298bc09553aa218ac404423d4e9b45f5a8d17563f0ae8a7a50547460fb497e06a98bc46b55aa53
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.2] - 2026-06-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Right-click context menu on workspace file tree with "Reveal in Finder" support (macOS/Linux/WSL)
|
|
12
|
+
- Resizable workspace panel with a drag handle
|
|
13
|
+
- Expandable task prompt preview — click a task card to view the full prompt
|
|
14
|
+
|
|
15
|
+
### Improved
|
|
16
|
+
- Skill cards now show a delete button, with shimmer skeleton loading while fetching
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Every-N-hours cron schedule label now renders correctly (e.g. `0 */2 * * *`)
|
|
20
|
+
- Billing sessions list now shows all rows instead of being clipped
|
|
21
|
+
- Session replay skips reasoning-only assistant messages to match the live view
|
|
22
|
+
- Set-as-default button in settings aligned to the footer row with the top-up link
|
|
23
|
+
|
|
8
24
|
## [1.3.1] - 2026-06-17
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -502,14 +502,24 @@ module Clacky
|
|
|
502
502
|
|
|
503
503
|
case msg[:role].to_s
|
|
504
504
|
when "assistant"
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
|
|
505
|
+
# Mirror the live guard at agent.rb (`if response[:content] && !response[:content].empty?`):
|
|
506
|
+
# only emit an assistant_message when the model produced actual content.
|
|
507
|
+
# Reasoning-only turns (empty content + reasoning_content + tool_calls)
|
|
508
|
+
# are silent in live mode; on replay they must stay silent too — otherwise
|
|
509
|
+
# a phantom <think>-only bubble splits consecutive tool_calls into separate
|
|
510
|
+
# UI groups, breaking the "N tool(s) used" collapse after refresh (C-5672).
|
|
511
|
+
raw_text = extract_text_from_content(msg[:content]).to_s.strip
|
|
508
512
|
reasoning = msg[:reasoning_content]
|
|
509
|
-
|
|
510
|
-
text =
|
|
513
|
+
unless raw_text.empty?
|
|
514
|
+
text = if reasoning && !reasoning.to_s.strip.empty?
|
|
515
|
+
# Prepend reasoning wrapped in <think> tags so the Web UI renders it
|
|
516
|
+
# as a collapsible thinking block.
|
|
517
|
+
"<think>\n#{reasoning}\n</think>\n#{raw_text}"
|
|
518
|
+
else
|
|
519
|
+
raw_text
|
|
520
|
+
end
|
|
521
|
+
ui.show_assistant_message(text, files: [])
|
|
511
522
|
end
|
|
512
|
-
ui.show_assistant_message(text, files: []) unless text.empty?
|
|
513
523
|
|
|
514
524
|
# Tool calls embedded in assistant message
|
|
515
525
|
Array(msg[:tool_calls]).each do |tc|
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -961,7 +961,7 @@ module Clacky
|
|
|
961
961
|
#
|
|
962
962
|
# @param skill_name [String] The slug/name of the skill to remove.
|
|
963
963
|
# @return [void]
|
|
964
|
-
|
|
964
|
+
def delete_brand_skill!(skill_name)
|
|
965
965
|
# Remove files from disk.
|
|
966
966
|
skill_dir = File.join(brand_skills_dir, skill_name)
|
|
967
967
|
FileUtils.rm_rf(skill_dir) if Dir.exist?(skill_dir)
|
|
@@ -630,6 +630,9 @@ module Clacky
|
|
|
630
630
|
elsif method == "DELETE" && path.match?(%r{^/api/skills/[^/]+$})
|
|
631
631
|
name = URI.decode_www_form_component(path.sub("/api/skills/", ""))
|
|
632
632
|
api_delete_skill(name, res)
|
|
633
|
+
elsif method == "DELETE" && path.match?(%r{^/api/brand/skills/[^/]+$})
|
|
634
|
+
slug = URI.decode_www_form_component(path.sub("/api/brand/skills/", ""))
|
|
635
|
+
api_delete_brand_skill(slug, res)
|
|
633
636
|
elsif method == "POST" && path.match?(%r{^/api/brand/skills/[^/]+/install$})
|
|
634
637
|
slug = URI.decode_www_form_component(path.sub("/api/brand/skills/", "").sub("/install", ""))
|
|
635
638
|
api_brand_skill_install(slug, req, res)
|
|
@@ -1942,6 +1945,23 @@ module Clacky
|
|
|
1942
1945
|
json_response(res, 500, { ok: false, error: e.message })
|
|
1943
1946
|
end
|
|
1944
1947
|
|
|
1948
|
+
# DELETE /api/brand/skills/:slug
|
|
1949
|
+
# Uninstalls a brand skill by removing its files and metadata.
|
|
1950
|
+
def api_delete_brand_skill(slug, res)
|
|
1951
|
+
brand = Clacky::BrandConfig.load
|
|
1952
|
+
installed = brand.installed_brand_skills
|
|
1953
|
+
unless installed.key?(slug)
|
|
1954
|
+
json_response(res, 404, { ok: false, error: "Brand skill '#{slug}' is not installed" })
|
|
1955
|
+
return
|
|
1956
|
+
end
|
|
1957
|
+
|
|
1958
|
+
brand.delete_brand_skill!(slug)
|
|
1959
|
+
@skill_loader = Clacky::SkillLoader.new(working_dir: nil, brand_config: brand)
|
|
1960
|
+
json_response(res, 200, { ok: true })
|
|
1961
|
+
rescue StandardError => e
|
|
1962
|
+
json_response(res, 500, { ok: false, error: e.message })
|
|
1963
|
+
end
|
|
1964
|
+
|
|
1945
1965
|
# GET /api/brand
|
|
1946
1966
|
# Returns brand metadata consumed by the WebUI on boot
|
|
1947
1967
|
# to dynamically replace branding strings.
|
|
@@ -2895,10 +2915,13 @@ module Clacky
|
|
|
2895
2915
|
result = Utils::EnvironmentDetector.open_file(linux_path)
|
|
2896
2916
|
return json_response(res, 501, { error: "unsupported OS" }) if result.nil?
|
|
2897
2917
|
json_response(res, 200, { ok: true })
|
|
2918
|
+
when "reveal"
|
|
2919
|
+
Utils::EnvironmentDetector.reveal_file(linux_path)
|
|
2920
|
+
json_response(res, 200, { ok: true })
|
|
2898
2921
|
when "download"
|
|
2899
2922
|
serve_file_download(res, linux_path)
|
|
2900
2923
|
else
|
|
2901
|
-
json_response(res, 400, { error: "invalid action. Must be 'open' or 'download'" })
|
|
2924
|
+
json_response(res, 400, { error: "invalid action. Must be 'open', 'reveal' or 'download'" })
|
|
2902
2925
|
end
|
|
2903
2926
|
rescue => e
|
|
2904
2927
|
json_response(res, 500, { ok: false, error: e.message })
|
|
@@ -36,6 +36,22 @@ module Clacky
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Reveal a file in the OS file manager (select/highlight it).
|
|
40
|
+
# macOS: open -R; Linux: xdg-open on parent dir; WSL: explorer /select
|
|
41
|
+
# @param path [String] Linux-side file path
|
|
42
|
+
# @return [Boolean, nil] true/false from system(), or nil on unsupported OS
|
|
43
|
+
def self.reveal_file(path)
|
|
44
|
+
case os_type
|
|
45
|
+
when :macos then system("open", "-R", path)
|
|
46
|
+
when :linux then system("xdg-open", File.dirname(path))
|
|
47
|
+
when :wsl
|
|
48
|
+
win_path = linux_to_win_path(path)
|
|
49
|
+
system("explorer.exe", "/select,#{win_path}")
|
|
50
|
+
else
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
39
55
|
# Convert a Windows-style path to a WSL/Linux-side path.
|
|
40
56
|
# e.g. "C:/Users/foo/file.txt" → "/mnt/c/Users/foo/file.txt"
|
|
41
57
|
# Returns the original path unchanged on non-WSL or if already a Linux path.
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -1754,6 +1754,30 @@ body {
|
|
|
1754
1754
|
overflow: hidden;
|
|
1755
1755
|
text-overflow: ellipsis;
|
|
1756
1756
|
}
|
|
1757
|
+
.task-card-preview-expandable {
|
|
1758
|
+
cursor: pointer;
|
|
1759
|
+
}
|
|
1760
|
+
.task-card-preview-expandable:hover {
|
|
1761
|
+
color: var(--color-accent-primary);
|
|
1762
|
+
}
|
|
1763
|
+
.task-card-detail {
|
|
1764
|
+
margin-top: 0.625rem;
|
|
1765
|
+
padding: 0.625rem 0.75rem;
|
|
1766
|
+
background: var(--color-bg-primary);
|
|
1767
|
+
border: 1px solid var(--color-border-primary);
|
|
1768
|
+
border-radius: 6px;
|
|
1769
|
+
}
|
|
1770
|
+
.task-card-detail-content {
|
|
1771
|
+
margin: 0;
|
|
1772
|
+
font-size: 0.75rem;
|
|
1773
|
+
font-family: inherit;
|
|
1774
|
+
color: var(--color-text-secondary);
|
|
1775
|
+
line-height: 1.6;
|
|
1776
|
+
white-space: pre-wrap;
|
|
1777
|
+
word-break: break-word;
|
|
1778
|
+
max-height: 12rem;
|
|
1779
|
+
overflow-y: auto;
|
|
1780
|
+
}
|
|
1757
1781
|
.task-card-actions {
|
|
1758
1782
|
display: flex;
|
|
1759
1783
|
align-items: center;
|
|
@@ -1907,7 +1931,8 @@ body {
|
|
|
1907
1931
|
|
|
1908
1932
|
/* ── Workspace panel (right file browser) ───────────────────────────────── */
|
|
1909
1933
|
#workspace-panel {
|
|
1910
|
-
width: 280px;
|
|
1934
|
+
--workspace-width: 280px;
|
|
1935
|
+
width: var(--workspace-width);
|
|
1911
1936
|
flex-shrink: 0;
|
|
1912
1937
|
display: flex;
|
|
1913
1938
|
flex-direction: column;
|
|
@@ -1915,8 +1940,21 @@ body {
|
|
|
1915
1940
|
background: var(--color-bg-primary);
|
|
1916
1941
|
overflow: hidden;
|
|
1917
1942
|
transition: width var(--transition-base);
|
|
1943
|
+
position: relative;
|
|
1918
1944
|
}
|
|
1919
1945
|
#workspace-panel.collapsed { width: 0; border-left: none; }
|
|
1946
|
+
#workspace-resize-handle {
|
|
1947
|
+
position: absolute;
|
|
1948
|
+
top: 0;
|
|
1949
|
+
left: -3px;
|
|
1950
|
+
bottom: 0;
|
|
1951
|
+
width: 6px;
|
|
1952
|
+
cursor: col-resize;
|
|
1953
|
+
z-index: 10;
|
|
1954
|
+
}
|
|
1955
|
+
#workspace-panel:has(#workspace-resize-handle.active) {
|
|
1956
|
+
transition: background-color var(--transition-base);
|
|
1957
|
+
}
|
|
1920
1958
|
#workspace-header {
|
|
1921
1959
|
display: flex;
|
|
1922
1960
|
align-items: center;
|
|
@@ -4633,14 +4671,15 @@ body {
|
|
|
4633
4671
|
}
|
|
4634
4672
|
.model-card-grid-toolbar {
|
|
4635
4673
|
display: flex;
|
|
4636
|
-
flex-wrap:
|
|
4674
|
+
flex-wrap: nowrap;
|
|
4637
4675
|
gap: 0.375rem;
|
|
4638
4676
|
margin-left: auto;
|
|
4639
4677
|
justify-content: flex-end;
|
|
4640
4678
|
}
|
|
4641
4679
|
.model-card-grid-footer {
|
|
4642
4680
|
display: flex;
|
|
4643
|
-
justify-content:
|
|
4681
|
+
justify-content: space-between;
|
|
4682
|
+
align-items: center;
|
|
4644
4683
|
margin-top: 0.25rem;
|
|
4645
4684
|
}
|
|
4646
4685
|
.model-card-grid-link {
|
|
@@ -11442,7 +11481,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11442
11481
|
min-height: 0;
|
|
11443
11482
|
}
|
|
11444
11483
|
#skills-body {
|
|
11445
|
-
padding: 1rem 1rem
|
|
11484
|
+
padding: 1rem 1rem 1rem;
|
|
11446
11485
|
gap: 0.75rem;
|
|
11447
11486
|
overflow-y: auto;
|
|
11448
11487
|
-webkit-overflow-scrolling: touch;
|
|
@@ -11543,9 +11582,9 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11543
11582
|
}
|
|
11544
11583
|
|
|
11545
11584
|
/* ── Billing / Trash / Creator pages ── */
|
|
11546
|
-
#billing-body { padding: 1rem 1rem
|
|
11547
|
-
#trash-body { padding: 1rem 1rem
|
|
11548
|
-
#creator-body { padding: 1rem 1rem
|
|
11585
|
+
#billing-body { padding: 1rem 1rem 1rem; }
|
|
11586
|
+
#trash-body { padding: 1rem 1rem 1rem; }
|
|
11587
|
+
#creator-body { padding: 1rem 1rem 1rem; }
|
|
11549
11588
|
|
|
11550
11589
|
.billing-heatmap-row { grid-template-columns: 1fr; }
|
|
11551
11590
|
|
data/lib/clacky/web/billing.js
CHANGED
|
@@ -878,7 +878,7 @@ const Billing = (() => {
|
|
|
878
878
|
function _renderSessionList() {
|
|
879
879
|
if (!_sessions || _sessions.length === 0) {
|
|
880
880
|
return `
|
|
881
|
-
<div class="billing-
|
|
881
|
+
<div class="billing-sessions-section">
|
|
882
882
|
<h3>${I18n.t("billing.sessions") || "Sessions"}</h3>
|
|
883
883
|
<div class="billing-sessions-empty">${I18n.t("billing.noSessions") || "No session data"}</div>
|
|
884
884
|
</div>
|
|
@@ -926,7 +926,7 @@ const Billing = (() => {
|
|
|
926
926
|
}).join("");
|
|
927
927
|
|
|
928
928
|
return `
|
|
929
|
-
<div class="billing-
|
|
929
|
+
<div class="billing-sessions-section">
|
|
930
930
|
<h3>${I18n.t("billing.sessions") || "Sessions"}</h3>
|
|
931
931
|
<div class="billing-sessions-header">
|
|
932
932
|
<span class="billing-cell billing-cell-index">#</span>
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -114,6 +114,8 @@ const I18n = (() => {
|
|
|
114
114
|
"workspace.loading": "Loading…",
|
|
115
115
|
"workspace.error": "Failed to load files",
|
|
116
116
|
"workspace.downloadFailed": "Download failed",
|
|
117
|
+
"workspace.revealInFinder": "Reveal in Finder",
|
|
118
|
+
"workspace.revealFailed": "Failed to reveal file",
|
|
117
119
|
"sib.model.tooltip": "Click to switch model",
|
|
118
120
|
"sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
|
|
119
121
|
"sib.variant.header": "Quick switch",
|
|
@@ -999,6 +1001,8 @@ const I18n = (() => {
|
|
|
999
1001
|
"workspace.loading": "加载中…",
|
|
1000
1002
|
"workspace.error": "加载文件失败",
|
|
1001
1003
|
"workspace.downloadFailed": "下载失败",
|
|
1004
|
+
"workspace.revealInFinder": "打开所在文件夹",
|
|
1005
|
+
"workspace.revealFailed": "无法打开文件位置",
|
|
1002
1006
|
"sib.model.tooltip": "点击切换模型",
|
|
1003
1007
|
"sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
|
|
1004
1008
|
"sib.variant.header": "快速切换",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -443,6 +443,7 @@
|
|
|
443
443
|
|
|
444
444
|
<!-- ── WORKSPACE PANEL (right) ──────────────────────────────────── -->
|
|
445
445
|
<aside id="workspace-panel" class="collapsed">
|
|
446
|
+
<div id="workspace-resize-handle"></div>
|
|
446
447
|
<div id="workspace-header">
|
|
447
448
|
<span id="workspace-title" data-i18n="workspace.title">Workspace</span>
|
|
448
449
|
<div class="workspace-header-actions">
|
data/lib/clacky/web/settings.js
CHANGED
|
@@ -104,10 +104,6 @@ const Settings = (() => {
|
|
|
104
104
|
</div>
|
|
105
105
|
</div>
|
|
106
106
|
<div class="model-card-grid-actions">
|
|
107
|
-
${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default">
|
|
108
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
|
109
|
-
<span>${I18n.t("settings.models.btn.setDefault")}</span>
|
|
110
|
-
</button>` : ""}
|
|
111
107
|
<div class="model-card-grid-toolbar">
|
|
112
108
|
<button class="btn-card-grid-action" data-index="${index}" data-action="test">
|
|
113
109
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
@@ -123,12 +119,16 @@ const Settings = (() => {
|
|
|
123
119
|
</button>` : ""}
|
|
124
120
|
</div>
|
|
125
121
|
</div>
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
<div class="model-card-grid-footer">
|
|
123
|
+
${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default">
|
|
124
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
|
125
|
+
<span>${I18n.t("settings.models.btn.setDefault")}</span>
|
|
126
|
+
</button>` : `<span></span>`}
|
|
127
|
+
${websiteUrl ? `<a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer">
|
|
128
128
|
${I18n.t("settings.models.link.topUp")}
|
|
129
129
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7"/><path d="M8 7h9v9"/></svg>
|
|
130
|
-
</a
|
|
131
|
-
</div
|
|
130
|
+
</a>` : ""}
|
|
131
|
+
</div>
|
|
132
132
|
`;
|
|
133
133
|
|
|
134
134
|
container.appendChild(card);
|
data/lib/clacky/web/skills.js
CHANGED
|
@@ -51,7 +51,21 @@ const Skills = (() => {
|
|
|
51
51
|
async function _loadBrandSkills() {
|
|
52
52
|
const container = $("brand-skills-list");
|
|
53
53
|
if (!container) return;
|
|
54
|
-
container.innerHTML =
|
|
54
|
+
container.innerHTML = Array.from({ length: 4 }).map(() => `
|
|
55
|
+
<div class="brand-skill-card">
|
|
56
|
+
<div class="brand-skill-card-main">
|
|
57
|
+
<div class="brand-skill-info">
|
|
58
|
+
<div class="brand-skill-title">
|
|
59
|
+
<span class="skel skel-title"></span>
|
|
60
|
+
<span class="skel" style="height:1rem;width:3.5rem;border-radius:4px;"></span>
|
|
61
|
+
</div>
|
|
62
|
+
<span class="skel skel-subtitle"></span>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="brand-skill-actions">
|
|
65
|
+
<span class="skel" style="height:1.75rem;width:4.5rem;border-radius:6px;"></span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>`).join("");
|
|
55
69
|
|
|
56
70
|
try {
|
|
57
71
|
const res = await fetch("/api/brand/skills");
|
|
@@ -163,11 +177,16 @@ const Skills = (() => {
|
|
|
163
177
|
<span class="brand-skill-version latest">v${escapeHtml(latestVersion)}</span>
|
|
164
178
|
<button class="btn-brand-update" data-name="${escapeHtml(name)}">${I18n.t("skills.brand.btn.update")}</button>`;
|
|
165
179
|
} else {
|
|
166
|
-
// Installed and up-to-date — show version badge + "Use"
|
|
180
|
+
// Installed and up-to-date — show version badge + "Use" + delete buttons
|
|
167
181
|
const displayVersion = installedVersion || latestVersion;
|
|
168
182
|
statusHtml = `
|
|
169
183
|
<span class="brand-skill-version installed">v${escapeHtml(displayVersion)} ✓</span>
|
|
170
|
-
<button class="btn-brand-use" data-name="${escapeHtml(name)}">${I18n.t("skills.brand.btn.use")}</button
|
|
184
|
+
<button class="btn-brand-use" data-name="${escapeHtml(name)}">${I18n.t("skills.brand.btn.use")}</button>
|
|
185
|
+
<button class="btn-skill-delete btn-brand-delete" data-name="${escapeHtml(name)}" title="${I18n.t("skills.btn.delete")}">
|
|
186
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
187
|
+
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
188
|
+
</svg>
|
|
189
|
+
</button>`;
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
// Free skills show a "Free" badge; paid (encrypted) brand skills show "Private".
|
|
@@ -196,12 +215,17 @@ const Skills = (() => {
|
|
|
196
215
|
</div>`;
|
|
197
216
|
|
|
198
217
|
// Bind install/update/use buttons
|
|
199
|
-
const installBtn
|
|
200
|
-
const updateBtn
|
|
201
|
-
const useBtn
|
|
218
|
+
const installBtn = card.querySelector(".btn-brand-install");
|
|
219
|
+
const updateBtn = card.querySelector(".btn-brand-update");
|
|
220
|
+
const useBtn = card.querySelector(".btn-brand-use");
|
|
221
|
+
const deleteBtn = card.querySelector(".btn-brand-delete");
|
|
202
222
|
if (installBtn) installBtn.addEventListener("click", () => _installBrandSkill(name, installBtn));
|
|
203
223
|
if (updateBtn) updateBtn.addEventListener("click", () => _installBrandSkill(name, updateBtn));
|
|
204
224
|
if (useBtn) useBtn.addEventListener("click", () => _useInstalledSkill(name));
|
|
225
|
+
if (deleteBtn) deleteBtn.addEventListener("click", (e) => {
|
|
226
|
+
e.stopPropagation();
|
|
227
|
+
_deleteBrandSkill(name);
|
|
228
|
+
});
|
|
205
229
|
|
|
206
230
|
return card;
|
|
207
231
|
}
|
|
@@ -266,6 +290,24 @@ const Skills = (() => {
|
|
|
266
290
|
}
|
|
267
291
|
}
|
|
268
292
|
|
|
293
|
+
async function _deleteBrandSkill(name) {
|
|
294
|
+
if (!confirm(I18n.t("skills.deleteConfirm", { name }))) return;
|
|
295
|
+
try {
|
|
296
|
+
const res = await fetch(`/api/brand/skills/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
297
|
+
const data = await res.json();
|
|
298
|
+
if (!res.ok || !data.ok) { alert(data.error || I18n.t("skills.deleteError")); return; }
|
|
299
|
+
|
|
300
|
+
const skill = _brandSkills.find(s => s.name === name);
|
|
301
|
+
if (skill) skill.installed_version = null;
|
|
302
|
+
|
|
303
|
+
_renderBrandSkills();
|
|
304
|
+
await Skills.load();
|
|
305
|
+
Modal.toast(I18n.t("skills.deleted", { name }), "success");
|
|
306
|
+
} catch (e) {
|
|
307
|
+
console.error("[Skills] brand skill delete failed", e);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
269
311
|
/** Open a new session and trigger a brand skill by sending "/{name}" as the first message. */
|
|
270
312
|
async function _useInstalledSkill(name) {
|
|
271
313
|
const maxN = Sessions.all.reduce((max, s) => {
|
data/lib/clacky/web/tasks.js
CHANGED
|
@@ -49,7 +49,7 @@ const Tasks = (() => {
|
|
|
49
49
|
return isZh ? `每 ${n} 分钟` : `Every ${n} min`;
|
|
50
50
|
}
|
|
51
51
|
// Every N hours
|
|
52
|
-
if (isAny(min) && hour.startsWith("*/") && isAny(dom) && isAny(month) && isAny(dow)) {
|
|
52
|
+
if ((isAny(min) || isNum(min)) && hour.startsWith("*/") && isAny(dom) && isAny(month) && isAny(dow)) {
|
|
53
53
|
const n = hour.slice(2);
|
|
54
54
|
return isZh ? `每 ${n} 小时` : `Every ${n} hr`;
|
|
55
55
|
}
|
|
@@ -108,13 +108,9 @@ const Tasks = (() => {
|
|
|
108
108
|
? `<span class="task-card-badge task-card-badge-paused">${I18n.t("tasks.paused")}</span>`
|
|
109
109
|
: "";
|
|
110
110
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.find(l => l.length > 0) || I18n.t("tasks.empty");
|
|
115
|
-
const previewText = preview.length > 120
|
|
116
|
-
? escapeHtml(preview.slice(0, 120)) + "…"
|
|
117
|
-
: escapeHtml(preview);
|
|
111
|
+
const content = t.content || "";
|
|
112
|
+
const isTruncated = content.trim().length > 0;
|
|
113
|
+
const previewText = escapeHtml(content.replace(/\s+/g, " ").trim()) || escapeHtml(I18n.t("tasks.empty"));
|
|
118
114
|
|
|
119
115
|
const toggleBtnHtml = t.scheduled ? (isPaused
|
|
120
116
|
? `<button class="task-action-btn task-btn-toggle task-btn-resume">
|
|
@@ -146,7 +142,7 @@ const Tasks = (() => {
|
|
|
146
142
|
${pausedBadge}
|
|
147
143
|
${schedLabel}
|
|
148
144
|
</div>
|
|
149
|
-
<div class="task-card-preview">${previewText}</div>
|
|
145
|
+
<div class="task-card-preview${isTruncated ? " task-card-preview-expandable" : ""}">${previewText}</div>
|
|
150
146
|
</div>
|
|
151
147
|
<div class="task-card-actions">
|
|
152
148
|
<button class="task-run-btn task-btn-run" title="${I18n.t("tasks.btn.run")}">
|
|
@@ -170,12 +166,24 @@ const Tasks = (() => {
|
|
|
170
166
|
<span>${I18n.t("tasks.btn.delete")}</span>
|
|
171
167
|
</button>
|
|
172
168
|
</div>
|
|
173
|
-
</div
|
|
169
|
+
</div>
|
|
170
|
+
${isTruncated ? `<div class="task-card-detail" hidden><pre class="task-card-detail-content">${escapeHtml(content)}</pre></div>` : ""}`;
|
|
174
171
|
|
|
175
172
|
row.querySelector(".task-btn-run").addEventListener("click", e => {
|
|
176
173
|
e.stopPropagation();
|
|
177
174
|
Tasks.run(t.name);
|
|
178
175
|
});
|
|
176
|
+
|
|
177
|
+
if (isTruncated) {
|
|
178
|
+
const previewEl = row.querySelector(".task-card-preview");
|
|
179
|
+
const detailEl = row.querySelector(".task-card-detail");
|
|
180
|
+
previewEl.addEventListener("click", e => {
|
|
181
|
+
e.stopPropagation();
|
|
182
|
+
const expanded = !detailEl.hidden;
|
|
183
|
+
detailEl.hidden = expanded;
|
|
184
|
+
row.classList.toggle("task-card-expanded", !expanded);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
179
187
|
const toggleBtn = row.querySelector(".task-btn-toggle");
|
|
180
188
|
if (toggleBtn) {
|
|
181
189
|
toggleBtn.addEventListener("click", e => {
|
data/lib/clacky/web/workspace.js
CHANGED
|
@@ -93,9 +93,71 @@ const Workspace = (() => {
|
|
|
93
93
|
row.addEventListener("click", () => downloadFile(entry));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
row.addEventListener("contextmenu", (e) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
showContextMenu(e, entry);
|
|
99
|
+
});
|
|
100
|
+
|
|
96
101
|
return node;
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
function showContextMenu(e, entry) {
|
|
105
|
+
closeContextMenu();
|
|
106
|
+
|
|
107
|
+
const iconFolder = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
108
|
+
|
|
109
|
+
const menu = document.createElement("div");
|
|
110
|
+
menu.className = "wt-context-menu session-context-menu";
|
|
111
|
+
menu.innerHTML = `
|
|
112
|
+
<div class="session-actions-menu-item" data-action="reveal">
|
|
113
|
+
<span class="session-actions-menu-icon">${iconFolder}</span>
|
|
114
|
+
<span class="session-actions-menu-label">${t("workspace.revealInFinder")}</span>
|
|
115
|
+
</div>
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
document.body.appendChild(menu);
|
|
119
|
+
menu.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
120
|
+
menu.style.position = "fixed";
|
|
121
|
+
menu.style.top = e.clientY + "px";
|
|
122
|
+
menu.style.left = e.clientX + "px";
|
|
123
|
+
requestAnimationFrame(() => {
|
|
124
|
+
const r = menu.getBoundingClientRect();
|
|
125
|
+
if (r.right > window.innerWidth) menu.style.left = (window.innerWidth - r.width - 8) + "px";
|
|
126
|
+
if (r.bottom > window.innerHeight) menu.style.top = (window.innerHeight - r.height - 8) + "px";
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
menu.addEventListener("click", async (ev) => {
|
|
130
|
+
const item = ev.target.closest(".session-actions-menu-item");
|
|
131
|
+
if (!item) return;
|
|
132
|
+
closeContextMenu();
|
|
133
|
+
if (item.dataset.action === "reveal") await revealFile(entry);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
document.addEventListener("click", closeContextMenu, { once: true });
|
|
138
|
+
}, 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function closeContextMenu() {
|
|
142
|
+
const existing = document.querySelector(".wt-context-menu");
|
|
143
|
+
if (existing) existing.remove();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function revealFile(entry) {
|
|
147
|
+
const fullPath = _workingDir.replace(/\/+$/, "") + "/" + entry.path;
|
|
148
|
+
try {
|
|
149
|
+
const resp = await fetch("/api/file-action", {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
body: JSON.stringify({ path: fullPath, action: "reveal" })
|
|
153
|
+
});
|
|
154
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error("reveal failed:", err);
|
|
157
|
+
if (typeof Modal !== "undefined") Modal.toast(t("workspace.revealFailed"), "error");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
99
161
|
async function toggleDir(entry, caret, children) {
|
|
100
162
|
const isOpen = caret.classList.contains("open");
|
|
101
163
|
if (isOpen) {
|
|
@@ -208,5 +270,47 @@ const Workspace = (() => {
|
|
|
208
270
|
};
|
|
209
271
|
})();
|
|
210
272
|
|
|
273
|
+
(function _initWorkspaceResize() {
|
|
274
|
+
const panel = document.getElementById("workspace-panel");
|
|
275
|
+
const handle = document.getElementById("workspace-resize-handle");
|
|
276
|
+
if (!panel || !handle) return;
|
|
277
|
+
|
|
278
|
+
const MIN_W = 160;
|
|
279
|
+
const MAX_W = 600;
|
|
280
|
+
|
|
281
|
+
const saved = localStorage.getItem("workspace-width");
|
|
282
|
+
if (saved) {
|
|
283
|
+
const w = parseFloat(saved);
|
|
284
|
+
if (w >= MIN_W && w <= MAX_W) panel.style.setProperty("--workspace-width", w + "px");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let startX = 0;
|
|
288
|
+
let startW = 0;
|
|
289
|
+
|
|
290
|
+
handle.addEventListener("mousedown", (e) => {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
startX = e.clientX;
|
|
293
|
+
startW = parseFloat(getComputedStyle(panel).getPropertyValue("--workspace-width"));
|
|
294
|
+
handle.classList.add("active");
|
|
295
|
+
document.body.style.cursor = "col-resize";
|
|
296
|
+
document.body.style.userSelect = "none";
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
document.addEventListener("mousemove", (e) => {
|
|
300
|
+
if (!handle.classList.contains("active")) return;
|
|
301
|
+
const dx = startX - e.clientX;
|
|
302
|
+
const newW = Math.min(MAX_W, Math.max(MIN_W, startW + dx));
|
|
303
|
+
panel.style.setProperty("--workspace-width", newW + "px");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
document.addEventListener("mouseup", () => {
|
|
307
|
+
if (!handle.classList.contains("active")) return;
|
|
308
|
+
handle.classList.remove("active");
|
|
309
|
+
document.body.style.cursor = "";
|
|
310
|
+
document.body.style.userSelect = "";
|
|
311
|
+
localStorage.setItem("workspace-width", parseFloat(getComputedStyle(panel).getPropertyValue("--workspace-width")));
|
|
312
|
+
});
|
|
313
|
+
})();
|
|
314
|
+
|
|
211
315
|
document.addEventListener("DOMContentLoaded", () => Workspace.init());
|
|
212
316
|
window.Workspace = Workspace;
|