@computesdk/loops

中文文档
0.1.19发布于 2026/11/04MITES Module近 30 天下载 3987

Loops for people who do not live in a terminal: one command starts a localhost UI that runs, watches and steers hill-climbing agents in your repos.

原始使用文档
文档目录16 节
  1. Commands, providers, and chat models
  2. One server per VM
  3. Run it
  4. Navigation links
  5. Attach to a server on another machine
  6. What it is made of
  7. Why an event stream and not a poll
  8. Proof that the judge saw the picture
  9. Live: one server, on demand
  10. Judging happens in a child process
  11. The checkout is not the attempts' to touch
  12. Cleanup
  13. Settings that change under you
  14. Actions
  15. Parity
  16. Development

@computesdk/loops

Loops for people who do not live in a terminal: a loops CLI and a localhost web UI, both clients of @computesdk/pi-loops. The extension is the foundation — it owns the loops, the records and every capability. This package owns none of that; it drives the engine.

loops status                  # what ran here, read straight from the records (no pi process, no tokens)
loops doctor --live           # can a round work here? boots serve, runs check, asks the judge to read an image
loops run "<goal>"            # a round — the one verb that costs tokens
loops clean | apply | take | rule | questions | answer
loops serve                   # one browser app for this VM; choose workspaces in the UI

Installed from npm it is one command, and that is the one to hand someone:

npx @computesdk/loops serve

It is a client of a real Pi session, not a re-implementation of one. Chat state uses RPC and SSE rather than polling or another database: the session is the durable record, and .pi/loops/ is a rebuildable index plus its worktrees.

Commands, providers, and chat models

Workspaces are persistent horizontal tabs above the entire app, including the loop inspector. The chat sidebar belongs to the selected workspace; tabs scroll independently when many projects are open. The compact conversation header opens full repository details. Commands (including model/provider actions) open through /, Ctrl/Cmd K, or the compact / composer button; there are no standalone Commands or Chat model buttons. Provider setup is also available before the first chat in workspace onboarding, and through Loop settings. The workspace strip, chat sidebar, and conversation use distinct neutral surfaces in light and dark themes.

Use the compact / control or press Cmd/Ctrl K. Typing / in the composer opens the same keyboard-navigable menu. Commands uses one palette frame and one close control. Other forms and Loop settings use native modal dialogs/sheets; Cmd/Ctrl K does not stack Commands over them or cancel an active provider connection. Use ↑/↓ to browse and Enter to choose. Model/provider forms focus their controls when ready; the model filter's arrows enter the model list, and Enter confirms the selected model. Pi choice forms also support ↑/↓ (plus Home/End), then Enter/Space to answer. Browsing alone never submits a choice. An incoming decision preserves focus if you have started a new, unsubmitted draft. Pi discovers that chat's extension commands, prompt templates, and skills; their exact names and source are shown. Choosing one inserts it into the composer—review its arguments and explicitly Send. Unknown slash commands are rejected without sending them to the model. This is not an emulation of Pi's full terminal interface.

App actionBehavior
/newCreate a new app chat in the selected workspace; retain the old chat and its draft
/modelChoose an exact provider/model from this writer's available catalog
/compactConfirm a potentially paid compaction; show Pi's progress/result without automatically retrying
/loginConnect a provider using Pi's OAuth/API-key flow

Discovered Pi names win collisions. The corresponding app action moves to an available app: alias displayed in the menu, instead of silently shadowing an extension, template, or skill. Model selection changes the parent chat only; it does not rewrite loop role/attempt configuration or global defaults. Model changes and compaction require an idle parent and no running background loops. Neither operation replaces the Pi session or creates another writer.

Connect provider also works before selecting a chat. Pi's public provider metadata determines the available login methods. OAuth supports browser authorization, manual callback/code prompts for remote VMs, and device-code flows when offered by the provider. Finish the provider's instructions in the dialog; on a remote VM, a localhost redirect may fail on your laptop, in which case copy its full callback URL into Pi's manual prompt. Closing the dialog cancels its pending flow. Expiry, cancellation, and server shutdown clean up callback listeners. Cancellation cannot undo a credential already committed or revoke an account grant.

API keys and authorization responses go only in JSON to the dialog's pinned server with its pinned bearer. They are not written to chat, URLs, logs, or browser storage. Pi owns credential persistence and OAuth refresh; the UI does not copy or implement provider token storage. A submitted key can require follow-up prompts: Connected means Pi confirmed persistence, not that a paid verification request succeeded. Merely opening the dialog lists stored-presence metadata and does not execute configured credential commands or sign in. There is one bounded, in-memory login flow per server; its status is polled only while the dialog is open/pending.

After login, idle writers refresh Pi's public model runtime without a model request or automatic model switch. The bundled SHA-pinned RPC adapter adds this metadata refresh; custom Pi executables without it show a restart warning and are never restarted automatically. Startup extension decisions remain separate and must be answered before that writer's commands/models are available. Loop settings retain role configuration and checks, while their former key-only panel now opens the shared provider dialog.

What the UI showsWhere it comes from
the transcriptget_messages from the chat's pi --mode rpc child
a loop, its rounds, its scoresthe loops-state snapshot and loops-round entries the extension appends to that session
the current winner / applied committhe newest loops-state snapshot (best, winner, baseCommit)
a round in flightthe loops-progress entries it appends while it works (throttled; phase changes always)
questions for youloops-question / loops-answer entries — append-only, so nothing is rewritten
the brief / approve cardthe extension's ctx.ui.select, as an extension_ui_request
diffs, screenshots, sheetsread-only routes over the run's worktrees and archived shots under .pi/loops/

Visual previews wait for actual screenshot files rather than treating configured output paths as images. Unavailable or failed images show an explicit state and Retry; retrying an image does not rerun the loop. Attempt details show archived captures without requiring a live server or applying the candidate. Use the capture selector for the actual desktop/mobile screenshots, and Open screenshot to inspect full size. Live pages use a sandboxed iframe only when a real live URL exists.

Outcome questions belong to their exact run and round. Later execution can supersede them (including a recorded same-round retry); expired/superseded cards leave Needs you but remain in the conversation with their reason. This is derived from Pi's entries, including existing histories, never a manufactured human answer. Generic asks remain open until answered or expired. Stale answer submissions are refused; stopping alone does not answer a card.

Completed results keep the metric, candidate check, changed files, and next step together in one header. Loop thumbnails occupy their own independently scrolling column below workspace and stop controls, beside the conversation and composer—not a floating overlay. Phones retain an in-flow horizontal strip. “Tracked edits applied” requires the engine's recorded best.applied commit; answering an Apply question does not prove success. Old records without that fact remain unconfirmed. Added/deleted files are called out separately because the engine's tracked-file apply does not include them. Apply stays in the conversation. “Best version vs original” and its file list keep comparing the first round's recorded fork commit to the winning candidate, even after Apply, resume, or a checkout rebase. This is historical candidate evidence, not a claim that added/deleted files landed in your checkout. Legacy records without that original anchor retain their recorded execution-base comparison; the server does not guess an original from a later round or commit parent.

A chat's directory comes from Pi itself: each session header records its cwd. The server resumes that exact session file in that directory; selecting a workspace cannot move a chat. Pi remains the only session writer, with one RPC child per opened chat, shared by its browser subscribers. There is no second transcript store and no workspace-wide editing lock.

workspaces.json indexes sessions and persists workspace IDs, session locations, and which tabs are open. Closing a tab retains its workspace, chat bindings, and sessions. Recovery can rebuild bindings from session headers if the registry is lost, but an intact registry's closed tabs stay closed across restarts.

One server per VM

The server defaults to 127.0.0.1. Remote listening requires a bearer token (generated automatically when needed); this process can prompt an agent with bash access, so protect the token as a password to the VM. An SSH tunnel is another option (ssh -L 8001:127.0.0.1:8001 …).

Behind an authenticating reverse proxy, keep the listener on loopback and explicitly name its public origin:

loops serve --public-origin https://my-vm.exe.xyz,https://my-vm.exe.xyz:8001 --no-open

This trusts exactly those Host/Origin names, not arbitrary X-Forwarded-* headers. The proxy must restrict who can access the app; this flag does not add authentication. Do not use it to expose an unauthenticated agent. See deploy/loops.service.example for a supervised checkout deployment. Rebuild and restart the server after an update; rebuilding the SPA alone can leave a new client talking to an old backend.

Run it

Start it once for the machine, then open directories from the workspace launcher:

npx @computesdk/loops serve        # from npm — one server, independent of launch directory
# from a checkout:
loops serve                        # or: node server/index.ts --port 8001
# optionally seed a workspace:
loops serve --cwd ~/code/some-repo

It ships as one bundled server plus the built SPA, so there is no build step on the other end. The pi-loops extension and Pi runtime are installed as runtime dependencies. Chats use the bundled Pi RPC runner by default; --pi <executable> explicitly selects another Pi installation.

It prints the address and opens it:

  open  http://127.0.0.1:8001
  loopback only

--no-open (or LOOPS_UI_OPEN=0) prints the address without launching a browser. When the URL carries a token — off loopback — the opened address is the attach form, /?server=…&token=…. The app adopts it once and removes the token from the address bar and current history entry. A non-secret external server address stays in navigation links.

A loop needs the engine, so the boot says when it is missing rather than leaving you to find out on the first prompt: if the selected Pi executable or an --extensions path does not exist, it prints what is wrong and the fix under !! a loop cannot run here yet, and /api/health reports ok: false with the same list. The app still serves — you can add repos and read what already ran.

A workspace is a directory on the connected VM, with its chats, runs, and .pi/loops.json. + opens the launcher: choose a recent workspace, browse the VM's directories, or paste an absolute path (~/ works).

Workspaces, chats, and loop views have bookmarkable URLs:

/workspaces
/workspaces/<workspace-id>
/workspaces/<workspace-id>/chats/<chat-id>
/workspaces/<workspace-id>/chats/<chat-id>/loops/<run-id>
/workspaces/<workspace-id>/chats/<chat-id>/loops/<run-id>?round=2&attempt=R2-safe

IDs and attempt names are URL-encoded, durable identifiers, not display titles. Links support copy-link, new tabs, reload, and browser Back/Forward. Explicit links take precedence over remembered selections; only the legacy bare / entry restores the last selection and replaces itself with its concrete URL. Opening a link never creates a chat or rebinds its directory. A known closed workspace reopens with the same ID. Unknown, unavailable, or mismatched targets show an error and recovery links, not another chat's content or actions. Round/attempt focus is validated against that chat's actual loop history.

When attached to another backend, links include ?server=… (alongside any round/attempt parameters), but never its bearer token. Recipients need their own access to that server. A server-only link cannot forward another backend's saved credential; same-origin authenticated links retain their token on reload. An explicit link without a server parameter refers to the origin serving the UI, not a stale attached-server preference. Home, Up, breadcrumbs, and Show hidden navigate the server's filesystem, not the browser's computer. The picker shows whether Git has a commit; opening a directory never initializes Git or edits the project.

Opening a directory never creates a chat. A new workspace starts with its chat list and no Pi child. Choose an existing chat or explicitly create one with New chat in that workspace. The active full path stays visible. Returning to a workspace restores its last selected chat (resuming that writer if necessary), remembered separately for each connected server. × only closes a tab: it does not stop its agent, delete history, or change the session's directory. Reopen it from Recent workspaces.

The first launch has no implicit project; --cwd optionally seeds one. All tabs can be closed. Canonical paths deduplicate symlink aliases, same-named directories retain distinct stable IDs, and missing directories remain visible as unavailable instead of silently falling back to another workspace.

If Pi exits, the chat keeps its diagnostic instead of restarting the child on every read. Retry agent explicitly restarts that same session after the problem is resolved. Startup extension decisions (including exe.dev's first-run LLM setup) can be answered directly in the browser. The default runner uses Pi 1.4.0's own CLI with a narrow, in-memory compatibility fix: attach the RPC input reader before awaiting startup extensions, handle their answers immediately, and hold ordinary commands until setup finishes. It verifies the pinned version and exact upstream source before applying the fix; no global Pi files are modified. Pi still owns the session, extensions, model configuration, and all decisions.

The bundled Loops extension is a fallback: if Pi's enabled, trusted packages already supply @computesdk/pi-loops, the default runner uses that installation instead of loading a second copy. Other extensions remain enabled, and genuine conflicts still fail. This selection is also SHA-pinned to Pi 1.4.0's resource loader; explicit --extensions or --pi retain normal Pi loading semantics. Failed answer submissions remain visible with a retryable error; Loops never accepts or declines an integration for you. An explicit --pi bypasses this compatibility runner, so that executable must support startup dialogs itself.

Server metadata is in ~/.pi/loops-ui; browser navigation state and unsent drafts are kept in local storage. Drafts and failed messages are scoped to the connected server and chat. A send keeps its draft until acknowledged; errors retain the message for review and retry, including after switching chats or reloading. loops serve is a foreground server command, not an attach command: start it once (or supervise it as a service), then use that URL for every workspace. Running it again on the same port does not replace or attach to the first process.

Flags (each has a LOOPS_UI_* environment variable):

flagdefaultmeaning
--port8001where the app listens
--cwdunsetoptionally seed a workspace; launch directory is otherwise irrelevant
--pibundled Pi RPC runnerexplicitly override the Pi executable each chat spawns
--extensions../pi-loops/extensions/loop.tscomma-separated -e paths for the child
--data~/.pi/loops-uiworkspaces.json and the chats of every repo
--host127.0.0.1off-loopback listening requires a bearer token
--public-originunsetcomma-separated exact origins of a trusted authenticating reverse proxy
--modelPi's defaultthe model every chat starts on
--no-openoffprint the URL without opening a browser (LOOPS_UI_OPEN=0)
--devoffproxy the SPA to Vite on :5173 (npm run dev)

Attach to a server on another machine

The SPA is a client. By default it talks to the process that served it; it can be pointed at another one:

# on the box that owns the repos — off-loopback listening needs a token
node server/index.ts --port 3000 --host 0.0.0.0 --token     # prints the URL + token once

# anywhere else: either open the served UI at box:3000, or attach this one to it
http://box:3000/?server=http://box:3000&token=<printed>

The server/token pair is adopted once and stripped from the address bar; after that the sidebar footer names the server you are attached to (click it to change). A URL that lands in a log is not a problem because the token lives in localStorage afterwards, but treat it as the password it is: ~/.pi/loops-ui/token, mode 0600, --token to rotate.

The API is the product. Anything that wants its own chat UI, its own loop canvas, or its own needs-you inbox should use it rather than this SPA: see API.md for every route and the loop lifecycle, and client.ts for a typed client (no dependencies) that can be imported straight into another app.

The routes are the contract, not the SPA: GET /api/workspaces, /api/chats?ws=, /api/state?chat=, /api/config?chat=, POST /api/chat|prompt|steer|answer|ui|config|live, and one event stream per chat:

GET /api/chats/:id/events        Accept: text/event-stream
  id: <session entry id>         ← the cursor; send it back as Last-Event-ID after a drop
  event: state                   ← the same snapshot /api/state returns
  event: ping                    ← keep-alive every 20s

SSE over fetch (not EventSource) so the token can travel in a header, and so a reconnect resumes from the cursor instead of starting blind.

A server on loopback answers only its own origins (127.0.0.1, localhost, [::1] on its port) and only under those names in the Host header — the second half is what stops a page that points its own hostname at 127.0.0.1 and becomes same-origin. A loopback server needs no token for that, because the boundary is the name. Once the port is reachable off the machine the token is the authorization and any origin may attach; --allow-origin a,b narrows it.

What it is made of

server/
  index.ts      Hono: /api/state, /api/prompt, /api/ui, /api/steer, /api/shot, /api/diff, /ws, static
  chats.ts      one Chat per session: spawns `pi --mode rpc`, folds its events into one snapshot
  pi-rpc.ts     the RPC client: JSONL framing, id correlation, extension dialogs
  runs.ts       the durable session entries first, the `.pi/loops` index as fallback -> the shapes the canvas draws
  workspaces.ts the repos this process may touch; chats are bound to one
  directories.ts bounded, authenticated directory browsing on the server
  config.ts     settings with provenance, agent-write proposals, and the doctor checks
  live.ts       one live server for the winner, on demand
src/
  shared/      the wire types both halves use
  client/      the SPA (React 19 + Tailwind v4), ported from the POC at ~/factory/mock

Why an event stream and not a poll

A loop runs in the background in Pi: the loop tool returns as soon as the round starts, and the round card arrives later as a message. In a terminal that is fine — the status line and the card are enough. A canvas wants more, so the extension appends a loops-progress entry (at most one per 1.5s, always on a phase change) and calls setStatus, which reaches us as an extension_ui_request. Either one wakes the server, which re-reads the session with a cursor (get_entries?since=). One event in, at most one cheap read out — and a browser that reloads mid-round rebuilds the same picture from the entries alone.

Proof that the judge saw the picture

A judge that never received an image used to score a visual round anyway — the visible symptom was reasons: "Image unavailable" next to a 5–5 "tie". Two mechanisms make that impossible to miss:

  • Declared capability is checked first. A judge (or panel member) whose model does not claim image input is refused before an attempt runs, with the setting to change. No model, no round.
  • Every judged screenshot carries a code in its top-right corner. The judge must read it back ("tokens": {"<label>": "<code>"}); one edit apart counts as a misread, anything else does not count at all. A candidate whose code was not read back is unseen: it gets no score, the round is marked inconclusive (never "even"), and a question is raised — retry, switch the judge, or keep the current best.

The code lives in the pixels only, so it cannot be echoed from the prompt. lib/shot.ts does the stamping (stamp, canaryImage), lib/judge.ts the comparison (sameCode), and probeVision() can settle a model's capability offline — not inside a tool call, where creating a session waits on the runtime the turn holds.

Live: one server, on demand

A screenshot is a still of an app that already runs — lib/shot.ts starts serve inside an attempt's worktree to take it. Run it keeps that same server alive for the winner instead: one slot, started when you ask, killed when you stop it, after ten idle minutes, or when this process exits. It is offered only when the repo has serve in .pi/loops.json. The iframe gets an opaque origin (sandbox, no allow-same-origin), because the code inside it was written by an attempt.

Judging happens in a child process

An in-session prompt(text, { images }) does not deliver the picture on this Pi — the message carries the block, the provider request does not — so a judge that has to look at screenshots is asked through pi --print (the same binary, model and auth, with @file arguments, which do carry images). Two plumbing rules matter there and both cost a bug each: close stdin (ignore) or --print waits for EOF forever, and parse stdout only (pi warns on stderr).

The checkout is not the attempts' to touch

An attempt has bash and inherits the repo's absolute path, so it can edit or commit into your checkout instead of its worktree. Every round therefore checks, after the attempts: did HEAD move, did the working tree gain changes, while every attempt's own worktree stayed at the base commit? If so the round is inconclusive (nothing of theirs exists to compare), your tree is put back, and the card says what happened — the loop's promise is that your checkout is untouched until loop_apply.

Cleanup

Worktrees are scratch copies: the commit lives in the repo and on the branch loops/<run>/rN-<style>, so removing a worktree loses only what the attempt left uncommitted. clean up (or /loop clean [run|all]) removes the attempts' worktrees, drops the losing branches and runs git worktree prune, keeping the winner's worktree, the durable session snapshot, the JSON index, the sessions and the archived screenshots. loop_apply does it for the losers as soon as a winner lands.

Settings that change under you

.pi/loops.json decides which models judge, what the checks are and how much runs. The server watches it and the app shows what changed (models judge exe/… → particle/…) while it is open — an agent editing config is a real event, not a detail.

Actions

The buttons talk to the engine, not to a model: /loop take, /loop apply, /loop stop, /loop rule, /loop talk, /loop answer. Anything that needs judgement ("two more rounds", "steer it this way") is sent to the agent as a message, because that is a decision the agent owns.

Stop response aborts the current chat's parent turn. Stop loop cancels only the named background run, even while the parent is busy; its worktrees stay. Controls appear only while their target is active, beside the composer, loop activity, or inspector (including mobile). Each target keeps one persistent controller as its button moves between views, preserving pending state and errors through resizing and inspector navigation. They guard duplicate requests and show errors. Acknowledging a button does not mark work stopped: streamed session state must confirm it. Missing signals, incomplete judging and budget/timeout failures are not labeled Done. All run actions keep the exact selected run ID; attempt selection sends its durable name, not its A/B display letter.

Invalid watched .pi/loops.json no longer terminates the server. The rejected text is saved beside the config, the last accepted settings are restored when possible, and a workspace-scoped error explains how to recover.

Parity

npm run shots writes ./shots/ at 1440, 1024 and 430 wide. The pass condition for this port was the screens the POC at ~/factory/mock shows, at the same density; see PARITY.md for the state-by-state comparison.

Development

npm run dev      # Hono on :8001 proxying the SPA to Vite on :5173
npm run check    # typecheck the server and the client separately
npm test         # offline engine/API tests, including workspace restart and routing regressions
npm run test:browser # build first; real Chromium workspace navigation, with a fake Pi (no model calls)
npm run shots    # headless-chrome screenshots of every state

版本与发布记录

@computesdk/loops 共发布 21 个版本,首次发布于 2026/11/03;过去 12 个月发布了 21 个版本。

版本发布日期距今
0.1.19latest2026/11/046 天前
0.1.182026/11/046 天前
0.1.172026/11/046 天前
0.1.162026/11/046 天前
0.1.152026/11/046 天前
0.1.142026/11/046 天前

关于 @computesdk/loops 的常见问题

@computesdk/loops 是什么?

Loops for people who do not live in a terminal: one command starts a localhost UI that runs, watches and steers hill-climbing agents in your repos.

如何安装 @computesdk/loops?

在项目目录运行 npm install @computesdk/loops,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 loops、pi-loops-ui,可以用 npx loops 直接运行。

@computesdk/loops 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/computesdk__loops 是否存在。

@computesdk/loops 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@computesdk/loops 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@computesdk/loops 可以商用吗?

@computesdk/loops 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@computesdk/loops 的下载量有多少?

根据 npm 官方下载统计,@computesdk/loops 在 2026/10/11 – 2026/11/09 这 30 天内被下载 3,987 次,日均约 133 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@computesdk/loops 还在维护吗?

最新版本 0.1.19 发布于 2026/11/04(6 天前);至今共发布 21 个版本,其中过去 12 个月发布了 21 个;首次发布于 2026/11/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。