loadtoagent 1.1.0 → 1.3.1

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 (83) hide show
  1. package/README.ko.md +7 -0
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +23 -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 +135 -38
  9. package/package.json +13 -4
  10. package/preload.js +6 -0
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -19
  13. package/renderer/app-dashboard.js +190 -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 +211 -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 +103 -33
  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 +833 -16
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +145 -59
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +6 -0
  38. package/renderer/styles-cards.css +26 -0
  39. package/renderer/styles-components.css +86 -9
  40. package/renderer/styles-management.css +355 -0
  41. package/renderer/styles-overlays.css +8 -2
  42. package/renderer/styles-product.css +24 -16
  43. package/renderer/styles-quality.css +312 -0
  44. package/renderer/styles-readability.css +862 -0
  45. package/renderer/styles-responsive-product.css +84 -12
  46. package/renderer/styles-responsive-runtime.css +22 -14
  47. package/renderer/styles-responsive-shell.css +44 -48
  48. package/renderer/styles-responsive-workflows.css +37 -3
  49. package/renderer/styles-run-composer.css +5 -0
  50. package/renderer/styles-runtime-overview.css +285 -0
  51. package/renderer/styles-settings.css +160 -0
  52. package/renderer/styles-terminal.css +339 -2
  53. package/renderer/styles-workflow-map.css +362 -15
  54. package/renderer/styles-workflows.css +69 -2
  55. package/renderer/styles.css +27 -12
  56. package/renderer/terminal-agent.js +17 -13
  57. package/renderer/terminal-events.js +233 -40
  58. package/renderer/terminal-workbench.js +165 -77
  59. package/renderer/terminal.js +341 -34
  60. package/src/agentMonitor/claudeParser.js +96 -7
  61. package/src/agentMonitor/codexParser.js +59 -4
  62. package/src/agentMonitor/executionActivity.js +282 -0
  63. package/src/agentMonitor/genericParser.js +56 -13
  64. package/src/agentMonitor/hierarchy.js +17 -0
  65. package/src/agentMonitor/responseIntent.js +51 -0
  66. package/src/agentMonitor.js +21 -3
  67. package/src/agentRunner.js +66 -1
  68. package/src/attentionNotifier.js +14 -10
  69. package/src/automationMonitor.js +258 -0
  70. package/src/contracts.js +10 -1
  71. package/src/ipc/registerAgentIpc.js +9 -3
  72. package/src/ipc/registerAppIpc.js +4 -1
  73. package/src/ipc/registerTerminalIpc.js +12 -6
  74. package/src/macUpdateHelper.js +210 -0
  75. package/src/monitorWorker.js +65 -6
  76. package/src/platformPath.js +80 -0
  77. package/src/processMonitor.js +80 -22
  78. package/src/providerVisibilityStore.js +45 -0
  79. package/src/sessionIntelligence.js +247 -0
  80. package/src/terminalHost.js +381 -0
  81. package/src/terminalHostDaemon.js +60 -0
  82. package/src/terminalManager.js +158 -3
  83. 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,14 @@ 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
 
16
21
  function renderProviderRail() {
17
- $("#providerRail").innerHTML = state.providers
22
+ $("#providerRail").innerHTML = visibleProviders()
18
23
  .map((provider) => {
19
24
  const available = !!state.availability[provider.id];
20
25
  return `<div class="provider-rail-item ${available ? "connected" : ""}" style="${providerStyle(provider.id)}">
@@ -27,34 +32,95 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
27
32
  }
28
33
 
29
34
  function isProjectlessSession(session) {
30
- if (!session || !session.cwd) return true;
35
+ const cwd = session && (session.originCwd || session.cwd);
36
+ if (!cwd) return true;
31
37
  if (typeof session.projectless === "boolean") return session.projectless;
32
- const normalized = String(session.cwd).replace(/\\/g, "/").replace(/\/+$/, "");
38
+ const normalized = String(cwd).replace(/\\/g, "/").replace(/\/+$/, "");
33
39
  return session.provider === "codex" && session.clientKind === "codex-desktop" && /(?:^|\/)Documents\/Codex\/\d{4}-\d{2}-\d{2}\/new-chat$/i.test(normalized);
34
40
  }
35
41
 
42
+ function sessionOriginPath(session) {
43
+ return String(session && (session.originCwd || session.cwd) || "").trim();
44
+ }
45
+
46
+ function normalizedProjectPath(value) {
47
+ return String(value || "").trim().replace(/\\/g, "/").replace(/\/+$/, "").toLocaleLowerCase();
48
+ }
49
+
50
+ function projectContainsPath(projectPath, candidatePath) {
51
+ const project = normalizedProjectPath(projectPath);
52
+ const candidate = normalizedProjectPath(candidatePath);
53
+ return Boolean(project && candidate && (candidate === project || candidate.startsWith(`${project}/`)));
54
+ }
55
+
56
+ function projectName(projectPath) {
57
+ const normalized = String(projectPath || "").replace(/\\/g, "/").replace(/\/+$/, "");
58
+ return normalized.split("/").filter(Boolean).pop() || t("workspace.unknown");
59
+ }
60
+
61
+ function observedProjects() {
62
+ const projects = new Map();
63
+ const saved = state.workspaces.map((item) => ({ ...item, key: normalizedProjectPath(item.path) }));
64
+ saved.forEach((item) => projects.set(item.key, { path: item.path, name: item.name || projectName(item.path), saved: true, count: 0 }));
65
+ visibleSessions().filter((session) => !session.parentId && !isProjectlessSession(session)).forEach((session) => {
66
+ const originPath = sessionOriginPath(session);
67
+ if (!originPath) return;
68
+ const owner = saved
69
+ .filter((item) => projectContainsPath(item.path, originPath))
70
+ .sort((a, b) => b.key.length - a.key.length)[0];
71
+ const path = owner ? owner.path : originPath;
72
+ const key = normalizedProjectPath(path);
73
+ const project = projects.get(key) || { path, name: projectName(path), saved: false, count: 0 };
74
+ project.count += 1;
75
+ project.lastActivityAt = !project.lastActivityAt || Date.parse(session.updatedAt || 0) > Date.parse(project.lastActivityAt || 0)
76
+ ? session.updatedAt
77
+ : project.lastActivityAt;
78
+ projects.set(key, project);
79
+ });
80
+ const items = [...projects.values()];
81
+ const duplicateNames = new Map();
82
+ items.forEach((item) => duplicateNames.set(item.name.toLocaleLowerCase(), (duplicateNames.get(item.name.toLocaleLowerCase()) || 0) + 1));
83
+ items.forEach((item) => {
84
+ if (duplicateNames.get(item.name.toLocaleLowerCase()) < 2) return;
85
+ const parts = String(item.path).replace(/\\/g, "/").replace(/\/+$/, "").split("/").filter(Boolean);
86
+ item.name = parts.slice(-2).join("/") || item.name;
87
+ });
88
+ return items.sort((a, b) => Number(b.count || 0) - Number(a.count || 0)
89
+ || Number(b.saved) - Number(a.saved)
90
+ || String(a.name).localeCompare(String(b.name), uiLocale()));
91
+ }
92
+
36
93
  function sessionWorkspaceLabel(session) {
37
- return isProjectlessSession(session) ? window.LoadToAgentI18n.t("ui.no_project") : (session && session.workspace) || "작업 폴더 미상";
94
+ return isProjectlessSession(session)
95
+ ? t("ui.no_project")
96
+ : (session && session.workspace) || projectName(sessionOriginPath(session));
38
97
  }
39
98
 
40
99
  function matchesWorkspaceFilter(session) {
41
100
  if (state.workspace === "all") return true;
42
101
  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
- );
102
+ return !isProjectlessSession(session) && projectContainsPath(state.workspace, sessionOriginPath(session));
49
103
  }
50
104
 
51
105
  function renderWorkspaces() {
52
- const list = $("#workspaceList");
53
- const projectlessCount = ((state.snapshot && state.snapshot.sessions) || []).filter((session) => !session.parentId && isProjectlessSession(session)).length;
54
- list.innerHTML =
106
+ const lists = [$("#workspaceList"), $("#mobileWorkspaceList")].filter(Boolean);
107
+ const rootSessions = visibleSessions().filter((session) => !session.parentId);
108
+ const projects = observedProjects();
109
+ const projectlessCount = rootSessions.filter(isProjectlessSession).length;
110
+ const savedWorkspaceExists = state.workspace === "all"
111
+ || (state.workspace === PROJECTLESS_WORKSPACE && projectlessCount > 0)
112
+ || projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
113
+ if (!savedWorkspaceExists) state.workspace = "all";
114
+ const projectButton = (item) => `<button type="button" class="workspace-item observed-project ${state.workspace === item.path ? "selected" : ""}"
115
+ data-workspace="${esc(item.path)}" title="${esc(item.path)}"
116
+ aria-label="${esc(t("project.filter_named", { name: item.name, count: item.count }))}"
117
+ aria-pressed="${state.workspace === item.path ? "true" : "false"}">
118
+ <strong>${esc(item.name)}</strong><small>${Number(item.count || 0)}</small>
119
+ </button>`;
120
+ const html =
55
121
  `<button type="button" class="workspace-item ${state.workspace === "all" ? "selected" : ""}"
56
122
  data-workspace="all" aria-pressed="${state.workspace === "all" ? "true" : "false"}">
57
- <strong>${window.LoadToAgentI18n.t("ui.all_workspaces")}</strong>
123
+ <strong>${window.LoadToAgentI18n.t("project.all")}</strong><small>${rootSessions.length}</small>
58
124
  </button>` +
59
125
  (projectlessCount
60
126
  ? `<button type="button" class="workspace-item projectless ${state.workspace === PROJECTLESS_WORKSPACE ? "selected" : ""}"
@@ -65,34 +131,39 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
65
131
  <small>${projectlessCount}</small>
66
132
  </button>`
67
133
  : "") +
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>
134
+ projects.map((item) => item.saved ? `<div class="workspace-row">
135
+ ${projectButton(item)}
76
136
  <button type="button" class="workspace-remove" data-remove-workspace="${esc(item.path)}"
77
- aria-label="${esc(item.name)} 작업 폴더를 목록에서 제거"
137
+ aria-label="${esc(t("workspace.remove_named", { name: item.name }))}"
78
138
  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>` : "");
139
+ </div>` : projectButton(item)).join("") +
140
+ (!projects.length && !projectlessCount ? `<div class="workspace-empty">${window.LoadToAgentI18n.t("project.empty")}</div>` : "");
141
+ lists.forEach((list) => { list.innerHTML = html; });
142
+ const selectedProject = projects.find((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
143
+ const mobileSummary = $("#mobileWorkspaceSummary");
144
+ if (mobileSummary) mobileSummary.textContent = state.workspace === "all"
145
+ ? t("project.all")
146
+ : state.workspace === PROJECTLESS_WORKSPACE
147
+ ? t("ui.no_project")
148
+ : selectedProject?.name || projectName(state.workspace);
83
149
  }
84
150
 
85
151
  function renderGlobalStats() {
86
- const totals = (state.snapshot && state.snapshot.summary && state.snapshot.summary.totals) || {};
87
- const sessions = (state.snapshot && state.snapshot.sessions) || [];
152
+ const sessions = visibleSessions();
153
+ const totals = {
154
+ active: sessions.filter((session) => session.status === "running" || session.status === "starting").length,
155
+ waiting: sessions.filter((session) => session.attention?.required).length,
156
+ usage: { total: sessions.reduce((sum, session) => sum + Number(session.usage && session.usage.total || 0), 0) },
157
+ };
88
158
  const rootCount = sessions.filter((session) => !session.parentId).length;
89
- const helperCount = sessions.filter((session) => session.parentId).length;
159
+ const criticalCount = sessions.filter((session) => session.health?.level === "critical").length;
160
+ const riskCount = sessions.filter((session) => session.health?.level === "warning" || Number(session.context?.percent || 0) >= 75).length;
90
161
  const items = [
91
162
  [window.LoadToAgentI18n.t("ui.all_tasks"), rootCount, window.LoadToAgentI18n.t("ui.items"), ""],
92
163
  [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"), ""],
164
+ [window.LoadToAgentI18n.t("management.action_required"), totals.waiting || 0, window.LoadToAgentI18n.t("ui.items"), "alert"],
165
+ [window.LoadToAgentI18n.t("management.health.critical"), criticalCount, window.LoadToAgentI18n.t("ui.items"), "critical"],
166
+ [window.LoadToAgentI18n.t("management.risk_total"), riskCount, window.LoadToAgentI18n.t("ui.items"), "warning"],
96
167
  ];
97
168
  $("#globalStats").innerHTML = items
98
169
  .map(
@@ -104,9 +175,46 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
104
175
  )
105
176
  .join("");
106
177
  $("#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;
178
+ const activeRootCount = sessions.filter((session) => !session.parentId && ["running", "starting"].includes(session.status)).length;
179
+ $("#navActiveCount").textContent = activeRootCount;
180
+ const reviewCount = sessions.filter((session) => context.managementBucket?.(session) !== "healthy").length;
181
+ $("#navWaitingCount").textContent = reviewCount;
182
+ const scheduledCount = (state.snapshot?.automations || [])
183
+ .filter((item) => isProviderVisible(item.provider || "codex")).length;
184
+ const loopCount = sessions.filter(isRuntimeLoopSession).length;
185
+ $("#navRuntimeCount").textContent = scheduledCount + loopCount;
186
+ const tmuxSessionCount = Number(state.snapshot?.tmux?.summary?.sessions || 0);
187
+ $("#navTmuxCount").textContent = tmuxSessionCount;
188
+ $("#advancedToolsCount").textContent = scheduledCount + loopCount + Number($("#navTerminalCount").textContent || 0) + tmuxSessionCount;
189
+ const navCounts = {
190
+ all: rootCount,
191
+ active: activeRootCount,
192
+ waiting: reviewCount,
193
+ runtime: scheduledCount + loopCount,
194
+ terminal: Number($("#navTerminalCount").textContent || 0),
195
+ tmux: tmuxSessionCount,
196
+ };
197
+ document.querySelectorAll(".nav-item[data-view]").forEach((button) => {
198
+ const key = {
199
+ all: "app.nav.home", active: "app.nav.active", waiting: "app.nav.needs_review", runtime: "app.nav.runtime",
200
+ terminal: "app.nav.session_terminal", tmux: "app.nav.tmux", settings: "app.nav.settings",
201
+ }[button.dataset.view];
202
+ const label = t(key);
203
+ const count = navCounts[button.dataset.view];
204
+ const unitKey = { all: "tasks", active: "tasks", waiting: "tasks", runtime: "runs", terminal: "sessions", tmux: "groups" }[button.dataset.view];
205
+ const unit = unitKey ? t(`quality.unit.${unitKey}`) : "";
206
+ const accessibleLabel = Number.isFinite(count) ? t("quality.nav_count_detailed", { label, count, unit }) : label;
207
+ button.setAttribute("aria-label", accessibleLabel);
208
+ button.setAttribute("title", accessibleLabel);
209
+ });
210
+ const advancedCount = scheduledCount + loopCount + Number($("#navTerminalCount").textContent || 0) + tmuxSessionCount;
211
+ $("#advancedToolsNav")?.querySelector("summary")?.setAttribute("aria-label", t("quality.nav_count_detailed", {
212
+ label: t("management.advanced_tools"), count: advancedCount, unit: t("quality.unit.items"),
213
+ }));
214
+ const tmuxShortcut = $("#openTmuxFromAgentWork");
215
+ $("#agentWorkTmuxCount").textContent = tmuxSessionCount;
216
+ tmuxShortcut.dataset.i18nParams = JSON.stringify({ count: tmuxSessionCount });
217
+ tmuxShortcut.setAttribute("aria-label", t("graph.open_tmux_workspace_count", { count: tmuxSessionCount }));
110
218
  }
111
219
 
112
220
  function formatBytes(value) {
@@ -159,7 +267,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
159
267
  ],
160
268
  downloaded: [
161
269
  "✓", 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"),
270
+ window.LoadToAgentI18n.t("settings.update.auto_install_restart"),
163
271
  ],
164
272
  error: [
165
273
  "!", window.LoadToAgentI18n.t("ui.check_failed"), window.LoadToAgentI18n.t("ui.could_not_check_for_updates"),
@@ -202,7 +310,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
202
310
  install.textContent = downloading
203
311
  ? window.LoadToAgentI18n.t("ui.downloading_2")
204
312
  : downloaded
205
- ? window.LoadToAgentI18n.t("ui.open_installer")
313
+ ? `${window.LoadToAgentI18n.t("settings.update.download")} · ${window.LoadToAgentI18n.t("ui.restart")}`
206
314
  : window.LoadToAgentI18n.t("settings.update.download");
207
315
  const progress = $("#updateProgress");
208
316
  progress.classList.toggle("hidden", !downloading && !downloaded);
@@ -210,11 +318,13 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
210
318
  $("#updateProgressBar").style.width = `${Math.max(0, Math.min(100, Number(update.progress || 0)))}%`;
211
319
  $(".update-progress-track").setAttribute("aria-valuenow", String(Math.max(0, Math.min(100, Number(update.progress || 0)))));
212
320
  $("#updateProgressBytes").textContent = downloaded
213
- ? `${formatBytes(update.totalBytes || update.downloadedBytes)} · 파일 검증 완료`
321
+ ? `${formatBytes(update.totalBytes || update.downloadedBytes)} · ${window.LoadToAgentI18n.t("settings.update.file_verified")}`
214
322
  : `${formatBytes(update.downloadedBytes)} / ${update.totalBytes ? formatBytes(update.totalBytes) : window.LoadToAgentI18n.t("ui.checking_size")}`;
215
323
  const error = $("#updateError");
216
324
  error.classList.toggle("hidden", !update.error);
217
- error.textContent = update.error || "";
325
+ error.textContent = update.error
326
+ ? window.LoadToAgentI18n.errorText(update.error, "ui.could_not_check_for_updates")
327
+ : "";
218
328
  const notes = $("#releaseNotes");
219
329
  notes.classList.toggle("hidden", !update.latestVersion);
220
330
  $("#releaseNotesText").textContent =
@@ -231,16 +341,20 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
231
341
  function renderProviderOverview() {
232
342
  pruneProviderFilters();
233
343
  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) => {
344
+ const sessions = visibleSessions();
345
+ const visibleSummaries = summaries.filter((provider) => isProviderVisible(provider.id));
346
+ const overviewTabStopId = state.providerFilters.size ? [...state.providerFilters][0] : visibleSummaries[0]?.id;
347
+ $("#providerOverview").innerHTML = visibleSummaries
348
+ .map((provider, index) => {
237
349
  const rootCount = sessions.filter((session) => session.provider === provider.id && !session.parentId).length;
238
350
  const selected = state.providerFilters.has(provider.id);
351
+ const tabStop = provider.id === overviewTabStopId;
239
352
  return `<button type="button" class="provider-overview-card ${selected ? "selected" : ""}"
240
353
  data-provider-card="${esc(provider.id)}"
241
354
  data-motion-key="provider:${esc(provider.id)}"
242
355
  data-motion-value="${provider.active || 0}:${rootCount}:${(provider.usage && provider.usage.total) || 0}"
243
356
  style="${providerStyle(provider.id)}"
357
+ tabindex="${tabStop ? "0" : "-1"}"
244
358
  aria-pressed="${selected ? "true" : "false"}">
245
359
  <div class="poc-head">
246
360
  <span class="provider-mark">${esc(provider.mark)}</span>
@@ -263,7 +377,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
263
377
  }
264
378
 
265
379
  function pruneProviderFilters() {
266
- const valid = new Set(state.providers.map((provider) => provider.id));
380
+ const valid = new Set(visibleProviders().map((provider) => provider.id));
267
381
  for (const id of [...state.providerFilters]) if (!valid.has(id)) state.providerFilters.delete(id);
268
382
  if (valid.size > 0 && state.providerFilters.size === valid.size) state.providerFilters.clear();
269
383
  }
@@ -273,28 +387,29 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
273
387
  if (providerId === "all") state.providerFilters.clear();
274
388
  else if (state.providerFilters.has(providerId)) state.providerFilters.delete(providerId);
275
389
  else state.providerFilters.add(providerId);
276
- if (state.providers.length > 0 && state.providerFilters.size === state.providers.length) state.providerFilters.clear();
390
+ if (visibleProviders().length > 0 && state.providerFilters.size === visibleProviders().length) state.providerFilters.clear();
277
391
  }
278
392
 
279
393
  function renderProviderFilter() {
280
394
  pruneProviderFilters();
281
395
  const allSelected = state.providerFilters.size === 0;
396
+ const tabStopId = allSelected ? "all" : [...state.providerFilters][0];
282
397
  const button = (id, label, mark = "") => {
283
398
  const selected = id === "all" ? allSelected : state.providerFilters.has(id);
284
399
  return `<button type="button" class="provider-filter-chip ${selected ? "selected" : ""}"
285
- data-provider-filter="${esc(id)}" aria-pressed="${selected ? "true" : "false"}">
400
+ data-provider-filter="${esc(id)}" tabindex="${id === tabStopId ? "0" : "-1"}" aria-pressed="${selected ? "true" : "false"}">
286
401
  <i class="provider-filter-check" aria-hidden="true">✓</i>
287
402
  ${mark ? `<span class="provider-filter-mark" aria-hidden="true">${esc(mark)}</span>` : ""}<b>${esc(label)}</b>
288
403
  </button>`;
289
404
  };
290
405
  $("#providerFilter").innerHTML =
291
406
  button("all", window.LoadToAgentI18n.t("ui.all_ai")) +
292
- state.providers.map((provider) => button(provider.id, provider.label, provider.mark)).join("");
407
+ visibleProviders().map((provider) => button(provider.id, provider.label, provider.mark)).join("");
293
408
  }
294
409
 
295
410
  function announceProviderFilter() {
296
411
  const labels = state.providerFilters.size
297
- ? state.providers.filter((provider) => state.providerFilters.has(provider.id)).map((provider) => provider.label).join(", ")
412
+ ? visibleProviders().filter((provider) => state.providerFilters.has(provider.id)).map((provider) => provider.label).join(", ")
298
413
  : window.LoadToAgentI18n.t("ui.all_ai");
299
414
  $("#providerFilterStatus").textContent = window.LoadToAgentI18n.t("filter.result_summary", {
300
415
  providers: labels,
@@ -303,16 +418,16 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
303
418
  }
304
419
 
305
420
  function filteredSessions() {
306
- const allSessions = [...((state.snapshot && state.snapshot.sessions) || [])];
421
+ const allSessions = [...visibleSessions()];
307
422
  let sessions = state.view === "waiting" ? allSessions : allSessions.filter((session) => !session.parentId);
308
423
  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");
424
+ if (state.view === "waiting") sessions = sessions.filter((session) => session.attention?.required || ["critical", "warning", "unknown"].includes(session.health?.level) || !session.health);
310
425
  if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
311
426
  sessions = sessions.filter(matchesWorkspaceFilter);
312
- const query = state.search.trim().toLowerCase();
427
+ const query = state.search.replace(/\s+/g, " ").trim().toLowerCase();
313
428
  if (query) {
314
429
  sessions = sessions.filter((session) =>
315
- [session.title, session.model, session.cwd, session.agentName, ...(session.messages || []).slice(-12).map((item) => item.text)]
430
+ [session.title, session.model, session.originCwd, session.cwd, session.workspace, session.agentName, ...(session.messages || []).slice(-12).map((item) => item.text)]
316
431
  .join(" ")
317
432
  .toLowerCase()
318
433
  .includes(query),
@@ -330,13 +445,13 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
330
445
  }
331
446
 
332
447
  function graphFilteredSessions() {
333
- let sessions = [...((state.snapshot && state.snapshot.sessions) || [])];
448
+ let sessions = [...visibleSessions()];
334
449
  if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
335
450
  sessions = sessions.filter(matchesWorkspaceFilter);
336
- const query = state.search.trim().toLowerCase();
451
+ const query = state.search.replace(/\s+/g, " ").trim().toLowerCase();
337
452
  if (query)
338
453
  sessions = sessions.filter((session) =>
339
- [session.title, session.model, session.cwd, session.agentName, session.agentRole, ...(session.messages || []).map((item) => item.text)]
454
+ [session.title, session.model, session.originCwd, session.cwd, session.workspace, session.agentName, session.agentRole, ...(session.messages || []).map((item) => item.text)]
340
455
  .join(" ")
341
456
  .toLowerCase()
342
457
  .includes(query),
@@ -344,9 +459,28 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
344
459
  return sessions;
345
460
  }
346
461
 
462
+ function renderProviderVisibilitySettings() {
463
+ const list = $("#providerVisibilityList");
464
+ if (!list) return;
465
+ list.innerHTML = state.providers.map((provider) => {
466
+ const visible = isProviderVisible(provider.id);
467
+ const status = window.LoadToAgentI18n.t(visible ? "settings.providers.visible" : "settings.providers.hidden");
468
+ return `<label class="provider-visibility-option ${visible ? "enabled" : "disabled"}" style="${providerStyle(provider.id)}">
469
+ <span class="provider-mark" aria-hidden="true">${esc(provider.mark)}</span>
470
+ <span class="provider-visibility-name"><b>${esc(provider.label)}</b><small>${esc(provider.company)}</small></span>
471
+ <span class="provider-visibility-status">${esc(status)}</span>
472
+ <input type="checkbox" data-provider-visibility="${esc(provider.id)}" ${visible ? "checked" : ""}
473
+ aria-label="${esc(`${provider.label} ${status}`)}">
474
+ <span class="provider-toggle" aria-hidden="true"><i></i></span>
475
+ </label>`;
476
+ }).join("");
477
+ }
478
+
347
479
  return {
348
480
  renderProviderRail,
349
481
  isProjectlessSession,
482
+ sessionOriginPath,
483
+ observedProjects,
350
484
  sessionWorkspaceLabel,
351
485
  matchesWorkspaceFilter,
352
486
  renderWorkspaces,
@@ -361,5 +495,6 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
361
495
  announceProviderFilter,
362
496
  filteredSessions,
363
497
  graphFilteredSessions,
498
+ renderProviderVisibilitySettings,
364
499
  };
365
500
  };
@@ -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