# OSC8 Terminal Links

**Source:** `src/terminal/hyperlink.ts` (inline rendering)  
**Category:** UI/UX Components - Terminal Rendering  
**Purpose:** Render clickable hyperlinks in terminal output using OSC8 escape sequences

## Overview

OSC8 (Operating System Command 8) is an ANSI escape sequence standard that enables clickable hyperlinks in terminal emulators. SophiaClaw uses OSC8 to render file paths, URLs, and error locations as clickable links that open in the user's default browser or IDE.

```
WITHOUT OSC8:
Error at /home/user/project/src/app.ts:42:15
User must: Copy path → Open editor → Navigate to file

WITH OSC8:
Error at src/app.ts:42:15  (clickable)
User: Click → Opens directly in editor at line 42
```

## OSC8 Specification

### Escape Sequence Format

```
OSC 8 ; params ; URI ST text ST

Where:
  OSC = \x1b] (Operating System Command)
  ST  = \x1b\\ (String Terminator)
  params = optional key=value pairs
  URI  = target URL/file path
  text = visible link text
```

### Binary Representation

```typescript
const OSC8_OPEN = "\x1b]8;";
const OSC8_CLOSE = "\x1b]8;;\x1b\\";

// Example: Link to file at line 42
const link =
  "\x1b]8;;file:///home/user/project/src/app.ts:42:15\x1b\\" +
  "src/app.ts:42:15" +
  "\x1b]8;;\x1b\\";
```

### URI Schemes

| Scheme                | Usage               | Opens In         |
| --------------------- | ------------------- | ---------------- |
| `https://`            | Web URLs            | Default browser  |
| `http://`             | Web URLs (insecure) | Default browser  |
| `file://`             | Local file paths    | IDE/file manager |
| `file://...:line:col` | File with position  | IDE at location  |

## Implementation

### Link Renderer

```typescript
interface HyperlinkOptions {
  id?: string; // Optional link ID for grouping
  underline?: boolean; // Force underline (default: auto)
}

function createHyperlink(text: string, uri: string, opts?: HyperlinkOptions): string {
  // Check if terminal supports OSC8
  if (!supportsOsc8()) {
    return text; // Fallback: plain text
  }

  const params = opts?.id ? `id=${opts.id}` : "";

  return "\x1b]8;" + params + ";" + uri + "\x1b\\\\" + text + "\x1b]8;;\x1b\\\\";
}

function supportsOsc8(): boolean {
  // Check COLORTERM for truecolor support (proxy for modern terminal)
  const colorTerm = process.env.COLORTERM ?? "";
  const term = process.env.TERM ?? "";

  // Modern terminals with OSC8 support
  const supported = [
    "xterm-kitty", // Kitty
    "xterm-ghostty", // Ghostty
    "xterm-wezterm", // WezTerm
    "xterm-alacritty", // Alacritty (recent versions)
    "vtcon", // Windows Terminal
  ];

  if (supported.some((t) => term.includes(t))) {
    return true;
  }

  // Check for COLORTERM=truecolor
  if (colorTerm === "truecolor" || colorTerm === "24bit") {
    return true;
  }

  // Conservative default: assume no support
  return false;
}
```

### File Path Links

```typescript
function createFileLink(filePath: string, line?: number, col?: number): string {
  // Build file:// URI with optional line:column
  const uri = line != null ? `file://${filePath}:${line}:${col ?? ""}` : `file://${filePath}`;

  // Display text: basename for long paths
  const displayText =
    line != null ? `${path.basename(filePath)}:${line}:${col ?? ""}` : path.basename(filePath);

  return createHyperlink(displayText, uri);
}

// Usage:
const errorLocation = createFileLink("/project/src/app.ts", 42, 15);
// Renders as: app.ts:42:15 (clickable in supported terminals)
```

### URL Links

```typescript
function createUrlLink(url: string, label?: string): string {
  const displayText = label ?? url;

  // Truncate long URLs for display
  const truncated = displayText.length > 50 ? displayText.slice(0, 47) + "..." : displayText;

  return createHyperlink(truncated, url);
}

