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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

### Changed

- **Trust: `/trust` opens on the ledger, and the ledger reads in order.** The ledger and the activity
read as disorder in a busy project; both are rebuilt around what you came to do.
- **The ledger first.** What Trust holds, and where you act on it, is what `/trust` opens on.
Today's answers are one row above it: select it and the card lists them; `a` (or `enter` on it)
opens the full activity, and `esc` comes back.
- **Grouped by kind, then by family.** Commands, Edits, Tools & fetches and OpenCode's own each have a
heading, the current one kept at the top as the list scrolls. Every edit sits under its folder,
one row per folder with its file count, instead of scattered between commands.
- **What was seen once is folded away.** Families approved once and never again — most of a busy
ledger — are one `seen once` row per kind; `→` opens it and `/` still finds them.
- **One column, one shape.** Every row ends in the same column: `✓` trusted, `▰` learning, `○` seen
once. A family's card lists one command a line with its standing, and its buttons sit on its last
row.
- **Paths cut in the middle.** `tail -10 ~/…/events.ndjson`, not `~/.local/share/opencode-cockpit/…`
five times with the file name cut off.
- **The activity lines up.** Every row starts with its mark, then the time, then the command; with one
agent, the header names it once instead of a chip on every row.

## [0.10.0] - 2026-10-05

