loadtoagent 1.1.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 (83) 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 +135 -38
  9. package/package.json +13 -4
  10. package/preload.js +6 -0
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -19
  13. package/renderer/app-dashboard.js +190 -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 +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 +52 -44
  32. package/renderer/app.js +153 -75
  33. package/renderer/i18n-messages.js +833 -16
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +145 -59
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +6 -0
  38. package/renderer/styles-cards.css +26 -0
  39. package/renderer/styles-components.css +86 -9
  40. package/renderer/styles-management.css +355 -0
  41. package/renderer/styles-overlays.css +8 -2
  42. package/renderer/styles-product.css +24 -16
  43. package/renderer/styles-quality.css +312 -0
  44. package/renderer/styles-readability.css +862 -0
  45. package/renderer/styles-responsive-product.css +84 -12
  46. package/renderer/styles-responsive-runtime.css +22 -14
  47. package/renderer/styles-responsive-shell.css +44 -48
  48. package/renderer/styles-responsive-workflows.css +37 -3
  49. package/renderer/styles-run-composer.css +5 -0
  50. package/renderer/styles-runtime-overview.css +285 -0
  51. package/renderer/styles-settings.css +160 -0
  52. package/renderer/styles-terminal.css +339 -2
  53. package/renderer/styles-workflow-map.css +362 -15
  54. package/renderer/styles-workflows.css +69 -2
  55. package/renderer/styles.css +27 -12
  56. package/renderer/terminal-agent.js +17 -13
  57. package/renderer/terminal-events.js +233 -40
  58. package/renderer/terminal-workbench.js +165 -77
  59. package/renderer/terminal.js +341 -34
  60. package/src/agentMonitor/claudeParser.js +96 -7
  61. package/src/agentMonitor/codexParser.js +59 -4
  62. package/src/agentMonitor/executionActivity.js +282 -0
  63. package/src/agentMonitor/genericParser.js +56 -13
  64. package/src/agentMonitor/hierarchy.js +17 -0
  65. package/src/agentMonitor/responseIntent.js +51 -0
  66. package/src/agentMonitor.js +21 -3
  67. package/src/agentRunner.js +66 -1
  68. package/src/attentionNotifier.js +14 -10
  69. package/src/automationMonitor.js +258 -0
  70. package/src/contracts.js +10 -1
  71. package/src/ipc/registerAgentIpc.js +9 -3
  72. package/src/ipc/registerAppIpc.js +4 -1
  73. package/src/ipc/registerTerminalIpc.js +12 -6
  74. package/src/macUpdateHelper.js +210 -0
  75. package/src/monitorWorker.js +65 -6
  76. package/src/platformPath.js +80 -0
  77. package/src/processMonitor.js +80 -22
  78. package/src/providerVisibilityStore.js +45 -0
  79. package/src/sessionIntelligence.js +247 -0
  80. package/src/terminalHost.js +381 -0
  81. package/src/terminalHostDaemon.js +60 -0
  82. package/src/terminalManager.js +158 -3
  83. package/src/updateInstaller.js +175 -0
@@ -3,6 +3,7 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRenderer(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
8
  $,
8
9
  esc,
@@ -27,17 +28,25 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
27
28
  statusIcon,
28
29
  renderProviderRail,
29
30
  isProjectlessSession,
31
+ sessionOriginPath,
30
32
  sessionWorkspaceLabel,
31
33
  renderWorkspaces,
32
34
  renderGlobalStats,
33
35
  renderUpdateSettings,
34
36
  renderProviderOverview,
35
37
  renderProviderFilter,
38
+ renderRuntimeOverview,
39
+ renderProviderVisibilitySettings = () => {},
40
+ visibleSnapshot = () => state.snapshot,
36
41
  filteredSessions,
37
42
  graphFilteredSessions,
38
43
  executionModeBadge,
39
44
  renderAgentMap,
40
45
  renderTmuxMap,
46
+ renderAttentionInbox,
47
+ renderOperationsOverview,
48
+ progressHtml,
49
+ healthHtml,
41
50
  } = context;
42
51
 
43
52
  function recentConversation(session) {
@@ -46,10 +55,10 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
46
55
  const assistant = [...messages].reverse().find((message) => message.role === "assistant");
47
56
  const tool = [...messages].reverse().find((message) => message.role === "tool");
48
57
  const rows = [];
49
- if (user) rows.push({ label: "", text: readablePreview(user.text, 140).text, tone: "user" });
58
+ if (user) rows.push({ label: t("session.me"), text: readablePreview(user.text, 140).text, tone: "user" });
50
59
  if (assistant) rows.push({ label: providerInfo(session.provider).label, text: readablePreview(assistant.text, 140).text, tone: "assistant" });
51
- else if (tool) rows.push({ label: tool.title || "도구", text: readablePreview(tool.text, 140).text, tone: "tool" });
52
- if (!rows.length) rows.push({ label: "상태", text: session.statusDetail || "대화 이벤트를 기다리는 중입니다.", tone: "system" });
60
+ else if (tool) rows.push({ label: tool.title || t("session.tool"), text: readablePreview(tool.text, 140).text, tone: "tool" });
61
+ if (!rows.length) rows.push({ label: t("session.status"), text: window.LoadToAgentI18n.observedText(session.statusDetail || t("session.waiting_for_event")), tone: "system" });
53
62
  return rows.slice(-2);
54
63
  }
