Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -161,20 +161,14 @@ export function createDeveloperToolsController() {
settings.developer.setAmicodePath(DEFAULT_AMICODE_PATH)
}
}
// Don't persist enabled=false — the marketplace build doesn't render
// Developer Tools at all, and persisting false prevents the dev build
// from showing it after a bash-script bootstrap without toggle interaction.
settings.developer.setEnabled(value)
if (value) {
settings.developer.setEnabled(value)
// Toggle ON: trigger a full rebuild (shows "Rebuilding..." status)
rebuild("local")
} else {
// Toggle OFF: show switching status, then the extension restores + reloads.
// Don't persist false — the reload brings up the marketplace build which
// doesn't have this section anyway.
// Toggle OFF: restore marketplace build and reload.
setRebuildState("rebuilding")
setRebuildError(undefined)
// Send enabled=false explicitly (can't rely on the signal since we didn't persist it)
if (!inAmicode()) return
setPending(true)
setStatus(undefined)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ const defaultSettings: Settings = {
configDir: "",
},
developer: {
enabled: true,
enabled: false,
opencodePath: "",
amicodePath: "",
},
Expand Down
1 change: 1 addition & 0 deletions packages/extension/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dev/brand_harness/main.js
dev/pulseplot_harness/main.js
scripts/benchmark/out/
scripts/benchmark/traces/
dist.marketplace-backup/
44 changes: 25 additions & 19 deletions packages/extension/src/chat_bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,34 +258,40 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean
};

