feat(links): clickable URLs + file:line paths in the terminal (W1)
Terminal output URLs and file paths (e.g. src/app.ts:42) are now tappable — the
walk-away device is a phone, so this removes soft-keyboard copy gymnastics.
- public/link-paths.ts (new, pure): findPathMatches — links tokens with a '/',
a :line suffix, or a code extension (src/app.ts:42, /abs/main.rs:10, README.md)
while rejecting example.com / v1.2.3 / 12:34 / URL tails.
- terminal-session.ts: hardened URL handler (scheme allowlist http/https/mailto +
window.open noopener,noreferrer, blocks javascript:/data:/file:) replacing the
addon default; a path link provider → openPath() POSTs {file,line} to
/open-in-editor (resolves rel paths against the OSC-7 cwd; in-flight guard).
- src/http/editor.ts: additive openFileInEditor + isGotoEditor (--goto file:line
only for goto-capable editors; execFile, no shell). openInEditor untouched.
- src/server.ts: /open-in-editor branches on body.file vs body.path (same CSRF guard).
Deviation from the "no server change" brief: an additive openFileInEditor was
required because the existing route only opens directories, not file:line (Option A
in docs/plans/w1-clickable-links.md). Backward-compatible; src/types.ts untouched.
Verified independently: typecheck + build:web clean, 1661 tests pass (link-paths
95%+ cov). Note: xterm buffer-row indexing (getLine(n-1)/range.y=n) is asserted in
jsdom but merits a real-browser smoke check.
This commit is contained in:
@@ -37,7 +37,7 @@ import { isOriginAllowed } from './http/origin.js'
|
||||
import { parseHookEvent } from './http/hook.js'
|
||||
import { listSessions } from './http/history.js'
|
||||
import { buildProjects, buildProjectDetail } from './http/projects.js'
|
||||
import { openInEditor } from './http/editor.js'
|
||||
import { openInEditor, openFileInEditor } from './http/editor.js'
|
||||
import { getDiff } from './http/diff.js'
|
||||
import { parseStatusLine } from './http/statusline.js'
|
||||
import { createWorktree } from './http/worktrees.js'
|
||||
@@ -380,11 +380,18 @@ export function startServer(cfg: Config): { close(): Promise<void> } {
|
||||
// Open a project in the host's desktop editor (v0.6 Projects panel — VS Code
|
||||
// logo). State-changing (spawns a GUI process), so it carries the same Origin
|
||||
// guard as the DELETE routes. The path is validated + passed via execFile (no
|
||||
// shell) in openInEditor.
|
||||
// shell) in openInEditor / openFileInEditor.
|
||||
//
|
||||
// W1: two modes on one route. `file` present ⇒ open that FILE at `line` (a
|
||||
// clicked terminal path like `src/app.ts:42`); else the original directory mode
|
||||
// (`path`). Discriminated by body shape so the Projects panel is unchanged.
|
||||
app.post('/open-in-editor', express.json({ limit: '4kb' }), async (req, res) => {
|
||||
if (!requireAllowedOrigin(req, res)) return
|
||||
const body = (req.body ?? {}) as Record<string, unknown>
|
||||
const result = await openInEditor(cfg, body['path'])
|
||||
const result =
|
||||
body['file'] !== undefined
|
||||
? await openFileInEditor(cfg, body['file'], body['line'])
|
||||
: await openInEditor(cfg, body['path'])
|
||||
if (result.ok) {
|
||||
res.status(204).end()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user