55
64
 
@@ -60,16 +69,18 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
60
69
  const activity = currentActivity(session);
61
70
  const running = session.status === "running" || session.status === "starting";
62
71
  const children = session.childIds || [];
63
- const model = session.model || "사용 모델 정보 없음";
72
+ const model = session.model || t("session.model_unknown");
64
73
  const contextPercent = Math.max(0, Math.min(100, Number(context.percent || 0)));
65
74
  const remaining = context.window ? Math.max(0, Number(context.window) - Number(context.used || 0)) : 0;
66
75
  const gaugeTone = contextPercent >= 90 ? "critical" : contextPercent >= 75 ? "warning" : "safe";
67
76
  const conversation = recentConversation(session);
68
77
  const runtime = session.runtimePresence || [];
69
78
  const titlePreview = readablePreview(session.title, 96);
70
- const activityCopy = latestWorkCopy(session) || session.statusDetail || "새 이벤트 대기";
79
+ const activityCopy = latestWorkCopy(session) || window.LoadToAgentI18n.observedText(session.statusDetail) || t("session.waiting_for_new_event");
71
80
  const activityPreview = readablePreview(activityCopy, 116);
72
81
  const accessibleId = `session-${String(session.id || "").replace(/[^a-zA-Z0-9_-]/g, "-")}`;
82
+ const originPath = sessionOriginPath(session);
83
+ const originLabel = sessionWorkspaceLabel(session);
73
84
  return `<article class="session-card ${opts.live ? "live-card" : ""} ${statusClass(session.status)} ${session.parentId ? "subagent" : ""}"
74
85
  data-session-id="${esc(session.id)}"
75
86
  data-motion-key="session:${esc(session.id)}"
@@ -88,8 +99,9 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
88
99
  <span>${esc(model)}</span>
89
100
  <i>
90
101
  </i>
91
- <span title="${esc(isProjectlessSession(session) ? window.LoadToAgentI18n.t("ui.session_not_linked_to_a_specific_project") : session.cwd)}">
92
- ${esc(sessionWorkspaceLabel(session))}
102
+ <span class="origin-project" title="${esc(isProjectlessSession(session) ? window.LoadToAgentI18n.t("ui.session_not_linked_to_a_specific_project") : originPath)}"
103
+ aria-label="${esc(t("project.origin_named", { name: originLabel }))}">
104
+ <small>${esc(t("project.origin"))}</small><b>${esc(originLabel)}</b>
93
105
  </span>${
94
106
  session.agentName
95
107
  ? `<i>
@@ -99,16 +111,18 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
99
111
  }</div>
100
112
  <div id="${accessibleId}-summary" class="now-strip ${running ? "is-live" : ""}">
101
113
  <span class="now-strip-icon">${statusIcon(activity.type)}</span>
102
- <div><b>${running ? "지금: " : ""}${esc(activity.title)}</b><span title="${esc(activityPreview.full)}">${esc(activityPreview.text)}</span></div>
114
+ <div><b>${running ? `${esc(t("session.now"))}: ` : ""}${esc(activity.title)}</b><span title="${esc(activityPreview.full)}">${esc(activityPreview.text)}</span></div>
103
115
  ${running ? '<span class="activity-wave"><i></i><i></i><i></i><i></i><i></i></span>' : ""}
104
116
  </div>
117
+ ${progressHtml(session, true)}
118
+ ${healthHtml(session, true)}
105
119
  ${
106
120
  runtime.length
107
121
  ? `<div class="runtime-strip">
108
122
  <span class="runtime-pulse">
109
123
  </span>
110
- <b>실제로 실행 중인 프로그램 ${runtime.length}개</b>
111
- <span>${esc(runtime.map((item) => item.label || `프로그램 ${item.pid}`).join(" · "))}</span>
124
+ <b>${esc(t("session.running_programs", { count: runtime.length }))}</b>
125
+ <span>${esc(runtime.map((item) => item.label || t("session.program_pid", { pid: item.pid })).join(" · "))}</span>
112
126
  </div>`
113
127
  : ""
114
128
  }
@@ -118,28 +132,28 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
118
132
  <div class="context-meter ${gaugeTone}">
119
133
  <div class="context-meter-head">
120
134
  <div>
121
- <span>AI의 기억 공간 사용량</span>
135
+ <span>${esc(t("session.context_usage"))}</span>
122
136
  <strong>${context.window ? `${fullNumber(context.used)} / ${fullNumber(context.window)}` : `${fullNumber(context.used)} / --`}</strong>
