Skip to content

[Bug]: Mobile leaves non-ASCII inline-code file paths untappable #35525

Closed

Description

@coygeek

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/mobile

Steps to reproduce

From the repository root of a checkout of the commit under Version or commit with dependencies installed, save the script below as repro.sh outside the checkout and run bash /path/to/repro.sh .. It writes temporary focused test files into apps/mobile/src/lib/ and apps/web/src/, runs them with env -u NODE_ENV and a private HOME, TMPDIR and T3CODE_HOME, prints the recorded results, and removes its files on exit. It asserts the current defect together with its controls (ASCII links on mobile; both workarounds link on mobile; web resolves every case), so exit 0 means the defect reproduced.

Reproduction script: repro.sh
#!/usr/bin/env bash
set -euo pipefail
checkout=$(cd "${1:-.}" && pwd)
tmp=$(mktemp -d)
fixture="$checkout/apps/mobile/src/lib/$(basename "$tmp").repro.test.ts"
web_fixture="$checkout/apps/web/src/$(basename "$tmp").repro.test.ts"
trap 'rm -f "$fixture" "$web_fixture"; rm -rf "$tmp"' EXIT
mkdir -p "$tmp/user-home" "$tmp/temp" "$tmp/t3-home"
cat > "$tmp/fixture.test.ts" <<'TEST'
import { expect, it } from "vite-plus/test";
import { appendFileSync } from "node:fs";
import { ENRICHED_INLINE_FILE_LINK_REGEX } from "./enrichedLinkPresentation";
import { markdownForNativeRendering, parseMarkdownDocument, markdownWithInlineFileLinks, markdownForNativeParsing, markdownWithFileEmbeds, markdownWithTableBreaks } from "./markdownDocument";
import { inlineCodeFilePathCandidate, parseMarkdownFileLink } from "@t3tools/shared/markdownLinks";
import { parseMarkdown } from "react-native-enriched-markdown/src/web/parseMarkdown";

