mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
Merge pull request #5189 from mpazik/feat/tool-title-hyperlinks
OSC 8 hyperlinks file paths in tool titles
This commit is contained in:
@@ -4,6 +4,7 @@ import { constants } from "fs";
|
||||
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { renderDiff } from "../../modes/interactive/components/diff.ts";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { ToolDefinition } from "../extensions/types.ts";
|
||||
import {
|
||||
applyEditsToNormalizedContent,
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
} from "./edit-diff.ts";
|
||||
import { withFileMutationQueue } from "./file-mutation-queue.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { renderToolPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
|
||||
type EditPreview = EditDiffResult | EditDiffError;
|
||||
@@ -191,14 +192,8 @@ function getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatEditCall(
|
||||
args: RenderableEditArgs | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const invalidArg = invalidArgText(theme);
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
||||
const pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
||||
function formatEditCall(args: RenderableEditArgs | undefined, theme: Theme, cwd: string): string {
|
||||
const pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);
|
||||
return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
|
||||
}
|
||||
|
||||
@@ -206,7 +201,7 @@ function formatEditResult(
|
||||
args: RenderableEditArgs | undefined,
|
||||
preview: EditPreview | undefined,
|
||||
result: EditToolResultLike,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
isError: boolean,
|
||||
): string | undefined {
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
@@ -234,7 +229,7 @@ function formatEditResult(
|
||||
function getEditHeaderBg(
|
||||
preview: EditPreview | undefined,
|
||||
settledError: boolean | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
): (text: string) => string {
|
||||
if (preview) {
|
||||
if ("error" in preview) {
|
||||
@@ -251,11 +246,12 @@ function getEditHeaderBg(
|
||||
function buildEditCallComponent(
|
||||
component: EditCallRenderComponent,
|
||||
args: RenderableEditArgs | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
cwd: string,
|
||||
): EditCallRenderComponent {
|
||||
component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));
|
||||
component.clear();
|
||||
component.addChild(new Text(formatEditCall(args, theme), 0, 0));
|
||||
component.addChild(new Text(formatEditCall(args, theme, cwd), 0, 0));
|
||||
|
||||
if (!component.preview) {
|
||||
return component;
|
||||
@@ -389,7 +385,7 @@ export function createEditToolDefinition(
|
||||
});
|
||||
}
|
||||
|
||||
return buildEditCallComponent(component, args, theme);
|
||||
return buildEditCallComponent(component, args, theme, context.cwd);
|
||||
},
|
||||
renderResult(result, _options, theme, context) {
|
||||
const callComponent = context.state.callComponent;
|
||||
@@ -414,7 +410,12 @@ export function createEditToolDefinition(
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
buildEditCallComponent(callComponent, context.args as RenderableEditArgs | undefined, theme);
|
||||
buildEditCallComponent(
|
||||
callComponent,
|
||||
context.args as RenderableEditArgs | undefined,
|
||||
theme,
|
||||
context.cwd,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import { ensureTool } from "../../utils/tools-manager.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { pathExists, resolveToCwd } from "./path-utils.ts";
|
||||
@@ -55,10 +56,7 @@ export interface FindToolOptions {
|
||||
operations?: FindOperations;
|
||||
}
|
||||
|
||||
function formatFindCall(
|
||||
args: { pattern: string; path?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
function formatFindCall(args: { pattern: string; path?: string; limit?: number } | undefined, theme: Theme): string {
|
||||
const pattern = str(args?.pattern);
|
||||
const rawPath = str(args?.path);
|
||||
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
||||
@@ -81,7 +79,7 @@ function formatFindResult(
|
||||
details?: FindToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import { ensureTool } from "../../utils/tools-manager.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
@@ -66,7 +67,7 @@ export interface GrepToolOptions {
|
||||
|
||||
function formatGrepCall(
|
||||
args: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
): string {
|
||||
const pattern = str(args?.pattern);
|
||||
const rawPath = str(args?.path);
|
||||
@@ -90,7 +91,7 @@ function formatGrepResult(
|
||||
details?: GrepToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
|
||||
@@ -4,9 +4,10 @@ import { Text } from "@earendil-works/pi-tui";
|
||||
import nodePath from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { pathExists, resolveToCwd } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { getTextOutput, renderToolPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
||||
|
||||
@@ -48,15 +49,10 @@ export interface LsToolOptions {
|
||||
operations?: LsOperations;
|
||||
}
|
||||
|
||||
function formatLsCall(
|
||||
args: { path?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const rawPath = str(args?.path);
|
||||
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
||||
function formatLsCall(args: { path?: string; limit?: number } | undefined, theme: Theme, cwd: string): string {
|
||||
const limit = args?.limit;
|
||||
const invalidArg = invalidArgText(theme);
|
||||
let text = `${theme.fg("toolTitle", theme.bold("ls"))} ${path === null ? invalidArg : theme.fg("accent", path)}`;
|
||||
const pathDisplay = renderToolPath(str(args?.path), theme, cwd, { emptyFallback: "." });
|
||||
let text = `${theme.fg("toolTitle", theme.bold("ls"))} ${pathDisplay}`;
|
||||
if (limit !== undefined) {
|
||||
text += theme.fg("toolOutput", ` (limit ${limit})`);
|
||||
}
|
||||
@@ -69,7 +65,7 @@ function formatLsResult(
|
||||
details?: LsToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
@@ -213,7 +209,7 @@ export function createLsToolDefinition(
|
||||
},
|
||||
renderCall(args, theme, context) {
|
||||
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
||||
text.setText(formatLsCall(args, theme));
|
||||
text.setText(formatLsCall(args, theme, context.cwd));
|
||||
return text;
|
||||
},
|
||||
renderResult(result, options, theme, context) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.ts";
|
||||
import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveReadPathAsync, resolveToCwd } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.ts";
|
||||
import { getTextOutput, renderToolPath, replaceTabs, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
||||
|
||||
@@ -71,11 +71,8 @@ function formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): st
|
||||
return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
||||
}
|
||||
|
||||
function formatReadCall(args: ReadRenderArgs | undefined, theme: Theme): string {
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
||||
const invalidArg = invalidArgText(theme);
|
||||
const pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
||||
function formatReadCall(args: ReadRenderArgs | undefined, theme: Theme, cwd: string): string {
|
||||
const pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);
|
||||
return `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;
|
||||
}
|
||||
|
||||
@@ -344,7 +341,9 @@ export function createReadToolDefinition(
|
||||
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
||||
const classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;
|
||||
text.setText(
|
||||
classification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme),
|
||||
classification
|
||||
? formatCompactReadCall(classification, args, theme)
|
||||
: formatReadCall(args, theme, context.cwd),
|
||||
);
|
||||
return text;
|
||||
},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as os from "node:os";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
||||
import { getCapabilities, getImageDimensions, imageFallback } from "@earendil-works/pi-tui";
|
||||
import { getCapabilities, getImageDimensions, hyperlink, imageFallback } from "@earendil-works/pi-tui";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import { stripAnsi } from "../../utils/ansi.ts";
|
||||
import { resolvePath } from "../../utils/paths.ts";
|
||||
import { sanitizeBinaryOutput } from "../../utils/shell.ts";
|
||||
|
||||
export function shortenPath(path: unknown): string {
|
||||
@@ -13,6 +16,12 @@ export function shortenPath(path: unknown): string {
|
||||
return path;
|
||||
}
|
||||
|
||||
export function linkPath(styledText: string, rawPath: string, cwd: string): string {
|
||||
if (!getCapabilities().hyperlinks) return styledText;
|
||||
const absolutePath = resolvePath(rawPath, cwd);
|
||||
return hyperlink(styledText, pathToFileURL(absolutePath).href);
|
||||
}
|
||||
|
||||
export function str(value: unknown): string | null {
|
||||
if (typeof value === "string") return value;
|
||||
if (value == null) return "";
|
||||
@@ -59,6 +68,18 @@ export type ToolRenderResultLike<TDetails> = {
|
||||
details: TDetails;
|
||||
};
|
||||
|
||||
export function invalidArgText(theme: { fg: (name: any, text: string) => string }): string {
|
||||
export function invalidArgText(theme: Theme): string {
|
||||
return theme.fg("error", "[invalid arg]");
|
||||
}
|
||||
|
||||
export function renderToolPath(
|
||||
rawPath: string | null,
|
||||
theme: Theme,
|
||||
cwd: string,
|
||||
options?: { emptyFallback?: string },
|
||||
): string {
|
||||
if (rawPath === null) return invalidArgText(theme);
|
||||
const value = rawPath || options?.emptyFallback;
|
||||
if (!value) return theme.fg("toolOutput", "...");
|
||||
return linkPath(theme.fg("accent", shortenPath(value)), value, cwd);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
|
||||
import { dirname } from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.ts";
|
||||
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { withFileMutationQueue } from "./file-mutation-queue.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { invalidArgText, normalizeDisplayText, replaceTabs, shortenPath, str } from "./render-utils.ts";
|
||||
import { normalizeDisplayText, renderToolPath, replaceTabs, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
|
||||
const writeSchema = Type.Object({
|
||||
@@ -131,14 +131,14 @@ function trimTrailingEmptyLines(lines: string[]): string[] {
|
||||
function formatWriteCall(
|
||||
args: { path?: string; file_path?: string; content?: string } | undefined,
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
cache: WriteHighlightCache | undefined,
|
||||
cwd: string,
|
||||
): string {
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
const fileContent = str(args?.content);
|
||||
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
||||
const invalidArg = invalidArgText(theme);
|
||||
let text = `${theme.fg("toolTitle", theme.bold("write"))} ${path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...")}`;
|
||||
const pathDisplay = renderToolPath(rawPath, theme, cwd);
|
||||
let text = `${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}`;
|
||||
|
||||
if (fileContent === null) {
|
||||
text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
|
||||
@@ -163,7 +163,7 @@ function formatWriteCall(
|
||||
|
||||
function formatWriteResult(
|
||||
result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean },
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
theme: Theme,
|
||||
): string | undefined {
|
||||
if (!result.isError) {
|
||||
return undefined;
|
||||
@@ -243,6 +243,7 @@ export function createWriteToolDefinition(
|
||||
{ expanded: context.expanded, isPartial: context.isPartial },
|
||||
theme,
|
||||
component.cache,
|
||||
context.cwd,
|
||||
),
|
||||
);
|
||||
return component;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export type ImageProtocol = "kitty" | "iterm2" | null;
|
||||
|
||||
export interface TerminalCapabilities {
|
||||
@@ -39,19 +41,42 @@ export function setCellDimensions(dims: CellDimensions): void {
|
||||
cellDimensions = dims;
|
||||
}
|
||||
|
||||
export function detectCapabilities(): TerminalCapabilities {
|
||||
/**
|
||||
* Checks whether the attached tmux client forwards OSC 8 hyperlinks to the
|
||||
* outer terminal. tmux only re-emits them when its `client_termfeatures` lists
|
||||
* `hyperlinks`, and strips them otherwise. On any error fallbacks `false`.
|
||||
*/
|
||||
function probeTmuxHyperlinks(): boolean {
|
||||
try {
|
||||
const termfeatures = execSync("tmux display-message -p '#{client_termfeatures}'", {
|
||||
encoding: "utf8",
|
||||
timeout: 250,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
return termfeatures
|
||||
.split(",")
|
||||
.map((feature) => feature.trim())
|
||||
.includes("hyperlinks");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function detectCapabilities(tmuxForwardsHyperlink: () => boolean = probeTmuxHyperlinks): TerminalCapabilities {
|
||||
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
|
||||
const terminalEmulator = process.env.TERMINAL_EMULATOR?.toLowerCase() || "";
|
||||
const term = process.env.TERM?.toLowerCase() || "";
|
||||
const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
|
||||
const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit";
|
||||
|
||||
// tmux and screen swallow OSC 8 by default (passthrough is opt-in and wraps
|
||||
// sequences differently). Force hyperlinks off whenever we detect them, even
|
||||
// when the outer terminal would otherwise support OSC 8. Image protocols are
|
||||
// also unreliable under tmux/screen, so leave `images: null` for safety.
|
||||
const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen");
|
||||
if (inTmuxOrScreen) {
|
||||
// Emit OSC 8 hyperlinks only when tmux confirms it forwards.
|
||||
// Image protocols are unreliable under tmux, so leave `images: null`.
|
||||
if (process.env.TMUX || term.startsWith("tmux")) {
|
||||
return { images: null, trueColor: hasTrueColorHint, hyperlinks: tmuxForwardsHyperlink() };
|
||||
}
|
||||
|
||||
// screen does not forward OSC 8 hyperlinks, so keep them off there.
|
||||
if (term.startsWith("screen")) {
|
||||
return { images: null, trueColor: hasTrueColorHint, hyperlinks: false };
|
||||
}
|
||||
|
||||
|
||||
@@ -207,19 +207,30 @@ describe("detectCapabilities", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("forces hyperlinks: false under tmux even if outer terminal supports OSC 8", () => {
|
||||
it("enables hyperlinks under tmux when the client forwards them", () => {
|
||||
withEnv({ TMUX: "/tmp/tmux-1000/default,1234,0", TERM_PROGRAM: "ghostty" }, () => {
|
||||
const caps = detectCapabilities();
|
||||
const caps = detectCapabilities(() => true);
|
||||
assert.strictEqual(caps.hyperlinks, true);
|
||||
assert.strictEqual(caps.images, null);
|
||||
});
|
||||
});
|
||||
|
||||
it("disables hyperlinks under tmux when the client does not forward them", () => {
|
||||
withEnv({ TMUX: "/tmp/tmux-1000/default,1234,0", TERM_PROGRAM: "ghostty" }, () => {
|
||||
const caps = detectCapabilities(() => false);
|
||||
assert.strictEqual(caps.hyperlinks, false);
|
||||
assert.strictEqual(caps.images, null);
|
||||
});
|
||||
});
|
||||
|
||||
it("forces hyperlinks: false when TERM starts with 'tmux'", () => {
|
||||
it("checks tmux capability when TERM starts with 'tmux'", () => {
|
||||
withEnv({ TERM: "tmux-256color", TERM_PROGRAM: "iterm.app" }, () => {
|
||||
const caps = detectCapabilities();
|
||||
assert.strictEqual(caps.hyperlinks, false);
|
||||
const caps = detectCapabilities(() => true);
|
||||
assert.strictEqual(caps.hyperlinks, true);
|
||||
assert.strictEqual(caps.images, null);
|
||||
|
||||
const caps2 = detectCapabilities(() => false);
|
||||
assert.strictEqual(caps2.hyperlinks, false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,7 +305,7 @@ describe("detectCapabilities", () => {
|
||||
|
||||
it("does not inherit Windows Terminal truecolor through tmux", () => {
|
||||
withEnv({ WT_SESSION: "session", TMUX: "/tmp/tmux-1000/default,1234,0", TERM: "tmux-256color" }, () => {
|
||||
const caps = detectCapabilities();
|
||||
const caps = detectCapabilities(() => false);
|
||||
assert.strictEqual(caps.trueColor, false);
|
||||
assert.strictEqual(caps.hyperlinks, false);
|
||||
assert.strictEqual(caps.images, null);
|
||||
@@ -303,7 +314,7 @@ describe("detectCapabilities", () => {
|
||||
|
||||
it("trusts explicit truecolor hints through tmux", () => {
|
||||
withEnv({ COLORTERM: "truecolor", TMUX: "/tmp/tmux-1000/default,1234,0", TERM: "tmux-256color" }, () => {
|
||||
const caps = detectCapabilities();
|
||||
const caps = detectCapabilities(() => false);
|
||||
assert.strictEqual(caps.trueColor, true);
|
||||
assert.strictEqual(caps.hyperlinks, false);
|
||||
assert.strictEqual(caps.images, null);
|
||||
|
||||
Reference in New Issue
Block a user