Talk in code (cocode) new
Type in any language, even one that doesn't exist. Claude predicts your next lines, prepares the real program while you type so Run is instant, and reads what you send as a message written in code. cocode lives in the store: it is not a default.
What it is
You type in any language: a real one, one you made up, or plain pseudo-code. The window never judges it. Three things happen around what you type:
- Prediction. Claude reads each settled change and offers the next lines as a ghost: dim, italic, slightly blurred text at the caret. It is not in the document until you press Tab. When Claude is away, a small local predictor offers weaker ghosts, so the page still feels alive.
- The build. While you type, Claude prepares the real program (Python, JavaScript, …) for the current revision and puts it in the Build pane. Run then executes that build at once instead of starting from zero, and the output streams into the Output pane.
- Talking in code. Ask and Send to Claude carry the code as the message. Claude works out what you mean with it (a question, something to explain, something to do, a bug, a note) and answers in that register.
An empty editor shows seeds: starting points Claude draws from memory and your saved sessions. cocode stores nothing on the host. The document lives in the window, and in a saved session after Send to Claude.
Install
cocode is not in the engine's defaults.json. It lives in lets-store, so you install it once:
/lets store cocode # inspect → confirm
/lets cocode # open it
Claude shows you the inspection (permissions, problems, the verified flag) and copies it into ~/.lets/uniques/cocode only after you say yes. Manual: clone the repo into ~/.lets/uniques/cocode.
cocode needs engine 0.4.0 or later ("engine": ">=0.4.0") for handoff sessions; it needs no new engine endpoint. It asks for no write or network permission: "fs.write": [], "net": []. The window writes nothing itself. Claude runs your program with its own tools (below), except where the engine's local runner can run it for you: new in 0.2.0 cocode also declares permissions.exec, the languages it may ask the engine to run, and the runner is new in lets 0.7.0 (Run on your machine).
This page describes cocode 0.1.0 (2026-10-13), plus the local runner marked new in 0.2.0 and reach marked new in 0.3.0 (2026-10-15). Like the other lets repositories, its repository is private for now.
What you can do
A toolbar sits on top. Below it, the editor on the left and a rail on the right with two panes, Build and Output.
| Part | You see | You can do |
|---|---|---|
| toolbar | The status dot, "cocode", the language chip, Suggest, Run, Ask, the intent select and Send to Claude; at the right end Undo, Redo, Build, Output, chat, Doto and ? | Click the language chip to type a language name: Enter sets it, Esc cancels, an empty name goes back to the detected one. |
| editor | A gutter with line numbers and note markers, your code with syntax colours, the current line tinted. Monospace 13 px, no wrapping (long lines scroll sideways), tab size 4. | Type in any language. The chip detects python, javascript, typescript, go, rust, bash, sql, c, cpp, java, ruby, html, css and json, and shows pseudo when nothing fits. A name you type works too: 1 to 24 characters, lowercase, a-z0-9+#.-_ (my-lang). |
| ghost | The next lines Claude predicts: the first one inline after the caret, the rest in a small floating block under the caret line, in cyan, italic, slightly blurred and a little tilted. The status line says whose it is. | Tab accepts it, Ctrl+→ takes one word, Esc dismisses it, Alt+] / Alt+[ cycle the alternatives. Suggest (Alt+\) asks for one now. |
| Build pane | The real program for one revision: its language, rev and status (building, ready, failed, or stale · rev N once you typed past it), the code with the same highlighting, and Claude's notes under it | Pick a target (auto, python, javascript, typescript, bash, go, rust, or type a name), press Prepare to ask for a build, Copy the code. |
| Output pane | Runs, newest first: id, language, rev, duration, an exit pill, then stdout and stderr (stderr in a dim red). Under them, Notes: every note Claude pinned to a line. | Read. The window keeps the last 10 runs. new in 0.2.0 A badge lists the languages the window can run itself on this machine (Run on your machine). |
| Ask | A one-line form under the toolbar, with a chip naming what the question is about (line 3, lines 2–7) | Ctrl+K opens it, Enter asks. Or type a line that starts with ?? and press Enter: it asks without the form. The answer comes in the chat. |
| Send to Claude | The intent select next to the button: you decide (the default), a question, explain this, do this, fix this, review this, just a note | Pick an intent or leave it to Claude, then press the button (Ctrl+Shift+Enter). The window closes and Claude answers in the Claude Code chat. |
The status line under the editor reads like this: Ln 3, Col 16 · 12 lines · rev 12 · ghost: claude · build: ready rev 11. The ghost part says claude or local; the build part says ready rev N, stale rev N, building or failed. Below 1100 px the rail becomes a bottom drawer with Build | Output tabs; Ctrl+B and Ctrl+J toggle each pane.
An empty page. An empty editor says "Type in any language. Even one that doesn't exist." and shows a start from list: Claude's seeds first, then your recent code from this browser. Each one is a button with a label, a language badge and a two-line preview; picking one fills the editor. Esc or typing hides the list. When the window opens empty, Claude sends two to four seeds drawn from your saved sessions ("Continue user welcome script"), Doto's special skills and what the project is about, and Doto says "Empty page. Start anywhere."
What stays in the browser. Your text, language and pane state are kept in sessionStorage for this tab, so a reload keeps the code. localStorage keeps preferences (rail open or closed, which panes are open, the drawer tab, Tab moves focus) and your last 10 recents, recorded on Run and on Send. Nothing else is stored. Without a host, the editor still works locally; Run, Ask and Send are off.
Doto on this page. He sits by the caret. Click him for Finish this, Explain this line (or Explain the selection), Run it, Show the real code and Open chat. He reacts once per thing you do, at most: a wink when you accept a ghost, a nod when a build lands ready, a cheer when a run exits 0 and when you send to Claude, an oops when a run fails, and a sparkle when you accept three ghosts in a row.
Mouse & keyboard
With the editor focused, unless the row says otherwise:
| Key | Does |
|---|---|
| Tab | Accept the ghost; otherwise insert two spaces (or indent the selected lines) |
| Shift+Tab | Outdent |
| Ctrl+→ | Accept the ghost's first word |
| Alt+] / Alt+[ | Next / previous alternative |
| Esc | Dismiss the ghost; then close the innermost open thing (ask form, note, help, empty state) |
| Alt+\ | Ask for a ghost now (Suggest) |
| Enter | New line with the current indentation, one level deeper after a line ending in :, { or (; on a ?? line it also asks Claude |
| F5 / Ctrl+Enter | Build & run 0.4.0 (F5 works anywhere in the window and never reloads the page) |
| Shift+F5 | Rebuild with Claude first, then run 0.4.0 |
| Ctrl+K | Ask |
| Ctrl+Shift+Enter | Send to Claude |
| Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z | Undo / redo: cocode's own stack, 200 steps, typing within 600 ms counts as one step |
| Ctrl+D | Duplicate the line (or the selection) |
| Alt+↑ / Alt+↓ | Move the line (or the selection) |
| Ctrl+; | Comment the line with the language's marker: # for python, bash and pseudo, // otherwise |
| Ctrl+/ | The shell's chat, as in every unique |
| Ctrl+B / Ctrl+J | Toggle the Build / Output pane |
| Ctrl+M | Toggle Tab moves focus: the editor stops trapping Tab, so the keyboard can leave it (the status line says so) |
| ? (outside the editor) | The shortcuts overlay |
| 1 / 2 (outside the editor) | Focus the editor / the Output pane |
Every control is reachable with Tab in page order and has an accessible name. The code area is labelled "Code", and a screen reader hears each ghost once, politely ("Suggestion: …"). Under reduced motion, jumping to a line is instant.
The ghost
The prediction loop, step by step:
- You type. After 500 ms of quiet, the window sends the settled text to Claude (
cocode.changed), with the caret line so far (prefix). - Meanwhile, after 250 ms, the local predictor may already show a weak ghost, labelled local in the status line.
- Claude answers with one ghost for that revision (
cocode.suggest): the next one to four lines, in your dialect. It replaces the local one. If the caret is mid-line, the ghost starts with the rest of that line. Two or three alternatives come along when the fork is real (elseorreturn). - Typing the characters the ghost starts with shrinks it instead of dismissing it. Any other key dismisses it, and so do moving the caret and Esc. Tab accepts all of it (the first line's indentation follows your current line); Ctrl+→ accepts one word.
There is never more than one ghost on screen, and the window shows at most 12 lines of it. A ghost Claude made for an older revision shows only while the text before the caret still matches what you had typed then; otherwise it is dropped without a word. An accepted ghost is your change: rev goes up and Claude sees it like anything you typed.
When Claude is away
The local predictor (ui/predict.js) keeps the page alive with weaker ghosts of at most two lines. It is synchronous and fast (under 2 ms for 64 KB), and the first rule that fits wins:
| Rule | When | Offers |
|---|---|---|
| Closers | A (, [ or { on the caret line is not closed | The missing closers, plus : after a Python def, if or for line head |
| Pairs | The previous block opened with if, try, for or while (python, pseudo) and the caret is on a fresh line at the block's indentation | else:, except Exception as e:, or nothing for loops. JS-like code gets } else { and } catch (e) {. |
| Sequences | Two or more previous lines differ only in a number or a letter (step 1, step 2) | The next one |
| Echo | The caret line (3 characters or more) is the start of an earlier, longer line | The rest of that line |
| Repeat shape | The two previous lines start with the same bullet marker (-, *, •, >) or with consecutive numbers (1., 2.) | That marker, or the next number |
Accept, dismiss, and what Claude learns
Every accept and dismiss goes to Claude as feedback: cocode.ghost.accept (with partial: true for one word) and cocode.ghost.reject with the reason (esc, typed-away, moved). Claude reads them plainly. Three rejects in a row mean shorter, more literal ghosts. A partial accept means the first word was right. A dismissed ghost is a "no", so Claude does not send it again for the same revision. Once per session at most, Claude writes a style note into Doto's memory, such as "writes python-ish pseudo-code, 2-space indents, accepts short ghosts". The window counts accepts and rejects in stats.
Build ahead & Run
While you type, Claude keeps a real, runnable version of your code in the Build pane (cocode.build). The language is your target if you picked one in the Build pane, or the chip's language if it names a real one; pseudo-code becomes Python unless your dialect is clearly JS-like. The build is self-contained: sample data where input is implied, prints for what the program computes, nothing interactive, no network, no writes outside the working folder.
Claude waits until the purpose is clear and the text is more than a line or two, and while you type fast it builds every few revisions. The pane shows the language, the rev and the status: a spinner while building, the reason when it failed. When you type past it, the status reads stale · rev N in dim text, and the code stays until a new build arrives.
0.4.0 Build & run is one button (F5, Ctrl+Enter, Doto's menu) for both halves: a ready build in a language your machine runs executes at once (below); a build Claude is still writing in such a language is waited for and run the moment it lands, with no second round trip; anything else goes through Claude, who builds first when the build is not fresh (the run entry says Claude builds for rev N first, then runs.). Shift+F5 asks Claude for a fresh build before the run (cocode.run {rebuild: true}). The button's icon is the wheel: the play triangle when idle, a spinning ring while building or running (a slow amber one while a run waits for Claude because Doto is away), ✓ when the last run of this text ended with exit 0, ✗ when it failed, ? when nothing answered for 60 seconds; an edit puts it back to idle, and the tab title carries the same mark so a hidden pane still tells.
When the window cannot run the build itself, it opens a new entry at the top of the Output pane and sends cocode.run with the latest build attached, stale or not. Claude:
- runs that build if it matches the current revision; otherwise it builds for this revision first (you see a dim "building first…" line), then runs;
- writes the file into its scratch folder and runs it with its own tools: 30 s timeout, no network, no sudo, no deletes. If the program would touch files outside that folder or needs secrets, Claude stops and asks one short question in the chat;
- streams stdout and stderr into the entry, and always closes it with the exit code and the time it took;
- says one Doto line: "Ran it. Exit 0, 3 lines." or "Failed on line 4. See Output." If it failed because your intent was unclear, Claude says what it assumed and offers the fix as a ghost or a note on the line, not a rewrite.
If the host is not connected, Run shows a toast and does nothing. If Claude is away, the entry still opens and a toast says "Doto is away. The run starts with your next message in Claude Code." (at most once every 30 seconds). A run with no output for 60 seconds shows no answer yet; output that arrives later puts it back to running, and done: true closes it as usual.
Run on your machine new in 0.2.0
With lets 0.7.0 or later, Run does not have to wait for Claude. The first time you press it, a consent card in the window says what will happen: the build you can see in the Build pane runs on this machine, as you, in a throwaway folder, with a 30 second limit and a 1 MB output cap. Nothing runs until you allow it with the button on the card. The answer is kept for cocode on this machine. A badge in the Output pane lists the languages this machine can run for cocode and how each one is isolated, and the output streams into the run entry within milliseconds.
| Language | Isolation | What it stops |
|---|---|---|
python | An audit hook, installed before your program runs and impossible to remove | Files outside the job folder (reading Python itself is allowed), every socket operation, subprocesses, ctypes and a few risky imports. A blocked call raises RuntimeError: blocked by the lets sandbox in your program. |
javascript, typescript | Node's permission model (Node 20 or later; TypeScript needs Node 22.6 or later) | Files outside the job folder, child processes, worker threads and native addons. The network is not blocked. |
This is not a VM. Time and output are capped, CPU and memory are not, and a JavaScript or TypeScript program can still reach the network. Languages the engine does not run (bash, go, rust and the rest) still go through Claude, as described above. When the window ran the build itself, Claude is told the result through the cocode.ran event (below). To switch the local runner off for every unique, put "runner": {"enabled": false} in ~/.lets/config.json. The full contract is in docs/RUNNER.md.
Reach: what leaves the sandbox new in 0.3.0
A sandbox can print and compute. It cannot ping a host, open your browser, search the web, start a research agent or run a shell command, and before 0.3.0 a line like ping('google.com') quietly became a build with a ping function that printed what a ping would have said. Such a line is now a reach, and Claude does it for real.
Write it any way you like, in any language: a bare verb (ping('google.com'), dns(h), fetch(url), curl(u), browser(u), screenshot(u), search(q), research(t), agent(task), hermes(task), shell(cmd), say(text), ask(q)), the claude namespace with any verb (claude.browser('https://example.com'), claude_research('…'), claude.whatever(1)), pseudo-code without parentheses (ping google.com, browse https://…, research the history of morse code), a shell line ($ dir), or the real library the sandbox would block (Python requests, socket, subprocess, os.system, webbrowser; JavaScript child_process, puppeteer). Not a reach: a comment, a ?? line, a function you defined yourself (def ping(host): makes ping(…) yours), a method of the same name (re.exec), and what the language's own sandbox does (fetch in JavaScript runs locally, since Node's permission model allows the network).
0.3.1 Any spelling works: the parenless form takes any separator, bracket or case (ping: google.com, PING -> google.com, (ping "google.com")), natural phrases count (go to https://…, open google.com, search the web for …, find out who maintains lodash, spawn an agent to …, shell: ls, notify me when done), and Claude addressed by name takes the next word as the verb (claude, ping google.com, ask claude to research …). The window's scan is a hint: an invented language can phrase a reach in words no regex knows, so Claude also reads the program for reaches himself and treats one he finds like a listed one. The build goes on as before: Claude still works out the real program in the Build pane, whatever your dialect, writing each reach as claude.<verb>(…).
The window marks each reach with a small cyan diamond in the gutter; hover it for "ping google.com — reaches the network. Claude does it for real when you Run." The status line counts them. Run with any reach present skips the local runner whatever the language: the run entry gets a reach · N badge and cocode.run carries reach: [{line, kind, name, args, raw}]. Claude is then the interpreter: he walks the program in order, performs each reach with the matching tool (the real ping, the app's browser pane with the tab left open for you, a web search, a sub-agent or the deep-research skill, the local Hermes agent, Bash, the chat), names each one in cyan as he goes (the new reach output stream: → ping google.com), and streams the real output. The pure parts still run in the sandbox. A reach that would change the world (send, post, delete, pay) waits for your yes in chat; probing someone else's security is refused; Claude Code's own permission prompts stand for every tool he calls. The scanner is a heuristic: a reach it misses in Python ends with RuntimeError: blocked by the lets sandbox, and Claude reads that as a reach and performs the program for real.
Talking in code
In cocode, the code is the message. There are three ways to talk:
- Ask (Ctrl+K, or a question from Doto's menu): a one-line question about the current line or the selection. Claude answers in the chat in two to eight lines, highlights the lines it talks about and clears the glow when done. If the question asks for a change, Claude makes it with an insert or a replace, then says one line.
- A
??line: type?? why is this slowin your code and press Enter. It goes to Claude as a question, like asking the person next to you. The line stays in the document as your note; Claude answers in the chat and pins a one-line reply to that line. - Send to Claude: the whole document as a handoff (next section).
The six intents
The intent select next to Send to Claude says how to read the code. Leave it on you decide and Claude reads the code as a message: ?? lines and comments are questions or asides; imperative lines ("deploy this", "make it faster") and TODOs are a task; a stack trace, a failing run or a fix line is a bug; a complete program is a review, or "does this do what I think?"; prose in code clothes is a note. Claude says in one line which reading it chose, then answers in that register:
| Intent | In the select | Claude answers with |
|---|---|---|
auto | you decide (default) | One of the six below, named in one line |
ask | a question | The answer |
explain | explain this | A walkthrough |
do | do this | The work done: the real program written and run, and what happened |
fix | fix this | The cause and the fix |
review | review this | Gaps, risks and three concrete changes |
note | just a note | A short acknowledgement and what Claude kept |
Send to Claude & resume
Answer in code new in 0.2.0 sits next to Send to Claude (also Ctrl+Shift+O and in Doto's menu). It is the same handoff with intent: "output" and answer: {lang}, the chip's language: Claude's whole answer comes back in the chat written as code in that language, and nothing else. Types, records, constants, functions and branches carry the content; comments appear only where syntax cannot say it; the code need not run. The same answer without a window: /lets cocode output [lang] <question>.
Send to Claude is a handoff. The window sends cocode.export: your code, its language, the caret and selection, the latest build, the last three runs (output tails of 2,000 characters), the notes and the ghost stats. The engine saves it as a session and closes the window, with a cheer from Doto. Claude answers in the Claude Code chat, not in the window.
The window names the session after your first non-empty line and the language, such as for each user in users · pseudo; Claude then labels it in three to six words. Say continue (or /lets cocode resume) and the window comes back with cocode.load: your text, language, build, runs, notes and caret, with no ghost or accept events. Claude carries on with ghosts and builds. If the tab also kept a draft, the load replaces it.
The manifest's session block names the roles:
"session": { "export": "cocode.export", "snapshot": "cocode.snapshot", "restore": "cocode.load" }
Events (up)
Each event says what you meant. Lines are 1-based, columns 0-based. In every event, text is at most 64,000 characters; longer text keeps the tail and carries truncated: true.
| Event | When |
|---|---|
cocode.changed {rev, text, lang, detected, cursor, selection, lines, prefix, by, delta?} | The text settled, 500 ms after the last keystroke: you typed, pasted, accepted a ghost or undid, or a command from Claude changed the text. by is "human", "claude" or "restore". prefix is the caret line up to the caret (at most 200 characters). delta {from, to, text} is the changed line range when known. |
cocode.ghost.accept {rev, source, text, partial} | You pressed Tab (the whole ghost) or Ctrl+→ (partial: true, one word). source is "claude" or "local". |
cocode.ghost.reject {rev, source, reason} | You pressed Esc ("esc"), typed something else ("typed-away") or moved the caret off the anchor ("moved"). |
cocode.ghost.request {rev, text, lang, cursor, prefix} | You pressed Suggest (Alt+\) or picked it from Doto's menu: you want a ghost now. |
cocode.build.request {rev, text, lang, target} | You picked a target language in the Build pane or pressed Prepare. target is "auto" or a language name. |
cocode.run {run, rev, text, lang, build?, reach?, rebuild?} | You pressed Build & run (F5, Ctrl+Enter) and the window could not run it itself, or Shift+F5 (rebuild: true: Claude builds anew first). run is the window's id for this run (r_<rev>_<n>). build {rev, lang, code, status} is the latest build if any, stale or not. 0.3.0 reach [{line, kind, name, args, raw}] lists the lines that reach outside the sandbox (Reach); Claude performs each one for real. |
cocode.ran {run, rev, buildRev, lang, sandbox, exit, ms, timedOut, killed, stdout, stderr} 0.2.0 | The window ran the build itself (Run on your machine). buildRev is the rev of the build that ran, sandbox names the isolation used, killed is null, "timeout" or "output", and exit is null when the process was killed. |
cocode.ask {question, rev, text, lang, line, selection?, source} | You pressed Ask (Ctrl+K, "toolbar"), ended a ?? line with Enter ("inline": question is the line without the ??), or picked a question from Doto's menu ("menu"). |
cocode.export {label, intent, text, lang, rev, cursor, selection?, build?, runs?, annotations?, stats} | You pressed Send to Claude: the handoff. intent is auto or what you picked: ask, explain, do, fix, review, note. runs holds at most the last 3, with output tails of 2,000 characters. label is the first non-empty line (up to 60 characters) plus the language. |
cocode.snapshot | Nothing you did: it is the reply to Claude's cocode.snapshot command (re is the command's id), the export payload without intent. |
Build status is building, ready or failed; stale is worked out by the pane (build.rev < rev) and never sent. Run status is running, done, failed or lost (no output for 60 s). rev starts at 1 on an empty editor and never goes down; a load bumps it like any change.
Commands (down)
Commands are idempotent and animate (300 ms at most, transform and opacity only); nothing teleports. Every command accepts "ack": true on the envelope and gets lip.ack {ok, error?} back. A bad command without ack gets lip.error {code: "bad_request", message, re}. Line numbers past the end are clamped to the document, except in cocode.annotate, where a line past the end is an error.
| Command | Does |
|---|---|
cocode.suggest {rev, text, at?, alternatives?, reason?, seq?, done?} | Show text as the ghost at the caret, or at at {line, col}. At most 12 lines; longer text is cut. Chunks with the same re or rev and a seq append; done: true ends the stream. A ghost for an older rev shows only while the text before the caret still matches the prefix that rev reported; otherwise it is dropped silently, with no reject event. text: "" clears the ghost. Alt+] / Alt+[ cycle the alternatives. |
cocode.seed {items: [{id, label, text, lang?, hint?}]} | Replace the start from list of an empty editor (up to 6 items). Picking one inserts text, sets lang if given, and counts as your change. Ignored while the editor has text, and kept for the next time it is empty. |
cocode.build {rev, lang, code, status?, file?, notes?, seq?, done?} | Put the real program in the Build pane, replacing any earlier build. status defaults to ready; building shows a spinner and may carry no code; failed shows notes as the reason. A message with seq for the same rev appends its code; one without seq, or for another rev, replaces the build. A build restored on resume is re-based on the new window's rev. |
cocode.output {run, stream, text, seq?, done?, exit?, ms?} | Append to the run named by run. stream is "stdout", "stderr", "system" (dim lines such as "building first…") or, 0.3.0, "reach" (cyan lines naming each reach as Claude performs it: → ping google.com). done: true closes the run: exit 0 shows ok, anything else failed, and ms is shown when given. Chunks are appended in arrival order; seq is informational. A chunk updates only its own run entry, so the pane keeps its scroll. A run id the window never issued creates an entry named by Claude. |
cocode.insert {at, text} | Insert text at {line, col}, "cursor" or "end". The new lines glow for 600 ms. Undoable; rev goes up and cocode.changed {by: "claude"} follows. |
cocode.replace {from, to?, text} | Replace lines from to to (1-based, inclusive; to defaults to from) with text. Animated and undoable. |
cocode.set {text, lang?} | Replace the whole document, with a fade. Undoable, but your text is gone from view, so Claude asks first. |
cocode.lang {lang} | Set the language chip (detected becomes false). |
cocode.annotate {line, text, kind?} | A marker in the gutter and a floating note that opens on hover, focus or click; it is also listed in the Output pane under Notes. kind is note, ok, warn or error. text: "" removes that line's note; line: 0 clears them all. |
cocode.highlight {from, to?, color?, ttl?} | Glow lines from to to. ttl defaults to 4000 ms, 0 keeps the glow, from: 0 clears every glow. |
cocode.focus {line, col?} | Scroll the line into view and put the caret there (smooth; instant under reduced motion). |
cocode.load {text, rev?, lang?, detected?, build?, runs?, annotations?, cursor?} | The restore command: render a session's export without ghost or accept events, bump rev, and emit one cocode.changed {by: "restore"}. A load replaces the tab's draft. |
cocode.snapshot {} | Reply with a cocode.snapshot event. |
A round trip, as the guide describes it: you type → cocode.changed {by: "human"} → cocode.suggest {rev, text} → you press Tab → cocode.ghost.accept → cocode.changed for the new revision.
// a ghost for rev 12, with one alternative
{"type":"cocode.suggest","data":{"rev":12,"text":" send welcome\n else:\n skip user","alternatives":[" send welcome"]}}
// the real program, ready before you press Run
{"type":"cocode.build","data":{"rev":12,"lang":"python","status":"ready","file":"welcome.py","code":"users = [...]\nfor user in users:\n ..."}}
// stream a run, then close it
{"type":"cocode.output","data":{"run":"r_12_1","stream":"stdout","text":"welcome, ada\n","seq":1}}
{"type":"cocode.output","data":{"run":"r_12_1","stream":"stdout","text":"","done":true,"exit":0,"ms":410}}
// pin a note to line 3
{"type":"cocode.annotate","data":{"line":3,"kind":"warn","text":"users is never filled; add sample data?"}}
Claude's etiquette
cocode's GUIDE.md is what Claude reads when it opens the page. Its promises, in short:
- The ghost is an offer. Claude never inserts what you did not accept, and a dismissed ghost is not sent again for the same revision.
- Your language wins. Ghosts follow your indentation, your invented keywords, your casing, your spelling of
print. Pseudo-code stays pseudo-code in the editor; the real program belongs in the Build pane. - Ask before replacing everything.
cocode.setneeds a yes in the chat. Claude prefers insert and replace: your text is the conversation. - An undo is a "no". Ctrl+Z reverts Claude's insert too, and Claude takes it that way.
- Quiet while you type. No chat line for a keystroke: the ghost is the answer. Claude stays silent on changes it made itself and on a restore.
- One line per thing that happened. Short, verb first, ending on a dot; here Doto counts and names: "Built it. Python, 14 lines." He speaks on Run, Ask, a ready build or a seed.
thinkingwhile Claude builds or runs,proudon a clean exit,curiouswhen it asks.
Repository layout
lets-unique-cocode/
unique.json # manifest: domain cocode, engine >=0.4.0, no fs or net permissions (exec for the local runner)
GUIDE.md # what Claude reads on open
CONTRACT.md # the unique's behaviour spec
README.md CHANGELOG.md LICENSE icon.svg
package.json # npm run validate
scripts/validate.mjs # zero-dependency manifest check
schemas/ # unique.schema.json and message schemas
.github/workflows/validate.yml
ui/index.html # the page
ui/cocode.js # the window
ui/editor.js # textarea, highlight layer, ghost, undo
ui/syntax.js # tokenizer and language detection
ui/predict.js # the local predictor
ui/style.css
Plain HTML, ES modules and CSS: no build step, no CDN, no inline script, no eval. MIT licensed. More: lets-unique-cocode has the full GUIDE.md, the behaviour contract (CONTRACT.md) and JSON Schemas for the messages.