Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/lib/components/TerminalPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@
let paintScheduler: PaintScheduler | undefined;
let paintTick = $state(0);
let isAltBuffer = $state(false);
// Per-tab: when true the fold store stops auto-folding new output (see
// shouldAutoFold below). Existing folds stay untouched; flip back to fold
// future output again. Survives reconnect and tab switches; resets only
// when the tab closes (this keyed component instance is destroyed).
let autoFoldDisabled = $state(false);

function schedulePaintTick() {
paintScheduler?.schedule();
Expand Down Expand Up @@ -536,6 +541,12 @@
disabled: n === 0 || !aiConfigured,
action: () => sendBlocksToAi(targets),
},
{
label: t(autoFoldDisabled
? "terminal.block.menu.enable_auto_fold"
: "terminal.block.menu.disable_auto_fold"),
action: () => { autoFoldDisabled = !autoFoldDisabled; },
},
],
};
}
Expand Down Expand Up @@ -1645,6 +1656,9 @@
maxVisibleLines: commandBlockMaxLines,
maxCachedLines: commandBlockFoldCacheLines(commandBlockMaxLines),
shouldAutoFold: () => app.commandBlockBar(),
// Per-tab "disable auto-fold" suspends folding of NEW output only;
// enforceAutoFold (resize/reflow) still restores existing folds.
shouldFoldNewOutput: () => !autoFoldDisabled,
});
paintScheduler = createPaintScheduler({
shouldPaint: () => app.commandBlockBar() && !isAltBuffer,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ const en = {
"terminal.block.menu.uncolor": "Remove color",
"terminal.block.menu.send_ai": "Send to AI",
"terminal.block.menu.send_ai_n": "Send {n} blocks to AI",
"terminal.block.menu.disable_auto_fold": "Disable auto-fold (this tab)",
"terminal.block.menu.enable_auto_fold": "Enable auto-fold (this tab)",
"ai.mobile.hint": "Tip: rotate to landscape for the AI panel. Tools download_file / analyze_locally are unavailable on this build.",
"ai.no_session": "Connect to a terminal before opening the AI panel.",
"settings.section.cli": "CLI Tool",
Expand Down
2 changes: 2 additions & 0 deletions src/lib/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ const zh: Messages = {
"terminal.block.menu.uncolor": "取消染色",
"terminal.block.menu.send_ai": "发送到 AI",
"terminal.block.menu.send_ai_n": "发送 {n} 块到 AI",
"terminal.block.menu.disable_auto_fold": "临时关闭自动折叠(当前标签)",
"terminal.block.menu.enable_auto_fold": "开启自动折叠(当前标签)",
"ai.mobile.hint": "建议横屏使用 AI 面板。本机型不支持 download_file / analyze_locally 两个工具。",
"ai.no_session": "请先连接到一个终端再打开 AI 面板。",
"settings.section.cli": "CLI 工具",
Expand Down
50 changes: 50 additions & 0 deletions src/lib/terminal/folds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,56 @@ describe("FoldStore — automatic command-block limit", () => {
expect(f.lineContents().slice(0, 4)).toEqual(["L0", "L8", "L9", "L10"]);
});

it("does not fold new output while new-output folding is disabled (bar still on)", () => {
const f = fakeTerm({ rows: 12, initialLines: 12, cursorY: 10 });
const start = f.makeMarker(0);
const store = createFoldStore(
f.term,
fakeTracker([makeBlock(1, start, null)]),
{ maxVisibleLines: 3, maxCachedLines: 8, shouldFoldNewOutput: () => false },
);

f.fireWriteParsed();

// shouldFoldNewOutput=false suspends incremental folding; the block stays
// fully visible even though it exceeds maxVisibleLines.
expect(store.getFold(1)).toBeUndefined();
expect(f.lineContents().slice(0, 4)).toEqual(["L0", "L1", "L2", "L3"]);
});

it("keeps an existing fold through resize/reflow while new-output folding is disabled", () => {
const f = fakeTerm({ rows: 12, initialLines: 12, cursorY: 10 });
const start = f.makeMarker(0);
let newOutputEnabled = true;
const store = createFoldStore(
f.term,
fakeTracker([makeBlock(1, start, null)]),
{
maxVisibleLines: 3,
maxCachedLines: 8,
shouldFoldNewOutput: () => newOutputEnabled,
},
);

// 1. Auto-fold on: a block over the limit gets prefix-folded.
f.fireWriteParsed();
expect(store.getFold(1)).toMatchObject({ kind: "prefix", count: 7 });

// 2. User disables auto-fold for this tab (future output only).
newOutputEnabled = false;

// 3. fitTerminal resize sequence: unfoldAll (so saved lines reflow at the
// new width) then enforceAutoFold. Disabling new-output folding must NOT
// block the reflow re-application, or the existing fold is lost.
store.unfoldAll();
expect(store.getFold(1)).toBeUndefined();
store.enforceAutoFold();

// 4. The existing fold is restored.
expect(store.getFold(1)).toMatchObject({ kind: "prefix", count: 7 });
expect(f.lineContents().slice(0, 4)).toEqual(["L0", "L8", "L9", "L10"]);
});

it("folds during a large parse batch before xterm can trim the block marker", () => {
const f = fakeTerm({ rows: 40, initialLines: 40, cursorY: 3 });
const start = f.makeMarker(0);
Expand Down
16 changes: 14 additions & 2 deletions src/lib/terminal/folds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export interface FoldStoreOptions {
maxCachedLines?: number;
/** Visual block controls must be available before automatic folding hides rows. */
shouldAutoFold?: () => boolean;
/** Suspend folding of *new* output only. enforceAutoFold (the resize/reflow
* re-application) is unaffected, so existing folds survive a resize while a
* per-tab "disable auto-fold" toggle suspends folding of future output. */
shouldFoldNewOutput?: () => boolean;
}

/** xterm 默认 attr(fg=0,bg=0),与 DEFAULT_ATTR_DATA 等价。getBlankLine 必填。 */
Expand Down Expand Up @@ -316,6 +320,14 @@ export function createFoldStore(
&& term.buffer.active.type === "normal";
}

/** Incremental folding of newly arriving output. Stricter than canAutoFold:
* also honors shouldFoldNewOutput so a per-tab "disable auto-fold" toggle
* suspends folding of future output WITHOUT disabling enforceAutoFold,
* which must still restore folds after a resize/reflow. */
function canFoldNewOutput(): boolean {
return canAutoFold() && options.shouldFoldNewOutput?.() !== false;
}

function foldBlockOverflow(block: CommandBlock, minimumExcess: number): void {
if (maxVisibleLines === null || block.start.isDisposed) return;
if (folds.get(block.id)?.kind === "full") return;
Expand All @@ -332,13 +344,13 @@ export function createFoldStore(
}

function foldActiveOverflow(minimumExcess: number): void {
if (!canAutoFold()) return;
if (!canFoldNewOutput()) return;
const block = tracker.blocks[tracker.blocks.length - 1];
if (block?.end === null) foldBlockOverflow(block, minimumExcess);
}

function foldRecentOverflow(): void {
if (!canAutoFold()) return;
if (!canFoldNewOutput()) return;
const blocks = tracker.blocks;
// Prompt detection runs before this listener and may close the previous
// block while opening a new prompt block in the same parse batch.
Expand Down