openclacky 0.9.35 → 0.9.36
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 +12 -0
- data/lib/clacky/agent/skill_manager.rb +20 -8
- data/lib/clacky/agent.rb +22 -17
- data/lib/clacky/agent_config.rb +166 -40
- data/lib/clacky/cli.rb +32 -13
- data/lib/clacky/server/http_server.rb +141 -59
- data/lib/clacky/tools/terminal.rb +89 -15
- data/lib/clacky/ui2/ui_controller.rb +16 -7
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +75 -149
- data/lib/clacky/web/app.js +23 -17
- data/lib/clacky/web/i18n.js +10 -0
- data/lib/clacky/web/index.html +8 -18
- data/lib/clacky/web/sessions.js +85 -56
- metadata +1 -1
data/lib/clacky/web/app.css
CHANGED
|
@@ -1359,144 +1359,36 @@ body {
|
|
|
1359
1359
|
|
|
1360
1360
|
/* ── Chat panel ──────────────────────────────────────────────────────────── */
|
|
1361
1361
|
#chat-panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
|
1362
|
-
#chat-header {
|
|
1363
|
-
padding: 10px 20px;
|
|
1364
|
-
display: flex;
|
|
1365
|
-
align-items: center;
|
|
1366
|
-
gap: 10px;
|
|
1367
|
-
min-height: 44px;
|
|
1368
|
-
}
|
|
1369
|
-
/* Title block — stacks the name + subtitle vertically so the header has
|
|
1370
|
-
two lines of information (like Linear / Arc) without growing wider. */
|
|
1371
|
-
#chat-title-block {
|
|
1372
|
-
display: flex;
|
|
1373
|
-
flex-direction: column;
|
|
1374
|
-
min-width: 0; /* allow ellipsis inside */
|
|
1375
|
-
flex: 1;
|
|
1376
|
-
line-height: 1.25;
|
|
1377
|
-
gap: 2px;
|
|
1378
|
-
}
|
|
1379
|
-
#chat-title {
|
|
1380
|
-
font-weight: 600;
|
|
1381
|
-
font-size: 15px;
|
|
1382
|
-
color: var(--color-text-primary);
|
|
1383
|
-
white-space: nowrap;
|
|
1384
|
-
overflow: hidden;
|
|
1385
|
-
text-overflow: ellipsis;
|
|
1386
|
-
}
|
|
1387
|
-
#chat-subtitle {
|
|
1388
|
-
display: flex;
|
|
1389
|
-
align-items: center;
|
|
1390
|
-
gap: 6px;
|
|
1391
|
-
font-size: 11px;
|
|
1392
|
-
color: var(--color-text-secondary);
|
|
1393
|
-
opacity: 0.75;
|
|
1394
|
-
overflow: hidden;
|
|
1395
|
-
white-space: nowrap;
|
|
1396
|
-
text-overflow: ellipsis;
|
|
1397
|
-
min-width: 0;
|
|
1398
|
-
}
|
|
1399
|
-
#chat-subtitle .chat-sub-source {
|
|
1400
|
-
font-weight: 500;
|
|
1401
|
-
color: var(--color-text-secondary);
|
|
1402
|
-
flex-shrink: 0;
|
|
1403
|
-
}
|
|
1404
|
-
#chat-subtitle .chat-sub-dir {
|
|
1405
|
-
font-family: var(--font-mono, monospace);
|
|
1406
|
-
overflow: hidden;
|
|
1407
|
-
text-overflow: ellipsis;
|
|
1408
|
-
min-width: 0;
|
|
1409
|
-
opacity: 0.85;
|
|
1410
|
-
}
|
|
1411
|
-
#chat-subtitle .chat-sub-sep {
|
|
1412
|
-
opacity: 0.35;
|
|
1413
|
-
flex-shrink: 0;
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
/* Status badge styles - softed design */
|
|
1417
|
-
.status-idle,
|
|
1418
|
-
.status-running,
|
|
1419
|
-
.status-error {
|
|
1420
|
-
display: inline-flex;
|
|
1421
|
-
align-items: center;
|
|
1422
|
-
padding: 2px 8px;
|
|
1423
|
-
border-radius: 4px;
|
|
1424
|
-
font-size: 11px;
|
|
1425
|
-
font-weight: 500;
|
|
1426
|
-
text-transform: lowercase;
|
|
1427
|
-
}
|
|
1428
1362
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
border: 1px solid var(--color-border-secondary);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
[data-theme="light"] .status-idle {
|
|
1436
|
-
background: rgba(0, 0, 0, 0.04);
|
|
1437
|
-
color: var(--color-text-secondary);
|
|
1438
|
-
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
.status-running {
|
|
1442
|
-
background: rgba(34, 197, 94, 0.1);
|
|
1443
|
-
color: var(--color-success);
|
|
1444
|
-
border: 1px solid rgba(34, 197, 94, 0.2);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
[data-theme="light"] .status-running {
|
|
1448
|
-
background: rgba(34, 197, 94, 0.08);
|
|
1449
|
-
color: #16a34a;
|
|
1450
|
-
border: 1px solid rgba(34, 197, 94, 0.15);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
.status-error {
|
|
1454
|
-
background: rgba(239, 68, 68, 0.1);
|
|
1455
|
-
color: var(--color-error);
|
|
1456
|
-
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
[data-theme="light"] .status-error {
|
|
1460
|
-
background: rgba(239, 68, 68, 0.08);
|
|
1461
|
-
color: #dc2626;
|
|
1462
|
-
border: 1px solid rgba(239, 68, 68, 0.15);
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
/* Delete session button in chat header */
|
|
1466
|
-
/* Back button in chat header — hidden on desktop, shown on mobile via media query */
|
|
1363
|
+
/* Mobile-only floating back button — replaces the old in-header back button.
|
|
1364
|
+
Hidden on desktop; mobile media query enables it. Positioned absolutely so
|
|
1365
|
+
it doesn't take layout space or add visual chrome on desktop. */
|
|
1467
1366
|
.chat-back-btn {
|
|
1468
1367
|
display: none;
|
|
1368
|
+
}
|
|
1369
|
+
.chat-back-floating {
|
|
1370
|
+
position: absolute;
|
|
1371
|
+
top: 8px;
|
|
1372
|
+
left: 8px;
|
|
1373
|
+
z-index: 5;
|
|
1469
1374
|
align-items: center;
|
|
1470
1375
|
justify-content: center;
|
|
1471
1376
|
width: 32px;
|
|
1472
1377
|
height: 32px;
|
|
1473
|
-
background:
|
|
1474
|
-
border:
|
|
1378
|
+
background: var(--color-bg-primary);
|
|
1379
|
+
border: 1px solid var(--color-border-secondary);
|
|
1475
1380
|
border-radius: 6px;
|
|
1476
1381
|
color: var(--color-text-secondary);
|
|
1477
1382
|
cursor: pointer;
|
|
1478
1383
|
padding: 0;
|
|
1479
|
-
flex-shrink: 0;
|
|
1480
1384
|
transition: background .15s, color .15s;
|
|
1385
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
1481
1386
|
}
|
|
1482
|
-
.chat-back-
|
|
1387
|
+
.chat-back-floating:hover { background: var(--color-bg-hover); }
|
|
1483
1388
|
|
|
1484
|
-
.
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
align-items: center;
|
|
1488
|
-
justify-content: center;
|
|
1489
|
-
width: 28px;
|
|
1490
|
-
height: 28px;
|
|
1491
|
-
background: transparent;
|
|
1492
|
-
border: none;
|
|
1493
|
-
border-radius: 6px;
|
|
1494
|
-
color: var(--color-text-tertiary);
|
|
1495
|
-
cursor: pointer;
|
|
1496
|
-
padding: 0;
|
|
1497
|
-
transition: background .15s, color .15s;
|
|
1498
|
-
}
|
|
1499
|
-
.btn-delete-session:hover { background: var(--color-error-bg); color: var(--color-error); }
|
|
1389
|
+
/* Status badges (.status-idle/running/error) used to live in #chat-header.
|
|
1390
|
+
That header has been removed; the bottom #session-info-bar uses its own
|
|
1391
|
+
.sib-status-* classes instead. Legacy .status-* styles removed. */
|
|
1500
1392
|
|
|
1501
1393
|
/* ── Messages ────────────────────────────────────────────────────────────── */
|
|
1502
1394
|
#messages {
|
|
@@ -1508,6 +1400,61 @@ body {
|
|
|
1508
1400
|
gap: 12px;
|
|
1509
1401
|
}
|
|
1510
1402
|
|
|
1403
|
+
/* Empty-state hint: shown inside #messages when a session has no messages yet.
|
|
1404
|
+
Designed to be quiet and low-contrast so it guides without distracting. */
|
|
1405
|
+
.chat-empty-hint {
|
|
1406
|
+
margin: auto; /* vertical + horizontal centering inside flex column */
|
|
1407
|
+
max-width: 420px;
|
|
1408
|
+
padding: 24px 16px;
|
|
1409
|
+
display: flex;
|
|
1410
|
+
flex-direction: column;
|
|
1411
|
+
align-items: center;
|
|
1412
|
+
text-align: center;
|
|
1413
|
+
color: var(--color-text-secondary);
|
|
1414
|
+
user-select: none;
|
|
1415
|
+
pointer-events: none; /* purely decorative — don't block clicks/scroll */
|
|
1416
|
+
opacity: 0.85;
|
|
1417
|
+
}
|
|
1418
|
+
.chat-empty-hint .chat-empty-icon {
|
|
1419
|
+
color: var(--color-text-tertiary);
|
|
1420
|
+
margin-bottom: 14px;
|
|
1421
|
+
opacity: 0.55;
|
|
1422
|
+
}
|
|
1423
|
+
.chat-empty-hint .chat-empty-title {
|
|
1424
|
+
color: var(--color-text-primary);
|
|
1425
|
+
font-size: 15px;
|
|
1426
|
+
font-weight: 600;
|
|
1427
|
+
margin-bottom: 4px;
|
|
1428
|
+
}
|
|
1429
|
+
.chat-empty-hint .chat-empty-subtitle {
|
|
1430
|
+
color: var(--color-text-secondary);
|
|
1431
|
+
font-size: 13px;
|
|
1432
|
+
margin-bottom: 18px;
|
|
1433
|
+
opacity: 0.85;
|
|
1434
|
+
}
|
|
1435
|
+
.chat-empty-hint .chat-empty-tips {
|
|
1436
|
+
list-style: none;
|
|
1437
|
+
padding: 0;
|
|
1438
|
+
margin: 0;
|
|
1439
|
+
display: flex;
|
|
1440
|
+
flex-direction: column;
|
|
1441
|
+
gap: 6px;
|
|
1442
|
+
font-size: 12px;
|
|
1443
|
+
color: var(--color-text-tertiary);
|
|
1444
|
+
}
|
|
1445
|
+
.chat-empty-hint .chat-empty-tips li {
|
|
1446
|
+
position: relative;
|
|
1447
|
+
padding-left: 14px;
|
|
1448
|
+
line-height: 1.5;
|
|
1449
|
+
}
|
|
1450
|
+
.chat-empty-hint .chat-empty-tips li::before {
|
|
1451
|
+
content: "·";
|
|
1452
|
+
position: absolute;
|
|
1453
|
+
left: 4px;
|
|
1454
|
+
top: -1px;
|
|
1455
|
+
opacity: 0.6;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1511
1458
|
/* New message notification banner */
|
|
1512
1459
|
.new-message-banner {
|
|
1513
1460
|
position: absolute;
|
|
@@ -5970,31 +5917,6 @@ body.setup-mode[data-theme="dark"] {
|
|
|
5970
5917
|
padding: 0 12px;
|
|
5971
5918
|
}
|
|
5972
5919
|
|
|
5973
|
-
/* Chat sub-header: tighten padding */
|
|
5974
|
-
#chat-header {
|
|
5975
|
-
padding: 8px 12px;
|
|
5976
|
-
gap: 8px;
|
|
5977
|
-
}
|
|
5978
|
-
|
|
5979
|
-
/* Truncate long session titles */
|
|
5980
|
-
#chat-title {
|
|
5981
|
-
font-size: 14px;
|
|
5982
|
-
overflow: hidden;
|
|
5983
|
-
text-overflow: ellipsis;
|
|
5984
|
-
white-space: nowrap;
|
|
5985
|
-
min-width: 0;
|
|
5986
|
-
flex: 1;
|
|
5987
|
-
}
|
|
5988
|
-
|
|
5989
|
-
/* Status badge: smaller on mobile */
|
|
5990
|
-
.status-idle,
|
|
5991
|
-
.status-running,
|
|
5992
|
-
.status-error {
|
|
5993
|
-
font-size: 10px;
|
|
5994
|
-
padding: 1px 6px;
|
|
5995
|
-
flex-shrink: 0;
|
|
5996
|
-
}
|
|
5997
|
-
|
|
5998
5920
|
/* Session info bar: single-line, no hover-expand, font smaller */
|
|
5999
5921
|
#session-info-bar {
|
|
6000
5922
|
padding: 3px 12px;
|
|
@@ -6050,6 +5972,10 @@ body.setup-mode[data-theme="dark"] {
|
|
|
6050
5972
|
.chat-back-btn {
|
|
6051
5973
|
display: flex;
|
|
6052
5974
|
}
|
|
5975
|
+
/* Reserve top space in messages so floating back button doesn't overlap content */
|
|
5976
|
+
#chat-panel #messages {
|
|
5977
|
+
padding-top: 52px;
|
|
5978
|
+
}
|
|
6053
5979
|
|
|
6054
5980
|
/* Welcome page: vertically centered but shifted up, add horizontal padding */
|
|
6055
5981
|
#welcome {
|
data/lib/clacky/web/app.js
CHANGED
|
@@ -437,9 +437,7 @@ WS.onEvent(ev => {
|
|
|
437
437
|
case "session_renamed": {
|
|
438
438
|
Sessions.patch(ev.session_id, { name: ev.name });
|
|
439
439
|
Sessions.renderList();
|
|
440
|
-
|
|
441
|
-
$("chat-title").textContent = ev.name;
|
|
442
|
-
}
|
|
440
|
+
// Title is now shown only in the sidebar; chat-header element was removed.
|
|
443
441
|
break;
|
|
444
442
|
}
|
|
445
443
|
|
|
@@ -847,7 +845,7 @@ $("sidebar-overlay").addEventListener("click", _closeSidebar);
|
|
|
847
845
|
// On mobile: start with sidebar hidden
|
|
848
846
|
if (_isMobile()) _closeSidebar();
|
|
849
847
|
|
|
850
|
-
// Mobile back button
|
|
848
|
+
// Mobile floating back button (visible only on mobile via CSS): tap to go back to welcome.
|
|
851
849
|
const _btnBack = document.querySelector(".chat-back-btn");
|
|
852
850
|
if (_btnBack) _btnBack.addEventListener("click", () => Router.navigate("welcome"));
|
|
853
851
|
|
|
@@ -985,9 +983,14 @@ document.addEventListener("change", e => {
|
|
|
985
983
|
if ($("theme-toggle-header")) {
|
|
986
984
|
$("theme-toggle-header").addEventListener("click", () => Theme.toggle());
|
|
987
985
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
986
|
+
// btn-delete-session was removed with chat-header; deletion is now triggered from
|
|
987
|
+
// the bottom session-info-bar actions dropdown (see Sessions._showActionsMenu).
|
|
988
|
+
const _btnDeleteSession = $("btn-delete-session");
|
|
989
|
+
if (_btnDeleteSession) {
|
|
990
|
+
_btnDeleteSession.addEventListener("click", () => {
|
|
991
|
+
if (Sessions.activeId) Sessions.deleteSession(Sessions.activeId);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
991
994
|
|
|
992
995
|
// Load older history when the user scrolls to the top of the message list
|
|
993
996
|
$("messages").addEventListener("scroll", () => {
|
|
@@ -1617,7 +1620,7 @@ window.bootAfterBrand = async function() {
|
|
|
1617
1620
|
dropdown.innerHTML = "";
|
|
1618
1621
|
|
|
1619
1622
|
models.forEach(m => {
|
|
1620
|
-
console.log("[Model Switcher] Adding model:", m.model, "current:", currentModel);
|
|
1623
|
+
console.log("[Model Switcher] Adding model:", m.model, "id:", m.id, "current:", currentModel);
|
|
1621
1624
|
const opt = document.createElement("div");
|
|
1622
1625
|
opt.className = "sib-model-option";
|
|
1623
1626
|
if (m.model === currentModel) opt.classList.add("current");
|
|
@@ -1633,7 +1636,8 @@ window.bootAfterBrand = async function() {
|
|
|
1633
1636
|
opt.appendChild(badge);
|
|
1634
1637
|
}
|
|
1635
1638
|
|
|
1636
|
-
|
|
1639
|
+
// Switch by id (stable across reorders/edits). Keep model name for UI update.
|
|
1640
|
+
opt.addEventListener("click", () => _switchModel(sessionId, m.id, m.model));
|
|
1637
1641
|
dropdown.appendChild(opt);
|
|
1638
1642
|
});
|
|
1639
1643
|
console.log("[Model Switcher] Dropdown populated, children count:", dropdown.children.length);
|
|
@@ -1644,7 +1648,9 @@ window.bootAfterBrand = async function() {
|
|
|
1644
1648
|
}
|
|
1645
1649
|
|
|
1646
1650
|
// Switch session model via API
|
|
1647
|
-
|
|
1651
|
+
// modelId — stable runtime id (required by backend)
|
|
1652
|
+
// modelName — display name, used for optimistic UI update
|
|
1653
|
+
async function _switchModel(sessionId, modelId, modelName) {
|
|
1648
1654
|
const dropdown = $("sib-model-dropdown");
|
|
1649
1655
|
if (dropdown) {
|
|
1650
1656
|
dropdown.style.display = "none";
|
|
@@ -1655,20 +1661,20 @@ window.bootAfterBrand = async function() {
|
|
|
1655
1661
|
const res = await fetch(`/api/sessions/${sessionId}/model`, {
|
|
1656
1662
|
method: "PATCH",
|
|
1657
1663
|
headers: { "Content-Type": "application/json" },
|
|
1658
|
-
body: JSON.stringify({
|
|
1664
|
+
body: JSON.stringify({ model_id: modelId })
|
|
1659
1665
|
});
|
|
1660
|
-
|
|
1666
|
+
|
|
1661
1667
|
const data = await res.json();
|
|
1662
|
-
|
|
1668
|
+
|
|
1663
1669
|
if (!res.ok) {
|
|
1664
1670
|
throw new Error(data.error || "Unknown error");
|
|
1665
1671
|
}
|
|
1666
|
-
|
|
1672
|
+
|
|
1667
1673
|
// Update UI optimistically (will be confirmed by session_update broadcast)
|
|
1668
1674
|
const sibModel = $("sib-model");
|
|
1669
|
-
if (sibModel) sibModel.textContent =
|
|
1670
|
-
|
|
1671
|
-
console.log(`Switched session ${sessionId} to model ${
|
|
1675
|
+
if (sibModel) sibModel.textContent = modelName;
|
|
1676
|
+
|
|
1677
|
+
console.log(`Switched session ${sessionId} to model ${modelName} (${modelId})`);
|
|
1672
1678
|
} catch (e) {
|
|
1673
1679
|
console.error("Failed to switch model:", e);
|
|
1674
1680
|
alert("Failed to switch model: " + e.message);
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -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": "暂无对话",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -234,24 +234,14 @@
|
|
|
234
234
|
|
|
235
235
|
<!-- Chat panel -->
|
|
236
236
|
<div id="chat-panel" style="display:none">
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
</
|
|
244
|
-
|
|
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">
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -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
|
|
@@ -1260,8 +1333,8 @@ const Sessions = (() => {
|
|
|
1260
1333
|
Sessions.patch(sessionId, { name: newName });
|
|
1261
1334
|
Sessions.renderList();
|
|
1262
1335
|
if (sessionId === Sessions.activeId) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1336
|
+
// chat-header was removed — no title element to update here.
|
|
1337
|
+
// Sidebar re-renders with the new name above.
|
|
1265
1338
|
}
|
|
1266
1339
|
} else {
|
|
1267
1340
|
console.error("Rename failed:", await res.text());
|
|
@@ -1399,64 +1472,20 @@ const Sessions = (() => {
|
|
|
1399
1472
|
},
|
|
1400
1473
|
|
|
1401
1474
|
updateStatusBar(status) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
$("chat-status").className = "status-error";
|
|
1407
|
-
} else {
|
|
1408
|
-
$("chat-status").className = "status-idle";
|
|
1409
|
-
}
|
|
1410
|
-
$("btn-interrupt").style.display = status === "running" ? "" : "none";
|
|
1475
|
+
// chat-header was removed; status text is now shown in the bottom session-info-bar (#sib-status).
|
|
1476
|
+
// Here we only update the interrupt button visibility.
|
|
1477
|
+
const interrupt = $("btn-interrupt");
|
|
1478
|
+
if (interrupt) interrupt.style.display = status === "running" ? "" : "none";
|
|
1411
1479
|
},
|
|
1412
1480
|
|
|
1413
1481
|
/**
|
|
1414
|
-
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
* this session is without glancing down at the status bar.
|
|
1482
|
+
* No-op: the chat header element (#chat-header) was removed. All session
|
|
1483
|
+
* metadata (title, source, working dir, status) is now shown in the
|
|
1484
|
+
* sidebar and the bottom #session-info-bar. Kept as a stub so existing
|
|
1485
|
+
* call sites don't need to be updated.
|
|
1419
1486
|
*/
|
|
1420
|
-
updateChatHeader(
|
|
1421
|
-
|
|
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
|
-
}
|
|
1487
|
+
updateChatHeader(_s) {
|
|
1488
|
+
// intentionally empty
|
|
1460
1489
|
},
|
|
1461
1490
|
|
|
1462
1491
|
/** Update the session info bar below the chat header with current session metadata. */
|