loadtoagent 1.1.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +7 -0
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/docs/PROVIDER-CONTRACTS.md +23 -1
- package/docs/UI-AUDIT-100.md +118 -0
- package/docs/UI-AUDIT-101-200.md +120 -0
- package/docs/UI-AUDIT-201-300.md +120 -0
- package/main.js +135 -38
- package/package.json +13 -4
- package/preload.js +6 -0
- package/renderer/app-agent-actions.js +125 -53
- package/renderer/app-bootstrap.js +54 -19
- package/renderer/app-dashboard.js +190 -55
- package/renderer/app-drawer-content.js +48 -44
- package/renderer/app-drawer-data.js +26 -13
- package/renderer/app-drawer.js +72 -54
- package/renderer/app-events-dialogs.js +146 -37
- package/renderer/app-events-filters.js +172 -13
- package/renderer/app-events-navigation.js +77 -30
- package/renderer/app-events-sessions.js +156 -9
- package/renderer/app-events.js +2 -1
- package/renderer/app-graph-model.js +20 -38
- package/renderer/app-graph-orchestration.js +15 -13
- package/renderer/app-graph-view.js +228 -113
- package/renderer/app-management.js +211 -0
- package/renderer/app-provider-visibility.js +124 -0
- package/renderer/app-quality.js +568 -0
- package/renderer/app-run-modal.js +103 -33
- package/renderer/app-runtime-overview.js +312 -0
- package/renderer/app-session-render.js +94 -37
- package/renderer/app-tmux-render.js +52 -44
- package/renderer/app.js +153 -75
- package/renderer/i18n-messages.js +833 -16
- package/renderer/i18n.js +104 -0
- package/renderer/index.html +145 -59
- package/renderer/shared.js +17 -0
- package/renderer/styles-agent-map.css +6 -0
- package/renderer/styles-cards.css +26 -0
- package/renderer/styles-components.css +86 -9
- package/renderer/styles-management.css +355 -0
- package/renderer/styles-overlays.css +8 -2
- package/renderer/styles-product.css +24 -16
- package/renderer/styles-quality.css +312 -0
- package/renderer/styles-readability.css +862 -0
- package/renderer/styles-responsive-product.css +84 -12
- package/renderer/styles-responsive-runtime.css +22 -14
- package/renderer/styles-responsive-shell.css +44 -48
- package/renderer/styles-responsive-workflows.css +37 -3
- package/renderer/styles-run-composer.css +5 -0
- package/renderer/styles-runtime-overview.css +285 -0
- package/renderer/styles-settings.css +160 -0
- package/renderer/styles-terminal.css +339 -2
- package/renderer/styles-workflow-map.css +362 -15
- package/renderer/styles-workflows.css +69 -2
- package/renderer/styles.css +27 -12
- package/renderer/terminal-agent.js +17 -13
- package/renderer/terminal-events.js +233 -40
- package/renderer/terminal-workbench.js +165 -77
- package/renderer/terminal.js +341 -34
- package/src/agentMonitor/claudeParser.js +96 -7
- package/src/agentMonitor/codexParser.js +59 -4
- package/src/agentMonitor/executionActivity.js +282 -0
- package/src/agentMonitor/genericParser.js +56 -13
- package/src/agentMonitor/hierarchy.js +17 -0
- package/src/agentMonitor/responseIntent.js +51 -0
- package/src/agentMonitor.js +21 -3
- package/src/agentRunner.js +66 -1
- package/src/attentionNotifier.js +14 -10
- package/src/automationMonitor.js +258 -0
- package/src/contracts.js +10 -1
- package/src/ipc/registerAgentIpc.js +9 -3
- package/src/ipc/registerAppIpc.js +4 -1
- package/src/ipc/registerTerminalIpc.js +12 -6
- package/src/macUpdateHelper.js +210 -0
- package/src/monitorWorker.js +65 -6
- package/src/platformPath.js +80 -0
- package/src/processMonitor.js +80 -22
- package/src/providerVisibilityStore.js +45 -0
- package/src/sessionIntelligence.js +247 -0
- package/src/terminalHost.js +381 -0
- package/src/terminalHostDaemon.js +60 -0
- package/src/terminalManager.js +158 -3
- package/src/updateInstaller.js +175 -0
package/renderer/terminal.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
(() => {
|
|
4
4
|
const { $, esc, uiLocale, providerLabel, reportRecoverableError } = window.LoadToAgentRendererUtils;
|
|
5
|
+
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
5
6
|
const SESSION_ORDER_KEY = 'loadtoagent:terminal-session-order:v1';
|
|
7
|
+
const TERMINAL_VIEW_KEY = 'loadtoagent:terminal-view:v1';
|
|
6
8
|
|
|
7
9
|
function loadSessionOrder() {
|
|
8
10
|
try {
|
|
@@ -14,6 +16,19 @@
|
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
function loadTerminalViewPreferences() {
|
|
20
|
+
try {
|
|
21
|
+
const saved = JSON.parse(localStorage.getItem(TERMINAL_VIEW_KEY) || '{}');
|
|
22
|
+
const fontSize = Math.min(20, Math.max(12, Number(saved.fontSize) || 15));
|
|
23
|
+
return { fontSize };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
reportRecoverableError('terminal-view-load', error);
|
|
26
|
+
return { fontSize: 15 };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const terminalViewPreferences = loadTerminalViewPreferences();
|
|
31
|
+
|
|
17
32
|
const state = {
|
|
18
33
|
sessions: [],
|
|
19
34
|
selectedId: null,
|
|
@@ -27,6 +42,7 @@
|
|
|
27
42
|
remoteCapture: '',
|
|
28
43
|
remoteViewportAnchor: null,
|
|
29
44
|
remoteViewportAtBottom: false,
|
|
45
|
+
remoteCaptureApplying: false,
|
|
30
46
|
captureTimer: null,
|
|
31
47
|
captureInFlight: false,
|
|
32
48
|
captureGeneration: 0,
|
|
@@ -41,20 +57,37 @@
|
|
|
41
57
|
historyCollapsed: false,
|
|
42
58
|
historyRefreshTimer: null,
|
|
43
59
|
historyRequests: new Map(),
|
|
60
|
+
historyRenderKey: '',
|
|
61
|
+
historyPointerActive: false,
|
|
62
|
+
historyUserRevision: 0,
|
|
63
|
+
historyFollowFrame: 0,
|
|
64
|
+
historyRenderPending: false,
|
|
65
|
+
historyFlushFrame: 0,
|
|
44
66
|
commandDrafts: new Map(),
|
|
67
|
+
commandHistory: new Map(),
|
|
68
|
+
commandHistoryNavigation: { targetId: '', index: -1, draft: '' },
|
|
45
69
|
commandSending: false,
|
|
70
|
+
pendingActions: new Set(),
|
|
46
71
|
sessionOrder: loadSessionOrder(),
|
|
72
|
+
sessionRenderKey: '',
|
|
47
73
|
draggedSessionId: '',
|
|
48
74
|
sessionDragJustEnded: false,
|
|
49
|
-
|
|
75
|
+
terminalFontSize: terminalViewPreferences.fontSize,
|
|
76
|
+
terminalFocusMode: false,
|
|
77
|
+
platform: { id: 'win32', label: 'Windows', localShell: 'powershell', localShellLabel: 'Windows Terminal', nativeTmux: false },
|
|
50
78
|
};
|
|
51
79
|
|
|
52
|
-
const STATUS_LABELS = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
const STATUS_LABELS = {};
|
|
81
|
+
|
|
82
|
+
function refreshStatusLabels() {
|
|
83
|
+
Object.assign(STATUS_LABELS, {
|
|
84
|
+
starting: t('ui.preparing'),
|
|
85
|
+
running: t('terminal.status.running'),
|
|
86
|
+
exited: t('terminal.status.exited'),
|
|
87
|
+
failed: t('terminal.status.failed'),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
refreshStatusLabels();
|
|
58
91
|
|
|
59
92
|
function notice(message, tone = '') {
|
|
60
93
|
const element = $('#terminalNotice');
|
|
@@ -64,7 +97,7 @@
|
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
function errorMessage(error) {
|
|
67
|
-
return
|
|
100
|
+
return window.LoadToAgentI18n.errorText(error, 'terminal.error.unknown');
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
function persistSessionOrder() {
|
|
@@ -134,12 +167,12 @@
|
|
|
134
167
|
}
|
|
135
168
|
|
|
136
169
|
function resumeSupport(agentSession) {
|
|
137
|
-
if (!agentSession) return { supported: false, reason: '
|
|
170
|
+
if (!agentSession) return { supported: false, reason: t('terminal.resume.no_session_info') };
|
|
138
171
|
const sessionId = String(agentSession.externalId || '').trim();
|
|
139
|
-
if (!sessionId) return { supported: false, reason: '
|
|
172
|
+
if (!sessionId) return { supported: false, reason: t('terminal.resume.no_session_id') };
|
|
140
173
|
const provider = String(agentSession.provider || '').toLowerCase();
|
|
141
174
|
if (!['codex', 'claude', 'gemini'].includes(provider)) {
|
|
142
|
-
return { supported: false, reason:
|
|
175
|
+
return { supported: false, reason: t('terminal.resume.unsupported_provider', { provider: providerLabel(provider) }) };
|
|
143
176
|
}
|
|
144
177
|
const args = provider === 'codex' ? ['resume', sessionId] : ['--resume', sessionId];
|
|
145
178
|
return { supported: true, provider, sessionId, args };
|
|
@@ -153,13 +186,13 @@
|
|
|
153
186
|
}
|
|
154
187
|
|
|
155
188
|
function terminalTypeLabel(session) {
|
|
156
|
-
if (!session) return '
|
|
189
|
+
if (!session) return t('terminal.type.terminal');
|
|
157
190
|
if (session.type === 'wsl') return session.distro || 'Linux';
|
|
158
191
|
if (session.type === 'agent') return providerLabel(session.provider);
|
|
159
192
|
if (session.type === 'powershell') return 'PowerShell';
|
|
160
|
-
if (session.type === 'cmd') return '
|
|
193
|
+
if (session.type === 'cmd') return t('terminal.type.command_prompt');
|
|
161
194
|
if (session.type === 'shell') return session.shell || 'Shell';
|
|
162
|
-
return String(session.type || '
|
|
195
|
+
return String(session.type || t('terminal.type.terminal')).toUpperCase();
|
|
163
196
|
}
|
|
164
197
|
|
|
165
198
|
function terminalTypeMark(session) {
|
|
@@ -179,6 +212,58 @@
|
|
|
179
212
|
element.dataset.status = status;
|
|
180
213
|
}
|
|
181
214
|
|
|
215
|
+
function persistTerminalViewPreferences() {
|
|
216
|
+
try {
|
|
217
|
+
localStorage.setItem(TERMINAL_VIEW_KEY, JSON.stringify({ fontSize: state.terminalFontSize }));
|
|
218
|
+
} catch (error) {
|
|
219
|
+
reportRecoverableError('terminal-view-save', error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function syncTerminalViewControls() {
|
|
224
|
+
const section = $('#terminalSection');
|
|
225
|
+
const focusButton = $('#terminalFocusBtn');
|
|
226
|
+
section?.classList.toggle('terminal-focus-mode', state.terminalFocusMode);
|
|
227
|
+
$('#terminalResourcePanel')?.toggleAttribute('inert', state.terminalFocusMode);
|
|
228
|
+
$('#terminalHistoryPanel')?.toggleAttribute('inert', state.terminalFocusMode);
|
|
229
|
+
if ($('#terminalFontSizeLabel')) $('#terminalFontSizeLabel').textContent = `${state.terminalFontSize}px`;
|
|
230
|
+
if ($('#terminalFontDecreaseBtn')) $('#terminalFontDecreaseBtn').disabled = state.terminalFontSize <= 12;
|
|
231
|
+
if ($('#terminalFontIncreaseBtn')) $('#terminalFontIncreaseBtn').disabled = state.terminalFontSize >= 20;
|
|
232
|
+
if (focusButton) {
|
|
233
|
+
focusButton.setAttribute('aria-pressed', state.terminalFocusMode ? 'true' : 'false');
|
|
234
|
+
focusButton.title = state.terminalFocusMode ? t('terminal.view.focus_exit') : t('terminal.view.focus');
|
|
235
|
+
const label = focusButton.querySelector('span');
|
|
236
|
+
if (label) label.textContent = state.terminalFocusMode ? t('terminal.view.focus_exit') : t('terminal.view.focus');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function refitVisibleTerminal() {
|
|
241
|
+
const session = currentSession();
|
|
242
|
+
const entry = session ? state.terminals.get(session.id) : state.remoteTerminal;
|
|
243
|
+
fitEntry(entry, session?.id || '');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function setTerminalFontSize(value) {
|
|
247
|
+
const next = Math.min(20, Math.max(12, Math.round(Number(value) || 15)));
|
|
248
|
+
if (next === state.terminalFontSize) return;
|
|
249
|
+
state.terminalFontSize = next;
|
|
250
|
+
for (const entry of [...state.terminals.values(), state.remoteTerminal].filter(Boolean)) {
|
|
251
|
+
entry.terminal.options.fontSize = next;
|
|
252
|
+
entry.terminal.options.lineHeight = next >= 17 ? 1.32 : 1.28;
|
|
253
|
+
}
|
|
254
|
+
persistTerminalViewPreferences();
|
|
255
|
+
syncTerminalViewControls();
|
|
256
|
+
refitVisibleTerminal();
|
|
257
|
+
window.LoadToAgentA11y?.announce(t('terminal.view.font_size_value', { size: next }));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function toggleTerminalFocusMode() {
|
|
261
|
+
state.terminalFocusMode = !state.terminalFocusMode;
|
|
262
|
+
syncTerminalViewControls();
|
|
263
|
+
requestAnimationFrame(refitVisibleTerminal);
|
|
264
|
+
window.LoadToAgentA11y?.announce(state.terminalFocusMode ? t('terminal.view.focus_on') : t('terminal.view.focus_off'));
|
|
265
|
+
}
|
|
266
|
+
|
|
182
267
|
function currentTargetId() {
|
|
183
268
|
const session = currentSession();
|
|
184
269
|
if (session) return session.id;
|
|
@@ -187,7 +272,9 @@
|
|
|
187
272
|
}
|
|
188
273
|
|
|
189
274
|
function visibleBoundAgent() {
|
|
190
|
-
|
|
275
|
+
if (!state.boundAgent || state.boundTargetId !== currentTargetId()) return null;
|
|
276
|
+
if (window.LoadToAgentApp?.isProviderVisible?.(state.boundAgent.provider) === false) return null;
|
|
277
|
+
return state.boundAgent;
|
|
191
278
|
}
|
|
192
279
|
|
|
193
280
|
function saveCurrentDraft() {
|
|
@@ -198,7 +285,144 @@
|
|
|
198
285
|
|
|
199
286
|
function restoreCurrentDraft() {
|
|
200
287
|
const input = $('#terminalCommandInput');
|
|
201
|
-
if (input)
|
|
288
|
+
if (input) {
|
|
289
|
+
input.value = state.commandDrafts.get(currentTargetId()) || '';
|
|
290
|
+
$('#terminalCommandClearBtn')?.classList.toggle('hidden', !input.value);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function observedProgressText(value) {
|
|
295
|
+
const observed = window.LoadToAgentI18n.observedText
|
|
296
|
+
? window.LoadToAgentI18n.observedText(value)
|
|
297
|
+
: String(value || '');
|
|
298
|
+
const readable = window.LoadToAgentApp?.readableActivityDetail?.(observed);
|
|
299
|
+
return String(readable || observed || '').replace(/\s+/g, ' ').trim();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function terminalSubagentRecords(root) {
|
|
303
|
+
const sessions = Array.isArray(state.snapshot?.sessions) ? state.snapshot.sessions : [];
|
|
304
|
+
const byId = new Map(sessions.map(session => [session.id, session]));
|
|
305
|
+
// A detail refresh can contain newer root collaboration events than the card
|
|
306
|
+
// in the snapshot, so keep it as the authoritative root record.
|
|
307
|
+
byId.set(root.id, root);
|
|
308
|
+
const records = [];
|
|
309
|
+
const seen = new Set([root.id]);
|
|
310
|
+
const visit = (parent, depth) => {
|
|
311
|
+
const declaredIds = Array.isArray(parent?.childIds) ? parent.childIds : [];
|
|
312
|
+
const inferredIds = sessions
|
|
313
|
+
.filter(session => session?.parentId === parent?.id)
|
|
314
|
+
.map(session => session.id);
|
|
315
|
+
for (const id of [...new Set([...declaredIds, ...inferredIds])]) {
|
|
316
|
+
if (!id || seen.has(id)) continue;
|
|
317
|
+
const session = byId.get(id);
|
|
318
|
+
if (!session) continue;
|
|
319
|
+
seen.add(id);
|
|
320
|
+
records.push({ session, parent, depth });
|
|
321
|
+
visit(session, depth + 1);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
visit(root, 1);
|
|
325
|
+
|
|
326
|
+
return records.map(record => {
|
|
327
|
+
const { session, parent, depth } = record;
|
|
328
|
+
const taskName = session.taskName || session.delegation?.taskName || session.title || '';
|
|
329
|
+
const communications = Array.isArray(parent?.collaboration?.communications)
|
|
330
|
+
? parent.collaboration.communications
|
|
331
|
+
: [];
|
|
332
|
+
const related = communications
|
|
333
|
+
.filter(event => event && !event.protected)
|
|
334
|
+
.filter(event => ['assignment', 'started', 'followup', 'message', 'result', 'interrupt'].includes(event.kind))
|
|
335
|
+
.filter(event => event.childId === session.id || (taskName && event.taskName === taskName));
|
|
336
|
+
const assignmentEvent = [...related].reverse().find(event => event.kind === 'assignment' && observedProgressText(event.text));
|
|
337
|
+
const delegation = session.delegation || {};
|
|
338
|
+
const assignment = delegation.assignmentObserved && !delegation.assignmentProtected && observedProgressText(delegation.assignment)
|
|
339
|
+
? observedProgressText(delegation.assignment)
|
|
340
|
+
: (assignmentEvent ? observedProgressText(assignmentEvent.text) : observedProgressText(session.title || taskName));
|
|
341
|
+
const lifecycle = (Array.isArray(session.lifecycle) ? session.lifecycle : [])
|
|
342
|
+
.filter(item => item && (item.label || item.detail))
|
|
343
|
+
.map((item, index) => ({
|
|
344
|
+
id: item.id || `lifecycle:${index}`,
|
|
345
|
+
kind: item.type || 'activity',
|
|
346
|
+
label: observedProgressText(item.label || t('ui.progress')),
|
|
347
|
+
detail: observedProgressText(item.detail),
|
|
348
|
+
timestamp: item.timestamp || session.updatedAt || '',
|
|
349
|
+
}));
|
|
350
|
+
const coordination = related
|
|
351
|
+
.filter(event => event.kind !== 'assignment')
|
|
352
|
+
.map(event => ({
|
|
353
|
+
id: event.id || `${event.kind}:${event.timestamp || ''}`,
|
|
354
|
+
kind: event.kind,
|
|
355
|
+
label: observedProgressText(event.label || event.kind),
|
|
356
|
+
detail: event.kind === 'started' && String(event.text || '').trim().toLowerCase() === 'started'
|
|
357
|
+
? ''
|
|
358
|
+
: observedProgressText(event.text),
|
|
359
|
+
timestamp: event.timestamp || session.updatedAt || '',
|
|
360
|
+
}));
|
|
361
|
+
const events = [...lifecycle, ...coordination]
|
|
362
|
+
.sort((left, right) => (Date.parse(left.timestamp || 0) || 0) - (Date.parse(right.timestamp || 0) || 0))
|
|
363
|
+
.filter((event, index, items) => index === items.findIndex(candidate => (
|
|
364
|
+
candidate.kind === event.kind
|
|
365
|
+
&& candidate.label === event.label
|
|
366
|
+
&& candidate.detail === event.detail
|
|
367
|
+
&& candidate.timestamp === event.timestamp
|
|
368
|
+
)))
|
|
369
|
+
.slice(-4);
|
|
370
|
+
const workState = window.LoadToAgentApp?.subagentWorkState?.(session)
|
|
371
|
+
|| (session.status === 'running' || session.status === 'starting' ? 'working' : (session.status === 'failed' ? 'attention' : 'resting'));
|
|
372
|
+
const stateLabel = window.LoadToAgentApp?.subagentWorkLabel?.(session)
|
|
373
|
+
|| ({ working: t('ui.working'), attention: t('ui.needs_attention'), resting: t('ui.idle') })[workState];
|
|
374
|
+
return {
|
|
375
|
+
session,
|
|
376
|
+
depth,
|
|
377
|
+
taskName,
|
|
378
|
+
assignment,
|
|
379
|
+
events,
|
|
380
|
+
workState,
|
|
381
|
+
stateLabel,
|
|
382
|
+
name: observedProgressText(session.agentName || taskName || session.title || providerLabel(session.provider)),
|
|
383
|
+
role: observedProgressText(session.agentRole || providerLabel(session.provider)),
|
|
384
|
+
statusDetail: observedProgressText(session.statusDetail),
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function terminalSubagentProgress(root) {
|
|
390
|
+
const records = terminalSubagentRecords(root);
|
|
391
|
+
if (!records.length) return { html: '', key: [], count: 0, working: 0, attention: 0 };
|
|
392
|
+
const working = records.filter(record => record.workState === 'working').length;
|
|
393
|
+
const attention = records.filter(record => record.workState === 'attention').length;
|
|
394
|
+
const summary = [
|
|
395
|
+
t('tmux.subagents.connected_count', { count: records.length }),
|
|
396
|
+
working ? t('tmux.subagents.working_count', { count: working }) : t('tmux.subagents.all_idle'),
|
|
397
|
+
attention ? t('tmux.subagents.attention_count', { count: attention }) : '',
|
|
398
|
+
].filter(Boolean).join(' · ');
|
|
399
|
+
const cards = records.map(record => {
|
|
400
|
+
const events = record.events.length
|
|
401
|
+
? record.events.map(event => `<li data-terminal-subagent-event="${esc(event.kind)}">
|
|
402
|
+
<i></i><div><b>${esc(event.label || t('ui.progress'))}</b>${event.detail ? `<span>${esc(event.detail)}</span>` : ''}</div><time>${esc(timeLabel(event.timestamp))}</time>
|
|
403
|
+
</li>`).join('')
|
|
404
|
+
: `<li class="empty"><i></i><span>${esc(t('terminal.subagents.no_updates'))}</span></li>`;
|
|
405
|
+
return `<article class="terminal-subagent-card ${esc(record.workState)}" data-terminal-subagent-id="${esc(record.session.id)}" data-terminal-subagent-state="${esc(record.workState)}" data-terminal-subagent-depth="${record.depth}" style="--terminal-subagent-depth:${Math.min(record.depth - 1, 3)}">
|
|
406
|
+
<header><div><span>${record.depth > 1 ? '↳ ' : ''}${esc(record.name)}</span><small>${esc(record.role)}</small></div><b>${esc(record.stateLabel)}</b></header>
|
|
407
|
+
<div class="terminal-subagent-assignment"><em>${esc(t('drawer.assignment'))}</em><span>${esc(record.assignment || t('tmux.subagents.checking_assignment'))}</span></div>
|
|
408
|
+
${record.statusDetail ? `<div class="terminal-subagent-current"><em>${esc(t('terminal.subagents.current'))}</em><span>${esc(record.statusDetail)}</span></div>` : ''}
|
|
409
|
+
<ol>${events}</ol>
|
|
410
|
+
</article>`;
|
|
411
|
+
}).join('');
|
|
412
|
+
return {
|
|
413
|
+
html: `<section class="terminal-subagent-progress" data-terminal-subagent-progress data-terminal-subagent-count="${records.length}">
|
|
414
|
+
<header><div><b>${esc(t('terminal.subagents.title'))}</b><span>${esc(t('terminal.subagents.description'))}</span></div><small>${esc(summary)}</small></header>
|
|
415
|
+
<div class="terminal-subagent-cards">${cards}</div>
|
|
416
|
+
</section>`,
|
|
417
|
+
key: records.map(record => [
|
|
418
|
+
record.session.id, record.session.parentId || '', record.depth, record.workState,
|
|
419
|
+
record.name, record.role, record.assignment, record.statusDetail,
|
|
420
|
+
record.events.map(event => [event.id, event.kind, event.label, event.detail, event.timestamp]),
|
|
421
|
+
]),
|
|
422
|
+
count: records.length,
|
|
423
|
+
working,
|
|
424
|
+
attention,
|
|
425
|
+
};
|
|
202
426
|
}
|
|
203
427
|
|
|
204
428
|
function renderHistoryPanel(forceBottom = false) {
|
|
@@ -212,28 +436,72 @@
|
|
|
212
436
|
if (toggle) {
|
|
213
437
|
toggle.setAttribute('aria-expanded', state.historyCollapsed ? 'false' : 'true');
|
|
214
438
|
toggle.textContent = state.historyCollapsed ? '›' : '‹';
|
|
215
|
-
toggle.title = state.historyCollapsed ? '
|
|
439
|
+
toggle.title = state.historyCollapsed ? t('terminal.history.expand') : t('ui.collapse_conversation_panel');
|
|
216
440
|
}
|
|
217
441
|
if (!agent) return;
|
|
218
442
|
const allMessages = Array.isArray(agent.messages) ? agent.messages.filter(message => message && message.text) : [];
|
|
219
443
|
const messages = allMessages.filter(message => message.role === 'user' || message.role === 'assistant');
|
|
220
444
|
const activityCount = allMessages.length - messages.length;
|
|
221
445
|
const shown = messages.slice(-80);
|
|
222
|
-
|
|
446
|
+
const subagents = terminalSubagentProgress(agent);
|
|
447
|
+
$('#terminalHistoryTitle').textContent = agent.title || t('terminal.history.provider_session', { provider: providerLabel(agent.provider) });
|
|
223
448
|
$('#terminalHistoryMeta').textContent = [
|
|
224
449
|
providerLabel(agent.provider),
|
|
225
450
|
window.LoadToAgentI18n.t('session.messages', { count: messages.length }),
|
|
451
|
+
subagents.count ? t('tmux.subagents.connected_count', { count: subagents.count }) : '',
|
|
226
452
|
activityCount ? window.LoadToAgentI18n.t('terminal.activity_details', { count: activityCount }) : '',
|
|
227
453
|
messages.length > shown.length ? window.LoadToAgentI18n.t('session.latest_count', { count: shown.length }) : '',
|
|
228
454
|
].filter(Boolean).join(' · ');
|
|
229
455
|
const list = $('#terminalHistoryList');
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
456
|
+
const previousTop = list.scrollTop;
|
|
457
|
+
const wasAtBottom = window.LoadToAgentRendererUtils.isScrolledToEnd(list);
|
|
458
|
+
const renderKey = JSON.stringify([
|
|
459
|
+
agent.id,
|
|
460
|
+
uiLocale(),
|
|
461
|
+
providerLabel(agent.provider),
|
|
462
|
+
subagents.key,
|
|
463
|
+
shown.map(message => [message.id || '', message.role || '', message.title || '', message.timestamp || '', message.text || '']),
|
|
464
|
+
]);
|
|
465
|
+
const contentChanged = state.historyRenderKey !== renderKey;
|
|
466
|
+
const selection = window.getSelection?.();
|
|
467
|
+
const selectionInside = Boolean(selection && !selection.isCollapsed && (
|
|
468
|
+
(selection.anchorNode && list.contains(selection.anchorNode))
|
|
469
|
+
|| (selection.focusNode && list.contains(selection.focusNode))
|
|
470
|
+
));
|
|
471
|
+
if (contentChanged && !forceBottom && (state.historyPointerActive || selectionInside)) {
|
|
472
|
+
state.historyRenderPending = true;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
state.historyRenderPending = false;
|
|
476
|
+
if (contentChanged) {
|
|
477
|
+
const conversationHtml = shown.length ? shown.map(message => {
|
|
478
|
+
const role = message.role === 'assistant' ? 'assistant' : (message.role === 'tool' ? 'tool' : (message.role === 'system' ? 'system' : 'user'));
|
|
479
|
+
const label = role === 'assistant' ? providerLabel(agent.provider) : (role === 'tool' ? (message.title || t('terminal.history.tool')) : (role === 'system' ? t('terminal.history.system') : t('terminal.history.me')));
|
|
480
|
+
return `<article class="terminal-history-message ${role}"><header><b>${esc(label)}</b><time>${esc(timeLabel(message.timestamp))}</time></header>${historyMessageHtml(message.text)}</article>`;
|
|
481
|
+
}).join('') : `<div class="terminal-history-empty${subagents.count ? ' with-subagents' : ''}"><b>${t('terminal.history.empty_title')}</b><span>${t('terminal.history.empty_description')}</span></div>`;
|
|
482
|
+
list.innerHTML = `${subagents.html}${conversationHtml}`;
|
|
483
|
+
state.historyRenderKey = renderKey;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const shouldFollow = forceBottom || (contentChanged && wasAtBottom && !state.historyPointerActive && !selectionInside);
|
|
487
|
+
if (state.historyFollowFrame) cancelAnimationFrame(state.historyFollowFrame);
|
|
488
|
+
state.historyFollowFrame = 0;
|
|
489
|
+
if (!contentChanged && !forceBottom) return;
|
|
490
|
+
if (!shouldFollow) {
|
|
491
|
+
list.scrollTop = Math.min(previousTop, Math.max(0, list.scrollHeight - list.clientHeight));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const revision = state.historyUserRevision;
|
|
495
|
+
state.historyFollowFrame = requestAnimationFrame(() => {
|
|
496
|
+
state.historyFollowFrame = 0;
|
|
497
|
+
if (revision !== state.historyUserRevision || state.historyPointerActive) return;
|
|
498
|
+
const activeSelection = window.getSelection?.();
|
|
499
|
+
if (activeSelection && !activeSelection.isCollapsed && (
|
|
500
|
+
(activeSelection.anchorNode && list.contains(activeSelection.anchorNode))
|
|
501
|
+
|| (activeSelection.focusNode && list.contains(activeSelection.focusNode))
|
|
502
|
+
)) return;
|
|
503
|
+
list.scrollTop = list.scrollHeight;
|
|
504
|
+
});
|
|
237
505
|
}
|
|
238
506
|
|
|
239
507
|
function bindAgent(agentSession, target) {
|
|
@@ -271,7 +539,9 @@
|
|
|
271
539
|
}, 240);
|
|
272
540
|
}
|
|
273
541
|
|
|
274
|
-
async function guarded(action, successMessage = '') {
|
|
542
|
+
async function guarded(action, successMessage = '', actionKey = '') {
|
|
543
|
+
if (actionKey && state.pendingActions.has(actionKey)) return null;
|
|
544
|
+
if (actionKey) state.pendingActions.add(actionKey);
|
|
275
545
|
try {
|
|
276
546
|
const result = await action();
|
|
277
547
|
if (successMessage) notice(successMessage, 'success');
|
|
@@ -279,6 +549,8 @@
|
|
|
279
549
|
} catch (error) {
|
|
280
550
|
notice(errorMessage(error), 'error');
|
|
281
551
|
return null;
|
|
552
|
+
} finally {
|
|
553
|
+
if (actionKey) state.pendingActions.delete(actionKey);
|
|
282
554
|
}
|
|
283
555
|
}
|
|
284
556
|
|
|
@@ -303,6 +575,8 @@
|
|
|
303
575
|
function modeSessions(mode = state.mode) {
|
|
304
576
|
const rank = new Map(normalizedSessionOrder().map((id, index) => [id, index]));
|
|
305
577
|
return state.sessions
|
|
578
|
+
.filter(Boolean)
|
|
579
|
+
.filter(session => session.type !== 'agent' || window.LoadToAgentApp?.isProviderVisible?.(session.provider) !== false)
|
|
306
580
|
.filter(session => mode === 'tmux' ? session.type === 'tmux' : session.type !== 'tmux')
|
|
307
581
|
.sort((left, right) => (rank.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(right.id) ?? Number.MAX_SAFE_INTEGER));
|
|
308
582
|
}
|
|
@@ -322,14 +596,14 @@
|
|
|
322
596
|
function configurePlatform() {
|
|
323
597
|
const localButton = $('#newPowerShellBtn');
|
|
324
598
|
const linuxButton = $('#newWslBtn');
|
|
325
|
-
if (localButton) localButton.textContent =
|
|
599
|
+
if (localButton) localButton.textContent = t('terminal.new_local_session', { platform: state.platform.label });
|
|
326
600
|
if (linuxButton) linuxButton.classList.toggle('hidden', state.platform.id !== 'win32');
|
|
327
601
|
const explain = $('#terminalPlatformExplain');
|
|
328
602
|
if (explain) explain.textContent = state.platform.id === 'win32'
|
|
329
|
-
? '
|
|
330
|
-
:
|
|
603
|
+
? t('terminal.platform.windows_description')
|
|
604
|
+
: t('terminal.platform.description', { platform: state.platform.label });
|
|
331
605
|
const environmentLabel = $('#tmuxEnvironmentLabel');
|
|
332
|
-
if (environmentLabel) environmentLabel.textContent = state.platform.nativeTmux ? '
|
|
606
|
+
if (environmentLabel) environmentLabel.textContent = state.platform.nativeTmux ? t('terminal.environment.local_tmux') : t('terminal.environment.wsl');
|
|
333
607
|
}
|
|
334
608
|
|
|
335
609
|
function xtermOptions(readOnly = false) {
|
|
@@ -341,8 +615,8 @@
|
|
|
341
615
|
convertEol: readOnly,
|
|
342
616
|
screenReaderMode: true,
|
|
343
617
|
fontFamily: '"Cascadia Code", Consolas, monospace',
|
|
344
|
-
fontSize:
|
|
345
|
-
lineHeight: 1.
|
|
618
|
+
fontSize: state.terminalFontSize,
|
|
619
|
+
lineHeight: state.terminalFontSize >= 17 ? 1.32 : 1.28,
|
|
346
620
|
scrollback: 10_000,
|
|
347
621
|
theme: {
|
|
348
622
|
background: '#080c12',
|
|
@@ -402,6 +676,8 @@
|
|
|
402
676
|
notice,
|
|
403
677
|
reorderSession,
|
|
404
678
|
moveSessionByOffset,
|
|
679
|
+
setTerminalFontSize,
|
|
680
|
+
toggleTerminalFocusMode,
|
|
405
681
|
});
|
|
406
682
|
}
|
|
407
683
|
|
|
@@ -435,13 +711,25 @@
|
|
|
435
711
|
}
|
|
436
712
|
|
|
437
713
|
function updateSnapshot(snapshot, workspaces = state.workspaces) {
|
|
438
|
-
|
|
714
|
+
const projected = snapshot && window.LoadToAgentApp?.projectVisibleSnapshot
|
|
715
|
+
? window.LoadToAgentApp.projectVisibleSnapshot(snapshot)
|
|
716
|
+
: snapshot;
|
|
717
|
+
state.snapshot = projected || state.snapshot;
|
|
439
718
|
state.workspaces = Array.isArray(workspaces) ? workspaces : state.workspaces;
|
|
440
719
|
if (!state.initialized) return;
|
|
441
720
|
if (state.boundAgent && state.snapshot && Array.isArray(state.snapshot.sessions)) {
|
|
442
721
|
const updated = state.snapshot.sessions.find(session => session.id === state.boundAgent.id);
|
|
722
|
+
// Keep the conversation associated with its live terminal even when a
|
|
723
|
+
// monitor refresh temporarily omits an ended or slow-to-scan AI session.
|
|
724
|
+
// The binding is explicitly cleared when the terminal closes, and the
|
|
725
|
+
// provider visibility guard above prevents hidden providers from leaking.
|
|
443
726
|
if (updated && updated.updatedAt !== state.boundAgent.updatedAt) queueHistoryRefresh(updated);
|
|
727
|
+
// Child sessions are updated independently from their parent. The render
|
|
728
|
+
// key prevents unnecessary DOM replacement while still reflecting their
|
|
729
|
+
// latest lifecycle and collaboration events in the terminal history.
|
|
730
|
+
renderHistoryPanel();
|
|
444
731
|
}
|
|
732
|
+
renderSessions();
|
|
445
733
|
renderTmuxResources();
|
|
446
734
|
renderTarget();
|
|
447
735
|
if (state.active && state.selectedTmux) startCapture();
|
|
@@ -457,6 +745,20 @@
|
|
|
457
745
|
return true;
|
|
458
746
|
}
|
|
459
747
|
|
|
748
|
+
function scrollTmuxByLines(lines) {
|
|
749
|
+
if (!state.remoteTerminal) return false;
|
|
750
|
+
state.remoteTerminal.terminal.scrollLines(Math.trunc(Number(lines) || 0));
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function scrollTerminalToLine(id, line) {
|
|
755
|
+
const entry = state.terminals.get(String(id || ''));
|
|
756
|
+
if (!entry) return false;
|
|
757
|
+
entry.userScrollRevision += 1;
|
|
758
|
+
entry.terminal.scrollToLine(Math.max(0, Math.floor(Number(line) || 0)));
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
|
|
460
762
|
function init() {
|
|
461
763
|
if (state.initPromise) return state.initPromise;
|
|
462
764
|
state.initPromise = (async () => {
|
|
@@ -471,6 +773,7 @@
|
|
|
471
773
|
state.wslDistros = Array.isArray(environments) ? environments : [];
|
|
472
774
|
state.initialized = true;
|
|
473
775
|
configurePlatform();
|
|
776
|
+
syncTerminalViewControls();
|
|
474
777
|
renderAll();
|
|
475
778
|
})().catch(error => {
|
|
476
779
|
state.initialized = false;
|
|
@@ -493,11 +796,15 @@
|
|
|
493
796
|
openForAgent,
|
|
494
797
|
resumeForAgent,
|
|
495
798
|
scrollTmuxToLine,
|
|
799
|
+
scrollTmuxByLines,
|
|
800
|
+
scrollTerminalToLine,
|
|
496
801
|
};
|
|
497
802
|
window.addEventListener('loadtoagent:locale-changed', () => {
|
|
803
|
+
refreshStatusLabels();
|
|
498
804
|
if (!state.initialized) return;
|
|
499
805
|
configurePlatform();
|
|
806
|
+
syncTerminalViewControls();
|
|
500
807
|
renderAll();
|
|
501
808
|
});
|
|
502
|
-
init().catch(error => notice(
|
|
809
|
+
init().catch(error => notice(t('terminal.error.initialization_failed', { message: errorMessage(error) }), 'error'));
|
|
503
810
|
})();
|