claude-code-kanban 2.2.0-rc.6 → 2.2.0-rc.8
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/package.json +2 -2
- package/public/app.js +501 -62
- package/public/index.html +36 -4
- package/public/style.css +218 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "2.2.0-rc.
|
|
3
|
+
"version": "2.2.0-rc.8",
|
|
4
4
|
"description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node server.js",
|
|
11
|
-
"dev": "node server.js
|
|
11
|
+
"dev": "node --watch server.js",
|
|
12
12
|
"test": "node --test test/contracts.test.js",
|
|
13
13
|
"test:hooks": "bash tests/test-agent-spy.sh",
|
|
14
14
|
"validate:schemas": "node test/validate-live-schemas.js",
|
package/public/app.js
CHANGED
|
@@ -114,7 +114,8 @@ let lastTasksHash = '';
|
|
|
114
114
|
//#region DATA_FETCHING
|
|
115
115
|
async function fetchSessions() {
|
|
116
116
|
try {
|
|
117
|
-
const
|
|
117
|
+
const allPinnedIds = new Set([...pinnedSessionIds, ...stickySessionIds]);
|
|
118
|
+
const pinnedParam = allPinnedIds.size > 0 ? `&pinned=${[...allPinnedIds].join(',')}` : '';
|
|
118
119
|
const [newSessions, newTasks] = await Promise.all([
|
|
119
120
|
fetch(`/api/sessions?limit=${sessionLimit}${pinnedParam}`).then((r) => r.json()),
|
|
120
121
|
fetch('/api/tasks/all').then((r) => r.json()),
|
|
@@ -435,6 +436,10 @@ async function fetchTasks(sessionId) {
|
|
|
435
436
|
if (agentLogMode && sessionId !== currentSessionId) exitAgentLogMode();
|
|
436
437
|
if (sessionId !== currentSessionId && document.getElementById('scratchpad-modal').classList.contains('visible'))
|
|
437
438
|
closeScratchpad();
|
|
439
|
+
if (revealedPlanSessionId && sessionId !== revealedPlanSessionId) {
|
|
440
|
+
revealedPlanSessionId = null;
|
|
441
|
+
renderSessions();
|
|
442
|
+
}
|
|
438
443
|
currentSessionId = sessionId;
|
|
439
444
|
currentPins = loadPins(sessionId);
|
|
440
445
|
ownerFilter = '';
|
|
@@ -500,9 +505,7 @@ async function fetchProjectView(projectPath) {
|
|
|
500
505
|
if (msgPinned) msgPinned.innerHTML = '';
|
|
501
506
|
const projectSessions = sessions.filter((s) => s.project === projectPath);
|
|
502
507
|
currentProjectSessionIds = projectSessions.map((s) => s.id);
|
|
503
|
-
const activeSessionIds = projectSessions
|
|
504
|
-
.filter((s) => isSessionActive(s) || pinnedSessionIds.has(s.id))
|
|
505
|
-
.map((s) => s.id);
|
|
508
|
+
const activeSessionIds = projectSessions.filter((s) => isSessionActive(s) || isAnyPinned(s.id)).map((s) => s.id);
|
|
506
509
|
|
|
507
510
|
const encoded = btoa(projectPath);
|
|
508
511
|
const [tasksResult, agentResults] = await Promise.all([
|
|
@@ -551,9 +554,7 @@ async function fetchProjectView(projectPath) {
|
|
|
551
554
|
async function refreshProjectAgents() {
|
|
552
555
|
if (!currentProjectPath) return;
|
|
553
556
|
const projectSessions = sessions.filter((s) => s.project === currentProjectPath);
|
|
554
|
-
const activeSessionIds = projectSessions
|
|
555
|
-
.filter((s) => isSessionActive(s) || pinnedSessionIds.has(s.id))
|
|
556
|
-
.map((s) => s.id);
|
|
557
|
+
const activeSessionIds = projectSessions.filter((s) => isSessionActive(s) || isAnyPinned(s.id)).map((s) => s.id);
|
|
557
558
|
const agentResults = await Promise.all(
|
|
558
559
|
activeSessionIds.map((id) =>
|
|
559
560
|
fetch(`/api/sessions/${id}/agents`)
|
|
@@ -758,7 +759,6 @@ function renderPinnedSection() {
|
|
|
758
759
|
<div class="msg-body"><div class="msg-text">${escapeHtml(p.tool || '')}${toolDetail}</div><div class="msg-time">${formatDate(p.timestamp)}</div></div>${pinnedAgentLogBtn}${unpin}
|
|
759
760
|
</div>`;
|
|
760
761
|
} else if (p.type === 'agent') {
|
|
761
|
-
const agentClick = `onclick="showAgentModal('${escapeHtml(p.agentId)}')" style="cursor:pointer"`;
|
|
762
762
|
const agentLogBtn = agentLogButton(p.agentId);
|
|
763
763
|
const msgTrunc = p.lastMessage
|
|
764
764
|
? escapeHtml(
|
|
@@ -768,7 +768,7 @@ function renderPinnedSection() {
|
|
|
768
768
|
)
|
|
769
769
|
: '';
|
|
770
770
|
const agentDetail = msgTrunc ? ` <span style="color:var(--text-muted)">${msgTrunc}</span>` : '';
|
|
771
|
-
return `<div class="msg-item msg-tool" ${
|
|
771
|
+
return `<div class="msg-item msg-tool" ${click}>
|
|
772
772
|
${MSG_ICON_TOOL}
|
|
773
773
|
<div class="msg-body"><div class="msg-text">${escapeHtml(p.agentType || 'Agent')}${agentDetail}</div><div class="msg-time">${formatDate(p.timestamp)}</div></div>${agentLogBtn}${unpin}
|
|
774
774
|
</div>`;
|
|
@@ -1019,27 +1019,8 @@ function showPinnedMsgDetail(pinIdx) {
|
|
|
1019
1019
|
}
|
|
1020
1020
|
currentMsgDetailIdx = null;
|
|
1021
1021
|
currentPinDetailId = pin.id;
|
|
1022
|
+
_renderPinToDetail(pin);
|
|
1022
1023
|
const body = document.getElementById('msg-detail-body');
|
|
1023
|
-
const agentBtn = document.getElementById('msg-detail-agent-btn');
|
|
1024
|
-
if (pin.type === 'tool_use') {
|
|
1025
|
-
document.getElementById('msg-detail-title').textContent = pin.tool || 'Tool';
|
|
1026
|
-
const fullText = pin.fullDetail || pin.detail || '';
|
|
1027
|
-
const pinParamsHtml = renderToolParamsHtml(pin.params);
|
|
1028
|
-
const pinResultHtml = renderToolResultHtml(pin.toolResult, pin.toolResultTruncated, pin.toolResultFull);
|
|
1029
|
-
const pinDetailEscaped = escapeHtml(fullText);
|
|
1030
|
-
const pinDetailRendered = pin.tool === 'Bash' ? highlightBash(pinDetailEscaped) : pinDetailEscaped;
|
|
1031
|
-
body.innerHTML =
|
|
1032
|
-
(fullText ? `<pre class="msg-detail-pre">${pinDetailRendered}</pre>` : '<em>No details</em>') +
|
|
1033
|
-
pinParamsHtml +
|
|
1034
|
-
pinResultHtml;
|
|
1035
|
-
agentBtn.style.display = 'none';
|
|
1036
|
-
} else {
|
|
1037
|
-
const text = stripAnsi(pin.fullText || pin.text || '');
|
|
1038
|
-
document.getElementById('msg-detail-title').textContent = pin.type === 'assistant' ? 'Claude' : 'User';
|
|
1039
|
-
agentBtn.style.display = 'none';
|
|
1040
|
-
body.innerHTML = renderMarkdown(text);
|
|
1041
|
-
}
|
|
1042
|
-
document.getElementById('msg-detail-meta').textContent = formatDate(pin.timestamp);
|
|
1043
1024
|
const pinModal = document.getElementById('msg-detail-modal').querySelector('.modal');
|
|
1044
1025
|
autoSizeModal(pinModal, body);
|
|
1045
1026
|
const pinBtn = document.getElementById('msg-detail-pin-btn');
|
|
@@ -1073,6 +1054,7 @@ function togglePinnedCollapse() {
|
|
|
1073
1054
|
|
|
1074
1055
|
//#region PINNING
|
|
1075
1056
|
let pinnedSessionIds = new Set();
|
|
1057
|
+
let stickySessionIds = new Set();
|
|
1076
1058
|
|
|
1077
1059
|
function loadPinnedSessions() {
|
|
1078
1060
|
try {
|
|
@@ -1082,18 +1064,72 @@ function loadPinnedSessions() {
|
|
|
1082
1064
|
}
|
|
1083
1065
|
}
|
|
1084
1066
|
|
|
1067
|
+
function loadStickySessions() {
|
|
1068
|
+
try {
|
|
1069
|
+
return new Set(JSON.parse(localStorage.getItem('sticky-sessions')) || []);
|
|
1070
|
+
} catch {
|
|
1071
|
+
return new Set();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1085
1075
|
function savePinnedSessions() {
|
|
1086
1076
|
localStorage.setItem('pinned-sessions', JSON.stringify([...pinnedSessionIds]));
|
|
1077
|
+
localStorage.setItem('sticky-sessions', JSON.stringify([...stickySessionIds]));
|
|
1087
1078
|
}
|
|
1088
1079
|
|
|
1080
|
+
// unpinned → pinned → sticky → unpinned
|
|
1089
1081
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1090
1082
|
function toggleSessionPin(sessionId) {
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1083
|
+
if (stickySessionIds.has(sessionId)) {
|
|
1084
|
+
stickySessionIds.delete(sessionId);
|
|
1085
|
+
pinnedSessionIds.delete(sessionId);
|
|
1086
|
+
} else if (pinnedSessionIds.has(sessionId)) {
|
|
1087
|
+
pinnedSessionIds.delete(sessionId);
|
|
1088
|
+
stickySessionIds.add(sessionId);
|
|
1089
|
+
} else {
|
|
1090
|
+
pinnedSessionIds.add(sessionId);
|
|
1091
|
+
}
|
|
1093
1092
|
savePinnedSessions();
|
|
1094
1093
|
renderSessions();
|
|
1095
1094
|
}
|
|
1096
1095
|
|
|
1096
|
+
function getSessionPinState(sessionId) {
|
|
1097
|
+
if (stickySessionIds.has(sessionId)) return 'sticky';
|
|
1098
|
+
if (pinnedSessionIds.has(sessionId)) return 'pinned';
|
|
1099
|
+
return 'none';
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function isAnyPinned(sessionId) {
|
|
1103
|
+
return pinnedSessionIds.has(sessionId) || stickySessionIds.has(sessionId);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function _renderPinToDetail(pin) {
|
|
1107
|
+
const body = document.getElementById('msg-detail-body');
|
|
1108
|
+
const agentBtn = document.getElementById('msg-detail-agent-btn');
|
|
1109
|
+
agentBtn.style.display = 'none';
|
|
1110
|
+
if (pin.type === 'tool_use') {
|
|
1111
|
+
document.getElementById('msg-detail-title').textContent = pin.tool || 'Tool';
|
|
1112
|
+
const fullText = pin.fullDetail || pin.detail || '';
|
|
1113
|
+
const pinParamsHtml = renderToolParamsHtml(pin.params);
|
|
1114
|
+
const pinResultHtml = renderToolResultHtml(pin.toolResult, pin.toolResultTruncated, pin.toolResultFull);
|
|
1115
|
+
const pinDetailEscaped = escapeHtml(fullText);
|
|
1116
|
+
const pinDetailRendered = pin.tool === 'Bash' ? highlightBash(pinDetailEscaped) : pinDetailEscaped;
|
|
1117
|
+
body.innerHTML =
|
|
1118
|
+
(fullText ? `<pre class="msg-detail-pre">${pinDetailRendered}</pre>` : '<em>No details</em>') +
|
|
1119
|
+
pinParamsHtml +
|
|
1120
|
+
pinResultHtml;
|
|
1121
|
+
} else if (pin.type === 'agent') {
|
|
1122
|
+
document.getElementById('msg-detail-title').textContent = pin.agentType || 'Agent';
|
|
1123
|
+
const lastMsg = stripAnsi(pin.lastMessage || '');
|
|
1124
|
+
body.innerHTML = lastMsg ? renderMarkdown(lastMsg) : '<em>No agent message</em>';
|
|
1125
|
+
} else {
|
|
1126
|
+
const text = stripAnsi(pin.fullText || pin.text || '');
|
|
1127
|
+
document.getElementById('msg-detail-title').textContent = pin.type === 'assistant' ? 'Claude' : 'User';
|
|
1128
|
+
body.innerHTML = renderMarkdown(text);
|
|
1129
|
+
}
|
|
1130
|
+
document.getElementById('msg-detail-meta').textContent = formatDate(pin.timestamp);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1097
1133
|
const SESSION_PIN_SVG = PIN_SVG.replace('width="14" height="14"', 'width="12" height="12"');
|
|
1098
1134
|
|
|
1099
1135
|
//#endregion
|
|
@@ -1737,6 +1773,22 @@ function closeAgentModal() {
|
|
|
1737
1773
|
//#endregion
|
|
1738
1774
|
|
|
1739
1775
|
//#region RENDERING
|
|
1776
|
+
let revealedPlanSessionId = null;
|
|
1777
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1778
|
+
function revealPlanSession(planSessionId) {
|
|
1779
|
+
if (revealedPlanSessionId === planSessionId) {
|
|
1780
|
+
revealedPlanSessionId = null;
|
|
1781
|
+
} else {
|
|
1782
|
+
revealedPlanSessionId = planSessionId;
|
|
1783
|
+
}
|
|
1784
|
+
renderSessions();
|
|
1785
|
+
if (revealedPlanSessionId) {
|
|
1786
|
+
fetchTasks(planSessionId);
|
|
1787
|
+
const el = document.querySelector(`.session-item[data-session-id="${CSS.escape(planSessionId)}"]`);
|
|
1788
|
+
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1740
1792
|
async function showAllTasks() {
|
|
1741
1793
|
try {
|
|
1742
1794
|
viewMode = 'all';
|
|
@@ -1810,16 +1862,9 @@ function renderSessions() {
|
|
|
1810
1862
|
if (isActive) activeSessionIds.add(s.id);
|
|
1811
1863
|
return isActive;
|
|
1812
1864
|
});
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
(
|
|
1816
|
-
s.planImplementationSessionId &&
|
|
1817
|
-
activeSessionIds.has(s.planImplementationSessionId) &&
|
|
1818
|
-
!activeSessionIds.has(s.id),
|
|
1819
|
-
);
|
|
1820
|
-
if (planSessions.length) {
|
|
1821
|
-
filteredSessions = filteredSessions.concat(planSessions);
|
|
1822
|
-
filteredSessions.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt));
|
|
1865
|
+
if (revealedPlanSessionId && !filteredSessions.some((s) => s.id === revealedPlanSessionId)) {
|
|
1866
|
+
const planSession = sessions.find((s) => s.id === revealedPlanSessionId);
|
|
1867
|
+
if (planSession) filteredSessions.push(planSession);
|
|
1823
1868
|
}
|
|
1824
1869
|
}
|
|
1825
1870
|
if (filterProject) {
|
|
@@ -1850,10 +1895,10 @@ function renderSessions() {
|
|
|
1850
1895
|
});
|
|
1851
1896
|
}
|
|
1852
1897
|
|
|
1853
|
-
// Always include pinned sessions even if they don't match filters
|
|
1854
|
-
if (pinnedSessionIds.size > 0 && !searchQuery) {
|
|
1898
|
+
// Always include pinned/sticky sessions even if they don't match filters
|
|
1899
|
+
if ((pinnedSessionIds.size > 0 || stickySessionIds.size > 0) && !searchQuery) {
|
|
1855
1900
|
const filteredIds = new Set(filteredSessions.map((s) => s.id));
|
|
1856
|
-
const missingPinned = sessions.filter((s) =>
|
|
1901
|
+
const missingPinned = sessions.filter((s) => isAnyPinned(s.id) && !filteredIds.has(s.id));
|
|
1857
1902
|
if (missingPinned.length) filteredSessions = [...missingPinned, ...filteredSessions];
|
|
1858
1903
|
}
|
|
1859
1904
|
|
|
@@ -1907,11 +1952,13 @@ function renderSessions() {
|
|
|
1907
1952
|
const isTeam = session.isTeam;
|
|
1908
1953
|
const memberCount = session.memberCount || 0;
|
|
1909
1954
|
|
|
1910
|
-
const
|
|
1955
|
+
const pinState = getSessionPinState(session.id);
|
|
1956
|
+
const pinClass = pinState === 'sticky' ? ' sticky' : pinState === 'pinned' ? ' pinned' : '';
|
|
1957
|
+
const pinTitle = pinState === 'sticky' ? 'Unpin' : pinState === 'pinned' ? 'Sticky pin' : 'Pin';
|
|
1911
1958
|
const showCtx = !!session.contextStatus;
|
|
1912
1959
|
return `
|
|
1913
1960
|
<button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${!session.hasRecentLog && !session.inProgress && !session.hasWaitingForUser ? 'stale' : ''} ${showCtx ? 'has-context' : ''}" title="${tooltip}">
|
|
1914
|
-
<span class="session-pin-btn${
|
|
1961
|
+
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${escapeHtml(session.id)}')" title="${pinTitle} session">${SESSION_PIN_SVG}</span>
|
|
1915
1962
|
<div class="session-name">${escapeHtml(primaryName)}</div>
|
|
1916
1963
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
1917
1964
|
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
@@ -1923,7 +1970,7 @@ function renderSessions() {
|
|
|
1923
1970
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
1924
1971
|
${session.hasPlan ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${session.id}')" title="View plan"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>` : ''}
|
|
1925
1972
|
${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">🤖</span>' : ''}
|
|
1926
|
-
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to
|
|
1973
|
+
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')">📋</span>` : ''}
|
|
1927
1974
|
${session.hasWaitingForUser ? '<span class="agent-badge" title="Waiting for user">❓</span>' : ''}
|
|
1928
1975
|
${isLive ? '<span class="pulse"></span>' : ''}
|
|
1929
1976
|
</span>
|
|
@@ -1951,10 +1998,14 @@ function renderSessions() {
|
|
|
1951
1998
|
const groupPinned = localStorage.getItem('groupPinnedSessions') !== 'false';
|
|
1952
1999
|
const renderGroupSessions = (sessions, pinKey) => {
|
|
1953
2000
|
if (!groupPinned || pinnedSessionIds.size === 0) return sessions.map(renderSessionCard).join('');
|
|
1954
|
-
const gPinned = sessions.filter((s) => pinnedSessionIds.has(s.id));
|
|
2001
|
+
const gPinned = sessions.filter((s) => pinnedSessionIds.has(s.id) && !stickySessionIds.has(s.id));
|
|
1955
2002
|
if (gPinned.length === 0) return sessions.map(renderSessionCard).join('');
|
|
1956
|
-
const
|
|
2003
|
+
const gIdlePinned = gPinned.filter((s) => !isSessionActive(s));
|
|
2004
|
+
const gUnpinned = sessions.filter(
|
|
2005
|
+
(s) => !pinnedSessionIds.has(s.id) || isSessionActive(s) || stickySessionIds.has(s.id),
|
|
2006
|
+
);
|
|
1957
2007
|
const pinCollapsed = collapsedProjectGroups.has(pinKey);
|
|
2008
|
+
if (gIdlePinned.length === 0 && !pinCollapsed) return gUnpinned.map(renderSessionCard).join('');
|
|
1958
2009
|
return (
|
|
1959
2010
|
'<div class="pinned-sub-section">' +
|
|
1960
2011
|
'<div class="pinned-sub-header' +
|
|
@@ -1965,21 +2016,23 @@ function renderSessions() {
|
|
|
1965
2016
|
'<svg class="group-chevron" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>' +
|
|
1966
2017
|
'<span class="pinned-sub-label">Pinned</span>' +
|
|
1967
2018
|
'<span class="group-count">' +
|
|
1968
|
-
|
|
2019
|
+
gIdlePinned.length +
|
|
1969
2020
|
'</span>' +
|
|
1970
2021
|
'<span class="pinned-ungroup-btn" title="Ungroup pinned sessions">×</span>' +
|
|
1971
2022
|
'</div>' +
|
|
1972
2023
|
'<div class="pinned-sub-items' +
|
|
1973
2024
|
(pinCollapsed ? ' collapsed' : '') +
|
|
1974
2025
|
'">' +
|
|
1975
|
-
|
|
2026
|
+
gIdlePinned.map(renderSessionCard).join('') +
|
|
1976
2027
|
'</div>' +
|
|
1977
2028
|
'</div>' +
|
|
1978
2029
|
gUnpinned.map(renderSessionCard).join('')
|
|
1979
2030
|
);
|
|
1980
2031
|
};
|
|
1981
|
-
if (!groupPinned && pinnedSessionIds.size > 0) {
|
|
1982
|
-
const
|
|
2032
|
+
if (!groupPinned && (pinnedSessionIds.size > 0 || stickySessionIds.size > 0)) {
|
|
2033
|
+
const pinWeight = (s) =>
|
|
2034
|
+
stickySessionIds.has(s.id) ? 2 : pinnedSessionIds.has(s.id) && !isSessionActive(s) ? 1 : 0;
|
|
2035
|
+
const pinSort = (a, b) => pinWeight(b) - pinWeight(a);
|
|
1983
2036
|
for (const [, arr] of groups) arr.sort(pinSort);
|
|
1984
2037
|
ungrouped.sort(pinSort);
|
|
1985
2038
|
}
|
|
@@ -2054,19 +2107,28 @@ function renderSessions() {
|
|
|
2054
2107
|
|
|
2055
2108
|
sessionsList.innerHTML = html;
|
|
2056
2109
|
} else {
|
|
2057
|
-
const
|
|
2058
|
-
const
|
|
2110
|
+
const sticky = filteredSessions.filter((s) => stickySessionIds.has(s.id));
|
|
2111
|
+
const idlePinned = filteredSessions.filter((s) => pinnedSessionIds.has(s.id) && !isSessionActive(s));
|
|
2112
|
+
const rest = filteredSessions.filter(
|
|
2113
|
+
(s) =>
|
|
2114
|
+
(!pinnedSessionIds.has(s.id) && !stickySessionIds.has(s.id)) ||
|
|
2115
|
+
(pinnedSessionIds.has(s.id) && isSessionActive(s)),
|
|
2116
|
+
);
|
|
2059
2117
|
let html = '';
|
|
2060
|
-
if (
|
|
2061
|
-
|
|
2118
|
+
if (sticky.length > 0) {
|
|
2119
|
+
html += sticky.map(renderSessionCard).join('');
|
|
2120
|
+
}
|
|
2121
|
+
const isCollapsed = collapsedProjectGroups.has('__pinned__');
|
|
2122
|
+
const hasPinned = pinnedSessionIds.size > 0 && filteredSessions.some((s) => pinnedSessionIds.has(s.id));
|
|
2123
|
+
if (idlePinned.length > 0 || (hasPinned && isCollapsed)) {
|
|
2062
2124
|
html += `
|
|
2063
2125
|
<div class="project-group-header${isCollapsed ? ' collapsed' : ''}" data-group-path="__pinned__">
|
|
2064
2126
|
<svg class="group-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>
|
|
2065
2127
|
<span class="group-name">Pinned</span>
|
|
2066
|
-
<span class="group-count">${
|
|
2128
|
+
<span class="group-count">${idlePinned.length}</span>
|
|
2067
2129
|
</div>
|
|
2068
2130
|
<div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}">
|
|
2069
|
-
${
|
|
2131
|
+
${idlePinned.map(renderSessionCard).join('')}
|
|
2070
2132
|
</div>
|
|
2071
2133
|
`;
|
|
2072
2134
|
}
|
|
@@ -3031,7 +3093,10 @@ const _scratchpadModal = document.getElementById('scratchpad-modal');
|
|
|
3031
3093
|
const _scratchpadTextarea = document.getElementById('scratchpad-textarea');
|
|
3032
3094
|
const _scratchpadCharcount = document.getElementById('scratchpad-charcount');
|
|
3033
3095
|
|
|
3096
|
+
let _scratchpadKeyOverride = null;
|
|
3097
|
+
|
|
3034
3098
|
function _scratchpadKey() {
|
|
3099
|
+
if (_scratchpadKeyOverride) return _scratchpadKeyOverride;
|
|
3035
3100
|
if (currentSessionId) return `scratchpad-${currentSessionId}`;
|
|
3036
3101
|
if (currentProjectPath) return `scratchpad-project:${currentProjectPath}`;
|
|
3037
3102
|
return null;
|
|
@@ -3045,7 +3110,8 @@ function toggleScratchpad() {
|
|
|
3045
3110
|
}
|
|
3046
3111
|
}
|
|
3047
3112
|
|
|
3048
|
-
function showScratchpad() {
|
|
3113
|
+
function showScratchpad(keyOverride) {
|
|
3114
|
+
_scratchpadKeyOverride = keyOverride || null;
|
|
3049
3115
|
const key = _scratchpadKey();
|
|
3050
3116
|
if (!key) return;
|
|
3051
3117
|
_scratchpadTextarea.value = localStorage.getItem(key) || '';
|
|
@@ -3060,13 +3126,19 @@ function closeScratchpad() {
|
|
|
3060
3126
|
_scratchpadSaveTimer = null;
|
|
3061
3127
|
}
|
|
3062
3128
|
saveScratchpad();
|
|
3129
|
+
_scratchpadKeyOverride = null;
|
|
3063
3130
|
_scratchpadModal.classList.remove('visible');
|
|
3064
3131
|
}
|
|
3065
3132
|
|
|
3066
3133
|
function saveScratchpad() {
|
|
3067
3134
|
const key = _scratchpadKey();
|
|
3068
3135
|
if (!key) return;
|
|
3069
|
-
|
|
3136
|
+
const val = _scratchpadTextarea.value;
|
|
3137
|
+
if (val.trim()) {
|
|
3138
|
+
localStorage.setItem(key, val);
|
|
3139
|
+
} else {
|
|
3140
|
+
localStorage.removeItem(key);
|
|
3141
|
+
}
|
|
3070
3142
|
}
|
|
3071
3143
|
|
|
3072
3144
|
_scratchpadTextarea.addEventListener('input', () => {
|
|
@@ -3080,6 +3152,367 @@ _scratchpadTextarea.addEventListener('input', () => {
|
|
|
3080
3152
|
|
|
3081
3153
|
//#endregion
|
|
3082
3154
|
|
|
3155
|
+
//#region STORAGE_MANAGER
|
|
3156
|
+
|
|
3157
|
+
function _getStorageTotalSize() {
|
|
3158
|
+
let bytes = 0;
|
|
3159
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3160
|
+
const k = localStorage.key(i);
|
|
3161
|
+
bytes += k.length + localStorage.getItem(k).length;
|
|
3162
|
+
}
|
|
3163
|
+
return bytes * 2; // UTF-16
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
function _updateStorageTotal() {
|
|
3167
|
+
const el = document.getElementById('storage-total');
|
|
3168
|
+
if (el) el.textContent = `${(_getStorageTotalSize() / 1024).toFixed(1)} KB`;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
function _getKnownSessionIds() {
|
|
3172
|
+
return new Set(sessions.map((s) => s.id));
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
function _sessionLabel(session, id) {
|
|
3176
|
+
return session ? escapeHtml(session.name || session.slug || id.slice(0, 12)) : escapeHtml(id.slice(0, 12));
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
function _groupByProject(sessionIds) {
|
|
3180
|
+
const sessionMap = new Map(sessions.map((s) => [s.id, s]));
|
|
3181
|
+
const groups = new Map();
|
|
3182
|
+
const orphans = [];
|
|
3183
|
+
for (const id of sessionIds) {
|
|
3184
|
+
const session = sessionMap.get(id);
|
|
3185
|
+
if (!session) {
|
|
3186
|
+
orphans.push({ id, session: null });
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
const project = session.project || '(no project)';
|
|
3190
|
+
if (!groups.has(project)) groups.set(project, []);
|
|
3191
|
+
groups.get(project).push({ id, session });
|
|
3192
|
+
}
|
|
3193
|
+
return { groups, orphans };
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function _projectLabel(project) {
|
|
3197
|
+
if (project === '(no project)') return '(no project)';
|
|
3198
|
+
return project.split(/[/\\]/).pop() || project;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
function _escapeForJsAttr(str) {
|
|
3202
|
+
const jsEscaped = str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
3203
|
+
return escapeHtml(jsEscaped);
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
function _renderProjectGroup(label, meta, innerHtml) {
|
|
3207
|
+
return `<div class="storage-project-group">
|
|
3208
|
+
<div class="storage-project-header">
|
|
3209
|
+
<span>${label}</span>
|
|
3210
|
+
<span class="storage-item-meta">${meta}</span>
|
|
3211
|
+
</div>
|
|
3212
|
+
<div class="storage-session-group">${innerHtml}</div>
|
|
3213
|
+
</div>`;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
function _renderOrphanGroup(count, innerHtml) {
|
|
3217
|
+
return _renderProjectGroup('Orphaned', `<span class="storage-item-badge orphan">${count}</span>`, innerHtml);
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
function showStorageManager() {
|
|
3221
|
+
_updateStorageTotal();
|
|
3222
|
+
_updateOrphanedCount();
|
|
3223
|
+
document.querySelectorAll('.storage-tab').forEach((t) => {
|
|
3224
|
+
t.classList.toggle('active', t.dataset.tab === 'sessions');
|
|
3225
|
+
});
|
|
3226
|
+
_renderStorageTab();
|
|
3227
|
+
document.getElementById('storage-modal').classList.add('visible');
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
function closeStorageManager() {
|
|
3231
|
+
document.getElementById('storage-modal').classList.remove('visible');
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
3235
|
+
function switchStorageTab(tab) {
|
|
3236
|
+
document.querySelectorAll('.storage-tab').forEach((t) => {
|
|
3237
|
+
t.classList.toggle('active', t.dataset.tab === tab);
|
|
3238
|
+
});
|
|
3239
|
+
_renderStorageTab();
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
function _renderStorageTab() {
|
|
3243
|
+
const body = document.getElementById('storage-modal-body');
|
|
3244
|
+
const tab = document.querySelector('.storage-tab.active')?.dataset.tab || 'sessions';
|
|
3245
|
+
if (tab === 'sessions') body.innerHTML = _renderStorageSessions();
|
|
3246
|
+
else if (tab === 'scratchpads') body.innerHTML = _renderStorageScratchpads();
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
function _renderStorageSessions() {
|
|
3250
|
+
const pinnedIds = [...new Set([...pinnedSessionIds, ...stickySessionIds])];
|
|
3251
|
+
|
|
3252
|
+
const msgMap = new Map();
|
|
3253
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3254
|
+
const key = localStorage.key(i);
|
|
3255
|
+
if (!key.startsWith('pinned-messages-')) continue;
|
|
3256
|
+
const sid = key.slice('pinned-messages-'.length);
|
|
3257
|
+
try {
|
|
3258
|
+
const pins = JSON.parse(localStorage.getItem(key)) || [];
|
|
3259
|
+
if (pins.length) msgMap.set(sid, { pins, key });
|
|
3260
|
+
} catch {}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
const allIds = [...new Set([...pinnedIds, ...msgMap.keys()])];
|
|
3264
|
+
if (!allIds.length) return '<div class="storage-empty">No pinned sessions or messages</div>';
|
|
3265
|
+
const { groups, orphans } = _groupByProject(allIds);
|
|
3266
|
+
|
|
3267
|
+
function renderMessageItems(id) {
|
|
3268
|
+
const g = msgMap.get(id);
|
|
3269
|
+
if (!g) return '';
|
|
3270
|
+
const eid = escapeHtml(id);
|
|
3271
|
+
const header = `<div class="storage-group-header" style="padding-left:12px;">
|
|
3272
|
+
<span>${g.pins.length} pinned message${g.pins.length > 1 ? 's' : ''}</span>
|
|
3273
|
+
<div class="storage-item-actions">
|
|
3274
|
+
<button class="danger" onclick="_storageClearSessionPins('${eid}')">Clear All</button>
|
|
3275
|
+
</div>
|
|
3276
|
+
</div>`;
|
|
3277
|
+
const items = g.pins
|
|
3278
|
+
.map((p) => {
|
|
3279
|
+
const type = escapeHtml(p.type || '?');
|
|
3280
|
+
const text = escapeHtml((p.text || p.tool || p.agentType || '').slice(0, 60));
|
|
3281
|
+
const pinId = _escapeForJsAttr(p.id || '');
|
|
3282
|
+
const sid = _escapeForJsAttr(id);
|
|
3283
|
+
return `<div class="storage-item storage-item-clickable" style="padding-left:24px;" onclick="_storagePreviewPin('${sid}','${pinId}')">
|
|
3284
|
+
<span class="storage-item-badge">${type}</span>
|
|
3285
|
+
<span class="storage-item-id">${text}</span>
|
|
3286
|
+
<span class="storage-item-meta">${formatDate(p.timestamp)}</span>
|
|
3287
|
+
<div class="storage-item-actions">
|
|
3288
|
+
<button onclick="event.stopPropagation();_storagePreviewPin('${sid}','${pinId}')">View</button>
|
|
3289
|
+
<button class="danger" onclick="event.stopPropagation();_storageUnpinMessage('${sid}','${pinId}')">Unpin</button>
|
|
3290
|
+
</div>
|
|
3291
|
+
</div>`;
|
|
3292
|
+
})
|
|
3293
|
+
.join('');
|
|
3294
|
+
return header + items;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
function renderSessionItem({ id, session }) {
|
|
3298
|
+
const isPinned = isAnyPinned(id);
|
|
3299
|
+
const eid = escapeHtml(id);
|
|
3300
|
+
const actions = isPinned
|
|
3301
|
+
? `<button onclick="_storageViewSession('${eid}')">View</button>
|
|
3302
|
+
<button class="danger" onclick="_storageUnpinSession('${eid}')">Unpin</button>`
|
|
3303
|
+
: `<button onclick="_storageViewSession('${eid}')">View</button>`;
|
|
3304
|
+
return `<div class="storage-group-header">
|
|
3305
|
+
<span>${_sessionLabel(session, id)}</span>
|
|
3306
|
+
<div class="storage-item-actions">${actions}</div>
|
|
3307
|
+
</div>${renderMessageItems(id)}`;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
let html = '';
|
|
3311
|
+
for (const [project, items] of groups) {
|
|
3312
|
+
const count = items.length;
|
|
3313
|
+
html += _renderProjectGroup(
|
|
3314
|
+
escapeHtml(_projectLabel(project)),
|
|
3315
|
+
`${count} session${count > 1 ? 's' : ''}`,
|
|
3316
|
+
items.map(renderSessionItem).join(''),
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
if (orphans.length) {
|
|
3320
|
+
html += _renderOrphanGroup(orphans.length, orphans.map(renderSessionItem).join(''));
|
|
3321
|
+
}
|
|
3322
|
+
return html;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
function _storageViewSession(id) {
|
|
3326
|
+
closeStorageManager();
|
|
3327
|
+
fetchTasks(id);
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
function _storageUnpinSession(id) {
|
|
3331
|
+
pinnedSessionIds.delete(id);
|
|
3332
|
+
stickySessionIds.delete(id);
|
|
3333
|
+
savePinnedSessions();
|
|
3334
|
+
renderSessions();
|
|
3335
|
+
_renderStorageTab();
|
|
3336
|
+
_updateStorageTotal();
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
function _storageClearSessionPins(sessionId) {
|
|
3340
|
+
localStorage.removeItem(`pinned-messages-${sessionId}`);
|
|
3341
|
+
if (currentSessionId === sessionId) {
|
|
3342
|
+
currentPins = [];
|
|
3343
|
+
const el = document.getElementById('message-panel-pinned');
|
|
3344
|
+
if (el) el.innerHTML = '';
|
|
3345
|
+
}
|
|
3346
|
+
_renderStorageTab();
|
|
3347
|
+
_updateStorageTotal();
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
function _storageUnpinMessage(sessionId, pinId) {
|
|
3351
|
+
const key = `pinned-messages-${sessionId}`;
|
|
3352
|
+
try {
|
|
3353
|
+
const pins = JSON.parse(localStorage.getItem(key)) || [];
|
|
3354
|
+
const idx = pins.findIndex((p) => p.id === pinId);
|
|
3355
|
+
if (idx < 0) return;
|
|
3356
|
+
pins.splice(idx, 1);
|
|
3357
|
+
if (pins.length) localStorage.setItem(key, JSON.stringify(pins));
|
|
3358
|
+
else localStorage.removeItem(key);
|
|
3359
|
+
if (currentSessionId === sessionId) {
|
|
3360
|
+
currentPins = pins;
|
|
3361
|
+
const el = document.getElementById('message-panel-pinned');
|
|
3362
|
+
if (el) el.innerHTML = renderPinnedSection();
|
|
3363
|
+
}
|
|
3364
|
+
} catch {}
|
|
3365
|
+
_renderStorageTab();
|
|
3366
|
+
_updateStorageTotal();
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
function _renderStorageScratchpads() {
|
|
3370
|
+
const allItems = [];
|
|
3371
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3372
|
+
const key = localStorage.key(i);
|
|
3373
|
+
if (!key.startsWith('scratchpad-')) continue;
|
|
3374
|
+
const val = localStorage.getItem(key) || '';
|
|
3375
|
+
const isProject = key.startsWith('scratchpad-project:');
|
|
3376
|
+
const id = isProject ? key.slice('scratchpad-project:'.length) : key.slice('scratchpad-'.length);
|
|
3377
|
+
allItems.push({ key, id, isProject, chars: val.length });
|
|
3378
|
+
}
|
|
3379
|
+
if (!allItems.length) return '<div class="storage-empty">No scratchpads</div>';
|
|
3380
|
+
|
|
3381
|
+
const projectItems = allItems.filter((i) => i.isProject);
|
|
3382
|
+
const sessionItems = allItems.filter((i) => !i.isProject);
|
|
3383
|
+
const sessionIds = sessionItems.map((i) => i.id);
|
|
3384
|
+
const { groups: projectGroups, orphans } = _groupByProject(sessionIds);
|
|
3385
|
+
const scratchBySession = new Map(sessionItems.map((i) => [i.id, i]));
|
|
3386
|
+
|
|
3387
|
+
function renderScratchItem(item) {
|
|
3388
|
+
const session = !item.isProject ? sessions.find((s) => s.id === item.id) : null;
|
|
3389
|
+
const typeBadge = item.isProject
|
|
3390
|
+
? '<span class="storage-item-badge">project</span>'
|
|
3391
|
+
: '<span class="storage-item-badge">session</span>';
|
|
3392
|
+
const jsKey = _escapeForJsAttr(item.key);
|
|
3393
|
+
const label = item.isProject ? escapeHtml(_projectLabel(item.id)) : _sessionLabel(session, item.id);
|
|
3394
|
+
return `<div class="storage-item">
|
|
3395
|
+
<span class="storage-item-id" title="${escapeHtml(item.id)}">${label}</span>
|
|
3396
|
+
${typeBadge}
|
|
3397
|
+
<span class="storage-item-meta">${item.chars} chars</span>
|
|
3398
|
+
<div class="storage-item-actions">
|
|
3399
|
+
<button onclick="_storagePreviewScratchpad('${jsKey}')">View</button>
|
|
3400
|
+
<button class="danger" onclick="_storageDeleteScratchpad('${jsKey}')">Delete</button>
|
|
3401
|
+
</div>
|
|
3402
|
+
</div>`;
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
let html = '';
|
|
3406
|
+
|
|
3407
|
+
if (projectItems.length) {
|
|
3408
|
+
html += _renderProjectGroup(
|
|
3409
|
+
'Project Scratchpads',
|
|
3410
|
+
`${projectItems.length}`,
|
|
3411
|
+
projectItems.map(renderScratchItem).join(''),
|
|
3412
|
+
);
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
for (const [project, items] of projectGroups) {
|
|
3416
|
+
const matching = items.map((i) => scratchBySession.get(i.id)).filter(Boolean);
|
|
3417
|
+
if (!matching.length) continue;
|
|
3418
|
+
html += _renderProjectGroup(
|
|
3419
|
+
escapeHtml(_projectLabel(project)),
|
|
3420
|
+
`${matching.length} scratchpad${matching.length > 1 ? 's' : ''}`,
|
|
3421
|
+
matching.map(renderScratchItem).join(''),
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
if (orphans.length) {
|
|
3426
|
+
const orphanItems = orphans.map((i) => scratchBySession.get(i.id)).filter(Boolean);
|
|
3427
|
+
if (orphanItems.length) {
|
|
3428
|
+
html += _renderOrphanGroup(orphanItems.length, orphanItems.map(renderScratchItem).join(''));
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return html;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
function _storagePreviewScratchpad(key) {
|
|
3435
|
+
closeStorageManager();
|
|
3436
|
+
showScratchpad(key);
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
function _storagePreviewPin(sessionId, pinId) {
|
|
3440
|
+
closeStorageManager();
|
|
3441
|
+
const key = `pinned-messages-${sessionId}`;
|
|
3442
|
+
try {
|
|
3443
|
+
const pins = JSON.parse(localStorage.getItem(key)) || [];
|
|
3444
|
+
const pin = pins.find((p) => p.id === pinId);
|
|
3445
|
+
if (!pin) return;
|
|
3446
|
+
document.getElementById('msg-detail-pin-btn').style.display = 'none';
|
|
3447
|
+
currentMsgDetailIdx = null;
|
|
3448
|
+
currentPinDetailId = null;
|
|
3449
|
+
_renderPinToDetail(pin);
|
|
3450
|
+
document.getElementById('msg-detail-modal').classList.add('visible');
|
|
3451
|
+
} catch (e) {
|
|
3452
|
+
console.error('_storagePreviewPin error:', e);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
function _storageDeleteScratchpad(key) {
|
|
3457
|
+
localStorage.removeItem(key);
|
|
3458
|
+
_renderStorageTab();
|
|
3459
|
+
_updateStorageTotal();
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
function _findOrphanedKeys() {
|
|
3463
|
+
const known = _getKnownSessionIds();
|
|
3464
|
+
if (!known.size) return [];
|
|
3465
|
+
const orphaned = [];
|
|
3466
|
+
for (const id of pinnedSessionIds) if (!known.has(id)) orphaned.push(`__pinned__${id}`);
|
|
3467
|
+
for (const id of stickySessionIds) if (!known.has(id)) orphaned.push(`__sticky__${id}`);
|
|
3468
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3469
|
+
const key = localStorage.key(i);
|
|
3470
|
+
if (key.startsWith('pinned-messages-')) {
|
|
3471
|
+
if (!known.has(key.slice('pinned-messages-'.length))) orphaned.push(key);
|
|
3472
|
+
} else if (key.startsWith('scratchpad-') && !key.startsWith('scratchpad-project:')) {
|
|
3473
|
+
if (!known.has(key.slice('scratchpad-'.length))) orphaned.push(key);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
return orphaned;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
function _updateOrphanedCount() {
|
|
3480
|
+
const btn = document.getElementById('storage-cleanup-btn');
|
|
3481
|
+
if (!btn) return;
|
|
3482
|
+
const count = _findOrphanedKeys().length;
|
|
3483
|
+
btn.textContent = count ? `Clean Orphaned (${count})` : 'Clean Orphaned';
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
3487
|
+
function cleanupOrphanedStorage() {
|
|
3488
|
+
if (!sessions.length) {
|
|
3489
|
+
showToast('Sessions not loaded yet — try again after they appear');
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
const orphaned = _findOrphanedKeys();
|
|
3493
|
+
let pinsChanged = false;
|
|
3494
|
+
for (const key of orphaned) {
|
|
3495
|
+
if (key.startsWith('__pinned__')) {
|
|
3496
|
+
pinnedSessionIds.delete(key.slice('__pinned__'.length));
|
|
3497
|
+
pinsChanged = true;
|
|
3498
|
+
} else if (key.startsWith('__sticky__')) {
|
|
3499
|
+
stickySessionIds.delete(key.slice('__sticky__'.length));
|
|
3500
|
+
pinsChanged = true;
|
|
3501
|
+
} else {
|
|
3502
|
+
localStorage.removeItem(key);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
if (pinsChanged) savePinnedSessions();
|
|
3506
|
+
const removed = orphaned.length;
|
|
3507
|
+
|
|
3508
|
+
showToast(removed ? `Cleaned ${removed} orphaned item${removed > 1 ? 's' : ''}` : 'No orphaned items found');
|
|
3509
|
+
renderSessions();
|
|
3510
|
+
_renderStorageTab();
|
|
3511
|
+
_updateStorageTotal();
|
|
3512
|
+
_updateOrphanedCount();
|
|
3513
|
+
}
|
|
3514
|
+
//#endregion
|
|
3515
|
+
|
|
3083
3516
|
//#region KEYBOARD_SHORTCUTS
|
|
3084
3517
|
function matchKey(e, ...keys) {
|
|
3085
3518
|
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return false;
|
|
@@ -3151,6 +3584,11 @@ document.addEventListener('keydown', (e) => {
|
|
|
3151
3584
|
}
|
|
3152
3585
|
return;
|
|
3153
3586
|
}
|
|
3587
|
+
if (e.code === 'KeyS' && e.shiftKey) {
|
|
3588
|
+
e.preventDefault();
|
|
3589
|
+
showStorageManager();
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3154
3592
|
|
|
3155
3593
|
// Tab toggles focus zone
|
|
3156
3594
|
if (e.key === 'Tab') {
|
|
@@ -4098,7 +4536,7 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
4098
4536
|
} else {
|
|
4099
4537
|
html += `<span style="${plainStyle}" title="${copyVal}">${escapeHtml(value)}</span>`;
|
|
4100
4538
|
}
|
|
4101
|
-
const jsCopyVal = copyVal
|
|
4539
|
+
const jsCopyVal = _escapeForJsAttr(copyVal);
|
|
4102
4540
|
html += `<button onclick="navigator.clipboard.writeText('${jsCopyVal}'); this.textContent='✓'; setTimeout(() => this.textContent='Copy', 1000)" style="padding: 2px 8px; font-size: 11px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; color: var(--text-secondary); cursor: pointer; white-space: nowrap;">Copy</button>`;
|
|
4103
4541
|
});
|
|
4104
4542
|
html += `</div>`;
|
|
@@ -4376,6 +4814,7 @@ searchQuery = urlState.search || '';
|
|
|
4376
4814
|
|
|
4377
4815
|
loadPreferences();
|
|
4378
4816
|
pinnedSessionIds = loadPinnedSessions();
|
|
4817
|
+
stickySessionIds = loadStickySessions();
|
|
4379
4818
|
setupEventSource();
|
|
4380
4819
|
|
|
4381
4820
|
if (urlState.search) {
|
package/public/index.html
CHANGED
|
@@ -161,6 +161,11 @@
|
|
|
161
161
|
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
|
162
162
|
</svg>
|
|
163
163
|
</button>
|
|
164
|
+
<button class="icon-btn" onclick="showStorageManager()" title="Storage manager (Shift+S)" aria-label="Storage manager">
|
|
165
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
166
|
+
<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
|
167
|
+
</svg>
|
|
168
|
+
</button>
|
|
164
169
|
<button id="theme-toggle" class="icon-btn" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
|
|
165
170
|
<svg id="theme-icon-dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
166
171
|
<path d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
|
|
@@ -390,6 +395,10 @@
|
|
|
390
395
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">N</kbd></td>
|
|
391
396
|
<td style="padding: 4px 0; color: var(--text-primary);">Toggle scratchpad</td>
|
|
392
397
|
</tr>
|
|
398
|
+
<tr>
|
|
399
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Shift+S</kbd></td>
|
|
400
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Storage manager</td>
|
|
401
|
+
</tr>
|
|
393
402
|
<tr>
|
|
394
403
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">J/K</kbd></td>
|
|
395
404
|
<td style="padding: 4px 0; color: var(--text-primary);">Navigate messages in detail modal</td>
|
|
@@ -406,7 +415,7 @@
|
|
|
406
415
|
|
|
407
416
|
<!-- Delete Confirmation Modal -->
|
|
408
417
|
<div id="delete-confirm-modal" class="modal-overlay" onclick="closeDeleteConfirmModal()">
|
|
409
|
-
<div class="modal" onclick="event.stopPropagation()"
|
|
418
|
+
<div class="modal modal-sm" onclick="event.stopPropagation()">
|
|
410
419
|
<div class="modal-header">
|
|
411
420
|
<h3 class="modal-title">Delete Task</h3>
|
|
412
421
|
<button class="modal-close" aria-label="Close dialog" onclick="closeDeleteConfirmModal()">
|
|
@@ -427,7 +436,7 @@
|
|
|
427
436
|
|
|
428
437
|
<!-- Delete All Session Tasks Confirmation Modal -->
|
|
429
438
|
<div id="delete-session-tasks-modal" class="modal-overlay" onclick="closeDeleteSessionTasksModal()">
|
|
430
|
-
<div class="modal" onclick="event.stopPropagation()"
|
|
439
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
431
440
|
<div class="modal-header">
|
|
432
441
|
<h3 class="modal-title">Delete All Tasks</h3>
|
|
433
442
|
<button class="modal-close" aria-label="Close dialog" onclick="closeDeleteSessionTasksModal()">
|
|
@@ -449,7 +458,7 @@
|
|
|
449
458
|
|
|
450
459
|
<!-- Delete Result Modal -->
|
|
451
460
|
<div id="delete-result-modal" class="modal-overlay" onclick="closeDeleteResultModal()">
|
|
452
|
-
<div class="modal" onclick="event.stopPropagation()"
|
|
461
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
453
462
|
<div class="modal-header">
|
|
454
463
|
<h3 class="modal-title">Deletion Result</h3>
|
|
455
464
|
<button class="modal-close" aria-label="Close dialog" onclick="closeDeleteResultModal()">
|
|
@@ -517,7 +526,7 @@
|
|
|
517
526
|
|
|
518
527
|
<!-- Blocked Task Warning Modal -->
|
|
519
528
|
<div id="blocked-task-modal" class="modal-overlay" onclick="closeBlockedTaskModal()">
|
|
520
|
-
<div class="modal" onclick="event.stopPropagation()"
|
|
529
|
+
<div class="modal modal-sm" onclick="event.stopPropagation()">
|
|
521
530
|
<div class="modal-header">
|
|
522
531
|
<h3 class="modal-title" style="display: flex; align-items: center; gap: 8px;">
|
|
523
532
|
<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" style="width: 20px; height: 20px;">
|
|
@@ -593,6 +602,29 @@
|
|
|
593
602
|
</div>
|
|
594
603
|
</div>
|
|
595
604
|
|
|
605
|
+
<div id="storage-modal" class="modal-overlay" onclick="closeStorageManager()">
|
|
606
|
+
<div class="modal storage-modal" onclick="event.stopPropagation()">
|
|
607
|
+
<div class="modal-header">
|
|
608
|
+
<h3 class="modal-title">Storage Manager</h3>
|
|
609
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
610
|
+
<span id="storage-total" class="storage-total"></span>
|
|
611
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeStorageManager()">
|
|
612
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
613
|
+
</button>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
<div class="storage-tabs">
|
|
617
|
+
<button class="storage-tab active" data-tab="sessions" onclick="switchStorageTab('sessions')">Sessions</button>
|
|
618
|
+
<button class="storage-tab" data-tab="scratchpads" onclick="switchStorageTab('scratchpads')">Scratchpads</button>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="modal-body" id="storage-modal-body" style="overflow-y:auto;min-height:200px;max-height:60vh;padding-top:16px;padding-right:8px;"></div>
|
|
621
|
+
<div class="modal-footer">
|
|
622
|
+
<button id="storage-cleanup-btn" class="btn btn-secondary" onclick="cleanupOrphanedStorage()">Clean Orphaned</button>
|
|
623
|
+
<button class="btn btn-primary" onclick="closeStorageManager()">Close</button>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
|
|
596
628
|
<div id="toast" class="toast"></div>
|
|
597
629
|
</body>
|
|
598
630
|
</html>
|
package/public/style.css
CHANGED
|
@@ -568,6 +568,11 @@ body::before {
|
|
|
568
568
|
text-overflow: ellipsis;
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
+
.session-item.plan-reveal {
|
|
572
|
+
outline: 1.5px solid var(--plan);
|
|
573
|
+
background: var(--plan-dim);
|
|
574
|
+
}
|
|
575
|
+
|
|
571
576
|
.session-progress {
|
|
572
577
|
display: flex;
|
|
573
578
|
align-items: center;
|
|
@@ -1962,6 +1967,14 @@ body::before {
|
|
|
1962
1967
|
fill: var(--accent);
|
|
1963
1968
|
stroke: var(--accent);
|
|
1964
1969
|
}
|
|
1970
|
+
.session-pin-btn.sticky {
|
|
1971
|
+
opacity: 1;
|
|
1972
|
+
color: var(--warning);
|
|
1973
|
+
}
|
|
1974
|
+
.session-pin-btn.sticky svg {
|
|
1975
|
+
fill: var(--warning);
|
|
1976
|
+
stroke: var(--warning);
|
|
1977
|
+
}
|
|
1965
1978
|
.pinned-sessions-divider {
|
|
1966
1979
|
height: 1px;
|
|
1967
1980
|
margin: 4px 8px;
|
|
@@ -2570,10 +2583,10 @@ body.light .msg-assistant .msg-text {
|
|
|
2570
2583
|
}
|
|
2571
2584
|
|
|
2572
2585
|
.modal.fullscreen {
|
|
2573
|
-
width:
|
|
2574
|
-
max-width:
|
|
2575
|
-
height:
|
|
2576
|
-
max-height:
|
|
2586
|
+
width: 90vw;
|
|
2587
|
+
max-width: 90vw;
|
|
2588
|
+
height: 92vh;
|
|
2589
|
+
max-height: 92vh;
|
|
2577
2590
|
}
|
|
2578
2591
|
|
|
2579
2592
|
.modal.plan-modal {
|
|
@@ -2584,6 +2597,17 @@ body.light .msg-assistant .msg-text {
|
|
|
2584
2597
|
flex-direction: column;
|
|
2585
2598
|
}
|
|
2586
2599
|
|
|
2600
|
+
.modal.plan-modal.fullscreen {
|
|
2601
|
+
width: 90vw;
|
|
2602
|
+
max-width: 90vw;
|
|
2603
|
+
height: 92vh;
|
|
2604
|
+
max-height: 92vh;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
.modal-sm {
|
|
2608
|
+
max-width: 440px;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2587
2611
|
@keyframes live-border-pulse {
|
|
2588
2612
|
0%,
|
|
2589
2613
|
100% {
|
|
@@ -2822,6 +2846,196 @@ select.form-input option:checked {
|
|
|
2822
2846
|
}
|
|
2823
2847
|
/* #endregion */
|
|
2824
2848
|
|
|
2849
|
+
/* #region STORAGE_MANAGER */
|
|
2850
|
+
.storage-modal {
|
|
2851
|
+
width: 90%;
|
|
2852
|
+
max-width: 860px;
|
|
2853
|
+
max-height: 85vh;
|
|
2854
|
+
display: flex;
|
|
2855
|
+
flex-direction: column;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
.storage-project-group {
|
|
2859
|
+
margin-bottom: 12px;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
.storage-project-header {
|
|
2863
|
+
font-size: 12px;
|
|
2864
|
+
font-weight: 600;
|
|
2865
|
+
color: var(--text-secondary);
|
|
2866
|
+
padding: 8px 10px;
|
|
2867
|
+
background: var(--bg-elevated);
|
|
2868
|
+
border-radius: 6px;
|
|
2869
|
+
margin-bottom: 4px;
|
|
2870
|
+
display: flex;
|
|
2871
|
+
justify-content: space-between;
|
|
2872
|
+
align-items: center;
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
.storage-project-header .storage-item-meta {
|
|
2876
|
+
font-weight: 400;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
.storage-session-group {
|
|
2880
|
+
padding-left: 12px;
|
|
2881
|
+
border-left: 2px solid color-mix(in srgb, var(--border) 60%, transparent);
|
|
2882
|
+
margin-left: 8px;
|
|
2883
|
+
margin-bottom: 8px;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
.storage-total {
|
|
2887
|
+
font-size: 11px;
|
|
2888
|
+
color: var(--text-muted);
|
|
2889
|
+
font-family: var(--mono);
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
.storage-tabs {
|
|
2893
|
+
display: flex;
|
|
2894
|
+
gap: 0;
|
|
2895
|
+
border-bottom: 1px solid var(--border);
|
|
2896
|
+
margin: -4px -24px 0;
|
|
2897
|
+
padding: 0 24px;
|
|
2898
|
+
flex-shrink: 0;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
.storage-tab {
|
|
2902
|
+
background: none;
|
|
2903
|
+
border: none;
|
|
2904
|
+
color: var(--text-secondary);
|
|
2905
|
+
padding: 8px 16px;
|
|
2906
|
+
font-size: 13px;
|
|
2907
|
+
font-family: var(--mono);
|
|
2908
|
+
cursor: pointer;
|
|
2909
|
+
border-bottom: 2px solid transparent;
|
|
2910
|
+
transition:
|
|
2911
|
+
color 0.15s,
|
|
2912
|
+
border-color 0.15s;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
.storage-tab:hover {
|
|
2916
|
+
color: var(--text-primary);
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
.storage-tab.active {
|
|
2920
|
+
color: var(--accent);
|
|
2921
|
+
border-bottom-color: var(--accent);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
.storage-item {
|
|
2925
|
+
display: flex;
|
|
2926
|
+
align-items: center;
|
|
2927
|
+
justify-content: space-between;
|
|
2928
|
+
padding: 8px 0;
|
|
2929
|
+
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
|
2930
|
+
gap: 8px;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
.storage-item:last-child {
|
|
2934
|
+
border-bottom: none;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
.storage-item-clickable {
|
|
2938
|
+
cursor: pointer;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
.storage-item-clickable:hover {
|
|
2942
|
+
background: var(--bg-hover);
|
|
2943
|
+
border-radius: 4px;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
.storage-item-id {
|
|
2947
|
+
font-family: var(--mono);
|
|
2948
|
+
font-size: 12px;
|
|
2949
|
+
color: var(--text-secondary);
|
|
2950
|
+
overflow: hidden;
|
|
2951
|
+
text-overflow: ellipsis;
|
|
2952
|
+
white-space: nowrap;
|
|
2953
|
+
flex: 1;
|
|
2954
|
+
min-width: 0;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
.storage-item-meta {
|
|
2958
|
+
font-size: 11px;
|
|
2959
|
+
color: var(--text-muted);
|
|
2960
|
+
flex-shrink: 0;
|
|
2961
|
+
white-space: nowrap;
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
.storage-item-badge {
|
|
2965
|
+
font-size: 10px;
|
|
2966
|
+
padding: 2px 6px;
|
|
2967
|
+
border-radius: 4px;
|
|
2968
|
+
background: var(--bg-hover);
|
|
2969
|
+
color: var(--text-secondary);
|
|
2970
|
+
flex-shrink: 0;
|
|
2971
|
+
font-family: var(--mono);
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
.storage-item-badge.orphan {
|
|
2975
|
+
background: rgba(239, 68, 68, 0.15);
|
|
2976
|
+
color: #ef4444;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
.storage-item-badge.pinned {
|
|
2980
|
+
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
|
2981
|
+
color: var(--accent);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
.storage-item-badge.sticky {
|
|
2985
|
+
background: rgba(234, 179, 8, 0.15);
|
|
2986
|
+
color: #eab308;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
.storage-item-actions {
|
|
2990
|
+
display: flex;
|
|
2991
|
+
gap: 4px;
|
|
2992
|
+
flex-shrink: 0;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
.storage-item-actions button {
|
|
2996
|
+
background: none;
|
|
2997
|
+
border: 1px solid var(--border);
|
|
2998
|
+
border-radius: 4px;
|
|
2999
|
+
color: var(--text-secondary);
|
|
3000
|
+
padding: 2px 8px;
|
|
3001
|
+
font-size: 11px;
|
|
3002
|
+
cursor: pointer;
|
|
3003
|
+
font-family: var(--mono);
|
|
3004
|
+
transition: all 0.15s;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
.storage-item-actions button:hover {
|
|
3008
|
+
background: var(--bg-hover);
|
|
3009
|
+
color: var(--text-primary);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
.storage-item-actions button.danger:hover {
|
|
3013
|
+
background: rgba(239, 68, 68, 0.15);
|
|
3014
|
+
color: #ef4444;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
.storage-group-header {
|
|
3018
|
+
font-size: 12px;
|
|
3019
|
+
color: var(--text-muted);
|
|
3020
|
+
padding: 12px 0 4px;
|
|
3021
|
+
font-weight: 600;
|
|
3022
|
+
display: flex;
|
|
3023
|
+
justify-content: space-between;
|
|
3024
|
+
align-items: center;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
.storage-group-header:first-child {
|
|
3028
|
+
padding-top: 0;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
.storage-empty {
|
|
3032
|
+
text-align: center;
|
|
3033
|
+
padding: 32px 0;
|
|
3034
|
+
color: var(--text-muted);
|
|
3035
|
+
font-size: 13px;
|
|
3036
|
+
}
|
|
3037
|
+
/* #endregion */
|
|
3038
|
+
|
|
2825
3039
|
/* #region A11Y */
|
|
2826
3040
|
.skip-link {
|
|
2827
3041
|
position: absolute;
|