loadtoagent 1.0.0 → 1.1.0
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.
- package/main.js +50 -1
- package/package.json +4 -2
- package/preload.js +5 -0
- package/renderer/app-bootstrap.js +11 -1
- package/renderer/app-dashboard.js +7 -3
- package/renderer/app-drawer-content.js +68 -55
- package/renderer/app-drawer.js +13 -7
- package/renderer/app-events-filters.js +5 -1
- package/renderer/app-events-navigation.js +1 -1
- package/renderer/app-events-sessions.js +21 -1
- package/renderer/app-graph-model.js +47 -1
- package/renderer/app-graph-orchestration.js +9 -8
- package/renderer/app-graph-view.js +6 -5
- package/renderer/app-tmux-render.js +75 -2
- package/renderer/app.js +2 -1
- package/renderer/i18n-messages.js +1 -0
- package/renderer/index.html +18 -17
- package/renderer/styles-agent-map.css +44 -1
- package/renderer/styles-collaboration.css +18 -88
- package/renderer/styles-components.css +40 -5
- package/renderer/styles-overlays.css +5 -0
- package/renderer/styles-responsive-runtime.css +9 -0
- package/renderer/styles-responsive-workflows.css +0 -14
- package/renderer/styles-tmux.css +154 -0
- package/renderer/terminal-workbench.js +11 -1
- package/renderer/terminal.js +9 -3
- package/src/attentionNotifier.js +67 -0
- package/src/processMonitor.js +1 -1
- package/src/tmuxMonitor.js +2 -2
package/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { app, BrowserWindow, ipcMain, dialog, shell, clipboard, Tray, Menu, net } = require('electron');
|
|
3
|
+
const { app, BrowserWindow, ipcMain, dialog, shell, clipboard, Tray, Menu, net, Notification } = require('electron');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
@@ -21,6 +21,12 @@ const { registerTerminalIpc } = require('./src/ipc/registerTerminalIpc');
|
|
|
21
21
|
const { registerTmuxIpc } = require('./src/ipc/registerTmuxIpc');
|
|
22
22
|
const { registerWorkspaceIpc } = require('./src/ipc/registerWorkspaceIpc');
|
|
23
23
|
const { reportRecoverableError } = require('./src/diagnostics');
|
|
24
|
+
const { AttentionNotifier } = require('./src/attentionNotifier');
|
|
25
|
+
|
|
26
|
+
const PRODUCT_NAME = 'LoadToAgent';
|
|
27
|
+
app.setName(PRODUCT_NAME);
|
|
28
|
+
process.title = PRODUCT_NAME;
|
|
29
|
+
if (process.platform === 'win32') app.setAppUserModelId('com.wincube.loadtoagent');
|
|
24
30
|
|
|
25
31
|
const demoCapture = process.env.LOADTOAGENT_DEMO_CAPTURE === '1';
|
|
26
32
|
let mainWindow = null;
|
|
@@ -31,6 +37,7 @@ let bridgeServer = null;
|
|
|
31
37
|
let bridgeLauncher = null;
|
|
32
38
|
let backgroundTray = null;
|
|
33
39
|
let updateManager = null;
|
|
40
|
+
let attentionNotifier = null;
|
|
34
41
|
let isQuitting = false;
|
|
35
42
|
let appLocale = 'ko';
|
|
36
43
|
const tmuxController = new TmuxController({ platform: process.platform });
|
|
@@ -45,6 +52,8 @@ const MAIN_COPY = {
|
|
|
45
52
|
trayQuit: '프로그램 끝내기 · AI 세션도 종료',
|
|
46
53
|
addWorkspaces: 'AI 작업 폴더 선택',
|
|
47
54
|
pickWorkspace: '작업 폴더 선택',
|
|
55
|
+
attentionTitle: '내 확인이 필요합니다',
|
|
56
|
+
attentionBody: '{provider} · {title}',
|
|
48
57
|
},
|
|
49
58
|
en: {
|
|
50
59
|
trayTooltip: 'LoadToAgent · {count} background AI sessions',
|
|
@@ -53,6 +62,8 @@ const MAIN_COPY = {
|
|
|
53
62
|
trayQuit: 'Quit app · End AI sessions too',
|
|
54
63
|
addWorkspaces: 'Choose AI workspaces',
|
|
55
64
|
pickWorkspace: 'Choose workspace',
|
|
65
|
+
attentionTitle: 'Your review is needed',
|
|
66
|
+
attentionBody: '{provider} · {title}',
|
|
56
67
|
},
|
|
57
68
|
'zh-CN': {
|
|
58
69
|
trayTooltip: 'LoadToAgent · {count} 个后台 AI 会话',
|
|
@@ -61,6 +72,8 @@ const MAIN_COPY = {
|
|
|
61
72
|
trayQuit: '退出应用 · 同时结束 AI 会话',
|
|
62
73
|
addWorkspaces: '选择 AI 工作文件夹',
|
|
63
74
|
pickWorkspace: '选择工作文件夹',
|
|
75
|
+
attentionTitle: '需要你的确认',
|
|
76
|
+
attentionBody: '{provider} · {title}',
|
|
64
77
|
},
|
|
65
78
|
};
|
|
66
79
|
let lastSnapshot = {
|
|
@@ -254,6 +267,39 @@ function sendSnapshot(snapshot) {
|
|
|
254
267
|
try { mainWindow.webContents.send('agents:snapshot', snapshot); } catch (error) { reportRecoverableError('ipc-send:agents:snapshot', error); }
|
|
255
268
|
}
|
|
256
269
|
|
|
270
|
+
function openAttentionSession(session) {
|
|
271
|
+
showMainWindow();
|
|
272
|
+
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
273
|
+
mainWindow.flashFrame(false);
|
|
274
|
+
try {
|
|
275
|
+
mainWindow.webContents.send('agents:attention-requested', { sessionId: String(session && session.id || '') });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
reportRecoverableError('ipc-send:agents:attention-requested', error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function createAttentionNotifier() {
|
|
282
|
+
return new AttentionNotifier({
|
|
283
|
+
Notification,
|
|
284
|
+
isSupported: () => Notification.isSupported(),
|
|
285
|
+
copy: session => {
|
|
286
|
+
const provider = providerList().find(item => item.id === session.provider);
|
|
287
|
+
return {
|
|
288
|
+
title: mainText('attentionTitle'),
|
|
289
|
+
body: mainText('attentionBody', {
|
|
290
|
+
provider: provider && provider.label || session.provider || 'AI',
|
|
291
|
+
title: session.title || '이름 없는 세션',
|
|
292
|
+
}),
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
onOpen: openAttentionSession,
|
|
296
|
+
onFallback: session => {
|
|
297
|
+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.flashFrame(true);
|
|
298
|
+
openAttentionSession(session);
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
257
303
|
function sendUpdateState(update) {
|
|
258
304
|
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
259
305
|
try { mainWindow.webContents.send('app:update-state', update); } catch (error) { reportRecoverableError('ipc-send:app:update-state', error); }
|
|
@@ -278,6 +324,7 @@ function setupRuntime() {
|
|
|
278
324
|
downloadsDir: path.join(app.getPath('userData'), 'updates'),
|
|
279
325
|
});
|
|
280
326
|
updateManager.on('state', sendUpdateState);
|
|
327
|
+
attentionNotifier = createAttentionNotifier();
|
|
281
328
|
updateManager.check().catch(error => reportRecoverableError('startup-update-check', error));
|
|
282
329
|
if (demoCapture) {
|
|
283
330
|
availability = Object.fromEntries(providerList().map(provider => [provider.id, true]));
|
|
@@ -305,6 +352,7 @@ function setupRuntime() {
|
|
|
305
352
|
monitorWorker.on('message', message => {
|
|
306
353
|
if (message && message.type === 'snapshot') {
|
|
307
354
|
lastSnapshot = message.snapshot;
|
|
355
|
+
attentionNotifier.sync(lastSnapshot);
|
|
308
356
|
sendSnapshot(lastSnapshot);
|
|
309
357
|
}
|
|
310
358
|
if (message && message.type === 'detail-result') {
|
|
@@ -479,6 +527,7 @@ app.on('window-all-closed', () => {
|
|
|
479
527
|
|
|
480
528
|
app.on('before-quit', () => {
|
|
481
529
|
isQuitting = true;
|
|
530
|
+
if (attentionNotifier) attentionNotifier.dispose();
|
|
482
531
|
if (bridgeServer) bridgeServer.dispose();
|
|
483
532
|
if (terminalManager) terminalManager.dispose();
|
|
484
533
|
if (monitorWorker) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtoagent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A local desktop dashboard for Claude, Codex, Gemini, and Grok sessions.",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"author": "wincube AX",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"test:interaction": "electron scripts/interaction-check.js",
|
|
52
52
|
"demo:readme": "electron scripts/readme-demo.js",
|
|
53
53
|
"dist": "electron-builder",
|
|
54
|
-
"dist:win": "electron-builder --win
|
|
54
|
+
"dist:win": "electron-builder --win",
|
|
55
55
|
"dist:mac": "electron-builder --mac --publish never",
|
|
56
56
|
"pack": "electron-builder --dir",
|
|
57
57
|
"prepublishOnly": "npm test"
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"node_modules/node-pty/**/*"
|
|
92
92
|
],
|
|
93
93
|
"win": {
|
|
94
|
+
"icon": "build/icon.ico",
|
|
94
95
|
"target": [
|
|
95
96
|
{
|
|
96
97
|
"target": "portable",
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
]
|
|
108
109
|
},
|
|
109
110
|
"mac": {
|
|
111
|
+
"icon": "build/icon.png",
|
|
110
112
|
"category": "public.app-category.developer-tools",
|
|
111
113
|
"target": [
|
|
112
114
|
{
|
package/preload.js
CHANGED
|
@@ -67,6 +67,11 @@ contextBridge.exposeInMainWorld('loadtoagent', {
|
|
|
67
67
|
ipcRenderer.on('agents:snapshot', handler);
|
|
68
68
|
return () => ipcRenderer.removeListener('agents:snapshot', handler);
|
|
69
69
|
},
|
|
70
|
+
onAttentionRequested: callback => {
|
|
71
|
+
const handler = (_event, payload) => callback(payload);
|
|
72
|
+
ipcRenderer.on('agents:attention-requested', handler);
|
|
73
|
+
return () => ipcRenderer.removeListener('agents:attention-requested', handler);
|
|
74
|
+
},
|
|
70
75
|
onUpdateState: callback => {
|
|
71
76
|
const handler = (_event, update) => callback(update);
|
|
72
77
|
ipcRenderer.on('app:update-state', handler);
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
].forEach(install);
|
|
31
31
|
window.LoadToAgentApp = app;
|
|
32
32
|
|
|
33
|
-
const { $, esc, state, loadGuideState, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, toast } = app;
|
|
33
|
+
const { $, esc, state, loadGuideState, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, selectView, openDrawer, openSubagentConversation, toast } = app;
|
|
34
34
|
|
|
35
35
|
async function init() {
|
|
36
36
|
loadGuideState();
|
|
@@ -64,6 +64,16 @@
|
|
|
64
64
|
if (card && detail && card.updatedAt !== detail.updatedAt) loadSessionDetail(state.selectedId, true);
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
|
+
if (window.loadtoagent.onAttentionRequested)
|
|
68
|
+
window.loadtoagent.onAttentionRequested((payload) => {
|
|
69
|
+
const sessionId = String(payload && payload.sessionId || '');
|
|
70
|
+
const session = (state.snapshot && state.snapshot.sessions || []).find(item => item.id === sessionId);
|
|
71
|
+
selectView('waiting');
|
|
72
|
+
if (session) {
|
|
73
|
+
if (session.parentId) openSubagentConversation(session.id);
|
|
74
|
+
else openDrawer(session.id);
|
|
75
|
+
} else toast('확인이 필요한 세션 목록을 열었습니다.');
|
|
76
|
+
});
|
|
67
77
|
if (window.loadtoagent.onUpdateState)
|
|
68
78
|
window.loadtoagent.onUpdateState((update) => {
|
|
69
79
|
state.update = update;
|
|
@@ -245,8 +245,11 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
245
245
|
<div class="poc-head">
|
|
246
246
|
<span class="provider-mark">${esc(provider.mark)}</span>
|
|
247
247
|
<div><strong>${esc(provider.label)}</strong><small>${esc(provider.company)}</small></div>
|
|
248
|
-
<span class="poc-
|
|
249
|
-
${
|
|
248
|
+
<span class="poc-head-states">
|
|
249
|
+
<span class="poc-filter-state ${selected ? "visible" : ""}" aria-hidden="true">✓ ${window.LoadToAgentI18n.t("filter.applied")}</span>
|
|
250
|
+
<span class="poc-state ${provider.installed ? "online" : ""}">
|
|
251
|
+
${provider.installed ? window.LoadToAgentI18n.t("ui.available") : window.LoadToAgentI18n.t("ui.setup_required")}
|
|
252
|
+
</span>
|
|
250
253
|
</span>
|
|
251
254
|
</div>
|
|
252
255
|
<div class="poc-metrics">
|
|
@@ -280,7 +283,8 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
280
283
|
const selected = id === "all" ? allSelected : state.providerFilters.has(id);
|
|
281
284
|
return `<button type="button" class="provider-filter-chip ${selected ? "selected" : ""}"
|
|
282
285
|
data-provider-filter="${esc(id)}" aria-pressed="${selected ? "true" : "false"}">
|
|
283
|
-
|
|
286
|
+
<i class="provider-filter-check" aria-hidden="true">✓</i>
|
|
287
|
+
${mark ? `<span class="provider-filter-mark" aria-hidden="true">${esc(mark)}</span>` : ""}<b>${esc(label)}</b>
|
|
284
288
|
</button>`;
|
|
285
289
|
};
|
|
286
290
|
$("#providerFilter").innerHTML =
|
|
@@ -5,9 +5,12 @@ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
|
5
5
|
window.LoadToAgentAppFactories.createDrawerContent = function createDrawerContent(context = {}) {
|
|
6
6
|
const { esc, uiLocale, state, messageContentHtml, compact, fullNumber, timeOnly, providerInfo, statusIcon, agentPathTaskName, snapshotSession } = context;
|
|
7
7
|
|
|
8
|
-
function chatHtml(session) {
|
|
8
|
+
function chatHtml(session, options = {}) {
|
|
9
9
|
const messages = session.messages || [];
|
|
10
10
|
if (!messages.length) return '<div class="empty-state"><h3>표시할 대화가 없습니다</h3></div>';
|
|
11
|
+
const userLabel = options.userLabel || "사용자";
|
|
12
|
+
const assistantLabel = options.assistantLabel || providerInfo(session.provider).label;
|
|
13
|
+
const conversationLabel = options.conversationLabel || "대화";
|
|
11
14
|
const conversation = messages.filter((message) => message.role === "user" || message.role === "assistant");
|
|
12
15
|
const activities = messages.filter((message) => message.role !== "user" && message.role !== "assistant");
|
|
13
16
|
const omitted = Number(session.omittedMessages || 0);
|
|
@@ -25,12 +28,12 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
25
28
|
const role = message.role === "assistant" ? "assistant" : message.role === "tool" ? "tool" : message.role === "system" ? "system" : "user";
|
|
26
29
|
const label =
|
|
27
30
|
role === "assistant"
|
|
28
|
-
?
|
|
31
|
+
? assistantLabel
|
|
29
32
|
: role === "tool"
|
|
30
33
|
? message.title || "도구"
|
|
31
34
|
: message.role === "system"
|
|
32
35
|
? "시스템"
|
|
33
|
-
:
|
|
36
|
+
: userLabel;
|
|
34
37
|
const avatar = role === "assistant" ? providerInfo(session.provider).mark : role === "tool" ? "⌘" : role === "system" ? "i" : "ME";
|
|
35
38
|
const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
|
|
36
39
|
return `<div class="chat-row ${role}" data-message-id="${esc(message.id || "")}">
|
|
@@ -60,7 +63,7 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
60
63
|
: "";
|
|
61
64
|
const emptyConversation = conversation.length ? "" : '<div class="empty-state compact"><h3>사용자와 AI의 대화는 아직 없습니다</h3></div>';
|
|
62
65
|
return `${notice}<div class="chat-history-head">
|
|
63
|
-
<span
|
|
66
|
+
<span>${esc(conversationLabel)} ${conversation.length}개${activities.length ? ` · 활동 ${activities.length}건` : ""}</span>
|
|
64
67
|
<button type="button" data-scroll-latest>가장 최근 대화 ↓</button>
|
|
65
68
|
</div>
|
|
66
69
|
<div class="chat-list">${rows}${emptyConversation}${activityHtml}<div class="chat-latest-anchor" aria-label="가장 최근 대화">
|
|
@@ -136,68 +139,78 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
136
139
|
return { text: `${text.slice(0, maxCharacters).trimEnd()}…`, truncated: true };
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
function subagentCoordinationEvents(session) {
|
|
143
|
+
return subagentCommunicationEvents(session).filter((event) => {
|
|
144
|
+
if (!event || event.protected || event.kind === "started") return false;
|
|
145
|
+
if (!["assignment", "followup", "message", "result", "interrupt"].includes(event.kind)) return false;
|
|
146
|
+
const text = String(event.text || "").trim();
|
|
147
|
+
return Boolean(text && text.toLowerCase() !== "started");
|
|
148
|
+
});
|
|
143
149
|
}
|
|
144
150
|
|
|
145
|
-
function
|
|
146
|
-
const
|
|
151
|
+
function subagentWorkMessages(session) {
|
|
152
|
+
const messages = [...(session.messages || [])];
|
|
153
|
+
const hasConversation = messages.some((message) =>
|
|
154
|
+
(message.role === "user" || message.role === "assistant") && String(message.text || "").trim(),
|
|
155
|
+
);
|
|
156
|
+
if (hasConversation) return messages;
|
|
157
|
+
const delegation = session.delegation || {};
|
|
158
|
+
if (delegation.assignmentObserved && !delegation.assignmentProtected && String(delegation.assignment || "").trim()) {
|
|
159
|
+
messages.push({
|
|
160
|
+
id: `${session.id}:delegation`, role: "user", text: delegation.assignment,
|
|
161
|
+
timestamp: delegation.startedAt || session.startedAt || session.updatedAt,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (String(session.result || delegation.result || "").trim()) {
|
|
165
|
+
messages.push({
|
|
166
|
+
id: `${session.id}:result`, role: "assistant", text: session.result || delegation.result,
|
|
167
|
+
timestamp: session.completedAt || delegation.completedAt || session.updatedAt,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return messages.sort((left, right) => Date.parse(left.timestamp || 0) - Date.parse(right.timestamp || 0));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function subagentCoordinationHtml(session) {
|
|
174
|
+
const events = subagentCoordinationEvents(session);
|
|
175
|
+
if (!events.length) return "";
|
|
147
176
|
const taskName = session.taskName || (session.delegation && session.delegation.taskName) || session.title;
|
|
148
177
|
const childPath = String(session.agentPath || "");
|
|
149
178
|
const endpointIsChild = (value) => {
|
|
150
179
|
const endpoint = String(value || "");
|
|
151
180
|
return endpoint === childPath || endpoint === session.id || agentPathTaskName(endpoint) === taskName;
|
|
152
181
|
};
|
|
153
|
-
const
|
|
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
|
|
182
|
+
const rows = events
|
|
160
183
|
.map((event) => {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const label =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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>`;
|
|
184
|
+
const fromChild = event.kind === "result" || endpointIsChild(event.from);
|
|
185
|
+
const preview = subagentTextPreview(event.text);
|
|
186
|
+
const label = fromChild ? `${session.agentName || taskName} → 메인 AI` : "메인 AI → 서브에이전트";
|
|
187
|
+
return `<article data-subagent-communication="${esc(event.kind)}">
|
|
188
|
+
<header><b>${esc(event.label || event.kind)}</b><span>${esc(label)} · ${esc(timeOnly(event.timestamp))}</span></header>
|
|
189
|
+
<div class="chat-content plain subagent-message-preview${preview.truncated ? " is-truncated" : ""}"
|
|
190
|
+
data-subagent-message-preview data-truncated="${preview.truncated ? "true" : "false"}"><p>${esc(preview.text)}</p></div>
|
|
191
|
+
</article>`;
|
|
185
192
|
})
|
|
186
193
|
.join("");
|
|
187
|
-
return `<
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
return `<details class="chat-activities subagent-coordination" data-subagent-coordination-count="${events.length}">
|
|
195
|
+
<summary>메인 AI와 주고받은 지시·결과 ${events.length}건</summary><div>${rows}</div>
|
|
196
|
+
</details>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function subagentConversationHtml(session) {
|
|
200
|
+
const messages = subagentWorkMessages(session);
|
|
201
|
+
const conversationCount = messages.filter((message) => message.role === "user" || message.role === "assistant").length;
|
|
202
|
+
const workSession = { ...session, messages };
|
|
203
|
+
const sourceCopy = session.source === "collaboration-history"
|
|
204
|
+
? "별도 세션 로그가 없어 협업 기록에서 복원한 작업 결과입니다."
|
|
205
|
+
: "이 서브에이전트 세션에 실제로 저장된 작업 지시와 답변을 시간순으로 표시합니다.";
|
|
206
|
+
return `<section class="subagent-work-source" data-subagent-work-messages="${conversationCount}">
|
|
207
|
+
<b>서브에이전트 실제 작업 기록</b><span>${esc(sourceCopy)}</span>
|
|
208
|
+
</section>${chatHtml(workSession, {
|
|
209
|
+
userLabel: "작업 지시",
|
|
210
|
+
assistantLabel: session.agentName || "서브 AI",
|
|
211
|
+
conversationLabel: "작업 기록",
|
|
212
|
+
})}${subagentCoordinationHtml(session)}`;
|
|
200
213
|
}
|
|
201
214
|
|
|
202
|
-
return { chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents,
|
|
215
|
+
return { chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents, subagentCoordinationEvents, subagentTextPreview, subagentConversationHtml };
|
|
203
216
|
};
|
package/renderer/app-drawer.js
CHANGED
|
@@ -7,7 +7,7 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
7
7
|
$, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger,
|
|
8
8
|
providerInfo, isLiveSession, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionWorkspaceLabel,
|
|
9
9
|
agentResumeSupport, originAppInfo, selectedSession, snapshotSession, loadSessionDetail, loadSubagentParentDetail,
|
|
10
|
-
chatHtml, lifecycleHtml, tokensHtml,
|
|
10
|
+
chatHtml, lifecycleHtml, tokensHtml, subagentCoordinationEvents, subagentConversationHtml,
|
|
11
11
|
} = context;
|
|
12
12
|
|
|
13
13
|
function openDrawer(id) {
|
|
@@ -44,6 +44,7 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
44
44
|
$("#detailDrawer").removeAttribute("inert");
|
|
45
45
|
$("#detailDrawer").setAttribute("aria-hidden", "false");
|
|
46
46
|
renderDrawer();
|
|
47
|
+
loadSessionDetail(id);
|
|
47
48
|
loadSubagentParentDetail(child);
|
|
48
49
|
setTimeout(() => $("#closeDrawerBtn").focus(), 0);
|
|
49
50
|
}
|
|
@@ -71,12 +72,12 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
71
72
|
if (!session) return closeDrawer();
|
|
72
73
|
const provider = providerInfo(session.provider);
|
|
73
74
|
const subagentMode = state.drawerMode === "subagent" && Boolean(session.parentId);
|
|
74
|
-
const detailLoading =
|
|
75
|
+
const detailLoading = state.detailLoadingIds.has(state.selectedId);
|
|
75
76
|
$("#detailDrawer").style.setProperty("--drawer-provider", provider.accent);
|
|
76
77
|
$("#drawerProviderMark").style.setProperty("--provider", provider.accent);
|
|
77
78
|
$("#drawerProviderMark").textContent = provider.mark;
|
|
78
79
|
$("#drawerProvider").textContent = subagentMode
|
|
79
|
-
? `${session.agentName || provider.label} ·
|
|
80
|
+
? `${session.agentName || provider.label} · 서브에이전트 작업 기록`
|
|
80
81
|
: `${provider.company} · ${STATUS[session.status] || session.status}`;
|
|
81
82
|
$("#drawerTitle").textContent = subagentMode ? session.taskName || (session.delegation && session.delegation.taskName) || session.title : session.title;
|
|
82
83
|
const stopping = session.runId && state.stopRequests.has(session.runId);
|
|
@@ -93,14 +94,19 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
93
94
|
<b>${originAppInfo(session) ? "백그라운드 터미널로 이어가기" : "터미널로 다시 일 시키기"}</b>
|
|
94
95
|
</button>`
|
|
95
96
|
: "";
|
|
96
|
-
const communicationCount = subagentMode ?
|
|
97
|
+
const communicationCount = subagentMode ? subagentCoordinationEvents(session).length : 0;
|
|
98
|
+
const subagentMessageCount = subagentMode
|
|
99
|
+
? (session.messages || []).filter((message) => message.role === "user" || message.role === "assistant").length
|
|
100
|
+
: 0;
|
|
97
101
|
$("#drawerMeta").innerHTML = subagentMode
|
|
98
102
|
? `<span class="meta-chip work-state ${subagentWorkState(session)}">
|
|
99
103
|
<b>${esc(subagentWorkLabel(session))}</b>
|
|
100
104
|
</span>
|
|
101
105
|
<span class="meta-chip">사용 모델 <b>${esc(session.model || "정보 없음")}</b>
|
|
102
106
|
</span>
|
|
103
|
-
<span class="meta-chip"
|
|
107
|
+
<span class="meta-chip">작업 기록 <b>${subagentMessageCount}건</b>
|
|
108
|
+
</span>
|
|
109
|
+
<span class="meta-chip">메인 지시·결과 <b>${communicationCount}건</b>
|
|
104
110
|
</span>${resume}`
|
|
105
111
|
: `<span class="meta-chip">사용 모델 <b>${esc(session.model || "정보 없음")}</b>
|
|
106
112
|
</span>
|
|
@@ -124,7 +130,7 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
124
130
|
$$(".drawer-tab").forEach((tab) => {
|
|
125
131
|
const hidden = subagentMode && tab.dataset.tab !== "chat";
|
|
126
132
|
tab.classList.toggle("hidden", hidden);
|
|
127
|
-
if (tab.dataset.tab === "chat") tab.textContent = subagentMode ? "
|
|
133
|
+
if (tab.dataset.tab === "chat") tab.textContent = subagentMode ? "작업 내용" : window.LoadToAgentI18n.t("ui.conversation");
|
|
128
134
|
const active = tab.dataset.tab === state.drawerTab;
|
|
129
135
|
tab.classList.toggle("active", active);
|
|
130
136
|
tab.setAttribute("aria-selected", active ? "true" : "false");
|
|
@@ -144,7 +150,7 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
144
150
|
const detailError = state.detailErrors.get(state.selectedId);
|
|
145
151
|
content.innerHTML = detailLoading
|
|
146
152
|
? '<div class="drawer-loading"><span></span><b>전체 작업 기록을 불러오는 중</b><small>잠시만 기다리면 대화와 진행 과정을 볼 수 있어요.</small></div>'
|
|
147
|
-
: detailError
|
|
153
|
+
: detailError
|
|
148
154
|
? `<div class="drawer-error">
|
|
149
155
|
<b>작업 기록을 불러오지 못했습니다</b>
|
|
150
156
|
<span>${esc(detailError)}</span>
|
|
@@ -48,7 +48,11 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
48
48
|
renderProviderOverview();
|
|
49
49
|
renderSessions("filter");
|
|
50
50
|
announceProviderFilter();
|
|
51
|
-
requestAnimationFrame(() =>
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
const next = $("#providerFilter").querySelector(`[data-provider-filter="${CSS.escape(chip.dataset.providerFilter)}"]`);
|
|
53
|
+
next?.classList.add("filter-clicked");
|
|
54
|
+
next?.focus();
|
|
55
|
+
});
|
|
52
56
|
});
|
|
53
57
|
$("#sortSelect").addEventListener("change", (event) => {
|
|
54
58
|
state.sort = event.target.value;
|
|
@@ -33,7 +33,7 @@ window.LoadToAgentAppFactories.createNavigationEventBindings = function createNa
|
|
|
33
33
|
state.guideExpanded = false;
|
|
34
34
|
saveGuideState();
|
|
35
35
|
renderSessions("guide");
|
|
36
|
-
$("#guideBtn").focus();
|
|
36
|
+
$("#guideBtn").focus({ preventScroll: true });
|
|
37
37
|
});
|
|
38
38
|
$("#beginnerGuide").addEventListener("click", (event) => {
|
|
39
39
|
const action = event.target.closest("[data-guide-action]")?.dataset.guideAction;
|
|
@@ -18,7 +18,11 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
18
18
|
renderProviderOverview();
|
|
19
19
|
renderSessions("filter");
|
|
20
20
|
announceProviderFilter();
|
|
21
|
-
requestAnimationFrame(() =>
|
|
21
|
+
requestAnimationFrame(() => {
|
|
22
|
+
const next = $("#providerOverview").querySelector(`[data-provider-card="${CSS.escape(card.dataset.providerCard)}"]`);
|
|
23
|
+
next?.classList.add("filter-clicked");
|
|
24
|
+
next?.focus();
|
|
25
|
+
});
|
|
22
26
|
});
|
|
23
27
|
$("#sessionGrid").addEventListener("click", (event) => {
|
|
24
28
|
const card = event.target.closest("[data-session-id]");
|
|
@@ -150,6 +154,22 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
150
154
|
|
|
151
155
|
function bindTmuxMapEvents() {
|
|
152
156
|
$("#tmuxMap").addEventListener("click", (event) => {
|
|
157
|
+
const subagentToggle = event.target.closest("[data-tmux-subagents-toggle]");
|
|
158
|
+
if (subagentToggle) {
|
|
159
|
+
event.stopPropagation();
|
|
160
|
+
const paneId = subagentToggle.dataset.tmuxSubagentsToggle;
|
|
161
|
+
if (state.expandedTmuxSubagents.has(paneId)) state.expandedTmuxSubagents.delete(paneId);
|
|
162
|
+
else state.expandedTmuxSubagents.add(paneId);
|
|
163
|
+
renderTmuxMap();
|
|
164
|
+
requestAnimationFrame(() => $("#tmuxMap").querySelector(`[data-tmux-subagents-toggle="${CSS.escape(paneId)}"]`)?.focus());
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const subagentChat = event.target.closest("[data-open-subagent-chat]");
|
|
168
|
+
if (subagentChat) {
|
|
169
|
+
event.stopPropagation();
|
|
170
|
+
openSubagentConversation(subagentChat.dataset.openSubagentChat);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
153
173
|
const control = event.target.closest("[data-control-tmux]");
|
|
154
174
|
if (control) {
|
|
155
175
|
event.stopPropagation();
|
|
@@ -103,5 +103,51 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
|
|
|
103
103
|
return seen.size;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
function runtimeAgentSummary(model, entries = []) {
|
|
107
|
+
const liveNodes = model.nodes.filter(isLiveSession);
|
|
108
|
+
const liveById = new Map(liveNodes.map((session) => [session.id, session]));
|
|
109
|
+
const activeEntries = entries.filter((entry) => entry && entry.pane && !entry.pane.dead);
|
|
110
|
+
const tmuxKeys = new Set();
|
|
111
|
+
const tmuxSessionIds = new Set();
|
|
112
|
+
const paneKeys = new Map();
|
|
113
|
+
|
|
114
|
+
for (const entry of activeEntries) {
|
|
115
|
+
const linkedSessionId = String(entry.agent && entry.agent.linkedSessionId || "");
|
|
116
|
+
if (linkedSessionId && liveById.has(linkedSessionId)) {
|
|
117
|
+
tmuxSessionIds.add(linkedSessionId);
|
|
118
|
+
tmuxKeys.add(`session:${linkedSessionId}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const paneId = String(entry.pane.nativeId || entry.pane.id || "");
|
|
122
|
+
const distroId = String(entry.distro && (entry.distro.id || entry.distro.name) || "");
|
|
123
|
+
if (paneId) {
|
|
124
|
+
const key = `pane:${distroId}:${paneId}`;
|
|
125
|
+
tmuxKeys.add(key);
|
|
126
|
+
paneKeys.set(String(entry.pane.id || ""), key);
|
|
127
|
+
paneKeys.set(String(entry.pane.nativeId || ""), key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const session of liveNodes) {
|
|
132
|
+
const presence = (session.runtimePresence || []).find((item) => item.kind === "tmux");
|
|
133
|
+
if (!presence) continue;
|
|
134
|
+
tmuxSessionIds.add(session.id);
|
|
135
|
+
const paneKey = [presence.paneId, presence.paneNativeId, presence.nativeId]
|
|
136
|
+
.map((value) => paneKeys.get(String(value || "")))
|
|
137
|
+
.find(Boolean);
|
|
138
|
+
tmuxKeys.add(paneKey || `session:${session.id}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const standardCount = liveNodes.filter((session) => !tmuxSessionIds.has(session.id)).length;
|
|
142
|
+
const tmuxCount = tmuxKeys.size;
|
|
143
|
+
return {
|
|
144
|
+
activeCount: standardCount + tmuxCount,
|
|
145
|
+
standardCount,
|
|
146
|
+
tmuxCount,
|
|
147
|
+
activeHelperCount: liveNodes.filter((session) => session.parentId).length,
|
|
148
|
+
helperRecordCount: model.nodes.filter((session) => session.parentId).length,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { graphPath, connectedGraphSessions, sortGraphNodes, graphChildren, agentExecutionMode, executionModeBadge, graphDescendantCount, runtimeAgentSummary };
|
|
107
153
|
};
|
|
@@ -13,8 +13,8 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
|
|
|
13
13
|
graphPath,
|
|
14
14
|
connectedGraphSessions,
|
|
15
15
|
sortGraphNodes,
|
|
16
|
-
agentExecutionMode,
|
|
17
16
|
liveTmuxEntries,
|
|
17
|
+
runtimeAgentSummary,
|
|
18
18
|
runtimeSeparatedOverview,
|
|
19
19
|
focusedGraph,
|
|
20
20
|
scheduleAgentWorkflowConnections,
|
|
@@ -50,24 +50,25 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
|
|
|
50
50
|
scheduleAgentWorkflowConnections();
|
|
51
51
|
} else {
|
|
52
52
|
const tmuxEntries = liveTmuxEntries();
|
|
53
|
-
const
|
|
54
|
-
const activeCount = model.nodes.filter(isLiveSession).length + tmuxEntries.filter((entry) => !entry.agent.linkedSessionId).length;
|
|
53
|
+
const runtime = runtimeAgentSummary(model, tmuxEntries);
|
|
55
54
|
$("#liveSessionGrid").innerHTML = `<details class="runtime-disclosure" open>
|
|
56
55
|
<summary>
|
|
57
56
|
<span>
|
|
58
|
-
<b>${activeCount}개 AI가 작업 중입니다</b>
|
|
59
|
-
<small>일반
|
|
57
|
+
<b>${runtime.activeCount}개 AI가 작업 중입니다</b>
|
|
58
|
+
<small>일반 실행 AI ${runtime.standardCount}개와 TMUX AI ${runtime.tmuxCount}개를 합산했습니다. 작업 중 도움 AI ${runtime.activeHelperCount}개는 이 수에 포함됩니다.</small>
|
|
60
59
|
</span>
|
|
61
60
|
<em>상세 흐름 보기 <i aria-hidden="true">↓</i>
|
|
62
61
|
</em>
|
|
63
62
|
</summary>${runtimeSeparatedOverview(roots, model)}</details>`;
|
|
64
63
|
$("#graphBreadcrumbs").innerHTML =
|
|
65
64
|
`<span class="map-hint">
|
|
66
|
-
일반 실행 <b>${
|
|
67
|
-
TMUX AI <b>${
|
|
68
|
-
<b>${
|
|
65
|
+
일반 실행 AI <b>${runtime.standardCount}</b>개 ·
|
|
66
|
+
TMUX AI <b>${runtime.tmuxCount}</b>개 ·
|
|
67
|
+
작업 중 도움 AI <b>${runtime.activeHelperCount}</b>개 ·
|
|
68
|
+
도움 AI 기록 <b>${runtime.helperRecordCount}</b>개
|
|
69
69
|
</span>`;
|
|
70
70
|
$("#graphResetBtn").classList.add("hidden");
|
|
71
|
+
return runtime.activeCount;
|
|
71
72
|
}
|
|
72
73
|
return model.nodes.filter(isLiveSession).length + liveTmuxEntries().filter((entry) => !entry.agent.linkedSessionId).length;
|
|
73
74
|
}
|