123
137
  </div>
124
138
  <b>${context.window ? `${contextPercent.toFixed(1)}%` : "--"}</b>
125
139
  </div>
126
140
  <div class="context-meter-track"><span style="width:${contextPercent}%"></span><i style="left:75%"></i><i style="left:90%"></i></div>
127
141
  <div class="context-meter-foot">
128
- <span>${context.window ? `아직 ${compact(remaining)} 토큰만큼 기억 가능` : "기억 공간 크기 정보 없음"}</span>
129
- <span>지금까지 ${compact(usage.total)} 토큰 사용</span>
142
+ <span>${esc(context.window ? t("session.context_remaining", { count: compact(remaining) }) : t("session.context_size_unknown"))}</span>
143
+ <span>${esc(t("session.tokens_used_so_far", { count: compact(usage.total) }))}</span>
130
144
  </div>
131
145
  </div>
132
146
  <div class="token-row">
133
- <div><span>받은 글</span><b>${compact(usage.input)}</b></div>
134
- <div><span>AI가 쓴 글</span><b>${compact(usage.output)}</b></div>
135
- <div><span>다시 쓴 기억</span><b>${compact(usage.cachedInput)}</b></div>
136
- <div class="total"><span>전체 사용</span><b>${compact(usage.total)}</b></div>
147
+ <div><span>${esc(t("session.input_tokens"))}</span><b>${compact(usage.input)}</b></div>
148
+ <div><span>${esc(t("session.output_tokens"))}</span><b>${compact(usage.output)}</b></div>
149
+ <div><span>${esc(t("session.cached_tokens"))}</span><b>${compact(usage.cachedInput)}</b></div>
150
+ <div class="total"><span>${esc(t("session.total_tokens"))}</span><b>${compact(usage.total)}</b></div>
137
151
  </div>
138
152
  ${
139
153
  children.length
140
154
  ? `<div class="child-row">
141
155
  <b>⑂</b>
142
- <span>서브에이전트 ${children.length}개 누적 생성</span>
156
+ <span>${esc(t("session.subagents_created", { count: children.length }))}</span>
143
157
  <span class="child-dots">${children
144
158
  .slice(0, 4)
145
159
  .map(() => "<i></i>")
@@ -148,31 +162,47 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
148
162
  : ""
149
163
  }
150
164
  <footer class="card-footer">
151
- <span class="source-tag">${esc(session.sourceLabel || "내 PC의 작업 기록")}</span>
165
+ <span class="source-tag">${esc(window.LoadToAgentI18n.observedText(session.sourceLabel || t("session.local_history")))}</span>
152
166
  <span>${esc(timeAgo(session.updatedAt))}</span>
153
167
  </footer>
154
168
  </article>`;
155
169
  }
156
170
 
