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
@@ -4,33 +4,40 @@ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDrawerContent = function createDrawerContent(context = {}) {
6
6
  const { esc, uiLocale, state, messageContentHtml, compact, fullNumber, timeOnly, providerInfo, statusIcon, agentPathTaskName, snapshotSession } = context;
7
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
7
8
 
8
- function chatHtml(session) {
9
+ function chatHtml(session, options = {}) {
9
10
  const messages = session.messages || [];
10
- if (!messages.length) return '<div class="empty-state"><h3>표시할 대화가 없습니다</h3></div>';
11
+ if (!messages.length) return `<div class="empty-state"><h3>${esc(t("drawer.no_conversation"))}</h3></div>`;
12
+ const userLabel = options.userLabel || t("drawer.user");
13
+ const assistantLabel = options.assistantLabel || providerInfo(session.provider).label;
14
+ const conversationLabel = options.conversationLabel || t("drawer.conversation");
11
15
  const conversation = messages.filter((message) => message.role === "user" || message.role === "assistant");
12
16
  const activities = messages.filter((message) => message.role !== "user" && message.role !== "assistant");
13
17
  const omitted = Number(session.omittedMessages || 0);
14
18
  const notice =
15
19
  omitted || session.truncated
16
- ? `<div class="chat-truncated">이 작업의 최근 기록을 표시합니다${omitted ? ` · 이전 ${omitted.toLocaleString(uiLocale())} 메시지 생략` : ""}</div>`
20
+ ? `<div class="chat-truncated">${esc(t("drawer.recent_history"))}${omitted ? ` · ${esc(t("drawer.messages_omitted", { count: omitted.toLocaleString(uiLocale()) }))}` : ""}</div>`
17
21
  : "";
18
22
  const statusLabels = {
19
- started: "실행 중", running: "실행 중",
20
- done: window.LoadToAgentI18n.t("ui.completed"), completed: window.LoadToAgentI18n.t("ui.completed"), failed: "실패",
23
+ started: t("ui.working"), running: t("ui.working"),
24
+ done: t("ui.completed"), completed: t("ui.completed"), failed: t("drawer.failed"),
21
25
  };
22
26
  const statusLabel = (value) => statusLabels[value] || value || "";
23
27
  const rows = conversation
24
28
  .map((message) => {
25
29
  const role = message.role === "assistant" ? "assistant" : message.role === "tool" ? "tool" : message.role === "system" ? "system" : "user";
30
+ const renderedMessage = role === "tool" || role === "system"
31
+ ? { ...message, text: window.LoadToAgentI18n.observedText(message.text) }
32
+ : message;
26
33
  const label =
27
34
  role === "assistant"
28
- ? providerInfo(session.provider).label
35
+ ? assistantLabel
29
36
  : role === "tool"
30
- ? message.title || "도구"
37
+ ? window.LoadToAgentI18n.observedText(message.title || t("session.tool"))
31
38
  : message.role === "system"
32
- ? "시스템"
33
- : "사용자";
39
+ ? t("drawer.system")
40
+ : userLabel;
34
41
  const avatar = role === "assistant" ? providerInfo(session.provider).mark : role === "tool" ? "⌘" : role === "system" ? "i" : "ME";
35
42
  const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
36
43
  return `<div class="chat-row ${role}" data-message-id="${esc(message.id || "")}">
@@ -40,44 +47,44 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
40
47
  <b>${esc(label)}</b>
41
48
  <span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>
42
49
  ${message.status ? `<span>${esc(statusLabel(message.status))}</span>` : ""}
43
- </div>${messageContentHtml(message)}</div>
50
+ </div>${messageContentHtml(renderedMessage, session.id)}</div>
44
51
  </div>`;
45
52
  })
46
53
  .join("");
47
54
  const activityHtml = activities.length
48
- ? `<details class="chat-activities">
49
- <summary>도구·시스템 활동 ${activities.length}건 보기</summary>
55
+ ? `<details class="chat-activities" data-disclosure-key="${esc(`drawer:${session.id}:activities`)}">
56
+ <summary>${esc(t("drawer.activities_view", { count: activities.length }))}</summary>
50
57
  <div>${activities
51
58
  .map(
52
59
  (message) => `<article>
53
60
  <header>
54
- <b>${esc(message.title || (message.role === "tool" ? "도구 실행" : "시스템"))}</b>
61
+ <b>${esc(window.LoadToAgentI18n.observedText(message.title || (message.role === "tool" ? t("drawer.tool_execution") : t("drawer.system"))))}</b>
55
62
  <span>${esc(statusLabel(message.status))} · ${esc(timeOnly(message.timestamp))}</span>
56
- </header>${messageContentHtml(message)}</article>`,
63
+ </header>${messageContentHtml({ ...message, text: window.LoadToAgentI18n.observedText(message.text) }, session.id)}</article>`,
57
64
  )
58
65
  .join("")}</div>
59
66
  </details>`
60
67
  : "";
61
- const emptyConversation = conversation.length ? "" : '<div class="empty-state compact"><h3>사용자와 AI의 대화는 아직 없습니다</h3></div>';
68
+ const emptyConversation = conversation.length ? "" : `<div class="empty-state compact"><h3>${esc(t("drawer.no_user_ai_conversation"))}</h3></div>`;
62
69
  return `${notice}<div class="chat-history-head">
63
- <span>대화 ${conversation.length}개${activities.length ? ` · 활동 ${activities.length}건` : ""}</span>
64
- <button type="button" data-scroll-latest>가장 최근 대화 ↓</button>
70
+ <span>${esc(t("drawer.conversation_summary", { label: conversationLabel, count: conversation.length, activities: activities.length ? ` · ${t("drawer.activities", { count: activities.length })}` : "" }))}</span>
71
+ <button type="button" data-scroll-latest>${esc(t("drawer.latest_conversation"))} ↓</button>
65
72
  </div>
66
- <div class="chat-list">${rows}${emptyConversation}${activityHtml}<div class="chat-latest-anchor" aria-label="가장 최근 대화">
73
+ <div class="chat-list">${rows}${emptyConversation}${activityHtml}<div class="chat-latest-anchor" aria-label="${esc(t("drawer.latest_conversation"))}">
67
74
  </div>
68
75
  </div>`;
69
76
  }
70
77
 
71
78
  function lifecycleHtml(session) {
72
79
  const events = session.lifecycle || [];
73
- if (!events.length) return '<div class="empty-state"><h3>아직 기록된 진행 과정이 없습니다</h3></div>';
80
+ if (!events.length) return `<div class="empty-state"><h3>${esc(t("drawer.no_lifecycle"))}</h3></div>`;
74
81
  return `<div class="lifecycle-list">${events
75
82
  .map(
76
83
  (event) => `<div class="lifecycle-event ${esc(event.status)}">
77
84
  <span class="life-node">${statusIcon(event.type)}</span>
78
85
  <div class="life-copy">
79
- <b>${esc(event.label)}</b>
80
- <span>${esc(event.detail || event.type)}</span>
86
+ <b>${esc(window.LoadToAgentI18n.observedText(event.label))}</b>
87
+ <span>${esc(window.LoadToAgentI18n.observedText(event.detail || event.type))}</span>
81
88
  </div>
82
89
  <time>${esc(timeOnly(event.timestamp))}</time>
83
90
  </div>`,
@@ -91,14 +98,14 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
91
98
  const context = session.context || {};
92
99
  const sourceLabel =
93
100
  context.source === "session"
94
- ? "이 작업 기록에서 직접 확인한 기억 공간"
101
+ ? t("drawer.context_source_session")
95
102
  : context.source === "model-catalog"
96
- ? "AI 모델 정보에 적힌 기억 공간"
97
- : "기억 공간 크기 정보 없음";
103
+ ? t("drawer.context_source_catalog")
104
+ : t("session.context_size_unknown");
98
105
  return `<div class="token-hero" style="--drawer-provider:${providerInfo(session.provider).accent}">
99
106
  <div class="token-hero-head">
100
- <span>AI의 기억 공간 사용량</span>
101
- <b>${context.window ? `${fullNumber(context.used)} / ${fullNumber(context.window)} 토큰` : `${fullNumber(context.used)} 토큰`}</b>
107
+ <span>${esc(t("session.context_usage"))}</span>
108
+ <b>${esc(t("drawer.tokens", { count: context.window ? `${fullNumber(context.used)} / ${fullNumber(context.window)}` : fullNumber(context.used) }))}</b>
102
109
  </div>
103
110
  <div class="big-context"><span style="width:${Math.min(100, context.percent || 0)}%"></span></div>
104
111
  <div class="context-scale">
@@ -107,15 +114,15 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
107
114
  </div>
108
115
  </div>
109
116
  <div class="token-grid">
110
- <div class="token-tile"><span>AI가 받은 글</span><strong>${fullNumber(usage.input)}</strong><small>AI에게 전달된 내용의 양</small></div>
111
- <div class="token-tile"><span>AI가 쓴 글</span><strong>${fullNumber(usage.output)}</strong><small>AI가 답하고 만든 내용의 양</small></div>
112
- <div class="token-tile"><span>다시 사용한 기억</span><strong>${fullNumber(usage.cachedInput)}</strong><small>전에 읽은 내용을 다시 활용한 양</small></div>
113
- <div class="token-tile"><span>새로 저장한 기억</span><strong>${fullNumber(usage.cacheWrite)}</strong><small>다음에 다시 쓰도록 저장한 양</small></div>
114
- <div class="token-tile"><span>생각에 사용</span><strong>${fullNumber(usage.reasoning)}</strong><small>AI가 따로 알려준 경우만 표시</small></div>
115
- <div class="token-tile"><span>전체 사용량</span><strong>${fullNumber(usage.total)}</strong><small>이 작업에서 사용한 토큰 합계</small></div>
116
- <div class="token-tile"><span>최근에 받은 글</span><strong>${fullNumber(turn.input)}</strong><small>가장 최근 대화 기준</small></div>
117
- <div class="token-tile"><span>최근 대화 전체</span><strong>${fullNumber(turn.total)}</strong><small>가장 최근 한 번의 사용량</small></div>
118
- </div><div class="token-note">${esc(sourceLabel)}입니다. 토큰은 AI가 글을 읽고 쓰는 양을 세는 단위이고, 기억 공간은 AI가 한 번에 기억할 수 있는 양입니다.</div>`;
117
+ <div class="token-tile"><span>${esc(t("drawer.input"))}</span><strong>${fullNumber(usage.input)}</strong><small>${esc(t("drawer.input_help"))}</small></div>
118
+ <div class="token-tile"><span>${esc(t("drawer.output"))}</span><strong>${fullNumber(usage.output)}</strong><small>${esc(t("drawer.output_help"))}</small></div>
119
+ <div class="token-tile"><span>${esc(t("drawer.cached"))}</span><strong>${fullNumber(usage.cachedInput)}</strong><small>${esc(t("drawer.cached_help"))}</small></div>
120
+ <div class="token-tile"><span>${esc(t("drawer.cache_write"))}</span><strong>${fullNumber(usage.cacheWrite)}</strong><small>${esc(t("drawer.cache_write_help"))}</small></div>
121
+ <div class="token-tile"><span>${esc(t("drawer.reasoning"))}</span><strong>${fullNumber(usage.reasoning)}</strong><small>${esc(t("drawer.reasoning_help"))}</small></div>
122
+ <div class="token-tile"><span>${esc(t("drawer.total"))}</span><strong>${fullNumber(usage.total)}</strong><small>${esc(t("drawer.total_help"))}</small></div>
123
+ <div class="token-tile"><span>${esc(t("drawer.last_input"))}</span><strong>${fullNumber(turn.input)}</strong><small>${esc(t("drawer.latest_turn"))}</small></div>
124
+ <div class="token-tile"><span>${esc(t("drawer.last_total"))}</span><strong>${fullNumber(turn.total)}</strong><small>${esc(t("drawer.last_total_help"))}</small></div>
125
+ </div><div class="token-note">${esc(t("drawer.token_note", { source: sourceLabel }))}</div>`;
119
126
  }
120
127
 
121
128
  function subagentCommunicationEvents(session) {
@@ -136,68 +143,78 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
136
143
  return { text: `${text.slice(0, maxCharacters).trimEnd()}…`, truncated: true };
137
144
  }
138
145
 
139
- function protectedSubagentEventText(event, taskName) {
140
- if (event.kind === "followup") return "보호된 추가 작업 지시를 전달했습니다.";
141
- if (event.kind === "message") return "보호된 메시지를 전달했습니다.";
142
- return `${taskName || " 작업"}을 서브에이전트에게 배정했습니다.`;
146
+ function subagentCoordinationEvents(session) {
147
+ return subagentCommunicationEvents(session).filter((event) => {
148
+ if (!event || event.protected || event.kind === "started") return false;
149
+ if (!["assignment", "followup", "message", "result", "interrupt"].includes(event.kind)) return false;
150
+ const text = String(event.text || "").trim();
151
+ return Boolean(text && text.toLowerCase() !== "started");
152
+ });
143
153
  }
144
154
 
145
- function subagentConversationHtml(session) {
146
- const events = subagentCommunicationEvents(session);
155
+ function subagentWorkMessages(session) {
156
+ const messages = [...(session.messages || [])];
157
+ const hasConversation = messages.some((message) =>
158
+ (message.role === "user" || message.role === "assistant") && String(message.text || "").trim(),
159
+ );
160
+ if (hasConversation) return messages;
161
+ const delegation = session.delegation || {};
162
+ if (delegation.assignmentObserved && !delegation.assignmentProtected && String(delegation.assignment || "").trim()) {
163
+ messages.push({
164
+ id: `${session.id}:delegation`, role: "user", text: delegation.assignment,
165
+ timestamp: delegation.startedAt || session.startedAt || session.updatedAt,
166
+ });
167
+ }
168
+ if (String(session.result || delegation.result || "").trim()) {
169
+ messages.push({
170
+ id: `${session.id}:result`, role: "assistant", text: session.result || delegation.result,
171
+ timestamp: session.completedAt || delegation.completedAt || session.updatedAt,
172
+ });
173
+ }
174
+ return messages.sort((left, right) => Date.parse(left.timestamp || 0) - Date.parse(right.timestamp || 0));
175
+ }
176
+
177
+ function subagentCoordinationHtml(session) {
178
+ const events = subagentCoordinationEvents(session);
179
+ if (!events.length) return "";
147
180
  const taskName = session.taskName || (session.delegation && session.delegation.taskName) || session.title;
148
181
  const childPath = String(session.agentPath || "");
149
182
  const endpointIsChild = (value) => {
150
183
  const endpoint = String(value || "");
151
184
  return endpoint === childPath || endpoint === session.id || agentPathTaskName(endpoint) === taskName;
152
185
  };
153
- const enriched = events.map((event) => ({ ...event, fromChild: event.kind === "result" || endpointIsChild(event.from) }));
154
- const received = enriched.filter((event) => !event.fromChild && event.kind !== "started").length;
155
- const answered = enriched.filter((event) => event.fromChild).length;
156
- if (!events.length)
157
- return '<div class="empty-state"><h3>메인 AI와 주고받은 기록이 없습니다</h3><p>세션 로그에서 배정·추가 지시·결과 반환 이벤트를 찾지 못했습니다.</p></div>';
158
- const provider = providerInfo(session.provider);
159
- const rows = enriched
186
+ const rows = events
160
187
  .map((event) => {
161
- const runtime = event.kind === "started";
162
- const role = runtime ? "system" : event.fromChild ? "assistant" : "user";
163
- const label = runtime ? "실행 상태" : event.fromChild ? `서브 AI · ${session.agentName || taskName}` : "메인 AI";
164
- const avatar = runtime ? "↗" : event.fromChild ? provider.mark : "M";
165
- const route = runtime ? "런타임 → 서브 AI" : event.fromChild ? "서브 AI → 메인 AI" : "메인 AI → 서브 AI";
166
- const text = event.protected
167
- ? protectedSubagentEventText(event, taskName)
168
- : event.text || (runtime ? "서브에이전트 실행이 시작되었습니다." : "내용 없이 통신 상태만 기록되었습니다.");
169
- const preview = subagentTextPreview(text);
170
- const eventLabel = `${event.label || event.kind}${event.assignmentSource === "parent-narration" ? " · 작업 시작 직전 메인 AI 설명" : ""}`;
171
- return `<div class="chat-row ${role} subagent-dialog-row" data-subagent-communication="${esc(event.kind)}">
172
- <span class="chat-avatar">${esc(avatar)}</span>
173
- <div class="chat-bubble">
174
- <div class="chat-bubble-head">
175
- <b>${esc(label)}</b><span class="subagent-route">${esc(route)}</span>
176
- <span>${esc(timeOnly(event.timestamp))}</span>
177
- </div>
178
- <small class="subagent-event-label">${esc(eventLabel)}</small>
179
- <div class="chat-content subagent-message-preview${preview.truncated ? " is-truncated" : ""}"
180
- data-subagent-message-preview data-truncated="${preview.truncated ? "true" : "false"}">
181
- <p>${esc(preview.text)}</p>
182
- </div>
183
- </div>
184
- </div>`;
188
+ const fromChild = event.kind === "result" || endpointIsChild(event.from);
189
+ const preview = subagentTextPreview(event.text);
190
+ const label = fromChild ? t("drawer.child_to_main", { child: session.agentName || taskName }) : t("drawer.main_to_child");
191
+ return `<article data-subagent-communication="${esc(event.kind)}">
192
+ <header><b>${esc(window.LoadToAgentI18n.observedText(event.label || event.kind))}</b><span>${esc(label)} · ${esc(timeOnly(event.timestamp))}</span></header>
193
+ <div class="chat-content plain subagent-message-preview${preview.truncated ? " is-truncated" : ""}"
194
+ data-subagent-message-preview data-truncated="${preview.truncated ? "true" : "false"}"><p>${esc(preview.text)}</p></div>
195
+ </article>`;
185
196
  })
186
197
  .join("");
187
- return `<div class="subagent-conversation-summary" data-subagent-dialog-count="${events.length}">
188
- <span>
189
- <small>받은 지시·응답</small>
190
- <b>${received}</b>건</span>
191
- <span>
192
- <small>메인에 보낸 답변</small>
193
- <b>${answered}</b>건</span>
194
- <span>
195
- <small>전체 소통</small>
196
- <b>${events.length}</b>건</span>
197
- </div>
198
- <div class="chat-history-head"><span><b>${esc(taskName)}</b>와 메인 AI가 주고받은 내용만 표시</span><button type="button" data-scroll-latest>가장 최근 대화 ↓</button></div>
199
- <div class="chat-list subagent-dialog-list">${rows}<div class="chat-latest-anchor" aria-label="가장 최근 대화"></div></div>`;
198
+ return `<details class="chat-activities subagent-coordination" data-subagent-coordination-count="${events.length}" data-disclosure-key="${esc(`drawer:${session.id}:coordination`)}">
199
+ <summary>${esc(t("drawer.coordination_events", { count: events.length }))}</summary><div>${rows}</div>
200
+ </details>`;
201
+ }
202
+
203
+ function subagentConversationHtml(session) {
204
+ const messages = subagentWorkMessages(session);
205
+ const conversationCount = messages.filter((message) => message.role === "user" || message.role === "assistant").length;
206
+ const workSession = { ...session, messages };
207
+ const sourceCopy = session.source === "collaboration-history"
208
+ ? t("drawer.subagent_history_reconstructed")
209
+ : t("drawer.subagent_history_actual");
210
+ return `<section class="subagent-work-source" data-subagent-work-messages="${conversationCount}">
211
+ <b>${esc(t("drawer.subagent_work_history"))}</b><span>${esc(sourceCopy)}</span>
212
+ </section>${chatHtml(workSession, {
213
+ userLabel: t("drawer.assignment"),
214
+ assistantLabel: session.agentName || t("drawer.sub_ai"),
215
+ conversationLabel: t("drawer.work_history"),
216
+ })}${subagentCoordinationHtml(session)}`;
200
217
  }
201
218
 
202
- return { chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents, subagentTextPreview, protectedSubagentEventText, subagentConversationHtml };
219
+ return { chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents, subagentCoordinationEvents, subagentTextPreview, subagentConversationHtml };
203
220
  };
@@ -3,27 +3,40 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDrawerData = function createDrawerData(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const { reportRecoverableError, state } = context;
8
+ const detailRequests = new Map();
9
+ let detailRequestGeneration = 0;
7
10
 
8
11
  async function loadSessionDetail(id, force = false) {
9
12
  if (!force && state.details.has(id)) return state.details.get(id);
13
+ if (!force && detailRequests.has(id)) return detailRequests.get(id).promise;
14
+ const generation = ++detailRequestGeneration;
10
15
  state.detailErrors.delete(id);
11
16
  state.detailLoadingIds.add(id);
12
17
  context.renderDrawer();
13
- try {
14
- const detail = await window.loadtoagent.sessionDetail(id);
15
- if (detail) state.details.set(id, detail);
16
- return detail;
17
- } catch (error) {
18
- state.detailErrors.set(id, (error && error.message) || "작업 기록을 불러오지 못했습니다.");
19
- return null;
20
- } finally {
21
- state.detailLoadingIds.delete(id);
22
- if (state.selectedId === id) {
23
- state.drawerForceLatest = state.drawerTab === "chat";
24
- context.renderDrawer();
18
+ const promise = (async () => {
19
+ try {
20
+ const detail = await window.loadtoagent.sessionDetail(id);
21
+ if (detailRequests.get(id)?.generation === generation && detail) state.details.set(id, detail);
22
+ return detail;
23
+ } catch (error) {
24
+ if (detailRequests.get(id)?.generation === generation)
25
+ state.detailErrors.set(id, window.LoadToAgentI18n.errorText(error, "drawer.history_failed"));
26
+ return null;
27
+ } finally {
28
+ if (detailRequests.get(id)?.generation === generation) {
29
+ detailRequests.delete(id);
30
+ state.detailLoadingIds.delete(id);
31
+ if (state.selectedId === id) {
32
+ state.drawerForceLatest = state.drawerTab === "chat";
33
+ context.renderDrawer();
34
+ }
35
+ }
25
36
  }
26
- }
37
+ })();
38
+ detailRequests.set(id, { generation, promise });
39
+ return promise;
27
40
  }
28
41
 
29
42
  async function loadSubagentParentDetail(child) {
@@ -3,11 +3,13 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}) {
6
+ const t = (key, params) => window.LoadToAgentI18n.t(key, params);
6
7
  const {
7
- $, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger,
8
- providerInfo, isLiveSession, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionWorkspaceLabel,
8
+ $, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger, setDialogOpenState,
9
+ providerInfo, isLiveSession, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionOriginPath, sessionWorkspaceLabel,
9
10
  agentResumeSupport, originAppInfo, selectedSession, snapshotSession, loadSessionDetail, loadSubagentParentDetail,
10
- chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents, subagentConversationHtml,
11
+ chatHtml, lifecycleHtml, tokensHtml, outcomeHtml, subagentCoordinationEvents, subagentConversationHtml,
12
+ rememberDisclosureStates = () => {}, restoreDisclosureStates = () => {},
11
13
  } = context;
12
14
 
13
15
  function openDrawer(id) {
@@ -15,17 +17,16 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
15
17
  markGuideStep("detail");
16
18
  state.selectedId = id;
17
19
  state.drawerMode = "session";
18
- state.drawerTab = "chat";
20
+ state.drawerTab = "summary";
19
21
  state.drawerForceLatest = true;
20
22
  clearTimeout(motionState.drawerTimer);
21
23
  $("#drawerBackdrop").classList.remove("hidden");
22
24
  $("#drawerBackdrop").classList.remove("closing");
23
25
  $("#detailDrawer").classList.add("open");
24
- $("#detailDrawer").removeAttribute("inert");
25
- $("#detailDrawer").setAttribute("aria-hidden", "false");
26
+ setDialogOpenState($("#detailDrawer"), true);
26
27
  renderDrawer();
27
28
  loadSessionDetail(id, true);
28
- setTimeout(() => $("#closeDrawerBtn").focus(), 0);
29
+ setTimeout(() => $("#closeDrawerBtn").focus({ preventScroll: true }), 0);
29
30
  }
30
31
 
31
32
  function openSubagentConversation(id) {
@@ -41,25 +42,26 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
41
42
  $("#drawerBackdrop").classList.remove("hidden");
42
43
  $("#drawerBackdrop").classList.remove("closing");
43
44
  $("#detailDrawer").classList.add("open");
44
- $("#detailDrawer").removeAttribute("inert");
45
- $("#detailDrawer").setAttribute("aria-hidden", "false");
45
+ setDialogOpenState($("#detailDrawer"), true);
46
46
  renderDrawer();
47
+ loadSessionDetail(id);
47
48
  loadSubagentParentDetail(child);
48
- setTimeout(() => $("#closeDrawerBtn").focus(), 0);
49
+ setTimeout(() => $("#closeDrawerBtn").focus({ preventScroll: true }), 0);
49
50
  }
50
51
 
51
52
  function closeDrawer(restoreFocus = true) {
52
53
  if (!$("#detailDrawer").classList.contains("open")) return;
54
+ const drawerGeneration = motionState.dialogGeneration;
53
55
  $("#detailDrawer").classList.remove("open");
54
- $("#detailDrawer").setAttribute("aria-hidden", "true");
55
- $("#detailDrawer").setAttribute("inert", "");
56
+ setDialogOpenState($("#detailDrawer"), false);
56
57
  $("#drawerBackdrop").classList.add("closing");
57
58
  clearTimeout(motionState.drawerTimer);
58
59
  motionState.drawerTimer = setTimeout(
59
60
  () => {
60
61
  $("#drawerBackdrop").classList.add("hidden");
61
62
  $("#drawerBackdrop").classList.remove("closing");
62
- if (restoreFocus) restoreDialogTrigger();
63
+ if (drawerGeneration !== motionState.dialogGeneration) return;
64
+ if (restoreFocus) restoreDialogTrigger(drawerGeneration);
63
65
  else motionState.activeDialogTrigger = null;
64
66
  },
65
67
  motionPreference.matches ? 0 : 260,
@@ -71,60 +73,70 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
71
73
  if (!session) return closeDrawer();
72
74
  const provider = providerInfo(session.provider);
73
75
  const subagentMode = state.drawerMode === "subagent" && Boolean(session.parentId);
74
- const detailLoading = !subagentMode && state.detailLoadingIds.has(state.selectedId);
76
+ const detailLoading = state.detailLoadingIds.has(state.selectedId);
75
77
  $("#detailDrawer").style.setProperty("--drawer-provider", provider.accent);
76
78
  $("#drawerProviderMark").style.setProperty("--provider", provider.accent);
77
79
  $("#drawerProviderMark").textContent = provider.mark;
78
80
  $("#drawerProvider").textContent = subagentMode
79
- ? `${session.agentName || provider.label} · 메인 AI와의 소통`
81
+ ? t("drawer.subagent_title", { name: session.agentName || provider.label })
80
82
  : `${provider.company} · ${STATUS[session.status] || session.status}`;
81
- $("#drawerTitle").textContent = subagentMode ? session.taskName || (session.delegation && session.delegation.taskName) || session.title : session.title;
83
+ const drawerTitle = subagentMode ? session.taskName || (session.delegation && session.delegation.taskName) || session.title : session.title;
84
+ $("#drawerTitle").textContent = drawerTitle;
85
+ $("#drawerTitle").title = drawerTitle;
82
86
  const stopping = session.runId && state.stopRequests.has(session.runId);
83
87
  const stop =
84
88
  session.runId && (session.status === "running" || session.status === "starting")
85
- ? `<button class="meta-chip stop-run" data-stop-run="${esc(session.runId)}"
89
+ ? `<button type="button" class="meta-chip stop-run" data-stop-run="${esc(session.runId)}"
86
90
  ${stopping ? 'disabled aria-busy="true"' : ""}>
87
- ${stopping ? "중지 요청 중…" : "■ 실행 중지"}</button>`
91
+ ${esc(t(stopping ? "drawer.stop_requested" : "drawer.stop_run"))}</button>`
88
92
  : "";
89
93
  const runtime = session.runtimePresence || [];
90
94
  const resume =
91
95
  !isLiveSession(session) && agentResumeSupport(session).supported
92
- ? `<button class="meta-chip resume-agent" data-resume-agent="${esc(session.id)}">▶
93
- <b>${originAppInfo(session) ? "백그라운드 터미널로 이어가기" : "터미널로 다시 일 시키기"}</b>
96
+ ? `<button type="button" class="meta-chip resume-agent" data-resume-agent="${esc(session.id)}">▶
97
+ <b>${esc(t(originAppInfo(session) ? "drawer.continue_background_terminal" : "drawer.resume_in_terminal"))}</b>
94
98
  </button>`
95
99
  : "";
96
- const communicationCount = subagentMode ? subagentCommunicationEvents(session).length : 0;
100
+ const communicationCount = subagentMode ? subagentCoordinationEvents(session).length : 0;
101
+ const subagentMessageCount = subagentMode
102
+ ? (session.messages || []).filter((message) => message.role === "user" || message.role === "assistant").length
103
+ : 0;
104
+ const taskId = String(session.externalId || session.id || "");
105
+ const copyTask = taskId
106
+ ? `<button type="button" class="meta-chip meta-copy" data-copy-text="${esc(taskId)}" aria-label="${esc(t("quality.copy_task_id"))}">${esc(t("quality.task_id"))} <b>${esc(taskId.slice(0, 12))}</b><span aria-hidden="true">⧉</span></button>`
107
+ : "";
108
+ const originPath = sessionOriginPath(session);
109
+ const copyWorkspace = !isProjectlessSession(session) && originPath
110
+ ? `<button type="button" class="meta-chip meta-copy origin-project-meta" data-copy-text="${esc(originPath)}" aria-label="${esc(t("quality.copy_workspace"))}">${esc(t("project.origin"))} <b>${esc(sessionWorkspaceLabel(session))}</b><span aria-hidden="true">⧉</span></button>`
111
+ : "";
97
112
  $("#drawerMeta").innerHTML = subagentMode
98
113
  ? `<span class="meta-chip work-state ${subagentWorkState(session)}">
99
114
  <b>${esc(subagentWorkLabel(session))}</b>
100
115
  </span>
101
- <span class="meta-chip">사용 모델 <b>${esc(session.model || "정보 없음")}</b>
116
+ <span class="meta-chip">${esc(t("drawer.model"))} <b>${esc(session.model || t("drawer.unknown"))}</b>
102
117
  </span>
103
- <span class="meta-chip">메인과 소통 <b>${communicationCount}건</b>
104
- </span>${resume}`
105
- : `<span class="meta-chip">사용 모델 <b>${esc(session.model || "정보 없음")}</b>
118
+ <span class="meta-chip">${esc(t("drawer.work_history"))} <b>${esc(t("drawer.event_count", { count: subagentMessageCount }))}</b>
106
119
  </span>
107
- <span class="meta-chip">작업 폴더
108
- <b title="${esc(isProjectlessSession(session) ? window.LoadToAgentI18n.t("ui.session_not_linked_to_a_specific_project") : session.cwd)}">
109
- ${esc(sessionWorkspaceLabel(session))}
110
- </b>
120
+ <span class="meta-chip">${esc(t("drawer.main_instructions_results"))} <b>${esc(t("drawer.event_count", { count: communicationCount }))}</b>
121
+ </span>${copyTask}${copyWorkspace}${resume}`
122
+ : `<span class="meta-chip">${esc(t("drawer.model"))} <b>${esc(session.model || t("drawer.unknown"))}</b>
111
123
  </span>
112
- <span class="meta-chip">작업 번호 <b>${esc(String(session.externalId || "").slice(0, 12) || "정보 없음")}</b>
113
- </span>${
124
+ ${copyWorkspace || `<span class="meta-chip origin-project-meta">${esc(t("project.origin"))} <b>${esc(sessionWorkspaceLabel(session))}</b></span>`}
125
+ ${copyTask}${
114
126
  session.parentId
115
- ? `<span class="meta-chip">⑂ <b>도움을 맡은 AI</b>
127
+ ? `<span class="meta-chip">⑂ <b>${esc(t("drawer.helper_ai"))}</b>
116
128
  </span>`
117
129
  : ""
118
130
  }${
119
131
  runtime.length
120
- ? `<span class="meta-chip runtime-meta">● <b>실행 중인 프로그램 ${runtime.length}개</b>
132
+ ? `<span class="meta-chip runtime-meta">● <b>${esc(t("session.running_programs", { count: runtime.length }))}</b>
121
133
  </span>`
122
134
  : ""
123
135
  }${resume}${stop}`;
124
136
  $$(".drawer-tab").forEach((tab) => {
125
137
  const hidden = subagentMode && tab.dataset.tab !== "chat";
126
138
  tab.classList.toggle("hidden", hidden);
127
- if (tab.dataset.tab === "chat") tab.textContent = subagentMode ? "메인과의 대화" : window.LoadToAgentI18n.t("ui.conversation");
139
+ if (tab.dataset.tab === "chat") tab.textContent = subagentMode ? t("drawer.work_content") : t("ui.conversation");
128
140
  const active = tab.dataset.tab === state.drawerTab;
129
141
  tab.classList.toggle("active", active);
130
142
  tab.setAttribute("aria-selected", active ? "true" : "false");
@@ -133,8 +145,9 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
133
145
  const activeTab = $(`.drawer-tab[data-tab="${state.drawerTab}"]`);
134
146
  if (activeTab) $("#drawerContent").setAttribute("aria-labelledby", activeTab.id);
135
147
  const content = $("#drawerContent");
148
+ rememberDisclosureStates(content);
136
149
  const previousTop = content.scrollTop;
137
- const wasNearBottom = content.scrollHeight - content.scrollTop - content.clientHeight < 90;
150
+ const wasAtBottom = window.LoadToAgentRendererUtils.isScrolledToEnd(content);
138
151
  const renderKey = `${state.drawerMode}:${state.selectedId}:${state.drawerTab}:${detailLoading ? "loading" : "ready"}`;
139
152
  const previousRenderKey = motionState.drawerRenderKey;
140
153
  const shouldAnimateContent = previousRenderKey !== renderKey;
@@ -143,40 +156,51 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
143
156
  motionState.drawerTab = state.drawerTab;
144
157
  const detailError = state.detailErrors.get(state.selectedId);
145
158
  content.innerHTML = detailLoading
146
- ? '<div class="drawer-loading"><span></span><b>전체 작업 기록을 불러오는 중</b><small>잠시만 기다리면 대화와 진행 과정을 볼 수 있어요.</small></div>'
147
- : detailError && !subagentMode
159
+ ? `<div class="drawer-loading"><span></span><b>${esc(t("drawer.loading_history"))}</b><small>${esc(t("drawer.loading_history_help"))}</small></div>`
160
+ : detailError
148
161
  ? `<div class="drawer-error">
149
- <b>작업 기록을 불러오지 못했습니다</b>
162
+ <b>${esc(t("drawer.history_failed"))}</b>
150
163
  <span>${esc(detailError)}</span>
151
- <button type="button" data-retry-detail="${esc(state.selectedId)}">다시 시도</button>
164
+ <button type="button" data-retry-detail="${esc(state.selectedId)}">${esc(t("drawer.retry"))}</button>
152
165
  </div>`
153
166
  : subagentMode
154
167
  ? subagentConversationHtml(session)
155
- : state.drawerTab === "chat"
168
+ : state.drawerTab === "summary"
169
+ ? outcomeHtml(session)
170
+ : state.drawerTab === "chat"
156
171
  ? chatHtml(session)
157
172
  : state.drawerTab === "lifecycle"
158
173
  ? lifecycleHtml(session)
159
174
  : tokensHtml(session);
175
+ restoreDisclosureStates(content);
160
176
  content.classList.toggle("motion-content-in", shouldAnimateContent && !motionPreference.matches);
161
177
  clearTimeout(motionState.drawerContentTimer);
162
178
  if (shouldAnimateContent)
163
179
  motionState.drawerContentTimer = setTimeout(() => content.classList.remove("motion-content-in"), motionPreference.matches ? 0 : 520);
164
- if (!detailLoading)
165
- requestAnimationFrame(() => {
166
- const forceLatest = state.drawerForceLatest;
167
- if (state.drawerTab === "chat" && forceLatest) {
168
- const rows = [...content.querySelectorAll(".chat-row")];
169
- const latest = rows[rows.length - 1];
170
- if (latest && latest.offsetHeight > content.clientHeight - 90) {
171
- const contentTop = content.getBoundingClientRect().top;
172
- const stickyHeight = content.querySelector(".chat-history-head")?.getBoundingClientRect().height || 0;
173
- content.scrollTop = Math.max(0, content.scrollTop + latest.getBoundingClientRect().top - contentTop - stickyHeight - 12);
174
- } else content.scrollTop = content.scrollHeight;
175
- } else if (tabChanged) content.scrollTop = 0;
176
- else if (state.drawerTab === "chat" && wasNearBottom) content.scrollTop = content.scrollHeight;
177
- else content.scrollTop = Math.min(previousTop, Math.max(0, content.scrollHeight - content.clientHeight));
180
+ if (!detailLoading) {
181
+ if (tabChanged) {
182
+ content.scrollTop = 0;
178
183
  state.drawerForceLatest = false;
179
- });
184
+ } else {
185
+ const scrollGeneration = (motionState.drawerScrollGeneration || 0) + 1;
186
+ motionState.drawerScrollGeneration = scrollGeneration;
187
+ requestAnimationFrame(() => {
188
+ if (motionState.drawerScrollGeneration !== scrollGeneration) return;
189
+ const forceLatest = state.drawerForceLatest;
190
+ if (state.drawerTab === "chat" && forceLatest) {
191
+ const rows = [...content.querySelectorAll(".chat-row")];
192
+ const latest = rows[rows.length - 1];
193
+ if (latest && latest.offsetHeight > content.clientHeight - 90) {
194
+ const contentTop = content.getBoundingClientRect().top;
195
+ const stickyHeight = content.querySelector(".chat-history-head")?.getBoundingClientRect().height || 0;
196
+ content.scrollTop = Math.max(0, content.scrollTop + latest.getBoundingClientRect().top - contentTop - stickyHeight - 12);
197
+ } else content.scrollTop = content.scrollHeight;
198
+ } else if (state.drawerTab === "chat" && wasAtBottom) content.scrollTop = content.scrollHeight;
199
+ else content.scrollTop = Math.min(previousTop, Math.max(0, content.scrollHeight - content.clientHeight));
200
+ state.drawerForceLatest = false;
201
+ });
202
+ }
203
+ }
180
204
  }
181
205
 
182
206
  return { openDrawer, openSubagentConversation, closeDrawer, renderDrawer };