loadtoagent 1.1.0 → 1.3.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 (86) hide show
  1. package/README.ko.md +14 -5
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +25 -1
  5. package/docs/UI-AUDIT-100.md +118 -0
  6. package/docs/UI-AUDIT-101-200.md +120 -0
  7. package/docs/UI-AUDIT-201-300.md +120 -0
  8. package/main.js +157 -38
  9. package/package.json +13 -4
  10. package/preload.js +13 -2
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -19
  13. package/renderer/app-dashboard.js +196 -55
  14. package/renderer/app-drawer-content.js +48 -44
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +72 -54
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -13
  19. package/renderer/app-events-navigation.js +77 -30
  20. package/renderer/app-events-sessions.js +156 -9
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +20 -38
  23. package/renderer/app-graph-orchestration.js +15 -13
  24. package/renderer/app-graph-view.js +228 -113
  25. package/renderer/app-management.js +257 -0
  26. package/renderer/app-provider-visibility.js +124 -0
  27. package/renderer/app-quality.js +568 -0
  28. package/renderer/app-run-modal.js +104 -34
  29. package/renderer/app-runtime-overview.js +312 -0
  30. package/renderer/app-session-render.js +94 -37
  31. package/renderer/app-tmux-render.js +52 -44
  32. package/renderer/app.js +153 -75
  33. package/renderer/i18n-messages.js +867 -37
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +154 -67
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +16 -10
  38. package/renderer/styles-cards.css +44 -18
  39. package/renderer/styles-collaboration.css +23 -23
  40. package/renderer/styles-components.css +122 -45
  41. package/renderer/styles-management.css +364 -0
  42. package/renderer/styles-onboarding.css +8 -8
  43. package/renderer/styles-overlays.css +13 -7
  44. package/renderer/styles-product.css +48 -40
  45. package/renderer/styles-quality.css +312 -0
  46. package/renderer/styles-readability.css +1040 -0
  47. package/renderer/styles-responsive-product.css +84 -12
  48. package/renderer/styles-responsive-runtime.css +25 -17
  49. package/renderer/styles-responsive-shell.css +44 -48
  50. package/renderer/styles-responsive-workflows.css +39 -5
  51. package/renderer/styles-run-composer.css +28 -23
  52. package/renderer/styles-runtime-overview.css +285 -0
  53. package/renderer/styles-settings.css +176 -16
  54. package/renderer/styles-terminal.css +374 -37
  55. package/renderer/styles-tmux.css +49 -49
  56. package/renderer/styles-workflow-map.css +362 -15
  57. package/renderer/styles-workflows.css +116 -49
  58. package/renderer/styles.css +34 -19
  59. package/renderer/terminal-agent.js +17 -13
  60. package/renderer/terminal-events.js +253 -37
  61. package/renderer/terminal-workbench.js +208 -89
  62. package/renderer/terminal.js +350 -35
  63. package/src/agentMonitor/claudeParser.js +96 -7
  64. package/src/agentMonitor/codexParser.js +59 -4
  65. package/src/agentMonitor/executionActivity.js +282 -0
  66. package/src/agentMonitor/genericParser.js +56 -13
  67. package/src/agentMonitor/hierarchy.js +17 -0
  68. package/src/agentMonitor/responseIntent.js +51 -0
  69. package/src/agentMonitor.js +21 -3
  70. package/src/agentRunner.js +66 -1
  71. package/src/attentionNotifier.js +14 -10
  72. package/src/automationMonitor.js +258 -0
  73. package/src/contracts.js +10 -1
  74. package/src/ipc/registerAgentIpc.js +9 -3
  75. package/src/ipc/registerAppIpc.js +4 -1
  76. package/src/ipc/registerTerminalIpc.js +12 -14
  77. package/src/macUpdateHelper.js +210 -0
  78. package/src/monitorWorker.js +65 -6
  79. package/src/platformPath.js +80 -0
  80. package/src/processMonitor.js +80 -22
  81. package/src/providerVisibilityStore.js +45 -0
  82. package/src/sessionIntelligence.js +261 -0
  83. package/src/terminalHost.js +453 -0
  84. package/src/terminalHostDaemon.js +61 -0
  85. package/src/terminalManager.js +218 -6
  86. package/src/updateInstaller.js +175 -0
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+
3
+ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
+
5
+ window.LoadToAgentAppFactories.createManagement = function createManagement(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
7
+ const { $, esc, state, providerInfo, timeAgo, readablePreview = value => ({ text: String(value || "") }) } = context;
8
+
9
+ const attentionLabel = kind => t(`management.attention.${kind || "response"}`);
10
+ const healthLabel = level => t(`management.health.${level || "unknown"}`);
11
+ const signalLabel = code => t(`management.signal.${code || "low-confidence"}`);
12
+ const evidenceLabel = value => t(`management.evidence.${value || "unverified"}`);
13
+ const RECENT_SESSION_WINDOW_MS = 24 * 60 * 60 * 1000;
14
+ const ALWAYS_VISIBLE_STATUSES = new Set(["starting", "running"]);
15
+ const CURRENT_RISK_STATUSES = new Set(["starting", "running", "waiting", "paused", "failed"]);
16
+ const RESPONSE_ATTENTION_KINDS = new Set(["approval", "decision", "input", "response"]);
17
+ const needsUserResponse = session => Boolean(
18
+ session.attention?.required && RESPONSE_ATTENTION_KINDS.has(session.attention.kind),
19
+ );
20
+ const sessionActivityTimestamp = session => Math.max(0, ...[
21
+ session.health?.lastActivityAt,
22
+ session.attention?.requestedAt,
23
+ session.updatedAt,
24
+ session.completedAt,
25
+ session.startedAt,
26
+ ].map(value => Date.parse(value || 0)).filter(Number.isFinite));
27
+ const isRecentSession = (session, now = Date.now()) => {
28
+ if (ALWAYS_VISIBLE_STATUSES.has(session.status)) return true;
29
+ const activityAt = sessionActivityTimestamp(session);
30
+ return Boolean(activityAt && Math.max(0, Number(now) - activityAt) <= RECENT_SESSION_WINDOW_MS);
31
+ };
32
+ const hasCurrentRisk = (session, now = Date.now()) => Boolean(
33
+ isRecentSession(session, now)
34
+ && CURRENT_RISK_STATUSES.has(session.status)
35
+ && ["critical", "warning"].includes(session.health?.level),
36
+ );
37
+ const needsManagementReview = (session, now = Date.now()) => Boolean(
38
+ isRecentSession(session, now) && (needsUserResponse(session) || hasCurrentRisk(session, now)),
39
+ );
40
+ const prioritySummary = value => {
41
+ const lines = String(value || "")
42
+ .replace(/```[\s\S]*?```/g, " ")
43
+ .split(/\r?\n/)
44
+ .map(line => {
45
+ let heading = /^\s{0,3}#{1,6}\s+/.test(line);
46
+ let text = line
47
+ .replace(/`([^`]*)`/g, "$1")
48
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
49
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
50
+ .replace(/^\s{0,3}(?:#{1,6}|>|[-*+]|\d+[.)])\s+/, "")
51
+ .replace(/\*\*|__|~~/g, "")
52
+ .replace(/\s+/g, " ")
53
+ .trim();
54
+ const inlineOrderedItem = heading ? text.match(/(?:^|\s)\d+[.)]\s+(.+)$/) : null;
55
+ if (inlineOrderedItem) {
56
+ text = inlineOrderedItem[1].trim();
57
+ heading = false;
58
+ }
59
+ return { heading, text };
60
+ })
61
+ .filter(line => line.text);
62
+ const plain = (lines.find(line => !line.heading) || lines[0])?.text || "";
63
+ const firstSentence = plain.match(/^.*?(?:[.!?。!?](?=\s|$)|$)/)?.[0]?.trim() || plain;
64
+ return readablePreview(firstSentence || t("management.signal_unavailable"), 104).text;
65
+ };
66
+
67
+ function managementBucket(session, now = Date.now()) {
68
+ if (!needsManagementReview(session, now)) return "healthy";
69
+ if (hasCurrentRisk(session, now) && session.health?.level === "critical") return "critical";
70
+ if (hasCurrentRisk(session, now) && session.health?.level === "warning") return "warning";
71
+ if (needsUserResponse(session)) return "attention";
72
+ return "healthy";
73
+ }
74
+
75
+ function matchesManagementFilter(session, filter, now = Date.now()) {
76
+ if (filter === "critical") return hasCurrentRisk(session, now) && session.health?.level === "critical";
77
+ if (filter === "warning") return hasCurrentRisk(session, now) && session.health?.level === "warning";
78
+ if (filter === "attention") return isRecentSession(session, now) && needsUserResponse(session);
79
+ return needsManagementReview(session, now);
80
+ }
81
+
82
+ function progressHtml(session, compactView = false) {
83
+ const progress = session.progress || { percent: 0, checkpoints: [] };
84
+ const checkpoints = compactView ? [] : (progress.checkpoints || []).slice(-8);
85
+ const completed = Number(progress.completedSteps || 0);
86
+ const total = Number(progress.totalSteps || 0);
87
+ const loggedRatio = total ? Math.round(completed / total * 100) : 0;
88
+ return `<section class="management-progress" data-progress-stage="${esc(progress.stage || "idle")}">
89
+ <header><span>${esc(t("management.progress"))}</span><b>${esc(t("management.progress_steps", {
90
+ completed, total,
91
+ }))}</b><strong>${completed}/${total}</strong></header>
92
+ <div class="management-progress-track" role="progressbar" aria-label="${esc(t("management.progress_steps", { completed, total }))}" aria-valuemin="0" aria-valuemax="${Math.max(1, total)}" aria-valuenow="${completed}"><i style="width:${Math.max(0, Math.min(100, loggedRatio))}%"></i></div>
93
+ <p><b>${esc(progress.currentStep || t("management.no_current_step"))}</b><span>${esc(progress.lastActivityAt ? t("management.last_signal", { time: timeAgo(progress.lastActivityAt) }) : t("management.signal_unavailable"))}</span></p>
94
+ ${progress.blocker ? `<aside><span>${esc(t("management.blocker"))}</span><b>${esc(progress.blocker)}</b></aside>` : ""}
95
+ ${checkpoints.length ? `<ol class="management-checkpoints">${checkpoints.map(row => `<li class="${esc(row.status)}"><i></i><div><b>${esc(row.label)}</b>${row.detail ? `<small>${esc(row.detail)}</small>` : ""}</div><span>${esc(row.timestamp ? timeAgo(row.timestamp) : "")}</span></li>`).join("")}</ol>` : ""}
96
+ </section>`;
97
+ }
98
+
99
+ function healthHtml(session, compactView = false) {
100
+ const health = session.health || { level: "unknown", signals: [] };
101
+ const signals = health.signals || [];
102
+ return `<section class="management-health ${esc(health.level || "unknown")}">
103
+ <header><span>${esc(t("management.health_title"))}</span><b>${esc(healthLabel(health.level))}</b><strong>${esc(t("common.count", { count: signals.length }))}</strong></header>
104
+ ${signals.length
105
+ ? `<ul>${signals.slice(0, compactView ? 2 : 8).map(signal => `<li class="${esc(signal.severity)}"><i></i><b>${esc(signalLabel(signal.code))}</b>${signal.detail ? `<span title="${esc(signal.detail)}">${esc(signal.detail)}</span>` : ""}</li>`).join("")}</ul>`
106
+ : `<p>${esc(t("management.no_health_signals"))}</p>`}
107
+ </section>`;
108
+ }
109
+
110
+ function controlButtonsHtml(session) {
111
+ const controls = session.controlCapabilities || {};
112
+ const canReassign = controls.reassign && (context.visibleProviders?.() || state.providers)
113
+ .some(provider => provider.id !== session.provider && state.availability[provider.id]);
114
+ const busy = action => state.runControlRequests.has(`${session.runId || session.id}:${action}`);
115
+ const button = (action, label, tone = "") => controls[action]
116
+ ? `<button type="button" class="${tone}" data-managed-run-action="${action}" data-management-session-id="${esc(session.id)}" ${busy(action) ? 'disabled aria-busy="true"' : ""}>${esc(t(label))}</button>`
117
+ : "";
118
+ return `<div class="management-control-buttons" aria-label="${esc(t("management.controls"))}">
119
+ ${button("pause", "management.pause")}
120
+ ${session.status === "paused" ? button("resume", "management.resume") : ""}
121
+ ${button("retry", "management.retry")}
122
+ ${canReassign ? `<button type="button" data-reassign-session="${esc(session.id)}">${esc(t("management.reassign"))}</button>` : ""}
123
+ ${button("stop", "management.stop", "danger")}
124
+ </div>`;
125
+ }
126
+
127
+ function quickActionsHtml(session) {
128
+ const attention = session.attention || {};
129
+ if (!attention.required) return "";
130
+ const controls = session.controlCapabilities || {};
131
+ const buttons = [];
132
+ // A yes/no approval message is only safe for explicit approval requests.
133
+ // Decisions may require selecting a concrete option, so those use the
134
+ // normal instruction composer instead of a misleading generic approval.
135
+ if (controls.sendInstruction && attention.kind === "approval") {
136
+ buttons.push(`<button type="button" class="approve" data-attention-session-id="${esc(session.id)}" data-attention-quick="${esc(t("management.quick.approve_text"))}">${esc(t("management.approve"))}</button>`);
137
+ buttons.push(`<button type="button" data-attention-session-id="${esc(session.id)}" data-attention-quick="${esc(t("management.quick.deny_text"))}">${esc(t("management.deny"))}</button>`);
138
+ }
139
+ buttons.push(`<button type="button" data-open-session="${esc(session.id)}">${esc(t("management.review_detail"))}</button>`);
140
+ return `<div class="management-quick-actions">${buttons.join("")}</div>`;
141
+ }
142
+
143
+ function attentionCardHtml(session) {
144
+ const provider = providerInfo(session.provider);
145
+ const attention = session.attention || {};
146
+ const health = session.health || { level: "unknown", signals: [] };
147
+ const evidence = session.evidence || {};
148
+ const cardLabel = attention.required ? attentionLabel(attention.kind) : healthLabel(health.level);
149
+ const statusSignals = (health.signals || []).map(signal => {
150
+ const label = signalLabel(signal.code);
151
+ return signal.detail ? `${label} · ${signal.detail}` : label;
152
+ }).join(" / ");
153
+ const reason = attention.required
154
+ ? (attention.summary || session.statusDetail || t("management.response_needed"))
155
+ : (statusSignals || session.statusDetail || t("management.signal_unavailable"));
156
+ return `<article class="attention-card ${esc(attention.kind || "response")}" data-management-session="${esc(session.id)}" style="--management-provider:${provider.accent}">
157
+ <header><span class="provider-mark">${esc(provider.mark)}</span><div><small>${esc(provider.label)} · ${esc(cardLabel)}</small><h3>${esc(session.title)}</h3></div><em class="confidence ${esc(evidence.confidence || "low")}">${esc(evidenceLabel(evidence.confidence))}</em></header>
158
+ <div class="attention-reason"><span>${esc(t(attention.required ? "management.requested_action" : "management.health_title"))}</span><p>${esc(reason)}</p><small>${esc(attention.requestedAt ? timeAgo(attention.requestedAt) : health.lastActivityAt ? timeAgo(health.lastActivityAt) : t("management.signal_unavailable"))}</small></div>
159
+ ${progressHtml(session, true)}
160
+ ${healthHtml(session, true)}
161
+ ${quickActionsHtml(session)}
162
+ ${session.controlCapabilities?.sendInstruction ? context.agentCommandComposer(session) : ""}
163
+ ${controlButtonsHtml(session)}
164
+ </article>`;
165
+ }
166
+
167
+ function renderAttentionInbox() {
168
+ const section = $("#attentionInbox");
169
+ if (!section) return 0;
170
+ const reviewSessions = context.filteredSessions().filter(needsManagementReview);
171
+ const filter = ["critical", "warning", "attention"].includes(state.managementFilter) ? state.managementFilter : "all";
172
+ const sessions = reviewSessions.filter(session => filter === "all" || matchesManagementFilter(session, filter));
173
+ const counts = {
174
+ critical: reviewSessions.filter(session => matchesManagementFilter(session, "critical")).length,
175
+ warning: reviewSessions.filter(session => matchesManagementFilter(session, "warning")).length,
176
+ attention: reviewSessions.filter(session => matchesManagementFilter(session, "attention")).length,
177
+ };
178
+ const filterButton = (value, label, count) => `<button type="button" data-management-inbox-filter="${value}" aria-pressed="${filter === value ? "true" : "false"}"><i></i><span>${esc(label)}</span><b>${count}</b></button>`;
179
+ section.innerHTML = `<header class="attention-inbox-head"><div><p>${esc(t("management.inbox_eyebrow"))}</p><h2>${esc(t("management.inbox_title"))}</h2><span>${esc(t("management.inbox_description"))}</span></div><strong>${sessions.length}</strong></header>
180
+ <div class="attention-inbox-summary" role="toolbar" aria-label="${esc(t("management.operations_severity_buckets"))}">
181
+ <div class="management-filter-all">${filterButton("all", t("management.filter_all"), reviewSessions.length)}</div>
182
+ <div class="management-filter-group response" role="group" aria-label="${esc(t("management.filter_group_response"))}"><small>${esc(t("management.filter_group_response"))}</small>${filterButton("attention", t("management.health.attention"), counts.attention)}</div>
183
+ <div class="management-filter-group risk" role="group" aria-label="${esc(t("management.filter_group_risk"))}"><small>${esc(t("management.filter_group_risk"))}</small>${filterButton("critical", t("management.health.critical"), counts.critical)}${filterButton("warning", t("management.health.warning"), counts.warning)}</div>
184
+ </div>
185
+ <div class="attention-card-list">${sessions.length ? sessions.map(attentionCardHtml).join("") : `<div class="management-empty"><b>${esc(t("management.inbox_empty"))}</b><span>${esc(t("management.inbox_empty_detail"))}</span></div>`}</div>`;
186
+ return sessions.length;
187
+ }
188
+
189
+ function renderOperationsOverview() {
190
+ const section = $("#operationsOverview");
191
+ if (!section) return;
192
+ const sessions = typeof context.graphFilteredSessions === "function"
193
+ ? context.graphFilteredSessions()
194
+ : (state.snapshot?.sessions || []);
195
+ const critical = sessions.filter(session => matchesManagementFilter(session, "critical"));
196
+ const warning = sessions.filter(session => matchesManagementFilter(session, "warning"));
197
+ const responses = sessions.filter(session => matchesManagementFilter(session, "attention"));
198
+ const flaggedIds = new Set([...critical, ...warning, ...responses].map(session => session.id));
199
+ const clear = sessions.filter(session => !flaggedIds.has(session.id));
200
+ const priority = [...new Map([...critical, ...warning, ...responses].map(session => [session.id, session])).values()].slice(0, 4);
201
+ const reviewCount = flaggedIds.size;
202
+ section.innerHTML = `<header>
203
+ <div class="operations-heading">
204
+ <span class="operations-signal" aria-hidden="true">!</span>
205
+ <div class="operations-heading-copy"><p>${esc(t("management.operations_eyebrow"))}</p><h2>${esc(t("management.operations_title"))}</h2><span>${esc(t("management.operations_description"))}</span></div>
206
+ </div>
207
+ <div class="operations-review-total"><strong>${reviewCount}</strong><span>${esc(t("management.operations_review_count"))}</span></div>
208
+ </header>
209
+ <div class="operations-metrics" aria-label="${esc(t("management.operations_severity_buckets"))}">
210
+ <button type="button" data-management-filter="critical" data-management-metric="critical"><span>${esc(t("management.health.critical"))}</span><b>${critical.length}</b></button>
211
+ <button type="button" data-management-filter="warning" data-management-metric="warning"><span>${esc(t("management.health.warning"))}</span><b>${warning.length}</b></button>
212
+ <button type="button" data-management-filter="attention" data-management-metric="attention"><span>${esc(t("management.health.attention"))}</span><b>${responses.length}</b></button>
213
+ <div data-management-metric="clear"><span>${esc(t("management.recent_clear"))}</span><b>${clear.length}</b></div>
214
+ </div>
215
+ ${priority.length ? `<div class="operations-priority">${priority.map(session => {
216
+ const health = session.health || {};
217
+ const responseSummary = needsUserResponse(session) ? session.attention?.summary : "";
218
+ const summary = prioritySummary(responseSummary
219
+ || (health.signals || []).map(signal => signalLabel(signal.code)).join(" · ")
220
+ || t("management.signal_unavailable"));
221
+ return `<button type="button" data-open-session="${esc(session.id)}"><i class="${esc(managementBucket(session))}"></i><span><b>${esc(session.title)}</b><small>${esc(summary)}</small></span><em>${esc(timeAgo(health.lastActivityAt || session.updatedAt))}</em></button>`;
222
+ }).join("")}</div>${reviewCount > priority.length ? `<div class="operations-more"><button type="button" data-management-filter="all"><span>${esc(t("management.operations_view_all"))}</span><b>${reviewCount}</b><i aria-hidden="true">→</i></button></div>` : ""}` : `<p class="operations-clear">${esc(t("management.operations_clear"))}</p>`}`;
223
+ }
224
+
225
+ function outcomeHtml(session) {
226
+ const outcome = session.outcome || { artifacts: [], checks: [] };
227
+ const evidence = session.evidence || { sources: [] };
228
+ const controls = session.controlCapabilities || {};
229
+ return `<div class="management-detail">
230
+ <section class="management-outcome ${esc(outcome.status || "in-progress")}">
231
+ <header><div><span>${esc(t("management.outcome"))}</span><h3>${esc(t(`management.outcome.${outcome.status || "in-progress"}`))}</h3></div><em class="${outcome.verified ? "verified" : "unverified"}">${esc(outcome.verified ? t("management.verified") : t("management.unverified"))}</em></header>
232
+ <p>${esc(outcome.summary || t("management.outcome_pending"))}</p>
233
+ </section>
234
+ ${session.attention?.required ? `<section class="management-attention-detail"><header><span>${esc(attentionLabel(session.attention.kind))}</span><b>${esc(session.attention.summary)}</b></header>${quickActionsHtml(session)}${controls.sendInstruction ? context.agentCommandComposer(session) : ""}</section>` : ""}
235
+ ${progressHtml(session)}
236
+ ${healthHtml(session)}
237
+ <section class="management-artifacts"><header><span>${esc(t("management.artifacts"))}</span><b>${esc(t("common.items", { count: (outcome.artifacts || []).length }))}</b></header>${outcome.artifacts?.length ? `<ul>${outcome.artifacts.map(item => `<li><i>${esc(item.kind)}</i><b title="${esc(item.value)}">${esc(item.value)}</b><span>${esc(t("management.detected"))}</span></li>`).join("")}</ul>` : `<p>${esc(t("management.no_artifacts"))}</p>`}</section>
238
+ <section class="management-checks"><header><span>${esc(t("management.verification_checks"))}</span><b>${esc(t("common.items", { count: (outcome.checks || []).length }))}</b></header>${outcome.checks?.length ? `<ul>${outcome.checks.map(check => `<li class="${esc(check.status)}"><i></i><b>${esc(check.label)}</b><span>${esc(t(`management.check.${check.status}`))}</span></li>`).join("")}</ul>` : `<p>${esc(t("management.no_checks"))}</p>`}</section>
239
+ <section class="management-evidence"><header><span>${esc(t("management.evidence_title"))}</span><b>${esc(evidenceLabel(evidence.confidence))}</b></header><dl><div><dt>${esc(t("management.evidence_status"))}</dt><dd>${esc(evidenceLabel(evidence.status))}</dd></div><div><dt>${esc(t("management.evidence_hierarchy"))}</dt><dd>${esc(evidenceLabel(evidence.hierarchy))}</dd></div><div><dt>${esc(t("management.evidence_completion"))}</dt><dd>${esc(evidenceLabel(evidence.completion))}</dd></div></dl><p>${esc((evidence.sources || []).join(" · ") || t("management.signal_unavailable"))}</p></section>
240
+ <section class="management-controls"><header><span>${esc(t("management.controls"))}</span><b>${esc(t("management.controls_description"))}</b></header>${controlButtonsHtml(session)}</section>
241
+ </div>`;
242
+ }
243
+
244
+ return {
245
+ isRecentSession,
246
+ managementBucket,
247
+ matchesManagementFilter,
248
+ needsManagementReview,
249
+ attentionCardHtml,
250
+ controlButtonsHtml,
251
+ healthHtml,
252
+ outcomeHtml,
253
+ progressHtml,
254
+ renderAttentionInbox,
255
+ renderOperationsOverview,
256
+ };
257
+ };
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+
3
+ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
+
5
+ window.LoadToAgentAppFactories.createProviderVisibility = function createProviderVisibility(context = {}) {
6
+ const { state, reportRecoverableError } = context;
7
+ const STORAGE_KEY = "loadtoagent:provider-visibility:v1";
8
+ const USAGE_KEYS = ["input", "cachedInput", "cacheWrite", "output", "reasoning", "total"];
9
+
10
+ function loadProviderVisibility(preference = null) {
11
+ try {
12
+ const saved = preference && typeof preference === "object"
13
+ ? preference
14
+ : JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
15
+ const known = new Set(state.providers.map((provider) => provider.id));
16
+ state.hiddenProviders = new Set((saved.hidden || []).filter((id) => known.has(id)));
17
+ } catch (error) {
18
+ reportRecoverableError("provider-visibility-load", error);
19
+ state.hiddenProviders = new Set();
20
+ }
21
+ }
22
+
23
+ function saveProviderVisibility() {
24
+ try {
25
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({ hidden: [...state.hiddenProviders] }));
26
+ } catch (error) {
27
+ reportRecoverableError("provider-visibility-save", error);
28
+ }
29
+ }
30
+
31
+ function isProviderVisible(providerId) {
32
+ return !state.hiddenProviders.has(String(providerId || ""));
33
+ }
34
+
35
+ function visibleProviders() {
36
+ return state.providers.filter((provider) => isProviderVisible(provider.id));
37
+ }
38
+
39
+ function visibleSessions() {
40
+ return (state.snapshot?.sessions || []).filter((session) => isProviderVisible(session.provider));
41
+ }
42
+
43
+ function visibleTmux(tmux = (state.rawSnapshot || state.snapshot)?.tmux) {
44
+ if (!tmux) return tmux;
45
+ const projected = {
46
+ ...tmux,
47
+ distros: (tmux.distros || []).map((distro) => ({
48
+ ...distro,
49
+ sessions: (distro.sessions || []).map((tmuxSession) => ({
50
+ ...tmuxSession,
51
+ windows: (tmuxSession.windows || []).map((window) => ({
52
+ ...window,
53
+ panes: (window.panes || []).filter((pane) => (!pane.dead || !pane.agent) && (!pane.agent || isProviderVisible(pane.agent.provider))),
54
+ })).filter((window) => window.panes.length),
55
+ })).filter((tmuxSession) => tmuxSession.windows.length),
56
+ })).filter((distro) => distro.sessions.length),
57
+ };
58
+ const panes = projected.distros.flatMap((distro) =>
59
+ distro.sessions.flatMap((session) => session.windows.flatMap((window) => window.panes)));
60
+ projected.summary = {
61
+ distros: projected.distros.length,
62
+ sessions: projected.distros.reduce((sum, distro) => sum + distro.sessions.length, 0),
63
+ windows: projected.distros.reduce((sum, distro) =>
64
+ sum + distro.sessions.reduce((count, session) => count + session.windows.length, 0), 0),
65
+ panes: panes.length,
66
+ aiPanes: panes.filter((pane) => pane.agent && !pane.dead).length,
67
+ linked: panes.filter((pane) => pane.agent?.linkedSessionId && !pane.dead).length,
68
+ };
69
+ return projected;
70
+ }
71
+
72
+ function projectVisibleSnapshot(snapshot = state.rawSnapshot || state.snapshot) {
73
+ if (!snapshot) return snapshot;
74
+ const sessions = (snapshot.sessions || []).filter((session) => isProviderVisible(session.provider));
75
+ const usage = Object.fromEntries(USAGE_KEYS.map((key) => [
76
+ key,
77
+ sessions.reduce((sum, session) => sum + Number(session.usage?.[key] || 0), 0),
78
+ ]));
79
+ return {
80
+ ...snapshot,
81
+ sessions,
82
+ tmux: visibleTmux(snapshot.tmux),
83
+ summary: {
84
+ ...(snapshot.summary || {}),
85
+ providers: (snapshot.summary?.providers || []).filter((provider) => isProviderVisible(provider.id)),
86
+ totals: {
87
+ sessions: sessions.length,
88
+ active: sessions.filter((session) => session.status === "running" || session.status === "starting").length,
89
+ waiting: sessions.filter((session) => session.status === "waiting").length,
90
+ subagents: sessions.filter((session) => session.parentId).length,
91
+ usage,
92
+ },
93
+ },
94
+ };
95
+ }
96
+
97
+ function setProviderVisible(providerId, visible) {
98
+ const id = String(providerId || "");
99
+ if (!state.providers.some((provider) => provider.id === id)) return;
100
+ if (visible) state.hiddenProviders.delete(id);
101
+ else state.hiddenProviders.add(id);
102
+ state.providerFilters.delete(id);
103
+ if (!isProviderVisible(state.runProvider)) {
104
+ state.runProvider = visibleProviders().find((provider) => state.availability[provider.id])?.id
105
+ || visibleProviders()[0]?.id
106
+ || "";
107
+ }
108
+ saveProviderVisibility();
109
+ if (state.rawSnapshot) state.snapshot = projectVisibleSnapshot(state.rawSnapshot);
110
+ }
111
+
112
+ return {
113
+ PROVIDER_VISIBILITY_STORAGE_KEY: STORAGE_KEY,
114
+ loadProviderVisibility,
115
+ saveProviderVisibility,
116
+ isProviderVisible,
117
+ visibleProviders,
118
+ visibleSessions,
119
+ visibleTmux,
120
+ projectVisibleSnapshot,
121
+ visibleSnapshot: () => projectVisibleSnapshot(),
122
+ setProviderVisible,
123
+ };
124
+ };