loadtoagent 1.1.0 → 1.3.2
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 +14 -5
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/docs/PROVIDER-CONTRACTS.md +25 -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 +157 -38
- package/package.json +13 -4
- package/preload.js +13 -2
- package/renderer/app-agent-actions.js +125 -53
- package/renderer/app-bootstrap.js +54 -19
- package/renderer/app-dashboard.js +196 -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 +257 -0
- package/renderer/app-provider-visibility.js +124 -0
- package/renderer/app-quality.js +568 -0
- package/renderer/app-run-modal.js +104 -34
- 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 +867 -37
- package/renderer/i18n.js +104 -0
- package/renderer/index.html +154 -67
- package/renderer/shared.js +17 -0
- package/renderer/styles-agent-map.css +16 -10
- package/renderer/styles-cards.css +44 -18
- package/renderer/styles-collaboration.css +23 -23
- package/renderer/styles-components.css +122 -45
- package/renderer/styles-management.css +364 -0
- package/renderer/styles-onboarding.css +8 -8
- package/renderer/styles-overlays.css +13 -7
- package/renderer/styles-product.css +48 -40
- package/renderer/styles-quality.css +312 -0
- package/renderer/styles-readability.css +1040 -0
- package/renderer/styles-responsive-product.css +84 -12
- package/renderer/styles-responsive-runtime.css +25 -17
- package/renderer/styles-responsive-shell.css +44 -48
- package/renderer/styles-responsive-workflows.css +39 -5
- package/renderer/styles-run-composer.css +28 -23
- package/renderer/styles-runtime-overview.css +285 -0
- package/renderer/styles-settings.css +176 -16
- package/renderer/styles-terminal.css +374 -37
- package/renderer/styles-tmux.css +49 -49
- package/renderer/styles-workflow-map.css +362 -15
- package/renderer/styles-workflows.css +116 -49
- package/renderer/styles.css +34 -19
- package/renderer/terminal-agent.js +17 -13
- package/renderer/terminal-events.js +253 -37
- package/renderer/terminal-workbench.js +208 -89
- package/renderer/terminal.js +350 -35
- 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 -14
- 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 +261 -0
- package/src/terminalHost.js +453 -0
- package/src/terminalHostDaemon.js +61 -0
- package/src/terminalManager.js +218 -6
- 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,90 @@
|
|
|
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,
|
|
12
|
+
isAiTerminalSession,
|
|
10
13
|
} = context;
|
|
11
14
|
|
|
15
|
+
const runBusy = async (button, action) => {
|
|
16
|
+
if (!button || button.dataset.busy === 'true') return null;
|
|
17
|
+
const wasDisabled = button.disabled;
|
|
18
|
+
button.dataset.busy = 'true';
|
|
19
|
+
button.disabled = true;
|
|
20
|
+
button.setAttribute('aria-busy', 'true');
|
|
21
|
+
try {
|
|
22
|
+
return await action();
|
|
23
|
+
} finally {
|
|
24
|
+
if (button.isConnected) {
|
|
25
|
+
delete button.dataset.busy;
|
|
26
|
+
button.disabled = wasDisabled;
|
|
27
|
+
button.removeAttribute('aria-busy');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const writeTerminalOutput = (entry, data) => {
|
|
33
|
+
if (!entry || !data) return;
|
|
34
|
+
const buffer = entry.terminal.buffer.active;
|
|
35
|
+
if (entry.outputWritePending === 0) {
|
|
36
|
+
entry.outputViewportAnchor = Number(buffer.viewportY) || 0;
|
|
37
|
+
entry.outputShouldFollow = entry.outputViewportAnchor >= Number(buffer.baseY || 0);
|
|
38
|
+
entry.outputUserScrollRevision = entry.userScrollRevision;
|
|
39
|
+
}
|
|
40
|
+
entry.outputWritePending += 1;
|
|
41
|
+
entry.terminal.write(data, () => {
|
|
42
|
+
entry.outputWritePending = Math.max(0, entry.outputWritePending - 1);
|
|
43
|
+
if (entry.outputWritePending > 0) return;
|
|
44
|
+
if (!entry.outputShouldFollow && entry.outputUserScrollRevision === entry.userScrollRevision) {
|
|
45
|
+
entry.terminal.scrollToLine(entry.outputViewportAnchor);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
12
50
|
bindTerminalSessionEvents();
|
|
13
51
|
bindTmuxEvents();
|
|
14
52
|
bindTerminalWindowAndPreloadEvents();
|
|
15
53
|
|
|
16
54
|
function bindTerminalSessionEvents() {
|
|
17
|
-
$('#newPowerShellBtn').addEventListener('click', () => createTerminal(state.platform.localShell));
|
|
18
|
-
$('#newWslBtn').addEventListener('click', () => createTerminal('wsl'));
|
|
55
|
+
$('#newPowerShellBtn').addEventListener('click', event => runBusy(event.currentTarget, () => createTerminal(state.platform.localShell)));
|
|
56
|
+
$('#newWslBtn').addEventListener('click', event => runBusy(event.currentTarget, () => createTerminal('wsl')));
|
|
19
57
|
$('#newTmuxSessionBtn').addEventListener('click', openTmuxModal);
|
|
20
|
-
$('#refreshTmuxTerminalBtn').addEventListener('click', refreshSnapshot);
|
|
58
|
+
$('#refreshTmuxTerminalBtn').addEventListener('click', event => runBusy(event.currentTarget, refreshSnapshot));
|
|
21
59
|
const sessionList = $('#terminalSessionList');
|
|
60
|
+
const historyList = $('#terminalHistoryList');
|
|
61
|
+
const cancelHistoryFollow = () => {
|
|
62
|
+
state.historyUserRevision += 1;
|
|
63
|
+
if (state.historyFollowFrame) cancelAnimationFrame(state.historyFollowFrame);
|
|
64
|
+
state.historyFollowFrame = 0;
|
|
65
|
+
};
|
|
66
|
+
const flushPendingHistory = () => {
|
|
67
|
+
if (!state.historyRenderPending || state.historyFlushFrame) return;
|
|
68
|
+
state.historyFlushFrame = requestAnimationFrame(() => {
|
|
69
|
+
state.historyFlushFrame = 0;
|
|
70
|
+
if (!state.historyPointerActive) renderHistoryPanel();
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
historyList.addEventListener('pointerdown', () => {
|
|
74
|
+
state.historyPointerActive = true;
|
|
75
|
+
cancelHistoryFollow();
|
|
76
|
+
}, true);
|
|
77
|
+
historyList.addEventListener('wheel', cancelHistoryFollow, { capture: true, passive: true });
|
|
78
|
+
historyList.addEventListener('click', () => {
|
|
79
|
+
cancelHistoryFollow();
|
|
80
|
+
flushPendingHistory();
|
|
81
|
+
}, true);
|
|
82
|
+
const finishHistoryPointer = () => {
|
|
83
|
+
state.historyPointerActive = false;
|
|
84
|
+
flushPendingHistory();
|
|
85
|
+
};
|
|
86
|
+
window.addEventListener('pointerup', finishHistoryPointer, true);
|
|
87
|
+
window.addEventListener('pointercancel', finishHistoryPointer, true);
|
|
88
|
+
document.addEventListener('selectionchange', flushPendingHistory);
|
|
22
89
|
const clearDropMarkers = () => {
|
|
23
90
|
sessionList.querySelectorAll('.dragging, .drop-before, .drop-after').forEach(item => {
|
|
24
91
|
item.classList.remove('dragging', 'drop-before', 'drop-after');
|
|
@@ -87,6 +154,19 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
87
154
|
});
|
|
88
155
|
sessionList.addEventListener('keydown', event => {
|
|
89
156
|
const item = event.target.closest('[data-terminal-id]');
|
|
157
|
+
if (item && !event.altKey && ['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
|
|
158
|
+
const items = Array.from(sessionList.querySelectorAll('[data-terminal-id]'));
|
|
159
|
+
const current = Math.max(0, items.indexOf(item));
|
|
160
|
+
const next = event.key === 'Home'
|
|
161
|
+
? 0
|
|
162
|
+
: event.key === 'End'
|
|
163
|
+
? items.length - 1
|
|
164
|
+
: (current + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
items.forEach((candidate, index) => { candidate.tabIndex = index === next ? 0 : -1; });
|
|
167
|
+
items[next]?.focus();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
90
170
|
if (!item || !event.altKey || !['ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
|
91
171
|
event.preventDefault();
|
|
92
172
|
const changed = moveSessionByOffset(item.dataset.terminalId, event.key === 'ArrowUp' ? -1 : 1);
|
|
@@ -99,62 +179,157 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
99
179
|
const item = event.target.closest('[data-tmux-distro][data-tmux-pane]');
|
|
100
180
|
if (item) selectTmux(item.dataset.tmuxDistro, item.dataset.tmuxPane);
|
|
101
181
|
});
|
|
182
|
+
$('#terminalTmuxList').addEventListener('keydown', event => {
|
|
183
|
+
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
|
|
184
|
+
const items = Array.from(event.currentTarget.querySelectorAll('[data-tmux-distro][data-tmux-pane]'));
|
|
185
|
+
const current = Math.max(0, items.indexOf(event.target.closest('[data-tmux-distro][data-tmux-pane]')));
|
|
186
|
+
const next = event.key === 'Home'
|
|
187
|
+
? 0
|
|
188
|
+
: event.key === 'End'
|
|
189
|
+
? items.length - 1
|
|
190
|
+
: (current + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
items.forEach((candidate, index) => { candidate.tabIndex = index === next ? 0 : -1; });
|
|
193
|
+
items[next]?.focus();
|
|
194
|
+
});
|
|
102
195
|
$('#terminalCommandForm').addEventListener('submit', async event => {
|
|
103
196
|
event.preventDefault();
|
|
104
197
|
if (state.commandSending) return;
|
|
105
198
|
const input = $('#terminalCommandInput');
|
|
106
199
|
const sent = await sendCommand(input.value);
|
|
107
|
-
if (!sent)
|
|
200
|
+
if (!sent) {
|
|
201
|
+
if ($('#terminalNotice')?.dataset.tone === 'error') $('#terminalNotice').focus({ preventScroll: true });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const targetId = currentTargetId();
|
|
205
|
+
const history = state.commandHistory.get(targetId) || [];
|
|
206
|
+
const command = input.value;
|
|
207
|
+
if (command && history[history.length - 1] !== command) state.commandHistory.set(targetId, [...history, command].slice(-100));
|
|
108
208
|
input.value = '';
|
|
109
|
-
state.commandDrafts.delete(
|
|
209
|
+
state.commandDrafts.delete(targetId);
|
|
210
|
+
state.commandHistoryNavigation = { targetId, index: -1, draft: '' };
|
|
211
|
+
$('#terminalCommandClearBtn').classList.add('hidden');
|
|
212
|
+
$('#terminalCommandCount').classList.remove('warning');
|
|
110
213
|
input.focus({ preventScroll: true });
|
|
111
214
|
});
|
|
112
215
|
$('#terminalCommandInput').addEventListener('input', event => {
|
|
113
216
|
const targetId = currentTargetId();
|
|
114
217
|
if (targetId) state.commandDrafts.set(targetId, event.target.value);
|
|
218
|
+
$('#terminalCommandCount').textContent = `${event.target.value.length.toLocaleString()} / 8,000`;
|
|
219
|
+
$('#terminalCommandClearBtn').classList.toggle('hidden', !event.target.value);
|
|
220
|
+
const count = $('#terminalCommandCount');
|
|
221
|
+
const wasWarning = count.dataset.warning === 'true';
|
|
222
|
+
const warning = event.target.value.length >= 7_200;
|
|
223
|
+
count.classList.toggle('warning', warning);
|
|
224
|
+
count.dataset.warning = warning ? 'true' : 'false';
|
|
225
|
+
if (warning && !wasWarning) window.LoadToAgentA11y?.announce(t('quality.command_near_limit', { count: 8_000 - event.target.value.length }));
|
|
226
|
+
state.commandHistoryNavigation = { targetId, index: -1, draft: event.target.value };
|
|
115
227
|
});
|
|
116
228
|
$('#terminalCommandInput').addEventListener('keydown', event => {
|
|
117
229
|
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing && event.keyCode !== 229) {
|
|
118
230
|
event.preventDefault();
|
|
119
231
|
$('#terminalCommandForm').requestSubmit();
|
|
232
|
+
return;
|
|
120
233
|
}
|
|
234
|
+
if (event.key === 'Escape' && event.currentTarget.value) {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
$('#terminalCommandClearBtn').click();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!['ArrowUp', 'ArrowDown'].includes(event.key) || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
240
|
+
const targetId = currentTargetId();
|
|
241
|
+
const history = state.commandHistory.get(targetId) || [];
|
|
242
|
+
if (!history.length) return;
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
let navigation = state.commandHistoryNavigation;
|
|
245
|
+
if (navigation.targetId !== targetId || navigation.index < 0) navigation = { targetId, index: history.length, draft: event.currentTarget.value };
|
|
246
|
+
if (event.key === 'ArrowUp') navigation.index = Math.max(0, navigation.index - 1);
|
|
247
|
+
else navigation.index = Math.min(history.length, navigation.index + 1);
|
|
248
|
+
event.currentTarget.value = navigation.index >= history.length ? navigation.draft : history[navigation.index];
|
|
249
|
+
state.commandHistoryNavigation = navigation;
|
|
250
|
+
state.commandDrafts.set(targetId, event.currentTarget.value);
|
|
251
|
+
$('#terminalCommandClearBtn').classList.toggle('hidden', !event.currentTarget.value);
|
|
252
|
+
$('#terminalCommandCount').textContent = `${event.currentTarget.value.length.toLocaleString()} / 8,000`;
|
|
253
|
+
const input = event.currentTarget;
|
|
254
|
+
requestAnimationFrame(() => {
|
|
255
|
+
if (input.isConnected) input.setSelectionRange(input.value.length, input.value.length);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
$('#terminalCommandClearBtn').addEventListener('click', () => {
|
|
259
|
+
const input = $('#terminalCommandInput');
|
|
260
|
+
input.value = '';
|
|
261
|
+
const targetId = currentTargetId();
|
|
262
|
+
if (targetId) state.commandDrafts.delete(targetId);
|
|
263
|
+
state.commandHistoryNavigation = { targetId, index: -1, draft: '' };
|
|
264
|
+
$('#terminalCommandClearBtn').classList.add('hidden');
|
|
265
|
+
$('#terminalCommandCount').textContent = '0 / 8,000';
|
|
266
|
+
$('#terminalCommandCount').classList.remove('warning');
|
|
267
|
+
input.focus({ preventScroll: true });
|
|
268
|
+
window.LoadToAgentA11y?.announce(t('quality.terminal_draft_cleared'));
|
|
121
269
|
});
|
|
270
|
+
$('#terminalFontDecreaseBtn').addEventListener('click', () => setTerminalFontSize(state.terminalFontSize - 1));
|
|
271
|
+
$('#terminalFontIncreaseBtn').addEventListener('click', () => setTerminalFontSize(state.terminalFontSize + 1));
|
|
272
|
+
$('#terminalFocusBtn').addEventListener('click', toggleTerminalFocusMode);
|
|
122
273
|
document.querySelectorAll('[data-terminal-signal]').forEach(button => button.addEventListener('click', () => sendSignal(button.dataset.terminalSignal)));
|
|
123
|
-
$('#terminalRestartBtn').addEventListener('click', async
|
|
274
|
+
$('#terminalRestartBtn').addEventListener('click', async event => {
|
|
124
275
|
const session = currentSession();
|
|
125
276
|
if (!session) return;
|
|
126
|
-
|
|
127
|
-
|
|
277
|
+
await runBusy(event.currentTarget, async () => {
|
|
278
|
+
const restarted = await guarded(() => window.loadtoagent.terminalRestart(session.id), t('terminal.session.restarted'), `terminal-restart:${session.id}`);
|
|
279
|
+
if (restarted) {
|
|
280
|
+
const entry = state.terminals.get(session.id);
|
|
281
|
+
if (entry) entry.terminal.reset();
|
|
282
|
+
await refreshSessions();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
renderAll();
|
|
286
|
+
});
|
|
287
|
+
const endTerminalSession = async (button, session) => {
|
|
288
|
+
if (!session) return;
|
|
289
|
+
const confirmation = isAiTerminalSession(session) ? 'terminal.session.confirm_end_ai' : 'terminal.session.confirm_end';
|
|
290
|
+
if (session.type !== 'tmux' && session.status === 'running' && !window.confirm(t(confirmation, { title: session.title }))) return;
|
|
291
|
+
await runBusy(button, async () => {
|
|
292
|
+
const message = session.type === 'tmux' ? t('terminal.tmux.detached_input') : t('terminal.session.ended');
|
|
293
|
+
const closed = await guarded(() => window.loadtoagent.terminalClose(session.id), message, `terminal-close:${session.id}`);
|
|
294
|
+
if (!closed) return;
|
|
128
295
|
const entry = state.terminals.get(session.id);
|
|
129
|
-
if (entry)
|
|
296
|
+
if (entry) {
|
|
297
|
+
entry.terminal.dispose();
|
|
298
|
+
entry.host.remove();
|
|
299
|
+
state.terminals.delete(session.id);
|
|
300
|
+
}
|
|
301
|
+
state.commandDrafts.delete(session.id);
|
|
302
|
+
state.selectedId = null;
|
|
303
|
+
if (state.boundTargetId === session.id) {
|
|
304
|
+
state.boundAgent = null;
|
|
305
|
+
state.boundTargetId = '';
|
|
306
|
+
}
|
|
130
307
|
await refreshSessions();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
308
|
+
});
|
|
309
|
+
renderAll();
|
|
310
|
+
};
|
|
311
|
+
$('#terminalCloseBtn').addEventListener('click', async event => {
|
|
134
312
|
const session = currentSession();
|
|
135
313
|
if (!session) {
|
|
136
314
|
state.selectedTmux = null;
|
|
137
315
|
renderAll();
|
|
138
|
-
showSelection();
|
|
316
|
+
await showSelection();
|
|
139
317
|
return;
|
|
140
318
|
}
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const entry = state.terminals.get(session.id);
|
|
145
|
-
if (entry) {
|
|
146
|
-
entry.terminal.dispose();
|
|
147
|
-
entry.host.remove();
|
|
148
|
-
state.terminals.delete(session.id);
|
|
149
|
-
}
|
|
150
|
-
state.commandDrafts.delete(session.id);
|
|
151
|
-
state.selectedId = null;
|
|
152
|
-
if (state.boundTargetId === session.id) {
|
|
319
|
+
if (isAiTerminalSession(session)) {
|
|
320
|
+
state.captureGeneration += 1;
|
|
321
|
+
state.selectedId = null;
|
|
153
322
|
state.boundAgent = null;
|
|
154
323
|
state.boundTargetId = '';
|
|
324
|
+
renderAll();
|
|
325
|
+
renderHistoryPanel();
|
|
326
|
+
await showSelection();
|
|
327
|
+
notice(t('terminal.view_closed_ai_kept'), 'success');
|
|
328
|
+
return;
|
|
155
329
|
}
|
|
156
|
-
await
|
|
330
|
+
await endTerminalSession(event.currentTarget, session);
|
|
157
331
|
});
|
|
332
|
+
$('#terminalEndSessionBtn').addEventListener('click', event => endTerminalSession(event.currentTarget, currentSession()));
|
|
158
333
|
$('#terminalHistoryToggle').addEventListener('click', () => {
|
|
159
334
|
state.historyCollapsed = !state.historyCollapsed;
|
|
160
335
|
renderHistoryPanel();
|
|
@@ -164,21 +339,26 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
164
339
|
}
|
|
165
340
|
|
|
166
341
|
function bindTmuxEvents() {
|
|
167
|
-
$('#terminalAttachBtn').addEventListener('click', attachTmux);
|
|
342
|
+
$('#terminalAttachBtn').addEventListener('click', event => runBusy(event.currentTarget, attachTmux));
|
|
168
343
|
$('#terminalTmuxTools').addEventListener('click', event => {
|
|
169
344
|
const button = event.target.closest('[data-tmux-manage]');
|
|
170
|
-
if (button) manageTmux(button.dataset.tmuxManage);
|
|
345
|
+
if (button) runBusy(button, () => manageTmux(button.dataset.tmuxManage));
|
|
171
346
|
});
|
|
172
347
|
$('#terminalTmuxLayout').addEventListener('change', async event => {
|
|
173
348
|
const remote = currentTmux();
|
|
174
349
|
if (!remote) return;
|
|
175
|
-
const result = await guarded(() => window.loadtoagent.tmuxSelectLayout({ distro: remote.distro.name, target: remote.window.nativeId, layout: event.target.value }), '
|
|
350
|
+
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
351
|
if (result) setTimeout(refreshSnapshot, 250);
|
|
177
352
|
});
|
|
178
353
|
$('#tmuxCreateForm').addEventListener('submit', async event => {
|
|
179
354
|
event.preventDefault();
|
|
180
355
|
const submit = event.currentTarget.querySelector('[type="submit"]');
|
|
356
|
+
if (submit.dataset.busy === 'true') return;
|
|
357
|
+
submit.dataset.busy = 'true';
|
|
181
358
|
submit.disabled = true;
|
|
359
|
+
submit.setAttribute('aria-busy', 'true');
|
|
360
|
+
$('#closeTmuxCreateBtn').disabled = true;
|
|
361
|
+
$('#cancelTmuxCreateBtn').disabled = true;
|
|
182
362
|
const error = $('#tmuxCreateError');
|
|
183
363
|
error.classList.add('hidden');
|
|
184
364
|
try {
|
|
@@ -189,23 +369,55 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
189
369
|
command: $('#tmuxCreateCommand').value,
|
|
190
370
|
});
|
|
191
371
|
if (result && result.ok) {
|
|
192
|
-
closeTmuxModal();
|
|
193
|
-
notice('
|
|
372
|
+
closeTmuxModal(true);
|
|
373
|
+
notice(t('terminal.tmux.workspace_created'), 'success');
|
|
194
374
|
setTimeout(refreshSnapshot, 300);
|
|
195
375
|
} else {
|
|
196
|
-
error.textContent = result && result.error || '
|
|
376
|
+
error.textContent = result && result.error || t('terminal.tmux.workspace_create_failed');
|
|
197
377
|
error.classList.remove('hidden');
|
|
378
|
+
error.focus({ preventScroll: true });
|
|
198
379
|
}
|
|
199
380
|
} catch (failure) {
|
|
200
381
|
error.textContent = errorMessage(failure);
|
|
201
382
|
error.classList.remove('hidden');
|
|
383
|
+
error.focus({ preventScroll: true });
|
|
202
384
|
} finally {
|
|
385
|
+
delete submit.dataset.busy;
|
|
203
386
|
submit.disabled = false;
|
|
387
|
+
submit.removeAttribute('aria-busy');
|
|
388
|
+
$('#closeTmuxCreateBtn').disabled = false;
|
|
389
|
+
$('#cancelTmuxCreateBtn').disabled = false;
|
|
204
390
|
}
|
|
205
391
|
});
|
|
206
|
-
$('#
|
|
207
|
-
|
|
208
|
-
|
|
392
|
+
$('#tmuxCreateForm').addEventListener('invalid', event => {
|
|
393
|
+
event.target.setAttribute('aria-invalid', 'true');
|
|
394
|
+
}, true);
|
|
395
|
+
$('#tmuxCreateForm').addEventListener('input', event => {
|
|
396
|
+
if (event.target.matches('input, textarea, select') && event.target.checkValidity()) event.target.removeAttribute('aria-invalid');
|
|
397
|
+
});
|
|
398
|
+
$('#tmuxCreateName').addEventListener('blur', event => {
|
|
399
|
+
const normalized = event.target.value.trim().replace(/\s+/g, '-');
|
|
400
|
+
if (normalized !== event.target.value) {
|
|
401
|
+
event.target.value = normalized;
|
|
402
|
+
event.target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
$('#pickTmuxCwdBtn').addEventListener('click', event => runBusy(event.currentTarget, async () => {
|
|
406
|
+
try {
|
|
407
|
+
const folder = await window.loadtoagent.pickWorkspace();
|
|
408
|
+
if (folder) $('#tmuxCreateCwd').value = folder;
|
|
409
|
+
} catch (failure) {
|
|
410
|
+
notice(errorMessage(failure), 'error');
|
|
411
|
+
}
|
|
412
|
+
}));
|
|
413
|
+
$('#closeTmuxCreateBtn').addEventListener('click', () => closeTmuxModal());
|
|
414
|
+
$('#cancelTmuxCreateBtn').addEventListener('click', () => closeTmuxModal());
|
|
415
|
+
let tmuxBackdropPress = null;
|
|
416
|
+
$('#tmuxCreateModal').addEventListener('pointerdown', event => { tmuxBackdropPress = event.target === event.currentTarget; });
|
|
417
|
+
$('#tmuxCreateModal').addEventListener('click', event => {
|
|
418
|
+
if (event.target === event.currentTarget && tmuxBackdropPress !== false) closeTmuxModal();
|
|
419
|
+
tmuxBackdropPress = null;
|
|
420
|
+
});
|
|
209
421
|
}
|
|
210
422
|
|
|
211
423
|
function bindTerminalWindowAndPreloadEvents() {
|
|
@@ -218,9 +430,13 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
|
|
|
218
430
|
});
|
|
219
431
|
window.loadtoagent.onTerminalData(payload => {
|
|
220
432
|
const entry = state.terminals.get(payload && payload.id);
|
|
221
|
-
|
|
433
|
+
writeTerminalOutput(entry, payload && payload.data);
|
|
222
434
|
});
|
|
223
435
|
window.loadtoagent.onTerminalState(payload => refreshSessions(payload));
|
|
224
|
-
window.loadtoagent.onTerminalError(payload => notice(payload && payload.message || '
|
|
436
|
+
window.loadtoagent.onTerminalError(payload => notice(payload && payload.message || t('terminal.error.input_failed'), 'error'));
|
|
437
|
+
window.loadtoagent.onTerminalConnection?.(payload => {
|
|
438
|
+
const tone = payload?.state === 'failed' ? 'error' : payload?.state === 'connected' ? 'success' : 'info';
|
|
439
|
+
notice(payload?.message || t('terminal.error.input_failed'), tone);
|
|
440
|
+
});
|
|
225
441
|
}
|
|
226
442
|
};
|