Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cae2101
refactor(profile): institution name/logo lookup extracted to institut…
Rchari1 Jul 8, 2026
ed80e7f
feat(onboarding): first-run welcome wizard — 3 steps (brand welcome →…
Rchari1 Jul 8, 2026
4db62ee
feat(library): papers that make Amico smarter — Library card replaces…
Rchari1 Jul 8, 2026
5a14614
amicode(home): fix silent dead-end on 'Open chat' with no tracked pro…
kateebonner Jul 8, 2026
99a5918
Merge pull request #8 from harmoniqs/kate/home-open-chat-empty-projects
kateebonner Jul 8, 2026
5b4bc4d
feat(onboarding): library step in the wizard — 'Teach Amico your work…
Rchari1 Jul 9, 2026
78ac315
merge local/amicode: Kate's #8 open-chat-on-fresh-profile fix (the de…
Rchari1 Jul 9, 2026
73d45a1
fix(onboarding): wizard finale lands on the HOME page (primary), chat…
Rchari1 Jul 9, 2026
012c079
Merge pull request #6 from harmoniqs/rchari/onboarding-wizard
Rchari1 Jul 9, 2026
5e27881
Consolidate the fork brand mark to one geometry, synced to amicode PR…
kateebonner Jul 9, 2026
9d8ba5d
Use the detailed mark in the chat landing and onboarding wizard hero
kateebonner Jul 9, 2026
7c72d72
Merge pull request #7 from harmoniqs/kate/mark-drift-fix
kateebonner Jul 9, 2026
ddfd469
feat(amicode): add "Inspect Run" button to the entity rail
kateebonner Jul 9, 2026
ac8246e
Fix chat scroll jitter during streaming; respect reduced motion
kateebonner Jul 9, 2026
4b19222
fix(amicode): remove the yellow accent rail from card surfaces
kateebonner Jul 9, 2026
e56492e
test(amicode): cover the Inspect Run bridge envelope
kateebonner Jul 9, 2026
d9cfcca
feat(amicode): redesign the receipt card + entity view
kateebonner Jul 9, 2026
8dd96e7
Merge pull request #10 from harmoniqs/kate/inspect-run-button
kateebonner Jul 9, 2026
8129912
Merge pull request #11 from harmoniqs/kate/chat-jitter-motion
kateebonner Jul 9, 2026
9fe0ea7
Merge pull request #12 from harmoniqs/kate/no-accent-rail
kateebonner Jul 9, 2026
e99e80d
feat(solver): show-only High-Performance Solver toggle — [Piccolo | ⚡…
Rchari1 Jul 9, 2026
e24ab1a
fix(brand): chat-window AMICODE wordmark back to the brand typeface —…
Rchari1 Jul 9, 2026
2575ed8
merge local/amicode into rchari/solver-toggle — resolve session-new-v…
Rchari1 Jul 9, 2026
dedb6fd
Merge pull request #13 from harmoniqs/rchari/solver-toggle
Rchari1 Jul 9, 2026
ee918de
feat(solver): wire the toggle to a real switch — file-backed GET/POST…
Rchari1 Jul 9, 2026
c7cb572
fix(solver): wizard never shows the internal 'issimo' codename — stag…
Rchari1 Jul 9, 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
33 changes: 33 additions & 0 deletions AMICODE-PATCHES.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,36 @@ Rebuilt with the exact T3 recipe (`OPENCODE_VERSION=1.17.3 bun run script/build.
- index.css: @font-face for both (JuliaMono full glyph set — Julia Unicode; Racing Sans One latin subset, font-display swap). logo.tsx + wordmark-v2.tsx: font-family 'Racing Sans One' first, weight 700→400. settings.tsx: monoDefault/monoFallback lead with JuliaMono. theme.css: --font-family-mono leads with JuliaMono. Terminal font DELIBERATELY unchanged (JetBrainsMono Nerd Font Mono via separate terminalFallback).
- New assets (git-added — build breaks without them): public/assets/RacingSansOne-Regular.woff2 (21 KB) + JuliaMono-Regular.woff2 (946 KB).
- Font build sha256: `2a15da111be516516fb1fbd1a4fb5ae02ad9bddd6373ede08cdf7b28c19d163a` (dist/opencode-local + vendored path, write-temp + mv -f swap; SUPERSEDES #13's a73d8583… — same code, fonts now committed). Verify (scratch port 14099): `GET /assets/RacingSansOne-Regular.woff2` → 200 font/woff2 21804 B; `GET /assets/JuliaMono-Regular.woff2` → 200 font/woff2 946516 B; "Racing Sans One" in built css + index/new-session chunks, "JuliaMono" in `index-Dwtxigfs.css`; `GET /amicode/problems` → 200; `GET /` → 200 `<title>Amicode</title>`; ui `bun test src` → 70 pass; typecheck green ui+app (no snapshots assert fonts, per Aaron — confirmed nothing went red). Bonus confirmation: KaTeX\_\* woff2 assets now in dist — the entity view's katex import (#13) pulls its font set into the embed.
9. (home CTA fallback) — amicode(home): "Open chat" works on a fresh profile.
- BUG: `startWithPrompt` (fork wiring for the Meet-Amico card, patch 5ef6b7e0e) dead-ended
silently when the persisted client-side project list was empty (fresh browser profile
against a bare `opencode serve`): the `!project` branch called `openNewSession()`, which
needs the SAME empty `newSessionProject()` and hits `if (!conn || !project) return`.
Primary home CTA did nothing, no error. Hit live 2026-07-08 (web UI on a scratch dir).
- FIX (packages/app/src/pages/home.tsx, `startWithPrompt` only): when no project is
tracked, fall back to the focused server's own working directory —
`focusedSync().data.path.directory` (synced from GET /path; "" until loaded, so the
falsy guard holds) — open+touch it as a project (self-heals the home page), then
`tabs.newDraft` with the prompt preserved. Deliberately NOT `sync.data.project`:
the server's "global" project record has worktree "/".
- Regression spec: packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts —
fresh profile (NO localStorage seed), mocked server, click the CTA (`exact: true` —
the whole card is also a button whose accessible name contains "Open chat"), expect
navigation to `/new-session?draftId=` + the cwd persisted as a tracked project.
Verified failing on the unfixed code, passing with the fix. Playwright note: config
reuses any server on port 3000 (`reuseExistingServer`) — run with `PLAYWRIGHT_PORT=<free>`
if something else (e.g. the harmoniqs website dev server) holds 3000.
- Checks: `tsgo -b` clean; `bun run test:unit` 376 pass / 0 fail.

15. (mark drift fix, synced to amicode PR #99 final) — amicode: consolidated the fork's brand mark to ONE geometry, matching amicode's redesigned mark. The "kept in sync manually" cross-repo promise from patch #8 had already silently failed.

- Trigger: amicode's mark was redesigned (PR #99) without a corresponding update here — the fork still rendered the OLD "digi" pixel-accented H-robot everywhere. PR #99 went through several iterations before landing on its final geometry; this entry tracks that FINAL state (square viewBox `0 0 3600 3600`), not the intermediate "hackathon mark" (viewBox `116 287 3377 3035`) an earlier draft of this fork PR had copied — that intermediate geometry is now itself stale and was replaced here.
- Two copies of near-identical geometry lived in THIS repo (logo.tsx's `Robot` used by `Mark`/`Splash`, and spinner.tsx's `AmicoSpinner`), plus a third in favicon/amico.svg. Consolidated to a single `MARK_PATH` exported from logo.tsx.
- Geometry now mirrors amicode PR #99's two authored SVGs (amicode:`packages/extension/media/amico{,_reduced}.svg`), both square `0 0 3600 3600`:
- `MARK_PATH` = amico_reduced.svg's outer-bracket path (fill-rule evenodd screen knockout). Used by `Mark`, `Splash`, `AmicoSpinner`, and mirrored as a literal in favicon/amico.svg — every SMALL context, matching amicode's own "small → reduced" rule.
- `MarkDetailed` = amico.svg's full detailed mark (bracket path + internal circuit-pattern rects/polygons). Used ONLY by the Meet Amico home card (`w-12`/48px), large enough for the detail to resolve.
- `Mark`/`Splash`/`MarkDetailed` viewBox `0 0 64 56`/`116 287 3377 3035` → `0 0 3600 3600`; logo.css aspect-ratio `8/7` → `1/1` (the mark is square now).
- Still theme-adaptive via currentColor + var(--icon-strong-base) — this is a live webview DOM, so currentColor resolves (unlike amicode's native VS Code tab icon, which needs committed {light,dark} files; see amicode PR #99).
- NOT a re-established cross-repo sync promise — `MARK_PATH` is the single source of truth WITHIN this repo; it happens to match amicode's current geometry, kept aligned by hand when the mark changes.
- Tests: ui `bun test src/amicode` green; typecheck green ui + app.
- NOT done this round: full native `bun run script/build.ts` compile + vendored-binary swap — this patch only touches the embedded web UI. Deferred to the next amicode.N release tag, same split as patches #8/#11.
33 changes: 33 additions & 0 deletions packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"

// Regression: on a fresh profile (no tracked projects in localStorage) the
// home "Open chat" CTA dead-ended silently — startWithPrompt fell through to
// openNewSession(), which needs the same newSessionProject() that just came
// back empty. It must instead fall back to the server's own working
// directory (GET /path → .directory) and start a draft there, tracking the
// directory as a project so the rest of the home page works from then on.
test("home 'Open chat' falls back to the server cwd on a fresh profile", async ({ page }) => {
await mockOpenCodeServer(page, {
sessions: [],
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
})

// Deliberately NO localStorage seed — an empty tracked-project list is the
// regression condition (contrast: session-list-path-loading.spec.ts seeds it).
await page.goto("/")
// exact: true — the whole Meet-Amico card is also a button whose accessible
// name contains "Open chat"; we want the CTA inside it.
await page.getByRole("button", { name: "Open chat", exact: true }).click()

// Navigates to a new-session draft instead of doing nothing.
await expect(page).toHaveURL(/\/new-session\?draftId=/)

// And the server cwd is now a tracked project (the self-healing part).
const persisted = await page.evaluate(() => localStorage.getItem("opencode.global.dat:server") ?? "")
expect(persisted).toContain(fixture.directory)
})
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Show, type JSX } from "solid-js"
import { Logo, Mark } from "@opencode-ai/ui/logo"
import { Logo, MarkDetailed } from "@opencode-ai/ui/logo"
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"

// amicode: new-session start screen, top→bottom — H-bot mark (hero), the AMICODE
// wordmark, tagline + how-it-works + starter chips, then the composer. Sizing
// mirrors the classic NewSessionView (Mark w-36 / Logo w-72); keeps the session
// tabs and centered composer. The chips are the one-tap path to the next task.
// The hero uses the DETAILED mark (144px, well above where the accents resolve).
export function NewSessionDesignView(props: { children: JSX.Element; gettingStarted?: JSX.Element }) {
return (
<div data-component="session-new-design" class="relative size-full overflow-y-auto bg-v2-background-bg-deep">
<div class="absolute inset-x-0 top-[12%] flex justify-center px-6 pb-24">
<div class={`${NEW_SESSION_CONTENT_WIDTH} flex flex-col items-center`}>
<Mark class="w-36 h-auto mb-4" />
<MarkDetailed class="w-36 h-auto mb-4" />
<Logo class="w-72 max-w-full h-auto mb-6" />
<Show when={props.gettingStarted}>
<div class="w-full flex justify-center mb-6">{props.gettingStarted}</div>
Expand Down
19 changes: 5 additions & 14 deletions packages/app/src/components/session/session-new-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { amicodeGet } from "@/utils/amicode-fetch"
import { parseProblemsResponse } from "@opencode-ai/ui/amicode-problem-switcher"
import { Icon } from "@opencode-ai/ui/icon"
import { AmicodeGettingStarted } from "@opencode-ai/ui/amicode-getting-started"
import { Mark } from "@opencode-ai/ui/logo"
import { Logo, MarkDetailed } from "@opencode-ai/ui/logo"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"

const MAIN_WORKTREE = "main"
Expand Down Expand Up @@ -78,19 +78,10 @@ export function NewSessionView(props: NewSessionViewProps) {
<div class="flex-1 px-6 pb-30 flex items-center justify-center text-center">
<div class="w-full max-w-200 flex flex-col items-center text-center gap-4">
<div class="flex flex-col items-center gap-6">
<Mark class="w-28" />
{/* amicode: straight wordmark (the Logo's Racing Sans One face reads as
italic); byline keeps the brand attribution. */}
<div
style={{
"font-size": "34px",
"font-weight": "700",
"letter-spacing": "0.1em",
color: "var(--v2-text-text-base)",
}}
>
AMICODE
</div>
<MarkDetailed class="w-28" />
{/* amicode: brand wordmark in the brand typeface (Racing Sans One — restored per review);
MarkDetailed is the redesign's detailed mark (kept from trunk) */}
<Logo class="w-56 max-w-full" />
</div>
{/* amicode: getting-started block (tagline + how-it-works + starter chips) */}
<AmicodeGettingStarted
Expand Down
147 changes: 134 additions & 13 deletions packages/app/src/pages/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ import { ServerHealthIndicator } from "@/components/server/server-row"
import { type ServerHealth } from "@/utils/server-health"
import { amicodeGet, amicodePost } from "@/utils/amicode-fetch"
import { AmicodeRunGallery } from "@opencode-ai/ui/amicode-run-gallery"
import { AmicodeOnboardingWizard, shouldShowWizard } from "@opencode-ai/ui/amicode-onboarding-wizard"
import { AmicodeSolverToggle, type SolverMode } from "@opencode-ai/ui/amicode-solver-toggle"
import { AmicodeSolverSwitchWizard } from "@opencode-ai/ui/amicode-solver-switch-wizard"
import { parseRunCardsResponse } from "@opencode-ai/ui/amicode-run-card"
import { AmicodeHomeCards, parseProfileResponse, type HomeLiveRun } from "@opencode-ai/ui/amicode-home-cards"
import { parseRunSeriesResponse } from "@opencode-ai/ui/amicode-run-window"
import { AMICODE_STARTERS } from "@opencode-ai/ui/amicode-getting-started"
import { parseProblemsResponse } from "@opencode-ai/ui/amicode-problem-switcher"
import { parseProblemResponse } from "@opencode-ai/ui/amicode-entity-view"
import { Mark } from "@opencode-ai/ui/logo"
Expand Down Expand Up @@ -339,13 +341,103 @@ function HomeDesign() {
})

const [sessionsExpanded, setSessionsExpanded] = createSignal(false)
// Shared by the About-You card and the onboarding wizard: identity fields
// ride query params on the raw POST route; refetch renders the saved state.
async function saveProfileFields(fields: Record<string, string | undefined>) {
const q = new URLSearchParams()
for (const [k, v] of Object.entries(fields)) if (v !== undefined) q.set(k, v)
await amicodePost(focusedServer(), `/amicode/profile?${q.toString()}`)
await refetchProfile()
}

// Onboarding wizard (session zero): decided ONCE when the profile first
// resolves — the mid-wizard profile refetch must not unmount the preview
// step, and a dismiss is remembered per install (localStorage).
// Library (papers that make Amico smarter): count + latest for the card.
const [libraryRaw, { refetch: refetchLibrary }] = createResource(
() => state.selection.server,
() => amicodeGet(focusedServer(), "/amicode/library").catch(() => undefined),
)
const libraryView = createMemo(() => {
const raw = libraryRaw() as { ok?: boolean; papers?: { name?: string; path?: string }[] } | undefined
if (!raw || raw.ok !== true || !Array.isArray(raw.papers)) return undefined
return {
count: raw.papers.length,
latestName: typeof raw.papers[0]?.name === "string" ? raw.papers[0].name : undefined,
latestPath: typeof raw.papers[0]?.path === "string" ? raw.papers[0].path : undefined,
}
})
async function uploadPaper(filename: string, dataB64: string) {
const res = await amicodePost(focusedServer(), "/amicode/library", { filename, data_b64: dataB64 })
if ((res as { ok?: boolean } | undefined)?.ok !== true) throw new Error("library save rejected")
await refetchLibrary()
}

// Solver mode (rchari/solver-wire): server-truth via /amicode/solver-mode;
// selecting a mode POSTs, then the switch wizard polls THROUGH the server
// restart (failures expected mid-switch) until the extension reports ready.
const [solverRaw, { refetch: refetchSolverMode }] = createResource(
() => state.selection.server,
() => amicodeGet(focusedServer(), "/amicode/solver-mode").catch(() => undefined),
)
const solverState = createMemo(() => {
const raw = solverRaw() as { ok?: boolean; mode?: string; status?: string } | undefined
if (!raw || raw.ok !== true) return undefined
return { mode: (raw.mode === "hp" ? "hp" : "piccolo") as SolverMode, switching: raw.status === "switching" }
})
const [switchTarget, setSwitchTarget] = createSignal<SolverMode | undefined>(undefined)
const selectSolver = (mode: SolverMode) => {
setSwitchTarget(mode)
void amicodePost(focusedServer(), `/amicode/solver-mode?mode=${mode}`).catch(() => setSwitchTarget(undefined))
}
const pollSolver = async () => {
const raw = (await amicodeGet(focusedServer(), "/amicode/solver-mode")) as { mode?: string; status?: string }
return { mode: String(raw?.mode ?? ""), status: String(raw?.status ?? "") }
}

const WIZARD_DISMISS_KEY = "amicode-onboarding-dismissed"
const [wizardOpen, setWizardOpen] = createSignal(false)
let wizardDecided = false
createEffect(() => {
const view = profileView()
if (wizardDecided || view === undefined || !view.ok) return
wizardDecided = true
let dismissed = false
try {
dismissed = localStorage.getItem(WIZARD_DISMISS_KEY) === "1"
} catch {
/* storage unavailable → treat as not dismissed */
}
setWizardOpen(shouldShowWizard(view.you, dismissed))
})
const dismissWizard = () => {
try {
localStorage.setItem(WIZARD_DISMISS_KEY, "1")
} catch {
/* best-effort */
}
setWizardOpen(false)
}

function startWithPrompt(prompt: string) {
const project = newSessionProject()
if (!project) {
openNewSession()
if (project) {
tabs.newDraft({ server: server.key, directory: project.worktree }, prompt)
return
}
tabs.newDraft({ server: server.key, directory: project.worktree }, prompt)
// No tracked projects (fresh profile against a bare `opencode serve`):
// openNewSession() would dead-end silently here — it needs the same
// newSessionProject() that just came back empty. Fall back to the server's
// own working directory (path.directory, synced from GET /path; "" until
// loaded) and start tracking it, so the home CTAs work on first visit.
// Deliberately NOT sync.data.project: its "global" record has worktree "/".
const conn = focusedServer()
const directory = focusedSync().data.path.directory
if (!conn || !directory) return
const ctx = global.createServerCtx(conn)
ctx.projects.open(directory)
ctx.projects.touch(directory)
tabs.newDraft({ server: ServerConnection.key(conn), directory }, prompt)
}

function setSelection(next: HomeProjectSelection) {
Expand Down Expand Up @@ -628,19 +720,21 @@ function HomeDesign() {
</div>
{/* Cards: full width across, sized to content so they never scroll */}
<div class="relative z-[1] flex-none pt-1">
{/* solver mode (show-only v1, spec-20260709-093000): right-aligned above the cards */}
<div style={{ display: "flex", "justify-content": "flex-end", "margin-bottom": "8px" }}>
<AmicodeSolverToggle
mode={solverState()?.mode}
switching={solverState()?.switching}
onSelect={selectSolver}
/>
</div>
<AmicodeHomeCards
profile={profileView()}
starters={AMICODE_STARTERS}
onStart={startWithPrompt}
library={libraryView()}
onUploadPaper={uploadPaper}
onEditProfile={() => startWithPrompt("update my profile — my name, affiliation, and what I work on")}
onSaveProfile={async (fields) => {
// In-place save (About-You card): identity fields ride query
// params on the raw POST route; refetch renders the saved state.
const q = new URLSearchParams()
for (const [k, v] of Object.entries(fields)) if (v !== undefined) q.set(k, v)
await amicodePost(focusedServer(), `/amicode/profile?${q.toString()}`)
await refetchProfile()
}}
onSaveProfile={saveProfileFields}
resumeName={resumeProblem()?.name}
resumeMeta={resumeMeta()}
onResume={() => {
Expand All @@ -657,6 +751,33 @@ function HomeDesign() {
/>
</div>
<AmicodeFooter />
<Show when={wizardOpen()}>
<AmicodeOnboardingWizard
initialName={(() => {
const v = profileView()
return v?.ok ? v.you.name : ""
})()}
onComplete={saveProfileFields}
onUploadPaper={uploadPaper}
onDismiss={dismissWizard}
onOpenChat={() => {
dismissWizard()
startWithPrompt("")
}}
/>
</Show>
<Show when={switchTarget()}>
{(target) => (
<AmicodeSolverSwitchWizard
target={target()}
poll={pollSolver}
onDone={() => {
setSwitchTarget(undefined)
void refetchSolverMode()
}}
/>
)}
</Show>
<Show when={galleryOpen()}>
<AmicodeRunGallery
cards={runCards()}
Expand Down
11 changes: 10 additions & 1 deletion packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1200,8 +1200,17 @@ export default function Page() {
),
)

// Only follow the bottom while the model is actually streaming. This was
// previously hard-coded to `true`, so the auto-scroller's ResizeObserver
// force-scrolled on *any* reflow (image load, tool accordion expand, font
// swap, layout settle) — which read as the chat "jumping" on its own. Gating
// it on the session's real working state also stops it from competing with
// the timeline's own bottom-lock outside of streaming.
const autoScroll = createAutoScroll({
working: () => true,
working: () => {
const id = params.id
return id ? sync.data.session_working(id) : false
},
overflowAnchor: "dynamic",
})

Expand Down
Loading
Loading