pi-cockpit 0.20.0 → 0.21.0

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/package.json CHANGED
@@ -1,85 +1,85 @@
1
- {
2
- "name": "pi-cockpit",
3
- "version": "0.20.0",
4
- "description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
5
- "type": "module",
6
- "keywords": [
7
- "pi-package",
8
- "pi",
9
- "pi-extension",
10
- "tui",
11
- "teammate",
12
- "todo"
13
- ],
14
- "license": "MIT",
15
- "files": [
16
- "src/",
17
- "themes/",
18
- "README.md"
19
- ],
20
- "scripts": {
21
- "test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.test.ts tests/quiet-tools.test.ts tests/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-overlay.test.ts tests/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/teammate-message.test.ts tests/input-routing.test.ts tests/input-history-store.test.ts tests/unified-editor.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/zen-render.test.ts tests/zen-browse.test.ts tests/zen-sheet.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/model-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/tui-i18n.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts tests/capturing-overlay.test.ts tests/claude-editor.test.ts tests/fullscreen-controller.test.ts tests/transcript-selection.test.ts tests/terminal-cleanup.test.ts tests/terminal-capability.test.ts tests/usage-core.test.ts tests/usage-extension.test.ts tests/usage-history.test.ts tests/usage-sparkline.test.ts",
22
- "typecheck": "tsc --noEmit",
23
- "prepack": "node ../../scripts/bundle-settings-core.mjs",
24
- "postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
25
- },
26
- "pi": {
27
- "extensions": [
28
- "./src/extension/index.ts"
29
- ],
30
- "themes": [
31
- "./themes"
32
- ]
33
- },
34
- "dependencies": {
35
- "pi-maestro-settings-core": "0.2.1",
36
- "proper-lockfile": "4.1.2"
37
- },
38
- "bundledDependencies": [
39
- "pi-maestro-settings-core"
40
- ],
41
- "peerDependencies": {
42
- "@earendil-works/pi-agent-core": "*",
43
- "@earendil-works/pi-ai": "*",
44
- "@earendil-works/pi-coding-agent": "*",
45
- "@earendil-works/pi-tui": "*",
46
- "pi-maestro-teammate": "^2.1.0",
47
- "typebox": "*"
48
- },
49
- "peerDependenciesMeta": {
50
- "@earendil-works/pi-agent-core": {
51
- "optional": true
52
- },
53
- "@earendil-works/pi-ai": {
54
- "optional": true
55
- },
56
- "@earendil-works/pi-coding-agent": {
57
- "optional": true
58
- },
59
- "@earendil-works/pi-tui": {
60
- "optional": true
61
- },
62
- "pi-maestro-teammate": {
63
- "optional": true
64
- },
65
- "typebox": {
66
- "optional": true
67
- }
68
- },
69
- "devDependencies": {
70
- "@earendil-works/pi-agent-core": "0.84.4",
71
- "@earendil-works/pi-ai": "0.84.4",
72
- "@earendil-works/pi-coding-agent": "0.84.4",
73
- "@earendil-works/pi-tui": "0.84.4",
74
- "@types/node": "^22.0.0",
75
- "pi-maestro-teammate": "2.3.0",
76
- "typebox": "1.3.7",
77
- "typescript": "^5.5.0"
78
- },
79
- "main": "./src/index.ts",
80
- "exports": {
81
- ".": "./src/index.ts",
82
- "./v1/events": "./src/public/v1/events.ts",
83
- "./src/*": "./src/*"
84
- }
85
- }
1
+ {
2
+ "name": "pi-cockpit",
3
+ "version": "0.21.0",
4
+ "description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "pi-extension",
10
+ "tui",
11
+ "teammate",
12
+ "todo"
13
+ ],
14
+ "license": "MIT",
15
+ "files": [
16
+ "src/",
17
+ "themes/",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.test.ts tests/quiet-tools.test.ts tests/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-overlay.test.ts tests/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/teammate-message.test.ts tests/input-routing.test.ts tests/input-history-store.test.ts tests/unified-editor.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/zen-render.test.ts tests/zen-browse.test.ts tests/zen-sheet.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/model-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/tui-i18n.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts tests/capturing-overlay.test.ts tests/claude-editor.test.ts tests/fullscreen-controller.test.ts tests/transcript-selection.test.ts tests/terminal-cleanup.test.ts tests/terminal-capability.test.ts tests/usage-core.test.ts tests/usage-extension.test.ts tests/usage-history.test.ts tests/usage-sparkline.test.ts",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepack": "node ../../scripts/bundle-settings-core.mjs",
24
+ "postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
25
+ },
26
+ "pi": {
27
+ "extensions": [
28
+ "./src/extension/index.ts"
29
+ ],
30
+ "themes": [
31
+ "./themes"
32
+ ]
33
+ },
34
+ "dependencies": {
35
+ "pi-maestro-settings-core": "0.2.1",
36
+ "proper-lockfile": "4.1.2"
37
+ },
38
+ "bundledDependencies": [
39
+ "pi-maestro-settings-core"
40
+ ],
41
+ "peerDependencies": {
42
+ "@earendil-works/pi-agent-core": "*",
43
+ "@earendil-works/pi-ai": "*",
44
+ "@earendil-works/pi-coding-agent": "*",
45
+ "@earendil-works/pi-tui": "*",
46
+ "pi-maestro-teammate": "^2.4.0",
47
+ "typebox": "*"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@earendil-works/pi-agent-core": {
51
+ "optional": true
52
+ },
53
+ "@earendil-works/pi-ai": {
54
+ "optional": true
55
+ },
56
+ "@earendil-works/pi-coding-agent": {
57
+ "optional": true
58
+ },
59
+ "@earendil-works/pi-tui": {
60
+ "optional": true
61
+ },
62
+ "pi-maestro-teammate": {
63
+ "optional": true
64
+ },
65
+ "typebox": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "devDependencies": {
70
+ "@earendil-works/pi-agent-core": "0.84.4",
71
+ "@earendil-works/pi-ai": "0.84.4",
72
+ "@earendil-works/pi-coding-agent": "0.84.4",
73
+ "@earendil-works/pi-tui": "0.84.4",
74
+ "@types/node": "^22.0.0",
75
+ "pi-maestro-teammate": "2.4.0",
76
+ "typebox": "1.3.7",
77
+ "typescript": "^5.5.0"
78
+ },
79
+ "main": "./src/index.ts",
80
+ "exports": {
81
+ ".": "./src/index.ts",
82
+ "./v1/events": "./src/public/v1/events.ts",
83
+ "./src/*": "./src/*"
84
+ }
85
+ }
package/src/config.ts CHANGED
@@ -76,6 +76,7 @@ const isCurrencyMode = (v: unknown): v is CurrencyMode => v === "usd" || v === "
76
76
  agentsMode: isMode(o.agentsMode) ? o.agentsMode : base.agentsMode,
