- add a `copyAssetForPublish` transport (device-control default, desktop IPC
gated by the external-file approval path, device RPC + gateway with
workspace containment on the destination) so the outside-workspace
publish guide can relocate assets on every runtime
- build `copyFile` / `writeFile` for local, device and sandbox files
- let `useBlockedWorkspaceHtmlPublish` hand the confirmed plan to a caller-
supplied `runPublish` instead of always publishing inline
- show a loader instead of spinning the action icon while a toolbar action is busy
Claude-Session: https://claude.ai/code/session_019jVhraxkznG9MtkXaux657
* ✨ feat(device-control): list one project directory on demand
The project index asks Git for ignored paths with `ls-files --directory`,
which reports a fully ignored folder as a single entry. Its children are
never enumerated, so a caller rendering that entry as an expandable row
has nothing to show.
Mark those entries `collapsed` and add `listProjectDirectory`, which reads
one level of a directory from disk. Callers can then fill a subtree only
when someone opens it, so an ignored folder still costs nothing until then
and `node_modules` is never walked.
The flag is only set when the directory truly has no indexed children — a
nested `.gitignore` that re-includes part of the subtree leaves those rows
complete, and they stay unflagged.
Wired for both transports: an Electron IPC method on the desktop
controller, and a device RPC plus TRPC procedure for remote devices. Also
adds `trashLocalFiles`, which moves paths to the OS trash via
`shell.trashItem` rather than unlinking them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* 🔒 fix(device-control): resolve symlinks before enforcing project containment
`path.resolve` is lexical, so a symlink that lives inside the project but
points outside it passed the prefix check and then had its target
enumerated. Verified against a real temp fixture: listing a link to an
outside directory returned that directory's files.
Check containment on the real paths of both the root and the target.
Entries are still built from the lexical path, so their ids stay anchored
to the path the caller expanded rather than wherever the link resolved —
a link that stays inside the project keeps working and keeps its row.
Also return per-path outcomes from `trashLocalFiles`. A batch is not
atomic, so bailing out on the first failure left the caller unable to
tell which earlier paths were already in the trash, and therefore unable
to refresh its tree or retry only what was left. This matches the shape
`moveLocalFiles` already returns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* 🐛 fix(portal): guide outside-workspace HTML publishing
* 💄 style(portal): keep external-read consent above footer
* ✨ feat(portal): confirm external file access
* 💄 style(portal): align external-read consent typography
* 🐛 fix(portal): harden outside-workspace publishing
* 🐛 fix: secure external workspace HTML publishing
* 🐛 fix(portal): bound external publish reads and honour local base hrefs
- reject an external publish asset by `stat` size before reading it, on both
the device-control and Electron transports, instead of letting the renderer
find out after a multi-gigabyte base64 round trip
- keep a `<base href>` that escapes the workspace, so its refs are reported as
real outside-workspace dependencies instead of resolving back inside
- drop a local `<base>` from the relocated copy, whose resource hrefs are
already rewritten relative to its own directory
Claude-Session: https://claude.ai/code/session_017KB2isGJ1RDpSaHiv88Xto
* 🔧 chore(deps): register html-artifact in the standalone app workspaces
`apps/cli` and `apps/desktop` enumerate their workspace packages instead of
globbing, so `packages/device-control`'s new `@lobechat/html-artifact`
dependency could not resolve there.
Claude-Session: https://claude.ai/code/session_017KB2isGJ1RDpSaHiv88Xto
* ✨ feat(heterogeneous-agent): support Devin CLI
* 🐛 fix(heterogeneous-agent): reuse cached Devin authentication
* ✨ feat(heterogeneous-agent,cli): default Devin ACP to bypass permission mode
- Add `permissionMode` to `DevinAcpSessionOptions` and `SpawnAgentOptions`.
- Update `buildDevinAcpArgs` to place `--permission-mode` before the `acp`
subcommand, since it is a global `devin` flag.
- Default the CLI `lh hetero exec` to start Devin with `permissionMode: 'bypass'`
so headless connected-device runs avoid blocking on permission prompts.
- Support user-supplied `--permission-mode` in `extraArgs`, which takes
precedence and is moved to the correct position.
* ✨ feat(heterogeneous-agent): set Devin ACP session mode via session/set_config_option
The `--permission-mode` global flag only affects the Devin CLI process;
the ACP session's mode still defaults to `accept-edits`, so shell/edit
tools still trigger `session/request_permission` and show askUserQuestion.
After `session/new` or `session/load`, call `session/set_config_option`
with `configId: 'mode'` and the resolved permission mode to actually put
the ACP session into bypass (or user-override) mode. The resolved mode is
derived from the built argv so an explicit `--permission-mode` in extra
args still wins over the default.
Add regression tests for new sessions, resumed sessions, and user override.
* ✨ feat(heterogeneous-agent): apply Devin ACP initial model via session/set_config_option
Previously `initialModel` was only used as a fallback when `session/new`
returned no model. Devin's `session/new` always reports a default model
(e.g. `swe-1-7-medium`), so a user-selected `glm-5-2` was ignored and the
default was used.
Add `applyInitialModel()` to resolve the requested model against the
`configOptions` catalog and call `session/set_config_option` with
`configId: 'model'`. It matches by raw model value and by normalized
human-readable name (so 'glm 5.2' maps to 'glm-5-2'). When only a raw
`initialModel` is provided and the catalog is unavailable (e.g. resume),
it attempts to set the value directly.
Also preserve the updated model after setting the permission mode so the
final `devin_session` event reports the selected model.
Add regression tests for model selection, label matching, default
preservation, and combined permission mode + model setup. Update the
`spawnAgent` fake to expose a 'sonnet' option for the new behavior.
* ♻️ refactor(heterogeneous-agent): stop injecting --permission-mode bypass CLI flag
Devin ACP permission mode is now applied through `session/set_config_option`
(configId: 'mode'), which is the only reliable way to change the active ACP
session mode. The global `--permission-mode` CLI flag before the `acp`
subcommand does not change the ACP session's effective mode.
- `buildDevinAcpArgs` no longer injects a default `--permission-mode`.
- It still moves a user-supplied `--permission-mode` in `extraArgs` before
the `acp` subcommand for backwards compatibility.
- `DevinAcpSession.resolvedPermissionMode` falls back to
`options.permissionMode` so the default CLI `bypass` is still sent via ACP.
- Updated tests to expect `acp` without the default flag and assert the
`session/set_config_option` mode request.
* fix(devin): do not default to bypass permission mode and protect selected model from mode response
- Remove the hard-coded `permissionMode: 'bypass'` default for Devin in
`lh hetero exec`. Users can still pass `--permission-mode` via
`--agent-arg` when needed.
- Stop using the `session/set_config_option` mode response to overwrite the
model resolved from `applyInitialModel`. Permission-mode negotiation can
return config-options whose `model` value is stale/default, causing a
freshly selected model (e.g. `glm-5-2`) to be replaced with the Devin
default (`swe-1-7`) on `stream_start`.
- Update CLI tests to no longer expect the bypass default.
Refs LOBE-xxx (feat/devin-cli).
* fix(devin): keep default bypass permission mode while protecting selected model
Re-introduce the default `permissionMode: 'bypass'` for Devin ACP so
headless connected-device runs still do not block on permission prompts.
The previous change that removed it was too aggressive; the real bug is
that the `session/set_config_option` mode response was overwriting the
selected model. That guard in `DevinAcpSession` remains in place.
Refs LOBE-xxx (feat/devin-cli).
* ✨ feat(heterogeneous-agents): capture and emit Devin ACP usage as turn_metadata
- Forward `session/prompt` result `usage` from `DevinAcpSession` into the
`devin_prompt_completed` synthetic event.
- Add generic ACP `Usage` parsing to `TraeAcpAdapter`, plus `usage_update`
handling with a protected `extractUsageFromUsageUpdate` hook.
- Override `extractUsageFromUsageUpdate` in `DevinAcpAdapter` to parse
`_meta.cognition.ai/{input,output,cachedRead,cachedWrite,thought}Tokens`.
- Emit `step_complete` with `phase: 'turn_metadata'` so the frontend reducer
writes `usage` (and optional `cost`) onto the assistant message.
- Add `UsageData.cost` to mirror `MessageMetadata.usage`.
- Add `devinAcp.test.ts` covering prompt-result usage, `usage_update` _meta,
and fallback paths.
* ✅ test(heterogeneous-agents): add missing request params type in spawnAgent test fixture
* 🐛 fix(heterogeneous-agent): cancel device-side CLI process on user stop
Previously only remote-task hetero types (openclaw / hermes) triggered a
`cancelHeteroTask` dispatch to the device when the user clicked Stop.
Local-cli types (claude-code / codex / devin / trae / qoder) that also
run on a remote device via `dispatchAgentRun` were silently skipped —
the UI showed cancelled but the CLI process kept running.
Two fixes:
- Server `interruptTask`: drop the `isRemoteHeterogeneousType` guard so
any hetero type with a deviceId in runningOperation triggers the cancel
dispatch.
- Desktop `spawnLhHeteroExec`: register the spawned child process into
`platformTasks` via a new `onChildSpawned` callback so `cancelHeteroTask`
can find and kill it by operationId. Also add `detached: true` so the
process-group kill reaches the CLI without signalling the desktop app.
* 🐛 fix(cli): cancel lh connect device CLI process on user stop
The previous cancel fix only covered the desktop device path. When a
local CLI agent (devin/claude-code/codex/…) is dispatched through
`lh connect`, the spawned `lh hetero exec` child was never registered
into the task registry, so `cancelHeteroTask` dispatched from the
server returned "No task found" and the remote process kept running.
- `agentRun.ts`: spawn with `detached: true` and register the child PID
into `taskRegistry` on spawn, keyed by operationId. Remove the entry
on exit only if the exiting PID still owns the task.
- `taskRegistry.ts`: widen `TaskEntry.agentType` from
`'hermes' | 'openclaw'` to `string` so local CLI agent types can be
persisted.
- `heteroTask.ts`: `cancelHeteroTask` now signals the whole process
group via negative-PID kill (Unix) or `taskkill /T /F` (Windows),
with a 2s SIGINT→SIGKILL escalation — mirroring the desktop path.
* 🐛 fix(cli): add cancel-trace logging to diagnose device CLI stop path
Add [cancel-trace] logs at every hop of the cancel path:
- server interruptTask: logs when dispatching cancelHeteroTask, when
condition is not met (with runningOp/targetOperation details), and
the dispatch result/error
- agentRun.ts: logs when task is registered into taskRegistry, and
when it is removed on exit (or skipped due to stale PID)
- heteroTask.ts cancelHeteroTask: logs when received, when task is
found or not found, and each kill attempt
* 🐛 fix(server): seed deviceId/heteroType into runningOperation for local CLI hetero agents
Local CLI hetero agents (devin / claude-code / codex / …) dispatch to a
device via `dispatchAgentRun`, but `childOperation` only embedded
`deviceId`/`heteroType` for remote platform agents (openclaw / hermes)
guarded by `isRemoteHetero`. Without these fields in
`topic.metadata.runningOperation`, `interruptTask` could not find the
target operation and never dispatched `cancelHeteroTask` — so clicking
Stop in the web UI did nothing for `lh connect` devices.
Patch `runningOperation` with the resolved `dispatchDeviceId` and
`heteroType` after a successful local CLI hetero dispatch, handling
both root and child (group supervisor) operation shapes.
* 🔥 chore: remove [cancel-trace] debug logs
Clean up the temporary diagnostic logs added to trace the cancel path.
Keep only the production-level `interruptTask` and `execAgent` logs that
were already present or are useful for long-term observability.
* 🐛 fix(heterogeneous-agent): preserve cancel lifecycle state
* ♻️ refactor(heterogeneous-agent): simplify Devin ACP integration
* 🐛 fix Devin intervention review flow
* 🐛 fix heterogeneous agent cancellation
* 🐛 fix heterogeneous agent cancellation confirmation
* 🐛 fix(heterogeneous-agent): preserve Devin permission option IDs
* 🐛 fix: resolve CI failures after rebase
* 🐛 fix: route Devin verification and permissions
* 🧪 test(cli): isolate agent run cancellation registry
* 🐛 fix(cli): cancel full live agent process group
* ✨ feat(chat): show hetero model selector for device-dispatched runs
Device dispatch (explicit device, auto routing, and web-initiated runs on a
bound desktop) already forwards --model/--effort via buildHeteroExecArgs, so
hiding the selector only made previously-persisted picks apply invisibly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ♻️ refactor(quota): move Claude quota sampler into @lobechat/heterogeneous-agents
The fetcher (OAuth usage API + keychain/config-dir credential read) and the
snapshot cache are pure Node with no Electron dependency. Hoist them to a
shared quota-sampler entry so connected devices can run the same sampler,
and move the snapshot types to the pure quota entry (electron-client-ipc
re-exports them for existing IPC callers).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(quota): sample Claude Code quota from bound devices with 2-minute refresh
- getClaudeCodeQuota device RPC in device-control (cached sampler on the
lh-connect / desktop gateway host), exposed via deviceGateway and the
device.getClaudeCodeQuota lambda procedure
- web/desktop quota menu picks its transport from the resolved execution
target: local IPC or the bound device RPC, so web clients now show live
quota for device runs; auto/sandbox stay quota-less
- QuotaMenu polls on a 2-minute cadence while the tab is visible, with the
sampler fresh window at 90 s so each poll observes fresh usage data
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(quota): scope persisted snapshots to devices
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(desktop): preview workspace HTML files
* ♻️ refactor(mime): unify MIME detection via @lobechat/utils/mimeType
Fold the ad-hoc extension→MIME maps in desktop, CLI, and device-control
into a single buffer-aware resolver. `resolveMimeType(path, buffer)`
trusts file-type's magic-byte detection first, then falls back to
mime.getType with a text-sniff downgrade so ambiguous extensions like
`.ts` (video/mp2t vs TypeScript source) resolve correctly everywhere.
* fix: resolve html preview ci failures
* ✨ feat: support device visibility, workspace device sharing and user agent/device preference
- add workspace_user_settings table for per-workspace user preferences
- support sharing devices to workspace with visibility control
- workspace device list filter and device refresh
- private-pool empty state copy for workspace device manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🌐 chore: update i18n files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: address device sharing review findings
- gate local auto-default until workspace preference loads
- transfer row ownership to the sharer on confirmed cross-user overwrite
- surface removed device ids from removeMember for gateway unenroll
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: enforce device visibility on workspace RPCs and defer share enrollment
- gate every deviceId-taking route behind assertWorkspaceDeviceVisible so a
member holding another member's private deviceId cannot drive gateway RPCs
- add identityOnly probe to the enrollWorkspace device RPC so cancelling the
share overwrite confirmation no longer leaves a persisted enrollment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: address device routing and removal review findings
- carry activeDeviceScope through operation metadata so a workspace run
routed to the caller's personal device (per-user local override) addresses
gateway calls via the personal pool instead of workspace principal
- block private workspace device removal while its socket is still live and
the unenroll was not acknowledged, so the row keeps hiding the device
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: honor explicit --public on workspace device re-enroll
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: fail closed on cross-user private workspace device collision
deviceId is client-supplied, so a collision with another member's private
enrollment must not auto-publish their row — registerWorkspaceDevice now
throws WorkspaceDevicePrivateConflictError (mapped to CONFLICT at the
routers) and the shared-box flow recovers by publishing the device first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: reject private device conflicts before any gateway connection opens
- share flow pre-checks the probed identity for a cross-user private
collision before the real enrollment, and best-effort unenrolls on the
pre-check/write race
- lh connect --workspace aborts before connecting when registration is
rejected with CONFLICT/FORBIDDEN instead of connecting anyway
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: make workspace enrollment cleanup cover all row-write failures
- share flow best-effort unenrolls the just-opened connection on ANY
register failure, not only the private-conflict race
- lh connect --workspace aborts before connecting on ANY registration
failure — the row carries the private marker that hides the device
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: deep-merge agentDeviceOverrides so single-agent patches keep other agents
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: block workspace device deletion under any live unacknowledged socket
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: stabilize workspace device identity on fallback machines
Namespace the per-install seed (desktop stored UUID / CLI connection id)
into the workspace derivation so probe, enroll, and restore agree when the
OS machine id is unreadable — a raw seed would collide with the personal
deviceId, and no seed meant a fresh random id per call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix: scope lh device delete --workspace to the workspace pool
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(skills): restore device execution branch for execScript in gateway mode
* 🐛 fix: derive device execScript success from exit status and forward observation timeout
* ✨ feat: fall back to sandbox for legacy device clients without prepareSkillDirectory RPC
* 🐛 fix: validate zipHash, resolve project skill cwd, and surface still-running device commands
* 🐛 fix: surface device service failures reported inside a delivered envelope
* 🐛 fix: gate tool-execution activeDeviceId on the resolved plan and access policy
* 🐛 fix: block hidden sandbox APIs on device runs and surface saved-output paths
* 🐛 fix: forward device skill context to batched tool calls
* 🐛 fix: thread activatedSkills through server tool execution and handle grouped message shape
* 🐛 fix: show running indicator for still-running execScript and parallelize device skill prepares
* 🐛 fix: preserve filesystem/builtin skill activations without ids in step context
* 🐛 fix: skip unresolvable activations when resolving desktop exec cwd
* 🐛 fix: serialize same-hash skill preparation and publish via staging rename
* 🐛 fix: move reactivated skills to the end of the activation order
* 🔨 chore: broaden agent-tracing skill triggers for tool-call debugging
* ✨ feat(device): add "Create worktree" flow to the worktree switcher
Add a "Create worktree…" entry at the bottom of the WorktreeSwitcher
dropdown: it opens a modal to enter a branch name, previews the derived
sibling directory (<repo>-<branch>), runs `git worktree add -b <branch>
<path>`, then switches the conversation into the new worktree.
Wires the flow end-to-end across every layer:
- local-file-shell: `addGitWorktree` (mirrors the branch switcher's create
flow, reuses `isInvalidBranchRef` guard) + `GitAddWorktreeResult`
- device-control: register the `addGitWorktree` device RPC
- packages/types + electron-client-ipc: result types
- server: `deviceGateway.addGitWorktree` + `device.addGitWorktree` TRPC
- desktop: `GitController.addGitWorktree` IPC handler
- client: `gitService.addGitWorktree` (local IPC / remote device RPC)
- UI: WorktreeSwitcher entry + CreateWorktreeModal, live path preview,
inline error on git-level failure
- i18n: en-US + zh-CN device strings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 🔒 fix(device): derive worktree path server-side & thread workspaceId
Address review findings on the remote create-worktree RPC:
- P1: `device.addGitWorktree` no longer accepts `worktreePath` from the
request. The target is re-derived on the trusted server from `path` +
`branch` (a `<repo>-<branch>` sibling; branch folded to `-` so it can't
traverse), so a crafted authenticated web call can't ask a remote device
to check out at an arbitrary absolute path. `deriveWorktreePath` is
lifted to `@lobechat/types` as the single source shared by the renderer
(preview + local IPC) and the server (derivation).
- P2: thread `ctx.workspaceId` through `addGitWorktree` (and the sibling
`removeGitWorktree`, which had the same gap) so workspace-scoped sessions
route to the workspace-owned device pool instead of the personal one.
Local desktop IPC is unchanged — this machine owns its filesystem, so the
renderer-derived path is still forwarded there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 🔒 fix(device): avoid polynomial-ReDoS trim in deriveWorktreePath
The trailing-dash trim `/^-+|-+$/` is a polynomial-ReDoS shape (CodeQL
high): on a crafted branch like `a------a` the `-+$` branch retries at
each dash position, O(n²). Since the branch name is user input and this
runs per request on the server, strip leading/trailing dashes with a
linear scan instead. Behavior is unchanged (verified against the old
regex across representative inputs, incl. internal `--` preserved).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 🔒 fix(device): strip trailing path separators without anchored-quantifier ReDoS
CodeQL flagged the sibling `sourcePath.replace(/[/\\]+$/, '')` — same
polynomial-ReDoS shape as the branch trim, now reachable server-side with
an untrusted `path`. Strip trailing separators with a linear scan instead.
Behavior verified identical to the old regex across 77 path×branch combos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ✨ feat(project-file): replicate desktop file operations to web via device RPC
Project file tree operations only ran over Electron IPC, so remote/web
devices could browse files but not move, rename, or edit them. This wires
move/rename/write through the device RPC the same way getProjectFileIndex
already does, reusing the host-agnostic @lobechat/local-file-shell impls.
- device-control: whitelist moveLocalFiles/renameLocalFile/writeLocalFile + dispatch cases
- deviceGateway: moveProjectFiles/renameProjectFile/writeProjectFile (mutations throw on failure, no silent degrade)
- device router: matching device.* procedures
- projectFileService: deviceId-aware chokepoint methods (IPC locally vs RPC remotely)
- saveLocalFile now routes through projectFileService; remote LocalFile editor is no longer read-only
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ♻️ refactor(provider): rename ClientMode to CustomProviderDetail
ClientMode is a leftover from the old client-mode (IndexedDB) vs
server-mode (Postgres) DB split; there is no ServerMode counterpart and
the name no longer reflects what the component does — it renders the
detail view for a custom (user-created) provider, fetched by id. Rename
it to CustomProviderDetail and update the debugId + import.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🔒 fix(project-file): contain remote file mutations to the workspace root
Guard the move/rename/write device RPCs against paths escaping the project
root: these routes accept absolute paths from an untrusted browser session,
so the gateway now confirms every path stays within the working directory
(Windows-aware) before forwarding to a device. Thread `workingDirectory`
through the service and tRPC layers.
Also scope edit buffers by tab identity (device + working directory + path)
instead of bare file path, so the same path opened on two devices/workspaces
keeps independent unsaved content, and surface write failures so a failed
save keeps the buffer dirty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ feat(device): share remote-device gateway RPC between desktop and CLI
Extract the desktop's remote-device gateway RPC surface into a shared
`@lobechat/device-control` package and wire it into the CLI so `lh connect`
serves the same git / workspace / file device RPCs as the desktop app.
- local-file-shell: relocate all git operations (branches, working-tree
patches, branch diff, checkout/rename/delete/pull/push/revert) from the
desktop GitCtr into the shared package as pure functions
- device-control (new): the `executeDeviceRpc` dispatch + workspace scan +
portable file-preview / file-index defaults, with platform hooks injected
- desktop: GitCtr / WorkspaceCtr / GatewayConnectionCtr become thin wrappers
delegating to the shared package (local IPC path unchanged)
- cli: handle `rpc_request` over the gateway via the shared dispatcher
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ✅ test(device): cover git branch ops and device-control portable defaults
- local-file-shell: real-git integration tests for branch checkout / rename /
delete (+ validation), working-tree files & patches, revert, branch-diff with
no remote, and push / pull / ahead-behind against a bare origin
- device-control: defaultGetLocalFilePreview (text / image / accept filter /
workspace containment / missing file) and defaultGetProjectFileIndex (git
ls-files path + glob fallback)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(device): preserve directory entries in the glob project-file index
The CLI `getProjectFileIndex` glob fallback used `globLocalFiles`, which returns
only non-hidden file paths and no directory entries — so the Files tree builder
flattened nested files to the root and dropped dot-directories.
Walk with fast-glob (`dot: true`) and synthesize directory entries via the same
`collectProjectDirectories` path the git branch uses, so nesting and dot-dirs
(e.g. `.agents`) render correctly. Extracted a shared `buildEntries` helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>