clay-server 3.0.0 → 3.1.0-beta.2

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.
Files changed (35) hide show
  1. package/lib/mates-prompts.js +3 -2
  2. package/lib/notes.js +6 -0
  3. package/lib/project-http.js +3 -6
  4. package/lib/project-session-notes.js +217 -0
  5. package/lib/project.js +84 -18
  6. package/lib/public/app.js +1 -4
  7. package/lib/public/css/icon-strip.css +190 -0
  8. package/lib/public/css/messages.css +104 -2
  9. package/lib/public/css/mobile-nav.css +0 -4
  10. package/lib/public/css/pane.css +27 -0
  11. package/lib/public/css/rewind.css +10 -0
  12. package/lib/public/css/sticky-notes.css +43 -0
  13. package/lib/public/css/title-bar.css +0 -169
  14. package/lib/public/index.html +0 -6
  15. package/lib/public/modules/app-favicon.js +10 -1
  16. package/lib/public/modules/app-home-hub.js +0 -2
  17. package/lib/public/modules/app-messages.js +18 -6
  18. package/lib/public/modules/app-projects.js +30 -43
  19. package/lib/public/modules/app-rendering.js +42 -3
  20. package/lib/public/modules/dom-refs.js +1 -0
  21. package/lib/public/modules/input.js +13 -0
  22. package/lib/public/modules/project-switcher.js +1 -2
  23. package/lib/public/modules/sidebar-mobile.js +6 -19
  24. package/lib/public/modules/sidebar-projects.js +232 -63
  25. package/lib/public/modules/split-pair-ui.js +30 -0
  26. package/lib/public/modules/sticky-note-markdown.js +89 -0
  27. package/lib/public/modules/sticky-notes.js +56 -103
  28. package/lib/sdk-bridge.js +25 -1
  29. package/lib/session-notes-mcp-server.js +39 -0
  30. package/lib/ws-schema.js +1 -0
  31. package/lib/yoke/adapters/codex.js +44 -8
  32. package/lib/yoke/mcp-bridge-server.js +7 -7
  33. package/package.json +1 -1
  34. package/lib/public/modules/branch-switcher.js +0 -178
  35. package/lib/public/modules/worktree-family.js +0 -60
@@ -20,7 +20,7 @@ import { renderMemoryList } from './mate-memory.js';
20
20
  import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
21
21
  import { handleFindInSessionResults } from './session-search.js';
22
22
  import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
23
- import { showPairDialog, handlePairCreated, handleSplitDelegation } from './split-pair-ui.js';
23
+ import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
24
24
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
25
25
  import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
26
26
  import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
@@ -33,7 +33,7 @@ import { handleTuiTranscriptState } from './tui-grab.js';
33
33
  import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTermExited, tuiModalHandleTermClosed, openTuiModal } from './tui-attention.js';
34
34
  import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult, handleEmailDefaults } from './context-sources.js';
35
35
  import { refreshEmailSettings } from './user-settings.js';
36
- import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted } from './sticky-notes.js';
36
+ import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, handleNoteWritten } from './sticky-notes.js';
37
37
  import { handleSkillInstalled, handleSkillUninstalled } from './skills.js';
38
38
  import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearPendingRewindUuid, addRewindButton } from './rewind.js';
39
39
  import { checkAdminAccess } from './admin.js';
