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
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
22 changes: 15 additions & 7 deletions packages/ui/src/amicode/connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ describe("driftCopy (170 AC4)", () => {

describe("connectionTitle", () => {
test("known products get names; unknown ids render verbatim", () => {
expect(connectionTitle("company-compute")).toBe("Solver API key")
expect(connectionTitle("company-compute")).toBe("Harmoniqs Cloud")
expect(connectionTitle("pasqal-cloud")).toBe("Pasqal Cloud")
expect(connectionTitle("(unknown)")).toBe("(unknown)")
})
Expand Down Expand Up @@ -592,17 +592,25 @@ describe("applyConnectionOverlay", () => {
})
})

// ── amicode#200 AC5: Company Compute relocated to the solver toggle ────────
// ── Harmoniqs Cloud is connectable in the Connections tab (reverses #200 AC5)
import { statusTabConnections, COMPANY_COMPUTE_ID as COMPUTE_ID } from "./connections"

describe("status-tab connection list (#200 AC5)", () => {
describe("status-tab connection list", () => {
const mk = (id: string) => ({ id, state: "connected" as const, rawState: "connected", validatedAt: "—", stale: false })
test("excludes company-compute; everything else passes through in order", () => {

// The regression this pins: #200 filtered company-compute out of this list, so
// Pasqal Cloud was connectable here and OUR cloud was not — users looked where
// Pasqal is, found nothing, and had nowhere to enter an API key.
test("includes company-compute, in wire order alongside the others", () => {
const list = [mk(COMPUTE_ID), mk("pasqal-cloud"), mk("future-target")]
expect(statusTabConnections(list).map((c) => c.id)).toEqual(["pasqal-cloud", "future-target"])
expect(statusTabConnections(list).map((c) => c.id)).toEqual([COMPUTE_ID, "pasqal-cloud", "future-target"])
})

test("a compute-only list still renders a card (the empty state must not swallow it)", () => {
expect(statusTabConnections([mk(COMPUTE_ID)]).map((c) => c.id)).toEqual([COMPUTE_ID])
})
test("empty and compute-only lists yield empty", () => {

test("an empty list stays empty", () => {
expect(statusTabConnections([])).toEqual([])
expect(statusTabConnections([mk(COMPUTE_ID)])).toEqual([])
})
})
27 changes: 20 additions & 7 deletions packages/ui/src/amicode/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,33 @@ export type ConnectionActionView = { ok: boolean; connection?: ConnectionView; e

export const COMPANY_COMPUTE_ID = "company-compute"

/** amicode#200: Company Compute lives in the solver toggle now — the status
* popover's Connections tab shows execution targets (Pasqal, future hardware)
* only. The wire still carries every connection; this is a render filter. */
/** Every connection renders in the Connections tab, Harmoniqs Cloud included.
*
* This REVERSES amicode#200's render filter. That change moved Company Compute
* out of the tab, reasoning that it was one credential for one service rather
* than a separate product. The effect, though, was that Pasqal Cloud appeared
* as a connectable service and Harmoniqs Cloud — ours — did not: users went
* looking for it exactly where Pasqal is, found nothing, and had nowhere to
* enter an API key (2026-07-28). A cloud we sell has to be connectable in the
* place that lists clouds.
*
* The solver capsule keeps its own connect affordance; both routes write the
* same credential, so connecting in either place shows up in both. Kept as a
* function rather than dropping the call sites, so there is still one obvious
* place to filter if a genuinely internal connection ever appears. */
export function statusTabConnections(connections: ConnectionView[]): ConnectionView[] {
return connections.filter((c) => c.id !== COMPANY_COMPUTE_ID)
return connections
}
export const PASQAL_ID = "pasqal-cloud"

/** Product names are not translated; ids without one render verbatim. */
export function connectionTitle(id: string): string {
// amicode#200 (Kate): one credential, one service — present it as what it
// is (the API key that unlocks the cloud solvers), not a separate product.
// Named as the product, alongside Pasqal Cloud. #200 called this "Solver API
// key" to avoid implying a second product, but in a list whose other entry is
// "Pasqal Cloud" that reads as a settings field rather than our service — and
// it is the name every downstream refusal uses (amico-run, the hpc gate).
// The wire id stays "company-compute": server contract, not presentation.
if (id === COMPANY_COMPUTE_ID) return "Solver API key"
if (id === COMPANY_COMPUTE_ID) return "Harmoniqs Cloud"
if (id === PASQAL_ID) return "Pasqal Cloud"
return id
}
Expand Down
Loading