loadtoagent 1.0.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 (86) 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 +183 -37
  9. package/package.json +15 -4
  10. package/preload.js +11 -0
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -9
  13. package/renderer/app-dashboard.js +197 -58
  14. package/renderer/app-drawer-content.js +103 -86
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +79 -55
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -9
  19. package/renderer/app-events-navigation.js +78 -31
  20. package/renderer/app-events-sessions.js +171 -4
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +32 -4
  23. package/renderer/app-graph-orchestration.js +16 -13
  24. package/renderer/app-graph-view.js +228 -112
  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 +119 -38
  32. package/renderer/app.js +155 -76
  33. package/renderer/i18n-messages.js +833 -15
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +162 -75
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +50 -1
  38. package/renderer/styles-cards.css +26 -0
  39. package/renderer/styles-collaboration.css +18 -88
  40. package/renderer/styles-components.css +126 -14
  41. package/renderer/styles-management.css +355 -0
  42. package/renderer/styles-overlays.css +13 -2
  43. package/renderer/styles-product.css +24 -16
  44. package/renderer/styles-quality.css +312 -0
  45. package/renderer/styles-readability.css +862 -0
  46. package/renderer/styles-responsive-product.css +84 -12
  47. package/renderer/styles-responsive-runtime.css +31 -14
  48. package/renderer/styles-responsive-shell.css +44 -48
  49. package/renderer/styles-responsive-workflows.css +37 -17
  50. package/renderer/styles-run-composer.css +5 -0
  51. package/renderer/styles-runtime-overview.css +285 -0
  52. package/renderer/styles-settings.css +160 -0
  53. package/renderer/styles-terminal.css +339 -2
  54. package/renderer/styles-tmux.css +154 -0
  55. package/renderer/styles-workflow-map.css +362 -15
  56. package/renderer/styles-workflows.css +69 -2
  57. package/renderer/styles.css +27 -12
  58. package/renderer/terminal-agent.js +17 -13
  59. package/renderer/terminal-events.js +233 -40
  60. package/renderer/terminal-workbench.js +176 -78
  61. package/renderer/terminal.js +350 -37
  62. package/src/agentMonitor/claudeParser.js +96 -7
  63. package/src/agentMonitor/codexParser.js +59 -4
  64. package/src/agentMonitor/executionActivity.js +282 -0
  65. package/src/agentMonitor/genericParser.js +56 -13
  66. package/src/agentMonitor/hierarchy.js +17 -0
  67. package/src/agentMonitor/responseIntent.js +51 -0
  68. package/src/agentMonitor.js +21 -3
  69. package/src/agentRunner.js +66 -1
  70. package/src/attentionNotifier.js +71 -0
  71. package/src/automationMonitor.js +258 -0
  72. package/src/contracts.js +10 -1
  73. package/src/ipc/registerAgentIpc.js +9 -3
  74. package/src/ipc/registerAppIpc.js +4 -1
  75. package/src/ipc/registerTerminalIpc.js +12 -6
  76. package/src/macUpdateHelper.js +210 -0
  77. package/src/monitorWorker.js +65 -6
  78. package/src/platformPath.js +80 -0
  79. package/src/processMonitor.js +81 -23
  80. package/src/providerVisibilityStore.js +45 -0
  81. package/src/sessionIntelligence.js +247 -0
  82. package/src/terminalHost.js +381 -0
  83. package/src/terminalHostDaemon.js +60 -0
  84. package/src/terminalManager.js +158 -3
  85. package/src/tmuxMonitor.js +2 -2
  86. package/src/updateInstaller.js +175 -0
@@ -3,18 +3,22 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDialogEventBindings = function createDialogEventBindings(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
- $, $$, state, providerInfo, renderProviderRail, scheduleAgentWorkflowConnections, resumeAgentTerminal, loadSessionDetail,
8
+ $, $$, state, providerInfo, visibleProviders = () => state.providers, renderProviderRail, scheduleAgentWorkflowConnections, resumeAgentTerminal, loadSessionDetail,
8
9
  closeDrawer, renderDrawer, providerPickerHtml, syncRunComposer, openRunModal, closeRunModal, toast, performUiAction,
9
- handleRun, trapDialogFocus,
10
+ handleRun, trapDialogFocus, currentDialog, selectView, saveRunDraft = () => {}, safeBackdrop = null,
11
+ copyText = async () => false,
12
+ dispatchAgentCommand, controlManagedRun, quickRespond, prepareReassignment,
10
13
  } = context;
