loadtoagent 1.3.11 → 1.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtoagent",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
4
4
  "description": "A local desktop dashboard for Claude, Codex, Gemini, and Grok sessions.",
5
5
  "main": "main.js",
6
6
  "author": "wincube AX",
@@ -145,16 +145,53 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
145
145
  return !isProjectlessSession(session) && projectContainsPath(state.workspace, sessionOriginPath(session));
146
146
  }
147
147
 
148
+ function unlinkedLiveTmuxSessions() {
149
+ const displayedSessionIds = new Set(displaySessions().map((session) => String(session.id || "")));
150
+ const allSessionsById = new Map((state.snapshot?.sessions || []).map((session) => [String(session.id || ""), session]));
151
+ const sessions = [];
152
+ for (const distro of state.snapshot?.tmux?.distros || []) {
153
+ for (const tmuxSession of distro.sessions || []) {
154
+ for (const window of tmuxSession.windows || []) {
155
+ for (const pane of window.panes || []) {
156
+ if (!pane.agent || pane.dead || !pane.cwd || !isProviderVisible(pane.agent.provider)) continue;
157
+ const linkedSessionId = String(pane.agent.linkedSessionId || "");
158
+ const linkedSession = linkedSessionId ? allSessionsById.get(linkedSessionId) : null;
159
+ if (linkedSessionId && displayedSessionIds.has(linkedSessionId)) continue;
160
+ // A tmux process can remain alive for days after its linked AI task
161
+ // becomes idle. Do not promote that stale shell back into the Home
162
+ // project count merely because the old conversation aged out of the
163
+ // recent-session list. A still-running linked task remains eligible.
164
+ if (linkedSession && !isControlRoomSession(linkedSession)) continue;
165
+ sessions.push({
166
+ id: `tmux:${pane.id}`,
167
+ provider: pane.agent.provider,
168
+ status: "running",
169
+ originCwd: pane.cwd,
170
+ cwd: pane.cwd,
171
+ workspace: tmuxSession.name,
172
+ title: pane.agent.title || tmuxSession.name,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ }
178
+ return sessions;
179
+ }
180
+
148
181
  function renderWorkspaces() {
149
182
  const rootSessions = displaySessions().filter((session) => !session.parentId);
150
183
  const liveRootSessions = rootSessions.filter(isControlRoomSession);
184
+ const tmuxRootSessions = unlinkedLiveTmuxSessions();
185
+ const allLiveRootSessions = [...liveRootSessions, ...tmuxRootSessions];
151
186
  const projects = observedProjects(rootSessions);
152
- const liveProjects = observedProjects(liveRootSessions);
187
+ const liveProjects = observedProjects(allLiveRootSessions)
188
+ .filter((project) => Number(project.count || 0) > 0);
153
189
  const projectlessCount = rootSessions.filter(isProjectlessSession).length;
154
190
  const liveProjectlessCount = liveRootSessions.filter(isProjectlessSession).length;
155
191
  const savedWorkspaceExists = state.workspace === "all"
156
192
  || (state.workspace === PROJECTLESS_WORKSPACE && projectlessCount > 0)
157
- || projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
193
+ || projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace))
194
+ || liveProjects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
158
195
  if (!savedWorkspaceExists) state.workspace = "all";
159
196
  const projectButton = (item, compactClass = "") => `<button type="button" class="workspace-item observed-project ${compactClass} ${Number(item.liveCount || 0) ? "has-live-sessions" : ""} ${state.workspace === item.path ? "selected" : ""}"
160
197
  data-workspace="${esc(item.path)}" title="${esc(item.path)}"
@@ -188,7 +225,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
188
225
  const desktopHtml =
189
226
  `<button type="button" class="workspace-item control-room-project-chip ${state.workspace === "all" ? "selected" : ""}"
190
227
  data-workspace="all" aria-pressed="${state.workspace === "all" ? "true" : "false"}">
191
- <strong>${esc(t("control.all_projects"))}</strong><small>${liveRootSessions.length}</small>
228
+ <strong>${esc(t("control.all_projects"))}</strong><small>${allLiveRootSessions.length}</small>
192
229
  </button>` +
193
230
  liveProjects.map((item) => projectButton(item, "control-room-project-chip")).join("") +
194
231
  (liveProjectlessCount
@@ -636,6 +673,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
636
673
  sessionWorkspaceLabel,
637
674
  controlRoomProject,
638
675
  matchesWorkspaceFilter,
676
+ unlinkedLiveTmuxSessions,
639
677
  renderWorkspaces,
640
678
  renderGlobalStats,
641
679
  formatBytes,
@@ -55,8 +55,11 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
55
55
  const label = assistant ? options.assistantLabel : options.userLabel;
56
56
  const avatar = assistant ? providerInfo(session.provider).mark : "ME";
57
57
  const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
58
+ const workingIndicator = options.live
59
+ ? `<span class="chat-working-dots" aria-hidden="true"><i></i><i></i><i></i></span>`
60
+ : "";
58
61
  const answerKind = assistant && options.answerKind
59
- ? `<span class="chat-answer-kind${options.live ? " is-live" : ""}">${esc(options.answerKind)}</span>`
62
+ ? `<span class="chat-answer-kind${options.live ? " is-live" : ""}">${esc(options.answerKind)}${workingIndicator}</span>`
60
63
  : "";
61
64
  const deliveryStatusKey = {
62
65
  sending: "drawer.message_sending",
@@ -61,7 +61,9 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
61
61
  const item = event.target.closest("[data-workspace]");
62
62
  if (item) {
63
63
  const label = item.querySelector("strong")?.textContent.trim() || t("project.all");
64
- state.workspace = item.dataset.workspace;
64
+ state.workspace = item.dataset.workspace !== "all" && state.workspace === item.dataset.workspace
65
+ ? "all"
66
+ : item.dataset.workspace;
65
67
  state.visibleLimit = 30;
66
68
  renderWorkspaces();
67
69
  renderSessions("filter");
@@ -17,6 +17,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
17
17
  stableSessionSort = sessions => [...sessions],
18
18
  runtimeAgentSummary,
19
19
  liveTmuxEntries,
20
+ filteredLiveTmuxEntries,
20
21
  runtimeSeparatedOverview,
21
22
  focusedGraph,
22
23
  scheduleAgentWorkflowConnections,
@@ -39,6 +40,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
39
40
  && liveSessionGrid.contains(document.activeElement);
40
41
  rememberDisclosureStates(liveSessionGrid);
41
42
  const model = connectedGraphSessions(sessions);
43
+ const tmuxEntries = filteredLiveTmuxEntries(model, liveTmuxEntries(state.snapshot && state.snapshot.tmux));
42
44
  const focus =
43
45
  state.graphFocusId && model.byId.get(state.graphFocusId) && model.included.has(state.graphFocusId) ? model.byId.get(state.graphFocusId) : null;
44
46
  if (state.graphFocusId && !focus) state.graphFocusId = null;
@@ -48,7 +50,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
48
50
  : state.controlRoomSort === "context"
49
51
  ? [...rootSessions].sort((a, b) => Number((b.context && b.context.percent) || 0) - Number((a.context && a.context.percent) || 0))
50
52
  : stableSessionSort(rootSessions);
51
- if (!model.nodes.length) {
53
+ if (!model.nodes.length && !tmuxEntries.length) {
52
54
  if (!preserveFocusedComposer) liveSessionGrid.innerHTML = "";
53
55
  $("#graphBreadcrumbs").innerHTML = "";
54
56
  $("#graphResetBtn").classList.add("hidden");
@@ -78,8 +80,8 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
78
80
  $("#graphResetBtn").classList.remove("hidden");
79
81
  scheduleAgentWorkflowConnections();
80
82
  } else {
81
- const runtime = runtimeAgentSummary(model, liveTmuxEntries(state.snapshot && state.snapshot.tmux));
82
- if (!preserveFocusedComposer) liveSessionGrid.innerHTML = runtimeSeparatedOverview(roots, model, roots);
83
+ const runtime = runtimeAgentSummary(model, tmuxEntries);
84
+ if (!preserveFocusedComposer) liveSessionGrid.innerHTML = runtimeSeparatedOverview(roots, model, roots, tmuxEntries);
83
85
  restoreDisclosureStates(liveSessionGrid);
84
86
  $("#graphBreadcrumbs").innerHTML = "";
85
87
  $("#agentMapToolbar")?.classList.add("hidden");
@@ -87,7 +89,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
87
89
  $("#controlRoomListToolbar")?.classList.remove("hidden");
88
90
  syncControlRoomDisclosureButtons();
89
91
  $("#graphResetBtn").classList.add("hidden");
90
- return runtime.activeCount;
92
+ return runtime.activeCount + tmuxEntries.length;
91
93
  }
92
94
  return model.nodes.filter(isControlRoomSession).length;
93
95
  }
@@ -35,6 +35,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
35
35
  graphDescendantCount,
36
36
  sessionWorkspaceLabel,
37
37
  controlRoomProject = session => ({ key: String(session?.workspace || session?.id || "unknown"), label: sessionWorkspaceLabel(session) }),
38
+ matchesWorkspaceFilter = () => true,
38
39
  ensureProjectOrder = projectKeys => projectKeys,
39
40
  } = context;
40
41
  const t = (key, params) => window.LoadToAgentI18n.t(key, params);
@@ -280,11 +281,49 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
280
281
  );
281
282
  }
282
283
 
284
+ function tmuxEntrySession(entry) {
285
+ return {
286
+ id: `tmux:${entry?.pane?.id || ""}`,
287
+ provider: entry?.agent?.provider || "",
288
+ status: "running",
289
+ title: entry?.agent?.title || entry?.tmuxSession?.name || "",
290
+ workspace: "",
291
+ originCwd: entry?.pane?.cwd || "",
292
+ cwd: entry?.pane?.cwd || "",
293
+ };
294
+ }
295
+
296
+ function filteredLiveTmuxEntries(model, entries = liveTmuxEntries(state.snapshot && state.snapshot.tmux)) {
297
+ const sessionsById = new Map((state.snapshot?.sessions || []).map((session) => [String(session.id || ""), session]));
298
+ const modelSessionIds = new Set((model?.nodes || []).map((session) => String(session.id || "")));
299
+ const query = String(state.search || "").replace(/\s+/g, " ").trim().toLowerCase();
300
+ return entries.filter((entry) => {
301
+ const linkedId = String(entry?.agent?.linkedSessionId || "");
302
+ const linkedSession = linkedId ? sessionsById.get(linkedId) : null;
303
+ if (linkedId && modelSessionIds.has(linkedId)) return false;
304
+ if (linkedSession && !isControlRoomSession(linkedSession)) return false;
305
+ const session = tmuxEntrySession(entry);
306
+ if (!session.originCwd || !matchesWorkspaceFilter(session)) return false;
307
+ if (!query) return true;
308
+ return [
309
+ session.title,
310
+ session.workspace,
311
+ session.originCwd,
312
+ entry?.distro?.name,
313
+ entry?.window?.name,
314
+ entry?.pane?.command,
315
+ entry?.agent?.command,
316
+ ].join(" ").toLowerCase().includes(query);
317
+ });
318
+ }
319
+
283
320
  function liveTmuxPaneCard(entry) {
284
321
  const { distro, tmuxSession, window, pane, agent } = entry;
285
322
  const provider = providerInfo(agent.provider);
286
323
  const linked = agent.linkedSessionId ? (state.snapshot?.sessions || []).find((session) => session.id === agent.linkedSessionId) || null : null;
287
- const title = linked ? linked.title : pane.title || t("graph.tmux_task", { provider: provider.label });
324
+ const title = linked
325
+ ? linked.title
326
+ : agent.title || pane.title || t("graph.tmux_task", { provider: provider.label });
288
327
  const stateLabel = pane.dead ? t("graph.ended") : pane.active ? t("graph.selected_pane") : t("graph.background_running");
289
328
  return `<article class="live-tmux-card ${pane.active ? "active" : ""} ${pane.dead ? "dead" : ""}"
290
329
  style="${providerStyle(agent.provider)}"
@@ -525,50 +564,69 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
525
564
  </article>`;
526
565
  }
527
566
 
528
- function runtimeSeparatedOverview(roots, model, allRoots = roots) {
567
+ function runtimeSeparatedOverview(roots, model, allRoots = roots, tmuxEntries = filteredLiveTmuxEntries(model)) {
529
568
  const projectDescriptor = (root) => {
530
569
  const project = controlRoomProject(root);
531
570
  return { key: project.key, name: project.label };
532
571
  };
572
+ const addGroupItem = (groups, key, name, field, value) => {
573
+ if (!groups.has(key)) groups.set(key, { name, roots: [], tmuxEntries: [] });
574
+ groups.get(key)[field].push(value);
575
+ };
533
576
  const allGroups = new Map();
534
577
  allRoots.forEach((root) => {
535
578
  const { key, name } = projectDescriptor(root);
536
- if (!allGroups.has(key)) allGroups.set(key, { name, roots: [] });
537
- allGroups.get(key).roots.push(root);
579
+ addGroupItem(allGroups, key, name, "roots", root);
538
580
  });
539
581
  const groups = new Map();
540
582
  roots.forEach((root) => {
541
583
  const { key, name } = projectDescriptor(root);
542
- if (!groups.has(key)) groups.set(key, { name, roots: [] });
543
- groups.get(key).roots.push(root);
584
+ addGroupItem(groups, key, name, "roots", root);
585
+ });
586
+ tmuxEntries.forEach((entry) => {
587
+ const { key, name } = projectDescriptor(tmuxEntrySession(entry));
588
+ addGroupItem(allGroups, key, name, "tmuxEntries", entry);
589
+ addGroupItem(groups, key, name, "tmuxEntries", entry);
544
590
  });
545
591
  const defaultOrderedGroups = [...groups.entries()].sort(([leftKey], [rightKey]) =>
546
- Number(allGroups.get(rightKey)?.roots.length || 0) - Number(allGroups.get(leftKey)?.roots.length || 0));
592
+ Number((allGroups.get(rightKey)?.roots.length || 0) + (allGroups.get(rightKey)?.tmuxEntries.length || 0))
593
+ - Number((allGroups.get(leftKey)?.roots.length || 0) + (allGroups.get(leftKey)?.tmuxEntries.length || 0)));
547
594
  const projectOrder = ensureProjectOrder(defaultOrderedGroups.map(([key]) => key));
548
595
  const projectRank = new Map(projectOrder.map((key, index) => [key, index]));
549
596
  const orderedGroups = defaultOrderedGroups.sort(([leftKey], [rightKey]) =>
550
597
  Number(projectRank.get(leftKey) ?? Number.MAX_SAFE_INTEGER) - Number(projectRank.get(rightKey) ?? Number.MAX_SAFE_INTEGER));
551
- const projectGroups = orderedGroups.map(([key, { name, roots: projectRoots }], index) => {
552
- const projectTotals = allGroups.get(key)?.roots || projectRoots;
553
- const activeCount = projectTotals.filter((root) => isLiveSession(root)).length;
554
- const attentionCount = projectTotals.filter((root) =>
598
+ const projectGroups = orderedGroups.map(([key, { name, roots: projectRoots, tmuxEntries: projectTmuxEntries }], index) => {
599
+ const projectTotals = allGroups.get(key) || { roots: projectRoots, tmuxEntries: projectTmuxEntries };
600
+ const activeCount = projectTotals.roots.filter((root) => isLiveSession(root)).length;
601
+ const attentionCount = projectTotals.roots.filter((root) =>
555
602
  !isLiveSession(root)
556
603
  && isControlRoomSession(root)
557
604
  && ["waiting", "failed", "paused"].includes(root.status)).length;
558
- const summary = attentionCount
605
+ const sessionSummary = attentionCount
559
606
  ? t("control.project_live_attention_summary", { active: activeCount, attention: attentionCount })
560
607
  : t("control.project_live_summary", { active: activeCount });
608
+ const tmuxSummary = projectTotals.tmuxEntries.length
609
+ ? t("control.project_tmux_summary", { count: projectTotals.tmuxEntries.length })
610
+ : "";
611
+ const summary = projectTotals.roots.length ? [sessionSummary, tmuxSummary].filter(Boolean).join(" · ") : tmuxSummary;
561
612
  const disclosureKey = `control-project:${key}`;
562
613
  const presentation = index === 0 ? "is-primary" : "is-secondary";
563
614
  const projectFocusId = projectRoots[0]?.id || "";
615
+ const totalCount = projectTotals.roots.length + projectTotals.tmuxEntries.length;
616
+ const tmuxCards = projectTmuxEntries.length
617
+ ? `<section class="control-project-tmux-section">
618
+ <header><span><i aria-hidden="true">▦</i><b>${esc(t("graph.tmux_session"))}</b></span><small>${esc(t("control.project_tmux_help"))}</small></header>
619
+ <div class="live-tmux-grid">${projectTmuxEntries.map((entry) => liveTmuxPaneCard(entry)).join("")}</div>
620
+ </section>`
621
+ : "";
564
622
  return `<details class="control-room-project-group ${presentation}" data-control-project="${esc(name)}" data-project-sortable="${esc(key)}" data-disclosure-key="${esc(disclosureKey)}">
565
623
  <summary class="control-project-header" data-project-toggle="${esc(name)}" draggable="true" aria-grabbed="false"
566
624
  aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown" aria-label="${esc(t("project.drag_label", { name }))}" aria-describedby="projectReorderHelp">
567
- <span class="control-project-heading"><i aria-hidden="true">□</i><b>${esc(name)}</b><small>${esc(summary)}</small><em>${projectTotals.length}</em></span>
625
+ <span class="control-project-heading"><i aria-hidden="true">□</i><b>${esc(name)}</b><small>${esc(summary)}</small><em>${totalCount}</em></span>
568
626
  <span class="control-project-handle" aria-hidden="true" title="${esc(t("project.reorder_hint"))}"></span>
569
627
  </summary>
570
- <button type="button" class="control-project-flow-link" data-graph-focus="${esc(projectFocusId)}"><span>${esc(t("control.open_full_flow"))} ↗</span></button>
571
- <div class="control-project-body">${projectRoots.map(root => controlRoomSession(root, model)).join("")}</div>
628
+ ${projectFocusId ? `<button type="button" class="control-project-flow-link" data-graph-focus="${esc(projectFocusId)}"><span>${esc(t("control.open_full_flow"))} ↗</span></button>` : ""}
629
+ <div class="control-project-body">${projectRoots.map(root => controlRoomSession(root, model)).join("")}${tmuxCards}</div>
572
630
  </details>`;
573
631
  }).join("");
574
632
  return `<div class="control-room-overview" data-control-room-overview="true">
@@ -892,7 +950,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
892
950
  }
893
951
 
894
952
  return {
895
- graphNode, compactGraphNode, providerFlowLane, workflowCompactNode, liveTmuxEntries, liveTmuxPaneCard, runtimeSeparatedOverview,
953
+ graphNode, compactGraphNode, providerFlowLane, workflowCompactNode, liveTmuxEntries, tmuxEntrySession, filteredLiveTmuxEntries, liveTmuxPaneCard, runtimeSeparatedOverview,
896
954
  controlRoomIntent, controlRoomSummary, controlRoomAgentGoal, inferredExecutionSummary,
897
955
  workflowMetrics, workflowChildrenSummary, splitSubagents, completedSubagentDisclosure, agentPathTaskName, communicationEndpoint,
898
956
  workflowCommunicationPanel, executionActivityLabel, executionActivityStatus, executionActivityCard, executionActivityPanel, focusedGraph,
@@ -168,7 +168,11 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
168
168
  const graphLiveCount = showMap ? renderAgentMap(graphFilteredSessions(), motionKind) : 0;
169
169
  const regular = state.view === "all" ? sessions.filter((session) => !isControlRoomSession(session)) : state.view === "active" ? [] : sessions;
170
170
  const visible = regular.slice(0, state.visibleLimit);
171
- const resultCount = attentionView ? attentionCount : graphLiveCount + regular.length;
171
+ const resultCount = attentionView
172
+ ? attentionCount
173
+ : state.view === "active"
174
+ ? graphLiveCount
175
+ : regular.length;
172
176
  $("#sessionResultSummary").textContent = window.LoadToAgentI18n.t("quality.results_summary", { count: resultCount });
173
177
  const activeEmpty = state.view === "active" && graphLiveCount === 0;
174
178
  $("#activeEmptyState").classList.toggle("hidden", !activeEmpty);
@@ -1476,6 +1476,8 @@
1476
1476
  "control.project_filter": {"ko":"프로젝트별 실행 세션 필터","en":"Filter live sessions by project","zh-CN":"按项目筛选运行会话"},
1477
1477
  "control.project_live_attention_summary": {"ko":"실행 중 {active} · 확인 필요 {attention}","en":"{active} running · {attention} need attention","zh-CN":"运行中 {active} · 需确认 {attention}"},
1478
1478
  "control.project_live_summary": {"ko":"실행 중 {active}","en":"{active} running","zh-CN":"运行中 {active}"},
1479
+ "control.project_tmux_summary": {"ko":"tmux {count}개","en":"{count} tmux","zh-CN":"tmux {count} 个"},
1480
+ "control.project_tmux_help": {"ko":"이 프로젝트 경로에서 직접 감지한 AI 터미널","en":"AI terminals detected directly from this project path","zh-CN":"从此项目路径直接检测到的 AI 终端"},
1479
1481
  "control.recent_activity": {"ko":"최근 활동순","en":"Recent activity","zh-CN":"最近活动"},
1480
1482
  "control.search_sessions": {"ko":"실행 세션 검색","en":"Search live sessions","zh-CN":"搜索运行会话"},
1481
1483
  "control.sort_sessions": {"ko":"실행 세션 정렬","en":"Sort live sessions","zh-CN":"排序运行会话"},
@@ -1125,6 +1125,9 @@ button[aria-busy="true"] {
1125
1125
 
1126
1126
  .chat-answer-kind.is-live {
1127
1127
  position: relative;
1128
+ display: inline-flex;
1129
+ align-items: center;
1130
+ gap: 6px;
1128
1131
  padding-left: 18px;
1129
1132
  border-color: rgba(71,214,153,.3);
1130
1133
  background: rgba(34,118,83,.12);
@@ -1144,6 +1147,51 @@ button[aria-busy="true"] {
1144
1147
  animation: control-room-pulse 1.35s ease-in-out infinite;
1145
1148
  }
1146
1149
 
1150
+ .chat-working-dots {
1151
+ display: inline-flex;
1152
+ align-items: center;
1153
+ gap: 2px;
1154
+ height: 9px;
1155
+ }
1156
+
1157
+ .chat-working-dots i {
1158
+ width: 3px;
1159
+ height: 3px;
1160
+ border-radius: 50%;
1161
+ background: currentColor;
1162
+ box-shadow: 0 0 5px rgba(89,223,167,.45);
1163
+ animation: chat-working-dot 1.05s ease-in-out infinite;
1164
+ }
1165
+
1166
+ .chat-working-dots i:nth-child(2) {
1167
+ animation-delay: 140ms;
1168
+ }
1169
+
1170
+ .chat-working-dots i:nth-child(3) {
1171
+ animation-delay: 280ms;
1172
+ }
1173
+
1174
+ @keyframes chat-working-dot {
1175
+ 0%,
1176
+ 60%,
1177
+ 100% {
1178
+ opacity: .38;
1179
+ transform: translateY(1px);
1180
+ }
1181
+ 30% {
1182
+ opacity: 1;
1183
+ transform: translateY(-2px);
1184
+ }
1185
+ }
1186
+
1187
+ @media (prefers-reduced-motion: reduce) {
1188
+ .chat-working-dots i {
1189
+ opacity: .72;
1190
+ animation: none;
1191
+ transform: none;
1192
+ }
1193
+ }
1194
+
1147
1195
  .chat-delivery-progress {
1148
1196
  display: grid;
1149
1197
  gap: 11px;
@@ -780,6 +780,51 @@ body details.control-room-project-group > summary.control-project-header {
780
780
  padding: 9px 10px;
781
781
  }
782
782
 
783
+ .control-project-tmux-section {
784
+ padding: 12px 14px 14px;
785
+ border-top: 1px solid rgba(104,130,151,.17);
786
+ background: rgba(8,14,20,.42);
787
+ }
788
+
789
+ .control-project-tmux-section > header {
790
+ min-height: 28px;
791
+ display: flex;
792
+ align-items: center;
793
+ justify-content: space-between;
794
+ gap: 12px;
795
+ padding: 0 2px 8px;
796
+ }
797
+
798
+ .control-project-tmux-section > header > span {
799
+ display: inline-flex;
800
+ align-items: center;
801
+ gap: 7px;
802
+ color: #78dfbd;
803
+ }
804
+
805
+ .control-project-tmux-section > header i {
806
+ font-size: 12px;
807
+ font-style: normal;
808
+ }
809
+
810
+ .control-project-tmux-section > header b {
811
+ color: #c8ddd6;
812
+ font-size: 12px;
813
+ }
814
+
815
+ .control-project-tmux-section > header small {
816
+ overflow: hidden;
817
+ color: #708493;
818
+ font-size: 12px;
819
+ text-overflow: ellipsis;
820
+ white-space: nowrap;
821
+ }
822
+
823
+ .control-project-tmux-section .live-tmux-grid {
824
+ grid-template-columns: repeat(auto-fit,minmax(260px,1fr));
825
+ padding: 0;
826
+ }
827
+
783
828
  .control-room-project-group:not([open]) .control-project-header {
784
829
  min-height: 50px !important;
785
830
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const path = require('path');
4
4
  const { createCodexCollaboration } = require('./codexCollaboration');
5
- const { createExecutionTracker } = require('./executionActivity');
5
+ const { createExecutionTracker, reconcileExecutionActivities } = require('./executionActivity');
6
6
 
7
7
  const COLLABORATION_TOOLS = new Set([
8
8
  'spawn_agent',
@@ -459,7 +459,11 @@ function createCodexParser(dependencies) {
459
459
  const collaboration = state.collaboration.finalize(session.collaboration.retainedAgents);
460
460
  session.collaboration.spawns = collaboration.spawns;
461
461
  session.collaboration.communications = collaboration.communications;
462
- session.executions = state.executionTracker.finalize();
462
+ session.executions = reconcileExecutionActivities(state.executionTracker.finalize(), {
463
+ staleAfterMs: STALE_TURN_THRESHOLD_MS,
464
+ turnFinished: state.lastTurnCompleted,
465
+ waitingForUser: session.status === 'waiting',
466
+ });
463
467
  const windowInfo = modelContextWindow('codex', session.model, state.observedWindow);
464
468
  session.context = contextInfo(session.turnUsage.total || session.turnUsage.input, windowInfo);
465
469
 
@@ -278,11 +278,10 @@ function reconcileExecutionActivities(activities = [], options = {}) {
278
278
  if (!activity || activity.status !== 'running') return activity;
279
279
  const observedAt = Date.parse(activity.updatedAt || activity.startedAt || 0);
280
280
  const observationAge = Number.isFinite(observedAt) ? Math.max(0, now - observedAt) : Number.POSITIVE_INFINITY;
281
+ const observationStale = staleAfterMs > 0 && observationAge >= staleAfterMs;
281
282
  const foregroundEnded = activity.mode !== 'background' && turnSettled;
282
- const backgroundUnobserved = activity.mode === 'background'
283
- && staleAfterMs > 0
284
- && observationAge >= staleAfterMs;
285
- if (!foregroundEnded && !backgroundUnobserved) return activity;
283
+ const executionUnobserved = observationStale;
284
+ if (!foregroundEnded && !executionUnobserved) return activity;
286
285
  return {
287
286
  ...activity,
288
287
  status: 'unverified',