Commit Graph

39091 Commits

Author SHA1 Message Date
taxbax
4e2f3e4a2f catalog: add meeting-overlay and profile-pane entries
Salvaged from #116324 (maintainerCanModify=false); contributor map added.
2026-09-20 10:47:02 -07:00
Ali Baydoun
a798600ce5 catalog: add Covision agent plugin
Salvaged from #116529 (org-owned fork, maintainer push denied); contributor map added.
2026-09-20 10:47:02 -07:00
yunze7373
9b69f441e6 feat(plugin-catalog): add xmemo memory provider entry
Salvaged from #116647 (org-owned fork, maintainer push denied) with the
reviewed disclosure line added to description and the contributor map.
2026-09-20 10:47:02 -07:00
teknium1
5c44d5df9c fix(git-auth): a rejected GITHUB_TOKEN yields to the gh CLI login instead of failing the clone (#115257)
`run_git_with_credential_fallback` sent exactly one stored credential after an
anonymous refusal: the first one `resolve_git_basic_auth` found, which is
`GITHUB_TOKEN`/`GH_TOKEN` from .env whenever it is set. An expired classic PAT
left there (older setup flows encouraged it) therefore shadowed a working
`gh auth login` for every private plugin/MCP/profile clone, and the failure
read as git's generic "could not read Username ... terminal prompts disabled".

Why: the remote, not the resolution order, knows which credential is live.
The run now walks every credential the user owns for the host (.env token,
`gh auth token`, git credential helper) until one is accepted or the failure
stops being about credentials, logs which source was rejected, and appends an
actionable hint naming the .env token when GitHub refused all of them.
`gh auth token` is asked with GH_TOKEN/GITHUB_TOKEN stripped from its
environment, otherwise it echoes the exported dead token back instead of its
keyring login and the fallback dedupes to nothing.

Test file also gets encoding="utf-8" on its bare read_text/write_text calls
(windows-footgun scanner population for the touched file).
2026-09-20 10:44:52 -07:00
teknium1
d5ee184773 fix(tools): grep fallback follows a symlinked search root on the content lane
`_search_with_grep_pruned` built `find <root> -type f -exec grep …`; find's
`-type f` tests the link itself, so a symlinked root (file or directory)
handed grep nothing and search_files answered total_count 0 with no error
and no warning — byte-identical to "no match" on every platform. `find -H`
follows the operand and only the operand, so links met inside the tree keep
their traversal semantics, and the follow happens in the command on the
host that owns the link (SSH/container backends included). Same shape as
the files-lane fix in the previous commit.

The plain `grep -r` lane is unchanged: GNU grep already follows a
command-line symlink (measured); BSD grep skips it, which needs a
macOS-side follow-up.

Based on the analysis in #116271 by @liuhao1024, whose local
`os.path.islink` resolution would not see a link that exists only on a
remote execution host.

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-09-20 10:44:16 -07:00
webtecnica
9308a168c3 fix(tools): follow a symlinked root on the files lane
search_files(..., target="files") answered total_count: 0, error=None and no
warning for a symlinked root: find <link> -type f tests the link itself, while
rg --files follows the operand, so one call answered differently per installed
engine. find -H follows an operand symlink (and only an operand), so a linked
root is walked where the filesystem lives and the link path stays in the output,
the same shape rg reports.

The no-rg breadth guard now classifies the link's target: making a linked root
traversable would otherwise let a link to $HOME or / slip a recursive find past
it.

Refs #116270
2026-09-20 10:44:16 -07:00
teknium1
e008c7a814 chore: map contributor emails for attribution check 2026-09-20 10:43:45 -07:00
Stephen Cross
4a0e63d8b7 plugin-catalog: re-pin custom-dangerous-patterns to v0.4.5
Bumps sha to a6cf35afc3ca607271284844ee79c60ee4083296 (tag v0.4.5),
which carries the review fix: catch-all allow patterns refused at YAML
load and in `add --type allow` unless forced interactively.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-09-20 10:43:45 -07:00
Stephen Cross
b05afa7e5f plugin-catalog: add custom-dangerous-patterns v0.4.4
Add catalog entry for the custom-dangerous-patterns plugin by scross01.
Provides pre_tool_call hook for user-defined dangerous command patterns.
2026-09-20 10:43:45 -07:00
liuhao1024
7427f53921 fix(gateway): resolve bundled platform config keys written under the manifest name
A bundled platform whose plugin.yaml `name:` differs from its directory
(dir `a2a`, name `a2a-platform`) could be enabled under the manifest name
— `plugins enable` writes that key and `gate_manifest` matches it — but
`Platform._missing_` only knew directory names, so `platforms.a2a-platform:`
in config.yaml raised inside `GatewayConfig.from_dict` and was silently
dropped: the gateway booted without the platform and its port never bound.

`_scan_bundled_plugin_platforms` now also collects each manifest `name:`
as an alias mapped to its directory, and `_missing_` resolves such an
alias to the directory-name member, so every consumer (registry lookup,
adapter creation, config round-trips) sees the canonical value and the
identity invariant holds for both spellings. Aliases never shadow a
directory name.

Fixes #116180
2026-09-20 10:43:38 -07:00
teknium1
edae430488 docs(bluebubbles): name the other-bridge case generically
The troubleshooting entry only needs 'another iMessage bridge'; naming a
specific third-party product is not needed for the instruction.
2026-09-20 10:43:00 -07:00
liuhao1024
5c245aa53b docs(bluebubbles): document webhook auto-registration, the enabled-false trap, and real inbound troubleshooting 2026-09-20 10:43:00 -07:00
teknium1
73774fae96 test(matrix): drop the docs-text change detector
The three tests asserted literal phrases in a module docstring and two
markdown files; they guard prose, not behaviour, and would break on any
rewording. The docs change itself is the deliverable.
2026-09-20 10:42:25 -07:00
chelsealong
e23a033e85 docs(matrix): surface the DM-classification bypass in the operator-facing docs
The prior commit only documented the <=2-member DM auto-classification
in the adapter.py module docstring — source an operator configuring
MATRIX_REQUIRE_MENTION etc. would never read. Issue #114733 explicitly
asks for the callout wherever MATRIX_ALLOWED_ROOMS,
MATRIX_FREE_RESPONSE_ROOMS, MATRIX_REQUIRE_MENTION, and MATRIX_AUTO_THREAD
are documented for operators, i.e. the env-var reference table and the
Matrix user guide. Add the same rule + bypassed vars + escape hatch to
both, matching the precedent set by 08aa67e473 for this kind of env-var
clarification, and extend the doc-content test to cover them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 10:42:25 -07:00
chelsealong
f45ed8e4ec docs(matrix): document the <=2-member DM auto-classification and its config bypass
_resolve_room_identity() classifies any room with <=2 joined members as a DM
regardless of m.direct or an explicit room name, so those rooms silently
bypass MATRIX_ALLOWED_ROOMS, MATRIX_FREE_RESPONSE_ROOMS, and
MATRIX_REQUIRE_MENTION, and use DM threading instead of
MATRIX_AUTO_THREAD/MATRIX_SESSION_SCOPE. This was previously only visible in
an inline code comment, not in the env-var docs an operator would read.

Fixes #114733
2026-09-20 10:42:25 -07:00
liuhao1024
1920b924bc fix(kanban): NFC-normalize worktree path identity checks
macOS hands back DECOMPOSED path strings (NFD) for names stored in
composed form (NFC), so raw Path equality in kanban_db_workspace
reported a real repo root as 'not inside a git repo' and worktree
dispatch died with ValueError for every task on such a board.

Add _path_key() (NFC-normalized identity key) and route the four
path-identity comparisons on the dispatch path through it:
_resolve_worktree_workspace, _ensure_git_worktree,
_cleanup_worktree_workspace, and the fallback branch.

Patch authored by the issue reporter; independently verified against
main by @KeyArgo (RED/GREEN + collateral). The reporter's test file
runs green locally on macOS including both macos_only end-to-end
rows (3 passed).

Fixes #115080
2026-09-20 10:41:25 -07:00
teknium1
8155f4357b fix(acp): run the cwd backfill once when the session manager first opens the DB
backfill_acp_session_cwd had no production caller, so rows minted before
the column was written stayed unassigned in Desktop until someone ran it by
hand. The manager now runs the idempotent UPDATE once per process on first
DB use (injected or acquired), with a test through create_session().
Also maps the contributor email for attribution.
2026-09-20 10:40:48 -07:00
Jeff
b58146cec3 fix(acp): persist session cwd and git metadata to the sessions table
ACP sessions stored their workspace only inside the model_config JSON blob
(a correct choice when the cwd column did not exist yet), so Desktop, the
Projects sidebar and hermes sessions list showed every editor session as
unassigned. create_session now passes cwd, an existing row (the live path,
since the agent flushes the transcript incrementally) gets the column
promoted via update_session_cwd, update_cwd() moves it on reopen, and git
branch/root are probed off the interactive path under the same generation
contract tui_gateway/session_workdir.py uses. backfill_acp_session_cwd
promotes model_config.cwd for rows minted before this change.

Squashed from the five commits of #115707; the accidentally committed
Windows cache files under %SystemDrive% are dropped.
2026-09-20 10:40:48 -07:00
Tyler Lyon
18f4b29daf fix(cli): symmetric Windows resume-failure handling + atexit disarm
Two distinct defects in the Windows gateway pause/resume path, one
symptom: "hermes update" aborting exit 1 on a checkout that was
already current and needed no code change.

1. Asymmetric error handling between the two finish paths. The pull
   path wraps the Windows gateway resume in
   _resume_windows_gateways_and_merge_outcome, whose contract is
   explicit: "Must never abort the update" -- it catches the failure,
   marks the outcome incomplete, warns, and continues (receipt status
   "partial", exit 1 only via the existing incomplete-repair gate).
   _finish_already_up_to_date ("Already up to date" path) called
   _resume_windows_gateways_after_update bare, so the identical
   RuntimeError -- e.g. the relaunch-verification race with a parent
   Job Object kill, #48820 -- killed the run as an unhandled exception
   instead. Fixed by routing through the same merge helper and folding
   a failed resume into current_checkout_complete, landing on the
   pre-existing "partial" finalize + sys.exit(1) contract the repair
   path already uses for other incomplete states.

2. The atexit callback could replay the same failure a second time.
   Every foreground call site registers
   _resume_windows_gateways_after_update via atexit.register as a
   dead-process safety net, but the function only cleared
   token["resume_needed"] on its OWN success path -- a raise from
   _verify_relaunched_gateways_alive left the registration live, so
   interpreter teardown called the function again with the same
   still-armed token, repeated the whole relaunch + verify, failed
   identically, and printed "Exception ignored in atexit callback".
   Fixed by unregistering the function from atexit as soon as
   execution reaches it (foreground or the atexit fallback itself) --
   ownership transfers to whichever call site got there first, and a
   failure below is reported exactly once. atexit.unregister is a
   no-op when the function was never registered, so a non-Windows or
   no-resume-needed call is unaffected.

Both fixes are surgical: they leave the "a failed relaunch keeps
resume_needed=True so the token is retryable" contract completely
untouched (see the existing
test_resume_windows_gateway_service_failure_stays_retryable /
test_resume_windows_gateway_launcher_refresh_failure_stays_retryable
tests, which this PR does not modify) -- that design is intentional,
not the bug.

Closes #115563.

Testing: Windows-specific behavior verified by code trace + full
resolution-chain tests against the real functions (mocked I/O, real
control flow) on Linux/macOS, per the "Don't fake the host OS" rule
this repo's own AGENTS.md sets -- no sys.platform patching, and the
Windows-only branch (_is_windows() == True) is exercised exactly as
the existing test suite for this module already does (monkeypatching
_is_windows on the shared test fixtures, not faking the platform).
True live-Windows process-topology proof is out of scope for this PR
(the wine2e lane is reserved for process-topology E2E per AGENTS.md);
this fix is control-flow/bookkeeping, not host-specific syscalls.

- 2 new regression tests, both proven red on the pre-fix code via
  git stash (both fail with the exact defect signature: the "Already
  up to date" path re-raises instead of demoting to partial; the
  atexit unregister call never happens) and green on the fix
- scripts/run_tests.sh across the full affected surface (Windows
  update/resume/reconciliation + related pause/resume/venv-repair
  test files): 228 passed, 0 failed, 4 skipped (pre-existing,
  unrelated to this change)
2026-09-20 10:40:12 -07:00
chelsealong
a0c7bc1a45 docs(sessions): fix DM topics prerequisite in handoff docs
Threaded Mode is a bot-owner setting enabled via BotFather, not a
per-chat toggle. Same bug class as 18f2828e87, flagged as a residual
spot on PR #115025.
2026-09-20 10:39:37 -07:00
chelsealong
0c872b611c fix(telegram): point dm_topics prerequisite text at BotFather Threaded Mode
The dm_topics "not a forum" warning and the matching docs section told
users to tap the bot's name in the DM and toggle "Topics" in chat
settings. That toggle only exists for group forums; a bot DM has no
such control. The actual prerequisite is Threaded Mode, enabled by the
bot owner via the BotFather Mini App (Bot Settings -> Threads
Settings) -- already documented correctly a few sections later in the
same file, under the /topic prerequisites.

Fixes #115019
2026-09-20 10:39:37 -07:00
teknium1
d5c2e0fb16 fix(env): a parent-injected dashboard session token survives the .env reload
A launcher that spawns `hermes dashboard` (Desktop shell, link-style
integrations) mints HERMES_DASHBOARD_SESSION_TOKEN into the child's
environment and keeps the same token for its own /api probes. Every
dotenv layer in load_hermes_dotenv() loads with override=True, so a
persisted HERMES_DASHBOARD_SESSION_TOKEN line in ~/.hermes/.env replaced
the injected token: the child authenticated with the persisted value and
the parent got HTTP 401 from its own child.

Treat the token as a spawn credential in the dotenv publisher: a value
that dotenv did not put into os.environ (tracked by _DOTENV_PUBLISHED)
is left alone, while a value an earlier pass published still reloads,
so .env edits and home switches behave as before. Other keys, including
the documented HERMES_DASHBOARD_PUBLIC_URL, keep .env-wins precedence.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: fangliquan <fangliquan@qq.com>
2026-09-20 10:39:01 -07:00
Apostol Apostolov
8e887332ab feat(plugin-catalog): pin intelligent-tool-break public edition 2026-09-20 10:38:46 -07:00
apoapostolov
696f0786da feat(plugin-catalog): pin intelligent-tool-break public edition 2026-09-20 10:38:46 -07:00
Apostol Apostolov
c8fe333483 feat(plugin-catalog): pin intelligent-tool-break catalog card 2026-09-20 10:38:46 -07:00
Apostol Apostolov
b85c552e26 feat(plugin-catalog): add intelligent-tool-break 2026-09-20 10:38:46 -07:00
esketids
26cfdf0974 chore(plugin-catalog): final pin 3f2478ab6e795da54547e7a4b162dd207d73079a (docs; no gui/ in tree) 2026-09-20 10:38:05 -07:00
esketids
506d407dcc chore(plugin-catalog): follow the pin to 841e7e4f5ade966f0fc45d5c30565ee6a4c9a366 (README-only delta; still no gui/) 2026-09-20 10:38:05 -07:00
esketids
eeddf9f3d3 chore(plugin-catalog): re-pin openrouter-voice to 885ea0b
Drops gui/ from the pinned tree per review (a catalog entry may not ship an app patch); the patch stays
on the plugin repo's gui-rows branch. Both gates re-checked at the new sha.
2026-09-20 10:38:05 -07:00
esketids
3ead0d79ef chore(plugin-catalog): re-pin openrouter-voice to 1139d37
Adds the microphone/speaker picker patch and its wiring. Both gates re-checked at the new sha.
2026-09-20 10:38:05 -07:00
esketids
a9809537b2 chore(plugin-catalog): re-pin openrouter-voice to 53fcb89
Adds the preview-button patch for the voice/speed rows. Both gates re-checked at the new sha.
2026-09-20 10:38:05 -07:00
esketids
6fe9d7118b chore(plugin-catalog): re-pin openrouter-voice to 87c5ce5
Adds per-model speed handling (measured ranges + local time-stretch) and the voice sampler.
Both gates re-checked at the new sha.
2026-09-20 10:38:05 -07:00
esketids
3d56f1877b chore(plugin-catalog): re-pin openrouter-voice to 7d535dd
Adds the per-model voice catalogs (361 voices), the refreshed model lists (21 STT / 18 TTS) and
the CLI listing. Both gates re-checked locally at the new sha.
2026-09-20 10:38:05 -07:00
esketids
83d5344e11 chore(plugin-catalog): re-pin openrouter-voice to 31ba32d
The entry now carries the GUI-row patch and the audio tests, so the pin moves with them
(README rule 4 — a new PR, re-reviewed as a SHA bump). Both CI gates re-checked locally:
the structural validator passes and the plugin at the new sha passes
`hermes plugins validate`.
2026-09-20 10:38:05 -07:00
esketids
97d925dfae feat(plugin-catalog): add openrouter-voice (community, voice)
Adds the OpenRouter voice plugin: speech-to-text and text-to-speech providers for
stt.provider / tts.provider = openrouter, reusing OPENROUTER_API_KEY. Stdlib only.

Owner submission (README rule 5): the plugin repository's owner is opening this PR.
Pinned to a1b01810cdd6be15be61f2c6f6e05e24dafbba14 (rule 2) — exact 40-hex, no
branch or tag.

Policy compliance:
- rule 3 (no self-updating code): pure Python; the plugin never fetches GitHub and
  never rewrites its own distribution. Updates reach users only through
  `hermes plugins update` and a SHA-bump PR here.
- rule 6 (declared capabilities match reality): registers no tools, hooks or
  middleware; requires OPENROUTER_API_KEY, which is the only env var it reads.

Both CI gates checked locally before opening: scripts/validate_plugin_catalog.py
passes on the whole directory, and an anonymous clone at the pinned sha followed by
`hermes plugins validate` passes.
2026-09-20 10:38:05 -07:00
teknium1
0a42929527 chore: map contributor email for attribution check 2026-09-20 10:37:29 -07:00
armandokun
a0e252d8f1 Re-pin openmail to bc8655e (v0.1.1) after security hardening
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 10:37:29 -07:00
Armandas Vaičikauskas
1291b43cbc Add openmail to the plugin catalog 2026-09-20 10:37:29 -07:00
ahmed-3li
6f05346df6 Add markifact plugin 2026-09-20 10:36:50 -07:00
teknium1
8109c400a2 fix: late resume/submit from a closed socket keeps the ws-orphan reap armed
A WebSocket that drops right after sending session.resume or prompt.submit
has its disconnect cleanup (_close_sessions_for_transport) run before the
RPC's rebind. The rebind then registered the already-closed socket and
cancelled the pending orphan reap; nothing ever detaches that socket
again, so the detached session kept its active-session lease (surface=ios
Bot Chat) with no Timer until the 300 s idle-reaper repair sweep, and the
canonical Bot Chat stayed "connecting" for the next client (#116464).

_rebind_live_transport now treats a dead rebinding transport as "client
not back": it leaves the reap armed (re-arming it when the caller already
cancelled, as the reuse fast path does) and does not register the dead
socket as a viewer. prompt.submit goes through the same seam instead of
its own attach + unconditional cancel.

Live: loopback dashboard rig, grace 3 s, ios session, owning socket aborted
mid-submit + 12 resume-then-drop sockets. Before: lease still held 15 s
later, no reap logged. After: lease released within the grace, reap logged,
fresh resume returns the stored history. Control (plain 1001 close + reply-
awaiting resume/close burst) still releases.
2026-09-20 10:35:26 -07:00
teknium1
5cabfa5d13 fix(desktop): room settings offer per-member Compress history for hidden member sessions
Bot Mode room members each keep a hidden `Group: <roomId> · <thread>`
plumbing session that grows with every drive. When it bloats the member
degrades into empty replies, yet nothing could compress it: the session is
hidden from every list, the focused-chat /compress never targets it, and
`-q` chat forwards slash text to the model. Group settings now lists each
member with a "Compress history" action that resumes every session the
room holds for that member (thread-scoped pointers, the legacy pre-thread
pointer, or the pre-thread title as last resort) and runs the gateway's
session.compress against the live runtime id — the exact RPC pair the
reporter had to issue by hand over JSON-RPC. A 4007 (no such session)
skips; anything else surfaces in the toast. requestForBot/host.request
gain an optional per-request timeout so the 660 s compress budget the
focused-chat /compress already uses applies here too instead of the
socket's 30 s default reporting a false timeout.

Slim redo of #102333 by @fangliquanflq (same action, without the
auto-hygiene sweep and the group-turns facade appendage).