11
14
 
12
15
  function bindRunComposerEvents() {
13
16
  $("#newRunBtn").addEventListener("click", openRunModal);
14
17
  $$("[data-open-run]").forEach((button) => button.addEventListener("click", openRunModal));
15
- $("#closeRunModalBtn").addEventListener("click", closeRunModal);
16
- $("#cancelRunBtn").addEventListener("click", closeRunModal);
17
- $("#runModal").addEventListener("click", (event) => {
18
+ $("#closeRunModalBtn").addEventListener("click", () => closeRunModal());
19
+ $("#cancelRunBtn").addEventListener("click", () => closeRunModal());
20
+ if (safeBackdrop) safeBackdrop($("#runModal"), () => closeRunModal());
21
+ else $("#runModal").addEventListener("click", (event) => {
18
22
  if (event.target === $("#runModal")) closeRunModal();
19
23
  });
20
24
  $("#runProviderPicker").addEventListener("click", (event) => {
@@ -23,6 +27,20 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
23
27
  state.runProvider = button.dataset.runProvider;
24
28
  $("#runProviderPicker").innerHTML = providerPickerHtml();
25
29
  syncRunComposer();
30
+ saveRunDraft();
31
+ });
32
+ $("#runProviderPicker").addEventListener("keydown", (event) => {
33
+ if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
34
+ const options = $$("#runProviderPicker [data-run-provider]:not(:disabled)");
35
+ const current = Math.max(0, options.indexOf(event.target.closest("[data-run-provider]")));
36
+ const next = event.key === "Home"
37
+ ? 0
38
+ : event.key === "End"
39
+ ? options.length - 1
40
+ : (current + (["ArrowRight", "ArrowDown"].includes(event.key) ? 1 : -1) + options.length) % options.length;
41
+ event.preventDefault();
42
+ options[next]?.click();
43
+ requestAnimationFrame(() => $("#runProviderPicker").querySelector(`[data-run-provider="${CSS.escape(state.runProvider)}"]`)?.focus());
26
44
  });
27
45
  $("#runProviderHelp").addEventListener("click", async (event) => {
28
46
  const docs = event.target.closest("[data-provider-docs]");
@@ -31,31 +49,33 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
31
49
  if (provider.docs)
32
50
  await performUiAction(async () => {
33
51
  const result = await window.loadtoagent.openExternal(provider.docs);
34
- if (!result || result.ok === false) throw new Error("공식 설치 문서를 열지 못했습니다.");
35
- }, "공식 설치 문서를 열지 못했습니다.");
52
+ if (!result || result.ok === false) throw new Error(t("run.docs_open_failed"));
53
+ }, t("run.docs_open_failed"), docs);
36
54
  return;
37
55
  }
38
- if (event.target.closest("[data-provider-recheck]")) {
39
- const nextAvailability = await performUiAction(() => window.loadtoagent.probeProviders(), "AI CLI 연결 상태를 확인하지 못했습니다.");
56
+ const recheck = event.target.closest("[data-provider-recheck]");
57
+ if (recheck) {
58
+ const nextAvailability = await performUiAction(() => window.loadtoagent.probeProviders(), t("run.cli_check_failed"), recheck);
40
59
  if (!nextAvailability) return;
41
60
  state.availability = nextAvailability;
42
- const installed = state.providers.find((provider) => state.availability[provider.id]);
61
+ const installed = visibleProviders().find((provider) => state.availability[provider.id]);
43
62
  if (installed) state.runProvider = installed.id;
44
63
  $("#runProviderPicker").innerHTML = providerPickerHtml();
45
64
  renderProviderRail();
46
65
  syncRunComposer();
47
- toast(installed ? `${installed.label} CLI를 찾았습니다. 이제 작업을 시작할 수 있어요.` : "아직 설치된 AI CLI를 찾지 못했습니다. 설치와 로그인을 확인해 주세요.");
66
+ toast(installed ? t("run.cli_ready", { provider: installed.label }) : t("run.cli_not_found"));
48
67
  }
49
68
  });
50
69
  $("#runPrompt").addEventListener("input", syncRunComposer);
51
70
  $(".run-prompt-examples").addEventListener("click", (event) => {
52
- const example = event.target.closest("[data-run-prompt-example]");
71
+ const example = event.target.closest("[data-run-prompt-key]");
53
72
  if (!example) return;
54
73
  const input = $("#runPrompt");
55
- const text = example.dataset.runPromptExample;
74
+ const text = t(example.dataset.runPromptKey);
56
75
  if (!input.value.trim()) input.value = text;
57
76
  else input.setRangeText(`${input.selectionStart ? "\n\n" : ""}${text}`, input.selectionStart, input.selectionEnd, "end");
58
77
  syncRunComposer();
78
+ saveRunDraft();
59
79
  input.focus();
60
80
  });
61
81
  $("#runWorkspaceSuggestions").addEventListener("click", (event) => {
@@ -63,14 +83,28 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
63
83
  if (!workspace) return;
64
84
  $("#runCwd").value = workspace.dataset.runWorkspace;
65
85
  syncRunComposer();
86
+ saveRunDraft();
87
+ });
88
+ $("#runWorkspaceSuggestions").addEventListener("keydown", (event) => {
89
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
90
+ const options = $$("#runWorkspaceSuggestions [data-run-workspace]");
91
+ const current = Math.max(0, options.indexOf(event.target.closest("[data-run-workspace]")));
92
+ const next = event.key === "Home"
93
+ ? 0
94
+ : event.key === "End"
95
+ ? options.length - 1
96
+ : (current + (event.key === "ArrowRight" ? 1 : -1) + options.length) % options.length;
97
+ event.preventDefault();
98
+ options[next]?.focus();
66
99
  });
67
100
  $("#runCwd").addEventListener("input", syncRunComposer);
68
101
  $("#allowWrites").addEventListener("change", syncRunComposer);
69
102
  $("#pickRunCwdBtn").addEventListener("click", async () => {
70
- const folder = await performUiAction(() => window.loadtoagent.pickWorkspace(), "작업 폴더를 선택하지 못했습니다.");
103
+ const folder = await performUiAction(() => window.loadtoagent.pickWorkspace(), t("workspace.pick_failed"), $("#pickRunCwdBtn"));
71
104
  if (folder) {
72
105
  $("#runCwd").value = folder;
73
106
  syncRunComposer();
107
+ saveRunDraft();
74
108
  }
75
109
  });
76
110
  $("#runForm").addEventListener("keydown", (event) => {
@@ -80,11 +114,27 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
80
114
  }
81
115
  });
