Skip to content

feat(dashboard): open a chat file in the user's editor via shell.openPath - #9007

Merged
bolichen97 merged 1 commit into
mainfrom
feat/open-file-in-editor-8119
Sep 8, 2026
Merged

feat(dashboard): open a chat file in the user's editor via shell.openPath#9007
bolichen97 merged 1 commit into
mainfrom
feat/open-file-in-editor-8119

Conversation

@chenmingwei23

@chenmingwei23 chenmingwei23 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

What is the problem?

When a chat response references a file, there is no supported way to open it in
the editor the user actually works in. A backticked absolute path opens
KiroCrew's built-in viewer (fine for a look, but not their tooling), and an
editor deep link (obsidian://, vscode://, zed://) is blocked by design and
silently reopens the dashboard in a new window with no error. external-scheme.js
documents why a configurable scheme allowlist was rejected, and that reasoning is
sound, so the handoff "the agent points at a file, I open it where I work" has no
first-party path today.

Why this matters to the user

For anyone whose notes or code live in an external editor -- Obsidian vaults, VS
Code workspaces -- the handoff breaks at the last step. The documented workaround
is standing up a localhost HTTP server that 302-redirects to the real scheme,
i.e. running a background process purely to launder a URL past a security control.
That it works is not a good sign.

There is already a partial route -- the chip's right-click "Open with default app"
calls POST /api/reveal, which shells out on the GATEWAY host (/usr/bin/open /
xdg-open). But it refuses Windows outright and degrades to a clipboard copy on a
remote or tunneled session, and it opens the file on the machine running the
gateway rather than the machine in front of the user. It is not the route the
issue asks for.

How the fix solves it

The issue names the mechanism: hand a filesystem PATH -- never a URL scheme -- to
shell.openPath, on the user's own desktop. This wires exactly that, additively,
reusing the primitives already in the tree:

  • New Electron main channel dashboard:open-file in ipc-registrar.js. It is a
    LAUNCH on the local machine, so it takes the SAME three-gate local-dashboard
    trust check (assertLocalDashboard) as its sibling launch channel
    crash-reports:reveal -- a connection window pointed at a remote gateway shares
    this preload and must never reach shell.openPath. It then validates the path
    with the shape the mochi-pet:open-image handler already uses: realpathSync
    BEFORE the extension test (so a .md symlink to a key file is judged by what it
    resolves to), a fixed text/document extension allowlist, a regular-file
    requirement, and a non-empty openPath return surfaced as an OS refusal. The
    launch itself goes through the shared openPathHardened wrapper
    (website/electron/open-path.js), the same one every other launch site uses, so
    a writable leading PATH entry cannot shadow xdg-open on Linux. The
    allowlist admits only types whose OS default handler cannot execute their
    contents, which excludes spreadsheets (.csv/.tsv): their default app
    evaluates a leading = cell, so an agent-authored =WEBSERVICE(...) would
    exfiltrate and =cmd|... would reach DDE.
  • New preload namespace fileOpenAPI.open(path) (preload.js), one more
    per-capability bridge in the established pattern, absent in a plain browser and
    the PWA.
  • Frontend feature-detect helpers canOpenFileInEditor() / openFileInEditor()
    (lib/electron.ts), and one new "Open in editor" row on the existing chat path
    chip (FilePathMenu), shown only when the bridge exists and the target is a
    file. In a browser tab the row is hidden and the built-in viewer stays the only
    handoff.

Symptom -> root cause chain: the file cannot reach the user's editor (symptom)
because the only host-open route runs on the gateway host and is Windows-refused /
remote-degraded, and the Electron shell.openPath primitive -- which runs on the
user's own machine -- was never exposed to the dashboard renderer (root cause).
The fix exposes it, gated and validated.

Security posture: this hands an arbitrary path to the OS, which is the whole risk.
Two boundaries bound it. (1) The affordance is only actionable AFTER the chip's
existing /api/file-read probe confirms a non-sensitive regular file -- the
credential-path denylist (is_sensitive_path) is enforced upstream before the row
can be clicked. (2) The launch channel itself realpath-canonicalizes, restricts to
a fixed text/document extension set (deliberately not user-extensible, which would
reopen the rejected scheme-allowlist surface), requires a regular file, and is
reachable only from the trusted local dashboard. Project-root confinement (the
issue's "worth discussing" item) is intentionally NOT added here: no sibling launch
channel (/api/reveal, crash-reports:reveal) project-confines, the Electron main
process has no session project dir to confine against, and adding a new bespoke
validator would contradict "reuse the existing validation." Flagging it for a
maintainer decision rather than inventing it.

Security-guidance note: the org security-guidance search tool was unavailable in
this environment (no such tool is loadable here). The query I would have run:
"handing an untrusted filesystem path to a host OS launch primitive -- path
confinement, realpath canonicalization, and sensitive-path denylisting before
launch." I did not treat its absence as clearance; the design applies the in-repo
controls above.

What tests we did

  • Electron main (node --test test/ipc-registrar.test.js, 26 tests): new cases
    assert the channel routes through assertLocalDashboard (a wrong-origin sender
    is rejected before any fs/shell work), and that validation rejects an empty /
    non-string path, an unsupported extension, an executable-association script
    (.js), a spreadsheet (.csv, .tsv), a directory, and a .md symlink whose
    realpath is an .exe; that a valid file is opened at its RESOLVED path; that
    the launch runs with the narrowed launcher PATH and restores it afterwards;
    and that an OS refusal and a thrown openPath are both reported, not swallowed.
    Six mutations hand-applied, each reddens a distinct assertion (gate removed; ext
    checked on raw path not realpath; isFile dropped; openPath return ignored; raw
    path passed to openPath instead of realpath; openPathHardened swapped back to
    a bare shell.openPath).
  • Shell contract (test/shell-contract.test.js): the drift guards that assert
    every preload-sent channel has a main handler pass with the new channel; the
    SHELL_HANDLES set and channel count were updated accordingly.
  • Frontend (vitest run MarkdownRenderer.contextmenu.test.tsx, 17 tests): 4 new
    cases -- the row shows and calls fileOpenAPI.open with the chip path when the
    bridge is present; is hidden in a plain browser; is hidden for a directory; and
    surfaces the bridge error on failure. Three mutations, each reddens a distinct
    test (drop the bridge check; drop the dir guard; drop the error surfacing).
  • i18n gate (node scripts/i18n-check.mjs, 19 checks PASS): one new key
    components.markdownPanel.open_in_editor beside open_with_default_app, added to
    en.manual.json and all 11 shipped non-English locales with faithful
    translations matching each locale's register; en-XA regenerated. key-refs,
    changed-passthrough, source-strings, and pseudolocale all pass; catalog
    parity + duplicate-key tests (104) pass. tsc and eslint clean on changed source.

Screenshots

The change is the right-click menu on a chat file-path chip gaining an "Open in
editor" row (shown only when the desktop shell bridge is present; a plain browser
sees no such row). Captured against the real stylesheet in both themes via
website/scripts/capture-open-in-editor-8119.mjs, which asserts the row is
present rather than only photographing it:

Open in editor row, dark theme

Open in editor row, light theme

If the images do not render, see the Files changed tab under
temp-screenshots/open-in-editor-8119/.

Any other suggestions

  • Launcher-PATH hardening is APPLIED here, not deferred. An earlier revision of
    this PR deferred it to Electron shell.openPath inherits PATH with no launcher hardening #9094 on the grounds that the sibling call site
    (mochi-pet:open-image) was equally bare, so the exposure was a repo-wide
    property of the primitive rather than something this affordance introduced.
    That is no longer true: website/electron/open-path.js (openPathHardened)
    has since landed on main and every other launch site now uses it, so shipping a
    bare shell.openPath would have made this handler the only unhardened call in
    the tree. The launch now goes through openPathHardened(shell, real), which
    narrows PATH to system launcher directories across the native launch on Linux
    and is a documented pass-through on macOS and Windows. Both review lanes
    (GPT 5.6 and First Principles) flagged the omission; both are addressed by the
    one-line routing change plus its regression test.
  • The row renders in a connection window pointed at a REMOTE gateway, where the
    main process refuses every click: canOpenFileInEditor() tests only for the
    preload bridge, and that preload is shared with connection windows, while
    assertLocalDashboard rejects the channel for any non-local sender. The refusal
    is safe and surfaces as the menu's error line, but the row is still an
    affordance that cannot succeed there. Narrowing the renderer-side gate needs a
    local-vs-remote signal the renderer does not have today, so it is deliberately
    not bundled here; declared rather than left implicit.
  • Project-root / configured-root confinement (issue's open question) is the
    natural follow-up if maintainers want it; it would need the session's project
    dir threaded to the Electron main process, and ideally applied to the sibling
    /api/reveal open too for consistency, so it is its own change.
  • Dashboard chat: a path chip used as a link label swallows the link (pre-existing sibling of #5730) #5745 (path chip used as a link label swallows the link) touches the same chip
    rendering and is a separate bug, unaddressed here.

Refs #8119

@chenmingwei23
chenmingwei23 requested a review from a team September 6, 2026 11:28
@chenmingwei23
chenmingwei23 requested a review from a team as a code owner September 6, 2026 11:28
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — 🟡 CONCERNS

UX-level review of 87be8d7737810608d48c45b565186ee4f5e18d80 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All evidence is in hand. The blind reader correctly parsed the new row but flagged the exact ambiguity ("which editor? where?"); the diff shows three states the screenshots never captured, plus two guaranteed-fail configurations the row doesn't gate on. Final review:

UX-Verdict: CONCERNS

"Open in editor" renders for files it will always refuse — every source-code chip click dead-ends in a raw "unsupported file type" error.

Watch

  • The row's visibility checks only bridge + not-dir, but the channel's allowlist (OPENABLE_EXTS) admits no code files (.py, .ts, .js…). In a coding-agent product most chat-referenced files are code, so the most frequent click on this row fails every time with "unsupported file type" and no next step. Fix: gate the row client-side on the same extension set (extname is available in filePath); keep the channel check as depth.
  • In an Electron window pointed at a remote gateway, canOpenFileInEditor() is still true, but the path exists only on the gateway host and assertLocalDashboard rejects — the only launch row shown on remote is a guaranteed fail surfacing raw IPC exception text. directLocal is already in the component; add isLocal && to canOpenInEditor.
  • setEditorError(r.error || 'error') renders untranslated mechanism fragments ("no path", "not a file", err.message, OS refusal strings) verbatim, while the sibling reveal path deliberately keeps "the raw denial text never reaches the UI" via localized reveal_failed/reveal_blocked. Map bridge outcomes to i18n strings the 14 translated locales can carry.
  • Sibling ambiguity: on a local desktop the menu shows both "Open in editor" and "Open with default app", which perform the same launch via different routes; the blind reader's one hesitation was exactly this — "I don't know which 'editor' it means or where it would open." Neither label states the difference; consider "Open on this computer"-style disambiguation or dropping the redundant row when both would act locally.

Evidence gaps

  • No screenshot shows the normal local-desktop menu where the new row sits beside "Open with default app" / "Open in Finder" — the capture harness ran without directLocal, so the blind reader never saw the sibling pair (re-run capture with the reveal rows enabled).
  • The new editorError inline ErrorNotice (e.g. "unsupported file type") appears in no screenshot; capture one failure state.

[UX-REVIEWED] 87be8d7

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of 87be8d7737810608d48c45b565186ee4f5e18d80 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All evidence gathered. I have what I need for the verdict: the capability is real but its honest scope is narrower than framed, and there are counted duplication/sibling concerns. Final review:

First-Principles-Verdict: CONCERNS

The genuinely new capability is Windows-only: on macOS/Linux local desktop the menu now shows two rows that hand the same file to the same OS default handler.

What this change ships

Intent: let a user open a chat-referenced file in their own desktop's default handler instead of the built-in viewer — an ADDITION.

  1. "Open in editor" row on the file-chip right-click menu (desktop shell) — second spelling on POSIX, see Watch
  2. Same row renders in a remote-gateway desktop window but every click errors — undeclared
  3. New Electron channel dashboard:open-file, local-dashboard-gated, extension-allowlisted — justified (agent-untrusted-path boundary)
  4. New preload bridge fileOpenAPI — justified, established per-capability pattern
  5. Helpers canOpenFileInEditor/openFileInEditor — justified, 1 consumer each, not generalized
  6. i18n key in 13 catalogs — justified, mandated by i18n gates
  7. Capture script + committed screenshots — follows convention (434 sibling capture-*.mjs, 1259 temp-screenshots/ files)
  8. Main/renderer tests + channel-count updates — justified

Watch

  • Second spelling: on macOS/Linux with a local gateway — the only place assertLocalDashboard lets the channel work — the existing "Open with default app" row (revealOrOpenWithAck('open')/api/revealplatform_compat.open_with_default_app, files.py:200) already opens the file with the same default handler on the same machine. shell.openPath is the same OS verb under a different label. The description's remote/tunneled motivation ("it opens the file on the machine running the gateway rather than the machine in front of the user") is not fixed: the new gate refuses exactly the case where those machines differ. The honest delta is Windows support plus the hardened launcher.
  • Dead control, undeclared: visibility is canOpenFileInEditor() (bridge presence) but actionability is the local-dashboard gate, so a desktop window on a remote gateway shows a row whose every click returns "restricted to the local dashboard".
  • Unfixed sibling of the allowlist's root cause (agent-authored path → content-executing default handler): the adjacent /api/reveal open route has no extension check (grep open_with_default_app: 1 launch consumer, files.py:200), so on POSIX local desktop the .csv/.html refusal is bypassable one menu row down. Windows is where the allowlist is load-bearing.

Subtractions

  • In the desktop shell where both rows render, drop one spelling: hide the gateway-routed "Open with default app" row (canOpen, FilePathMenu.tsx:344) when the shell bridge row is shown — two adjacent rows performing the same OS verb with different refusal sets will diverge and confuse.

[FIRST-PRINCIPLES-REVIEWED] 87be8d7

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of 87be8d7737810608d48c45b565186ee4f5e18d80 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

shell.openPath's default-handler primitive structurally can't serve the "code in VS Code" half of the stated problem — only the notes half ships.

Watch

  • The security predicate ("handler cannot execute contents") correctly excludes every source-code extension, yet the problem statement names "VS Code workspaces" and the row is labeled "Open in editor." Cause: default-handler launch means the handler is chosen by extension; mechanism: OPENABLE_EXTS can never admit .py/.ts/.sh; consequence: the code-workflow user still has no handoff, and every future "add my extension" request pressures a fixed allowlist the design cannot safely grow. The dissolving alternative — launch a known editor binary with the path as an argument, where the handler is fixed and the predicate holds for all types — is follow-up-sized, but it should be named as the intended endgame now, before the allowlist becomes the API users bargain with.

Suggestions

  • The realpath→allowlist→isFile sequence is now hand-copied between dashboard:open-file and mochi-pet:open-image; this file's own header warns "hand-maintained spellings of a security check are chances for one to be tightened while the others are not" — extract one shared validator taking the extension set.
  • Rename the row (or key it per shipped reality, e.g. "Open on this computer"): it invokes the OS default app, not an editor, and a mislabeled affordance invites the allowlist-growth requests above.

[DESIGN-REVIEWED] 87be8d7

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 87be8d7737810608d48c45b565186ee4f5e18d80 — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 87be8d7

Verdict parsed from the review's SHA-scoped output markers for commit 87be8d7737810608d48c45b565186ee4f5e18d80.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 87be8d7737810608d48c45b565186ee4f5e18d80: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 87be8d7737810608d48c45b565186ee4f5e18d80 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 87be8d7

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 87be8d7737810608d48c45b565186ee4f5e18d80: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 6, 2026
@chenmingwei23
chenmingwei23 force-pushed the feat/open-file-in-editor-8119 branch from 7f16627 to a76ef75 Compare September 6, 2026 11:52
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 6, 2026
@chenmingwei23
chenmingwei23 force-pushed the feat/open-file-in-editor-8119 branch from a76ef75 to 9f079ed Compare September 6, 2026 12:20
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 6, 2026
@chenmingwei23
chenmingwei23 force-pushed the feat/open-file-in-editor-8119 branch from 125b994 to 3c4552b Compare September 6, 2026 12:53
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 6, 2026
@chenmingwei23
chenmingwei23 force-pushed the feat/open-file-in-editor-8119 branch from 3c4552b to 86521cd Compare September 6, 2026 12:59
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 6, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 6, 2026
@chenmingwei23
chenmingwei23 force-pushed the feat/open-file-in-editor-8119 branch from 86521cd to f74f3fb Compare September 8, 2026 00:56
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 8, 2026
…Path

Add an 'Open in editor' affordance on the chat file-path chip, backed by a new
dashboard:open-file Electron IPC channel that validates a filesystem path and
calls shell.openPath, so a referenced file opens in the OS default handler on
the user's own machine -- never a URL scheme.

- New channel in ipc-registrar.js, gated by the existing three-gate
  assertLocalDashboard check (sibling of crash-reports:reveal), then realpath ->
  a plain-text-only extension allowlist -> regular-file -> shell.openPath. The
  allowlist admits ONLY types whose OS default handler cannot execute embedded
  content (text/data/config); scripts, browser active documents and rich
  office/pdf documents are excluded because their handlers can run code.
- New fileOpenAPI preload namespace, feature-detected; absent in a plain browser.
- Frontend helpers in lib/electron.ts and one menu row on the existing chip,
  shown only when the bridge is present and the target is a file.
- One new i18n key across all locales; en-XA regenerated.
- Electron + frontend tests, each mutation-verified; capture harness + light/dark
  screenshots under temp-screenshots/ per that dir's convention.

Refs #8119
@chenmingwei23
chenmingwei23 force-pushed the feat/open-file-in-editor-8119 branch from f74f3fb to 87be8d7 Compare September 8, 2026 01:14
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 8, 2026
@bolichen97
bolichen97 merged commit ddf6ae5 into main Sep 8, 2026
65 checks passed
@bolichen97
bolichen97 deleted the feat/open-file-in-editor-8119 branch September 8, 2026 06:39
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants