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,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
  });
@@ -33,7 +55,7 @@ window.LoadToAgentAppFactories.createNavigationEventBindings = function createNa
33
55
  state.guideExpanded = false;
34
56
  saveGuideState();
35
57
  renderSessions("guide");
36
- $("#guideBtn").focus();
58
+ $("#guideBtn").focus({ preventScroll: true });
37
59
  });
38
60
  $("#beginnerGuide").addEventListener("click", (event) => {
39
61
  const action = event.target.closest("[data-guide-action]")?.dataset.guideAction;
@@ -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,7 +107,23 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
18
107
  renderProviderOverview();
19
108
  renderSessions("filter");
20
109
  announceProviderFilter();
21
- requestAnimationFrame(() => $("#providerOverview").querySelector(`[data-provider-card="${CSS.escape(card.dataset.providerCard)}"]`)?.focus());
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();
22
127
  });
23
128
  $("#sessionGrid").addEventListener("click", (event) => {
24
129
  const card = event.target.closest("[data-session-id]");
@@ -34,7 +139,23 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
34
139
  }
35
140
 
36
141
  function bindLiveAgentEvents() {
37
- $("#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
+ }
38
159
  const tmuxPane = event.target.closest('.live-tmux-pane[data-tmux-type="pane"][data-tmux-id]');
39
160
  const tmuxOverview = event.target.closest(".live-tmux-overview-open");
40
161
  if (tmuxPane || tmuxOverview) {
@@ -133,6 +254,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
133
254
  }
134
255
 
135
256
  function bindGraphNavigationEvents() {
257
+ $("#openTmuxFromAgentWork").addEventListener("click", () => selectView("tmux", { focusMain: true }));
136
258
  $("#graphBreadcrumbs").addEventListener("click", (event) => {
137
259
  if (event.target.closest("[data-graph-reset]")) state.graphFocusId = null;
138
260
  else {
@@ -150,6 +272,22 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
150
272
 
151
273
  function bindTmuxMapEvents() {
152
274
  $("#tmuxMap").addEventListener("click", (event) => {
275
+ const subagentToggle = event.target.closest("[data-tmux-subagents-toggle]");
276
+ if (subagentToggle) {
277
+ event.stopPropagation();
278
+ const paneId = subagentToggle.dataset.tmuxSubagentsToggle;
279
+ if (state.expandedTmuxSubagents.has(paneId)) state.expandedTmuxSubagents.delete(paneId);
280
+ else state.expandedTmuxSubagents.add(paneId);
281
+ renderTmuxMap();
282
+ requestAnimationFrame(() => $("#tmuxMap").querySelector(`[data-tmux-subagents-toggle="${CSS.escape(paneId)}"]`)?.focus({ preventScroll: true }));
283
+ return;
284
+ }
285
+ const subagentChat = event.target.closest("[data-open-subagent-chat]");
286
+ if (subagentChat) {
287
+ event.stopPropagation();
288
+ openSubagentConversation(subagentChat.dataset.openSubagentChat);
289
+ return;
290
+ }
153
291
  const control = event.target.closest("[data-control-tmux]");
154
292
  if (control) {
155
293
  event.stopPropagation();
@@ -165,10 +303,25 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
165
303
  }
166
304
  const node = event.target.closest("[data-tmux-type][data-tmux-id]");
167
305
  if (!node) return;
168
- 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;
169
308
  renderTmuxMap();
309
+ requestAnimationFrame(() => $("#tmuxMap")?.querySelector(`[data-tmux-type="${CSS.escape(nextFocus.type)}"][data-tmux-id="${CSS.escape(nextFocus.id)}"]`)?.focus({ preventScroll: true }));
170
310
  if (node.dataset.tmuxType === "pane") window.LoadToAgentTerminal?.selectTmuxById(node.dataset.tmuxId);
171
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
+ });
172
325
  $("#tmuxBreadcrumbs").addEventListener("click", (event) => {
173
326
  if (event.target.closest("[data-tmux-reset]")) state.tmuxFocus = null;
174
327
  else {
@@ -178,6 +331,19 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
178
331
  }
179
332
  renderTmuxMap();
180
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
+ });
181
347
  $("#tmuxResetBtn").addEventListener("click", () => {
182
348
  state.tmuxFocus = null;
183
349
  renderTmuxMap();
@@ -185,6 +351,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
185
351
  }
186
352
 
187
353
  function bindSessionAndAgentEvents() {
354
+ bindManagementEvents();
188
355
  bindSessionListEvents();
189
356
  bindLiveAgentEvents();
190
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 };
@@ -3,6 +3,7 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
8
  $,
8
9
  esc,
@@ -78,10 +79,10 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
78
79
  if (tmux)
79
80
  return {
80
81
  kind: "tmux",
81
- label: "TMUX 사용",
82
- detail: [tmux.distro, tmux.sessionName, tmux.paneNativeId || tmux.paneId].filter(Boolean).join(" · ") || "분할 터미널에서 실행",
82
+ label: t("graph.tmux_used"),
83
+ detail: [tmux.distro, tmux.sessionName, tmux.paneNativeId || tmux.paneId].filter(Boolean).join(" · ") || t("graph.running_in_split_terminal"),
83
84
  };
84
- return { kind: "standard", label: "일반 실행", detail: "TMUX 미사용" };
85
+ return { kind: "standard", label: t("graph.standard_run"), detail: t("graph.tmux_not_used") };
85
86
  }
86
87
 
87
88
  function executionModeBadge(session, compact = false) {
@@ -103,5 +104,32 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
103
104
  return seen.size;
104
105
  }
105
106
 
106
- return { graphPath, connectedGraphSessions, sortGraphNodes, graphChildren, agentExecutionMode, executionModeBadge, graphDescendantCount };
107
+ function runtimeAgentSummary(model, tmuxEntries = []) {
108
+ const liveNodes = model.nodes.filter(isLiveSession);
109
+ const liveById = new Map(liveNodes.map((session) => [session.id, session]));
110
+ const tmuxSessionIds = new Set(
111
+ tmuxEntries
112
+ .filter((entry) => entry && entry.pane && !entry.pane.dead)
113
+ .map((entry) => String(entry.agent && entry.agent.linkedSessionId || ""))
114
+ .filter((id) => id && liveById.has(id)),
115
+ );
116
+ for (const session of liveNodes) {
117
+ if (agentExecutionMode(session).kind === "tmux") tmuxSessionIds.add(session.id);
118
+ }
119
+ const tmuxCount = liveNodes.filter((session) => tmuxSessionIds.has(session.id)).length;
120
+ const runningExecutions = liveNodes.flatMap((session) => (session.executions || []).filter((activity) => activity.status === "running"));
121
+ return {
122
+ activeCount: liveNodes.length,
123
+ standardCount: liveNodes.length - tmuxCount,
124
+ tmuxCount,
125
+ rootCount: liveNodes.filter((session) => !session.parentId).length,
126
+ activeHelperCount: liveNodes.filter((session) => session.parentId).length,
127
+ helperRecordCount: model.nodes.filter((session) => session.parentId).length,
128
+ runningExecutionCount: runningExecutions.length,
129
+ shellExecutionCount: runningExecutions.filter((activity) => activity.kind === "shell").length,
130
+ backgroundExecutionCount: runningExecutions.filter((activity) => activity.mode === "background" || activity.kind === "background").length,
131
+ };
132
+ }
133
+
134
+ return { graphPath, connectedGraphSessions, sortGraphNodes, graphChildren, agentExecutionMode, executionModeBadge, graphDescendantCount, runtimeAgentSummary };
107
135
  };
@@ -13,12 +13,13 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
13
13
  graphPath,
14
14
  connectedGraphSessions,
15
15
  sortGraphNodes,
16
- agentExecutionMode,
16
+ runtimeAgentSummary,
17
17
  liveTmuxEntries,
18
18
  runtimeSeparatedOverview,
19
19
  focusedGraph,
20
20
  scheduleAgentWorkflowConnections,
21
21
  } = context;
22
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
22
23
 
23
24
  function renderAgentMap(sessions, motionKind = "refresh") {
24
25
  const model = connectedGraphSessions(sessions);
@@ -36,7 +37,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
36
37
  if (focus) {
37
38
  $("#liveSessionGrid").innerHTML = focusedGraph(focus, model, motionKind);
38
39
  const path = graphPath(focus, model.byId);
39
- $("#graphBreadcrumbs").innerHTML = `<button type="button" data-graph-reset>작업 목록</button>${path
40
+ $("#graphBreadcrumbs").innerHTML = `<button type="button" data-graph-reset>${esc(t("graph.task_list"))}</button>${path
40
41
  .map((item) => {
41
42
  const label = item.parentId ? item.agentName || agentRoleLabel(item.agentRole) : item.title;
42
43
  const preview = readablePreview(label, item.parentId ? 42 : 72);
@@ -49,27 +50,29 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
49
50
  $("#graphResetBtn").classList.remove("hidden");
50
51
  scheduleAgentWorkflowConnections();
51
52
  } else {
52
- const tmuxEntries = liveTmuxEntries();
53
- const standardRoots = roots.filter((root) => agentExecutionMode(root).kind !== "tmux");
54
- const activeCount = model.nodes.filter(isLiveSession).length + tmuxEntries.filter((entry) => !entry.agent.linkedSessionId).length;
55
- $("#liveSessionGrid").innerHTML = `<details class="runtime-disclosure" open>
53
+ const runtime = runtimeAgentSummary(model, liveTmuxEntries(state.snapshot && state.snapshot.tmux));
54
+ $("#liveSessionGrid").innerHTML = `<details class="runtime-disclosure" data-disclosure-key="home:runtime-overview" open>
56
55
  <summary>
57
56
  <span>
58
- <b>${activeCount}개 AI가 작업 중입니다</b>
59
- <small>일반 실행과 tmux 세부 흐름은 필요할 펼쳐보세요.</small>
57
+ <b>${esc(t("graph.ai_working_count", { count: runtime.activeCount }))}</b>
58
+ <small>${esc(t("graph.runtime_count_explanation", { standard: runtime.standardCount, tmux: runtime.tmuxCount, helpers: runtime.activeHelperCount }))}${runtime.runningExecutionCount ? ` · ${esc(t("graph.runtime_execution_count", { shell: runtime.shellExecutionCount, background: runtime.backgroundExecutionCount }))}` : ""}</small>
60
59
  </span>
61
- <em>상세 흐름 보기 <i aria-hidden="true">↓</i>
60
+ <em>${esc(t("graph.view_detailed_flow"))} <i aria-hidden="true">↓</i>
62
61
  </em>
63
62
  </summary>${runtimeSeparatedOverview(roots, model)}</details>`;
64
63
  $("#graphBreadcrumbs").innerHTML =
65
64
  `<span class="map-hint">
66
- 일반 실행 <b>${standardRoots.length}</b>개 ·
67
- TMUX AI <b>${tmuxEntries.length}</b>개 ·
68
- <b>${model.nodes.filter((item) => item.parentId).length}</b>개 도움 AI
65
+ ${esc(t("graph.standard_ai"))} <b>${runtime.standardCount}</b> ·
66
+ ${esc(t("graph.tmux_ai"))} <b>${runtime.tmuxCount}</b> ·
67
+ ${esc(t("graph.active_helper_ai"))} <b>${runtime.activeHelperCount}</b> ·
68
+ ${esc(t("graph.helper_ai_history"))} <b>${runtime.helperRecordCount}</b> ·
69
+ ${esc(t("graph.running_shell"))} <b>${runtime.shellExecutionCount}</b> ·
70
+ ${esc(t("graph.running_background"))} <b>${runtime.backgroundExecutionCount}</b>
69
71
  </span>`;
70
72
  $("#graphResetBtn").classList.add("hidden");
73
+ return runtime.activeCount;
71
74
  }
72
- return model.nodes.filter(isLiveSession).length + liveTmuxEntries().filter((entry) => !entry.agent.linkedSessionId).length;
75
+ return model.nodes.filter(isLiveSession).length;
73
76
  }
74
77
 
75
78
  return { renderAgentMap };