claude-code-kanban 4.23.2 → 4.24.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 +1 -0
- package/package.json +1 -1
- package/public/app.js +359 -83
- package/public/index.html +10 -131
- package/public/style.css +251 -4
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ Tasks, agents, and messages appear on the board automatically — Claude Code wr
|
|
|
53
53
|
- **Agent teams** — Color-coded team members, owner filtering, member count badges
|
|
54
54
|
- **17 color themes** — Dracula, Nord, Catppuccin, Gruvbox, Tokyo Night, and more — each in light and dark
|
|
55
55
|
- **Storage manager** — Inspect disk usage and clean up stale sessions and tasks
|
|
56
|
+
- **Session picker** — Jump to any session in the sidebar with `Shift+P`, filtering by name, project or branch
|
|
56
57
|
- **Keyboard-first** — Press `?` for the full shortcut reference
|
|
57
58
|
|
|
58
59
|

|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -15,6 +15,9 @@ let filterProject = FILTER_DEFAULTS.project; // null = all, '__recent__' = last
|
|
|
15
15
|
let recentProjects = new Set();
|
|
16
16
|
let projectsCacheDirty = true;
|
|
17
17
|
const collapsedProjectGroups = new Set();
|
|
18
|
+
const SECTION_GROUPS = '__section_groups__';
|
|
19
|
+
const SECTION_PROJECTS = '__section_projects__';
|
|
20
|
+
const SECTION_SESSIONS = '__section_sessions__';
|
|
18
21
|
let stableGroupOrder = []; // cached project path order to prevent jumping
|
|
19
22
|
let sessionGroups = []; // user-named groups: [{id, name, color, members:[{type,ref}]}]
|
|
20
23
|
let sgDrag = null; // in-flight sidebar drag: {kind:'session'|'project'|'group', ref}
|
|
@@ -280,6 +283,11 @@ function isWaitingSession(s) {
|
|
|
280
283
|
function isActiveSession(s) {
|
|
281
284
|
return !s.hasWaitingForUser && (s.inProgress > 0 || s.hasRecentLog || s.hasRunningAgents);
|
|
282
285
|
}
|
|
286
|
+
// hasRecentLog carries the server's registry-idle correction: an open-but-idle
|
|
287
|
+
// terminal keeps touching its JSONL, so mtime alone is not proof of activity.
|
|
288
|
+
function isSessionLive(s) {
|
|
289
|
+
return !!s.hasRecentLog && Date.now() - Date.parse(s.modifiedAt) <= LIVE_INDICATOR_MS;
|
|
290
|
+
}
|
|
283
291
|
|
|
284
292
|
const ACTIVITY_PREDICATES = {
|
|
285
293
|
waiting: isWaitingSession,
|
|
@@ -2951,6 +2959,19 @@ function highlightSelectedAgent() {
|
|
|
2951
2959
|
//#region RENDERING
|
|
2952
2960
|
let revealedPlanSessionId = null;
|
|
2953
2961
|
let revealedStorageSessionId = null;
|
|
2962
|
+
|
|
2963
|
+
// Opens a session and scrolls the sidebar to it, refetching first when the id
|
|
2964
|
+
// fell outside the session list the current filters asked for.
|
|
2965
|
+
async function revealSession(id) {
|
|
2966
|
+
if (!sessions.some((s) => s.id === id)) {
|
|
2967
|
+
lastSessionsHash = '';
|
|
2968
|
+
await fetchSessions();
|
|
2969
|
+
}
|
|
2970
|
+
await fetchTasks(id);
|
|
2971
|
+
const el = document.querySelector(`.session-item[data-session-id="${escSel(id)}"]`);
|
|
2972
|
+
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2954
2975
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
2955
2976
|
async function revealPlanSession(planSessionId) {
|
|
2956
2977
|
if (revealedPlanSessionId === planSessionId) {
|
|
@@ -2959,13 +2980,7 @@ async function revealPlanSession(planSessionId) {
|
|
|
2959
2980
|
return;
|
|
2960
2981
|
}
|
|
2961
2982
|
revealedPlanSessionId = planSessionId;
|
|
2962
|
-
|
|
2963
|
-
lastSessionsHash = '';
|
|
2964
|
-
await fetchSessions();
|
|
2965
|
-
}
|
|
2966
|
-
await fetchTasks(planSessionId);
|
|
2967
|
-
const el = document.querySelector(`.session-item[data-session-id="${CSS.escape(planSessionId)}"]`);
|
|
2968
|
-
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
2983
|
+
await revealSession(planSessionId);
|
|
2969
2984
|
}
|
|
2970
2985
|
|
|
2971
2986
|
async function showAllTasks() {
|
|
@@ -3017,15 +3032,10 @@ function renderAllTasks() {
|
|
|
3017
3032
|
renderKanban();
|
|
3018
3033
|
}
|
|
3019
3034
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
// Update project dropdown
|
|
3025
|
-
updateProjectDropdown();
|
|
3026
|
-
|
|
3027
|
-
// Filter pipeline: active filter → force-include revealed/current (non-pinned) sessions →
|
|
3028
|
-
// project filter → search filter → ensure pinned/sticky sessions are always included
|
|
3035
|
+
// Filter pipeline: active filter → force-include revealed/current (non-pinned) sessions →
|
|
3036
|
+
// project filter → search filter → ensure pinned/sticky sessions are always included.
|
|
3037
|
+
// Pure over module state, so the session picker can ask for the same list without a render.
|
|
3038
|
+
function getFilteredSessions() {
|
|
3029
3039
|
let filteredSessions = sessions;
|
|
3030
3040
|
if (sessionFilter === 'active') {
|
|
3031
3041
|
const activeSessionIds = new Set();
|
|
@@ -3109,6 +3119,18 @@ function renderSessions() {
|
|
|
3109
3119
|
if (missingPinned.length) filteredSessions = [...missingPinned, ...filteredSessions];
|
|
3110
3120
|
}
|
|
3111
3121
|
|
|
3122
|
+
return filteredSessions;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
function renderSessions() {
|
|
3126
|
+
// Rebuilding the list under the pointer cancels an in-flight drop and would swallow a
|
|
3127
|
+
// half-typed group name, and the SSE path can fire at any moment — defer instead.
|
|
3128
|
+
if (sgDrag || sgIsEditing()) return;
|
|
3129
|
+
// Update project dropdown
|
|
3130
|
+
updateProjectDropdown();
|
|
3131
|
+
|
|
3132
|
+
const filteredSessions = getFilteredSessions();
|
|
3133
|
+
|
|
3112
3134
|
if (filteredSessions.length === 0) {
|
|
3113
3135
|
let emptyMsg = 'No sessions found';
|
|
3114
3136
|
let emptyHint = 'Tasks appear when you use Claude Code';
|
|
@@ -3140,13 +3162,8 @@ function renderSessions() {
|
|
|
3140
3162
|
const total = session.taskCount;
|
|
3141
3163
|
const percent = total > 0 ? Math.round((session.completed / total) * 100) : 0;
|
|
3142
3164
|
const isActive = session.id === currentSessionId && viewMode === 'session';
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
const isLive = session.hasRecentLog && Date.now() - Date.parse(session.modifiedAt) <= LIVE_INDICATOR_MS;
|
|
3146
|
-
const rawName = session.name || session.id;
|
|
3147
|
-
const sessionName = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawName)
|
|
3148
|
-
? rawName.slice(0, 8)
|
|
3149
|
-
: rawName;
|
|
3165
|
+
const isLive = isSessionLive(session);
|
|
3166
|
+
const sessionName = sessionDisplayName(session);
|
|
3150
3167
|
const useGrouped = sessionFilter === 'active' && session.project;
|
|
3151
3168
|
const primaryName = useGrouped ? sessionName : session.project ? session.project.split('/').pop() : sessionName;
|
|
3152
3169
|
const secondaryName = useGrouped ? null : session.project ? sessionName : null;
|
|
@@ -3212,6 +3229,15 @@ function renderSessions() {
|
|
|
3212
3229
|
const active = arr.reduce((n, s) => n + (isSessionActive(s) ? 1 : 0), 0);
|
|
3213
3230
|
return `<span class="group-count" title="${active} active / ${arr.length} total">${active > 0 ? `<span class="group-count-active">${active}</span><span class="group-count-sep">/</span>` : ''}${arr.length}</span>`;
|
|
3214
3231
|
};
|
|
3232
|
+
// `ungroup` marks the label as the drop target for pulling an item out of a named group.
|
|
3233
|
+
const sectionHtml = (key, text, ungroup, body) => {
|
|
3234
|
+
const collapsed = collapsedProjectGroups.has(key);
|
|
3235
|
+
const escPath = escapeHtml(key);
|
|
3236
|
+
const title = ungroup ? 'Click to collapse — drop here to remove from a group' : 'Click to collapse';
|
|
3237
|
+
return `<div class="sg-section-label sg-section-toggle${ungroup ? ' sg-ungroup-zone' : ''}${collapsed ? ' collapsed' : ''}" data-group-path="${escPath}" title="${escapeHtml(title)}">${groupChevronSvg(10)}<span>${text}</span></div>
|
|
3238
|
+
<div class="sg-section-body${collapsed ? ' collapsed' : ''}" data-group-path="${escPath}">${body}</div>`;
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3215
3241
|
const renderGroupSessions = (sessions, pinKey) => {
|
|
3216
3242
|
if (!groupPinned || pinnedSessionIds.size === 0) return sessions.map(renderSessionCard).join('');
|
|
3217
3243
|
const gPinned = sessions.filter((s) => isPlacedPinned(s.id) && !isPlacedSticky(s.id));
|
|
@@ -3298,7 +3324,7 @@ function renderSessions() {
|
|
|
3298
3324
|
// flat = the "All sessions" view, which has no project sub-blocks.
|
|
3299
3325
|
const sgSectionHtml = (flat) => {
|
|
3300
3326
|
if (sessionGroups.length === 0) return '';
|
|
3301
|
-
let out = '
|
|
3327
|
+
let out = '';
|
|
3302
3328
|
for (const group of sessionGroups) {
|
|
3303
3329
|
const bucket = sgBuckets.get(group.id) || [];
|
|
3304
3330
|
if (bucket.length === 0 && sgFiltering) continue;
|
|
@@ -3341,7 +3367,8 @@ function renderSessions() {
|
|
|
3341
3367
|
out += sgHeaderHtml(group, countHtml(bucket));
|
|
3342
3368
|
out += `<div class="session-group-sessions${collapsed ? ' collapsed' : ''}" data-group-id="${escapeHtml(group.id)}">${body}</div>`;
|
|
3343
3369
|
}
|
|
3344
|
-
return
|
|
3370
|
+
if (!out) return '';
|
|
3371
|
+
return sectionHtml(SECTION_GROUPS, 'Groups', false, out);
|
|
3345
3372
|
};
|
|
3346
3373
|
|
|
3347
3374
|
let html = '';
|
|
@@ -3389,17 +3416,19 @@ function renderSessions() {
|
|
|
3389
3416
|
stableGroupOrder = [...keptOrder, ...newPaths];
|
|
3390
3417
|
const sortedGroups = stableGroupOrder.filter((p) => groups.has(p)).map((p) => [p, groups.get(p)]);
|
|
3391
3418
|
|
|
3392
|
-
//
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3419
|
+
// The section header is rendered even when nothing is left under it: it is the drop target
|
|
3420
|
+
// for ungrouping. Its body is skipped while collapsed — nothing would be visible, and this is
|
|
3421
|
+
// the whole project list, re-parsed on every live refresh.
|
|
3422
|
+
const sectioned = sessionGroups.length > 0;
|
|
3423
|
+
let projectsHtml = '';
|
|
3424
|
+
if (!sectioned || !collapsedProjectGroups.has(SECTION_PROJECTS)) {
|
|
3425
|
+
for (const [projectPath, projectSessions] of sortedGroups) {
|
|
3426
|
+
projectsHtml += projectBlock(projectPath, projectSessions, false);
|
|
3427
|
+
}
|
|
3399
3428
|
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3429
|
+
if (ungrouped.length > 0 && sortedGroups.length > 0) {
|
|
3430
|
+
const isCollapsed = collapsedProjectGroups.has('__ungrouped__');
|
|
3431
|
+
projectsHtml += `
|
|
3403
3432
|
<div class="project-group-header${isCollapsed ? ' collapsed' : ''}" data-group-path="__ungrouped__">
|
|
3404
3433
|
${groupChevronSvg()}
|
|
3405
3434
|
<span class="group-name">Ungrouped</span>
|
|
@@ -3409,17 +3438,20 @@ function renderSessions() {
|
|
|
3409
3438
|
${renderGroupSessions(ungrouped, '__pinned___ungrouped__')}
|
|
3410
3439
|
</div>
|
|
3411
3440
|
`;
|
|
3412
|
-
|
|
3413
|
-
|
|
3441
|
+
} else {
|
|
3442
|
+
projectsHtml += ungrouped.map(renderSessionCard).join('');
|
|
3443
|
+
}
|
|
3414
3444
|
}
|
|
3415
3445
|
|
|
3446
|
+
html += sectioned ? sectionHtml(SECTION_PROJECTS, 'Projects', true, projectsHtml) : projectsHtml;
|
|
3447
|
+
|
|
3416
3448
|
sessionsList.innerHTML = html;
|
|
3417
3449
|
} else {
|
|
3418
|
-
const
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
sessionsList.innerHTML = html + sgSectionHtml(true) +
|
|
3450
|
+
const sectioned = sessionGroups.length > 0;
|
|
3451
|
+
const restHtml =
|
|
3452
|
+
sectioned && collapsedProjectGroups.has(SECTION_SESSIONS) ? '' : sgRest.map(renderSessionCard).join('');
|
|
3453
|
+
const tail = sectioned ? sectionHtml(SECTION_SESSIONS, 'Sessions', true, restHtml) : restHtml;
|
|
3454
|
+
sessionsList.innerHTML = html + sgSectionHtml(true) + tail;
|
|
3423
3455
|
}
|
|
3424
3456
|
|
|
3425
3457
|
const navItems = getNavigableItems();
|
|
@@ -4414,14 +4446,24 @@ function getKbId(el) {
|
|
|
4414
4446
|
return el.dataset.sessionId || el.dataset.groupPath || null;
|
|
4415
4447
|
}
|
|
4416
4448
|
|
|
4449
|
+
// Every collapsible header class and the body class it opens. Order matters only in that a
|
|
4450
|
+
// header carries exactly one of these.
|
|
4451
|
+
const COLLAPSIBLE_BODY_CLASS = {
|
|
4452
|
+
'pinned-sub-header': 'pinned-sub-items',
|
|
4453
|
+
'session-group-header': 'session-group-sessions',
|
|
4454
|
+
'sg-section-toggle': 'sg-section-body',
|
|
4455
|
+
'project-group-header': 'project-group-sessions',
|
|
4456
|
+
};
|
|
4457
|
+
const COLLAPSIBLE_HEADER_SELECTOR = Object.keys(COLLAPSIBLE_BODY_CLASS)
|
|
4458
|
+
.map((c) => `.${c}`)
|
|
4459
|
+
.join(', ');
|
|
4460
|
+
|
|
4417
4461
|
function getGroupSessionsContainer(header) {
|
|
4418
|
-
const cls = header.classList.contains(
|
|
4419
|
-
|
|
4420
|
-
: header.classList.contains('session-group-header')
|
|
4421
|
-
? 'session-group-sessions'
|
|
4422
|
-
: 'project-group-sessions';
|
|
4462
|
+
const cls = Object.keys(COLLAPSIBLE_BODY_CLASS).find((c) => header.classList.contains(c));
|
|
4463
|
+
if (!cls) return null;
|
|
4423
4464
|
let el = header.nextElementSibling;
|
|
4424
|
-
|
|
4465
|
+
const bodyCls = COLLAPSIBLE_BODY_CLASS[cls];
|
|
4466
|
+
while (el && !el.classList.contains(bodyCls)) el = el.nextElementSibling;
|
|
4425
4467
|
return el;
|
|
4426
4468
|
}
|
|
4427
4469
|
|
|
@@ -4448,16 +4490,27 @@ function getNavigableItems() {
|
|
|
4448
4490
|
}
|
|
4449
4491
|
}
|
|
4450
4492
|
};
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4493
|
+
// A section label sits next to its body div rather than wrapping it, so the top level walks
|
|
4494
|
+
// into the body to reach the blocks inside.
|
|
4495
|
+
const walkTopLevel = (children) => {
|
|
4496
|
+
for (const el of children) {
|
|
4497
|
+
if (
|
|
4498
|
+
el.classList.contains('project-group-header') ||
|
|
4499
|
+
el.classList.contains('session-group-header') ||
|
|
4500
|
+
el.classList.contains('sg-section-toggle')
|
|
4501
|
+
) {
|
|
4502
|
+
items.push(el);
|
|
4503
|
+
if (!el.classList.contains('sg-section-toggle') && !collapsedProjectGroups.has(el.dataset.groupPath)) {
|
|
4504
|
+
walkGroupContainer(getGroupSessionsContainer(el));
|
|
4505
|
+
}
|
|
4506
|
+
} else if (el.classList.contains('session-item')) {
|
|
4507
|
+
items.push(el);
|
|
4508
|
+
} else if (el.classList.contains('sg-section-body') && !el.classList.contains('collapsed')) {
|
|
4509
|
+
walkTopLevel(el.children);
|
|
4456
4510
|
}
|
|
4457
|
-
} else if (el.classList.contains('session-item')) {
|
|
4458
|
-
items.push(el);
|
|
4459
4511
|
}
|
|
4460
|
-
}
|
|
4512
|
+
};
|
|
4513
|
+
walkTopLevel(sessionsList.children);
|
|
4461
4514
|
return items;
|
|
4462
4515
|
}
|
|
4463
4516
|
|
|
@@ -4541,6 +4594,10 @@ function expandActiveGroups({ onlyNew = false } = {}) {
|
|
|
4541
4594
|
// A named group wrapping that project (or the session itself) would keep it hidden.
|
|
4542
4595
|
const group = sgGroupForSession(s);
|
|
4543
4596
|
if (group && collapsedProjectGroups.delete(sgKey(group.id))) changed = true;
|
|
4597
|
+
// Which section holds it depends on the view, so open every one rather than re-deriving it.
|
|
4598
|
+
for (const key of [SECTION_GROUPS, SECTION_PROJECTS, SECTION_SESSIONS]) {
|
|
4599
|
+
if (collapsedProjectGroups.delete(key)) changed = true;
|
|
4600
|
+
}
|
|
4544
4601
|
}
|
|
4545
4602
|
prevActiveSessionIds = activeIds;
|
|
4546
4603
|
if (changed) persistCollapsedGroups();
|
|
@@ -4548,11 +4605,7 @@ function expandActiveGroups({ onlyNew = false } = {}) {
|
|
|
4548
4605
|
}
|
|
4549
4606
|
|
|
4550
4607
|
function isGroupHeader(el) {
|
|
4551
|
-
return (
|
|
4552
|
-
el.classList.contains('project-group-header') ||
|
|
4553
|
-
el.classList.contains('pinned-sub-header') ||
|
|
4554
|
-
el.classList.contains('session-group-header')
|
|
4555
|
-
);
|
|
4608
|
+
return el.matches(COLLAPSIBLE_HEADER_SELECTOR);
|
|
4556
4609
|
}
|
|
4557
4610
|
|
|
4558
4611
|
function findParentHeader(el) {
|
|
@@ -5030,19 +5083,122 @@ async function confirmDelete() {
|
|
|
5030
5083
|
//#endregion
|
|
5031
5084
|
|
|
5032
5085
|
//#region HELP
|
|
5086
|
+
// Each entry pairs a left and a right group onto the same grid rows, so their
|
|
5087
|
+
// headings sit level and the shorter group just leaves empty rows.
|
|
5088
|
+
// `combo` joins the keys with a plus (a chord) instead of listing them as
|
|
5089
|
+
// alternatives; `hub` marks rows that only work inside Claude Code Hub.
|
|
5090
|
+
const SHORTCUT_PAIRS = [
|
|
5091
|
+
[
|
|
5092
|
+
{
|
|
5093
|
+
title: 'Navigate',
|
|
5094
|
+
rows: [
|
|
5095
|
+
{ keys: ['J', '↓'], label: 'Next item' },
|
|
5096
|
+
{ keys: ['K', '↑'], label: 'Previous item' },
|
|
5097
|
+
{ keys: ['H', '←'], label: 'Left column / collapse group' },
|
|
5098
|
+
{ keys: ['L', '→'], label: 'Right column / expand group' },
|
|
5099
|
+
{ keys: ['Tab'], label: 'Switch sidebar ↔ board' },
|
|
5100
|
+
{ keys: ['Enter', 'Space'], label: 'Open selected item' },
|
|
5101
|
+
],
|
|
5102
|
+
},
|
|
5103
|
+
{
|
|
5104
|
+
title: 'Session',
|
|
5105
|
+
rows: [
|
|
5106
|
+
{ keys: ['P'], label: 'Open plan' },
|
|
5107
|
+
{ keys: ['Shift', 'P'], combo: true, label: 'Session picker' },
|
|
5108
|
+
{ keys: ['I'], label: 'Session info' },
|
|
5109
|
+
{ keys: ['.'], label: 'Pin / unpin' },
|
|
5110
|
+
{ keys: ['>'], label: 'Toggle sticky' },
|
|
5111
|
+
{ keys: ['Ctrl', 'D'], combo: true, label: 'Dismiss session' },
|
|
5112
|
+
{ keys: ['Shift', 'L'], combo: true, label: 'Toggle session log' },
|
|
5113
|
+
{ keys: ['Shift', 'M'], combo: true, label: 'Open last message' },
|
|
5114
|
+
{ keys: ['J', 'K'], label: 'Previous / next message in detail' },
|
|
5115
|
+
],
|
|
5116
|
+
},
|
|
5117
|
+
],
|
|
5118
|
+
[
|
|
5119
|
+
{
|
|
5120
|
+
title: 'Board',
|
|
5121
|
+
rows: [
|
|
5122
|
+
{ keys: ['Enter'], label: 'Toggle task detail panel' },
|
|
5123
|
+
{ keys: ['D'], label: 'Delete selected task' },
|
|
5124
|
+
{ keys: ['N'], label: 'Toggle scratchpad' },
|
|
5125
|
+
{ keys: ['R'], label: 'Refresh data' },
|
|
5126
|
+
{ keys: ['Esc'], label: 'Close panel / clear selection' },
|
|
5127
|
+
],
|
|
5128
|
+
},
|
|
5129
|
+
{
|
|
5130
|
+
title: 'View',
|
|
5131
|
+
rows: [
|
|
5132
|
+
{ keys: ['['], label: 'Toggle sidebar' },
|
|
5133
|
+
{ keys: ['T'], label: 'Toggle theme' },
|
|
5134
|
+
{ keys: ['Shift', 'S'], combo: true, label: 'Storage manager' },
|
|
5135
|
+
{ keys: ['Ctrl', '+'], combo: true, label: 'Larger modal text' },
|
|
5136
|
+
{ keys: ['Ctrl', '−'], combo: true, label: 'Smaller modal text' },
|
|
5137
|
+
{ keys: ['Ctrl', '0'], combo: true, label: 'Reset modal text size' },
|
|
5138
|
+
{ keys: ['?'], label: 'Show this help' },
|
|
5139
|
+
],
|
|
5140
|
+
},
|
|
5141
|
+
],
|
|
5142
|
+
[
|
|
5143
|
+
{
|
|
5144
|
+
title: 'Copy',
|
|
5145
|
+
rows: [
|
|
5146
|
+
{ keys: ['Shift', 'C'], combo: true, label: 'Session id' },
|
|
5147
|
+
{ keys: ['Ctrl', 'Shift', 'C'], combo: true, label: 'Resume command (claude -r <id>)' },
|
|
5148
|
+
],
|
|
5149
|
+
},
|
|
5150
|
+
{
|
|
5151
|
+
title: 'Hub',
|
|
5152
|
+
hub: true,
|
|
5153
|
+
rows: [
|
|
5154
|
+
{ keys: ['M'], label: 'Jump to marketplace' },
|
|
5155
|
+
{ keys: ['$'], label: 'Jump to cost' },
|
|
5156
|
+
{ keys: ['Ctrl', 'M'], combo: true, label: 'Jump to memory' },
|
|
5157
|
+
{ keys: ['Ctrl', 'Alt', '←/→'], combo: true, label: 'Previous / next hub app' },
|
|
5158
|
+
{ keys: ['Alt', '1…9'], combo: true, label: 'Jump to hub app by number' },
|
|
5159
|
+
{ keys: ['Ctrl', 'Alt', 'P'], combo: true, label: 'Project picker' },
|
|
5160
|
+
],
|
|
5161
|
+
},
|
|
5162
|
+
],
|
|
5163
|
+
];
|
|
5164
|
+
|
|
5165
|
+
const EMPTY_GROUP = { title: '', rows: [] };
|
|
5166
|
+
|
|
5167
|
+
// Interleaves each pair's rows left-then-right so CSS grid auto-placement lands
|
|
5168
|
+
// them on shared row tracks (see .shortcuts in style.css).
|
|
5169
|
+
function buildHelpShortcuts() {
|
|
5170
|
+
const cells = [];
|
|
5171
|
+
SHORTCUT_PAIRS.forEach(([left, right = EMPTY_GROUP], pair) => {
|
|
5172
|
+
const first = pair === 0 ? ' sc-first' : '';
|
|
5173
|
+
const sides = [
|
|
5174
|
+
[left, 'sc-l'],
|
|
5175
|
+
[right, 'sc-r'],
|
|
5176
|
+
];
|
|
5177
|
+
const head = (g, side) =>
|
|
5178
|
+
g.title ? `<div class="${escapeHtml(`sc-group ${side}${first}${g.hub ? ' sc-hub' : ''}`)}">${g.title}</div>` : '';
|
|
5179
|
+
cells.push(sides.map(([g, side]) => head(g, side)).join(''));
|
|
5180
|
+
for (let i = 0; i < Math.max(left.rows.length, right.rows.length); i++) {
|
|
5181
|
+
for (const [group, side] of sides) {
|
|
5182
|
+
const row = group.rows[i];
|
|
5183
|
+
if (!row) continue;
|
|
5184
|
+
const sep = row.combo ? '<span class="sc-plus">+</span>' : '<span class="sc-or">/</span>';
|
|
5185
|
+
const keys = row.keys.map((k) => `<kbd>${escapeHtml(k)}</kbd>`).join(sep);
|
|
5186
|
+
const cls = side + (group.hub ? ' sc-hub' : '');
|
|
5187
|
+
cells.push(
|
|
5188
|
+
`<dt class="${escapeHtml(cls)}">${keys}</dt><dd class="${escapeHtml(cls)}">${escapeHtml(row.label)}</dd>`,
|
|
5189
|
+
);
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
});
|
|
5193
|
+
return cells.join('');
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5033
5196
|
function showHelpModal() {
|
|
5034
5197
|
const modal = document.getElementById('help-modal');
|
|
5198
|
+
const list = document.getElementById('help-shortcuts');
|
|
5199
|
+
if (!list.childElementCount) list.innerHTML = buildHelpShortcuts();
|
|
5200
|
+
list.classList.toggle('sc-standalone', !window.__HUB__?.enabled);
|
|
5035
5201
|
modal.classList.add('visible');
|
|
5036
|
-
|
|
5037
|
-
// Handle keyboard shortcuts
|
|
5038
|
-
const keyHandler = (e) => {
|
|
5039
|
-
if (e.key === 'Escape' || e.key === '?') {
|
|
5040
|
-
e.preventDefault();
|
|
5041
|
-
closeHelpModal();
|
|
5042
|
-
document.removeEventListener('keydown', keyHandler);
|
|
5043
|
-
}
|
|
5044
|
-
};
|
|
5045
|
-
document.addEventListener('keydown', keyHandler);
|
|
5046
5202
|
}
|
|
5047
5203
|
|
|
5048
5204
|
function closeHelpModal() {
|
|
@@ -5318,13 +5474,7 @@ function _renderStorageSessions() {
|
|
|
5318
5474
|
async function _storageViewSession(id) {
|
|
5319
5475
|
closeStorageManager();
|
|
5320
5476
|
revealedStorageSessionId = id;
|
|
5321
|
-
|
|
5322
|
-
lastSessionsHash = '';
|
|
5323
|
-
await fetchSessions();
|
|
5324
|
-
}
|
|
5325
|
-
await fetchTasks(id);
|
|
5326
|
-
const el = document.querySelector(`.session-item[data-session-id="${CSS.escape(id)}"]`);
|
|
5327
|
-
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
5477
|
+
await revealSession(id);
|
|
5328
5478
|
}
|
|
5329
5479
|
|
|
5330
5480
|
function _storageUnpinSession(id) {
|
|
@@ -5616,6 +5766,7 @@ const MODAL_CLOSERS = {
|
|
|
5616
5766
|
'team-modal': () => closeTeamModal(),
|
|
5617
5767
|
'agent-modal': () => closeAgentModal(),
|
|
5618
5768
|
'help-modal': () => closeHelpModal(),
|
|
5769
|
+
'session-picker-modal': () => closeSessionPicker(),
|
|
5619
5770
|
};
|
|
5620
5771
|
|
|
5621
5772
|
document.addEventListener('keydown', (e) => {
|
|
@@ -5649,6 +5800,9 @@ document.addEventListener('keydown', (e) => {
|
|
|
5649
5800
|
if (close) close();
|
|
5650
5801
|
else document.getElementById(topId).classList.remove('visible');
|
|
5651
5802
|
e.stopImmediatePropagation();
|
|
5803
|
+
} else if (e.key === '?' && document.getElementById('help-modal').classList.contains('visible')) {
|
|
5804
|
+
e.preventDefault();
|
|
5805
|
+
closeHelpModal();
|
|
5652
5806
|
} else if (
|
|
5653
5807
|
e.code === 'KeyM' &&
|
|
5654
5808
|
e.shiftKey &&
|
|
@@ -5718,6 +5872,12 @@ document.addEventListener('keydown', (e) => {
|
|
|
5718
5872
|
showStorageManager();
|
|
5719
5873
|
return;
|
|
5720
5874
|
}
|
|
5875
|
+
// Ctrl+Shift+P is the browser's own; only the bare chord opens the picker.
|
|
5876
|
+
if (e.code === 'KeyP' && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
5877
|
+
e.preventDefault();
|
|
5878
|
+
openSessionPicker();
|
|
5879
|
+
return;
|
|
5880
|
+
}
|
|
5721
5881
|
if (e.key === '.' || e.key === '>') {
|
|
5722
5882
|
const sid = sessionsList.querySelector('.kb-selected')?.dataset.sessionId || currentSessionId;
|
|
5723
5883
|
if (sid) {
|
|
@@ -5892,16 +6052,18 @@ document.addEventListener('keydown', (e) => {
|
|
|
5892
6052
|
}
|
|
5893
6053
|
return;
|
|
5894
6054
|
}
|
|
5895
|
-
if (e.code === 'KeyC' && e.shiftKey) {
|
|
6055
|
+
if (e.code === 'KeyC' && e.shiftKey && !e.altKey && !e.metaKey) {
|
|
5896
6056
|
e.preventDefault();
|
|
5897
6057
|
if (!contextSid) {
|
|
5898
6058
|
showToast('No session selected');
|
|
5899
6059
|
return;
|
|
5900
6060
|
}
|
|
6061
|
+
const text = e.ctrlKey ? `claude -r ${contextSid}` : contextSid;
|
|
6062
|
+
const label = e.ctrlKey ? 'resume command' : 'session id';
|
|
5901
6063
|
navigator.clipboard
|
|
5902
|
-
.writeText(
|
|
5903
|
-
.then(() => showToast(`Copied
|
|
5904
|
-
.catch(() => showToast(
|
|
6064
|
+
.writeText(text)
|
|
6065
|
+
.then(() => showToast(`Copied ${label}: ${contextSid.slice(0, 8)}`, 'success'))
|
|
6066
|
+
.catch(() => showToast(`Failed to copy ${label}`));
|
|
5905
6067
|
return;
|
|
5906
6068
|
}
|
|
5907
6069
|
if (matchKey(e, 'KeyR')) {
|
|
@@ -6861,6 +7023,13 @@ function isSessionActive(s) {
|
|
|
6861
7023
|
return s.hasRecentLog || s.inProgress > 0 || s.hasActiveAgents || s.hasWaitingForUser;
|
|
6862
7024
|
}
|
|
6863
7025
|
|
|
7026
|
+
const SESSION_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7027
|
+
|
|
7028
|
+
function sessionDisplayName(session) {
|
|
7029
|
+
const raw = session.name || session.id;
|
|
7030
|
+
return SESSION_UUID_RE.test(raw) ? raw.slice(0, 8) : raw;
|
|
7031
|
+
}
|
|
7032
|
+
|
|
6864
7033
|
function formatDate(dateStr) {
|
|
6865
7034
|
const date = new Date(dateStr);
|
|
6866
7035
|
const now = new Date();
|
|
@@ -7414,7 +7583,7 @@ document.addEventListener('click', (e) => {
|
|
|
7414
7583
|
}
|
|
7415
7584
|
}
|
|
7416
7585
|
|
|
7417
|
-
const header = e.target.closest(
|
|
7586
|
+
const header = e.target.closest(COLLAPSIBLE_HEADER_SELECTOR);
|
|
7418
7587
|
if (header) {
|
|
7419
7588
|
setGroupCollapsed(header, !collapsedProjectGroups.has(header.dataset.groupPath));
|
|
7420
7589
|
}
|
|
@@ -8027,6 +8196,112 @@ let _planSessionId = null;
|
|
|
8027
8196
|
|
|
8028
8197
|
//#endregion
|
|
8029
8198
|
|
|
8199
|
+
//#region SESSION_PICKER
|
|
8200
|
+
// Snapshot of what the sidebar is showing, taken once per open: the picker only ever offers
|
|
8201
|
+
// sessions already on screen, and the recency order cannot change while it is up.
|
|
8202
|
+
let spSource = [];
|
|
8203
|
+
let spRows = [];
|
|
8204
|
+
let spIdx = 0;
|
|
8205
|
+
|
|
8206
|
+
function openSessionPicker() {
|
|
8207
|
+
const input = document.getElementById('session-picker-input');
|
|
8208
|
+
input.value = '';
|
|
8209
|
+
spSource = getFilteredSessions().sort((a, b) => Date.parse(b.modifiedAt) - Date.parse(a.modifiedAt));
|
|
8210
|
+
document.getElementById('session-picker-modal').classList.add('visible');
|
|
8211
|
+
renderSessionPicker();
|
|
8212
|
+
input.focus();
|
|
8213
|
+
}
|
|
8214
|
+
|
|
8215
|
+
function closeSessionPicker() {
|
|
8216
|
+
hideModalOverlay('session-picker-modal');
|
|
8217
|
+
}
|
|
8218
|
+
|
|
8219
|
+
function spMatches(session, query) {
|
|
8220
|
+
return (
|
|
8221
|
+
fuzzyMatch(session.name, query) ||
|
|
8222
|
+
fuzzyMatch(session.id, query) ||
|
|
8223
|
+
fuzzyMatch(session.project, query) ||
|
|
8224
|
+
fuzzyMatch(session.gitBranch, query)
|
|
8225
|
+
);
|
|
8226
|
+
}
|
|
8227
|
+
|
|
8228
|
+
// Spelled out rather than interpolated so the class never comes from a variable.
|
|
8229
|
+
const SP_DOTS = {
|
|
8230
|
+
waiting: '<span class="activity-dot waiting"></span>',
|
|
8231
|
+
live: '<span class="activity-dot live"></span>',
|
|
8232
|
+
active: '<span class="activity-dot active"></span>',
|
|
8233
|
+
idle: '<span class="activity-dot"></span>',
|
|
8234
|
+
};
|
|
8235
|
+
|
|
8236
|
+
function spDotHtml(session) {
|
|
8237
|
+
if (isWaitingSession(session)) return SP_DOTS.waiting;
|
|
8238
|
+
if (isSessionLive(session)) return SP_DOTS.live;
|
|
8239
|
+
return isActiveSession(session) ? SP_DOTS.active : SP_DOTS.idle;
|
|
8240
|
+
}
|
|
8241
|
+
|
|
8242
|
+
function renderSessionPicker() {
|
|
8243
|
+
const list = document.getElementById('session-picker-list');
|
|
8244
|
+
const query = document.getElementById('session-picker-input').value.trim();
|
|
8245
|
+
spRows = spSource.filter((s) => spMatches(s, query));
|
|
8246
|
+
|
|
8247
|
+
if (!spRows.length) {
|
|
8248
|
+
spIdx = -1;
|
|
8249
|
+
list.innerHTML = `<div class="sp-empty">${spSource.length ? 'No session matches' : 'No sessions in the current sidebar filter'}</div>`;
|
|
8250
|
+
return;
|
|
8251
|
+
}
|
|
8252
|
+
|
|
8253
|
+
list.innerHTML = spRows
|
|
8254
|
+
.map((s, i) => {
|
|
8255
|
+
const project = s.project ? s.project.split(/[/\\]/).pop() : '';
|
|
8256
|
+
return `<button class="sp-row${s.id === currentSessionId ? ' current' : ''}" data-idx="${i}" title="${escapeHtml(s.id)}">
|
|
8257
|
+
${spDotHtml(s)}
|
|
8258
|
+
<span class="sp-name">${escapeHtml(sessionDisplayName(s))}</span>
|
|
8259
|
+
<span class="sp-project">${escapeHtml(project)}</span>
|
|
8260
|
+
${s.gitBranch ? `<span class="sp-branch">${escapeHtml(s.gitBranch)}</span>` : ''}
|
|
8261
|
+
<span class="sp-count">${s.completed}/${s.taskCount}</span>
|
|
8262
|
+
<span class="sp-time">${formatDate(s.modifiedAt)}</span>
|
|
8263
|
+
</button>`;
|
|
8264
|
+
})
|
|
8265
|
+
.join('');
|
|
8266
|
+
spSelect(0);
|
|
8267
|
+
}
|
|
8268
|
+
|
|
8269
|
+
function spSelect(idx) {
|
|
8270
|
+
const list = document.getElementById('session-picker-list');
|
|
8271
|
+
list.children[spIdx]?.classList.remove('selected');
|
|
8272
|
+
spIdx = Math.min(Math.max(idx, 0), spRows.length - 1);
|
|
8273
|
+
const row = list.children[spIdx];
|
|
8274
|
+
row?.classList.add('selected');
|
|
8275
|
+
row?.scrollIntoView({ block: 'nearest' });
|
|
8276
|
+
}
|
|
8277
|
+
|
|
8278
|
+
async function spOpen(idx) {
|
|
8279
|
+
const session = spRows[idx];
|
|
8280
|
+
if (!session) return;
|
|
8281
|
+
closeSessionPicker();
|
|
8282
|
+
await revealSession(session.id);
|
|
8283
|
+
}
|
|
8284
|
+
|
|
8285
|
+
function initSessionPicker() {
|
|
8286
|
+
const input = document.getElementById('session-picker-input');
|
|
8287
|
+
input.addEventListener('input', renderSessionPicker);
|
|
8288
|
+
// Bound on the input: the global handler returns early on INPUT targets.
|
|
8289
|
+
input.addEventListener('keydown', (e) => {
|
|
8290
|
+
if (e.key === 'Escape') closeSessionPicker();
|
|
8291
|
+
else if (matchKey(e, 'ArrowDown') || (e.ctrlKey && e.key === 'n')) spSelect(spIdx + 1);
|
|
8292
|
+
else if (matchKey(e, 'ArrowUp') || (e.ctrlKey && e.key === 'p')) spSelect(spIdx - 1);
|
|
8293
|
+
else if (e.key === 'Enter') spOpen(spIdx);
|
|
8294
|
+
else return;
|
|
8295
|
+
e.preventDefault();
|
|
8296
|
+
e.stopPropagation();
|
|
8297
|
+
});
|
|
8298
|
+
document.getElementById('session-picker-list').addEventListener('click', (e) => {
|
|
8299
|
+
const row = e.target.closest('.sp-row');
|
|
8300
|
+
if (row) spOpen(Number(row.dataset.idx));
|
|
8301
|
+
});
|
|
8302
|
+
}
|
|
8303
|
+
//#endregion
|
|
8304
|
+
|
|
8030
8305
|
//#region PLAN
|
|
8031
8306
|
function refreshOpenPlan() {
|
|
8032
8307
|
if (!_planSessionId || !document.getElementById('plan-modal').classList.contains('visible')) return;
|
|
@@ -8663,6 +8938,7 @@ try {
|
|
|
8663
8938
|
} catch (_) {}
|
|
8664
8939
|
loadSessionGroups();
|
|
8665
8940
|
initSessionGroupsDnd();
|
|
8941
|
+
initSessionPicker();
|
|
8666
8942
|
try {
|
|
8667
8943
|
const af = JSON.parse(localStorage.getItem('activityFilter') || '[]');
|
|
8668
8944
|
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach side-effect
|
package/public/index.html
CHANGED
|
@@ -346,7 +346,7 @@
|
|
|
346
346
|
|
|
347
347
|
<!-- Help Modal -->
|
|
348
348
|
<div id="help-modal" class="modal-overlay" onclick="closeHelpModal()">
|
|
349
|
-
<div class="modal" onclick="event.stopPropagation()">
|
|
349
|
+
<div class="modal modal-help" onclick="event.stopPropagation()">
|
|
350
350
|
<div class="modal-header">
|
|
351
351
|
<h3 class="modal-title">Keyboard Shortcuts</h3>
|
|
352
352
|
<button class="modal-close" aria-label="Close dialog" onclick="closeHelpModal()">
|
|
@@ -356,136 +356,7 @@
|
|
|
356
356
|
</button>
|
|
357
357
|
</div>
|
|
358
358
|
<div class="modal-body modal-zoomable">
|
|
359
|
-
<
|
|
360
|
-
<div style="display: grid; gap: 16px; align-content: start;">
|
|
361
|
-
<div>
|
|
362
|
-
<h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Global</h4>
|
|
363
|
-
<table style="width: 100%; font-size: 13px;">
|
|
364
|
-
<tr>
|
|
365
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">?</kbd></td>
|
|
366
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Show keyboard shortcuts</td>
|
|
367
|
-
</tr>
|
|
368
|
-
<tr>
|
|
369
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Esc</kbd></td>
|
|
370
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Close panels or cancel</td>
|
|
371
|
-
</tr>
|
|
372
|
-
<tr>
|
|
373
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Tab</kbd></td>
|
|
374
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Toggle sidebar / board focus</td>
|
|
375
|
-
</tr>
|
|
376
|
-
<tr>
|
|
377
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">[</kbd></td>
|
|
378
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Toggle sidebar collapse</td>
|
|
379
|
-
</tr>
|
|
380
|
-
<tr>
|
|
381
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">R</kbd></td>
|
|
382
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Reload page</td>
|
|
383
|
-
</tr>
|
|
384
|
-
</table>
|
|
385
|
-
</div>
|
|
386
|
-
<div>
|
|
387
|
-
<h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Navigation</h4>
|
|
388
|
-
<table style="width: 100%; font-size: 13px;">
|
|
389
|
-
<tr>
|
|
390
|
-
<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</kbd> / <kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">↓</kbd></td>
|
|
391
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Next task in column</td>
|
|
392
|
-
</tr>
|
|
393
|
-
<tr>
|
|
394
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">K</kbd> / <kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">↑</kbd></td>
|
|
395
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Previous task in column</td>
|
|
396
|
-
</tr>
|
|
397
|
-
<tr>
|
|
398
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">H</kbd> / <kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">←</kbd></td>
|
|
399
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Move to left column</td>
|
|
400
|
-
</tr>
|
|
401
|
-
<tr>
|
|
402
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">L</kbd> / <kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">→</kbd></td>
|
|
403
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Move to right column</td>
|
|
404
|
-
</tr>
|
|
405
|
-
<tr>
|
|
406
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Enter</kbd></td>
|
|
407
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Toggle task detail panel</td>
|
|
408
|
-
</tr>
|
|
409
|
-
</table>
|
|
410
|
-
</div>
|
|
411
|
-
</div>
|
|
412
|
-
<div style="align-content: start;">
|
|
413
|
-
<h4 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 14px; font-weight: 600;">Actions</h4>
|
|
414
|
-
<table style="width: 100%; font-size: 13px;">
|
|
415
|
-
<tr>
|
|
416
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">P</kbd></td>
|
|
417
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Open session plan</td>
|
|
418
|
-
</tr>
|
|
419
|
-
<tr>
|
|
420
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">I</kbd></td>
|
|
421
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Open session info</td>
|
|
422
|
-
</tr>
|
|
423
|
-
<tr>
|
|
424
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Ctrl+D</kbd></td>
|
|
425
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Dismiss selected session</td>
|
|
426
|
-
</tr>
|
|
427
|
-
<tr>
|
|
428
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">D</kbd></td>
|
|
429
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Delete selected task</td>
|
|
430
|
-
</tr>
|
|
431
|
-
<tr>
|
|
432
|
-
<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>
|
|
433
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Toggle scratchpad</td>
|
|
434
|
-
</tr>
|
|
435
|
-
<tr>
|
|
436
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">.</kbd></td>
|
|
437
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Pin/unpin selected session</td>
|
|
438
|
-
</tr>
|
|
439
|
-
<tr>
|
|
440
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">></kbd></td>
|
|
441
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Toggle sticky on selected session</td>
|
|
442
|
-
</tr>
|
|
443
|
-
<tr>
|
|
444
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">T</kbd></td>
|
|
445
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Toggle theme</td>
|
|
446
|
-
</tr>
|
|
447
|
-
<tr>
|
|
448
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">M</kbd></td>
|
|
449
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Jump to marketplace</td>
|
|
450
|
-
</tr>
|
|
451
|
-
<tr>
|
|
452
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">$</kbd></td>
|
|
453
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Jump to cost</td>
|
|
454
|
-
</tr>
|
|
455
|
-
<tr>
|
|
456
|
-
<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+L</kbd></td>
|
|
457
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Toggle session log</td>
|
|
458
|
-
</tr>
|
|
459
|
-
<tr>
|
|
460
|
-
<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+M</kbd></td>
|
|
461
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Open last message detail</td>
|
|
462
|
-
</tr>
|
|
463
|
-
<tr>
|
|
464
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Ctrl++</kbd> / <kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Ctrl+-</kbd></td>
|
|
465
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Modal text size</td>
|
|
466
|
-
</tr>
|
|
467
|
-
<tr>
|
|
468
|
-
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Ctrl+0</kbd></td>
|
|
469
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Reset text size</td>
|
|
470
|
-
</tr>
|
|
471
|
-
<tr>
|
|
472
|
-
<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>
|
|
473
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Storage manager</td>
|
|
474
|
-
</tr>
|
|
475
|
-
<tr>
|
|
476
|
-
<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+C</kbd></td>
|
|
477
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Copy session id</td>
|
|
478
|
-
</tr>
|
|
479
|
-
<tr>
|
|
480
|
-
<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>
|
|
481
|
-
<td style="padding: 4px 0; color: var(--text-primary);">Navigate messages in detail modal</td>
|
|
482
|
-
</tr>
|
|
483
|
-
</table>
|
|
484
|
-
</div>
|
|
485
|
-
</div>
|
|
486
|
-
</div>
|
|
487
|
-
<div class="modal-footer">
|
|
488
|
-
<button class="btn btn-primary" onclick="closeHelpModal()">Got it</button>
|
|
359
|
+
<dl class="shortcuts" id="help-shortcuts"></dl>
|
|
489
360
|
</div>
|
|
490
361
|
</div>
|
|
491
362
|
</div>
|
|
@@ -737,6 +608,14 @@
|
|
|
737
608
|
</div>
|
|
738
609
|
</div>
|
|
739
610
|
|
|
611
|
+
<div id="session-picker-modal" class="modal-overlay top-aligned" onclick="closeSessionPicker()">
|
|
612
|
+
<div class="modal session-picker" onclick="event.stopPropagation()">
|
|
613
|
+
<input type="text" id="session-picker-input" class="form-input sp-input" placeholder="Jump to session..." autocomplete="off" spellcheck="false">
|
|
614
|
+
<div id="session-picker-list" class="sp-list"></div>
|
|
615
|
+
<div class="sp-hint"><kbd>↑</kbd><kbd>↓</kbd> move · <kbd>Enter</kbd> open · <kbd>Esc</kbd> close</div>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
740
619
|
<div id="toast" class="toast"></div>
|
|
741
620
|
</body>
|
|
742
621
|
</html>
|
package/public/style.css
CHANGED
|
@@ -4264,6 +4264,13 @@ body.light .msg-assistant .msg-text {
|
|
|
4264
4264
|
display: flex;
|
|
4265
4265
|
}
|
|
4266
4266
|
|
|
4267
|
+
/* For pickers: the dialog sits above the vertical centre so its list grows
|
|
4268
|
+
downward instead of shifting rows under the cursor as it filters. */
|
|
4269
|
+
.modal-overlay.top-aligned {
|
|
4270
|
+
align-items: flex-start;
|
|
4271
|
+
padding-top: 12vh;
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4267
4274
|
.plan-modal-overlay {
|
|
4268
4275
|
z-index: 10001;
|
|
4269
4276
|
background: rgba(0, 0, 0, 0.6);
|
|
@@ -4512,6 +4519,106 @@ body.light .msg-assistant .msg-text {
|
|
|
4512
4519
|
max-width: 440px;
|
|
4513
4520
|
}
|
|
4514
4521
|
|
|
4522
|
+
.modal-help {
|
|
4523
|
+
max-width: 960px;
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
/* One shared grid for both halves (key | label | spacer | key | label): each key
|
|
4527
|
+
gutter is sized by its column's widest combo, and left/right rows share row
|
|
4528
|
+
tracks, so paired groups line up — a shorter group just leaves empty rows.
|
|
4529
|
+
Cells opt into a half via sc-l / sc-r; the markup is generated from
|
|
4530
|
+
SHORTCUT_PAIRS in app.js. */
|
|
4531
|
+
.shortcuts {
|
|
4532
|
+
margin: 0;
|
|
4533
|
+
display: grid;
|
|
4534
|
+
grid-template-columns: max-content 1fr 28px max-content 1fr;
|
|
4535
|
+
column-gap: 14px;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
.shortcuts dt.sc-l {
|
|
4539
|
+
grid-column: 1;
|
|
4540
|
+
}
|
|
4541
|
+
.shortcuts dd.sc-l {
|
|
4542
|
+
grid-column: 2;
|
|
4543
|
+
}
|
|
4544
|
+
.shortcuts dt.sc-r {
|
|
4545
|
+
grid-column: 4;
|
|
4546
|
+
}
|
|
4547
|
+
.shortcuts dd.sc-r {
|
|
4548
|
+
grid-column: 5;
|
|
4549
|
+
}
|
|
4550
|
+
.shortcuts .sc-group.sc-l {
|
|
4551
|
+
grid-column: 1 / 3;
|
|
4552
|
+
}
|
|
4553
|
+
.shortcuts .sc-group.sc-r {
|
|
4554
|
+
grid-column: 4 / 6;
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
.shortcuts dt {
|
|
4558
|
+
margin: 0;
|
|
4559
|
+
display: flex;
|
|
4560
|
+
align-items: center;
|
|
4561
|
+
align-self: center;
|
|
4562
|
+
gap: 4px;
|
|
4563
|
+
padding: 3px 0;
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
.shortcuts dd {
|
|
4567
|
+
margin: 0;
|
|
4568
|
+
font-size: 13px;
|
|
4569
|
+
color: var(--text-secondary);
|
|
4570
|
+
align-self: center;
|
|
4571
|
+
padding: 3px 0;
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
/* Section headings carry the column: full-width rule + primary ink, so the eye
|
|
4575
|
+
finds the group before it reads any single row. */
|
|
4576
|
+
.shortcuts .sc-group {
|
|
4577
|
+
padding: 18px 0 6px;
|
|
4578
|
+
margin-bottom: 6px;
|
|
4579
|
+
font-size: 11px;
|
|
4580
|
+
font-weight: 700;
|
|
4581
|
+
letter-spacing: 0.16em;
|
|
4582
|
+
text-transform: uppercase;
|
|
4583
|
+
color: var(--text-primary);
|
|
4584
|
+
border-bottom: 1px solid var(--border);
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
.shortcuts .sc-group.sc-first {
|
|
4588
|
+
padding-top: 2px;
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
/* keycap chips: elevated fill + thicker bottom border reads as a key */
|
|
4592
|
+
.sp-hint kbd,
|
|
4593
|
+
.shortcuts kbd {
|
|
4594
|
+
display: inline-flex;
|
|
4595
|
+
align-items: center;
|
|
4596
|
+
justify-content: center;
|
|
4597
|
+
min-width: 22px;
|
|
4598
|
+
height: 22px;
|
|
4599
|
+
padding: 0 6px;
|
|
4600
|
+
box-sizing: border-box;
|
|
4601
|
+
font-family: var(--font-mono, monospace);
|
|
4602
|
+
font-size: 11px;
|
|
4603
|
+
color: var(--text-primary);
|
|
4604
|
+
background: var(--bg-hover);
|
|
4605
|
+
border: 1px solid var(--border);
|
|
4606
|
+
border-bottom-width: 2px;
|
|
4607
|
+
border-radius: 5px;
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4610
|
+
.shortcuts .sc-plus,
|
|
4611
|
+
.shortcuts .sc-or {
|
|
4612
|
+
color: var(--text-muted);
|
|
4613
|
+
font-size: 11px;
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
/* Hub jumps are no-ops outside Claude Code Hub, so the standalone board hides
|
|
4617
|
+
that whole column rather than listing keys that do nothing. */
|
|
4618
|
+
.shortcuts.sc-standalone .sc-hub {
|
|
4619
|
+
display: none;
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4515
4622
|
@keyframes live-border-pulse {
|
|
4516
4623
|
0%,
|
|
4517
4624
|
100% {
|
|
@@ -4721,6 +4828,130 @@ select.form-input option:checked {
|
|
|
4721
4828
|
|
|
4722
4829
|
/* #endregion */
|
|
4723
4830
|
|
|
4831
|
+
/* #region SESSION_PICKER */
|
|
4832
|
+
.modal.session-picker {
|
|
4833
|
+
max-height: 70vh;
|
|
4834
|
+
padding: 12px;
|
|
4835
|
+
display: flex;
|
|
4836
|
+
flex-direction: column;
|
|
4837
|
+
gap: 8px;
|
|
4838
|
+
overflow: hidden;
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4841
|
+
.sp-input {
|
|
4842
|
+
padding: 9px 12px;
|
|
4843
|
+
background: var(--bg-deep);
|
|
4844
|
+
font-size: 13px;
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4847
|
+
.sp-list {
|
|
4848
|
+
overflow-y: auto;
|
|
4849
|
+
display: flex;
|
|
4850
|
+
flex-direction: column;
|
|
4851
|
+
gap: 2px;
|
|
4852
|
+
}
|
|
4853
|
+
|
|
4854
|
+
.sp-row {
|
|
4855
|
+
display: flex;
|
|
4856
|
+
align-items: center;
|
|
4857
|
+
gap: 8px;
|
|
4858
|
+
width: 100%;
|
|
4859
|
+
padding: 6px 8px;
|
|
4860
|
+
background: none;
|
|
4861
|
+
border: 1px solid transparent;
|
|
4862
|
+
border-radius: 5px;
|
|
4863
|
+
color: var(--text-secondary);
|
|
4864
|
+
font-family: var(--mono);
|
|
4865
|
+
font-size: 12px;
|
|
4866
|
+
text-align: left;
|
|
4867
|
+
cursor: pointer;
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
.sp-row:hover {
|
|
4871
|
+
background: var(--bg-hover);
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4874
|
+
.sp-row.selected {
|
|
4875
|
+
background: var(--bg-elevated);
|
|
4876
|
+
border-color: var(--accent);
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
.sp-row .activity-dot.live {
|
|
4880
|
+
background: var(--success);
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
.sp-row .activity-dot.active {
|
|
4884
|
+
background: var(--accent);
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
.sp-row .activity-dot.waiting {
|
|
4888
|
+
background: var(--warning);
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
.sp-name,
|
|
4892
|
+
.sp-project,
|
|
4893
|
+
.sp-branch {
|
|
4894
|
+
overflow: hidden;
|
|
4895
|
+
text-overflow: ellipsis;
|
|
4896
|
+
white-space: nowrap;
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
.sp-name {
|
|
4900
|
+
flex: none;
|
|
4901
|
+
max-width: 40%;
|
|
4902
|
+
color: var(--text-primary);
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
.sp-row.current .sp-name {
|
|
4906
|
+
color: var(--accent-text);
|
|
4907
|
+
}
|
|
4908
|
+
|
|
4909
|
+
.sp-project {
|
|
4910
|
+
flex: 1;
|
|
4911
|
+
min-width: 0;
|
|
4912
|
+
color: var(--text-muted);
|
|
4913
|
+
}
|
|
4914
|
+
|
|
4915
|
+
.sp-branch {
|
|
4916
|
+
flex: none;
|
|
4917
|
+
max-width: 25%;
|
|
4918
|
+
padding: 1px 5px;
|
|
4919
|
+
border-radius: 3px;
|
|
4920
|
+
background: var(--bg-hover);
|
|
4921
|
+
color: var(--text-tertiary);
|
|
4922
|
+
font-size: 11px;
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4925
|
+
.sp-count,
|
|
4926
|
+
.sp-time {
|
|
4927
|
+
flex: none;
|
|
4928
|
+
color: var(--text-muted);
|
|
4929
|
+
font-size: 11px;
|
|
4930
|
+
white-space: nowrap;
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4933
|
+
.sp-time {
|
|
4934
|
+
min-width: 62px;
|
|
4935
|
+
text-align: right;
|
|
4936
|
+
}
|
|
4937
|
+
|
|
4938
|
+
.sp-empty {
|
|
4939
|
+
padding: 20px 8px;
|
|
4940
|
+
color: var(--text-muted);
|
|
4941
|
+
font-size: 12px;
|
|
4942
|
+
text-align: center;
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
.sp-hint {
|
|
4946
|
+
display: flex;
|
|
4947
|
+
align-items: center;
|
|
4948
|
+
gap: 5px;
|
|
4949
|
+
color: var(--text-muted);
|
|
4950
|
+
font-size: 11px;
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
/* #endregion */
|
|
4954
|
+
|
|
4724
4955
|
/* #region TOOL_STATS */
|
|
4725
4956
|
.modal.tool-stats-modal {
|
|
4726
4957
|
width: 90%;
|
|
@@ -5314,21 +5545,24 @@ pre.mermaid svg {
|
|
|
5314
5545
|
}
|
|
5315
5546
|
|
|
5316
5547
|
.project-group-header.kb-selected,
|
|
5317
|
-
.pinned-sub-header.kb-selected
|
|
5548
|
+
.pinned-sub-header.kb-selected,
|
|
5549
|
+
.sg-section-toggle.kb-selected {
|
|
5318
5550
|
color: var(--text-primary);
|
|
5319
5551
|
background: var(--bg-hover);
|
|
5320
5552
|
border-radius: 4px;
|
|
5321
5553
|
}
|
|
5322
5554
|
|
|
5323
5555
|
.project-group-header .group-chevron,
|
|
5324
|
-
.session-group-header .group-chevron
|
|
5556
|
+
.session-group-header .group-chevron,
|
|
5557
|
+
.sg-section-toggle .group-chevron {
|
|
5325
5558
|
transition: transform 0.15s ease;
|
|
5326
5559
|
flex-shrink: 0;
|
|
5327
5560
|
display: block;
|
|
5328
5561
|
}
|
|
5329
5562
|
|
|
5330
5563
|
.project-group-header.collapsed .group-chevron,
|
|
5331
|
-
.session-group-header.collapsed .group-chevron
|
|
5564
|
+
.session-group-header.collapsed .group-chevron,
|
|
5565
|
+
.sg-section-toggle.collapsed .group-chevron {
|
|
5332
5566
|
transform: rotate(-90deg);
|
|
5333
5567
|
}
|
|
5334
5568
|
|
|
@@ -5522,6 +5756,18 @@ pre.mermaid svg {
|
|
|
5522
5756
|
margin-top: 0;
|
|
5523
5757
|
}
|
|
5524
5758
|
|
|
5759
|
+
.sg-section-toggle {
|
|
5760
|
+
display: flex;
|
|
5761
|
+
align-items: center;
|
|
5762
|
+
gap: 4px;
|
|
5763
|
+
padding-bottom: 4px;
|
|
5764
|
+
cursor: pointer;
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
.sg-section-toggle:hover {
|
|
5768
|
+
color: var(--text-primary);
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5525
5771
|
.session-group-header {
|
|
5526
5772
|
display: flex;
|
|
5527
5773
|
align-items: center;
|
|
@@ -5584,7 +5830,8 @@ pre.mermaid svg {
|
|
|
5584
5830
|
margin-left: 6px;
|
|
5585
5831
|
}
|
|
5586
5832
|
|
|
5587
|
-
.session-group-sessions.collapsed
|
|
5833
|
+
.session-group-sessions.collapsed,
|
|
5834
|
+
.sg-section-body.collapsed {
|
|
5588
5835
|
display: none;
|
|
5589
5836
|
}
|
|
5590
5837
|
|