claude-code-kanban 5.0.0 → 5.1.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/package.json +1 -1
- package/public/app.js +74 -20
- package/public/style.css +53 -6
- package/server.js +1 -1
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -446,6 +446,33 @@ function isActiveSession(s) {
|
|
|
446
446
|
function isSessionLive(s) {
|
|
447
447
|
return !!s.hasRecentLog && Date.now() - Date.parse(s.modifiedAt) <= LIVE_INDICATOR_MS;
|
|
448
448
|
}
|
|
449
|
+
function isJustFinished(s) {
|
|
450
|
+
return (
|
|
451
|
+
!isWaitingSession(s) &&
|
|
452
|
+
!isActiveSession(s) &&
|
|
453
|
+
!!s.hasRecentActivity &&
|
|
454
|
+
Date.now() - Date.parse(s.modifiedAt) < JUST_NOW_MS
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// The JUST_NOW_MS mark is a client-side boundary; no server update arrives when a session crosses it.
|
|
459
|
+
let justFinishedTimer = null;
|
|
460
|
+
function scheduleJustFinishedExpiry() {
|
|
461
|
+
clearTimeout(justFinishedTimer);
|
|
462
|
+
let next = Infinity;
|
|
463
|
+
for (const s of sessions) {
|
|
464
|
+
if (isJustFinished(s)) next = Math.min(next, Date.parse(s.modifiedAt) + JUST_NOW_MS - Date.now());
|
|
465
|
+
}
|
|
466
|
+
if (next === Infinity) return;
|
|
467
|
+
justFinishedTimer = setTimeout(() => {
|
|
468
|
+
if (!skipOffScreen()) renderSessionViews();
|
|
469
|
+
}, next + 50);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function renderSessionViews() {
|
|
473
|
+
renderSessions();
|
|
474
|
+
if (isSessionPickerOpen()) renderSessionPicker();
|
|
475
|
+
}
|
|
449
476
|
|
|
450
477
|
const ACTIVITY_PREDICATES = {
|
|
451
478
|
waiting: isWaitingSession,
|
|
@@ -619,6 +646,7 @@ async function fetchTasks(sessionId) {
|
|
|
619
646
|
const WAITING_TTL_MS = 30 * 60 * 1000;
|
|
620
647
|
const AGENT_LOG_MAX = 8;
|
|
621
648
|
const LIVE_INDICATOR_MS = 10 * 1000;
|
|
649
|
+
const JUST_NOW_MS = 60 * 1000;
|
|
622
650
|
// #endregion
|
|
623
651
|
|
|
624
652
|
function resetAgentState() {
|
|
@@ -1698,6 +1726,7 @@ const ICON_AGENT_ACTIVE =
|
|
|
1698
1726
|
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="2"/><path d="M12 7v4"/><line x1="8" y1="16" x2="8" y2="16"/><line x1="16" y1="16" x2="16" y2="16"/></svg>';
|
|
1699
1727
|
const ICON_TERMINAL =
|
|
1700
1728
|
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
|
|
1729
|
+
const TERMINAL_TITLE = 'Running in a terminal here';
|
|
1701
1730
|
const ICON_CHAT =
|
|
1702
1731
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
|
1703
1732
|
const TOOL_ICONS = {
|
|
@@ -3356,7 +3385,12 @@ function getFilteredSessions() {
|
|
|
3356
3385
|
// The open session keeps its row even when it belongs to another project, so that
|
|
3357
3386
|
// revealing one by id has something to scroll to. Same exemption the active branch
|
|
3358
3387
|
// above already makes, for the same reason.
|
|
3359
|
-
filteredSessions = filteredSessions.filter(
|
|
3388
|
+
filteredSessions = filteredSessions.filter(
|
|
3389
|
+
(s) =>
|
|
3390
|
+
matchesProjectFilter(s.project) ||
|
|
3391
|
+
s.id === currentSessionId ||
|
|
3392
|
+
(filterProject === '__recent__' && isAnyPinned(s.id)),
|
|
3393
|
+
);
|
|
3360
3394
|
}
|
|
3361
3395
|
|
|
3362
3396
|
if (activityFilter.size > 0) {
|
|
@@ -3425,6 +3459,7 @@ function renderSessions() {
|
|
|
3425
3459
|
// Rebuilding the list under the pointer cancels an in-flight drop and would swallow a
|
|
3426
3460
|
// half-typed group name, and the SSE path can fire at any moment — defer instead.
|
|
3427
3461
|
if (sgDrag || sgIsEditing() || zenPanelIsEditing()) return;
|
|
3462
|
+
scheduleJustFinishedExpiry();
|
|
3428
3463
|
refreshProjectList();
|
|
3429
3464
|
|
|
3430
3465
|
// Zen narrows the rendered list only — the session picker keeps calling getFilteredSessions()
|
|
@@ -3493,15 +3528,19 @@ function renderSessions() {
|
|
|
3493
3528
|
const linkedDocsCount = getSessionPreviewPaths(session.id).length;
|
|
3494
3529
|
const bookmarksCount = loadPins(session.id).length;
|
|
3495
3530
|
const hasScratchpad = _hasScratchpad(_sessionScratchpadKey(session.id));
|
|
3496
|
-
const tempClass =
|
|
3531
|
+
const tempClass =
|
|
3532
|
+
session.hasRecentLog || session.hasRecentActivity || session.inProgress || session.hasWaitingForUser
|
|
3533
|
+
? 'warm'
|
|
3534
|
+
: 'stale';
|
|
3497
3535
|
// Open tasks keep an idle session warm, but the green dot is for a session that is working.
|
|
3498
3536
|
const idleClass =
|
|
3499
3537
|
tempClass === 'warm' && !session.hasWaitingForUser && !session.hasRecentLog && !session.hasRunningAgents
|
|
3500
3538
|
? 'idle'
|
|
3501
3539
|
: '';
|
|
3540
|
+
const justFinishedClass = isJustFinished(session) ? 'just-finished' : '';
|
|
3502
3541
|
const sid = escAttrJs(session.id);
|
|
3503
3542
|
return `
|
|
3504
|
-
<button onclick="openSession('${sid}')" draggable="true" data-session-id="${escapeHtml(session.id)}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${session.unread ? 'unread' : ''} ${tempClass} ${idleClass} ${showCtx ? 'has-context' : ''}" title="${escapeHtml(tooltip)}">
|
|
3543
|
+
<button onclick="openSession('${sid}')" draggable="true" data-session-id="${escapeHtml(session.id)}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${session.unread ? 'unread' : ''} ${tempClass} ${idleClass} ${justFinishedClass} ${showCtx ? 'has-context' : ''}" title="${escapeHtml(tooltip)}">
|
|
3505
3544
|
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${sid}')" title="${pinTitle} session">${pinState === 'sticky' ? SESSION_STAR_SVG : SESSION_PIN_SVG}</span>
|
|
3506
3545
|
<div class="session-name">${escapeHtml(sessionName)}</div>
|
|
3507
3546
|
${projectHtml ? `<div class="session-secondary">${projectHtml}</div>` : ''}
|
|
@@ -3519,7 +3558,7 @@ function renderSessions() {
|
|
|
3519
3558
|
${session.hasPlan && !session.planSourceSessionId ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${sid}')" title="View plan">${ICON_PLAN}</span>` : ''}
|
|
3520
3559
|
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escAttrJs(session.planSourceSessionId)}')">${ICON_PLAN}</span>` : ''}
|
|
3521
3560
|
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
|
|
3522
|
-
${runningTerminals.has(session.id) ? `<span class="terminal-badge" onclick="event.stopPropagation(); showSessionTerminal('${escAttrJs(session.id)}')" title="
|
|
3561
|
+
${runningTerminals.has(session.id) ? `<span class="terminal-badge" onclick="event.stopPropagation(); showSessionTerminal('${escAttrJs(session.id)}')" title="${TERMINAL_TITLE}">${ICON_TERMINAL}</span>` : ''}
|
|
3523
3562
|
${session.hasWaitingForUser ? `<span class="agent-badge agent-badge-waiting" title="Waiting for user">${ICON_AGENT_WAITING}</span>` : ''}
|
|
3524
3563
|
${session.hasRunningAgents && !session.hasWaitingForUser ? `<span class="agent-badge agent-badge-active" title="Agents running">${ICON_AGENT_ACTIVE}</span>` : ''}
|
|
3525
3564
|
${isLive || session.hasRunningAgents ? `<span class="pulse" title="${isLive ? 'Live' : 'Active agents'}"></span>` : ''}
|
|
@@ -4451,9 +4490,9 @@ function sgHeaderHtml(group, countHtml) {
|
|
|
4451
4490
|
const actions = group.transient
|
|
4452
4491
|
? `<span class="sg-action sg-keep" title="Keep this group after its sessions end">Keep</span>
|
|
4453
4492
|
<span class="sg-action sg-delete" title="Delete group (sessions return to Projects)">×</span>`
|
|
4454
|
-
:
|
|
4455
|
-
<span class="sg-action sg-
|
|
4456
|
-
|
|
4493
|
+
: `<span class="sg-action sg-rename" title="Rename group"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></span>
|
|
4494
|
+
<span class="sg-action sg-delete" title="Delete group (members return to Projects)">×</span>
|
|
4495
|
+
${headerPadBtnHtml(_groupScratchpadKey(group.id), group.name)}`;
|
|
4457
4496
|
return `
|
|
4458
4497
|
<div ${attrs} data-group-path="${escapeHtml(sgKey(group.id))}">
|
|
4459
4498
|
${groupChevronSvg()}
|
|
@@ -8283,7 +8322,7 @@ function formatDate(dateStr) {
|
|
|
8283
8322
|
const now = new Date();
|
|
8284
8323
|
const diff = now - date;
|
|
8285
8324
|
|
|
8286
|
-
if (diff <
|
|
8325
|
+
if (diff < JUST_NOW_MS) return 'just now';
|
|
8287
8326
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
8288
8327
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
8289
8328
|
return date.toLocaleDateString();
|
|
@@ -9704,8 +9743,12 @@ function closeSessionPicker({ refocus = true } = {}) {
|
|
|
9704
9743
|
if (refocus && spFromTerminal && wantsTerminal()) focusTerminalPane();
|
|
9705
9744
|
}
|
|
9706
9745
|
|
|
9746
|
+
function isSessionPickerOpen() {
|
|
9747
|
+
return document.getElementById('session-picker-modal').classList.contains('visible');
|
|
9748
|
+
}
|
|
9749
|
+
|
|
9707
9750
|
function toggleSessionPicker() {
|
|
9708
|
-
if (
|
|
9751
|
+
if (isSessionPickerOpen()) closeSessionPicker();
|
|
9709
9752
|
else openSessionPicker();
|
|
9710
9753
|
}
|
|
9711
9754
|
|
|
@@ -9772,24 +9815,35 @@ function spMatches(session, query) {
|
|
|
9772
9815
|
}
|
|
9773
9816
|
|
|
9774
9817
|
// Spelled out rather than interpolated so the class never comes from a variable.
|
|
9775
|
-
const
|
|
9776
|
-
waiting: '<span class="
|
|
9777
|
-
live: '<span class="
|
|
9778
|
-
active: '<span class="
|
|
9779
|
-
|
|
9818
|
+
const SP_STATES = {
|
|
9819
|
+
waiting: '<span class="sp-state waiting">',
|
|
9820
|
+
live: '<span class="sp-state live">',
|
|
9821
|
+
active: '<span class="sp-state active">',
|
|
9822
|
+
justFinished: '<span class="sp-state just-finished">',
|
|
9823
|
+
recent: '<span class="sp-state recent">',
|
|
9824
|
+
idle: '<span class="sp-state idle">',
|
|
9780
9825
|
};
|
|
9781
9826
|
|
|
9827
|
+
const SP_DOT = '<span class="activity-dot"></span>';
|
|
9828
|
+
const SP_TERMINAL = `<span class="sp-terminal" title="${TERMINAL_TITLE}">${ICON_TERMINAL}</span>`;
|
|
9829
|
+
|
|
9830
|
+
function spState(session) {
|
|
9831
|
+
if (isWaitingSession(session)) return 'waiting';
|
|
9832
|
+
if (isSessionLive(session)) return 'live';
|
|
9833
|
+
if (isActiveSession(session)) return 'active';
|
|
9834
|
+
if (isJustFinished(session)) return 'justFinished';
|
|
9835
|
+
return session.hasRecentActivity || session.inProgress > 0 ? 'recent' : 'idle';
|
|
9836
|
+
}
|
|
9837
|
+
|
|
9782
9838
|
function spDotHtml(session) {
|
|
9783
|
-
|
|
9784
|
-
if (isSessionLive(session)) return SP_DOTS.live;
|
|
9785
|
-
return isActiveSession(session) ? SP_DOTS.active : SP_DOTS.idle;
|
|
9839
|
+
return `${SP_STATES[spState(session)]}${runningTerminals.has(session.id) ? SP_TERMINAL : SP_DOT}</span>`;
|
|
9786
9840
|
}
|
|
9787
9841
|
|
|
9788
9842
|
function spPinHtml(session) {
|
|
9789
9843
|
const state = getSessionPinState(session.id);
|
|
9790
9844
|
if (state === 'sticky') return `<span class="sp-pin sticky" title="Sticky">${SESSION_STAR_SVG}</span>`;
|
|
9791
9845
|
if (state === 'pinned') return `<span class="sp-pin pinned" title="Pinned">${SESSION_PIN_SVG}</span>`;
|
|
9792
|
-
return '';
|
|
9846
|
+
return '<span class="sp-pin"></span>';
|
|
9793
9847
|
}
|
|
9794
9848
|
|
|
9795
9849
|
function renderSessionPicker() {
|
|
@@ -9821,11 +9875,11 @@ function renderSessionPicker() {
|
|
|
9821
9875
|
return `<button class="sp-row${s.id === currentSessionId ? ' current' : ''}${outside ? ' outside' : ''}" data-idx="${i}" title="${escapeHtml(s.project ? `${s.id}\n${s.project}` : s.id)}">
|
|
9822
9876
|
${spDotHtml(s)}
|
|
9823
9877
|
<span class="sp-name">${escapeHtml(sessionDisplayName(s))}</span>
|
|
9824
|
-
${spPinHtml(s)}
|
|
9825
9878
|
<span class="sp-project">${escapeHtml(project)}</span>
|
|
9826
9879
|
${s.gitBranch ? `<span class="sp-branch">${escapeHtml(s.gitBranch)}</span>` : ''}
|
|
9827
9880
|
<span class="sp-count">${s.completed}/${s.taskCount}</span>
|
|
9828
9881
|
<span class="sp-time">${formatDate(s.modifiedAt)}</span>
|
|
9882
|
+
${spPinHtml(s)}
|
|
9829
9883
|
</button>`;
|
|
9830
9884
|
})
|
|
9831
9885
|
.join('');
|
|
@@ -11619,7 +11673,7 @@ async function loadTerminals() {
|
|
|
11619
11673
|
function setRunningTerminals(ids) {
|
|
11620
11674
|
runningTerminals = new Set(ids);
|
|
11621
11675
|
if (ids.some((id) => !sessions.some((s) => s.id === id))) fetchSessions(false).catch(() => {});
|
|
11622
|
-
else
|
|
11676
|
+
else renderSessionViews();
|
|
11623
11677
|
renderActivityChip();
|
|
11624
11678
|
}
|
|
11625
11679
|
|
package/public/style.css
CHANGED
|
@@ -969,6 +969,20 @@ body.booting .boot-splash {
|
|
|
969
969
|
background: var(--text-tertiary);
|
|
970
970
|
}
|
|
971
971
|
|
|
972
|
+
/* Declared on the element, not :root, so it picks up body.light's --success. */
|
|
973
|
+
.session-item.just-finished,
|
|
974
|
+
.sp-state.just-finished {
|
|
975
|
+
--success-faded: color-mix(in srgb, var(--success) 35%, var(--text-muted));
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.session-item.warm.idle.just-finished .session-time {
|
|
979
|
+
color: var(--success-faded);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.session-item.warm.idle.just-finished .session-time::before {
|
|
983
|
+
background: var(--success-faded);
|
|
984
|
+
}
|
|
985
|
+
|
|
972
986
|
.session-item.warm.permission-pending .session-time {
|
|
973
987
|
color: var(--warning);
|
|
974
988
|
}
|
|
@@ -6104,16 +6118,46 @@ select.form-input option:checked {
|
|
|
6104
6118
|
border-color: var(--accent);
|
|
6105
6119
|
}
|
|
6106
6120
|
|
|
6107
|
-
.sp-
|
|
6108
|
-
|
|
6121
|
+
.sp-state {
|
|
6122
|
+
flex: none;
|
|
6123
|
+
width: 14px;
|
|
6124
|
+
display: flex;
|
|
6125
|
+
align-items: center;
|
|
6126
|
+
justify-content: center;
|
|
6127
|
+
line-height: 0;
|
|
6109
6128
|
}
|
|
6110
6129
|
|
|
6111
|
-
.sp-
|
|
6112
|
-
|
|
6130
|
+
.sp-state.live {
|
|
6131
|
+
color: var(--success);
|
|
6113
6132
|
}
|
|
6114
6133
|
|
|
6115
|
-
.sp-
|
|
6116
|
-
|
|
6134
|
+
.sp-state.active {
|
|
6135
|
+
color: var(--accent);
|
|
6136
|
+
}
|
|
6137
|
+
|
|
6138
|
+
.sp-state.waiting {
|
|
6139
|
+
color: var(--warning);
|
|
6140
|
+
}
|
|
6141
|
+
|
|
6142
|
+
.sp-state.just-finished {
|
|
6143
|
+
color: var(--success-faded);
|
|
6144
|
+
}
|
|
6145
|
+
|
|
6146
|
+
.sp-state.recent {
|
|
6147
|
+
color: var(--text-tertiary);
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
.sp-state.idle {
|
|
6151
|
+
color: var(--text-muted);
|
|
6152
|
+
}
|
|
6153
|
+
|
|
6154
|
+
.sp-state .activity-dot {
|
|
6155
|
+
background: currentColor;
|
|
6156
|
+
}
|
|
6157
|
+
|
|
6158
|
+
.sp-state.idle .activity-dot {
|
|
6159
|
+
background: transparent;
|
|
6160
|
+
border: 1px solid currentColor;
|
|
6117
6161
|
}
|
|
6118
6162
|
|
|
6119
6163
|
.sp-name,
|
|
@@ -6132,6 +6176,9 @@ select.form-input option:checked {
|
|
|
6132
6176
|
|
|
6133
6177
|
.sp-pin {
|
|
6134
6178
|
flex: none;
|
|
6179
|
+
width: 14px;
|
|
6180
|
+
display: flex;
|
|
6181
|
+
justify-content: center;
|
|
6135
6182
|
line-height: 0;
|
|
6136
6183
|
}
|
|
6137
6184
|
|
package/server.js
CHANGED
|
@@ -1580,7 +1580,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
1580
1580
|
if (recentHours > 0) {
|
|
1581
1581
|
const cutoff = Date.now() - recentHours * 3600 * 1000;
|
|
1582
1582
|
const activity = projectActivity();
|
|
1583
|
-
sessions = sessions.filter(s => (activity.get(s.project) || 0) > cutoff ||
|
|
1583
|
+
sessions = sessions.filter(s => (activity.get(s.project) || 0) > cutoff || pinnedIds.has(s.id));
|
|
1584
1584
|
}
|
|
1585
1585
|
|
|
1586
1586
|
const paged = limit !== null && limit > 0;
|