it("reproduces mobile Unicode inline-code link loss beside controls and the web resolver", async () => {
  const cases = [
    ["ASCII", "src/math.ts:2", true],
    ["CJK", "src/數學.ts:2", false],
    ["CJK_FOLDER", "數學/math.ts:2", false],
    ["CJK_BASENAME", "數學.ts:2", false],
    ["CJK_RANGE", "src/數學.ts:2-4", false],
    ["PREFIX_WORKAROUND", "./src/數學.ts:2", true],
  ] as const;
  for (const [label, path, linked] of cases) {
    const input = `See \`${path}\` now`;
    const mobile = markdownForNativeRendering(input, ENRICHED_INLINE_FILE_LINK_REGEX);
    const threadBranch = markdownWithTableBreaks(markdownWithFileEmbeds(markdownWithInlineFileLinks(markdownForNativeParsing(input), ENRICHED_INLINE_FILE_LINK_REGEX), () => null).markdown);
    expect(threadBranch).toBe(mobile);
    const candidate = inlineCodeFilePathCandidate(path);
    const shared = parseMarkdownFileLink(path);
    const ast = await parseMarkdown(mobile);
    const urls: string[] = [];
    const types: string[] = [];
    const collect = (node: { type?: string; attributes?: { url?: string }; children?: unknown[] }) => {
      if (node.type) types.push(node.type);
      if (node.attributes?.url) urls.push(node.attributes.url);
      node.children?.forEach((child) => collect(child as typeof node));
    };
    collect(ast);
    const sourceAst = parseMarkdownDocument(input);
    const code = sourceAst.children[0];
    expect(code?.type).toBe("paragraph");
    if (code?.type === "paragraph") expect(code.children[1]).toMatchObject({ type: "inlineCode", value: path });
    appendFileSync(process.env.REPRO_RESULTS! + "/mobile.jsonl", JSON.stringify({ label, input, mobile, threadBranch, regex: ENRICHED_INLINE_FILE_LINK_REGEX.test(path), candidate, shared, nativeUrls: urls, nativeTypes: types }) + "\n");
    expect(candidate).toBe(path);
    expect(shared).not.toBeNull();
    expect(mobile).toBe(linked ? `See [${path}](<${path}>) now` : input);
    expect(urls).toEqual(linked ? [path] : []);
  }
  const input = "See [src/數學.ts:2](<src/數學.ts:2>) now";
  const mobile = markdownForNativeRendering(input, ENRICHED_INLINE_FILE_LINK_REGEX);
  const ast = await parseMarkdown(mobile);
  appendFileSync(process.env.REPRO_RESULTS! + "/mobile.jsonl", "EXPLICIT_LINK_WORKAROUND " + mobile + " " + JSON.stringify(ast) + "\n");
  expect(mobile).toBe(input);
  expect(JSON.stringify(ast)).toContain('"url":"src/數學.ts:2"');
  appendFileSync(process.env.REPRO_RESULTS! + "/mobile.jsonl", "DEFECT_PRESENT: ASCII is linked; CJK remains code with no native link URL; both workarounds link.\n");
});
TEST
cp "$tmp/fixture.test.ts" "$fixture"
cat > "$web_fixture" <<'WEBTEST'
import { expect, it } from "vite-plus/test";
import { appendFileSync } from "node:fs";
import { extractInlineCodeSpans } from "@t3tools/shared/markdownLinks";
import { resolveInlineCodeFileLinkMeta } from "./markdown-links";
it("web resolves the same Markdown's ASCII and Unicode inline-code paths", () => {
  for (const path of ["src/math.ts:2", "src/數學.ts:2", "數學/math.ts:2", "數學.ts:2", "src/數學.ts:2-4", "./src/數學.ts:2"]) {
    const markdown = `See \`${path}\` now`;
    const spans = extractInlineCodeSpans(markdown);
    expect(spans).toEqual([path]);
    const web = resolveInlineCodeFileLinkMeta(spans[0]!, process.env.REPRO_PROJECT!);
    appendFileSync(process.env.REPRO_RESULTS! + "/web.jsonl", "WEB " + JSON.stringify({ markdown, workspaceRelativePath: web?.workspaceRelativePath, line: web?.line, endLine: web?.endLine }) + "\n");
    expect(web).not.toBeNull();
    expect(web?.line).toBe(2);
    expect(web?.workspaceRelativePath).toBe(path.replace(/:2(?:-4)?$/, ""));
  }
});
WEBTEST
cd "$checkout"
status=0
env -u NODE_ENV REPRO_RESULTS="$tmp" HOME="$tmp/user-home" TMPDIR="$tmp/temp" T3CODE_HOME="$tmp/t3-home" REPRO_PROJECT="$tmp/project" PATH="$checkout/node_modules/.bin:$PATH" vp test run "${fixture#"$checkout/"}" "${web_fixture#"$checkout/"}" || status=$?
cat "$tmp/mobile.jsonl" "$tmp/web.jsonl"
exit "$status"

In the app: have an assistant reply contain See `src/數學.ts:2` now next to See `src/math.ts:2` now and view it in the mobile client. The script calls the exported markdownForNativeRendering with that Markdown and the same ENRICHED_INLINE_FILE_LINK_REGEX that MobileEnrichedMarkdownText passes, and also composes the alternate file-embed preprocessing branch used for thread messages.

Expected behavior

A valid relative file path such as src/數學.ts:2 gets the same tappable file-link behavior as src/math.ts:2. The mobile inline-code regression test establishes inline-code file linking for src/math.ts:2, and the shared path patterns explicitly accept names in any script. For this input, the shared inlineCodeFilePathCandidate returns src/數學.ts:2 and parseMarkdownFileLink returns {"path":"src/數學.ts","line":2}. The web resolver resolveInlineCodeFileLinkMeta, given the same Markdown and a workspace root, returns file metadata for both the ASCII and non-ASCII names, and ChatMarkdown returns a file chip when that metadata exists.

Actual behavior

On mobile, an assistant message that mentions a relative file path in inline code, such as `src/math.ts:2`, gets a file pill and a tap target that opens the file. The same reference with a non-ASCII name, such as `src/數學.ts:2`, stays plain inline code with no link. Unicode folder names (數學/math.ts:2), a Unicode bare filename with a line (數學.ts:2) and a line range (src/數學.ts:2-4) are affected the same way. The shared file-link parser and the web resolver accept all of these inputs, and web returns file metadata for them. The filename text is preserved; only the link is missing. Native device pixels and taps were not captured: the result comes from the production preprocessing functions and the mobile app's pinned Enriched Markdown md4c parser.

