@toddzheng024/dscode-desktop 0.7.40 → 0.7.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  DSCODE adds a coding preset, browser workflows, cross-session collaboration,
4
4
  memory and task management to the official DeepSeek Harness Desktop application.
5
5
 
6
- **Version:** 0.7.40 · **Harness runtime:** 0.2.0-rc.2
6
+ **Version:** 0.7.42 · **Harness runtime:** 0.2.0-rc.2
7
7
 
8
8
  Preview release candidate; publication is a separate step.
9
9
 
@@ -48,7 +48,7 @@ public release; a candidate build alone does not publish a package.
48
48
  ## Install a published version
49
49
 
50
50
  After this version is available on npm, enter
51
- `@toddzheng024/dscode-desktop@0.7.40` in **Plugins → Add plugin**,
51
+ `@toddzheng024/dscode-desktop@0.7.42` in **Plugins → Add plugin**,
52
52
  review the installation, install, then choose **Enable now**. Using the exact
53
53
  version keeps the installation tied to these instructions and compatibility
54
54
  requirements. The `preview` tag tracks the current Desktop preview release.
package/docs/CHANGELOG.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to DSCODE, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions match the npm launcher and the Hub profile.
3
+ For the self-contained terminal, `DSCODE_HOME` defaults to `~/.local/share/dscode-state`. Legacy installations retain their previous paths; see [migration](distribution.md#migration-from-older-dscode-installations).
4
+
5
+ All notable changes to DSCODE, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions identify the complete application release.
4
6
 
5
7
  Per-release notes live in [`docs/releases/`](releases/); the entries below summarise them.
6
8
 
@@ -8,6 +10,22 @@ Per-release notes live in [`docs/releases/`](releases/); the entries below summa
8
10
 
9
11
  ## [Unreleased]
10
12
 
13
+ ## [0.7.42] - 2026-10-12
14
+
15
+ ### Fixed
16
+
17
+ - Allow complete archive file listings above Node's default subprocess buffer limit. Version 0.7.41 failed with `ENOBUFS` before replacing the application; user data and the installed package were retained.
18
+ - Let the shell installer repair 0.7.41 with the newly verified package's updater, retaining target ownership and active-session checks. Rerun the curl installer once, then use `dscode update` for subsequent upgrades.
19
+ - Require full candidate archive replacement and effective user-setting preservation in both architecture qualification jobs. Small updater fixtures did not expose the original failure.
20
+
21
+ ## [0.7.41] - 2026-10-12
22
+
23
+ ### Changed
24
+
25
+ - Ship one self-contained terminal package per Mac architecture with private Node 24.21.0, pinned Harness, compiled TUI and native dependencies. Install and first launch no longer use npm or Hub.
26
+ - Keep terminal data outside the application, isolate existing DSH configuration and terminal preferences, and preserve explicit migration paths for older installations.
27
+ - Replace the complete application only after digest, version, platform and module checks; restore the old program on failed replacement and never move user data. Generate a Homebrew formula for the same archives; external tap publication remains separate.
28
+
11
29
  ## [0.7.40] - 2026-10-12
12
30
 
13
31
  ### Added
@@ -415,7 +433,7 @@ The 0.7.37 candidate was cancelled before publication after its Node 22.19.0 cov
415
433
  ### Fixed
416
434
 
417
435
  - Code review asks for thinking off instead of the lightest reasoning level. A DeepSeek reviewer spent its whole 90-second deadline reasoning at `low` (24k-56k reasoning tokens per call) and never wrote a verdict, so reviews of a real diff came back incomplete; with thinking off the reviewer so the deadline is spent on the report instead of tokens that never reach it; a model without an `off` level keeps its own default. `DSCODE_REVIEW_EFFORT` still overrides the level.
418
- - DSCODE's credentials provider mounts again in a source checkout. A patch can only override a row's config keys and is skipped when its `name` differs from the row it targets, so the overlay that pointed the upstream `credentials` row at DSCODE's provider had mounted nothing outside the npm bundle (which rewrites that name at build time). The upstream row is disabled and DSCODE's provider inserted in its place: the shared `~/.dscode/credentials.yaml` store, the read-only Grok CLI login and the per-provider credential facts now agree on both surfaces, and a key saved before this change still resolves through the profile store.
436
+ - DSCODE's credentials provider mounts again in a source checkout. A patch can only override a row's config keys and is skipped when its `name` differs from the row it targets, so the overlay that pointed the upstream `credentials` row at DSCODE's provider had mounted nothing outside the npm bundle (which rewrites that name at build time). The upstream row is disabled and DSCODE's provider inserted in its place: the shared `$DSCODE_HOME/credentials.yaml` store, the read-only Grok CLI login and the per-provider credential facts now agree on both surfaces, and a key saved before this change still resolves through the profile store.
419
437
  - The composer no longer rides up on a static flush. Ink clears its repaint ledger every time `<Static>` history is written, so the rows a shrinking frame freed are now kept reserved across that clear (ink.js reports how many rows the static write consumed). The only-ever-pushed-down guarantee covered frames between flushes before this.
420
438
  - A running shell command keeps its own output window: every sanitized chunk feeds a per-command buffer, the tool reads that buffer instead of re-reading the shared scrollback on every poll, and a poll without new output no longer replaces the accumulated text with the current viewport. A chatty neighbour on the same terminal — a background job, a release publisher, another session — can no longer make the shell report a dropped beginning for output that was never the command.
421
439
 
@@ -495,7 +513,7 @@ The 0.7.37 candidate was cancelled before publication after its Node 22.19.0 cov
495
513
 
496
514
  - `/model` searches as you type, with no `/` first, and ranks with BM25 on every keystroke: the model name counts most, then the model id, then the provider. The word being typed matches as a prefix (`kim` finds Kimi, `glm5` finds GLM 5.x); rows matching every word come first, and rows matching some words show only when none matches them all. A changed search focuses no row: Enter or an arrow focuses the first match, and Enter on a focused row selects it. Esc clears the search, then closes; Tab still opens providers and retry moves to Ctrl+R, since letters now always search.
497
515
  - Pressing Enter on `/provider`, `/login` or `/openrouter` in the slash-command menu opens its picker straight away, instead of leaving the command in the input for another Enter or a typed argument.
498
- - `/openrouter` shows the OpenRouter account: the balance, this key's limit and its spend today, this week and this month, and, with a management key, every API key's usage and the last 30 days of spend by model and the providers that served it (`r` refreshes, `m` sets the management key). After the OpenRouter API key is saved in `/provider openrouter` or `/login openrouter`, DSCODE offers an optional management key; it is checked against OpenRouter (an inference key is refused account activity), stored beside the other keys in `~/.dscode/credentials.yaml` (or taken from `OPENROUTER_MANAGEMENT_KEY`), and only read from.
516
+ - `/openrouter` shows the OpenRouter account: the balance, this key's limit and its spend today, this week and this month, and, with a management key, every API key's usage and the last 30 days of spend by model and the providers that served it (`r` refreshes, `m` sets the management key). After the OpenRouter API key is saved in `/provider openrouter` or `/login openrouter`, DSCODE offers an optional management key; it is checked against OpenRouter (an inference key is refused account activity), stored beside the other keys in `$DSCODE_HOME/credentials.yaml` (or taken from `OPENROUTER_MANAGEMENT_KEY`), and only read from.
499
517
  - A running compaction, automatic or `/compact`, replaces the activity line with a small Tetris bot clearing rows and the message "Compacting context, please wait" (translated with `/language`), plus its elapsed time. A short terminal shows one board row.
500
518
  - Picking a model in `/model` (or through `/provider`) whose route would compact the current context asks first: it shows the context size, the new model's threshold and window, and whether the next request overflows. `y` switches, `n`/Esc goes back.
501
519
 
@@ -551,7 +569,7 @@ The 0.7.37 candidate was cancelled before publication after its Node 22.19.0 cov
551
569
 
552
570
  ### Changed
553
571
 
554
- - `/login [deepseek|openrouter]` stores either key; without an argument it targets the current provider. `OPENROUTER_API_KEY` joins `DEEPSEEK_API_KEY` in the shared `~/.dscode/credentials.yaml`, with the same environment precedence. An argument that is not a provider name is refused without being echoed.
572
+ - `/login [deepseek|openrouter]` stores either key; without an argument it targets the current provider. `OPENROUTER_API_KEY` joins `DEEPSEEK_API_KEY` in the shared `$DSCODE_HOME/credentials.yaml`, with the same environment precedence. An argument that is not a provider name is refused without being echoed.
555
573
  - The footer follows the active provider: its balance is that provider's (OpenRouter: credits minus usage from `/api/v1/credits`), OpenRouter calls are estimated from the pinned pi-ai catalog's list prices (ledger `priceVersion` `openrouter-pi-ai-0.85.1`), and the 🔥/❄️ peak marker shows only for DeepSeek. `/status` names the wire effort Ultra sends on each route, and a missing-credential error now points at `/login`.
556
574
 
557
575
  - `dscode exec` ships in the npm launcher. It runs the installed profile's exec runner with the same options, stdin prompt and exit codes as the source checkout; the argument parser and overlay moved to `plugins/exec/cli.mjs` so both entry points share them.
@@ -607,7 +625,7 @@ Interface languages, copyable text, a verbose view, accurate TPS figures, and st
607
625
 
608
626
  ### Added
609
627
 
610
- - `/language` switches the interface between English, Simplified Chinese, Traditional Chinese, Japanese, Korean and Spanish. The choice is stored in `~/.dsh/dsh-code/language.json`, with `DSCODE_LANGUAGE` as a temporary override. The activity line, agents line, welcome box labels, footer telemetry labels, input placeholders, hints and the doctor report are all translated.
628
+ - `/language` switches the interface between English, Simplified Chinese, Traditional Chinese, Japanese, Korean and Spanish. The choice is stored in `$DSCODE_HOME/tui/language.json`, with `DSCODE_LANGUAGE` as a temporary override. The activity line, agents line, welcome box labels, footer telemetry labels, input placeholders, hints and the doctor report are all translated.
611
629
  - `/verbose` (Ctrl/Alt+R) shows each step's `· Thinking:`, `· Tool Call:` and `Output:` in a dim style, with up to 8 lines of thinking and a pointer to Ctrl+O for the full history. The toggle is persisted.
612
630
  - npm publish tokens can be kept in the macOS keychain via `pbpaste | npm run publish:token store`.
613
631
 
@@ -1,5 +1,7 @@
1
1
  # DeepSeek account login
2
2
 
3
+ For the self-contained terminal, `DSCODE_HOME` defaults to `~/.local/share/dscode-state`. Legacy installations retain their previous paths; see [migration](distribution.md#migration-from-older-dscode-installations).
4
+
3
5
  DSCODE can authenticate the DeepSeek route with a **DeepSeek account** instead of an API key. The sign-in happens in your system browser; the grant is stored on this machine and the model route picks it up automatically.
4
6
 
5
7
  `/account` is the whole surface:
@@ -41,4 +43,4 @@ Set `DSCODE_ACCOUNT_LOGIN=0` to remove the listener entirely. `/account login` t
41
43
 
42
44
  ## Where the credential lives
43
45
 
44
- In the same local credential store as your API keys (`~/.dscode/credentials.yaml`, `0600`), under the account provider's own record. It never reaches the model: account state is a client-safe projection with no token in it, tool results and session logs never carry it, and the command's output is redacted like every other DSCODE command result.
46
+ In the same local credential store as your API keys (`$DSCODE_HOME/credentials.yaml`, `0600`), under the account provider's own record. It never reaches the model: account state is a client-safe projection with no token in it, tool results and session logs never carry it, and the command's output is redacted like every other DSCODE command result.
package/docs/acp.md CHANGED
@@ -12,7 +12,7 @@ To select an initial model explicitly, use:
12
12
  dscode acp --model custom-your-provider-id/your-model-id
13
13
  ```
14
14
 
15
- Without `--model`, the bridge uses DSCODE's saved default provider and model. Set up credentials and custom services in the normal TUI first with `/login` or `/provider custom`. The stable custom provider ID is recorded in `~/.dscode/providers.yaml`; it is not the service's display name. API keys stay in the existing credential store. Clients supporting ACP configuration options can select an advertised model and reasoning effort after creating a session.
15
+ Without `--model`, the bridge uses DSCODE's saved default provider and model. Set up credentials and custom services in the normal TUI first with `/login` or `/provider custom`. The stable custom provider ID is recorded in `$DSCODE_HOME/providers.yaml`; it is not the service's display name. API keys stay in the existing credential store. Clients supporting ACP configuration options can select an advertised model and reasoning effort after creating a session.
16
16
 
17
17
  The ACP model picker lists models from providers with configured credentials or account login, plus saved custom services using no authentication. Credential checks follow each provider's configured reference, including environment variables and saved keys. DeepSeek Account and DeepSeek API-key routes are checked separately. Providers with no recognized configuration metadata are omitted.
18
18
 
@@ -421,7 +421,7 @@ npm run eval:browser -- --model deepseek-flash
421
421
  ```
422
422
 
423
423
  To evaluate a saved custom service, use its `custom-…` ID from
424
- `~/.dscode/providers.yaml` and an explicitly configured model:
424
+ `$DSCODE_HOME/providers.yaml` and an explicitly configured model:
425
425
 
426
426
  ```bash
427
427
  npm run eval:browser -- --provider custom-YOUR-ID --model your-model-id
@@ -1,5 +1,7 @@
1
1
  # Custom model providers
2
2
 
3
+ For the self-contained terminal, `DSCODE_HOME` defaults to `~/.local/share/dscode-state`. Legacy installations retain their previous paths; see [migration](distribution.md#migration-from-older-dscode-installations).
4
+
3
5
  Use `/provider custom`, or choose **Custom** in `/provider`, to connect a self-hosted or private model API. DSCODE runs its tools in the current workspace; pointing inference at another computer does not move shell commands or file edits to that computer.
4
6
 
5
7
  Saved services appear by name in `/provider`. Choose a service and press Enter to switch to its current or first configured model; `/model` then selects among that service's models. **Custom · add / edit model services** opens configuration instead of switching. A service without a model context window opens its editor so the missing setting can be completed.
@@ -64,7 +66,7 @@ Automatic review follows its configured model route. When that route is Custom,
64
66
 
65
67
  ## Persistence
66
68
 
67
- Service and model configuration lives in `~/.dscode/providers.yaml`; terminal keys use the existing owner-only `~/.dscode/credentials.yaml` store. In Desktop 0.7.35 and later, existing native credentials take precedence and new keys are saved to the native Harness store (`~/.dsh/.credentials.yaml` by default). Old DSCODE keys remain a fallback; explicit removal clears both copies to avoid reactivating a legacy key. Provider records contain no key values. Each service receives a stable `custom-…` ID and a derived `DSCODE_CUSTOM_CUSTOM_…_API_KEY` credential reference. An environment variable with that exact reference takes precedence over the saved key, following the existing credential-store rules.
69
+ The self-contained terminal uses `~/.local/share/dscode-state` as its default `DSCODE_HOME`; legacy and Desktop installations retain `~/.dscode/providers.yaml`. Service and model configuration lives in `$DSCODE_HOME/providers.yaml`; terminal keys use the existing owner-only `$DSCODE_HOME/credentials.yaml` store. In Desktop 0.7.35 and later, existing native credentials take precedence and new keys are saved to the native Harness store (`~/.dsh/.credentials.yaml` by default). Old DSCODE keys remain a fallback; explicit removal clears both copies to avoid reactivating a legacy key. Provider records contain no key values. Each service receives a stable `custom-…` ID and a derived `DSCODE_CUSTOM_CUSTOM_…_API_KEY` credential reference. An environment variable with that exact reference takes precedence over the saved key, following the existing credential-store rules.
68
70
 
69
71
  Configuration is shared across projects and installed versions, written atomically, and reloaded by other running Hosts. Stale editors cannot overwrite newer configuration. Renaming a service preserves its ID, so persisted sessions continue to resolve it. Removing a provider removes its route and writable stored credential; it does not reroute existing sessions or delete their history. An environment-supplied key remains in the environment. If it masks a stored key, clear the environment override before removing the provider to remove that stored key too.
70
72
 
@@ -1,58 +1,88 @@
1
- # Distribution and one-command start
1
+ # Self-contained package distribution
2
2
 
3
- The npm + Hub release path is covered in [Hub distribution](hub-distribution.md). The tar installation below is retained.
3
+ The terminal is distributed as one complete application archive per platform. Download `dscode-<version>-darwin-arm64-standalone.tar.gz` for Apple Silicon or `dscode-<version>-darwin-x64-standalone.tar.gz` for Intel. Each archive contains its own Node 24.21.0, pinned Harness, compiled TUI, plugins and native modules. No system Node, npm, pnpm or DSH is needed, and first launch does not download runtime dependencies.
4
4
 
5
- The deliverable is an installable `dscode-0.7.8.tar.gz`, not just a configured `.dshprofile`. After installing, the user runs `dscode` directly. The npm + Hub installation is available and the tar packages ship through GitHub Releases; a Homebrew formula in the [`qiz029/tap`](https://github.com/qiz029/homebrew-tap) tap installs the same launcher (Homebrew 7 wants `brew trust qiz029/tap` before the first install).
5
+ macOS 14+ is supported. Git is used for repository and patch operations. Google Chrome and macOS Accessibility/Screen Recording permission are needed only for the features that use them. Models, external MCP servers and project tools retain their own configuration and requirements.
6
6
 
7
- ## Publisher
7
+ ## Download and run
8
8
 
9
9
  ```sh
10
- npm run dist
10
+ mkdir dscode-app
11
+ tar -xzf dscode-0.7.42-darwin-arm64-standalone.tar.gz -C dscode-app
12
+ ./dscode-app/dscode
11
13
  ```
12
14
 
13
- It produces three tarballs, all attached to the GitHub release:
15
+ Keep the extracted directory together. `dscode` is the executable entry; its private runtime and libraries are siblings inside that directory. You can move the complete directory while no session is running. User data does not move with it.
14
16
 
15
- | File | Contents | Needed to install |
16
- |---|---|---|
17
- | `dscode-<version>-darwin-arm64.tar.gz`, `dscode-<version>-darwin-x64.tar.gz` | Prebuilt package: the source tree + the locked `node_modules` installed for that platform + a `.dscode-prebuilt` marker (about 70 MB) | Node, Git; **no npm and no npm registry access** |
18
- | `dscode-<version>.tar.gz` | Source package: launcher, preset, dependency manifest and the complete lockfile, without node_modules (about 1 MB) | Node, npm, Git; downloads the locked dependencies from npm at install time |
17
+ The optional installer downloads and verifies the same package:
19
18
 
20
- None of them contains model keys, local override configuration, sessions or research files. The prebuilt package is produced in a staging directory with `npm ci --ignore-scripts --os=darwin --cpu=<arch>` — the same command the installer runs itself, and the native modules are all precompiled optional dependencies selected by `os`/`cpu`, so one runner can build both architectures. `DSCODE_PREBUILT_ARCHS=arm64` builds one architecture only, and an empty string builds the source package alone.
19
+ ```sh
20
+ curl -fsSL https://raw.githubusercontent.com/qiz029/dscode/main/install.sh | sh
21
+ # Pin an exact release:
22
+ curl -fsSL https://raw.githubusercontent.com/qiz029/dscode/main/install.sh | sh -s -- 0.7.42
23
+ # Or install an already unpacked package, without a download:
24
+ sh dscode-app/install.sh
25
+ ```
21
26
 
22
- ## User
27
+ The installer uses standard macOS shell tools, not Node. It verifies the published SHA-256 sidecar before extraction, checks the package with its private runtime, then installs it at `~/.local/share/dscode-app` and links `~/.local/bin/dscode`. Put that bin directory on PATH. Absolute `DSCODE_INSTALL_DIR` and `DSCODE_BIN_DIR` override those locations. An existing foreign command is never overwritten. Re-running the installer for a managed package delegates to its updater. Releases without a self-contained package or checksum are refused; there is no source/npm fallback.
23
28
 
24
- macOS 14+, Node 22.19+ (22.x) or 24+, Git (for `apply_patch`) and Google Chrome are required. npm is needed only when installing from the source package.
29
+ ## Existing DSH installations
25
30
 
26
- The same script can fetch the package by itself: run from a pipe it enters remote mode, resolves the latest release, verifies the publisher's sha256 digest, unpacks, and then hands over to the `install.sh` inside the package, so the downloaded version and the install script's understanding of the layout cannot diverge. It prefers the prebuilt package for the current platform, and the whole installation only reaches GitHub — a corporate npm registry proxy blocking installs does not affect it; when a release has no prebuilt package for the platform it falls back to the source package with a notice, and `DSCODE_INSTALL_SOURCE=1` asks for the source package explicitly. When the packaged `install.sh` sees `.dscode-prebuilt` and `node_modules` it skips `npm ci` and provisions directly with `node scripts/harness.mjs setup`; a marker for a different platform refuses the installation.
31
+ DSCODE only installs a `dscode` command. It never creates or replaces global `dsh`, `node`, `npm` or `pnpm` commands. Runtime startup uses absolute package paths. The private Node directory is on PATH only inside DSCODE's process tree, so bundled helpers use the same runtime.
27
32
 
28
- ```sh
29
- curl -fsSL https://raw.githubusercontent.com/qiz029/dscode/main/install.sh | sh
30
- # pin a version
31
- curl -fsSL https://raw.githubusercontent.com/qiz029/dscode/main/install.sh | sh -s -- 0.7.8
32
- ```
33
+ Inherited `DSH_*` variables, `NODE_OPTIONS` and `NODE_PATH` are removed before the application starts. DSCODE supplies its own `DSH_HOME` and `DSH_AGENTS_HOME`. Provider keys, proxies, custom CA certificates and the user's remaining tool PATH are preserved. Project-local instructions, hooks and skills remain supported; the standalone application excludes the global `~/.dsh/skills` ancestor root.
34
+
35
+ Sessions, native credentials, provider configuration, caches, user skills and local overrides live at `~/.local/share/dscode-state`, overridable with an absolute `DSCODE_HOME`. This directory must be outside the application and `~/.dsh`, including symlink aliases. Terminal preferences and input history use `tui/` under that directory. Shared DSCODE provider credentials use `credentials.yaml`; Harness-native credentials use `.credentials.yaml`. Custom services use `providers.yaml`; the mailbox uses `email/` unless `DSCODE_EMAIL_DIR` is explicitly set. Local overrides use `.env` and `config/hooks.local.json`, `config/mcp.local.yml` and `config/harness.local.yml` there.
36
+
37
+ The application does not import `~/.dsh` configuration, Hub tokens or the old `~/.dscode/credentials.yaml` automatically. Run `dscode --installation` to inspect the actual program, runtime and data locations. This isolation applies to the terminal package; the separate Desktop plugin deliberately runs inside its owning Desktop application.
33
38
 
34
- Or unpack the same tarball by hand:
39
+ ## Upgrade and rollback
35
40
 
36
41
  ```sh
37
- mkdir dscode-install
38
- tar -xzf dscode-0.7.8-darwin-arm64.tar.gz -C dscode-install
39
- sh dscode-install/install.sh
40
- cd /path/to/project
41
- dscode
42
+ dscode update # newest published self-contained release
43
+ dscode update 0.7.42 # an exact release, including an explicit rollback
42
44
  ```
43
45
 
44
- The default installation goes to `~/.local/share/dscode` with the command linked as `~/.local/bin/dscode`. If that bin directory is not on PATH, add `export PATH="$HOME/.local/bin:$PATH"` to the shell configuration and reopen the terminal.
46
+ Exit running DSCODE sessions and schedulers first. Startup and update share kernel admission locks; each running child retains protection even if its launcher is killed. Both the program location and the selected data directory are protected. Concurrent ordinary sessions from the same package remain supported. A different package must wait until those sessions exit before taking over the same data directory. The stable `dscode-app` profile preserves native model/settings edits in `cordis.patch.yml` across upgrades and package relocation. Generated application defaults live in a separate private base bundle under the data directory.
47
+
48
+ The updater downloads a matching platform archive from GitHub, requires its published SHA-256 digest, checks its version/platform and bundled modules, and only then swaps the complete program directory. A failed check leaves the old program in place; a failed swap or relocated check restores it. Successful updates retain a sibling `dscode-app.backup-<version>-<timestamp>` directory. State never enters the staged package and is not moved or rolled back. The directory swap has a brief rename interval; a new launch in that interval may need to be retried. This is not a filesystem transaction across power failure.
49
+
50
+ A default update never downgrades an application newer than the published release. An explicit version may downgrade only to another self-contained package; older source/prebuilt releases are refused. `/update` schedules this same updater after the TUI exits. Its version check uses GitHub, not npm.
45
51
 
46
- Enter `/model` once to configure a provider and a model. macOS Computer Use still needs the user to grant Accessibility/Screen Recording permission; those permissions and credentials cannot be distributed with the package.
52
+ Version 0.7.41 has an archive-listing buffer bug in its updater. Rerun the `curl` installation command once to repair that version using the new package's updater. The repair retains the same session locks, checksum checks and external user data. From 0.7.42 onward, use `dscode update` normally. Homebrew installations continue to use `brew upgrade dscode`.
47
53
 
48
- `dscode --continue`, `dscode --resume SESSION_ID` and `dscode --cwd /path/to/project` are supported. By default it operates on the current terminal directory; state is kept in the installation's `.runtime`. A custom location can be set at install time with the absolute `DSCODE_INSTALL_DIR` and `DSCODE_BIN_DIR`.
54
+ ## Migration from older DSCODE installations
49
55
 
50
- Re-running the installer keeps a tar installation current rather than refusing: it reads the installed version, leaves an equal or newer one alone, and hands an older one to that installation's own `dscode update`, which performs the swap with its state migration and backup. A directory that is not a DSCODE installation, or a `dscode` command that belongs to another installation (a source checkout, an npm launcher), is still never overwritten — the installer refuses and names the path it found. Since the version with a self-contained `dscode update`, a tar installation supports self-upgrade: `dscode update [exact version]` downloads the tarball from GitHub Releases, verifies it against the publisher's sha256 digest, migrates `.runtime`, `.env` and the local config, and swaps the directory in place (the `dscode` command path does not change and needs no relinking), keeping the old installation as a sibling backup directory to switch back to or discard by hand. An update requires no running session; when the release has a prebuilt package for the current platform it takes that, so the upgrade needs no npm either, otherwise it takes the source package and downloads the locked dependencies with npm ci. A tar installation older than that has no updater: unpack the new tarball into a new directory and migrate the state by hand once.
56
+ Old installations remain usable. New archives have a `-standalone` suffix so an old updater refuses them instead of applying the wrong installation or state-migration procedure. Check `type -a dscode` and `dscode --version` before changing your command link; npm, Homebrew and source links may coexist. Install the new package in a separate program directory and remove or rename an old command link only after identifying its owner.
51
57
 
52
- ## Why not plain npm install -g today
58
+ Migration is explicit, with all old sessions and schedulers stopped and a backup made first:
59
+
60
+ - An existing npm/Hub data directory can be selected with `DSCODE_HOME="$HOME/.local/share/dscode-hub"`. The standalone application creates its own stable `dscode-app` profile there and does not reuse the Hub-managed dependency graph. Transfer only the desired user overrides from the old profile into `profiles/dscode-app/cordis.patch.yml`; do not copy node_modules, generated plugin insertions or absolute paths to the old installation. Do not run legacy and standalone applications against the same data concurrently.
61
+ - For a tar/source installation, copy the old `.runtime` contents to a separate data directory, and copy only the desired `.env` and local `config/` overrides into it. Select that directory with `DSCODE_HOME`. To retain native model/settings edits, transfer only user overrides from the old `profiles/tui/cordis.patch.yml` into `profiles/dscode-app/cordis.patch.yml`; exclude generated plugin insertions and paths to the old installation. Keep the original until sessions have been checked. Do not point the new application at the old program directory.
62
+ - To retain shared provider logins, explicitly copy `~/.dscode/credentials.yaml` to `credentials.yaml` in the selected data directory if no destination credential file exists, preserving owner-only permissions. Native `.credentials.yaml` already lives in the old runtime data. Re-login instead if combining stores would be ambiguous.
63
+ - Custom service definitions in `~/.dscode/providers.yaml` and mail in `~/.dscode/email/` can be copied explicitly to `providers.yaml` and `email/` in the data directory. Keep the matching provider keys when migrating custom services.
64
+ - Old terminal UI preferences in `~/.dsh/dsh-code` can be copied explicitly to the new data directory's `tui/`; no other global DSH files need to be copied.
65
+
66
+ Automatic credential merging and conversion of future incompatible session schemas are not provided. Review a resumed session before discarding old data.
67
+
68
+ ## Homebrew
69
+
70
+ The release build generates `artifacts/dscode.rb` from the exact two application archives and their checksums. The new formula installs the complete package into `libexec`, links only `dscode`, and has no Node/npm dependency. Its marker makes `dscode update` direct users to `brew upgrade dscode`; Homebrew replaces the whole package, never a separate Hub profile.
71
+
72
+ The maintained [qiz029/homebrew-tap](https://github.com/qiz029/homebrew-tap) uses this generated formula. Its daily and on-demand updater follows GitHub releases, verifies the formula digest and both archive checksums, and rejects incomplete releases before changing the tap. The repository build produces the formula; tap synchronization remains a separate publication step.
73
+
74
+ ## Build and release
75
+
76
+ ```sh
77
+ npm run dist # both platform archives plus SHA-256 sidecars
78
+ node scripts/homebrew-formula.mjs # the matching formula
79
+ node scripts/verify-standalone.mjs # qualify this machine's architecture
80
+ make release # full local terminal candidate gate
81
+ make publish # attach terminal assets to GitHub
82
+ ```
53
83
 
54
- An npm `overrides` entry in the dependencies does not become the resolution rule of a root project when the package is installed as a third-party dependency. This composition needs a single pinned DSH rc line, so it keeps the verified locked dependency graph by using the root manifest inside the install package and `npm ci`. `package.json` already declares the `dscode` bin, but a bare bin declaration is not evidence of npm global-install compatibility.
84
+ `DSCODE_PREBUILT_ARCHS=arm64` or `x64` selects one build for development. An empty selection is an error. Builds install the repository lockfile in a fresh staging directory, compile the TUI and apply runtime patches there, then add the official Node archive verified against checked-in SHA-256 hashes. They never copy a developer's node_modules, credentials or runtime state. Both runtime archives include Node's license.
55
85
 
56
- A one-line `curl | sh` installation is available, reusing the GitHub Releases tarball and the publisher's sha256 digest, and preferring the npm-free prebuilt package; a release without a digest is refused rather than installed unverified. A Homebrew formula ships in the `qiz029/tap` tap: it installs the published launcher tarball into `libexec`, so the pinned dependency graph is npm's rather than the formula's, writes the `.dscode-brew` marker that keeps `dscode update` off the npm pass, and leaves the launcher to `brew upgrade`.
86
+ Release CI validates the ARM64 archive and downloads that exact candidate for a separate Intel qualification. Verification covers an unusable foreign Node/npm/DSH on PATH, foreign DSH variables, an untouched global DSH home, native module imports and a real PTY, cold profile composition, local Host diagnostics and local installation. Native macOS permission UI and real model inference remain separate acceptance checks.
57
87
 
58
- The complete tarball contains `presets/dscode`, `bin/apply_patch` and the version-locked runtime patch scripts. setup enables dscode by default and installs Ultra effort support; the node_modules inside a prebuilt package comes from a fresh install of the lockfile in a staging directory, so the local node_modules is never packed. An old session keeps resuming under its original preset; use `dscode --mode dscode` to create a new session explicitly.
88
+ The default terminal release has no npm or Hub publication dependency. Desktop continues through its own package and verification path. Historical npm/Hub tooling remains documented in [Legacy Hub distribution](hub-distribution.md); it is not used to install or update the self-contained terminal.
package/docs/email.md CHANGED
@@ -13,7 +13,7 @@ On narrow terminals, Tab switches between list and preview.
13
13
  stays on the same message when newer messages arrive.
14
14
 
15
15
  The inbox belongs to the local user and is shared across sessions. Its default
16
- directory is `~/.dscode/email`; `DSCODE_EMAIL_DIR` overrides it. New directories
16
+ directory is `$DSCODE_HOME/email` for the self-contained terminal (`DSCODE_HOME` defaults to `~/.local/share/dscode-state`). Legacy and Desktop installations retain `~/.dscode/email`; `DSCODE_EMAIL_DIR` overrides it. New directories
17
17
  use mode 0700 and messages use 0600. Message content enters session history only
18
18
  when selected with Enter. Existing composer drafts and attachments are preserved.
19
19
 
@@ -218,7 +218,7 @@ by source content. UIDVALIDITY changes trigger recovery using the original
218
218
  connection timestamp (at IMAP's second precision) and content identity, instead
219
219
  of trusting old UIDs or importing the whole historical inbox.
220
220
 
221
- All sessions share `~/.dscode/email/imap/connection.json` (or
221
+ All sessions share `$DSCODE_HOME/email/imap/connection.json` (or
222
222
  `$DSCODE_EMAIL_DIR/imap/connection.json`). It contains the credentials and cursor,
223
223
  is written atomically with mode 0600 inside a new 0700 directory, and is guarded
224
224
  by a kernel lock. A running TUI polls every 30 seconds even with `/email` closed;
@@ -284,7 +284,7 @@ and [filter](https://developers.google.com/workspace/gmail/api/reference/rest/v1
284
284
  permission documentation. Login uses [desktop loopback OAuth with PKCE and state validation](https://developers.google.com/identity/protocols/oauth2/native-app).
285
285
 
286
286
  The client JSON, tokens and sync state are stored privately under
287
- `~/.dscode/email/gmail/` (or `$DSCODE_EMAIL_DIR/gmail/`). Files are atomic 0600
287
+ `$DSCODE_HOME/email/gmail/` (or `$DSCODE_EMAIL_DIR/gmail/`). Files are atomic 0600
288
288
  writes inside a new 0700 directory. `DSCODE_GMAIL_CLIENT_FILE` optionally points
289
289
  at an existing client JSON instead of importing it. Keep that file available
290
290
  for token refresh. Only one account is supported; reconnecting another account
@@ -327,7 +327,7 @@ sender supports Gmail app-password connections, not custom IMAP hosts or the
327
327
  optional OAuth connector. Attachments, CC/BCC and automatic replies are not part
328
328
  of this tool.
329
329
 
330
- Receipts are shared across sessions under `~/.dscode/email/outbox` (or
330
+ Receipts are shared across sessions under `$DSCODE_HOME/email/outbox` (or
331
331
  `DSCODE_EMAIL_DIR/outbox`), with private file permissions. `accepted` means the
332
332
  SMTP server accepted the mail; it does not prove delivery or that someone read
333
333
  it. `uncertain` means the send may have succeeded despite an interrupted
@@ -1,4 +1,6 @@
1
- # npm + Hub distribution
1
+ # Legacy npm and Hub distribution
2
+
3
+ The default terminal distribution from 0.7.41 is the [self-contained application package](distribution.md). This guide describes retained legacy npm/Hub tooling and the separate Desktop plugin. The legacy release order below does not apply to the new terminal package. Do not use it as the default installation or release checklist.
2
4
 
3
5
  ## Packages and responsibilities
4
6
 
@@ -1,5 +1,7 @@
1
1
  # OpenCode Go
2
2
 
3
+ For the self-contained terminal, `DSCODE_HOME` defaults to `~/.local/share/dscode-state`. Legacy installations retain their previous paths; see [migration](distribution.md#migration-from-older-dscode-installations).
4
+
3
5
  DSCODE can run a session on an [OpenCode Go](https://opencode.ai/docs/go/) subscription: $10 a month for open coding models. You sign in with your OpenCode account; there is no API key to paste. The route is `opencode-go`, next to DeepSeek, OpenRouter and Grok.
4
6
 
5
7
  ## Setting it up
@@ -10,7 +12,7 @@ DSCODE can run a session on an [OpenCode Go](https://opencode.ai/docs/go/) subsc
10
12
 
11
13
  `/provider opencode-go` (or `/provider opencode`) before signing in, and `/login opencode-go`, point you to `/opencode login` instead of asking for a key.
12
14
 
13
- **Upgrading from 0.7.31**, which used a console API key: that key is no longer read. Run `/opencode login` once. The stored `OPENCODE_API_KEY` stays in `~/.dscode/credentials.yaml` unused until you remove it.
15
+ **Upgrading from 0.7.31**, which used a console API key: that key is no longer read. Run `/opencode login` once. The stored `OPENCODE_API_KEY` stays in `$DSCODE_HOME/credentials.yaml` unused until you remove it.
14
16
 
15
17
  ## The account login
16
18
 
@@ -21,7 +23,7 @@ DSCODE can run a session on an [OpenCode Go](https://opencode.ai/docs/go/) subsc
21
23
  /opencode logout remove the login from this machine
22
24
  ```
23
25
 
24
- - DSCODE keeps its own login, stored as `OPENCODE_OAUTH` in `~/.dscode/credentials.yaml`. It is separate from any `opencode auth login` on the same machine, and signing in or out of one does not affect the other.
26
+ - DSCODE keeps its own login, stored as `OPENCODE_OAUTH` in `$DSCODE_HOME/credentials.yaml`. It is separate from any `opencode auth login` on the same machine, and signing in or out of one does not affect the other.
25
27
  - The login picks the first organisation (by name) with a Go subscription. Requests go to the inference endpoint the OpenCode console names for that organisation. The public `zen/go/v1` endpoint accepts only console API keys, not the login.
26
28
  - The access token lasts 30 days and renews itself before it expires. Each renewal replaces the refresh token, and the new one is written back at once. Several DSCODE sessions share the login: when one has renewed first, the others pick up its token instead of failing.
27
29
  - If the login is revoked or cannot be renewed, requests fail with a message asking for `/opencode login` again.
@@ -0,0 +1,28 @@
1
+ # DSCODE 0.7.41
2
+
3
+ DSCODE's terminal now ships as one self-contained application package per Mac architecture, with a private runtime and separate user data.
4
+
5
+ ## Self-contained installation
6
+
7
+ Each archive includes pinned Node 24.21.0, Harness, the compiled TUI, plugins and native modules. The installer and first launch no longer need Node, npm, pnpm, a Hub profile or runtime downloads. Runtime patches are applied while building a clean staging tree. External project tools, Chrome and model credentials remain user-configured.
8
+
9
+ ## Coexistence with existing DSH
10
+
11
+ DSCODE supplies its own Harness home and private runtime, discards inherited DSH runtime configuration and Node injection options, and installs only the `dscode` command. Sessions, credentials, configuration and terminal UI preferences live outside the package. Generated defaults form a separate base bundle so startup preserves native user overrides across upgrades and relocation. Existing DSH configuration and old DSCODE credentials are not imported automatically; the distribution guide documents explicit migration.
12
+
13
+ ## Whole-package upgrades
14
+
15
+ Public installation passed, but a real full-archive upgrade subsequently exposed an `ENOBUFS` error while listing the archive. This fails before replacement and preserves the old application and user data. Version 0.7.42 fixes the updater. Users of 0.7.41 must rerun the curl installer once to receive the repair; Homebrew users run `brew upgrade dscode`.
16
+
17
+
18
+ The updater checks the release digest, platform, version and bundled modules before replacing the application. Failed checks preserve the old version and failed swaps restore it. User data is never moved into the update staging tree. Kernel locks protect running sessions; successful updates keep the previous package. The default update does not downgrade and there is no npm fallback. Power-failure transactional recovery is not promised.
19
+
20
+ The build also generates a Homebrew formula consuming the same archives. The maintained Homebrew tap has adopted this formula and now follows verified GitHub release assets instead of npm launcher versions. Desktop remains a separate extension distribution.
21
+
22
+ ## Verification
23
+
24
+ The Node 22 and Node 24 CI gates each passed 1,148 unit/component cases with 80.04% measured first-party `.mjs` coverage. The normal-exit lease regression passed with the existing launcher lifecycle cases. The final provider, email and standalone data-isolation suite passed all 94 cases, and all 28 terminal rendering/panel cases passed. New cases cover environment and path isolation, installer checksum rejection, foreign-command protection, and successful/failed whole-package replacement without moving user data. Archive qualification uses an isolated home and deliberately unusable foreign Node/npm/DSH commands; digest-bound receipts record private native imports, PTY, cold composition, local Host diagnostics, shell installation and settings preservation. Local Intel rehearsal used Rosetta; release CI also passed on a separate native Intel runner. See the [verification record](https://github.com/qiz029/dscode/blob/main/docs/verification.md).
25
+
26
+ Lint, both TypeScript configurations, native integration, package checks and 63 offline evaluations passed. After unifying the TUI startup lookup and `/update` routing with the package updater, lint, both TypeScript checks, documentation checks and 21 update/standalone regressions passed. The deferred updater has process tests; the TUI keyboard dispatch has no dedicated end-to-end interactive case.
27
+
28
+ Live provider inference, macOS permission dialogs, public release installation and external Homebrew tap publication are not automated acceptance claims. No power-loss recovery case is provided.
@@ -0,0 +1,23 @@
1
+ # DSCODE 0.7.42
2
+
3
+ Whole-package updates now handle the complete archive, and the installer can repair the updater shipped in 0.7.41.
4
+
5
+ ## Full archive updates
6
+
7
+ The real 0.7.41 archive has a file listing larger than Node's default subprocess output buffer. Public installation succeeded, but `dscode update` stopped with `ENOBUFS` before replacing the application. Increase the command buffer to a bounded 64 MiB. Digest, platform, version, native module and rollback checks remain in force. This does not add power-loss transaction guarantees.
8
+
9
+ ## Repairing 0.7.41
10
+
11
+ Rerun the installation command once:
12
+
13
+ ```sh
14
+ curl -fsSL https://raw.githubusercontent.com/qiz029/dscode/main/install.sh | sh
15
+ ```
16
+
17
+ When the installed version is 0.7.41, the installer verifies a fresh package and uses its updater against the existing installation. The target's session locks, Homebrew ownership checks and external data directory still apply. This fixes the installed updater without requiring users to delete their program or settings. Later updates use `dscode update` normally. Homebrew users continue to run `brew upgrade dscode`.
18
+
19
+ ## Verification
20
+
21
+ Regression coverage includes subprocess output above 1 MiB, installer repair routing, target active-session protection and Homebrew ownership rejection. The archive qualification now replaces an installation with the entire candidate archive on both architectures and checks that a saved native setting remains effective afterward; release metadata is local in this probe and the prior version is synthetic.
22
+
23
+ The local repository gate passed lint, both TypeScript configurations, all 1,150 unit/component cases, native integration, package checks and 63 offline evaluations; measured first-party `.mjs` coverage was 80.04%. Both archives built, and Apple Silicon qualification passed, including real full-archive replacement. Release CI also requires the same qualification on a native Intel runner. Public installation and repair are checked after assets become available. Live provider inference and macOS permission dialogs are outside this verification; no power-loss recovery case is provided.
@@ -1,5 +1,7 @@
1
1
  # dscode TUI commands
2
2
 
3
+ For the self-contained terminal, `DSCODE_HOME` defaults to `~/.local/share/dscode-state`. Legacy installations retain their previous paths; see [migration](distribution.md#migration-from-older-dscode-installations).
4
+
3
5
  Restart `dscode` to load these commands. Most commands run locally; `/doctor` makes one bounded call to the selected model for analysis. At startup the TUI reads the npm registry once, within eight seconds, and shows a notice when a newer release exists; set `DSCODE_UPDATE_CHECK=off` to skip that check.
4
6
 
5
7
  If a provider rejects a request for content filtering, the turn stops with a `CONTENT_POLICY` or `CONTENT_FILTER` error and keeps the provider's explanation. Review the request and relevant context before sending a follow-up in the same session. The default retry policy does not repeat these rejected requests, and changing an API key does not resolve a content-filter rejection. DSCODE cannot identify the triggering passage unless the provider supplies it.
@@ -10,12 +12,12 @@ If a provider rejects a request for content filtering, the turn stops with a `CO
10
12
  | `/status` | Session ID, workspace, model route, permission mode, recorded token totals/context pressure, tool/plugin counts. Unknown usage stays unknown. `/review-usage` reports the independent reviewer separately. |
11
13
  | `/provider [deepseek\|openrouter\|grok\|opencode-go\|custom]` | For self-hosted or private APIs, `/provider custom` opens [Custom model providers](custom-providers.md). Saved custom services also appear by name in the picker: Enter switches to their current or first configured model. The Custom entry adds or edits services. Switch the session between DeepSeek's official API, OpenRouter, Grok and OpenCode Go. Without an argument, open a picker (↑↓ choose, Enter switch, Esc close) that shows each provider's key status. OpenRouter is DSCODE's own adapter over OpenRouter's live model listing (cached for a day in `$DSH_HOME/openrouter-models.json`, settings in the `llm-openrouter` section; `/model` offers DSCODE's curated list from that listing rather than all of it); a switch asks for the key when none is stored, then moves to the counterpart of the current model and keeps the effort when the target offers it. When the new model would compact the current context, the switch asks first. Ultra works on both routes: on OpenRouter it sends `xhigh` plus the collaboration policy, and `low` is sent as `high`, which is how DeepSeek itself answers low. The footer shows the active provider's remaining balance (for OpenRouter, the account's remaining credits, or the key's own remaining limit when OpenRouter refuses the key those) and records the cost OpenRouter reports for each call (estimated from list prices only when it reports none); the 🔥/❄️ peak marker appears only for DeepSeek, because OpenRouter bills no peak window. OpenCode Go serves its chat-completions models to an OpenCode account login, never a key: before `/opencode login` the switch names that command instead of asking for a key, and the footer shows the subscription's usage instead of a cost; see [OpenCode Go](opencode-go.md). Stop a running turn first. |
12
14
  | `/model` | Picker for the session's current provider only, listed alphabetically by the displayed model name (digits compare naturally, so GLM 5.2 precedes GLM 5.3). Typing searches as you type (BM25 over model name, id and provider; the word being typed matches prefixes) and narrows the list without re-ranking it; Enter focuses the first match, a second Enter selects it, Esc clears the search then closes, Tab opens the provider picker. |
13
- | `/opencode [login\|cancel\|logout]` | Sign in to OpenCode Go with an OpenCode account, the route's only credential. `login` shows a code, opens the console in the browser and waits in the background for up to ten minutes; the consent page names the OpenCode CLI, whose login client DSCODE uses. Without an argument it shows who is signed in or how the last attempt ended. The login is stored in `~/.dscode/credentials.yaml` and renews itself. See [OpenCode Go](opencode-go.md). |
14
- | `/openrouter` | OpenRouter account panel: the account balance, this key's limit and its spend today, this week and this month; with a management key, also every API key's usage and the last 30 days of spend by model and serving provider. `r` refreshes, `m` sets the management key (checked against OpenRouter, stored in `~/.dscode/credentials.yaml`, read-only use; `OPENROUTER_MANAGEMENT_KEY` also works). Saving the OpenRouter API key through `/provider openrouter` or `/login openrouter` offers the management key as an optional step. |
15
+ | `/opencode [login\|cancel\|logout]` | Sign in to OpenCode Go with an OpenCode account, the route's only credential. `login` shows a code, opens the console in the browser and waits in the background for up to ten minutes; the consent page names the OpenCode CLI, whose login client DSCODE uses. Without an argument it shows who is signed in or how the last attempt ended. The login is stored in `$DSCODE_HOME/credentials.yaml` and renews itself. See [OpenCode Go](opencode-go.md). |
16
+ | `/openrouter` | OpenRouter account panel: the account balance, this key's limit and its spend today, this week and this month; with a management key, also every API key's usage and the last 30 days of spend by model and serving provider. `r` refreshes, `m` sets the management key (checked against OpenRouter, stored in `$DSCODE_HOME/credentials.yaml`, read-only use; `OPENROUTER_MANAGEMENT_KEY` also works). Saving the OpenRouter API key through `/provider openrouter` or `/login openrouter` offers the management key as an optional step. |
15
17
  | `/status` | Query the current session's provider, model and credential balance without a model turn. Supports DeepSeek API/account login and OpenRouter; account funds and key limits are labeled separately. Uses the active credential, refreshes on every call, and reports unsupported endpoints, missing credentials and query failures explicitly. Also available in [ACP](acp.md#current-credential-balance), which documents the lookup limits. |
16
18
  | `/review` | Independent review of the current changes. `DSCODE_REVIEW_MODEL=provider/model` pins the reviewer to its own model and `DSCODE_REVIEW_EFFORT=<level>` sets its reasoning level, so a small session model cannot cap the review; the budget leaves room for reasoning plus the report. |
17
- | `/update [exact-version]` | Upgrade DSCODE after this session exits: the TUI starts a detached helper that waits for the session to end and then runs the same `dscode update` entry (npm/Hub launcher, or tar/source self-update). Progress and the outcome are written to `$DSH_HOME/update.log` and `$DSH_HOME/update-result.json`. |
18
- | `/login [deepseek\|openrouter]` | Paste an API key into the private input; it is saved to `~/.dscode/credentials.yaml` (`0600`, shared across projects and installed versions). Without an argument it targets the current provider. `DEEPSEEK_API_KEY` or `OPENROUTER_API_KEY` in the environment takes precedence and makes that key read-only here. |
19
+ | `/update [exact-version]` | Upgrade DSCODE after this session exits: the TUI starts a detached helper that waits for the session to end and then runs the same `dscode update` entry (whole-package replacement for the self-contained terminal; legacy installs keep their own updater). Progress and the outcome are written to `$DSH_HOME/update.log` and `$DSH_HOME/update-result.json`. |
20
+ | `/login [deepseek\|openrouter]` | Paste an API key into the private input; it is saved to `$DSCODE_HOME/credentials.yaml` (`0600`, shared across projects and installed versions). Without an argument it targets the current provider. `DEEPSEEK_API_KEY` or `OPENROUTER_API_KEY` in the environment takes precedence and makes that key read-only here. |
19
21
  | `/memories [status\|on\|off\|global-on\|global-off\|run\|note <text>\|clear]` | Global cross-session memory controls and background model usage. [Behavior and configuration](memory.md). |
20
22
  | `/session` | Current session ID, local socket, and external send/read/watch commands. [Multi-source sessions](session-bridge.md). |
21
23
  | `/mailbox [cancel MESSAGE_ID]` | Read messages and deferred notes, or cancel a request/message. [Session communication](session-communication.md). |
@@ -35,9 +37,9 @@ If a provider rejects a request for content filtering, the turn stops with a `CO
35
37
  | `/review [--staged\|--base REF\|--commit REF] [--path RELATIVE_PATH]` | Independently review the selected Git diff in a read-only, tool-free model request. Default includes tracked and untracked uncommitted changes, and when there are none it reviews the commits made since the latest user task started (read from the HEAD reflog), so work that was committed or merged is still reviewed; `--commit` reviews a merge commit against its first parent; empty scopes do not call the model. Narrow large or unrelated diffs with `--path`. |
36
38
  | `/delegate <task>` | Hand the task to the main agent as coordinator: it records the parts on a delegate board, launches them as child agents with `worktree: true` in the background while child slots are free (five below Ultra, twenty at Ultra), answers the questions they send to `/`, verifies each finished worktree, and once every task is done merges the accepted ones into the main workspace with `git apply --3way`, leaving them staged and uncommitted. Refused unless the session starts at a clean Git repository root, and inside a child agent. See [Ultra](dscode-ultra.md#delegate-mode). |
37
39
  | `/delegate-dashboard` | Popup kanban of the current session's delegate board with four columns — pending, running, verifying, complete — and the child slots in use. A task is running while its child works or waits on an answer from the main agent (marked `?`), and verifying once the child has settled and the main agent has not yet recorded the result. Columns are divided by vertical rules and every task is its own bordered card with its id, child name and title; `▲` marks high priority and `▼` low. Pending cards run in launch order, ready ones first; a blocked task has a grey card with a hollow `◌` and names what it waits on (`after t1,t2`). Waiting cards lead their column. On a short terminal each card folds to one line so more of them fit, and a narrow terminal stacks the columns. It refreshes every second; Esc or q closes it. |
38
- | `/verbose` | Toggle verbose chat: thinking and tool calls appear dimmed as `· Thinking: …`, `· Tool Call: name args` and ` Output: …` (thinking capped at eight lines), with a blank line between blocks. The setting is saved per machine in `~/.dsh/dsh-code/verbose.json`. Ctrl/Alt+R toggles the same setting; Ctrl+O still opens the full history inspector. |
39
- | `/mouse` | Toggle mouse capture. On (default) makes the wheel scroll the chat one row per notch, merging the notches that arrive in the same frame; off hands the mouse back to the terminal for drag-select and copy, leaving PageUp/PageDown to scroll the chat. While capture is on, hold Shift (Option in iTerm2, Fn in Terminal.app) to select text. The choice is saved per machine in `~/.dsh/dsh-code/mouse.json`. |
40
- | `/language [code]` | Without an argument, open a picker (↑↓ choose, enter apply, esc close) for the interface language for DSCODE's own labels, notices, the status-line telemetry labels and the composer placeholder: `en` (default), `zh-CN`, `zh-TW`, `ja`, `ko`, `es`. Names and common aliases work too (`简体中文`, `jp`). The choice is saved per machine in `~/.dsh/dsh-code/language.json`; `DSCODE_LANGUAGE` overrides it for one process. |
40
+ | `/verbose` | Toggle verbose chat: thinking and tool calls appear dimmed as `· Thinking: …`, `· Tool Call: name args` and ` Output: …` (thinking capped at eight lines), with a blank line between blocks. The setting is saved per machine in `$DSCODE_HOME/tui/verbose.json`. Ctrl/Alt+R toggles the same setting; Ctrl+O still opens the full history inspector. |
41
+ | `/mouse` | Toggle mouse capture. On (default) makes the wheel scroll the chat one row per notch, merging the notches that arrive in the same frame; off hands the mouse back to the terminal for drag-select and copy, leaving PageUp/PageDown to scroll the chat. While capture is on, hold Shift (Option in iTerm2, Fn in Terminal.app) to select text. The choice is saved per machine in `$DSCODE_HOME/tui/mouse.json`. |
42
+ | `/language [code]` | Without an argument, open a picker (↑↓ choose, enter apply, esc close) for the interface language for DSCODE's own labels, notices, the status-line telemetry labels and the composer placeholder: `en` (default), `zh-CN`, `zh-TW`, `ja`, `ko`, `es`. Names and common aliases work too (`简体中文`, `jp`). The choice is saved per machine in `$DSCODE_HOME/tui/language.json`; `DSCODE_LANGUAGE` overrides it for one process. |
41
43
  | `/clear` | Start a fresh session and clear the screen after successful activation; the previous session remains available through `/resume`. Stop a running turn first. |
42
44
  | Ctrl+L | Clear the screen only, keeping the conversation context. |
43
45
  | Ctrl+C | While an agent runs, cancel it immediately; a second press exits the TUI without waiting for the busy display to settle. With an idle draft, the first press clears it and the second exits. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toddzheng024/dscode-desktop",
3
- "version": "0.7.40",
3
+ "version": "0.7.42",
4
4
  "private": false,
5
5
  "description": "DSCODE for Harness Desktop: coding collaboration, session memory, browser workflows and task management.",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@ export function validateProfile(input) {
52
52
 
53
53
  /** Whole-file revision + cross-process lock: stale editors never overwrite another Host. */
54
54
  export class CustomStore {
55
- constructor(path = join(homedir(), '.dscode', 'providers.yaml')) { this.path = path; }
55
+ constructor(path = join(process.env.DSCODE_STANDALONE_ROOT && process.env.DSCODE_HOME ? process.env.DSCODE_HOME : join(homedir(), '.dscode'), 'providers.yaml')) { this.path = path; }
56
56
  async read() {
57
57
  let raw;
58
58
  try { raw = await readFile(this.path, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') throw error; raw = ''; }
@@ -138,7 +138,7 @@ export async function apply(ctx, config = {}) {
138
138
  else if (ids.length) registration = ctx.llm.registerAdapter(ids, service.adapter);
139
139
  } });
140
140
  ctx.provide('dscodeCustom', service);
141
- try { await service.refresh(); } catch { ctx.logger.warn('Could not read custom providers; repair ~/.dscode/providers.yaml'); }
141
+ try { await service.refresh(); } catch { ctx.logger.warn('Could not read custom providers; repair %s', service.store.path); }
142
142
  const refresh = () => { void service.refresh().catch(() => ctx.logger.warn('Invalid custom provider edit; retaining the last valid configuration')); };
143
143
  watchFile(service.store.path, { persistent: false, interval: 1000 }, refresh);
144
144
  ctx.effect(() => () => unwatchFile(service.store.path, refresh));
@@ -39,7 +39,7 @@ export function emailPrompt(mail) {
39
39
  }, null, 2);
40
40
  }
41
41
 
42
- export function createEmailInbox({ directory = process.env.DSCODE_EMAIL_DIR || join(homedir(), '.dscode', 'email') } = {}) {
42
+ export function createEmailInbox({ directory = process.env.DSCODE_EMAIL_DIR || join(process.env.DSCODE_STANDALONE_ROOT && process.env.DSCODE_HOME ? process.env.DSCODE_HOME : join(homedir(), '.dscode'), 'email') } = {}) {
43
43
  return {
44
44
  directory,
45
45
  receive(value) {
@@ -19,3 +19,5 @@ export function normalizeLanguage(value: unknown): string | null;
19
19
  export function languageName(code: string): string;
20
20
  /** One message in one locale, with `{name}` parameters interpolated. */
21
21
  export function t(locale: string, key: string, params?: Readonly<Record<string, unknown>>): string;
22
+ /** Package-local user preferences; legacy installs retain their existing location. */
23
+ export function terminalStateDirectory(home?: string, env?: NodeJS.ProcessEnv): string;
@@ -512,8 +512,12 @@ export function t(locale, key, params) {
512
512
  return text;
513
513
  }
514
514
 
515
- export function languageFile(home = homedir()) {
516
- return join(home, '.dsh', 'dsh-code', 'language.json');
515
+ export function terminalStateDirectory(home = homedir(), env = process.env) {
516
+ return env.DSCODE_STANDALONE_ROOT && env.DSCODE_HOME ? join(env.DSCODE_HOME, 'tui') : join(home, '.dsh', 'dsh-code');
517
+ }
518
+
519
+ export function languageFile(home = homedir(), env = process.env) {
520
+ return join(terminalStateDirectory(home, env), 'language.json');
517
521
  }
518
522
 
519
523
  /** The stored language, or English; DSCODE_LANGUAGE overrides the file for one process. */
@@ -521,17 +525,17 @@ export function readLanguage({ home = homedir(), env = process.env } = {}) {
521
525
  const override = normalizeLanguage(env.DSCODE_LANGUAGE);
522
526
  if (override) return override;
523
527
  try {
524
- const file = languageFile(home);
528
+ const file = languageFile(home, env);
525
529
  if (!existsSync(file)) return 'en';
526
530
  return normalizeLanguage(JSON.parse(readFileSync(file, 'utf8')).language) ?? 'en';
527
531
  } catch { return 'en'; }
528
532
  }
529
533
 
530
- export function saveLanguage(code, { home = homedir() } = {}) {
534
+ export function saveLanguage(code, { home = homedir(), env = process.env } = {}) {
531
535
  const normalized = normalizeLanguage(code);
532
536
  if (!normalized) throw new Error(`Unknown language: ${code}`);
533
- const file = languageFile(home);
534
- mkdirSync(join(home, '.dsh', 'dsh-code'), { recursive: true });
537
+ const file = languageFile(home, env);
538
+ mkdirSync(terminalStateDirectory(home, env), { recursive: true });
535
539
  writeFileSync(file, JSON.stringify({ language: normalized }, null, 2) + '\n');
536
540
  return normalized;
537
541
  }
@@ -8,7 +8,7 @@ export const schedulerLabel = home => `ai.dscode.scheduler.${createHash('sha256'
8
8
  export const schedulerPath = (home, directory = join(homedir(), 'Library', 'LaunchAgents')) => join(directory, `${schedulerLabel(home)}.plist`);
9
9
 
10
10
  export function schedulerPlist(home, dscodePath) {
11
- const args = [process.execPath, dscodePath, 'trigger', 'scheduler', 'start'];
11
+ const args = [...(/\.[cm]?js$/.test(dscodePath) ? [process.execPath, dscodePath] : [dscodePath]), 'trigger', 'scheduler', 'start'];
12
12
  const log = join(home, 'triggers', 'scheduler.log');
13
13
  return `<?xml version="1.0" encoding="UTF-8"?>
14
14
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -15,7 +15,8 @@ export function launchJobWorker({ home, dscodePath, job }) {
15
15
  const fd = openSync(join(directory, `${job.id}.log`), 'a', 0o600);
16
16
  let child;
17
17
  try {
18
- child = spawn(process.execPath, [dscodePath, 'trigger', 'run-job', job.id], {
18
+ const script = /\.[cm]?js$/.test(dscodePath);
19
+ child = spawn(script ? process.execPath : dscodePath, [...(script ? [dscodePath] : []), 'trigger', 'run-job', job.id], {
19
20
  cwd: home, env: { ...process.env, DSH_HOME: home, DSCODE_HOME: home },
20
21
  stdio: ['ignore', fd, fd],
21
22
  });
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { hookEvents, validateHooks } from './hooks.mjs';
4
- import { enabledFlag } from './workspace-discovery.mjs';
4
+ import { enabledFlag, isolatedDshPath } from './workspace-discovery.mjs';
5
5
 
6
6
  // Project hook files layer on top of the installation file by default. They run as
7
7
  // the OS user outside tool approval, so a cloned repository can install gates;
@@ -67,6 +67,7 @@ export function resolveHookSources({ root, cwd = root, env = process.env }) {
67
67
  if (!projectHooksEnabled(env)) return { sources, skipped };
68
68
  for (const file of projectHookFiles) {
69
69
  const path = join(cwd, file.path);
70
+ if (isolatedDshPath(path, env)) continue;
70
71
  if (!existsSync(path)) continue;
71
72
  let parsed;
72
73
  try {
@@ -12,6 +12,7 @@ import { hookEvents, validateHooks } from './hooks.mjs';
12
12
  import { redact } from '../auto-review/policy.mjs';
13
13
  import { plainConfig } from '../cordis-config/plain.mjs';
14
14
  import { analyzeDoctorEvidence, collectDoctorEvidence, recordRuntimeLog } from './doctor.mjs';
15
+ import { isolatedDshPath } from './workspace-discovery.mjs';
15
16
 
16
17
  export const name = 'dscode-tui-tools';
17
18
  export const inject = ['commands', 'agents', 'tools', 'skills', 'permissionPresets', 'sessionPersistence', 'llm'];
@@ -53,6 +54,7 @@ export async function findConflicts(cwd, configs, winners, env = process.env) {
53
54
  const candidates = new Map();
54
55
  const errors = [];
55
56
  for (const root of new Set(roots)) {
57
+ if (isolatedDshPath(root, env)) continue;
56
58
  let entries;
57
59
  try { entries = await readdir(root, { withFileTypes: true }); }
58
60
  catch (e) { if (e.code !== 'ENOENT') errors.push(`${root}: ${e.code}`); continue; }
@@ -29,16 +29,17 @@ export function newerVersion(candidate, current) {
29
29
  return false;
30
30
  }
31
31
 
32
- /** Newest launcher version on npm, or undefined when the registry cannot be read. */
33
- export async function fetchLatestVersion({ fetchImpl = fetch, timeoutMs = 8000 } = {}) {
32
+ /** Newest GitHub application or legacy npm launcher; undefined on lookup failure. */
33
+ export async function fetchLatestVersion({ fetchImpl = fetch, timeoutMs = 8000, standalone = !!process.env.DSCODE_STANDALONE_ROOT } = {}) {
34
34
  const controller = new AbortController();
35
35
  const timer = setTimeout(() => controller.abort(), timeoutMs);
36
36
  try {
37
37
  // Plain JSON: the registry answers 406 for the abbreviated metadata type on the
38
38
  // `/latest` dist-tag endpoint.
39
- const response = await fetchImpl(REGISTRY_URL, { headers: { accept: 'application/json' }, signal: controller.signal });
39
+ const response = await fetchImpl(standalone ? 'https://api.github.com/repos/qiz029/dscode/releases/latest' : REGISTRY_URL, { headers: { accept: 'application/json' }, signal: controller.signal });
40
40
  if (!response.ok) return undefined;
41
- const version = (await response.json())?.version;
41
+ const body = await response.json();
42
+ const version = standalone ? (body.draft || body.prerelease ? undefined : body.tag_name?.replace(/^v/, '')) : body.version;
42
43
  return typeof version === 'string' && VERSION_PATTERN.test(version) ? version : undefined;
43
44
  } catch { return undefined; }
44
45
  finally { clearTimeout(timer); }
@@ -55,6 +56,7 @@ export function stateHome(env = process.env, home = process.env.HOME) {
55
56
  */
56
57
  export function planUpdate({ moduleUrl, version = 'latest', execPath = process.execPath } = {}) {
57
58
  const root = resolve(dirname(fileURLToPath(moduleUrl)), '..', '..');
59
+ if (existsSync(join(root, '.dscode-standalone.json'))) return { label: `self-contained ${root}`, command: join(root, 'dscode'), args: ['update', version] };
58
60
  const entry = join(root, 'bin/dscode.mjs');
59
61
  if (existsSync(entry)) return { label: `tar/source ${root}`, command: execPath, args: [entry, 'update', version] };
60
62
  if (existsSync(join(root, 'node_modules', LAUNCHER_PACKAGE, 'cli.mjs'))) return { label: 'npm launcher (bundle tree)', command: execPath, args: [join(root, 'node_modules', LAUNCHER_PACKAGE, 'cli.mjs'), 'update', version] };
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
2
- import { dirname, join, resolve, sep } from 'node:path';
2
+ import { basename, dirname, join, resolve, sep } from 'node:path';
3
+ import { homedir } from 'node:os';
3
4
 
4
5
  // Discovery above the project root, bounded at the home directory rather than the
5
6
  // filesystem root: a shared workspace directory such as ~/Workspace can contribute
@@ -25,7 +26,20 @@ export const skillAncestorsEnabled = (env = process.env) => !disabledFlag(env.DS
25
26
  // whatever $HOME says, so the comparison canonicalizes both sides. The chain itself
26
27
  // keeps the caller's logical paths, which is what the skill and instruction probes
27
28
  // then stat.
28
- const canonical = path => { try { return realpathSync(path); } catch { return resolve(path); } };
29
+ const canonical = path => {
30
+ try { return realpathSync(path); } catch {
31
+ const absolute = resolve(path), parent = dirname(absolute);
32
+ return parent === absolute ? absolute : join(canonical(parent), basename(absolute));
33
+ }
34
+ };
35
+
36
+ /** Even when ~ is the working directory, its .dsh is another application's home. */
37
+ export function isolatedDshPath(path, env = process.env) {
38
+ if (!env.DSCODE_STANDALONE_ROOT) return false;
39
+ const foreign = canonical(join(env.HOME || homedir(), '.dsh'));
40
+ const target = canonical(path);
41
+ return target === foreign || target.startsWith(foreign + sep);
42
+ }
29
43
 
30
44
  // Every directory from the working directory up to home, nearest first. An empty
31
45
  // list means home is not an ancestor, and the mode then contributes nothing.
@@ -68,7 +82,7 @@ export function ancestorSkillDirs({ cwd, home, env = process.env }) {
68
82
  // The provider already scans these two at rank 100/200 for the owning project.
69
83
  if (dir === projectRoot && (root === '.dsh/skills' || root === '.agents/skills')) continue;
70
84
  const path = join(dir, root);
71
- if (existsSync(path) && !dirs.includes(path)) dirs.push(path);
85
+ if (existsSync(path) && !isolatedDshPath(path, env) && !dirs.includes(path)) dirs.push(path);
72
86
  }
73
87
  }
74
88
  return dirs;