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,6 +3,8 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
7
+ const errorText = (error, key, params) => window.LoadToAgentI18n.errorText(error, key, params);
6
8
  const {
7
9
  $,
8
10
  esc,
@@ -27,18 +29,18 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
27
29
  try {
28
30
  return window.LoadToAgentTerminal && typeof window.LoadToAgentTerminal.resumeSupport === "function"
29
31
  ? window.LoadToAgentTerminal.resumeSupport(session)
30
- : { supported: false, reason: "터미널 재개 기능을 준비하는 중입니다." };
32
+ : { supported: false, reason: t("agent.resume_preparing") };
31
33
  } catch (error) {
32
- return { supported: false, reason: (error && error.message) || "세션 재개 가능 여부를 확인하지 못했습니다." };
34
+ return { supported: false, reason: errorText(error, "agent.resume_check_failed") };
33
35
  }
34
36
  }
35
37
 
36
38
  function originAppInfo(session) {
37
39
  if (session && session.provider === "codex" && session.clientKind === "codex-desktop") {
38
- return { provider: "Codex", label: "Codex 데스크톱 " };
40
+ return { provider: "Codex", label: t("agent.desktop_app", { provider: "Codex" }) };
39
41
  }
40
42
  if (session && session.provider === "claude" && session.clientKind === "claude-desktop") {
41
- return { provider: "Claude", label: "Claude 데스크톱 " };
43
+ return { provider: "Claude", label: t("agent.desktop_app", { provider: "Claude" }) };
42
44
  }
43
45
  return null;
44
46
  }
@@ -64,71 +66,71 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
64
66
  const origin = originAppInfo(session);
65
67
  const status =
66
68
  mode === "direct"
67
- ? `직접 입력 가능 · ${targets.length === 1 ? target.label : `${targets.length} 터미널 중 선택`}`
69
+ ? t("agent.direct_status", { target: targets.length === 1 ? target.label : t("agent.choose_terminal_count", { count: targets.length }) })
68
70
  : mode === "handoff"
69
- ? "외부 터미널에서 실행 중 · 같은 대화로 이어받기 가능"
71
+ ? t("agent.handoff_status")
70
72
  : mode === "resume"
71
- ? "원래 터미널이 종료됨 · 같은 세션으로 복구 가능"
73
+ ? t("agent.resume_status")
72
74
  : mode === "origin-resume"
73
- ? "쉬는 데스크톱 작업 · 백그라운드 터미널로 이어가기 가능"
75
+ ? t("agent.origin_resume_status")
74
76
  : mode === "connect"
75
- ? "연결 후 입력 가능 · 현재 세션은 보기 전용"
77
+ ? t("agent.connect_status")
76
78
  : mode === "origin"
77
- ? "보기 전용 · 원래 앱에서 계속"
79
+ ? t("agent.origin_status")
78
80
  : window.LoadToAgentI18n.t("ui.ended_session");
79
81
  const help =
80
82
  mode === "direct"
81
- ? "기존 터미널에 바로 보냅니다. 앱 창을 닫아도 백그라운드에서 세션을 계속 유지합니다."
83
+ ? t("agent.direct_help")
82
84
  : mode === "handoff"
83
- ? "같은 세션 ID와 대화 내역을 LoadToAgent 관리 터미널로 이어받고 백그라운드에서 유지합니다."
85
+ ? t("agent.handoff_help")
84
86
  : mode === "resume"
85
- ? "기존 세션 ID와 대화 맥락을 복구하고, 앱 창을 닫아도 백그라운드에서 유지합니다."
87
+ ? t("agent.resume_help")
86
88
  : mode === "origin-resume"
87
- ? `원래 ${(origin && origin.provider) || "데스크톱"} 앱으로 돌아가거나, 같은 세션을 백그라운드 터미널로 이어서 작업할 수 있습니다.`
89
+ ? t("agent.origin_resume_help", { provider: (origin && origin.provider) || t("agent.desktop") })
88
90
  : mode === "connect"
89
- ? `새 터미널에서 loadtoagent run ${session.provider}로 시작하면 창을 닫아도 안전하게 유지됩니다.`
91
+ ? t("agent.connect_help", { provider: session.provider })
90
92
  : mode === "origin"