if (!enabled) {
// Toggle OFF: clear overrides, restore marketplace extension, and reload.
// Toggle OFF: clear overrides, reinstall the marketplace extension, and reload.
void vscode.workspace.getConfiguration("amicode").update("opencodeBinary", "", vscode.ConfigurationTarget.Global);
void vscode.workspace.getConfiguration("amicode").update("devAssetRoot", "", vscode.ConfigurationTarget.Global);

// Restore the marketplace extension dist if a backup exists
// Guard: write a temporary marker so onboarding won't re-trigger after
// the reinstall. The marker is consumed (deleted) on next activation.
// A manual uninstall by the user does NOT write this marker, so
// onboarding correctly re-triggers for genuine fresh installs.
try {
const { writeDevtoolsRestoreMarker } = require("./substrate/vault_store") as typeof import("./substrate/vault_store");
writeDevtoolsRestoreMarker();
} catch { /* non-critical — worst case onboarding re-shows */ }

// Reinstall from the marketplace to restore the user's current release.
// The old backup approach was fragile (went stale on extension updates).
// Uninstall+install is the only reliable way to restore a clean dist —
// `--force` alone says "already installed" for the same version.
const installedExt = vscode.extensions.getExtension("harmoniqs.amicode");
if (installedExt) {
const backupDist = path.join(installedExt.extensionPath, "dist.marketplace-backup");
const installedDist = path.join(installedExt.extensionPath, "dist");
if (fs.existsSync(backupDist)) {
try {
const backupFiles = fs.readdirSync(backupDist).filter(f => f.endsWith(".js") || f.endsWith(".js.map"));
for (const f of backupFiles) {
fs.copyFileSync(path.join(backupDist, f), path.join(installedDist, f));
}
console.log("[amicode/bridge] restored marketplace dist from backup");
} catch (restoreErr) {
console.warn("[amicode/bridge] marketplace dist restore failed:", restoreErr);
const { exec } = require("child_process") as typeof import("child_process");
const extId = "harmoniqs.amicode";
exec(`code --uninstall-extension ${extId} && code --install-extension ${extId}`, { timeout: 60_000 }, (err) => {
if (err) {
console.warn("[amicode/bridge] marketplace reinstall failed:", err.message);
} else {
console.log("[amicode/bridge] reinstalled marketplace extension");
}
}
void vscode.commands.executeCommand("workbench.action.reloadWindow");
});
} else {
void vscode.commands.executeCommand("workbench.action.reloadWindow");
}

// Don't restart server separately — reloading the window does it.
// Don't send reloadNeeded — the auto-reload handles it silently.
io.postToWebview(reply);
setTimeout(() => {
void vscode.commands.executeCommand("workbench.action.reloadWindow");
}, 300);
return true;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/extension/src/opencode_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
readMemoryIndexLines,
hasOnboardingCompleted,
onboardingDir,
consumeDevtoolsRestoreMarker,
} from "./substrate/vault_store";
import { resolveMountStack, personalMount, type Mount, type MountStack } from "./substrate/mount_store";
import {
Expand Down Expand Up @@ -614,6 +615,7 @@ export function prepareOpencodeProject(opts: OpencodeConfigOptions): OpencodePro
vaultDir !== "" &&
readProfileMd(vaultDir) === "" &&
!hasOnboardingCompleted(onboardingDir()) &&
!consumeDevtoolsRestoreMarker() &&
!profileHasIdentity(); // the welcome WIZARD already collected identity — don't re-interview
if (shouldOnboard && overture && score0) {
// Chained: ONE compiled section, ONE manifest (id `overture`, stages =
Expand Down
25 changes: 25 additions & 0 deletions packages/extension/src/substrate/vault_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,28 @@ export function hasOnboardingCompleted(onboardingStreamDir: string): boolean {
}
return false;
}

// ─── Devtools restore marker ─────────────────────────────────────────────────
// A temporary file that tells the next activation "this reinstall was triggered
// by the devtools toggle, not a manual user action — skip onboarding." The
// marker is consumed (deleted) on read so it only suppresses once.

const DEVTOOLS_RESTORE_MARKER = ".devtools-restore";

/** Write the devtools restore marker. Called by toggle-OFF before uninstall. */
export function writeDevtoolsRestoreMarker(opsDir: string = amicodeOpsDir()): void {
fs.mkdirSync(opsDir, { recursive: true });
fs.writeFileSync(path.join(opsDir, DEVTOOLS_RESTORE_MARKER), String(Date.now()));
}

/** Check and consume the devtools restore marker. Returns true if it existed
* (meaning this activation follows a toggle-OFF reinstall, not a fresh install). */
export function consumeDevtoolsRestoreMarker(opsDir: string = amicodeOpsDir()): boolean {
const markerPath = path.join(opsDir, DEVTOOLS_RESTORE_MARKER);
try {
fs.unlinkSync(markerPath);
return true;
} catch {
return false;
}
}
39 changes: 39 additions & 0 deletions packages/extension/test/onboarding_routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ import {
writeWelcomeShown,
} from "../src/onboarding_routing";

import {
hasOnboardingCompleted,
writeDevtoolsRestoreMarker,
consumeDevtoolsRestoreMarker,
} from "../src/substrate/vault_store";

// ─── AC10: Routing predicate (pure function, table-driven) ───────────────────

describe("resolveOnboardingAction — routing predicate (AC10)", () => {
Expand Down Expand Up @@ -237,3 +243,36 @@ describe("welcome_shown flag semantics (AC6)", () => {
expect(readWelcomeShown(file)).toBe(true);
});
});

// ─── devtools restore marker — toggle-OFF guard ─────────────────────────────

describe("devtools restore marker — toggle-OFF guard", () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devtools-marker-"));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it("consumeDevtoolsRestoreMarker returns false when no marker exists", () => {
expect(consumeDevtoolsRestoreMarker(tmpDir)).toBe(false);
});

it("writeDevtoolsRestoreMarker + consumeDevtoolsRestoreMarker returns true and deletes the marker", () => {
writeDevtoolsRestoreMarker(tmpDir);
expect(consumeDevtoolsRestoreMarker(tmpDir)).toBe(true);
// Second consume returns false (marker was deleted)
expect(consumeDevtoolsRestoreMarker(tmpDir)).toBe(false);
});

it("does not interfere with onboarding completion state", () => {
// Marker exists but onboarding events.jsonl does not
writeDevtoolsRestoreMarker(tmpDir);
expect(hasOnboardingCompleted(tmpDir)).toBe(false);
// Consuming the marker doesn't create onboarding_completed
consumeDevtoolsRestoreMarker(tmpDir);
expect(hasOnboardingCompleted(tmpDir)).toBe(false);
});
});
Loading