claude-code-kanban 4.23.1 → 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/lib/parsers.js +32 -19
- package/package.json +1 -1
- package/public/app.js +360 -86
- package/public/index.html +10 -131
- package/public/style.css +251 -4
- package/server.js +13 -9
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/lib/parsers.js
CHANGED
|
@@ -160,6 +160,17 @@ function formatTaskUsage(usage) {
|
|
|
160
160
|
return parts.length ? ' · ' + parts.join(' · ') : '';
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// Raw per-agent cost for the agent modal's chips plus the formatted one-liner for
|
|
164
|
+
// the inline message row. Null when the source carried no numbers.
|
|
165
|
+
function agentUsageEntry(subagentTokens, toolUses, durationMs) {
|
|
166
|
+
const usage = {};
|
|
167
|
+
if (subagentTokens != null) usage.tokens = subagentTokens;
|
|
168
|
+
if (toolUses != null) usage.tools = toolUses;
|
|
169
|
+
if (durationMs != null) usage.durationMs = durationMs;
|
|
170
|
+
if (!Object.keys(usage).length) return null;
|
|
171
|
+
return { usage, usageText: formatTaskUsage({ subagentTokens, toolUses, durationMs }) };
|
|
172
|
+
}
|
|
173
|
+
|
|
163
174
|
function pushUserMessage(messages, text, timestamp, sysLabel, extras) {
|
|
164
175
|
if (sysLabel === '__skip__') return;
|
|
165
176
|
let safeText = text || '';
|
|
@@ -1099,6 +1110,7 @@ function buildSessionDigest(jsonlPath) {
|
|
|
1099
1110
|
const taskIdRe = /<task-id>([a-zA-Z0-9_-]+)<\/task-id>/;
|
|
1100
1111
|
const nameByToolUseId = {};
|
|
1101
1112
|
const descByToolUseId = {};
|
|
1113
|
+
const usageByToolUseId = {};
|
|
1102
1114
|
for (const line of content.split('\n')) {
|
|
1103
1115
|
// Terminated-teammate detection: check first since cheap substring guards
|
|
1104
1116
|
if (line.includes('teammate-message') &&
|
|
@@ -1171,34 +1183,34 @@ function buildSessionDigest(jsonlPath) {
|
|
|
1171
1183
|
} else if (line.includes('User rejected tool use') && line.includes('"tool_use_id"')) {
|
|
1172
1184
|
const m = tmToolIdRe.exec(line);
|
|
1173
1185
|
if (m) rejectedToolUseIds.add(m[1]);
|
|
1174
|
-
} else if (line.includes('<task-notification>')
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1186
|
+
} else if (line.includes('<task-notification>')) {
|
|
1187
|
+
// The envelope's tags survive JSON escaping, so read them off the raw line.
|
|
1188
|
+
// This covers every delivery shape (user message, queue-operation,
|
|
1189
|
+
// queued_command attachment) without enumerating them.
|
|
1190
|
+
if (line.includes('<status>killed</status>') || line.includes('<status>error</status>')) {
|
|
1191
|
+
const idMatch = taskIdRe.exec(line);
|
|
1192
|
+
if (idMatch) killedAgentIds.add(idMatch[1]);
|
|
1193
|
+
}
|
|
1194
|
+
if (line.includes('<subagent_tokens>')) {
|
|
1195
|
+
const notif = parseTaskNotification(line);
|
|
1196
|
+
const u = notif?.usage;
|
|
1197
|
+
const entry = u && notif.toolUseId ? agentUsageEntry(u.subagentTokens, u.toolUses, u.durationMs) : null;
|
|
1198
|
+
if (entry) usageByToolUseId[notif.toolUseId] = entry;
|
|
1199
|
+
}
|
|
1178
1200
|
} else if (line.includes('"toolUseResult"') && line.includes('"agentId"') && line.includes('"tool_result"')) {
|
|
1179
1201
|
try {
|
|
1180
1202
|
const obj = JSON.parse(line);
|
|
1181
1203
|
const tur = obj.toolUseResult;
|
|
1182
1204
|
if (tur?.agentId) {
|
|
1183
|
-
// Foreground agents report their cost on the completion toolUseResult
|
|
1184
|
-
//
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
// one-liner via the shared formatter (for the inline message row):
|
|
1188
|
-
// " · 31.5k tok · 5 tools · 34s".
|
|
1189
|
-
const usage = {};
|
|
1190
|
-
if (tur.totalTokens != null) usage.tokens = tur.totalTokens;
|
|
1191
|
-
if (tur.totalToolUseCount != null) usage.tools = tur.totalToolUseCount;
|
|
1192
|
-
if (tur.totalDurationMs != null) usage.durationMs = tur.totalDurationMs;
|
|
1193
|
-
const hasUsage = Object.keys(usage).length > 0;
|
|
1194
|
-
const usageText = hasUsage
|
|
1195
|
-
? formatTaskUsage({ subagentTokens: usage.tokens, toolUses: usage.tools, durationMs: usage.durationMs })
|
|
1196
|
-
: null;
|
|
1205
|
+
// Foreground agents report their cost on the completion toolUseResult;
|
|
1206
|
+
// background agents report the same numbers in their <task-notification>.
|
|
1207
|
+
const { usage, usageText } = agentUsageEntry(tur.totalTokens, tur.totalToolUseCount, tur.totalDurationMs)
|
|
1208
|
+
|| { usage: null, usageText: null };
|
|
1197
1209
|
const blocks = obj.message?.content;
|
|
1198
1210
|
if (Array.isArray(blocks)) {
|
|
1199
1211
|
for (const b of blocks) {
|
|
1200
1212
|
if (b.type === 'tool_result' && b.tool_use_id && !map[b.tool_use_id]) {
|
|
1201
|
-
map[b.tool_use_id] = { agentId: tur.agentId, prompt: tur.prompt || null, usage
|
|
1213
|
+
map[b.tool_use_id] = { agentId: tur.agentId, prompt: tur.prompt || null, usage, usageText };
|
|
1202
1214
|
}
|
|
1203
1215
|
}
|
|
1204
1216
|
}
|
|
@@ -1207,6 +1219,7 @@ function buildSessionDigest(jsonlPath) {
|
|
|
1207
1219
|
}
|
|
1208
1220
|
}
|
|
1209
1221
|
for (const [key, entry] of Object.entries(map)) {
|
|
1222
|
+
if (!entry.usage && usageByToolUseId[key]) Object.assign(entry, usageByToolUseId[key]);
|
|
1210
1223
|
if (nameByToolUseId[key]) entry.name = nameByToolUseId[key];
|
|
1211
1224
|
if (descByToolUseId[key]) entry.description = descByToolUseId[key];
|
|
1212
1225
|
// Prefer the full prompt from the assistant's tool_use input — Claude Code's
|
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,
|
|
@@ -2883,10 +2891,8 @@ function showAgentModal(agentId) {
|
|
|
2883
2891
|
return `<span class="agent-chip${cls}"${style}${title}>${labelHtml}<span class="agent-chip-val">${value}</span></span>`;
|
|
2884
2892
|
};
|
|
2885
2893
|
|
|
2886
|
-
// Agent cost stats live on the launch row's message (resolved from the completion
|
|
2887
|
-
// toolUseResult), not on the agent-activity record — so read them from there.
|
|
2888
2894
|
const agentMsg = currentMessages.find((m) => m.tool === 'Agent' && m.agentId === agentId);
|
|
2889
|
-
const usageStats =
|
|
2895
|
+
const usageStats = agent.usage || null;
|
|
2890
2896
|
const fmtTok = fmtTokens;
|
|
2891
2897
|
|
|
2892
2898
|
const chips = [];
|
|
@@ -2953,6 +2959,19 @@ function highlightSelectedAgent() {
|
|
|
2953
2959
|
//#region RENDERING
|
|
2954
2960
|
let revealedPlanSessionId = null;
|
|
2955
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
|
+
|
|
2956
2975
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
2957
2976
|
async function revealPlanSession(planSessionId) {
|
|
2958
2977
|
if (revealedPlanSessionId === planSessionId) {
|
|
@@ -2961,13 +2980,7 @@ async function revealPlanSession(planSessionId) {
|
|
|
2961
2980
|
return;
|
|
2962
2981
|
}
|
|
2963
2982
|
revealedPlanSessionId = planSessionId;
|
|
2964
|
-
|
|
2965
|
-
lastSessionsHash = '';
|
|
2966
|
-
await fetchSessions();
|
|
2967
|
-
}
|
|
2968
|
-
await fetchTasks(planSessionId);
|
|
2969
|
-
const el = document.querySelector(`.session-item[data-session-id="${CSS.escape(planSessionId)}"]`);
|
|
2970
|
-
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
2983
|
+
await revealSession(planSessionId);
|
|
2971
2984
|
}
|
|
2972
2985
|
|
|
2973
2986
|
async function showAllTasks() {
|
|
@@ -3019,15 +3032,10 @@ function renderAllTasks() {
|
|
|
3019
3032
|
renderKanban();
|
|
3020
3033
|
}
|
|
3021
3034
|
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
// Update project dropdown
|
|
3027
|
-
updateProjectDropdown();
|
|
3028
|
-
|
|
3029
|
-
// Filter pipeline: active filter → force-include revealed/current (non-pinned) sessions →
|
|
3030
|
-
// 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() {
|
|
3031
3039
|
let filteredSessions = sessions;
|
|
3032
3040
|
if (sessionFilter === 'active') {
|
|
3033
3041
|
const activeSessionIds = new Set();
|
|
@@ -3111,6 +3119,18 @@ function renderSessions() {
|
|
|
3111
3119
|
if (missingPinned.length) filteredSessions = [...missingPinned, ...filteredSessions];
|
|
3112
3120
|
}
|
|
3113
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
|
+
|
|
3114
3134
|
if (filteredSessions.length === 0) {
|
|
3115
3135
|
let emptyMsg = 'No sessions found';
|
|
3116
3136
|
let emptyHint = 'Tasks appear when you use Claude Code';
|
|
@@ -3142,13 +3162,8 @@ function renderSessions() {
|
|
|
3142
3162
|
const total = session.taskCount;
|
|
3143
3163
|
const percent = total > 0 ? Math.round((session.completed / total) * 100) : 0;
|
|
3144
3164
|
const isActive = session.id === currentSessionId && viewMode === 'session';
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
const isLive = session.hasRecentLog && Date.now() - Date.parse(session.modifiedAt) <= LIVE_INDICATOR_MS;
|
|
3148
|
-
const rawName = session.name || session.id;
|
|
3149
|
-
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)
|
|
3150
|
-
? rawName.slice(0, 8)
|
|
3151
|
-
: rawName;
|
|
3165
|
+
const isLive = isSessionLive(session);
|
|
3166
|
+
const sessionName = sessionDisplayName(session);
|
|
3152
3167
|
const useGrouped = sessionFilter === 'active' && session.project;
|
|
3153
3168
|
const primaryName = useGrouped ? sessionName : session.project ? session.project.split('/').pop() : sessionName;
|
|
3154
3169
|
const secondaryName = useGrouped ? null : session.project ? sessionName : null;
|
|
@@ -3214,6 +3229,15 @@ function renderSessions() {
|
|
|
3214
3229
|
const active = arr.reduce((n, s) => n + (isSessionActive(s) ? 1 : 0), 0);
|
|
3215
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>`;
|
|
3216
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
|
+
|
|
3217
3241
|
const renderGroupSessions = (sessions, pinKey) => {
|
|
3218
3242
|
if (!groupPinned || pinnedSessionIds.size === 0) return sessions.map(renderSessionCard).join('');
|
|
3219
3243
|
const gPinned = sessions.filter((s) => isPlacedPinned(s.id) && !isPlacedSticky(s.id));
|
|
@@ -3300,7 +3324,7 @@ function renderSessions() {
|
|
|
3300
3324
|
// flat = the "All sessions" view, which has no project sub-blocks.
|
|
3301
3325
|
const sgSectionHtml = (flat) => {
|
|
3302
3326
|
if (sessionGroups.length === 0) return '';
|
|
3303
|
-
let out = '
|
|
3327
|
+
let out = '';
|
|
3304
3328
|
for (const group of sessionGroups) {
|
|
3305
3329
|
const bucket = sgBuckets.get(group.id) || [];
|
|
3306
3330
|
if (bucket.length === 0 && sgFiltering) continue;
|
|
@@ -3343,7 +3367,8 @@ function renderSessions() {
|
|
|
3343
3367
|
out += sgHeaderHtml(group, countHtml(bucket));
|
|
3344
3368
|
out += `<div class="session-group-sessions${collapsed ? ' collapsed' : ''}" data-group-id="${escapeHtml(group.id)}">${body}</div>`;
|
|
3345
3369
|
}
|
|
3346
|
-
return
|
|
3370
|
+
if (!out) return '';
|
|
3371
|
+
return sectionHtml(SECTION_GROUPS, 'Groups', false, out);
|
|
3347
3372
|
};
|
|
3348
3373
|
|
|
3349
3374
|
let html = '';
|
|
@@ -3391,17 +3416,19 @@ function renderSessions() {
|
|
|
3391
3416
|
stableGroupOrder = [...keptOrder, ...newPaths];
|
|
3392
3417
|
const sortedGroups = stableGroupOrder.filter((p) => groups.has(p)).map((p) => [p, groups.get(p)]);
|
|
3393
3418
|
|
|
3394
|
-
//
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
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
|
+
}
|
|
3401
3428
|
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3429
|
+
if (ungrouped.length > 0 && sortedGroups.length > 0) {
|
|
3430
|
+
const isCollapsed = collapsedProjectGroups.has('__ungrouped__');
|
|
3431
|
+
projectsHtml += `
|
|
3405
3432
|
<div class="project-group-header${isCollapsed ? ' collapsed' : ''}" data-group-path="__ungrouped__">
|
|
3406
3433
|
${groupChevronSvg()}
|
|
3407
3434
|
<span class="group-name">Ungrouped</span>
|
|
@@ -3411,17 +3438,20 @@ function renderSessions() {
|
|
|
3411
3438
|
${renderGroupSessions(ungrouped, '__pinned___ungrouped__')}
|
|
3412
3439
|
</div>
|
|
3413
3440
|
`;
|
|
3414
|
-
|
|
3415
|
-
|
|
3441
|
+
} else {
|
|
3442
|
+
projectsHtml += ungrouped.map(renderSessionCard).join('');
|
|
3443
|
+
}
|
|
3416
3444
|
}
|
|
3417
3445
|
|
|
3446
|
+
html += sectioned ? sectionHtml(SECTION_PROJECTS, 'Projects', true, projectsHtml) : projectsHtml;
|
|
3447
|
+
|
|
3418
3448
|
sessionsList.innerHTML = html;
|
|
3419
3449
|
} else {
|
|
3420
|
-
const
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
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;
|
|
3425
3455
|
}
|
|
3426
3456
|
|
|
3427
3457
|
const navItems = getNavigableItems();
|
|
@@ -4416,14 +4446,24 @@ function getKbId(el) {
|
|
|
4416
4446
|
return el.dataset.sessionId || el.dataset.groupPath || null;
|
|
4417
4447
|
}
|
|
4418
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
|
+
|
|
4419
4461
|
function getGroupSessionsContainer(header) {
|
|
4420
|
-
const cls = header.classList.contains(
|
|
4421
|
-
|
|
4422
|
-
: header.classList.contains('session-group-header')
|
|
4423
|
-
? 'session-group-sessions'
|
|
4424
|
-
: 'project-group-sessions';
|
|
4462
|
+
const cls = Object.keys(COLLAPSIBLE_BODY_CLASS).find((c) => header.classList.contains(c));
|
|
4463
|
+
if (!cls) return null;
|
|
4425
4464
|
let el = header.nextElementSibling;
|
|
4426
|
-
|
|
4465
|
+
const bodyCls = COLLAPSIBLE_BODY_CLASS[cls];
|
|
4466
|
+
while (el && !el.classList.contains(bodyCls)) el = el.nextElementSibling;
|
|
4427
4467
|
return el;
|
|
4428
4468
|
}
|
|
4429
4469
|
|
|
@@ -4450,16 +4490,27 @@ function getNavigableItems() {
|
|
|
4450
4490
|
}
|
|
4451
4491
|
}
|
|
4452
4492
|
};
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
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);
|
|
4458
4510
|
}
|
|
4459
|
-
} else if (el.classList.contains('session-item')) {
|
|
4460
|
-
items.push(el);
|
|
4461
4511
|
}
|
|
4462
|
-
}
|
|
4512
|
+
};
|
|
4513
|
+
walkTopLevel(sessionsList.children);
|
|
4463
4514
|
return items;
|
|
4464
4515
|
}
|
|
4465
4516
|
|
|
@@ -4543,6 +4594,10 @@ function expandActiveGroups({ onlyNew = false } = {}) {
|
|
|
4543
4594
|
// A named group wrapping that project (or the session itself) would keep it hidden.
|
|
4544
4595
|
const group = sgGroupForSession(s);
|
|
4545
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
|
+
}
|
|
4546
4601
|
}
|
|
4547
4602
|
prevActiveSessionIds = activeIds;
|
|
4548
4603
|
if (changed) persistCollapsedGroups();
|
|
@@ -4550,11 +4605,7 @@ function expandActiveGroups({ onlyNew = false } = {}) {
|
|
|
4550
4605
|
}
|
|
4551
4606
|
|
|
4552
4607
|
function isGroupHeader(el) {
|
|
4553
|
-
return (
|
|
4554
|
-
el.classList.contains('project-group-header') ||
|
|
4555
|
-
el.classList.contains('pinned-sub-header') ||
|
|
4556
|
-
el.classList.contains('session-group-header')
|
|
4557
|
-
);
|
|
4608
|
+
return el.matches(COLLAPSIBLE_HEADER_SELECTOR);
|
|
4558
4609
|
}
|
|
4559
4610
|
|
|
4560
4611
|
function findParentHeader(el) {
|
|
@@ -5032,19 +5083,122 @@ async function confirmDelete() {
|
|
|
5032
5083
|
//#endregion
|
|
5033
5084
|
|
|
5034
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
|
+
|
|
5035
5196
|
function showHelpModal() {
|
|
5036
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);
|
|
5037
5201
|
modal.classList.add('visible');
|
|
5038
|
-
|
|
5039
|
-
// Handle keyboard shortcuts
|
|
5040
|
-
const keyHandler = (e) => {
|
|
5041
|
-
if (e.key === 'Escape' || e.key === '?') {
|
|
5042
|
-
e.preventDefault();
|
|
5043
|
-
closeHelpModal();
|
|
5044
|
-
document.removeEventListener('keydown', keyHandler);
|
|
5045
|
-
}
|
|
5046
|
-
};
|
|
5047
|
-
document.addEventListener('keydown', keyHandler);
|
|
5048
5202
|
}
|
|
5049
5203
|
|
|
5050
5204
|
function closeHelpModal() {
|
|
@@ -5320,13 +5474,7 @@ function _renderStorageSessions() {
|
|
|
5320
5474
|
async function _storageViewSession(id) {
|
|
5321
5475
|
closeStorageManager();
|
|
5322
5476
|
revealedStorageSessionId = id;
|
|
5323
|
-
|
|
5324
|
-
lastSessionsHash = '';
|
|
5325
|
-
await fetchSessions();
|
|
5326
|
-
}
|
|
5327
|
-
await fetchTasks(id);
|
|
5328
|
-
const el = document.querySelector(`.session-item[data-session-id="${CSS.escape(id)}"]`);
|
|
5329
|
-
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
5477
|
+
await revealSession(id);
|
|
5330
5478
|
}
|
|
5331
5479
|
|
|
5332
5480
|
function _storageUnpinSession(id) {
|
|
@@ -5618,6 +5766,7 @@ const MODAL_CLOSERS = {
|
|
|
5618
5766
|
'team-modal': () => closeTeamModal(),
|
|
5619
5767
|
'agent-modal': () => closeAgentModal(),
|
|
5620
5768
|
'help-modal': () => closeHelpModal(),
|
|
5769
|
+
'session-picker-modal': () => closeSessionPicker(),
|
|
5621
5770
|
};
|
|
5622
5771
|
|
|
5623
5772
|
document.addEventListener('keydown', (e) => {
|
|
@@ -5651,6 +5800,9 @@ document.addEventListener('keydown', (e) => {
|
|
|
5651
5800
|
if (close) close();
|
|
5652
5801
|
else document.getElementById(topId).classList.remove('visible');
|
|
5653
5802
|
e.stopImmediatePropagation();
|
|
5803
|
+
} else if (e.key === '?' && document.getElementById('help-modal').classList.contains('visible')) {
|
|
5804
|
+
e.preventDefault();
|
|
5805
|
+
closeHelpModal();
|
|
5654
5806
|
} else if (
|
|
5655
5807
|
e.code === 'KeyM' &&
|
|
5656
5808
|
e.shiftKey &&
|
|
@@ -5720,6 +5872,12 @@ document.addEventListener('keydown', (e) => {
|
|
|
5720
5872
|
showStorageManager();
|
|
5721
5873
|
return;
|
|
5722
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
|
+
}
|
|
5723
5881
|
if (e.key === '.' || e.key === '>') {
|
|
5724
5882
|
const sid = sessionsList.querySelector('.kb-selected')?.dataset.sessionId || currentSessionId;
|
|
5725
5883
|
if (sid) {
|
|
@@ -5894,16 +6052,18 @@ document.addEventListener('keydown', (e) => {
|
|
|
5894
6052
|
}
|
|
5895
6053
|
return;
|
|
5896
6054
|
}
|
|
5897
|
-
if (e.code === 'KeyC' && e.shiftKey) {
|
|
6055
|
+
if (e.code === 'KeyC' && e.shiftKey && !e.altKey && !e.metaKey) {
|
|
5898
6056
|
e.preventDefault();
|
|
5899
6057
|
if (!contextSid) {
|
|
5900
6058
|
showToast('No session selected');
|
|
5901
6059
|
return;
|
|
5902
6060
|
}
|
|
6061
|
+
const text = e.ctrlKey ? `claude -r ${contextSid}` : contextSid;
|
|
6062
|
+
const label = e.ctrlKey ? 'resume command' : 'session id';
|
|
5903
6063
|
navigator.clipboard
|
|
5904
|
-
.writeText(
|
|
5905
|
-
.then(() => showToast(`Copied
|
|
5906
|
-
.catch(() => showToast(
|
|
6064
|
+
.writeText(text)
|
|
6065
|
+
.then(() => showToast(`Copied ${label}: ${contextSid.slice(0, 8)}`, 'success'))
|
|
6066
|
+
.catch(() => showToast(`Failed to copy ${label}`));
|
|
5907
6067
|
return;
|
|
5908
6068
|
}
|
|
5909
6069
|
if (matchKey(e, 'KeyR')) {
|
|
@@ -6863,6 +7023,13 @@ function isSessionActive(s) {
|
|
|
6863
7023
|
return s.hasRecentLog || s.inProgress > 0 || s.hasActiveAgents || s.hasWaitingForUser;
|
|
6864
7024
|
}
|
|
6865
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
|
+
|
|
6866
7033
|
function formatDate(dateStr) {
|
|
6867
7034
|
const date = new Date(dateStr);
|
|
6868
7035
|
const now = new Date();
|
|
@@ -7416,7 +7583,7 @@ document.addEventListener('click', (e) => {
|
|
|
7416
7583
|
}
|
|
7417
7584
|
}
|
|
7418
7585
|
|
|
7419
|
-
const header = e.target.closest(
|
|
7586
|
+
const header = e.target.closest(COLLAPSIBLE_HEADER_SELECTOR);
|
|
7420
7587
|
if (header) {
|
|
7421
7588
|
setGroupCollapsed(header, !collapsedProjectGroups.has(header.dataset.groupPath));
|
|
7422
7589
|
}
|
|
@@ -8029,6 +8196,112 @@ let _planSessionId = null;
|
|
|
8029
8196
|
|
|
8030
8197
|
//#endregion
|
|
8031
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
|
+
|
|
8032
8305
|
//#region PLAN
|
|
8033
8306
|
function refreshOpenPlan() {
|
|
8034
8307
|
if (!_planSessionId || !document.getElementById('plan-modal').classList.contains('visible')) return;
|
|
@@ -8665,6 +8938,7 @@ try {
|
|
|
8665
8938
|
} catch (_) {}
|
|
8666
8939
|
loadSessionGroups();
|
|
8667
8940
|
initSessionGroupsDnd();
|
|
8941
|
+
initSessionPicker();
|
|
8668
8942
|
try {
|
|
8669
8943
|
const af = JSON.parse(localStorage.getItem('activityFilter') || '[]');
|
|
8670
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
|
|
package/server.js
CHANGED
|
@@ -2007,16 +2007,16 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
2007
2007
|
// (same pattern as agentsNeedingModel below). Each field shares the same resolve flow:
|
|
2008
2008
|
// look up in progressMap by agentId, fall back to per-field extractor, persist only
|
|
2009
2009
|
// on actual change.
|
|
2010
|
+
// progressMap is keyed by tool_use_id; re-key by agentId, first value per field wins.
|
|
2010
2011
|
const byAgentId = {};
|
|
2011
|
-
const nameByAgentId = {};
|
|
2012
|
-
const descByAgentId = {};
|
|
2013
2012
|
if (meta.jsonlPath) {
|
|
2014
2013
|
try {
|
|
2015
2014
|
const progressMap = getProgressMap(meta.jsonlPath);
|
|
2016
2015
|
for (const entry of Object.values(progressMap)) {
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2016
|
+
const e = (byAgentId[entry.agentId] ||= {});
|
|
2017
|
+
for (const f of ['prompt', 'name', 'description', 'usage']) {
|
|
2018
|
+
if (entry[f] && !e[f]) e[f] = entry[f];
|
|
2019
|
+
}
|
|
2020
2020
|
}
|
|
2021
2021
|
} catch (_) {}
|
|
2022
2022
|
}
|
|
@@ -2025,12 +2025,12 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
2025
2025
|
field: 'prompt',
|
|
2026
2026
|
flag: 'promptUnavailable',
|
|
2027
2027
|
lookup: (a) => {
|
|
2028
|
-
if (byAgentId[a.agentId]) return byAgentId[a.agentId];
|
|
2028
|
+
if (byAgentId[a.agentId]?.prompt) return byAgentId[a.agentId].prompt;
|
|
2029
2029
|
try { return extractPromptFromTranscript(subagentJsonlForExtraction(meta, a.agentId)); } catch (_) { return null; }
|
|
2030
2030
|
},
|
|
2031
2031
|
},
|
|
2032
|
-
{ field: 'agentName', flag: 'agentNameUnavailable', lookup: (a) =>
|
|
2033
|
-
{ field: 'description', flag: 'descriptionUnavailable', lookup: (a) =>
|
|
2032
|
+
{ field: 'agentName', flag: 'agentNameUnavailable', lookup: (a) => byAgentId[a.agentId]?.name || null },
|
|
2033
|
+
{ field: 'description', flag: 'descriptionUnavailable', lookup: (a) => byAgentId[a.agentId]?.description || null },
|
|
2034
2034
|
];
|
|
2035
2035
|
if (meta.jsonlPath) {
|
|
2036
2036
|
for (const { field, flag, lookup } of reconcileFields) {
|
|
@@ -2133,6 +2133,11 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
2133
2133
|
}
|
|
2134
2134
|
|
|
2135
2135
|
const waitingForUser = checkWaitingForUser(agentDir, logMtime);
|
|
2136
|
+
// Attached after persistAgent so it stays response-only.
|
|
2137
|
+
for (const agent of visibleAgents) {
|
|
2138
|
+
const usage = byAgentId[agent.agentId]?.usage;
|
|
2139
|
+
if (usage) agent.usage = usage;
|
|
2140
|
+
}
|
|
2136
2141
|
res.json({ agents: visibleAgents, waitingForUser, teamColors });
|
|
2137
2142
|
} catch {
|
|
2138
2143
|
res.json({ agents: [], waitingForUser: null });
|
|
@@ -2499,7 +2504,6 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
|
2499
2504
|
msg.agentId = entry.agentId;
|
|
2500
2505
|
if (entry.description) msg.agentDescription = entry.description;
|
|
2501
2506
|
if (entry.usageText) msg.agentUsage = entry.usageText;
|
|
2502
|
-
if (entry.usage) msg.agentUsageStats = entry.usage;
|
|
2503
2507
|
if (entry.prompt && !msg.agentPrompt) msg.agentPrompt = entry.prompt;
|
|
2504
2508
|
try {
|
|
2505
2509
|
const agentFile = path.join(agentDir, `${entry.agentId}.jsonl`);
|