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
package/renderer/app.js CHANGED
@@ -2,6 +2,8 @@
2
2
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
3
3
  window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
4
4
  const { $, $$, esc, uiLocale, providerLabel, reportRecoverableError } = window.LoadToAgentRendererUtils;
5
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
+ const observedText = (value) => window.LoadToAgentI18n.observedText(value);
5
7
  const PROJECTLESS_WORKSPACE = "__projectless__";
6
8
  const state = {
7
9
  providers: [],
@@ -9,16 +11,18 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
9
11
  availability: {},
10
12
  workspaces: [],
11
13
  snapshot: null,
14
+ rawSnapshot: null,
12
15
  activeRuns: [],
13
16
  versions: {},
14
17
  update: null,
15
18
  view: "all",
16
19
  providerFilters: new Set(),
20
+ hiddenProviders: new Set(),
17
21
  workspace: "all",
18
22
  search: "",
19
23
  sort: "recent",
20
24
  selectedId: null,
21
- drawerTab: "chat",
25
+ drawerTab: "summary",
22
26
  drawerMode: "session",
23
27
  runProvider: "claude",
24
28
  details: new Map(),
@@ -27,16 +31,22 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
27
31
  visibleLimit: 30,
28
32
  graphFocusId: null,
29
33
  graphExpandedProviders: new Set(),
34
+ expandedExecutionSessions: new Set(),
30
35
  expandedCompletedSubagents: new Set(),
36
+ expandedTmuxSubagents: new Set(),
37
+ selectedRuntimeLoopId: null,
31
38
  tmuxFocus: null,
32
39
  agentCommandDrafts: new Map(),
33
40
  agentCommandTargets: new Map(),
34
41
  agentCommandSending: new Set(),
35
42
  stopRequests: new Set(),
43
+ runControlRequests: new Set(),
44
+ managementFilter: "all",
36
45
  detailErrors: new Map(),
46
+ disclosureStates: new Map(),
37
47
  guideCompleted: new Set(),
38
- guideExpanded: true,
39
- platform: { id: "win32", label: "Windows", localShell: "powershell", localShellLabel: "Windows 명령창", nativeTmux: false },
48
+ guideExpanded: false,
49
+ platform: { id: "win32", label: "Windows", localShell: "powershell", localShellLabel: t("terminal.windows_shell"), nativeTmux: false },
40
50
  };
41
51
  Object.defineProperty(state, "provider", {
42
52
  enumerable: true,
@@ -51,9 +61,24 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
51
61
  });
52
62
  const motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");
53
63
  const motionState = {
54
- ready: false, modalTimer: 0, toastTimer: 0, drawerTimer: 0, drawerContentTimer: 0,
55
- drawerRenderKey: "", drawerTab: "", activeDialogTrigger: null,
64
+ ready: false, modalTimer: 0, modalFocusTimer: 0, toastTimer: 0, drawerTimer: 0, drawerContentTimer: 0,
65
+ drawerRenderKey: "", drawerTab: "", activeDialogTrigger: null, dialogGeneration: 0,
56
66
  };
67
+ function disclosureElements(root = document) {
68
+ const elements = [];
69
+ if (root instanceof Element && root.matches("details[data-disclosure-key]")) elements.push(root);
70
+ root.querySelectorAll?.("details[data-disclosure-key]").forEach((element) => elements.push(element));
71
+ return elements;
72
+ }
73
+ function rememberDisclosureStates(root = document) {
74
+ disclosureElements(root).forEach((element) => state.disclosureStates.set(element.dataset.disclosureKey, element.open));
75
+ }
76
+ function restoreDisclosureStates(root = document) {
77
+ disclosureElements(root).forEach((element) => {
78
+ const key = element.dataset.disclosureKey;
79
+ if (state.disclosureStates.has(key)) element.open = state.disclosureStates.get(key);
80
+ });
81
+ }
57
82
  document.documentElement.dataset.motion = motionPreference.matches ? "reduced" : "full";
58
83
  motionPreference.addEventListener("change", (event) => {
59
84
  document.documentElement.dataset.motion = event.matches ? "reduced" : "full";
@@ -127,54 +152,43 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
127
152
  requestAnimationFrame(() => section.classList.add("motion-section-in"));
128
153
  });
129
154
  }
130
- const STATUS = {
131
- starting: window.LoadToAgentI18n.t("ui.preparing"), running: window.LoadToAgentI18n.t("ui.working"),
132
- waiting: window.LoadToAgentI18n.t("app.nav.needs_review"), idle: window.LoadToAgentI18n.t("ui.idle"),
133
- completed: window.LoadToAgentI18n.t("ui.completed"), failed: window.LoadToAgentI18n.t("ui.problem"),
134
- cancelled: window.LoadToAgentI18n.t("ui.stopped"),
135
- };
136
- const VIEW_TITLES = {
137
- all: window.LoadToAgentI18n.t("ui.recent_conversations_and_tasks"), active: window.LoadToAgentI18n.t("ui.active_tasks"),
138
- waiting: window.LoadToAgentI18n.t("ui.tasks_needing_review"), terminal: window.LoadToAgentI18n.t("app.nav.session_terminal"),
139
- tmux: window.LoadToAgentI18n.t("app.nav.tmux"), settings: window.LoadToAgentI18n.t("settings.title"),
155
+ const localizedLookup = (keys) => new Proxy(Object.create(null), {
156
+ get: (_target, property) => keys[property] ? t(keys[property]) : undefined,
157
+ });
158
+ const STATUS = localizedLookup({
159
+ starting: "ui.preparing", running: "ui.working", paused: "management.status.paused", waiting: "app.nav.needs_review", idle: "ui.idle",
160
+ completed: "ui.completed", failed: "ui.problem", cancelled: "ui.stopped",
161
+ });
162
+ const VIEW_TITLES = localizedLookup({
163
+ all: "ui.recent_conversations_and_tasks", active: "ui.active_tasks", waiting: "ui.tasks_needing_review",
164
+ runtime: "runtime.title", terminal: "app.nav.session_terminal", tmux: "app.nav.tmux", settings: "settings.title",
165
+ });
166
+ const VIEW_META_KEYS = {
167
+ all: ["ui.ai_work_overview", "ui.see_all_ai_work_at_a_glance", "ui.active_work_and_items_needing_your_review_appear_first_find"],
168
+ active: ["ui.active_now", "ui.see_which_ai_is_working_now", "ui.see_what_is_being_handled_then_open_a_task_for"],
169
+ waiting: ["ui.your_turn", "ui.handle_items_that_need_your_review_first", "ui.only_tasks_waiting_for_your_response_or_choice_are_shown"],
170
+ runtime: ["runtime.eyebrow", "runtime.title", "runtime.description"],
171
+ terminal: ["ui.continue_an_existing_conversation", "ui.continue_ai_sessions_in_the_terminal", "ui.continue_the_same_task_with_its_previous_conversation_beside_the"],
172
+ tmux: ["ui.advanced_work_tools", "ui.manage_multi_terminal_work_in_one_place", "ui.this_view_is_only_for_existing_tmux_workflows_home_and"],
173
+ settings: ["ui.application_management", "ui.check_versions_and_updates", "ui.compare_the_installed_and_latest_stable_versions_then_download_a"],
140
174
  };
141
- const VIEW_META = {
142
- all: {
143
- eyebrow: window.LoadToAgentI18n.t("ui.ai_work_overview"), title: window.LoadToAgentI18n.t("ui.see_all_ai_work_at_a_glance"),
144
- subtitle: window.LoadToAgentI18n.t("ui.active_work_and_items_needing_your_review_appear_first_find"),
175
+ const VIEW_META = new Proxy(Object.create(null), {
176
+ get: (_target, property) => {
177
+ const keys = VIEW_META_KEYS[property];
178
+ return keys ? { eyebrow: t(keys[0]), title: t(keys[1]), subtitle: t(keys[2]) } : undefined;
145
179
  },
146
- active: {
147
- eyebrow: window.LoadToAgentI18n.t("ui.active_now"), title: window.LoadToAgentI18n.t("ui.see_which_ai_is_working_now"),
148
- subtitle: window.LoadToAgentI18n.t("ui.see_what_is_being_handled_then_open_a_task_for"),
149
- },
150
- waiting: {
151
- eyebrow: window.LoadToAgentI18n.t("ui.your_turn"), title: window.LoadToAgentI18n.t("ui.handle_items_that_need_your_review_first"),
152
- subtitle: window.LoadToAgentI18n.t("ui.only_tasks_waiting_for_your_response_or_choice_are_shown"),
153
- },
154
- terminal: {
155
- eyebrow: window.LoadToAgentI18n.t("ui.continue_an_existing_conversation"), title: window.LoadToAgentI18n.t("ui.continue_ai_sessions_in_the_terminal"),
156
- subtitle: window.LoadToAgentI18n.t("ui.continue_the_same_task_with_its_previous_conversation_beside_the"),
157
- },
158
- tmux: {
159
- eyebrow: window.LoadToAgentI18n.t("ui.advanced_work_tools"), title: window.LoadToAgentI18n.t("ui.manage_multi_terminal_work_in_one_place"),
160
- subtitle: window.LoadToAgentI18n.t("ui.this_view_is_only_for_existing_tmux_workflows_home_and"),
161
- },
162
- settings: {
163
- eyebrow: window.LoadToAgentI18n.t("ui.application_management"), title: window.LoadToAgentI18n.t("ui.check_versions_and_updates"),
164
- subtitle: window.LoadToAgentI18n.t("ui.compare_the_installed_and_latest_stable_versions_then_download_a"),
165
- },
166
- };
180
+ });
167
181
  const GUIDE_STORAGE_KEY = "loadtoagent:start-guide:v1";
168
182
  const GUIDE_STEPS = ["create", "active", "waiting", "detail"];
169
183
  function loadGuideState() {
170
184
  try {
171
185
  const saved = JSON.parse(localStorage.getItem(GUIDE_STORAGE_KEY) || "{}");
172
186
  state.guideCompleted = new Set((saved.completed || []).filter((step) => GUIDE_STEPS.includes(step)));
173
- state.guideExpanded = saved.expanded !== false;
187
+ state.guideExpanded = saved.expanded === true;
174
188
  } catch (error) {
175
189
  reportRecoverableError("guide-state-load", error);
176
190
  state.guideCompleted = new Set();
177
- state.guideExpanded = true;
191
+ state.guideExpanded = false;
178
192
  }
179
193
  }
180
194
  function saveGuideState() {
@@ -225,22 +239,39 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
225
239
  else item.removeAttribute("aria-current");
226
240
  });
227
241
  const advancedView = ["terminal", "tmux", "settings"].includes(state.view);
242
+ const advancedToolsView = ["runtime", "terminal", "tmux"].includes(state.view);
243
+ if (advancedToolsView) $("#advancedToolsNav")?.setAttribute("open", "");
244
+ $("#advancedToolsNav")?.classList.toggle("active", advancedToolsView);
228
245
  $("#mobileMoreBtn")?.classList.toggle("active", advancedView);
229
246
  if (advancedView) $("#mobileMoreBtn")?.setAttribute("aria-current", "page");
230
247
  else $("#mobileMoreBtn")?.removeAttribute("aria-current");
231
248
  }
232
249
  function selectView(view, options = {}) {
233
250
  state.view = view;
251
+ state.managementFilter = view === "waiting" ? (options.managementFilter || "all") : "all";
234
252
  state.visibleLimit = 30;
235
253
  if (view === "active" || view === "waiting") markGuideStep(view);
236
254
  syncViewChrome();
237
255
  context.renderSessions(options.motionKind || "view");
238
- $("#mobileToolsMenu")?.classList.add("hidden");
256
+ const mobileToolsMenu = $("#mobileToolsMenu");
257
+ if (mobileToolsMenu && !mobileToolsMenu.classList.contains("hidden")) {
258
+ setDialogOpenState(mobileToolsMenu, false);
259
+ mobileToolsMenu.classList.add("hidden");
260
+ }
239
261
  $("#mobileMoreBtn")?.setAttribute("aria-expanded", "false");
240
262
  document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
241
263
  if (options.focusMain) $("#mainContent")?.focus({ preventScroll: true });
264
+ const resultCount = ["all", "active", "waiting"].includes(view) && typeof context.filteredSessions === "function"
265
+ ? context.filteredSessions().length
266
+ : null;
267
+ announce(resultCount == null
268
+ ? t("navigation.view_changed", { view: VIEW_TITLES[view] || view })
269
+ : t("navigation.view_results", { view: VIEW_TITLES[view] || view, count: resultCount }));
242
270
  }
