claude-code-kanban 3.2.4 โ†’ 3.4.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/public/app.js CHANGED
@@ -135,7 +135,11 @@ async function fetchSessions(includeTasks = true) {
135
135
  if (revealedPlanSessionId) allPinnedIds.add(revealedPlanSessionId);
136
136
  if (revealedStorageSessionId) allPinnedIds.add(revealedStorageSessionId);
137
137
  const pinnedParam = allPinnedIds.size > 0 ? `&pinned=${[...allPinnedIds].join(',')}` : '';
138
- const sessionsPromise = fetch(`/api/sessions?limit=${sessionLimit}${pinnedParam}`).then((r) => r.json());
138
+ const projectParam =
139
+ filterProject && filterProject !== '__recent__' ? `&project=${encodeURIComponent(filterProject)}` : '';
140
+ const sessionsPromise = fetch(`/api/sessions?limit=${sessionLimit}${pinnedParam}${projectParam}`).then((r) =>
141
+ r.json(),
142
+ );
139
143
 
140
144
  let newSessions, newTasks;
141
145
  if (includeTasks) {
@@ -474,9 +478,10 @@ async function fetchTasks(sessionId) {
474
478
  for (const k of Object.keys(ownerColorCache)) delete ownerColorCache[k];
475
479
  for (const k of Object.keys(teamColorMap)) delete teamColorMap[k];
476
480
  sessionJustSelected = true;
481
+ resetAgentState();
477
482
  updateUrl();
478
483
  renderSession();
479
- await fetchAgents(sessionId);
484
+ fetchAgents(sessionId);
480
485
  if (!agentLogMode) fetchMessages(sessionId);
481
486
  } catch (error) {
482
487
  console.error('Failed to fetch tasks:', error);
@@ -493,13 +498,18 @@ const _AGENT_STALE_MS = 5 * 60 * 1000; // kept for reference; no longer used for
493
498
  const WAITING_TTL_MS = 30 * 60 * 1000;
494
499
  const AGENT_LOG_MAX = 8;
495
500
 
501
+ function resetAgentState() {
502
+ currentAgents = [];
503
+ currentWaiting = null;
504
+ lastAgentsHash = '';
505
+ renderAgentFooter();
506
+ }
507
+
496
508
  async function fetchAgents(sessionId) {
497
509
  try {
498
510
  const res = await fetch(`/api/sessions/${sessionId}/agents`);
499
511
  if (!res.ok) {
500
- currentAgents = [];
501
- currentWaiting = null;
502
- renderAgentFooter();
512
+ resetAgentState();
503
513
  return;
504
514
  }
505
515
  const data = await res.json();
@@ -628,6 +638,17 @@ function toggleMessagePanel() {
628
638
  updateUrl();
629
639
  }
630
640
 
641
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
642
+ async function openSessionWithBookmarks(sessionId) {
643
+ if (!messagePanelOpen) {
644
+ const panel = document.getElementById('message-panel');
645
+ messagePanelOpen = true;
646
+ panel.classList.add('visible');
647
+ document.getElementById('message-toggle')?.classList.add('active');
648
+ }
649
+ await fetchTasks(sessionId);
650
+ }
651
+
631
652
  // biome-ignore lint/correctness/noUnusedVariables: used in HTML
632
653
  async function viewAgentLog(agentId) {
633
654
  let agent = findAgentById(agentId);
@@ -1332,6 +1353,10 @@ const MARKETPLACE_SVG =
1332
1353
  '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>';
1333
1354
  const MEMORY_SVG =
1334
1355
  '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>';
1356
+ const LINK_SVG_PATHS =
1357
+ '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>';
1358
+ const linkSvg = (size) =>
1359
+ `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${LINK_SVG_PATHS}</svg>`;
1335
1360
 
1336
1361
  //#endregion
1337
1362
 
@@ -2060,10 +2085,7 @@ async function showAllTasks() {
2060
2085
  if (agentLogMode) exitAgentLogMode();
2061
2086
  currentSessionId = null;
2062
2087
  ownerFilter = '';
2063
- currentAgents = [];
2064
- currentWaiting = null;
2065
- lastAgentsHash = '';
2066
- renderAgentFooter();
2088
+ resetAgentState();
2067
2089
  const res = await fetch('/api/tasks/all');
2068
2090
  allTasksCache = await res.json();
2069
2091
  let tasks = allTasksCache;
@@ -2237,6 +2259,8 @@ function renderSessions() {
2237
2259
  const pinClass = pinState === 'sticky' ? ' sticky' : pinState === 'pinned' ? ' pinned' : '';
2238
2260
  const pinTitle = pinState === 'pinned' || pinState === 'sticky' ? 'Unpin' : 'Pin';
2239
2261
  const showCtx = !!session.contextStatus;
2262
+ const linkedDocsCount = getSessionPreviewPaths(session.id).length;
2263
+ const bookmarksCount = loadPins(session.id).length;
2240
2264
  return `
2241
2265
  <button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${!session.hasRecentLog && !session.inProgress && !session.hasWaitingForUser ? 'stale' : ''} ${showCtx ? 'has-context' : ''}" title="${tooltip}">
2242
2266
  <span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${escapeHtml(session.id)}')" title="${pinTitle} session">${SESSION_PIN_SVG}</span>
@@ -2247,9 +2271,11 @@ function renderSessions() {
2247
2271
  <div class="session-progress">
2248
2272
  <span class="session-indicators">
2249
2273
  ${isTeam ? `<span class="team-badge" title="${memberCount} team members"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${memberCount}</span>` : ''}
2250
- ${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>` : ''}
2274
+ ${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
2251
2275
  ${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">โ„น</span>` : ''}
2252
2276
  ${session.hasPlan ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${session.id}')" title="View plan"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>` : ''}
2277
+ ${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
2278
+ ${bookmarksCount > 0 ? `<span class="bookmarks-badge" onclick="event.stopPropagation(); openSessionWithBookmarks('${session.id}')" title="${bookmarksCount} bookmarked message${bookmarksCount > 1 ? 's' : ''}"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>${bookmarksCount}</span>` : ''}
2253
2279
  ${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">๐Ÿค–</span>' : ''}
2254
2280
  ${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan โ€” click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')">๐Ÿ“‹</span>` : ''}
2255
2281
  ${session.hasWaitingForUser ? '<span class="agent-badge" title="Waiting for user">โ“</span>' : ''}
@@ -3527,6 +3553,7 @@ function _renderStorageTab() {
3527
3553
  const tab = document.querySelector('.storage-tab.active')?.dataset.tab || 'sessions';
3528
3554
  if (tab === 'sessions') body.innerHTML = _renderStorageSessions();
3529
3555
  else if (tab === 'scratchpads') body.innerHTML = _renderStorageScratchpads();
3556
+ else if (tab === 'linked-docs') body.innerHTML = _renderStorageLinkedDocs();
3530
3557
  }
3531
3558
 
3532
3559
  function _renderStorageSessions() {
@@ -3749,6 +3776,87 @@ function _storageDeleteScratchpad(key) {
3749
3776
  _updateStorageTotal();
3750
3777
  }
3751
3778
 
3779
+ function _renderStorageLinkedDocs() {
3780
+ const entries = [];
3781
+ for (let i = 0; i < localStorage.length; i++) {
3782
+ const key = localStorage.key(i);
3783
+ if (!key.startsWith(PREVIEW_STORAGE_PREFIX)) continue;
3784
+ try {
3785
+ const arr = JSON.parse(localStorage.getItem(key)) || [];
3786
+ if (Array.isArray(arr) && arr.length) {
3787
+ entries.push({ sessionId: key.slice(PREVIEW_STORAGE_PREFIX.length), paths: arr });
3788
+ }
3789
+ } catch {}
3790
+ }
3791
+ if (!entries.length) return '<div class="storage-empty">No linked documents</div>';
3792
+
3793
+ const byId = new Map(entries.map((e) => [e.sessionId, e]));
3794
+ const { groups, orphans } = _groupByProject(entries.map((e) => e.sessionId));
3795
+
3796
+ function renderDocRow(sessionId, p) {
3797
+ const name = p.split(/[\\/]/).pop();
3798
+ const sid = _escapeForJsAttr(sessionId);
3799
+ const jsPath = _escapeForJsAttr(p);
3800
+ return `<div class="storage-item" style="padding-left:24px;">
3801
+ <span class="storage-item-id" title="${escapeHtml(p)}">${escapeHtml(name)}</span>
3802
+ <div class="storage-item-actions">
3803
+ <button onclick="_storagePreviewLinkedDoc('${jsPath}')">View</button>
3804
+ <button class="danger" onclick="_storageUnlinkDoc('${sid}','${jsPath}')">Unlink</button>
3805
+ </div>
3806
+ </div>`;
3807
+ }
3808
+
3809
+ function renderSessionItem({ id, session }) {
3810
+ const entry = byId.get(id);
3811
+ if (!entry) return '';
3812
+ const eid = escapeHtml(id);
3813
+ const count = entry.paths.length;
3814
+ const header = `<div class="storage-group-header">
3815
+ <span>${_sessionLabel(session, id)} <span class="storage-item-badge">${count} doc${count > 1 ? 's' : ''}</span></span>
3816
+ <div class="storage-item-actions">
3817
+ <button class="danger" onclick="_storageClearLinkedDocs('${eid}')">Clear All</button>
3818
+ </div>
3819
+ </div>`;
3820
+ const rows = entry.paths.map((p) => renderDocRow(id, p)).join('');
3821
+ return header + rows;
3822
+ }
3823
+
3824
+ let html = '';
3825
+ for (const [project, items] of groups) {
3826
+ const count = items.length;
3827
+ html += _renderProjectGroup(
3828
+ escapeHtml(_projectLabel(project)),
3829
+ `${count} session${count > 1 ? 's' : ''}`,
3830
+ items.map(renderSessionItem).join(''),
3831
+ );
3832
+ }
3833
+ if (orphans.length) {
3834
+ html += _renderOrphanGroup(orphans.length, orphans.map(renderSessionItem).join(''));
3835
+ }
3836
+ return html;
3837
+ }
3838
+
3839
+ function _storagePreviewLinkedDoc(path) {
3840
+ closeStorageManager();
3841
+ openPreviewByPath(path);
3842
+ }
3843
+
3844
+ function _storageUnlinkDoc(sessionId, path) {
3845
+ removeSessionPreviewPath(sessionId, path);
3846
+ if (sessionId === _infoModalSessionId) refreshInfoModalLinkedDocs();
3847
+ renderSessions();
3848
+ _renderStorageTab();
3849
+ _updateStorageTotal();
3850
+ }
3851
+
3852
+ function _storageClearLinkedDocs(sessionId) {
3853
+ localStorage.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
3854
+ if (sessionId === _infoModalSessionId) refreshInfoModalLinkedDocs();
3855
+ renderSessions();
3856
+ _renderStorageTab();
3857
+ _updateStorageTotal();
3858
+ }
3859
+
3752
3860
  function _findOrphanedKeys() {
3753
3861
  const known = _getKnownSessionIds();
3754
3862
  if (!known.size) return [];
@@ -3761,6 +3869,8 @@ function _findOrphanedKeys() {
3761
3869
  if (!known.has(key.slice('pinned-messages-'.length))) orphaned.push(key);
3762
3870
  } else if (key.startsWith('scratchpad-') && !key.startsWith('scratchpad-project:')) {
3763
3871
  if (!known.has(key.slice('scratchpad-'.length))) orphaned.push(key);
3872
+ } else if (key.startsWith(PREVIEW_STORAGE_PREFIX)) {
3873
+ if (!known.has(key.slice(PREVIEW_STORAGE_PREFIX.length))) orphaned.push(key);
3764
3874
  }
3765
3875
  }
3766
3876
  return orphaned;
@@ -4036,6 +4146,224 @@ document.addEventListener('keydown', (e) => {
4036
4146
 
4037
4147
  //#endregion
4038
4148
 
4149
+ //#region MARKDOWN_PREVIEW
4150
+ const PREVIEW_STORAGE_PREFIX = 'preview-paths-';
4151
+ let currentPreviewPath = null;
4152
+
4153
+ function getSessionPreviewPaths(sessionId) {
4154
+ if (!sessionId) return [];
4155
+ try {
4156
+ const raw = localStorage.getItem(PREVIEW_STORAGE_PREFIX + sessionId);
4157
+ const arr = raw ? JSON.parse(raw) : [];
4158
+ return Array.isArray(arr) ? arr : [];
4159
+ } catch {
4160
+ return [];
4161
+ }
4162
+ }
4163
+
4164
+ function addSessionPreviewPath(sessionId, filePath) {
4165
+ if (!sessionId || !filePath) return;
4166
+ const paths = getSessionPreviewPaths(sessionId).filter((p) => p !== filePath);
4167
+ paths.unshift(filePath);
4168
+ localStorage.setItem(PREVIEW_STORAGE_PREFIX + sessionId, JSON.stringify(paths.slice(0, 20)));
4169
+ }
4170
+
4171
+ function removeSessionPreviewPath(sessionId, filePath) {
4172
+ if (!sessionId) return;
4173
+ const paths = getSessionPreviewPaths(sessionId).filter((p) => p !== filePath);
4174
+ if (paths.length) localStorage.setItem(PREVIEW_STORAGE_PREFIX + sessionId, JSON.stringify(paths));
4175
+ else localStorage.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
4176
+ }
4177
+
4178
+ function splitFrontmatter(text) {
4179
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
4180
+ if (!m) return { fm: null, body: text };
4181
+ const fm = {};
4182
+ for (const line of m[1].split(/\r?\n/)) {
4183
+ const kv = line.match(/^([A-Za-z0-9_.-]+)\s*:\s*(.*)$/);
4184
+ if (kv) fm[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '');
4185
+ }
4186
+ return { fm, body: m[2] };
4187
+ }
4188
+
4189
+ function renderFrontmatterBlock(fm) {
4190
+ const rows = Object.entries(fm)
4191
+ .map(
4192
+ ([k, v]) =>
4193
+ `<div class="fm-row"><span class="fm-k">${escapeHtml(k)}</span><span class="fm-v">${escapeHtml(String(v))}</span></div>`,
4194
+ )
4195
+ .join('');
4196
+ return `<details class="preview-fm" open><summary>frontmatter</summary><div class="fm-grid">${rows}</div></details>`;
4197
+ }
4198
+
4199
+ function openPreviewModal(filePath, content) {
4200
+ currentPreviewPath = filePath;
4201
+ document.getElementById('preview-modal-title').textContent = filePath.split(/[\\/]/).pop();
4202
+ const { fm, body } = /\.(md|markdown)$/i.test(filePath) ? splitFrontmatter(content) : { fm: null, body: content };
4203
+ document.getElementById('preview-modal-body').innerHTML =
4204
+ (fm ? renderFrontmatterBlock(fm) : '') + renderMarkdown(body);
4205
+ document.getElementById('preview-modal-meta').textContent = filePath;
4206
+ document.getElementById('preview-modal').classList.add('visible');
4207
+ updatePreviewLinkBtn();
4208
+ }
4209
+
4210
+ function isPreviewLinkedToCurrentSession() {
4211
+ if (!currentPreviewPath || !currentSessionId) return false;
4212
+ return getSessionPreviewPaths(currentSessionId).includes(currentPreviewPath);
4213
+ }
4214
+
4215
+ function updatePreviewLinkBtn() {
4216
+ const btn = document.getElementById('preview-link-btn');
4217
+ if (!btn) return;
4218
+ if (!currentSessionId) {
4219
+ btn.style.display = 'none';
4220
+ return;
4221
+ }
4222
+ btn.style.display = '';
4223
+ const linked = isPreviewLinkedToCurrentSession();
4224
+ btn.title = linked ? 'Unlink from current session' : 'Link to current session';
4225
+ btn.style.color = linked ? 'var(--accent, #5b9a6b)' : '';
4226
+ }
4227
+
4228
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML
4229
+ function togglePreviewSessionLink() {
4230
+ if (!currentPreviewPath || !currentSessionId) {
4231
+ showToast('Select a session first');
4232
+ return;
4233
+ }
4234
+ if (isPreviewLinkedToCurrentSession()) {
4235
+ removeSessionPreviewPath(currentSessionId, currentPreviewPath);
4236
+ showToast('Unlinked from session');
4237
+ } else {
4238
+ addSessionPreviewPath(currentSessionId, currentPreviewPath);
4239
+ showToast('Linked to session');
4240
+ }
4241
+ updatePreviewLinkBtn();
4242
+ if (_infoModalSessionId === currentSessionId) {
4243
+ refreshInfoModalLinkedDocs();
4244
+ }
4245
+ renderSessions();
4246
+ }
4247
+
4248
+ function refreshInfoModalLinkedDocs() {
4249
+ const bodyEl = document.getElementById('team-modal-body');
4250
+ if (!bodyEl) return;
4251
+ const existing = bodyEl.querySelector('.linked-docs-section');
4252
+ const html = renderLinkedDocsHtml(_infoModalSessionId);
4253
+ if (!existing) {
4254
+ if (!html) return;
4255
+ const planCard = bodyEl.querySelector('[data-plan-card]');
4256
+ const wrap = document.createElement('div');
4257
+ wrap.innerHTML = html;
4258
+ const node = wrap.firstElementChild;
4259
+ if (planCard?.nextSibling) planCard.parentNode.insertBefore(node, planCard.nextSibling);
4260
+ else bodyEl.appendChild(node);
4261
+ bindLinkedDocsHandlers(node, _infoModalSessionId);
4262
+ return;
4263
+ }
4264
+ if (!html) {
4265
+ existing.remove();
4266
+ return;
4267
+ }
4268
+ const wrap = document.createElement('div');
4269
+ wrap.innerHTML = html;
4270
+ const node = wrap.firstElementChild;
4271
+ existing.replaceWith(node);
4272
+ bindLinkedDocsHandlers(node, _infoModalSessionId);
4273
+ }
4274
+
4275
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML
4276
+ function closePreviewModal() {
4277
+ resetModalFullscreen('preview-modal');
4278
+ currentPreviewPath = null;
4279
+ }
4280
+
4281
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML
4282
+ function openPreviewInEditor() {
4283
+ if (!currentPreviewPath) return;
4284
+ postAndToast('/api/open-in-editor', { file: currentPreviewPath }, 'in editor');
4285
+ }
4286
+
4287
+ async function openPreviewByPath(filePath) {
4288
+ if (!filePath) return;
4289
+ try {
4290
+ const r = await fetch(`/api/preview?path=${encodeURIComponent(filePath)}`);
4291
+ if (!r.ok) {
4292
+ showToast('Preview file unavailable');
4293
+ return;
4294
+ }
4295
+ const data = await r.json();
4296
+ openPreviewModal(data.path, data.content);
4297
+ } catch {
4298
+ showToast('Failed to load preview');
4299
+ }
4300
+ }
4301
+
4302
+ function handleSessionOpenEvent(data) {
4303
+ const { id } = data;
4304
+ if (!id) return;
4305
+ const target = sessions.find((s) => s.id === id);
4306
+ if (!target) {
4307
+ showToast(`Session not found: ${id.slice(0, 8)}`);
4308
+ return;
4309
+ }
4310
+ if (sessionFilter !== 'active') {
4311
+ sessionFilter = 'active';
4312
+ const sel = document.getElementById('session-filter');
4313
+ if (sel) sel.value = 'active';
4314
+ updateUrl();
4315
+ }
4316
+ if (!isSessionActive(target)) {
4317
+ stickySessionIds.add(id);
4318
+ }
4319
+ fetchTasks(id);
4320
+ }
4321
+
4322
+ async function handlePreviewOpenEvent(data) {
4323
+ const { path: filePath, content, sessionId } = data;
4324
+ if (sessionId && sessionId !== currentSessionId) {
4325
+ if (sessions.find((s) => s.id === sessionId)) {
4326
+ await fetchTasks(sessionId);
4327
+ } else {
4328
+ showToast(`Preview received for unknown session ${sessionId.slice(0, 8)}`);
4329
+ }
4330
+ }
4331
+ openPreviewModal(filePath, content);
4332
+ }
4333
+
4334
+ function renderLinkedDocsHtml(sessionId) {
4335
+ const paths = getSessionPreviewPaths(sessionId);
4336
+ if (!paths.length) return '';
4337
+ const items = paths
4338
+ .map((p, i) => {
4339
+ const name = p.split(/[\\/]/).pop();
4340
+ return `<a href="#" class="linked-doc-link" data-idx="${i}" title="${escapeHtml(p)}" style="color:var(--accent-text);text-decoration:underline;text-decoration-style:dotted;text-underline-offset:3px;">${escapeHtml(name)}</a>`;
4341
+ })
4342
+ .join(', ');
4343
+ return `<div class="linked-docs-section" style="margin-bottom:16px;font-size:12px;">
4344
+ <div style="font-size:11px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px;">
4345
+ ${linkSvg(12)}
4346
+ <span>Linked documents</span>
4347
+ <span style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;padding:0 6px;font-size:10px;color:var(--text-secondary);">${paths.length}</span>
4348
+ </div>
4349
+ <div>${items}</div>
4350
+ </div>`;
4351
+ }
4352
+
4353
+ function bindLinkedDocsHandlers(container, sessionId) {
4354
+ if (!container) return;
4355
+ const links = container.querySelectorAll('.linked-doc-link');
4356
+ if (!links.length) return;
4357
+ const paths = getSessionPreviewPaths(sessionId);
4358
+ for (const link of links) {
4359
+ link.addEventListener('click', (e) => {
4360
+ e.preventDefault();
4361
+ openPreviewByPath(paths[+link.dataset.idx]);
4362
+ });
4363
+ }
4364
+ }
4365
+ //#endregion
4366
+
4039
4367
  //#region SSE
4040
4368
  function setupEventSource() {
4041
4369
  let retryDelay = 1000;
@@ -4149,6 +4477,15 @@ function setupEventSource() {
4149
4477
 
4150
4478
  if (data.type === 'context-update') {
4151
4479
  debouncedRefresh(data.sessionId, true);
4480
+ refreshRateLimits();
4481
+ }
4482
+
4483
+ if (data.type === 'preview:open') {
4484
+ handlePreviewOpenEvent(data);
4485
+ }
4486
+
4487
+ if (data.type === 'session:open') {
4488
+ handleSessionOpenEvent(data);
4152
4489
  }
4153
4490
 
4154
4491
  if (data.type === 'team-update') {
@@ -4590,7 +4927,7 @@ document.addEventListener('click', (e) => {
4590
4927
  function filterByProject(project) {
4591
4928
  filterProject = project || null;
4592
4929
  updateUrl();
4593
- renderSessions();
4930
+ fetchSessions(false);
4594
4931
  showAllTasks();
4595
4932
  }
4596
4933
 
@@ -4936,7 +5273,7 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
4936
5273
  _pendingPlanContent = planContent;
4937
5274
  const titleMatch = planContent.match(/^#\s+(.+)$/m);
4938
5275
  const planTitle = titleMatch ? titleMatch[1].trim() : null;
4939
- html += `<div onclick="openPlanModal()" style="margin-bottom: 16px; padding: 10px 14px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 10px; transition: all 0.15s ease;" onmouseover="this.style.borderColor='var(--accent)';this.style.background='var(--bg-hover)'" onmouseout="this.style.borderColor='var(--border)';this.style.background='var(--bg-elevated)'">
5276
+ html += `<div data-plan-card="1" onclick="openPlanModal()" style="margin-bottom: 16px; padding: 10px 14px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 10px; transition: all 0.15s ease;" onmouseover="this.style.borderColor='var(--accent)';this.style.background='var(--bg-hover)'" onmouseout="this.style.borderColor='var(--border)';this.style.background='var(--bg-elevated)'">
4940
5277
  <span style="font-size: 14px;">๐Ÿ“‹</span>
4941
5278
  <div style="flex: 1; min-width: 0;">
4942
5279
  <div style="font-size: 11px; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;">Plan</div>
@@ -4946,6 +5283,8 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
4946
5283
  </div>`;
4947
5284
  }
4948
5285
 
5286
+ html += renderLinkedDocsHtml(session.id);
5287
+
4949
5288
  // Team info section
4950
5289
  if (teamConfig) {
4951
5290
  const ownerCounts = {};
@@ -5000,6 +5339,7 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
5000
5339
  }
5001
5340
 
5002
5341
  bodyEl.innerHTML = html;
5342
+ bindLinkedDocsHandlers(bodyEl, session.id);
5003
5343
  const alreadyVisible = modal.classList.contains('visible');
5004
5344
  _infoModalSessionId = session.id;
5005
5345
  updateStickyBtnState();
@@ -5298,10 +5638,76 @@ msgContentEl.addEventListener('wheel', function (e) {
5298
5638
  }
5299
5639
  });
5300
5640
 
5641
+ const footerState = { version: null, limitsKey: null, timer: null };
5642
+ function formatResetIn(epochSec) {
5643
+ if (!epochSec) return null;
5644
+ const ms = epochSec * 1000 - Date.now();
5645
+ if (ms <= 0) return 'now';
5646
+ const m = Math.round(ms / 60000);
5647
+ if (m < 60) return `${m}m`;
5648
+ const h = Math.floor(m / 60);
5649
+ const rm = m % 60;
5650
+ if (h < 24) return rm ? `${h}h ${rm}m` : `${h}h`;
5651
+ const d = Math.floor(h / 24);
5652
+ const rh = h % 24;
5653
+ return rh ? `${d}d ${rh}h` : `${d}d`;
5654
+ }
5655
+ function makeLimitCell(label, bucket) {
5656
+ const pct = bucket?.used_percentage;
5657
+ const cell = document.createElement('span');
5658
+ cell.className = 'footer-limit-cell';
5659
+ const reset = formatResetIn(bucket?.resets_at);
5660
+ if (reset) cell.title = `${label}: resets in ${reset}`;
5661
+ cell.append(document.createTextNode(`${label} `));
5662
+ const strong = document.createElement('strong');
5663
+ strong.textContent = pct == null ? '-%' : `${Math.ceil(pct)}%`;
5664
+ cell.appendChild(strong);
5665
+ return cell;
5666
+ }
5667
+ function makeLimitSpan(rl) {
5668
+ const span = document.createElement('span');
5669
+ span.className = 'footer-limits';
5670
+ span.append(makeLimitCell('5h', rl?.five_hour), document.createTextNode(' ยท '), makeLimitCell('7d', rl?.seven_day));
5671
+ return span;
5672
+ }
5673
+ function renderSidebarFooter(rateLimits) {
5674
+ const el = document.getElementById('sidebar-footer');
5675
+ if (!el) return;
5676
+ const fh = rateLimits?.five_hour?.used_percentage ?? null;
5677
+ const sd = rateLimits?.seven_day?.used_percentage ?? null;
5678
+ const children = [];
5679
+ if (footerState.version) {
5680
+ const v = document.createElement('span');
5681
+ v.textContent = `v${footerState.version}`;
5682
+ children.push(v);
5683
+ }
5684
+ if (fh != null || sd != null) children.push(makeLimitSpan(rateLimits));
5685
+ el.replaceChildren(...children);
5686
+ }
5687
+ function refreshRateLimits() {
5688
+ if (footerState.timer) return;
5689
+ footerState.timer = setTimeout(() => {
5690
+ footerState.timer = null;
5691
+ fetch('/api/context-status')
5692
+ .then((r) => r.json())
5693
+ .then((all) => {
5694
+ const rl = Object.values(all || {}).find((e) => e?.rate_limits)?.rate_limits || null;
5695
+ const fh = rl?.five_hour?.used_percentage ?? null;
5696
+ const sd = rl?.seven_day?.used_percentage ?? null;
5697
+ const key = `${fh}|${sd}`;
5698
+ if (key === footerState.limitsKey) return;
5699
+ footerState.limitsKey = key;
5700
+ renderSidebarFooter(rl);
5701
+ })
5702
+ .catch(() => {});
5703
+ }, 1500);
5704
+ }
5301
5705
  fetch('/api/version')
5302
5706
  .then((r) => r.json())
5303
5707
  .then((d) => {
5304
- document.getElementById('sidebar-footer').textContent = `v${d.version}`;
5708
+ footerState.version = d.version;
5709
+ renderSidebarFooter(null);
5710
+ refreshRateLimits();
5305
5711
  })
5306
5712
  .catch(() => {});
5307
5713
 
package/public/index.html CHANGED
@@ -310,6 +310,33 @@
310
310
  </div>
311
311
  </div>
312
312
 
313
+ <!-- Markdown Preview Modal -->
314
+ <div id="preview-modal" class="modal-overlay" style="z-index:10002;" onclick="closePreviewModal()">
315
+ <div class="modal preview-modal-dialog" onclick="event.stopPropagation()">
316
+ <div class="modal-header">
317
+ <h3 class="modal-title" id="preview-modal-title">Preview</h3>
318
+ <div style="display:flex;gap:4px;align-items:center;">
319
+ <button id="preview-link-btn" class="icon-btn" aria-label="Link to current session" title="Link to current session" onclick="togglePreviewSessionLink()">
320
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
321
+ </button>
322
+ <button class="icon-btn" aria-label="Open in editor" title="Open in editor" onclick="openPreviewInEditor()">
323
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
324
+ </button>
325
+ <button id="preview-modal-fullscreen-btn" class="icon-btn" aria-label="Toggle fullscreen" title="Toggle fullscreen" onclick="toggleModalFullscreen('preview-modal')">
326
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
327
+ </button>
328
+ <button class="modal-close" aria-label="Close" onclick="closePreviewModal()">
329
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
330
+ </button>
331
+ </div>
332
+ </div>
333
+ <div class="modal-body" style="flex: 0 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden;">
334
+ <div id="preview-modal-body" class="rendered-md" style="word-break: break-word; font-size: 13px; line-height: 1.6; overflow-y: auto; flex: 0 1 auto; min-height: 0; padding-right: 8px;"></div>
335
+ <div id="preview-modal-meta" style="margin-top: 12px; font-size: 11px; color: var(--text-muted); flex-shrink: 0;"></div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+
313
340
  <!-- Help Modal -->
314
341
  <div id="help-modal" class="modal-overlay" onclick="closeHelpModal()">
315
342
  <div class="modal" onclick="event.stopPropagation()">
@@ -517,7 +544,7 @@
517
544
  </div>
518
545
  <div id="team-modal-body" class="modal-body"></div>
519
546
  <div class="modal-footer">
520
- <button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId)">Dismiss</button>
547
+ <button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId); closeTeamModal()">Dismiss</button>
521
548
  <button class="btn btn-primary" onclick="closeTeamModal()">Close</button>
522
549
  </div>
523
550
  </div>
@@ -644,6 +671,7 @@
644
671
  <div class="storage-tabs">
645
672
  <button class="storage-tab active" data-tab="sessions" onclick="switchStorageTab('sessions')">Sessions</button>
646
673
  <button class="storage-tab" data-tab="scratchpads" onclick="switchStorageTab('scratchpads')">Scratchpads</button>
674
+ <button class="storage-tab" data-tab="linked-docs" onclick="switchStorageTab('linked-docs')">Linked Docs</button>
647
675
  </div>
648
676
  <div class="modal-body" id="storage-modal-body" style="overflow-y:auto;min-height:200px;max-height:60vh;padding-top:16px;padding-right:8px;"></div>
649
677
  <div class="modal-footer">