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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/** Own xterm views, terminal/tmux selection, capture, and management actions. */
|
|
4
4
|
window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
5
|
+
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
5
6
|
const {
|
|
6
7
|
$, state, notice, setConnectionState, currentSession, currentTmux, saveCurrentDraft, restoreCurrentDraft,
|
|
7
8
|
renderHistoryPanel, terminalTypeMark, terminalTypeLabel, xtermOptions, preferredWorkspace, firstDistro, guarded,
|
|
@@ -9,7 +10,7 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
9
10
|
} = context;
|
|
10
11
|
|
|
11
12
|
function createXtermHost(key, readOnly = false) {
|
|
12
|
-
if (!window.Terminal || !window.FitAddon || !window.FitAddon.FitAddon) throw new Error('
|
|
13
|
+
if (!window.Terminal || !window.FitAddon || !window.FitAddon.FitAddon) throw new Error(t('terminal.error.screen_unavailable'));
|
|
13
14
|
const host = document.createElement('div');
|
|
14
15
|
host.className = 'terminal-screen hidden';
|
|
15
16
|
host.dataset.terminalScreen = key;
|
|
@@ -18,26 +19,29 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
18
19
|
const fit = new window.FitAddon.FitAddon();
|
|
19
20
|
terminal.loadAddon(fit);
|
|
20
21
|
terminal.open(host);
|
|
22
|
+
const entry = { terminal, fit, host, readOnly, userScrollRevision: 0, outputWritePending: 0 };
|
|
21
23
|
const syncScrollState = viewportY => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const normalizedViewport = Number(viewportY) || 0;
|
|
25
|
+
const baseY = Number(terminal.buffer.active.baseY) || 0;
|
|
26
|
+
host.dataset.viewportY = String(normalizedViewport);
|
|
27
|
+
host.dataset.baseY = String(baseY);
|
|
28
|
+
// Xterm may consume wheel events before they bubble to the host. Its
|
|
29
|
+
// scroll event is the reliable source for mouse, keyboard and scrollbar
|
|
30
|
+
// viewport changes.
|
|
31
|
+
if (readOnly && !state.remoteCaptureApplying) {
|
|
32
|
+
state.remoteViewportAnchor = normalizedViewport;
|
|
33
|
+
state.remoteViewportAtBottom = normalizedViewport >= baseY;
|
|
34
|
+
}
|
|
24
35
|
};
|
|
25
36
|
terminal.onScroll(syncScrollState);
|
|
26
37
|
syncScrollState(0);
|
|
27
|
-
const entry = { terminal, fit, host, readOnly };
|
|
28
|
-
if (readOnly) {
|
|
29
|
-
const rememberUserViewport = () => requestAnimationFrame(() => {
|
|
30
|
-
const buffer = terminal.buffer.active;
|
|
31
|
-
state.remoteViewportAnchor = Number(buffer.viewportY) || 0;
|
|
32
|
-
state.remoteViewportAtBottom = state.remoteViewportAnchor >= Number(buffer.baseY || 0);
|
|
33
|
-
});
|
|
34
|
-
host.addEventListener('wheel', rememberUserViewport, { passive: true });
|
|
35
|
-
host.addEventListener('pointerup', rememberUserViewport);
|
|
36
|
-
host.addEventListener('keyup', event => {
|
|
37
|
-
if (['PageUp', 'PageDown', 'Home', 'End', 'ArrowUp', 'ArrowDown'].includes(event.key)) rememberUserViewport();
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
38
|
if (!readOnly) {
|
|
39
|
+
const rememberUserScroll = () => { entry.userScrollRevision += 1; };
|
|
40
|
+
host.addEventListener('wheel', rememberUserScroll, { capture: true, passive: true });
|
|
41
|
+
host.addEventListener('pointerup', rememberUserScroll, true);
|
|
42
|
+
host.addEventListener('keyup', event => {
|
|
43
|
+
if (['PageUp', 'PageDown', 'Home', 'End', 'ArrowUp', 'ArrowDown'].includes(event.key)) rememberUserScroll();
|
|
44
|
+
}, true);
|
|
41
45
|
terminal.onData(data => {
|
|
42
46
|
if (state.selectedId === key) Promise.resolve(window.loadtoagent.terminalWrite(key, data)).catch(error => notice(errorMessage(error), 'error'));
|
|
43
47
|
});
|
|
@@ -88,49 +92,97 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
88
92
|
$('#terminalEmpty').classList.add('hidden');
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
function linkedAgentSession(session) {
|
|
96
|
+
if (!session) return null;
|
|
97
|
+
if (state.boundTargetId === session.id && state.boundAgent) return state.boundAgent;
|
|
98
|
+
const bridgeId = String(session.bridgeId || '');
|
|
99
|
+
return bridgeId && Array.isArray(state.snapshot?.sessions)
|
|
100
|
+
? state.snapshot.sessions.find(item => item.id === bridgeId) || null
|
|
101
|
+
: null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function terminalPresentation(session) {
|
|
105
|
+
const agent = linkedAgentSession(session);
|
|
106
|
+
if (agent?.attention?.required || agent?.status === 'waiting') return { tone: 'attention', label: t('app.nav.needs_review') };
|
|
107
|
+
if (agent?.status === 'failed' || session?.status === 'failed') return { tone: 'failed', label: t('terminal.status.failed') };
|
|
108
|
+
if (agent?.status === 'completed') return { tone: 'completed', label: t('ui.completed') };
|
|
109
|
+
if (agent && ['running', 'starting'].includes(agent.status)) return { tone: 'running', label: t('ui.working') };
|
|
110
|
+
if (session?.status === 'running' || session?.status === 'starting') return { tone: 'running', label: STATUS_LABELS[session.status] || session.status };
|
|
111
|
+
return { tone: session?.status || 'idle', label: STATUS_LABELS[session?.status] || session?.status || t('ui.idle') };
|
|
112
|
+
}
|
|
113
|
+
|
|
91
114
|
function renderSessions() {
|
|
92
115
|
const general = modeSessions('general');
|
|
93
116
|
const running = general.filter(item => item.status === 'running').length;
|
|
94
117
|
const background = general.filter(item => item.background && item.status === 'running').length;
|
|
118
|
+
const attention = general.filter(item => terminalPresentation(item).tone === 'attention').length;
|
|
95
119
|
$('#navTerminalCount').textContent = running;
|
|
120
|
+
const terminalNav = document.querySelector('.nav-item[data-view="terminal"]');
|
|
121
|
+
if (terminalNav) terminalNav.setAttribute('aria-label', t('quality.nav_count_detailed', { label: t('app.nav.session_terminal'), count: running, unit: t('quality.unit.sessions') }));
|
|
122
|
+
const advancedCount = ['navRuntimeCount', 'navTerminalCount', 'navTmuxCount']
|
|
123
|
+
.reduce((total, id) => total + Number(document.getElementById(id)?.textContent || 0), 0);
|
|
124
|
+
const advancedCounter = document.getElementById('advancedToolsCount');
|
|
125
|
+
if (advancedCounter) advancedCounter.textContent = String(advancedCount);
|
|
126
|
+
document.querySelector('#advancedToolsNav > summary')?.setAttribute('aria-label', t('quality.nav_count_detailed', {
|
|
127
|
+
label: t('management.advanced_tools'), count: advancedCount, unit: t('quality.unit.items'),
|
|
128
|
+
}));
|
|
96
129
|
$('#terminalSessionSummary').textContent = [
|
|
97
130
|
window.LoadToAgentI18n.t('common.active', { count: running }),
|
|
131
|
+
attention ? t('terminal.monitor.attention_count', { count: attention }) : '',
|
|
98
132
|
background ? window.LoadToAgentI18n.t('session.background_count', { count: background }) : '',
|
|
99
133
|
window.LoadToAgentI18n.t('session.total_count', { count: general.length }),
|
|
100
134
|
].filter(Boolean).join(' · ');
|
|
101
|
-
|
|
135
|
+
const renderKey = JSON.stringify([
|
|
136
|
+
state.selectedId,
|
|
137
|
+
general.map(session => {
|
|
138
|
+
const presentation = terminalPresentation(session);
|
|
139
|
+
return [session.id, session.title, session.type, session.status, session.cwd, session.background, presentation.tone, presentation.label];
|
|
140
|
+
}),
|
|
141
|
+
]);
|
|
142
|
+
if (renderKey === state.sessionRenderKey) return;
|
|
143
|
+
state.sessionRenderKey = renderKey;
|
|
144
|
+
$('#terminalSessionList').innerHTML = general.length ? general.map((session, index) => {
|
|
145
|
+
const presentation = terminalPresentation(session);
|
|
146
|
+
return `
|
|
102
147
|
<div class="terminal-session-row">
|
|
103
148
|
<button type="button" draggable="true"
|
|
104
149
|
class="terminal-session-item ${state.selectedId === session.id ? 'active' : ''}"
|
|
150
|
+
data-status="${esc(presentation.tone)}"
|
|
105
151
|
data-terminal-id="${esc(session.id)}"
|
|
152
|
+
role="option"
|
|
153
|
+
aria-selected="${state.selectedId === session.id ? 'true' : 'false'}"
|
|
154
|
+
tabindex="${state.selectedId === session.id || (!state.selectedId && index === 0) ? '0' : '-1'}"
|
|
155
|
+
aria-pressed="${state.selectedId === session.id ? 'true' : 'false'}"
|
|
106
156
|
aria-grabbed="false"
|
|
107
157
|
aria-describedby="terminalReorderHelp"
|
|
108
|
-
title="${esc(window.LoadToAgentI18n.t('terminal.reorder_hint'))}">
|
|
158
|
+
title="${esc(session.cwd || window.LoadToAgentI18n.t('terminal.reorder_hint'))}">
|
|
109
159
|
<span class="terminal-session-icon">${esc(terminalTypeMark(session))}</span>
|
|
110
|
-
<span><b>${esc(session.title)}</b><small>${esc(terminalTypeLabel(session))}${session.background ?
|
|
111
|
-
<
|
|
160
|
+
<span><b>${esc(session.title)}</b><small>${esc(terminalTypeLabel(session))}${session.background ? ` · ${t('terminal.background_kept')}` : ''}</small><em>${esc(session.cwd || session.distro || `PID ${session.pid || '--'}`)}</em><span class="sr-only">${index + 1}/${general.length}</span></span>
|
|
161
|
+
<span class="terminal-session-status" data-status="${esc(presentation.tone)}"><i></i>${esc(presentation.label)}</span>
|
|
112
162
|
</button>
|
|
113
163
|
<span class="terminal-reorder-actions" aria-label="${esc(window.LoadToAgentI18n.t('terminal.reorder_group', { title: session.title }))}">
|
|
114
164
|
<button type="button" data-session-move="-1" data-session-move-id="${esc(session.id)}" aria-label="${esc(window.LoadToAgentI18n.t('terminal.move_up', { title: session.title }))}" ${index === 0 ? 'disabled' : ''}>↑</button>
|
|
115
165
|
<button type="button" data-session-move="1" data-session-move-id="${esc(session.id)}" aria-label="${esc(window.LoadToAgentI18n.t('terminal.move_down', { title: session.title }))}" ${index === general.length - 1 ? 'disabled' : ''}>↓</button>
|
|
116
166
|
</span>
|
|
117
|
-
</div
|
|
167
|
+
</div>`;
|
|
168
|
+
}).join('') : `<div class="terminal-resource-empty">${t('terminal.empty.general')}</div>`;
|
|
118
169
|
}
|
|
119
170
|
|
|
120
171
|
function renderTmuxResources() {
|
|
121
172
|
const distros = state.snapshot && state.snapshot.tmux && state.snapshot.tmux.distros || [];
|
|
122
173
|
if (!distros.length) {
|
|
123
|
-
$('#terminalTmuxList').innerHTML =
|
|
174
|
+
$('#terminalTmuxList').innerHTML = `<div class="terminal-resource-empty">${t('terminal.empty.tmux')}</div>`;
|
|
124
175
|
return;
|
|
125
176
|
}
|
|
177
|
+
let paneIndex = 0;
|
|
126
178
|
$('#terminalTmuxList').innerHTML = distros.map(distro => `
|
|
127
179
|
<section class="terminal-tmux-group">
|
|
128
|
-
<header><b>${esc(distro.name)}</b><span
|
|
180
|
+
<header><b>${esc(distro.name)}</b><span>${t('terminal.tmux.workspace_count', { count: (distro.sessions || []).length })}</span></header>
|
|
129
181
|
${(distro.sessions || []).map(session => `
|
|
130
|
-
<div class="terminal-tmux-session"><strong>${esc(session.name)}</strong><small>${session.attached ? '
|
|
182
|
+
<div class="terminal-tmux-session"><strong>${esc(session.name)}</strong><small>${session.attached ? t('terminal.tmux.attached') : t('terminal.tmux.running_background')}</small></div>
|
|
131
183
|
${(session.windows || []).flatMap(windowItem => (windowItem.panes || []).map(pane => `
|
|
132
|
-
<button type="button" class="terminal-tmux-pane ${state.selectedTmux && state.selectedTmux.distro.name === distro.name && state.selectedTmux.pane.nativeId === pane.nativeId ? 'active' : ''}" data-tmux-distro="${esc(distro.name)}" data-tmux-pane="${esc(pane.nativeId)}">
|
|
133
|
-
<span><b>${esc(pane.nativeId)} · ${esc(windowItem.index)}:${esc(windowItem.name)}</b><small>${esc(pane.command || 'shell')} · ${esc(pane.cwd || '
|
|
184
|
+
<button type="button" role="option" class="terminal-tmux-pane ${state.selectedTmux && state.selectedTmux.distro.name === distro.name && state.selectedTmux.pane.nativeId === pane.nativeId ? 'active' : ''}" data-tmux-distro="${esc(distro.name)}" data-tmux-pane="${esc(pane.nativeId)}" aria-selected="${state.selectedTmux && state.selectedTmux.distro.name === distro.name && state.selectedTmux.pane.nativeId === pane.nativeId ? 'true' : 'false'}" aria-pressed="${state.selectedTmux && state.selectedTmux.distro.name === distro.name && state.selectedTmux.pane.nativeId === pane.nativeId ? 'true' : 'false'}" tabindex="${state.selectedTmux && state.selectedTmux.distro.name === distro.name && state.selectedTmux.pane.nativeId === pane.nativeId || (!state.selectedTmux && paneIndex === 0) ? '0' : '-1'}" data-pane-index="${paneIndex++}">
|
|
185
|
+
<span><b>${esc(pane.nativeId)} · ${esc(windowItem.index)}:${esc(windowItem.name)}</b><small>${esc(pane.command || 'shell')} · ${esc(pane.cwd || t('terminal.path_unreported'))}</small></span>
|
|
134
186
|
<i class="${pane.agent ? 'agent' : (pane.active ? 'live' : '')}">${pane.agent ? 'AI' : (pane.active ? 'ON' : '')}</i>
|
|
135
187
|
</button>`)).join('')}`).join('')}
|
|
136
188
|
</section>`).join('');
|
|
@@ -144,29 +196,33 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
144
196
|
const canInput = Boolean((remote && !remote.pane.dead) || (session && session.status === 'running'));
|
|
145
197
|
const closeButton = $('#terminalCloseBtn');
|
|
146
198
|
closeButton.disabled = !hasTarget;
|
|
147
|
-
closeButton.textContent = remote && !session ? '
|
|
199
|
+
closeButton.textContent = remote && !session ? t('terminal.clear_selection') : t('ui.end_session');
|
|
148
200
|
closeButton.classList.toggle('terminal-danger-button', Boolean(session));
|
|
149
201
|
$('#terminalRestartBtn').classList.toggle('hidden', !session || session.status === 'running' || session.type === 'agent');
|
|
150
202
|
$('#terminalRestartBtn').disabled = !session || session.status === 'running';
|
|
151
203
|
$('#terminalCommandInput').disabled = !canInput;
|
|
152
204
|
const commandForm = $('#terminalCommandForm');
|
|
153
|
-
const commandButton = commandForm.querySelector('button');
|
|
205
|
+
const commandButton = commandForm.querySelector('button[type="submit"]');
|
|
154
206
|
commandButton.disabled = !canInput || state.commandSending;
|
|
207
|
+
commandButton.toggleAttribute('aria-busy', state.commandSending);
|
|
155
208
|
commandForm.toggleAttribute('aria-busy', state.commandSending);
|
|
156
209
|
const commandButtonLabel = commandButton.querySelector('span');
|
|
157
|
-
if (commandButtonLabel) commandButtonLabel.textContent = state.commandSending ? '
|
|
210
|
+
if (commandButtonLabel) commandButtonLabel.textContent = state.commandSending ? t('terminal.sending') : t('common.send');
|
|
158
211
|
document.querySelectorAll('[data-terminal-signal]').forEach(button => { button.disabled = !canInput; });
|
|
159
212
|
$('#terminalAttachBtn').classList.toggle('hidden', !remote || Boolean(session));
|
|
160
213
|
$('#terminalTmuxTools').classList.toggle('hidden', !remote || Boolean(session));
|
|
161
214
|
if (session) {
|
|
162
|
-
|
|
215
|
+
const presentation = terminalPresentation(session);
|
|
216
|
+
setConnectionState(presentation.label, presentation.tone);
|
|
163
217
|
$('#terminalTargetIcon').textContent = terminalTypeMark(session);
|
|
164
|
-
$('#terminalTargetMeta').innerHTML = `<b>${esc(session.title)}</b><span>${bound ? '
|
|
218
|
+
$('#terminalTargetMeta').innerHTML = `<b>${esc(session.title)}</b><span>${bound ? `● ${t('terminal.bound_ai_session')} · ` : ''}${esc(session.type.toUpperCase())} · PID ${session.pid || '--'} · ${esc(session.cwd || session.distro || '')}</span>`;
|
|
165
219
|
$('#terminalConsoleCaption').textContent = `${terminalTypeLabel(session)} · PID ${session.pid || '--'}`;
|
|
166
|
-
$('#terminalConsoleState').textContent =
|
|
167
|
-
|
|
220
|
+
$('#terminalConsoleState').textContent = presentation.tone === 'attention' || presentation.tone === 'completed'
|
|
221
|
+
? presentation.label
|
|
222
|
+
: canInput ? window.LoadToAgentI18n.t("ui.direct_input_available") : window.LoadToAgentI18n.t("ui.ended_session");
|
|
223
|
+
$('#terminalConsoleState').dataset.status = presentation.tone;
|
|
168
224
|
} else if (remote) {
|
|
169
|
-
setConnectionState(remote.pane.dead ? '
|
|
225
|
+
setConnectionState(remote.pane.dead ? t('terminal.tmux.ended_pane') : t('terminal.tmux.connected'), remote.pane.dead ? 'exited' : 'running');
|
|
170
226
|
$('#terminalTargetIcon').textContent = 'tm';
|
|
171
227
|
$('#terminalTargetMeta').innerHTML = `<b>${esc(remote.distro.name)} · ${esc(remote.session.name)} · ${esc(remote.pane.nativeId)}</b><span>${esc(remote.window.index)}:${esc(remote.window.name)} · ${esc(remote.pane.command || 'shell')} · ${esc(remote.pane.cwd || '')}</span>`;
|
|
172
228
|
$('#terminalConsoleCaption').textContent = `${remote.window.index}:${remote.window.name} · ${remote.pane.command || 'shell'}`;
|
|
@@ -176,7 +232,7 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
176
232
|
setConnectionState(window.LoadToAgentI18n.t("ui.waiting_for_selection"));
|
|
177
233
|
$('#terminalTargetIcon').textContent = '›_';
|
|
178
234
|
$('#terminalTargetMeta').innerHTML = state.mode === 'tmux'
|
|
179
|
-
? '
|
|
235
|
+
? `<b>${t('terminal.tmux.no_selection_title')}</b><span>${t('terminal.tmux.no_selection_description')}</span>`
|
|
180
236
|
: `<b>${window.LoadToAgentI18n.t("ui.select_a_session")}</b><span>${window.LoadToAgentI18n.t("ui.choose_a_session_on_the_left_or_create_a_new")}</span>`;
|
|
181
237
|
$('#terminalConsoleCaption').textContent = window.LoadToAgentI18n.t("ui.select_a_session_to_show_its_output_here");
|
|
182
238
|
$('#terminalConsoleState').textContent = window.LoadToAgentI18n.t("ui.waiting_for_selection");
|
|
@@ -187,25 +243,40 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
187
243
|
if (commandLabel) commandLabel.textContent = bound ? window.LoadToAgentI18n.t("ui.continue_instructing_ai") : (remote ? window.LoadToAgentI18n.t("ui.send_to_tmux_terminal") : window.LoadToAgentI18n.t("ui.send_command_to_terminal"));
|
|
188
244
|
if (commandInput) commandInput.placeholder = !hasTarget
|
|
189
245
|
? window.LoadToAgentI18n.t("ui.select_a_session_on_the_left_first")
|
|
190
|
-
: (bound ? '
|
|
246
|
+
: (bound ? t('terminal.command.continue_ai_placeholder') : t('ui.enter_a_command_to_run'));
|
|
247
|
+
if (commandInput) {
|
|
248
|
+
$('#terminalCommandCount').textContent = `${commandInput.value.length.toLocaleString()} / 8,000`;
|
|
249
|
+
$('#terminalCommandClearBtn')?.classList.toggle('hidden', !commandInput.value);
|
|
250
|
+
}
|
|
191
251
|
renderHistoryPanel();
|
|
192
252
|
}
|
|
193
253
|
|
|
194
254
|
async function showSelection() {
|
|
255
|
+
const generation = state.captureGeneration;
|
|
256
|
+
const expectedMode = state.mode;
|
|
257
|
+
const expectedSessionId = state.selectedId;
|
|
258
|
+
const expectedTmuxId = state.selectedTmux?.pane?.id || state.selectedTmux?.pane?.nativeId || '';
|
|
259
|
+
const selectionIsCurrent = () => generation === state.captureGeneration
|
|
260
|
+
&& expectedMode === state.mode
|
|
261
|
+
&& expectedSessionId === state.selectedId
|
|
262
|
+
&& expectedTmuxId === (state.selectedTmux?.pane?.id || state.selectedTmux?.pane?.nativeId || '');
|
|
195
263
|
hideScreens();
|
|
196
264
|
const session = currentSession();
|
|
197
265
|
const remote = currentTmux();
|
|
198
266
|
if (session) {
|
|
199
267
|
const entry = await ensureSessionTerminal(session);
|
|
268
|
+
if (!selectionIsCurrent()) return;
|
|
200
269
|
entry.host.classList.remove('hidden');
|
|
201
270
|
fitEntry(entry, session.id);
|
|
202
271
|
stopCapture();
|
|
203
272
|
} else if (remote) {
|
|
273
|
+
if (!selectionIsCurrent()) return;
|
|
204
274
|
const entry = ensureRemoteTerminal();
|
|
205
275
|
entry.host.classList.remove('hidden');
|
|
206
276
|
fitEntry(entry);
|
|
207
277
|
startCapture();
|
|
208
278
|
} else {
|
|
279
|
+
if (!selectionIsCurrent()) return;
|
|
209
280
|
$('#terminalEmpty').classList.remove('hidden');
|
|
210
281
|
stopCapture();
|
|
211
282
|
}
|
|
@@ -214,20 +285,22 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
214
285
|
|
|
215
286
|
async function selectSession(id) {
|
|
216
287
|
saveCurrentDraft();
|
|
217
|
-
state.captureGeneration
|
|
288
|
+
const generation = ++state.captureGeneration;
|
|
218
289
|
state.selectedId = id;
|
|
219
290
|
state.selectedTmux = null;
|
|
220
291
|
renderSessions();
|
|
221
292
|
renderTmuxResources();
|
|
222
293
|
await showSelection();
|
|
294
|
+
if (!state.active || state.captureGeneration !== generation || state.selectedId !== id || state.mode !== 'general') return;
|
|
223
295
|
restoreCurrentDraft();
|
|
296
|
+
if (!$('#terminalCommandInput')?.disabled) $('#terminalCommandInput').focus({ preventScroll: true });
|
|
224
297
|
}
|
|
225
298
|
|
|
226
299
|
async function selectTmux(distroName, paneId) {
|
|
227
300
|
const row = tmuxRows().find(item => item.distro.name === distroName && item.pane.nativeId === paneId);
|
|
228
|
-
if (!row) return notice('
|
|
301
|
+
if (!row) return notice(t('terminal.error.selected_split_missing'), 'error');
|
|
229
302
|
saveCurrentDraft();
|
|
230
|
-
state.captureGeneration
|
|
303
|
+
const generation = ++state.captureGeneration;
|
|
231
304
|
state.selectedId = null;
|
|
232
305
|
state.selectedTmux = row;
|
|
233
306
|
state.remoteCapture = '';
|
|
@@ -237,12 +310,15 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
237
310
|
renderSessions();
|
|
238
311
|
renderTmuxResources();
|
|
239
312
|
await showSelection();
|
|
313
|
+
if (!state.active || state.captureGeneration !== generation || state.selectedId || state.mode !== 'tmux'
|
|
314
|
+
|| state.selectedTmux?.distro?.name !== distroName || state.selectedTmux?.pane?.nativeId !== paneId) return;
|
|
240
315
|
restoreCurrentDraft();
|
|
316
|
+
if (!$('#terminalCommandInput')?.disabled) $('#terminalCommandInput').focus({ preventScroll: true });
|
|
241
317
|
}
|
|
242
318
|
|
|
243
319
|
async function selectTmuxById(paneId) {
|
|
244
320
|
const row = tmuxRows().find(item => item.pane.id === paneId || item.pane.nativeId === paneId);
|
|
245
|
-
if (!row) return notice('
|
|
321
|
+
if (!row) return notice(t('terminal.error.selected_tmux_missing'), 'error');
|
|
246
322
|
state.mode = 'tmux';
|
|
247
323
|
moveWorkbench('tmux');
|
|
248
324
|
return selectTmux(row.distro.name, row.pane.nativeId);
|
|
@@ -272,15 +348,15 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
272
348
|
|
|
273
349
|
async function createTerminal(type) {
|
|
274
350
|
const distro = type === 'wsl' ? firstDistro() : null;
|
|
275
|
-
if (type === 'wsl' && !distro) return notice('
|
|
351
|
+
if (type === 'wsl' && !distro) return notice(t('terminal.error.no_linux_environment'), 'error');
|
|
276
352
|
const created = await guarded(() => window.loadtoagent.terminalCreate({
|
|
277
353
|
type,
|
|
278
354
|
cwd: (type === 'powershell' || type === 'shell') ? (preferredWorkspace() || undefined) : undefined,
|
|
279
355
|
distro: distro && distro.name,
|
|
280
|
-
title: type === 'powershell' ? 'PowerShell' : (type === 'shell' ? state.platform.localShellLabel :
|
|
356
|
+
title: type === 'powershell' ? 'PowerShell' : (type === 'shell' ? state.platform.localShellLabel : t('terminal.linux_shell_title', { distro: distro.name })),
|
|
281
357
|
cols: 120,
|
|
282
358
|
rows: 32,
|
|
283
|
-
}),
|
|
359
|
+
}), t('terminal.opened', { platform: type === 'powershell' ? 'Windows' : (type === 'shell' ? state.platform.label : 'Linux') }), `terminal-create:${type}`);
|
|
284
360
|
if (!created) return;
|
|
285
361
|
await refreshSessions();
|
|
286
362
|
await selectSession(created.id);
|
|
@@ -308,6 +384,7 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
308
384
|
const wasAtBottom = state.remoteViewportAnchor == null
|
|
309
385
|
? Boolean(buffer && buffer.viewportY >= buffer.baseY)
|
|
310
386
|
: state.remoteViewportAtBottom;
|
|
387
|
+
state.remoteCaptureApplying = true;
|
|
311
388
|
entry.terminal.reset();
|
|
312
389
|
await new Promise(resolve => entry.terminal.write(result.output.replace(/\n/g, '\r\n'), resolve));
|
|
313
390
|
const selected = currentTmux();
|
|
@@ -317,19 +394,26 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
317
394
|
setTimeout(captureRemote, 0);
|
|
318
395
|
return;
|
|
319
396
|
}
|
|
320
|
-
requestAnimationFrame(() => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
entry.terminal.scrollToTop();
|
|
325
|
-
state.remoteViewportAnchor
|
|
326
|
-
state.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
397
|
+
await new Promise(resolve => requestAnimationFrame(() => {
|
|
398
|
+
try {
|
|
399
|
+
const latest = currentTmux();
|
|
400
|
+
if (captureGeneration !== state.captureGeneration || !latest || `${latest.distro.name}:${latest.pane.nativeId}` !== captureKey) return;
|
|
401
|
+
if (firstCapture) entry.terminal.scrollToTop();
|
|
402
|
+
else if (state.remoteViewportAnchor == null ? wasAtBottom : state.remoteViewportAtBottom) entry.terminal.scrollToBottom();
|
|
403
|
+
else entry.terminal.scrollToLine(state.remoteViewportAnchor == null ? previousViewport : state.remoteViewportAnchor);
|
|
404
|
+
const restoredBuffer = entry.terminal.buffer.active;
|
|
405
|
+
state.remoteViewportAnchor = Number(restoredBuffer.viewportY) || 0;
|
|
406
|
+
state.remoteViewportAtBottom = !firstCapture && state.remoteViewportAnchor >= Number(restoredBuffer.baseY || 0);
|
|
407
|
+
state.captureRevision += 1;
|
|
408
|
+
entry.host.dataset.captureRevision = String(state.captureRevision);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
window.LoadToAgentRendererUtils.reportRecoverableError('tmux-capture-render', error);
|
|
411
|
+
} finally {
|
|
412
|
+
resolve();
|
|
413
|
+
}
|
|
414
|
+
}));
|
|
332
415
|
} finally {
|
|
416
|
+
state.remoteCaptureApplying = false;
|
|
333
417
|
state.captureInFlight = false;
|
|
334
418
|
}
|
|
335
419
|
}
|
|
@@ -349,21 +433,21 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
349
433
|
const text = String(command || '');
|
|
350
434
|
if (state.commandSending) return false;
|
|
351
435
|
if (!text.trim()) {
|
|
352
|
-
notice('
|
|
436
|
+
notice(t('terminal.command.required'), 'error');
|
|
353
437
|
return false;
|
|
354
438
|
}
|
|
355
439
|
const session = currentSession();
|
|
356
440
|
const remote = currentTmux();
|
|
357
441
|
if (!session && !remote) {
|
|
358
|
-
notice('
|
|
442
|
+
notice(t('terminal.command.select_first'), 'error');
|
|
359
443
|
return false;
|
|
360
444
|
}
|
|
361
445
|
state.commandSending = true;
|
|
362
446
|
renderTarget();
|
|
363
447
|
try {
|
|
364
448
|
const result = session
|
|
365
|
-
? await guarded(() => window.loadtoagent.terminalCommand(session.id, text), '
|
|
366
|
-
: await guarded(() => window.loadtoagent.tmuxSendText({ distro: remote.distro.name, target: remote.pane.nativeId, text, enter: true }), '
|
|
449
|
+
? await guarded(() => window.loadtoagent.terminalCommand(session.id, text), t('terminal.command.sent'))
|
|
450
|
+
: await guarded(() => window.loadtoagent.tmuxSendText({ distro: remote.distro.name, target: remote.pane.nativeId, text, enter: true }), t('terminal.command.executed_in_split'));
|
|
367
451
|
if (result && remote) setTimeout(captureRemote, 160);
|
|
368
452
|
return Boolean(result);
|
|
369
453
|
} finally {
|
|
@@ -375,12 +459,12 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
375
459
|
async function sendSignal(signal) {
|
|
376
460
|
const session = currentSession();
|
|
377
461
|
const remote = currentTmux();
|
|
378
|
-
if (session) return guarded(() => window.loadtoagent.terminalSignal(session.id, signal), signal === 'interrupt' ? '
|
|
462
|
+
if (session) return guarded(() => window.loadtoagent.terminalSignal(session.id, signal), signal === 'interrupt' ? t('terminal.signal.interrupt_sent') : t('terminal.signal.cleared'));
|
|
379
463
|
if (remote) {
|
|
380
464
|
const key = signal === 'interrupt' ? 'C-c' : 'C-l';
|
|
381
|
-
return guarded(() => window.loadtoagent.tmuxSendKey({ distro: remote.distro.name, target: remote.pane.nativeId, key }),
|
|
465
|
+
return guarded(() => window.loadtoagent.tmuxSendKey({ distro: remote.distro.name, target: remote.pane.nativeId, key }), t('terminal.signal.key_sent', { key }));
|
|
382
466
|
}
|
|
383
|
-
notice('
|
|
467
|
+
notice(t('terminal.command.select_first'), 'error');
|
|
384
468
|
}
|
|
385
469
|
|
|
386
470
|
function openTmuxModal() {
|
|
@@ -388,18 +472,22 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
388
472
|
const distros = state.snapshot && state.snapshot.tmux && state.snapshot.tmux.distros || [];
|
|
389
473
|
$('#tmuxCreateDistro').innerHTML = distros.map(item => `<option value="${esc(item.name)}">${esc(item.name)}</option>`).join('');
|
|
390
474
|
$('#tmuxCreateError').classList.add('hidden');
|
|
475
|
+
window.LoadToAgentA11y?.setDialogOpenState($('#tmuxCreateModal'), true);
|
|
391
476
|
$('#tmuxCreateModal').classList.remove('hidden');
|
|
392
477
|
$('#tmuxCreateName').focus();
|
|
393
478
|
}
|
|
394
479
|
|
|
395
|
-
function closeTmuxModal() {
|
|
480
|
+
function closeTmuxModal(force = false) {
|
|
481
|
+
if (force !== true && $('#tmuxCreateForm [type="submit"]').dataset.busy === 'true') return;
|
|
396
482
|
$('#tmuxCreateModal').classList.add('hidden');
|
|
483
|
+
window.LoadToAgentA11y?.setDialogOpenState($('#tmuxCreateModal'), false);
|
|
397
484
|
$('#tmuxCreateForm').reset();
|
|
485
|
+
$('#tmuxCreateForm').querySelectorAll('[aria-invalid="true"]').forEach(element => element.removeAttribute('aria-invalid'));
|
|
398
486
|
window.LoadToAgentA11y?.restoreDialogTrigger();
|
|
399
487
|
}
|
|
400
488
|
|
|
401
489
|
async function refreshSnapshot() {
|
|
402
|
-
const snapshot = await guarded(() => window.loadtoagent.snapshot(), '
|
|
490
|
+
const snapshot = await guarded(() => window.loadtoagent.snapshot(), t('terminal.tmux.refreshed'), 'tmux-refresh');
|
|
403
491
|
if (snapshot) updateSnapshot(snapshot, state.workspaces);
|
|
404
492
|
}
|
|
405
493
|
|
|
@@ -414,7 +502,7 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
414
502
|
title: `tmux · ${remote.session.name} · ${remote.pane.nativeId}`,
|
|
415
503
|
cols: 120,
|
|
416
504
|
rows: 32,
|
|
417
|
-
}), '
|
|
505
|
+
}), t('terminal.tmux.attached_for_input'), `tmux-attach:${remote.distro.name}:${remote.pane.nativeId}`);
|
|
418
506
|
if (!created) return;
|
|
419
507
|
await refreshSessions();
|
|
420
508
|
await selectSession(created.id);
|
|
@@ -427,33 +515,33 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
|
|
|
427
515
|
let operation = null;
|
|
428
516
|
let message = '';
|
|
429
517
|
if (action === 'rename-session') {
|
|
430
|
-
const name = window.prompt('
|
|
518
|
+
const name = window.prompt(t('terminal.tmux.prompt_workspace_name'), remote.session.name);
|
|
431
519
|
if (!name || name === remote.session.name) return;
|
|
432
520
|
operation = () => window.loadtoagent.tmuxRenameSession({ ...base, target: remote.session.nativeId, name });
|
|
433
|
-
message = '
|
|
521
|
+
message = t('terminal.tmux.workspace_renamed');
|
|
434
522
|
} else if (action === 'new-window') {
|
|
435
|
-
const name = window.prompt('
|
|
523
|
+
const name = window.prompt(t('terminal.tmux.prompt_window_name'), 'window');
|
|
436
524
|
if (!name) return;
|
|
437
525
|
operation = () => window.loadtoagent.tmuxNewWindow({ ...base, target: remote.session.nativeId, name, cwd: remote.pane.cwd });
|
|
438
|
-
message = '
|
|
526
|
+
message = t('terminal.tmux.window_created');
|
|
439
527
|
} else if (action === 'split-horizontal' || action === 'split-vertical') {
|
|
440
528
|
operation = () => window.loadtoagent.tmuxSplitPane({ ...base, target: remote.pane.nativeId, direction: action === 'split-horizontal' ? 'horizontal' : 'vertical', cwd: remote.pane.cwd });
|
|
441
|
-
message = '
|
|
529
|
+
message = t('terminal.tmux.pane_split');
|
|
442
530
|
} else if (action === 'kill-pane') {
|
|
443
|
-
if (!window.confirm(
|
|
531
|
+
if (!window.confirm(t('terminal.tmux.confirm_close_pane', { pane: remote.pane.nativeId }))) return;
|
|
444
532
|
operation = () => window.loadtoagent.tmuxKillPane({ ...base, target: remote.pane.nativeId });
|
|
445
|
-
message = '
|
|
533
|
+
message = t('terminal.tmux.pane_closed');
|
|
446
534
|
} else if (action === 'kill-window') {
|
|
447
|
-
if (!window.confirm(`${remote.window.index}:${remote.window.name}
|
|
535
|
+
if (!window.confirm(t('terminal.tmux.confirm_close_window', { window: `${remote.window.index}:${remote.window.name}` }))) return;
|
|
448
536
|
operation = () => window.loadtoagent.tmuxKillWindow({ ...base, target: remote.window.nativeId });
|
|
449
|
-
message = '
|
|
537
|
+
message = t('terminal.tmux.window_closed');
|
|
450
538
|
} else if (action === 'kill-session') {
|
|
451
|
-
if (!window.confirm(
|
|
539
|
+
if (!window.confirm(t('terminal.tmux.confirm_end_workspace', { workspace: remote.session.name }))) return;
|
|
452
540
|
operation = () => window.loadtoagent.tmuxKillSession({ ...base, target: remote.session.nativeId });
|
|
453
|
-
message = '
|
|
541
|
+
message = t('terminal.tmux.workspace_ended');
|
|
454
542
|
}
|
|
455
543
|
if (!operation) return;
|
|
456
|
-
const result = await guarded(operation, message);
|
|
544
|
+
const result = await guarded(operation, message, `tmux-manage:${action}`);
|
|
457
545
|
if (result) {
|
|
458
546
|
if (action.startsWith('kill-')) {
|
|
459
547
|
stopCapture();
|