openclacky 0.9.35 → 0.9.37

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.
@@ -46,6 +46,11 @@ const I18n = (() => {
46
46
  "chat.retry": "Retry",
47
47
  "chat.copy": "Copy",
48
48
  "chat.copied": "Copied",
49
+ "chat.empty.title": "Start the conversation",
50
+ "chat.empty.subtitle": "Ask anything, or use a skill to get going.",
51
+ "chat.empty.tip1": "Type / to browse skills",
52
+ "chat.empty.tip2": "Attach images, PDFs, or docs for context",
53
+ "chat.empty.tip3": "Shift+Enter for a new line",
49
54
 
50
55
  // ── Session list ──
51
56
  "sessions.empty": "No sessions yet",
@@ -422,6 +427,11 @@ const I18n = (() => {
422
427
  "chat.retry": "重试",
423
428
  "chat.copy": "复制",
424
429
  "chat.copied": "已复制",
430
+ "chat.empty.title": "开始新的对话",
431
+ "chat.empty.subtitle": "直接提问,或用一个 Skill 启动。",
432
+ "chat.empty.tip1": "输入 / 浏览 Skill",
433
+ "chat.empty.tip2": "可粘贴或拖入图片、PDF、文档",
434
+ "chat.empty.tip3": "Shift+Enter 换行",
425
435
 
426
436
  // ── Session list ──
427
437
  "sessions.empty": "暂无对话",
@@ -234,24 +234,14 @@
234
234
 
235
235
  <!-- Chat panel -->
236
236
  <div id="chat-panel" style="display:none">
237
- <header id="chat-header">
238
- <!-- Mobile-only back button: shown inside a session -->
239
- <button id="btn-back-mobile" class="chat-back-btn" title="Back">
240
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
241
- <path d="M15 18l-6-6 6-6"/>
242
- </svg>
243
- </button>
244
- <span id="chat-title-block">
245
- <span id="chat-title">Session</span>
246
- <span id="chat-subtitle"></span>
247
- </span>
248
- <span id="chat-status" class="status-idle">idle</span>
249
- <button id="btn-delete-session" class="btn-delete-session" title="Delete session">
250
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
251
- <polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
252
- </svg>
253
- </button>
254
- </header>
237
+ <!-- Mobile-only floating back button: shown inside a session on small screens.
238
+ Desktop: hidden. All other session info (title, status, dir, delete) is
239
+ provided by the bottom #session-info-bar. -->
240
+ <button id="btn-back-mobile" class="chat-back-btn chat-back-floating" title="Back">
241
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
242
+ <path d="M15 18l-6-6 6-6"/>
243
+ </svg>
244
+ </button>
255
245
  <div id="messages"></div>
256
246
  <!-- New message notification banner -->
257
247
  <div id="new-message-banner" class="new-message-banner" style="display:none">
@@ -183,6 +183,75 @@ const Sessions = (() => {
183
183
  banner.style.display = "none";
184
184
  }
185
185
 
186
+ // ── Empty-state hint ──────────────────────────────────────────────────
187
+ //
188
+ // Shows a small centered hint inside #messages when the message list is
189
+ // empty (e.g. just-created session with no history). Uses a MutationObserver
190
+ // so we don't have to instrument every append/clear call site.
191
+
192
+ const _EMPTY_HINT_ID = "chat-empty-hint";
193
+
194
+ function _buildEmptyHintHtml() {
195
+ const title = I18n.t("chat.empty.title");
196
+ const subtitle = I18n.t("chat.empty.subtitle");
197
+ const tip1 = I18n.t("chat.empty.tip1");
198
+ const tip2 = I18n.t("chat.empty.tip2");
199
+ const tip3 = I18n.t("chat.empty.tip3");
200
+ return `
201
+ <div class="chat-empty-icon" aria-hidden="true">
202
+ <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
203
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
204
+ </svg>
205
+ </div>
206
+ <div class="chat-empty-title">${escapeHtml(title)}</div>
207
+ <div class="chat-empty-subtitle">${escapeHtml(subtitle)}</div>
208
+ <ul class="chat-empty-tips">
209
+ <li>${escapeHtml(tip1)}</li>
210
+ <li>${escapeHtml(tip2)}</li>
211
+ <li>${escapeHtml(tip3)}</li>
212
+ </ul>
213
+ `;
214
+ }
215
+
216
+ function _updateEmptyHint() {
217
+ const messages = $("messages");
218
+ if (!messages) return;
219
+ // Check if there's any real content besides the hint itself
220
+ const hasReal = Array.from(messages.children).some(
221
+ (el) => el.id !== _EMPTY_HINT_ID
222
+ );
223
+ const existing = document.getElementById(_EMPTY_HINT_ID);
224
+ // While history is still loading, don't flash the hint — wait until the
225
+ // first fetch completes so we know whether the session is actually empty.
226
+ const loading = !!(_activeId && _historyState[_activeId] && _historyState[_activeId].loading);
227
+ if (hasReal || loading) {
228
+ if (existing) existing.remove();
229
+ } else {
230
+ if (!existing) {
231
+ const el = document.createElement("div");
232
+ el.id = _EMPTY_HINT_ID;
233
+ el.className = "chat-empty-hint";
234
+ el.innerHTML = _buildEmptyHintHtml();
235
+ messages.appendChild(el);
236
+ }
237
+ }
238
+ }
239
+
240
+ function _initEmptyHint() {
241
+ const messages = $("messages");
242
+ if (!messages) return;
243
+ // Re-evaluate whenever children change (append/insertBefore/innerHTML="")
244
+ const observer = new MutationObserver(() => _updateEmptyHint());
245
+ observer.observe(messages, { childList: true });
246
+ // Re-render hint text on language change
247
+ document.addEventListener("langchange", () => {
248
+ const existing = document.getElementById(_EMPTY_HINT_ID);
249
+ if (existing) existing.innerHTML = _buildEmptyHintHtml();
250
+ });
251
+ // Initial paint
252
+ _updateEmptyHint();
253
+ }
254
+
186
255
  function _initNewMessageBanner() {
187
256
  const banner = $("new-message-banner");
188
257
  const messages = $("messages");
@@ -640,6 +709,9 @@ const Sessions = (() => {
640
709
  }
641
710
  } finally {
642
711
  state.loading = false;
712
+ // After loading finishes, re-evaluate the empty-state hint in case
713
+ // the session is genuinely empty (no events + no existing DOM content).
714
+ if (id === _activeId) _updateEmptyHint();
643
715
  }
644
716
  }
645
717
 
@@ -891,6 +963,7 @@ const Sessions = (() => {
891
963
  // ── Init ──────────────────────────────────────────────────────────────
892
964
  init() {
893
965
  _initNewMessageBanner();
966
+ _initEmptyHint();
894
967
  // Re-render session list (badges/labels) when the user switches language
895
968
  document.addEventListener("langchange", () => Sessions.renderList());
896
969
  // Browsers block file:// navigation from http:// pages. Intercept clicks on
@@ -954,8 +1027,14 @@ const Sessions = (() => {
954
1027
  Sessions.renderList();
955
1028
 
956
1029
  try {
957
- // Cursor: oldest created_at in the current list
1030
+ // Cursor: oldest created_at in the current list, EXCLUDING pinned
1031
+ // sessions. The backend always returns ALL pinned sessions on the
1032
+ // first page (they bypass pagination), so their created_at is
1033
+ // irrelevant for cursor calculation. Including them here would
1034
+ // cause the cursor to jump too far back and skip sessions between
1035
+ // the oldest pinned one and the real last-loaded non-pinned row.
958
1036
  const oldest = _sessions.reduce((min, s) => {
1037
+ if (s.pinned) return min; // ignore pinned
959
1038
  if (!s.created_at) return min;
960
1039
  return (!min || s.created_at < min) ? s.created_at : min;
961
1040
  }, null);
@@ -1260,8 +1339,8 @@ const Sessions = (() => {
1260
1339
  Sessions.patch(sessionId, { name: newName });
1261
1340
  Sessions.renderList();
1262
1341
  if (sessionId === Sessions.activeId) {
1263
- const titleEl = document.getElementById("chat-title");
1264
- if (titleEl) titleEl.textContent = newName;
1342
+ // chat-header was removed — no title element to update here.
1343
+ // Sidebar re-renders with the new name above.
1265
1344
  }
1266
1345
  } else {
1267
1346
  console.error("Rename failed:", await res.text());
@@ -1399,64 +1478,20 @@ const Sessions = (() => {
1399
1478
  },
1400
1479
 
1401
1480
  updateStatusBar(status) {
1402
- $("chat-status").textContent = status || "idle";
1403
- if (status === "running") {
1404
- $("chat-status").className = "status-running";
1405
- } else if (status === "error") {
1406
- $("chat-status").className = "status-error";
1407
- } else {
1408
- $("chat-status").className = "status-idle";
1409
- }
1410
- $("btn-interrupt").style.display = status === "running" ? "" : "none";
1481
+ // chat-header was removed; status text is now shown in the bottom session-info-bar (#sib-status).
1482
+ // Here we only update the interrupt button visibility.
1483
+ const interrupt = $("btn-interrupt");
1484
+ if (interrupt) interrupt.style.display = status === "running" ? "" : "none";
1411
1485
  },
1412
1486
 
1413
1487
  /**
1414
- /**
1415
- * Populate the chat header (title + subtitle) for the given session.
1416
- * Subtitle shows a source tag (Auto / Channel / Setup — no emoji) and
1417
- * the working directory gives the user immediate context for what
1418
- * this session is without glancing down at the status bar.
1488
+ * No-op: the chat header element (#chat-header) was removed. All session
1489
+ * metadata (title, source, working dir, status) is now shown in the
1490
+ * sidebar and the bottom #session-info-bar. Kept as a stub so existing
1491
+ * call sites don't need to be updated.
1419
1492
  */
1420
- updateChatHeader(s) {
1421
- const titleEl = $("chat-title");
1422
- const subEl = $("chat-subtitle");
1423
- if (!titleEl || !subEl) return;
1424
-
1425
- if (!s) {
1426
- titleEl.textContent = "";
1427
- subEl.innerHTML = "";
1428
- subEl.style.display = "none";
1429
- return;
1430
- }
1431
-
1432
- titleEl.textContent = s.name || _relativeTime(s.created_at);
1433
-
1434
- // Build subtitle pieces. No emoji — keeps the UI clean and avoids
1435
- // the "AI-generated" feel that overuse of emoji creates.
1436
- const parts = [];
1437
- let sourceLabel = "";
1438
- if (s.source === "cron") sourceLabel = I18n.t("sessions.badge.cron");
1439
- else if (s.source === "channel") sourceLabel = I18n.t("sessions.badge.channel");
1440
- else if (s.source === "setup") sourceLabel = I18n.t("sessions.badge.setup");
1441
-
1442
- if (sourceLabel) {
1443
- parts.push(
1444
- `<span class="chat-sub-source">${escapeHtml(sourceLabel)}</span>`
1445
- );
1446
- }
1447
- if (s.working_dir) {
1448
- parts.push(
1449
- `<span class="chat-sub-dir" title="${escapeHtml(s.working_dir)}">${escapeHtml(s.working_dir)}</span>`
1450
- );
1451
- }
1452
-
1453
- if (parts.length === 0) {
1454
- subEl.innerHTML = "";
1455
- subEl.style.display = "none";
1456
- } else {
1457
- subEl.innerHTML = parts.join(`<span class="chat-sub-sep">·</span>`);
1458
- subEl.style.display = "";
1459
- }
1493
+ updateChatHeader(_s) {
1494
+ // intentionally empty
1460
1495
  },
1461
1496
 
1462
1497
  /** Update the session info bar below the chat header with current session metadata. */
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.35
4
+ version: 0.9.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy