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
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,17 +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(),
31
36
  expandedTmuxSubagents: new Set(),
37
+ selectedRuntimeLoopId: null,
32
38
  tmuxFocus: null,
33
39
  agentCommandDrafts: new Map(),
34
40
  agentCommandTargets: new Map(),
35
41
  agentCommandSending: new Set(),
36
42
  stopRequests: new Set(),
43
+ runControlRequests: new Set(),
44
+ managementFilter: "all",
37
45
  detailErrors: new Map(),
46
+ disclosureStates: new Map(),
38
47
  guideCompleted: new Set(),
39
- guideExpanded: true,
40
- 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 },
41
50
  };
42
51
  Object.defineProperty(state, "provider", {
43
52
  enumerable: true,
@@ -52,9 +61,24 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
52
61
  });
53
62
  const motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");
54
63
  const motionState = {
55
- ready: false, modalTimer: 0, toastTimer: 0, drawerTimer: 0, drawerContentTimer: 0,
56
- drawerRenderKey: "", drawerTab: "", activeDialogTrigger: null,
64
+ ready: false, modalTimer: 0, modalFocusTimer: 0, toastTimer: 0, drawerTimer: 0, drawerContentTimer: 0,
65
+ drawerRenderKey: "", drawerTab: "", activeDialogTrigger: null, dialogGeneration: 0,
57
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
+ }
58
82
  document.documentElement.dataset.motion = motionPreference.matches ? "reduced" : "full";
59
83
  motionPreference.addEventListener("change", (event) => {
60
84
  document.documentElement.dataset.motion = event.matches ? "reduced" : "full";
@@ -128,54 +152,43 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
128
152
  requestAnimationFrame(() => section.classList.add("motion-section-in"));
129
153
  });
130
154
  }
131
- const STATUS = {
132
- starting: window.LoadToAgentI18n.t("ui.preparing"), running: window.LoadToAgentI18n.t("ui.working"),
133
- waiting: window.LoadToAgentI18n.t("app.nav.needs_review"), idle: window.LoadToAgentI18n.t("ui.idle"),
134
- completed: window.LoadToAgentI18n.t("ui.completed"), failed: window.LoadToAgentI18n.t("ui.problem"),
135
- cancelled: window.LoadToAgentI18n.t("ui.stopped"),
136
- };
137
- const VIEW_TITLES = {
138
- all: window.LoadToAgentI18n.t("ui.recent_conversations_and_tasks"), active: window.LoadToAgentI18n.t("ui.active_tasks"),
139
- waiting: window.LoadToAgentI18n.t("ui.tasks_needing_review"), terminal: window.LoadToAgentI18n.t("app.nav.session_terminal"),
140
- 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: "ui.waiting_for_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"],
141
174
  };
142
- const VIEW_META = {
143
- all: {
144
- eyebrow: window.LoadToAgentI18n.t("ui.ai_work_overview"), title: window.LoadToAgentI18n.t("ui.see_all_ai_work_at_a_glance"),
145
- 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;
146
179
  },
147
- active: {
148
- eyebrow: window.LoadToAgentI18n.t("ui.active_now"), title: window.LoadToAgentI18n.t("ui.see_which_ai_is_working_now"),
149
- subtitle: window.LoadToAgentI18n.t("ui.see_what_is_being_handled_then_open_a_task_for"),
150
- },
151
- waiting: {
152
- eyebrow: window.LoadToAgentI18n.t("ui.your_turn"), title: window.LoadToAgentI18n.t("ui.handle_items_that_need_your_review_first"),
153
- subtitle: window.LoadToAgentI18n.t("ui.only_tasks_waiting_for_your_response_or_choice_are_shown"),
154
- },
155
- terminal: {
156
- eyebrow: window.LoadToAgentI18n.t("ui.continue_an_existing_conversation"), title: window.LoadToAgentI18n.t("ui.continue_ai_sessions_in_the_terminal"),
157
- subtitle: window.LoadToAgentI18n.t("ui.continue_the_same_task_with_its_previous_conversation_beside_the"),
158
- },
159
- tmux: {
160
- eyebrow: window.LoadToAgentI18n.t("ui.advanced_work_tools"), title: window.LoadToAgentI18n.t("ui.manage_multi_terminal_work_in_one_place"),
161
- subtitle: window.LoadToAgentI18n.t("ui.this_view_is_only_for_existing_tmux_workflows_home_and"),
162
- },
163
- settings: {
164
- eyebrow: window.LoadToAgentI18n.t("ui.application_management"), title: window.LoadToAgentI18n.t("ui.check_versions_and_updates"),
165
- subtitle: window.LoadToAgentI18n.t("ui.compare_the_installed_and_latest_stable_versions_then_download_a"),
166
- },
167
- };
180
+ });
168
181
  const GUIDE_STORAGE_KEY = "loadtoagent:start-guide:v1";
169
182
  const GUIDE_STEPS = ["create", "active", "waiting", "detail"];
170
183
  function loadGuideState() {
171
184
  try {
172
185
  const saved = JSON.parse(localStorage.getItem(GUIDE_STORAGE_KEY) || "{}");
173
186
  state.guideCompleted = new Set((saved.completed || []).filter((step) => GUIDE_STEPS.includes(step)));
174
- state.guideExpanded = saved.expanded !== false;
187
+ state.guideExpanded = saved.expanded === true;
175
188
  } catch (error) {
176
189
  reportRecoverableError("guide-state-load", error);
177
190
  state.guideCompleted = new Set();
178
- state.guideExpanded = true;
191
+ state.guideExpanded = false;
179
192
  }
180
193
  }
181
194
  function saveGuideState() {
@@ -226,22 +239,39 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
226
239
  else item.removeAttribute("aria-current");
227
240
  });
228
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);
229
245
  $("#mobileMoreBtn")?.classList.toggle("active", advancedView);
230
246
  if (advancedView) $("#mobileMoreBtn")?.setAttribute("aria-current", "page");
231
247
  else $("#mobileMoreBtn")?.removeAttribute("aria-current");
232
248
  }
233
249
  function selectView(view, options = {}) {
234
250
  state.view = view;
251
+ state.managementFilter = view === "waiting" ? (options.managementFilter || "all") : "all";
235
252
  state.visibleLimit = 30;
236
253
  if (view === "active" || view === "waiting") markGuideStep(view);
237
254
  syncViewChrome();
238
255
  context.renderSessions(options.motionKind || "view");
239
- $("#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
+ }
240
261
  $("#mobileMoreBtn")?.setAttribute("aria-expanded", "false");
241
262
  document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
242
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 }));
243
270
  }
244
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");
245
275
  if (!$("#runModal").classList.contains("hidden")) return $("#runModal");
246
276
  if (!$("#tmuxCreateModal").classList.contains("hidden")) return $("#tmuxCreateModal");
247
277
  if ($("#detailDrawer").classList.contains("open")) return $("#detailDrawer");
@@ -273,14 +303,44 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
273
303
  }
274
304
  }
275
305
  function rememberDialogTrigger() {
306
+ motionState.dialogGeneration += 1;
276
307
  motionState.activeDialogTrigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
277
308
  }
278
- function restoreDialogTrigger() {
309
+ function restoreDialogTrigger(expectedGeneration = null) {
310
+ if (expectedGeneration != null && expectedGeneration !== motionState.dialogGeneration) return false;
279
311
  const trigger = motionState.activeDialogTrigger;
280
312
  motionState.activeDialogTrigger = null;
281
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
+ });
282
342
  }
283
- window.LoadToAgentA11y = { rememberDialogTrigger, restoreDialogTrigger };
343
+ window.LoadToAgentA11y = { rememberDialogTrigger, restoreDialogTrigger, setDialogOpenState, announce };
284
344
  function readablePreview(value, maxCharacters = 120) {
285
345
  const full = String(value == null ? "" : value)
286
346
  .replace(/\s+/g, " ")
@@ -292,19 +352,22 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
292
352
  return { full, text: `${sample.slice(0, cut).trimEnd()}…`, truncated: true };
293
353
  }
294
354
  function memoryCategoryLabel(value) {
295
- const labels = { insight: "인사이트", convention: "작업 규칙", failure: "실패 기록", decision: "결정", pattern: "반복 패턴" };
296
- 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"));
297
360
  }
