Files
orca/mobile/src/components/markdown-inline-token-rules.ts
T
Brennan Benson 9ee359550b fix(mobile): make native-chat file links and path citations tappable (STA-3331) (#12364)
* fix(mobile): make native-chat file links and path citations tappable (STA-3331)

- Linkify POSIX absolute paths in chat prose (leading-/ regex alternative;
  URL guard now keys off the char before the matched slash)
- Parse agent-style path:line(:col) citations in prose, code spans, and the
  open flow; line/column ride into the mobile file preview route
- Route non-web markdown hrefs (file: URIs, relative/absolute paths) to the
  file opener instead of silently dropping them; unknown schemes stay dead
- Resolve chat paths against the worktree root, not the terminal's live cwd
- Reuse the terminal tap-to-open flow for chat taps (haptic, preview route,
  tab activation with retries) via a shared identity-stable hook, and toast
  on misses instead of silent no-ops
- Keep snake_case paths whole (intraword underscores are literal text),
  scan bold/italic/strike spans for paths, split trailing punctuation off
  autolinks, and let taps land while the composer keyboard is up

* fix(mobile): harden chat file tap handling

* refactor(chat): share native chat href routing

* fix(mobile): detect files directly under path roots

* fix(mobile): keep inline tokens and dunder paths intact around emphasis

Review follow-ups on the chat file-link work:

- A rejected intraword `_` token left the scan index past its closing
  underscore, so every inline token between two snake_case words was
  swallowed and rendered as literal source — including markdown links,
  which became untappable. Rescan from just past the opening delimiter.
- Treat a path separator as an intraword flank so `src/__init__.py` and
  `a/__tests__/x.ts` stay whole; previously they rendered as bold plus a
  remnant that the new absolute-root pattern turned into a tap on `/x.ts`.
- Bound the `:line(:col)` tail so `src/app.ts:1e3` and `:80%` no longer
  parse a line number, while a cited range still opens its first line.
- Route chat tap failures through the composer banner (toast fallback):
  chat taps happen with the keyboard up, which covers the toast.
- Drop the tap-handler mirror's dep list; the call site rebuilds its
  accessors every render, so it could never skip on a route that
  rerenders per keystroke.

* Revert "fix(mobile): keep inline tokens and dunder paths intact around emphasis"

This reverts commit 308bfaf22b.

* fix(mobile): preserve chat file-link parsing and feedback
2026-08-04 15:46:08 -07:00

56 lines
1.7 KiB
TypeScript

// Post-checks for inline markdown tokens that a single-pass tokenizer regex
// cannot express on its own.
const INTRAWORD_FLANK_PATTERN = /[\w\\/]/
/**
* True when a `_…_` / `__…__` token sits inside a word (snake_case, dunder
* tails). CommonMark treats intraword underscores as literal text; path
* separators count as flanks so dunder path segments also stay whole.
*/
export function isIntrawordUnderscoreToken(text: string, index: number, token: string): boolean {
if (!token.startsWith('_')) {
return false
}
const prev = index > 0 ? text[index - 1]! : ''
const next = text[index + token.length] ?? ''
return INTRAWORD_FLANK_PATTERN.test(prev) || INTRAWORD_FLANK_PATTERN.test(next)
}
/**
* Split sentence punctuation off an autolinked URL tail ("see https://x.com/a."),
* keeping a trailing ')' only when the URL itself opened a paren.
*/
export function trimAutolinkTrailingPunctuation(url: string): { url: string; trailing: string } {
let end = url.length
let parenthesisCountsReady = false
let openParentheses = 0
let closeParentheses = 0
while (end > 0) {
const char = url[end - 1]!
if ('.,;:!?'.includes(char)) {
end--
continue
}
if (char === ')') {
if (!parenthesisCountsReady) {
for (let index = 0; index < end; index++) {
if (url[index] === '(') {
openParentheses++
} else if (url[index] === ')') {
closeParentheses++
}
}
parenthesisCountsReady = true
}
if (closeParentheses > openParentheses) {
end--
closeParentheses--
continue
}
}
break
}
return { url: url.slice(0, end), trailing: url.slice(end) }
}