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,34 +3,43 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createRunModal = function createRunModal(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
8
  $,
8
9
  esc,
9
10
  uiLocale,
10
11
  state,
12
+ PROJECTLESS_WORKSPACE,
11
13
  motionPreference,
12
14
  motionState,
13
15
  markGuideStep,
14
16
  rememberDialogTrigger,
15
17
  restoreDialogTrigger,
18
+ setDialogOpenState,
19
+ announce,
16
20
  providerInfo,
17
21
  providerStyle,
22
+ visibleProviders = () => state.providers,
23
+ isProviderVisible = () => true,
24
+ restoreRunDraft = () => {},
25
+ clearRunDraft = () => {},
18
26
  } = context;
19
27
 
20
28
  function providerPickerHtml() {
21
- return state.providers
29
+ return visibleProviders()
22
30
  .map((provider) => {
23
31
  const installed = !!state.availability[provider.id];
24
32
  const selected = state.runProvider === provider.id;
25
33
  return `<button type="button" class="run-provider-option ${selected ? "selected" : ""}"
26
34
  data-run-provider="${esc(provider.id)}"
27
35
  style="${providerStyle(provider.id)}"
28
- aria-pressed="${selected ? "true" : "false"}"
36
+ role="radio" aria-checked="${selected ? "true" : "false"}"
37
+ tabindex="${selected ? "0" : "-1"}"
29
38
  ${installed ? "" : "disabled"}>
30
39
  <span class="provider-mini-mark">${esc(provider.mark)}</span>
31
40
  <span class="run-provider-copy">
32
41
  <b>${esc(provider.label)}</b>
33
- <small>${esc(installed ? `${provider.company} · CLI 발견됨` : window.LoadToAgentI18n.t("ui.setup_required"))}</small>
42
+ <small>${esc(installed ? t("run.cli_found", { company: provider.company }) : t("ui.setup_required"))}</small>
34
43
  </span>
35
44
  <span class="run-provider-check" aria-hidden="true">✓</span>
36
45
  </button>`;
@@ -39,26 +48,30 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
39
48
  }
40
49
 
41
50
  function runProviderHelpHtml() {
42
- const available = state.providers.filter((provider) => state.availability[provider.id]);
51
+ if (!visibleProviders().length) return `<div class="run-provider-help-copy">
52
+ <b>${t("settings.providers.all_hidden_title")}</b>
53
+ <p>${t("settings.providers.all_hidden_description")}</p>
54
+ </div>`;
55
+ const available = visibleProviders().filter((provider) => state.availability[provider.id]);
43
56
  if (available.length) return "";
44
- const docs = state.providers
57
+ const docs = visibleProviders()
45
58
  .map(
46
59
  (provider) => `<button type="button" data-provider-docs="${esc(provider.id)}">
47
60
  <span class="provider-mini-mark" style="${providerStyle(provider.id)}">${esc(provider.mark)}</span>
48
61
  <span>
49
- <b>${esc(window.LoadToAgentI18n.t("provider.install_guide", { provider: provider.label }))}</b>
50
- <small>공식 문서에서 CLI 설치와 로그인을 확인하세요.</small>
62
+ <b>${esc(t("provider.install_guide", { provider: provider.label }))}</b>
63
+ <small>${esc(t("run.check_official_docs"))}</small>
51
64
  </span>
52
65
  <i aria-hidden="true">↗</i>
53
66
  </button>`,
54
67
  )
55
68
  .join("");
56
69
  return `<div class="run-provider-help-copy">
57
- <b>먼저 AI CLI 한 개를 준비해 주세요</b>
58
- <p>1. 아래 공식 설치 안내 열기 → 2. 터미널에서 로그인 → 3. 설치 확인을 누르면 됩니다.</p>
70
+ <b>${esc(t("run.prepare_cli"))}</b>
71
+ <p>${esc(t("run.prepare_cli_steps"))}</p>
59
72
  </div>
60
73
  <div class="run-provider-docs">${docs}</div>
61
- <button type="button" class="provider-recheck" data-provider-recheck>↻ 설치 상태 다시 확인</button>`;
74
+ <button type="button" class="provider-recheck" data-provider-recheck>↻ ${esc(t("run.recheck_installation"))}</button>`;
62
75
  }
63
76
 
64
77
  function runWorkspaceSuggestionsHtml() {
@@ -68,7 +81,7 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
68
81
  .map((workspace) => {
69
82
  const path = workspace.path || workspace.name || "";
70
83
  const active = path === selected;
71
- return `<button type="button" data-run-workspace="${esc(path)}" class="${active ? "selected" : ""}" title="${esc(path)}">
84
+ return `<button type="button" data-run-workspace="${esc(path)}" class="${active ? "selected" : ""}" title="${esc(path)}" aria-pressed="${active ? "true" : "false"}">
72
85
  <span aria-hidden="true">⌘</span>
73
86
  ${esc(workspace.name || path.split(/[\\/]/).filter(Boolean).pop() || window.LoadToAgentI18n.t("ui.workspaces"))}
74
87
  </button>`;
@@ -79,23 +92,30 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
79
92
  function syncRunComposer() {
80
93
  const prompt = $("#runPrompt");
81
94
  const count = $("#runPromptCount");
82
- if (prompt && count) count.textContent = `${prompt.value.length.toLocaleString(uiLocale())} / 8,000`;
95
+ if (prompt && count) {
96
+ const wasWarning = count.dataset.warning === "true";
97
+ const warning = prompt.value.length >= 7_200;
98
+ count.textContent = `${prompt.value.length.toLocaleString(uiLocale())} / 8,000`;
99
+ count.classList.toggle("warning", warning);
100
+ count.dataset.warning = warning ? "true" : "false";
101
+ if (warning && !wasWarning) announce(t("run.prompt_near_limit", { count: Math.max(0, 8_000 - prompt.value.length) }));
102
+ }
83
103
  const submitLabel = $("#runSubmitLabel");
84
104
  const submit = $('#runForm button[type="submit"]');
85
- const hasProvider = Boolean(state.availability[state.runProvider]);
105
+ const hasProvider = isProviderVisible(state.runProvider) && Boolean(state.availability[state.runProvider]);
86
106
  const providerHelp = $("#runProviderHelp");
87
107
  if (providerHelp) {
88
108
  providerHelp.innerHTML = runProviderHelpHtml();
89
109
  providerHelp.classList.toggle(
90
110
  "hidden",
91
- state.providers.some((provider) => state.availability[provider.id]),
111
+ visibleProviders().some((provider) => state.availability[provider.id]),
92
112
  );
93
113
  }
94
114
  if (submit) submit.disabled = submit.dataset.submitting === "true" || !hasProvider;
95
115
  if (submitLabel && submit.dataset.submitting !== "true")
96
116
  submitLabel.textContent = hasProvider
97
- ? window.LoadToAgentI18n.t("provider.assign", { provider: providerInfo(state.runProvider).label })
98
- : "AI 설치가 필요합니다";
117
+ ? t("provider.assign", { provider: providerInfo(state.runProvider).label })
118
+ : visibleProviders().length ? t("run.ai_installation_required") : t("settings.providers.enable_to_run");
99
119
  const writeIntent = /(고치|수정|추가|구현|변경|삭제|작성|리팩터|fix|implement|update|edit|refactor)/i.test((prompt && prompt.value) || "");
100
120
  const permissionHint = $("#runPermissionHint");
101
121
  const permissionNeeded = writeIntent && !$("#allowWrites").checked;
@@ -110,40 +130,57 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
110
130
  const submit = $('#runForm button[type="submit"]');
111
131
  if (!submit) return;
112
132
  submit.dataset.submitting = submitting ? "true" : "false";
113
- submit.disabled = submitting || !state.availability[state.runProvider];
133
+ submit.disabled = submitting || !isProviderVisible(state.runProvider) || !state.availability[state.runProvider];
114
134
  submit.setAttribute("aria-busy", submitting ? "true" : "false");
135
+ $("#closeRunModalBtn").disabled = submitting;
136
+ $("#cancelRunBtn").disabled = submitting;
115
137
  const label = $("#runSubmitLabel");
116
138
  if (label) label.textContent = submitting
117
- ? "AI 작업을 준비하는 중…"
118
- : window.LoadToAgentI18n.t("provider.assign", { provider: providerInfo(state.runProvider).label });
139
+ ? t("run.preparing")
140
+ : t("provider.assign", { provider: providerInfo(state.runProvider).label });
119
141
  }
120
142
 
121
143
  function openRunModal() {
122
144
  rememberDialogTrigger();
123
- const installed = state.providers.find((provider) => state.availability[provider.id]);
124
- if (!state.availability[state.runProvider] && installed) state.runProvider = installed.id;
145
+ restoreRunDraft();
146
+ const installed = visibleProviders().find((provider) => state.availability[provider.id]);
147
+ if ((!isProviderVisible(state.runProvider) || !state.availability[state.runProvider]) && installed) state.runProvider = installed.id;
148
+ if (!isProviderVisible(state.runProvider)) state.runProvider = visibleProviders()[0]?.id || "";
125
149
  $("#runProviderPicker").innerHTML = providerPickerHtml();
126
- if (!$("#runCwd").value) $("#runCwd").value = state.workspace !== "all" ? state.workspace : (state.workspaces[0] && state.workspaces[0].path) || "";
150
+ if (!$("#runCwd").value) $("#runCwd").value = state.workspace !== "all" && state.workspace !== PROJECTLESS_WORKSPACE
151
+ ? state.workspace
152
+ : state.workspace === PROJECTLESS_WORKSPACE ? "" : (state.workspaces[0] && state.workspaces[0].path) || "";
127
153
  $("#runCwd").placeholder = state.platform.id === "win32" ? "D:\\project" : "/Users/me/project";
128
- const advanced = $("#runForm .run-advanced");
129
- if (advanced) advanced.open = Boolean($("#runModel").value.trim());
130
154
  $("#runError").classList.add("hidden");
131
155
  syncRunComposer();
132
156
  clearTimeout(motionState.modalTimer);
157
+ clearTimeout(motionState.modalFocusTimer);
158
+ setDialogOpenState($("#runModal"), true);
133
159
  $("#runModal").classList.remove("hidden", "closing");
134
- setTimeout(() => $("#runPrompt").focus(), 0);
160
+ const focusPromptIfOutside = () => {
161
+ const modal = $("#runModal");
162
+ if (!modal.classList.contains("hidden") && !modal.classList.contains("closing") && !modal.contains(document.activeElement)) {
163
+ $("#runPrompt").focus();
164
+ }
165
+ };
166
+ setTimeout(focusPromptIfOutside, 0);
167
+ motionState.modalFocusTimer = setTimeout(focusPromptIfOutside, motionPreference.matches ? 0 : 300);
135
168
  }
136
169
 
137
- function closeRunModal() {
170
+ function closeRunModal(force = false) {
138
171
  const modal = $("#runModal");
139
172
  if (modal.classList.contains("hidden") || modal.classList.contains("closing")) return;
173
+ if (force !== true && $('#runForm button[type="submit"]').dataset.submitting === "true") return;
174
+ const modalGeneration = motionState.dialogGeneration;
175
+ clearTimeout(motionState.modalFocusTimer);
140
176
  modal.classList.add("closing");
141
177
  clearTimeout(motionState.modalTimer);
142
178
  motionState.modalTimer = setTimeout(
143
179
  () => {
144
180
  modal.classList.add("hidden");
145
181
  modal.classList.remove("closing");
146
- restoreDialogTrigger();
182
+ setDialogOpenState(modal, false);
183
+ restoreDialogTrigger(modalGeneration);
147
184
  },
148
185
  motionPreference.matches ? 0 : 220,
149
186
  );
@@ -166,20 +203,52 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
166
203
  }, 3200);
167
204
  }
168
205
 
169
- async function performUiAction(action, failureMessage) {
206
+ async function performUiAction(action, failureMessage, control = null) {
207
+ if (control?.dataset.busy === "true") return null;
208
+ const wasDisabled = Boolean(control?.disabled);
209
+ if (control) {
210
+ control.dataset.busy = "true";
211
+ control.disabled = true;
212
+ control.setAttribute("aria-busy", "true");
213
+ }
170
214
  try {
171
215
  return await action();
172
216
  } catch (error) {
173
- toast((error && error.message) || failureMessage);
217
+ toast(window.LoadToAgentI18n.errorText(error, failureMessage));
174
218
  return null;
219
+ } finally {
220
+ if (control?.isConnected) {
221
+ delete control.dataset.busy;
222
+ control.disabled = wasDisabled;
223
+ control.removeAttribute("aria-busy");
224
+ }
175
225
  }
176
226
  }
177
227
 
178
228
  async function handleRun(event) {
179
229
  event.preventDefault();
180
- if (!state.availability[state.runProvider]) {
230
+ const prompt = $("#runPrompt");
231
+ const cwd = $("#runCwd");
232
+ const invalid = [];
233
+ if (!prompt.value.trim()) {
234
+ prompt.setAttribute("aria-invalid", "true");
235
+ invalid.push({ element: prompt, message: t("quality.run_prompt_required") });
236
+ }
237
+ if (!cwd.value.trim()) {
238
+ cwd.setAttribute("aria-invalid", "true");
239
+ invalid.push({ element: cwd, message: t("quality.run_cwd_required") });
240
+ }
241
+ if (invalid.length) {
242
+ $("#runError").textContent = invalid[0].message;
243
+ $("#runError").classList.remove("hidden");
244
+ invalid[0].element.focus({ preventScroll: true });
245
+ announce(invalid[0].message);
246
+ return;
247
+ }
248
+ if (!isProviderVisible(state.runProvider) || !state.availability[state.runProvider]) {
181
249
  $("#runError").textContent = window.LoadToAgentI18n.t("ui.no_ai_cli_is_ready_follow_the_official_setup_guide");
182
250
  $("#runError").classList.remove("hidden");
251
+ $("#runError").focus({ preventScroll: true });
183
252
  return;
184
253
  }
185
254
  setRunSubmitting(true);
@@ -194,13 +263,14 @@ window.LoadToAgentAppFactories.createRunModal = function createRunModal(context
194
263
  });
195
264
  if (!result.ok) throw new Error(result.error || window.LoadToAgentI18n.t("ui.could_not_start_the_task"));
196
265
  markGuideStep("create");
197
- closeRunModal();
198
- $("#runPrompt").value = "";
266
+ closeRunModal(true);
267
+ clearRunDraft({ silent: true, focus: false });
199
268
  syncRunComposer();
200
269
  toast(window.LoadToAgentI18n.t("provider.started", { provider: providerInfo(state.runProvider).label }));
201
270
  } catch (error) {
202
- $("#runError").textContent = error.message;
271
+ $("#runError").textContent = window.LoadToAgentI18n.errorText(error, "ui.could_not_start_the_task");
203
272
  $("#runError").classList.remove("hidden");
273
+ $("#runError").focus({ preventScroll: true });
204
274
  } finally {
205
275
  setRunSubmitting(false);
206
276
  }
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+
3
+ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
+
5
+ window.LoadToAgentAppFactories.createRuntimeOverview = function createRuntimeOverview(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
7
+ const {
8
+ $,
9
+ esc,
10
+ uiLocale,
11
+ state,
12
+ providerInfo,
13
+ providerStyle,
14
+ currentActivity,
15
+ visibleSessions = () => ((state.snapshot && state.snapshot.sessions) || []),
16
+ isProviderVisible = () => true,
17
+ isRuntimeLoopSession = () => false,
18
+ } = context;
19
+
20
+ let runtimeTicker = 0;
21
+ let runtimeRenderVersion = 0;
22
+ let pendingRuntimeFocus = null;
23
+
24
+ function activeRootLoops() {
25
+ return visibleSessions()
26
+ .filter(isRuntimeLoopSession)
27
+ .sort((a, b) => Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0));
28
+ }
29
+
30
+ function visibleAutomations() {
31
+ return ((state.snapshot && state.snapshot.automations) || [])
32
+ .filter((item) => isProviderVisible(item.provider || "codex"))
33
+ .sort((a, b) => Date.parse(a.nextRunAt || 0) - Date.parse(b.nextRunAt || 0));
34
+ }
35
+
36
+ function automationSession(item) {
37
+ if (!item || !item.targetThreadId) return null;
38
+ return visibleSessions().find((session) => session.externalId === item.targetThreadId || session.id === item.targetThreadId) || null;
39
+ }
40
+
41
+ function scheduleTime(value) {
42
+ const date = new Date(value);
43
+ if (Number.isNaN(date.getTime())) return t("runtime.not_scheduled");
44
+ const now = new Date();
45
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
46
+ const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
47
+ const day = Math.round((target.getTime() - start.getTime()) / 86_400_000);
48
+ const time = date.toLocaleTimeString(uiLocale(), { hour: "2-digit", minute: "2-digit" });
49
+ if (day === 0) return t("runtime.today_at", { time });
50
+ if (day === 1) return t("runtime.tomorrow_at", { time });
51
+ return date.toLocaleString(uiLocale(), { month: "short", day: "numeric", weekday: "short", hour: "2-digit", minute: "2-digit" });
52
+ }
53
+
54
+ function scheduleRule(value) {
55
+ const rule = Object.fromEntries(String(value || "").split(";").map((pair) => pair.split("=", 2)).filter((pair) => pair.length === 2));
56
+ const interval = Math.max(1, Number.parseInt(rule.INTERVAL || "1", 10) || 1);
57
+ const frequencyKeys = {
58
+ DAILY: "runtime.every_day",
59
+ WEEKLY: "runtime.every_week",
60
+ HOURLY: "runtime.every_hour",
61
+ MINUTELY: "runtime.every_minute",
62
+ };
63
+ const intervalKeys = {
64
+ DAILY: "runtime.every_n_days",
65
+ WEEKLY: "runtime.every_n_weeks",
66
+ HOURLY: "runtime.every_n_hours",
67
+ MINUTELY: "runtime.every_n_minutes",
68
+ };
69
+ const base = interval > 1 && intervalKeys[rule.FREQ]
70
+ ? t(intervalKeys[rule.FREQ], { count: interval })
71
+ : frequencyKeys[rule.FREQ] ? t(frequencyKeys[rule.FREQ]) : t("runtime.recurring");
72
+ const details = [];
73
+ if (rule.BYDAY) {
74
+ const weekdayDates = { SU: 4, MO: 5, TU: 6, WE: 7, TH: 8, FR: 9, SA: 10 };
75
+ const days = rule.BYDAY.split(",")
76
+ .map((day) => weekdayDates[day])
77
+ .filter(Boolean)
78
+ .map((day) => new Intl.DateTimeFormat(uiLocale(), { weekday: "short" }).format(new Date(2026, 0, day)));
79
+ if (days.length) details.push(days.join("·"));
80
+ }
81
+ if (rule.BYHOUR) {
82
+ const hour = String(rule.BYHOUR).padStart(2, "0");
83
+ const minute = String(rule.BYMINUTE || "0").padStart(2, "0");
84
+ details.push(`${hour}:${minute}`);
85
+ } else if (rule.FREQ === "HOURLY" && rule.BYMINUTE) {
86
+ details.push(t("runtime.at_minute", { minute: Number.parseInt(rule.BYMINUTE, 10) || 0 }));
87
+ }
88
+ return [base, ...details].join(" · ");
89
+ }
90
+
91
+ function loopPhase(session) {
92
+ const explicitPhase = String(session.loop && session.loop.phase || "").toLowerCase();
93
+ const explicitIndex = { input: 0, decide: 1, decision: 1, act: 2, action: 2, observe: 3, observation: 3 }[explicitPhase];
94
+ if (Number.isInteger(explicitIndex)) return explicitIndex;
95
+ const event = [...(session.lifecycle || [])].reverse().find((item) => item.status === "running")
96
+ || (session.lifecycle || [])[session.lifecycle.length - 1]
97
+ || {};
98
+ const signal = `${event.type || ""} ${event.label || ""}`.toLowerCase();
99
+ if (/result|output|wait|complete|observe|검수|확인/.test(signal)) return 3;
100
+ if (/tool|collaboration|command|exec|실행|도구/.test(signal)) return 2;
101
+ if (/reason|think|decid|추론|판단/.test(signal)) return 1;
102
+ return 0;
103
+ }
104
+
105
+ function loopPhases(session) {
106
+ const active = loopPhase(session);
107
+ const definitions = [
108
+ ["input", "runtime.phase_input", "runtime.phase_input_detail"],
109
+ ["decide", "runtime.phase_decide", "runtime.phase_decide_detail"],
110
+ ["act", "runtime.phase_act", "runtime.phase_act_detail"],
111
+ ["observe", "runtime.phase_observe", "runtime.phase_observe_detail"],
112
+ ];
113
+ return definitions.map(([key, label, detail], index) => ({
114
+ key,
115
+ label: t(label),
116
+ detail: t(detail),
117
+ state: index < active ? "done" : index === active ? "active" : "queued",
118
+ }));
119
+ }
120
+
121
+ function phaseStatusLabel(stateValue) {
122
+ return t({ done: "runtime.phase_done", active: "runtime.phase_active", queued: "runtime.phase_queued" }[stateValue] || "runtime.phase_queued");
123
+ }
124
+
125
+ function elapsedSince(value) {
126
+ if (!value) return t("runtime.just_started");
127
+ const elapsed = Date.now() - Date.parse(value || 0);
128
+ if (!Number.isFinite(elapsed) || elapsed < 60_000) return t("runtime.just_started");
129
+ const minutes = Math.floor(elapsed / 60_000);
130
+ if (minutes < 60) return t("runtime.elapsed_minutes", { count: minutes });
131
+ return t("runtime.elapsed_hours", { count: Math.floor(minutes / 60) });
132
+ }
133
+
134
+ function scheduleCard(item) {
135
+ const session = automationSession(item);
136
+ const cwd = (item.cwds || [])[0] || "";
137
+ const workspace = String(cwd).replace(/\\/g, "/").split("/").filter(Boolean).pop();
138
+ const location = workspace || (item.environment?.kind === "wsl" && item.sourceLabel) || t("runtime.workspace_unspecified");
139
+ const body = `<span class="runtime-schedule-time" ${item.enabled ? `data-runtime-next-run-at="${esc(item.nextRunAt || "")}"` : ""}>${esc(item.enabled ? scheduleTime(item.nextRunAt) : t("runtime.paused"))}</span>
140
+ <strong>${esc(item.name)}</strong>
141
+ <small>${esc(scheduleRule(item.rrule))} · ${esc(location)}</small>`;
142
+ const card = session
143
+ ? `<button type="button" class="runtime-schedule-card ${item.enabled ? "" : "paused"}" data-automation-id="${esc(item.id)}" data-automation-enabled="${item.enabled ? "true" : "false"}" data-automation-session="${esc(session.id)}">${body}<i aria-hidden="true">↗</i></button>`
144
+ : `<article class="runtime-schedule-card ${item.enabled ? "" : "paused"}" data-automation-id="${esc(item.id)}" data-automation-enabled="${item.enabled ? "true" : "false"}">${body}<i aria-hidden="true">${item.enabled ? "●" : "Ⅱ"}</i></article>`;
145
+ return `<div class="runtime-schedule-item" role="listitem">${card}</div>`;
146
+ }
147
+
148
+ function emptySchedules() {
149
+ return `<div class="runtime-schedule-empty"><span aria-hidden="true">+</span><div><b>${esc(t("runtime.no_schedules"))}</b><small>${esc(t("runtime.no_schedules_detail"))}</small></div></div>`;
150
+ }
151
+
152
+ function loopSelector(loop, selected) {
153
+ const provider = providerInfo(loop.provider);
154
+ const activePhase = loopPhases(loop).find((phase) => phase.state === "active") || loopPhases(loop)[0];
155
+ return `<button type="button" class="runtime-loop-tab ${selected ? "selected" : ""}" data-loop-select="${esc(loop.id)}"
156
+ id="runtime-loop-tab-${esc(loop.id)}" role="tab" aria-controls="runtime-loop-panel-${esc(loop.id)}"
157
+ style="${providerStyle(loop.provider)}" aria-selected="${selected ? "true" : "false"}" aria-pressed="${selected ? "true" : "false"}" tabindex="${selected ? "0" : "-1"}">
158
+ <span class="runtime-loop-tab-mark">${esc(provider.mark)}</span>
159
+ <span><b>${esc(loop.title)}</b><small>${esc(provider.label)} · ${esc(activePhase.label)} · <span data-runtime-started-at="${esc(loop.startedAt || "")}">${esc(elapsedSince(loop.startedAt))}</span></small></span>
160
+ <i aria-hidden="true"></i>
161
+ </button>`;
162
+ }
163
+
164
+ function loopDiagram(session) {
165
+ const phases = loopPhases(session);
166
+ const activeIndex = Math.max(0, phases.findIndex((phase) => phase.state === "active"));
167
+ const activePhase = phases[activeIndex];
168
+ return `<div class="runtime-loop-cycle" role="img" aria-label="${esc(t("runtime.loop_flow_state", { phase: activePhase.label }))}" style="--loop-progress:${activeIndex / Math.max(1, phases.length - 1) * 100}%">
169
+ <div class="runtime-loop-spine" aria-hidden="true"><span></span></div>
170
+ ${phases.map((phase, index) => `<div class="runtime-loop-phase ${phase.state}" data-loop-phase="${phase.key}">
171
+ <span class="runtime-loop-phase-index">0${index + 1}<em>${esc(phaseStatusLabel(phase.state))}</em></span>
172
+ <i aria-hidden="true">${phase.state === "done" ? "✓" : phase.state === "active" ? "●" : "·"}</i>
173
+ <b>${esc(phase.label)}</b>
174
+ <small>${esc(phase.detail)}</small>
175
+ </div>`).join("")}
176
+ <div class="runtime-loop-return" aria-hidden="true"><span>↺</span><b>${esc(t("runtime.phase_repeat"))}</b></div>
177
+ </div>`;
178
+ }
179
+
180
+ function loopDetail(session) {
181
+ const provider = providerInfo(session.provider);
182
+ const activity = currentActivity(session);
183
+ const children = (session.childIds || []).map((id) => visibleSessions().find((item) => item.id === id)).filter(Boolean);
184
+ const runningChildren = children.filter((item) => ["running", "starting"].includes(item.status)).length;
185
+ const iteration = Number(session.loop && session.loop.iteration || 0);
186
+ const iterationLabel = iteration > 0
187
+ ? t("runtime.iteration_value", { count: iteration })
188
+ : session.loop ? t("runtime.iteration_observed") : t("runtime.iteration_scheduled");
189
+ const activePhase = loopPhases(session).find((phase) => phase.state === "active") || loopPhases(session)[0];
190
+ const activityTitle = window.LoadToAgentI18n.observedText(activity.title);
191
+ const activityDetail = window.LoadToAgentI18n.observedText(activity.detail || session.statusDetail || "");
192
+ return `<article id="runtime-loop-panel-${esc(session.id)}" class="runtime-loop-detail" role="tabpanel" aria-labelledby="runtime-loop-tab-${esc(session.id)}" style="${providerStyle(session.provider)}" data-motion-key="runtime-loop:${esc(session.id)}" data-motion-value="${esc(session.updatedAt || "")}">
193
+ <header>
194
+ <div><span class="runtime-loop-kicker"><i></i>${esc(t("runtime.active_loop"))}</span><h3>${esc(session.title)}</h3><p>${esc(provider.label)} · ${esc(session.model || t("session.model_unknown"))}</p></div>
195
+ <div class="runtime-loop-header-actions"><span class="runtime-active-phase"><small>${esc(t("runtime.current_phase"))}</small><b>${esc(activePhase.label)}</b></span><button type="button" class="runtime-open-task" data-loop-open="${esc(session.id)}">${esc(t("runtime.open_task"))}<span aria-hidden="true">↗</span></button></div>
196
+ </header>
197
+ <section class="runtime-now-strip" aria-label="${esc(t("runtime.now_working"))}">
198
+ <span class="runtime-now-mark" aria-hidden="true">NOW</span>
199
+ <div><small>${esc(t("runtime.now_working"))}</small><b title="${esc(activityTitle)}">${esc(activityTitle)}</b><p title="${esc(activityDetail)}">${esc(activityDetail)}</p></div>
200
+ <time data-runtime-updated-at="${esc(session.updatedAt || session.startedAt || "")}">${esc(t("runtime.last_signal_time", { time: elapsedSince(session.updatedAt || session.startedAt) }))}</time>
201
+ </section>
202
+ ${loopDiagram(session)}
203
+ <footer class="runtime-loop-footer">
204
+ <dl>
205
+ <div><dt>${esc(t("runtime.running_time"))}</dt><dd data-runtime-started-at="${esc(session.startedAt || "")}">${esc(elapsedSince(session.startedAt))}</dd></div>
206
+ <div><dt>${esc(t("runtime.iteration"))}</dt><dd>${esc(iterationLabel)}</dd></div>
207
+ <div><dt>${esc(t("runtime.subagents"))}</dt><dd>${runningChildren ? esc(t("runtime.subagents_running", { running: runningChildren, total: children.length })) : esc(t("runtime.subagents_total", { count: children.length }))}</dd></div>
208
+ </dl>
209
+ </footer>
210
+ </article>`;
211
+ }
212
+
213
+ function noActiveLoop() {
214
+ return `<div class="runtime-loop-empty"><span class="runtime-loop-empty-orbit" aria-hidden="true"><i></i></span><div><b>${esc(t("runtime.no_active_loop"))}</b><p>${esc(t("runtime.no_active_loop_detail"))}</p></div></div>`;
215
+ }
216
+
217
+ function refreshRuntimeTimes(section = $("#automationOverview")) {
218
+ if (!section || section.classList.contains("hidden")) return;
219
+ section.querySelectorAll("[data-runtime-next-run-at]").forEach((element) => {
220
+ element.textContent = scheduleTime(element.dataset.runtimeNextRunAt);
221
+ });
222
+ section.querySelectorAll("[data-runtime-started-at]").forEach((element) => {
223
+ element.textContent = elapsedSince(element.dataset.runtimeStartedAt);
224
+ });
225
+ section.querySelectorAll("[data-runtime-updated-at]").forEach((element) => {
226
+ element.textContent = t("runtime.last_signal_time", { time: elapsedSince(element.dataset.runtimeUpdatedAt) });
227
+ });
228
+ }
229
+
230
+ function ensureRuntimeTicker() {
231
+ if (runtimeTicker) return;
232
+ runtimeTicker = window.setInterval(() => refreshRuntimeTimes(), 30_000);
233
+ }
234
+
235
+ function renderRuntimeOverview() {
236
+ const renderVersion = ++runtimeRenderVersion;
237
+ const section = $("#automationOverview");
238
+ const previousScheduleList = section.querySelector(".runtime-schedule-list");
239
+ const previousLoopTabs = section.querySelector(".runtime-loop-tabs");
240
+ const previousSelectedId = section.querySelector(".runtime-loop-tab.selected")?.dataset.loopSelect || "";
241
+ const scheduleScrollTop = previousScheduleList?.scrollTop || 0;
242
+ const loopScrollLeft = previousLoopTabs?.scrollLeft || 0;
243
+ const restoreScheduleFocus = document.activeElement === previousScheduleList;
244
+ const focusedAutomationId = document.activeElement?.closest?.("[data-automation-id]")?.dataset.automationId || "";
245
+ const focusedLoopId = document.activeElement?.closest?.("[data-loop-select]")?.dataset.loopSelect || "";
246
+ const detectedFocus = restoreScheduleFocus
247
+ ? { type: "schedule-list", id: "" }
248
+ : focusedAutomationId ? { type: "automation", id: focusedAutomationId }
249
+ : focusedLoopId ? { type: "loop", id: focusedLoopId } : null;
250
+ if (detectedFocus) pendingRuntimeFocus = detectedFocus;
251
+ const focusIntent = detectedFocus || pendingRuntimeFocus;
252
+ const automations = visibleAutomations();
253
+ const interactiveScheduleCount = automations.filter((item) => automationSession(item)).length;
254
+ const scheduleListFocusable = automations.length > 0 && interactiveScheduleCount === 0;
255
+ const scheduleListLabel = scheduleListFocusable
256
+ ? t("runtime.schedule_list_scroll_label")
257
+ : interactiveScheduleCount > 0 ? t("runtime.schedule_list_action_label") : t("runtime.schedule_list_label");
258
+ const loops = activeRootLoops();
259
+ const enabled = automations.filter((item) => item.enabled);
260
+ const paused = automations.filter((item) => !item.enabled);
261
+ if (!loops.some((loop) => loop.id === state.selectedRuntimeLoopId)) state.selectedRuntimeLoopId = loops[0] && loops[0].id || null;
262
+ const selected = loops.find((loop) => loop.id === state.selectedRuntimeLoopId) || loops[0] || null;
263
+ const selectedId = selected?.id || "";
264
+ section.innerHTML = `<header class="runtime-overview-head">
265
+ <div class="runtime-overview-title"><span class="runtime-overview-emblem" aria-hidden="true"><i></i><b>↻</b></span><div><p>${esc(t("runtime.eyebrow"))}</p><h2>${esc(t("runtime.status_summary"))}</h2></div></div>
266
+ <div class="runtime-overview-counts"><span><i></i>${esc(t("runtime.schedules_count", { count: enabled.length }))}</span>${paused.length ? `<span class="paused"><i></i>${esc(t("runtime.paused_count", { count: paused.length }))}</span>` : ""}<span><i></i>${esc(t("runtime.loops_count", { count: loops.length }))}</span><b><i></i>${esc(t("runtime.live_status"))}</b></div>
267
+ </header>
268
+ <div class="runtime-overview-grid">
269
+ <aside class="runtime-schedule-lane">
270
+ <header><div><span>${esc(t("runtime.schedule_lane"))}</span><b>${esc(t("runtime.schedule_list"))}</b></div><em>${String(automations.length).padStart(2, "0")}</em></header>
271
+ <div class="runtime-schedule-list" role="list" tabindex="${scheduleListFocusable ? "0" : "-1"}" aria-label="${esc(scheduleListLabel)}">${automations.length ? automations.map(scheduleCard).join("") : emptySchedules()}</div>
272
+ </aside>
273
+ <section class="runtime-loop-lane" aria-label="${esc(t("runtime.loop_lane"))}">
274
+ <header class="runtime-loop-lane-head"><div><span>${esc(t("runtime.loop_lane"))}</span><b>${esc(t("runtime.loop_system"))}</b><small>${esc(t("runtime.inferred_phase"))}</small></div>${loops.length ? `<div class="runtime-loop-tabs" role="tablist" aria-orientation="horizontal" aria-label="${esc(t("runtime.choose_loop"))}">${loops.map((loop) => loopSelector(loop, loop.id === selectedId)).join("")}</div>` : ""}</header>
275
+ ${selected ? loopDetail(selected) : noActiveLoop()}
276
+ </section>
277
+ </div>`;
278
+ requestAnimationFrame(() => {
279
+ if (renderVersion !== runtimeRenderVersion) return;
280
+ const scheduleList = section.querySelector(".runtime-schedule-list");
281
+ const selectedTab = section.querySelector(".runtime-loop-tab.selected");
282
+ const tabList = selectedTab && selectedTab.closest(".runtime-loop-tabs");
283
+ if (scheduleList) scheduleList.scrollTop = scheduleScrollTop;
284
+ if (tabList) tabList.scrollLeft = loopScrollLeft;
285
+ if (selectedTab && tabList && (!previousLoopTabs || previousSelectedId !== selectedId)) {
286
+ const item = selectedTab.getBoundingClientRect();
287
+ const list = tabList.getBoundingClientRect();
288
+ if (item.left < list.left || item.right > list.right) selectedTab.scrollIntoView({ block: "nearest", inline: "nearest" });
289
+ }
290
+ const focusTarget = focusIntent?.type === "schedule-list"
291
+ ? scheduleList
292
+ : focusIntent?.type === "automation" ? section.querySelector(`[data-automation-id="${CSS.escape(focusIntent.id)}"]`)
293
+ : focusIntent?.type === "loop" ? section.querySelector(`[data-loop-select="${CSS.escape(focusIntent.id)}"]`) : null;
294
+ focusTarget?.focus({ preventScroll: true });
295
+ if (focusTarget && document.activeElement === focusTarget) pendingRuntimeFocus = null;
296
+ if (scheduleList) scheduleList.scrollTop = scheduleScrollTop;
297
+ if (tabList && previousLoopTabs && previousSelectedId === selectedId) tabList.scrollLeft = loopScrollLeft;
298
+ });
299
+ ensureRuntimeTicker();
300
+ return automations.length + loops.length;
301
+ }
302
+
303
+ return {
304
+ activeRootLoops,
305
+ visibleAutomations,
306
+ scheduleTime,
307
+ scheduleRule,
308
+ loopPhases,
309
+ refreshRuntimeTimes,
310
+ renderRuntimeOverview,
311
+ };
312
+ };