mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
feat(file-preview): open markdown clicked in the terminal in the rendered viewer
An in-workspace .md path clicked in the terminal went to the log viewer (tail -f, a "streaming" header over raw text), because md was kept out of FILE_PREVIEW_EXTENSIONS on purpose; every other surface already rendered it. Owner decision 2026-10-13: markdown is a document to read, so md and markdown join FILE_PREVIEW_EXTENSIONS and a click or tap opens the file preview, rendered by default with the MD pill for source. The main pane, every grid tile and the split's Pane B share the link provider, so all of them route the same way. Other in-workspace text (.log, .txt, .json, code) keeps the log viewer and its live follow. FILE_PATH_LINK_PATTERN now also links `.markdown` and `.htm`, which were not clickable at all. Tests: file-preview-markdown.test.ts now asserts md and markdown ARE routed to the preview (and plain text is not); terminal-touch-tap pins a tapped .md, .markdown and .html opening the preview while .log, .txt and .json open the log viewer, and a tile-registered provider routing the same way against the tile's own session. Docs: CLAUDE.md, the architecture invariants and the wiki (Working With Files, Mobile Guide). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0168444fda
commit
79b9181932
7 files changed
+68
-14
No files matched your search
@@ -305,7 +305,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
||||
|
||||
**File Viewer edit mode** (issue #212): the file-preview overlay edits workspace text files in place — `GET .../file-content?edit=1` + `PUT /api/sessions/:id/file-content`, policy in `src/config/file-editing.ts`. This is a **third file surface and the only one that WRITES**: read-path confinement (realpath + workspace + ownership) plus sensitive/blocked/`.git` denies and an extension **allowlist**; writes are `wx`-temp + rename (no `O_CREAT` anywhere = edit-in-place is structural); optimistic concurrency via sha256 `baseHash` → 409. ⚠️ `edit=1` never truncates and the client must never save a plain-preview buffer (the 500-line truncation would silently delete the rest). ⚠️ CRLF/UTF-8 guards: EOL re-applied server-side, non-UTF-8 refused via round-trip compare. → [architecture-invariants#file-viewer-edit-mode](docs/architecture-invariants.md#file-viewer-edit-mode), `docs/file-viewer-edit-plan.md`
|
||||
|
||||
**File Viewer text view: rendered markdown + Lines/Wrap toggles** (`_renderFilePreviewText()` in panels-ui.js): a `.md`/`.markdown` opens RENDERED by default with an `MD` pill back to source; the plain-text view has `Lines` (CSS-counter gutter) and `Wrap` toggles. ⚠️ ONE markdown pipeline: the viewer calls `_renderMarkdown(text, { breaks: false })` (marked + the DOMPurify allowlist, the Response Viewer's; chat keeps the default `breaks: true`, a file must not turn every hard wrap into a `<br>`) and binds the Response Viewer's click delegate (`_bindResponseViewerInteractions`) on the preview body for code-copy buttons and path links; never a second parser or handler. ⚠️ The document is built inside a `<template>` (a detached div with `innerHTML` set starts fetching every `<img src>` before the rewrite), then `_rebaseFilePreviewMarkdownRefs()` points relative images at the workspace-confined `file-raw` under the document's directory and root-relative ones under the workspace root (never a widened route; a failed load degrades to alt text), after `decodeURIComponent`ing the ref and dropping `?query`/`#fragment` (marked percent-encodes destinations, and the route encodes again), and turns workspace links into `a.rv-path` carrying `data-session-id` for the delegate (the linkifier's absolute paths get it too), stripping the `target` marked gave them. ⚠️ A preview opened by attachment id under a bare file name (attachment cards, history drawer: the registry keeps no relative path) has no directory, so its workspace refs degrade (images to alt text, links to their text), never resolve against the workspace root. ⚠️ The container carries `data-i18n-skip` or the translator rewrites the document's prose. ⚠️ Toggles are per-device localStorage keys (`codeman:filePreview*`), never `SettingsUpdateSchema`; Lines/Wrap are class flips on the ONE `<pre>`, with rules scoped `.file-preview-body > pre.file-preview-text` so they never leak into the document's code blocks. Markdown fetches `lines=10000` (the route ceiling), other text keeps 500. ⚠️ `md` stays OUT of `FILE_PREVIEW_EXTENSIONS`: only an IN-WORKSPACE `.md` path clicked in the TERMINAL keeps the tail viewer (live follow); the Files panel, chat paths, out-of-workspace terminal paths and attachment cards all reach `openFilePreview()` and render it. Tests: `test/file-preview-markdown.test.ts`. → [architecture-invariants#file-viewer-text-view-rendered-markdown-and-text-toggles](docs/architecture-invariants.md#file-viewer-text-view-rendered-markdown-and-text-toggles)
|
||||
**File Viewer text view: rendered markdown + Lines/Wrap toggles** (`_renderFilePreviewText()` in panels-ui.js): a `.md`/`.markdown` opens RENDERED by default with an `MD` pill back to source; the plain-text view has `Lines` (CSS-counter gutter) and `Wrap` toggles. ⚠️ ONE markdown pipeline: the viewer calls `_renderMarkdown(text, { breaks: false })` (marked + the DOMPurify allowlist, the Response Viewer's; chat keeps the default `breaks: true`, a file must not turn every hard wrap into a `<br>`) and binds the Response Viewer's click delegate (`_bindResponseViewerInteractions`) on the preview body for code-copy buttons and path links; never a second parser or handler. ⚠️ The document is built inside a `<template>` (a detached div with `innerHTML` set starts fetching every `<img src>` before the rewrite), then `_rebaseFilePreviewMarkdownRefs()` points relative images at the workspace-confined `file-raw` under the document's directory and root-relative ones under the workspace root (never a widened route; a failed load degrades to alt text), after `decodeURIComponent`ing the ref and dropping `?query`/`#fragment` (marked percent-encodes destinations, and the route encodes again), and turns workspace links into `a.rv-path` carrying `data-session-id` for the delegate (the linkifier's absolute paths get it too), stripping the `target` marked gave them. ⚠️ A preview opened by attachment id under a bare file name (attachment cards, history drawer: the registry keeps no relative path) has no directory, so its workspace refs degrade (images to alt text, links to their text), never resolve against the workspace root. ⚠️ The container carries `data-i18n-skip` or the translator rewrites the document's prose. ⚠️ Toggles are per-device localStorage keys (`codeman:filePreview*`), never `SettingsUpdateSchema`; Lines/Wrap are class flips on the ONE `<pre>`, with rules scoped `.file-preview-body > pre.file-preview-text` so they never leak into the document's code blocks. Markdown fetches `lines=10000` (the route ceiling), other text keeps 500. ⚠️ `md`/`markdown` are IN `FILE_PREVIEW_EXTENSIONS` (owner decision 2026-10-13): a markdown path clicked or tapped in the TERMINAL (main pane, grid tiles and split Pane B share `registerFilePathLinkProvider`) opens this rendered view like every other surface, never the streaming log viewer; other in-workspace text keeps the tail viewer (live follow). The terminal pattern links `.markdown` and `.htm` too. Tests: `test/file-preview-markdown.test.ts`. → [architecture-invariants#file-viewer-text-view-rendered-markdown-and-text-toggles](docs/architecture-invariants.md#file-viewer-text-view-rendered-markdown-and-text-toggles)
|
||||
|
||||
**Files panel search** (COD-236, the `q` param on `GET /api/sessions/:id/files`): `compileFileQuery()` (`utils/file-query.ts`, pure) compiles the query into a predicate the server-side walk prunes with; a query returns a FLAT match list and the walk recurses past non-matching directories. An empty, whitespace-only or overlong (`MAX_QUERY_LENGTH`, 256) query compiles to `null`, keeping the default tree response byte-identical. ⚠️ **Never compile a glob into a RegExp** (`*a*a*a…` backtracks and freezes the event loop for the whole server): `globMatch()` is a two-pointer wildcard walk. → [architecture-invariants#files-panel-search](docs/architecture-invariants.md#files-panel-search)
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ A file path an agent prints is a link on both surfaces it can appear on, and cli
|
||||
|
||||
**Out-of-workspace paths go through the attachment routes, not the file routes.** `file-content`/`file-raw` resolve against `workingDir` and 404 anything that escapes it, which is correct and unchanged — but the paths agents most often print (a `/tmp` capture, Claude's own scratchpad, another checkout) are exactly that, so clicking one used to report "File not found" for a file sitting on disk. `openFilePreview()` now detects the case (`_isExternalPreviewPath`, a string compare for ROUTING only; the real decision stays server-side) and registers the path via `POST /api/sessions/:id/attachments` first, rendering by id. ⚠️ That registration passes `notify: false`, which suppresses ONLY the `attachment:detected` broadcast — the guard, the registry entry and the by-id routes are identical either way. Without it every click also popped an attachment card announcing the file already filling the screen. ⚠️ The click is an explicit user action on the **explicit, Origin-guarded** registration route, which is why it may cross the workspace boundary at all; the passive magic-link scanner stays force-confined. A type outside `SUPPORTED_ATTACHMENT_EXTENSIONS` (`.svg`, `.bmp`) is refused with a message naming what IS previewable, rather than the registry's own policy term.
|
||||
|
||||
⚠️ **The terminal routes an out-of-workspace path to the preview, not the log viewer.** The log viewer spawns `tail -f` and allows only the workspace, `/var/log` and `~/logs`, so an external `.log`/`.json`/code path answered `Path must be within working directory or allowed log directories` while the SAME path clicked in the response viewer previewed fine. `activate()` now checks `_isExternalPreviewPath` alongside `previewsInFileViewer`. In-workspace text keeps the tail viewer, which is the point of it (live follow), except `.html`/`.htm`, which always go to the preview so they render as a page (see below); nothing widened `file-stream-manager`'s allowlist, so no `tail -f` is spawned on an arbitrary host path.
|
||||
⚠️ **The terminal routes an out-of-workspace path to the preview, not the log viewer.** The log viewer spawns `tail -f` and allows only the workspace, `/var/log` and `~/logs`, so an external `.log`/`.json`/code path answered `Path must be within working directory or allowed log directories` while the SAME path clicked in the response viewer previewed fine. `activate()` now checks `_isExternalPreviewPath` alongside `previewsInFileViewer`. In-workspace text keeps the tail viewer, which is the point of it (live follow), except markdown (`.md`/`.markdown`, in `FILE_PREVIEW_EXTENSIONS` since the owner's 2026-10-13 decision, rendered as a document) and `.html`/`.htm` (`rendersAsPage()`, rendered as a page, see below), which always go to the preview; nothing widened `file-stream-manager`'s allowlist, so no `tail -f` is spawned on an arbitrary host path.
|
||||
|
||||
**Text reuses the edit-mode allowlist; markup stays download-only.** `TEXT_ATTACHMENT_EXTENSIONS` IS `EDITABLE_EXTENSIONS` (`config/file-editing.ts`) rather than a second curated list that would drift from it: if the viewer would open a file for editing inside the workspace, the same file outside it can be read. The justification for widening is that the agent in the session can already `cat` any of these and the picker already previews them, so the suffix was never the confidentiality gate; the path guard is (sensitive-file blocklist, `/root` and `/etc` trees, realpath first). ⚠️ Two consequences had to be handled at the same time: `~/.codeman*/state.json` joined `isSensitivePath` (it persists `SessionState.envOverrides`, and the env allowlist admits key-shaped names like `GEMINI_API_KEY`, so it can hold a live credential), and `html`/`htm` joined `svg` in `serveRawFile`'s **download-only** branch so that widening what can be READ never widens what can RUN on our own origin. Text with no dedicated MIME entry goes out as inert `text/plain; charset=utf-8` + `nosniff`, matching the picker. ⚠️ **The one place HTML renders is `/html-view/<cap>/...`** (`html-view-routes.ts`, `html-view-capabilities.ts`), never these routes: `POST /api/sessions/:id/html-view` mints a capability for the HTML file's own directory, and every response carries `Content-Security-Policy: sandbox` WITHOUT `allow-same-origin`, so the page is opaque-origin even as a top-level tab and cannot read Codeman or drive its API. The directory is served recursively to the page's scripts (non-dot, asset-allowlisted, realpath-confined, blocklist applied, GET/HEAD only; the dot and extension rules run on the requested name AND on the symlink-resolved target, or a `cfg.json -> .git/config` link would serve the git config), which is why minting is refused for an HTML file in a hidden directory (for a file inside the workspace, a hidden directory below the workspace root, so a workspace that itself lives under one still renders) or directly in a broad root (`/`, home, `/tmp`, `/var/tmp` and `$TMPDIR` (named literally, since macOS sets `TMPDIR` in every terminal), the cases and user-space roots, a drive's mount point such as WSL's `/mnt/d`) or in any directory above one of them (`/home`, `/Users`, macOS's `/private`; `isBroadHtmlViewRoot`). A capability minted for a folder outside the workspace records that, and the serving route refuses it per request once `attachmentConfineToWorkspace` is on, the same re-check the attachment serve path makes. The preview renders it by default; the **Page** pill (`htmlRendered`, per-device like `mdRendered`) and any refused mint fall through to the source view with its Edit button. Tests: `test/routes/html-view-routes.test.ts`, the html-view block in `test/webview-auth-exemption.test.ts`, `test/webview-capability-revocation.test.ts`. The by-id text preview is bounded like the workspace one: a `Range` request for the first 512KB (a real partial read, not a discarded 50MB download) plus a 500-line cap, with the footer saying so.
|
||||
|
||||
@@ -416,7 +416,7 @@ Tests: `test/file-editing-policy.test.ts` (pure policy), `test/routes/file-write
|
||||
- ⚠️ **`data-i18n-skip` on the container.** The translator's MutationObserver translates inserted headings and paragraphs, and the `.file-preview-content` entry in its skip list matches nothing (no element has that class), so the attribute is what keeps a Chinese UI from rewriting a README.
|
||||
- ⚠️ **Toggles are per-device, in their own localStorage keys** (`codeman:filePreviewMdRendered` / `LineNumbers` / `Wrap`), for the same reason as the Files panel's show-hidden toggle: the app-settings object is rebuilt from the settings modal on every save, and they are display state, not synced settings (`SettingsUpdateSchema` is `.strict()`). MD re-renders from the kept source (`filePreviewContent`, which is also what Copy copies) without a refetch; Lines/Wrap are class flips on the one `<pre>`, whose rules are scoped `.file-preview-body > pre.file-preview-text` so they never leak into the document's own code blocks. Lines are one inline `<span class="fp-line">` per line joined by real newlines, the counter in `::before` with `user-select: none`, so select and copy return the exact text.
|
||||
- ⚠️ **Caps.** Markdown fetches `lines=10000` (the route's `MAX_LINES_LIMIT`), because a rendered document cut at 500 lines reads as the whole document; other text keeps 500, which is what stops a huge log locking the tab in one `<pre>`. The attachment (out-of-workspace) branch keeps its 512 KB Range read and skips the 500-line clip for markdown. Edit mode is unchanged and still re-fetches `edit=1`; the three toggles hide while editing and for images, media and PDFs.
|
||||
- ⚠️ **`md` is NOT in `FILE_PREVIEW_EXTENSIONS`.** Only an in-workspace `.md` path clicked in the terminal still opens the tail viewer (see File-path links above: in-workspace text keeps live follow, which is what Ralph's `fix_plan.md` needs). Everything else reaches `openFilePreview()` and renders it: the Files panel, a path clicked in the Response Viewer (its delegate calls `openFilePreview()` directly), an out-of-workspace terminal path, and attachment cards. `avif` and `ico` were added to the set (a printed `favicon.ico` used to tail binary noise), with `avif` also in file-content's image set and file-raw's MIME map; out-of-workspace avif/ico stay unregistrable, like svg/bmp.
|
||||
- ⚠️ **`md` and `markdown` ARE in `FILE_PREVIEW_EXTENSIONS`** (owner decision 2026-10-13). Until then an in-workspace `.md` path clicked in the terminal opened the tail viewer, which showed "streaming" over raw text instead of the document (the live follow it gave Ralph's `fix_plan.md` was not worth that). Now every surface reaches `openFilePreview()` and renders it: the terminal (the main pane, grid tiles and the split's Pane B share `registerFilePathLinkProvider`, and a touch tap activates the same provider), the Files panel, a path clicked in the Response Viewer (its delegate calls `openFilePreview()` directly), and attachment cards. `FILE_PATH_LINK_PATTERN` links `.markdown` (and `.htm`) as well, or such a path would not be clickable at all. Other in-workspace text (`.log`, `.txt`, `.json`, code) keeps the tail viewer. `avif` and `ico` were added to the set (a printed `favicon.ico` used to tail binary noise), with `avif` also in file-content's image set and file-raw's MIME map; out-of-workspace avif/ico stay unregistrable, like svg/bmp.
|
||||
|
||||
Tests: `test/file-preview-markdown.test.ts` (jsdom-in-vm, pins every rule above), `test/routes/file-routes.test.ts` (avif).
|
||||
|
||||
|
||||
@@ -105,8 +105,9 @@ into the Run dropdown.
|
||||
- **Tap a link** in terminal output and it opens in a new tab. Same for a link in an agent's
|
||||
answer in the response viewer — it opens a tab rather than navigating the dashboard away,
|
||||
which on a phone would unload the whole session view.
|
||||
- **Tap a file path** an agent printed and the file-preview overlay opens; a log path opens the
|
||||
log viewer. Works in scrolled-up transcript too.
|
||||
- **Tap a file path** an agent printed and the file-preview overlay opens (Markdown rendered,
|
||||
HTML as a page); a log or other plain-text path opens the log viewer. Works in scrolled-up
|
||||
transcript too.
|
||||
- A tap on the prose *beside* a link still places the cursor as usual, and a tap on a dialog's
|
||||
numbered choice still answers the dialog even when the row contains a path — the dialog wins,
|
||||
because on a phone it is the only interaction that matters.
|
||||
|
||||
@@ -87,9 +87,10 @@ File paths in a session are links. That works in two places:
|
||||
render as underlined monospace links.
|
||||
|
||||
Clicking one opens it in the preview: images and PDFs render, video and audio play with a
|
||||
working scrub bar, documents convert, text shows inline and Markdown renders. The exception is
|
||||
a text or Markdown file inside the workspace clicked in the terminal: that opens in the tail
|
||||
viewer instead, which follows a file that is still being written.
|
||||
working scrub bar, documents convert, text shows inline, Markdown renders and HTML opens as a
|
||||
page. The exception is a plain text file (a log, `.txt`, `.json`, code) inside the workspace
|
||||
clicked in the terminal: that opens in the tail viewer instead, which follows a file that is
|
||||
still being written. A Markdown file always opens rendered, wherever you click it.
|
||||
|
||||
Paths **outside** the session's workspace work too, which matters because that is where most
|
||||
of an agent's output lands: a screenshot in `/tmp`, a capture in its own scratchpad, a file in
|
||||
|
||||
@@ -1949,7 +1949,7 @@ function computeRewriteScrollLine(input) {
|
||||
* a `/g` regex, so {@link absoluteFilePathPattern} mints a fresh one per call.
|
||||
*/
|
||||
const FILE_PATH_LINK_PATTERN =
|
||||
/(\/(?:home|Users|tmp|var|private|opt|mnt|srv|media|data|workspace)\/[^\s"'<>|;&\n\x00-\x1f]*\.(?:log|txt|json|md|ya?ml|csv|xml|sh|py|tsx|ts|jsx|js|mjs|cjs|css|html|toml|ini|sql|png|jpe?g|gif|webp|avif|bmp|ico|svg|pdf|docx|pptx|xlsx|mp4|webm|mov|mp3|wav))\b/g;
|
||||
/(\/(?:home|Users|tmp|var|private|opt|mnt|srv|media|data|workspace)\/[^\s"'<>|;&\n\x00-\x1f]*\.(?:log|txt|json|markdown|md|ya?ml|csv|xml|sh|py|tsx|ts|jsx|js|mjs|cjs|css|html?|toml|ini|sql|png|jpe?g|gif|webp|avif|bmp|ico|svg|pdf|docx|pptx|xlsx|mp4|webm|mov|mp3|wav))\b/g;
|
||||
|
||||
/** A fresh, zero-state instance of {@link FILE_PATH_LINK_PATTERN}. */
|
||||
function absoluteFilePathPattern() {
|
||||
@@ -1961,13 +1961,18 @@ function absoluteFilePathPattern() {
|
||||
* points at goes to the tail/log viewer, which is the right home for a growing
|
||||
* text file and the wrong one for bytes (tailing a PNG shows binary noise).
|
||||
*
|
||||
* Markdown is here too (owner decision 2026-10-13): an agent's `.md` is a
|
||||
* document to read, so the preview renders it (its MD pill gives the source)
|
||||
* instead of the log viewer streaming it as raw text. Other text (.log, .txt,
|
||||
* .json, code) keeps the log viewer, which follows a file still being written.
|
||||
*
|
||||
* The media entries mirror VIDEO_ATTACHMENT_EXTENSIONS/AUDIO_ATTACHMENT_EXTENSIONS
|
||||
* (src/attachment-registry.ts, the single source) — they diverged once and an
|
||||
* in-workspace `.m4a` opened as binary noise in the log viewer while the same
|
||||
* file in /tmp played fine. test/media-extension-parity.test.ts pins the sync.
|
||||
*/
|
||||
const FILE_PREVIEW_EXTENSIONS = new Set(
|
||||
('png jpg jpeg gif webp avif bmp ico svg pdf docx pptx xlsx mp4 webm mov m4v ogv mp3 wav ogg oga m4a aac flac opus').split(' ')
|
||||
('png jpg jpeg gif webp avif bmp ico svg pdf docx pptx xlsx mp4 webm mov m4v ogv mp3 wav ogg oga m4a aac flac opus md markdown').split(' ')
|
||||
);
|
||||
|
||||
/** Whether a path's extension is one {@link FILE_PREVIEW_EXTENSIONS} covers. */
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
* 4. Lines/Wrap flip classes on the <pre> and persist, and the text the <pre>
|
||||
* holds is byte-identical to the file; every toggle is hidden for an image
|
||||
* and while editing.
|
||||
* 5. `FILE_PREVIEW_EXTENSIONS` gained avif/ico and still has no `md`
|
||||
* (in-workspace text keeps the tail viewer, see architecture-invariants).
|
||||
* 5. `FILE_PREVIEW_EXTENSIONS` gained avif/ico, and `md`/`markdown` (owner
|
||||
* decision 2026-10-13: a markdown path clicked in the terminal opens this
|
||||
* rendered view, not the streaming log viewer), while other in-workspace
|
||||
* text keeps the tail viewer (see architecture-invariants).
|
||||
* 6. A preview opened by attachment id under a bare file name has no
|
||||
* directory to resolve against, so its relative images degrade to alt text
|
||||
* and its relative links to plain text instead of landing on the workspace
|
||||
@@ -464,10 +466,13 @@ describe('file viewer markdown line breaks', () => {
|
||||
});
|
||||
|
||||
describe('FILE_PREVIEW_EXTENSIONS', () => {
|
||||
it('routes avif and ico paths to the viewer and leaves .md with the tail viewer', () => {
|
||||
it('routes avif, ico and markdown paths to the viewer and leaves other text with the tail viewer', () => {
|
||||
const { exts } = loadApp();
|
||||
expect(exts.has('avif')).toBe(true);
|
||||
expect(exts.has('ico')).toBe(true);
|
||||
expect(exts.has('md')).toBe(false);
|
||||
// Markdown is a document to read rendered, not a log to follow.
|
||||
expect(exts.has('md')).toBe(true);
|
||||
expect(exts.has('markdown')).toBe(true);
|
||||
for (const text of ['log', 'txt', 'json', 'ts', 'yaml', 'csv']) expect(exts.has(text), text).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1017,6 +1017,48 @@ describe('terminal link tap', () => {
|
||||
expect(app.openLogViewerWindow).toHaveBeenCalledWith('/var/log/app.log', 'sess-1');
|
||||
});
|
||||
|
||||
it('opens a tapped markdown or HTML path in the preview, and other text in the log viewer', () => {
|
||||
// In-workspace paths (_isExternalPreviewPath is false here): markdown renders as a
|
||||
// document and HTML as a page in the preview; other text keeps the live tail.
|
||||
for (const path of ['/home/u/ws/notes.md', '/home/u/ws/README.markdown', '/home/u/ws/report/index.html']) {
|
||||
const line = `wrote ${path} just now`;
|
||||
const { app } = linkHarness([line, '', '❯ ']);
|
||||
expect(app._handleMobileTerminalTap(at(line.indexOf('/home')), false, 'content'), path).toBe('link');
|
||||
expect(app.openFilePreview, path).toHaveBeenCalledWith(path, 'sess-1');
|
||||
expect(app.openLogViewerWindow, path).not.toHaveBeenCalled();
|
||||
}
|
||||
for (const path of ['/home/u/ws/run.log', '/home/u/ws/notes.txt', '/home/u/ws/data.json']) {
|
||||
const line = `wrote ${path} just now`;
|
||||
const { app } = linkHarness([line, '', '❯ ']);
|
||||
expect(app._handleMobileTerminalTap(at(line.indexOf('/home')), false, 'content'), path).toBe('link');
|
||||
expect(app.openLogViewerWindow, path).toHaveBeenCalledWith(path, 'sess-1');
|
||||
expect(app.openFilePreview, path).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("routes a path clicked in a tile or split pane the same way, against that pane's session", () => {
|
||||
// TerminalTile registers the SAME provider with its own terminal and session
|
||||
// (terminal-tile.js), so a grid tile and the split's Pane B route like the main pane.
|
||||
const { app } = linkHarness(['', '', '❯ ']);
|
||||
const tileTerminal = createTerminalGrid(['see /home/u/ws/notes.md and /home/u/ws/run.log', '', '❯ '], 2);
|
||||
let provider: any;
|
||||
tileTerminal.registerLinkProvider = vi.fn((p: unknown) => {
|
||||
provider = p;
|
||||
});
|
||||
app.registerFilePathLinkProvider({ terminal: tileTerminal, getSessionId: () => 'tile-2', setHovered: vi.fn() });
|
||||
|
||||
let links: any[] = [];
|
||||
provider.provideLinks(1, (found: any[]) => {
|
||||
links = found || [];
|
||||
});
|
||||
const byText = (text: string) => links.find((l) => l.text === text);
|
||||
byText('/home/u/ws/notes.md').activate({}, '/home/u/ws/notes.md');
|
||||
byText('/home/u/ws/run.log').activate({}, '/home/u/ws/run.log');
|
||||
|
||||
expect(app.openFilePreview).toHaveBeenCalledWith('/home/u/ws/notes.md', 'tile-2');
|
||||
expect(app.openLogViewerWindow).toHaveBeenCalledWith('/home/u/ws/run.log', 'tile-2');
|
||||
});
|
||||
|
||||
it('activates a link in scrollback, where the tap sends no report at all', () => {
|
||||
// A scrolled-up tap deliberately reports nothing (it would land on whatever row now
|
||||
// occupies the cell), but reading old output and tapping a URL in it is the common case.
|
||||
|
||||
Reference in new issue
Block a user