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,42 +3,137 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createFilterEventBindings = function createFilterEventBindings(context = {}) {
6
- const { $, state, renderSessions, render, renderWorkspaces, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, performUiAction, toast } = context;
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
7
+ const { $, state, setProviderVisible = () => {}, visibleSnapshot = () => state.snapshot, closeDrawer = () => {}, openDrawer = () => {}, renderSessions, render, renderWorkspaces, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, filteredSessions, performUiAction, toast, announce, normalizedSearch = (value) => String(value || "").trim(), saveDashboardPreferences = () => {}, restoreDialogTrigger = () => {}, setDialogOpenState = () => {} } = context;
7
8
 
8
9
  function bindFilterAndWorkspaceEvents() {
10
+ const syncFilterResetButton = () => {
11
+ const hasFilters = Boolean(
12
+ $("#searchInput").value || state.search || state.providerFilters.size || state.workspace !== "all" || state.sort !== "recent",
13
+ );
14
+ $("#resetFiltersBtn").classList.toggle("hidden", !hasFilters);
15
+ };
16
+ const moveFocus = (event, container, selector, previousKeys, nextKeys) => {
17
+ if (![...previousKeys, ...nextKeys, "Home", "End"].includes(event.key)) return false;
18
+ const items = Array.from(container.querySelectorAll(selector)).filter((item) => !item.disabled && !item.hidden);
19
+ if (!items.length) return false;
20
+ const current = Math.max(0, items.indexOf(event.target.closest(selector)));
21
+ const next = event.key === "Home"
22
+ ? 0
23
+ : event.key === "End"
24
+ ? items.length - 1
25
+ : (current + (nextKeys.includes(event.key) ? 1 : -1) + items.length) % items.length;
26
+ event.preventDefault();
27
+ items[next].focus();
28
+ return true;
29
+ };
9
30
  $("#loadMoreBtn").addEventListener("click", () => {
31
+ const previousCount = document.querySelectorAll("#sessionGrid [data-session-id]").length;
10
32
  state.visibleLimit += 30;
11
33
  renderSessions("load-more");
34
+ const cards = document.querySelectorAll("#sessionGrid [data-session-id]");
35
+ cards[Math.min(previousCount, cards.length - 1)]?.focus({ preventScroll: true });
36
+ announce(window.LoadToAgentI18n.t("filter.more_loaded", { count: Math.max(0, cards.length - previousCount) }));
12
37
  });
13
- $("#workspaceList").addEventListener("click", async (event) => {
38
+ const workspaceLists = [$("#workspaceList"), $("#mobileWorkspaceList")].filter(Boolean);
39
+ const handleWorkspaceClick = async (event) => {
40
+ const activeList = event.currentTarget;
14
41
  const remove = event.target.closest("[data-remove-workspace]");
15
42
  if (remove) {
16
43
  event.stopPropagation();
17
- const workspaces = await performUiAction(() => window.loadtoagent.removeWorkspace(remove.dataset.removeWorkspace), "작업 폴더를 제거하지 못했습니다.");
44
+ const path = remove.dataset.removeWorkspace;
45
+ const workspaceItems = Array.from(activeList.querySelectorAll("[data-workspace]"));
46
+ const workspaceIndex = Math.max(0, workspaceItems.indexOf(remove.closest(".workspace-row")?.querySelector("[data-workspace]")));
47
+ const workspaces = await performUiAction(() => window.loadtoagent.removeWorkspace(remove.dataset.removeWorkspace), t("workspace.remove_failed"), remove);
18
48
  if (!workspaces) return;
19
49
  state.workspaces = workspaces;
20
50
  if (state.workspace === remove.dataset.removeWorkspace) state.workspace = "all";
21
51
  render();
52
+ syncFilterResetButton();
53
+ saveDashboardPreferences();
54
+ requestAnimationFrame(() => {
55
+ const nextItems = Array.from(activeList.querySelectorAll("[data-workspace]"));
56
+ nextItems[Math.min(workspaceIndex, nextItems.length - 1)]?.focus();
57
+ });
58
+ announce(window.LoadToAgentI18n.t("quality.workspace_removed", { name: path.split(/[\\/]/).filter(Boolean).pop() || path }));
22
59
  return;
23
60
  }
24
61
  const item = event.target.closest("[data-workspace]");
25
62
  if (item) {
63
+ const label = item.querySelector("strong")?.textContent.trim() || t("project.all");
26
64
  state.workspace = item.dataset.workspace;
27
65
  state.visibleLimit = 30;
28
66
  renderWorkspaces();
29
67
  renderSessions("filter");
68
+ syncFilterResetButton();
69
+ saveDashboardPreferences();
70
+ announce(t("filter.workspace_results", { project: label, count: filteredSessions().length }));
71
+ if (activeList.id === "mobileWorkspaceList") {
72
+ const menu = $("#mobileToolsMenu");
73
+ setDialogOpenState(menu, false);
74
+ menu?.classList.add("hidden");
75
+ $("#mobileMoreBtn")?.setAttribute("aria-expanded", "false");
76
+ const focusResults = () => ($("#liveSessionGrid")?.querySelector("[data-graph-focus], [data-open-session]")
77
+ || $("#sessionGrid")?.querySelector("[data-session-id]")
78
+ || $("#mainContent"))?.focus({ preventScroll: true });
79
+ restoreDialogTrigger();
80
+ focusResults();
81
+ requestAnimationFrame(focusResults);
82
+ }
30
83
  }
84
+ };
85
+ workspaceLists.forEach((list) => {
86
+ list.addEventListener("click", handleWorkspaceClick);
87
+ list.addEventListener("keydown", (event) => {
88
+ moveFocus(event, event.currentTarget, "[data-workspace]", ["ArrowUp"], ["ArrowDown"]);
89
+ });
31
90
  });
32
91
  let searchTimer = null;
33
92
  $("#searchInput").addEventListener("input", (event) => {
34
93
  clearTimeout(searchTimer);
35
94
  const value = event.target.value;
95
+ $("#searchClearBtn").classList.toggle("hidden", !value);
96
+ syncFilterResetButton();
36
97
  searchTimer = setTimeout(() => {
37
- state.search = value;
98
+ state.search = normalizedSearch(value);
38
99
  state.visibleLimit = 30;
39
100
  renderSessions("filter");
101
+ announce(window.LoadToAgentI18n.t("filter.search_results", { count: filteredSessions().length }));
102
+ syncFilterResetButton();
103
+ saveDashboardPreferences();
40
104
  }, 120);
41
105
  });
106
+ $("#searchClearBtn").addEventListener("click", () => {
107
+ clearTimeout(searchTimer);
108
+ $("#searchInput").value = "";
109
+ $("#searchClearBtn").classList.add("hidden");
110
+ state.search = "";
111
+ state.visibleLimit = 30;
112
+ renderSessions("filter");
113
+ announce(window.LoadToAgentI18n.t("filter.search_cleared"));
114
+ $("#searchInput").focus();
115
+ syncFilterResetButton();
116
+ saveDashboardPreferences();
117
+ });
118
+ $("#searchInput").addEventListener("keydown", (event) => {
119
+ if (event.key === "Escape" && event.currentTarget.value) {
120
+ event.preventDefault();
121
+ $("#searchClearBtn").click();
122
+ return;
123
+ }
124
+ if (event.key === "ArrowDown") {
125
+ const first = $("#sessionGrid [data-session-id]") || $("#liveSessionGrid button, #liveSessionGrid [tabindex='0']");
126
+ if (first) {
127
+ event.preventDefault();
128
+ first.focus();
129
+ }
130
+ return;
131
+ }
132
+ if (event.key === "Enter" && filteredSessions().length === 1) {
133
+ event.preventDefault();
134
+ openDrawer(filteredSessions()[0].id);
135
+ }
136
+ });
42
137
  $("#providerFilter").addEventListener("click", (event) => {
43
138
  const chip = event.target.closest("[data-provider-filter]");
44
139
  if (!chip) return;
@@ -48,30 +143,94 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
48
143
  renderProviderOverview();
49
144
  renderSessions("filter");
50
145
  announceProviderFilter();
51
- requestAnimationFrame(() => {
52
- const next = $("#providerFilter").querySelector(`[data-provider-filter="${CSS.escape(chip.dataset.providerFilter)}"]`);
53
- next?.classList.add("filter-clicked");
54
- next?.focus();
55
- });
146
+ const next = $("#providerFilter").querySelector(`[data-provider-filter="${CSS.escape(chip.dataset.providerFilter)}"]`);
147
+ next?.classList.add("filter-clicked");
148
+ next?.focus();
149
+ syncFilterResetButton();
150
+ saveDashboardPreferences();
151
+ });
152
+ $("#providerFilter").addEventListener("keydown", (event) => {
153
+ moveFocus(event, event.currentTarget, "[data-provider-filter]", ["ArrowLeft", "ArrowUp"], ["ArrowRight", "ArrowDown"]);
56
154
  });
57
155
  $("#sortSelect").addEventListener("change", (event) => {
58
156
  state.sort = event.target.value;
59
157
  state.visibleLimit = 30;
60
158
  renderSessions("filter");
159
+ const label = event.target.selectedOptions[0]?.textContent || event.target.value;
160
+ announce(window.LoadToAgentI18n.t("filter.sort_changed", { sort: label, count: filteredSessions().length }));
161
+ syncFilterResetButton();
162
+ saveDashboardPreferences();
61
163
  });
62
- $("#addWorkspaceBtn").addEventListener("click", async () => {
63
- const workspaces = await performUiAction(() => window.loadtoagent.addWorkspaces(), "작업 폴더를 추가하지 못했습니다.");
164
+ $("#resetFiltersBtn").addEventListener("click", () => {
165
+ clearTimeout(searchTimer);
166
+ state.search = "";
167
+ state.providerFilters.clear();
168
+ state.workspace = "all";
169
+ state.sort = "recent";
170
+ state.visibleLimit = 30;
171
+ $("#searchInput").value = "";
172
+ $("#searchClearBtn").classList.add("hidden");
173
+ $("#sortSelect").value = "recent";
174
+ renderWorkspaces();
175
+ renderProviderFilter();
176
+ renderProviderOverview();
177
+ renderSessions("filter");
178
+ syncFilterResetButton();
179
+ saveDashboardPreferences();
180
+ announce(window.LoadToAgentI18n.t("filter.reset_done", { count: filteredSessions().length }));
181
+ $("#searchInput").focus();
182
+ });
183
+ $("#providerVisibilityList").addEventListener("change", async (event) => {
184
+ const input = event.target.closest("[data-provider-visibility]");
185
+ if (!input) return;
186
+ const providerId = input.dataset.providerVisibility;
187
+ const previousVisible = !input.checked;
188
+ const selectedBeforeChange = (state.rawSnapshot?.sessions || []).find((session) => session.id === state.selectedId)
189
+ || state.details.get(state.selectedId);
190
+ setProviderVisible(providerId, input.checked);
191
+ state.visibleLimit = 30;
192
+ if (state.selectedId && selectedBeforeChange && state.hiddenProviders.has(selectedBeforeChange.provider)) closeDrawer();
193
+ if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.updateSnapshot(visibleSnapshot(), state.workspaces);
194
+ render("filter");
195
+ try {
196
+ await Promise.resolve(window.loadtoagent.setProviderVisibility?.({ hidden: [...state.hiddenProviders] }));
197
+ } catch (error) {
198
+ window.LoadToAgentRendererUtils.reportRecoverableError("provider-visibility-persistence", error);
199
+ setProviderVisible(providerId, previousVisible);
200
+ if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.updateSnapshot(visibleSnapshot(), state.workspaces);
201
+ render("filter");
202
+ toast(t("settings.providers.save_failed"));
203
+ return;
204
+ }
205
+ toast(t(input.checked ? "settings.providers.shown_toast" : "settings.providers.hidden_toast"));
206
+ });
207
+ const addWorkspaceButtons = [$("#addWorkspaceBtn"), $("#mobileAddWorkspaceBtn")].filter(Boolean);
208
+ const addWorkspace = async (event) => {
209
+ const trigger = event.currentTarget;
210
+ const previousPaths = new Set(state.workspaces.map((workspace) => workspace.path));
211
+ const workspaces = await performUiAction(() => window.loadtoagent.addWorkspaces(), t("workspace.add_failed"), trigger);
64
212
  if (!workspaces) return;
65
213
  state.workspaces = workspaces;
66
214
  renderWorkspaces();
67
- });
215
+ syncFilterResetButton();
216
+ const added = state.workspaces.find((workspace) => !previousPaths.has(workspace.path));
217
+ requestAnimationFrame(() => {
218
+ const targetList = trigger.id === "mobileAddWorkspaceBtn" ? $("#mobileWorkspaceList") : $("#workspaceList");
219
+ if (added) targetList?.querySelector(`[data-workspace="${CSS.escape(added.path)}"]`)?.focus();
220
+ else trigger.focus();
221
+ });
222
+ announce(window.LoadToAgentI18n.t("quality.workspace_added", { count: state.workspaces.length }));
223
+ };
224
+ addWorkspaceButtons.forEach((button) => button.addEventListener("click", addWorkspace));
68
225
  $("#probeBtn").addEventListener("click", async () => {
69
- const nextAvailability = await performUiAction(() => window.loadtoagent.probeProviders(), "AI CLI 연결 상태를 확인하지 못했습니다.");
226
+ const nextAvailability = await performUiAction(() => window.loadtoagent.probeProviders(), t("run.cli_check_failed"), $("#probeBtn"));
70
227
  if (!nextAvailability) return;
71
228
  state.availability = nextAvailability;
72
229
  render();
73
230
  toast(window.LoadToAgentI18n.t("ui.ai_cli_connections_were_checked_again"));
74
231
  });
232
+ $("#searchClearBtn").classList.toggle("hidden", !$("#searchInput").value);
233
+ syncFilterResetButton();
75
234
  }
76
235
 
77
236
  return { bindFilterAndWorkspaceEvents };
@@ -3,17 +3,39 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createNavigationEventBindings = function createNavigationEventBindings(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
8
  $, state, motionPreference, saveGuideState, selectView, renderUpdateSettings,
8
- filteredSessions, renderSessions, openRunModal, openDrawer, toast,
9
+ filteredSessions, renderSessions, openRunModal, openDrawer, toast, performUiAction,
10
+ rememberDialogTrigger, restoreDialogTrigger, setDialogOpenState, trapDialogFocus,
9
11
  } = context;
10
12
 
11
13
  function bindNavigationAndUpdateEvents() {
14
+ const closeMobileTools = (restoreFocus = true) => {
15
+ const menu = $("#mobileToolsMenu");
16
+ setDialogOpenState(menu, false);
17
+ menu.classList.add("hidden");
18
+ $("#mobileMoreBtn").setAttribute("aria-expanded", "false");
19
+ if (restoreFocus) restoreDialogTrigger();
20
+ };
12
21
  $(".view-nav").addEventListener("click", (event) => {
13
22
  const button = event.target.closest(".nav-item");
14
23
  if (!button || !button.dataset.view) return;
15
24
  selectView(button.dataset.view);
16
25
  });
26
+ $(".view-nav").addEventListener("keydown", (event) => {
27
+ if (!["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
28
+ const buttons = Array.from(document.querySelectorAll(".view-nav .nav-item[data-view]"))
29
+ .filter((button) => !button.hidden && button.getClientRects().length > 0 && getComputedStyle(button).visibility !== "hidden");
30
+ const current = Math.max(0, buttons.indexOf(event.target.closest(".nav-item[data-view]")));
31
+ const next = event.key === "Home"
32
+ ? 0
33
+ : event.key === "End"
34
+ ? buttons.length - 1
35
+ : (current + (event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length;
36
+ event.preventDefault();
37
+ buttons[next]?.focus();
38
+ });
17
39
  $("#updateNoticeBtn").addEventListener("click", () => {
18
40
  selectView("settings");
19
41
  });
@@ -44,7 +66,7 @@ window.LoadToAgentAppFactories.createNavigationEventBindings = function createNa
44
66
  const first = filteredSessions()[0] || ((state.snapshot && state.snapshot.sessions) || [])[0];
45
67
  if (first) openDrawer(first.id);
46
68
  else {
47
- toast("아직 열어볼 작업이 없어요. 먼저 AI에게 첫 일을 맡겨보세요.");
69
+ toast(t("guide.no_task_to_open"));
48
70
  openRunModal();
49
71
  }
50
72
  }
@@ -52,43 +74,68 @@ window.LoadToAgentAppFactories.createNavigationEventBindings = function createNa
52
74
  $("#mobileMoreBtn").addEventListener("click", () => {
53
75
  const menu = $("#mobileToolsMenu");
54
76
  const opening = menu.classList.contains("hidden");
55
- menu.classList.toggle("hidden", !opening);
56
- $("#mobileMoreBtn").setAttribute("aria-expanded", opening ? "true" : "false");
57
- if (opening) setTimeout(() => menu.querySelector("button")?.focus(), 0);
77
+ if (!opening) return closeMobileTools(true);
78
+ $("#mobileMoreBtn").focus({ preventScroll: true });
79
+ rememberDialogTrigger();
80
+ menu.classList.remove("hidden");
81
+ setDialogOpenState(menu, true);
82
+ $("#mobileMoreBtn").setAttribute("aria-expanded", "true");
83
+ menu.querySelector("button")?.focus({ preventScroll: true });
84
+ });
85
+ $("#mobileToolsCloseBtn").addEventListener("click", () => closeMobileTools(true));
86
+ $("#mobileToolsMenu").addEventListener("keydown", (event) => {
87
+ if (event.key === "Escape") {
88
+ event.preventDefault();
89
+ closeMobileTools(true);
90
+ return;
91
+ }
92
+ if (event.key === "Tab") {
93
+ trapDialogFocus(event);
94
+ return;
95
+ }
96
+ if (!["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
97
+ const buttons = Array.from(event.currentTarget.querySelectorAll("button:not([disabled])"));
98
+ const current = Math.max(0, buttons.indexOf(event.target.closest("button")));
99
+ const next = event.key === "Home"
100
+ ? 0
101
+ : event.key === "End"
102
+ ? buttons.length - 1
103
+ : (current + (event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length;
104
+ event.preventDefault();
105
+ buttons[next]?.focus();
58
106
  });
59
107
  $("#mobileToolsMenu").addEventListener("click", (event) => {
60
108
  const button = event.target.closest("[data-mobile-view]");
61
- if (button) selectView(button.dataset.mobileView, { focusMain: true });
109
+ if (button) {
110
+ closeMobileTools(false);
111
+ selectView(button.dataset.mobileView, { focusMain: true });
112
+ }
113
+ });
114
+ document.addEventListener("pointerdown", (event) => {
115
+ const menu = $("#mobileToolsMenu");
116
+ if (menu.classList.contains("hidden") || menu.contains(event.target) || $("#mobileMoreBtn").contains(event.target)) return;
117
+ const focusWasInside = menu.contains(document.activeElement);
118
+ closeMobileTools(focusWasInside);
62
119
  });
63
120
  $("#checkUpdateBtn").addEventListener("click", async () => {
64
- try {
65
- state.update = { ...(state.update || {}), status: "checking", error: "" };
66
- renderUpdateSettings();
67
- state.update = await window.loadtoagent.checkForUpdate();
68
- renderUpdateSettings();
69
- } catch (error) {
70
- toast((error && error.message) || window.LoadToAgentI18n.t("ui.could_not_check_for_updates"));
71
- }
121
+ state.update = { ...(state.update || {}), status: "checking", error: "" };
122
+ renderUpdateSettings();
123
+ const update = await performUiAction(() => window.loadtoagent.checkForUpdate(), "ui.could_not_check_for_updates", $("#checkUpdateBtn"));
124
+ if (update) state.update = update;
125
+ else state.update = { ...(state.update || {}), status: "error" };
126
+ renderUpdateSettings();
72
127
  });
73
128
  $("#installUpdateBtn").addEventListener("click", async () => {
74
- try {
75
- if (state.update && state.update.status === "downloaded") {
76
- await window.loadtoagent.openDownloadedUpdate();
77
- toast("설치 파일을 열었습니다. 화면의 안내에 따라 업데이트를 마무리하세요.");
78
- } else {
79
- state.update = await window.loadtoagent.downloadUpdate();
80
- renderUpdateSettings();
81
- }
82
- } catch (error) {
83
- toast((error && error.message) || window.LoadToAgentI18n.t("ui.could_not_prepare_the_update_file"));
84
- }
129
+ state.update = { ...(state.update || {}), status: "downloading", error: "" };
130
+ renderUpdateSettings();
131
+ const update = await performUiAction(() => window.loadtoagent.installDownloadedUpdate(), "ui.could_not_prepare_the_update_file", $("#installUpdateBtn"));
132
+ if (update) state.update = update;
133
+ else if (state.update && state.update.asset) state.update.status = "available";
134
+ renderUpdateSettings();
135
+ if (state.update && state.update.installMode === "manual") toast(t("ui.open_installer"));
85
136
  });
86
137
  $("#openReleaseBtn").addEventListener("click", async () => {
87
- try {
88
- await window.loadtoagent.openUpdateRelease();
89
- } catch (error) {
90
- toast((error && error.message) || window.LoadToAgentI18n.t("ui.could_not_open_the_github_release_page"));
91
- }
138
+ await performUiAction(() => window.loadtoagent.openUpdateRelease(), "ui.could_not_open_the_github_release_page", $("#openReleaseBtn"));
92
139
  });
93
140
  }
94
141
 
@@ -5,10 +5,99 @@ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
5
5
  window.LoadToAgentAppFactories.createSessionEventBindings = function createSessionEventBindings(context = {}) {
6
6
  const {
7
7
  $, state, selectView, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, renderSessions, renderTmuxMap, openDrawer, openSubagentConversation,
8
- dispatchAgentCommand, openAgentTerminal, copyBridgeCommand, openSessionOrigin,
8
+ dispatchAgentCommand, openAgentTerminal, copyBridgeCommand, openSessionOrigin, saveDashboardPreferences = () => {},
9
+ controlManagedRun, quickRespond, prepareReassignment,
10
+ copyText = async () => false,
11
+ announce = () => {},
9
12
  } = context;
10
13
 
14
+ const managementFilterLabel = value => value === "all" ? window.LoadToAgentI18n.t("management.filter_all") : window.LoadToAgentI18n.t(`management.health.${value}`);
15
+ const announceManagementFilter = value => announce(window.LoadToAgentI18n.t("management.filter_results", {
16
+ filter: managementFilterLabel(value),
17
+ count: $("#attentionInbox")?.querySelectorAll("[data-management-session]").length || 0,
18
+ }));
19
+
20
+ function bindManagementEvents() {
21
+ $("#operationsOverview").addEventListener("click", (event) => {
22
+ const open = event.target.closest("[data-open-session]");
23
+ if (open) return openDrawer(open.dataset.openSession);
24
+ const filter = event.target.closest("[data-management-filter]");
25
+ if (!filter) return;
26
+ selectView("waiting", { focusMain: true, managementFilter: filter.dataset.managementFilter });
27
+ announceManagementFilter(filter.dataset.managementFilter);
28
+ });
29
+ $("#attentionInbox").addEventListener("click", async (event) => {
30
+ const filter = event.target.closest("[data-management-inbox-filter]");
31
+ if (filter) {
32
+ state.managementFilter = filter.dataset.managementInboxFilter;
33
+ renderSessions("filter");
34
+ announceManagementFilter(state.managementFilter);
35
+ requestAnimationFrame(() => $("#attentionInbox")?.querySelector(`[data-management-inbox-filter="${CSS.escape(state.managementFilter)}"]`)?.focus({ preventScroll: true }));
36
+ return;
37
+ }
38
+ const open = event.target.closest("[data-open-session]");
39
+ if (open) return openDrawer(open.dataset.openSession);
40
+ const quick = event.target.closest("[data-attention-quick]");
41
+ if (quick) return quickRespond(quick.dataset.attentionSessionId, quick.dataset.attentionQuick, $("#attentionInbox"));
42
+ const managed = event.target.closest("[data-managed-run-action]");
43
+ if (managed) return controlManagedRun(managed.dataset.managementSessionId, managed.dataset.managedRunAction);
44
+ const reassign = event.target.closest("[data-reassign-session]");
45
+ if (reassign) prepareReassignment(reassign.dataset.reassignSession);
46
+ });
47
+ $("#attentionInbox").addEventListener("input", (event) => {
48
+ const input = event.target.closest("[data-agent-command-draft]");
49
+ if (input) state.agentCommandDrafts.set(input.dataset.agentCommandDraft, input.value);
50
+ });
51
+ $("#attentionInbox").addEventListener("change", (event) => {
52
+ const picker = event.target.closest("[data-agent-command-target]");
53
+ if (!picker) return;
54
+ if (picker.value) state.agentCommandTargets.set(picker.dataset.agentCommandTarget, picker.value);
55
+ else state.agentCommandTargets.delete(picker.dataset.agentCommandTarget);
56
+ picker.closest("form")?.querySelectorAll("button").forEach(button => { button.disabled = !picker.value; });
57
+ });
58
+ $("#attentionInbox").addEventListener("keydown", (event) => {
59
+ const input = event.target.closest("[data-agent-command-draft]");
60
+ if (!input || event.key !== "Enter" || event.shiftKey || event.isComposing || event.keyCode === 229) return;
61
+ event.preventDefault();
62
+ input.closest("form")?.requestSubmit();
63
+ });
64
+ $("#attentionInbox").addEventListener("submit", (event) => {
65
+ const form = event.target.closest("[data-agent-command-form]");
66
+ if (!form) return;
67
+ event.preventDefault();
68
+ dispatchAgentCommand(form.dataset.agentCommandForm, form);
69
+ });
70
+ }
71
+
11
72
  function bindSessionListEvents() {
73
+ $("#automationOverview").addEventListener("click", (event) => {
74
+ const loopSelect = event.target.closest("[data-loop-select]");
75
+ if (loopSelect) {
76
+ state.selectedRuntimeLoopId = loopSelect.dataset.loopSelect;
77
+ renderSessions("focus");
78
+ requestAnimationFrame(() => $("#automationOverview").querySelector(`[data-loop-select="${CSS.escape(state.selectedRuntimeLoopId)}"]`)?.focus({ preventScroll: true }));
79
+ return;
80
+ }
81
+ const sessionTarget = event.target.closest("[data-loop-open], [data-automation-session]");
82
+ if (sessionTarget) openDrawer(sessionTarget.dataset.loopOpen || sessionTarget.dataset.automationSession);
83
+ });
84
+ $("#automationOverview").addEventListener("keydown", (event) => {
85
+ const loop = event.target.closest("[data-loop-select]");
86
+ if (loop && ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) {
87
+ const tabs = Array.from(event.currentTarget.querySelectorAll("[data-loop-select]"));
88
+ const current = Math.max(0, tabs.indexOf(loop));
89
+ const next = event.key === "Home"
90
+ ? 0
91
+ : event.key === "End"
92
+ ? tabs.length - 1
93
+ : (current + (["ArrowRight", "ArrowDown"].includes(event.key) ? 1 : -1) + tabs.length) % tabs.length;
94
+ event.preventDefault();
95
+ state.selectedRuntimeLoopId = tabs[next].dataset.loopSelect;
96
+ renderSessions("focus");
97
+ requestAnimationFrame(() => $("#automationOverview")?.querySelector(`[data-loop-select="${CSS.escape(state.selectedRuntimeLoopId)}"]`)?.focus({ preventScroll: true }));
98
+ return;
99
+ }
100
+ });
12
101
  $("#providerOverview").addEventListener("click", (event) => {
13
102
  const card = event.target.closest("[data-provider-card]");
14
103
  if (!card) return;
@@ -18,11 +107,23 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
18
107
  renderProviderOverview();
19
108
  renderSessions("filter");
20
109
  announceProviderFilter();
21
- requestAnimationFrame(() => {
22
- const next = $("#providerOverview").querySelector(`[data-provider-card="${CSS.escape(card.dataset.providerCard)}"]`);
23
- next?.classList.add("filter-clicked");
24
- next?.focus();
25
- });
110
+ saveDashboardPreferences();
111
+ const next = $("#providerOverview").querySelector(`[data-provider-card="${CSS.escape(card.dataset.providerCard)}"]`);
112
+ next?.classList.add("filter-clicked");
113
+ next?.focus();
114
+ });
115
+ $("#providerOverview").addEventListener("keydown", (event) => {
116
+ if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
117
+ const cards = Array.from(event.currentTarget.querySelectorAll("[data-provider-card]"));
118
+ if (!cards.length) return;
119
+ const current = Math.max(0, cards.indexOf(event.target.closest("[data-provider-card]")));
120
+ const next = event.key === "Home"
121
+ ? 0
122
+ : event.key === "End"
123
+ ? cards.length - 1
124
+ : (current + (["ArrowRight", "ArrowDown"].includes(event.key) ? 1 : -1) + cards.length) % cards.length;
125
+ event.preventDefault();
126
+ cards[next].focus();
26
127
  });
27
128
  $("#sessionGrid").addEventListener("click", (event) => {
28
129
  const card = event.target.closest("[data-session-id]");
@@ -38,7 +139,23 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
38
139
  }
39
140
 
40
141
  function bindLiveAgentEvents() {
41
- $("#liveSessionGrid").addEventListener("click", (event) => {
142
+ $("#liveSessionGrid").addEventListener("click", async (event) => {
143
+ const copy = event.target.closest("[data-copy-text]");
144
+ if (copy) {
145
+ event.stopPropagation();
146
+ await copyText(copy.dataset.copyText);
147
+ return;
148
+ }
149
+ const executionHistory = event.target.closest("[data-execution-history-toggle]");
150
+ if (executionHistory) {
151
+ event.stopPropagation();
152
+ const ownerId = executionHistory.dataset.executionHistoryToggle;
153
+ if (state.expandedExecutionSessions.has(ownerId)) state.expandedExecutionSessions.delete(ownerId);
154
+ else state.expandedExecutionSessions.add(ownerId);
155
+ renderSessions("expand");
156
+ requestAnimationFrame(() => $("#liveSessionGrid")?.querySelector(`[data-execution-history-toggle="${CSS.escape(ownerId)}"]`)?.focus({ preventScroll: true }));
157
+ return;
158
+ }
42
159
  const tmuxPane = event.target.closest('.live-tmux-pane[data-tmux-type="pane"][data-tmux-id]');
43
160
  const tmuxOverview = event.target.closest(".live-tmux-overview-open");
44
161
  if (tmuxPane || tmuxOverview) {
@@ -137,6 +254,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
137
254
  }
138
255
 
139
256
  function bindGraphNavigationEvents() {
257
+ $("#openTmuxFromAgentWork").addEventListener("click", () => selectView("tmux", { focusMain: true }));
140
258
  $("#graphBreadcrumbs").addEventListener("click", (event) => {
141
259
  if (event.target.closest("[data-graph-reset]")) state.graphFocusId = null;
142
260
  else {
@@ -161,7 +279,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
161
279
  if (state.expandedTmuxSubagents.has(paneId)) state.expandedTmuxSubagents.delete(paneId);
162
280
  else state.expandedTmuxSubagents.add(paneId);
163
281
  renderTmuxMap();
164
- requestAnimationFrame(() => $("#tmuxMap").querySelector(`[data-tmux-subagents-toggle="${CSS.escape(paneId)}"]`)?.focus());
282
+ requestAnimationFrame(() => $("#tmuxMap").querySelector(`[data-tmux-subagents-toggle="${CSS.escape(paneId)}"]`)?.focus({ preventScroll: true }));
165
283
  return;
166
284
  }
167
285
  const subagentChat = event.target.closest("[data-open-subagent-chat]");
@@ -185,10 +303,25 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
185
303
  }
186
304
  const node = event.target.closest("[data-tmux-type][data-tmux-id]");
187
305
  if (!node) return;
188
- state.tmuxFocus = { type: node.dataset.tmuxType, id: node.dataset.tmuxId };
306
+ const nextFocus = { type: node.dataset.tmuxType, id: node.dataset.tmuxId };
307
+ state.tmuxFocus = nextFocus;
189
308
  renderTmuxMap();
309
+ requestAnimationFrame(() => $("#tmuxMap")?.querySelector(`[data-tmux-type="${CSS.escape(nextFocus.type)}"][data-tmux-id="${CSS.escape(nextFocus.id)}"]`)?.focus({ preventScroll: true }));
190
310
  if (node.dataset.tmuxType === "pane") window.LoadToAgentTerminal?.selectTmuxById(node.dataset.tmuxId);
191
311
  });
312
+ $("#tmuxMap").addEventListener("keydown", (event) => {
313
+ if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
314
+ const nodes = Array.from(event.currentTarget.querySelectorAll("[data-tmux-type][data-tmux-id]"));
315
+ const current = Math.max(0, nodes.indexOf(event.target.closest("[data-tmux-type][data-tmux-id]")));
316
+ const next = event.key === "Home"
317
+ ? 0
318
+ : event.key === "End"
319
+ ? nodes.length - 1
320
+ : (current + (["ArrowRight", "ArrowDown"].includes(event.key) ? 1 : -1) + nodes.length) % nodes.length;
321
+ event.preventDefault();
322
+ nodes.forEach((candidate, index) => { candidate.tabIndex = index === next ? 0 : -1; });
323
+ nodes[next]?.focus();
324
+ });
192
325
  $("#tmuxBreadcrumbs").addEventListener("click", (event) => {
193
326
  if (event.target.closest("[data-tmux-reset]")) state.tmuxFocus = null;
194
327
  else {
@@ -198,6 +331,19 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
198
331
  }
199
332
  renderTmuxMap();
200
333
  });
334
+ $("#tmuxBreadcrumbs").addEventListener("keydown", (event) => {
335
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
336
+ const items = Array.from(event.currentTarget.querySelectorAll("button"));
337
+ const current = Math.max(0, items.indexOf(event.target.closest("button")));
338
+ const next = event.key === "Home"
339
+ ? 0
340
+ : event.key === "End"
341
+ ? items.length - 1
342
+ : (current + (event.key === "ArrowRight" ? 1 : -1) + items.length) % items.length;
343
+ event.preventDefault();
344
+ items.forEach((candidate, index) => { candidate.tabIndex = index === next ? 0 : -1; });
345
+ items[next]?.focus();
346
+ });
201
347
  $("#tmuxResetBtn").addEventListener("click", () => {
202
348
  state.tmuxFocus = null;
203
349
  renderTmuxMap();
@@ -205,6 +351,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
205
351
  }
206
352
 
207
353
  function bindSessionAndAgentEvents() {
354
+ bindManagementEvents();
208
355
  bindSessionListEvents();
209
356
  bindLiveAgentEvents();
210
357
  bindGraphNavigationEvents();
@@ -3,13 +3,14 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createEventBindings = function createEventBindings(context = {}) {
6
- const { bindNavigationAndUpdateEvents, bindSessionAndAgentEvents, bindFilterAndWorkspaceEvents, bindDialogAndGlobalEvents } = context;
6
+ const { bindNavigationAndUpdateEvents, bindSessionAndAgentEvents, bindFilterAndWorkspaceEvents, bindDialogAndGlobalEvents, bindQualityEvents = () => {} } = context;
7
7
 
8
8
  function bindEvents() {
9
9
  bindNavigationAndUpdateEvents();
10
10
  bindSessionAndAgentEvents();
11
11
  bindFilterAndWorkspaceEvents();
12
12
  bindDialogAndGlobalEvents();
13
+ bindQualityEvents();
13
14
  }
14
15
 
15
16
  return { bindEvents };