### Changed
Expand Down
22 changes: 16 additions & 6 deletions packages/trust/src/cli/preview.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/usr/bin/env bun
/**
* The see-it loop: the sidebar block and both Trust screens — the activity `/trust` opens on and the
* ledger behind `l` — drawn in this terminal from sample worlds, with no OpenCode running. The same
* The see-it loop: the sidebar block and both Trust screens — the ledger `/trust` opens on and the
* activity behind `a` — drawn in this terminal from sample worlds, with no OpenCode running. The same
* rows OpenCode draws; only the colours come from a fixed palette (OpenCode's default theme) instead
* of the user's.
*
* bunx @opencode-cockpit/trust preview every sample, both screens
* bunx @opencode-cockpit/trust preview --sample busy one of them
* bunx @opencode-cockpit/trust preview --view activity only the screen /trust opens on
* bunx @opencode-cockpit/trust preview --view activity only the activity, behind `a`
* bunx @opencode-cockpit/trust preview --view ledger only the ledger (the explorer and its card)
* bunx @opencode-cockpit/trust preview --columns 80 the dialog at another width
* bunx @opencode-cockpit/trust preview --rows 20 the dialog in a short window
Expand Down Expand Up @@ -165,7 +165,7 @@ for (const name of names) {
out.push("", title, "")
out.push(...frame(rows))
}
drawActivity("Activity — as /trust opens")
drawActivity("Activity — behind a")
if (keys) drawActivity("Activity — ? keys", undefined, true)
}

Expand All @@ -180,7 +180,8 @@ for (const name of names) {
extra: { focus?: { button: number }; keys?: boolean; typing?: string; filter?: string } = {},
) => {
const filter = extra.filter ?? ""
const model = explorerModel({ ...reading, open, full, filter })
/** `today`: the strip is a node the cursor can stop on; its count does not matter here. */
const model = explorerModel({ ...reading, open, full, filter, today: 0 })
const selected = pick(model.nodes)?.key
const { rows } = explorerRows({
...reading,
Expand All @@ -198,7 +199,16 @@ for (const name of names) {
out.push("", title, "")
out.push(...frame(rows))
}
drawLedger("Ledger — as l opens it", (nodes) => nodes[0])
drawLedger("Ledger — as /trust opens it", () => undefined)
drawLedger("Ledger — the Today strip", (nodes) => nodes.find((node) => node.kind === "today"))
const fold = explorerModel({ ...reading, ...tree }).nodes.find((node) => node.kind === "once")
if (fold) {
open.add(fold.key)
drawLedger("Ledger — what was seen once, opened", (nodes) =>
nodes.find((node) => node.key === fold.key),
)
open.delete(fold.key)
}
/** A trusted command in a family of several, revealed the way `enter` on the activity does. */
const trusted = families
.flatMap((family) => (family.commands.length > 1 ? family.commands : []))
Expand Down
90 changes: 87 additions & 3 deletions packages/trust/src/core/sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { createEngine, type Engine } from "./engine.ts"
import type { Request } from "./keys.ts"
import { DAY } from "./ledger.ts"
import { DAY, type Event } from "./ledger.ts"
import { rulesFrom } from "./rules.ts"

export const SAMPLE_ROOT = "/work/app"
Expand All @@ -25,8 +25,14 @@ const RULES = rulesFrom({
permission: { bash: { "*": "ask", "npm publish *": "ask" }, edit: "ask", webfetch: "ask" },
})

function build(script: (step: Steps) => void): Engine {
const engine = createEngine(SAMPLE_SETTINGS)
/** Runs `script` on a fresh engine; `record` gets every event it loads, in order — a ledger file's lines. */
function build(script: (step: Steps) => void, record?: Event[]): Engine {
const real = createEngine(SAMPLE_SETTINGS)
const engine: Engine = Object.create(real)
engine.load = (events, options) => {
record?.push(...events)
real.load(events, options)
}
let n = 0
let clock = SAMPLE_NOW - 3 * DAY
const ask = (line: string, permission = "bash", patterns = [line], agent = "build") => {
Expand Down Expand Up @@ -135,6 +141,81 @@ function crowd(s: Steps): void {
s.pending("git push origin feat/trust")
}

/**
* A crowded storefront project (`acme-store`), as a user's ledger looked: one agent, `orchestrator`; a
* `tail` family on one long ledger path, widened by hand; `ls` widened too; edits in five folders,
* one of them outside the project; MCP calls through `mcpx`, `gh` and `jq`; a fetch and a subagent
* type; a long tail of commands seen once; and OpenCode's own "always" given twice.
*/
function storefront(s: Steps): void {
const agent = "orchestrator"
const run = (line: string, times: number, how: "once" | "always" = "once") =>
s.approve(line, times, how, "bash", agent)
const edit = (path: string, times: number) => s.approve(path, times, "once", "edit", agent)
const hour = 3_600_000
const ledger = "~/.local/share/opencode-cockpit/trust/Projects-acme-store/events.ndjson"
const review = "/var/folders/ab/x7k2q9/T/opencode-review"
s.at(SAMPLE_NOW - 2 * DAY)
run(`tail -4 ${ledger}`, 3)
run(`tail -10 ${ledger}`, 2)
for (const n of [3, 9, 8]) run(`tail -${n} ${ledger}`, 1)
run(`ls -la ${review}`, 3)
run(`ls -l ${review}/screens`, 2)
run("git status --short", 2)
for (const line of ["git log --oneline -20", "git log -1 --format=%H", "git log --stat -3"]) run(line, 1)
run("git merge-base HEAD origin/main", 1)
run("git merge-base --is-ancestor HEAD main", 1)
run("gh pr view 482 --json url,title,state", 2)
run("gh pr list --search SHOP-21 --state all", 2)
run("gh api --method PATCH /repos/acme/store/pulls/482 -f body=@body.md", 2)
run("sed -n 1p .env.example", 2)
for (const line of [
"jq '.items | length' report.json",
"jq -r '.name' package.json",
"jq '.scripts' package.json",
"jq -c '.[]' out.json",
])
run(line, 1)
for (const line of ["head -40 README.md", "head -5 CHANGELOG.md", "echo $STORE_URL", "echo done"])
run(line, 1)
for (const line of ["grep -rn TODO src", "grep -c error build.log", "git branch -a"]) run(line, 1)
run("mcpx jira getJiraIssue --site acme --issue SHOP-34", 2)
run("mcpx jira searchIssues --jql 'project = SHOP'", 1)
run("mcpx github list_pull_requests --repo acme/store", 1)
edit(`../../../../../var/folders/ab/x7k2q9/T/opencode-review/notes.md`, 3)
edit("packages/api/src/customers.ts", 2)
edit("packages/api/src/shopify.ts", 1)
edit("packages/api/src/orders.ts", 1)
edit("packages/web/context/auth.tsx", 1)
edit("packages/web/context/cart.tsx", 1)
edit("packages/web/app/checkout/page.tsx", 2)
s.approve("https://docs.example.com/api", 3, "once", "webfetch", agent)
s.approve("explore", 2, "once", "task", agent)
s.approve("find . -name '*.md'", 1, "always", "bash", agent)
s.approve("rg --files", 1, "always", "bash", agent)
s.at(SAMPLE_NOW - 2 * hour)
s.widen("tail", agent)
s.widen("ls", agent)
s.at(SAMPLE_NOW - 75 * 60_000)
s.auto(`tail -4 ${ledger}`, 1, "bash", agent)
s.at(SAMPLE_NOW - 30 * 60_000)
s.auto(`ls -la ${review}`, 2, "bash", agent)
s.auto(`../../../../../var/folders/ab/x7k2q9/T/opencode-review/notes.md`, 1, "edit", agent)
s.auto(`ls -l ${review}`, 1, "bash", agent)
s.auto(`tail -4 ${ledger}`, 1, "bash", agent)
s.auto(`tail -10 ${ledger}`, 1, "bash", agent)
s.auto("https://docs.example.com/api", 1, "webfetch", agent)
s.at(SAMPLE_NOW - 2_000)
s.pending("gh api --method PATCH /repos/acme/store/pulls/482 -f body=@body.md")
}

/** The `storefront` sample's events, as its ledger file would hold them, for a test project's ledger. */
export function storefrontEvents(): Event[] {
const events: Event[] = []
build(storefront, events)
return events
}

/** A week of a project, as steps: rules earned early on, answered on most days since, the newest today. */
function week(s: Steps): void {
const general = (line: string, times: number, how: "once" | "always" = "once") =>
Expand Down Expand Up @@ -262,6 +343,9 @@ export const SAMPLES: Record<string, () => Sample> = {
*/
crowded: () => ({ engine: build(crowd) }),

/** The project the ledger's redesign was drawn from: kinds, folders of edits, a long tail seen once. */
storefront: () => ({ engine: build(storefront) }),

/** The same afternoon, paused: the dialog has to say it before anything else. */
"crowded-paused": () => {
const engine = build(crowd)
Expand Down
Loading
Loading