157
- function renderSessions(motionKind = "refresh", deferMotion = false) {
171
+ function renderSessionsContent(motionKind = "refresh", deferMotion = false) {
158
172
  const previousLayout = deferMotion ? null : captureMotionLayout();
159
173
  syncViewChrome();
160
174
  renderGuide();
161
175
  const tmuxView = state.view === "tmux";
162
176
  const terminalView = state.view === "terminal";
163
177
  const settingsView = state.view === "settings";
178
+ const runtimeView = state.view === "runtime";
179
+ const attentionView = state.view === "waiting";
180
+ const operationsView = state.view === "all" || state.view === "active";
181
+ const focusedToolView = tmuxView || terminalView || settingsView || runtimeView;
164
182
  $("#terminalSection").classList.toggle("hidden", !terminalView);
165
183
  $("#tmuxSection").classList.toggle("hidden", !tmuxView);
166
184
  $("#settingsSection").classList.toggle("hidden", !settingsView);
167
- $("#globalStats").classList.toggle("hidden", tmuxView || terminalView || settingsView);
168
- $("#providerOverview").classList.toggle("hidden", tmuxView || terminalView || settingsView);
169
- $("#sessionSection").classList.toggle("hidden", tmuxView || terminalView || settingsView);
185
+ $("#globalStats").classList.toggle("hidden", focusedToolView);
186
+ $("#providerOverview").classList.toggle("hidden", focusedToolView || state.view !== "all");
187
+ $("#sessionSection").classList.toggle("hidden", focusedToolView || state.view === "active" || attentionView);
188
+ $("#operationsOverview").classList.toggle("hidden", !operationsView);
189
+ $("#attentionInbox").classList.toggle("hidden", !attentionView);
190
+ if (runtimeView) renderRuntimeOverview();
191
+ $("#automationOverview").classList.toggle("hidden", !runtimeView);
170
192
  const guideVisible = state.view === "all" && state.guideExpanded && !state.graphFocusId;
171
193
  $("#beginnerGuide").classList.toggle("hidden", !guideVisible);
172
194
  $("#guideBtn").setAttribute("aria-expanded", guideVisible ? "true" : "false");
173
195
  renderUpdateSettings();
196
+ if (runtimeView) {
197
+ $("#liveSection").classList.add("hidden");
198
+ if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.deactivate();
199
+ if (!deferMotion) playMotionLayout(previousLayout, motionKind);
200
+ if (motionKind === "view") animateVisibleSections();
201
+ return;
202
+ }
174
203
  if (settingsView) {
175
204
  $("#liveSection").classList.add("hidden");
205
+ renderProviderVisibilitySettings();
176
206
  if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.deactivate();
177
207
  if (!deferMotion) playMotionLayout(previousLayout, motionKind);
178
208
  if (motionKind === "view") animateVisibleSections();
@@ -180,7 +210,7 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
180
210
  }
181
211
  if (terminalView) {
182
212
  $("#liveSection").classList.add("hidden");
183
- if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.activate(state.snapshot, state.workspaces, "general");
213
+ if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.activate(visibleSnapshot(), state.workspaces, "general");
184
214
  if (!deferMotion) playMotionLayout(previousLayout, motionKind);
185
215
  if (motionKind === "view") animateVisibleSections();
186
216
  return;
@@ -188,24 +218,32 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
188
218
  if (tmuxView) {
189
219
  $("#liveSection").classList.add("hidden");
190
220
  renderTmuxMap();
191
- if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.activate(state.snapshot, state.workspaces, "tmux");
221
+ if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.activate(visibleSnapshot(), state.workspaces, "tmux");
192
222
  if (!deferMotion) playMotionLayout(previousLayout, motionKind);
193
223
  if (motionKind === "view") animateVisibleSections();
194
224
  return;
195
225
  }
196
226
  if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.deactivate();
197
227
  const sessions = filteredSessions();
228
+ if (operationsView) renderOperationsOverview();
229
+ const attentionCount = attentionView ? renderAttentionInbox() : 0;
198
230
  const showMap = ["all", "active"].includes(state.view);
199
231
  const graphLiveCount = showMap ? renderAgentMap(graphFilteredSessions(), motionKind) : 0;
200
232
  const regular = state.view === "all" ? sessions.filter((session) => !isLiveSession(session)) : state.view === "active" ? [] : sessions;
201
233
  const visible = regular.slice(0, state.visibleLimit);
202
- $("#liveSection").classList.toggle("hidden", graphLiveCount === 0);
234
+ const resultCount = attentionView ? attentionCount : graphLiveCount + regular.length;
235
+ $("#sessionResultSummary").textContent = window.LoadToAgentI18n.t("quality.results_summary", { count: resultCount });
236
+ const activeEmpty = state.view === "active" && graphLiveCount === 0;
237
+ $("#activeEmptyState").classList.toggle("hidden", !activeEmpty);
238
+ $("#liveSection").classList.toggle("hidden", attentionView || (graphLiveCount === 0 && state.view !== "active"));
203
239
  $("#viewTitle").textContent = VIEW_TITLES[state.view] || window.LoadToAgentI18n.t("ui.recent_conversations_and_tasks");
204
240
  $("#sessionGrid").innerHTML = visible.map((session) => sessionCard(session)).join("");
205
241
  $("#sessionGrid").classList.toggle("hidden", visible.length === 0);
206
242
  $("#loadMoreBtn").classList.toggle("hidden", regular.length <= state.visibleLimit);
207
243
  $("#loadMoreBtn").textContent = window.LoadToAgentI18n.t("common.remaining", { count: regular.length - state.visibleLimit });
208
- $("#emptyState").classList.toggle("hidden", graphLiveCount + regular.length !== 0);
244
+ $("#emptyState").classList.toggle("hidden", attentionView || graphLiveCount + regular.length !== 0);
245
+ const hasConditions = Boolean(state.search || state.providerFilters.size || state.workspace !== "all" || state.sort !== "recent");
246
+ $("#emptyClearFiltersBtn").classList.toggle("hidden", resultCount !== 0 || !hasConditions);
209
247
  if (graphLiveCount + regular.length === 0) {
210
248
  const emptyCopy = state.search
211
249
  ? [window.LoadToAgentI18n.t("ui.no_search_results"), window.LoadToAgentI18n.t("ui.clear_the_search_or_change_the_ai_and_workspace_filters")]
@@ -221,17 +259,36 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
221
259
  if (motionKind === "view") animateVisibleSections();
222
260
  }
223
261
 
262
+ function renderSessions(motionKind = "refresh", deferMotion = false) {
263
+ const restoreScroll = window.LoadToAgentRendererUtils.preserveScrollPositions([".main-stage", ".sidebar"]);
264
+ context.rememberDisclosureStates?.(document);
265
+ try {
266
+ return renderSessionsContent(motionKind, deferMotion);
267
+ } finally {
268
+ context.restoreDisclosureStates?.(document);
269
+ restoreScroll();
270
+ }
271
+ }
272
+
224
273
  function render(motionKind = "refresh") {
225
- const previousLayout = captureMotionLayout();
226
- renderProviderRail();
227
- renderWorkspaces();
228
- renderGlobalStats();
229
- renderProviderOverview();
230
- renderProviderFilter();
231
- renderSessions(motionKind, true);
232
- if (state.selectedId && $("#detailDrawer").classList.contains("open")) context.renderDrawer();
233
- playMotionLayout(previousLayout, motionKind);
234
- if (motionKind === "view") animateVisibleSections();
274
+ const restoreScroll = window.LoadToAgentRendererUtils.preserveScrollPositions([".main-stage", ".sidebar"]);
275
+ context.rememberDisclosureStates?.(document);
276
+ try {
277
+ const previousLayout = captureMotionLayout();
278
+ renderProviderRail();
279
+ renderWorkspaces();
280
+ renderGlobalStats();
281
+ renderProviderOverview();
282
+ renderProviderFilter();
283
+ renderProviderVisibilitySettings();
284
+ renderSessions(motionKind, true);
285
+ if (state.selectedId && $("#detailDrawer").classList.contains("open")) context.renderDrawer();
286
+ playMotionLayout(previousLayout, motionKind);
287
+ if (motionKind === "view") animateVisibleSections();
288
+ } finally {
289
+ context.restoreDisclosureStates?.(document);
290
+ restoreScroll();
291
+ }
235
292
  }
236
293
 
237
294
  return {
@@ -3,6 +3,7 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
8
  $,
8
9
  esc,
@@ -16,6 +17,8 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
16
17
  latestWorkCopy,
17
18
  readablePreview,
18
19
  timeAgo,
20
+ visibleTmux = () => state.snapshot && state.snapshot.tmux,
21
+ visibleSessions = () => ((state.snapshot && state.snapshot.sessions) || []),
19
22
  } = context;
20
23
 
21
24
  function tmuxEntities(tmux) {
@@ -68,14 +71,14 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
68
71
  { type: "distro", id: found.distro.id, label: found.distro.name },
69
72
  { type: "session", id: found.session.id, label: found.session.name },
70
73
  { type: "window", id: found.window.id, label: `${found.window.index}:${found.window.name}` },
71
- { type: "pane", id: found.item.id, label: `pane ${found.item.index}` },
74
+ { type: "pane", id: found.item.id, label: t('tmux.pane_label', { index: found.item.index }) },
72
75
  ]
73
76
  : [];
74
77
  }
75
78
 
76
79
  function linkedTmuxSubagents(agent) {
77
80
  if (!agent || !agent.linkedSessionId) return [];
78
- const sessions = (state.snapshot && state.snapshot.sessions) || [];
81
+ const sessions = visibleSessions();
79
82
  const byId = new Map(sessions.map((session) => [session.id, session]));
80
83
  const root = byId.get(agent.linkedSessionId);
81
84
  const queue = (root && root.childIds || agent.childIds || []).map((id) => ({ id, depth: 1 }));
@@ -101,15 +104,15 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
101
104
  const working = children.filter(({ session }) => subagentWorkState(session) === "working").length;
102
105
  const attention = children.filter(({ session }) => subagentWorkState(session) === "attention").length;
103
106
  const statusSummary = [
104
- working ? `${working} 작업 중` : "",
105
- attention ? `${attention} 확인 필요` : "",
106
- ].filter(Boolean).join(" · ") || "현재 모두 쉬는 중";
107
+ working ? t('tmux.subagents.working_count', { count: working }) : "",
108
+ attention ? t('tmux.subagents.attention_count', { count: attention }) : "",
109
+ ].filter(Boolean).join(" · ") || t('tmux.subagents.all_idle');
107
110
  const rows = children
108
111
  .map(({ session, depth }) => {
109
112
  const provider = providerInfo(session.provider);
110
113
  const role = session.agentName || agentRoleLabel(session.agentRole);
111
- const assigned = session.delegation && session.delegation.assignment || session.taskName || session.title || "담당 작업 확인 중";
112
- const work = readablePreview(latestWorkCopy(session) || session.statusDetail || "상태 확인 중", 96);
114
+ const assigned = session.delegation && session.delegation.assignment || session.taskName || session.title || t('tmux.subagents.checking_assignment');
115
+ const work = readablePreview(latestWorkCopy(session) || window.LoadToAgentI18n.observedText(session.statusDetail) || t('tmux.subagents.checking_status'), 96);
113
116
  const workState = subagentWorkState(session);
114
117
  return `<article class="tmux-subagent-row work-${workState}" data-tmux-subagent-id="${esc(session.id)}"
115
118
  style="${providerStyle(session.provider)};--tmux-subagent-depth:${Math.min(2, Math.max(0, depth - 1))}">
@@ -119,13 +122,13 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
119
122
  <strong>${esc(assigned)}</strong>
120
123
  <em title="${esc(work.full)}">${esc(work.text)}</em>
121
124
  </span>
122
- <button type="button" data-open-subagent-chat="${esc(session.id)}" aria-label="${esc(`${role} · ${assigned} 대화 보기`)}">대화 보기 ↗</button>
125
+ <button type="button" data-open-subagent-chat="${esc(session.id)}" aria-label="${esc(t('tmux.subagents.view_conversation_aria', { role, assignment: assigned }))}">${t('tmux.subagents.view_conversation')}</button>
123
126
  </article>`;
124
127
  })
125
128
  .join("");
126
129
  return `<section class="tmux-subagents ${expanded ? "expanded" : ""}" data-tmux-subagents="${esc(pane.id)}">
127
130
  <button type="button" class="tmux-subagents-toggle" data-tmux-subagents-toggle="${esc(pane.id)}" aria-expanded="${expanded}" aria-controls="${esc(listId)}">
128
- <span><b>연결된 도움 AI ${children.length}개</b><small>${statusSummary} · tmux 메인 세션 기준</small></span>
131
+ <span><b>${t('tmux.subagents.connected_count', { count: children.length })}</b><small>${statusSummary} · ${t('tmux.subagents.main_session_basis')}</small></span>
129
132
  <i aria-hidden="true">↓</i>
130
133
  </button>
131
134
  <div id="${esc(listId)}" class="tmux-subagent-list ${expanded ? "" : "hidden"}">${rows}</div>
@@ -140,26 +143,26 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
140
143
  const percent = Math.max(0, Math.min(100, Number(context.percent || 0)));
141
144
  return `<article class="tmux-pane-node ${pane.active ? "active" : ""} ${pane.dead ? "dead" : ""} ${agent ? "has-agent" : ""}"
142
145
  ${agent ? `style="${providerStyle(agent.provider)}"` : ""}>
143
- <button type="button" class="tmux-pane-main" data-tmux-type="pane" data-tmux-id="${esc(pane.id)}">
146
+ <button type="button" class="tmux-pane-main" data-tmux-type="pane" data-tmux-id="${esc(pane.id)}" aria-pressed="${state.tmuxFocus?.type === "pane" && state.tmuxFocus?.id === pane.id ? "true" : "false"}">
144
147
  <span class="tmux-pane-head">
145
- <b>나눠진 ${pane.index + 1}</b><span>프로그램 ${pane.pid || "--"}</span>
146
- <i>${pane.dead ? "끝남" : pane.active ? "사용 중" : "뒤에서 실행"}</i>
148
+ <b>${t('tmux.split_pane_number', { number: pane.index + 1 })}</b><span>${t('tmux.process_number', { pid: pane.pid || "--" })}</span>
149
+ <i>${pane.dead ? t('tmux.state.ended') : pane.active ? t('tmux.state.active') : t('tmux.state.background')}</i>
147
150
  </span>
148
151
  <strong class="tmux-pane-command">${esc(pane.command || "shell")}</strong>
149
- <span class="tmux-pane-cwd" title="${esc(pane.cwd)}">${esc(pane.cwd || "경로 미보고")}</span>
152
+ <span class="tmux-pane-cwd" title="${esc(pane.cwd)}">${esc(pane.cwd || t('terminal.path_unreported'))}</span>
150
153
  ${
151
154
  agent
152
155
  ? `<span class="tmux-agent-block">
153
156
  <span class="provider-mark">${esc(provider.mark)}</span>
154
157
  <span>
155
- <small>${esc(provider.label)} · 실행 번호 ${agent.pid}</small>
158
+ <small>${esc(provider.label)} · ${t('tmux.process_number', { pid: agent.pid })}</small>
156
159
  <strong>${esc(agent.title)}</strong>
157
- <em>${esc(agent.statusDetail)}</em>
160
+ <em>${esc(window.LoadToAgentI18n.observedText(agent.statusDetail))}</em>
158
161
  </span>
159
162
  </span>
160
163
  <span class="tmux-agent-metrics">
161
164
  <span>
162
- <small>기억 사용</small>
165
+ <small>${t('tmux.context_usage')}</small>
163
166
  <b>${context.window ? `${percent.toFixed(1)}%` : "--"}</b>
164
167
  </span>
165
168
  <span>
@@ -167,20 +170,20 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
167
170
  <b>${compact(usage.total)}</b>
168
171
  </span>
169
172
  <span>
170
- <small>도움 AI</small>
173
+ <small>${t('tmux.helper_ai')}</small>
171
174
  <b>${(agent.childIds || []).length}</b>
172
175
  </span>
173
176
  </span>
174
177
  <span class="tmux-context-track"><i style="width:${percent}%"></i></span>`
175
- : '<span class="tmux-shell-note">AI가 아닌 일반 명령창입니다.</span>'
178
+ : `<span class="tmux-shell-note">${t('tmux.regular_terminal_note')}</span>`
176
179
  }
177
180
  </button>
178
181
  ${tmuxSubagentPanel(pane, agent)}
179
182
  <footer>
180
- <span>${agent ? (agent.linkedSessionId ? "대화 기록과 연결됨" : "AI가 실행 중인 것을 확인함") : pane.title || "명령창"}</span>
183
+ <span>${agent ? (agent.linkedSessionId ? t('tmux.linked_to_history') : t('tmux.ai_detected')) : pane.title || t('terminal.type.terminal')}</span>
181
184
  <span class="tmux-pane-actions">
182
- <button type="button" data-control-tmux="${esc(pane.id)}">이 칸 조작하기 ↓</button>
183
- ${agent && agent.linkedSessionId ? `<button type="button" data-open-session="${esc(agent.linkedSessionId)}">대화 내용 보기 ↗</button>` : ""}
185
+ <button type="button" data-control-tmux="${esc(pane.id)}">${t('tmux.control_pane')}</button>
186
+ ${agent && agent.linkedSessionId ? `<button type="button" data-open-session="${esc(agent.linkedSessionId)}">${t('tmux.view_conversation')}</button>` : ""}
184
187
  </span>
185
188
  </footer>
186
189
  </article>`;
@@ -188,10 +191,10 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
188
191
 
189
192
  function tmuxWindowTree(window) {
190
193
  return `<div class="tmux-window-tree">
191
- <button type="button" class="tmux-window-node ${window.active ? "active" : ""}" data-tmux-type="window" data-tmux-id="${esc(window.id)}">
192
- <small>열린 창</small>
194
+ <button type="button" class="tmux-window-node ${window.active ? "active" : ""}" data-tmux-type="window" data-tmux-id="${esc(window.id)}" aria-pressed="${state.tmuxFocus?.type === "window" && state.tmuxFocus?.id === window.id ? "true" : "false"}">
195
+ <small>${t('tmux.open_window')}</small>
193
196
  <strong>${window.index + 1}. ${esc(window.name)}</strong>
194
- <span>${window.panes.length}개 칸으로 나눔</span>
197
+ <span>${t('tmux.split_count', { count: window.panes.length })}</span>
195
198
  </button>
196
199
  <div class="tmux-link-line" aria-hidden="true">
197
200
  <i>
@@ -203,10 +206,10 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
203
206
 
204
207
  function tmuxSessionTree(tmuxSession) {
205
208
  return `<div class="tmux-session-tree">
206
- <button type="button" class="tmux-session-node ${tmuxSession.attached ? "attached" : ""}" data-tmux-type="session" data-tmux-id="${esc(tmuxSession.id)}">
207
- <small>작업 묶음</small>
209
+ <button type="button" class="tmux-session-node ${tmuxSession.attached ? "attached" : ""}" data-tmux-type="session" data-tmux-id="${esc(tmuxSession.id)}" aria-pressed="${state.tmuxFocus?.type === "session" && state.tmuxFocus?.id === tmuxSession.id ? "true" : "false"}">
210
+ <small>${t('tmux.workspace')}</small>
208
211
  <strong>${esc(tmuxSession.name)}</strong>
209
- <span>${tmuxSession.attached ? "화면에 연결됨" : "뒤에서 실행 중"} · 열린 ${tmuxSession.windows.length}개</span>
212
+ <span>${tmuxSession.attached ? t('terminal.tmux.attached') : t('terminal.tmux.running_background')} · ${t('tmux.open_window_count', { count: tmuxSession.windows.length })}</span>
210
213
  </button>
211
214
  <div class="tmux-link-line session-link" aria-hidden="true">
212
215
  <i>
@@ -252,15 +255,16 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
252
255
  }
253
256
 
254
257
  function renderTmuxMap() {
255
- const tmux = (state.snapshot && state.snapshot.tmux) || { available: false, status: "확인 중", distros: [], summary: {} };
258
+ const tmux = visibleTmux() || { available: false, status: t('tmux.status.checking'), distros: [], summary: {} };
256
259
  const summary = tmux.summary || {};
260
+ const environmentLabel = state.platform?.nativeTmux ? (state.platform.label || t('tmux.local_environment')) : t('tmux.stats.linux_environments');
257
261
  $("#tmuxStats").innerHTML = [
258
- ["Linux 환경", summary.distros || 0, window.LoadToAgentI18n.t("ui.items")],
259
- ["작업 묶음", summary.sessions || 0, window.LoadToAgentI18n.t("ui.items")],
260
- ["열린 창", summary.windows || 0, window.LoadToAgentI18n.t("ui.items")],
261
- ["나눠진 칸", summary.panes || 0, window.LoadToAgentI18n.t("ui.items")],
262
- ["AI가 일하는 칸", summary.aiPanes || 0, window.LoadToAgentI18n.t("ui.items")],
263
- ["대화 기록 연결", summary.linked || 0, window.LoadToAgentI18n.t("ui.items")],
262
+ [t('tmux.stats.environments'), summary.distros || 0, t('ui.items')],
263
+ [t('tmux.workspace'), summary.sessions || 0, t('ui.items')],
264
+ [t('tmux.open_window'), summary.windows || 0, t('ui.items')],
265
+ [t('tmux.stats.split_panes'), summary.panes || 0, t('ui.items')],
266
+ [t('tmux.stats.ai_panes'), summary.aiPanes || 0, t('ui.items')],
267
+ [t('tmux.stats.linked_history'), summary.linked || 0, t('ui.items')],
264
268
  ]
265
269
  .map(
266
270
  ([label, value, unit], index) => `<div class="${index >= 4 ? "accent" : ""}">
@@ -273,40 +277,41 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
273
277
  const index = tmuxEntities(tmux);
274
278
  const path = tmuxFocusPath(index);
275
279
  $("#tmuxBreadcrumbs").innerHTML = path.length
276
- ? `<button type="button" data-tmux-reset>전체 목록</button>${path
280
+ ? `<button type="button" data-tmux-reset tabindex="-1">${t('tmux.full_list')}</button>${path
277
281
  .map(
278
- (item) => `<i>›</i>
282
+ (item) => `<i aria-hidden="true">›</i>
279
283
  <button type="button"
280
284
  class="${item.type === state.tmuxFocus.type && item.id === state.tmuxFocus.id ? "current" : ""}"
285
+ ${item.type === state.tmuxFocus.type && item.id === state.tmuxFocus.id ? 'aria-current="location" tabindex="0"' : 'tabindex="-1"'}
281
286
  data-tmux-type="${item.type}" data-tmux-id="${esc(item.id)}">
282
287
  ${esc(item.label)}
283
288
  </button>`,
284
289
  )
285
290
  .join("")}`
286
291
  : `<span class="map-hint">
287
- <b>${summary.sessions || 0}</b>개 작업 묶음 · <b>${summary.aiPanes || 0}</b>개 칸에서 AI가 일하는 중</span>`;
292
+ ${t('tmux.summary', { sessions: `<b>${summary.sessions || 0}</b>`, panes: `<b>${summary.aiPanes || 0}</b>` })}</span>`;
288
293
  $("#tmuxResetBtn").classList.toggle("hidden", !path.length);
289
294
  const distros = filteredTmuxDistros(tmux, index);
290
295
  if (!distros.length || !Number(summary.sessions || 0)) {
291
296
  $("#tmuxMap").innerHTML = `<div class="tmux-empty">
292
297
  <span>▦</span>
293
- <h3>나눠서 실행 중인 명령창이 없습니다</h3>
294
- <p>${esc(tmux.status || "Linux 명령창 상태를 확인하는 중입니다.")}</p>
295
- <small>이 화면은 tmux를 사용하는 고급 작업이 있을 때 자동으로 채워집니다.</small>
298
+ <h3>${t('tmux.empty.title')}</h3>
299
+ <p>${esc(window.LoadToAgentI18n.observedText(tmux.status || t('tmux.empty.checking_linux')))}</p>
300
+ <small>${t('tmux.empty.description')}</small>
296
301
  </div>`;
297
302
  return;
298
303
  }
299
304
  $("#tmuxMap").innerHTML = distros
300
305
  .map(
301
306
  (distro) => `<section class="tmux-distro-group">
302
- <button type="button" class="tmux-distro-node" data-tmux-type="distro" data-tmux-id="${esc(distro.id)}">
303
- <span>Linux</span>
307
+ <button type="button" class="tmux-distro-node" data-tmux-type="distro" data-tmux-id="${esc(distro.id)}" aria-pressed="${state.tmuxFocus?.type === "distro" && state.tmuxFocus?.id === distro.id ? "true" : "false"}">
308
+ <span>${esc(environmentLabel)}</span>
304
309
  <div>
305
- <small>실행 환경</small>
310
+ <small>${t('tmux.runtime_environment')}</small>
306
311
  <strong>${esc(distro.name)}</strong>
307
312
  <em>${esc(distro.tmuxVersion || "tmux")}</em>
308
313
  </div>
309
- <b>작업 묶음 ${distro.sessions.length}개</b>
314
+ <b>${t('terminal.tmux.workspace_count', { count: distro.sessions.length })}</b>
310
315
  </button>
311
316
  <div class="tmux-distro-line" aria-hidden="true">
312
317
  </div>
@@ -314,6 +319,9 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
314
319
  </section>`,
315
320
  )
316
321
  .join("");
322
+ const mapNodes = Array.from($("#tmuxMap").querySelectorAll("[data-tmux-type][data-tmux-id]"));
323
+ const focusedNode = mapNodes.find((node) => node.dataset.tmuxType === state.tmuxFocus?.type && node.dataset.tmuxId === state.tmuxFocus?.id) || mapNodes[0];
324
+ mapNodes.forEach((node) => { node.tabIndex = node === focusedNode ? 0 : -1; });
317
325
  }
318
326
 
319
327
  return { tmuxEntities, tmuxFocusPath, linkedTmuxSubagents, tmuxPaneCard, tmuxWindowTree, tmuxSessionTree, filteredTmuxDistros, renderTmuxMap };