82
116
  $("#runForm").addEventListener("submit", handleRun);
117
+ $("#runForm").addEventListener("invalid", (event) => {
118
+ event.target.setAttribute("aria-invalid", "true");
119
+ if (!$("#runForm").dataset.invalidFocusQueued) {
120
+ $("#runForm").dataset.invalidFocusQueued = "true";
121
+ queueMicrotask(() => {
122
+ delete $("#runForm").dataset.invalidFocusQueued;
123
+ $("#runForm").querySelector(":invalid")?.focus({ preventScroll: true });
124
+ });
125
+ }
126
+ }, true);
127
+ $("#runForm").addEventListener("input", (event) => {
128
+ if (event.target.matches("input, textarea, select") && event.target.checkValidity()) event.target.removeAttribute("aria-invalid");
129
+ $("#runError").classList.add("hidden");
130
+ $("#runError").textContent = "";
131
+ });
83
132
  }
84
133
 
85
134
  function bindDrawerAndGlobalEvents() {
86
135
  $("#closeDrawerBtn").addEventListener("click", closeDrawer);
87
- $("#drawerBackdrop").addEventListener("click", closeDrawer);
136
+ if (safeBackdrop) safeBackdrop($("#drawerBackdrop"), closeDrawer, $("#detailDrawer"));
137
+ else $("#drawerBackdrop").addEventListener("click", closeDrawer);
88
138
  $(".drawer-tabs").addEventListener("click", (event) => {
89
139
  const tab = event.target.closest("[data-tab]");
90
140
  if (tab) {
@@ -94,7 +144,8 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
94
144
  }
95
145
  });
96
146
  $(".drawer-tabs").addEventListener("keydown", (event) => {
97
- if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
147
+ const pageKey = event.ctrlKey && ["PageUp", "PageDown"].includes(event.key);
148
+ if (!pageKey && !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
98
149
  const tabs = $$(".drawer-tab:not(.hidden)");
99
150
  const current = Math.max(0, tabs.indexOf(event.target.closest(".drawer-tab")));
100
151
  const next =
@@ -102,7 +153,7 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
102
153
  ? 0
103
154
  : event.key === "End"
104
155
  ? tabs.length - 1
105
- : (current + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
156
+ : (current + (["ArrowRight", "ArrowDown", "PageDown"].includes(event.key) ? 1 : -1) + tabs.length) % tabs.length;
106
157
  event.preventDefault();
107
158
  state.drawerTab = tabs[next].dataset.tab;
108
159
  state.drawerForceLatest = state.drawerTab === "chat";
@@ -110,14 +161,50 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
110
161
  $(`.drawer-tab[data-tab="${state.drawerTab}"]`)?.focus();
111
162
  });
112
163
  $("#detailDrawer").addEventListener("click", async (event) => {
164
+ const copy = event.target.closest("[data-copy-text]");
165
+ if (copy) {
166
+ await copyText(copy.dataset.copyText);
167
+ return;
168
+ }
113
169
  const resume = event.target.closest("[data-resume-agent]");
114
170
  if (resume) {
115
- await resumeAgentTerminal(resume.dataset.resumeAgent);
171
+ if (resume.dataset.busy === "true") return;
172
+ resume.dataset.busy = "true";
173
+ resume.disabled = true;
174
+ resume.setAttribute("aria-busy", "true");
175
+ try {
176
+ await resumeAgentTerminal(resume.dataset.resumeAgent);
177
+ } finally {
178
+ if (resume.isConnected) {
179
+ delete resume.dataset.busy;
180
+ resume.disabled = false;
181
+ resume.removeAttribute("aria-busy");
182
+ }
183
+ }
184
+ return;
185
+ }
186
+ const quick = event.target.closest("[data-attention-quick]");
187
+ if (quick) {
188
+ quickRespond(quick.dataset.attentionSessionId || state.selectedId, quick.dataset.attentionQuick, $("#detailDrawer"));
189
+ return;
190
+ }
191
+ const managedAction = event.target.closest("[data-managed-run-action]");
192
+ if (managedAction) {
193
+ await controlManagedRun(managedAction.dataset.managementSessionId || state.selectedId, managedAction.dataset.managedRunAction);
194
+ return;
195
+ }
196
+ const reassign = event.target.closest("[data-reassign-session]");
197
+ if (reassign) {
198
+ prepareReassignment(reassign.dataset.reassignSession);
116
199
  return;
117
200
  }
118
201
  const retry = event.target.closest("[data-retry-detail]");
119
202
  if (retry) {
120
- loadSessionDetail(retry.dataset.retryDetail, true);
203
+ if (retry.dataset.busy === "true") return;
204
+ retry.dataset.busy = "true";
205
+ retry.disabled = true;
206
+ retry.setAttribute("aria-busy", "true");
207
+ await loadSessionDetail(retry.dataset.retryDetail, true);
121
208
  return;
122
209
  }
123
210
  const latest = event.target.closest("[data-scroll-latest]");
@@ -127,33 +214,55 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
127
214
  return;
128
215
  }
129
216
  const stop = event.target.closest("[data-stop-run]");
130
- if (!stop) return;
131
- const runId = stop.dataset.stopRun;
132
- if (state.stopRequests.has(runId)) return;
133
- state.stopRequests.add(runId);
134
- renderDrawer();
135
- try {
136
- const result = await window.loadtoagent.stopAgent(runId);
137
- toast(result.ok ? window.LoadToAgentI18n.t("ui.stop_request_sent") : result.error);
138
- } catch (error) {
139
- toast((error && error.message) || window.LoadToAgentI18n.t("ui.could_not_send_the_stop_request"));
140
- } finally {
141
- state.stopRequests.delete(runId);
142
- if (state.selectedId) renderDrawer();
143
- }
217
+ if (stop) await controlManagedRun(state.selectedId, "stop");
218
+ });
219
+ $("#detailDrawer").addEventListener("input", (event) => {
220
+ const input = event.target.closest("[data-agent-command-draft]");
221
+ if (input) state.agentCommandDrafts.set(input.dataset.agentCommandDraft, input.value);
222
+ });
223
+ $("#detailDrawer").addEventListener("change", (event) => {
224
+ const picker = event.target.closest("[data-agent-command-target]");
225
+ if (!picker) return;
226
+ if (picker.value) state.agentCommandTargets.set(picker.dataset.agentCommandTarget, picker.value);
227
+ else state.agentCommandTargets.delete(picker.dataset.agentCommandTarget);
228
+ picker.closest("form")?.querySelectorAll("button").forEach(button => { button.disabled = !picker.value; });
229
+ });
230
+ $("#detailDrawer").addEventListener("keydown", (event) => {
231
+ const input = event.target.closest("[data-agent-command-draft]");
232
+ if (!input || event.key !== "Enter" || event.shiftKey || event.isComposing || event.keyCode === 229) return;
233
+ event.preventDefault();
234
+ input.closest("form")?.requestSubmit();
235
+ });
236
+ $("#detailDrawer").addEventListener("submit", (event) => {
237
+ const form = event.target.closest("[data-agent-command-form]");
238
+ if (!form) return;
239
+ event.preventDefault();
240
+ dispatchAgentCommand(form.dataset.agentCommandForm, form);
144
241
  });
145
242
  document.addEventListener("keydown", (event) => {
146
243
  trapDialogFocus(event);
147
- if (event.key.toLowerCase() === "n" && (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey) {
244
+ const editable = event.target instanceof HTMLElement && Boolean(event.target.closest("input, textarea, select, [contenteditable='true']"));
245
+ const dialogOpen = Boolean(currentDialog?.());
246
+ const viewShortcuts = ["all", "active", "waiting", "runtime", "terminal", "tmux", "settings"];
247
+ if (!editable && !dialogOpen && (event.metaKey || event.ctrlKey) && /^[1-7]$/.test(event.key)) {
248
+ event.preventDefault();
249
+ selectView(viewShortcuts[Number(event.key) - 1], { focusMain: true });
250
+ return;
251
+ }
252
+ if (!editable && !dialogOpen && event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey) {
253
+ event.preventDefault();
254
+ if (state.view !== "all") selectView("all");
255
+ $("#searchInput").focus();
256
+ return;
257
+ }
258
+ if (!editable && !dialogOpen && event.key.toLowerCase() === "n" && (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey) {
148
259
  event.preventDefault();
149
260
  if ($("#runModal").classList.contains("hidden")) openRunModal();
150
261
  return;
151
262
  }
152
263
  if (event.key !== "Escape") return;
153
264
  if (!$("#mobileToolsMenu").classList.contains("hidden")) {
154
- $("#mobileToolsMenu").classList.add("hidden");
155
- $("#mobileMoreBtn").setAttribute("aria-expanded", "false");
156
- $("#mobileMoreBtn").focus();
265
+ $("#mobileToolsCloseBtn")?.click();
157
266
  } else if (!$("#runModal").classList.contains("hidden")) closeRunModal();
158
267
  else closeDrawer();
159
268
  });
@@ -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,26 +143,94 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
48
143
  renderProviderOverview();
49
144
  renderSessions("filter");
50
145
  announceProviderFilter();
51
- requestAnimationFrame(() => $("#providerFilter").querySelector(`[data-provider-filter="${CSS.escape(chip.dataset.providerFilter)}"]`)?.focus());
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"]);
52
154
  });
53
155
  $("#sortSelect").addEventListener("change", (event) => {
54
156
  state.sort = event.target.value;
55
157
  state.visibleLimit = 30;
56
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();
57
163
  });
58
- $("#addWorkspaceBtn").addEventListener("click", async () => {
59
- 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);
60
212
  if (!workspaces) return;
61
213
  state.workspaces = workspaces;
62
214
  renderWorkspaces();
63
- });
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));
64
225
  $("#probeBtn").addEventListener("click", async () => {
65
- const nextAvailability = await performUiAction(() => window.loadtoagent.probeProviders(), "AI CLI 연결 상태를 확인하지 못했습니다.");
226
+ const nextAvailability = await performUiAction(() => window.loadtoagent.probeProviders(), t("run.cli_check_failed"), $("#probeBtn"));
66
227
  if (!nextAvailability) return;
67
228
  state.availability = nextAvailability;
68
229
  render();
69
230
  toast(window.LoadToAgentI18n.t("ui.ai_cli_connections_were_checked_again"));
70
231
  });
232
+ $("#searchClearBtn").classList.toggle("hidden", !$("#searchInput").value);
233
+ syncFilterResetButton();
71
234
  }
72
235
 
73
236
  return { bindFilterAndWorkspaceEvents };