@@ -50,7 +50,7 @@ import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWh
50
50
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
51
51
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
52
52
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
53
- import { updateProjectList, updateTitleBarProjectIdentity, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
53
+ import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
54
54
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
55
55
  import { hideHomeHub, handleHubSchedules } from './app-home-hub.js';
56
56
  import { openDm, enterDmMode, exitDmMode, handleMateCreatedInApp, updateMateIconStatus, appendDmMessage, showDmTypingIndicator, buildMateInterviewPrompt } from './app-dm.js';
@@ -85,7 +85,8 @@ export function processMessage(msg) {
85
85
  // Override title bar with mate name and re-apply color
86
86
  var _mdn = (store.get('dmTargetUser').displayName || "New Mate");
87
87
  if (headerTitleEl) headerTitleEl.textContent = _mdn;
88
- updateTitleBarProjectIdentity(_mdn, null);
88
+ var _tbpn = document.getElementById("title-bar-project-name");
89
+ if (_tbpn) _tbpn.textContent = _mdn;
89
90
  var _mc2 = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
90
91
  var _tbc2 = document.querySelector(".title-bar-content");
91
92
  if (_tbc2) { _tbc2.style.background = _mc2; _tbc2.classList.add("mate-dm-active"); }
@@ -298,7 +299,8 @@ export function processMessage(msg) {
298
299
  if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
299
300
  var _mateDN = store.get('dmTargetUser').displayName || "New Mate";
300
301
  headerTitleEl.textContent = _mateDN;
301
- updateTitleBarProjectIdentity(_mateDN, null);
302
+ var tbProjectName = document.getElementById("title-bar-project-name");
303
+ if (tbProjectName) tbProjectName.textContent = _mateDN;
302
304
  // Re-apply mate title bar styling (may be lost during project switch)
303
305
  var _mc = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
304
306
  var _tbc = document.querySelector(".title-bar-content");
@@ -306,7 +308,8 @@ export function processMessage(msg) {
306
308
  document.body.classList.add("mate-dm-active");
307
309
  } else {
308
310
  headerTitleEl.textContent = store.get('projectName');
309
- updateTitleBarProjectIdentity(msg.title || store.get('projectName'), null);
311
+ var tbProjectName = document.getElementById("title-bar-project-name");
312
+ if (tbProjectName) tbProjectName.textContent = msg.title || store.get('projectName');
310
313
  }
311
314
  updatePageTitle();
312
315
  if (msg.version) {
@@ -788,6 +791,9 @@ export function processMessage(msg) {
788
791
  } else {
789
792
  addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName, msg.delegated ? msg : null);
790
793
  }
794
+ // Worker pane: a delegated instruction starts (or replays into) a
795
+ // driven turn; surface the composer notice. Cleared on "done".
796
+ if (msg.delegated) showWorkerDelegationNotice(msg.delegatedByTitle || null);
791
797
  break;
792
798
 
793
799
  case "plan_content":
@@ -1108,6 +1114,7 @@ export function processMessage(msg) {
1108
1114
  removeMatePreThinking();
1109
1115
  setActivity(null);
1110
1116
  stopThinking();
1117
+ hideWorkerDelegationNotice();
1111
1118
  markAllToolsDone();
1112
1119
  closeToolGroup();
1113
1120
  finalizeAssistantBlock();
@@ -1441,6 +1448,11 @@ export function processMessage(msg) {
1441
1448
  handleNoteUpdated(msg);
1442
1449
  break;
1443
1450
 
1451
+ case "note_written":
1452
+ showToast((msg.byTitle || "Agent") + ' left a note: "' + (msg.preview || "") + '"');
1453
+ handleNoteWritten(msg);
1454
+ break;
1455
+
1444
1456
  case "note_deleted":
1445
1457
  handleNoteDeleted(msg);
1446
1458
  break;
@@ -32,7 +32,6 @@ import { resetRateLimitState } from './app-rate-limit.js';
32
32
  import { closeSessionInfoPopover } from './app-header.js';
33
33
  import { resetDebateState } from './debate.js';
34
34
  import { removeDebateBottomBar } from './app-debate-ui.js';
35
- import { familyOf as deriveFamily, displayProject } from './worktree-family.js';
36
35
 
37
36
  // --- Module-owned state ---
38
37
  var cachedProjects = [];
@@ -202,32 +201,6 @@ export function setCachedProjectCount(v) { cachedProjectCount = v; }
202
201
  export function getCachedRemovedProjects() { return cachedRemovedProjects; }
203
202
  export function setCachedRemovedProjects(v) { cachedRemovedProjects = v; }
204
203
 
205
- export function familyOf(slug) { return deriveFamily(cachedProjects, slug); }
206
- export function currentFamily() { return familyOf(store.get('currentSlug')); }
207
-
208
- export function updateTitleBarProjectIdentity(fallbackName, fallbackIcon) {
209
- if (store.get('dmMode') || store.get('mateProjectSlug')) {
210
- var dmNameEl = document.getElementById("title-bar-project-name");
211
- if (dmNameEl && fallbackName) dmNameEl.textContent = fallbackName;
212
- return;
213
- }
214
- var identity = displayProject(cachedProjects, store.get('currentSlug'));
215
- var name = identity ? (identity.title || identity.project || identity.slug) : fallbackName;
216
- var icon = identity ? (identity.icon || null) : (fallbackIcon || null);
217
- var nameEl = document.getElementById("title-bar-project-name");
218
- if (nameEl && name) nameEl.textContent = name;
219
- var iconEl = document.getElementById("title-bar-project-icon");
220
- if (!iconEl) return;
221
- if (icon) {
222
- iconEl.textContent = icon;
223
- parseEmojis(iconEl);
224
- iconEl.classList.add("has-icon");
225
- } else {
226
- iconEl.textContent = "";
227
- iconEl.classList.remove("has-icon");
228
- }
229
- }
230
-
231
204
  // --- Functions ---
232
205
 
233
206
  export function updateProjectList(msg) {
@@ -242,7 +215,6 @@ export function updateProjectList(msg) {
242
215
  _lastProjectsJson = projectsJson;
243
216
  }
244
217
  cachedProjects = msg.projects;
245
- store.set({ projects: cachedProjects });
246
218
  }
247
219
  if (msg.removedProjects) cachedRemovedProjects = msg.removedProjects;
248
220
  else if (msg.removedProjects === undefined) { /* keep cached */ }
@@ -354,12 +326,31 @@ export function renderProjectList() {
354
326
  });
355
327
  var st = store.snap();
356
328
  var iconStripActiveSlug = (st.mateProjectSlug && st.savedMainSlug) ? st.savedMainSlug : st.currentSlug;
357
- var activeFamily = deriveFamily(cachedProjects, iconStripActiveSlug);
358
- if (activeFamily.parent) iconStripActiveSlug = activeFamily.parent.slug;
359
329
  renderIconStrip(iconStripProjects, iconStripActiveSlug);
360
330
  // Update title bar project name and icon if it changed
361
331
  if (!st.mateProjectSlug) {
362
- updateTitleBarProjectIdentity(store.get('projectName'), null);
332
+ for (var pi = 0; pi < cachedProjects.length; pi++) {
333
+ if (cachedProjects[pi].slug === st.currentSlug) {
334
+ var updatedName = cachedProjects[pi].title || cachedProjects[pi].project;
335
+ var tbName = document.getElementById("title-bar-project-name");
336
+ if (tbName && updatedName) tbName.textContent = updatedName;
337
+ var tbIcon = document.getElementById("title-bar-project-icon");
338
+ if (tbIcon) {
339
+ var pIcon = cachedProjects[pi].icon || null;
340
+ if (pIcon) {
341
+ tbIcon.textContent = pIcon;
342
+ parseEmojis(tbIcon);
343
+ tbIcon.classList.add("has-icon");
344
+ try { localStorage.setItem("clay-project-icon-" + (st.currentSlug || "default"), pIcon); } catch (e) {}
345
+ } else {
346
+ tbIcon.textContent = "";
347
+ tbIcon.classList.remove("has-icon");
348
+ try { localStorage.removeItem("clay-project-icon-" + (st.currentSlug || "default")); } catch (e) {}
349
+ }
350
+ }
351
+ break;
352
+ }
353
+ }
363
354
  }
364
355
  // Re-apply current socket status to the active icon's dot
365
356
  var dot = getStatusDot();
@@ -566,13 +557,9 @@ function showRemoveProjectTaskDialog(slug, name, taskCount) {
566
557
  export function handleRemoveProjectResult(msg) {
567
558
  if (msg.ok) {
568
559
  var currentSlug = store.get('currentSlug');
569
- var removedProj = null;
570
- for (var ri = 0; ri < cachedProjects.length; ri++) {
571
- if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
572
- }
573
- var isWorktree = removedProj ? !!removedProj.isWorktree : msg.slug.indexOf("--") !== -1;
574
560
  if (msg.slug === currentSlug) {
575
- var parentSlug = isWorktree ? ((removedProj && removedProj.parentSlug) || msg.slug.split("--")[0]) : null;
561
+ var isWorktree = msg.slug.indexOf("--") !== -1;
562
+ var parentSlug = isWorktree ? msg.slug.split("--")[0] : null;
576
563
 
577
564
  showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
578
565
 
@@ -583,6 +570,10 @@ export function handleRemoveProjectResult(msg) {
583
570
  store.set({ connected: false });
584
571
  document.getElementById("connect-overlay").classList.add("hidden");
585
572
  if (!isWorktree) {
573
+ var removedProj = null;
574
+ for (var ri = 0; ri < cachedProjects.length; ri++) {
575
+ if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
576
+ }
586
577
  if (removedProj) {
587
578
  cachedRemovedProjects.push({
588
579
  path: removedProj.path || "",
@@ -594,7 +585,7 @@ export function handleRemoveProjectResult(msg) {
594
585
  }
595
586
  cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
596
587
  cachedProjectCount = cachedProjects.length;
597
- store.set({ currentSlug: null, projects: cachedProjects });
588
+ store.set({ currentSlug: null });
598
589
  renderProjectList();
599
590
  resetClientState();
600
591
 
@@ -604,11 +595,7 @@ export function handleRemoveProjectResult(msg) {
604
595
  showHomeHub();
605
596
  }
606
597
  } else {
607
- showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
608
- cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
609
- cachedProjectCount = cachedProjects.length;
610
- store.set({ projects: cachedProjects });
611
- renderProjectList();
598
+ showToast(msg.slug.indexOf("--") !== -1 ? "Worktree removed" : "Project removed", "success");
612
599
  }
613
600
  } else {
614
601
  showToast(msg.error || "Failed to remove project", "error");
@@ -430,6 +430,35 @@ export function addUserMessage(text, images, pastes, fromUserId, fromUserName, d
430
430
  bubble.className = "bubble";
431
431
  bubble.dir = "auto";
432
432
 
433
+ // Delegated work orders render as a self-contained card: identity lives
434
+ // in an in-card header strip (the generic bubble avatar/header are hidden
435
+ // via CSS) and the body is clean markdown on a neutral background.
436
+ if (isDelegated) {
437
+ var cardHead = document.createElement("div");
438
+ cardHead.className = "delegated-card-head";
439
+ var cardAvi = document.createElement("img");
440
+ cardAvi.className = "delegated-card-avatar";
441
+ cardAvi.src = VENDOR_AVATARS[delegatedMeta.delegatedByVendor] || VENDOR_AVATARS.claude;
442
+ cardAvi.alt = "";
443
+ cardHead.appendChild(cardAvi);
444
+ var cardHeadText = document.createElement("div");
445
+ cardHeadText.className = "delegated-card-headtext";
446
+ var eyebrow = document.createElement("span");
447
+ eyebrow.className = "delegated-card-eyebrow";
448
+ eyebrow.textContent = "Delegated task";
449
+ cardHeadText.appendChild(eyebrow);
450
+ var cardFrom = document.createElement("span");
451
+ cardFrom.className = "delegated-card-from";
452
+ cardFrom.textContent = delegatedMeta.delegatedByTitle || "Driver";
453
+ cardHeadText.appendChild(cardFrom);
454
+ cardHead.appendChild(cardHeadText);
455
+ var cardTime = document.createElement("span");
456
+ cardTime.className = "delegated-card-time";
457
+ cardTime.textContent = getMsgTime();
458
+ cardHead.appendChild(cardTime);
459
+ bubble.appendChild(cardHead);
460
+ }
461
+
433
462
  if (images && images.length > 0) {
434
463
  var imgRow = document.createElement("div");
435
464
  imgRow.className = "bubble-images";
@@ -475,9 +504,19 @@ export function addUserMessage(text, images, pastes, fromUserId, fromUserName, d
475
504
  }
476
505
 
477
506
  if (text) {
478
- var textEl = document.createElement("span");
479
- textEl.textContent = text;
480
- bubble.appendChild(textEl);
507
+ if (isDelegated) {
508
+ // Delegated instructions are agent-authored work orders: render them
509
+ // as markdown (headings, lists, code) instead of a plain text blob.
510
+ var mdEl = document.createElement("div");
511
+ mdEl.className = "md-content delegated-brief";
512
+ mdEl.innerHTML = renderMarkdown(text);
513
+ highlightCodeBlocks(mdEl);
514
+ bubble.appendChild(mdEl);
515
+ } else {
516
+ var textEl = document.createElement("span");
517
+ textEl.textContent = text;
518
+ bubble.appendChild(textEl);
519
+ }
481
520
  }
482
521
 
483
522
 
@@ -16,5 +16,6 @@ export function getSessionListEl() { return ref("session-list"); }
16
16
 
17
17
  export function getStatusDot() {
18
18
  return document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status") ||
19
+ document.querySelector("#icon-strip-projects .icon-strip-wt-item.active .icon-strip-status") ||
19
20
  document.querySelector("#icon-strip-users .icon-strip-mate.active .icon-strip-status");
20
21
  }
@@ -6,6 +6,7 @@ import { store } from './store.js';
6
6
  import { mateAvatarUrl } from './avatar.js';
7
7
  import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
8
8
  import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
9
+ import { showToast } from './utils.js';
9
10
 
10
11
  var ctx;
11
12
 
@@ -284,6 +285,18 @@ export function sendMessage() {
284
285
  if (_selVendor) payload.vendor = _selVendor;
285
286
  ctx.ws.send(JSON.stringify(payload));
286
287
 
288
+ // A turn blocked on an unanswered tool-permission request looks like a
289
+ // black hole: the new message queues silently while the dots keep
290
+ // bouncing, so users re-send the same text (and both copies land once
291
+ // the permission is answered). Point them at the pending card instead.
292
+ var pendingPerm = document.querySelector("#messages .permission-container:not(.resolved)");
293
+ if (pendingPerm) {
294
+ pendingPerm.scrollIntoView({ behavior: "smooth", block: "center" });
295
+ pendingPerm.classList.add("permission-attention");
296
+ setTimeout(function () { pendingPerm.classList.remove("permission-attention"); }, 2600);
297
+ showToast("A tool permission request is still waiting — your message is queued until you answer it.", "error");
298
+ }
299
+
287
300
  // First message commits the vendor and bumps the session into
288
301
  // has-history state. The server won't re-fire session_switched after a
289
302
  // message, so mirror the vendor-toggle/active-indicator swap locally:
@@ -19,7 +19,6 @@ import { getCachedProjects, switchProject } from './app-projects.js';
19
19
  import { openDm } from './app-dm.js';
20
20
  import { mateAvatarUrl } from './avatar.js';
21
21
  import { parseEmojis } from './markdown.js';
22
- import { switcherProjectName } from './worktree-family.js';
23
22
 
24
23
  var _projectMru = []; // project slugs, most recent first
25
24
  var _mateMru = []; // mate ids (mate_XXX), most recent first
@@ -264,7 +263,7 @@ function buildProjectEntries() {
264
263
  var unreachable = p.isWorktree && p.worktreeAccessible === false;
265
264
  return {
266
265
  key: p.slug,
267
- title: switcherProjectName(projects, p),
266
+ title: p.title || p.project || p.slug,
268
267
  icon: p.icon || null,
269
268
  fallbackIcon: 'folder',
270
269
  isCurrent: p.slug === store.get('currentSlug'),
@@ -38,7 +38,6 @@ import { showHomeHub } from './app-home-hub.js';
38
38
  import { openTerminal } from './terminal.js';
39
39
  import { requestKnowledgeList } from './mate-knowledge.js';
40
40
  import { loadRootDirectory } from './filebrowser.js';
41
- import { aggregateFamily, familyOf, parentProjects } from './worktree-family.js';
42
41
 
43
42
  // --- Mobile state ---
44
43
  var mobileChatSheetOpen = false;
@@ -148,22 +147,10 @@ function closeMobileSheet() {
148
147
  }
149
148
 
150
149
  function renderSheetProjects(listEl) {
151
- var projects = getCachedProjectList();
152
- var parents = parentProjects(projects);
153
- var activeFamily = familyOf(projects, getCachedCurrentSlug());
154
- var activeSlug = activeFamily.parent ? activeFamily.parent.slug : getCachedCurrentSlug();
155
-
156
- for (var i = 0; i < parents.length; i++) {
150
+ for (var i = 0; i < getCachedProjectList().length; i++) {
157
151
  (function (p) {
158
- var family = familyOf(projects, p.slug);
159
- var displayProject = aggregateFamily(p, family.worktrees);
160
- var hasWorktreeUnread = family.worktrees.some(function (worktree) { return worktree.unread > 0; });
161
- var hasWorktreePending = family.worktrees.some(function (worktree) { return worktree.pendingPermissions > 0; });
162
152
  var el = document.createElement("button");
163
- el.className = "mobile-project-item" + (p.slug === activeSlug ? " active" : "");
164
- if ((displayProject.pendingPermissions > 0 && p.slug !== activeSlug) || hasWorktreePending) {
165
- el.classList.add("has-pending-perm");
166
- }
153
+ el.className = "mobile-project-item" + (p.slug === getCachedCurrentSlug() ? " active" : "");
167
154
 
168
155
  var abbrev = document.createElement("span");
169
156
  abbrev.className = "mobile-project-abbrev";
@@ -180,16 +167,16 @@ function renderSheetProjects(listEl) {
180
167
  name.textContent = p.name;
181
168
  el.appendChild(name);
182
169
 
183
- if (displayProject.isProcessing) {
170
+ if (p.isProcessing) {
184
171
  var dot = document.createElement("span");
185
172
  dot.className = "mobile-project-processing";
186
173
  el.appendChild(dot);
187
174
  }
188
175
 
189
- if (displayProject.unread > 0 && (p.slug !== activeSlug || hasWorktreeUnread)) {
176
+ if (p.unread > 0 && p.slug !== getCachedCurrentSlug()) {
190
177
  var mBadge = document.createElement("span");
191
178
  mBadge.className = "mobile-project-unread";
192
- mBadge.textContent = displayProject.unread > 99 ? "99+" : String(displayProject.unread);
179
+ mBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
193
180
  el.appendChild(mBadge);
194
181
  }
195
182
 
@@ -199,7 +186,7 @@ function renderSheetProjects(listEl) {
199
186
  });
200
187
 
201
188
  listEl.appendChild(el);
202
- })(parents[i]);
189
+ })(getCachedProjectList()[i]);
203
190
  }
204
191
  }
205
192