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 {...@@ -43,7 +43,7 @@ import {
43} from "~/terminal/ghostty/surface";43} from "~/terminal/ghostty/surface";
44import { type GhosttyColor, type GhosttyTheme } from "~/terminal/ghostty/core";44import { type GhosttyColor, type GhosttyTheme } from "~/terminal/ghostty/core";
45import { useOpenInPreferredEditor } from "../editorPreferences";45import { useOpenInPreferredEditor } from "../editorPreferences";
46import { isTerminalLinkActivation, resolvePathLinkTarget } from "../terminal-links";46import { isTerminalLinkActivation, isTerminalUrl, resolvePathLinkTarget } from "../terminal-links";
47import {47import {
48 isDiffToggleShortcut,48 isDiffToggleShortcut,
49 isTerminalClearShortcut,49 isTerminalClearShortcut,
...@@ -750,7 +750,7 @@ export function TerminalViewport({...@@ -750,7 +750,7 @@ export function TerminalViewport({
750 if (!isTerminalLinkActivation(event)) return;750 if (!isTerminalLinkActivation(event)) return;
751 const latestTerminal = terminalRef.current;751 const latestTerminal = terminalRef.current;
752 if (!latestTerminal) return;752 if (!latestTerminal) return;
753 if (/^https?:\/\//u.test(text)) {753 if (isTerminalUrl(text)) {
754 if (!localApi) {754 if (!localApi) {
755 writeSystemMessage(latestTerminal, "Opening links is unavailable in this browser.");755 writeSystemMessage(latestTerminal, "Opening links is unavailable in this browser.");
756 return;756 return;
apps/web/src/terminal-links.test.ts+19
...@@ -4,6 +4,7 @@ import {...@@ -4,6 +4,7 @@ import {
4 collectWrappedTerminalLinkLine,4 collectWrappedTerminalLinkLine,
5 extractTerminalLinks,5 extractTerminalLinks,
6 isTerminalLinkActivation,6 isTerminalLinkActivation,
7 isTerminalUrl,
7 resolvePathLinkTarget,8 resolvePathLinkTarget,
8 resolveWrappedTerminalLinkRange,9 resolveWrappedTerminalLinkRange,
9 wrappedTerminalLinkRangeIntersectsBufferLine,10 wrappedTerminalLinkRangeIntersectsBufferLine,
...@@ -37,6 +38,24 @@ describe("extractTerminalLinks", () => {...@@ -37,6 +38,24 @@ describe("extractTerminalLinks", () => {
37 ]);38 ]);
38 });39 });
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
40 it("trims trailing punctuation from links", () => {59 it("trims trailing punctuation from links", () => {
41 const line = "(https://example.com/docs), ./src/main.ts:12.";60 const line = "(https://example.com/docs), ./src/main.ts:12.";
42 expect(extractTerminalLinks(line)).toEqual([61 expect(extractTerminalLinks(line)).toEqual([
apps/web/src/terminal-links.ts+6-2
...@@ -36,7 +36,7 @@ export interface WrappedTerminalLinkLine {...@@ -36,7 +36,7 @@ export interface WrappedTerminalLinkLine {
36 segments: ReadonlyArray<WrappedTerminalLinkLineSegment>;36 segments: ReadonlyArray<WrappedTerminalLinkLineSegment>;
37}37}
3838
39const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/g;39const URL_PATTERN = /https?:\/\/[^\s"'`<>]+/giu;
40const FILE_PATH_PATTERN =40const FILE_PATH_PATTERN =
41 /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g;41 /(?:~\/|\.{1,2}\/|\/|[A-Za-z]:[\\/]|\\\\)[^\s"'`<>]+|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}/g;
42const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/;42const TRAILING_PUNCTUATION_PATTERN = /[.,;!?]+$/;
...@@ -80,7 +80,7 @@ function collectMatches(...@@ -80,7 +80,7 @@ function collectMatches(
8080
81 const trimmed = trimClosingDelimiters(raw);81 const trimmed = trimClosingDelimiters(raw);
82 if (trimmed.length === 0) continue;82 if (trimmed.length === 0) continue;
83 if (kind === "path" && /^https?:\/\//i.test(trimmed)) continue;83 if (kind === "path" && isTerminalUrl(trimmed)) continue;
8484
85 const candidate: TerminalLinkMatch = {85 const candidate: TerminalLinkMatch = {
86 kind,86 kind,
...@@ -172,6 +172,10 @@ export function extractTerminalLinks(line: string): TerminalLinkMatch[] {...@@ -172,6 +172,10 @@ export function extractTerminalLinks(line: string): TerminalLinkMatch[] {
172 return [...urlMatches, ...pathMatches].toSorted((a, b) => a.start - b.start);172 return [...urlMatches, ...pathMatches].toSorted((a, b) => a.start - b.start);
173}173}
174174
175export function isTerminalUrl(value: string): boolean {
176 return /^https?:\/\//iu.test(value);
177}
178
175export function collectWrappedTerminalLinkLine(179export function collectWrappedTerminalLinkLine(
176 bufferLineNumber: number,180 bufferLineNumber: number,
177 getLine: (bufferLineIndex: number) => TerminalBufferLineLike | null | undefined,181 getLine: (bufferLineIndex: number) => TerminalBufferLineLike | null | undefined,
apps/web/src/terminal/ghostty/surface.test.ts-8
...@@ -20,7 +20,6 @@ import {...@@ -20,7 +20,6 @@ import {
20 resolveTerminalMouseTrackingState,20 resolveTerminalMouseTrackingState,
21 shouldBlinkTerminalCursor,21 shouldBlinkTerminalCursor,
22 shouldReportTerminalMouse,22 shouldReportTerminalMouse,
23 shouldShowTerminalLinkHover,
24 terminalGridCellAt,23 terminalGridCellAt,
25 terminalScrollbarGeometry,24 terminalScrollbarGeometry,
26 terminalScrollbarOffsetAtPointer,25 terminalScrollbarOffsetAtPointer,
...@@ -438,13 +437,6 @@ describe("application mouse reporting", () => {...@@ -438,13 +437,6 @@ describe("application mouse reporting", () => {
438 motionData: "\u001b[<35;8;4M",437 motionData: "\u001b[<35;8;4M",
439 });438 });
440 });439 });
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 });
448});440});
449441
450describe("terminal font resolution", () => {442describe("terminal font resolution", () => {
apps/web/src/terminal/ghostty/surface.ts+1-11
...@@ -483,13 +483,6 @@ export function isTerminalLinkPointerGesture(...@@ -483,13 +483,6 @@ export function isTerminalLinkPointerGesture(
483 : event.ctrlKey && !event.metaKey;483 : event.ctrlKey && !event.metaKey;
484}484}
485485
486export function shouldShowTerminalLinkHover(
487 mouseTracking: boolean,
488 linkModifierActive: boolean,
489): boolean {
490 return !mouseTracking || linkModifierActive;
491}
492
493export function ghosttyMouseButton(button: number): number | null {486export function ghosttyMouseButton(button: number): number | null {
494 switch (button) {487 switch (button) {
495 case 0:488 case 0:
...@@ -1409,10 +1402,7 @@ export class GhosttyTerminalSurface {...@@ -1409,10 +1402,7 @@ export class GhosttyTerminalSurface {
14091402
1410 private refreshHoveredLink(): void {1403 private refreshHoveredLink(): void {
1411 const pointer = this.hoverPointer;1404 const pointer = this.hoverPointer;
1412 const link =1405 const link = pointer && this.linkModifierActive ? this.linkAt(pointer.x, pointer.y) : null;
1413 pointer && shouldShowTerminalLinkHover(this.core.isMouseTracking(), this.linkModifierActive)
1414 ? this.linkAt(pointer.x, pointer.y)
1415 : null;
1416 this.setHoveredLink(link);1406 this.setHoveredLink(link);
1417 }1407 }
14181408