298
361
  function jsonValueHtml(value, depth = 0) {
299
- if (value == null) return '<span class="json-empty">없음</span>';
300
- 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>`;
301
364
  if (typeof value === "number") return `<span class="json-primitive">${esc(value.toLocaleString(uiLocale()))}</span>`;
302
365
  if (typeof value === "string") return `<span class="json-string">${esc(value)}</span>`;
303
366
  if (depth >= 4) return `<span class="json-string">${esc(JSON.stringify(value))}</span>`;
304
367
  if (Array.isArray(value)) {
305
368
  const shown = value.slice(0, 40);
306
369
  const visibleItems = shown.map((item) => `<li>${jsonValueHtml(item, depth + 1)}</li>`).join("");
307
- 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>` : "";
308
371
  return `<ol class="json-array">${visibleItems}${moreItems}</ol>`;
309
372
  }
310
373
  const entries = Object.entries(value).slice(0, 40);
@@ -313,8 +376,8 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
313
376
  function memoryCandidatesHtml(items) {
314
377
  return `<div class="memory-candidates">
315
378
  <div class="structured-heading">
316
- <b>저장할 작업 기억</b>
317
- <span>${items.length}개 항목</span>
379
+ <b>${esc(t("content.memory.to_save"))}</b>
380
+ <span>${esc(t("common.items", { count: items.length }))}</span>
318
381
  </div>${items
319
382
  .map(
320
383
  (item, index) => `<article class="memory-candidate">
@@ -323,7 +386,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
323
386
  <span class="memory-target">${esc(item.target || "MEMORY")}</span>
324
387
  <span class="memory-category">${esc(memoryCategoryLabel(item.category))}</span>
325
388
  </header>
326
- <p>${esc(item.content || "내용 없음")}</p>
389
+ <p>${esc(item.content || t("content.empty"))}</p>
327
390
  </article>`,
328
391
  )
329
392
  .join("")}</div>`;
@@ -385,7 +448,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
385
448
  if (fence) output.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
386
449
  return `<div class="chat-content markdown">${output.join("")}</div>`;
387
450
  }
