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
|
/** Connect dashboard agent sessions to live or resumed terminal targets. */
|
|
4
4
|
window.LoadToAgentTerminalAgentActions = function createModule(context) {
|
|
5
|
+
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
5
6
|
const {
|
|
6
7
|
$, state, init, notice, moveWorkbench, selectTmux, selectSession, bindAgent, queueHistoryRefresh,
|
|
7
8
|
renderTarget, fitEntry, refreshSessions, resumeSupport, resumeLaunchArgs, preferredWorkspace, providerLabel, esc,
|
|
@@ -35,7 +36,7 @@ window.LoadToAgentTerminalAgentActions = function createModule(context) {
|
|
|
35
36
|
id: `tmux:${row.distro.name}:${pane.nativeId}`,
|
|
36
37
|
kind: 'tmux',
|
|
37
38
|
label: `${row.distro.name} · ${row.session.name} · ${pane.nativeId}`,
|
|
38
|
-
detail: `${row.window.index}:${row.window.name} · ${pane.command || '
|
|
39
|
+
detail: `${row.window.index}:${row.window.name} · ${pane.command || t('terminal.agent.ai_terminal')}`,
|
|
39
40
|
distro: row.distro.name,
|
|
40
41
|
paneId: pane.id,
|
|
41
42
|
paneNativeId: pane.nativeId,
|
|
@@ -60,26 +61,26 @@ window.LoadToAgentTerminalAgentActions = function createModule(context) {
|
|
|
60
61
|
|
|
61
62
|
function requiredAgentTarget(agentSession, targetId = '') {
|
|
62
63
|
const targets = agentTargets(agentSession);
|
|
63
|
-
if (!targets.length) throw new Error('
|
|
64
|
+
if (!targets.length) throw new Error(t('terminal.agent.no_input_target'));
|
|
64
65
|
if (targetId) {
|
|
65
66
|
const selected = targets.find(target => target.id === targetId);
|
|
66
|
-
if (!selected) throw new Error('
|
|
67
|
+
if (!selected) throw new Error(t('terminal.agent.target_expired'));
|
|
67
68
|
return selected;
|
|
68
69
|
}
|
|
69
|
-
if (targets.length > 1) throw new Error('
|
|
70
|
+
if (targets.length > 1) throw new Error(t('terminal.agent.select_target'));
|
|
70
71
|
return targets[0];
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
async function dispatchAgentCommand(agentSession, command, targetId = '') {
|
|
74
75
|
await init();
|
|
75
76
|
const text = String(command || '').trim();
|
|
76
|
-
if (!text) throw new Error('
|
|
77
|
+
if (!text) throw new Error(t('terminal.agent.command_required'));
|
|
77
78
|
const target = requiredAgentTarget(agentSession, targetId);
|
|
78
79
|
const result = target.kind === 'tmux'
|
|
79
80
|
? await window.loadtoagent.tmuxSendText({ distro: target.distro, target: target.paneNativeId, text, enter: true })
|
|
80
81
|
: await window.loadtoagent.terminalCommand(target.terminalId, text);
|
|
81
|
-
if (!result || result.ok === false) throw new Error(result && result.error || '
|
|
82
|
-
notice(
|
|
82
|
+
if (!result || result.ok === false) throw new Error(result && result.error || t('terminal.agent.send_failed'));
|
|
83
|
+
notice(t('terminal.agent.command_sent', { target: target.label }), 'success');
|
|
83
84
|
return { ok: true, target };
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -99,7 +100,7 @@ window.LoadToAgentTerminalAgentActions = function createModule(context) {
|
|
|
99
100
|
input.value = String(draft || '');
|
|
100
101
|
state.commandDrafts.set(target.id, input.value);
|
|
101
102
|
input.focus({ preventScroll: true });
|
|
102
|
-
notice(
|
|
103
|
+
notice(t('terminal.agent.session_kept', { target: target.label }), 'success');
|
|
103
104
|
return target;
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -108,9 +109,12 @@ window.LoadToAgentTerminalAgentActions = function createModule(context) {
|
|
|
108
109
|
const support = resumeSupport(agentSession);
|
|
109
110
|
if (!support.supported) throw new Error(support.reason);
|
|
110
111
|
const cwd = String(agentSession.cwd || preferredWorkspace() || '').trim();
|
|
111
|
-
if (!cwd) throw new Error('
|
|
112
|
+
if (!cwd) throw new Error(t('terminal.agent.cwd_missing'));
|
|
112
113
|
const prompt = String(draft || '').trim();
|
|
113
|
-
const title =
|
|
114
|
+
const title = t('terminal.agent.resume_title', {
|
|
115
|
+
provider: providerLabel(agentSession.provider),
|
|
116
|
+
session: agentSession.taskName || agentSession.agentName || t('terminal.type.session'),
|
|
117
|
+
});
|
|
114
118
|
const created = await window.loadtoagent.terminalCreate({
|
|
115
119
|
type: 'agent',
|
|
116
120
|
provider: support.provider,
|
|
@@ -121,7 +125,7 @@ window.LoadToAgentTerminalAgentActions = function createModule(context) {
|
|
|
121
125
|
cols: 120,
|
|
122
126
|
rows: 32,
|
|
123
127
|
});
|
|
124
|
-
if (!created || !created.id) throw new Error('
|
|
128
|
+
if (!created || !created.id) throw new Error(t('terminal.agent.resume_terminal_failed'));
|
|
125
129
|
state.mode = 'general';
|
|
126
130
|
moveWorkbench('general');
|
|
127
131
|
await refreshSessions();
|
|
@@ -143,8 +147,8 @@ window.LoadToAgentTerminalAgentActions = function createModule(context) {
|
|
|
143
147
|
input.focus({ preventScroll: true });
|
|
144
148
|
}
|
|
145
149
|
notice(sendDraft && prompt
|
|
146
|
-
?
|
|
147
|
-
:
|
|
150
|
+
? t('terminal.agent.resumed_and_sent', { provider: providerLabel(agentSession.provider), sessionId: support.sessionId.slice(0, 12) })
|
|
151
|
+
: t('terminal.agent.reconnected', { provider: providerLabel(agentSession.provider), sessionId: support.sessionId.slice(0, 12) }), 'success');
|
|
148
152
|
return { ...target, promptSent: Boolean(sendDraft && prompt) };
|
|
149
153
|
}
|
|
150
154
|
|
|
@@ -2,23 +2,89 @@
|
|
|
2
2
|
|
|
3
3
|
/** Bind terminal DOM/preload events using dependencies owned by terminal.js. */
|
|
4
4
|
window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
5
|
+
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
5
6
|
const {
|
|
6
7
|
$, state, createTerminal, openTmuxModal, refreshSnapshot, selectSession, selectTmux,
|
|
7
8
|
sendCommand, currentTargetId, sendSignal, currentSession, guarded, renderAll, showSelection,
|
|
8
9
|
refreshSessions, renderHistoryPanel, fitEntry, attachTmux, currentTmux, manageTmux,
|
|
9
10
|
closeTmuxModal, errorMessage, notice, reorderSession, moveSessionByOffset,
|
|
11
|
+
setTerminalFontSize, toggleTerminalFocusMode,
|
|
10
12
|
} = context;
|
|
11
13
|
|
|
14
|
+
const runBusy = async (button, action) => {
|
|
15
|
+
if (!button || button.dataset.busy === 'true') return null;
|
|
16
|
+
const wasDisabled = button.disabled;
|
|
17
|
+
button.dataset.busy = 'true';
|
|
18
|
+
button.disabled = true;
|
|
19
|
+
button.setAttribute('aria-busy', 'true');
|
|
20
|
+
try {
|
|
21
|
+
return await action();
|
|
22
|
+
} finally {
|
|
23
|
+
if (button.isConnected) {
|
|
24
|
+
delete button.dataset.busy;
|
|
25
|
+
button.disabled = wasDisabled;
|
|
26
|
+
button.removeAttribute('aria-busy');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const writeTerminalOutput = (entry, data) => {
|
|
32
|
+
if (!entry || !data) return;
|
|
33
|
+
const buffer = entry.terminal.buffer.active;
|
|
34
|
+
if (entry.outputWritePending === 0) {
|
|
35
|
+
entry.outputViewportAnchor = Number(buffer.viewportY) || 0;
|
|
36
|
+
entry.outputShouldFollow = entry.outputViewportAnchor >= Number(buffer.baseY || 0);
|
|
37
|
+
entry.outputUserScrollRevision = entry.userScrollRevision;
|
|
38
|
+
}
|
|
39
|
+
entry.outputWritePending += 1;
|
|
40
|
+
entry.terminal.write(data, () => {
|
|
41
|
+
entry.outputWritePending = Math.max(0, entry.outputWritePending - 1);
|
|
42
|
+
if (entry.outputWritePending > 0) return;
|
|
43
|
+
if (!entry.outputShouldFollow && entry.outputUserScrollRevision === entry.userScrollRevision) {
|
|
44
|
+
entry.terminal.scrollToLine(entry.outputViewportAnchor);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
12
49
|
bindTerminalSessionEvents();
|
|
13
50
|
bindTmuxEvents();
|
|
14
51
|
bindTerminalWindowAndPreloadEvents();
|
|
15
52
|
|
|
16
53
|
function bindTerminalSessionEvents() {
|
|
17
|
-
$('#newPowerShellBtn').addEventListener('click', () => createTerminal(state.platform.localShell));
|
|
18
|
-
$('#newWslBtn').addEventListener('click', () => createTerminal('wsl'));
|
|
54
|
+
$('#newPowerShellBtn').addEventListener('click', event => runBusy(event.currentTarget, () => createTerminal(state.platform.localShell)));
|
|
55
|
+
$('#newWslBtn').addEventListener('click', event => runBusy(event.currentTarget, () => createTerminal('wsl')));
|
|
19
56
|
$('#newTmuxSessionBtn').addEventListener('click', openTmuxModal);
|
|
20
|
-
$('#refreshTmuxTerminalBtn').addEventListener('click', refreshSnapshot);
|
|
57
|
+
$('#refreshTmuxTerminalBtn').addEventListener('click', event => runBusy(event.currentTarget, refreshSnapshot));
|
|
21
58
|
const sessionList = $('#terminalSessionList');
|
|
59
|
+
const historyList = $('#terminalHistoryList');
|
|
60
|
+
const cancelHistoryFollow = () => {
|
|
61
|
+
state.historyUserRevision += 1;
|
|
62
|
+
if (state.historyFollowFrame) cancelAnimationFrame(state.historyFollowFrame);
|
|
63
|
+
state.historyFollowFrame = 0;
|
|
64
|
+
};
|
|
65
|
+
const flushPendingHistory = () => {
|
|
66
|
+
if (!state.historyRenderPending || state.historyFlushFrame) return;
|
|
67
|
+
state.historyFlushFrame = requestAnimationFrame(() => {
|
|
68
|
+
state.historyFlushFrame = 0;
|
|
69
|
+
if (!state.historyPointerActive) renderHistoryPanel();
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
historyList.addEventListener('pointerdown', () => {
|
|
73
|
+
state.historyPointerActive = true;
|
|
74
|
+
cancelHistoryFollow();
|
|
75
|
+
}, true);
|
|
76
|
+
historyList.addEventListener('wheel', cancelHistoryFollow, { capture: true, passive: true });
|
|
77
|
+
historyList.addEventListener('click', () => {
|
|
78
|
+
cancelHistoryFollow();
|
|
79
|
+
flushPendingHistory();
|
|
80
|
+
}, true);
|
|
81
|
+
const finishHistoryPointer = () => {
|
|
82
|
+
state.historyPointerActive = false;
|
|
83
|
+
flushPendingHistory();
|
|
84
|
+
};
|
|
85
|
+
window.addEventListener('pointerup', finishHistoryPointer, true);
|
|
86
|
+
window.addEventListener('pointercancel', finishHistoryPointer, true);
|
|
87
|
+
document.addEventListener('selectionchange', flushPendingHistory);
|
|
22
88
|
const clearDropMarkers = () => {
|
|
23
89
|
sessionList.querySelectorAll('.dragging, .drop-before, .drop-after').forEach(item => {
|
|
24
90
|
item.classList.remove('dragging', 'drop-before', 'drop-after');
|
|
@@ -87,6 +153,19 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
87
153
|
});
|
|
88
154
|
sessionList.addEventListener('keydown', event => {
|
|
89
155
|
const item = event.target.closest('[data-terminal-id]');
|
|
156
|
+
if (item && !event.altKey && ['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
|
|
157
|
+
const items = Array.from(sessionList.querySelectorAll('[data-terminal-id]'));
|
|
158
|
+
const current = Math.max(0, items.indexOf(item));
|
|
159
|
+
const next = event.key === 'Home'
|
|
160
|
+
? 0
|
|
161
|
+
: event.key === 'End'
|
|
162
|
+
? items.length - 1
|
|
163
|
+
: (current + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
items.forEach((candidate, index) => { candidate.tabIndex = index === next ? 0 : -1; });
|
|
166
|
+
items[next]?.focus();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
90
169
|
if (!item || !event.altKey || !['ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
|
91
170
|
event.preventDefault();
|
|
92
171
|
const changed = moveSessionByOffset(item.dataset.terminalId, event.key === 'ArrowUp' ? -1 : 1);
|
|
@@ -99,38 +178,112 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
99
178
|
const item = event.target.closest('[data-tmux-distro][data-tmux-pane]');
|
|
100
179
|
if (item) selectTmux(item.dataset.tmuxDistro, item.dataset.tmuxPane);
|
|
101
180
|
});
|
|
181
|
+
$('#terminalTmuxList').addEventListener('keydown', event => {
|
|
182
|
+
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
|
|
183
|
+
const items = Array.from(event.currentTarget.querySelectorAll('[data-tmux-distro][data-tmux-pane]'));
|
|
184
|
+
const current = Math.max(0, items.indexOf(event.target.closest('[data-tmux-distro][data-tmux-pane]')));
|
|
185
|
+
const next = event.key === 'Home'
|
|
186
|
+
? 0
|
|
187
|
+
: event.key === 'End'
|
|
188
|
+
? items.length - 1
|
|
189
|
+
: (current + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
items.forEach((candidate, index) => { candidate.tabIndex = index === next ? 0 : -1; });
|
|
192
|
+
items[next]?.focus();
|
|
193
|
+
});
|
|
102
194
|
$('#terminalCommandForm').addEventListener('submit', async event => {
|
|
103
195
|
event.preventDefault();
|
|
104
196
|
if (state.commandSending) return;
|
|
105
197
|
const input = $('#terminalCommandInput');
|
|
106
198
|
const sent = await sendCommand(input.value);
|
|
107
|
-
if (!sent)
|
|
199
|
+
if (!sent) {
|
|
200
|
+
if ($('#terminalNotice')?.dataset.tone === 'error') $('#terminalNotice').focus({ preventScroll: true });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const targetId = currentTargetId();
|
|
204
|
+
const history = state.commandHistory.get(targetId) || [];
|
|
205
|
+
const command = input.value;
|
|
206
|
+
if (command && history[history.length - 1] !== command) state.commandHistory.set(targetId, [...history, command].slice(-100));
|
|
108
207
|
input.value = '';
|
|
109
|
-
state.commandDrafts.delete(
|
|
208
|
+
state.commandDrafts.delete(targetId);
|
|
209
|
+
state.commandHistoryNavigation = { targetId, index: -1, draft: '' };
|
|
210
|
+
$('#terminalCommandClearBtn').classList.add('hidden');
|
|
211
|
+
$('#terminalCommandCount').classList.remove('warning');
|
|
110
212
|
input.focus({ preventScroll: true });
|
|
111
213
|
});
|
|
112
214
|
$('#terminalCommandInput').addEventListener('input', event => {
|
|
113
215
|
const targetId = currentTargetId();
|
|
114
216
|
if (targetId) state.commandDrafts.set(targetId, event.target.value);
|
|
217
|
+
$('#terminalCommandCount').textContent = `${event.target.value.length.toLocaleString()} / 8,000`;
|
|
218
|
+
$('#terminalCommandClearBtn').classList.toggle('hidden', !event.target.value);
|
|
219
|
+
const count = $('#terminalCommandCount');
|
|
220
|
+
const wasWarning = count.dataset.warning === 'true';
|
|
221
|
+
const warning = event.target.value.length >= 7_200;
|
|
222
|
+
count.classList.toggle('warning', warning);
|
|
223
|
+
count.dataset.warning = warning ? 'true' : 'false';
|
|
224
|
+
if (warning && !wasWarning) window.LoadToAgentA11y?.announce(t('quality.command_near_limit', { count: 8_000 - event.target.value.length }));
|
|
225
|
+
state.commandHistoryNavigation = { targetId, index: -1, draft: event.target.value };
|
|
115
226
|
});
|
|
116
227
|
$('#terminalCommandInput').addEventListener('keydown', event => {
|
|
117
228
|
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing && event.keyCode !== 229) {
|
|
118
229
|
event.preventDefault();
|
|
119
230
|
$('#terminalCommandForm').requestSubmit();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (event.key === 'Escape' && event.currentTarget.value) {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
$('#terminalCommandClearBtn').click();
|
|
236
|
+
return;
|
|
120
237
|
}
|
|
238
|
+
if (!['ArrowUp', 'ArrowDown'].includes(event.key) || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
239
|
+
const targetId = currentTargetId();
|
|
240
|
+
const history = state.commandHistory.get(targetId) || [];
|
|
241
|
+
if (!history.length) return;
|
|
242
|
+
event.preventDefault();
|
|
243
|
+
let navigation = state.commandHistoryNavigation;
|
|
244
|
+
if (navigation.targetId !== targetId || navigation.index < 0) navigation = { targetId, index: history.length, draft: event.currentTarget.value };
|
|
245
|
+
if (event.key === 'ArrowUp') navigation.index = Math.max(0, navigation.index - 1);
|
|
246
|
+
else navigation.index = Math.min(history.length, navigation.index + 1);
|
|
247
|
+
event.currentTarget.value = navigation.index >= history.length ? navigation.draft : history[navigation.index];
|
|
248
|
+
state.commandHistoryNavigation = navigation;
|
|
249
|
+
state.commandDrafts.set(targetId, event.currentTarget.value);
|
|
250
|
+
$('#terminalCommandClearBtn').classList.toggle('hidden', !event.currentTarget.value);
|
|
251
|
+
$('#terminalCommandCount').textContent = `${event.currentTarget.value.length.toLocaleString()} / 8,000`;
|
|
252
|
+
const input = event.currentTarget;
|
|
253
|
+
requestAnimationFrame(() => {
|
|
254
|
+
if (input.isConnected) input.setSelectionRange(input.value.length, input.value.length);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
$('#terminalCommandClearBtn').addEventListener('click', () => {
|
|
258
|
+
const input = $('#terminalCommandInput');
|
|
259
|
+
input.value = '';
|
|
260
|
+
const targetId = currentTargetId();
|
|
261
|
+
if (targetId) state.commandDrafts.delete(targetId);
|
|
262
|
+
state.commandHistoryNavigation = { targetId, index: -1, draft: '' };
|
|
263
|
+
$('#terminalCommandClearBtn').classList.add('hidden');
|
|
264
|
+
$('#terminalCommandCount').textContent = '0 / 8,000';
|
|
265
|
+
$('#terminalCommandCount').classList.remove('warning');
|
|
266
|
+
input.focus({ preventScroll: true });
|
|
267
|
+
window.LoadToAgentA11y?.announce(t('quality.terminal_draft_cleared'));
|
|
121
268
|
});
|
|
269
|
+
$('#terminalFontDecreaseBtn').addEventListener('click', () => setTerminalFontSize(state.terminalFontSize - 1));
|
|
270
|
+
$('#terminalFontIncreaseBtn').addEventListener('click', () => setTerminalFontSize(state.terminalFontSize + 1));
|
|
271
|
+
$('#terminalFocusBtn').addEventListener('click', toggleTerminalFocusMode);
|
|
122
272
|
document.querySelectorAll('[data-terminal-signal]').forEach(button => button.addEventListener('click', () => sendSignal(button.dataset.terminalSignal)));
|
|
123
|
-
$('#terminalRestartBtn').addEventListener('click', async
|
|
273
|
+
$('#terminalRestartBtn').addEventListener('click', async event => {
|
|
124
274
|
const session = currentSession();
|
|
125
275
|
if (!session) return;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
276
|
+
await runBusy(event.currentTarget, async () => {
|
|
277
|
+
const restarted = await guarded(() => window.loadtoagent.terminalRestart(session.id), t('terminal.session.restarted'), `terminal-restart:${session.id}`);
|
|
278
|
+
if (restarted) {
|
|
279
|
+
const entry = state.terminals.get(session.id);
|
|
280
|
+
if (entry) entry.terminal.reset();
|
|
281
|
+
await refreshSessions();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
renderAll();
|
|
132
285
|
});
|
|
133
|
-
$('#terminalCloseBtn').addEventListener('click', async
|
|
286
|
+
$('#terminalCloseBtn').addEventListener('click', async event => {
|
|
134
287
|
const session = currentSession();
|
|
135
288
|
if (!session) {
|
|
136
289
|
state.selectedTmux = null;
|
|
@@ -138,22 +291,25 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
138
291
|
showSelection();
|
|
139
292
|
return;
|
|
140
293
|
}
|
|
141
|
-
if (session.status === 'running' && !window.confirm(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
entry
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
state.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
294
|
+
if (session.status === 'running' && !window.confirm(t('terminal.session.confirm_end', { title: session.title }))) return;
|
|
295
|
+
await runBusy(event.currentTarget, async () => {
|
|
296
|
+
const closed = await guarded(() => window.loadtoagent.terminalClose(session.id), t('terminal.session.ended'), `terminal-close:${session.id}`);
|
|
297
|
+
if (!closed) return;
|
|
298
|
+
const entry = state.terminals.get(session.id);
|
|
299
|
+
if (entry) {
|
|
300
|
+
entry.terminal.dispose();
|
|
301
|
+
entry.host.remove();
|
|
302
|
+
state.terminals.delete(session.id);
|
|
303
|
+
}
|
|
304
|
+
state.commandDrafts.delete(session.id);
|
|
305
|
+
state.selectedId = null;
|
|
306
|
+
if (state.boundTargetId === session.id) {
|
|
307
|
+
state.boundAgent = null;
|
|
308
|
+
state.boundTargetId = '';
|
|
309
|
+
}
|
|
310
|
+
await refreshSessions();
|
|
311
|
+
});
|
|
312
|
+
renderAll();
|
|
157
313
|
});
|
|
158
314
|
$('#terminalHistoryToggle').addEventListener('click', () => {
|
|
159
315
|
state.historyCollapsed = !state.historyCollapsed;
|
|
@@ -164,21 +320,26 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
164
320
|
}
|
|
165
321
|
|
|
166
322
|
function bindTmuxEvents() {
|
|
167
|
-
$('#terminalAttachBtn').addEventListener('click', attachTmux);
|
|
323
|
+
$('#terminalAttachBtn').addEventListener('click', event => runBusy(event.currentTarget, attachTmux));
|
|
168
324
|
$('#terminalTmuxTools').addEventListener('click', event => {
|
|
169
325
|
const button = event.target.closest('[data-tmux-manage]');
|
|
170
|
-
if (button) manageTmux(button.dataset.tmuxManage);
|
|
326
|
+
if (button) runBusy(button, () => manageTmux(button.dataset.tmuxManage));
|
|
171
327
|
});
|
|
172
328
|
$('#terminalTmuxLayout').addEventListener('change', async event => {
|
|
173
329
|
const remote = currentTmux();
|
|
174
330
|
if (!remote) return;
|
|
175
|
-
const result = await guarded(() => window.loadtoagent.tmuxSelectLayout({ distro: remote.distro.name, target: remote.window.nativeId, layout: event.target.value }), '
|
|
331
|
+
const result = await guarded(() => window.loadtoagent.tmuxSelectLayout({ distro: remote.distro.name, target: remote.window.nativeId, layout: event.target.value }), t('terminal.tmux.layout_changed'), `tmux-layout:${remote.window.nativeId}`);
|
|
176
332
|
if (result) setTimeout(refreshSnapshot, 250);
|
|
177
333
|
});
|
|
178
334
|
$('#tmuxCreateForm').addEventListener('submit', async event => {
|
|
179
335
|
event.preventDefault();
|
|
180
336
|
const submit = event.currentTarget.querySelector('[type="submit"]');
|
|
337
|
+
if (submit.dataset.busy === 'true') return;
|
|
338
|
+
submit.dataset.busy = 'true';
|
|
181
339
|
submit.disabled = true;
|
|
340
|
+
submit.setAttribute('aria-busy', 'true');
|
|
341
|
+
$('#closeTmuxCreateBtn').disabled = true;
|
|
342
|
+
$('#cancelTmuxCreateBtn').disabled = true;
|
|
182
343
|
const error = $('#tmuxCreateError');
|
|
183
344
|
error.classList.add('hidden');
|
|
184
345
|
try {
|
|
@@ -189,23 +350,55 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
189
350
|
command: $('#tmuxCreateCommand').value,
|
|
190
351
|
});
|
|
191
352
|
if (result && result.ok) {
|
|
192
|
-
closeTmuxModal();
|
|
193
|
-
notice('
|
|
353
|
+
closeTmuxModal(true);
|
|
354
|
+
notice(t('terminal.tmux.workspace_created'), 'success');
|
|
194
355
|
setTimeout(refreshSnapshot, 300);
|
|
195
356
|
} else {
|
|
196
|
-
error.textContent = result && result.error || '
|
|
357
|
+
error.textContent = result && result.error || t('terminal.tmux.workspace_create_failed');
|
|
197
358
|
error.classList.remove('hidden');
|
|
359
|
+
error.focus({ preventScroll: true });
|
|
198
360
|
}
|
|
199
361
|
} catch (failure) {
|
|
200
362
|
error.textContent = errorMessage(failure);
|
|
201
363
|
error.classList.remove('hidden');
|
|
364
|
+
error.focus({ preventScroll: true });
|
|
202
365
|
} finally {
|
|
366
|
+
delete submit.dataset.busy;
|
|
203
367
|
submit.disabled = false;
|
|
368
|
+
submit.removeAttribute('aria-busy');
|
|
369
|
+
$('#closeTmuxCreateBtn').disabled = false;
|
|
370
|
+
$('#cancelTmuxCreateBtn').disabled = false;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
$('#tmuxCreateForm').addEventListener('invalid', event => {
|
|
374
|
+
event.target.setAttribute('aria-invalid', 'true');
|
|
375
|
+
}, true);
|
|
376
|
+
$('#tmuxCreateForm').addEventListener('input', event => {
|
|
377
|
+
if (event.target.matches('input, textarea, select') && event.target.checkValidity()) event.target.removeAttribute('aria-invalid');
|
|
378
|
+
});
|
|
379
|
+
$('#tmuxCreateName').addEventListener('blur', event => {
|
|
380
|
+
const normalized = event.target.value.trim().replace(/\s+/g, '-');
|
|
381
|
+
if (normalized !== event.target.value) {
|
|
382
|
+
event.target.value = normalized;
|
|
383
|
+
event.target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
$('#pickTmuxCwdBtn').addEventListener('click', event => runBusy(event.currentTarget, async () => {
|
|
387
|
+
try {
|
|
388
|
+
const folder = await window.loadtoagent.pickWorkspace();
|
|
389
|
+
if (folder) $('#tmuxCreateCwd').value = folder;
|
|
390
|
+
} catch (failure) {
|
|
391
|
+
notice(errorMessage(failure), 'error');
|
|
204
392
|
}
|
|
393
|
+
}));
|
|
394
|
+
$('#closeTmuxCreateBtn').addEventListener('click', () => closeTmuxModal());
|
|
395
|
+
$('#cancelTmuxCreateBtn').addEventListener('click', () => closeTmuxModal());
|
|
396
|
+
let tmuxBackdropPress = null;
|
|
397
|
+
$('#tmuxCreateModal').addEventListener('pointerdown', event => { tmuxBackdropPress = event.target === event.currentTarget; });
|
|
398
|
+
$('#tmuxCreateModal').addEventListener('click', event => {
|
|
399
|
+
if (event.target === event.currentTarget && tmuxBackdropPress !== false) closeTmuxModal();
|
|
400
|
+
tmuxBackdropPress = null;
|
|
205
401
|
});
|
|
206
|
-
$('#closeTmuxCreateBtn').addEventListener('click', closeTmuxModal);
|
|
207
|
-
$('#cancelTmuxCreateBtn').addEventListener('click', closeTmuxModal);
|
|
208
|
-
$('#tmuxCreateModal').addEventListener('click', event => { if (event.target === event.currentTarget) closeTmuxModal(); });
|
|
209
402
|
}
|
|
210
403
|
|
|
211
404
|
function bindTerminalWindowAndPreloadEvents() {
|
|
@@ -218,9 +411,9 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
218
411
|
});
|
|
219
412
|
window.loadtoagent.onTerminalData(payload => {
|
|
220
413
|
const entry = state.terminals.get(payload && payload.id);
|
|
221
|
-
|
|
414
|
+
writeTerminalOutput(entry, payload && payload.data);
|
|
222
415
|
});
|
|
223
416
|
window.loadtoagent.onTerminalState(payload => refreshSessions(payload));
|
|
224
|
-
window.loadtoagent.onTerminalError(payload => notice(payload && payload.message || '
|
|
417
|
+
window.loadtoagent.onTerminalError(payload => notice(payload && payload.message || t('terminal.error.input_failed'), 'error'));
|
|
225
418
|
}
|
|
226
419
|
};
|