243
271
  function currentDialog() {
272
+ if (!$("#mobileToolsMenu")?.classList.contains("hidden")) return $("#mobileToolsMenu");
273
+ if (!$("#quickPaletteModal")?.classList.contains("hidden")) return $("#quickPaletteModal");
274
+ if (!$("#shortcutHelpModal")?.classList.contains("hidden")) return $("#shortcutHelpModal");
244
275
  if (!$("#runModal").classList.contains("hidden")) return $("#runModal");
245
276
  if (!$("#tmuxCreateModal").classList.contains("hidden")) return $("#tmuxCreateModal");
246
277
  if ($("#detailDrawer").classList.contains("open")) return $("#detailDrawer");
@@ -272,14 +303,44 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
272
303
  }
273
304
  }
274
305
  function rememberDialogTrigger() {
306
+ motionState.dialogGeneration += 1;
275
307
  motionState.activeDialogTrigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
276
308
  }
277
- function restoreDialogTrigger() {
309
+ function restoreDialogTrigger(expectedGeneration = null) {
310
+ if (expectedGeneration != null && expectedGeneration !== motionState.dialogGeneration) return false;
278
311
  const trigger = motionState.activeDialogTrigger;
279
312
  motionState.activeDialogTrigger = null;
280
- if (trigger && trigger.isConnected) trigger.focus();
313
+ if (trigger && trigger.isConnected) trigger.focus({ preventScroll: true });
314
+ return true;
315
+ }
316
+ function setDialogOpenState(dialog, open) {
317
+ if (!dialog) return;
318
+ const shell = $("#appShell");
319
+ if (open) {
320
+ dialog.removeAttribute("inert");
321
+ dialog.setAttribute("aria-hidden", "false");
322
+ shell?.setAttribute("inert", "");
323
+ document.body.classList.add("dialog-open");
324
+ return;
325
+ }
326
+ dialog.setAttribute("inert", "");
327
+ dialog.setAttribute("aria-hidden", "true");
328
+ const anotherDialog = [$("#mobileToolsMenu"), $("#runModal"), $("#tmuxCreateModal"), $("#detailDrawer"), $("#quickPaletteModal"), $("#shortcutHelpModal")]
329
+ .some((item) => item && item !== dialog && !item.classList.contains("hidden") && (item.classList.contains("open") || item.matches(".modal-backdrop") || item.id === "mobileToolsMenu"));
330
+ if (!anotherDialog) {
331
+ shell?.removeAttribute("inert");
332
+ document.body.classList.remove("dialog-open");
333
+ }
334
+ }
335
+ function announce(message) {
336
+ const region = $("#globalStatus");
337
+ if (!region) return;
338
+ region.textContent = "";
339
+ requestAnimationFrame(() => {
340
+ region.textContent = String(message || "");
341
+ });
281
342
  }
282
- window.LoadToAgentA11y = { rememberDialogTrigger, restoreDialogTrigger };
343
+ window.LoadToAgentA11y = { rememberDialogTrigger, restoreDialogTrigger, setDialogOpenState, announce };
283
344
  function readablePreview(value, maxCharacters = 120) {
284
345
  const full = String(value == null ? "" : value)
285
346
  .replace(/\s+/g, " ")
@@ -291,19 +352,22 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
291
352
  return { full, text: `${sample.slice(0, cut).trimEnd()}…`, truncated: true };
292
353
  }
293
354
  function memoryCategoryLabel(value) {
294
- const labels = { insight: "인사이트", convention: "작업 규칙", failure: "실패 기록", decision: "결정", pattern: "반복 패턴" };
295
- return labels[String(value || "").toLowerCase()] || String(value || "기록");
355
+ const keys = {
356
+ insight: "content.memory.insight", convention: "content.memory.convention", failure: "content.memory.failure",
357
+ decision: "content.memory.decision", pattern: "content.memory.pattern",
358
+ };
359
+ return keys[String(value || "").toLowerCase()] ? t(keys[String(value || "").toLowerCase()]) : String(value || t("content.memory.record"));
296
360
  }
297
361
  function jsonValueHtml(value, depth = 0) {
298
- if (value == null) return '<span class="json-empty">없음</span>';
299
- if (typeof value === "boolean") return `<span class="json-primitive">${value ? "" : "아니요"}</span>`;
362
+ if (value == null) return `<span class="json-empty">${esc(t("content.none"))}</span>`;
363
+ if (typeof value === "boolean") return `<span class="json-primitive">${esc(t(value ? "content.yes" : "content.no"))}</span>`;
300
364
  if (typeof value === "number") return `<span class="json-primitive">${esc(value.toLocaleString(uiLocale()))}</span>`;
301
365
  if (typeof value === "string") return `<span class="json-string">${esc(value)}</span>`;
302
366
  if (depth >= 4) return `<span class="json-string">${esc(JSON.stringify(value))}</span>`;
303
367
  if (Array.isArray(value)) {
304
368
  const shown = value.slice(0, 40);
305
369
  const visibleItems = shown.map((item) => `<li>${jsonValueHtml(item, depth + 1)}</li>`).join("");
306
- const moreItems = value.length > shown.length ? `<li class="json-more">외 ${value.length - shown.length}개</li>` : "";
370
+ const moreItems = value.length > shown.length ? `<li class="json-more">${esc(t("content.more_items", { count: value.length - shown.length }))}</li>` : "";
307
371
  return `<ol class="json-array">${visibleItems}${moreItems}</ol>`;
308
372
  }
309
373
  const entries = Object.entries(value).slice(0, 40);
@@ -312,8 +376,8 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
312
376
  function memoryCandidatesHtml(items) {
313
377
  return `<div class="memory-candidates">
314
378
  <div class="structured-heading">
315
- <b>저장할 작업 기억</b>
316
- <span>${items.length}개 항목</span>
379
+ <b>${esc(t("content.memory.to_save"))}</b>
380
+ <span>${esc(t("common.items", { count: items.length }))}</span>
317
381
  </div>${items
318
382
  .map(
319
383
  (item, index) => `<article class="memory-candidate">
@@ -322,7 +386,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
322
386
  <span class="memory-target">${esc(item.target || "MEMORY")}</span>
323
387
  <span class="memory-category">${esc(memoryCategoryLabel(item.category))}</span>
324
388
  </header>
325
- <p>${esc(item.content || "내용 없음")}</p>
389
+ <p>${esc(item.content || t("content.empty"))}</p>
326
390
  </article>`,
327
391
  )
328
392
  .join("")}</div>`;
@@ -384,7 +448,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
384
448
  if (fence) output.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
385
449
  return `<div class="chat-content markdown">${output.join("")}</div>`;
386
450
  }
387
- function roadmapHtml(value) {
451
+ function roadmapHtml(value, disclosureId = "") {
388
452
  const text = String(value || "").trim();
389
453
  const lines = text
390
454
  .replace(/\r\n/g, "\n")
@@ -395,17 +459,18 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
395
459
  const steps = lines.filter((line) => /^(?:[-*]\s+|\d+[.)]\s+|#{2,3}\s+(?!로드맵|roadmap))/i.test(line));
396
460
  if (!hasRoadmapSignal || (text.length < 420 && steps.length < 6)) return "";
397
461
  const heading = lines.find((line) => /^#{1,3}\s+/.test(line));
398
- const title = readablePreview((heading || "작업 로드맵").replace(/^#{1,3}\s+/, ""), 72).text;
462
+ const title = readablePreview((heading || t("content.roadmap.title")).replace(/^#{1,3}\s+/, ""), 72).text;
399
463
  const previewSteps = (steps.length ? steps : lines.filter((line) => !/^#{1,3}\s+/.test(line)))
400
464
  .slice(0, 3)
401
465
  .map((line) => readablePreview(line.replace(/^(?:[-*]\s+|\d+[.)]\s+|#{2,3}\s+)/, "").replace(/\*\*/g, ""), 92).text);
402
- const countLabel = steps.length ? `${steps.length} 단계` : "긴 계획";
403
- return `<details class="chat-roadmap" data-roadmap-collapsed="true">
466
+ const countLabel = steps.length ? t("content.roadmap.steps", { count: steps.length }) : t("content.roadmap.long_plan");
467
+ const disclosureKey = `roadmap:${disclosureId || `${text.length}:${title}`}`;
468
+ return `<details class="chat-roadmap" data-roadmap-collapsed="true" data-disclosure-key="${esc(disclosureKey)}">
404
469
  <summary>
405
470
  <span class="chat-roadmap-mark" aria-hidden="true">MAP</span>
406
471
  <span>
407
472
  <b>${esc(title)}</b>
408
- <small>${esc(countLabel)} · 접어서 표시</small>
473
+ <small>${esc(t("content.roadmap.collapsed", { count: countLabel }))}</small>
409
474
  </span>
410
475
  <i aria-hidden="true">↓</i>
411
476
  </summary>
@@ -413,9 +478,9 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
413
478
  <div class="chat-roadmap-full">${markdownHtml(text)}</div>
414
479
  </details>`;
415
480
  }
416
- function messageContentHtml(message) {
481
+ function messageContentHtml(message, ownerId = "") {
417
482
  const text = String((message && message.text) || "").trim();
418
- if (!text) return '<div class="chat-content empty">표시할 내용이 없습니다.</div>';
483
+ if (!text) return `<div class="chat-content empty">${esc(t("content.no_displayable_content"))}</div>`;
419
484
  if (/^[\[{]/.test(text) && /[\]}]$/.test(text)) {
420
485
  try {
421
486
  const parsed = JSON.parse(text);
@@ -426,14 +491,15 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
426
491
  }
427
492
  return `<div class="structured-json">
428
493
  <div class="structured-heading">
429
- <b>구조화된 데이터</b>
430
- <span>${Array.isArray(parsed) ? `${parsed.length} 항목` : "JSON"}</span>
494
+ <b>${esc(t("content.structured_data"))}</b>
495
+ <span>${Array.isArray(parsed) ? esc(t("common.items", { count: parsed.length })) : "JSON"}</span>
431
496
  </div>${jsonValueHtml(parsed)}</div>`;
432
497
  } catch (_plainChatMessage) {
433
498
  // A normal chat message may begin with JSON punctuation without being JSON.
434
499
  }
435
500
  }
436
- const roadmap = message && message.role === "assistant" ? roadmapHtml(text) : "";
501
+ const messageId = message && (message.id || message.timestamp) || text.length;
502
+ const roadmap = message && message.role === "assistant" ? roadmapHtml(text, `${ownerId}:${messageId}`) : "";
437
503
  if (roadmap) return roadmap;
438
504
  return markdownHtml(text);
439
505
  }
@@ -479,7 +545,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
479
545
  return labels[String(value || "").toLowerCase()] || String(value || window.LoadToAgentI18n.t("ui.assistance"));
480
546
  }
481
547
  function statusClass(status) {
482
- return ["running", "waiting", "completed", "failed", "cancelled"].includes(status) ? status : "";
548
+ return ["running", "paused", "waiting", "completed", "failed", "cancelled"].includes(status) ? status : "";
483
549
  }
484
550
  function currentActivity(session) {
485
551
  const items = session.lifecycle || [];
@@ -487,15 +553,22 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
487
553
  const last = running || items[items.length - 1];
488
554
  if (last) {
489
555
  return {
490
- title: last.label || window.LoadToAgentI18n.t("ui.activity"), detail: last.detail || session.statusDetail || "", type: last.type || "activity",
556
+ title: observedText(last.label || t("ui.activity")), detail: observedText(last.detail || session.statusDetail || ""), type: last.type || "activity",
491
557
  };
492
558
  }
493
559
  const message = (session.messages || [])[session.messages.length - 1];
494
- return { title: session.statusDetail || window.LoadToAgentI18n.t("ui.temporarily_idle"), detail: (message && message.text) || "", type: "activity" };
560
+ return { title: observedText(session.statusDetail || t("ui.temporarily_idle")), detail: observedText((message && message.text) || ""), type: "activity" };
495
561
  }
496
562
  function isLiveSession(session) {
497
563
  return session && (session.status === "running" || session.status === "starting");
498
564
  }
565
+ function isRuntimeLoopSession(session) {
566
+ if (!session || session.parentId || !isLiveSession(session)) return false;
567
+ if (session.loop === true || (session.loop && typeof session.loop === "object")) return true;
568
+ const ids = new Set([String(session.id || ""), String(session.externalId || "")].filter(Boolean));
569
+ return (state.snapshot?.automations || []).some((automation) =>
570
+ automation.enabled && automation.targetThreadId && ids.has(String(automation.targetThreadId)));
571
+ }
499
572
  function subagentWorkState(session) {
500
573
  if (isLiveSession(session)) return "working";
501
574
  if (session && session.status === "failed") return "attention";
@@ -514,13 +587,13 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
514
587
  try {
515
588
  const parsed = JSON.parse(text);
516
589
  if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") return text;
517
- if (parsed.cell_id) return `실행 중인 작업 결과를 기다리는 중${parsed.yield_time_ms ? ` · 최대 ${Math.round(Number(parsed.yield_time_ms) / 1000)}초` : ""}`;
518
- if (parsed.command) return `명령 실행 · ${String(parsed.command).replace(/\s+/g, " ").slice(0, 180)}`;
519
- if (parsed.path || parsed.file_path) return `파일 확인 · ${parsed.path || parsed.file_path}`;
520
- if (parsed.prompt) return `AI에게 맡긴 · ${String(parsed.prompt).replace(/\s+/g, " ").slice(0, 180)}`;
590
+ if (parsed.cell_id) return t("activity.waiting_for_result", { wait: parsed.yield_time_ms ? t("activity.max_seconds", { count: Math.round(Number(parsed.yield_time_ms) / 1000) }) : "" });
591
+ if (parsed.command) return t("activity.command", { value: String(parsed.command).replace(/\s+/g, " ").slice(0, 180) });
592
+ if (parsed.path || parsed.file_path) return t("activity.file", { value: parsed.path || parsed.file_path });
593
+ if (parsed.prompt) return t("activity.delegated", { value: String(parsed.prompt).replace(/\s+/g, " ").slice(0, 180) });
521
594
  const summary = Object.entries(parsed)
522
595
  .slice(0, 3)
523
- .map(([key, item]) => `${key}: ${typeof item === "object" ? "구조화 데이터" : item}`)
596
+ .map(([key, item]) => `${key}: ${typeof item === "object" ? t("activity.structured_data") : item}`)
524
597
  .join(" · ");
525
598
  return summary || text;
526
599
  } catch (_malformedCommandPreview) {
@@ -531,16 +604,16 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
531
604
  function latestWorkCopy(session) {
532
605
  const delegation = session.delegation || {};
533
606
  const completedResult = delegation.result || session.result;
534
- if (session.status === "completed" && completedResult) return `완료 결과 · ${completedResult}`;
535
- if (session.status === "completed") return "담당 작업을 완료하고 메인 AI에 결과를 반환했습니다.";
607
+ if (session.status === "completed" && completedResult) return t("activity.completed_result", { value: completedResult });
608
+ if (session.status === "completed") return t("activity.completed_returned");
536
609
  const activity = currentActivity(session);
537
610
  if (activity.detail) return readableActivityDetail(activity.detail);
538
611
  const messages = session.messages || [];
539
612
  const assistant = [...messages].reverse().find((item) => item.role === "assistant" && item.text);
540
613
  if (assistant) return assistant.text;
541
614
  const tool = [...messages].reverse().find((item) => item.role === "tool");
542
- if (tool) return `${tool.title || "도구"} 실행 · ${tool.text || "결과를 기다리는 중"}`;
543
- return activity.title || session.statusDetail || "다음 할 일을 기다리는 중";
615
+ if (tool) return t("activity.tool_execution", { tool: tool.title || t("session.tool"), value: tool.text || t("activity.waiting_for_result_short") });
616
+ return activity.title || session.statusDetail || t("activity.waiting_for_next");
544
617
  }
545
618
  function statusIcon(type) {
546
619
  if (/tool/.test(type)) return "⌘";
@@ -557,10 +630,13 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
557
630
  uiLocale,
558
631
  providerLabel,
559
632
  reportRecoverableError,
633
+ observedText,
560
634
  PROJECTLESS_WORKSPACE,
561
635
  state,
562
636
  motionPreference,
563
637
  motionState,
638
+ rememberDisclosureStates,
639
+ restoreDisclosureStates,
564
640
  STATUS,
565
641
  VIEW_TITLES,
566
642
  VIEW_META,
@@ -581,6 +657,8 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
581
657
  trapDialogFocus,
582
658
  rememberDialogTrigger,
583
659
  restoreDialogTrigger,
660
+ setDialogOpenState,
661
+ announce,
584
662
  readablePreview,
585
663
  memoryCategoryLabel,
586
664
  jsonValueHtml,
@@ -599,6 +677,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
599
677
  statusClass,
600
678
  currentActivity,
601
679
  isLiveSession,
680
+ isRuntimeLoopSession,
602
681
  subagentWorkState,
603
682
  subagentWorkLabel,
604
683
  readableActivityDetail,