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,27 +3,40 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDrawerData = function createDrawerData(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const { reportRecoverableError, state } = context;
8
+ const detailRequests = new Map();
9
+ let detailRequestGeneration = 0;
7
10
 
8
11
  async function loadSessionDetail(id, force = false) {
9
12
  if (!force && state.details.has(id)) return state.details.get(id);
13
+ if (!force && detailRequests.has(id)) return detailRequests.get(id).promise;
14
+ const generation = ++detailRequestGeneration;
10
15
  state.detailErrors.delete(id);
11
16
  state.detailLoadingIds.add(id);
12
17
  context.renderDrawer();
13
- try {
14
- const detail = await window.loadtoagent.sessionDetail(id);
15
- if (detail) state.details.set(id, detail);
16
- return detail;
17
- } catch (error) {
18
- state.detailErrors.set(id, (error && error.message) || "작업 기록을 불러오지 못했습니다.");
19
- return null;
20
- } finally {
21
- state.detailLoadingIds.delete(id);
22
- if (state.selectedId === id) {
23
- state.drawerForceLatest = state.drawerTab === "chat";
24
- context.renderDrawer();
18
+ const promise = (async () => {
19
+ try {
20
+ const detail = await window.loadtoagent.sessionDetail(id);
21
+ if (detailRequests.get(id)?.generation === generation && detail) state.details.set(id, detail);
22
+ return detail;
23
+ } catch (error) {
24
+ if (detailRequests.get(id)?.generation === generation)
25
+ state.detailErrors.set(id, window.LoadToAgentI18n.errorText(error, "drawer.history_failed"));
26
+ return null;
27
+ } finally {
28
+ if (detailRequests.get(id)?.generation === generation) {
29
+ detailRequests.delete(id);
30
+ state.detailLoadingIds.delete(id);
31
+ if (state.selectedId === id) {
32
+ state.drawerForceLatest = state.drawerTab === "chat";
33
+ context.renderDrawer();
34
+ }
35
+ }
25
36
  }
26
- }
37
+ })();
38
+ detailRequests.set(id, { generation, promise });
39
+ return promise;
27
40
  }
28
41
 
29
42
  async function loadSubagentParentDetail(child) {
@@ -3,11 +3,13 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
- $, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger,
8
- providerInfo, isLiveSession, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionWorkspaceLabel,
8
+ $, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger, setDialogOpenState,
9
+ providerInfo, isLiveSession, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionOriginPath, sessionWorkspaceLabel,
9
10
  agentResumeSupport, originAppInfo, selectedSession, snapshotSession, loadSessionDetail, loadSubagentParentDetail,
10
- chatHtml, lifecycleHtml, tokensHtml, subagentCoordinationEvents, subagentConversationHtml,
11
+ chatHtml, lifecycleHtml, tokensHtml, outcomeHtml, subagentCoordinationEvents, subagentConversationHtml,
12
+ rememberDisclosureStates = () => {}, restoreDisclosureStates = () => {},
11
13
  } = context;
12
14
 
13
15
  function openDrawer(id) {
@@ -15,17 +17,16 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
15
17
  markGuideStep("detail");
16
18
  state.selectedId = id;
17
19
  state.drawerMode = "session";
18
- state.drawerTab = "chat";
20
+ state.drawerTab = "summary";
19
21
  state.drawerForceLatest = true;
20
22
  clearTimeout(motionState.drawerTimer);
21
23
  $("#drawerBackdrop").classList.remove("hidden");
22
24
  $("#drawerBackdrop").classList.remove("closing");
23
25
  $("#detailDrawer").classList.add("open");
24
- $("#detailDrawer").removeAttribute("inert");
25
- $("#detailDrawer").setAttribute("aria-hidden", "false");
26
+ setDialogOpenState($("#detailDrawer"), true);
26
27
  renderDrawer();
27
28
  loadSessionDetail(id, true);
28
- setTimeout(() => $("#closeDrawerBtn").focus(), 0);
29
+ setTimeout(() => $("#closeDrawerBtn").focus({ preventScroll: true }), 0);
29
30
  }
30
31
 
31
32
  function openSubagentConversation(id) {
@@ -41,26 +42,26 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
41
42
  $("#drawerBackdrop").classList.remove("hidden");
42
43
  $("#drawerBackdrop").classList.remove("closing");
43
44
  $("#detailDrawer").classList.add("open");
44
- $("#detailDrawer").removeAttribute("inert");
45
- $("#detailDrawer").setAttribute("aria-hidden", "false");
45
+ setDialogOpenState($("#detailDrawer"), true);
46
46
  renderDrawer();
47
47
  loadSessionDetail(id);
48
48
  loadSubagentParentDetail(child);
49
- setTimeout(() => $("#closeDrawerBtn").focus(), 0);
49
+ setTimeout(() => $("#closeDrawerBtn").focus({ preventScroll: true }), 0);
50
50
  }
51
51
 
52
52
  function closeDrawer(restoreFocus = true) {
53
53
  if (!$("#detailDrawer").classList.contains("open")) return;
54
+ const drawerGeneration = motionState.dialogGeneration;
54
55
  $("#detailDrawer").classList.remove("open");
55
- $("#detailDrawer").setAttribute("aria-hidden", "true");
56
- $("#detailDrawer").setAttribute("inert", "");
56
+ setDialogOpenState($("#detailDrawer"), false);
57
57
  $("#drawerBackdrop").classList.add("closing");
58
58
  clearTimeout(motionState.drawerTimer);
59
59
  motionState.drawerTimer = setTimeout(
60
60
  () => {
61
61
  $("#drawerBackdrop").classList.add("hidden");
62
62
  $("#drawerBackdrop").classList.remove("closing");
63
- if (restoreFocus) restoreDialogTrigger();
63
+ if (drawerGeneration !== motionState.dialogGeneration) return;
64
+ if (restoreFocus) restoreDialogTrigger(drawerGeneration);
64
65
  else motionState.activeDialogTrigger = null;
65
66
  },
66
67
  motionPreference.matches ? 0 : 260,
@@ -77,60 +78,65 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
77
78
  $("#drawerProviderMark").style.setProperty("--provider", provider.accent);
78
79
  $("#drawerProviderMark").textContent = provider.mark;
79
80
  $("#drawerProvider").textContent = subagentMode
80
- ? `${session.agentName || provider.label} · 서브에이전트 작업 기록`
81
+ ? t("drawer.subagent_title", { name: session.agentName || provider.label })
81
82
  : `${provider.company} · ${STATUS[session.status] || session.status}`;
82
- $("#drawerTitle").textContent = subagentMode ? session.taskName || (session.delegation && session.delegation.taskName) || session.title : session.title;
83
+ const drawerTitle = subagentMode ? session.taskName || (session.delegation && session.delegation.taskName) || session.title : session.title;
84
+ $("#drawerTitle").textContent = drawerTitle;
85
+ $("#drawerTitle").title = drawerTitle;
83
86
  const stopping = session.runId && state.stopRequests.has(session.runId);
84
87
  const stop =
85
88
  session.runId && (session.status === "running" || session.status === "starting")
86
- ? `<button class="meta-chip stop-run" data-stop-run="${esc(session.runId)}"
89
+ ? `<button type="button" class="meta-chip stop-run" data-stop-run="${esc(session.runId)}"
87
90
  ${stopping ? 'disabled aria-busy="true"' : ""}>
88
- ${stopping ? "중지 요청 중…" : "■ 실행 중지"}</button>`
91
+ ${esc(t(stopping ? "drawer.stop_requested" : "drawer.stop_run"))}</button>`
89
92
  : "";
90
93
  const runtime = session.runtimePresence || [];
91
94
  const resume =
92
95
  !isLiveSession(session) && agentResumeSupport(session).supported
93
- ? `<button class="meta-chip resume-agent" data-resume-agent="${esc(session.id)}">▶
94
- <b>${originAppInfo(session) ? "백그라운드 터미널로 이어가기" : "터미널로 다시 일 시키기"}</b>
96
+ ? `<button type="button" class="meta-chip resume-agent" data-resume-agent="${esc(session.id)}">▶
97
+ <b>${esc(t(originAppInfo(session) ? "drawer.continue_background_terminal" : "drawer.resume_in_terminal"))}</b>
95
98
  </button>`
96
99
  : "";
97
100
  const communicationCount = subagentMode ? subagentCoordinationEvents(session).length : 0;
98
101
  const subagentMessageCount = subagentMode
99
102
  ? (session.messages || []).filter((message) => message.role === "user" || message.role === "assistant").length
100
103
  : 0;
104
+ const taskId = String(session.externalId || session.id || "");
105
+ const copyTask = taskId
106
+ ? `<button type="button" class="meta-chip meta-copy" data-copy-text="${esc(taskId)}" aria-label="${esc(t("quality.copy_task_id"))}">${esc(t("quality.task_id"))} <b>${esc(taskId.slice(0, 12))}</b><span aria-hidden="true">⧉</span></button>`
107
+ : "";
108
+ const originPath = sessionOriginPath(session);
109
+ const copyWorkspace = !isProjectlessSession(session) && originPath
110
+ ? `<button type="button" class="meta-chip meta-copy origin-project-meta" data-copy-text="${esc(originPath)}" aria-label="${esc(t("quality.copy_workspace"))}">${esc(t("project.origin"))} <b>${esc(sessionWorkspaceLabel(session))}</b><span aria-hidden="true">⧉</span></button>`
111
+ : "";
101
112
  $("#drawerMeta").innerHTML = subagentMode
102
113
  ? `<span class="meta-chip work-state ${subagentWorkState(session)}">
103
114
  <b>${esc(subagentWorkLabel(session))}</b>
104
115
  </span>
105
- <span class="meta-chip">사용 모델 <b>${esc(session.model || "정보 없음")}</b>
106
- </span>
107
- <span class="meta-chip">작업 기록 <b>${subagentMessageCount}건</b>
116
+ <span class="meta-chip">${esc(t("drawer.model"))} <b>${esc(session.model || t("drawer.unknown"))}</b>
108
117
  </span>
109
- <span class="meta-chip">메인 지시·결과 <b>${communicationCount}건</b>
110
- </span>${resume}`
111
- : `<span class="meta-chip">사용 모델 <b>${esc(session.model || "정보 없음")}</b>
118
+ <span class="meta-chip">${esc(t("drawer.work_history"))} <b>${esc(t("drawer.event_count", { count: subagentMessageCount }))}</b>
112
119
  </span>
113
- <span class="meta-chip">작업 폴더
114
- <b title="${esc(isProjectlessSession(session) ? window.LoadToAgentI18n.t("ui.session_not_linked_to_a_specific_project") : session.cwd)}">
115
- ${esc(sessionWorkspaceLabel(session))}
116
- </b>
120
+ <span class="meta-chip">${esc(t("drawer.main_instructions_results"))} <b>${esc(t("drawer.event_count", { count: communicationCount }))}</b>
121
+ </span>${copyTask}${copyWorkspace}${resume}`
122
+ : `<span class="meta-chip">${esc(t("drawer.model"))} <b>${esc(session.model || t("drawer.unknown"))}</b>
117
123
  </span>
118
- <span class="meta-chip">작업 번호 <b>${esc(String(session.externalId || "").slice(0, 12) || "정보 없음")}</b>
119
- </span>${
124
+ ${copyWorkspace || `<span class="meta-chip origin-project-meta">${esc(t("project.origin"))} <b>${esc(sessionWorkspaceLabel(session))}</b></span>`}
125
+ ${copyTask}${
120
126
  session.parentId
121
- ? `<span class="meta-chip">⑂ <b>도움을 맡은 AI</b>
127
+ ? `<span class="meta-chip">⑂ <b>${esc(t("drawer.helper_ai"))}</b>
122
128
  </span>`
123
129
  : ""
124
130
  }${
125
131
  runtime.length
126
- ? `<span class="meta-chip runtime-meta">● <b>실행 중인 프로그램 ${runtime.length}개</b>
132
+ ? `<span class="meta-chip runtime-meta">● <b>${esc(t("session.running_programs", { count: runtime.length }))}</b>
127
133
  </span>`
128
134
  : ""
129
135
  }${resume}${stop}`;
130
136
  $$(".drawer-tab").forEach((tab) => {
131
137
  const hidden = subagentMode && tab.dataset.tab !== "chat";
132
138
  tab.classList.toggle("hidden", hidden);
133
- if (tab.dataset.tab === "chat") tab.textContent = subagentMode ? "작업 내용" : window.LoadToAgentI18n.t("ui.conversation");
139
+ if (tab.dataset.tab === "chat") tab.textContent = subagentMode ? t("drawer.work_content") : t("ui.conversation");
134
140
  const active = tab.dataset.tab === state.drawerTab;
135
141
  tab.classList.toggle("active", active);
136
142
  tab.setAttribute("aria-selected", active ? "true" : "false");
@@ -139,8 +145,9 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
139
145
  const activeTab = $(`.drawer-tab[data-tab="${state.drawerTab}"]`);
140
146
  if (activeTab) $("#drawerContent").setAttribute("aria-labelledby", activeTab.id);
141
147
  const content = $("#drawerContent");
148
+ rememberDisclosureStates(content);
142
149
  const previousTop = content.scrollTop;
143
- const wasNearBottom = content.scrollHeight - content.scrollTop - content.clientHeight < 90;
150
+ const wasAtBottom = window.LoadToAgentRendererUtils.isScrolledToEnd(content);
144
151
  const renderKey = `${state.drawerMode}:${state.selectedId}:${state.drawerTab}:${detailLoading ? "loading" : "ready"}`;
145
152
  const previousRenderKey = motionState.drawerRenderKey;
146
153
  const shouldAnimateContent = previousRenderKey !== renderKey;
@@ -149,40 +156,51 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
149
156
  motionState.drawerTab = state.drawerTab;
150
157
  const detailError = state.detailErrors.get(state.selectedId);
151
158
  content.innerHTML = detailLoading
152
- ? '<div class="drawer-loading"><span></span><b>전체 작업 기록을 불러오는 중</b><small>잠시만 기다리면 대화와 진행 과정을 볼 수 있어요.</small></div>'
159
+ ? `<div class="drawer-loading"><span></span><b>${esc(t("drawer.loading_history"))}</b><small>${esc(t("drawer.loading_history_help"))}</small></div>`
153
160
  : detailError
154
161
  ? `<div class="drawer-error">
155
- <b>작업 기록을 불러오지 못했습니다</b>
162
+ <b>${esc(t("drawer.history_failed"))}</b>
156
163
  <span>${esc(detailError)}</span>
157
- <button type="button" data-retry-detail="${esc(state.selectedId)}">다시 시도</button>
164
+ <button type="button" data-retry-detail="${esc(state.selectedId)}">${esc(t("drawer.retry"))}</button>
158
165
  </div>`
159
166
  : subagentMode
160
167
  ? subagentConversationHtml(session)
161
- : state.drawerTab === "chat"
168
+ : state.drawerTab === "summary"
169
+ ? outcomeHtml(session)
170
+ : state.drawerTab === "chat"
162
171
  ? chatHtml(session)
163
172
  : state.drawerTab === "lifecycle"
164
173
  ? lifecycleHtml(session)
165
174
  : tokensHtml(session);
175
+ restoreDisclosureStates(content);
166
176
  content.classList.toggle("motion-content-in", shouldAnimateContent && !motionPreference.matches);
167
177
  clearTimeout(motionState.drawerContentTimer);
168
178
  if (shouldAnimateContent)
169
179
  motionState.drawerContentTimer = setTimeout(() => content.classList.remove("motion-content-in"), motionPreference.matches ? 0 : 520);
170
- if (!detailLoading)
171
- requestAnimationFrame(() => {
172
- const forceLatest = state.drawerForceLatest;
173
- if (state.drawerTab === "chat" && forceLatest) {
174
- const rows = [...content.querySelectorAll(".chat-row")];
175
- const latest = rows[rows.length - 1];
176
- if (latest && latest.offsetHeight > content.clientHeight - 90) {
177
- const contentTop = content.getBoundingClientRect().top;
178
- const stickyHeight = content.querySelector(".chat-history-head")?.getBoundingClientRect().height || 0;
179
- content.scrollTop = Math.max(0, content.scrollTop + latest.getBoundingClientRect().top - contentTop - stickyHeight - 12);
180
- } else content.scrollTop = content.scrollHeight;
181
- } else if (tabChanged) content.scrollTop = 0;
182
- else if (state.drawerTab === "chat" && wasNearBottom) content.scrollTop = content.scrollHeight;
183
- else content.scrollTop = Math.min(previousTop, Math.max(0, content.scrollHeight - content.clientHeight));
180
+ if (!detailLoading) {
181
+ if (tabChanged) {
182
+ content.scrollTop = 0;
184
183
  state.drawerForceLatest = false;
185
- });
184
+ } else {
185
+ const scrollGeneration = (motionState.drawerScrollGeneration || 0) + 1;
186
+ motionState.drawerScrollGeneration = scrollGeneration;
187
+ requestAnimationFrame(() => {
188
+ if (motionState.drawerScrollGeneration !== scrollGeneration) return;
189
+ const forceLatest = state.drawerForceLatest;
190
+ if (state.drawerTab === "chat" && forceLatest) {
191
+ const rows = [...content.querySelectorAll(".chat-row")];
192
+ const latest = rows[rows.length - 1];
193
+ if (latest && latest.offsetHeight > content.clientHeight - 90) {
194
+ const contentTop = content.getBoundingClientRect().top;
195
+ const stickyHeight = content.querySelector(".chat-history-head")?.getBoundingClientRect().height || 0;
196
+ content.scrollTop = Math.max(0, content.scrollTop + latest.getBoundingClientRect().top - contentTop - stickyHeight - 12);
197
+ } else content.scrollTop = content.scrollHeight;
198
+ } else if (state.drawerTab === "chat" && wasAtBottom) content.scrollTop = content.scrollHeight;
199
+ else content.scrollTop = Math.min(previousTop, Math.max(0, content.scrollHeight - content.clientHeight));
200
+ state.drawerForceLatest = false;
201
+ });
202
+ }
203
+ }
186
204
  }
187
205
 
188
206
  return { openDrawer, openSubagentConversation, closeDrawer, renderDrawer };
@@ -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
  });