fix: 更新进程管理器以支持键盘复制功能,并优化多流解码器的使用

This commit is contained in:
cmliu committed 2026-07-31 19:30:01 +08:00
1 parent 50830414e9
commit fd621efaa2
2 files changed
+39 -14

No files matched your search

+15 -1
View File
@@ -219,6 +219,19 @@ export class ProcessManager {
return;
}
});
// Copyable PID/command cells are keyboard-focusable (tabIndex=0); mirror the click-to-copy
// behavior for Enter/Space so the interaction is operable without a mouse.
this.elements.panel.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' && event.key !== ' ' && event.key !== 'Spacebar') return;
if (event.ctrlKey || event.metaKey || event.altKey) return;
const target = event.target as HTMLElement;
const copyCell = target.closest<HTMLTableCellElement>('[data-process-copy-value]');
if (!copyCell || !this.elements.tableBody.contains(copyCell)) return;
const value = copyCell.dataset.processCopyValue ?? '';
if (!value) return;
event.preventDefault();
void this.copyProcessValue(value, copyCell.dataset.processCopyKind as 'pid' | 'command' | undefined);
});
this.bindKillDialog();
this.render();
}
@@ -553,7 +566,8 @@ export class ProcessManager {
zh = `结束 PID ${result.pid} 失败`;
en = `Failed to terminate PID ${result.pid}`;
}
this.bilingualToast(zh, en, 'error');
const kind: 'info' | 'error' = result.ok ? 'info' : 'error';
this.bilingualToast(zh, en, kind);
}
private bilingualToast(zh: string, en: string, kind: 'info' | 'error' = 'info'): void {
+24 -13
View File
@@ -71,7 +71,8 @@ interface PendingProcessKillChannel {
timeout: ReturnType<typeof setTimeout> | null;
cancelled: boolean;
openConfirmed: boolean;
decoder: TextDecoder;
stdoutDecoder: TextDecoder;
stderrDecoder: TextDecoder;
stdout: string;
stderr: string;
stdoutBytes: number;
@@ -913,7 +914,8 @@ export class SSHSession {
timeout: null,
cancelled: false,
openConfirmed: false,
decoder: new TextDecoder(),
stdoutDecoder: new TextDecoder(),
stderrDecoder: new TextDecoder(),
stdout: '',
stderr: '',
stdoutBytes: 0,
@@ -928,6 +930,7 @@ export class SSHSession {
} catch (error) {
this.clearPendingProcessKill(channel);
this.channels.delete(channelID);
this.pendingProcessKillChannels.delete(channelID);
throw error;
}
}
@@ -1029,7 +1032,7 @@ export class SSHSession {
private appendKillOutput(kill: PendingProcessKillChannel, output: Uint8Array, isStderr: boolean): void {
if (kill.finalized) return;
const text = kill.decoder.decode(output, { stream: true });
const text = (isStderr ? kill.stderrDecoder : kill.stdoutDecoder).decode(output, { stream: true });
if (!text) return;
if (isStderr) {
const remaining = PROCESS_KILL_MAX_BUFFER_BYTES - kill.stderrBytes;
@@ -1050,16 +1053,24 @@ export class SSHSession {
if (kill.finalized) return;
kill.finalized = true;
if (kill.timeout) { clearTimeout(kill.timeout); kill.timeout = null; }
// Flush any decoder tail so callers see a complete (possibly empty) string.
if (!errorMessage) {
const tail = kill.decoder.decode();
if (tail) {
const remaining = PROCESS_KILL_MAX_BUFFER_BYTES - kill.stdoutBytes;
if (remaining > 0) {
const slice = tail.length > remaining ? tail.slice(0, remaining) : tail;
kill.stdout += slice;
kill.stdoutBytes += slice.length;
}
// Flush any decoder tail so callers see a complete (possibly empty) string. Each stream uses
// its own decoder, so an interleaved multibyte sequence can never corrupt the other stream.
const stdoutTail = kill.stdoutDecoder.decode();
if (stdoutTail) {
const remaining = PROCESS_KILL_MAX_BUFFER_BYTES - kill.stdoutBytes;
if (remaining > 0) {
const slice = stdoutTail.length > remaining ? stdoutTail.slice(0, remaining) : stdoutTail;
kill.stdout += slice;
kill.stdoutBytes += slice.length;
}
}
const stderrTail = kill.stderrDecoder.decode();
if (stderrTail) {
const remaining = PROCESS_KILL_MAX_BUFFER_BYTES - kill.stderrBytes;
if (remaining > 0) {
const slice = stderrTail.length > remaining ? stderrTail.slice(0, remaining) : stderrTail;
kill.stderr += slice;
kill.stderrBytes += slice.length;
}
}
const stdout = kill.stdout;