loadtoagent 1.0.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.ko.md +7 -0
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +23 -1
  5. package/docs/UI-AUDIT-100.md +118 -0
  6. package/docs/UI-AUDIT-101-200.md +120 -0
  7. package/docs/UI-AUDIT-201-300.md +120 -0
  8. package/main.js +183 -37
  9. package/package.json +15 -4
  10. package/preload.js +11 -0
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -9
  13. package/renderer/app-dashboard.js +197 -58
  14. package/renderer/app-drawer-content.js +103 -86
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +79 -55
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -9
  19. package/renderer/app-events-navigation.js +78 -31
  20. package/renderer/app-events-sessions.js +171 -4
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +32 -4
  23. package/renderer/app-graph-orchestration.js +16 -13
  24. package/renderer/app-graph-view.js +228 -112
  25. package/renderer/app-management.js +211 -0
  26. package/renderer/app-provider-visibility.js +124 -0
  27. package/renderer/app-quality.js +568 -0
  28. package/renderer/app-run-modal.js +103 -33
  29. package/renderer/app-runtime-overview.js +312 -0
  30. package/renderer/app-session-render.js +94 -37
  31. package/renderer/app-tmux-render.js +119 -38
  32. package/renderer/app.js +155 -76
  33. package/renderer/i18n-messages.js +833 -15
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +162 -75
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +50 -1
  38. package/renderer/styles-cards.css +26 -0
  39. package/renderer/styles-collaboration.css +18 -88
  40. package/renderer/styles-components.css +126 -14
  41. package/renderer/styles-management.css +355 -0
  42. package/renderer/styles-overlays.css +13 -2
  43. package/renderer/styles-product.css +24 -16
  44. package/renderer/styles-quality.css +312 -0
  45. package/renderer/styles-readability.css +862 -0
  46. package/renderer/styles-responsive-product.css +84 -12
  47. package/renderer/styles-responsive-runtime.css +31 -14
  48. package/renderer/styles-responsive-shell.css +44 -48
  49. package/renderer/styles-responsive-workflows.css +37 -17
  50. package/renderer/styles-run-composer.css +5 -0
  51. package/renderer/styles-runtime-overview.css +285 -0
  52. package/renderer/styles-settings.css +160 -0
  53. package/renderer/styles-terminal.css +339 -2
  54. package/renderer/styles-tmux.css +154 -0
  55. package/renderer/styles-workflow-map.css +362 -15
  56. package/renderer/styles-workflows.css +69 -2
  57. package/renderer/styles.css +27 -12
  58. package/renderer/terminal-agent.js +17 -13
  59. package/renderer/terminal-events.js +233 -40
  60. package/renderer/terminal-workbench.js +176 -78
  61. package/renderer/terminal.js +350 -37
  62. package/src/agentMonitor/claudeParser.js +96 -7
  63. package/src/agentMonitor/codexParser.js +59 -4
  64. package/src/agentMonitor/executionActivity.js +282 -0
  65. package/src/agentMonitor/genericParser.js +56 -13
  66. package/src/agentMonitor/hierarchy.js +17 -0
  67. package/src/agentMonitor/responseIntent.js +51 -0
  68. package/src/agentMonitor.js +21 -3
  69. package/src/agentRunner.js +66 -1
  70. package/src/attentionNotifier.js +71 -0
  71. package/src/automationMonitor.js +258 -0
  72. package/src/contracts.js +10 -1
  73. package/src/ipc/registerAgentIpc.js +9 -3
  74. package/src/ipc/registerAppIpc.js +4 -1
  75. package/src/ipc/registerTerminalIpc.js +12 -6
  76. package/src/macUpdateHelper.js +210 -0
  77. package/src/monitorWorker.js +65 -6
  78. package/src/platformPath.js +80 -0
  79. package/src/processMonitor.js +81 -23
  80. package/src/providerVisibilityStore.js +45 -0
  81. package/src/sessionIntelligence.js +247 -0
  82. package/src/terminalHost.js +381 -0
  83. package/src/terminalHostDaemon.js +60 -0
  84. package/src/terminalManager.js +158 -3
  85. package/src/tmuxMonitor.js +2 -2
  86. package/src/updateInstaller.js +175 -0
