#1 GitView: hide/disable 'open in editor' for external linked worktrees
(path outside session workspace is rejected by the fence); add
isWithinWorkspace client helper for the gate.
#2 DiffTab: untracked diff read resolves against diff.repoRoot (child-repo
paths are repo-root-relative, not cwd-relative); fs.read route threads
selectedRepoOf into resolveGitPath so the host resolves the right base.
#3 GitView: chooseRepo handler mirrors chooseWorktree — clears status/
history/log and bumps generation before async refresh, preventing stale
rows from acting on the newly selected repository.
#4 git.ts: repoRoots/repoRoot compare roots via pathIdentity() (platform-
aware) instead of raw includes(), fixing win32 forward/backslash
mismatch that silently fell back to roots[0].
#5 markdown-images: mask fenced code blocks and inline code spans before
rewriting; only rewrite reference definitions whose label is actually
referenced by image syntax (not plain links). +3 tests.
#6 package.json: downgrade unrun ^0.3.1 → ^0.2.39 (0.3.x requires Node
^22.13, incompatible with declared engines >=20).
#7 market-manifest.spec: shell:true on win32 so execFileSync resolves the
pnpm .cmd shim.
#8 smoke.spec: build html-route URL via encodeHtmlUrl() (production
encoder) so the Windows drive separator is correct.
#9 index.ts git.worktrees route: thread selectedRepoOf so a workspace
container's selected child repo's linked worktrees appear.
PR #285 added the zh/en 'worktree' key but the 19 third-language dicts
(de/fr/pt/ko/ar/hi/id/tr/vi/th/ru/it/nl/sv/pl/zhHK/zhTW/zhMO) were missing
it — only ja was patched in the prior commit. The dicts are typed
Record<keyof typeof zh, string> via an unchecked 'as' cast, so tsc did
not catch the gap.
This commit:
- adds the worktree translation to all 19 dicts
- strengthens tests/locales.spec.ts to assert key-set equality for EVERY
shipped dictionary (not just zh/en/ja), preventing future regressions
Sidebar.tsx read ctx.get('betterLocale') unconditionally. In real
cordis hosts ctx.get exists and returns undefined when better-locale
is absent — but the unit-test mocks build a minimal ctx without a
get method, throwing 'TypeError: ctx.get is not a function' inside
<Sidebar> render and taking down 7 tests
(bottom-auto-terminal + sidebar-crash suites).
Add a typeof guard so the optional contract holds even when ctx.get
is missing. The activation-order-safe ctx.effect path in index.tsx
is unaffected (real cordis Context, runs once).
- bump version: package.json / dsh.plugin.json / SIDEBAR_SERVICE_VERSION
- README / README_EN: feature tour converted to a table (two screenshots
per row, saves space); add v0.15.2 changelog (file-tree open-in-app
menu #334, tab context menu #331, diff collapse by default #270, prune
empty restored panes #268, hide git windows on Windows #301, untracked
files inside new folders #242, toggle/drag per-frame re-render perf
#315); community QR codes landed in #325
* feat(editor): file tree 'open with' submenu (external editors, pin, SSH-remote, custom editors)
The file tree right-click menu gains an 'Open with >' submenu with the
resource manager (reveal in file manager), VS Code, Cursor and Zed, plus
user-defined custom editors (name + URL template + VSCode-family flag).
Each submenu row carries a pushpin that promotes the target to a direct
top-level menu item.
A configurable SSH host (settings: Files card gear) switches VSCode-family
openers to <scheme>://vscode-remote/ssh-remote+<host>/<path> and hides
host-local targets (file manager / Zed / non-VSCode custom editors).
External opens run through the new fenced /sidebar/api/open.external route
which spawns the platform opener with an argv array (no shell); the client
never navigates raw scheme URLs itself. The settings seam's render panel now
appends after the declarative rows instead of replacing them.
* feat(editor): polish open-with menu UI — brand icons, pushpin, chevron, row alignment
- Submenu rows: the custom label is now a block-level flex row filling the
primitives itemLabel, so the pushpin docks at the row's far right (matching
the trailing check of every other menu); menu row icons normalized to 16px
(DSH native menus use 16, ours were 14).
- The submenu parent row gains a right-aligned chevron (the primitives Menu
renders no arrow for submenu parents).
- Icons via react-icons: codicons VscPin/VscPinned pushpins (replacing the
hand-drawn pin), VscFile/VscFolder/VscFolderOpened tree glyphs (replacing
the </> file mark), VscLinkExternal for the open-with parent and the
browser tab's open-external button (globally), SiCursor/SiZedindustries
brand silhouettes; the VS Code mark is inlined from simple-icons@11.0.0
(CC0) as IconVscode16 since later simple-icons releases dropped it. All
monochrome currentColor to follow the active skin.
- tsdown: pin react-icons si/vsc to their ESM entries via resolve.alias —
the package's exports map lists require before import, so the shared
conditionNames picked the unshakeable CJS entry and bloated the core
bundle by ~6.4 MB; tree-shaken it now adds only ~12 KB.
Right-clicking a tab now takes over the browser menu and shows four
close operations scoped to the current pane:
- Close
- Close Other Tabs
- Close Tabs to the Left
- Close Tabs to the Right
Items gray out when nothing is eligible (single tab / strip ends).
Bulk closes reuse the per-tab onClose path, so descriptor onClose
callbacks and terminal pty release still run for every close. Labels
are i18n'd (zh/en).
The ui-cordis CordisPanel is position:fixed at z-index 30; the unified
panel host at 40 made the bottom workbench cover the cordis inventory
and approval surfaces. Host now sits at 25 (above AppFrame overlay 20,
below cordis 30 and the 100+ float stack). Update skin contract docs
and add a mount e2e assertion for the layering band.
Switching from session A to B unmounted A's TerminalView, whose socket
close the host treated as a transient drop and started the 30s reconnect-
grace countdown — leaving the user in B for >30s killed A's shell, so
switching back respawned a fresh one instead of reattaching.
Add a third control frame {type:'park'} distinct from refresh/crash
drops: the client sends it on unmount when the tab is still open in its
session's state but the active session has moved on; the host marks the
pty as parked and skips the grace countdown. Switching back (open()
cancels the parked state) or explicitly closing the tab (close frame's
scheduleClose clears it) resume the normal lifecycle. Agent terminals
skip park — their lifetime is already indefinite on a bare drop.
* fix(sidechat): split context injection from the first user message
The first composer message rode the same user/message event as the side
boundary prompt (+ parked snapshot), and the transcript dropped that whole
row by its boundary prefix — swallowing the user's first question.
Deliver first contact as TWO messages: the boundary+snapshot rides
agent.inject (queued next-step context, claimed first at the opening step,
no wake) stamped with source kind 'plugin', and the question is the
follow-up that wakes the driver. The log records injection and question as
separate events; the transcript maps injections (plugin-sourced, or the
legacy boundary-prefix blob of pre-split threads) onto a collapsible
injection row and renders genuine user messages — the first one included —
as user rows. boundaryDelivered still recognizes the injection by its
unchanged boundary-prefix opening.
* feat(sidechat): codex-style collapsible rows and creating shimmer
Tool calls, thinking and context injections now share one quiet
single-line chrome (chevron + label + a one-line argument summary) that
expands into an indented body on a hairline thread — no cards or fills.
Streaming labels and the creating hero shimmer (shimmer = generating);
failed tools go danger; reduced-motion stills every loop.
* fix(sidebar): debounce subagent auto-open past the title frame
The host delivers a new child's origin and title in separate frames, so a
Side Chat thread's first visible frame lacks its 'Side: ' label and
tripped the 0→N auto-open — every thread creation popped the task page.
The trigger now rearms for 500ms and re-evaluates the original baseline
against the live snapshot, by which time the title filter recognizes the
thread. Genuine subagents still auto-open, half a second later.
* fix(sidechat): render legacy threads' first message as its own bubble
Threads created before the host split still log the boundary (+snapshot)
and the question as ONE 'user' message; the transcript mapped that row
wholly onto the injection row, so the user's first question only appeared
inside the expansion. The boundary prompt is a known constant, so the
legacy row now splits THERE: the injection row keeps the prompt, the
remainder renders as the user's real message (a pre-split snapshot, when
present, rides along — the common case is a pure question). Row React keys
become kind:seq so the split's two rows never collide.