388
- function roadmapHtml(value) {
451
+ function roadmapHtml(value, disclosureId = "") {
389
452
  const text = String(value || "").trim();
390
453
  const lines = text
391
454
  .replace(/\r\n/g, "\n")
@@ -396,17 +459,18 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
396
459
  const steps = lines.filter((line) => /^(?:[-*]\s+|\d+[.)]\s+|#{2,3}\s+(?!로드맵|roadmap))/i.test(line));
397
460
  if (!hasRoadmapSignal || (text.length < 420 && steps.length < 6)) return "";
398
461
  const heading = lines.find((line) => /^#{1,3}\s+/.test(line));
399
- 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;
400
463
  const previewSteps = (steps.length ? steps : lines.filter((line) => !/^#{1,3}\s+/.test(line)))
401
464
  .slice(0, 3)
402
465
  .map((line) => readablePreview(line.replace(/^(?:[-*]\s+|\d+[.)]\s+|#{2,3}\s+)/, "").replace(/\*\*/g, ""), 92).text);
403
- const countLabel = steps.length ? `${steps.length} 단계` : "긴 계획";
404
- 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)}">
405
469
  <summary>
406
470
  <span class="chat-roadmap-mark" aria-hidden="true">MAP</span>
407
471
  <span>
408
472
  <b>${esc(title)}</b>
409
- <small>${esc(countLabel)} · 접어서 표시</small>
473
+ <small>${esc(t("content.roadmap.collapsed", { count: countLabel }))}</small>
410
474
  </span>
411
475
  <i aria-hidden="true">↓</i>
412
476
  </summary>
@@ -414,9 +478,9 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
414
478
  <div class="chat-roadmap-full">${markdownHtml(text)}</div>
415
479
  </details>`;
416
480
  }
417
- function messageContentHtml(message) {
481
+ function messageContentHtml(message, ownerId = "") {
418
482
  const text = String((message && message.text) || "").trim();
419
- 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>`;
420
484
  if (/^[\[{]/.test(text) && /[\]}]$/.test(text)) {
421
485
  try {
422
486
  const parsed = JSON.parse(text);
@@ -427,14 +491,15 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
427
491
  }
428
492
  return `<div class="structured-json">
429
493
  <div class="structured-heading">
430
- <b>구조화된 데이터</b>
431
- <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>
432
496
  </div>${jsonValueHtml(parsed)}</div>`;
433
497
  } catch (_plainChatMessage) {
434
498
  // A normal chat message may begin with JSON punctuation without being JSON.
435
499
  }
436
500
  }
437
- 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}`) : "";
438
503
  if (roadmap) return roadmap;
439
504
  return markdownHtml(text);
440
505
  }
@@ -480,7 +545,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
480
545
  return labels[String(value || "").toLowerCase()] || String(value || window.LoadToAgentI18n.t("ui.assistance"));
481
546
  }
482
547
  function statusClass(status) {
483
- return ["running", "waiting", "completed", "failed", "cancelled"].includes(status) ? status : "";
548
+ return ["running", "paused", "waiting", "completed", "failed", "cancelled"].includes(status) ? status : "";
484
549
  }
485
550
  function currentActivity(session) {
486
551
  const items = session.lifecycle || [];
@@ -488,15 +553,22 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
488
553
  const last = running || items[items.length - 1];
489
554
  if (last) {
490
555
  return {
491
- 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",
492
557
  };
493
558
  }
494
559
  const message = (session.messages || [])[session.messages.length - 1];
495
- 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" };
496
561
  }
497
562
  function isLiveSession(session) {
498
563
  return session && (session.status === "running" || session.status === "starting");
499
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
+ }
500
572
  function subagentWorkState(session) {
501
573
  if (isLiveSession(session)) return "working";
502
574
  if (session && session.status === "failed") return "attention";
@@ -515,13 +587,13 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
515
587
  try {
516
588
  const parsed = JSON.parse(text);
517
589
  if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") return text;
518
- if (parsed.cell_id) return `실행 중인 작업 결과를 기다리는 중${parsed.yield_time_ms ? ` · 최대 ${Math.round(Number(parsed.yield_time_ms) / 1000)}초` : ""}`;
519
- if (parsed.command) return `명령 실행 · ${String(parsed.command).replace(/\s+/g, " ").slice(0, 180)}`;
520
- if (parsed.path || parsed.file_path) return `파일 확인 · ${parsed.path || parsed.file_path}`;
521
- 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) });
522
594
  const summary = Object.entries(parsed)
523
595
  .slice(0, 3)
524
- .map(([key, item]) => `${key}: ${typeof item === "object" ? "구조화 데이터" : item}`)
596
+ .map(([key, item]) => `${key}: ${typeof item === "object" ? t("activity.structured_data") : item}`)
525
597
  .join(" · ");
526
598
  return summary || text;
527
599
  } catch (_malformedCommandPreview) {
@@ -532,16 +604,16 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
532
604
  function latestWorkCopy(session) {
533
605
  const delegation = session.delegation || {};
534
606
  const completedResult = delegation.result || session.result;
535
- if (session.status === "completed" && completedResult) return `완료 결과 · ${completedResult}`;
536
- 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");
537
609
  const activity = currentActivity(session);
538
610
  if (activity.detail) return readableActivityDetail(activity.detail);
539
611
  const messages = session.messages || [];
540
612
  const assistant = [...messages].reverse().find((item) => item.role === "assistant" && item.text);
541
613
  if (assistant) return assistant.text;
542
614
  const tool = [...messages].reverse().find((item) => item.role === "tool");
543
- if (tool) return `${tool.title || "도구"} 실행 · ${tool.text || "결과를 기다리는 중"}`;
544
- 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");
545
617
  }
546
618
  function statusIcon(type) {
547
619
  if (/tool/.test(type)) return "⌘";
@@ -558,10 +630,13 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
558
630
  uiLocale,
559
631
  providerLabel,
560
632
  reportRecoverableError,
633
+ observedText,
561
634
  PROJECTLESS_WORKSPACE,
562
635
  state,
563
636
  motionPreference,
564
637
  motionState,
638
+ rememberDisclosureStates,
639
+ restoreDisclosureStates,
565
640
  STATUS,
566
641
  VIEW_TITLES,
567
642
  VIEW_META,
@@ -582,6 +657,8 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
582
657
  trapDialogFocus,
583
658
  rememberDialogTrigger,
584
659
  restoreDialogTrigger,
660
+ setDialogOpenState,
661
+ announce,
585
662
  readablePreview,
586
663
  memoryCategoryLabel,
587
664
  jsonValueHtml,
@@ -600,6 +677,7 @@ window.LoadToAgentAppFactories.createCore = function createCore(context = {}) {
600
677
  statusClass,
601
678
  currentActivity,
602
679
  isLiveSession,
680
+ isRuntimeLoopSession,
603
681
  subagentWorkState,
604
682
  subagentWorkLabel,
605
683
  readableActivityDetail,