@code-yeongyu/senpi 2026.6.30 → 2026.7.3
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/CHANGELOG.md +70 -0
- package/dist/core/agent-session.d.ts +6 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +42 -25
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +13 -5
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +5 -8
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/dynamic-prompt/build.d.ts +13 -0
- package/dist/core/dynamic-prompt/build.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/build.js +24 -21
- package/dist/core/dynamic-prompt/build.js.map +1 -1
- package/dist/core/dynamic-prompt/exploration.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/exploration.js +1 -3
- package/dist/core/dynamic-prompt/exploration.js.map +1 -1
- package/dist/core/dynamic-prompt/index.d.ts +1 -1
- package/dist/core/dynamic-prompt/index.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/index.js.map +1 -1
- package/dist/core/dynamic-prompt/intent-gate.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/intent-gate.js +20 -35
- package/dist/core/dynamic-prompt/intent-gate.js.map +1 -1
- package/dist/core/dynamic-prompt/parallel-tools.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/parallel-tools.js +2 -4
- package/dist/core/dynamic-prompt/parallel-tools.js.map +1 -1
- package/dist/core/dynamic-prompt/style.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/style.js +7 -26
- package/dist/core/dynamic-prompt/style.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.d.ts +3 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.d.ts.map +1 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +16 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js.map +1 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js +3 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js +3 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js +6 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js +5 -3
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +68 -8
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/presets.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/presets.js +9 -0
- package/dist/core/extensions/builtin/prompt-preset/presets.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
- package/dist/core/extensions/builtin/prompt-preset/settings.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +2 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +33 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +16 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +28 -1
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/model-resolver.d.ts +10 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +21 -27
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/session-manager.d.ts +20 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +87 -68
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +20 -5
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/extension-system-migration.d.ts +2 -0
- package/dist/extension-system-migration.d.ts.map +1 -0
- package/dist/extension-system-migration.js +51 -0
- package/dist/extension-system-migration.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy-senpi-dir-migration.d.ts +2 -0
- package/dist/legacy-senpi-dir-migration.d.ts.map +1 -0
- package/dist/legacy-senpi-dir-migration.js +76 -0
- package/dist/legacy-senpi-dir-migration.js.map +1 -0
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +3 -95
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +15 -7
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
- package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
- package/dist/modes/interactive/components/custom-entry.js +49 -0
- package/dist/modes/interactive/components/custom-entry.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +13 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/status-indicator.d.ts +27 -0
- package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -0
- package/dist/modes/interactive/components/status-indicator.js +64 -0
- package/dist/modes/interactive/components/status-indicator.js.map +1 -0
- package/dist/modes/interactive/components/user-message.d.ts +6 -2
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +15 -4
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +14 -15
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +177 -137
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +15 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +14 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +16 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +26 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/extensions.md +55 -8
- package/docs/rpc.md +58 -0
- package/docs/sdk.md +28 -0
- package/docs/session-format.md +18 -8
- package/docs/settings.md +2 -1
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/entry-renderer.ts +41 -0
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/question.ts +1 -0
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts +3 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +9 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +4 -6
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js +20 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js +2 -14
- package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +16 -5
- package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js +2 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js +2 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +56 -39
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +8 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +2 -14
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js +2 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +15 -45
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +15 -45
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +456 -37
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts +123 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +124 -4
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.d.ts +20 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js +18 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.d.ts +21 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.js +18 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.d.ts +21 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js +18 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.d.ts +10 -5
- package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js +4 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts +82 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js +57 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.d.ts +0 -28
- package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js +0 -19
- package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +5 -5
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.d.ts +89 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js +75 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +46 -4
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +91 -55
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts +44 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +41 -4
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts +25 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js +109 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.d.ts +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +3 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts +4 -0
- package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-tui/dist/mux.js +10 -0
- package/node_modules/@earendil-works/pi-tui/dist/mux.js.map +1 -0
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts +42 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.js +422 -101
- package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts +10 -0
- package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/utils.js +53 -13
- package/node_modules/@earendil-works/pi-tui/dist/utils.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +2 -1
- package/npm-shrinkwrap.json +9 -9
- package/package.json +4 -4
|
@@ -6,6 +6,7 @@ import * as os from "node:os";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
8
8
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
|
9
|
+
import { isMultiplexerSession, useLegacyMuxRender, viewportRenderEnabled } from "./mux.js";
|
|
9
10
|
import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
|
|
10
11
|
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
|
11
12
|
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
|
@@ -55,6 +56,103 @@ export function isFocusable(component) {
|
|
|
55
56
|
*/
|
|
56
57
|
export const CURSOR_MARKER = "\x1b_pi:c\x07";
|
|
57
58
|
export { visibleWidth };
|
|
59
|
+
const renderErrorLoggedClasses = new Set();
|
|
60
|
+
let renderErrorLogWrites = 0;
|
|
61
|
+
const DIAGNOSTIC_LOG_MODE = 0o600;
|
|
62
|
+
const VIEWPORT_RENDER_OVERSCAN = 16;
|
|
63
|
+
// Keep scroll-region wins cheap when a few visible rows mutate during append streaming.
|
|
64
|
+
const MAX_SCROLL_DIFF_ROWS = 4;
|
|
65
|
+
const viewportRenderStats = {
|
|
66
|
+
lastNormalizedLines: 0,
|
|
67
|
+
lastKittyImageScannedLines: 0,
|
|
68
|
+
totalNormalizedLines: 0,
|
|
69
|
+
totalKittyImageScannedLines: 0,
|
|
70
|
+
boundedFrames: 0,
|
|
71
|
+
escapedFrames: 0,
|
|
72
|
+
fullFrames: 0,
|
|
73
|
+
};
|
|
74
|
+
export function __renderErrorLogStats() {
|
|
75
|
+
if (process.env.PI_TUI_TEST_SEAMS !== "1") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return { writes: renderErrorLogWrites };
|
|
79
|
+
}
|
|
80
|
+
export function __viewportRenderStats() {
|
|
81
|
+
if (process.env.PI_TUI_TEST_SEAMS !== "1") {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return { ...viewportRenderStats };
|
|
85
|
+
}
|
|
86
|
+
function recordViewportRenderStats(normalizedLines, mode) {
|
|
87
|
+
viewportRenderStats.lastNormalizedLines = normalizedLines;
|
|
88
|
+
viewportRenderStats.totalNormalizedLines += normalizedLines;
|
|
89
|
+
if (mode === "bounded") {
|
|
90
|
+
viewportRenderStats.boundedFrames += 1;
|
|
91
|
+
}
|
|
92
|
+
else if (mode === "escaped") {
|
|
93
|
+
viewportRenderStats.escapedFrames += 1;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
viewportRenderStats.fullFrames += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function recordKittyImageScanStats(scannedLines) {
|
|
100
|
+
viewportRenderStats.lastKittyImageScannedLines = scannedLines;
|
|
101
|
+
viewportRenderStats.totalKittyImageScannedLines += scannedLines;
|
|
102
|
+
}
|
|
103
|
+
function componentRenderErrorName(component) {
|
|
104
|
+
return component.constructor.name || "AnonymousComponent";
|
|
105
|
+
}
|
|
106
|
+
function logRenderErrorOnce(component, error) {
|
|
107
|
+
const componentName = componentRenderErrorName(component);
|
|
108
|
+
if (renderErrorLoggedClasses.has(componentName)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
renderErrorLoggedClasses.add(componentName);
|
|
112
|
+
renderErrorLogWrites += 1;
|
|
113
|
+
const errorText = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
114
|
+
const logPath = path.join(os.homedir(), ".senpi", "agent", "senpi-debug.log");
|
|
115
|
+
const msg = `[${new Date().toISOString()}] render error: ${componentName}: ${errorText}\n`;
|
|
116
|
+
appendRenderErrorLogBestEffort(logPath, msg);
|
|
117
|
+
}
|
|
118
|
+
function appendRenderErrorLogBestEffort(logPath, msg) {
|
|
119
|
+
try {
|
|
120
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
121
|
+
fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
122
|
+
chmodDiagnosticLogBestEffort(logPath);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error instanceof Error) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function writeRenderDiagnosticBestEffort(logPath, data) {
|
|
132
|
+
try {
|
|
133
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
134
|
+
fs.writeFileSync(logPath, data, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
135
|
+
chmodDiagnosticLogBestEffort(logPath);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof Error) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function chmodDiagnosticLogBestEffort(logPath) {
|
|
146
|
+
try {
|
|
147
|
+
fs.chmodSync(logPath, DIAGNOSTIC_LOG_MODE);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof Error) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
58
156
|
/** Parse a SizeValue into absolute value given a reference size */
|
|
59
157
|
function parseSizeValue(value, referenceSize) {
|
|
60
158
|
if (value === undefined)
|
|
@@ -120,7 +218,16 @@ export class Container {
|
|
|
120
218
|
render(width) {
|
|
121
219
|
const lines = [];
|
|
122
220
|
for (const child of this.children) {
|
|
123
|
-
|
|
221
|
+
let childLines;
|
|
222
|
+
try {
|
|
223
|
+
childLines = child.render(width);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
logRenderErrorOnce(child, error);
|
|
227
|
+
const componentName = componentRenderErrorName(child);
|
|
228
|
+
// Focus ownership stays unchanged; render containment must not steal or clear focus implicitly.
|
|
229
|
+
childLines = [`[render error: ${componentName}]`];
|
|
230
|
+
}
|
|
124
231
|
for (const line of childLines) {
|
|
125
232
|
lines.push(line);
|
|
126
233
|
}
|
|
@@ -133,9 +240,12 @@ export class Container {
|
|
|
133
240
|
*/
|
|
134
241
|
export class TUI extends Container {
|
|
135
242
|
static { this.MIN_RENDER_INTERVAL_MS = 16; }
|
|
136
|
-
|
|
243
|
+
#lastCursorVisibility;
|
|
244
|
+
#muxDetector;
|
|
245
|
+
constructor(terminal, options) {
|
|
137
246
|
super();
|
|
138
247
|
this.previousLines = [];
|
|
248
|
+
this.previousRawLines = [];
|
|
139
249
|
this.previousKittyImageIds = new Set();
|
|
140
250
|
this.previousWidth = 0;
|
|
141
251
|
this.previousHeight = 0;
|
|
@@ -151,6 +261,8 @@ export class TUI extends Container {
|
|
|
151
261
|
this.maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
|
|
152
262
|
this.previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
153
263
|
this.fullRedrawCount = 0;
|
|
264
|
+
this.muxViewportRepaintCount = 0;
|
|
265
|
+
this.overWideCrashDumpWritten = false;
|
|
154
266
|
this.stopped = false;
|
|
155
267
|
this.pendingOsc11BackgroundReplies = 0;
|
|
156
268
|
this.pendingOsc11BackgroundQueries = [];
|
|
@@ -161,13 +273,19 @@ export class TUI extends Container {
|
|
|
161
273
|
this.overlayStack = [];
|
|
162
274
|
this.overlayFocusRestore = { status: "inactive" };
|
|
163
275
|
this.terminal = terminal;
|
|
164
|
-
|
|
165
|
-
|
|
276
|
+
// Preserve existing positional boolean callers while allowing explicit render-policy overrides.
|
|
277
|
+
const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
|
|
278
|
+
this.#muxDetector = normalizedOptions.muxDetector ?? isMultiplexerSession;
|
|
279
|
+
if (normalizedOptions.showHardwareCursor !== undefined) {
|
|
280
|
+
this.showHardwareCursor = normalizedOptions.showHardwareCursor;
|
|
166
281
|
}
|
|
167
282
|
}
|
|
168
283
|
get fullRedraws() {
|
|
169
284
|
return this.fullRedrawCount;
|
|
170
285
|
}
|
|
286
|
+
get muxViewportRepaints() {
|
|
287
|
+
return this.muxViewportRepaintCount;
|
|
288
|
+
}
|
|
171
289
|
getShowHardwareCursor() {
|
|
172
290
|
return this.showHardwareCursor;
|
|
173
291
|
}
|
|
@@ -176,7 +294,7 @@ export class TUI extends Container {
|
|
|
176
294
|
return;
|
|
177
295
|
this.showHardwareCursor = enabled;
|
|
178
296
|
if (!enabled) {
|
|
179
|
-
this
|
|
297
|
+
this.#setCursorVisibility(false);
|
|
180
298
|
}
|
|
181
299
|
this.requestRender();
|
|
182
300
|
}
|
|
@@ -318,7 +436,7 @@ export class TUI extends Container {
|
|
|
318
436
|
if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
|
|
319
437
|
this.setFocus(component);
|
|
320
438
|
}
|
|
321
|
-
this
|
|
439
|
+
this.#setCursorVisibility(false);
|
|
322
440
|
this.requestRender();
|
|
323
441
|
// Return handle for controlling this overlay
|
|
324
442
|
return {
|
|
@@ -334,7 +452,7 @@ export class TUI extends Container {
|
|
|
334
452
|
this.setFocus(topVisible?.component ?? entry.preFocus);
|
|
335
453
|
}
|
|
336
454
|
if (this.overlayStack.length === 0)
|
|
337
|
-
this
|
|
455
|
+
this.#setCursorVisibility(false);
|
|
338
456
|
this.requestRender();
|
|
339
457
|
}
|
|
340
458
|
},
|
|
@@ -416,7 +534,7 @@ export class TUI extends Container {
|
|
|
416
534
|
this.setFocus(topVisible?.component ?? overlay.preFocus);
|
|
417
535
|
}
|
|
418
536
|
if (this.overlayStack.length === 0)
|
|
419
|
-
this
|
|
537
|
+
this.#setCursorVisibility(false);
|
|
420
538
|
this.requestRender();
|
|
421
539
|
}
|
|
422
540
|
/** Check if there are any visible overlays */
|
|
@@ -451,8 +569,9 @@ export class TUI extends Container {
|
|
|
451
569
|
}
|
|
452
570
|
start() {
|
|
453
571
|
this.stopped = false;
|
|
572
|
+
this.#lastCursorVisibility = undefined;
|
|
454
573
|
this.terminal.start((data) => this.handleInput(data), () => this.requestRender());
|
|
455
|
-
this
|
|
574
|
+
this.#setCursorVisibility(false);
|
|
456
575
|
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
457
576
|
this.terminal.write("\x1b[?2031h");
|
|
458
577
|
}
|
|
@@ -513,9 +632,12 @@ export class TUI extends Container {
|
|
|
513
632
|
}
|
|
514
633
|
this.terminal.write("\r\n");
|
|
515
634
|
}
|
|
516
|
-
this
|
|
635
|
+
this.#lastCursorVisibility = undefined;
|
|
636
|
+
this.#setCursorVisibility(true);
|
|
517
637
|
this.terminal.stop();
|
|
638
|
+
this.#lastCursorVisibility = undefined;
|
|
518
639
|
this.previousLines = [];
|
|
640
|
+
this.previousRawLines = [];
|
|
519
641
|
this.previousKittyImageIds.clear();
|
|
520
642
|
this.previousWidth = 0;
|
|
521
643
|
this.previousHeight = 0;
|
|
@@ -524,10 +646,22 @@ export class TUI extends Container {
|
|
|
524
646
|
this.maxLinesRendered = 0;
|
|
525
647
|
this.previousViewportTop = 0;
|
|
526
648
|
}
|
|
649
|
+
#setCursorVisibility(visible) {
|
|
650
|
+
if (this.#lastCursorVisibility === visible)
|
|
651
|
+
return;
|
|
652
|
+
if (visible) {
|
|
653
|
+
this.terminal.showCursor();
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
this.terminal.hideCursor();
|
|
657
|
+
}
|
|
658
|
+
this.#lastCursorVisibility = visible;
|
|
659
|
+
}
|
|
527
660
|
requestRender(force = false, source = "unknown") {
|
|
528
661
|
if (force) {
|
|
529
662
|
this.inputRenderPending = false;
|
|
530
663
|
this.previousLines = [];
|
|
664
|
+
this.previousRawLines = [];
|
|
531
665
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
532
666
|
this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
|
|
533
667
|
this.cursorRow = 0;
|
|
@@ -892,17 +1026,96 @@ export class TUI extends Container {
|
|
|
892
1026
|
return result;
|
|
893
1027
|
}
|
|
894
1028
|
static { this.SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07"; }
|
|
895
|
-
|
|
896
|
-
|
|
1029
|
+
/**
|
|
1030
|
+
* Every frame write is bracketed by synchronized output (DECSET 2026) and
|
|
1031
|
+
* disables autowrap (DECAWM, DECRST 7) while rows are painted. Differential
|
|
1032
|
+
* rendering tracks the cursor with relative moves only, so a row the
|
|
1033
|
+
* terminal draws wider than visibleWidth() measured (East-Asian-ambiguous
|
|
1034
|
+
* glyphs, emoji newer than the terminal's Unicode tables, decomposed jamo)
|
|
1035
|
+
* would wrap, silently shift the cursor one row down, and leave stale ghost
|
|
1036
|
+
* rows behind on every following diff. With autowrap off such rows clip at
|
|
1037
|
+
* the last column instead. Autowrap is restored at frame end so the shell
|
|
1038
|
+
* never observes the disabled state.
|
|
1039
|
+
*/
|
|
1040
|
+
static { this.FRAME_BEGIN = "\x1b[?2026h\x1b[?7l"; }
|
|
1041
|
+
static { this.FRAME_END = "\x1b[?7h\x1b[?2026l"; }
|
|
1042
|
+
setPreviousLines(lines, rawLines) {
|
|
1043
|
+
this.previousLines = lines;
|
|
1044
|
+
this.previousRawLines = rawLines;
|
|
1045
|
+
}
|
|
1046
|
+
normalizeLine(line) {
|
|
1047
|
+
if (isImageLine(line)) {
|
|
1048
|
+
return { line, normalized: false };
|
|
1049
|
+
}
|
|
1050
|
+
return { line: normalizeTerminalOutput(line) + TUI.SEGMENT_RESET, normalized: true };
|
|
1051
|
+
}
|
|
1052
|
+
applyLineResets(lines, mode = "full") {
|
|
1053
|
+
const normalizedLines = [];
|
|
1054
|
+
let normalizedCount = 0;
|
|
897
1055
|
for (let i = 0; i < lines.length; i++) {
|
|
898
1056
|
const line = lines[i];
|
|
899
|
-
if (
|
|
900
|
-
|
|
1057
|
+
if (isImageLine(line)) {
|
|
1058
|
+
normalizedLines.push(line);
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
normalizedLines.push(normalizeTerminalOutput(line) + TUI.SEGMENT_RESET);
|
|
1062
|
+
normalizedCount += 1;
|
|
901
1063
|
}
|
|
902
1064
|
}
|
|
903
|
-
|
|
1065
|
+
recordViewportRenderStats(normalizedCount, mode);
|
|
1066
|
+
return {
|
|
1067
|
+
lines: normalizedLines,
|
|
1068
|
+
firstRawChanged: 0,
|
|
1069
|
+
compareEndExclusive: normalizedLines.length,
|
|
1070
|
+
bounded: false,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
applyViewportLineResets(rawLines, viewportTop, height, stableDimensions) {
|
|
1074
|
+
if (!viewportRenderEnabled() ||
|
|
1075
|
+
!stableDimensions ||
|
|
1076
|
+
this.previousLines.length === 0 ||
|
|
1077
|
+
this.previousLines.length !== rawLines.length ||
|
|
1078
|
+
this.previousRawLines.length !== rawLines.length) {
|
|
1079
|
+
return this.applyLineResets(rawLines);
|
|
1080
|
+
}
|
|
1081
|
+
const windowStart = Math.max(0, viewportTop - VIEWPORT_RENDER_OVERSCAN);
|
|
1082
|
+
const windowEnd = Math.min(rawLines.length, viewportTop + height + VIEWPORT_RENDER_OVERSCAN);
|
|
1083
|
+
let firstRawChanged = -1;
|
|
1084
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
1085
|
+
if (rawLines[i] === this.previousRawLines[i]) {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (firstRawChanged === -1) {
|
|
1089
|
+
firstRawChanged = i;
|
|
1090
|
+
}
|
|
1091
|
+
if (i < windowStart || i >= windowEnd) {
|
|
1092
|
+
return this.applyLineResets(rawLines, "escaped");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const lines = this.previousLines.slice();
|
|
1096
|
+
let normalizedCount = 0;
|
|
1097
|
+
if (firstRawChanged !== -1) {
|
|
1098
|
+
for (let i = windowStart; i < windowEnd; i++) {
|
|
1099
|
+
if (rawLines[i] === this.previousRawLines[i]) {
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const normalized = this.normalizeLine(rawLines[i] ?? "");
|
|
1103
|
+
lines[i] = normalized.line;
|
|
1104
|
+
if (normalized.normalized) {
|
|
1105
|
+
normalizedCount += 1;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
recordViewportRenderStats(normalizedCount, "bounded");
|
|
1110
|
+
return {
|
|
1111
|
+
lines,
|
|
1112
|
+
firstRawChanged,
|
|
1113
|
+
compareEndExclusive: windowEnd,
|
|
1114
|
+
bounded: true,
|
|
1115
|
+
};
|
|
904
1116
|
}
|
|
905
1117
|
collectKittyImageIds(lines) {
|
|
1118
|
+
recordKittyImageScanStats(lines.length);
|
|
906
1119
|
const ids = new Set();
|
|
907
1120
|
for (const line of lines) {
|
|
908
1121
|
for (const id of extractKittyImageIds(line)) {
|
|
@@ -950,6 +1163,24 @@ export class TUI extends Container {
|
|
|
950
1163
|
expandForLines(newLines);
|
|
951
1164
|
return { firstChanged: expandedFirstChanged, lastChanged: expandedLastChanged };
|
|
952
1165
|
}
|
|
1166
|
+
changedRangeNeedsKittyImageExpansion(lines, firstChanged, lastChanged) {
|
|
1167
|
+
if (this.previousKittyImageIds.size > 0) {
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
1170
|
+
const start = Math.max(0, firstChanged);
|
|
1171
|
+
const end = Math.min(lines.length - 1, lastChanged);
|
|
1172
|
+
let scannedLines = 0;
|
|
1173
|
+
for (let i = start; i <= end; i++) {
|
|
1174
|
+
scannedLines += 1;
|
|
1175
|
+
const line = lines[i] ?? "";
|
|
1176
|
+
if (isImageLine(line) || extractKittyImageIds(line).length > 0) {
|
|
1177
|
+
recordKittyImageScanStats(scannedLines);
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
recordKittyImageScanStats(scannedLines);
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
953
1184
|
deleteChangedKittyImages(firstChanged, lastChanged) {
|
|
954
1185
|
if (firstChanged < 0 || lastChanged < firstChanged)
|
|
955
1186
|
return "";
|
|
@@ -965,6 +1196,9 @@ export class TUI extends Container {
|
|
|
965
1196
|
getViewportRows(lines, viewportTop, height) {
|
|
966
1197
|
return Array.from({ length: height }, (_, row) => lines[viewportTop + row] ?? "");
|
|
967
1198
|
}
|
|
1199
|
+
shouldPreserveMuxScrollback() {
|
|
1200
|
+
return this.#muxDetector() && !useLegacyMuxRender();
|
|
1201
|
+
}
|
|
968
1202
|
createViewportInsertScrollPlan(newLines, prevViewportTop, height, lineCountDelta) {
|
|
969
1203
|
if (lineCountDelta <= 0 || lineCountDelta >= height || this.overlayStack.length > 0) {
|
|
970
1204
|
return undefined;
|
|
@@ -988,19 +1222,24 @@ export class TUI extends Container {
|
|
|
988
1222
|
if (regionHeight <= 0 || regionHeight < lineCountDelta) {
|
|
989
1223
|
return undefined;
|
|
990
1224
|
}
|
|
1225
|
+
const mutatedRows = [];
|
|
991
1226
|
for (let row = 0; row < regionHeight - lineCountDelta; row++) {
|
|
992
1227
|
if (previousVisible[row + lineCountDelta] !== nextVisible[row]) {
|
|
993
|
-
|
|
1228
|
+
mutatedRows.push({ screenRow: row, content: nextVisible[row] });
|
|
1229
|
+
if (mutatedRows.length > MAX_SCROLL_DIFF_ROWS) {
|
|
1230
|
+
return undefined;
|
|
1231
|
+
}
|
|
994
1232
|
}
|
|
995
1233
|
}
|
|
996
1234
|
return {
|
|
997
1235
|
viewportTop,
|
|
998
1236
|
regionBottom: regionHeight - 1,
|
|
999
1237
|
insertedRows: nextVisible.slice(regionHeight - lineCountDelta, regionHeight),
|
|
1238
|
+
mutatedRows,
|
|
1000
1239
|
};
|
|
1001
1240
|
}
|
|
1002
|
-
renderViewportInsertScroll(plan, newLines, cursorPos, width, height) {
|
|
1003
|
-
let buffer =
|
|
1241
|
+
renderViewportInsertScroll(plan, newLines, rawLines, cursorPos, width, height) {
|
|
1242
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1004
1243
|
const regionTop = 0;
|
|
1005
1244
|
const regionBottom = plan.regionBottom;
|
|
1006
1245
|
buffer += `\x1b[${regionTop + 1};${regionBottom + 1}r`;
|
|
@@ -1008,27 +1247,35 @@ export class TUI extends Container {
|
|
|
1008
1247
|
buffer += "\n".repeat(plan.insertedRows.length);
|
|
1009
1248
|
buffer += "\x1b[r";
|
|
1010
1249
|
const firstInsertedScreenRow = regionBottom - plan.insertedRows.length + 1;
|
|
1250
|
+
let finalPaintedScreenRow = firstInsertedScreenRow + plan.insertedRows.length - 1;
|
|
1011
1251
|
for (let index = 0; index < plan.insertedRows.length; index++) {
|
|
1012
1252
|
const screenRow = firstInsertedScreenRow + index;
|
|
1013
|
-
buffer += `\x1b[${screenRow + 1};1H\x1b[2K`;
|
|
1253
|
+
buffer += `\x1b[${screenRow + 1};1H\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1014
1254
|
buffer += plan.insertedRows[index] ?? "";
|
|
1015
1255
|
}
|
|
1016
|
-
|
|
1256
|
+
for (const row of plan.mutatedRows) {
|
|
1257
|
+
buffer += `\x1b[${row.screenRow + 1};1H\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1258
|
+
buffer += row.content;
|
|
1259
|
+
finalPaintedScreenRow = row.screenRow;
|
|
1260
|
+
}
|
|
1261
|
+
buffer += TUI.FRAME_END;
|
|
1017
1262
|
this.terminal.write(buffer);
|
|
1018
1263
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1019
|
-
this.hardwareCursorRow = plan.viewportTop +
|
|
1264
|
+
this.hardwareCursorRow = plan.viewportTop + finalPaintedScreenRow;
|
|
1020
1265
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1021
1266
|
this.previousViewportTop = plan.viewportTop;
|
|
1022
1267
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1023
|
-
this.
|
|
1268
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1024
1269
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1025
1270
|
this.previousWidth = width;
|
|
1026
1271
|
this.previousHeight = height;
|
|
1027
1272
|
}
|
|
1028
|
-
renderScrollbackReplay(newLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
|
|
1029
|
-
let buffer =
|
|
1273
|
+
renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
|
|
1274
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1030
1275
|
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
|
1031
|
-
|
|
1276
|
+
if (!this.shouldPreserveMuxScrollback()) {
|
|
1277
|
+
buffer += "\x1b[3J";
|
|
1278
|
+
}
|
|
1032
1279
|
const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
|
|
1033
1280
|
if (currentScreenRow > 0) {
|
|
1034
1281
|
buffer += `\x1b[${currentScreenRow}A`;
|
|
@@ -1037,21 +1284,52 @@ export class TUI extends Container {
|
|
|
1037
1284
|
for (let row = 0; row < bufferLength; row++) {
|
|
1038
1285
|
if (row > 0)
|
|
1039
1286
|
buffer += "\r\n";
|
|
1040
|
-
buffer +=
|
|
1287
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1041
1288
|
buffer += newLines[row] ?? "";
|
|
1042
1289
|
}
|
|
1043
|
-
buffer +=
|
|
1290
|
+
buffer += TUI.FRAME_END;
|
|
1044
1291
|
this.terminal.write(buffer);
|
|
1045
1292
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1046
1293
|
this.hardwareCursorRow = bufferLength - 1;
|
|
1047
1294
|
this.maxLinesRendered = newLines.length;
|
|
1048
1295
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1049
1296
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1050
|
-
this.
|
|
1297
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1051
1298
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1052
1299
|
this.previousWidth = width;
|
|
1053
1300
|
this.previousHeight = height;
|
|
1054
1301
|
}
|
|
1302
|
+
renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop = Math.max(0, newLines.length - height)) {
|
|
1303
|
+
const previousVisible = this.getViewportRows(this.previousLines, this.previousViewportTop, height);
|
|
1304
|
+
const nextVisible = this.getViewportRows(newLines, viewportTop, height);
|
|
1305
|
+
if (previousVisible.some(isImageLine) || nextVisible.some(isImageLine)) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1309
|
+
const currentScreenRow = Math.max(0, Math.min(height - 1, this.hardwareCursorRow - this.previousViewportTop));
|
|
1310
|
+
if (currentScreenRow > 0) {
|
|
1311
|
+
buffer += `\x1b[${currentScreenRow}A`;
|
|
1312
|
+
}
|
|
1313
|
+
for (let row = 0; row < height; row++) {
|
|
1314
|
+
if (row > 0)
|
|
1315
|
+
buffer += "\r\n";
|
|
1316
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1317
|
+
buffer += newLines[viewportTop + row] ?? "";
|
|
1318
|
+
}
|
|
1319
|
+
buffer += TUI.FRAME_END;
|
|
1320
|
+
this.terminal.write(buffer);
|
|
1321
|
+
this.muxViewportRepaintCount += 1;
|
|
1322
|
+
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1323
|
+
this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1324
|
+
this.maxLinesRendered = newLines.length;
|
|
1325
|
+
this.previousViewportTop = viewportTop;
|
|
1326
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1327
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1328
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1329
|
+
this.previousWidth = width;
|
|
1330
|
+
this.previousHeight = height;
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1055
1333
|
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
|
1056
1334
|
compositeLineAt(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
|
|
1057
1335
|
if (isImageLine(baseLine))
|
|
@@ -1140,17 +1418,23 @@ export class TUI extends Container {
|
|
|
1140
1418
|
}
|
|
1141
1419
|
// Extract cursor position before applying line resets (marker must be found first)
|
|
1142
1420
|
const cursorPos = this.extractCursorPosition(newLines, height);
|
|
1143
|
-
|
|
1421
|
+
const rawLines = newLines;
|
|
1422
|
+
const normalizedLines = this.applyViewportLineResets(rawLines, prevViewportTop, height, !widthChanged && !heightChanged);
|
|
1423
|
+
newLines = normalizedLines.lines;
|
|
1424
|
+
const preserveMuxScrollback = this.shouldPreserveMuxScrollback();
|
|
1144
1425
|
// Helper to clear scrollback and viewport and render all new lines
|
|
1145
|
-
const fullRender = (clear) => {
|
|
1426
|
+
const fullRender = (clear, clearScrollback = clear) => {
|
|
1146
1427
|
this.fullRedrawCount += 1;
|
|
1147
|
-
let buffer =
|
|
1428
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1148
1429
|
if (clear) {
|
|
1149
1430
|
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
|
1150
|
-
buffer += "\x1b[2J\x1b[H
|
|
1431
|
+
buffer += "\x1b[2J\x1b[H";
|
|
1432
|
+
if (clearScrollback && !preserveMuxScrollback) {
|
|
1433
|
+
buffer += "\x1b[3J";
|
|
1434
|
+
}
|
|
1151
1435
|
}
|
|
1152
1436
|
else {
|
|
1153
|
-
buffer +=
|
|
1437
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1154
1438
|
}
|
|
1155
1439
|
for (let i = 0; i < newLines.length; i++) {
|
|
1156
1440
|
if (i > 0)
|
|
@@ -1170,7 +1454,7 @@ export class TUI extends Container {
|
|
|
1170
1454
|
}
|
|
1171
1455
|
buffer += line;
|
|
1172
1456
|
}
|
|
1173
|
-
buffer +=
|
|
1457
|
+
buffer += TUI.FRAME_END;
|
|
1174
1458
|
this.terminal.write(buffer);
|
|
1175
1459
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1176
1460
|
this.hardwareCursorRow = this.cursorRow;
|
|
@@ -1184,7 +1468,7 @@ export class TUI extends Container {
|
|
|
1184
1468
|
const bufferLength = Math.max(height, newLines.length);
|
|
1185
1469
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1186
1470
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1187
|
-
this.
|
|
1471
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1188
1472
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1189
1473
|
this.previousWidth = width;
|
|
1190
1474
|
this.previousHeight = height;
|
|
@@ -1195,7 +1479,8 @@ export class TUI extends Container {
|
|
|
1195
1479
|
return;
|
|
1196
1480
|
const logPath = path.join(os.homedir(), ".senpi", "agent", "senpi-debug.log");
|
|
1197
1481
|
const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`;
|
|
1198
|
-
fs.appendFileSync(logPath, msg);
|
|
1482
|
+
fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1483
|
+
chmodDiagnosticLogBestEffort(logPath);
|
|
1199
1484
|
};
|
|
1200
1485
|
// First render - just output everything without clearing (assumes clean screen)
|
|
1201
1486
|
if (this.previousLines.length === 0 && !widthChanged && !heightChanged) {
|
|
@@ -1206,7 +1491,8 @@ export class TUI extends Container {
|
|
|
1206
1491
|
// Width changes always need a full re-render because wrapping changes.
|
|
1207
1492
|
if (widthChanged) {
|
|
1208
1493
|
logRedraw(`terminal width changed (${this.previousWidth} -> ${width})`);
|
|
1209
|
-
|
|
1494
|
+
// In multiplexers, re-emit the viewport without 3J so pane history survives; an older copy may remain above.
|
|
1495
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1210
1496
|
return;
|
|
1211
1497
|
}
|
|
1212
1498
|
// Height changes normally need a full re-render to keep the visible viewport aligned,
|
|
@@ -1214,7 +1500,14 @@ export class TUI extends Container {
|
|
|
1214
1500
|
// In that environment, a full redraw causes the entire history to replay on every toggle.
|
|
1215
1501
|
if (heightChanged && !isTermuxSession()) {
|
|
1216
1502
|
logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`);
|
|
1217
|
-
|
|
1503
|
+
if (preserveMuxScrollback) {
|
|
1504
|
+
if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height)) {
|
|
1505
|
+
fullRender(true, false);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
else {
|
|
1509
|
+
fullRender(true);
|
|
1510
|
+
}
|
|
1218
1511
|
return;
|
|
1219
1512
|
}
|
|
1220
1513
|
// Content shrunk below the working area and no overlays - re-render to clear empty rows
|
|
@@ -1222,14 +1515,16 @@ export class TUI extends Container {
|
|
|
1222
1515
|
// Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
|
|
1223
1516
|
if (this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
|
|
1224
1517
|
logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
|
|
1225
|
-
fullRender(true);
|
|
1518
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1226
1519
|
return;
|
|
1227
1520
|
}
|
|
1228
1521
|
// Find first and last changed lines
|
|
1229
1522
|
let firstChanged = -1;
|
|
1230
1523
|
let lastChanged = -1;
|
|
1231
1524
|
const maxLines = Math.max(newLines.length, this.previousLines.length);
|
|
1232
|
-
|
|
1525
|
+
const diffScanStart = normalizedLines.bounded && normalizedLines.firstRawChanged !== -1 ? normalizedLines.firstRawChanged : 0;
|
|
1526
|
+
const diffScanEndExclusive = normalizedLines.bounded ? normalizedLines.compareEndExclusive : maxLines;
|
|
1527
|
+
for (let i = diffScanStart; i < diffScanEndExclusive; i++) {
|
|
1233
1528
|
const oldLine = i < this.previousLines.length ? this.previousLines[i] : "";
|
|
1234
1529
|
const newLine = i < newLines.length ? newLines[i] : "";
|
|
1235
1530
|
if (oldLine !== newLine) {
|
|
@@ -1247,7 +1542,8 @@ export class TUI extends Container {
|
|
|
1247
1542
|
}
|
|
1248
1543
|
lastChanged = newLines.length - 1;
|
|
1249
1544
|
}
|
|
1250
|
-
|
|
1545
|
+
const needsKittyImageExpansion = firstChanged !== -1 && this.changedRangeNeedsKittyImageExpansion(newLines, firstChanged, lastChanged);
|
|
1546
|
+
if (needsKittyImageExpansion) {
|
|
1251
1547
|
const expandedRange = this.expandChangedRangeForKittyImages(firstChanged, lastChanged, newLines);
|
|
1252
1548
|
firstChanged = expandedRange.firstChanged;
|
|
1253
1549
|
lastChanged = expandedRange.lastChanged;
|
|
@@ -1262,19 +1558,19 @@ export class TUI extends Container {
|
|
|
1262
1558
|
return;
|
|
1263
1559
|
}
|
|
1264
1560
|
if (insertScrollPlan) {
|
|
1265
|
-
this.renderViewportInsertScroll(insertScrollPlan, newLines, cursorPos, width, height);
|
|
1561
|
+
this.renderViewportInsertScroll(insertScrollPlan, newLines, rawLines, cursorPos, width, height);
|
|
1266
1562
|
return;
|
|
1267
1563
|
}
|
|
1268
1564
|
// All changes are in deleted lines (nothing to render, just clear)
|
|
1269
1565
|
if (firstChanged >= newLines.length) {
|
|
1270
1566
|
if (this.previousLines.length > newLines.length) {
|
|
1271
|
-
let buffer =
|
|
1567
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1272
1568
|
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
|
1273
1569
|
// Move to end of new content (clamp to 0 for empty content)
|
|
1274
1570
|
const targetRow = Math.max(0, newLines.length - 1);
|
|
1275
1571
|
if (targetRow < prevViewportTop) {
|
|
1276
1572
|
logRedraw(`deleted lines moved viewport up (${targetRow} < ${prevViewportTop})`);
|
|
1277
|
-
fullRender(true);
|
|
1573
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1278
1574
|
return;
|
|
1279
1575
|
}
|
|
1280
1576
|
const lineDiff = computeLineDiff(targetRow);
|
|
@@ -1287,7 +1583,7 @@ export class TUI extends Container {
|
|
|
1287
1583
|
const extraLines = this.previousLines.length - newLines.length;
|
|
1288
1584
|
if (extraLines > height) {
|
|
1289
1585
|
logRedraw(`extraLines > height (${extraLines} > ${height})`);
|
|
1290
|
-
fullRender(true);
|
|
1586
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1291
1587
|
return;
|
|
1292
1588
|
}
|
|
1293
1589
|
const clearStartOffset = newLines.length === 0 ? 0 : 1;
|
|
@@ -1295,7 +1591,7 @@ export class TUI extends Container {
|
|
|
1295
1591
|
buffer += `\x1b[${clearStartOffset}B`;
|
|
1296
1592
|
}
|
|
1297
1593
|
for (let i = 0; i < extraLines; i++) {
|
|
1298
|
-
buffer +=
|
|
1594
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1299
1595
|
if (i < extraLines - 1)
|
|
1300
1596
|
buffer += "\x1b[1B";
|
|
1301
1597
|
}
|
|
@@ -1303,13 +1599,13 @@ export class TUI extends Container {
|
|
|
1303
1599
|
if (moveBack > 0) {
|
|
1304
1600
|
buffer += `\x1b[${moveBack}A`;
|
|
1305
1601
|
}
|
|
1306
|
-
buffer +=
|
|
1602
|
+
buffer += TUI.FRAME_END;
|
|
1307
1603
|
this.terminal.write(buffer);
|
|
1308
1604
|
this.cursorRow = targetRow;
|
|
1309
1605
|
this.hardwareCursorRow = targetRow;
|
|
1310
1606
|
}
|
|
1311
1607
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1312
|
-
this.
|
|
1608
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1313
1609
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1314
1610
|
this.previousWidth = width;
|
|
1315
1611
|
this.previousHeight = height;
|
|
@@ -1339,12 +1635,20 @@ export class TUI extends Container {
|
|
|
1339
1635
|
}
|
|
1340
1636
|
if (firstVisibleChanged === -1) {
|
|
1341
1637
|
if (lineCountDelta !== 0) {
|
|
1342
|
-
|
|
1638
|
+
if (preserveMuxScrollback) {
|
|
1639
|
+
// Above-viewport scrollback may stay stale in mux panes; only the visible viewport is repainted.
|
|
1640
|
+
if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop)) {
|
|
1641
|
+
fullRender(true, false);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
else {
|
|
1645
|
+
this.renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow);
|
|
1646
|
+
}
|
|
1343
1647
|
return;
|
|
1344
1648
|
}
|
|
1345
1649
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1346
1650
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1347
|
-
this.
|
|
1651
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1348
1652
|
this.previousWidth = width;
|
|
1349
1653
|
this.previousHeight = height;
|
|
1350
1654
|
this.previousViewportTop = viewportTop;
|
|
@@ -1352,7 +1656,7 @@ export class TUI extends Container {
|
|
|
1352
1656
|
}
|
|
1353
1657
|
if (viewportTop !== prevViewportTop) {
|
|
1354
1658
|
const previousViewportBottom = Math.min(this.previousLines.length - 1, prevViewportTop + height - 1);
|
|
1355
|
-
let buffer =
|
|
1659
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1356
1660
|
buffer += this.deleteChangedKittyImages(prevViewportTop, previousViewportBottom);
|
|
1357
1661
|
const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
|
|
1358
1662
|
if (currentScreenRow > 0) {
|
|
@@ -1361,17 +1665,17 @@ export class TUI extends Container {
|
|
|
1361
1665
|
for (let row = 0; row < height; row++) {
|
|
1362
1666
|
if (row > 0)
|
|
1363
1667
|
buffer += "\r\n";
|
|
1364
|
-
buffer +=
|
|
1668
|
+
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1365
1669
|
buffer += newLines[viewportTop + row] ?? "";
|
|
1366
1670
|
}
|
|
1367
|
-
buffer +=
|
|
1671
|
+
buffer += TUI.FRAME_END;
|
|
1368
1672
|
this.terminal.write(buffer);
|
|
1369
1673
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1370
1674
|
this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1371
1675
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1372
1676
|
this.previousViewportTop = viewportTop;
|
|
1373
1677
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1374
|
-
this.
|
|
1678
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1375
1679
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1376
1680
|
this.previousWidth = width;
|
|
1377
1681
|
this.previousHeight = height;
|
|
@@ -1382,7 +1686,7 @@ export class TUI extends Container {
|
|
|
1382
1686
|
}
|
|
1383
1687
|
// Render from first changed line to end
|
|
1384
1688
|
// Build buffer with all updates wrapped in synchronized output
|
|
1385
|
-
let buffer =
|
|
1689
|
+
let buffer = TUI.FRAME_BEGIN;
|
|
1386
1690
|
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
|
1387
1691
|
const prevViewportBottom = prevViewportTop + height - 1;
|
|
1388
1692
|
const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
|
|
@@ -1420,12 +1724,12 @@ export class TUI extends Container {
|
|
|
1420
1724
|
const imageStartScreenRow = i - viewportTop;
|
|
1421
1725
|
if (imageStartScreenRow < 0 || imageStartScreenRow + imageReservedRows > height) {
|
|
1422
1726
|
logRedraw(`kitty image pre-clear would scroll (${imageStartScreenRow} + ${imageReservedRows} > ${height})`);
|
|
1423
|
-
fullRender(true);
|
|
1727
|
+
fullRender(true, !preserveMuxScrollback);
|
|
1424
1728
|
return;
|
|
1425
1729
|
}
|
|
1426
|
-
buffer +=
|
|
1730
|
+
buffer += `\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1427
1731
|
for (let row = 1; row < imageReservedRows; row++) {
|
|
1428
|
-
buffer +=
|
|
1732
|
+
buffer += `\r\n\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1429
1733
|
}
|
|
1430
1734
|
buffer += `\x1b[${imageReservedRows - 1}A`;
|
|
1431
1735
|
buffer += line;
|
|
@@ -1433,32 +1737,44 @@ export class TUI extends Container {
|
|
|
1433
1737
|
i += imageReservedRows - 1;
|
|
1434
1738
|
continue;
|
|
1435
1739
|
}
|
|
1436
|
-
buffer +=
|
|
1437
|
-
|
|
1740
|
+
buffer += `\x1b[2K${TUI.SEGMENT_RESET}`; // Clear current line
|
|
1741
|
+
const lineWidth = visibleWidth(line);
|
|
1742
|
+
if (!isImage && lineWidth > width) {
|
|
1438
1743
|
// Log all lines to crash file for debugging
|
|
1439
1744
|
const crashLogPath = path.join(os.homedir(), ".senpi", "agent", "senpi-crash.log");
|
|
1440
1745
|
const crashData = [
|
|
1441
1746
|
`Crash at ${new Date().toISOString()}`,
|
|
1442
1747
|
`Terminal width: ${width}`,
|
|
1443
|
-
`Line ${i} visible width: ${
|
|
1748
|
+
`Line ${i} visible width: ${lineWidth}`,
|
|
1444
1749
|
"",
|
|
1445
1750
|
"=== All rendered lines ===",
|
|
1446
1751
|
...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),
|
|
1447
1752
|
"",
|
|
1448
1753
|
].join("\n");
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1754
|
+
if (process.env.PI_TUI_STRICT_RENDER === "1") {
|
|
1755
|
+
const crashDumpWritten = writeRenderDiagnosticBestEffort(crashLogPath, crashData);
|
|
1756
|
+
// Clean up terminal state before throwing
|
|
1757
|
+
this.stop();
|
|
1758
|
+
const errorMsg = [
|
|
1759
|
+
`Rendered line ${i} exceeds terminal width (${lineWidth} > ${width}).`,
|
|
1760
|
+
"",
|
|
1761
|
+
"This is likely caused by a custom TUI component not truncating its output.",
|
|
1762
|
+
"Use visibleWidth() to measure and truncateToWidth() to truncate lines.",
|
|
1763
|
+
"",
|
|
1764
|
+
crashDumpWritten
|
|
1765
|
+
? `Debug log written to: ${crashLogPath}`
|
|
1766
|
+
: `Debug log could not be written to: ${crashLogPath}`,
|
|
1767
|
+
].join("\n");
|
|
1768
|
+
throw new Error(errorMsg);
|
|
1769
|
+
}
|
|
1770
|
+
if (!this.overWideCrashDumpWritten) {
|
|
1771
|
+
writeRenderDiagnosticBestEffort(crashLogPath, crashData);
|
|
1772
|
+
this.overWideCrashDumpWritten = true;
|
|
1773
|
+
}
|
|
1774
|
+
const truncatedLine = sliceByColumn(line, 0, width, true) + TUI.SEGMENT_RESET;
|
|
1775
|
+
newLines[i] = truncatedLine;
|
|
1776
|
+
buffer += truncatedLine;
|
|
1777
|
+
continue;
|
|
1462
1778
|
}
|
|
1463
1779
|
buffer += line;
|
|
1464
1780
|
}
|
|
@@ -1474,12 +1790,12 @@ export class TUI extends Container {
|
|
|
1474
1790
|
}
|
|
1475
1791
|
const extraLines = this.previousLines.length - newLines.length;
|
|
1476
1792
|
for (let i = newLines.length; i < this.previousLines.length; i++) {
|
|
1477
|
-
buffer +=
|
|
1793
|
+
buffer += `\r\n\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1478
1794
|
}
|
|
1479
1795
|
// Move cursor back to end of new content
|
|
1480
1796
|
buffer += `\x1b[${extraLines}A`;
|
|
1481
1797
|
}
|
|
1482
|
-
buffer +=
|
|
1798
|
+
buffer += TUI.FRAME_END;
|
|
1483
1799
|
if (process.env.PI_TUI_DEBUG === "1") {
|
|
1484
1800
|
const debugDir = "/tmp/tui";
|
|
1485
1801
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
@@ -1506,7 +1822,8 @@ export class TUI extends Container {
|
|
|
1506
1822
|
"=== buffer ===",
|
|
1507
1823
|
JSON.stringify(buffer),
|
|
1508
1824
|
].join("\n");
|
|
1509
|
-
fs.writeFileSync(debugPath, debugData);
|
|
1825
|
+
fs.writeFileSync(debugPath, debugData, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1826
|
+
chmodDiagnosticLogBestEffort(debugPath);
|
|
1510
1827
|
}
|
|
1511
1828
|
// Write entire buffer at once
|
|
1512
1829
|
this.terminal.write(buffer);
|
|
@@ -1520,8 +1837,10 @@ export class TUI extends Container {
|
|
|
1520
1837
|
this.previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
|
|
1521
1838
|
// Position hardware cursor for IME
|
|
1522
1839
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1523
|
-
this.
|
|
1524
|
-
|
|
1840
|
+
this.setPreviousLines(newLines, rawLines);
|
|
1841
|
+
if (needsKittyImageExpansion) {
|
|
1842
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1843
|
+
}
|
|
1525
1844
|
this.previousWidth = width;
|
|
1526
1845
|
this.previousHeight = height;
|
|
1527
1846
|
}
|
|
@@ -1531,34 +1850,36 @@ export class TUI extends Container {
|
|
|
1531
1850
|
* @param totalLines Total number of rendered lines
|
|
1532
1851
|
*/
|
|
1533
1852
|
positionHardwareCursor(cursorPos, totalLines) {
|
|
1534
|
-
if (!cursorPos || totalLines <= 0) {
|
|
1535
|
-
this.terminal.hideCursor();
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
// Clamp cursor position to valid range
|
|
1539
|
-
const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
|
|
1540
|
-
const targetCol = Math.max(0, cursorPos.col);
|
|
1541
|
-
// Move cursor from current position to target
|
|
1542
|
-
const rowDelta = targetRow - this.hardwareCursorRow;
|
|
1543
1853
|
let buffer = "";
|
|
1544
|
-
if (
|
|
1545
|
-
|
|
1854
|
+
if (!cursorPos || totalLines <= 0) {
|
|
1855
|
+
if (this.#lastCursorVisibility !== false) {
|
|
1856
|
+
buffer += "\x1b[?25l";
|
|
1857
|
+
this.#lastCursorVisibility = false;
|
|
1858
|
+
}
|
|
1546
1859
|
}
|
|
1547
|
-
else
|
|
1548
|
-
|
|
1860
|
+
else {
|
|
1861
|
+
// Clamp cursor position to valid range
|
|
1862
|
+
const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
|
|
1863
|
+
const targetCol = Math.max(0, cursorPos.col);
|
|
1864
|
+
// Move cursor from current position to target
|
|
1865
|
+
const rowDelta = targetRow - this.hardwareCursorRow;
|
|
1866
|
+
if (rowDelta > 0) {
|
|
1867
|
+
buffer += `\x1b[${rowDelta}B`; // Move down
|
|
1868
|
+
}
|
|
1869
|
+
else if (rowDelta < 0) {
|
|
1870
|
+
buffer += `\x1b[${-rowDelta}A`; // Move up
|
|
1871
|
+
}
|
|
1872
|
+
// Move to absolute column (1-indexed)
|
|
1873
|
+
buffer += `\x1b[${targetCol + 1}G`;
|
|
1874
|
+
this.hardwareCursorRow = targetRow;
|
|
1875
|
+
if (this.#lastCursorVisibility !== this.showHardwareCursor) {
|
|
1876
|
+
buffer += this.showHardwareCursor ? "\x1b[?25h" : "\x1b[?25l";
|
|
1877
|
+
this.#lastCursorVisibility = this.showHardwareCursor;
|
|
1878
|
+
}
|
|
1549
1879
|
}
|
|
1550
|
-
// Move to absolute column (1-indexed)
|
|
1551
|
-
buffer += `\x1b[${targetCol + 1}G`;
|
|
1552
1880
|
if (buffer) {
|
|
1553
1881
|
this.terminal.write(buffer);
|
|
1554
1882
|
}
|
|
1555
|
-
this.hardwareCursorRow = targetRow;
|
|
1556
|
-
if (this.showHardwareCursor) {
|
|
1557
|
-
this.terminal.showCursor();
|
|
1558
|
-
}
|
|
1559
|
-
else {
|
|
1560
|
-
this.terminal.hideCursor();
|
|
1561
|
-
}
|
|
1562
1883
|
}
|
|
1563
1884
|
/**
|
|
1564
1885
|
* Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).
|