claudeck 1.1.1 → 1.2.0
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.
- package/README.md +30 -4
- package/config/skillsmp-config.json +5 -0
- package/db.js +248 -0
- package/package.json +11 -2
- package/public/css/panels/git-panel.css +220 -0
- package/public/css/panels/skills-manager.css +975 -0
- package/public/css/ui/input-history.css +109 -0
- package/public/css/ui/messages.css +51 -0
- package/public/css/ui/notification-bell.css +421 -0
- package/public/css/ui/sessions.css +41 -0
- package/public/css/ui/worktree.css +442 -0
- package/public/index.html +43 -10
- package/public/js/core/api.js +83 -0
- package/public/js/core/dom.js +15 -0
- package/public/js/features/background-sessions.js +11 -0
- package/public/js/features/chat.js +501 -3
- package/public/js/features/input-history.js +122 -0
- package/public/js/features/projects.js +16 -1
- package/public/js/features/sessions.js +77 -30
- package/public/js/main.js +3 -0
- package/public/js/panels/git-panel.js +385 -6
- package/public/js/panels/skills-manager.js +1005 -0
- package/public/js/ui/messages.js +58 -0
- package/public/js/ui/notification-bell.js +240 -0
- package/public/js/ui/notification-history.js +210 -0
- package/public/js/ui/parallel.js +11 -0
- package/public/js/ui/tab-sdk.js +1 -1
- package/public/style.css +4 -0
- package/server/agent-loop.js +13 -0
- package/server/notification-logger.js +27 -0
- package/server/routes/notifications.js +57 -1
- package/server/routes/sessions.js +41 -0
- package/server/routes/skills.js +454 -0
- package/server/routes/worktrees.js +93 -0
- package/server/utils/git-worktree.js +297 -0
- package/server/ws-handler.js +708 -629
- package/server.js +17 -1
|
@@ -4,8 +4,8 @@ import { getState, setState } from '../core/store.js';
|
|
|
4
4
|
import { CHAT_IDS, BOT_CHAT_ID } from '../core/constants.js';
|
|
5
5
|
import { on } from '../core/events.js';
|
|
6
6
|
import { commandRegistry, dismissAutocomplete, handleAutocompleteKeydown, handleSlashAutocomplete, registerCommand } from '../ui/commands.js';
|
|
7
|
-
import { addUserMessage, appendAssistantText, appendToolIndicator, appendToolResult, showThinking, removeThinking, addResultSummary, addStatus, showWhalyPlaceholder } from '../ui/messages.js';
|
|
8
|
-
import { getPane, panes, _setChatFns } from '../ui/parallel.js';
|
|
7
|
+
import { addUserMessage, appendAssistantText, appendToolIndicator, appendToolResult, showThinking, removeThinking, addResultSummary, addStatus, showWhalyPlaceholder, addSkillUsedMessage } from '../ui/messages.js';
|
|
8
|
+
import { getPane, panes, _setChatFns, _setInputHistoryGetter } from '../ui/parallel.js';
|
|
9
9
|
import { loadSessions } from './sessions.js';
|
|
10
10
|
import { loadStats, loadAccountInfo } from './cost-dashboard.js';
|
|
11
11
|
import { loadProjects } from './projects.js';
|
|
@@ -24,10 +24,41 @@ import { getSelectedModel } from '../ui/model-selector.js';
|
|
|
24
24
|
import { getMaxTurns } from '../ui/max-turns.js';
|
|
25
25
|
import { getDisabledTools } from '../ui/disabled-tools.js';
|
|
26
26
|
import { updateContextGauge, resetContextGauge, loadContextGauge } from '../ui/context-gauge.js';
|
|
27
|
+
import { InputHistory, handleHistoryKeydown } from './input-history.js';
|
|
27
28
|
|
|
28
29
|
// ── "Waiting for input" indicator ──
|
|
29
30
|
const inputWaitingEl = document.getElementById("input-waiting");
|
|
30
31
|
|
|
32
|
+
// ── Input history (message recall) ──
|
|
33
|
+
function historyKey() {
|
|
34
|
+
return "claudeck-input-history-" + ($.projectSelect?.value || "default");
|
|
35
|
+
}
|
|
36
|
+
let inputHistory = new InputHistory(historyKey());
|
|
37
|
+
|
|
38
|
+
$.projectSelect?.addEventListener("change", () => {
|
|
39
|
+
inputHistory = new InputHistory(historyKey());
|
|
40
|
+
// Defer visibility update — updateHistoryButtonVisibility is defined later in this file
|
|
41
|
+
queueMicrotask(() => updateHistoryButtonVisibility());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export function getInputHistory() {
|
|
45
|
+
// Re-sync key if it drifted (e.g. project loaded after module init)
|
|
46
|
+
const expected = historyKey();
|
|
47
|
+
if (inputHistory.storageKey !== expected) {
|
|
48
|
+
inputHistory = new InputHistory(expected);
|
|
49
|
+
}
|
|
50
|
+
return inputHistory;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Worktree mode toggle ──
|
|
54
|
+
let worktreeMode = false;
|
|
55
|
+
if ($.worktreeBtn) {
|
|
56
|
+
$.worktreeBtn.addEventListener("click", () => {
|
|
57
|
+
worktreeMode = !worktreeMode;
|
|
58
|
+
$.worktreeBtn.classList.toggle("active", worktreeMode);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
function isQuestionText(text) {
|
|
32
63
|
if (!text) return false;
|
|
33
64
|
// Get the last meaningful line (skip empty lines, code blocks, lists)
|
|
@@ -63,6 +94,9 @@ function hideWaitingForInput(pane) {
|
|
|
63
94
|
|
|
64
95
|
export function sendMessage(pane) {
|
|
65
96
|
pane = pane || getPane(null);
|
|
97
|
+
// Re-sync history key in case project loaded after module init
|
|
98
|
+
const ek = historyKey();
|
|
99
|
+
if (inputHistory.storageKey !== ek) inputHistory = new InputHistory(ek);
|
|
66
100
|
const text = pane.messageInput.value.trim();
|
|
67
101
|
const cwd = $.projectSelect.value;
|
|
68
102
|
|
|
@@ -76,6 +110,9 @@ export function sendMessage(pane) {
|
|
|
76
110
|
if (cmdName === "run" && !cwd) {
|
|
77
111
|
// /run needs a project, fall through
|
|
78
112
|
} else {
|
|
113
|
+
inputHistory.add(text);
|
|
114
|
+
inputHistory.reset();
|
|
115
|
+
updateHistoryButtonVisibility();
|
|
79
116
|
pane.messageInput.value = "";
|
|
80
117
|
pane.messageInput.style.height = "auto";
|
|
81
118
|
dismissAutocomplete(pane);
|
|
@@ -107,6 +144,8 @@ export function sendMessage(pane) {
|
|
|
107
144
|
const [, cmdName, args] = match;
|
|
108
145
|
const cmd = commandRegistry[cmdName];
|
|
109
146
|
if (cmd) {
|
|
147
|
+
inputHistory.add(text);
|
|
148
|
+
inputHistory.reset();
|
|
110
149
|
pane.messageInput.value = "";
|
|
111
150
|
pane.messageInput.style.height = "auto";
|
|
112
151
|
dismissAutocomplete(pane);
|
|
@@ -129,6 +168,9 @@ export function sendMessage(pane) {
|
|
|
129
168
|
const images = getImageAttachments();
|
|
130
169
|
const filePaths = attachedFiles.map(f => f.path);
|
|
131
170
|
addUserMessage(text, pane, images, filePaths);
|
|
171
|
+
inputHistory.add(text);
|
|
172
|
+
inputHistory.reset();
|
|
173
|
+
updateHistoryButtonVisibility();
|
|
132
174
|
pane.messageInput.value = "";
|
|
133
175
|
pane.messageInput.style.height = "auto";
|
|
134
176
|
setState("streamingCharCount", 0);
|
|
@@ -179,6 +221,14 @@ export function sendMessage(pane) {
|
|
|
179
221
|
payload.chatId = pane.chatId;
|
|
180
222
|
}
|
|
181
223
|
|
|
224
|
+
// Worktree confirmation: show approve/reject before sending
|
|
225
|
+
if (worktreeMode) {
|
|
226
|
+
worktreeMode = false;
|
|
227
|
+
$.worktreeBtn?.classList.remove("active");
|
|
228
|
+
showWorktreeConfirmation(ws, payload, pane);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
182
232
|
ws.send(JSON.stringify(payload));
|
|
183
233
|
showThinking("Connecting to Claude...", pane);
|
|
184
234
|
}
|
|
@@ -220,10 +270,17 @@ export function finishStreamingHandler(pane) {
|
|
|
220
270
|
$.sendBtn.disabled = false;
|
|
221
271
|
$.messageInput.focus();
|
|
222
272
|
}
|
|
273
|
+
|
|
274
|
+
// Re-render messages from DB so fork buttons appear on the completed turn
|
|
275
|
+
const sid = getState("sessionId");
|
|
276
|
+
if (sid) {
|
|
277
|
+
import('./sessions.js').then(({ loadMessages }) => loadMessages(sid));
|
|
278
|
+
}
|
|
223
279
|
}
|
|
224
280
|
|
|
225
281
|
// Register the chat functions with parallel.js to break circular dependency
|
|
226
282
|
_setChatFns({ sendMessage, stopGeneration });
|
|
283
|
+
_setInputHistoryGetter(() => inputHistory);
|
|
227
284
|
|
|
228
285
|
// Render a collapsible memory indicator in the chat
|
|
229
286
|
function appendMemoryIndicator(memories, pane) {
|
|
@@ -347,6 +404,13 @@ function handleServerMessage(msg) {
|
|
|
347
404
|
break;
|
|
348
405
|
|
|
349
406
|
case "tool":
|
|
407
|
+
// Detect model-invoked skill usage
|
|
408
|
+
if (msg.name === "Skill" && msg.input?.skill) {
|
|
409
|
+
import('./projects.js').then(({ skillLookup }) => {
|
|
410
|
+
const info = skillLookup.get(msg.input.skill);
|
|
411
|
+
addSkillUsedMessage(msg.input.skill, info?.description || "", pane);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
350
414
|
appendToolIndicator(msg.name, msg.input, pane, msg.id);
|
|
351
415
|
showThinking(`Running ${msg.name}...`, pane);
|
|
352
416
|
break;
|
|
@@ -456,9 +520,293 @@ function handleServerMessage(msg) {
|
|
|
456
520
|
case "memory_saved":
|
|
457
521
|
// /remember command response — already handled by "text" message
|
|
458
522
|
break;
|
|
523
|
+
|
|
524
|
+
case "worktree_created":
|
|
525
|
+
showWorktreeBanner(msg.branchName, msg.baseBranch, pane);
|
|
526
|
+
showThinking(`Working in worktree: ${msg.branchName}...`, pane);
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case "worktree_completed":
|
|
530
|
+
removeThinking(pane);
|
|
531
|
+
showWorktreeActions(msg.worktreeId, msg.branchName, msg.stats, pane);
|
|
532
|
+
break;
|
|
533
|
+
|
|
534
|
+
case "worktree_error":
|
|
535
|
+
addStatus(`Worktree failed: ${msg.error} — running on current branch instead`, true, pane);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── Worktree UI functions ──────────────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
const BRANCH_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`;
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Show a persistent banner indicating the agent is working in a worktree.
|
|
546
|
+
* This is clearly visible so the user knows they're NOT on the main branch.
|
|
547
|
+
*/
|
|
548
|
+
function showWorktreeBanner(branchName, baseBranch, pane) {
|
|
549
|
+
const container = pane.messagesDiv || $.messagesDiv;
|
|
550
|
+
const banner = document.createElement("div");
|
|
551
|
+
banner.className = "worktree-banner";
|
|
552
|
+
banner.innerHTML = `
|
|
553
|
+
<div class="worktree-banner-content">
|
|
554
|
+
${BRANCH_SVG}
|
|
555
|
+
<span class="worktree-banner-label">Worktree active</span>
|
|
556
|
+
<code class="worktree-banner-branch">${escapeHtml(branchName)}</code>
|
|
557
|
+
<span class="worktree-banner-base">branched from <strong>${escapeHtml(baseBranch)}</strong></span>
|
|
558
|
+
</div>
|
|
559
|
+
`;
|
|
560
|
+
container.appendChild(banner);
|
|
561
|
+
banner.scrollIntoView({ behavior: "smooth" });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Show inline confirmation card: "Run in Worktree" or "Use Current Branch".
|
|
566
|
+
* The user's message is already displayed — this card appears below it.
|
|
567
|
+
*/
|
|
568
|
+
function showWorktreeConfirmation(ws, payload, pane) {
|
|
569
|
+
const container = pane.messagesDiv || $.messagesDiv;
|
|
570
|
+
const card = document.createElement("div");
|
|
571
|
+
card.className = "worktree-confirm-card";
|
|
572
|
+
card.innerHTML = `
|
|
573
|
+
<div class="wt-confirm-header">
|
|
574
|
+
${BRANCH_SVG}
|
|
575
|
+
<span>Run this task in an isolated worktree?</span>
|
|
576
|
+
</div>
|
|
577
|
+
<div class="wt-confirm-desc">
|
|
578
|
+
Your working branch stays untouched. You can merge or discard the result after completion.
|
|
579
|
+
</div>
|
|
580
|
+
<div class="wt-confirm-btns">
|
|
581
|
+
<button class="wt-btn-worktree">Run in Worktree</button>
|
|
582
|
+
<button class="wt-btn-current">Use Current Branch</button>
|
|
583
|
+
</div>
|
|
584
|
+
`;
|
|
585
|
+
|
|
586
|
+
function send(useWorktree) {
|
|
587
|
+
if (useWorktree) payload.worktree = true;
|
|
588
|
+
console.log("[worktree] Sending payload with worktree:", payload.worktree, "cwd:", payload.cwd);
|
|
589
|
+
card.remove();
|
|
590
|
+
ws.send(JSON.stringify(payload));
|
|
591
|
+
showThinking(useWorktree ? "Creating worktree..." : "Connecting to Claude...", pane);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
card.querySelector(".wt-btn-worktree").addEventListener("click", () => send(true));
|
|
595
|
+
card.querySelector(".wt-btn-current").addEventListener("click", () => send(false));
|
|
596
|
+
|
|
597
|
+
container.appendChild(card);
|
|
598
|
+
card.scrollIntoView({ behavior: "smooth" });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Show inline action card after worktree completes: View Diff / Merge / Discard.
|
|
603
|
+
*/
|
|
604
|
+
function showWorktreeActions(worktreeId, branchName, stats, pane) {
|
|
605
|
+
const container = pane.messagesDiv || $.messagesDiv;
|
|
606
|
+
const card = document.createElement("div");
|
|
607
|
+
card.className = "worktree-action-card";
|
|
608
|
+
const statsText = stats ? `+${stats.insertions} -${stats.deletions} in ${stats.files} file(s)` : "";
|
|
609
|
+
card.innerHTML = `
|
|
610
|
+
<div class="worktree-action-header">
|
|
611
|
+
${BRANCH_SVG}
|
|
612
|
+
<span>Branch: <strong>${escapeHtml(branchName)}</strong></span>
|
|
613
|
+
<span class="worktree-stats">${statsText}</span>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="worktree-action-btns">
|
|
616
|
+
<button class="wt-diff-btn">View Diff</button>
|
|
617
|
+
<button class="wt-merge-btn">Squash Merge</button>
|
|
618
|
+
<button class="wt-discard-btn">Discard</button>
|
|
619
|
+
</div>
|
|
620
|
+
`;
|
|
621
|
+
|
|
622
|
+
const diffBtn = card.querySelector(".wt-diff-btn");
|
|
623
|
+
const mergeBtn = card.querySelector(".wt-merge-btn");
|
|
624
|
+
const discardBtn = card.querySelector(".wt-discard-btn");
|
|
625
|
+
|
|
626
|
+
diffBtn.addEventListener("click", async () => {
|
|
627
|
+
diffBtn.disabled = true;
|
|
628
|
+
diffBtn.textContent = "Loading...";
|
|
629
|
+
try {
|
|
630
|
+
const res = await fetch(`/api/worktrees/${encodeURIComponent(worktreeId)}/diff`);
|
|
631
|
+
const data = await res.json();
|
|
632
|
+
if (data.error) throw new Error(data.error);
|
|
633
|
+
showWorktreeDiffModal(data.diff, branchName);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
addStatus("Diff error: " + err.message, true, pane);
|
|
636
|
+
} finally {
|
|
637
|
+
diffBtn.disabled = false;
|
|
638
|
+
diffBtn.textContent = "View Diff";
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
mergeBtn.addEventListener("click", async () => {
|
|
643
|
+
mergeBtn.disabled = true;
|
|
644
|
+
mergeBtn.textContent = "Merging...";
|
|
645
|
+
discardBtn.disabled = true;
|
|
646
|
+
try {
|
|
647
|
+
const res = await fetch(`/api/worktrees/${encodeURIComponent(worktreeId)}/merge`, {
|
|
648
|
+
method: "POST",
|
|
649
|
+
headers: { "Content-Type": "application/json" },
|
|
650
|
+
body: JSON.stringify({}),
|
|
651
|
+
});
|
|
652
|
+
const data = await res.json();
|
|
653
|
+
if (data.error) throw new Error(data.error);
|
|
654
|
+
card.innerHTML = `<div class="worktree-action-header">${BRANCH_SVG}<span>Merged <strong>${escapeHtml(branchName)}</strong> into current branch</span></div>`;
|
|
655
|
+
card.classList.add("worktree-merged");
|
|
656
|
+
} catch (err) {
|
|
657
|
+
addStatus("Merge error: " + err.message, true, pane);
|
|
658
|
+
mergeBtn.disabled = false;
|
|
659
|
+
mergeBtn.textContent = "Squash Merge";
|
|
660
|
+
discardBtn.disabled = false;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
discardBtn.addEventListener("click", () => {
|
|
665
|
+
// Replace buttons with confirmation
|
|
666
|
+
const btnsDiv = card.querySelector(".worktree-action-btns");
|
|
667
|
+
btnsDiv.innerHTML = `
|
|
668
|
+
<span class="wt-confirm-label">Discard this worktree? This cannot be undone.</span>
|
|
669
|
+
<button class="wt-confirm-yes">Yes, Discard</button>
|
|
670
|
+
<button class="wt-confirm-no">Cancel</button>
|
|
671
|
+
`;
|
|
672
|
+
|
|
673
|
+
btnsDiv.querySelector(".wt-confirm-no").addEventListener("click", () => {
|
|
674
|
+
btnsDiv.innerHTML = "";
|
|
675
|
+
btnsDiv.appendChild(diffBtn);
|
|
676
|
+
btnsDiv.appendChild(mergeBtn);
|
|
677
|
+
btnsDiv.appendChild(discardBtn);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
btnsDiv.querySelector(".wt-confirm-yes").addEventListener("click", async () => {
|
|
681
|
+
const yesBtn = btnsDiv.querySelector(".wt-confirm-yes");
|
|
682
|
+
yesBtn.disabled = true;
|
|
683
|
+
yesBtn.textContent = "Discarding...";
|
|
684
|
+
btnsDiv.querySelector(".wt-confirm-no").disabled = true;
|
|
685
|
+
try {
|
|
686
|
+
const res = await fetch(`/api/worktrees/${encodeURIComponent(worktreeId)}`, { method: "DELETE" });
|
|
687
|
+
const data = await res.json();
|
|
688
|
+
if (data.error) throw new Error(data.error);
|
|
689
|
+
card.innerHTML = `<div class="worktree-action-header">${BRANCH_SVG}<span>Discarded <strong>${escapeHtml(branchName)}</strong></span></div>`;
|
|
690
|
+
card.classList.add("worktree-discarded");
|
|
691
|
+
} catch (err) {
|
|
692
|
+
addStatus("Discard error: " + err.message, true, pane);
|
|
693
|
+
btnsDiv.innerHTML = "";
|
|
694
|
+
btnsDiv.appendChild(diffBtn);
|
|
695
|
+
btnsDiv.appendChild(mergeBtn);
|
|
696
|
+
btnsDiv.appendChild(discardBtn);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
container.appendChild(card);
|
|
702
|
+
card.scrollIntoView({ behavior: "smooth" });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Show a modal with the raw unified diff.
|
|
707
|
+
*/
|
|
708
|
+
function showWorktreeDiffModal(diffText, branchName) {
|
|
709
|
+
const overlay = document.createElement("div");
|
|
710
|
+
overlay.className = "modal-overlay";
|
|
711
|
+
overlay.innerHTML = `
|
|
712
|
+
<div class="modal git-diff-modal">
|
|
713
|
+
<div class="modal-header">
|
|
714
|
+
<h3>Diff: ${escapeHtml(branchName)}</h3>
|
|
715
|
+
<button class="modal-close">×</button>
|
|
716
|
+
</div>
|
|
717
|
+
<div class="git-diff-body"></div>
|
|
718
|
+
</div>
|
|
719
|
+
`;
|
|
720
|
+
|
|
721
|
+
const body = overlay.querySelector(".git-diff-body");
|
|
722
|
+
|
|
723
|
+
if (!diffText || !diffText.trim()) {
|
|
724
|
+
body.innerHTML = '<div class="git-diff-empty">(no changes)</div>';
|
|
725
|
+
} else {
|
|
726
|
+
// Parse into per-file sections
|
|
727
|
+
const sections = [];
|
|
728
|
+
let current = null;
|
|
729
|
+
for (const line of diffText.split("\n")) {
|
|
730
|
+
if (line.startsWith("diff --git ")) {
|
|
731
|
+
if (current) sections.push(current);
|
|
732
|
+
const match = line.match(/diff --git a\/(.+?) b\/(.+)/);
|
|
733
|
+
current = { fileName: match ? match[2] : line, lines: [] };
|
|
734
|
+
} else if (current) {
|
|
735
|
+
current.lines.push(line);
|
|
736
|
+
} else {
|
|
737
|
+
if (!current) current = { fileName: "", lines: [] };
|
|
738
|
+
current.lines.push(line);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (current) sections.push(current);
|
|
742
|
+
|
|
743
|
+
if (sections.length <= 1 && (!sections[0]?.fileName || sections[0].fileName === "")) {
|
|
744
|
+
// Single file — plain view
|
|
745
|
+
const pre = document.createElement("pre");
|
|
746
|
+
pre.className = "git-diff-content";
|
|
747
|
+
renderColoredDiff(pre, sections[0]?.lines || diffText.split("\n"));
|
|
748
|
+
body.appendChild(pre);
|
|
749
|
+
} else {
|
|
750
|
+
// Multi-file — per-file collapsible sections
|
|
751
|
+
for (const section of sections) {
|
|
752
|
+
let add = 0, del = 0;
|
|
753
|
+
for (const l of section.lines) {
|
|
754
|
+
if (l.startsWith("+") && !l.startsWith("+++")) add++;
|
|
755
|
+
else if (l.startsWith("-") && !l.startsWith("---")) del++;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const fileDiv = document.createElement("div");
|
|
759
|
+
fileDiv.className = "git-diff-file";
|
|
760
|
+
|
|
761
|
+
const header = document.createElement("div");
|
|
762
|
+
header.className = "git-diff-file-header";
|
|
763
|
+
header.innerHTML = `
|
|
764
|
+
<svg class="git-diff-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
765
|
+
<span class="git-diff-file-name">${escapeHtml(section.fileName)}</span>
|
|
766
|
+
<span class="git-diff-file-stats">
|
|
767
|
+
${add ? `<span class="diff-stat-add">+${add}</span>` : ""}
|
|
768
|
+
${del ? `<span class="diff-stat-del">-${del}</span>` : ""}
|
|
769
|
+
</span>
|
|
770
|
+
`;
|
|
771
|
+
header.addEventListener("click", () => fileDiv.classList.toggle("collapsed"));
|
|
772
|
+
|
|
773
|
+
const content = document.createElement("pre");
|
|
774
|
+
content.className = "git-diff-content git-diff-file-content";
|
|
775
|
+
renderColoredDiff(content, section.lines);
|
|
776
|
+
|
|
777
|
+
fileDiv.appendChild(header);
|
|
778
|
+
fileDiv.appendChild(content);
|
|
779
|
+
body.appendChild(fileDiv);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
overlay.querySelector(".modal-close").addEventListener("click", () => overlay.remove());
|
|
785
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
|
786
|
+
document.addEventListener("keydown", function esc(e) {
|
|
787
|
+
if (e.key === "Escape") { overlay.remove(); document.removeEventListener("keydown", esc); }
|
|
788
|
+
});
|
|
789
|
+
document.body.appendChild(overlay);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function renderColoredDiff(container, lines) {
|
|
793
|
+
for (const line of lines) {
|
|
794
|
+
const span = document.createElement("span");
|
|
795
|
+
span.textContent = line + "\n";
|
|
796
|
+
if (line.startsWith("+++") || line.startsWith("---")) span.className = "diff-line-meta";
|
|
797
|
+
else if (line.startsWith("+")) span.className = "diff-line-added";
|
|
798
|
+
else if (line.startsWith("-")) span.className = "diff-line-removed";
|
|
799
|
+
else if (line.startsWith("@@")) span.className = "diff-line-hunk";
|
|
800
|
+
container.appendChild(span);
|
|
459
801
|
}
|
|
460
802
|
}
|
|
461
803
|
|
|
804
|
+
function escapeHtml(str) {
|
|
805
|
+
const d = document.createElement("div");
|
|
806
|
+
d.textContent = str;
|
|
807
|
+
return d.innerHTML;
|
|
808
|
+
}
|
|
809
|
+
|
|
462
810
|
// Listen for WebSocket messages via event bus
|
|
463
811
|
on("ws:message", handleServerMessage);
|
|
464
812
|
|
|
@@ -618,6 +966,7 @@ $.stopBtn.addEventListener("click", () => stopGeneration(getPane(null)));
|
|
|
618
966
|
|
|
619
967
|
$.messageInput.addEventListener("keydown", (e) => {
|
|
620
968
|
if (handleAutocompleteKeydown(e, getPane(null))) return;
|
|
969
|
+
if (handleHistoryKeydown(e, getPane(null), inputHistory)) return;
|
|
621
970
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
622
971
|
e.preventDefault();
|
|
623
972
|
sendMessage(getPane(null));
|
|
@@ -625,19 +974,168 @@ $.messageInput.addEventListener("keydown", (e) => {
|
|
|
625
974
|
});
|
|
626
975
|
|
|
627
976
|
$.messageInput.addEventListener("input", () => {
|
|
977
|
+
if (inputHistory.isNavigating) inputHistory.reset();
|
|
628
978
|
$.messageInput.style.height = "auto";
|
|
629
979
|
$.messageInput.style.height = Math.min($.messageInput.scrollHeight, 200) + "px";
|
|
630
980
|
handleSlashAutocomplete(getPane(null));
|
|
631
981
|
});
|
|
632
982
|
|
|
983
|
+
// ── History button + popover ──
|
|
984
|
+
export function updateHistoryButtonVisibility() {
|
|
985
|
+
// Re-sync key in case project loaded after module init
|
|
986
|
+
const ek = historyKey();
|
|
987
|
+
if (inputHistory.storageKey !== ek) inputHistory = new InputHistory(ek);
|
|
988
|
+
if ($.historyBtn) {
|
|
989
|
+
$.historyBtn.classList.toggle("hidden", inputHistory.entries.length === 0);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function renderHistoryPopover() {
|
|
994
|
+
const el = $.historyPopover;
|
|
995
|
+
if (!el) return;
|
|
996
|
+
const entries = inputHistory.getAll();
|
|
997
|
+
|
|
998
|
+
if (entries.length === 0) {
|
|
999
|
+
el.innerHTML = '<div class="history-popover-empty">No messages yet</div>';
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
el.innerHTML = `<div class="history-popover-header"><span>Recent messages</span><button class="history-popover-clear">Clear</button></div>`;
|
|
1004
|
+
const clearBtn = el.querySelector(".history-popover-clear");
|
|
1005
|
+
clearBtn.addEventListener("click", (e) => {
|
|
1006
|
+
e.stopPropagation();
|
|
1007
|
+
inputHistory.entries.length = 0;
|
|
1008
|
+
inputHistory._save();
|
|
1009
|
+
closeHistoryPopover();
|
|
1010
|
+
updateHistoryButtonVisibility();
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
entries.forEach((text) => {
|
|
1014
|
+
const item = document.createElement("div");
|
|
1015
|
+
item.className = "history-popover-item";
|
|
1016
|
+
const truncated = text.length > 80 ? text.slice(0, 80) + "\u2026" : text;
|
|
1017
|
+
const span = document.createElement("span");
|
|
1018
|
+
span.className = "history-popover-item-text" + (text.startsWith("/") ? " is-slash" : "");
|
|
1019
|
+
span.textContent = truncated;
|
|
1020
|
+
item.appendChild(span);
|
|
1021
|
+
item.addEventListener("click", () => {
|
|
1022
|
+
const pane = getPane(null);
|
|
1023
|
+
pane.messageInput.value = text;
|
|
1024
|
+
pane.messageInput.style.height = "auto";
|
|
1025
|
+
pane.messageInput.style.height = Math.min(pane.messageInput.scrollHeight, 200) + "px";
|
|
1026
|
+
closeHistoryPopover();
|
|
1027
|
+
pane.messageInput.focus();
|
|
1028
|
+
});
|
|
1029
|
+
el.appendChild(item);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function closeHistoryPopover() {
|
|
1034
|
+
if ($.historyPopover) $.historyPopover.classList.add("hidden");
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if ($.historyBtn) {
|
|
1038
|
+
$.historyBtn.addEventListener("click", (e) => {
|
|
1039
|
+
e.stopPropagation();
|
|
1040
|
+
const el = $.historyPopover;
|
|
1041
|
+
if (!el) return;
|
|
1042
|
+
if (el.classList.contains("hidden")) {
|
|
1043
|
+
renderHistoryPopover();
|
|
1044
|
+
el.classList.remove("hidden");
|
|
1045
|
+
} else {
|
|
1046
|
+
closeHistoryPopover();
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
document.addEventListener("click", (e) => {
|
|
1052
|
+
if ($.historyPopover && !$.historyPopover.classList.contains("hidden")) {
|
|
1053
|
+
if (!$.historyPopover.contains(e.target) && e.target !== $.historyBtn) {
|
|
1054
|
+
closeHistoryPopover();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
633
1059
|
// Initialize mermaid
|
|
634
1060
|
if (typeof mermaid !== "undefined") {
|
|
635
1061
|
mermaid.initialize({ startOnLoad: false, theme: "dark" });
|
|
636
1062
|
}
|
|
637
1063
|
|
|
1064
|
+
// ── Fork button handler (delegated) ──
|
|
1065
|
+
$.messagesDiv.addEventListener("click", async (e) => {
|
|
1066
|
+
const forkBtn = e.target.closest(".fork-btn");
|
|
1067
|
+
if (!forkBtn) return;
|
|
1068
|
+
|
|
1069
|
+
// Block fork during active streaming
|
|
1070
|
+
const currentPane = getPane(null);
|
|
1071
|
+
if (currentPane && currentPane.isStreaming) return;
|
|
1072
|
+
|
|
1073
|
+
const messageId = Number(forkBtn.dataset.messageId);
|
|
1074
|
+
const sessionId = getState("sessionId");
|
|
1075
|
+
if (!sessionId || !messageId) return;
|
|
1076
|
+
|
|
1077
|
+
forkBtn.disabled = true;
|
|
1078
|
+
forkBtn.classList.add("fork-loading");
|
|
1079
|
+
try {
|
|
1080
|
+
const forked = await api.forkSession(sessionId, messageId);
|
|
1081
|
+
// Switch to the forked session
|
|
1082
|
+
setState("sessionId", forked.id);
|
|
1083
|
+
$.messagesDiv.innerHTML = "";
|
|
1084
|
+
const { loadMessages } = await import('./sessions.js');
|
|
1085
|
+
await loadMessages(forked.id);
|
|
1086
|
+
await loadSessions();
|
|
1087
|
+
$.messageInput.focus();
|
|
1088
|
+
// Show toast
|
|
1089
|
+
showForkToast(forked.title || "Forked session");
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
console.error("Fork failed:", err);
|
|
1092
|
+
} finally {
|
|
1093
|
+
forkBtn.disabled = false;
|
|
1094
|
+
forkBtn.classList.remove("fork-loading");
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
function showForkToast(title) {
|
|
1099
|
+
const container = document.getElementById("toast-container");
|
|
1100
|
+
if (!container) return;
|
|
1101
|
+
const toast = document.createElement("div");
|
|
1102
|
+
toast.className = "bg-toast";
|
|
1103
|
+
const dot = document.createElement("span");
|
|
1104
|
+
dot.className = "bg-toast-dot";
|
|
1105
|
+
const body = document.createElement("div");
|
|
1106
|
+
body.className = "bg-toast-body";
|
|
1107
|
+
const label = document.createElement("div");
|
|
1108
|
+
label.className = "bg-toast-label";
|
|
1109
|
+
label.textContent = "Session forked";
|
|
1110
|
+
const titleEl = document.createElement("div");
|
|
1111
|
+
titleEl.className = "bg-toast-title";
|
|
1112
|
+
titleEl.textContent = title;
|
|
1113
|
+
body.appendChild(label);
|
|
1114
|
+
body.appendChild(titleEl);
|
|
1115
|
+
const closeBtn = document.createElement("button");
|
|
1116
|
+
closeBtn.className = "bg-toast-close";
|
|
1117
|
+
closeBtn.title = "Dismiss";
|
|
1118
|
+
closeBtn.innerHTML = "×";
|
|
1119
|
+
toast.appendChild(dot);
|
|
1120
|
+
toast.appendChild(body);
|
|
1121
|
+
toast.appendChild(closeBtn);
|
|
1122
|
+
const dismiss = () => {
|
|
1123
|
+
toast.classList.add("toast-exit");
|
|
1124
|
+
toast.addEventListener("animationend", () => toast.remove());
|
|
1125
|
+
};
|
|
1126
|
+
closeBtn.addEventListener("click", dismiss);
|
|
1127
|
+
container.appendChild(toast);
|
|
1128
|
+
setTimeout(() => { if (toast.parentNode) dismiss(); }, 3000);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
638
1131
|
// ── Boot sequence ──
|
|
639
1132
|
showWhalyPlaceholder();
|
|
640
|
-
|
|
1133
|
+
updateHistoryButtonVisibility();
|
|
1134
|
+
loadProjects().then(() => {
|
|
1135
|
+
// Re-sync history after projects load (programmatic .value= doesn't fire change event)
|
|
1136
|
+
inputHistory = new InputHistory(historyKey());
|
|
1137
|
+
updateHistoryButtonVisibility();
|
|
1138
|
+
}); // loadSessions() is called inside loadProjects() after dropdown is populated
|
|
641
1139
|
loadAccountInfo();
|
|
642
1140
|
loadStats();
|
|
643
1141
|
loadPrompts();
|