77
77
  todoMode: isMode(o.todoMode) ? o.todoMode : base.todoMode,
78
78
  todoExpanded: typeof o.todoExpanded === "boolean" ? o.todoExpanded : base.todoExpanded,
79
+ todoDurationChart: typeof o.todoDurationChart === "boolean" ? o.todoDurationChart : base.todoDurationChart,
79
80
  stackStyle: isStackStyle(o.stackStyle) ? o.stackStyle : base.stackStyle,
80
81
  hideNativeAgents: typeof o.hideNativeAgents === "boolean" ? o.hideNativeAgents : base.hideNativeAgents,
81
82
  pinEditorBottom: typeof o.pinEditorBottom === "boolean" ? o.pinEditorBottom : base.pinEditorBottom,
@@ -168,6 +169,7 @@ export function mergeConfigDocument(raw: unknown, config: CockpitConfig): Record
168
169
  agentsMode: config.agentsMode,
169
170
  todoMode: config.todoMode,
170
171
  todoExpanded: config.todoExpanded,
172
+ todoDurationChart: config.todoDurationChart,
171
173
  stackStyle: config.stackStyle,
172
174
  hideNativeAgents: config.hideNativeAgents,
173
175
  pinEditorBottom: config.pinEditorBottom,
package/src/index.ts CHANGED
@@ -357,6 +357,27 @@ export default function (pi: ExtensionAPI): void {
357
357
  // thinking label / teammate tree still stream above the visible viewport.
358
358
  let stabilityTui: TUI | undefined;
359
359
  let viewportStabilityPatch: ViewportStabilityPatch | undefined;
360
+ const ensureViewportStability = (tui: TUI): void => {
361
+ // Native fullscreen owns a fixed application viewport and has no main-screen
362
+ // applyLineResets hook. Do not cache that expected miss: the same dynamic TUI
363
+ // reference can later dispatch to the regular renderer.
364
+ if (tui.mode === "fullscreen") return;
365
+ if (stabilityTui === tui && viewportStabilityPatch?.active) return;
366
+ viewportStabilityPatch?.detach();
367
+ const patch = attachViewportStability(tui);
368
+ viewportStabilityPatch = patch;
369
+ stabilityTui = patch.active ? tui : undefined;
370
+ };
371
+ const requestCapturedRender = (): void => {
372
+ try {
373
+ const tui = capturedTui;
374
+ if (!tui) return;
375
+ ensureViewportStability(tui);
376
+ tui.requestRender();
377
+ } catch {
378
+ // tui may be gone between sessions
379
+ }
380
+ };
360
381
  // True while Cockpit owns a capturing overlay (bash jobs, theme picker,
361
382
  // settings panel). Split-pane resize must yield to the overlay's focus and
362
383
  // refuse to start while one is open; otherwise the resize listener — a
@@ -455,13 +476,7 @@ export default function (pi: ExtensionAPI): void {
455
476
  // while the model thinks, then the settled duration. Pi owns the fold.
456
477
  const thinkingTimer = new ThinkingFoldTimer({
457
478
  getTui: () => capturedTui,
458
- requestRender: () => {
459
- try {
460
- capturedTui?.requestRender();
461
- } catch {
462
- // tui may be gone between sessions
463
- }
464
- },
479
+ requestRender: requestCapturedRender,
465
480
  getBaseLabel: () => (config.quietMode ? quietThinkingLabel() : undefined),
466
481
  getGlyphs: () => resolveGlyphs(config.icons.mode),
467
482
  isThinkingHidden: () => (lastCtx ? readHideThinkingBlock(lastCtx.cwd) : false),
@@ -601,7 +616,7 @@ export default function (pi: ExtensionAPI): void {
601
616
  if (refreshAmbientSurface) refreshAmbient(now);
602
617
  if (agentPriorityChanged) publishUiOwnership();
603
618
  try {
604
- capturedTui?.requestRender();
619
+ requestCapturedRender();
605
620
  activeAgentOverlayRender?.();
606
621
  sidebarController?.requestRender();
607
622
  } catch {
@@ -691,6 +706,7 @@ export default function (pi: ExtensionAPI): void {
691
706
  sidebar: ownsDock,
692
707
  goal: ownsDock,
693
708
  todoExpanded: effectiveTodoExpanded(),
709
+ todoDurationChart: config.todoDurationChart,
694
710
  quiet: config.enabled && config.quietMode,
695
711
  quietSymbols: config.quietSymbols,
696
712
  static: config.staticMode,
@@ -747,13 +763,6 @@ export default function (pi: ExtensionAPI): void {
747
763
  };
748
764
 
749
765
 
750
- const ensureViewportStability = (tui: TUI): void => {
751
- if (stabilityTui === tui) return;
752
- viewportStabilityPatch?.detach();
753
- viewportStabilityPatch = attachViewportStability(tui);
754
- stabilityTui = tui;
755
- };
756
-
757
766
  const sessionListOverlayActive = (): boolean =>
758
767
  capturingOverlayActive || ambientKeysShouldYield(capturedTui);
759
768
 
@@ -1916,6 +1925,7 @@ export default function (pi: ExtensionAPI): void {
1916
1925
  sidebar: false,
1917
1926
  goal: false,
1918
1927
  todoExpanded: config.todoExpanded,
1928
+ todoDurationChart: config.todoDurationChart,
1919
1929
  quiet: false,
1920
1930
  quietSymbols: config.quietSymbols,
1921
1931
  static: config.staticMode,
@@ -195,6 +195,8 @@ export interface CockpitUiOwnershipV1 {
195
195
  /** True only while the dock renders Maestro Goal state. */
196
196
  goal: boolean;
197
197
  todoExpanded: boolean;
198
+ /** Whether Flow should render the duration chart after a terminal todo advance. */
199
+ todoDurationChart?: boolean;
198
200
  quiet: boolean;
199
201
  quietSymbols: "check" | "dot";
200
202
  /** Static mode: per-second churn (elapsed ticks, spinners) is suppressed. */
@@ -68,6 +68,7 @@ const CONFIG_KEYS = [
68
68
  "agentsMode",
69
69
  "todoMode",
70
70
  "todoExpanded",
71
+ "todoDurationChart",
71
72
  "stackStyle",
72
73
  "hideNativeAgents",
73
74
  "toolPalette",
@@ -164,6 +165,8 @@ const CATALOGS = {
164
165
  "cockpit.agentsMode": "Agent display",
165
166
  "cockpit.todoMode": "Todo display",
166
167
  "cockpit.todoExpanded": "Todo expanded",
168
+ "cockpit.todoDurationChart": "Advance duration chart",
169
+ "cockpit.todoDurationChart.description": "Show a task-duration chart after advance completes the entire Todo list.",
167
170
  "cockpit.stackStyle": "Stack projection",
168
171
  "cockpit.hideNativeAgents": "Hide native agent widget",
169
172
  "cockpit.icons.mode": "Icon mode",
@@ -249,6 +252,8 @@ const CATALOGS = {
249
252
  "cockpit.agentsMode": "Agent 显示",
250
253
  "cockpit.todoMode": "Todo 显示",
251
254
  "cockpit.todoExpanded": "展开 Todo",
255
+ "cockpit.todoDurationChart": "Advance 耗时图",
256
+ "cockpit.todoDurationChart.description": "当 advance 完成整个 Todo 列表后显示任务耗时统计图。",
252
257
  "cockpit.stackStyle": "栈投影",
253
258
  "cockpit.hideNativeAgents": "隐藏原生 Agent 面板",
254
259
  "cockpit.icons.mode": "图标模式",
@@ -367,8 +372,9 @@ const DEFINITIONS: readonly SettingDefinition[] = [
367
372
  enumDefinition("agentsMode", "cockpit.group.panels", 0, "cockpit.agentsMode", ["list", "compact"], "live"),
368
373
  enumDefinition("todoMode", "cockpit.group.panels", 1, "cockpit.todoMode", ["list", "compact"], "live"),
369
374
  booleanDefinition("todoExpanded", "cockpit.group.panels", 2, "cockpit.todoExpanded", "live"),
375
+ booleanDefinition("todoDurationChart", "cockpit.group.panels", 3, "cockpit.todoDurationChart", "live", "cockpit.todoDurationChart.description"),
370
376
  enumDefinition("stackStyle", "cockpit.group.layout", 4, "cockpit.stackStyle", ["classic", "zen"], "live"),
371
- booleanDefinition("hideNativeAgents", "cockpit.group.panels", 3, "cockpit.hideNativeAgents", "live"),
377
+ booleanDefinition("hideNativeAgents", "cockpit.group.panels", 4, "cockpit.hideNativeAgents", "live"),
372
378
  enumDefinition("sidebar.mode", "cockpit.group.sidebar", 0, "cockpit.sidebar.mode", ["auto", "on", "off"], "live"),
373
379
  {
374
380
  key: "sidebar.width",
@@ -789,7 +795,7 @@ function setUsageValue(usage: CockpitConfig["usage"], field: string, value: Json
789
795
  }
790
796
 
791
797
  function validValue(key: CockpitSettingKey, value: JsonValue): boolean {
792
- if (["enabled", "staticMode", "pinEditorBottom", "doubleEscapeClearInput", "fullscreenInput", "copyOnSelect", "historyEnabled", "quietMode", "todoExpanded", "hideNativeAgents"].includes(key)) return typeof value === "boolean";
798
+ if (["enabled", "staticMode", "pinEditorBottom", "doubleEscapeClearInput", "fullscreenInput", "copyOnSelect", "historyEnabled", "quietMode", "todoExpanded", "todoDurationChart", "hideNativeAgents"].includes(key)) return typeof value === "boolean";
793
799
  if (key === "sidebar.width") return typeof value === "number" && Number.isSafeInteger(value) && value >= 32 && value <= 56;
794
800
  if (key === "title.maxLength") return typeof value === "number" && Number.isSafeInteger(value) && value >= 20 && value <= 200;
795
801
  if (key === "title.generationModel") return typeof value === "string";
package/src/types.ts CHANGED
@@ -260,6 +260,8 @@ export interface CockpitConfig {
260
260
  agentsMode: ViewMode;
261
261
  todoMode: ViewMode;
262
262
  todoExpanded: boolean;
263
+ /** Show the duration chart after a terminal todo advance. */
264
+ todoDurationChart: boolean;
263
265
  /** Above-editor stack projection: classic Todo/Agents widgets or the unified Zen stack. */
264
266
  stackStyle: StackStyle;
265
267
  hideNativeAgents: boolean;
@@ -318,6 +320,7 @@ export const DEFAULT_CONFIG: CockpitConfig = {
318
320
  agentsMode: "list",
319
321
  todoMode: "list",
320
322
  todoExpanded: false,
323
+ todoDurationChart: true,
321
324
  stackStyle: "classic",
322
325
  hideNativeAgents: true,
323
326
  pinEditorBottom: false,
@@ -10,7 +10,8 @@ interface ViewportTuiInternals {
10
10
  previousLines?: string[];
11
11
  previousViewportTop?: number;
12
12
  previousHeight?: number;
13
- terminal?: { rows: number };
13
+ previousWidth?: number;
14
+ terminal?: { columns: number; rows: number };
14
15
  }
15
16
 
16
17
  interface ViewportStabilityMarker {
@@ -44,18 +45,6 @@ function markerOf(fn: ApplyLineResets): ViewportStabilityMarker | undefined {
44
45
  return (fn as ApplyLineResets & Record<symbol, ViewportStabilityMarker | undefined>)[VIEWPORT_STABILITY_MARKER];
45
46
  }
46
47
 
47
- const ANSI_ESCAPE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
48
- const TRAILING_DURATION = /(\s(?:·\s*)?)(?:\d+(?:\.\d+)?s|\d+m\d{1,2}s)$/;
49
-
50
- function differsOnlyByTrailingDuration(previous: string, next: string): boolean {
51
- const previousPlain = previous.replace(ANSI_ESCAPE, "");
52
- const nextPlain = next.replace(ANSI_ESCAPE, "");
53
- if (previousPlain === nextPlain) return false;
54
- if (!TRAILING_DURATION.test(previousPlain) || !TRAILING_DURATION.test(nextPlain)) return false;
55
- return previousPlain.replace(TRAILING_DURATION, "$1<duration>")
56
- === nextPlain.replace(TRAILING_DURATION, "$1<duration>");
57
- }
58
-
59
48
  function isKittyImageLine(line: string | undefined): boolean {
60
49
  return typeof line === "string" && line.includes("\x1b_G");
61
50
  }
@@ -110,13 +99,17 @@ function resolveApplyLineResetsSlot(internals: ViewportTuiInternals): ApplyLineR
110
99
  }
111
100
 
112
101
  /**
113
- * Prevents timer-only churn above the visible viewport from forcing a full
114
- * redraw. The next frame keeps the terminal's actual hidden text, so the diff
115
- * baseline remains truthful; status, content, layout and image changes retain
116
- * pi-tui's native redraw path.
102
+ * Treats the hidden viewport prefix as immutable scrollback while frame size is
103
+ * stable. The next frame keeps the terminal's actual hidden text, so visible
104
+ * changes can use pi-tui's differential path instead of clearing and replaying
105
+ * the screen. Structural, resize and image changes retain the native path.
117
106
  */
118
107
  export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
119
108
  try {
109
+ // Native fullscreen has an application-owned fixed viewport. Its inherited
110
+ // applyLineResets method is not part of that renderer's diff path, so patching
111
+ // it would report a false-positive attachment on the shared TUI base class.
112
+ if (tui.mode === "fullscreen") return { active: false, detach() {} };
120
113
  const internals = tui as unknown as ViewportTuiInternals;
121
114
  const slot = resolveApplyLineResetsSlot(internals);
122
115
  if (!slot) return { active: false, detach() {} };
@@ -137,6 +130,7 @@ export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
137
130
  && Number.isFinite(viewportTop)
138
131
  && viewportTop > 0
139
132
  && this.previousHeight === this.terminal?.rows
133
+ && this.previousWidth === this.terminal?.columns
140
134
  ) {
141
135
  const hiddenEnd = Math.min(previousLines.length, Math.trunc(viewportTop));
142
136
  const hiddenHasKittyImage = previousLines.slice(0, hiddenEnd).some(isKittyImageLine)
@@ -144,10 +138,7 @@ export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
144
138
  if (!hiddenHasKittyImage) {
145
139
  for (let index = 0; index < hiddenEnd; index += 1) {
146
140
  const previous = previousLines[index];
147
- const next = nextLines[index];
148
- if (previous !== undefined && next !== undefined && differsOnlyByTrailingDuration(previous, next)) {
149
- nextLines[index] = previous;
150
- }
141
+ if (previous !== undefined) nextLines[index] = previous;
151
142
  }
152
143
  }
153
144
  }