@@ -3,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,7 +3,23 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(context = {}) {
6
- const { $, esc, state, compact, providerInfo, providerStyle } = context;
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
7
+ const {
8
+ $,
9
+ esc,
10
+ state,
11
+ compact,
12
+ providerInfo,
13
+ providerStyle,
14
+ agentRoleLabel,
15
+ subagentWorkState,
16
+ subagentWorkLabel,
17
+ latestWorkCopy,
18
+ readablePreview,
19
+ timeAgo,
20
+ visibleTmux = () => state.snapshot && state.snapshot.tmux,
21
+ visibleSessions = () => ((state.snapshot && state.snapshot.sessions) || []),
22
+ } = context;
7
23
 
8
24
  function tmuxEntities(tmux) {
9
25
  const distros = new Map();
@@ -55,11 +71,70 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
55
71
  { type: "distro", id: found.distro.id, label: found.distro.name },
56
72
  { type: "session", id: found.session.id, label: found.session.name },
57
73
  { type: "window", id: found.window.id, label: `${found.window.index}:${found.window.name}` },
58
- { 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 }) },
59
75
  ]
60
76
  : [];
61
77
  }
62
78
 
79
+ function linkedTmuxSubagents(agent) {
80
+ if (!agent || !agent.linkedSessionId) return [];
81
+ const sessions = visibleSessions();
82
+ const byId = new Map(sessions.map((session) => [session.id, session]));
83
+ const root = byId.get(agent.linkedSessionId);
84
+ const queue = (root && root.childIds || agent.childIds || []).map((id) => ({ id, depth: 1 }));
85
+ const seen = new Set(root ? [root.id] : []);
86
+ const children = [];
87
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
88
+ const item = queue[cursor];
89
+ if (!item.id || seen.has(item.id)) continue;
90
+ seen.add(item.id);
91
+ const child = byId.get(item.id);
92
+ if (!child) continue;
93
+ children.push({ session: child, depth: item.depth });
94
+ for (const childId of child.childIds || []) queue.push({ id: childId, depth: item.depth + 1 });
95
+ }
96
+ return children;
97
+ }
98
+
99
+ function tmuxSubagentPanel(pane, agent) {
100
+ const children = linkedTmuxSubagents(agent);
101
+ if (!children.length) return "";
102
+ const expanded = state.expandedTmuxSubagents.has(pane.id);
103
+ const listId = `tmux-subagents-list-${encodeURIComponent(pane.id)}`;
104
+ const working = children.filter(({ session }) => subagentWorkState(session) === "working").length;
105
+ const attention = children.filter(({ session }) => subagentWorkState(session) === "attention").length;
106
+ const statusSummary = [
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');
110
+ const rows = children
111
+ .map(({ session, depth }) => {
112
+ const provider = providerInfo(session.provider);
113
+ const role = session.agentName || agentRoleLabel(session.agentRole);
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);
116
+ const workState = subagentWorkState(session);
117
+ return `<article class="tmux-subagent-row work-${workState}" data-tmux-subagent-id="${esc(session.id)}"
118
+ style="${providerStyle(session.provider)};--tmux-subagent-depth:${Math.min(2, Math.max(0, depth - 1))}">
119
+ <span class="provider-mark" aria-hidden="true">${esc(provider.mark)}</span>
120
+ <span class="tmux-subagent-copy">
121
+ <span><b>${esc(role)}</b><i>${esc(subagentWorkLabel(session))}</i><small>${esc(timeAgo(session.updatedAt))}</small></span>
122
+ <strong>${esc(assigned)}</strong>
123
+ <em title="${esc(work.full)}">${esc(work.text)}</em>
124
+ </span>
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>
126
+ </article>`;
127
+ })
128
+ .join("");
129
+ return `<section class="tmux-subagents ${expanded ? "expanded" : ""}" data-tmux-subagents="${esc(pane.id)}">
130
+ <button type="button" class="tmux-subagents-toggle" data-tmux-subagents-toggle="${esc(pane.id)}" aria-expanded="${expanded}" aria-controls="${esc(listId)}">
131
+ <span><b>${t('tmux.subagents.connected_count', { count: children.length })}</b><small>${statusSummary} · ${t('tmux.subagents.main_session_basis')}</small></span>
132
+ <i aria-hidden="true">↓</i>
133
+ </button>
134
+ <div id="${esc(listId)}" class="tmux-subagent-list ${expanded ? "" : "hidden"}">${rows}</div>
135
+ </section>`;
136
+ }
137
+
63
138
  function tmuxPaneCard(pane) {
64
139
  const agent = pane.agent;
65
140
  const provider = agent && providerInfo(agent.provider);
@@ -68,26 +143,26 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
68
143
  const percent = Math.max(0, Math.min(100, Number(context.percent || 0)));
69
144
  return `<article class="tmux-pane-node ${pane.active ? "active" : ""} ${pane.dead ? "dead" : ""} ${agent ? "has-agent" : ""}"
70
145
  ${agent ? `style="${providerStyle(agent.provider)}"` : ""}>
71
- <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"}">
72
147
  <span class="tmux-pane-head">
73
- <b>나눠진 ${pane.index + 1}</b><span>프로그램 ${pane.pid || "--"}</span>
74
- <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>
75
150
  </span>
76
151
  <strong class="tmux-pane-command">${esc(pane.command || "shell")}</strong>
77
- <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>
78
153
  ${
79
154
  agent
80
155
  ? `<span class="tmux-agent-block">
81
156
  <span class="provider-mark">${esc(provider.mark)}</span>
82
157
  <span>
83
- <small>${esc(provider.label)} · 실행 번호 ${agent.pid}</small>
158
+ <small>${esc(provider.label)} · ${t('tmux.process_number', { pid: agent.pid })}</small>
84
159
  <strong>${esc(agent.title)}</strong>
85
- <em>${esc(agent.statusDetail)}</em>
160
+ <em>${esc(window.LoadToAgentI18n.observedText(agent.statusDetail))}</em>
86
161
  </span>
87
162
  </span>
88
163
  <span class="tmux-agent-metrics">
89
164
  <span>
90
- <small>기억 사용</small>
165
+ <small>${t('tmux.context_usage')}</small>
91
166
  <b>${context.window ? `${percent.toFixed(1)}%` : "--"}</b>
92
167
  </span>
93
168
  <span>
@@ -95,19 +170,20 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
95
170
  <b>${compact(usage.total)}</b>
96
171
  </span>
97
172
  <span>
98
- <small>도움 AI</small>
173
+ <small>${t('tmux.helper_ai')}</small>
99
174
  <b>${(agent.childIds || []).length}</b>
100
175
  </span>
101
176
  </span>
102
177
  <span class="tmux-context-track"><i style="width:${percent}%"></i></span>`
103
- : '<span class="tmux-shell-note">AI가 아닌 일반 명령창입니다.</span>'
178
+ : `<span class="tmux-shell-note">${t('tmux.regular_terminal_note')}</span>`
104
179
  }
105
180
  </button>
181
+ ${tmuxSubagentPanel(pane, agent)}
106
182
  <footer>
107
- <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>
108
184
  <span class="tmux-pane-actions">
109
- <button type="button" data-control-tmux="${esc(pane.id)}">이 칸 조작하기 ↓</button>
110
- ${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>` : ""}
111
187
  </span>