91
- ? `이 대화는 ${(origin && origin.label) || "데스크톱 앱"}에서 현재 실행 중이므로 원래 작업에서 계속합니다.`
92
- : agentResumeSupport(session).reason || "이 제공사의 세션 재개 방식을 확인할 수 없습니다.";
93
+ ? t("agent.origin_help", { app: (origin && origin.label) || t("agent.desktop_app", { provider: "" }).trim() })
94
+ : agentResumeSupport(session).reason || t("agent.resume_method_unknown");
93
95
  const picker =
94
96
  targets.length > 1
95
97
  ? `<label class="agent-command-target">
96
- <span>보낼 터미널</span>
98
+ <span>${esc(t("agent.target_terminal"))}</span>
97
99
  <select data-agent-command-target="${esc(session.id)}">
98
- <option value="">터미널을 선택하세요</option>
100
+ <option value="">${esc(t("agent.choose_terminal"))}</option>
99
101
  ${targets.map((item) => `<option value="${esc(item.id)}" ${item.id === targetId ? "selected" : ""}>${esc(item.label)}</option>`).join("")}
100
102
  </select>
101
103
  </label>`
102
104
  : "";
103
- const originAction = `<button type="button" data-agent-open-origin="${esc(session.id)}">원래 ${esc((origin && origin.provider) || "데스크톱")} 앱에서 계속하기</button>`;
105
+ const originAction = `<button type="button" data-agent-open-origin="${esc(session.id)}">${esc(t("agent.continue_in_origin", { provider: (origin && origin.provider) || t("agent.desktop") }))}</button>`;
104
106
  const actions =
105
107
  mode === "direct"
106
- ? `<button type="button" data-agent-terminal-open="${esc(session.id)}" ${canSend ? "" : "disabled"}>터미널에서 열기</button>
107
- <button type="submit" ${canSend ? "" : "disabled"}>${sending ? "보내는 중…" : "바로 보내기 ↵"}</button>`
108
+ ? `<button type="button" data-agent-terminal-open="${esc(session.id)}" ${canSend ? "" : "disabled"}>${esc(t("agent.open_terminal"))}</button>
109
+ <button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.sending" : "agent.send_now"))}</button>`
108
110
  : mode === "resume"
109
- ? `<button type="submit" ${canSend ? "" : "disabled"}>${sending ? "복구하는 중…" : "같은 세션으로 복구해 보내기 ↵"}</button>`
111
+ ? `<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.restoring" : "agent.restore_and_send"))}</button>`
110
112
  : mode === "handoff"
111
- ? `<button type="submit" ${canSend ? "" : "disabled"}>${sending ? "이어받는 중…" : "관리 터미널로 이어받아 보내기 ↵"}</button>`
113
+ ? `<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.handing_off" : "agent.handoff_and_send"))}</button>`
112
114
  : mode === "origin-resume"
113
- ? `${originAction}<button type="submit" ${canSend ? "" : "disabled"}>${sending ? "연결하는 중…" : "백그라운드 터미널로 이어서 보내기 ↵"}</button>`
115
+ ? `${originAction}<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.connecting" : "agent.background_and_send"))}</button>`
114
116
  : mode === "connect"
115
- ? `<button type="button" data-agent-bridge-copy="${esc(session.provider)}">연결 명령 복사</button>`
117
+ ? `<button type="button" data-agent-bridge-copy="${esc(session.provider)}">${esc(t("agent.copy_bridge"))}</button>`
116
118
  : mode === "origin"
117
119
  ? originAction
118
120
  : "";
119
121
  const editable = ["direct", "resume", "handoff", "origin-resume"].includes(mode);
120
- const placeholder = editable ? "예: 이전 작업에 이어서 테스트를 실행하고 결과를 알려줘" : status;
122
+ const placeholder = editable ? t("agent.command_example") : status;
121
123
  const availabilityClass = mode === "direct" ? "connected" : ["resume", "handoff", "origin-resume"].includes(mode) ? "resume-ready" : "unavailable";