The script exited 0 on the commit under Version or commit and left git status --porcelain empty. Through production markdownForNativeRendering, See `src/math.ts:2` now becomes See [src/math.ts:2](<src/math.ts:2>) now, and the md4c parser produces a Link node with URL src/math.ts:2. See `src/數學.ts:2` now is returned unchanged, and the parser produces a Code node with no link URL. Both mobile preprocessing branches give the same result for every case. ./src/數學.ts:2 and the explicit link [src/數學.ts:2](<src/數學.ts:2>) both produce Link nodes.

ASCII              mobile="See [src/math.ts:2](<src/math.ts:2>) now"   native Link  url=src/math.ts:2
CJK                mobile="See `src/數學.ts:2` now"                    native Code  no url
CJK_FOLDER         mobile="See `數學/math.ts:2` now"                   native Code  no url
CJK_BASENAME       mobile="See `數學.ts:2` now"                        native Code  no url
CJK_RANGE          mobile="See `src/數學.ts:2-4` now"                  native Code  no url
PREFIX_WORKAROUND  mobile="See [./src/數學.ts:2](<./src/數學.ts:2>) now"  native Link  url=./src/數學.ts:2
WEB                every case above resolves to workspaceRelativePath + line 2 (range keeps endLine 4)

The component passes the preprocessed Markdown and its link press handler to Enriched; its extra text-link recognition covers skills and models, with no inline-code file fallback. The mobile user therefore sees plain inline code with the filename intact instead of a file pill. This is a classification failure before any file is opened, not a failure to load the file after a tap.

Evidence

  • Expected source: apps/mobile/src/lib/markdownInlineFileLinks.test.ts lines 93-106 (inline-code file linking), packages/shared/src/markdownLinks.ts lines 17-25 (path patterns that accept names in any script) and lines 178-217 (inlineCodeFilePathCandidate), and the web resolver output in the script's WEB lines, which apps/web/src/components/ChatMarkdown.tsx lines 1393-1408 turns into a file chip.
  • Failure source: the script output above and under Logs or stack traces; markdownForNativeRendering returns the non-ASCII inputs unchanged and the md4c parser yields Code with no URL. In source, the two relative-path branches of ENRICHED_INLINE_FILE_LINK_REGEX allow only [A-Za-z0-9._-], and the precheck and per-node guard in markdownDocument.ts require that regex before the Unicode-aware shared parser is consulted. This locator is a hypothesis about the cause, not a prescribed fix.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The web comparison runs the production resolver and reads its rendering branch; no browser or Electron navigation was exercised. iOS and Android device pixels and taps were not captured; the missing tap target is derived from the parsed Code node, the component's link handling, and the pinned iOS CodeRenderer.m, which styles Code without a link attribute.

Related

Closed issue #16096 and merged #25802 (1a4f3c4a3c) fixed shared parsing and mobile Markdown link destinations for non-ASCII paths; the #25802 description explicitly lists the mobile ENRICHED_INLINE_FILE_LINK_REGEX inline-code chips as an excluded follow-up. #21176 included a change to this regex but was closed without merging. Open #29657, #28523, #29953 and #31037 touch chip labels, prose link labels, line ranges and streaming conversion, and none of them changes this regex.

Restoration check

With the script's inputs, src/數學.ts:2, 數學/math.ts:2, 數學.ts:2 and src/數學.ts:2-4 produce Markdown links in both mobile preprocessing branches and Link nodes whose URL is the original path, as src/math.ts:2 does, so the mobile client shows a file pill that opens the file at the given line. The script's CJK* assertions encode the current defect and fail once it is fixed. Controls keep passing: src/math.ts:2, ./src/數學.ts:2 and the explicit Markdown link still link, inline code that is not a file path (commands, identifiers, URLs, as pinned by markdownInlineFileLinks.test.ts) stays code, and the web resolver results are unchanged.

Impact

Minor bug or occasional failure

Version or commit

d9805b2, rerun from a clean checkout. The same script, differing only in a temporary directory name, also passed its defect assertions at upstream main fc2b5de, where the mobile regex and both preprocessing paths are unchanged.

Environment

macOS on arm64, Node v26.12.1, Vite+ test runner v5.0.5, the mobile app's pinned react-native-enriched-markdown md4c WASM parser; no native device client, browser, provider turn or server launched.

