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
@@ -3,6 +3,7 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDashboard = function createDashboard(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
8
  $,
8
9
  esc,
@@ -11,10 +12,20 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
11
12
  state,
12
13
  compact,
13
14
  providerStyle,
15
+ visibleProviders = () => state.providers,
16
+ visibleSessions = () => ((state.snapshot && state.snapshot.sessions) || []),
17
+ isProviderVisible = () => true,
18
+ isRuntimeLoopSession = () => false,
14
19
  } = context;
15
20
 
21
+ function displaySessions() {
22
+ return visibleSessions().filter((session) => (
23
+ typeof context.isRecentSession !== "function" || context.isRecentSession(session)
24
+ ));
25
+ }
26
+
16
27
  function renderProviderRail() {
17
- $("#providerRail").innerHTML = state.providers
28
+ $("#providerRail").innerHTML = visibleProviders()
18
29
  .map((provider) => {
19
30
  const available = !!state.availability[provider.id];
20
31
  return `<div class="provider-rail-item ${available ? "connected" : ""}" style="${providerStyle(provider.id)}">
@@ -27,34 +38,95 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
27
38
  }
28
39
 
29
40
  function isProjectlessSession(session) {
30
- if (!session || !session.cwd) return true;
41
+ const cwd = session && (session.originCwd || session.cwd);
42
+ if (!cwd) return true;
31
43
  if (typeof session.projectless === "boolean") return session.projectless;
32
- const normalized = String(session.cwd).replace(/\\/g, "/").replace(/\/+$/, "");
44
+ const normalized = String(cwd).replace(/\\/g, "/").replace(/\/+$/, "");
33
45
  return session.provider === "codex" && session.clientKind === "codex-desktop" && /(?:^|\/)Documents\/Codex\/\d{4}-\d{2}-\d{2}\/new-chat$/i.test(normalized);
34
46
  }
35
47
 
48
+ function sessionOriginPath(session) {
49
+ return String(session && (session.originCwd || session.cwd) || "").trim();
50
+ }
51
+
52
+ function normalizedProjectPath(value) {
53
+ return String(value || "").trim().replace(/\\/g, "/").replace(/\/+$/, "").toLocaleLowerCase();
54
+ }
55
+
56
+ function projectContainsPath(projectPath, candidatePath) {
57
+ const project = normalizedProjectPath(projectPath);
58
+ const candidate = normalizedProjectPath(candidatePath);
59
+ return Boolean(project && candidate && (candidate === project || candidate.startsWith(`${project}/`)));
60
+ }
61
+
62
+ function projectName(projectPath) {
63
+ const normalized = String(projectPath || "").replace(/\\/g, "/").replace(/\/+$/, "");
64
+ return normalized.split("/").filter(Boolean).pop() || t("workspace.unknown");
65
+ }
66
+
67
+ function observedProjects() {
68
+ const projects = new Map();
69
+ const saved = state.workspaces.map((item) => ({ ...item, key: normalizedProjectPath(item.path) }));
70
+ saved.forEach((item) => projects.set(item.key, { path: item.path, name: item.name || projectName(item.path), saved: true, count: 0 }));
71
+ displaySessions().filter((session) => !session.parentId && !isProjectlessSession(session)).forEach((session) => {
72
+ const originPath = sessionOriginPath(session);
73
+ if (!originPath) return;
74
+ const owner = saved
75
+ .filter((item) => projectContainsPath(item.path, originPath))
76
+ .sort((a, b) => b.key.length - a.key.length)[0];
77
+ const path = owner ? owner.path : originPath;
78
+ const key = normalizedProjectPath(path);
79
+ const project = projects.get(key) || { path, name: projectName(path), saved: false, count: 0 };
80
+ project.count += 1;
81
+ project.lastActivityAt = !project.lastActivityAt || Date.parse(session.updatedAt || 0) > Date.parse(project.lastActivityAt || 0)
82
+ ? session.updatedAt
83
+ : project.lastActivityAt;
84
+ projects.set(key, project);
85
+ });
86
+ const items = [...projects.values()];
87
+ const duplicateNames = new Map();
88
+ items.forEach((item) => duplicateNames.set(item.name.toLocaleLowerCase(), (duplicateNames.get(item.name.toLocaleLowerCase()) || 0) + 1));
89
+ items.forEach((item) => {
90
+ if (duplicateNames.get(item.name.toLocaleLowerCase()) < 2) return;
91
+ const parts = String(item.path).replace(/\\/g, "/").replace(/\/+$/, "").split("/").filter(Boolean);
92
+ item.name = parts.slice(-2).join("/") || item.name;
93
+ });
94
+ return items.sort((a, b) => Number(b.count || 0) - Number(a.count || 0)
95
+ || Number(b.saved) - Number(a.saved)
96
+ || String(a.name).localeCompare(String(b.name), uiLocale()));
97
+ }
98
+
36
99
  function sessionWorkspaceLabel(session) {
37
- return isProjectlessSession(session) ? window.LoadToAgentI18n.t("ui.no_project") : (session && session.workspace) || "작업 폴더 미상";
100
+ return isProjectlessSession(session)
101
+ ? t("ui.no_project")
102
+ : (session && session.workspace) || projectName(sessionOriginPath(session));
38
103
  }
39
104
 
40
105
  function matchesWorkspaceFilter(session) {
41
106
  if (state.workspace === "all") return true;
42
107
  if (state.workspace === PROJECTLESS_WORKSPACE) return isProjectlessSession(session);
43
- return (
44
- !isProjectlessSession(session) &&
45
- String((session && session.cwd) || "")
46
- .toLowerCase()
47
- .startsWith(state.workspace.toLowerCase())
48
- );
108
+ return !isProjectlessSession(session) && projectContainsPath(state.workspace, sessionOriginPath(session));
49
109
  }
50
110
 
51
111
  function renderWorkspaces() {
52
- const list = $("#workspaceList");
53
- const projectlessCount = ((state.snapshot && state.snapshot.sessions) || []).filter((session) => !session.parentId && isProjectlessSession(session)).length;
54
- list.innerHTML =
112
+ const lists = [$("#workspaceList"), $("#mobileWorkspaceList")].filter(Boolean);
113
+ const rootSessions = displaySessions().filter((session) => !session.parentId);
114
+ const projects = observedProjects();
115
+ const projectlessCount = rootSessions.filter(isProjectlessSession).length;
116
+ const savedWorkspaceExists = state.workspace === "all"
117
+ || (state.workspace === PROJECTLESS_WORKSPACE && projectlessCount > 0)
118
+ || projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
119
+ if (!savedWorkspaceExists) state.workspace = "all";
120
+ const projectButton = (item) => `<button type="button" class="workspace-item observed-project ${state.workspace === item.path ? "selected" : ""}"
121
+ data-workspace="${esc(item.path)}" title="${esc(item.path)}"
122
+ aria-label="${esc(t("project.filter_named", { name: item.name, count: item.count }))}"
123
+ aria-pressed="${state.workspace === item.path ? "true" : "false"}">
124
+ <strong>${esc(item.name)}</strong><small>${Number(item.count || 0)}</small>
125
+ </button>`;
126
+ const html =
55
127
  `<button type="button" class="workspace-item ${state.workspace === "all" ? "selected" : ""}"
56
128
  data-workspace="all" aria-pressed="${state.workspace === "all" ? "true" : "false"}">
57
- <strong>${window.LoadToAgentI18n.t("ui.all_workspaces")}</strong>
129
+ <strong>${window.LoadToAgentI18n.t("project.all")}</strong><small>${rootSessions.length}</small>
58
130
  </button>` +
59
131
  (projectlessCount
60
132
  ? `<button type="button" class="workspace-item projectless ${state.workspace === PROJECTLESS_WORKSPACE ? "selected" : ""}"
@@ -65,34 +137,39 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
65
137
  <small>${projectlessCount}</small>
66
138
  </button>`
67
139
  : "") +
68
- state.workspaces
69
- .map(
70
- (item) => `<div class="workspace-row">
71
- <button type="button" class="workspace-item ${state.workspace === item.path ? "selected" : ""}"
72
- data-workspace="${esc(item.path)}" title="${esc(item.path)}"
73
- aria-pressed="${state.workspace === item.path ? "true" : "false"}">
74
- <strong>${esc(item.name)}</strong>
75
- </button>
140
+ projects.map((item) => item.saved ? `<div class="workspace-row">
141
+ ${projectButton(item)}
76
142
  <button type="button" class="workspace-remove" data-remove-workspace="${esc(item.path)}"
77
- aria-label="${esc(item.name)} 작업 폴더를 목록에서 제거"
143
+ aria-label="${esc(t("workspace.remove_named", { name: item.name }))}"
78
144
  title="${esc(window.LoadToAgentI18n.t("ui.remove_from_list"))}">×</button>
79
- </div>`,
80
- )
81
- .join("") +
82
- (!state.workspaces.length ? `<div class="workspace-empty">${window.LoadToAgentI18n.t("ui.use_the_button_to_add_frequently_used_workspaces")}</div>` : "");
145
+ </div>` : projectButton(item)).join("") +
146
+ (!projects.length && !projectlessCount ? `<div class="workspace-empty">${window.LoadToAgentI18n.t("project.empty")}</div>` : "");
147
+ lists.forEach((list) => { list.innerHTML = html; });
148
+ const selectedProject = projects.find((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
149
+ const mobileSummary = $("#mobileWorkspaceSummary");
150
+ if (mobileSummary) mobileSummary.textContent = state.workspace === "all"
151
+ ? t("project.all")
152
+ : state.workspace === PROJECTLESS_WORKSPACE
153
+ ? t("ui.no_project")
154
+ : selectedProject?.name || projectName(state.workspace);
83
155
  }
84
156
 
85
157
  function renderGlobalStats() {
86
- const totals = (state.snapshot && state.snapshot.summary && state.snapshot.summary.totals) || {};
87
- const sessions = (state.snapshot && state.snapshot.sessions) || [];
158
+ const sessions = displaySessions();
159
+ const totals = {
160
+ active: sessions.filter((session) => session.status === "running" || session.status === "starting").length,
161
+ waiting: sessions.filter((session) => context.matchesManagementFilter?.(session, "attention")).length,
162
+ usage: { total: sessions.reduce((sum, session) => sum + Number(session.usage && session.usage.total || 0), 0) },
163
+ };
88
164
  const rootCount = sessions.filter((session) => !session.parentId).length;
89
- const helperCount = sessions.filter((session) => session.parentId).length;
165
+ const criticalCount = sessions.filter((session) => context.matchesManagementFilter?.(session, "critical")).length;
166
+ const riskCount = sessions.filter((session) => context.matchesManagementFilter?.(session, "warning")).length;
90
167
  const items = [
91
168
  [window.LoadToAgentI18n.t("ui.all_tasks"), rootCount, window.LoadToAgentI18n.t("ui.items"), ""],
92
169
  [window.LoadToAgentI18n.t("ui.ai_working_now"), totals.active || 0, window.LoadToAgentI18n.t("ui.items"), "live"],
93
- [window.LoadToAgentI18n.t("ui.waiting_for_review"), totals.waiting || 0, window.LoadToAgentI18n.t("ui.items"), "alert"],
94
- [window.LoadToAgentI18n.t("ui.helper_ai_history"), helperCount, window.LoadToAgentI18n.t("ui.items"), ""],
95
- [window.LoadToAgentI18n.t("ui.tokens_used"), compact(totals.usage && totals.usage.total), window.LoadToAgentI18n.t("ui.items"), ""],
170
+ [window.LoadToAgentI18n.t("management.action_required"), totals.waiting || 0, window.LoadToAgentI18n.t("ui.items"), "alert"],
171
+ [window.LoadToAgentI18n.t("management.health.critical"), criticalCount, window.LoadToAgentI18n.t("ui.items"), "critical"],
172
+ [window.LoadToAgentI18n.t("management.risk_total"), riskCount, window.LoadToAgentI18n.t("ui.items"), "warning"],
96
173
  ];
97
174
  $("#globalStats").innerHTML = items
98
175
  .map(
@@ -104,9 +181,46 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
104
181
  )
105
182
  .join("");
106
183
  $("#navAllCount").textContent = rootCount;
107
- $("#navActiveCount").textContent = totals.active || 0;
108
- $("#navWaitingCount").textContent = totals.waiting || 0;
109
- $("#navTmuxCount").textContent = (state.snapshot && state.snapshot.tmux && state.snapshot.tmux.summary && state.snapshot.tmux.summary.aiPanes) || 0;
184
+ const activeRootCount = sessions.filter((session) => !session.parentId && ["running", "starting"].includes(session.status)).length;
185
+ $("#navActiveCount").textContent = activeRootCount;
186
+ const reviewCount = sessions.filter((session) => context.needsManagementReview?.(session)).length;
187
+ $("#navWaitingCount").textContent = reviewCount;
188
+ const scheduledCount = (state.snapshot?.automations || [])
189
+ .filter((item) => isProviderVisible(item.provider || "codex")).length;
190
+ const loopCount = sessions.filter(isRuntimeLoopSession).length;
191
+ $("#navRuntimeCount").textContent = scheduledCount + loopCount;
192
+ const tmuxSessionCount = Number(state.snapshot?.tmux?.summary?.sessions || 0);
193
+ $("#navTmuxCount").textContent = tmuxSessionCount;
194
+ $("#advancedToolsCount").textContent = scheduledCount + loopCount + Number($("#navTerminalCount").textContent || 0) + tmuxSessionCount;
195
+ const navCounts = {
196
+ all: rootCount,
197
+ active: activeRootCount,
198
+ waiting: reviewCount,
199
+ runtime: scheduledCount + loopCount,
200
+ terminal: Number($("#navTerminalCount").textContent || 0),
201
+ tmux: tmuxSessionCount,
202
+ };
203
+ document.querySelectorAll(".nav-item[data-view]").forEach((button) => {
204
+ const key = {
205
+ all: "app.nav.home", active: "app.nav.active", waiting: "app.nav.needs_review", runtime: "app.nav.runtime",
206
+ terminal: "app.nav.session_terminal", tmux: "app.nav.tmux", settings: "app.nav.settings",
207
+ }[button.dataset.view];
208
+ const label = t(key);
209
+ const count = navCounts[button.dataset.view];
210
+ const unitKey = { all: "tasks", active: "tasks", waiting: "items", runtime: "runs", terminal: "sessions", tmux: "sessions" }[button.dataset.view];
211
+ const unit = unitKey ? t(`quality.unit.${unitKey}`) : "";
212
+ const accessibleLabel = Number.isFinite(count) ? t("quality.nav_count_detailed", { label, count, unit }) : label;
213
+ button.setAttribute("aria-label", accessibleLabel);
214
+ button.setAttribute("title", accessibleLabel);
215
+ });
216
+ const advancedCount = scheduledCount + loopCount + Number($("#navTerminalCount").textContent || 0) + tmuxSessionCount;
217
+ $("#advancedToolsNav")?.querySelector("summary")?.setAttribute("aria-label", t("quality.nav_count_detailed", {
218
+ label: t("management.advanced_tools"), count: advancedCount, unit: t("quality.unit.items"),
219
+ }));
220
+ const tmuxShortcut = $("#openTmuxFromAgentWork");
221
+ $("#agentWorkTmuxCount").textContent = tmuxSessionCount;
222
+ tmuxShortcut.dataset.i18nParams = JSON.stringify({ count: tmuxSessionCount });
223
+ tmuxShortcut.setAttribute("aria-label", t("graph.open_tmux_workspace_count", { count: tmuxSessionCount }));
110
224
  }
111
225
 
112
226
  function formatBytes(value) {
@@ -159,7 +273,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
159
273
  ],
160
274
  downloaded: [
161
275
  "✓", window.LoadToAgentI18n.t("ui.ready_to_install"), window.LoadToAgentI18n.t("ui.the_update_file_is_ready"),
162
- window.LoadToAgentI18n.t("ui.open_the_installer_and_follow_its_instructions_to_finish_updating"),
276
+ window.LoadToAgentI18n.t("settings.update.auto_install_restart"),
163
277
  ],
164
278
  error: [
165
279
  "!", window.LoadToAgentI18n.t("ui.check_failed"), window.LoadToAgentI18n.t("ui.could_not_check_for_updates"),
@@ -202,7 +316,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
202
316
  install.textContent = downloading
203
317
  ? window.LoadToAgentI18n.t("ui.downloading_2")
204
318
  : downloaded
205
- ? window.LoadToAgentI18n.t("ui.open_installer")
319
+ ? `${window.LoadToAgentI18n.t("settings.update.download")} · ${window.LoadToAgentI18n.t("ui.restart")}`
206
320
  : window.LoadToAgentI18n.t("settings.update.download");
207
321
  const progress = $("#updateProgress");
208
322
  progress.classList.toggle("hidden", !downloading && !downloaded);
@@ -210,11 +324,13 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
210
324
  $("#updateProgressBar").style.width = `${Math.max(0, Math.min(100, Number(update.progress || 0)))}%`;
211
325
  $(".update-progress-track").setAttribute("aria-valuenow", String(Math.max(0, Math.min(100, Number(update.progress || 0)))));
212
326
  $("#updateProgressBytes").textContent = downloaded
213
- ? `${formatBytes(update.totalBytes || update.downloadedBytes)} · 파일 검증 완료`
327
+ ? `${formatBytes(update.totalBytes || update.downloadedBytes)} · ${window.LoadToAgentI18n.t("settings.update.file_verified")}`
214
328
  : `${formatBytes(update.downloadedBytes)} / ${update.totalBytes ? formatBytes(update.totalBytes) : window.LoadToAgentI18n.t("ui.checking_size")}`;
215
329
  const error = $("#updateError");
216
330
  error.classList.toggle("hidden", !update.error);
217
- error.textContent = update.error || "";
331
+ error.textContent = update.error
332
+ ? window.LoadToAgentI18n.errorText(update.error, "ui.could_not_check_for_updates")
333
+ : "";
218
334
  const notes = $("#releaseNotes");
219
335
  notes.classList.toggle("hidden", !update.latestVersion);
220
336
  $("#releaseNotesText").textContent =
@@ -231,16 +347,20 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
231
347
  function renderProviderOverview() {
232
348
  pruneProviderFilters();
233
349
  const summaries = (state.snapshot && state.snapshot.summary && state.snapshot.summary.providers) || state.providers;
234
- const sessions = (state.snapshot && state.snapshot.sessions) || [];
235
- $("#providerOverview").innerHTML = summaries
236
- .map((provider) => {
350
+ const sessions = displaySessions();
351
+ const visibleSummaries = summaries.filter((provider) => isProviderVisible(provider.id));
352
+ const overviewTabStopId = state.providerFilters.size ? [...state.providerFilters][0] : visibleSummaries[0]?.id;
353
+ $("#providerOverview").innerHTML = visibleSummaries
354
+ .map((provider, index) => {
237
355
  const rootCount = sessions.filter((session) => session.provider === provider.id && !session.parentId).length;
238
356
  const selected = state.providerFilters.has(provider.id);
357
+ const tabStop = provider.id === overviewTabStopId;
239
358
  return `<button type="button" class="provider-overview-card ${selected ? "selected" : ""}"
240
359
  data-provider-card="${esc(provider.id)}"
241
360
  data-motion-key="provider:${esc(provider.id)}"
242
361
  data-motion-value="${provider.active || 0}:${rootCount}:${(provider.usage && provider.usage.total) || 0}"
243
362
  style="${providerStyle(provider.id)}"
363
+ tabindex="${tabStop ? "0" : "-1"}"
244
364
  aria-pressed="${selected ? "true" : "false"}">
245
365
  <div class="poc-head">
246
366
  <span class="provider-mark">${esc(provider.mark)}</span>
@@ -263,7 +383,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
263
383
  }
264
384
 
265
385
  function pruneProviderFilters() {
266
- const valid = new Set(state.providers.map((provider) => provider.id));
386
+ const valid = new Set(visibleProviders().map((provider) => provider.id));
267
387
  for (const id of [...state.providerFilters]) if (!valid.has(id)) state.providerFilters.delete(id);
268
388
  if (valid.size > 0 && state.providerFilters.size === valid.size) state.providerFilters.clear();
269
389
  }
@@ -273,28 +393,29 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
273
393
  if (providerId === "all") state.providerFilters.clear();
274
394
  else if (state.providerFilters.has(providerId)) state.providerFilters.delete(providerId);
275
395
  else state.providerFilters.add(providerId);
276
- if (state.providers.length > 0 && state.providerFilters.size === state.providers.length) state.providerFilters.clear();
396
+ if (visibleProviders().length > 0 && state.providerFilters.size === visibleProviders().length) state.providerFilters.clear();
277
397
  }
278
398
 
279
399
  function renderProviderFilter() {
280
400
  pruneProviderFilters();
281
401
  const allSelected = state.providerFilters.size === 0;
402
+ const tabStopId = allSelected ? "all" : [...state.providerFilters][0];
282
403
  const button = (id, label, mark = "") => {
283
404
  const selected = id === "all" ? allSelected : state.providerFilters.has(id);
284
405
  return `<button type="button" class="provider-filter-chip ${selected ? "selected" : ""}"
285
- data-provider-filter="${esc(id)}" aria-pressed="${selected ? "true" : "false"}">
406
+ data-provider-filter="${esc(id)}" tabindex="${id === tabStopId ? "0" : "-1"}" aria-pressed="${selected ? "true" : "false"}">
286
407
  <i class="provider-filter-check" aria-hidden="true">✓</i>
287
408
  ${mark ? `<span class="provider-filter-mark" aria-hidden="true">${esc(mark)}</span>` : ""}<b>${esc(label)}</b>
288
409
  </button>`;
289
410
  };
290
411
  $("#providerFilter").innerHTML =
291
412
  button("all", window.LoadToAgentI18n.t("ui.all_ai")) +
292
- state.providers.map((provider) => button(provider.id, provider.label, provider.mark)).join("");
413
+ visibleProviders().map((provider) => button(provider.id, provider.label, provider.mark)).join("");
293
414
  }
294
415
 
295
416
  function announceProviderFilter() {
296
417
  const labels = state.providerFilters.size
297
- ? state.providers.filter((provider) => state.providerFilters.has(provider.id)).map((provider) => provider.label).join(", ")
418
+ ? visibleProviders().filter((provider) => state.providerFilters.has(provider.id)).map((provider) => provider.label).join(", ")
298
419
  : window.LoadToAgentI18n.t("ui.all_ai");
299
420
  $("#providerFilterStatus").textContent = window.LoadToAgentI18n.t("filter.result_summary", {
300
421
  providers: labels,
@@ -303,16 +424,16 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
303
424
  }
304
425
 
305
426
  function filteredSessions() {
306
- const allSessions = [...((state.snapshot && state.snapshot.sessions) || [])];
427
+ const allSessions = displaySessions();
307
428
  let sessions = state.view === "waiting" ? allSessions : allSessions.filter((session) => !session.parentId);
308
429
  if (state.view === "active") sessions = sessions.filter((session) => session.status === "running" || session.status === "starting");
309
- if (state.view === "waiting") sessions = sessions.filter((session) => session.status === "waiting");
430
+ if (state.view === "waiting") sessions = sessions.filter((session) => context.needsManagementReview?.(session));
310
431
  if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
311
432
  sessions = sessions.filter(matchesWorkspaceFilter);
312
- const query = state.search.trim().toLowerCase();
433
+ const query = state.search.replace(/\s+/g, " ").trim().toLowerCase();
313
434
  if (query) {
314
435
  sessions = sessions.filter((session) =>
315
- [session.title, session.model, session.cwd, session.agentName, ...(session.messages || []).slice(-12).map((item) => item.text)]
436
+ [session.title, session.model, session.originCwd, session.cwd, session.workspace, session.agentName, ...(session.messages || []).slice(-12).map((item) => item.text)]
316
437
  .join(" ")
317
438
  .toLowerCase()
318
439
  .includes(query),
@@ -330,13 +451,13 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
330
451
  }
331
452
 
332
453
  function graphFilteredSessions() {
333
- let sessions = [...((state.snapshot && state.snapshot.sessions) || [])];
454
+ let sessions = displaySessions();
334
455
  if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
335
456
  sessions = sessions.filter(matchesWorkspaceFilter);
336
- const query = state.search.trim().toLowerCase();
457
+ const query = state.search.replace(/\s+/g, " ").trim().toLowerCase();
337
458
  if (query)
338
459
  sessions = sessions.filter((session) =>
339
- [session.title, session.model, session.cwd, session.agentName, session.agentRole, ...(session.messages || []).map((item) => item.text)]
460
+ [session.title, session.model, session.originCwd, session.cwd, session.workspace, session.agentName, session.agentRole, ...(session.messages || []).map((item) => item.text)]
340
461
  .join(" ")
341
462
  .toLowerCase()
342
463
  .includes(query),
@@ -344,9 +465,28 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
344
465
  return sessions;
345
466
  }
346
467
 
468
+ function renderProviderVisibilitySettings() {
469
+ const list = $("#providerVisibilityList");
470
+ if (!list) return;
471
+ list.innerHTML = state.providers.map((provider) => {
472
+ const visible = isProviderVisible(provider.id);
473
+ const status = window.LoadToAgentI18n.t(visible ? "settings.providers.visible" : "settings.providers.hidden");
474
+ return `<label class="provider-visibility-option ${visible ? "enabled" : "disabled"}" style="${providerStyle(provider.id)}">
475
+ <span class="provider-mark" aria-hidden="true">${esc(provider.mark)}</span>
476
+ <span class="provider-visibility-name"><b>${esc(provider.label)}</b><small>${esc(provider.company)}</small></span>
477
+ <span class="provider-visibility-status">${esc(status)}</span>
478
+ <input type="checkbox" data-provider-visibility="${esc(provider.id)}" ${visible ? "checked" : ""}
479
+ aria-label="${esc(`${provider.label} ${status}`)}">
480
+ <span class="provider-toggle" aria-hidden="true"><i></i></span>
481
+ </label>`;
482
+ }).join("");
483
+ }
484
+
347
485
  return {
348
486
  renderProviderRail,
349
487
  isProjectlessSession,
488
+ sessionOriginPath,
489
+ observedProjects,
350
490
  sessionWorkspaceLabel,
351
491
  matchesWorkspaceFilter,
352
492
  renderWorkspaces,
@@ -361,5 +501,6 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
361
501
  announceProviderFilter,
362
502
  filteredSessions,
363
503
  graphFilteredSessions,
504
+ renderProviderVisibilitySettings,
364
505
  };
365
506
  };
@@ -4,35 +4,39 @@ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDrawerContent = function createDrawerContent(context = {}) {
6
6
  const { esc, uiLocale, state, messageContentHtml, compact, fullNumber, timeOnly, providerInfo, statusIcon, agentPathTaskName, snapshotSession } = context;
7
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
7
8
 
8
9
  function chatHtml(session, options = {}) {
9
10
  const messages = session.messages || [];
10
- if (!messages.length) return '<div class="empty-state"><h3>표시할 대화가 없습니다</h3></div>';
11
- const userLabel = options.userLabel || "사용자";
11
+ if (!messages.length) return `<div class="empty-state"><h3>${esc(t("drawer.no_conversation"))}</h3></div>`;
12
+ const userLabel = options.userLabel || t("drawer.user");
12
13
  const assistantLabel = options.assistantLabel || providerInfo(session.provider).label;
13
- const conversationLabel = options.conversationLabel || "대화";
14
+ const conversationLabel = options.conversationLabel || t("drawer.conversation");
14
15
  const conversation = messages.filter((message) => message.role === "user" || message.role === "assistant");
15
16
  const activities = messages.filter((message) => message.role !== "user" && message.role !== "assistant");
16
17
  const omitted = Number(session.omittedMessages || 0);
17
18
  const notice =
18
19
  omitted || session.truncated
19
- ? `<div class="chat-truncated">이 작업의 최근 기록을 표시합니다${omitted ? ` · 이전 ${omitted.toLocaleString(uiLocale())} 메시지 생략` : ""}</div>`
20
+ ? `<div class="chat-truncated">${esc(t("drawer.recent_history"))}${omitted ? ` · ${esc(t("drawer.messages_omitted", { count: omitted.toLocaleString(uiLocale()) }))}` : ""}</div>`
20
21
  : "";
21
22
  const statusLabels = {
22
- started: "실행 중", running: "실행 중",
23
- done: window.LoadToAgentI18n.t("ui.completed"), completed: window.LoadToAgentI18n.t("ui.completed"), failed: "실패",
23
+ started: t("ui.working"), running: t("ui.working"),
24
+ done: t("ui.completed"), completed: t("ui.completed"), failed: t("drawer.failed"),
24
25
  };
25
26
  const statusLabel = (value) => statusLabels[value] || value || "";
26
27
  const rows = conversation
27
28
  .map((message) => {
28
29
  const role = message.role === "assistant" ? "assistant" : message.role === "tool" ? "tool" : message.role === "system" ? "system" : "user";
30
+ const renderedMessage = role === "tool" || role === "system"
31
+ ? { ...message, text: window.LoadToAgentI18n.observedText(message.text) }
32
+ : message;
29
33
  const label =
30
34
  role === "assistant"
31
35
  ? assistantLabel
32
36
  : role === "tool"
33
- ? message.title || "도구"
37
+ ? window.LoadToAgentI18n.observedText(message.title || t("session.tool"))
34
38
  : message.role === "system"
35
- ? "시스템"
39
+ ? t("drawer.system")
36
40
  : userLabel;
37
41
  const avatar = role === "assistant" ? providerInfo(session.provider).mark : role === "tool" ? "⌘" : role === "system" ? "i" : "ME";
38
42
  const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
@@ -43,44 +47,44 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
43
47
  <b>${esc(label)}</b>
44
48
  <span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>
45
49
  ${message.status ? `<span>${esc(statusLabel(message.status))}</span>` : ""}
46
- </div>${messageContentHtml(message)}</div>
50
+ </div>${messageContentHtml(renderedMessage, session.id)}</div>
47
51
  </div>`;
48
52
  })
49
53
  .join("");
50
54
  const activityHtml = activities.length
51
- ? `<details class="chat-activities">
52
- <summary>도구·시스템 활동 ${activities.length}건 보기</summary>
55
+ ? `<details class="chat-activities" data-disclosure-key="${esc(`drawer:${session.id}:activities`)}">
56
+ <summary>${esc(t("drawer.activities_view", { count: activities.length }))}</summary>
53
57
  <div>${activities
54
58
  .map(
55
59
  (message) => `<article>
56
60
  <header>
57
- <b>${esc(message.title || (message.role === "tool" ? "도구 실행" : "시스템"))}</b>
61
+ <b>${esc(window.LoadToAgentI18n.observedText(message.title || (message.role === "tool" ? t("drawer.tool_execution") : t("drawer.system"))))}</b>
58
62
  <span>${esc(statusLabel(message.status))} · ${esc(timeOnly(message.timestamp))}</span>
59
- </header>${messageContentHtml(message)}</article>`,
63
+ </header>${messageContentHtml({ ...message, text: window.LoadToAgentI18n.observedText(message.text) }, session.id)}</article>`,
60
64
  )
61
65
  .join("")}</div>
62
66
  </details>`
63
67
  : "";
64
- const emptyConversation = conversation.length ? "" : '<div class="empty-state compact"><h3>사용자와 AI의 대화는 아직 없습니다</h3></div>';
68
+ const emptyConversation = conversation.length ? "" : `<div class="empty-state compact"><h3>${esc(t("drawer.no_user_ai_conversation"))}</h3></div>`;
65
69
  return `${notice}<div class="chat-history-head">
66
- <span>${esc(conversationLabel)} ${conversation.length}개${activities.length ? ` · 활동 ${activities.length}건` : ""}</span>
67
- <button type="button" data-scroll-latest>가장 최근 대화 ↓</button>
70
+ <span>${esc(t("drawer.conversation_summary", { label: conversationLabel, count: conversation.length, activities: activities.length ? ` · ${t("drawer.activities", { count: activities.length })}` : "" }))}</span>
71
+ <button type="button" data-scroll-latest>${esc(t("drawer.latest_conversation"))} ↓</button>
68
72
  </div>
69
- <div class="chat-list">${rows}${emptyConversation}${activityHtml}<div class="chat-latest-anchor" aria-label="가장 최근 대화">
73
+ <div class="chat-list">${rows}${emptyConversation}${activityHtml}<div class="chat-latest-anchor" aria-label="${esc(t("drawer.latest_conversation"))}">
70
74
  </div>
71
75
  </div>`;
72
76
  }
73
77
 
74
78
  function lifecycleHtml(session) {
75
79
  const events = session.lifecycle || [];
76
- if (!events.length) return '<div class="empty-state"><h3>아직 기록된 진행 과정이 없습니다</h3></div>';
80
+ if (!events.length) return `<div class="empty-state"><h3>${esc(t("drawer.no_lifecycle"))}</h3></div>`;
77
81
  return `<div class="lifecycle-list">${events
78
82
  .map(
79
83
  (event) => `<div class="lifecycle-event ${esc(event.status)}">
80
84
  <span class="life-node">${statusIcon(event.type)}</span>
81
85
  <div class="life-copy">
82
- <b>${esc(event.label)}</b>
83
- <span>${esc(event.detail || event.type)}</span>
86
+ <b>${esc(window.LoadToAgentI18n.observedText(event.label))}</b>
87
+ <span>${esc(window.LoadToAgentI18n.observedText(event.detail || event.type))}</span>
84
88
  </div>
85
89
  <time>${esc(timeOnly(event.timestamp))}</time>
86
90
  </div>`,
@@ -94,14 +98,14 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
94
98
  const context = session.context || {};
95
99
  const sourceLabel =
96
100
  context.source === "session"
97
- ? "이 작업 기록에서 직접 확인한 기억 공간"
101
+ ? t("drawer.context_source_session")
98
102
  : context.source === "model-catalog"
99
- ? "AI 모델 정보에 적힌 기억 공간"
100
- : "기억 공간 크기 정보 없음";
103
+ ? t("drawer.context_source_catalog")
104
+ : t("session.context_size_unknown");
101
105
  return `<div class="token-hero" style="--drawer-provider:${providerInfo(session.provider).accent}">
102
106
  <div class="token-hero-head">
103
- <span>AI의 기억 공간 사용량</span>
104
- <b>${context.window ? `${fullNumber(context.used)} / ${fullNumber(context.window)} 토큰` : `${fullNumber(context.used)} 토큰`}</b>
107
+ <span>${esc(t("session.context_usage"))}</span>
108
+ <b>${esc(t("drawer.tokens", { count: context.window ? `${fullNumber(context.used)} / ${fullNumber(context.window)}` : fullNumber(context.used) }))}</b>
105
109
  </div>
106
110
  <div class="big-context"><span style="width:${Math.min(100, context.percent || 0)}%"></span></div>
107
111
  <div class="context-scale">
@@ -110,15 +114,15 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
110
114
  </div>
111
115
  </div>
112
116
  <div class="token-grid">
113
- <div class="token-tile"><span>AI가 받은 글</span><strong>${fullNumber(usage.input)}</strong><small>AI에게 전달된 내용의 양</small></div>
114
- <div class="token-tile"><span>AI가 쓴 글</span><strong>${fullNumber(usage.output)}</strong><small>AI가 답하고 만든 내용의 양</small></div>
115
- <div class="token-tile"><span>다시 사용한 기억</span><strong>${fullNumber(usage.cachedInput)}</strong><small>전에 읽은 내용을 다시 활용한 양</small></div>
116
- <div class="token-tile"><span>새로 저장한 기억</span><strong>${fullNumber(usage.cacheWrite)}</strong><small>다음에 다시 쓰도록 저장한 양</small></div>
117
- <div class="token-tile"><span>생각에 사용</span><strong>${fullNumber(usage.reasoning)}</strong><small>AI가 따로 알려준 경우만 표시</small></div>
118
- <div class="token-tile"><span>전체 사용량</span><strong>${fullNumber(usage.total)}</strong><small>이 작업에서 사용한 토큰 합계</small></div>
119
- <div class="token-tile"><span>최근에 받은 글</span><strong>${fullNumber(turn.input)}</strong><small>가장 최근 대화 기준</small></div>
120
- <div class="token-tile"><span>최근 대화 전체</span><strong>${fullNumber(turn.total)}</strong><small>가장 최근 한 번의 사용량</small></div>
121
- </div><div class="token-note">${esc(sourceLabel)}입니다. 토큰은 AI가 글을 읽고 쓰는 양을 세는 단위이고, 기억 공간은 AI가 한 번에 기억할 수 있는 양입니다.</div>`;
117
+ <div class="token-tile"><span>${esc(t("drawer.input"))}</span><strong>${fullNumber(usage.input)}</strong><small>${esc(t("drawer.input_help"))}</small></div>
118
+ <div class="token-tile"><span>${esc(t("drawer.output"))}</span><strong>${fullNumber(usage.output)}</strong><small>${esc(t("drawer.output_help"))}</small></div>
119
+ <div class="token-tile"><span>${esc(t("drawer.cached"))}</span><strong>${fullNumber(usage.cachedInput)}</strong><small>${esc(t("drawer.cached_help"))}</small></div>
120
+ <div class="token-tile"><span>${esc(t("drawer.cache_write"))}</span><strong>${fullNumber(usage.cacheWrite)}</strong><small>${esc(t("drawer.cache_write_help"))}</small></div>
121
+ <div class="token-tile"><span>${esc(t("drawer.reasoning"))}</span><strong>${fullNumber(usage.reasoning)}</strong><small>${esc(t("drawer.reasoning_help"))}</small></div>
122
+ <div class="token-tile"><span>${esc(t("drawer.total"))}</span><strong>${fullNumber(usage.total)}</strong><small>${esc(t("drawer.total_help"))}</small></div>
123
+ <div class="token-tile"><span>${esc(t("drawer.last_input"))}</span><strong>${fullNumber(turn.input)}</strong><small>${esc(t("drawer.latest_turn"))}</small></div>
124
+ <div class="token-tile"><span>${esc(t("drawer.last_total"))}</span><strong>${fullNumber(turn.total)}</strong><small>${esc(t("drawer.last_total_help"))}</small></div>
125
+ </div><div class="token-note">${esc(t("drawer.token_note", { source: sourceLabel }))}</div>`;
122
126
  }
123
127
 
124
128
  function subagentCommunicationEvents(session) {
@@ -183,16 +187,16 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
183
187
  .map((event) => {
184
188
  const fromChild = event.kind === "result" || endpointIsChild(event.from);
185
189
  const preview = subagentTextPreview(event.text);
186
- const label = fromChild ? `${session.agentName || taskName} → 메인 AI` : "메인 AI → 서브에이전트";
190
+ const label = fromChild ? t("drawer.child_to_main", { child: session.agentName || taskName }) : t("drawer.main_to_child");
187
191
  return `<article data-subagent-communication="${esc(event.kind)}">
188
- <header><b>${esc(event.label || event.kind)}</b><span>${esc(label)} · ${esc(timeOnly(event.timestamp))}</span></header>
192
+ <header><b>${esc(window.LoadToAgentI18n.observedText(event.label || event.kind))}</b><span>${esc(label)} · ${esc(timeOnly(event.timestamp))}</span></header>
189
193
  <div class="chat-content plain subagent-message-preview${preview.truncated ? " is-truncated" : ""}"
190
194
  data-subagent-message-preview data-truncated="${preview.truncated ? "true" : "false"}"><p>${esc(preview.text)}</p></div>
191
195
  </article>`;
192
196
  })
193
197
  .join("");
194
- return `<details class="chat-activities subagent-coordination" data-subagent-coordination-count="${events.length}">
195
- <summary>메인 AI와 주고받은 지시·결과 ${events.length}건</summary><div>${rows}</div>
198
+ return `<details class="chat-activities subagent-coordination" data-subagent-coordination-count="${events.length}" data-disclosure-key="${esc(`drawer:${session.id}:coordination`)}">
199
+ <summary>${esc(t("drawer.coordination_events", { count: events.length }))}</summary><div>${rows}</div>
196
200
  </details>`;
197
201
  }
198
202
 
@@ -201,14 +205,14 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
201
205
  const conversationCount = messages.filter((message) => message.role === "user" || message.role === "assistant").length;
202
206
  const workSession = { ...session, messages };
203
207
  const sourceCopy = session.source === "collaboration-history"
204
- ? "별도 세션 로그가 없어 협업 기록에서 복원한 작업 결과입니다."
205
- : "이 서브에이전트 세션에 실제로 저장된 작업 지시와 답변을 시간순으로 표시합니다.";
208
+ ? t("drawer.subagent_history_reconstructed")
209
+ : t("drawer.subagent_history_actual");
206
210
  return `<section class="subagent-work-source" data-subagent-work-messages="${conversationCount}">
207
- <b>서브에이전트 실제 작업 기록</b><span>${esc(sourceCopy)}</span>
211
+ <b>${esc(t("drawer.subagent_work_history"))}</b><span>${esc(sourceCopy)}</span>
208
212
  </section>${chatHtml(workSession, {
209
- userLabel: "작업 지시",
210
- assistantLabel: session.agentName || "서브 AI",
211
- conversationLabel: "작업 기록",
213
+ userLabel: t("drawer.assignment"),
214
+ assistantLabel: session.agentName || t("drawer.sub_ai"),
215
+ conversationLabel: t("drawer.work_history"),
212
216
  })}${subagentCoordinationHtml(session)}`;
213
217
  }
214
218