122
124
  return `<form class="agent-command-panel ${availabilityClass} control-${mode}"
123
125
  data-agent-command-form="${esc(session.id)}">
124
126
  <header>
125
127
  <span class="agent-command-icon" aria-hidden="true">›_</span>
126
- <span><b>이 AI에게 바로 지시하기</b><small>${esc(status)}</small></span>
128
+ <span><b>${esc(t("agent.command_title"))}</b><small>${esc(status)}</small></span>
127
129
  <i class="${mode === "direct" ? "connected" : ""}" aria-hidden="true"></i>
128
130
  </header>
129
131
  ${picker}
130
132
  <label class="agent-command-input">
131
- <span class="sr-only">AI에게 보낼 터미널 지시</span>
133
+ <span class="sr-only">${esc(t("agent.command_sr"))}</span>
132
134
  <textarea data-agent-command-draft="${esc(session.id)}" maxlength="8000" rows="3"
133
135
  placeholder="${esc(placeholder)}" ${editable ? "" : "disabled"}>${editable ? esc(draft) : ""}</textarea>
134
136
  </label>
@@ -156,9 +158,9 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
156
158
  async function resumeAgentTerminal(sessionId, sendDraft = false) {
157
159
  if (state.agentCommandSending.has(sessionId)) return;
158
160
  const session = snapshotSession(sessionId) || state.details.get(sessionId);
159
- if (!session || !window.LoadToAgentTerminal) return context.toast("다시 연결할 AI 세션 정보를 찾지 못했습니다.");
161
+ if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.session_not_found"));
160
162
  const support = agentResumeSupport(session);
161
- if (!support.supported) return context.toast(support.reason || "이 AI 세션은 터미널에서 다시 연결할 수 없습니다.");
163
+ if (!support.supported) return context.toast(support.reason || t("agent.cannot_reconnect"));
162
164
  state.agentCommandSending.add(sessionId);
163
165
  try {
164
166
  if ($("#detailDrawer").classList.contains("open")) context.closeDrawer(false);
@@ -167,9 +169,9 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
167
169
  await window.LoadToAgentTerminal.resumeForAgent(session, draft, sendDraft);
168
170
  if (sendDraft && draft.trim()) state.agentCommandDrafts.delete(sessionId);
169
171
  document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
170
- context.toast(`${providerInfo(session.provider).label}의 기존 세션을 터미널에 다시 연결했습니다.`);
172
+ context.toast(t("agent.reconnected", { provider: providerInfo(session.provider).label }));
171
173
  } catch (error) {
172
- context.toast((error && error.message) || "AI 세션을 다시 연결하지 못했습니다.");
174
+ context.toast(errorText(error, "agent.reconnect_failed"));
173
175
  } finally {
174
176
  state.agentCommandSending.delete(sessionId);
175
177
  }
@@ -178,31 +180,31 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
178
180
  async function dispatchAgentCommand(sessionId, form) {
179
181
  if (state.agentCommandSending.has(sessionId)) return;
180
182
  const session = snapshotSession(sessionId);
181
- if (!session || !window.LoadToAgentTerminal) return context.toast("선택한 AI의 최신 정보를 찾지 못했습니다.");
183
+ if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.latest_not_found"));
182
184
  const mode = agentControlMode(session, agentCommandTargets(session));
183
185
  if (mode === "resume" || mode === "handoff" || mode === "origin-resume") {
184
186
  const input = form.querySelector("[data-agent-command-draft]");
185
187
  if (input) state.agentCommandDrafts.set(sessionId, input.value);
186
- if (!String((input && input.value) || "").trim()) return context.toast("AI에게 보낼 지시를 입력하세요.");
188
+ if (!String((input && input.value) || "").trim()) return context.toast(t("agent.enter_command"));
187
189
  return resumeAgentTerminal(sessionId, true);
188
190
  }
189
191
  const target = chosenAgentCommandTarget(session);
190
192
  const input = form.querySelector("[data-agent-command-draft]");
191
193
  const command = String((input && input.value) || "").trim();
192
194
  if (!target)
193
- return context.toast(agentCommandTargets(session).length ? "지시를 보낼 터미널을 먼저 선택하세요." : "이 AI에 연결된 입력 가능한 터미널이 없습니다.");
194
- if (!command) return context.toast("AI에게 보낼 지시를 입력하세요.");
195
+ return context.toast(t(agentCommandTargets(session).length ? "agent.select_target_first" : "agent.no_writable_terminal"));
196
+ if (!command) return context.toast(t("agent.enter_command"));
195
197
  state.agentCommandSending.add(sessionId);
196
198
  const submit = form.querySelector('[type="submit"]');
197
199
  if (submit) {
198
200
  submit.disabled = true;
199
- submit.textContent = "보내는 중…";
201
+ submit.textContent = t("agent.sending");
200
202
  }
201
203
  try {
202
204
  await window.LoadToAgentTerminal.dispatchAgentCommand(session, command, target.id);
203
205
  state.agentCommandDrafts.delete(sessionId);
204
206
  if (input) input.value = "";
205
- context.toast(`${target.label}에 지시를 보냈습니다.`);
207
+ context.toast(t("agent.command_sent", { target: target.label }));
206
208
  } catch (error) {
207
209
  const latest = snapshotSession(sessionId) || session;
208
210
  const support = agentResumeSupport(latest);
@@ -212,61 +214,128 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
212
214
  await window.LoadToAgentTerminal.resumeForAgent(latest, command, true);
213
215
  state.agentCommandDrafts.delete(sessionId);
214
216
  if (input) input.value = "";
215
- context.toast("원래 터미널 연결이 종료되어 같은 AI 세션으로 복구한 뒤 지시를 보냈습니다.");
217
+ context.toast(t("agent.recovered_and_sent"));
216
218
  return;
217
219
  } catch (resumeError) {
218
- context.toast((resumeError && resumeError.message) || "터미널 연결이 끊어졌고 세션 복구에도 실패했습니다.");
220
+ context.toast(errorText(resumeError, "agent.recovery_failed"));
219
221
  }
220
- } else context.toast((error && error.message) || "터미널에 지시를 보내지 못했습니다.");
222
+ } else context.toast(errorText(error, "agent.send_failed"));
221
223
  } finally {
222
224
  state.agentCommandSending.delete(sessionId);
223
225
  if (submit && submit.isConnected) {
224
226
  submit.disabled = false;
225
- submit.textContent = "바로 보내기 ↵";
227
+ submit.textContent = t("agent.send_now");
226
228
  }
227
229
  }
228
230
  }
229
231
 
230
232
  async function openAgentTerminal(sessionId) {
231
233
  const session = snapshotSession(sessionId);
232
- if (!session || !window.LoadToAgentTerminal) return context.toast("선택한 AI의 터미널 정보를 찾지 못했습니다.");
234
+ if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.terminal_info_not_found"));
233
235
  const target = chosenAgentCommandTarget(session);
234
236
  if (!target)
235
- return context.toast(agentCommandTargets(session).length ? "열어볼 터미널을 먼저 선택하세요." : "이 AI에 연결된 입력 가능한 터미널이 없습니다.");
237
+ return context.toast(t(agentCommandTargets(session).length ? "agent.select_open_target" : "agent.no_writable_terminal"));
236
238
  selectView(target.kind === "tmux" ? "tmux" : "terminal");
237
239
  try {
238
240
  await window.LoadToAgentTerminal.openForAgent(session, target.id, state.agentCommandDrafts.get(sessionId) || "");
239
241
  document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
240
242
  } catch (error) {
241
- context.toast((error && error.message) || "AI의 터미널을 열지 못했습니다.");
243
+ context.toast(errorText(error, "agent.open_terminal_failed"));
242
244
  }
243
245
  }
244
246
 
245
247
  async function copyBridgeCommand(provider) {
246
248
  try {
247
249
  const result = await window.loadtoagent.bridgeCommand(provider);
248
- if (!result || !result.ok) throw new Error("연결 명령을 만들지 못했습니다.");
250
+ if (!result || !result.ok) throw new Error(t("agent.bridge_create_failed"));
249
251
  const command = result.command;
250
252
  await window.loadtoagent.writeClipboard(command);
251
- context.toast(`${command} 명령을 복사했습니다.`);
253
+ context.toast(t("agent.command_copied", { command }));
252
254
  } catch (error) {
253
- context.toast((error && error.message) || "연결 명령을 복사하지 못했습니다.");
255
+ context.toast(errorText(error, "agent.bridge_copy_failed"));
254
256
  }
255
257
  }
256
258
 
257
259
  async function openSessionOrigin(sessionId) {
258
260
  const session = snapshotSession(sessionId);
259
261
  const origin = originAppInfo(session);
260
- if (!session || !origin) return context.toast("원래 데스크톱 작업 정보를 찾지 못했습니다.");
262
+ if (!session || !origin) return context.toast(t("agent.origin_not_found"));
261
263
  try {
262
264
  const result = await window.loadtoagent.openSessionOrigin(session);
263
- if (!result || !result.ok) return context.toast(`이 작업은 ${origin.label}에서 직접 열 수 없습니다.`);
264
- context.toast(`원래 ${origin.provider} 작업을 열었습니다.`);
265
+ if (!result || !result.ok) return context.toast(t("agent.origin_cannot_open", { app: origin.label }));
266
+ context.toast(t("agent.origin_opened", { provider: origin.provider }));
265
267
  } catch (error) {
266
- context.toast((error && error.message) || `${origin.label}을 열지 못했습니다.`);
268
+ context.toast(errorText(error, "agent.origin_open_failed", { app: origin.label }));
267
269
  }
268
270
  }
269
271
 
272
+ async function controlManagedRun(sessionId, action) {
273
+ const session = snapshotSession(sessionId) || state.details.get(sessionId);
274
+ const runId = session && session.runId;
275
+ const key = `${runId || sessionId}:${action}`;
276
+ if (!session || !runId || state.runControlRequests.has(key)) return;
277
+ const methods = {
278
+ stop: "stopAgent",
279
+ pause: "pauseAgent",
280
+ resume: "resumeAgentRun",
281
+ retry: "retryAgent",
282
+ };
283
+ const method = methods[action];
284
+ if (!method || typeof window.loadtoagent?.[method] !== "function") return context.toast(t("management.control_unavailable"));
285
+ state.runControlRequests.add(key);
286
+ if (action === "stop") state.stopRequests.add(runId);
287
+ context.renderDrawer?.();
288
+ try {
289
+ const result = await window.loadtoagent[method](runId);
290
+ if (!result || result.ok === false) throw new Error(result && result.error || t("management.control_failed"));
291
+ context.toast(t(`management.control_${action}_sent`));
292
+ } catch (error) {
293
+ context.toast(errorText(error, "management.control_failed"));
294
+ } finally {
295
+ state.runControlRequests.delete(key);
296
+ state.stopRequests.delete(runId);
297
+ if (state.selectedId) context.renderDrawer?.();
298
+ }
299
+ }
300
+
301
+ function quickRespond(sessionId, value, root = document) {
302
+ const command = String(value || "").trim();
303
+ if (!command) return;
304
+ state.agentCommandDrafts.set(sessionId, command);
305
+ const form = root.querySelector?.(`[data-agent-command-form="${CSS.escape(sessionId)}"]`);
306
+ const input = form?.querySelector("[data-agent-command-draft]");
307
+ if (input) input.value = command;
308
+ if (form) form.requestSubmit();
309
+ else {
310
+ state.drawerTab = "summary";
311
+ context.openDrawer?.(sessionId);
312
+ }
313
+ }
314
+
315
+ function prepareReassignment(sessionId) {
316
+ const session = snapshotSession(sessionId) || state.details.get(sessionId);
317
+ if (!session) return context.toast(t("agent.session_not_found"));
318
+ const provider = (context.visibleProviders?.() || state.providers)
319
+ .find(item => item.id !== session.provider && state.availability[item.id]);
320
+ if (!provider) return context.toast(t("management.reassign_unavailable"));
321
+ state.runProvider = provider.id;
322
+ context.closeDrawer?.(false);
323
+ context.openRunModal?.();
324
+ const request = session.sharedGoal
325
+ || session.delegation?.assignment
326
+ || [...(session.messages || [])].find(message => message.role === "user" && message.text)?.text
327
+ || session.title;
328
+ const task = request && request !== session.title ? `${session.title}\n\n${request}` : (request || session.title);
329
+ const prompt = t("management.reassign_prompt", { task, provider: providerInfo(session.provider).label });
330
+ const promptInput = $("#runPrompt");
331
+ const cwdInput = $("#runCwd");
332
+ if (promptInput) promptInput.value = prompt;
333
+ if (cwdInput && session.cwd) cwdInput.value = session.cwd;
334
+ $("#runProviderPicker") && ($("#runProviderPicker").innerHTML = context.providerPickerHtml?.() || "");
335
+ context.syncRunComposer?.();
336
+ promptInput?.focus({ preventScroll: true });
337
+ }
338
+
270
339
  return {
271
340
  agentCommandTargets,
272
341
  agentResumeSupport,
@@ -281,5 +350,8 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
281
350
  openAgentTerminal,
282
351
  copyBridgeCommand,
283
352
  openSessionOrigin,
353
+ controlManagedRun,
354
+ quickRespond,
355
+ prepareReassignment,
284
356
  };
285
357
  };
@@ -2,26 +2,31 @@
2
2
 
3
3
  (() => {
4
4
  const factories = window.LoadToAgentAppFactories || {};
5
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
5
6
  const app = {};
6
7
  const install = (name) => {
7
- if (typeof factories[name] !== "function") throw new Error(`앱 모듈을 찾지 못했습니다: ${name}`);
8
+ if (typeof factories[name] !== "function") throw new Error(t("bootstrap.module_missing", { name }));
8
9
  Object.assign(app, factories[name](app));
9
10
  };
10
11
 
11
12
  [
12
13
  "createCore",
14
+ "createProviderVisibility",
13
15
  "createDashboard",
16
+ "createRuntimeOverview",
14
17
  "createGraphModel",
15
18
  "createGraphView",
16
19
  "createGraphLayout",
17
20
  "createGraphOrchestration",
18
21
  "createTmuxRenderer",
19
- "createSessionRenderer",
20
22
  "createAgentActions",
23
+ "createManagement",
24
+ "createSessionRenderer",
21
25
  "createDrawerData",
22
26
  "createDrawerContent",
23
27
  "createDrawer",
24
28
  "createRunModal",
29
+ "createQualityEnhancements",
25
30
  "createNavigationEventBindings",
26
31
  "createSessionEventBindings",
27
32
  "createFilterEventBindings",
@@ -30,32 +35,70 @@
30
35
  ].forEach(install);
31
36
  window.LoadToAgentApp = app;
32
37
 
33
- const { $, esc, state, loadGuideState, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, selectView, openDrawer, openSubagentConversation, toast } = app;
38
+ const { $, esc, state, loadGuideState, loadQualityState = () => {}, loadProviderVisibility, projectVisibleSnapshot, visibleSnapshot, isProviderVisible, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, selectView, openDrawer, openSubagentConversation, toast } = app;
39
+
40
+ let initializationError = "";
41
+ const showInitializationError = (message) => {
42
+ initializationError = String(message || t("ui.connection_failed"));
43
+ $("#lastSync").textContent = t("ui.connection_failed");
44
+ $("#appConnectionState")?.classList.add("connection-error");
45
+ $("#appErrorMessage").textContent = initializationError;
46
+ $("#appErrorBanner").classList.remove("hidden");
47
+ };
48
+ $("#appRetryBtn")?.addEventListener("click", () => window.location.reload());
49
+ $("#appErrorCopyBtn")?.addEventListener("click", async () => {
50
+ try {
51
+ await navigator.clipboard.writeText(initializationError);
52
+ toast(t("quality.copy_success"));
53
+ } catch (error) {
54
+ window.LoadToAgentRendererUtils.reportRecoverableError("initialization-error-copy", error);
55
+ toast(t("quality.copy_failed"));
56
+ }
57
+ });
34
58
 
35
59
  async function init() {
60
+ loadQualityState();
36
61
  loadGuideState();
37
62
  if (!window.loadtoagent) {
38
63
  $("#emptyState").classList.remove("hidden");
39
- $("#emptyState p").textContent = "LoadToAgent 프로그램에서 열면 이 컴퓨터의 AI 작업 기록을 불러옵니다.";
64
+ $("#emptyState p").textContent = t("bootstrap.open_in_app");
65
+ showInitializationError(t("bootstrap.open_in_app"));
40
66
  return;
41
67
  }
42
68
  const bootstrap = await window.loadtoagent.bootstrap();
43
69
  if (window.loadtoagent.setLocale) await window.loadtoagent.setLocale(window.LoadToAgentI18n?.getLocale() || "ko");
44
70
  state.providers = bootstrap.providers || [];
45
71
  state.providerMap = new Map(state.providers.map((provider) => [provider.id, provider]));
72
+ loadProviderVisibility(bootstrap.providerVisibility);
46
73
  state.availability = bootstrap.availability || {};
47
74
  state.workspaces = bootstrap.workspaces || [];
48
- state.snapshot = bootstrap.snapshot;
75
+ state.rawSnapshot = bootstrap.snapshot;
76
+ state.snapshot = projectVisibleSnapshot(bootstrap.snapshot);
49
77
  state.activeRuns = bootstrap.activeRuns || [];
50
78
  state.platform = bootstrap.platform || state.platform;
51
79
  state.versions = bootstrap.versions || {};
52
80
  state.update = bootstrap.update || { status: "idle", currentVersion: state.versions.app || "" };
81
+ const handleAttentionRequested = (payload) => {
82
+ const sessionId = String(payload && payload.sessionId || '');
83
+ const session = (state.snapshot && state.snapshot.sessions || []).find(item => item.id === sessionId);
84
+ if (session && !isProviderVisible(session.provider)) return;
85
+ selectView('waiting');
86
+ if (session) {
87
+ if (session.parentId) openSubagentConversation(session.id);
88
+ else openDrawer(session.id);
89
+ } else toast(t("bootstrap.opened_attention_list"));
90
+ };
91
+ if (window.loadtoagent.onAttentionRequested) window.loadtoagent.onAttentionRequested(handleAttentionRequested);
53
92
  bindEvents();
54
93
  render();
94
+ $("#appConnectionState")?.classList.remove("connection-error");
95
+ $("#appErrorBanner").classList.add("hidden");
96
+ app.initialized = true;
55
97
  $("#lastSync").textContent = timeOnly(state.snapshot && state.snapshot.generatedAt);
56
98
  window.loadtoagent.onSnapshot((snapshot) => {
57
- state.snapshot = snapshot;
58
- if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.updateSnapshot(snapshot, state.workspaces);
99
+ state.rawSnapshot = snapshot;
100
+ state.snapshot = projectVisibleSnapshot(snapshot);
101
+ if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.updateSnapshot(visibleSnapshot(), state.workspaces);
59
102
  $("#lastSync").textContent = timeOnly(snapshot.generatedAt);
60
103
  render();
61
104
  if (state.selectedId && $("#detailDrawer").classList.contains("open") && !state.detailLoadingIds.has(state.selectedId)) {
@@ -64,16 +107,7 @@
64
107
  if (card && detail && card.updatedAt !== detail.updatedAt) loadSessionDetail(state.selectedId, true);
65
108
  }
66
109
  });
67
- if (window.loadtoagent.onAttentionRequested)
68
- window.loadtoagent.onAttentionRequested((payload) => {
69
- const sessionId = String(payload && payload.sessionId || '');
70
- const session = (state.snapshot && state.snapshot.sessions || []).find(item => item.id === sessionId);
71
- selectView('waiting');
72
- if (session) {
73
- if (session.parentId) openSubagentConversation(session.id);
74
- else openDrawer(session.id);
75
- } else toast('확인이 필요한 세션 목록을 열었습니다.');
76
- });
110
+ if (window.loadtoagent.rendererReady) await window.loadtoagent.rendererReady();
77
111
  if (window.loadtoagent.onUpdateState)
78
112
  window.loadtoagent.onUpdateState((update) => {
79
113
  state.update = update;
@@ -99,7 +133,8 @@
99
133
 
100
134
  init().catch((error) => {
101
135
  console.error(error);
102
- $("#lastSync").textContent = window.LoadToAgentI18n.t("ui.connection_failed");
103
- toast(`초기화 실패: ${error.message}`);
136
+ const message = t("bootstrap.initialization_failed", { message: window.LoadToAgentI18n.errorText(error, "ui.connection_failed") });
137
+ showInitializationError(message);
138
+ toast(message);
104
139
  });
105
140
  })();