authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-08-24 14:32:40-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-24 17:32:40-04:00
log883e1a3cd9d70f6906a8eb1458a7a05d1d9e0d55
treeae7f4b3df0c2734646b121ae85e1d04408edf974
parent8287f2c3a771f4a88f781e9c98830692b186c5c3
signature Signed by PGP key B5690EEEBB952194

fix(web): make terminal links appear clickable only when clickable (#7488)


5 files changed, 28 insertions(+), 23 deletions(-)

apps/web/src/components/ThreadTerminalDrawer.tsx+2-2
......@@ -43,7 +43,7 @@ import {
4343} from "~/terminal/ghostty/surface";
4444import { type GhosttyColor, type GhosttyTheme } from "~/terminal/ghostty/core";
4545import { useOpenInPreferredEditor } from "../editorPreferences";
46import { isTerminalLinkActivation, resolvePathLinkTarget } from "../terminal-links";
46import { isTerminalLinkActivation, isTerminalUrl, resolvePathLinkTarget } from "../terminal-links";
4747import {
4848 isDiffToggleShortcut,
4949 isTerminalClearShortcut,
......@@ -750,7 +750,7 @@ export function TerminalViewport({
750750 if (!isTerminalLinkActivation(event)) return;
751751 const latestTerminal = terminalRef.current;
752752 if (!latestTerminal) return;
753 if (/^https?:\/\//u.test(text)) {
753 if (isTerminalUrl(text)) {
754754 if (!localApi) {
755755 writeSystemMessage(latestTerminal, "Opening links is unavailable in this browser.");
756756 return;
apps/web/src/terminal-links.test.ts+19
......@@ -4,6 +4,7 @@ import {
44 collectWrappedTerminalLinkLine,
55 extractTerminalLinks,
66 isTerminalLinkActivation,
7 isTerminalUrl,
78 resolvePathLinkTarget,
89 resolveWrappedTerminalLinkRange,
910 wrappedTerminalLinkRangeIntersectsBufferLine,
......@@ -37,6 +38,24 @@ describe("extractTerminalLinks", () => {
3738 ]);
3839 });
3940
41 it("classifies uppercase schemes as URLs at activation time too", () => {
42 expect(isTerminalUrl("HTTPS://example.com/docs")).toBe(true);
43 expect(isTerminalUrl("Http://example.com")).toBe(true);
44 expect(isTerminalUrl("src/components/main.ts")).toBe(false);
45 expect(isTerminalUrl("httpsdocs/readme.md")).toBe(false);
46 });
47
48 it("finds URLs regardless of scheme casing", () => {
49 expect(extractTerminalLinks("open HTTPS://example.com/docs")).toEqual([
50 {
51 kind: "url",
52 text: "HTTPS://example.com/docs",
53 start: 5,
54 end: 29,
55 },
56 ]);
57 });
58
4059 it("trims trailing punctuation from links", () => {
4160 const line = "(https://example.com/docs), ./src/main.ts:12.";
4261 expect(extractTerminalLinks(line)).toEqual([
apps/web/src/terminal-links.ts+6-2
......@@ -36,7 +36,7 @@ export interface WrappedTerminalLinkLine {
3636 segments: ReadonlyArray<WrappedTerminalLinkLineSegment>;
3737}
3838
39const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/g;
39const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/giu;
4040const FILE_PATH_PATTERN =
4141 /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g;
4242const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/;
......@@ -80,7 +80,7 @@ function collectMatches(
8080
8181 const trimmed = trimClosingDelimiters(raw);
8282 if (trimmed.length === 0) continue;
83 if (kind === "path" && /^https?:\/\//i.test(trimmed)) continue;
83 if (kind === "path" && isTerminalUrl(trimmed)) continue;
8484
8585 const candidate: TerminalLinkMatch = {
8686 kind,
......@@ -172,6 +172,10 @@ export function extractTerminalLinks(line: string): TerminalLinkMatch[] {
172172 return [...urlMatches, ...pathMatches].toSorted((a, b) => a.start - b.start);
173173}
174174
175export function isTerminalUrl(value: string): boolean {
176 return /^https?:\/\//iu.test(value);
177}
178
175179export function collectWrappedTerminalLinkLine(
176180 bufferLineNumber: number,
177181 getLine: (bufferLineIndex: number) => TerminalBufferLineLike | null | undefined,
apps/web/src/terminal/ghostty/surface.test.ts-8
......@@ -20,7 +20,6 @@ import {
2020 resolveTerminalMouseTrackingState,
2121 shouldBlinkTerminalCursor,
2222 shouldReportTerminalMouse,
23 shouldShowTerminalLinkHover,
2423 terminalGridCellAt,
2524 terminalScrollbarGeometry,
2625 terminalScrollbarOffsetAtPointer,
......@@ -438,13 +437,6 @@ describe("application mouse reporting", () => {
438437 motionData: "\u001b[<35;8;4M",
439438 });
440439 });
441
442 it("only shows link hover during mouse tracking when the link modifier is held", () => {
443 expect(shouldShowTerminalLinkHover(false, false)).toBe(true);
444 expect(shouldShowTerminalLinkHover(false, true)).toBe(true);
445 expect(shouldShowTerminalLinkHover(true, false)).toBe(false);
446 expect(shouldShowTerminalLinkHover(true, true)).toBe(true);
447 });
448440});
449441
450442describe("terminal font resolution", () => {
apps/web/src/terminal/ghostty/surface.ts+1-11
......@@ -483,13 +483,6 @@ export function isTerminalLinkPointerGesture(
483483 : event.ctrlKey && !event.metaKey;
484484}
485485
486export function shouldShowTerminalLinkHover(
487 mouseTracking: boolean,
488 linkModifierActive: boolean,
489): boolean {
490 return !mouseTracking || linkModifierActive;
491}
492
493486export function ghosttyMouseButton(button: number): number | null {
494487 switch (button) {
495488 case 0:
......@@ -1409,10 +1402,7 @@ export class GhosttyTerminalSurface {
14091402
14101403 private refreshHoveredLink(): void {
14111404 const pointer = this.hoverPointer;
1412 const link =
1413 pointer && shouldShowTerminalLinkHover(this.core.isMouseTracking(), this.linkModifierActive)
1414 ? this.linkAt(pointer.x, pointer.y)
1415 : null;
1405 const link = pointer && this.linkModifierActive ? this.linkAt(pointer.x, pointer.y) : null;
14161406 this.setHoveredLink(link);
14171407 }
14181408