Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
59321d1
style(rail): at-rest chevron on the pending-but-clickable pulse chip
kateebonner Jul 28, 2026
43c1c51
style(capsule): the HP row names Harmoniqs Cloud
Rchari1 Jul 28, 2026
67bfdf7
feat(connections): Harmoniqs Cloud is connectable where Pasqal is
Rchari1 Jul 28, 2026
e642558
feat(amicode): context tree marks proprietary vault context locked
kateebonner Jul 28, 2026
2f7c9e1
refactor(rail): drop the Inspect Run button — the pulse chip is the o…
kateebonner Jul 28, 2026
bb29005
fix(run): fidelity readout never rounds a near-unity F to a bare 1
kateebonner Jul 28, 2026
54accef
Merge pull request #94 from harmoniqs/kate/rail-pulse-chip-affordance
kateebonner Jul 28, 2026
e0105cd
Merge pull request #96 from harmoniqs/kate/run-fidelity-precision
kateebonner Jul 28, 2026
d5f2d60
fix(session): header backdrop-blur off while the context-tree band is…
kateebonner Jul 28, 2026
94df1cd
Merge pull request #97 from harmoniqs/kate/session-header-blur-bleed
kateebonner Jul 28, 2026
9a8268d
Merge pull request #86 from harmoniqs/kate/context-tree-locked-afford…
kateebonner Jul 29, 2026
42ecbab
Merge pull request #74 from harmoniqs/fix/amicode-receipt-currency
jack-champagne Jul 29, 2026
95e915e
Merge pull request #92 from harmoniqs/rchari/harmoniqs-cloud-copy
jack-champagne Jul 29, 2026
27b8ed1
test(http-recorder): match cassettes prompt-agnostically for transpor…
jack-champagne Jul 29, 2026
9bd4649
feat(amicode): system card renders the recorded Hamiltonian, and stop…
aarontrowbridge Jul 29, 2026
ae4a31c
feat(amicode): approval state machine + ledger transport (S3, partial)
aarontrowbridge Jul 27, 2026
76157d1
feat(amicode): the in-chat approval card, end to end
aarontrowbridge Jul 28, 2026
9fbdf62
fix(amicode): a receipt is informative or inert, never a dead link
aarontrowbridge Jul 28, 2026
72b08af
feat(amicode): warrant status on the rail (S4)
aarontrowbridge Jul 28, 2026
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
43 changes: 36 additions & 7 deletions packages/app/src/components/amicode-defaults-capsule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,18 +335,22 @@ export function AmicodeDefaultsCapsule(props: { compute?: AmicodeComputeControl
}}
onClick={onHpClick}
aria-label={
// Name the cloud, not just "the cloud": users are picking a
// paid service here, and every refusal they can hit downstream
// (amico-run's local-launch refusal, the hpc gate) calls it
// Harmoniqs Cloud too — one name end to end.
dot() === "connected"
? "Piccolissimo + Altissimo solver — API key connected"
? "Piccolissimo + Altissimo solver — connected to Harmoniqs Cloud"
: dot() === "attention"
? "Piccolissimo + Altissimo solver — API key needs attention"
: "Piccolissimo + Altissimo solver — add your API key to enable"
? "Piccolissimo + Altissimo solver — Harmoniqs Cloud key needs attention"
: "Piccolissimo + Altissimo solver — add your Harmoniqs Cloud API key to enable"
}
title={
dot() === "connected"
? "API key connected"
? "Connected to Harmoniqs Cloud — every solve on this solver runs there"
: dot() === "attention"
? "API key needs attention — click to fix"
: "Runs in the cloud — click to add your API key"
? "Harmoniqs Cloud key needs attention — click to fix"
: "Runs in Harmoniqs Cloud — click to add your API key"
}
>
<span style={{ overflow: "hidden", "text-overflow": "ellipsis", "white-space": "nowrap" }}>
Expand All @@ -370,12 +374,37 @@ export function AmicodeDefaultsCapsule(props: { compute?: AmicodeComputeControl
>
PRO
</span>
{/* Connection state, ON THE ROW. Without this the row is silent:
with a key already on file a click just activates HP, which is
correct but indistinguishable from "nothing happened" (reported
2026-07-28 as "it did not ask me for an API key" — it had no
reason to ask, and no way to say so). Text, not a bare dot:
Kate's idiom is that the dot accompanies words rather than the
solver name, and words survive colour-blindness and a
screenshot. It also states what a click will DO. */}
<span
data-slot="amicode-solver-hp-state"
style={{
"font-size": "10px",
"font-weight": "600",
"letter-spacing": "0.3px",
"flex-shrink": "0",
color:
dot() === "connected"
? "var(--v2-text-text-success, var(--v2-text-text-muted))"
: dot() === "attention"
? "var(--v2-text-text-warning, var(--v2-text-text-muted))"
: "var(--v2-text-text-muted)",
}}
>
{dot() === "connected" ? "CONNECTED" : dot() === "attention" ? "CHECK KEY" : "ADD KEY"}
</span>
</button>
<Show when={props.compute}>
<button
type="button"
data-slot="amicode-solver-hp-details"
aria-label="Solver API key — connection details"
aria-label="Harmoniqs Cloud — connection details and API key"
aria-expanded={computeOpen()}
aria-controls="amicode-capsule-compute"
title="Connection details"
Expand Down
42 changes: 37 additions & 5 deletions packages/app/src/pages/session/context-tree-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
// the real file (project files in a session tab, vault files in the Vault
// panel). Hovering a tool row in the log still glances at its node here via
// the same amicode:brain-hover event the strip used.
import { createEffect, createMemo, createSignal, onCleanup, onMount, For, Show } from "solid-js"
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, For, Show } from "solid-js"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { useSync } from "@/context/sync"
import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useServer } from "@/context/server"
import { amicodeGet } from "@/utils/amicode-fetch"
import { amicoBrainRef } from "@opencode-ai/ui/brain-ref"
import {
createContextTreeEngine,
Expand Down Expand Up @@ -66,8 +68,33 @@ function ContextTreeFrame(props: { sessionID: string }) {
const sync = useSync()
const file = useFile()
const language = useLanguage()
const server = useServer()
const { tabs, view } = useSessionLayout()

// per-mount browsability from GET /amicode/vaults (`browsable`, stamped by
// the server's fail-closed law) — proprietary mounts mark their nodes
// locked upfront instead of dead-ending in a Vault-panel refusal on click
const [vaultsRaw] = createResource(
() => server.current,
(conn) => amicodeGet(conn, "/amicode/vaults").catch(() => undefined),
)
const browsableMounts = createMemo<Map<string, boolean | undefined> | undefined>(() => {
const raw = vaultsRaw() as { mounts?: { id?: string; browsable?: boolean }[] } | undefined
if (!raw || !Array.isArray(raw.mounts)) return undefined
return new Map(
raw.mounts.filter((m) => typeof m?.id === "string").map((m) => [m.id as string, m.browsable]),
)
})
const vaultLocked = (mount: string) => {
const map = browsableMounts()
// list unavailable → status quo (no lock claims we can't back);
// a mount the server doesn't list can't be browsed → locked;
// `browsable` absent (older server) → unknown, again no lock claim
if (!map) return false
if (!map.has(mount)) return true
return map.get(mount) === false
}

const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
const getParts = (msgId: string) => sync.data.part[msgId] ?? []
const busy = createMemo(() => (sync.data.session_status[props.sessionID]?.type ?? "idle") !== "idle")
Expand Down Expand Up @@ -134,7 +161,7 @@ function ContextTreeFrame(props: { sessionID: string }) {
setActive: (tab) => tabs().setActive(tab),
})
const onSelect = (node: ContextTreeSelection) => {
if (!node.path) return
if (!node.path || node.locked) return
const vaultRef = vaultRefFromPath(node.path)
if (vaultRef) {
vaultPanel.open({ mount: vaultRef.mount, path: vaultRef.rel })
Expand All @@ -161,7 +188,7 @@ function ContextTreeFrame(props: { sessionID: string }) {
window.addEventListener("amicode:brain-hover", onToolHover)
onCleanup(() => window.removeEventListener("amicode:brain-hover", onToolHover))

const tree = createMemo(() => buildContextTree(turns()))
const tree = createMemo(() => buildContextTree(turns(), { vaultLocked }))
createEffect(() => {
const brain = engine()
if (!brain) return
Expand All @@ -173,7 +200,8 @@ function ContextTreeFrame(props: { sessionID: string }) {
const flatNodes = createMemo(() => {
const out: ContextTreeSelection[] = []
const walk = (n: ContextTreeNodeInput) => {
if (n.kind !== "root") out.push({ id: n.id, label: n.label, kind: n.kind, path: n.path, vault: n.vault })
if (n.kind !== "root")
out.push({ id: n.id, label: n.label, kind: n.kind, path: n.path, vault: n.vault, locked: n.locked })
for (const c of n.children ?? []) walk(c)
}
walk(tree())
Expand All @@ -188,7 +216,11 @@ function ContextTreeFrame(props: { sessionID: string }) {
setKbIndex(next)
const node = list[next]
engine()?.focus(node.id)
setAnnounce(`${node.label} — ${node.kind}${node.path ? ", press Enter to open" : ""}`)
setAnnounce(
`${node.label} — ${node.kind}${
node.locked ? ", locked — this vault does not allow browsing" : node.path ? ", press Enter to open" : ""
}`,
)
}
const onCanvasKeyDown = (e: KeyboardEvent) => {
const list = flatNodes()
Expand Down
52 changes: 51 additions & 1 deletion packages/app/src/pages/session/message-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,32 @@ export function MessageTimeline(props: {
const raw = problemRaw.latest
return raw === undefined ? undefined : parseProblemResponse(raw)
})
// Capability warrants (spec-20260727-164748 §9.5). Fetched unconditionally rather
// than gated on a dialog: the approval card lives IN the transcript, so it needs
// its state on first paint. Cheap — one ledger read, and the card's state derives
// from these rows rather than from optimistic local state, so a press round-trips
// through the ledger before it reads as granted.
const [warrantsRaw, { refetch: refetchWarrants }] = createResource(() =>
amicodeGet(server.current, "/amicode/warrants").catch(() => undefined),
)
const warrants = createMemo(() => {
const raw = warrantsRaw.latest as { ok?: boolean; warrants?: unknown } | undefined
if (!raw?.ok || !Array.isArray(raw.warrants)) return []
// Tolerant per row: a malformed row is dropped, never defaulted into a warrant.
return raw.warrants.flatMap((r) => {
const row = r as Record<string, unknown>
if (typeof row.plan_hash !== "string" || typeof row.expires_at !== "string") return []
return [{
plan_hash: row.plan_hash,
bounds: (typeof row.bounds === "object" && row.bounds !== null ? row.bounds : {}) as Record<string, never>,
expires_at: row.expires_at,
issued_by: typeof row.issued_by === "string" ? row.issued_by : "unknown",
// Omitted when the server did not send it, so the rail chip drops the count
// rather than rendering a wrong "0 of N".
...(typeof row.solves_used === "number" ? { solves_used: row.solves_used } : {}),
}]
})
})
// Run verdict data for the entity view: /amicode/run-status is fetched
// whenever a dialog opens (cheap, server-cached ~1s); only the Run dialog
// reads it. Same endpoint the rail's run chip polls.
Expand Down Expand Up @@ -1409,7 +1435,15 @@ export function MessageTimeline(props: {
}}
data-session-title
classList={{
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)] backdrop-blur-[10px]": true,
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
// The frosted fade only while the header is slim. With the
// context-tree band open the header is ~300px tall and the band
// sits inside the pl-4 gutter — the blur samples transcript
// content through that transparent 16px strip and smears it into
// a grey streak along the straight left edge (worst on light).
// The band is opaque below the 48px title strip anyway, so the
// blur buys nothing there.
"backdrop-blur-[10px]": !contextTreeVisible(),
"w-full": true,
// the tall padding exists to seat the entity-chip rail; the
// context-tree block closes the header itself when it renders,
Expand Down Expand Up @@ -1746,6 +1780,22 @@ export function MessageTimeline(props: {
if (!id) return
void sdk.client.session.promptAsync({ sessionID: id, parts: [{ type: "text", text }] })
}}
// Warrant transport (spec-20260727-164748 §9.5). NOT routed through
// onAsk on purpose: an approval delivered as a chat message would be
// read by the agent, which would then write the ledger row, leaving
// the only provenance as "the agent says the user approved".
warrants={() => warrants() ?? []}
onApprove={(request) => {
void amicodePost(server.current, "/amicode/approve", {
plan_hash: request.plan_hash,
bounds: request.bounds,
})
// Refetch so the card flips to "granted" from the LEDGER rather
// than from optimistic local state — same discipline as deriving
// state from the durable log in the first place.
.then(() => void refetchWarrants())
.catch(() => void refetchWarrants())
}}
/>
{/* amicode: the context tree, folded into the header (ADR 0003) —
closes the sticky block beneath the title row + chip rail */}
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/pages/session/use-amicode-commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, expect, spyOn, test } from "bun:test"
import { inAmicode, postAmicode } from "./use-amicode-commands"

// The "Inspect Run" button (entity rail) and the Amico command palette both
// reach the VS Code extension through postAmicode(). chat_panel.ts relays the
// The entity rail's pulse chip (its one inspector entry) and the Amico command
// palette both reach the VS Code extension through postAmicode(). chat_panel.ts relays the
// envelope to the host and executes ONLY commands on its BRIDGE_ALLOWED_COMMANDS
// allowlist — so the exact envelope shape and command string are a contract.
describe("postAmicode bridge envelope", () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/pages/session/use-amicode-commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { useCommand, type CommandOption } from "@/context/command"
// Each command posts {source:"amicode",kind:"command",command} to window.parent;
// chat_panel.ts relays it to an ALLOWLISTED vscode command.

// Exported so non-palette surfaces (e.g. the "Inspect Run" button on the entity
// rail) can fire the same host-bridged commands. inAmicode() gates them out of
// Exported so non-palette surfaces (e.g. the pulse chip on the entity rail —
// the rail's one inspector entry) can fire the same host-bridged commands. inAmicode() gates them out of
// the public web/share build, where there is no extension host to relay to.
export const inAmicode = () => typeof window !== "undefined" && window.self !== window.top

Expand Down
1 change: 1 addition & 0 deletions packages/http-recorder/src/internal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { CassetteNotFoundError, hasCassetteSync, UnsafeCassetteError } from "./cassette.js"
export { cassetteLayer, recordingLayer, type RecordReplayMode, type RecordReplayOptions } from "./internal-effect.js"
export { defaultMatcher, promptAgnosticMatcher } from "./matching.js"
export { redactHeaders, redactUrl, secretFindings, type SecretFinding } from "./redaction.js"
export { socketLayer } from "./socket.js"
export {
Expand Down
59 changes: 59 additions & 0 deletions packages/http-recorder/src/matching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,65 @@ export const canonicalSnapshot = (snapshot: RequestSnapshot): string =>
export const defaultMatcher: RequestMatcher = (incoming, recorded) =>
canonicalSnapshot(incoming) === canonicalSnapshot(recorded)

/** Placeholder standing in for elided system-prompt prose. Substituting rather than
* deleting keeps "a system message is present, with content" part of the match. */
const SYSTEM_PROMPT_ELIDED = "<system prompt elided for matching>"

/** Top-level keys carrying the system prompt: OpenAI Responses uses `instructions`,
* Anthropic Messages uses `system`. */
const SYSTEM_PROMPT_KEYS = ["instructions", "system"] as const

/** Keys carrying a message list, where the prompt may instead arrive as a
* `role: "system"` entry (the shape the OpenCode proxy sends). */
const MESSAGE_LIST_KEYS = ["input", "messages"] as const

/** Neutralizes system-prompt prose wherever a provider puts it, leaving every other
* part of the request — model, tools, the conversation itself — matched exactly. */
const withoutSystemPrompt = (snapshot: RequestSnapshot): RequestSnapshot => {
const body = jsonBody(snapshot.body)
if (!isRecord(body)) return snapshot

const next: Record<string, unknown> = { ...body }
let changed = false

for (const key of SYSTEM_PROMPT_KEYS) {
if (!(key in next)) continue
delete next[key]
changed = true
}

for (const key of MESSAGE_LIST_KEYS) {
const list = next[key]
if (!Array.isArray(list)) continue
let listChanged = false
const elided = list.map((entry) => {
if (!isRecord(entry) || entry["role"] !== "system" || !("content" in entry)) return entry
listChanged = true
return { ...entry, content: SYSTEM_PROMPT_ELIDED }
})
if (!listChanged) continue
next[key] = elided
changed = true
}

return changed ? { ...snapshot, body: JSON.stringify(next) } : snapshot
}

/**
* Like {@link defaultMatcher}, but ignores the system prompt.
*
* A cassette records the exact request that produced its response, system prompt
* included — so any edit to a prompt invalidates every cassette that carries it,
* even when the behaviour under test has nothing to do with prompt text. Tests that
* assert on transport mechanics (tool loops, streaming, retries) should pin the
* mechanics and stay indifferent to wording.
*
* Use this when the prompt is incidental to what the test asserts. Do NOT use it for
* a test whose subject IS the prompt — there the exact text is the assertion.
*/
export const promptAgnosticMatcher: RequestMatcher = (incoming, recorded) =>
canonicalSnapshot(withoutSystemPrompt(incoming)) === canonicalSnapshot(withoutSystemPrompt(recorded))

export const safeText = (value: unknown) => {
if (value === undefined) return "undefined"
if (secretFindings(value).length > 0) return JSON.stringify(REDACTED)
Expand Down
Loading
Loading