diff --git a/CONTEXT.md b/CONTEXT.md index afa4775de..e62f16dbd 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -182,6 +182,20 @@ _Avoid_: Explorer (VS Code's native file explorer is separate), Panel (the in-ap The multi-document file workspace in the side panel. Holds zero or more files as inner tabs, each rendering its content (markdown rendered with a toggle to edit; text/code files in a CodeMirror editor; images and PDFs inline). Files arrive via Sidebar single-click or a Chat file pill and accumulate as tabs — each closeable and drag-reorderable; opening an already-open file focuses its existing tab rather than duplicating it. A breadcrumb bar under each pane's tab strip shows the active file's project-relative path with interactive sibling navigation. Supports recursive split panes via edge-drop: dragging a tab toward a pane's edge divides the view, and each resulting pane keeps its own tab bar, breadcrumb, zoom, and preview/edit toggle. Empty panes auto-collapse; a minimum pane dimension is enforced so splits can't shrink below a usable size. Opens and activates automatically when the first file is selected; shows a placeholder when no file is open. For committed editing, double-click the Sidebar entry to open a native VS Code tab. _Avoid_: Editor (Preview is a multi-document viewer, not a primary editor — committed editing belongs in a native VS Code tab), File browser (the Sidebar is still the primary project-wide file tree; the breadcrumb is a contextual sibling-navigation aid scoped to the open file, not a second tree) +### Developer tooling + +**Rebuild (local)**: +One of the two developer-tools Rebuild buttons. Builds the amicode binary and extension from the current working tree exactly as it sits — no git checkout, no pull — so a developer's in-progress edits are what gets built. The inner-loop "build what I have" action. Its request mode is `local`. +_Avoid_: Rebuild Remotely (retired wording), local build (as the concept name — it is a Rebuild mode) + +**Rebuild (from main)**: +The other developer-tools Rebuild button. Syncs the amicode repo to `origin/main` (fetch + checkout main + fast-forward-only pull) and then builds — the "reset to the shared tip and build that" action. Its request mode is `main`. The two Rebuild buttons diverge only at this git step; every later build phase is identical. +_Avoid_: Rebuild Remotely / Rebuild from Latest (retired wordings), remote rebuild (the fork-era `remote` mode is retired) + +**binary live-swap (retired)**: +The retired mechanism by which the developer-tools "opencode repo path" field resolved a fork binary from a path on disk and set the `opencodeBinary` override to it, restarting the server. Retired with fork absorption (#1091/#1115): the binary is now produced by the in-repo overlay build, so the full local Rebuild is the single dev build path. The general `opencodeBinary` override itself survives — it is consumed by the boot/health paths and still cleared on the developer-mode toggle-off; only the path-field-fed swap is gone. +_Avoid_: opencode repo path field (removed), binary override (the general override is not the live-swap) + ### Orthogonal axes **Domain Pack**: diff --git a/docs/adr/0018-rebuild-button-semantics.md b/docs/adr/0018-rebuild-button-semantics.md new file mode 100644 index 000000000..fafe19b98 --- /dev/null +++ b/docs/adr/0018-rebuild-button-semantics.md @@ -0,0 +1,54 @@ +# The developer Rebuild has two modes — local builds the working tree, main syncs first — and the binary live-swap is retired + +Status: accepted (2026-09-13) + +The developer-tools Rebuild surface offers two buttons, and after the opencode fork was +absorbed (#1091) the amicode binary is built from the overlay/materialized tree in-repo — +there is no longer a separate opencode repository to point at or a fork binary to swap in. +This ADR records what the two buttons now mean and why the field that fed the old binary +live-swap is gone. + +**The two modes.** The Rebuild request carries a `mode` with exactly two values. +`local` builds the current working tree **as it sits** — no git checkout, no pull — so a +developer's in-progress edits are what gets built. `main` syncs the amicode repo to +`origin/main` first (`git fetch origin && git checkout main && git pull --ff-only origin +main`) and then builds. The two are intentionally non-interchangeable: `local` is the +inner-loop "build what I have" action, `main` is the "reset to the shared tip and build +that" action. The vocabulary is `local | main` end to end — UI button, controller +payload, extension-host handler, and tests — replacing the fork-era `remote` value, which +is retired and resolves to the safe, non-destructive `local` default if it is ever seen. + +**Why they must differ observably.** Before this decision the handler ignored `mode` and +always ran the checkout-main + fast-forward pull, so "Rebuild Locally" silently discarded +the working tree by pulling to main — the two buttons were identical and the local button +was actively misleading. The git step is now the single point where the two modes diverge, +derived from `mode` by one pure function so the difference is directly testable: `local` +emits no git command, `main` emits the fetch/checkout/ff-pull. + +**The binary live-swap is retired.** The old surface carried an "opencode repo path" field +whose only job was to resolve a fork binary from a path on disk and set the +`amicode.opencodeBinary` override to it (a live-swap), restarting the server. In the +overlay-build world that path points at nothing meaningful: the binary is produced by the +in-repo build, not resolved from a sibling fork checkout. The field, its controller +getter/setter/commit, its settings-context entry, its localization keys, and the two +path-fed binary writes in the update handler are all removed. The full local Rebuild is now +the single dev build path. + +**What is preserved.** The general `amicode.opencodeBinary` override setting is *not* the +live-swap and is kept intact: it is consumed by the fleet-health and boot-override paths, +and the developer-mode toggle-off must still clear it (alongside `devAssetRoot`) when +restoring the marketplace build. Only the binary write that the removed path field fed is +gone; the override's boot/health consumers and its toggle-off clear are untouched. + +**Alternatives considered.** Keeping a single Rebuild button was rejected — developers +genuinely need both "build my tree" and "build the shared tip", and collapsing them is what +produced the silent data-losing pull. Repurposing the opencode-path field to point at the +amicode repo was rejected as redundant with the existing amicode-path field. Retiring the +buttons in favor of a CLI-only rebuild was rejected as a workflow regression for the +in-editor developer loop. + +**Consequence.** Retiring the fork shell scripts (S9) and the orphaned coordinator +main-path (S10) are follow-ups outside this slice; this decision is the request surface and +the removal, not a change to the overlay build engine itself. + +Implementation: harmoniqs/amicode#1115 diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index aa4ae8426..aad277ead 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -161,31 +161,31 @@ "packages/app/src/context/server-session.ts": "a9e1f089ad328a705848d11ba25e890c5e374bb3a33066ffe67b6f09bd48ed16", "packages/app/src/context/server-sync.tsx": "43154ffe6227203d778378d190af3c46d7f49098619721fea5e6263e8794dc84", "packages/app/src/context/server.tsx": "08e978e4b4b3094221a2a0b92e069f39cbedb6d6cf95935881e21cde52265ee2", - "packages/app/src/context/settings.tsx": "53025fbfbd7eb641cdb0e7c49e21ced3617050f4f98ee45c745affdd8ab17b48", + "packages/app/src/context/settings.tsx": "19e078e81620ffa21a92797335009b84d4311485a37c5ede2f76906b734292ad", "packages/app/src/context/split.tsx": "891aeb290b369b327a63ce548dcaab1daca08e41fab430648787be7f84e53ec0", "packages/app/src/context/tabs.tsx": "eb494b5785defdd3a4901874be355ba8ddd7bab4c009359b558c413f9ee63ffa", "packages/app/src/context/vault-panel.ts": "f91cb5fb49a9f0d46bd2a1b131f9a218b30147e2ad3617d2a2dc4f1e14599a53", "packages/app/src/context/workbench.tsx": "69a44da2b69989bd2c67ebffdb49941841f3f4718d6a70d659c828b57928d9eb", "packages/app/src/context/zoom-keybind.test.ts": "a1385c93639687a7dac7d21f8f02841d82a55e42465ce3836b97b16c82b52469", - "packages/app/src/i18n/ar.ts": "b4f9ca56ed157c34f6248bc053f09c1ce565159a30eb971570c77884b780e686", - "packages/app/src/i18n/br.ts": "1bef54ba8796b2c94ff6e6b1c40f924a130f203da919b096c1ea6d363ee28e50", - "packages/app/src/i18n/bs.ts": "1317264cf4bd5f6ae1fb0636209c6f169978eb62685ea3c1f7ab80c34c3bf499", - "packages/app/src/i18n/da.ts": "dea807e84b8cc7f9410dd492682523f51966bc79d0f8f47fbc83afd47cad52ea", - "packages/app/src/i18n/de.ts": "fa9a91f52895e4dc421847595bf2ed0a8e88d4ca8afd7c50c4fcb063979f3512", - "packages/app/src/i18n/en.ts": "da4fde0a74423baca743182041a4028e6b18650e01b6dc8d5b81974d75e7ace2", - "packages/app/src/i18n/es.ts": "d16b85c3b53558f74dbbc16866c253102920438c4df5488a7b1ff9ba431f733a", - "packages/app/src/i18n/fr.ts": "9f6871fe2556b6ef864a84a285d95793a242f92c94b8b6a8f3774cb045aea929", - "packages/app/src/i18n/ja.ts": "46f8392f5de380a3212afd73d879837a6e1c70b986188de7ee6c1fd3ae7dca96", - "packages/app/src/i18n/ko.ts": "fdc97342c447915cbf5d6387c07d4c445c53f54d4bb669f876d7344876ca8bd4", - "packages/app/src/i18n/no.ts": "6e1a75c32e3ea0d7576da0255316a8ab450e1e3d963db3d5b92d25c55971b987", - "packages/app/src/i18n/pl.ts": "cbbdb0bd13405da313bba396a91356500bcc95d3a68f68060689773200b49442", - "packages/app/src/i18n/rebuild-label-rename.test.ts": "c9ad20e491a5c9c6b8f6d56858271988e827398afe8b7cbe333e5f970fe76142", - "packages/app/src/i18n/ru.ts": "43f38453f2192102dace0690bce8b81a60e376bfa44f62133cb99ae55a392125", - "packages/app/src/i18n/th.ts": "527fd9de237b2397063c3ee39de62856d9c174af4b62afc48b53f266cd2ab533", - "packages/app/src/i18n/tr.ts": "08f8794e6858898ec0dcdd8b0c9ae7dae218ef3f0878354b1b0f1282c3727877", - "packages/app/src/i18n/uk.ts": "7b534484e8abd0f3efcd0447efa414cdf7c2694c59e614767477d5578c570d8c", - "packages/app/src/i18n/zh.ts": "d31d243ecaf762b747a67354c7f929317fd340cf6896b6cbd2b402e55ca0bf9a", - "packages/app/src/i18n/zht.ts": "e21b1f58be6482bcd3121861b3ed6620acc8878ae1729fdff2b05cce92eedac4", + "packages/app/src/i18n/ar.ts": "61d4386d766bf6c870a23963d75da3910d6b882431d038728e01bff50d3c285a", + "packages/app/src/i18n/br.ts": "fb90ce7d989b06d61ff2792f63ebdf17f828e5233553817a8be084996471e1b5", + "packages/app/src/i18n/bs.ts": "eb6bd1f45e7f6bd70b9ace7ca85150de1c0a19ada66507ac97459be44419ea06", + "packages/app/src/i18n/da.ts": "18ee0ecb46c02bac0c9f85126515167427c4ab496aaa9bce552de45742843c0d", + "packages/app/src/i18n/de.ts": "c6a954bda39d8a7fda7d93c9bfc6d954c4e403675a9c95389f9e1d0ac4064f06", + "packages/app/src/i18n/en.ts": "83e4410780d17104a49765b07256e1b4f6d6da15b848029dbd5910ecb0b829e7", + "packages/app/src/i18n/es.ts": "1996572d2aff5931bb6fe3f634b3b055ac8c08327d22338939ff2121120110bc", + "packages/app/src/i18n/fr.ts": "fd496581ff6825ea323dec3ae18c2e1da4694a924fad9f1536a5a9bf1c1adb7a", + "packages/app/src/i18n/ja.ts": "3d37435f6761b0210db71ac85b8df5b8a0a31af61ad12a26374b595e0f3077ab", + "packages/app/src/i18n/ko.ts": "d10f60b9abd1b5ffbe0deaed8db4b17e77c9ba2c230b7b832bc4df6f48a7c5ee", + "packages/app/src/i18n/no.ts": "d6f23e78bb2b9f8a8a7ca35fa9a9fe8857c78fbe2e4efa93ad688bb00442502d", + "packages/app/src/i18n/pl.ts": "2479ba155669ce47d81526b426d0cbb9aa601884b00b258b71a86a6ce09d7f4a", + "packages/app/src/i18n/rebuild-label-rename.test.ts": "c481e79fcef5bd2edebdc3f4818afa8588787cb82110651cb7769958b356351d", + "packages/app/src/i18n/ru.ts": "6d3107cd3436d8ab6a1b5086c337af5331166acce2b51b2169bb3c1e75a6a81e", + "packages/app/src/i18n/th.ts": "81ad962167450fbdbb52f50ade1c2f49823fafcfdb3e781204f788c4ed696397", + "packages/app/src/i18n/tr.ts": "28862f80793b2e92c64280cb923b6c2321b04b8fea8da574220fd36abef4363c", + "packages/app/src/i18n/uk.ts": "428ad662bd8389e86458dc18eeb69e262006f6065ae183a6e082d879181d034a", + "packages/app/src/i18n/zh.ts": "99cb8baa717976e4f7e1bc682cffaa58661030d819d1b40d9dec6f8b56453c80", + "packages/app/src/i18n/zht.ts": "c330050df718d50f7cc55eb8ab4a1a5cde859fad9ca95f68315f0c5f399e36aa", "packages/app/src/pages/error.tsx": "db9ba0847cfb205100ad6133cce30c6ea2ce983802af44947e6d2b746c2a6ec5", "packages/app/src/pages/home-projects.test.ts": "a90155c37210a0e5b7a095bbfc3ba2d40681f02927d9726148a706bed5ba7891", "packages/app/src/pages/home-projects.ts": "d55124578dc839b98e2084b488d818e30e4a1abf1ae264499b83e403c2aabca7", @@ -611,12 +611,12 @@ "packages/app/src/components/session/vscode-explorer-file-icon.tsx": "b5ba0cafe61594dd2afe45ef6e070e512861162933c85e9545db2ec697d50f17", "packages/app/src/components/settings-v2/data-storage-controller.ts": "fa5d143cc101f3a3b9d5ad445edddc981e0d02783021d52dbfed6c8d8bf62498", "packages/app/src/components/settings-v2/data-storage.tsx": "a56b423b32c952c4379688d88d13a9f43b163a4169b9393f8c35b18a763a1180", - "packages/app/src/components/settings-v2/developer-tools-controller.ts": "8888e7da23d0e092c531c706470edb7ae410aef1aa8f3067729b3f7dfec5f48d", + "packages/app/src/components/settings-v2/developer-tools-controller.ts": "ad7bd9bd13271e98a074f59a479efda1a7a874e04086e8eaa3c67422c27cbd3c", "packages/app/src/components/settings-v2/developer-tools-rebuild-flags.test.ts": "4d11815fe5acfa930bc418d0be82602f03821997300d0deef0aa2da2a743b3b0", "packages/app/src/components/settings-v2/developer-tools-rebuild-flags.ts": "561a75fa456b832d4af6f782c7ecf5269ce768fc2adc59c15a79197a2dde84c0", "packages/app/src/components/settings-v2/developer-tools-request-state.test.ts": "29711b3bc17fddbb8d046e104bc78e775a9ec7c5aa0ee2e181fbcd8b2b5e2135", "packages/app/src/components/settings-v2/developer-tools-request-state.ts": "e9678ed32e7c2562b18143f39a51c0a94dbf0a0884d056653c6c77e4cd07a9bd", - "packages/app/src/components/settings-v2/developer-tools.tsx": "ee2d586cfa1202f5fc253b502dd8752256166192541e7addab6bb5da18d1d0f5", + "packages/app/src/components/settings-v2/developer-tools.tsx": "0e3a739a1ed3cb6311f93174080a3bce71219cee9bcaccd0719d3bf9e6932988", "packages/app/src/components/settings-v2/dialog-settings-v2.tsx": "a1e1fb7eea7bbad7708b97a721b54a4897c8cb6f582b71d6bb7c8ca7f44930db", "packages/app/src/components/settings-v2/general.tsx": "2abf6c9c7b4b9bf020ed224dbc7fff4574db47711956fc08e1b8e84e781d395a", "packages/app/src/components/settings-v2/permission-scope-relocation.test.ts": "39539c4fc4b7433f08b913100f48a7fa75d6ab47615ba697aa9ae8eee09d316b", @@ -968,7 +968,7 @@ "review": "harmoniqs/amicode#1004" } ], - "extracted_at": "2026-09-14T04:43:06.305Z", + "extracted_at": "2026-09-14T05:04:46.530Z", "per_package": { "packages/app": { "A": 164, diff --git a/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts b/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts index be227e3ce..0993e44ab 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts +++ b/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts @@ -40,7 +40,7 @@ export function createDeveloperToolsController() { // On mount, check if we just came back from a rebuild (successful or in-progress). // The "rebuilding" flag survives iframe reloads caused by file-watcher churn - // (e.g. git checkout in a watched workspace folder during remote rebuild). + // (e.g. git checkout in a watched workspace folder during a from-main rebuild). onMount(() => { try { const wasRebuilding = localStorage.getItem("amicode:devtools-rebuilding") === "1" @@ -179,14 +179,13 @@ export function createDeveloperToolsController() { source: "amicode", kind: "dev-tools-update", enabled: settings.developer.enabled(), - opencodePath: settings.developer.opencodePath(), amicodePath: settings.developer.amicodePath(), }, "*", ) } - const rebuild = (mode: "local" | "remote") => { + const rebuild = (mode: "local" | "main") => { if (!inAmicode()) return if (rebuildState() === "rebuilding") return // prevent double-clicks setRebuildState("rebuilding") @@ -197,7 +196,6 @@ export function createDeveloperToolsController() { source: "amicode", kind: "dev-tools-rebuild", mode, - opencodePath: settings.developer.opencodePath(), amicodePath: settings.developer.amicodePath(), }, "*", @@ -229,29 +227,21 @@ export function createDeveloperToolsController() { source: "amicode", kind: "dev-tools-update", enabled: false, - opencodePath: settings.developer.opencodePath(), amicodePath: settings.developer.amicodePath(), }, "*", ) } }, - opencodePath: settings.developer.opencodePath, - setOpencodePath: (value: string) => { - settings.developer.setOpencodePath(value) - }, amicodePath: settings.developer.amicodePath, setAmicodePath: (value: string) => { settings.developer.setAmicodePath(value) }, /** Trigger validation + apply on blur — only in developer mode (not devcontainer-only mode) */ - commitOpencodePath: () => { - if (settings.developer.enabled()) sendUpdate() - }, commitAmicodePath: () => { if (settings.developer.enabled()) sendUpdate() }, - /** Trigger a full rebuild (local = from disk, remote = git pull first) */ + /** Trigger a full rebuild (local = build the working tree as-is; main = checkout main + ff-pull first) */ rebuild, status, pending, @@ -275,7 +265,6 @@ export function createDeveloperToolsController() { window.parent.postMessage({ source: "amicode", kind: "dev-tools-build-vsix", - opencodePath: settings.developer.opencodePath(), amicodePath: settings.developer.amicodePath(), outputPath: settings.developer.vsixOutputPath(), }, "*") diff --git a/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools.tsx b/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools.tsx index e9c67dcbf..99546c289 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools.tsx @@ -73,11 +73,6 @@ const RebuildErrorPanel: Component<{ controller: DeveloperToolsController }> = ( const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> = (props) => { const language = useLanguage() - const opencodeError = () => { - const s = props.controller.status() - if (!s || s.opencodeValid) return undefined - return s.opencodeError ?? language.t("settings.general.row.opencodePath.error.notFound") - } const amicodeError = () => { const s = props.controller.status() if (!s || s.amicodeValid) return undefined @@ -103,7 +98,7 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> props.controller.rebuild("remote")} + onClick={() => props.controller.rebuild("main")} disabled={isRebuilding()} > {language.t("settings.general.row.devTools.rebuildRemotely")} @@ -162,40 +157,6 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> {/* Path inputs — visible when EITHER mode is ON */} - - {language.t("settings.general.row.opencodePath.description")} - - - {opencodeError()} - - - - Validating… - - - } - > -
- props.controller.setOpencodePath(event.currentTarget.value)} - onBlur={() => props.controller.commitOpencodePath()} - placeholder={language.t("settings.general.row.opencodePath.placeholder")} - spellcheck={false} - autocorrect="off" - autocomplete="off" - autocapitalize="off" - aria-label={language.t("settings.general.row.opencodePath.title")} - /> -
-
- store.developer?.opencodePath, defaultSettings.developer.opencodePath), - setOpencodePath(value: string) { - setStore("developer", "opencodePath", value) - }, amicodePath: withFallback(() => store.developer?.amicodePath, defaultSettings.developer.amicodePath), setAmicodePath(value: string) { setStore("developer", "amicodePath", value) diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts index e8ff442a8..0407a5030 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ar.ts @@ -759,12 +759,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/br.ts b/packages/app-bundle/overlay/packages/app/src/i18n/br.ts index 9d0584c27..faca5ab49 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/br.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/br.ts @@ -771,12 +771,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts b/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts index 2c8708f9f..35b1baaf0 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/bs.ts @@ -837,12 +837,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/da.ts b/packages/app-bundle/overlay/packages/app/src/i18n/da.ts index a5eec9195..7f2a3d508 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/da.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/da.ts @@ -829,12 +829,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/de.ts b/packages/app-bundle/overlay/packages/app/src/i18n/de.ts index b36acacf6..90845c58e 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/de.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/de.ts @@ -783,12 +783,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/en.ts b/packages/app-bundle/overlay/packages/app/src/i18n/en.ts index 7aae3a50f..c593a9c9d 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/en.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/en.ts @@ -1015,12 +1015,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/es.ts b/packages/app-bundle/overlay/packages/app/src/i18n/es.ts index 0709ff7e8..9f49c3dab 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/es.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/es.ts @@ -842,12 +842,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts b/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts index c3e8d1483..b9ccbad71 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/fr.ts @@ -781,12 +781,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts index ad14f203c..3b35bc2fd 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ja.ts @@ -766,12 +766,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts index ae25d7021..171f532da 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ko.ts @@ -608,12 +608,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/no.ts b/packages/app-bundle/overlay/packages/app/src/i18n/no.ts index 214343306..6e3c50532 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/no.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/no.ts @@ -682,12 +682,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts b/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts index 21d869569..0e0861c43 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/pl.ts @@ -771,12 +771,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/rebuild-label-rename.test.ts b/packages/app-bundle/overlay/packages/app/src/i18n/rebuild-label-rename.test.ts index 6b9d0bf04..91aeb2327 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/rebuild-label-rename.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/rebuild-label-rename.test.ts @@ -51,3 +51,25 @@ describe("devtools rebuild-remotely label rename (#940)", () => { expect(dict["settings.general.row.devTools.rebuildRemotely"]).toBe(NEW_LABEL) }) }) + +// ============================================================================ +// The "opencode repo path" developer-tools field is retired in the +// overlay-build world (#1115): the fork is absorbed, so there is no separate +// opencode repo to point at. Every locale that carried the row's keys must +// drop them — a partial removal would leave a dangling key referenced by no +// UI, or (worse) a stale key the UI still reads. +// ============================================================================ + +describe("devtools opencode-path row key removal (#1115)", () => { + const OPENCODE_PATH_KEY = ["settings.general.row", "opencodePath"].join(".") + + test("no locale file carries the retired opencode-path row key", () => { + const localeFiles = readdirSync(i18nDir) + .filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")) + .map((name) => join(i18nDir, name)) + const offenders = localeFiles.filter((file) => + readFileSync(file, "utf8").includes(OPENCODE_PATH_KEY), + ) + expect(offenders).toEqual([]) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts b/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts index aac8fae82..ab9ec6cb6 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/ru.ts @@ -838,12 +838,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/th.ts b/packages/app-bundle/overlay/packages/app/src/i18n/th.ts index 09ef788f8..9589137c2 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/th.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/th.ts @@ -825,12 +825,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts b/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts index 0134657cb..b0ba4e3cb 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/tr.ts @@ -844,12 +844,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts b/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts index 71ca74a8e..688320269 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/uk.ts @@ -931,12 +931,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts b/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts index fac211ae0..936b2a1ad 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/zh.ts @@ -818,12 +818,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts b/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts index 124a36b60..8881060e6 100644 --- a/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts +++ b/packages/app-bundle/overlay/packages/app/src/i18n/zht.ts @@ -815,12 +815,6 @@ export const dict = { "settings.general.row.developerMode.title": "Use developer build", "settings.general.row.developerMode.description": "Switch between the release and a local developer build of the opencode server and Amicode extension", - "settings.general.row.opencodePath.title": "opencode repo path", - "settings.general.row.opencodePath.description": - "Path to your local opencode repository root (the binary is resolved from here)", - "settings.general.row.opencodePath.placeholder": "/path/to/opencode", - "settings.general.row.opencodePath.error.notFound": "Binary not found at this path", - "settings.general.row.opencodePath.error.notExecutable": "Binary exists but is not executable", "settings.general.row.amicodePath.title": "Amicode repo path", "settings.general.row.amicodePath.description": "Path to your local Amicode repository root (rebuilds extension and reloads on change)", diff --git a/packages/extension/src/chat_bridge.ts b/packages/extension/src/chat_bridge.ts index dcde5fd3b..de22aa34e 100644 --- a/packages/extension/src/chat_bridge.ts +++ b/packages/extension/src/chat_bridge.ts @@ -39,18 +39,29 @@ export function resolveDbBackupDir(sessionDatabase: string): string { return opencodeDataDir(); } -export type RebuildMode = "overlay" | "local" | "main"; - -/** The app build has two intentionally non-interchangeable source contracts. - * @deprecated — retained for backward compat; overlay mode is the only path. */ -export function appBundleBuildCommand(mode: RebuildMode, opencodePath: string): string { - const contract = mode === "local" ? "--direct-worktree" : "--verified-main"; - return `pnpm --filter amicode run build:app -- --work "${opencodePath}" ${contract}`; +export type RebuildMode = "local" | "main"; + +/** + * Resolve the rebuild request's `mode` to the two-button vocabulary (#1115). + * `local` builds the working tree as-is; `main` syncs to origin/main first. + * Anything missing or unrecognised (including any retired fork-era mode) + * resolves to the safe, non-destructive `local` default. + */ +export function resolveRebuildMode(msg: unknown): RebuildMode { + return (msg as { mode?: unknown } | null)?.mode === "main" ? "main" : "local"; } -/** @deprecated — retained for backward compat; overlay mode verifies via build:binary. */ -export function mainOverlayVerificationCommand(opencodePath: string): string { - return `node packages/app-bundle/scripts/overlay-promotion.mjs --check --source "${opencodePath}" --revision "$(git -C "${opencodePath}" rev-parse HEAD)"`; +/** + * The git step a rebuild runs before building, keyed on the button pressed — + * the one place the two Rebuild buttons diverge (#1115). `local` builds the + * working tree exactly as it sits (no git mutation → null); `main` syncs to + * origin/main first (fetch + checkout main + fast-forward-only pull). + */ +export function rebuildGitCommand(mode: RebuildMode): string | null { + if (mode === "main") { + return "git fetch origin && git checkout main && git pull --ff-only origin main"; + } + return null; } // Commands the in-app palette (opencode "Amico" command group) may trigger via @@ -414,15 +425,14 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean return true; } - // Developer Tools settings: validate paths, swap the opencode binary + - // restart its server as appropriate. The app posts on blur and on toggle. - // Committing the amicode path is validate-only — no build, no reload; see - // the note at that branch below (#941). + // Developer Tools settings: validate the amicode repo path. The app posts + // on blur and on toggle. Committing the amicode path is validate-only — no + // build, no reload; see the note at that branch below (#941). The opencode + // repo-path field and the binary live-swap it fed are retired in the + // overlay-build world (#1115) — the general opencodeBinary override is still + // cleared on toggle-off, but nothing here sets it from a resolved path. if (msg.kind === "dev-tools-update") { const enabled = (msg as { enabled?: unknown }).enabled === true; - const opencodePath = typeof (msg as { opencodePath?: unknown }).opencodePath === "string" - ? (msg as unknown as { opencodePath: string }).opencodePath.trim().replace(/^~/, os.homedir()) - : ""; const amicodePath = typeof (msg as { amicodePath?: unknown }).amicodePath === "string" ? (msg as unknown as { amicodePath: string }).amicodePath.trim().replace(/^~/, os.homedir()) : ""; @@ -480,21 +490,6 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean return true; } - // Validate opencode path: resolve the binary from the repo root - let resolvedBinary = ""; - if (opencodePath) { - const resolution = findForkedOpencodeBinary(opencodePath); - if (resolution.found) { - resolvedBinary = resolution.path; - } else if (resolution.reason === "not-executable") { - reply.opencodeValid = false; - reply.opencodeError = "Binary exists but is not executable"; - } else { - reply.opencodeValid = false; - reply.opencodeError = "Binary not found at this path"; - } - } - // Validate amicode path: must be a repo root with packages/extension if (amicodePath) { const extensionDir = path.join(amicodePath, "packages", "extension"); @@ -523,30 +518,14 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean } } - // Apply valid settings - if (reply.opencodeValid && opencodePath) { - void vscode.workspace.getConfiguration("amicode").update( - "opencodeBinary", resolvedBinary || opencodePath, vscode.ConfigurationTarget.Global, - ); - void vscode.commands.executeCommand("amicode.restartServer"); - reply.serverRestarted = true; - } else if (reply.opencodeValid && !opencodePath) { - // Empty path with enabled ON → clear the override (use vendored) - void vscode.workspace.getConfiguration("amicode").update("opencodeBinary", "", vscode.ConfigurationTarget.Global); - void vscode.commands.executeCommand("amicode.restartServer"); - reply.serverRestarted = true; - } - // Committing the amicode path only validates it (above) — it never - // builds or reloads on its own (#941). Blurring a path field used to - // eagerly run a real `bun run build` and auto-reload the window with no - // confirmation, and since the app always resends BOTH current paths on - // any field's blur, even an unrelated edit to the opencode field would - // retrigger it. Building is now exclusively an explicit action — the - // "Rebuild Locally" / "Rebuild from Main" buttons (dev-tools-rebuild, - // below), which are self-sufficient and don't depend on anything set - // here. Clearing the path to empty still clears any devAssetRoot - // override, since that's just removing a setting, not building one. + // builds or reloads on its own (#941). Building is exclusively an explicit + // action — the "Rebuild Locally" / "Rebuild from Main" buttons + // (dev-tools-rebuild, below), which are self-sufficient and don't depend + // on anything set here. The binary live-swap that the removed opencode-path + // field fed is retired (#1115): nothing here sets opencodeBinary from a + // resolved path anymore. Clearing the amicode path to empty still clears + // any devAssetRoot override, since that's just removing a setting. if (reply.amicodeValid && !amicodePath) { void vscode.workspace.getConfiguration("amicode").update("devAssetRoot", "", vscode.ConfigurationTarget.Global); } @@ -556,8 +535,10 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean } // Full rebuild: builds the binary from the materialized overlay tree - // via build:binary (bun required). Replaces the old fork-based dual modes. + // via build:binary (bun required). Two buttons, two modes (#1115): + // `local` builds the working tree as-is; `main` syncs to origin/main first. if (msg.kind === "dev-tools-rebuild") { + const mode = resolveRebuildMode(msg); const amicodePath = typeof (msg as { amicodePath?: unknown }).amicodePath === "string" ? (msg as unknown as { amicodePath: string }).amicodePath.trim().replace(/^~/, os.homedir()) : ""; @@ -666,19 +647,25 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean // DB backup is best-effort } - // ── Git pull (amicode repo) ── - io.postToWebview({ - source: "amicode", kind: "dev-tools-rebuild-status", - tab: (msg as { tab?: string }).tab, state: "rebuilding", - phase: "pulling", detail: "Pulling amicode repo...", - }); - const checkoutAc = await run("git fetch origin && git checkout main && git pull --ff-only origin main", amicodePath); - if (!checkoutAc.ok) { + // ── Git sync (amicode repo) — mode-dependent (#1115) ── + // `local` builds the working tree exactly as it sits (no git mutation); + // `main` syncs to origin/main first. This is the one place the two + // Rebuild buttons diverge. + const gitCommand = rebuildGitCommand(mode); + if (gitCommand) { io.postToWebview({ - source: "amicode", kind: "dev-tools-rebuild-status", tab: (msg as { tab?: string }).tab, - state: "failed", error: `git pull (amicode) failed: ${checkoutAc.error?.slice(0, 150)}`, + source: "amicode", kind: "dev-tools-rebuild-status", + tab: (msg as { tab?: string }).tab, state: "rebuilding", + phase: "pulling", detail: "Syncing amicode repo to origin/main...", }); - return; + const checkoutAc = await run(gitCommand, amicodePath); + if (!checkoutAc.ok) { + io.postToWebview({ + source: "amicode", kind: "dev-tools-rebuild-status", tab: (msg as { tab?: string }).tab, + state: "failed", error: `git sync (amicode) failed: ${checkoutAc.error?.slice(0, 150)}`, + }); + return; + } } // ── Install amicode dependencies ── diff --git a/packages/extension/test/chat_bridge.test.ts b/packages/extension/test/chat_bridge.test.ts index ad06224ec..209a19b31 100644 --- a/packages/extension/test/chat_bridge.test.ts +++ b/packages/extension/test/chat_bridge.test.ts @@ -4,11 +4,11 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { - appBundleBuildCommand, extractReportBugModel, handleAmicodeBridgeMessage, - mainOverlayVerificationCommand, + rebuildGitCommand, resolveDbBackupDir, + resolveRebuildMode, type BridgeIo, } from "../src/chat_bridge"; @@ -53,33 +53,41 @@ beforeEach(() => { ws.configUpdates.length = 0; }); -describe("developer-tools rebuild contracts (#1004)", () => { - it("builds a local worktree without a deployment or overlay gate", () => { - expect(appBundleBuildCommand("local", "/tmp/opencode")).toBe( - 'pnpm --filter amicode run build:app -- --work "/tmp/opencode" --direct-worktree', - ); +describe("developer-tools rebuild mode — local vs main (#1115)", () => { + it("resolves the request mode to local | main, defaulting missing/legacy values to local", () => { + expect(resolveRebuildMode({ mode: "main" })).toBe("main"); + expect(resolveRebuildMode({ mode: "local" })).toBe("local"); + // Any missing or unrecognised mode is the safe, non-destructive default + // (build the working tree as-is) — a retired fork-era mode never silently + // triggers a checkout-main + pull. + expect(resolveRebuildMode({})).toBe("local"); + expect(resolveRebuildMode({ mode: "legacy-unknown" })).toBe("local"); }); - it("verifies the exact OpenCode revision before a main rebuild installs dependencies", () => { - expect(mainOverlayVerificationCommand("/tmp/opencode")).toContain( - 'node packages/app-bundle/scripts/overlay-promotion.mjs --check --source "/tmp/opencode" --revision "$(git -C "/tmp/opencode" rev-parse HEAD)"', - ); - expect(appBundleBuildCommand("main", "/tmp/opencode")).toBe( - 'pnpm --filter amicode run build:app -- --work "/tmp/opencode" --verified-main', + it("local builds the working tree as-is — no git checkout/pull is emitted", () => { + expect(rebuildGitCommand("local")).toBeNull(); + }); + + it("main syncs to origin/main first — fetch + checkout main + ff-only pull", () => { + expect(rebuildGitCommand("main")).toBe( + "git fetch origin && git checkout main && git pull --ff-only origin main", ); }); + + it("the two buttons produce observably different git behavior", () => { + expect(rebuildGitCommand("local")).not.toEqual(rebuildGitCommand("main")); + }); }); -describe("developer-tools rebuild from main (#1018)", () => { - it("main mode requires only amicodePath, not opencodePath", async () => { +describe("developer-tools rebuild — only amicodePath is required (#1018/#1115)", () => { + it("main mode requires only amicodePath, not an opencode path", async () => { const host = io(); - // Main mode with amicodePath but no opencodePath should not fail with - // "Both repo paths must be set" — only amicodePath is required. + // Main mode with amicodePath should not fail with "Both repo paths must + // be set" — only amicodePath is required. const handled = handleAmicodeBridgeMessage({ source: "amicode", kind: "dev-tools-rebuild", - mode: "remote", - opencodePath: "", + mode: "main", amicodePath: "/tmp/amicode", }, host); expect(handled).toBe(true); @@ -90,13 +98,12 @@ describe("developer-tools rebuild from main (#1018)", () => { expect(failMsg).toBeUndefined(); }); - it("main mode fails when amicodePath is empty", async () => { + it("fails when amicodePath is empty (either mode)", async () => { const host = io(); handleAmicodeBridgeMessage({ source: "amicode", kind: "dev-tools-rebuild", - mode: "remote", - opencodePath: "", + mode: "main", amicodePath: "", }, host); const failMsg = host.posted.find( @@ -106,12 +113,13 @@ describe("developer-tools rebuild from main (#1018)", () => { expect(failMsg!.error).toContain("Amicode repo path"); }); - it("rebuild requires only amicodePath (no opencodePath needed)", async () => { + it("a local rebuild requires only amicodePath", async () => { const host = io(); // With amicodePath only — should not fail with a path-validation error handleAmicodeBridgeMessage({ source: "amicode", kind: "dev-tools-rebuild", + mode: "local", amicodePath: "/tmp/amicode", }, host); const failMsg = host.posted.find( @@ -736,7 +744,9 @@ describe("amicode bridge — project-selected (#663)", () => { }); // ============================================================================ -// dev-tools-update: path validation, tilde expansion (#940) +// dev-tools-update: amicode path validation, tilde expansion (#940). The +// opencode repo-path field is retired (#1115): the update handler validates +// only the amicode path and no longer reads or validates an opencode path. // ============================================================================ describe("amicode bridge — dev-tools-update path validation", () => { @@ -744,16 +754,9 @@ describe("amicode bridge — dev-tools-update path validation", () => { let fakeAmicodeRepo: string; beforeEach(() => { - // Create a temporary directory tree that mimics valid repo layouts: - // /opencode/dist/opencode/bin/opencode (executable binary) - // /amicode/packages/extension/ (directory) + // Create a temporary directory tree that mimics a valid amicode repo: + // /amicode/packages/extension/ (directory) tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "devtools-test-")); - const binDir = path.join(tmpRoot, "opencode", "dist", "opencode", "bin"); - fs.mkdirSync(binDir, { recursive: true }); - const fakeBinary = path.join(binDir, "opencode"); - fs.writeFileSync(fakeBinary, "#!/bin/sh\n"); - fs.chmodSync(fakeBinary, 0o755); - const extDir = path.join(tmpRoot, "amicode", "packages", "extension"); fs.mkdirSync(extDir, { recursive: true }); fakeAmicodeRepo = path.join(tmpRoot, "amicode"); @@ -763,56 +766,24 @@ describe("amicode bridge — dev-tools-update path validation", () => { fs.rmSync(tmpRoot, { recursive: true, force: true }); }); - it("validates absolute paths correctly (baseline)", async () => { + it("validates a valid amicode path (baseline)", async () => { const host = io(); - const fakeOpencodeRepo = path.join(tmpRoot, "opencode"); handleAmicodeBridgeMessage({ source: "amicode", kind: "dev-tools-update", enabled: true, - opencodePath: fakeOpencodeRepo, amicodePath: fakeAmicodeRepo, }, host); await flush(); const reply = host.posted.find((m: any) => m.kind === "dev-tools-status") as any; expect(reply).toBeDefined(); - expect(reply.opencodeValid).toBe(true); expect(reply.amicodeValid).toBe(true); }); - it("expands ~ in opencodePath before validation", async () => { - // tmpRoot lives under os.tmpdir(), which is NOT under the home directory - // on macOS/Linux CI — so we symlink a home-relative alias into it and - // exercise the real tilde-expansion path the way a user's default - // "~/harmoniqs/opencode" setting would. - const home = os.homedir(); - const symlink = path.join(home, `.devtools-test-oc-${Date.now()}`); - fs.symlinkSync(path.join(tmpRoot, "opencode"), symlink); - try { - const tildePath = "~/" + path.basename(symlink); - const host = io(); - handleAmicodeBridgeMessage({ - source: "amicode", - kind: "dev-tools-update", - enabled: true, - opencodePath: tildePath, - amicodePath: fakeAmicodeRepo, - }, host); - await flush(); - const reply = host.posted.find((m: any) => m.kind === "dev-tools-status") as any; - expect(reply).toBeDefined(); - expect(reply.opencodeValid).toBe(true); - expect(reply.opencodeError).toBeUndefined(); - } finally { - fs.rmSync(symlink, { force: true }); - } - }); - it("expands ~ in amicodePath before validation", async () => { const home = os.homedir(); const symlink = path.join(home, `.devtools-test-am-${Date.now()}`); fs.symlinkSync(fakeAmicodeRepo, symlink); - const fakeOpencodeRepo = path.join(tmpRoot, "opencode"); try { const tildePath = "~/" + path.basename(symlink); const host = io(); @@ -820,7 +791,6 @@ describe("amicode bridge — dev-tools-update path validation", () => { source: "amicode", kind: "dev-tools-update", enabled: true, - opencodePath: fakeOpencodeRepo, amicodePath: tildePath, }, host); await flush(); @@ -833,34 +803,30 @@ describe("amicode bridge — dev-tools-update path validation", () => { } }); - it("reports error for invalid paths (not a false positive)", async () => { + it("reports error for an invalid amicode path (not a false positive)", async () => { const host = io(); handleAmicodeBridgeMessage({ source: "amicode", kind: "dev-tools-update", enabled: true, - opencodePath: "/definitely/not/a/real/repo", amicodePath: "/also/not/real", }, host); await flush(); const reply = host.posted.find((m: any) => m.kind === "dev-tools-status") as any; expect(reply).toBeDefined(); - expect(reply.opencodeValid).toBe(false); expect(reply.amicodeValid).toBe(false); }); // #941: committing a path must never build or reload on its own — only // the explicit "Rebuild Locally"/"Rebuild from Main" buttons (a separate - // dev-tools-rebuild message) may do that. Before this fix, blurring EITHER - // path field re-sent both current paths, and a valid non-empty amicodePath - // eagerly kicked off a real `bun run build` + an unconfirmed window reload. + // dev-tools-rebuild message) may do that. Before this fix, blurring the + // path field eagerly kicked off a real build + an unconfirmed window reload. it("committing a valid amicode path only validates — no build, no reload, no devAssetRoot write (#941)", async () => { const host = io(); handleAmicodeBridgeMessage({ source: "amicode", kind: "dev-tools-update", enabled: true, - opencodePath: path.join(tmpRoot, "opencode"), amicodePath: fakeAmicodeRepo, }, host); await flush(); @@ -881,46 +847,48 @@ describe("amicode bridge — dev-tools-update path validation", () => { source: "amicode", kind: "dev-tools-update", enabled: true, - opencodePath: "", amicodePath: "", }, host); await flush(); expect(ws.configUpdates).toContainEqual(["devAssetRoot", ""]); }); +}); + +// ============================================================================ +// dev-tools binary-override lifecycle (#1115). The binary live-swap the +// opencode-path field fed is retired, but the GENERAL opencodeBinary override +// (consumed by boot/health paths) must be preserved and still cleared on the +// developer-mode toggle-off. +// ============================================================================ + +describe("amicode bridge — dev-tools binary override lifecycle (#1115)", () => { + it("toggle-off clears the general opencodeBinary override (boot/health consumer preserved)", async () => { + const host = io(); + handleAmicodeBridgeMessage({ + source: "amicode", + kind: "dev-tools-update", + enabled: false, + }, host); + await flush(); + expect(ws.configUpdates).toContainEqual(["opencodeBinary", ""]); + }); - // #943: a real fork build never lands at the unsuffixed path the - // beforeEach fixture above uses — it always produces a platform-suffixed - // dist directory (dist/opencode--/bin/opencode). Before - // the shared findForkedOpencodeBinary() helper, dev-tools-update's - // candidate list never checked that layout, so a genuinely valid repo - // path was reported "Binary not found at this path" — identically to an - // actual typo. Correcting a bad path to a real one just re-ran the same - // broken check and got the same wrong answer, which looked like nothing - // had updated. - it("validates a real fork build's platform-suffixed layout (#943)", async () => { - const realRoot = fs.mkdtempSync(path.join(os.tmpdir(), "devtools-real-build-")); - const platformKey = `${process.platform}-${process.arch}`; - const binDir = path.join(realRoot, "packages", "opencode", "dist", `opencode-${platformKey}`, "bin"); - fs.mkdirSync(binDir, { recursive: true }); - const realBinary = path.join(binDir, "opencode"); - fs.writeFileSync(realBinary, "#!/bin/sh\n"); - fs.chmodSync(realBinary, 0o755); + it("enabling dev mode no longer live-swaps the binary — no opencodeBinary write on an enabled update", async () => { + const host = io(); + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "devtools-noswap-")); + const extDir = path.join(tmp, "packages", "extension"); + fs.mkdirSync(extDir, { recursive: true }); try { - const host = io(); handleAmicodeBridgeMessage({ source: "amicode", kind: "dev-tools-update", enabled: true, - opencodePath: realRoot, - amicodePath: fakeAmicodeRepo, + amicodePath: tmp, }, host); await flush(); - const reply = host.posted.find((m: any) => m.kind === "dev-tools-status") as any; - expect(reply).toBeDefined(); - expect(reply.opencodeValid).toBe(true); - expect(reply.opencodeError).toBeUndefined(); + expect(ws.configUpdates.some(([key]) => key === "opencodeBinary")).toBe(false); } finally { - fs.rmSync(realRoot, { recursive: true, force: true }); + fs.rmSync(tmp, { recursive: true, force: true }); } }); });