112
188
  </footer>
113
189
  </article>`;
@@ -115,10 +191,10 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
115
191
 
116
192
  function tmuxWindowTree(window) {
117
193
  return `<div class="tmux-window-tree">
118
- <button type="button" class="tmux-window-node ${window.active ? "active" : ""}" data-tmux-type="window" data-tmux-id="${esc(window.id)}">
119
- <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>
120
196
  <strong>${window.index + 1}. ${esc(window.name)}</strong>
121
- <span>${window.panes.length}개 칸으로 나눔</span>
197
+ <span>${t('tmux.split_count', { count: window.panes.length })}</span>
122
198
  </button>
123
199
  <div class="tmux-link-line" aria-hidden="true">
124
200
  <i>
@@ -130,10 +206,10 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
130
206
 
131
207
  function tmuxSessionTree(tmuxSession) {
132
208
  return `<div class="tmux-session-tree">
133
- <button type="button" class="tmux-session-node ${tmuxSession.attached ? "attached" : ""}" data-tmux-type="session" data-tmux-id="${esc(tmuxSession.id)}">
134
- <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>
135
211
  <strong>${esc(tmuxSession.name)}</strong>
136
- <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>
137
213
  </button>
138
214
  <div class="tmux-link-line session-link" aria-hidden="true">
139
215
  <i>
@@ -179,15 +255,16 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
179
255
  }
180
256
 
181
257
  function renderTmuxMap() {
182
- 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: {} };
183
259
  const summary = tmux.summary || {};
260
+ const environmentLabel = state.platform?.nativeTmux ? (state.platform.label || t('tmux.local_environment')) : t('tmux.stats.linux_environments');
184
261
  $("#tmuxStats").innerHTML = [
185
- ["Linux 환경", summary.distros || 0, window.LoadToAgentI18n.t("ui.items")],
186
- ["작업 묶음", summary.sessions || 0, window.LoadToAgentI18n.t("ui.items")],
187
- ["열린 창", summary.windows || 0, window.LoadToAgentI18n.t("ui.items")],
188
- ["나눠진 칸", summary.panes || 0, window.LoadToAgentI18n.t("ui.items")],
189
- ["AI가 일하는 칸", summary.aiPanes || 0, window.LoadToAgentI18n.t("ui.items")],
190
- ["대화 기록 연결", 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')],
191
268
  ]
192
269
  .map(
193
270
  ([label, value, unit], index) => `<div class="${index >= 4 ? "accent" : ""}">
@@ -200,40 +277,41 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
200
277
  const index = tmuxEntities(tmux);
201
278
  const path = tmuxFocusPath(index);
202
279
  $("#tmuxBreadcrumbs").innerHTML = path.length
203
- ? `<button type="button" data-tmux-reset>전체 목록</button>${path
280
+ ? `<button type="button" data-tmux-reset tabindex="-1">${t('tmux.full_list')}</button>${path
204
281
  .map(
205
- (item) => `<i>›</i>
282
+ (item) => `<i aria-hidden="true">›</i>
206
283
  <button type="button"
207
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"'}
208
286
  data-tmux-type="${item.type}" data-tmux-id="${esc(item.id)}">
209
287
  ${esc(item.label)}
210
288
  </button>`,
211
289
  )
212
290
  .join("")}`
213
291
  : `<span class="map-hint">
214
- <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>`;
215
293
  $("#tmuxResetBtn").classList.toggle("hidden", !path.length);
216
294
  const distros = filteredTmuxDistros(tmux, index);
217
295
  if (!distros.length || !Number(summary.sessions || 0)) {
218
296
  $("#tmuxMap").innerHTML = `<div class="tmux-empty">
219
297
  <span>▦</span>
220
- <h3>나눠서 실행 중인 명령창이 없습니다</h3>
221
- <p>${esc(tmux.status || "Linux 명령창 상태를 확인하는 중입니다.")}</p>
222
- <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>
223
301
  </div>`;
224
302
  return;
225
303
  }
226
304
  $("#tmuxMap").innerHTML = distros
227
305
  .map(
228
306
  (distro) => `<section class="tmux-distro-group">
229
- <button type="button" class="tmux-distro-node" data-tmux-type="distro" data-tmux-id="${esc(distro.id)}">
230
- <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>
231
309
  <div>
232
- <small>실행 환경</small>
310
+ <small>${t('tmux.runtime_environment')}</small>
233
311
  <strong>${esc(distro.name)}</strong>
234
312
  <em>${esc(distro.tmuxVersion || "tmux")}</em>
235
313
  </div>
236
- <b>작업 묶음 ${distro.sessions.length}개</b>
314
+ <b>${t('terminal.tmux.workspace_count', { count: distro.sessions.length })}</b>
237
315
  </button>
238
316
  <div class="tmux-distro-line" aria-hidden="true">
239
317
  </div>
@@ -241,7 +319,10 @@ window.LoadToAgentAppFactories.createTmuxRenderer = function createTmuxRenderer(
241
319
  </section>`,
242
320
  )
243
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; });
244
325
  }
245
326
 
246
- return { tmuxEntities, tmuxFocusPath, tmuxPaneCard, tmuxWindowTree, tmuxSessionTree, filteredTmuxDistros, renderTmuxMap };
327
+ return { tmuxEntities, tmuxFocusPath, linkedTmuxSubagents, tmuxPaneCard, tmuxWindowTree, tmuxSessionTree, filteredTmuxDistros, renderTmuxMap };
247
328
  };