-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Developer Tools section in General Settings (amicode#377) #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3008687
1039f43
3be1447
e3ade60
af45bf2
08f3918
bfa2660
9d35fd1
bd0f0bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| import { createSignal, onCleanup, onMount } from "solid-js" | ||
| import { useSettings } from "@/context/settings" | ||
| import { inAmicode } from "@/utils/amicode-bridge" | ||
|
|
||
| export interface DevToolsStatus { | ||
| opencodeValid: boolean | ||
| opencodeError?: string | ||
| amicodeValid: boolean | ||
| amicodeError?: string | ||
| serverRestarted: boolean | ||
| reloadNeeded: boolean | ||
| building?: boolean | ||
| buildError?: string | ||
| } | ||
|
|
||
| export type RebuildState = "idle" | "rebuilding" | "rebuilt" | "failed" | ||
|
|
||
| /** Default repo paths autofilled when the toggle is turned ON with empty fields. */ | ||
| const DEFAULT_OPENCODE_PATH = "~/harmoniqs/opencode" | ||
| const DEFAULT_AMICODE_PATH = "~/harmoniqs/amicode" | ||
|
|
||
| export function createDeveloperToolsController() { | ||
| const settings = useSettings() | ||
| const [status, setStatus] = createSignal<DevToolsStatus | undefined>(undefined) | ||
| const [pending, setPending] = createSignal(false) | ||
| const [rebuildState, setRebuildState] = createSignal<RebuildState>("idle") | ||
| const [rebuildError, setRebuildError] = createSignal<string | undefined>(undefined) | ||
|
|
||
| // On mount, check if we just came back from a successful rebuild reload | ||
| onMount(() => { | ||
| try { | ||
| if (localStorage.getItem("amicode:devtools-rebuilt") === "1") { | ||
| localStorage.removeItem("amicode:devtools-rebuilt") | ||
| setRebuildState("rebuilt") | ||
| // Clear the "rebuilt" badge after 5 seconds | ||
| setTimeout(() => setRebuildState("idle"), 5000) | ||
| } | ||
| } catch { | ||
| // non-critical | ||
| } | ||
| }) | ||
|
|
||
| // Listen for the extension host's replies | ||
| const handleMessage = (event: MessageEvent) => { | ||
| const d = event.data | ||
| if (d && d.source === "amicode" && d.kind === "dev-tools-status") { | ||
| setStatus({ | ||
| opencodeValid: d.opencodeValid ?? true, | ||
| opencodeError: d.opencodeError, | ||
| amicodeValid: d.amicodeValid ?? true, | ||
| amicodeError: d.amicodeError, | ||
| serverRestarted: d.serverRestarted ?? false, | ||
| reloadNeeded: d.reloadNeeded ?? false, | ||
| building: d.building ?? false, | ||
| buildError: d.buildError, | ||
| }) | ||
| setPending(false) | ||
|
|
||
| // When a reload is needed (extension was rebuilt), set a flag so the app | ||
| // reopens settings at the developer tools section after the reload. | ||
| if (d.reloadNeeded) { | ||
| try { | ||
| localStorage.setItem("amicode:devtools-reopen", "1") | ||
| localStorage.setItem("amicode:devtools-rebuilt", "1") | ||
| } catch { | ||
| // localStorage unavailable — non-critical | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Rebuild status messages | ||
| if (d && d.source === "amicode" && d.kind === "dev-tools-rebuild-status") { | ||
| if (d.state === "rebuilding") { | ||
| setRebuildState("rebuilding") | ||
| setRebuildError(undefined) | ||
| } else if (d.state === "failed") { | ||
| setRebuildState("failed") | ||
| setRebuildError(d.error ?? "Unknown error") | ||
| } | ||
|
Comment on lines
+76
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use localized fallback text for a rebuild failure. If the host omits 🤖 Prompt for AI Agents |
||
| // "done" state triggers a reload — the "rebuilt" flag is read on next mount | ||
| } | ||
| } | ||
|
|
||
| if (typeof window !== "undefined") { | ||
| window.addEventListener("message", handleMessage) | ||
| onCleanup(() => window.removeEventListener("message", handleMessage)) | ||
| } | ||
|
|
||
| const sendUpdate = () => { | ||
| if (!inAmicode()) return | ||
| setPending(true) | ||
| setStatus(undefined) | ||
| window.parent.postMessage( | ||
| { | ||
| source: "amicode", | ||
| kind: "dev-tools-update", | ||
| enabled: settings.developer.enabled(), | ||
| opencodePath: settings.developer.opencodePath(), | ||
| amicodePath: settings.developer.amicodePath(), | ||
| }, | ||
| "*", | ||
|
Comment on lines
+44
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Trace the bridge implementation and framing policy.
rg -n -C 3 --glob '*.{ts,tsx,html,json}' \
'dev-tools-(update|status)|inAmicode|postMessage\(|frame-ancestors|Content-Security-Policy' \
packagesRepository: harmoniqs/opencode Length of output: 38506 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- developer tools controller ---'
cat -n packages/app/src/components/settings-v2/developer-tools-controller.ts
printf '%s\n' '--- all dev-tools bridge handlers ---'
rg -n -C 6 --glob '*.{ts,tsx,js,jsx}' \
'dev-tools-status|dev-tools-update|opencodePath|amicodePath|opencode_path|amicode_path' .
printf '%s\n' '--- framing and bridge definitions ---'
cat -n packages/app/src/utils/amicode-bridge.ts
rg -n -C 5 --glob '*.{ts,tsx,js,jsx,html,json}' \
'sandbox=|allow-same-origin|frame-ancestors|Content-Security-Policy|widget-frame|iframe' \
packages/app packages/opencode packages/uiRepository: harmoniqs/opencode Length of output: 50375 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- dev-tools occurrences outside generated or test paths ---'
rg -n --glob '*.{ts,tsx,js,jsx}' \
'dev-tools-status|dev-tools-update' . || true
printf '%s\n' '--- iframe creation and sandbox attributes ---'
rg -n -C 4 --glob '*.{ts,tsx,html}' \
'sandbox|<iframe|createElement\(["'\'']iframe|frame-ancestors' \
packages/app/src packages/ui/src packages/opencode/src
printf '%s\n' '--- application CSP implementation ---'
rg -n -C 8 --glob '*.{ts,tsx}' \
'function csp|const csp|frame-ancestors|Content-Security-Policy|X-Frame-Options' \
packages/opencode/src packages/app/src packages/ui/srcRepository: harmoniqs/opencode Length of output: 50374 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- explicit anti-framing policy ---'
rg -n --hidden --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' \
'frame-ancestors|X-Frame-Options' . || true
printf '%s\n' '--- exact application frame code ---'
sed -n '330,360p' packages/app/src/components/split-frame.tsx
sed -n '136,152p' packages/ui/src/amicode/widget-frame.tsx
printf '%s\n' '--- all app response security headers ---'
rg -n -C 4 --glob '*.{ts,tsx}' \
'headers\.set\(|HttpServerResponse|csp\(' packages/opencode/src/server packages/app/src \
| rg 'Content-Security|X-Frame|frame-ancestors|headers\.set|csp\(' | head -120Repository: harmoniqs/opencode Length of output: 2745 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("packages/app/src/components/settings-v2/developer-tools-controller.ts")
source = path.read_text()
handler = source[source.index("const handleMessage"):source.index(" if (typeof window !==", source.index("const handleMessage"))]
sender = source[source.index("const sendUpdate"):source.index(" return {", source.index("const sendUpdate"))]
print("checks_origin:", "event.origin" in handler)
print("checks_source:", "event.source" in handler)
print("checks_payload_types:", "typeof" in handler or "Array.isArray" in handler)
print("uses_wildcard_target:", '"*"' in sender)
print("sends_opencode_path:", "opencodePath:" in sender)
print("sends_amicode_path:", "amicodePath:" in sender)
PY
node - <<'JS'
const accepted = (event) => {
const d = event.data
return Boolean(d && d.source === "amicode" && d.kind === "dev-tools-status")
}
const hostileEvent = {
origin: "https://attacker.example",
source: { name: "attacker" },
data: {
source: "amicode",
kind: "dev-tools-status",
opencodeValid: false,
amicodeValid: false,
},
}
console.log("hostile_status_accepted:", accepted(hostileEvent))
JSRepository: harmoniqs/opencode Length of output: 334 Authenticate the host bridge.
🤖 Prompt for AI Agents |
||
| ) | ||
| } | ||
|
|
||
| const rebuild = (mode: "local" | "remote") => { | ||
| if (!inAmicode()) return | ||
| if (rebuildState() === "rebuilding") return // prevent double-clicks | ||
| setRebuildState("rebuilding") | ||
| setRebuildError(undefined) | ||
| try { | ||
| localStorage.setItem("amicode:devtools-reopen", "1") | ||
| localStorage.setItem("amicode:devtools-rebuilt", "1") | ||
| } catch { | ||
| // non-critical | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| window.parent.postMessage( | ||
| { | ||
| source: "amicode", | ||
| kind: "dev-tools-rebuild", | ||
| mode, | ||
| opencodePath: settings.developer.opencodePath(), | ||
| amicodePath: settings.developer.amicodePath(), | ||
| }, | ||
| "*", | ||
| ) | ||
| } | ||
|
|
||
| return { | ||
| enabled: settings.developer.enabled, | ||
| setEnabled: (value: boolean) => { | ||
| // Autofill paths with defaults when toggling ON with empty fields | ||
| if (value) { | ||
| if (!settings.developer.opencodePath()) { | ||
| settings.developer.setOpencodePath(DEFAULT_OPENCODE_PATH) | ||
| } | ||
| if (!settings.developer.amicodePath()) { | ||
| 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. | ||
| 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. | ||
| 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) | ||
| window.parent.postMessage( | ||
| { | ||
| 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 */ | ||
| commitOpencodePath: () => sendUpdate(), | ||
| commitAmicodePath: () => sendUpdate(), | ||
| /** Trigger a full rebuild (local = from disk, remote = git pull first) */ | ||
| rebuild, | ||
| status, | ||
| pending, | ||
| rebuildState, | ||
| rebuildError, | ||
| } | ||
| } | ||
|
|
||
| export type DeveloperToolsController = ReturnType<typeof createDeveloperToolsController> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the ownership in the deprecation comment.
DevToolsReopenBridgeis a no-op, but the comment says the reopen logic moved intouseSettingsCommand. The current reopen flow is implemented inpackages/app/src/components/titlebar.tsx. Update the comment so future changes follow the active implementation.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents