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
42 changes: 25 additions & 17 deletions packages/app/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ other surface opens beside or in front of the Chat; the Chat is never replaced b
another surface, only overlaid. There is no view in which the Chat is absent.
_Avoid_: Main view, editor, session pane (as the whole hub)

**Brain**:
The data-true living background of the **Chat** — a map of the current session's thought that
fills the Chat behind every component and is never interactive. One per window, present wherever
the Chat is (Landing included) and nowhere else.
_Avoid_: Background, wallpaper, atmosphere, brain strip, canvas

**Glass**:
The frosted, translucent surface every chat component floats on above the **Brain** — tinted
just enough that text stays legible over the moving graph. (How the blur and tint are derived
is an ADR-0002 concern, not a glossary one.)
_Avoid_: Card background, frosted panel, blur layer, tier
**Context tree**:
The collapsible band folded into the session header, beneath the title row and entity
chips — a left-to-right tree of what the agent is actually holding in mind: root = amico,
branches = the session's turns (one per user prompt, however many assistant messages it
spans), leaves = the markdown, source, skills, and agents each turn pulled into context.
Interactive: clicking a file node opens the real file (project files as a session tab,
vault files in the **Vault panel**). One per session view; absent until the session holds
context, and absent on the Landing. See ADR 0003.
_Avoid_: Brain (that was the ambient, non-interactive strip), knowledge graph, minimap

**Vault panel**:
The dismissible drawer that puts the vault — every attached mount and every file inside
it — in the interface. Read-only browsing with inline markdown/source rendering; opened
from the titlebar vault button, the command palette, or a **Context tree** click on a
vault node.
_Avoid_: Vaults tab (that is the mount *list* in status), file manager, explorer

**Rail**:
The one narrow vertical strip of icons along the window's leading edge. Collapsed to
Expand Down Expand Up @@ -80,9 +85,10 @@ only surfaces its state), setup wall
## Relationships

- The **Landing** is a **Chat**; the window never opens on anything else.
- The **Brain** is the background of the **Chat** and only the Chat; every chat component
floats on **Glass** above it, while the **Rail**, titlebar, and **Panels** stay solid and
frame the living pane.
- The **Context tree** sits above the **Chat** and belongs to one session; the **Vault
panel** belongs to the window and opens beside whatever Chat is showing. A Context-tree
click on a vault node opens the Vault panel on that file — the two surfaces reference
each other but neither owns the other.
- Every **Rail surface** opens as a **Panel** beside the **Chat**; the Chat is never
replaced, only overlaid, and at most one Panel is open at a time.
- The **Rail** carries navigation surfaces above and the **Account zone** below; both
Expand Down Expand Up @@ -115,6 +121,8 @@ reader meeting them in older code or notes should map them forward:
a possible future, not part of this redesign. See ADR 0001.
- **Home cards** "Meet Amico" and "Jump back in" → folded into the first-run Landing and
the Chats surface respectively; they are no longer standalone surfaces.
- **Inline brain strip** (the Brain as a single row inside the message timeline) → absorbed
into the **Brain** background. One render loop, promoted from a timeline row to the room;
the timeline keeps only its text shimmer. See ADR 0002.
- **Brain** (the ambient session map — first an inline timeline strip, then ADR 0002's
proposed chat-wide background) → superseded by the **Context tree** top panel. The
background direction was parked (ADR 0003); the strip row is deleted; the ambient engine
survives as a library only. **Glass** (the frosted card surface of the background design)
retires with it.
3 changes: 2 additions & 1 deletion packages/app/docs/adr/0002-brain-as-chat-background.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 0002 — The Brain becomes the Chat's living background; components float on legibility-floored glass

- Status: Proposed
- Status: Superseded by ADR 0003 (direction parked 2026-07-26; the Brain moved to the
context-tree top panel instead of becoming the chat background)
- Date: 2026-07-24
- Deciders: Kate Bonner (Head of Product)
- Tracking: harmoniqs/opencode#56
Expand Down
74 changes: 74 additions & 0 deletions packages/app/docs/adr/0003-context-tree-top-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# 0003 — The knowledge graph moves to a top panel as the agent's context tree; the vault gets a first-class surface

- Status: Accepted
- Date: 2026-07-27
- Deciders: Kate Bonner (Head of Product), on Aaron Trowbridge's design feedback (2026-07-26)
- Supersedes: ADR 0002 (Brain as chat background — direction parked)

## Context

The amico **Brain** rendered as an inline strip inside the message timeline: one row among
many, easy to scroll past, non-interactive by design (`aria-hidden` canvas, no hit-testing),
and drawn over a **hardcoded sample of the armonissima vault** rather than anything the
agent is actually doing. ADR 0002 proposed promoting it to a chat-wide living background;
that direction was reviewed and parked.

Aaron's feedback reframed what the graph is *for*: not ambience, but **orientation** — a
researcher watching the agent work wants to see *what the agent is holding in mind* and to
reach those artifacts directly. Two gaps followed from that framing:

1. The graph showed a decorative vault skeleton, not the session's real context, and
nothing on it could be clicked.
2. The **vault itself** — the knowledge base the agent reads and writes — had no surface in
the interface at all. The app could list mount names (`/amicode/vaults`) but could not
browse or read a single note.

## Decision

**1. The graph moves out of the timeline into a pinned top panel** between the session
header and the chat, collapsible, one per session view. The inline brain strip row is
removed from the timeline (`TimelineRow.Brain` deleted); the ambient background direction
of ADR 0002 is not pursued.

**2. The graph is redesigned as the agent's context tree.** A left-to-right tidy tree:
root = amico; branches = the session's turns (roman-numeral plates carrying the prompt
excerpt — the atlas idea, restructured); leaves = the distinct markdown, source, skills,
agents, and web references each turn pulled into context. Commits only — searches/globs
are transient scouting and never enter the tree. A leaf re-touched by a later turn is
**deduplicated into a thin recall link** back to the existing node, so the tree stays a map
of distinct context, not a log. Marathon sessions fold their oldest turns into one
"earlier" branch.

**3. The tree is interactive — the graph is now a navigation instrument.** Nodes
hit-test; hover raises the label and hands the node to the host; **clicking a file node
opens the real file** — project files as a session file tab (the same flow as the file
tree), vault files in the Vault panel, landed directly on that note. Camera: auto-fit
until the user pans/zooms; wheel zooms about the cursor; double-click refits. The log's
hover-glance events (`amicode:brain-hover`) keep working against the tree.

**4. The vault lives in the interface.** A read-only **Vault panel** (drawer beside the
Chat, per the CONTEXT.md Panel concept) lists every attached mount and every file inside
it, and renders markdown and source inline. Two new raw server routes back it —
`GET /amicode/vault-files` (recursive listing) and `GET /amicode/vault-file` (single read,
real-path traversal guard, size cap) — beside the existing `/amicode/vaults` mount list.
Entry points: a titlebar vault button (both titlebar variants), the command palette
("Toggle vault panel"), and context-tree clicks on vault nodes.

## Consequences

- `brain-engine.ts` stays as-is (the ambient engine remains a library; the strip component
is deleted). The context tree is a **separate, purpose-built engine**
(`context-tree-engine.ts`): declarative `setTree`, tidy layout, pointer interaction,
DPR-change-aware canvas. Both engines remain exempt from the single-yellow accent rule as
data visualizations; `#fff676` still marks only the live position.
- The tree derivation is pure and headless (`context-tree-data.ts`), unit-tested without
the sync store; `brain-ref` now carries the full file path so nodes can open files.
- The vault routes are read-only and refuse path escapes even through symlinks; binaries
are listed but marked unreadable, so the tree still shows the vault's true shape.
- Vault contents are proprietary knowledge, so browsing is a **local-researcher
capability, not a server API**: the routes refuse on any non-loopback bind (same
signal as the credential-mutation guard; `AMICO_VAULT_BROWSER=1` opts a shared
deployment in, `=0` forces off), and a mount can go fully dark with
`browse = false` in its `.amico-vault.toml` — the agent's read grants are unaffected.
- Keyboard reachability of individual canvas nodes is an open follow-up; every action the
canvas offers also exists via keyboard-reachable surfaces (file tree, Vault panel).
77 changes: 77 additions & 0 deletions packages/app/docs/design/agentic-surfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Agentic surfaces — static elements generated from the researcher's work state

- Status: Proposal for discussion (Kate × Aaron) — no implementation yet
- Date: 2026-07-27
- Origin: Aaron Trowbridge's feedback (2026-07-26): "the potential to make static
elements agentically generated based on the user's current work state"

## The idea

Most of the interface outside the conversation is **authored once and shown to
everyone**: the new-session view, the setup nudge, empty states, panel headers, the
defaults capsule. Aaron's observation is that an agent product doesn't have to treat
these as fixed copy — the agent knows the researcher's work state (active problems, run
history, vault recency, calibration drift, what the last session ended on) and could
**compose these surfaces** the way it composes an answer.

The context-tree top panel (ADR 0003) is the first surface of this kind already shipped:
its content is entirely derived from work state, with a fixed *frame* and generated
*content*. This document asks where else that pattern pays for itself.

## Inventory — static elements that could be work-state-driven

| Surface | Today | Agentically generated would mean |
|---|---|---|
| New-session view | Fixed problem list + copy | "You were mid-sweep on the CZ ladder; run 9 finished overnight — resume?" with the 2–3 next actions ranked from ledger + session history |
| Setup nudge | Profile completeness flag | Nudge text chosen from the actual missing step (vault, Julia, connection) and phrased against what the user tried last |
| Empty states (run gallery, pulse bank, library) | Generic copy | Seeded with the nearest real thing: last run, closest catalog pulse, most-recent paper |
| Session header context | Session title | One generated line of "where this thread left off" on resume |
| Composer placeholder | Fixed prompt copy | Rotates against work state ("ask about run 12's stagnation…") |
| Defaults capsule | Static solver defaults | Defaults proposed from the last N solves of this problem family |

## Architecture sketch — three tiers, in order of trust

1. **Derived (no LLM, deterministic).** Templates filled from ledger/vault/session
queries. Cheap, always fresh, testable — the context tree lives here. Most of the
table above can too, and this tier should be exhausted first.
2. **Generated-and-cached.** A background agent (the distiller cadence fits) writes
short surface copy into a per-user store; the UI renders it as data. Latency-free at
render time; staleness bounded by the cadence. The existing server-rendered widget
pipeline (`widgets-src/*` → `/amicode/widget-frame`) is the natural substrate — it
already solves sandboxing, CSP, and per-widget data plumbing.
3. **Live-generated.** Composed at render time by a model call. Highest freshness,
highest cost, needs skeleton states and a hard timeout fallback to tier-1 content.
Probably justified only for the new-session view, if at all.

## Constraints that must hold

- **Frames are fixed; content is generated.** Layout, tokens, and interaction patterns
stay authored (amicode-design-system governs them); the agent fills slots. No
agent-invented UI structure.
- **Fallback is the current static copy.** Every agentic surface degrades to exactly
what ships today — an empty work state must never produce a broken or blank surface.
- **Provenance visible on anything actionable.** If a surface proposes an action
("resume the sweep"), it must say what it derived that from, and be wrong-safe:
clicking a stale suggestion can never destroy state.
- **No surprise tokens.** Tier 2/3 generation runs on explicit cadences or user action,
never silently per-render.

## Open questions for Aaron

1. Which surface first? The new-session view has the highest leverage (it's the landing
for every returning user), but empty states are the cheapest proof of the pattern.
2. Is tier 2 (cached generation on the distiller cadence) fresh enough, or does the
resume-context line need tier 3?
3. Should generated copy be visibly marked (e.g. the thought-color accent) so
researchers learn which parts of the interface are alive?
4. Does the widget kernel become the delivery mechanism for all of this, or is it
simpler to add a `GET /amicode/surface?id=…` route returning structured slots?

## Relationship to shipped work

- **Context tree (ADR 0003)** — tier 1, shipped. The template for "fixed frame,
work-state content."
- **Command zoom fixes** — unrelated mechanically, but they remove the "static chrome is
broken" noise that would undermine trust in smarter chrome.
- **Vault panel** — the read path any generated surface will cite when it references
vault knowledge.
29 changes: 27 additions & 2 deletions packages/app/src/components/prompt-input/clipboard-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { readClipboardViaBridge, writeClipboardViaBridge } from "./clipboard-bri
type Listener = (event: MessageEvent) => void

// A stand-in for a framed window (parent !== self) that records outgoing
// clipboard-requests and lets a test play back the host's reply.
function fakeFramedWindow() {
// clipboard-requests and lets a test play back the host's reply. `readText`
// simulates the host's navigator.clipboard grant: a function to grant it,
// absent to model the VS Code webview iframe (no permission).
function fakeFramedWindow(readText?: () => Promise<string>) {
const listeners = new Set<Listener>()
const posted: Array<Record<string, unknown>> = []
const win = {
addEventListener: (_type: string, fn: Listener) => listeners.add(fn),
removeEventListener: (_type: string, fn: Listener) => listeners.delete(fn),
navigator: readText ? { clipboard: { readText } } : {},
parent: {
postMessage: (message: Record<string, unknown>) => posted.push(message),
},
Expand Down Expand Up @@ -60,6 +63,28 @@ describe("readClipboardViaBridge", () => {
expect(await pending).toBe("")
})

test("framed host WITHOUT the amicode relay (Simple Browser, plain embeds) falls back to navigator.clipboard", async () => {
// No reply ever comes — the request went into the void. The API key being
// pasted must still arrive via the host's own clipboard-read grant.
const bridge = fakeFramedWindow(() => Promise.resolve("sk-cloud-key-123"))
expect(await readClipboardViaBridge(bridge.win, 15)).toBe("sk-cloud-key-123")
})

test("a live bridge reply wins — navigator.clipboard is not consulted", async () => {
const bridge = fakeFramedWindow(() => {
throw new Error("must not be called when the bridge answered")
})
const pending = readClipboardViaBridge(bridge.win)
const request = bridge.posted[0]
bridge.reply({ source: "amicode", kind: "clipboard", nonce: request.nonce, text: "from the bridge" })
expect(await pending).toBe("from the bridge")
})

test("no relay AND no clipboard permission resolves empty (the caller's no-op stands)", async () => {
const bridge = fakeFramedWindow(() => Promise.reject(new Error("NotAllowedError")))
expect(await readClipboardViaBridge(bridge.win, 15)).toBe("")
})

test("resolves empty without posting when the app is not framed", async () => {
const posted: unknown[] = []
const win = {
Expand Down
23 changes: 22 additions & 1 deletion packages/app/src/components/prompt-input/clipboard-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ export function writeClipboardViaBridge(text: string, win: Window = window): boo
return true
}

// The bridge only answers inside the AMICODE webview — its parent runs the
// relay. Every OTHER framed host (VS Code Simple Browser pointed at :3002, any
// plain iframe embed) posts into the void, times out, and — because the ⌘V
// handlers preventDefault before asking — pastes NOTHING, silently. So an
// empty/timed-out bridge reply falls through to navigator.clipboard.readText():
// dead in the VS Code webview (no permission to lose), but it restores paste in
// every framed host that grants clipboard-read. API-key entry was the reported
// casualty (credential fields are exactly what you paste into an embedded app).
async function readTextDirect(win: Window): Promise<string> {
try {
const text = await win.navigator?.clipboard?.readText?.()
return typeof text === "string" ? text : ""
} catch {
return "" // no permission / no API — the caller's no-op stands
}
}

export function readClipboardViaBridge(win: Window = window, timeoutMs = BRIDGE_TIMEOUT_MS): Promise<string> {
return new Promise<string>((resolve) => {
// Unframed: native paste works — don't post into the void or wait out the timeout.
Expand All @@ -40,7 +57,11 @@ export function readClipboardViaBridge(win: Window = window, timeoutMs = BRIDGE_
const finish = (text: string) => {
win.removeEventListener("message", onMessage)
if (timer !== undefined) clearTimeout(timer)
resolve(text)
if (text) {
resolve(text)
return
}
void readTextDirect(win).then(resolve)
}

const onMessage = (event: MessageEvent) => {
Expand Down
Loading
Loading