Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edf013164b | ||
|
|
504b576e1c | ||
|
|
890abd1c4c |
@@ -957,19 +957,12 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
||||
}
|
||||
return filtered
|
||||
.sort((a, b) => (b.lastConnectedAt || 0) - (a.lastConnectedAt || 0))
|
||||
.slice(0, 20);
|
||||
.slice(0, 6);
|
||||
}, [hosts, selectedGroupPath, search, selectedTags]);
|
||||
|
||||
// IDs of hosts already shown in Pinned/Recent sections at root level,
|
||||
// so the main host list can exclude them to avoid duplicates.
|
||||
const pinnedRecentIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const h of pinnedHosts) ids.add(h.id);
|
||||
if (showRecentHosts) {
|
||||
for (const h of recentHosts) ids.add(h.id);
|
||||
}
|
||||
return ids;
|
||||
}, [pinnedHosts, recentHosts, showRecentHosts]);
|
||||
// No longer deduplicate pinned/recent hosts from the main list,
|
||||
// so hosts always appear in their groups regardless of pinned/recent status.
|
||||
const pinnedRecentIds = useMemo(() => new Set<string>(), []);
|
||||
|
||||
// For tree view: apply search, tag filter, and sorting, but not group filtering
|
||||
const treeViewHosts = useMemo(() => {
|
||||
|
||||
85
components/terminal/clearTerminalViewport.ts
Normal file
85
components/terminal/clearTerminalViewport.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { Terminal as XTerm } from "@xterm/xterm";
|
||||
|
||||
type CsiParam = number | number[];
|
||||
type InternalTerminal = XTerm & {
|
||||
_core?: {
|
||||
scroll?: (eraseAttr: unknown, isWrapped?: boolean) => void;
|
||||
_inputHandler?: {
|
||||
_eraseAttrData?: () => unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const getVisibleContentRowCount = (term: XTerm): number => {
|
||||
const buffer = term.buffer.active;
|
||||
if (buffer.type !== "normal") {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const baseY = buffer.baseY;
|
||||
for (let row = term.rows - 1; row >= 0; row--) {
|
||||
const line = buffer.getLine(baseY + row);
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
if (line.translateToString(true).length > 0) {
|
||||
return row + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const preserveTerminalViewportInScrollback = (term: XTerm): void => {
|
||||
const rowsToPreserve = getVisibleContentRowCount(term);
|
||||
if (rowsToPreserve <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const internal = term as InternalTerminal;
|
||||
const scroll = internal._core?.scroll;
|
||||
const eraseAttr = internal._core?._inputHandler?._eraseAttrData?.();
|
||||
|
||||
if (typeof scroll !== "function" || eraseAttr === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let row = 0; row < rowsToPreserve; row++) {
|
||||
scroll.call(internal._core, eraseAttr, false);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearTerminalViewport = (term: XTerm): void => {
|
||||
const buffer = term.buffer.active;
|
||||
if (buffer.type !== "normal") return;
|
||||
|
||||
const cursorY = buffer.cursorY;
|
||||
const cursorX = buffer.cursorX;
|
||||
|
||||
if (cursorY === 0 && buffer.baseY === 0) return;
|
||||
|
||||
const internal = term as InternalTerminal;
|
||||
const scroll = internal._core?.scroll;
|
||||
const eraseAttr = internal._core?._inputHandler?._eraseAttrData?.();
|
||||
|
||||
if (typeof scroll !== "function" || eraseAttr === undefined) return;
|
||||
|
||||
// Push lines above cursor into scrollback so they are preserved.
|
||||
// After cursorY scrolls the prompt line shifts to active-screen row 0.
|
||||
for (let i = 0; i < cursorY; i++) {
|
||||
scroll.call(internal._core, eraseAttr, false);
|
||||
}
|
||||
|
||||
// Clear everything below the prompt and reposition the cursor on it.
|
||||
// CSI coordinates are 1-indexed.
|
||||
const col = cursorX + 1;
|
||||
term.write(`\x1b[2;1H\x1b[J\x1b[1;${col}H`, () => {
|
||||
term.scrollToBottom();
|
||||
});
|
||||
};
|
||||
|
||||
export const isEraseScrollbackSequence = (params: CsiParam[]): boolean =>
|
||||
params.length > 0 && params[0] === 3;
|
||||
|
||||
export const isEraseViewportSequence = (params: CsiParam[]): boolean =>
|
||||
params.length > 0 && params[0] === 2;
|
||||
@@ -3,6 +3,7 @@ import { useCallback } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import { logger } from "../../../lib/logger";
|
||||
import { normalizeLineEndings, wrapBracketedPaste } from "../../../lib/utils";
|
||||
import { clearTerminalViewport } from "../clearTerminalViewport";
|
||||
|
||||
type TerminalBackendWriteApi = {
|
||||
writeToSession: (sessionId: string, data: string) => void;
|
||||
@@ -65,7 +66,7 @@ export const useTerminalContextActions = ({
|
||||
const onClear = useCallback(() => {
|
||||
const term = termRef.current;
|
||||
if (!term) return;
|
||||
term.clear();
|
||||
clearTerminalViewport(term);
|
||||
}, [termRef]);
|
||||
|
||||
const onSelectWord = useCallback(() => {
|
||||
|
||||
@@ -31,6 +31,12 @@ import {
|
||||
import { logger } from "../../../lib/logger";
|
||||
import { isMacPlatform, normalizeLineEndings, wrapBracketedPaste } from "../../../lib/utils";
|
||||
import { netcattyBridge } from "../../../infrastructure/services/netcattyBridge";
|
||||
import {
|
||||
clearTerminalViewport,
|
||||
isEraseViewportSequence,
|
||||
isEraseScrollbackSequence,
|
||||
preserveTerminalViewportInScrollback,
|
||||
} from "../clearTerminalViewport";
|
||||
import type {
|
||||
Host,
|
||||
KeyBinding,
|
||||
@@ -498,7 +504,7 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
break;
|
||||
}
|
||||
case "clearBuffer": {
|
||||
term.clear();
|
||||
clearTerminalViewport(term);
|
||||
break;
|
||||
}
|
||||
case "searchTerminal": {
|
||||
@@ -641,6 +647,17 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
// OSC 7 format: \x1b]7;file://hostname/path\x07 or \x1b]7;file://hostname/path\x1b\\
|
||||
let currentCwd: string | undefined = undefined;
|
||||
|
||||
const eraseScrollbackDisposable = term.parser.registerCsiHandler({ final: "J" }, (params) => {
|
||||
if (isEraseViewportSequence(params)) {
|
||||
preserveTerminalViewportInScrollback(term);
|
||||
return false;
|
||||
}
|
||||
if (!isEraseScrollbackSequence(params)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Register OSC 7 handler using xterm.js parser
|
||||
// OSC 7 is the standard way for shells to report the current working directory
|
||||
const osc7Disposable = term.parser.registerOscHandler(7, (data) => {
|
||||
@@ -763,6 +780,7 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
|
||||
dispose: () => {
|
||||
cleanupMiddleClick?.();
|
||||
keywordHighlighter.dispose();
|
||||
eraseScrollbackDisposable.dispose();
|
||||
osc7Disposable.dispose();
|
||||
osc52Disposable.dispose();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user