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.
@@ -854,69 +854,106 @@ body {
854
854
  display: none;
855
855
  align-items: center;
856
856
  justify-content: center;
857
- width: 18px;
858
- height: 18px;
857
+ width: 24px;
858
+ height: 24px;
859
859
  background: transparent;
860
860
  border: none;
861
- border-radius: 3px;
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
- font-weight: bold;
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-bg-hover);
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
- font-size: 11px;
882
- opacity: 0.7;
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: 6px;
894
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
895
- padding: 3px;
896
- min-width: 120px;
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 4px 12px rgba(0, 0, 0, 0.4);
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: 4px;
907
+ border-radius: 5px;
906
908
  cursor: pointer;
907
909
  color: var(--color-text-primary);
908
- font-size: 12px;
909
- transition: background .15s;
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
- color: var(--color-error);
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 {
@@ -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), ${{cost}}",
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}} 步,${{cost}}",
546
+ "chat.done": "完成 — {{n}} 步,{{cost}}",
545
547
  "chat.interrupted": "已中断。",
546
548
  "chat.feedback_hint": "或在下方输入框自由作答 ↓",
547
549
  "chat.newMessageHint": "有新消息 ↓",
@@ -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">📌</span>` : "";
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">⋯</button>`;
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
- const activeEl = list.querySelector(".session-item.active");
1881
- if (activeEl) {
1882
- // If the active session is the very first item, scroll to top of the sidebar
1883
- // container so sticky headers / expanded panels don't obscure it.
1884
- if (activeEl === list.firstElementChild) {
1885
- const sidebarList = document.getElementById("sidebar-list");
1886
- if (sidebarList) sidebarList.scrollTop = 0;
1887
- } else {
1888
- activeEl.scrollIntoView({ block: "nearest" });
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
- ${session.pinned ? "📌 " + I18n.t("sessions.actions.unpin") : "📌 " + I18n.t("sessions.actions.pin")}
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
- ✏️ ${I18n.t("sessions.actions.rename")}
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
- 🗑️ ${I18n.t("sessions.actions.delete")}
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.2
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-07 00:00:00.000000000 Z
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