Co-authored-by: fangliquanflq <fangliquanflq@users.noreply.github.com>
2026-09-20 10:34:45 -07:00
彬少
77eba560bb chore(plugin-catalog): re-pin prompt-enhancer to v1.3.0 (compliance head, #116031/#116030 review) 2026-09-20 10:34:11 -07:00
彬少
20d7521fab feat(plugin-catalog): add prompt-enhancer 2026-09-20 10:34:11 -07:00
teknium1
ae7bf98971 fix(desktop): Bot chats speak through their own (connection, profile) in tiles and the main pane
Review follow-up on the Bot voice routing (#100864, salvage #101545 @FalconOrtiz).

- Owner identity is (connection, profile) per the Bot Mode standing ruling:
  ComposerScope publishes `connectionId` beside `profile`; `voice-client-direct`
  keys its config cache and `hermesApi` scope on both (new `ownerScoped` in
  api/client.ts, beside `profileScoped`), and `voice-playback` mints the
  speak-stream against the OWNER connection (`getConnectionFor`) and pins the
  POST fallback the same way. Two `default` Bots on two gateways no longer
  collide in the cache or mint against the active gateway.
- Main pane: `ChatRuntimeBoundary` publishes the session owner hint's
  (connection, profile) on the composer scope when the ambient scope has no
  owner, so a Bot chat opened in place (openStoredBotChat) speaks with its
  owner voice; a tile's scope already names its owner and is kept as is.
- Tests: the two direct helper tests are folded into (1) a production-path
  test that renders useAutoSpeakReplies under a Bot scope and asserts every
  REST audio leg carries the owner (red when the hook's wiring is reverted to
  base AND when only `profile` is threaded), and (2) the routing test on
  resolveSpeakStreamUrl with an owner object.
- Docs: the fallback sentence matches the code (profile server defaults, active
  profile only for ownerless chats); STT stays on the active profile.

Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
2026-09-20 10:34:04 -07:00
teknium1
5e691f9e06 docs: Bots speak with their own profile's TTS voice
Document the per-Bot voice behaviour restored by the voice-playback owner
profile change, including the active-profile fallback.
2026-09-20 10:34:04 -07:00
teknium1
f3d98d4fda fix(desktop): Bot chats speak with the Bot's own profile TTS voice
Voice playback resolved its (connection, profile) scope from the ACTIVE
gateway profile (`getApiRequestProfile()`) on every leg of the ladder —
client-direct `fetchVoiceClientConfig`, the speak-stream WS URL and the
`/api/audio/speak` relay — so every Bot spoke with the active profile's
voice regardless of its own `tts.*` config.

`VoicePlaybackOptions.profile` now carries the speaking session's owner
profile; `fetchVoiceClientConfig`/`directTtsConfig`, `resolveSpeakStreamUrl`
and `speakText` accept it and fall back to the active profile when absent.
The session tile publishes `ownerRoute.targetProfile || ownerRoute.profile`
on its ComposerScope, and the three speakers (read-aloud button, auto-speak,
voice conversation) pass it through. The config cache keys on the owner
profile so two Bots never share credentials.

Slim redo of #101545 (@FalconOrtiz).

Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
2026-09-20 10:34:04 -07:00
彬少
b3d7c06fe4 chore(plugin-catalog): re-pin prompt-snippets to v1.5.1 (compliance head, #116031/#116030 review) 2026-09-20 10:33:32 -07:00
彬少
b95c822f5a feat(plugin-catalog): add prompt-snippets 2026-09-20 10:33:32 -07:00
teknium1
d147a3794b fix(desktop): reveal is hidden per focused workspace and the Projects menu toasts a missing path
Review follow-up on the slim redo of #115168 (@jonpol01).

- `store/projects.ts::revealPath` (Projects kebab, worktree header) routes
  through `revealFile`, so a path that is not on this computer toasts
  `fileMenu.revealMissing` instead of silently showing nothing; both menus
  drop the reveal item on a remote backend (`isDesktopFsRemoteMode`), like
  the file trees.
- Statusbar "Open containing folder" is gated on the FOCUSED session's owner
  (`isSessionRemote(focusedStoredSessionId)`), not the window's primary
  connection: a Connections-tagged tile on a remote gateway inside a
  local-primary window hides it, and a local tile in a remote-primary window
  keeps it (#115167's per-tile rule).
- The facade unit test is replaced by a statusbar-rendering test that drives
  `useStatusbarItems` under a remote connection and under a focused remote
  tile in a local window; it goes red when only the hook's gate is reverted.
- `fileMenu.revealMissing` in every locale (ar, ja, ru, zh, zh-hant).

Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
2026-09-20 10:33:24 -07:00
teknium1
d58831c36b fix(desktop): "Open containing folder" is hidden for remote workspaces and reports a missing path
Two seams made the statusbar's "Open containing folder" a silent no-op on a
remote bot's workspace: `hermes:fs:reveal` returned true after
`shell.showItemInFolder`, which silently no-ops on a path that does not exist
on this computer, and `revealDesktopPath` discarded the bridge boolean so
`revealFile` had nothing to toast. The statusbar also built the item
unconditionally while the sidebar trees already hide reveal on a remote
backend (`isDesktopFsRemoteMode`).

- `fs-ipc.ts`: reveal returns false when `fs.existsSync(target)` is false,
  before touching the file manager.
- `desktop-fs.ts::revealDesktopPath`: throws `fileMenu.revealMissing` when
  the bridge answers false, so `revealFile` toasts instead of reporting success.
- `use-statusbar-items.tsx`: the reveal item is dropped when the connection is
  remote, mirroring `file-actions.tsx`.

Slim redo of #115168 (@jonpol01).

Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
2026-09-20 10:33:24 -07:00