// Usage:
const link = createUrlLink("https://docs.sophiaclaw.ai/gateway", "Docs");
// Renders as: Docs (clickable)
```

## Terminal Support Detection

### Feature Detection Strategy

```typescript
function detectTerminalCapabilities(): TerminalCaps {
  const caps: TerminalCaps = {
    osc8: false,
    truecolor: false,
    kitty: false,
    wezterm: false,
    iterm2: false,
  };

  const term = process.env.TERM ?? "";
  const termProgram = process.env.TERM_PROGRAM ?? "";
  const colorterm = process.env.COLORTERM ?? "";

  // Truecolor detection
  if (colorterm === "truecolor" || colorterm === "24bit") {
    caps.truecolor = true;
  }

  // Terminal-specific detection
  if (term.includes("kitty")) {
    caps.kitty = true;
    caps.osc8 = true;
  }

  if (term.includes("wezterm") || termProgram.includes("WezTerm")) {
    caps.wezterm = true;
    caps.osc8 = true;
  }

  if (termProgram.includes("iTerm")) {
    caps.iterm2 = true;
    caps.osc8 = true; // Recent versions
  }

  if (termProgram.includes("Hyper")) {
    caps.osc8 = true;
  }

  // Windows Terminal
  if (process.env.WT_SESSION) {
    caps.osc8 = true;
  }

  return caps;
}
```

### Known Terminal Support Matrix

| Terminal         | OSC8        | Truecolor | Detection Method       |
| ---------------- | ----------- | --------- | ---------------------- |
| Kitty            | ✅          | ✅        | `TERM=*kitty*`         |
| WezTerm          | ✅          | ✅        | `TERM_PROGRAM=WezTerm` |
| Ghostty          | ✅          | ✅        | `TERM=*ghostty*`       |
| Alacritty        | ✅ (recent) | ✅        | `TERM=*alacritty*`     |
| iTerm2           | ✅ (3.4+)   | ✅        | `TERM_PROGRAM=iTerm`   |
| Windows Terminal | ✅          | ✅        | `WT_SESSION` env       |
| Hyper            | ✅          | ✅        | `TERM_PROGRAM=Hyper`   |
| VSCode Terminal  | ✅          | ✅        | `TERM_PROGRAM=vscode`  |
| gnome-terminal   | ✅          | ✅        | `COLORTERM=truecolor`  |
| foot             | ✅          | ✅        | `TERM=foot*`           |
| rxvt/urxvt       | ❌          | ❌        | Legacy                 |
| xterm            | ❌          | ❌        | Legacy                 |

## Usage Patterns

### Error Messages

```typescript
function formatErrorLocation(error: LocationError): string {
  const { file, line, column, message } = error;

  const link = createFileLink(file, line, column);
  const formattedMsg = theme.error(`Error: ${message}`);

  return `${formattedMsg} at ${link}`;
}

// Output:
// Error: Cannot find module 'xyz' at app.ts:42:15
//                                          ^^^^^ (clickable)
```

### Tool Call Results

```typescript
function formatShellResult(result: ShellExecResult): string {
  const lines: string[] = [];

  // Working directory link
  lines.push(`Working directory: ${createFileLink(result.cwd)}`);

  // Output with file links detected
  const output = result.output
    .split("\n")
    .map((line) => {
      // Detect file:line:col patterns in output
      return line.replace(/(\S+):(\d+):(\d+)/g, (match, file, line, col) =>
        createFileLink(file, Number(line), Number(col)),
      );
    })
    .join("\n");

  lines.push(output);
  return lines.join("\n");
}
```

### Documentation Links

```typescript
function formatHelpWithLinks(docs: HelpDoc): string {
  const baseUrl = "https://docs.sophiaclaw.ai";

  return docs.sections
    .map((section) => {
      const sectionLink = createUrlLink(`${baseUrl}/${section.slug}`, section.title);
      return `\n${theme.bold(sectionLink)}\n${section.description}`;
    })
    .join("\n");
}
```

## Rendering Pipeline

### Integration with TableComponent

```typescript
class TableComponent {
  renderCell(value: CellValue): string {
    // URLs → OSC8 links
    if (typeof value === "string" && this.isUrl(value)) {
      return createUrlLink(value);
    }

    // File paths → file:// links
    if (typeof value === "string" && this.isFilePath(value)) {
      return createFileLink(value);
    }

    // Default rendering
    return String(value);
  }

  private isUrl(str: string): boolean {
    return /^https?:\/\//i.test(str);
  }

  private isFilePath(str: string): boolean {
    return str.startsWith("/") || str.startsWith("./") || /^[A-Z]:[/\\]/i.test(str);
  }
}
```

### Chaining with Other Formatting

```typescript
// OSC8 + Colors + Bold
const link =
  "\x1b[1m" + // Bold
  "\x1b[38;2;100;150;200m" + // RGB color
  createHyperlink("Click me", "https://example.com") +
  "\x1b[0m"; // Reset

// Order matters: OSC8 wraps text, colors apply to visible text
```

## Compatibility and Fallbacks

### Graceful Degradation

```typescript
function safeHyperlink(text: string, uri: string): string {
  if (!supportsOsc8()) {
    // Fallback: show URL in parentheses after text
    return `${text} (${uri})`;
  }

  return createHyperlink(text, uri);
}

// Output in unsupported terminals:
// Docs (https://docs.sophiaclaw.ai)
```

### User Override

```typescript
// CLI flag to disable OSC8
if (process.env.SOPHIACLAW_OSC8 === "0") {
  // Force disable
  globalThis.osc8Enabled = false;
}

// Force enable for testing
if (process.env.SOPHIACLAW_OSC8 === "1") {
  globalThis.osc8Enabled = true;
}
```

## Testing Strategy

### Unit Tests

```typescript
describe("createHyperlink", () => {
  it("generates correct OSC8 sequence", () => {
    const link = createHyperlink("text", "https://example.com");
    expect(link).toBe("\x1b]8;;https://example.com\x1b\\\\text\x1b]8;;\x1b\\\\");
  });

  it("includes link ID when provided", () => {
    const link = createHyperlink("text", "https://example.com", { id: "foo" });
    expect(link).toContain("id=foo");
  });

  it("returns plain text when OSC8 unsupported", () => {
    vi.stubEnv("TERM", "xterm");
    vi.stubEnv("COLORTERM", "");

    const link = createHyperlink("text", "https://example.com");
    expect(link).toBe("text");
  });
});
```

### Visual Tests

```bash
# Test OSC8 support in current terminal
echo -e '\x1b]8;;https://example.com\x1b\\\\Click Me\x1b]8;;\x1b\\\\'

# Expected: "Click Me" is clickable and opens example.com
```

## Security Considerations

### Link Sanitization

```typescript
function sanitizeUrl(url: string): string | null {
  try {
    const parsed = new URL(url);

    // Only allow http/https/file schemes
    if (!["http:", "https:", "file:"].includes(parsed.protocol)) {
      return null;
    }

    // Block javascript: URLs (XSS risk)
    if (parsed.protocol === "javascript:") {
      return null;
    }

    // Block data: URLs (potential phishing)
    if (parsed.protocol === "data:") {
      return null;
    }

    return url;
  } catch {
    return null; // Invalid URL
  }
}
```

### File Path Validation

```typescript
function sanitizeFilePath(filePath: string): string | null {
  // Resolve to absolute path
  const resolved = path.resolve(filePath);

  // Block paths outside workspace (optional policy)
  if (workspaceRoot && !resolved.startsWith(workspaceRoot)) {
    return null;
  }

  // Normalize path separators
  return path.normalize(resolved);
}
```

## Performance

### Overhead Analysis

```
OSC8 sequence overhead per link:
  Open sequence:  ~8 bytes + URI length
  Close sequence: ~6 bytes
  Total: ~14 + URI length bytes

Example: 100 links in output
  - Average URI: 50 bytes
  - Overhead: 100 × (14 + 50) = 6,400 bytes
  - Impact: Negligible on modern terminals (<10ms render time)
```

### String Concatenation Optimization

```typescript
// BAD: Multiple string concatenations
const link = "\x1b]8;" + params + ";" + uri + "\x1b\\\\" + text + "\x1b]8;;\x1b\\\\";

// BETTER: Template literals
const link = `\x1b]8;${params};${uri}\x1b\\\\${text}\x1b]8;;\x1b\\\\`;

// BEST: Pre-computed sequences for static links
const STATIC_LINK_CACHE = new Map<string, string>();
```

## Troubleshooting

### Common Issues

**Problem: Links not clickable**

```bash
# Check terminal support
echo $TERM
echo $COLORTERM

# Test OSC8 directly
echo -e '\x1b]8;;https://example.com\x1b\\\\Test Link\x1b]8;;\x1b\\\\'

# If not clickable: terminal doesn't support OSC8
```

**Problem: Links show escape codes**

```typescript
// Cause: Terminal is escaping output
// Solution: Use raw output mode

// Node.js: process.stdout.write() not console.log()
process.stdout.write(link); // ✅
console.log(link); // ❌ (may escape)
```

**Problem: File links don't open in editor**

```typescript
// Cause: URI format incorrect
// Solution: Include line:column in file:// URI

// Wrong:
file:///path/to/file.ts

// Correct:
file:///path/to/file.ts:42:15
//                         ^^^^ Line and column
```

## Related Documentation

- [Terminal Output](/docs/cli/terminal-output.md)
- [Table Rendering](/docs/cli/table-rendering.md)
- [ANSI Color Palette](/docs/cli/ansi-colors.md)

## References

- [OSC8 Specification](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
- [Terminal Colors Reference](https://github.com/termstandard/colors)
- [VT100 Escape Sequences](https://vt100.net/docs/vt100-ug/chapter3.html)
