loadtoagent 1.0.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/README.ko.md +175 -0
- package/README.md +175 -0
- package/README.zh-CN.md +175 -0
- package/bin/loadtoagent.js +171 -0
- package/docs/ARCHITECTURE.md +30 -0
- package/docs/PROVIDER-CONTRACTS.md +58 -0
- package/docs/assets/loadtoagent-dashboard.png +0 -0
- package/docs/assets/loadtoagent-demo.gif +0 -0
- package/main.js +493 -0
- package/package.json +133 -0
- package/preload.js +75 -0
- package/renderer/app-agent-actions.js +285 -0
- package/renderer/app-bootstrap.js +95 -0
- package/renderer/app-dashboard.js +361 -0
- package/renderer/app-drawer-content.js +203 -0
- package/renderer/app-drawer-data.js +41 -0
- package/renderer/app-drawer.js +183 -0
- package/renderer/app-events-dialogs.js +169 -0
- package/renderer/app-events-filters.js +74 -0
- package/renderer/app-events-navigation.js +96 -0
- package/renderer/app-events-sessions.js +195 -0
- package/renderer/app-events.js +16 -0
- package/renderer/app-graph-layout.js +71 -0
- package/renderer/app-graph-model.js +107 -0
- package/renderer/app-graph-orchestration.js +76 -0
- package/renderer/app-graph-view.js +544 -0
- package/renderer/app-run-modal.js +221 -0
- package/renderer/app-session-render.js +243 -0
- package/renderer/app-tmux-render.js +247 -0
- package/renderer/app.js +608 -0
- package/renderer/fonts/geist-ext.woff2 +0 -0
- package/renderer/fonts/geist.woff2 +0 -0
- package/renderer/i18n-messages.js +355 -0
- package/renderer/i18n.js +122 -0
- package/renderer/index.html +386 -0
- package/renderer/shared.js +26 -0
- package/renderer/styles-agent-map.css +401 -0
- package/renderer/styles-cards.css +600 -0
- package/renderer/styles-collaboration.css +534 -0
- package/renderer/styles-components.css +1293 -0
- package/renderer/styles-onboarding.css +344 -0
- package/renderer/styles-overlays.css +284 -0
- package/renderer/styles-product.css +686 -0
- package/renderer/styles-responsive-product.css +689 -0
- package/renderer/styles-responsive-runtime.css +718 -0
- package/renderer/styles-responsive-shell.css +533 -0
- package/renderer/styles-responsive-workflows.css +778 -0
- package/renderer/styles-run-composer.css +695 -0
- package/renderer/styles-settings.css +606 -0
- package/renderer/styles-terminal.css +1241 -0
- package/renderer/styles-tmux.css +1162 -0
- package/renderer/styles-workflow-map.css +513 -0
- package/renderer/styles-workflows.css +1217 -0
- package/renderer/styles.css +486 -0
- package/renderer/terminal-agent.js +152 -0
- package/renderer/terminal-events.js +226 -0
- package/renderer/terminal-workbench.js +464 -0
- package/renderer/terminal.js +497 -0
- package/src/agentMonitor/claudeParser.js +197 -0
- package/src/agentMonitor/codexCollaboration.js +95 -0
- package/src/agentMonitor/codexParser.js +447 -0
- package/src/agentMonitor/genericParser.js +244 -0
- package/src/agentMonitor/hierarchy.js +248 -0
- package/src/agentMonitor/sessionFiles.js +86 -0
- package/src/agentMonitor.js +591 -0
- package/src/agentRunner.js +465 -0
- package/src/bridgeServer.js +246 -0
- package/src/contracts.js +71 -0
- package/src/diagnostics.js +23 -0
- package/src/ipc/registerAgentIpc.js +12 -0
- package/src/ipc/registerAppIpc.js +20 -0
- package/src/ipc/registerTerminalIpc.js +50 -0
- package/src/ipc/registerTmuxIpc.js +30 -0
- package/src/ipc/registerWorkspaceIpc.js +14 -0
- package/src/monitorWorker.js +273 -0
- package/src/processMonitor.js +394 -0
- package/src/providerRegistry.js +120 -0
- package/src/terminalManager.js +438 -0
- package/src/tmuxController.js +183 -0
- package/src/tmuxMonitor.js +432 -0
- package/src/updateManager.js +339 -0
- package/src/workspaceStore.js +77 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
(() => {
|
|
4
|
+
const { $, esc, uiLocale, providerLabel, reportRecoverableError } = window.LoadToAgentRendererUtils;
|
|
5
|
+
const SESSION_ORDER_KEY = 'loadtoagent:terminal-session-order:v1';
|
|
6
|
+
|
|
7
|
+
function loadSessionOrder() {
|
|
8
|
+
try {
|
|
9
|
+
const value = JSON.parse(localStorage.getItem(SESSION_ORDER_KEY) || '[]');
|
|
10
|
+
return Array.isArray(value) ? value.filter(id => typeof id === 'string' && id) : [];
|
|
11
|
+
} catch (error) {
|
|
12
|
+
reportRecoverableError('terminal-session-order-load', error);
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const state = {
|
|
18
|
+
sessions: [],
|
|
19
|
+
selectedId: null,
|
|
20
|
+
selectedTmux: null,
|
|
21
|
+
snapshot: null,
|
|
22
|
+
workspaces: [],
|
|
23
|
+
wslDistros: [],
|
|
24
|
+
active: false,
|
|
25
|
+
terminals: new Map(),
|
|
26
|
+
remoteTerminal: null,
|
|
27
|
+
remoteCapture: '',
|
|
28
|
+
remoteViewportAnchor: null,
|
|
29
|
+
remoteViewportAtBottom: false,
|
|
30
|
+
captureTimer: null,
|
|
31
|
+
captureInFlight: false,
|
|
32
|
+
captureGeneration: 0,
|
|
33
|
+
captureRevision: 0,
|
|
34
|
+
resizeObserver: null,
|
|
35
|
+
initialized: false,
|
|
36
|
+
eventsBound: false,
|
|
37
|
+
initPromise: null,
|
|
38
|
+
mode: 'general',
|
|
39
|
+
boundAgent: null,
|
|
40
|
+
boundTargetId: '',
|
|
41
|
+
historyCollapsed: false,
|
|
42
|
+
historyRefreshTimer: null,
|
|
43
|
+
historyRequests: new Map(),
|
|
44
|
+
commandDrafts: new Map(),
|
|
45
|
+
commandSending: false,
|
|
46
|
+
sessionOrder: loadSessionOrder(),
|
|
47
|
+
draggedSessionId: '',
|
|
48
|
+
sessionDragJustEnded: false,
|
|
49
|
+
platform: { id: 'win32', label: 'Windows', localShell: 'powershell', localShellLabel: 'Windows 명령창', nativeTmux: false },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const STATUS_LABELS = {
|
|
53
|
+
starting: window.LoadToAgentI18n.t("ui.preparing"),
|
|
54
|
+
running: '세션 유지 중',
|
|
55
|
+
exited: '끝남',
|
|
56
|
+
failed: '열지 못함',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function notice(message, tone = '') {
|
|
60
|
+
const element = $('#terminalNotice');
|
|
61
|
+
if (!element) return;
|
|
62
|
+
element.innerHTML = `<span class="terminal-notice-dot"></span><span>${esc(message)}</span>`;
|
|
63
|
+
element.dataset.tone = tone;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function errorMessage(error) {
|
|
67
|
+
return error && error.message ? error.message : String(error || '알 수 없는 오류');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function persistSessionOrder() {
|
|
71
|
+
try {
|
|
72
|
+
localStorage.setItem(SESSION_ORDER_KEY, JSON.stringify(state.sessionOrder));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
reportRecoverableError('terminal-session-order-save', error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizedSessionOrder() {
|
|
79
|
+
const currentIds = state.sessions.map(session => session.id);
|
|
80
|
+
const validIds = new Set(currentIds);
|
|
81
|
+
const next = [
|
|
82
|
+
...state.sessionOrder.filter(id => validIds.has(id)),
|
|
83
|
+
...currentIds.filter(id => !state.sessionOrder.includes(id)),
|
|
84
|
+
];
|
|
85
|
+
if (next.length !== state.sessionOrder.length || next.some((id, index) => id !== state.sessionOrder[index])) {
|
|
86
|
+
state.sessionOrder = next;
|
|
87
|
+
persistSessionOrder();
|
|
88
|
+
}
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function reorderSession(sourceId, targetId, placeAfter = false) {
|
|
93
|
+
if (!sourceId || !targetId || sourceId === targetId) return false;
|
|
94
|
+
const order = normalizedSessionOrder().filter(id => id !== sourceId);
|
|
95
|
+
const targetIndex = order.indexOf(targetId);
|
|
96
|
+
if (targetIndex < 0) return false;
|
|
97
|
+
order.splice(targetIndex + (placeAfter ? 1 : 0), 0, sourceId);
|
|
98
|
+
state.sessionOrder = order;
|
|
99
|
+
persistSessionOrder();
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function moveSessionByOffset(sessionId, offset) {
|
|
104
|
+
const visible = modeSessions('general');
|
|
105
|
+
const currentIndex = visible.findIndex(session => session.id === sessionId);
|
|
106
|
+
const target = visible[currentIndex + offset];
|
|
107
|
+
if (currentIndex < 0 || !target) return false;
|
|
108
|
+
return reorderSession(sessionId, target.id, offset > 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function timeLabel(value) {
|
|
112
|
+
const date = new Date(value || 0);
|
|
113
|
+
return Number.isFinite(date.getTime()) ? date.toLocaleTimeString(uiLocale(), { hour: '2-digit', minute: '2-digit' }) : '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function historyMessageHtml(value) {
|
|
117
|
+
const blocks = [];
|
|
118
|
+
let fenced = false;
|
|
119
|
+
let code = [];
|
|
120
|
+
for (const line of String(value || '').replace(/\r\n/g, '\n').split('\n')) {
|
|
121
|
+
if (/^```/.test(line)) {
|
|
122
|
+
if (fenced) { blocks.push(`<pre><code>${esc(code.join('\n'))}</code></pre>`); code = []; fenced = false; } else fenced = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (fenced) { code.push(line); continue; }
|
|
126
|
+
const bullet = line.match(/^\s*[-*]\s+(.+)$/);
|
|
127
|
+
const safe = esc(bullet ? bullet[1] : line).replace(/`([^`]+)`/g, '<code>$1</code>').replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
128
|
+
if (bullet) blocks.push(`<p class="bullet">${safe}</p>`);
|
|
129
|
+
else if (line.trim()) blocks.push(`<p>${safe}</p>`);
|
|
130
|
+
else blocks.push('<br>');
|
|
131
|
+
}
|
|
132
|
+
if (fenced) blocks.push(`<pre><code>${esc(code.join('\n'))}</code></pre>`);
|
|
133
|
+
return `<div class="terminal-history-copy">${blocks.join('')}</div>`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resumeSupport(agentSession) {
|
|
137
|
+
if (!agentSession) return { supported: false, reason: '세션 정보가 없습니다.' };
|
|
138
|
+
const sessionId = String(agentSession.externalId || '').trim();
|
|
139
|
+
if (!sessionId) return { supported: false, reason: '재개에 필요한 세션 ID가 기록되지 않았습니다.' };
|
|
140
|
+
const provider = String(agentSession.provider || '').toLowerCase();
|
|
141
|
+
if (!['codex', 'claude', 'gemini'].includes(provider)) {
|
|
142
|
+
return { supported: false, reason: `${providerLabel(provider)} CLI의 세션 ID 재개 방식이 공식 문서에서 확인되지 않았습니다.` };
|
|
143
|
+
}
|
|
144
|
+
const args = provider === 'codex' ? ['resume', sessionId] : ['--resume', sessionId];
|
|
145
|
+
return { supported: true, provider, sessionId, args };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resumeLaunchArgs(support, prompt = '') {
|
|
149
|
+
const args = [...support.args];
|
|
150
|
+
const text = String(prompt || '').trim();
|
|
151
|
+
if (text) args.push(text);
|
|
152
|
+
return args;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function terminalTypeLabel(session) {
|
|
156
|
+
if (!session) return '터미널';
|
|
157
|
+
if (session.type === 'wsl') return session.distro || 'Linux';
|
|
158
|
+
if (session.type === 'agent') return providerLabel(session.provider);
|
|
159
|
+
if (session.type === 'powershell') return 'PowerShell';
|
|
160
|
+
if (session.type === 'cmd') return '명령 프롬프트';
|
|
161
|
+
if (session.type === 'shell') return session.shell || 'Shell';
|
|
162
|
+
return String(session.type || '터미널').toUpperCase();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function terminalTypeMark(session) {
|
|
166
|
+
if (!session) return '›_';
|
|
167
|
+
if (session.type === 'wsl') return 'WSL';
|
|
168
|
+
if (session.type === 'agent') return 'AI';
|
|
169
|
+
if (session.type === 'powershell') return 'PS';
|
|
170
|
+
if (session.type === 'cmd') return 'CMD';
|
|
171
|
+
if (session.type === 'shell') return 'SH';
|
|
172
|
+
return '›_';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function setConnectionState(label, status = '') {
|
|
176
|
+
const element = $('#terminalConnectionState');
|
|
177
|
+
if (!element) return;
|
|
178
|
+
element.innerHTML = `<i></i><span>${esc(label)}</span>`;
|
|
179
|
+
element.dataset.status = status;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function currentTargetId() {
|
|
183
|
+
const session = currentSession();
|
|
184
|
+
if (session) return session.id;
|
|
185
|
+
const remote = currentTmux();
|
|
186
|
+
return remote ? `tmux:${remote.distro.name}:${remote.pane.nativeId}` : '';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function visibleBoundAgent() {
|
|
190
|
+
return state.boundAgent && state.boundTargetId === currentTargetId() ? state.boundAgent : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function saveCurrentDraft() {
|
|
194
|
+
const targetId = currentTargetId();
|
|
195
|
+
const input = $('#terminalCommandInput');
|
|
196
|
+
if (targetId && input) state.commandDrafts.set(targetId, input.value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function restoreCurrentDraft() {
|
|
200
|
+
const input = $('#terminalCommandInput');
|
|
201
|
+
if (input) input.value = state.commandDrafts.get(currentTargetId()) || '';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderHistoryPanel(forceBottom = false) {
|
|
205
|
+
const panel = $('#terminalHistoryPanel');
|
|
206
|
+
if (!panel) return;
|
|
207
|
+
const agent = visibleBoundAgent();
|
|
208
|
+
panel.classList.toggle('hidden', !agent);
|
|
209
|
+
panel.classList.toggle('collapsed', Boolean(agent && state.historyCollapsed));
|
|
210
|
+
$('#terminalStage')?.classList.toggle('history-collapsed', Boolean(agent && state.historyCollapsed));
|
|
211
|
+
const toggle = $('#terminalHistoryToggle');
|
|
212
|
+
if (toggle) {
|
|
213
|
+
toggle.setAttribute('aria-expanded', state.historyCollapsed ? 'false' : 'true');
|
|
214
|
+
toggle.textContent = state.historyCollapsed ? '›' : '‹';
|
|
215
|
+
toggle.title = state.historyCollapsed ? '대화 영역 펼치기' : window.LoadToAgentI18n.t("ui.collapse_conversation_panel");
|
|
216
|
+
}
|
|
217
|
+
if (!agent) return;
|
|
218
|
+
const allMessages = Array.isArray(agent.messages) ? agent.messages.filter(message => message && message.text) : [];
|
|
219
|
+
const messages = allMessages.filter(message => message.role === 'user' || message.role === 'assistant');
|
|
220
|
+
const activityCount = allMessages.length - messages.length;
|
|
221
|
+
const shown = messages.slice(-80);
|
|
222
|
+
$('#terminalHistoryTitle').textContent = agent.title || `${providerLabel(agent.provider)} 세션`;
|
|
223
|
+
$('#terminalHistoryMeta').textContent = [
|
|
224
|
+
providerLabel(agent.provider),
|
|
225
|
+
window.LoadToAgentI18n.t('session.messages', { count: messages.length }),
|
|
226
|
+
activityCount ? window.LoadToAgentI18n.t('terminal.activity_details', { count: activityCount }) : '',
|
|
227
|
+
messages.length > shown.length ? window.LoadToAgentI18n.t('session.latest_count', { count: shown.length }) : '',
|
|
228
|
+
].filter(Boolean).join(' · ');
|
|
229
|
+
const list = $('#terminalHistoryList');
|
|
230
|
+
const nearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 90;
|
|
231
|
+
list.innerHTML = shown.length ? shown.map(message => {
|
|
232
|
+
const role = message.role === 'assistant' ? 'assistant' : (message.role === 'tool' ? 'tool' : (message.role === 'system' ? 'system' : 'user'));
|
|
233
|
+
const label = role === 'assistant' ? providerLabel(agent.provider) : (role === 'tool' ? (message.title || '도구') : (role === 'system' ? '시스템' : '나'));
|
|
234
|
+
return `<article class="terminal-history-message ${role}"><header><b>${esc(label)}</b><time>${esc(timeLabel(message.timestamp))}</time></header>${historyMessageHtml(message.text)}</article>`;
|
|
235
|
+
}).join('') : '<div class="terminal-history-empty"><b>아직 표시할 대화가 없습니다</b><span>터미널 출력은 오른쪽에 그대로 유지됩니다.</span></div>';
|
|
236
|
+
if (forceBottom || nearBottom) requestAnimationFrame(() => { list.scrollTop = list.scrollHeight; });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function bindAgent(agentSession, target) {
|
|
240
|
+
state.boundAgent = agentSession || null;
|
|
241
|
+
state.boundTargetId = target && target.id || '';
|
|
242
|
+
state.historyCollapsed = false;
|
|
243
|
+
renderHistoryPanel(true);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function queueHistoryRefresh(cardSession) {
|
|
247
|
+
if (!state.boundAgent || !cardSession || cardSession.id !== state.boundAgent.id) return;
|
|
248
|
+
const currentMessages = state.boundAgent.messages || [];
|
|
249
|
+
const cardMessages = cardSession.messages || [];
|
|
250
|
+
const messages = cardMessages.length && (!currentMessages.length || Date.parse(cardSession.updatedAt || 0) >= Date.parse(state.boundAgent.updatedAt || 0)) ? cardMessages : currentMessages;
|
|
251
|
+
state.boundAgent = { ...state.boundAgent, ...cardSession, messages };
|
|
252
|
+
renderHistoryPanel();
|
|
253
|
+
clearTimeout(state.historyRefreshTimer);
|
|
254
|
+
state.historyRefreshTimer = setTimeout(async () => {
|
|
255
|
+
if (!state.boundAgent) return;
|
|
256
|
+
const id = state.boundAgent.id;
|
|
257
|
+
if (state.historyRequests.has(id)) return;
|
|
258
|
+
const request = window.loadtoagent.sessionDetail(id);
|
|
259
|
+
state.historyRequests.set(id, request);
|
|
260
|
+
try {
|
|
261
|
+
const detail = await request;
|
|
262
|
+
if (detail && state.boundAgent && state.boundAgent.id === id) {
|
|
263
|
+
state.boundAgent = detail;
|
|
264
|
+
renderHistoryPanel();
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
reportRecoverableError("terminal-history-refresh", error);
|
|
268
|
+
} finally {
|
|
269
|
+
state.historyRequests.delete(id);
|
|
270
|
+
}
|
|
271
|
+
}, 240);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function guarded(action, successMessage = '') {
|
|
275
|
+
try {
|
|
276
|
+
const result = await action();
|
|
277
|
+
if (successMessage) notice(successMessage, 'success');
|
|
278
|
+
return result;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
notice(errorMessage(error), 'error');
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function currentSession() {
|
|
286
|
+
const session = state.sessions.find(item => item.id === state.selectedId) || null;
|
|
287
|
+
if (!session) return null;
|
|
288
|
+
return state.mode === 'tmux' ? (session.type === 'tmux' ? session : null) : (session.type !== 'tmux' ? session : null);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function currentTmux() {
|
|
292
|
+
if (state.mode !== 'tmux') return null;
|
|
293
|
+
if (!state.selectedTmux) return null;
|
|
294
|
+
const match = tmuxRows().find(row => row.distro.name === state.selectedTmux.distro.name && row.pane.nativeId === state.selectedTmux.pane.nativeId);
|
|
295
|
+
state.selectedTmux = match || null;
|
|
296
|
+
return state.selectedTmux;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function preferredWorkspace() {
|
|
300
|
+
return state.workspaces[0] && state.workspaces[0].path || '';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function modeSessions(mode = state.mode) {
|
|
304
|
+
const rank = new Map(normalizedSessionOrder().map((id, index) => [id, index]));
|
|
305
|
+
return state.sessions
|
|
306
|
+
.filter(session => mode === 'tmux' ? session.type === 'tmux' : session.type !== 'tmux')
|
|
307
|
+
.sort((left, right) => (rank.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(right.id) ?? Number.MAX_SAFE_INTEGER));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function moveWorkbench(mode = state.mode) {
|
|
311
|
+
const workbench = $('#terminalWorkbench');
|
|
312
|
+
const mount = mode === 'tmux' ? $('#tmuxWorkbenchMount') : $('#terminalWorkbenchMount');
|
|
313
|
+
if (workbench && mount && workbench.parentElement !== mount) mount.appendChild(workbench);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function firstDistro() {
|
|
317
|
+
const name = state.wslDistros[0];
|
|
318
|
+
if (name) return { name };
|
|
319
|
+
return state.snapshot && state.snapshot.tmux && state.snapshot.tmux.distros && state.snapshot.tmux.distros[0] || null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function configurePlatform() {
|
|
323
|
+
const localButton = $('#newPowerShellBtn');
|
|
324
|
+
const linuxButton = $('#newWslBtn');
|
|
325
|
+
if (localButton) localButton.textContent = `+ ${state.platform.localShellLabel.replace('명령창', '세션')}`;
|
|
326
|
+
if (linuxButton) linuxButton.classList.toggle('hidden', state.platform.id !== 'win32');
|
|
327
|
+
const explain = $('#terminalPlatformExplain');
|
|
328
|
+
if (explain) explain.textContent = state.platform.id === 'win32'
|
|
329
|
+
? '기존 Windows·WSL 터미널 세션을 유지한 채 같은 화면에서 계속 입력합니다. AI 카드에서 열면 이전 대화도 함께 표시됩니다.'
|
|
330
|
+
: `기존 ${state.platform.label} 터미널 세션을 유지한 채 계속 입력합니다. AI 카드에서 열면 이전 대화도 함께 표시됩니다.`;
|
|
331
|
+
const environmentLabel = $('#tmuxEnvironmentLabel');
|
|
332
|
+
if (environmentLabel) environmentLabel.textContent = state.platform.nativeTmux ? '로컬 tmux 환경' : 'WSL 환경';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function xtermOptions(readOnly = false) {
|
|
336
|
+
return {
|
|
337
|
+
allowProposedApi: false,
|
|
338
|
+
cursorBlink: !readOnly,
|
|
339
|
+
cursorStyle: 'bar',
|
|
340
|
+
disableStdin: readOnly,
|
|
341
|
+
convertEol: readOnly,
|
|
342
|
+
screenReaderMode: true,
|
|
343
|
+
fontFamily: '"Cascadia Code", Consolas, monospace',
|
|
344
|
+
fontSize: 13,
|
|
345
|
+
lineHeight: 1.22,
|
|
346
|
+
scrollback: 10_000,
|
|
347
|
+
theme: {
|
|
348
|
+
background: '#080c12',
|
|
349
|
+
foreground: '#dce6ef',
|
|
350
|
+
cursor: '#5de5ba',
|
|
351
|
+
cursorAccent: '#080c12',
|
|
352
|
+
selectionBackground: '#28475c',
|
|
353
|
+
black: '#0c1119', red: '#ff7486', green: '#5de5a5', yellow: '#f4c66a',
|
|
354
|
+
blue: '#68aef5', magenta: '#c790f5', cyan: '#60dbea', white: '#d6e0e9',
|
|
355
|
+
brightBlack: '#657286', brightRed: '#ff93a1', brightGreen: '#83efbd', brightYellow: '#f7d891',
|
|
356
|
+
brightBlue: '#90c3fa', brightMagenta: '#d9b1fa', brightCyan: '#8ae8f1', brightWhite: '#f4f7fa',
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const {
|
|
362
|
+
createXtermHost, fitEntry, ensureSessionTerminal, ensureRemoteTerminal, hideScreens, renderSessions, renderTmuxResources, renderTarget, showSelection, selectSession, selectTmux, selectTmuxById, renderAll, refreshSessions, createTerminal, captureRemote, startCapture, stopCapture, sendCommand, sendSignal, openTmuxModal, closeTmuxModal, refreshSnapshot, attachTmux, manageTmux,
|
|
363
|
+
} = window.LoadToAgentTerminalWorkbench({
|
|
364
|
+
$, state, notice, setConnectionState, currentSession, currentTmux, saveCurrentDraft, restoreCurrentDraft,
|
|
365
|
+
renderHistoryPanel, terminalTypeMark, terminalTypeLabel, xtermOptions, preferredWorkspace, firstDistro, guarded,
|
|
366
|
+
esc, errorMessage, modeSessions, STATUS_LABELS, visibleBoundAgent, moveWorkbench,
|
|
367
|
+
tmuxRows: (...args) => tmuxRows(...args),
|
|
368
|
+
updateSnapshot: (...args) => updateSnapshot(...args),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const {
|
|
372
|
+
tmuxRows, agentTargets, requiredAgentTarget, dispatchAgentCommand, openForAgent, resumeForAgent,
|
|
373
|
+
} = window.LoadToAgentTerminalAgentActions({
|
|
374
|
+
$, state, init, notice, moveWorkbench, selectTmux, selectSession, bindAgent, queueHistoryRefresh,
|
|
375
|
+
renderTarget, fitEntry, refreshSessions, resumeSupport, resumeLaunchArgs, preferredWorkspace, providerLabel, esc,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
function bindEvents() {
|
|
379
|
+
window.LoadToAgentTerminalEvents({
|
|
380
|
+
$,
|
|
381
|
+
state,
|
|
382
|
+
createTerminal,
|
|
383
|
+
openTmuxModal,
|
|
384
|
+
refreshSnapshot,
|
|
385
|
+
selectSession,
|
|
386
|
+
selectTmux,
|
|
387
|
+
sendCommand,
|
|
388
|
+
currentTargetId,
|
|
389
|
+
sendSignal,
|
|
390
|
+
currentSession,
|
|
391
|
+
guarded,
|
|
392
|
+
renderAll,
|
|
393
|
+
showSelection,
|
|
394
|
+
refreshSessions,
|
|
395
|
+
renderHistoryPanel,
|
|
396
|
+
fitEntry,
|
|
397
|
+
attachTmux,
|
|
398
|
+
currentTmux,
|
|
399
|
+
manageTmux,
|
|
400
|
+
closeTmuxModal,
|
|
401
|
+
errorMessage,
|
|
402
|
+
notice,
|
|
403
|
+
reorderSession,
|
|
404
|
+
moveSessionByOffset,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function activate(snapshot, workspaces, mode = 'general') {
|
|
409
|
+
state.active = true;
|
|
410
|
+
state.mode = mode === 'tmux' ? 'tmux' : 'general';
|
|
411
|
+
moveWorkbench(state.mode);
|
|
412
|
+
if (state.mode === 'general') state.selectedTmux = null;
|
|
413
|
+
if (state.selectedId && !modeSessions().some(item => item.id === state.selectedId)) state.selectedId = null;
|
|
414
|
+
updateSnapshot(snapshot, workspaces);
|
|
415
|
+
if (!state.initialized) return;
|
|
416
|
+
await refreshSessions();
|
|
417
|
+
if (!state.selectedId && !state.selectedTmux) {
|
|
418
|
+
const visible = modeSessions();
|
|
419
|
+
if (visible.length) state.selectedId = visible[0].id;
|
|
420
|
+
else if (state.mode === 'tmux') state.selectedTmux = tmuxRows()[0] || null;
|
|
421
|
+
}
|
|
422
|
+
renderAll();
|
|
423
|
+
await showSelection();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function deactivate() {
|
|
427
|
+
state.active = false;
|
|
428
|
+
stopCapture();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function updateSnapshot(snapshot, workspaces = state.workspaces) {
|
|
432
|
+
state.snapshot = snapshot || state.snapshot;
|
|
433
|
+
state.workspaces = Array.isArray(workspaces) ? workspaces : state.workspaces;
|
|
434
|
+
if (!state.initialized) return;
|
|
435
|
+
if (state.boundAgent && state.snapshot && Array.isArray(state.snapshot.sessions)) {
|
|
436
|
+
const updated = state.snapshot.sessions.find(session => session.id === state.boundAgent.id);
|
|
437
|
+
if (updated && updated.updatedAt !== state.boundAgent.updatedAt) queueHistoryRefresh(updated);
|
|
438
|
+
}
|
|
439
|
+
renderTmuxResources();
|
|
440
|
+
renderTarget();
|
|
441
|
+
if (state.active && state.selectedTmux) startCapture();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function scrollTmuxToLine(line) {
|
|
445
|
+
if (!state.remoteTerminal) return false;
|
|
446
|
+
const target = Math.max(0, Math.floor(Number(line) || 0));
|
|
447
|
+
state.remoteTerminal.terminal.scrollToLine(target);
|
|
448
|
+
const buffer = state.remoteTerminal.terminal.buffer.active;
|
|
449
|
+
state.remoteViewportAnchor = Number(buffer.viewportY) || 0;
|
|
450
|
+
state.remoteViewportAtBottom = state.remoteViewportAnchor >= Number(buffer.baseY || 0);
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function init() {
|
|
455
|
+
if (state.initPromise) return state.initPromise;
|
|
456
|
+
state.initPromise = (async () => {
|
|
457
|
+
if (!window.loadtoagent) return;
|
|
458
|
+
if (!state.eventsBound) {
|
|
459
|
+
bindEvents();
|
|
460
|
+
state.eventsBound = true;
|
|
461
|
+
}
|
|
462
|
+
const [bootstrap, sessions, environments] = await Promise.all([window.loadtoagent.bootstrap(), window.loadtoagent.terminalList(), window.loadtoagent.wslDistros()]);
|
|
463
|
+
state.platform = bootstrap.platform || state.platform;
|
|
464
|
+
state.sessions = Array.isArray(sessions) ? sessions : [];
|
|
465
|
+
state.wslDistros = Array.isArray(environments) ? environments : [];
|
|
466
|
+
state.initialized = true;
|
|
467
|
+
configurePlatform();
|
|
468
|
+
renderAll();
|
|
469
|
+
})().catch(error => {
|
|
470
|
+
state.initialized = false;
|
|
471
|
+
state.initPromise = null;
|
|
472
|
+
throw error;
|
|
473
|
+
});
|
|
474
|
+
return state.initPromise;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
window.LoadToAgentTerminal = {
|
|
478
|
+
activate,
|
|
479
|
+
deactivate,
|
|
480
|
+
updateSnapshot,
|
|
481
|
+
refresh: refreshSessions,
|
|
482
|
+
selectTmuxById,
|
|
483
|
+
openTmuxModal,
|
|
484
|
+
agentTargets,
|
|
485
|
+
resumeSupport,
|
|
486
|
+
dispatchAgentCommand,
|
|
487
|
+
openForAgent,
|
|
488
|
+
resumeForAgent,
|
|
489
|
+
scrollTmuxToLine,
|
|
490
|
+
};
|
|
491
|
+
window.addEventListener('loadtoagent:locale-changed', () => {
|
|
492
|
+
if (!state.initialized) return;
|
|
493
|
+
configurePlatform();
|
|
494
|
+
renderAll();
|
|
495
|
+
});
|
|
496
|
+
init().catch(error => notice(`명령창 준비 실패: ${errorMessage(error)}`, 'error'));
|
|
497
|
+
})();
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function createClaudeParser(dependencies) {
|
|
6
|
+
const {
|
|
7
|
+
ACTIVE_THRESHOLD_MS,
|
|
8
|
+
STALE_TURN_THRESHOLD_MS,
|
|
9
|
+
addLifecycle,
|
|
10
|
+
addMessage,
|
|
11
|
+
baseSession,
|
|
12
|
+
compactText,
|
|
13
|
+
contextInfo,
|
|
14
|
+
finalizeUsage,
|
|
15
|
+
modelContextWindow,
|
|
16
|
+
readJsonLines,
|
|
17
|
+
settleLifecycle,
|
|
18
|
+
sumUsage,
|
|
19
|
+
timestamp,
|
|
20
|
+
trimSession,
|
|
21
|
+
} = dependencies;
|
|
22
|
+
|
|
23
|
+
function normalizeUsage(raw = {}) {
|
|
24
|
+
return finalizeUsage({
|
|
25
|
+
input: raw.input_tokens,
|
|
26
|
+
cachedInput: raw.cache_read_input_tokens,
|
|
27
|
+
cacheWrite: raw.cache_creation_input_tokens,
|
|
28
|
+
output: raw.output_tokens,
|
|
29
|
+
reasoning: raw.reasoning_tokens,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function visibleUserText(value) {
|
|
34
|
+
const raw = compactText(value, 12000);
|
|
35
|
+
if (!raw) return '';
|
|
36
|
+
const objective = raw.match(/<objective>\s*([\s\S]*?)\s*<\/objective>/i);
|
|
37
|
+
if (objective) return compactText(objective[1], 6000);
|
|
38
|
+
if (/^<(?:local-command-[^>]+|command-name|command-message|system-reminder|task-notification)>/i.test(raw)) return '';
|
|
39
|
+
if (/^Extract durable memory candidates from this Claude Code transcript tail/i.test(raw)) return '';
|
|
40
|
+
if (/^(?:Updated task #\d+|Your questions have been answered:)/i.test(raw)) return '';
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function recordContent(session, row, item, index) {
|
|
45
|
+
const kind = item && item.type;
|
|
46
|
+
const id = `${row.uuid || row.requestId || session.externalId}:${index}`;
|
|
47
|
+
if (kind === 'text' && item.text) {
|
|
48
|
+
const text = row.message.role === 'user' ? visibleUserText(item.text) : item.text;
|
|
49
|
+
if (text) addMessage(session, { id, role: row.message.role, text, timestamp: row.timestamp });
|
|
50
|
+
} else if (kind === 'tool_use') {
|
|
51
|
+
const name = item.name || 'tool';
|
|
52
|
+
addMessage(session, {
|
|
53
|
+
id,
|
|
54
|
+
role: 'tool',
|
|
55
|
+
type: 'tool',
|
|
56
|
+
title: name,
|
|
57
|
+
text: compactText(item.input
|
|
58
|
+
&& (item.input.command || item.input.description || item.input.prompt || JSON.stringify(item.input)), 1200),
|
|
59
|
+
status: 'started',
|
|
60
|
+
timestamp: row.timestamp,
|
|
61
|
+
});
|
|
62
|
+
addLifecycle(session, {
|
|
63
|
+
id: `tool:${item.id || id}`,
|
|
64
|
+
type: 'tool',
|
|
65
|
+
label: name,
|
|
66
|
+
detail: compactText(item.input, 260),
|
|
67
|
+
status: 'running',
|
|
68
|
+
timestamp: row.timestamp,
|
|
69
|
+
});
|
|
70
|
+
} else if (kind === 'tool_result') {
|
|
71
|
+
settleLifecycle(session, item.tool_use_id, item.is_error ? 'failed' : 'done', row.timestamp);
|
|
72
|
+
addLifecycle(session, {
|
|
73
|
+
id: `result:${item.tool_use_id || id}`,
|
|
74
|
+
type: 'tool-result',
|
|
75
|
+
label: item.is_error ? '도구 실패' : '도구 완료',
|
|
76
|
+
status: item.is_error ? 'failed' : 'done',
|
|
77
|
+
timestamp: row.timestamp,
|
|
78
|
+
});
|
|
79
|
+
} else if (kind === 'thinking') {
|
|
80
|
+
addLifecycle(session, {
|
|
81
|
+
id,
|
|
82
|
+
type: 'reasoning',
|
|
83
|
+
label: '추론',
|
|
84
|
+
status: 'done',
|
|
85
|
+
timestamp: row.timestamp,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function initializeSession(fileInfo, parsed) {
|
|
91
|
+
const basename = path.basename(fileInfo.file, '.jsonl');
|
|
92
|
+
const subMatch = fileInfo.file.match(/[\\/]([^\\/]+)[\\/]subagents[\\/]agent-([^\\/]+)\.jsonl$/i);
|
|
93
|
+
const externalId = subMatch ? subMatch[2] : basename;
|
|
94
|
+
const session = baseSession('claude', externalId, fileInfo.file, fileInfo);
|
|
95
|
+
session.truncated = parsed.truncated;
|
|
96
|
+
session.parentId = subMatch ? `claude:${subMatch[1]}` : null;
|
|
97
|
+
session.depth = subMatch ? 1 : 0;
|
|
98
|
+
session.agentName = subMatch ? `agent-${subMatch[2].slice(0, 8)}` : '';
|
|
99
|
+
const desktopSignals = new Set(parsed.rows.map(row => String(row && row.type || '')).filter(Boolean));
|
|
100
|
+
const isDesktop = !subMatch
|
|
101
|
+
&& (desktopSignals.has('queue-operation') || desktopSignals.has('last-prompt') || desktopSignals.has('ai-title'));
|
|
102
|
+
session.clientKind = isDesktop ? 'claude-desktop' : 'claude-cli';
|
|
103
|
+
if (isDesktop) session.sourceLabel = 'Claude 데스크톱 앱';
|
|
104
|
+
return session;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function processMessageRow(session, state, row) {
|
|
108
|
+
const role = row.message.role === 'assistant' ? 'assistant' : 'user';
|
|
109
|
+
state.lastRole = role;
|
|
110
|
+
if (row.message.model) session.model = row.message.model;
|
|
111
|
+
const content = Array.isArray(row.message.content)
|
|
112
|
+
? row.message.content
|
|
113
|
+
: [{ type: 'text', text: row.message.content }];
|
|
114
|
+
content.forEach((item, index) => recordContent(session, row, item, index));
|
|
115
|
+
if (role === 'user') {
|
|
116
|
+
const visibleUser = visibleUserText(content
|
|
117
|
+
.filter(item => !item.type || item.type === 'text')
|
|
118
|
+
.map(item => typeof item === 'string' ? item : item.text)
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.join('\n'));
|
|
121
|
+
if (visibleUser) state.latestUser = visibleUser;
|
|
122
|
+
}
|
|
123
|
+
if (role === 'assistant' && row.message.usage) {
|
|
124
|
+
const key = row.requestId || row.message.id || row.uuid;
|
|
125
|
+
const usage = normalizeUsage(row.message.usage);
|
|
126
|
+
const previous = state.requestUsage.get(key);
|
|
127
|
+
if (!previous || usage.total >= previous.total) state.requestUsage.set(key, usage);
|
|
128
|
+
session.turnUsage = usage;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function processRows(session, rows) {
|
|
133
|
+
const state = {
|
|
134
|
+
requestUsage: new Map(),
|
|
135
|
+
latestUser: '',
|
|
136
|
+
lastRole: '',
|
|
137
|
+
latestTs: session.updatedAt,
|
|
138
|
+
lastTurnFinished: false,
|
|
139
|
+
};
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
state.latestTs = timestamp(row.timestamp, state.latestTs);
|
|
142
|
+
if (row.cwd && !session.cwd) session.cwd = row.cwd;
|
|
143
|
+
if (row.gitBranch) session.branch = row.gitBranch;
|
|
144
|
+
if (row.agentId && session.depth) session.agentName = row.agentId;
|
|
145
|
+
if (row.type === 'system' && row.subtype === 'init') {
|
|
146
|
+
session.model = row.model || session.model;
|
|
147
|
+
addLifecycle(session, {
|
|
148
|
+
id: row.uuid,
|
|
149
|
+
type: 'session-start',
|
|
150
|
+
label: '세션 시작',
|
|
151
|
+
status: 'done',
|
|
152
|
+
timestamp: row.timestamp,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (row.type === 'system' && /turn_duration|turn_complete|stop/i.test(String(row.subtype || ''))) {
|
|
156
|
+
state.lastTurnFinished = true;
|
|
157
|
+
}
|
|
158
|
+
if (row.message && row.message.role) processMessageRow(session, state, row);
|
|
159
|
+
}
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function finalizeSession(session, state, parsed, fileInfo) {
|
|
164
|
+
session.updatedAt = state.latestTs;
|
|
165
|
+
session.startedAt = timestamp(parsed.rows[0].timestamp, session.updatedAt);
|
|
166
|
+
session.usage = sumUsage([...state.requestUsage.values()]);
|
|
167
|
+
session.title = compactText(state.latestUser, 180)
|
|
168
|
+
|| (session.depth ? `Claude ${session.agentName}` : 'Claude 세션');
|
|
169
|
+
const currentInput = session.turnUsage.input + session.turnUsage.cachedInput
|
|
170
|
+
+ session.turnUsage.cacheWrite + session.turnUsage.output + session.turnUsage.reasoning;
|
|
171
|
+
session.context = contextInfo(currentInput, modelContextWindow('claude', session.model, 0));
|
|
172
|
+
const age = Date.now() - fileInfo.mtimeMs;
|
|
173
|
+
if (age < ACTIVE_THRESHOLD_MS && !state.lastTurnFinished) {
|
|
174
|
+
session.status = 'running';
|
|
175
|
+
session.statusDetail = state.lastRole === 'user' ? '응답 생성 중' : '도구 실행 또는 스트리밍 중';
|
|
176
|
+
} else if (state.lastRole === 'user' && age < STALE_TURN_THRESHOLD_MS) {
|
|
177
|
+
session.status = 'waiting';
|
|
178
|
+
session.statusDetail = '응답 또는 권한 확인 필요';
|
|
179
|
+
} else {
|
|
180
|
+
session.status = 'idle';
|
|
181
|
+
session.statusDetail = state.lastRole === 'user' ? '마지막 응답 기록이 종료됨' : '다음 요청 대기';
|
|
182
|
+
}
|
|
183
|
+
session.statusObserved = age < ACTIVE_THRESHOLD_MS;
|
|
184
|
+
trimSession(session);
|
|
185
|
+
return session;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return function parseClaude(fileInfo) {
|
|
189
|
+
const parsed = readJsonLines(fileInfo.file);
|
|
190
|
+
if (!parsed.rows.length) return null;
|
|
191
|
+
const session = initializeSession(fileInfo, parsed);
|
|
192
|
+
const state = processRows(session, parsed.rows);
|
|
193
|
+
return finalizeSession(session, state, parsed, fileInfo);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { createClaudeParser };
|