Logs or stack traces

The script's output (test runner summary, then the recorded mobile and web results):

Test Files  2 passed (2)
Tests  2 passed (2)
{"label":"ASCII","input":"See `src/math.ts:2` now","mobile":"See [src/math.ts:2](<src/math.ts:2>) now","threadBranch":"See [src/math.ts:2](<src/math.ts:2>) now","regex":true,"candidate":"src/math.ts:2","shared":{"path":"src/math.ts","line":2},"nativeUrls":["src/math.ts:2"],"nativeTypes":["Document","Paragraph","Text","Link","Text","Text"]}
{"label":"CJK","input":"See `src/數學.ts:2` now","mobile":"See `src/數學.ts:2` now","threadBranch":"See `src/數學.ts:2` now","regex":false,"candidate":"src/數學.ts:2","shared":{"path":"src/數學.ts","line":2},"nativeUrls":[],"nativeTypes":["Document","Paragraph","Text","Code","Text","Text"]}
{"label":"CJK_FOLDER","input":"See `數學/math.ts:2` now","mobile":"See `數學/math.ts:2` now","threadBranch":"See `數學/math.ts:2` now","regex":false,"candidate":"數學/math.ts:2","shared":{"path":"數學/math.ts","line":2},"nativeUrls":[],"nativeTypes":["Document","Paragraph","Text","Code","Text","Text"]}
{"label":"CJK_BASENAME","input":"See `數學.ts:2` now","mobile":"See `數學.ts:2` now","threadBranch":"See `數學.ts:2` now","regex":false,"candidate":"數學.ts:2","shared":{"path":"數學.ts","line":2},"nativeUrls":[],"nativeTypes":["Document","Paragraph","Text","Code","Text","Text"]}
{"label":"CJK_RANGE","input":"See `src/數學.ts:2-4` now","mobile":"See `src/數學.ts:2-4` now","threadBranch":"See `src/數學.ts:2-4` now","regex":false,"candidate":"src/數學.ts:2-4","shared":{"path":"src/數學.ts","line":2,"endLine":4},"nativeUrls":[],"nativeTypes":["Document","Paragraph","Text","Code","Text","Text"]}
{"label":"PREFIX_WORKAROUND","input":"See `./src/數學.ts:2` now","mobile":"See [./src/數學.ts:2](<./src/數學.ts:2>) now","threadBranch":"See [./src/數學.ts:2](<./src/數學.ts:2>) now","regex":true,"candidate":"./src/數學.ts:2","shared":{"path":"./src/數學.ts","line":2},"nativeUrls":["./src/數學.ts:2"],"nativeTypes":["Document","Paragraph","Text","Link","Text","Text"]}
EXPLICIT_LINK_WORKAROUND See [src/數學.ts:2](<src/數學.ts:2>) now {"type":"Document","children":[{"type":"Paragraph","children":[{"type":"Text","content":"See "},{"type":"Link","attributes":{"url":"src/數學.ts:2"},"children":[{"type":"Text","content":"src/數學.ts:2"}]},{"type":"Text","content":" now"}]}]}
DEFECT_PRESENT: ASCII is linked; CJK remains code with no native link URL; both workarounds link.
WEB {"markdown":"See `src/math.ts:2` now","workspaceRelativePath":"src/math.ts","line":2}
WEB {"markdown":"See `src/數學.ts:2` now","workspaceRelativePath":"src/數學.ts","line":2}
WEB {"markdown":"See `數學/math.ts:2` now","workspaceRelativePath":"數學/math.ts","line":2}
WEB {"markdown":"See `數學.ts:2` now","workspaceRelativePath":"數學.ts","line":2}
WEB {"markdown":"See `src/數學.ts:2-4` now","workspaceRelativePath":"src/數學.ts","line":2,"endLine":4}
WEB {"markdown":"See `./src/數學.ts:2` now","workspaceRelativePath":"./src/數學.ts","line":2}

Screenshots, recordings, or supporting files

No response

Workaround

Write the path as `./src/數學.ts:2` or as the explicit Markdown link [src/數學.ts:2](<src/數學.ts:2>); both produce link nodes in the same reproduction. Existing messages keep the unlinked form unless their references are rewritten.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions