openclacky 1.0.2 → 1.0.3
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 -1
- data/lib/clacky/agent/llm_caller.rb +218 -0
- data/lib/clacky/agent/message_compressor.rb +15 -4
- data/lib/clacky/agent/message_compressor_helper.rb +41 -2
- data/lib/clacky/agent/tool_registry.rb +109 -0
- data/lib/clacky/agent.rb +16 -0
- data/lib/clacky/agent_config.rb +17 -0
- data/lib/clacky/cli.rb +65 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +57 -3
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +7 -0
- data/lib/clacky/server/channel/channel_manager.rb +91 -0
- data/lib/clacky/server/discover.rb +77 -0
- data/lib/clacky/server/epipe_safe_io.rb +105 -0
- data/lib/clacky/server/http_server.rb +80 -40
- data/lib/clacky/server/server_master.rb +6 -0
- data/lib/clacky/skill.rb +30 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +58 -22
- data/lib/clacky/web/i18n.js +4 -2
- data/lib/clacky/web/sessions.js +29 -17
- metadata +4 -2
data/lib/clacky/web/app.css
CHANGED
|
@@ -854,69 +854,106 @@ body {
|
|
|
854
854
|
display: none;
|
|
855
855
|
align-items: center;
|
|
856
856
|
justify-content: center;
|
|
857
|
-
width:
|
|
858
|
-
height:
|
|
857
|
+
width: 24px;
|
|
858
|
+
height: 24px;
|
|
859
859
|
background: transparent;
|
|
860
860
|
border: none;
|
|
861
|
-
border-radius:
|
|
861
|
+
border-radius: 4px;
|
|
862
862
|
color: var(--color-text-muted);
|
|
863
|
-
font-size: 14px;
|
|
864
|
-
line-height: 1;
|
|
865
863
|
cursor: pointer;
|
|
866
864
|
padding: 0;
|
|
867
|
-
margin-top: -3px;
|
|
868
865
|
transition: background .15s, color .15s;
|
|
869
|
-
|
|
870
|
-
letter-spacing: -1px;
|
|
866
|
+
align-self: center;
|
|
871
867
|
}
|
|
872
868
|
.session-item:hover .session-actions-btn { display: flex; }
|
|
873
869
|
.session-actions-btn:hover {
|
|
874
|
-
background: var(--color-
|
|
870
|
+
background: var(--color-border-primary);
|
|
875
871
|
color: var(--color-text-primary);
|
|
876
872
|
}
|
|
877
873
|
|
|
878
874
|
/* Pin icon in session name */
|
|
879
875
|
.session-pin-icon {
|
|
880
876
|
flex-shrink: 0;
|
|
881
|
-
|
|
882
|
-
|
|
877
|
+
display: inline-flex;
|
|
878
|
+
align-items: center;
|
|
879
|
+
opacity: 0.6;
|
|
883
880
|
margin-left: 2px;
|
|
881
|
+
color: var(--color-text-tertiary);
|
|
884
882
|
}
|
|
885
883
|
.session-item.active .session-pin-icon {
|
|
886
884
|
opacity: 1;
|
|
885
|
+
color: var(--color-accent-primary);
|
|
887
886
|
}
|
|
888
887
|
|
|
889
888
|
/* Actions menu dropdown */
|
|
890
889
|
.session-actions-menu {
|
|
891
890
|
background: var(--color-bg-secondary);
|
|
892
891
|
border: 1px solid var(--color-border-primary);
|
|
893
|
-
border-radius:
|
|
894
|
-
box-shadow: 0
|
|
895
|
-
padding:
|
|
896
|
-
min-width:
|
|
892
|
+
border-radius: 8px;
|
|
893
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.04);
|
|
894
|
+
padding: 4px;
|
|
895
|
+
min-width: 160px;
|
|
897
896
|
z-index: 1000;
|
|
898
897
|
animation: fadeIn 0.15s ease;
|
|
899
898
|
}
|
|
900
899
|
[data-theme="dark"] .session-actions-menu {
|
|
901
|
-
box-shadow: 0
|
|
900
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
|
|
902
901
|
}
|
|
903
902
|
.session-actions-menu-item {
|
|
903
|
+
display: flex;
|
|
904
|
+
align-items: center;
|
|
905
|
+
gap: 8px;
|
|
904
906
|
padding: 6px 10px;
|
|
905
|
-
border-radius:
|
|
907
|
+
border-radius: 5px;
|
|
906
908
|
cursor: pointer;
|
|
907
909
|
color: var(--color-text-primary);
|
|
908
|
-
font-size:
|
|
909
|
-
|
|
910
|
+
font-size: 13px;
|
|
911
|
+
line-height: 1.4;
|
|
912
|
+
transition: background .12s ease, color .12s ease;
|
|
910
913
|
user-select: none;
|
|
911
914
|
}
|
|
915
|
+
.session-actions-menu-icon {
|
|
916
|
+
display: inline-flex;
|
|
917
|
+
align-items: center;
|
|
918
|
+
justify-content: center;
|
|
919
|
+
width: 14px;
|
|
920
|
+
height: 14px;
|
|
921
|
+
color: var(--color-text-secondary);
|
|
922
|
+
flex-shrink: 0;
|
|
923
|
+
}
|
|
924
|
+
.session-actions-menu-label {
|
|
925
|
+
flex: 1;
|
|
926
|
+
min-width: 0;
|
|
927
|
+
}
|
|
912
928
|
.session-actions-menu-item:hover {
|
|
913
929
|
background: var(--color-bg-hover);
|
|
914
930
|
}
|
|
931
|
+
.session-actions-menu-item:hover .session-actions-menu-icon {
|
|
932
|
+
color: var(--color-text-primary);
|
|
933
|
+
}
|
|
934
|
+
.session-actions-menu-item--danger .session-actions-menu-icon {
|
|
935
|
+
color: var(--color-text-secondary);
|
|
936
|
+
}
|
|
915
937
|
.session-actions-menu-item--danger {
|
|
916
|
-
|
|
938
|
+
margin-top: 5px;
|
|
939
|
+
position: relative;
|
|
940
|
+
}
|
|
941
|
+
.session-actions-menu-item--danger::before {
|
|
942
|
+
content: "";
|
|
943
|
+
position: absolute;
|
|
944
|
+
left: 0;
|
|
945
|
+
right: 0;
|
|
946
|
+
top: -3px;
|
|
947
|
+
height: 1px;
|
|
948
|
+
background: var(--color-border-secondary);
|
|
949
|
+
pointer-events: none;
|
|
917
950
|
}
|
|
918
951
|
.session-actions-menu-item--danger:hover {
|
|
919
952
|
background: var(--color-error-bg);
|
|
953
|
+
color: var(--color-error);
|
|
954
|
+
}
|
|
955
|
+
.session-actions-menu-item--danger:hover .session-actions-menu-icon {
|
|
956
|
+
color: var(--color-error);
|
|
920
957
|
}
|
|
921
958
|
|
|
922
959
|
@keyframes fadeIn {
|
|
@@ -1557,7 +1594,7 @@ body {
|
|
|
1557
1594
|
.msg-user .msg-time { color: var(--color-text-secondary); right: 0; left: auto; padding-right: 4px; }
|
|
1558
1595
|
.msg-assistant .msg-time { color: var(--color-text-secondary); left: 0; right: auto; padding-left: 4px; }
|
|
1559
1596
|
|
|
1560
|
-
.msg-user { background: var(--color-accent-primary); color: var(--color-button-primary-text); align-self: flex-end; }
|
|
1597
|
+
.msg-user { background: var(--color-accent-primary); color: var(--color-button-primary-text); align-self: flex-end; white-space: pre-wrap; }
|
|
1561
1598
|
[data-theme="dark"] .msg-user { background: var(--color-accent-hover); }
|
|
1562
1599
|
.msg-assistant { background: var(--color-bg-tertiary); border: 1px solid var(--color-border-primary); align-self: flex-start; }
|
|
1563
1600
|
|
|
@@ -1610,7 +1647,6 @@ body {
|
|
|
1610
1647
|
}
|
|
1611
1648
|
|
|
1612
1649
|
/* ── Markdown rendering inside assistant messages ────────────────────────── */
|
|
1613
|
-
.msg-assistant p { margin: 0 0 0.6em; }
|
|
1614
1650
|
.msg-assistant p:last-child { margin-bottom: 0; }
|
|
1615
1651
|
.msg-assistant h1, .msg-assistant h2, .msg-assistant h3,
|
|
1616
1652
|
.msg-assistant h4, .msg-assistant h5, .msg-assistant h6 {
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -43,11 +43,13 @@ const I18n = (() => {
|
|
|
43
43
|
"chat.history_load_failed": "Could not load history",
|
|
44
44
|
"chat.history_start": "No more history",
|
|
45
45
|
"chat.image_expired": "Expired",
|
|
46
|
-
"chat.done": "Done — {{n}} iteration(s),
|
|
46
|
+
"chat.done": "Done — {{n}} iteration(s), {{cost}}",
|
|
47
47
|
"chat.interrupted": "Interrupted.",
|
|
48
48
|
"chat.feedback_hint": "Or type your own answer below ↓",
|
|
49
49
|
"chat.newMessageHint": "New messages ↓",
|
|
50
50
|
"chat.retry": "Retry",
|
|
51
|
+
"chat.resetSession": "Reset session",
|
|
52
|
+
"chat.resetSessionConfirm": "Reset will start a brand-new session. The current conversation history stays in your sidebar but will no longer be active. Continue?",
|
|
51
53
|
"chat.copy": "Copy",
|
|
52
54
|
"chat.copied": "Copied",
|
|
53
55
|
"chat.empty.title": "Start the conversation",
|
|
@@ -541,7 +543,7 @@ const I18n = (() => {
|
|
|
541
543
|
"chat.history_load_failed": "历史记录加载失败",
|
|
542
544
|
"chat.history_start": "没有更多历史了",
|
|
543
545
|
"chat.image_expired": "已过期",
|
|
544
|
-
"chat.done": "完成 — {{n}}
|
|
546
|
+
"chat.done": "完成 — {{n}} 步,{{cost}}",
|
|
545
547
|
"chat.interrupted": "已中断。",
|
|
546
548
|
"chat.feedback_hint": "或在下方输入框自由作答 ↓",
|
|
547
549
|
"chat.newMessageHint": "有新消息 ↓",
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -1492,7 +1492,7 @@ const Sessions = (() => {
|
|
|
1492
1492
|
: "";
|
|
1493
1493
|
|
|
1494
1494
|
// Pin icon (always visible for pinned sessions)
|
|
1495
|
-
const pinIcon = s.pinned ? `<span class="session-pin-icon"
|
|
1495
|
+
const pinIcon = s.pinned ? `<span class="session-pin-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(45deg);display:block"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg></span>` : "";
|
|
1496
1496
|
|
|
1497
1497
|
// Status dot: only rendered for non-idle states. Idle is the default
|
|
1498
1498
|
// state for 95% of sessions and doesn't deserve a persistent visual marker.
|
|
@@ -1505,7 +1505,7 @@ const Sessions = (() => {
|
|
|
1505
1505
|
<div class="session-name">${dotHtml}<span class="session-name__text">${escapeHtml(displayName)}</span>${badgeHtml}${codingBadgeHtml}${pinIcon}</div>
|
|
1506
1506
|
<div class="session-meta">${metaText}</div>
|
|
1507
1507
|
</div>
|
|
1508
|
-
<button class="session-actions-btn" title="Actions"
|
|
1508
|
+
<button class="session-actions-btn" title="Actions"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="2.5" cy="7" r="1.2" fill="currentColor"/><circle cx="7" cy="7" r="1.2" fill="currentColor"/><circle cx="11.5" cy="7" r="1.2" fill="currentColor"/></svg></button>`;
|
|
1509
1509
|
|
|
1510
1510
|
// Use a click timer to distinguish single-click (select) from double-click (old rename behavior).
|
|
1511
1511
|
let clickTimer = null;
|
|
@@ -1618,7 +1618,7 @@ const Sessions = (() => {
|
|
|
1618
1618
|
// session when renderList() forces scroll-to-active.
|
|
1619
1619
|
const sidebarList = document.getElementById("sidebar-list");
|
|
1620
1620
|
const savedScrollTop = sidebarList ? sidebarList.scrollTop : 0;
|
|
1621
|
-
Sessions.renderList();
|
|
1621
|
+
Sessions.renderList({ skipScrollToActive: true });
|
|
1622
1622
|
|
|
1623
1623
|
try {
|
|
1624
1624
|
// Cursor: oldest created_at in the current list, EXCLUDING pinned
|
|
@@ -1651,7 +1651,7 @@ const Sessions = (() => {
|
|
|
1651
1651
|
console.error("loadMore error:", e);
|
|
1652
1652
|
} finally {
|
|
1653
1653
|
_loadingMore = false;
|
|
1654
|
-
Sessions.renderList();
|
|
1654
|
+
Sessions.renderList({ skipScrollToActive: true });
|
|
1655
1655
|
// Restore scroll position so the user stays where they were
|
|
1656
1656
|
if (sidebarList) sidebarList.scrollTop = savedScrollTop;
|
|
1657
1657
|
}
|
|
@@ -1826,7 +1826,7 @@ const Sessions = (() => {
|
|
|
1826
1826
|
|
|
1827
1827
|
// ── Rendering ─────────────────────────────────────────────────────────
|
|
1828
1828
|
|
|
1829
|
-
renderList() {
|
|
1829
|
+
renderList({ skipScrollToActive = false } = {}) {
|
|
1830
1830
|
// Sort helper: pinned first, then newest-first by created_at
|
|
1831
1831
|
const byPinnedAndTime = (a, b) => {
|
|
1832
1832
|
// Pinned sessions always come first
|
|
@@ -1877,15 +1877,17 @@ const Sessions = (() => {
|
|
|
1877
1877
|
if (_hasMore) list.appendChild(_makeLoadMoreBtn());
|
|
1878
1878
|
|
|
1879
1879
|
// Scroll active session into view so the sidebar always shows the current session.
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1880
|
+
if (!skipScrollToActive) {
|
|
1881
|
+
const activeEl = list.querySelector(".session-item.active");
|
|
1882
|
+
if (activeEl) {
|
|
1883
|
+
// If the active session is the very first item, scroll to top of the sidebar
|
|
1884
|
+
// container so sticky headers / expanded panels don't obscure it.
|
|
1885
|
+
if (activeEl === list.firstElementChild) {
|
|
1886
|
+
const sidebarList = document.getElementById("sidebar-list");
|
|
1887
|
+
if (sidebarList) sidebarList.scrollTop = 0;
|
|
1888
|
+
} else {
|
|
1889
|
+
activeEl.scrollIntoView({ block: "nearest" });
|
|
1890
|
+
}
|
|
1889
1891
|
}
|
|
1890
1892
|
}
|
|
1891
1893
|
},
|
|
@@ -1961,17 +1963,27 @@ const Sessions = (() => {
|
|
|
1961
1963
|
// Close any existing menu first
|
|
1962
1964
|
Sessions._closeActionsMenu();
|
|
1963
1965
|
|
|
1966
|
+
// Lucide-style stroked icons to match the rest of the UI
|
|
1967
|
+
const iconPin = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:rotate(45deg);display:block"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>`;
|
|
1968
|
+
const iconRename = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4z"/></svg>`;
|
|
1969
|
+
const iconTrash = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>`;
|
|
1970
|
+
|
|
1971
|
+
const pinLabel = session.pinned ? I18n.t("sessions.actions.unpin") : I18n.t("sessions.actions.pin");
|
|
1972
|
+
|
|
1964
1973
|
const menu = document.createElement("div");
|
|
1965
1974
|
menu.className = "session-actions-menu";
|
|
1966
1975
|
menu.innerHTML = `
|
|
1967
1976
|
<div class="session-actions-menu-item" data-action="pin">
|
|
1968
|
-
|
|
1977
|
+
<span class="session-actions-menu-icon">${iconPin}</span>
|
|
1978
|
+
<span class="session-actions-menu-label">${escapeHtml(pinLabel)}</span>
|
|
1969
1979
|
</div>
|
|
1970
1980
|
<div class="session-actions-menu-item" data-action="rename">
|
|
1971
|
-
|
|
1981
|
+
<span class="session-actions-menu-icon">${iconRename}</span>
|
|
1982
|
+
<span class="session-actions-menu-label">${escapeHtml(I18n.t("sessions.actions.rename"))}</span>
|
|
1972
1983
|
</div>
|
|
1973
1984
|
<div class="session-actions-menu-item session-actions-menu-item--danger" data-action="delete">
|
|
1974
|
-
|
|
1985
|
+
<span class="session-actions-menu-icon">${iconTrash}</span>
|
|
1986
|
+
<span class="session-actions-menu-label">${escapeHtml(I18n.t("sessions.actions.delete"))}</span>
|
|
1975
1987
|
</div>
|
|
1976
1988
|
`;
|
|
1977
1989
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -395,6 +395,8 @@ files:
|
|
|
395
395
|
- lib/clacky/server/channel/channel_config.rb
|
|
396
396
|
- lib/clacky/server/channel/channel_manager.rb
|
|
397
397
|
- lib/clacky/server/channel/channel_ui_controller.rb
|
|
398
|
+
- lib/clacky/server/discover.rb
|
|
399
|
+
- lib/clacky/server/epipe_safe_io.rb
|
|
398
400
|
- lib/clacky/server/http_server.rb
|
|
399
401
|
- lib/clacky/server/scheduler.rb
|
|
400
402
|
- lib/clacky/server/server_master.rb
|