openclacky 1.5.13 → 1.5.14
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 +51 -0
- data/lib/clacky/agent/chunk_index.rb +83 -0
- data/lib/clacky/agent/history_navigation.rb +239 -0
- data/lib/clacky/agent/session_serializer.rb +38 -97
- data/lib/clacky/agent.rb +281 -276
- data/lib/clacky/agent_config.rb +1 -1
- data/lib/clacky/billing/billing_store.rb +2 -2
- data/lib/clacky/billing/platform_billing.rb +6 -0
- data/lib/clacky/brand_config.rb +13 -2
- data/lib/clacky/cli.rb +27 -0
- data/lib/clacky/client.rb +2 -2
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +91 -5
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
- data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
- data/lib/clacky/default_extensions/preview/ext.yml +20 -0
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
- data/lib/clacky/extension/api_extension.rb +1 -1
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/openai_compat.rb +70 -30
- data/lib/clacky/message_format/bedrock.rb +6 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/providers.rb +38 -3
- data/lib/clacky/rich_ui/rich_ui_controller.rb +1 -1
- data/lib/clacky/search_config.rb +3 -3
- data/lib/clacky/server/channel/channel_manager.rb +8 -5
- data/lib/clacky/server/git_panel.rb +10 -2
- data/lib/clacky/server/http_server.rb +161 -14
- data/lib/clacky/server/preview.rb +351 -0
- data/lib/clacky/server/web_ui_controller.rb +1 -1
- data/lib/clacky/skill.rb +48 -0
- data/lib/clacky/tools/browser.rb +176 -19
- data/lib/clacky/tools/web_search.rb +79 -8
- data/lib/clacky/ui2/components/command_suggestions.rb +2 -1
- data/lib/clacky/ui2/components/input_area.rb +22 -2
- data/lib/clacky/ui2/components/modal_component.rb +35 -6
- data/lib/clacky/ui2/ui_controller.rb +44 -7
- data/lib/clacky/utils/file_processor.rb +19 -7
- data/lib/clacky/utils/mac_app_detector.rb +186 -0
- data/lib/clacky/utils/model_pricing.rb +114 -66
- data/lib/clacky/utils/windows_app_detector.rb +334 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +596 -130
- data/lib/clacky/web/components/chat-navigator.js +489 -202
- data/lib/clacky/web/components/code-editor.js +191 -5
- data/lib/clacky/web/components/model-picker.js +2 -1
- data/lib/clacky/web/components/quote-select.js +235 -0
- data/lib/clacky/web/core/aside.js +157 -8
- data/lib/clacky/web/core/ext.js +16 -0
- data/lib/clacky/web/features/billing/view.js +75 -21
- data/lib/clacky/web/features/skills/store.js +18 -1
- data/lib/clacky/web/features/skills/view.js +128 -7
- data/lib/clacky/web/features/workspace/store.js +80 -7
- data/lib/clacky/web/features/workspace/view.js +392 -40
- data/lib/clacky/web/i18n.js +90 -5
- data/lib/clacky/web/index.html +15 -6
- data/lib/clacky/web/sessions.js +193 -38
- data/lib/clacky/web/utils.js +34 -0
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
- data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
- data/lib/clacky/web/vendor/codemirror/package.json +29 -0
- data/lib/clacky/web/ws-dispatcher.js +20 -0
- data/lib/clacky.rb +2 -0
- metadata +11 -1
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const Sessions = (() => {
|
|
19
19
|
const _sessions = []; // [{ id, name, status, total_tasks, total_cost }]
|
|
20
|
-
const _historyState = {}; //
|
|
20
|
+
const _historyState = {}; // Per-session history window, cursors and in-flight request.
|
|
21
21
|
const _renderedCreatedAt = {}; // { [session_id]: Set<number> } — dedup by created_at
|
|
22
22
|
const _drafts = new Map(); // { [session_id]: composer textarea draft }
|
|
23
23
|
const _attachmentDrafts = new Map(); // { [session_id]: { images: [...], files: [...] } }
|
|
@@ -347,11 +347,8 @@ const Sessions = (() => {
|
|
|
347
347
|
// Clear the pane and dedup state; history will be re-fetched from API.
|
|
348
348
|
RenderTarget.outer().innerHTML = "";
|
|
349
349
|
delete _renderedCreatedAt[id];
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
_historyState[id].hasMore = true;
|
|
353
|
-
_historyState[id].loading = false; // reset so next fetch is not skipped
|
|
354
|
-
}
|
|
350
|
+
_historyState[id]?.controller?.abort();
|
|
351
|
+
delete _historyState[id];
|
|
355
352
|
// Reset scroll tracking when switching sessions
|
|
356
353
|
_userScrolledUp = false;
|
|
357
354
|
}
|
|
@@ -412,6 +409,12 @@ const Sessions = (() => {
|
|
|
412
409
|
function _showNewMessageBanner() {
|
|
413
410
|
const banner = $("new-message-banner");
|
|
414
411
|
if (!banner) return;
|
|
412
|
+
const label = banner.querySelector("span");
|
|
413
|
+
if (label) {
|
|
414
|
+
const key = Sessions.isHistoricalWindow() ? "chat.nav.latest" : "chat.newMessageHint";
|
|
415
|
+
label.dataset.i18n = key;
|
|
416
|
+
label.textContent = I18n.t(key);
|
|
417
|
+
}
|
|
415
418
|
banner.style.display = "block";
|
|
416
419
|
}
|
|
417
420
|
|
|
@@ -548,6 +551,10 @@ const Sessions = (() => {
|
|
|
548
551
|
|
|
549
552
|
// Click to scroll to bottom
|
|
550
553
|
banner.addEventListener("click", () => {
|
|
554
|
+
if (Sessions.isHistoricalWindow()) {
|
|
555
|
+
Sessions.loadLatestHistory();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
551
558
|
messages.scrollTop = messages.scrollHeight;
|
|
552
559
|
_userScrolledUp = false;
|
|
553
560
|
_hideNewMessageBanner();
|
|
@@ -569,7 +576,7 @@ const Sessions = (() => {
|
|
|
569
576
|
// so a content update never moves us "away from bottom" and never trips a
|
|
570
577
|
// false positive here. The 150px threshold in _isAtBottom is extra slack.
|
|
571
578
|
messages.addEventListener("scroll", () => {
|
|
572
|
-
if (_isAtBottom(messages)) {
|
|
579
|
+
if (_isAtBottom(messages) && !Sessions.isHistoricalWindow()) {
|
|
573
580
|
_userScrolledUp = false;
|
|
574
581
|
_hideNewMessageBanner();
|
|
575
582
|
} else {
|
|
@@ -1123,7 +1130,7 @@ const Sessions = (() => {
|
|
|
1123
1130
|
`</span>`;
|
|
1124
1131
|
}
|
|
1125
1132
|
|
|
1126
|
-
function _sendMessage() {
|
|
1133
|
+
async function _sendMessage() {
|
|
1127
1134
|
if (_sending) return;
|
|
1128
1135
|
const input = $("user-input");
|
|
1129
1136
|
const content = Composer.text(input).trim();
|
|
@@ -1147,6 +1154,11 @@ const Sessions = (() => {
|
|
|
1147
1154
|
}
|
|
1148
1155
|
|
|
1149
1156
|
_sending = true;
|
|
1157
|
+
if (Sessions.isHistoricalWindow()) {
|
|
1158
|
+
const id = Sessions.activeId;
|
|
1159
|
+
const loaded = await Sessions.loadLatestHistory();
|
|
1160
|
+
if (!loaded || Sessions.activeId !== id) { _sending = false; return; }
|
|
1161
|
+
}
|
|
1150
1162
|
|
|
1151
1163
|
let bubbleHtml = content ? SkillAC.renderUserMessageHtml(content) : "";
|
|
1152
1164
|
if (mentionChips.length > 0) {
|
|
@@ -1407,6 +1419,8 @@ const Sessions = (() => {
|
|
|
1407
1419
|
const messages = e.currentTarget;
|
|
1408
1420
|
if (messages.scrollTop < 80 && Sessions.activeId && Sessions.hasMoreHistory(Sessions.activeId)) {
|
|
1409
1421
|
Sessions.loadMoreHistory(Sessions.activeId);
|
|
1422
|
+
} else if (_isAtBottom(messages) && Sessions.isHistoricalWindow()) {
|
|
1423
|
+
Sessions.loadNewerHistory();
|
|
1410
1424
|
}
|
|
1411
1425
|
});
|
|
1412
1426
|
|
|
@@ -1804,6 +1818,7 @@ const Sessions = (() => {
|
|
|
1804
1818
|
bubbleHtml += SkillAC.renderUserMessageHtml(ev.content || "", ev.skill_command, ev.skill_command_display);
|
|
1805
1819
|
el.innerHTML = bubbleHtml;
|
|
1806
1820
|
if (ev.created_at) el.dataset.createdAt = ev.created_at;
|
|
1821
|
+
if (ev.round_id) el.dataset.roundId = ev.round_id;
|
|
1807
1822
|
// Messages archived into a compressed chunk can't be edited (the backend
|
|
1808
1823
|
// truncate keys off the active in-memory history). Flag them so the edit
|
|
1809
1824
|
// affordance stays hidden.
|
|
@@ -1987,40 +2002,56 @@ const Sessions = (() => {
|
|
|
1987
2002
|
|
|
1988
2003
|
// Fetch one page of history and insert into #messages or cache.
|
|
1989
2004
|
// before=null means most recent page; prepend=true for scroll-up load.
|
|
1990
|
-
async function _fetchHistory(id, before = null, prepend = false) {
|
|
1991
|
-
const state = _historyState[id] || (_historyState[id] = { hasMore: true,
|
|
1992
|
-
if (state.loading) return;
|
|
2005
|
+
async function _fetchHistory(id, before = null, prepend = false, options = {}) {
|
|
2006
|
+
const state = _historyState[id] || (_historyState[id] = { hasMore: true, loading: false });
|
|
2007
|
+
if (state.loading && !options.replace) return false;
|
|
2008
|
+
state.controller?.abort();
|
|
2009
|
+
const controller = new AbortController();
|
|
2010
|
+
state.controller = controller;
|
|
2011
|
+
const wasHistorical = !!state.hasAfter;
|
|
2012
|
+
const liveRevision = state.liveRevision || 0;
|
|
2013
|
+
if (options.around) state.hasAfter = true;
|
|
2014
|
+
if (options.replace) state.replacing = true;
|
|
1993
2015
|
state.loading = true;
|
|
2016
|
+
let succeeded = false;
|
|
1994
2017
|
|
|
1995
2018
|
try {
|
|
1996
|
-
const params = new URLSearchParams({ limit: 30 });
|
|
1997
|
-
if (before) params.set("
|
|
2019
|
+
const params = new URLSearchParams({ limit: 30, window: 1 });
|
|
2020
|
+
if (before) params.set("before_id", before);
|
|
2021
|
+
if (options.around) params.set("around", options.around);
|
|
2022
|
+
if (options.after) params.set("after_id", options.after);
|
|
1998
2023
|
|
|
1999
|
-
const res = await fetch(`/api/sessions/${id}/messages?${params}
|
|
2024
|
+
const res = await fetch(`/api/sessions/${id}/messages?${params}`, { signal: controller.signal });
|
|
2025
|
+
if (id !== _activeId || _historyState[id] !== state || state.controller !== controller) return false;
|
|
2000
2026
|
if (!res.ok) {
|
|
2027
|
+
if (res.status === 409) window.ChatNavigator?.refresh();
|
|
2001
2028
|
if (id === _activeId) {
|
|
2002
2029
|
let reason = "";
|
|
2003
2030
|
try { const d = await res.json(); reason = d.error || ""; } catch {}
|
|
2004
2031
|
const suffix = reason ? `: ${reason}` : "";
|
|
2005
2032
|
Sessions.appendMsg("info", `${I18n.t("chat.history_load_failed")} (${res.status}${suffix})`);
|
|
2006
2033
|
}
|
|
2007
|
-
return;
|
|
2034
|
+
return false;
|
|
2008
2035
|
}
|
|
2009
2036
|
const data = await res.json();
|
|
2037
|
+
if (id !== _activeId || _historyState[id] !== state || state.controller !== controller) return false;
|
|
2010
2038
|
|
|
2011
|
-
|
|
2039
|
+
if (!options.after) {
|
|
2040
|
+
state.hasMore = !!data.has_more;
|
|
2041
|
+
state.beforeCursor = data.before_cursor;
|
|
2042
|
+
}
|
|
2043
|
+
if (!prepend) {
|
|
2044
|
+
state.hasAfter = !!data.has_after;
|
|
2045
|
+
state.afterCursor = data.after_cursor;
|
|
2046
|
+
}
|
|
2047
|
+
if (options.replace) {
|
|
2048
|
+
window._closeAllPhases?.("incomplete");
|
|
2049
|
+
Sessions.collapseToolGroup();
|
|
2050
|
+
RenderTarget.outer().replaceChildren();
|
|
2051
|
+
delete _renderedCreatedAt[id];
|
|
2052
|
+
}
|
|
2012
2053
|
|
|
2013
2054
|
const events = data.events || [];
|
|
2014
|
-
if (events.length === 0) return;
|
|
2015
|
-
|
|
2016
|
-
// Track oldest created_at for next cursor (scroll-up pagination)
|
|
2017
|
-
events.forEach(ev => {
|
|
2018
|
-
if (ev.type === "history_user_message" && ev.created_at) {
|
|
2019
|
-
if (state.oldestCreatedAt === null || ev.created_at < state.oldestCreatedAt) {
|
|
2020
|
-
state.oldestCreatedAt = ev.created_at;
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
});
|
|
2024
2055
|
|
|
2025
2056
|
// Pre-scan: mark each request_feedback as answered.
|
|
2026
2057
|
// A feedback card is disabled as soon as any subsequent history_user_message appears
|
|
@@ -2028,7 +2059,7 @@ const Sessions = (() => {
|
|
|
2028
2059
|
{
|
|
2029
2060
|
let lastFeedbackIdx = -1;
|
|
2030
2061
|
events.forEach((ev, i) => {
|
|
2031
|
-
if (ev.type === "request_feedback") { ev._answered =
|
|
2062
|
+
if (ev.type === "request_feedback") { ev._answered = !!data.has_after; lastFeedbackIdx = i; }
|
|
2032
2063
|
if (ev.type === "history_user_message" && lastFeedbackIdx >= 0 && i > lastFeedbackIdx) {
|
|
2033
2064
|
events[lastFeedbackIdx]._answered = true;
|
|
2034
2065
|
lastFeedbackIdx = -1;
|
|
@@ -2047,8 +2078,8 @@ const Sessions = (() => {
|
|
|
2047
2078
|
|
|
2048
2079
|
events.forEach(ev => {
|
|
2049
2080
|
if (ev.type === "history_user_message") {
|
|
2050
|
-
currentCreatedAt = ev.created_at;
|
|
2051
|
-
skipRound
|
|
2081
|
+
currentCreatedAt = ev.round_id || ev.created_at;
|
|
2082
|
+
skipRound = currentCreatedAt && (dedup.has(currentCreatedAt) || (ev.editable !== false && dedup.has(ev.created_at)));
|
|
2052
2083
|
if (!skipRound && currentCreatedAt) dedup.add(currentCreatedAt);
|
|
2053
2084
|
}
|
|
2054
2085
|
if (!skipRound) _renderHistoryEvent(ev, frag, historyCtx);
|
|
@@ -2075,10 +2106,16 @@ const Sessions = (() => {
|
|
|
2075
2106
|
} else {
|
|
2076
2107
|
messages.appendChild(frag);
|
|
2077
2108
|
}
|
|
2078
|
-
|
|
2109
|
+
if (options.around) {
|
|
2110
|
+
messages.scrollTop = 0;
|
|
2111
|
+
_userScrolledUp = true;
|
|
2112
|
+
} else if (!options.after) {
|
|
2113
|
+
messages.scrollTop = messages.scrollHeight;
|
|
2114
|
+
_userScrolledUp = false;
|
|
2115
|
+
}
|
|
2079
2116
|
// Flush any tool_stdout lines that arrived via WS before this history
|
|
2080
2117
|
// fetch completed (race condition on session switch).
|
|
2081
|
-
if (!prepend) _flushPendingStdout();
|
|
2118
|
+
if (!prepend && !state.hasAfter) _flushPendingStdout();
|
|
2082
2119
|
}
|
|
2083
2120
|
|
|
2084
2121
|
// If no more history remains, insert a "beginning of conversation" marker at the top.
|
|
@@ -2094,7 +2131,7 @@ const Sessions = (() => {
|
|
|
2094
2131
|
|
|
2095
2132
|
// Restore transient UI state based on session status after initial load
|
|
2096
2133
|
// (not prepend, which is scroll-up pagination — no need to re-restore then)
|
|
2097
|
-
if (!prepend) {
|
|
2134
|
+
if (!prepend && !state.hasAfter) {
|
|
2098
2135
|
const session = _sessions.find(s => s.id === id);
|
|
2099
2136
|
if (session) {
|
|
2100
2137
|
if (session.status === "running") {
|
|
@@ -2116,13 +2153,35 @@ const Sessions = (() => {
|
|
|
2116
2153
|
}
|
|
2117
2154
|
}
|
|
2118
2155
|
}
|
|
2156
|
+
if (state.hasAfter) _showNewMessageBanner();
|
|
2157
|
+
else _hideNewMessageBanner();
|
|
2158
|
+
window.ChatNavigator?.syncMessages();
|
|
2159
|
+
}
|
|
2160
|
+
succeeded = true;
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
if (error.name !== "AbortError" && id === _activeId && state.controller === controller) {
|
|
2163
|
+
Sessions.appendMsg("info", escapeHtml(I18n.t("chat.history_load_failed")));
|
|
2119
2164
|
}
|
|
2165
|
+
return false;
|
|
2120
2166
|
} finally {
|
|
2121
|
-
state.
|
|
2167
|
+
if (state.controller === controller) {
|
|
2168
|
+
state.loading = false;
|
|
2169
|
+
state.replacing = false;
|
|
2170
|
+
if (!succeeded) state.hasAfter = wasHistorical;
|
|
2171
|
+
}
|
|
2122
2172
|
// After loading finishes, re-evaluate the empty-state hint in case
|
|
2123
2173
|
// the session is genuinely empty (no events + no existing DOM content).
|
|
2124
|
-
if (id === _activeId)
|
|
2174
|
+
if (id === _activeId) {
|
|
2175
|
+
_updateEmptyHint();
|
|
2176
|
+
_refreshEditButtons(RenderTarget.outer());
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
// A live message may have arrived after the server took its snapshot while
|
|
2180
|
+
// this detached window was loading. Reconcile before resuming live output.
|
|
2181
|
+
if (succeeded && !state.hasAfter && (state.liveRevision || 0) !== liveRevision && id === _activeId) {
|
|
2182
|
+
return _fetchHistory(id, null, false, { replace: true });
|
|
2125
2183
|
}
|
|
2184
|
+
return succeeded;
|
|
2126
2185
|
}
|
|
2127
2186
|
|
|
2128
2187
|
// ── Private helpers ───────────────────────────────────────────────────
|
|
@@ -2238,7 +2297,7 @@ const Sessions = (() => {
|
|
|
2238
2297
|
}
|
|
2239
2298
|
|
|
2240
2299
|
function _refreshEditButtons(container, status) {
|
|
2241
|
-
const running = (status || Sessions.find(Sessions.activeId)?.status) === "running";
|
|
2300
|
+
const running = (status || Sessions.find(Sessions.activeId)?.status) === "running" || Sessions.isHistoricalWindow();
|
|
2242
2301
|
const btns = Array.from(container.querySelectorAll(".msg-edit-btn"));
|
|
2243
2302
|
btns.forEach((btn, i) => {
|
|
2244
2303
|
btn.style.display = !running && i === btns.length - 1 ? "" : "none";
|
|
@@ -2256,6 +2315,7 @@ const Sessions = (() => {
|
|
|
2256
2315
|
}
|
|
2257
2316
|
|
|
2258
2317
|
function _enterEditMode(el) {
|
|
2318
|
+
if (Sessions.isHistoricalWindow()) return;
|
|
2259
2319
|
if (_activeSessionIsRunning() || el.classList.contains("editing")) return;
|
|
2260
2320
|
el.classList.add("editing");
|
|
2261
2321
|
|
|
@@ -2330,6 +2390,7 @@ const Sessions = (() => {
|
|
|
2330
2390
|
}
|
|
2331
2391
|
|
|
2332
2392
|
function _submitEdit(el, newContent) {
|
|
2393
|
+
if (Sessions.isHistoricalWindow()) return;
|
|
2333
2394
|
if (!newContent) return;
|
|
2334
2395
|
if (!Sessions.activeId) return;
|
|
2335
2396
|
if (_activeSessionIsRunning()) {
|
|
@@ -2785,6 +2846,40 @@ const Sessions = (() => {
|
|
|
2785
2846
|
|
|
2786
2847
|
|
|
2787
2848
|
|
|
2849
|
+
// Compact a working directory for the status bar: keep a short leading
|
|
2850
|
+
// anchor (up to two segments, e.g. "/Users/leo") plus as many trailing
|
|
2851
|
+
// segments as fit, ellipsising the middle so the current directory name
|
|
2852
|
+
// stays visible. The full path lives in title/dataset, not here.
|
|
2853
|
+
function _compactWorkingDir(p, maxLen = 40) {
|
|
2854
|
+
if (!p) return p;
|
|
2855
|
+
if (p.length <= maxLen) return p;
|
|
2856
|
+
|
|
2857
|
+
const abs = /^[\\/]/.test(p);
|
|
2858
|
+
const segs = p.split(/[\\/]+/).filter((s) => s !== "");
|
|
2859
|
+
if (!segs.length) return p;
|
|
2860
|
+
|
|
2861
|
+
// The final segment (current directory name) must always stay intact;
|
|
2862
|
+
// shrink the leading anchor first when a longer tail won't fit.
|
|
2863
|
+
for (let headCount = Math.min(2, segs.length - 1); headCount >= 0; headCount--) {
|
|
2864
|
+
const headStr = headCount > 0
|
|
2865
|
+
? (abs ? "/" : "") + segs.slice(0, headCount).join("/")
|
|
2866
|
+
: "";
|
|
2867
|
+
let budget = maxLen - headStr.length - 3; // reserve "/…/"
|
|
2868
|
+
const tail = [];
|
|
2869
|
+
for (let i = segs.length - 1; i >= headCount; i--) {
|
|
2870
|
+
const need = segs[i].length + (tail.length ? 1 : 0);
|
|
2871
|
+
if (need > budget) break;
|
|
2872
|
+
tail.unshift(segs[i]);
|
|
2873
|
+
budget -= need;
|
|
2874
|
+
}
|
|
2875
|
+
if (tail.length) {
|
|
2876
|
+
return (headStr ? headStr + "/…/" : "…/") + tail.join("/");
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
// Final segment alone exceeds maxLen — cut it (practically never).
|
|
2880
|
+
return "…/" + segs[segs.length - 1].slice(-Math.max(1, maxLen - 2));
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2788
2883
|
// ── Public API ─────────────────────────────────────────────────────────
|
|
2789
2884
|
return {
|
|
2790
2885
|
get all() { return _sessions; },
|
|
@@ -2850,6 +2945,10 @@ const Sessions = (() => {
|
|
|
2850
2945
|
_initComposer();
|
|
2851
2946
|
_initSearch();
|
|
2852
2947
|
_initMessageHistory();
|
|
2948
|
+
// Quote-on-selection: floating quote button over message selections.
|
|
2949
|
+
// quote-select.js is loaded after sessions.js; it's available at init
|
|
2950
|
+
// time because app.js (which calls Sessions.init()) loads last.
|
|
2951
|
+
if (typeof QuoteSelect !== "undefined") QuoteSelect.init();
|
|
2853
2952
|
_ensureAgentsLoaded().then(() => Sessions.renderList());
|
|
2854
2953
|
// Re-render session list (badges/labels) when the user switches language
|
|
2855
2954
|
document.addEventListener("langchange", () => Sessions.renderList());
|
|
@@ -2866,6 +2965,21 @@ const Sessions = (() => {
|
|
|
2866
2965
|
// Local deployments (localhost / 127.0.0.1 / ::1): open the file with the
|
|
2867
2966
|
// OS default handler. Remote deployments: download the file.
|
|
2868
2967
|
document.addEventListener("click", async (e) => {
|
|
2968
|
+
// Localhost dev-server URLs open in the preview panel instead of a new
|
|
2969
|
+
// tab (local deployments only — the proxy can't reach a remote user's
|
|
2970
|
+
// localhost).
|
|
2971
|
+
const localLink = e.target.closest("a[href]");
|
|
2972
|
+
if (localLink) {
|
|
2973
|
+
const href = localLink.getAttribute("href") || "";
|
|
2974
|
+
if (/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?([/?#]|$)/i.test(href) &&
|
|
2975
|
+
["localhost", "127.0.0.1", "::1"].includes(window.location.hostname) &&
|
|
2976
|
+
window.Clacky && Clacky.Preview) {
|
|
2977
|
+
e.preventDefault();
|
|
2978
|
+
Clacky.Preview.open(href);
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2869
2983
|
const link = e.target.closest("a[href^='file://']");
|
|
2870
2984
|
if (!link) return;
|
|
2871
2985
|
e.preventDefault();
|
|
@@ -2878,6 +2992,12 @@ const Sessions = (() => {
|
|
|
2878
2992
|
|
|
2879
2993
|
const hostname = window.location.hostname;
|
|
2880
2994
|
const isLocal = ["localhost", "127.0.0.1", "::1"].includes(hostname);
|
|
2995
|
+
|
|
2996
|
+
// Local deployments: open the file in the Files tab of the aside
|
|
2997
|
+
// panel (inline preview; binary formats show an "open with app"
|
|
2998
|
+
// fallback page). Fall back to the OS default handler when the
|
|
2999
|
+
// viewer is unavailable. Remote deployments: download the file.
|
|
3000
|
+
if (isLocal && window.Clacky && Clacky.WorkspaceView && Clacky.WorkspaceView.openFile(filePath)) return;
|
|
2881
3001
|
const action = isLocal ? "open" : "download";
|
|
2882
3002
|
|
|
2883
3003
|
try {
|
|
@@ -3987,10 +4107,10 @@ const Sessions = (() => {
|
|
|
3987
4107
|
}
|
|
3988
4108
|
}
|
|
3989
4109
|
|
|
3990
|
-
// Working dir — show full path
|
|
4110
|
+
// Working dir — show a compacted path (middle-ellipsised), full path in title.
|
|
3991
4111
|
const sibDir = $("sib-dir");
|
|
3992
4112
|
if (sibDir && s.working_dir) {
|
|
3993
|
-
sibDir.textContent = s.working_dir;
|
|
4113
|
+
sibDir.textContent = _compactWorkingDir(s.working_dir);
|
|
3994
4114
|
sibDir.title = `${s.working_dir} (${I18n.t("sib.dir.tooltip")})`;
|
|
3995
4115
|
sibDir.dataset.workingDir = s.working_dir;
|
|
3996
4116
|
sibDir.dataset.sessionId = s.id;
|
|
@@ -4896,6 +5016,7 @@ const Sessions = (() => {
|
|
|
4896
5016
|
|
|
4897
5017
|
/** Load the most recent page of history for a session (called on first visit). */
|
|
4898
5018
|
loadHistory(id) {
|
|
5019
|
+
window.ChatNavigator?.setSession(id);
|
|
4899
5020
|
return _fetchHistory(id, null, false);
|
|
4900
5021
|
},
|
|
4901
5022
|
|
|
@@ -4903,7 +5024,41 @@ const Sessions = (() => {
|
|
|
4903
5024
|
loadMoreHistory(id) {
|
|
4904
5025
|
const state = _historyState[id];
|
|
4905
5026
|
if (!state || !state.hasMore) return;
|
|
4906
|
-
return _fetchHistory(id, state.
|
|
5027
|
+
return _fetchHistory(id, state.beforeCursor, true);
|
|
5028
|
+
},
|
|
5029
|
+
|
|
5030
|
+
isHistoricalWindow() {
|
|
5031
|
+
const state = _historyState[_activeId];
|
|
5032
|
+
return !!(state?.hasAfter || state?.replacing);
|
|
5033
|
+
},
|
|
5034
|
+
|
|
5035
|
+
loadNewerHistory() {
|
|
5036
|
+
const state = _historyState[_activeId];
|
|
5037
|
+
if (!state?.hasAfter || !state.afterCursor) return;
|
|
5038
|
+
return _fetchHistory(_activeId, null, false, { after: state.afterCursor });
|
|
5039
|
+
},
|
|
5040
|
+
|
|
5041
|
+
jumpToHistory(roundId) {
|
|
5042
|
+
return _fetchHistory(_activeId, null, false, { around: roundId, replace: true });
|
|
5043
|
+
},
|
|
5044
|
+
|
|
5045
|
+
async loadLatestHistory() {
|
|
5046
|
+
const id = _activeId;
|
|
5047
|
+
const loaded = await _fetchHistory(id, null, false, { replace: true });
|
|
5048
|
+
if (loaded && id === _activeId && Sessions.find(id)?.status === "running") {
|
|
5049
|
+
WS.send({ type: "subscribe", session_id: id });
|
|
5050
|
+
}
|
|
5051
|
+
return loaded;
|
|
5052
|
+
},
|
|
5053
|
+
|
|
5054
|
+
deferLiveHistory(event) {
|
|
5055
|
+
if (!Sessions.isHistoricalWindow()) return false;
|
|
5056
|
+
if (["history_user_message", "assistant_message", "tool_call", "tool_result"].includes(event.type)) {
|
|
5057
|
+
const state = _historyState[_activeId];
|
|
5058
|
+
state.liveRevision = (state.liveRevision || 0) + 1;
|
|
5059
|
+
}
|
|
5060
|
+
_showNewMessageBanner();
|
|
5061
|
+
return true;
|
|
4907
5062
|
},
|
|
4908
5063
|
|
|
4909
5064
|
/** Check if there is more history to load for a session. */
|
data/lib/clacky/web/utils.js
CHANGED
|
@@ -56,6 +56,40 @@ const IME = (() => {
|
|
|
56
56
|
return { track, bindEnter };
|
|
57
57
|
})();
|
|
58
58
|
|
|
59
|
+
// Compact, non-interactive Markdown for short previews. Only inline formatting
|
|
60
|
+
// is emitted; raw HTML and images cannot introduce active content.
|
|
61
|
+
const MarkdownPreview = (() => {
|
|
62
|
+
function render(text) {
|
|
63
|
+
if (!text) return "";
|
|
64
|
+
if (typeof marked === "undefined") return escapeHtml(text);
|
|
65
|
+
|
|
66
|
+
const renderer = new marked.Renderer();
|
|
67
|
+
renderer.html = renderer.image = renderer.checkbox = () => "";
|
|
68
|
+
renderer.hr = renderer.br = () => " ";
|
|
69
|
+
renderer.link = function({ tokens }) { return this.parser.parseInline(tokens); };
|
|
70
|
+
renderer.paragraph = renderer.heading = function({ tokens }) {
|
|
71
|
+
return this.parser.parseInline(tokens) + " ";
|
|
72
|
+
};
|
|
73
|
+
renderer.blockquote = renderer.listitem = function({ tokens }) {
|
|
74
|
+
return this.parser.parse(tokens).trim() + " ";
|
|
75
|
+
};
|
|
76
|
+
renderer.list = function({ items }) {
|
|
77
|
+
return items.map(item => this.listitem(item)).join("");
|
|
78
|
+
};
|
|
79
|
+
renderer.table = function({ header, rows }) {
|
|
80
|
+
return [header, ...rows].map(row => row.map(cell => this.parser.parseInline(cell.tokens)).join(" ")).join(" ") + " ";
|
|
81
|
+
};
|
|
82
|
+
renderer.code = function({ text }) { return `<code>${escapeHtml(text)}</code> `; };
|
|
83
|
+
try {
|
|
84
|
+
return marked.parse(text, { gfm: true, breaks: false, renderer }).trim();
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return escapeHtml(text);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { render };
|
|
91
|
+
})();
|
|
92
|
+
|
|
59
93
|
const Tooltip = (() => {
|
|
60
94
|
const GAP = 8;
|
|
61
95
|
let el = null;
|