Brainstorm tasks interactively — cyberine chat
cyberine run needs a task up front; cyberine chat is the interactive way to arrive at one. On a real TTY
it opens a full-screen Ink TUI (a welcome banner with the model/provider/cwd + command panel, a
windowed conversation transcript that streams the reply live with a spinner, a rounded-border
input box, and a dim status line) where you converse with a single planner model to brainstorm and
sharpen a concrete, well-scoped task. When piped/redirected (non-TTY) it falls back to a plain
readline REPL so scripts still work. When the model proposes a ready task it ends its reply with a
TASK: line, which becomes the pending task (shown in the status line).
Planner model is provider-agnostic. No provider is a privileged fallback. On first run with
several providers authed, cyberine chat shows a picker (provider, then model from the local catalog, or
a custom id) and persists your choice as CYBERINE_CHAT_PROVIDER / CYBERINE_CHAT_MODEL in ~/.cyberine/.env
— so next time it just uses it (like OpenCode remembering your model). With one provider authed it
uses that; with none it points you to cyberine setup. Re-pick anytime via cyberine setup. Only
OpenAI-wire providers (deepseek / moonshot / xiaomi / openrouter) can be the planner; Anthropic's
native API is reserved for the worker/judge path.
The planner can also dispatch the run itself: it carries a real dispatch_run tool, so when the
task is concrete and you clearly want it executed ("run it", "go ahead", "lam luon di"), the model
calls the tool instead of asking again — the chat launches the background run, prints
auto-dispatched <runId>, and the model confirms in its next breath. No /run needed (it still
works as the manual path). Every run dispatched from the chat — auto or /run — is then watched:
when it finishes (done / error / cancelled) the chat posts a structured report (per-step state,
winning worker, judge rationale, an output excerpt, and the run dir) and the model narrates the
outcome in plain language with a suggested next step. The report turn waits politely: it never
interrupts a streaming reply or queued prompts, and goes out within a second of the chat going idle.
This dispatch<->chat linkage is persisted on the session record (dispatchedRuns, bounded to
the 50 most recent), not just held in memory — so a /resume <id> or a fresh --continue after
the process restarted still watches its own in-flight runs and reports on them, instead of losing
attribution the moment the chat process exits.
The session record also persists checkpoints (bounded to the 50 most recent) — one per user
turn, each carrying its message index, the runs it dispatched, and any shadow-git file refs those
runs' workers left behind. This is what /rewind (below) reads; it survives /resume the same
way dispatchedRuns does.
The planner also carries a dispatch_tasks tool for splitting a set of distinct tasks across the fleet. When you give the model a list of independent tasks and ask it to distribute them ("chia tasks cho mate", "fan out these tasks", "run all of these in parallel"), the model calls dispatch_tasks and each task launches as a SEPARATE background run IN PARALLEL — division of labor, not a compete. This is distinct from dispatch_run, which races every worker on the SAME single task for quality. The chat prints a confirmation line per task (dispatched <runId>: <task>) and a final dispatched N of N tasks summary. All dispatched runs are watched and report back the same way as /run runs.
Example: paste a list of tasks into the chat and say "chia het cho fleet chay" (split all to the fleet) — the planner calls dispatch_tasks with each item, prints a run id per task, then watches them all.
Commands:
/run [task]— dispatch to the whole fleet as a background run (uses the inline text, or the lastTASK:the model proposed if you give none). Prints the runId; watch it incyberine agents— or in the chat's own running-agents footer (below). The dispatched run can BOTH plan and BUILD (edit files) by default — the interactive launcher runs OpenCode'sbuildagent, so you never passOPENCODE_AGENT=buildby hand (setOPENCODE_AGENT=planto force read-only)./steer [<runId>] <text>— inject operator guidance into a running dispatch. For loop workers (DeepSeek, OpenRouter, etc.) the steer lands between tool rounds so the model can change course in the same step; for CLI workers (OpenCode etc.) it applies at the next step boundary.runIdis optional — omitted targets the most recently dispatched run. Acks withsteered run <id>: <text>. See "Steering a running loop worker" below for the full contract and CLI-worker limitation./agents— list recent runs across all projects./bg [kill|tail <id>]— list, kill, or tail the background work spawned FROM THIS CHAT: every detached run dispatched via/run//dispatch/dispatch_run/dispatch_tasks(shown by its 8-char runId prefix), plus every!&<cmd>background shell (shown by itssh-xxxxxxxxid). Unlike/agents(global history across every project)/bgis scoped to this session, and unlike the completion-report linkage (dropped the instant a run finishes)/bg's history is append-only for the life of the session, so finished items stay listed too — each line shows a state glyph, elapsed time, and a task/command preview./bg kill <id>stops a still-running dispatched run (reaps it instead when the owning engine process is gone) or sendsSIGTERMto a!&shell's pid; killing an already-finished item just reports its final state./bg tail <id>prints the last ~20 lines: for a run, the currently-active worker's live output (falling back to its captured final output once done); for a shell, its captured log file. With no args and nothing to show,/bgexplains how to start background work (/run <task>or!&<cmd>)./team(T564 outbound + T565 inbound) — the human-facing surface onto the cross-run team-presence layer (src/store/presence.ts+src/store/busTools.ts).cyberine chatjoins the roster on start (announces itself asrole=chat,capabilities=[chat], heartbeats everyDEFAULT_TTL_MS/2= 30s) and tombstones on/exit//quit/Ctrl+C, so it is discoverable and addressable from other runs, workers, and the MCPlist_teamtool for the lifetime of the session.-
/teamor/team list— print the live roster: handle, name, team, role, capabilities, and last-heartbeat age / TTL for every announced run, chat session, worker, and MCP server within its TTL (stale/tombstoned entries are excluded, same rule aslist_team). -
/team send <handle|name|role|team> <text>— resolve<target>against the live roster (an exacthandlematch first — unique, so exactly one addressee — thenname, thenrole, then falls back to ateam-<team>broadcast viasendToTeameven if no member currently matches) and publish<text>as anoteenvelope. -
/team join <team-name>— set this session'steamfield and re-announce immediately (does not wait for the next heartbeat), so/team listelsewhere reflects the new team right away. -
/team leave— clear this session'steamfield and re-announce. -
Session handles (k511d) — every presence record also carries a
handle: a unique, typeable label ([a-z0-9]then[a-z0-9_-], max 32 chars) minted automatically asadjective-nounand shown first on each/team listline. It is the string a teammate types to reach one specific session, and the fix fornamebeing self-reported and NOT unique (two sessions could claim one name and/team send <name>fanned out to both). Minting is deterministic in the session / run id (src/store/sessionHandle.ts), so a resumed session keeps its handle without persisting anything; uniqueness is a two-phase claim against the live roster (announceWithHandleinsrc/store/presence.ts) where the INCUMBENT — olderstartedAt, ties broken by address key — always keeps the handle and the newcomer takes<handle>-2,-3, ... A run's handle appears inlist_teamtoo, andsend_to_roleacceptshandleas a filter./handle— print this session's handle plus the exact/team sendline a teammate would use./handle <new>— re-claim a different handle (arbitrary text is normalized: lowercased, non-handle characters collapsed to-). This is NOT/rename:/renamesets the session TITLE shown in the sessions list,/handlechanges the address teammates type. A rename onto a handle a live peer holds yields a variant and says so, rather than shadowing the incumbent. Pin one at startup withCYBERINE_AGENT_HANDLE.
-
When
@opens the popup, and what accepting inserts (kwebs, measured 2026-09-17 against Claude Code 2.1.273 on a real terminal) — the rule is Claude Code's, case by case:@opens only at the start of the input or after whitespace.user@example.comandfoo@src/tuinever open it, even when the text after@would match a path.- A token that matches nothing renders nothing:
install @anthropic-ai/sdkis a trigger, but with no matching path there is no popup and Enter submits normally. A line-leading decorator such as@Componentopens it whenever something fuzzy-matches. - The quoted form
@"...opens it too and allows spaces up to the closing quote, so@"my dir/ficompletesmy dir/file.ts; a closed@"path"ends the token. Unquoted, a space ends the token. - Accepting a row from a quoted query inserts
@"path", as Claude Code does. One DELIBERATE difference: accepting a path that contains a space from an UNQUOTED query also inserts the quoted form. Claude Code inserts it bare, which its own mention extractor (and Cyberine's send-time expander, which mirrors it) then cuts at the first space. Regression guard:npm run selftest:at-mention-parity(the observed cases as a table, plus a round trip from the inserted text through the send-time expander). Known limitation: the observations are from one Claude Code version; a later release can change the rule, and the table names the version it was measured on.
-
@mention of a live session (ks1ay) — the chat input's@popup completes live sessions from the presence roster alongside file paths, in the same list. Sessions rank above files (there are at most 5 of them, capped so file rows keep room), are matched on handle AND name, and each row shows a dimkind · role · repodetail. Two deliberate rules:- Sessions appear only once at least one character follows the
@. A bare@stays a plain file browser, matching Claude Code (whose bare@lists paths) and keeping the common case free of a roster read — the roster is loaded lazily and cached for 2s. - A mention is a REFERENCE, not a send. Accepting a row with Tab/Enter only inserts
@<handle>into the buffer, and submitting a turn that mentions a session delivers NOTHING — exactly Claude Code's@semantics. At submit time the handle is resolved against the live roster and the handle→BusAddrbindings are injected as one synthetic system message, so the planner knows who was named and that nothing has been sent. Delivery is the planner's ownmessage_sessiontool call ({handle, text, topic?: note|steer}), which publishes a real envelope into that session's inbox withfromset to the mentioning session so a reply has an address. An unknown handle fails and lists the live handles — a handle only reaches a running, heartbeating process. Resolution logic lives insrc/chat/sessionMention.ts(UI-agnostic, so the fleet TUI and web composer can reuse it). Regression guard:npm run selftest:at-mention-session(pure popup-ordering checks plus a real roster + real bus read; the send-nothing invariant is asserted directly, no TTY).
- Sessions appear only once at least one character follows the
-
A duplicate handle fans out and is NAMED — Claude Code's ask-which-one is declined (kr3ac). Claude Code asks which session you meant when a typed name matches more than one, and its listing adds an identifier per row to disambiguate. Cyberine deliberately does the opposite on the send path, in both the chat REPL's
/team send, the planner'smessage_sessionand the MCPsend_to_role: it delivers to every live match and prints the full resolved id/name/role/team list back. That was T568/EV-003's anti-spoof design — a silent single-winner pick is exactly how a name-squatter intercepts a message, because the real session never learns it was addressed. Handles make the case stronger rather than weaker. A handle IS unique among cooperating processes (the two-phase claim yields to the incumbent), so more than one live holder is not an accident: it means a process wrote its presence record without claiming — a squatter, or a pre-handle writer. A "which one did you mean?" prompt would present that squatter as a legitimate choice; fan-out plus a named warning makes it visible instead. So a multi-holder handle send now carries an explicitWARNING: a handle is unique by construction, ...line in the chat REPL and awarningfield on themessage_session/send_to_roleresult, on top of the per-address list. A single-holder send carries no warning — it is an anomaly signal, not decoration. The other two paths that cannot ask anyone anything (an MCP client, a worker's in-loop send) are a second, independent reason the interactive prompt is not adopted. Regression guard:npm run selftest:at-mention-sessionsection (e) announces a squatter through the pre-handle path and asserts both holders resolve, the real session still receives the message, and the warning reaches the model;selftest:team-bus-hardeningstill passes unchanged. -
Where
@and the roster reach (kr7qq) — the four other human text inputs got a decision, not a copy of the feature, and each decision is recorded at its own input site in the source:- Fleet TUI
topens a live team roster overlay (RosterPanel), rendered through the same sanitized formatter as the chat REPL's/team list, so a handle read in one window can be confirmed in the other.tis remappable asfleet.rosterin~/.claude/keybindings.jsonlike any other fleet key; Esc ortcloses it, arrows scroll, and a failed roster READ is reported as such rather than as an empty roster. - Web dashboard chat composer:
@handlementions DO work — the server resolves them at submit time with the same reference-not-send contract — but only when session messaging is allowed on that bind: loopback, orCYBERINE_WEB_ALLOW_SESSION_MESSAGING=1. Publishing into another process's inbox (asteerespecially) is a local side effect, so it is gated exactly like the web file tools, in both the offered tool list and the executor. There is deliberately no client-side completion popup yet (the browser has no roster endpoint and the TUI popup is an Ink component; typing a handle you already know works today). - Fleet TUI compose bar, quick reply, steer input, and the web DISPATCH composer: no
@mention, deliberately. Their text becomes a WORKER's prompt or a run's steer, and neither a worker nor the engine has a roster, amessage_sessiontool, or file-mention expansion — an@handletyped there would travel as inert prose. Thetoverlay covers the "which handle exists?" need those inputs would otherwise have to answer. Regression guard:npm run selftest:mention-surfaces(renders the real roster panel headlessly, checks the action registry and the web gate, and CENSUSES the four recorded decisions so deleting one fails the build). - Known limitations: a handle addresses a LIVE, TTL-fresh process only — there is no
registry of past handles, so a session that exited (or missed its heartbeat window) cannot be
reached and its handle is free for the next claimant. Handles are unique across the live
roster of ONE
$CYBERINE_HOME, not across machines. Uniqueness is best-effort under a simultaneous claim: the two-phase settle re-checks up to 3 times, and a roster read that fails falls back to announcing the un-settled handle rather than refusing to announce. Regression guard:npm run selftest:session-handle(drives the realsessionHandle/presencemodules against a throwawayCYBERINE_HOME, no TTY; carries a negative control per invariant).
- Fleet TUI
-
Inbound (T565): a background poll (every 2s,
pollBusInboxagainst this chat's own addressed inbox plus — once/team joined — the team's broadcast channel) surfaces teammate messages LIVE in the transcript as a distinct⇄row (from-addr + topic + text), never blocking the input loop and never duplicated across polls (de-duped by envelope id, same boundary discipline as the T486 in-loop drain). A message that carries a reply-expectingcorr(abus_request/RPC waiting on this session) is flagged inline as(<topic> -- reply requested); a plain livenesspingis auto-answered silently, matchingloopComms.drainInbox's ping handling./team inboxprints the last 50 inbound messages on demand (e.g. after scrolling past them, or right after joining a team whose backlog predates this session) — this is a recap of the same history already streamed live, not a separate fetch. -
Mid-stream delivery (T566/EV-001): an inbound team message can land in the transcript WHILE the assistant is still streaming a reply — it is never gated on streaming state, and never delays or blocks delivery. The streaming answer/thinking bubble is located by a stable id (
src/tui/chatStreamBubble.ts), not by array position, so an interposed team message never causes a dropped token. Regression guard:npm run selftest:team-inbox-race(model-free; pure function scenarios, no TTY). -
Identity + backlog fixes (T567/EV-002):
presenceAddrRef(the chat's own bus address) is reassigned — and the roster re-announced viasyncPresenceNow()— at every session-identity swap (/resume, fork,/clear); it previously stayed pinned to the pre-mutation address, so messages to the current identity never surfaced. The team-inbox poll cursor is now persisted onChatSessionRecord.teamInboxSinceMsand seeded from it on resume, so a session closed while teammates kept posting replays that backlog instead of silently dropping it. Regression guard:npm run selftest:team-inbox-identity(drives the realChatSessionStore+ bus files against a throwawayCYBERINE_HOME, no TTY). -
Hardening (T568/EV-003): any local process that can write to
~/.cyberine/buscan address an Envelope to this chat, so its payload text is treated as untrusted.sanitizeControlChars(src/tui/components/chat/sanitizeControlChars.ts) strips C0/C1 control bytes and ANSI/OSC escape sequences (keeping\n/\t) at the ingestion point — before the text is ever appended to a message orteamInboxHistoryRef— for both team-inbox chatter and run-progresstooldetail, so a hostile/buggy peer cannot inject cursor moves, OSC 52 clipboard writes, or a title spoof into the operator's real terminal, and the stored/team inboxrecap stays clean too.listTeam(src/store/busTools.ts) now returns{ok, items, detail}instead of collapsing a genuinely empty roster and a roster-read failure into the same[];/team listrenders a distinctteam roster error: ...line on failure instead of misreporting "empty"./team sendresolves viaresolveRecords(fullPresenceRecords, not just addresses) and prints every resolvedid / role / teamalongside the send result, so a name collision (accidental or spoofed) is visible instead of a bare match count. Regression guard:npm run selftest:team-bus-hardening(drives the real bus + presence roster against a throwawayCYBERINE_HOME, no TTY).
-
/cost— show the session's accumulated prompt/completion token usage./compact— summarize the conversation with one model call, then reset the history to [system prompt + summary] to free context. Refused while a reply is streaming./export [clip]— write the transcript as markdown to./chat-export-<stamp>.mdin the cwd (clipcopies it to the clipboard via pbcopy instead, macOS only)./rename <title>— set a session title (also becomes the terminal title). Pins it: the background auto-title generator (see below) never overwrites a/renamed session again./sessions— list the stored chat sessions for this folder (short id, title, message count, age)./resume [<id>]— with an id (or unique prefix): swap the current chat for a stored session; transcript, API history, title, and token usage all come back. Refused while a reply is streaming. With no id: opens an inline fuzzy picker (Enterresumes,Ctrl+Fforks the highlighted session into a new one instead — see "Fork a session" below,Esccancels).
Auto-generated titles (T526). New sessions start with an immediate first-line-truncation
title (the first ~48 chars of your first message), then — right after the first user+assistant
exchange completes — a background call to the cheapest available configured (non-premium)
chat-eligible provider generates a short (<=6 word) topic title and replaces the placeholder.
This upgraded title shows up everywhere a session title is read: /sessions, the inline
/resume picker, the global session browser (cyberine agents, press s), and the terminal
title. No configured provider (offline / unauthed) -> the call is skipped and the first-line
truncation title stands, no error surfaced. A /rename at any point — before or after the
background title lands — pins the session; it is never auto-retitled again. Disable entirely
with CYBERINE_CHAT_AUTOTITLE=0.
Fork a session (T528). Forking clones a stored session into a brand-new id (fresh
createdAt, title suffixed " (fork)") and opens the CLONE — the original session file is
left completely untouched, so you can branch a conversation and explore a different direction
without losing the original thread. Three ways to fork:
- Inline
/resumepicker (no id): highlight a session, pressCtrl+Finstead ofEnter. - Global session browser (
cyberine agents, presss) or the unified chats view (pressc): highlight a chat row, pressCtrl+F(single press — non-destructive, no confirm needed, unlike the double-pressCtrl+Xdelete). - CLI flag:
cyberine chat --resume <id> --forkclones<id>and opens the clone directly (also works from the barecyberine chat --resumepicker'sCtrl+Faction, which is the same code path under the hood). /rewind [<id>]— rewind the conversation to an earlier checkpoint. One checkpoint is recorded right before each user turn (message index + any runs dispatched during that turn). With no argument, opens a picker (title, age, dispatched-run count,[files]tag when a checkpoint has a live file ref) — Enter picks, Esc cancels;/rewind <id-prefix>jumps straight to a match. Conversation restore (truncating the transcript back to the checkpoint) always happens immediately. If any turn being undone dispatched a run whose worker left a shadow-git checkpoint (T315 — loop workers snapshot before every mutating tool call), a confirm prompt offers to also restore those files to their pre-edit state; files are never touched without that explicit confirmation, and a checkpoint with no live ref (the common case — CLI workers, a run that made no edits, or an isolated worktree already swept) restores conversation-only with a clear notice instead. Refused while a reply is streaming./skill— list all discovered skills (global<CYBERINE_HOME>/skills/+ project.cyberine/skills/)./skill <id>injects that skill's full SKILL.md body into the planner context so the model reads and follows the skill instructions in subsequent turns. Acks withinjected skill "<name>" into planner context./backlog— surface open backlog tasks from.local/dogfood/backlog-*.mdand inject the list into the planner context as a system message. Lets the model help you pick and sharpen the next task without leaving the chat. Showsno open backlog tasks foundwhen the backlog is empty./backlog run <n> [<n2> ...]— dispatch the selected open backlog items in PARALLEL through the isolated fleet path (implementTaskWorkflow semantics: per-worker git worktrees + judge gate; honors the currentCYBERINE_BUILD_FLEET). Each dispatch is registered like/run, so completion reports push back into the chat. Echoes#<n> → <runId>per item./next— show the runnable / blocked / waiting sets derived from the project task manifest (.claude/TASKS.mdor.local/tasks/ACTIVE.md). A task isrunnablewhen its[todo]AND itsdeps:are all[done]AND it carries noblocker:line.blockedis[todo]with at least one unmet dep;waitingis[todo]held by ablocker:line. Output lists every runnable id with its[deps: ...], then a "waiting on: Tnn, Tmm" line per blocked task — so the same T-id is safe to feed straight into/dispatch <T-id>. Same shape as the globaloc-tasks runplan (thenf-tasksscript behindoc-tasks action=run), so a T-id printed here will claim cleanly./dispatch <T-id>— look up<T-id>in the project manifest, run the runnable predicate (statustodo+ noblocker:+ deps alldone), and dispatch itsdo:line (ortitleas a fallback) as a background run. Reports why a non-runnable id was refused —already [done]/already [doing]/[backlog] -- flip to [todo] first/[forbidden] -- permanent tombstone, never runs/held by blocker: <text>/blocked -- waiting on: Tnn, Tmm— so the user knows what to fix before retrying. Subject to the sameplan/dontAskpermission-mode gates as/run, and the samecompeteexecution-mode gate. Close withoc-tasks done Tnnwhen the run lands./land <runId...>— run thecyberine landbatch-integrate flow (wait -> digest -> apply+commit per run -> one final typecheck) without leaving the chat; its output streams in as system messages./version— show the Cyberine version (read from the repo'spackage.json, resolved relative to the source file so it works from any launch directory)./mdtest— render a markdown gallery (one assistant bubble exercising every construct: H1-H4, bold/italic/bold-italic/strikethrough, inline code, links + autolinks, fenced code in every per-language-highlighted lang (ts/tsx/json/bash/python/go/rust/sql/yaml) plus diff highlighting, a GFM table, nested ordered/unordered lists, task-list checkboxes, blockquote, horizontal rule). The TUI cannot be auto-snapshotted, so this is the one-screen way to eyeball the renderer after any change toMarkdown.tsx./doctor— unified health-check report, identical to thecyberine doctorCLI subcommand (see "Unified health check" below): runtime, config-file validity across layers, keybindings,CYBERINE_HOMEwritability, run-store sanity, provider keys, gateway reachability (only when configured), build-fleet CLI availability, fleet.json drift, MCP servers, and hook-script executability. Every line is tagged[PASS]/[WARN]/[FAIL]with a fix hint. Posts the full report as one system message; offline-safe by default (no network call unless a gateway is configured, bounded to a 1.5s timeout). Runnpm run selftest:doctorfor the headless smoke test (no TTY needed)./permissions— read-only view of the ACTIVE permission policy, composed exactly the wayLoopAgentAdapterevaluates a tool call: the simpleallow/deny/asklists (config.permissions, sourced from.cyberine/config.json/config.local.json/ the global~/.cyberine/config.json— each list tagged with the highest-precedence layer that set it), the fine-grainedpolicy.jsonrule engine (the built-in destructive-deny floor, always active and never overridable, plus global/project rules — each tagged by source file), and the sandbox/trust flagsCYBERINE_TASK_TRUSTED+HOST_SANDBOX_MODEalongside the auto-detected sandbox backend. Ends with an "Edit" section printing the exact file paths to hand-edit — no in-chat editor yet (planned for T418)./hooks— read-only view of configured lifecycle hooks: every event with at least one hook, each entry's matcher/command/timeout tagged[global]or[project]by whichhooks.jsonit came from, the exit-code contract inline (0=allow,2=block, other=non-blocking error), and the last 20 recorded firings (event, exit code, duration, whetheradditionalContextwas returned) from an in-memory ring buffer that resets per process. An empty hooks config prints a copy-pasteable example instead of a blank section./mcp— status view of every MCP server fromCYBERINE_MCP_SERVERS/CYBERINE_MCP_CONFIG: connection state, tool count, transport (stdio/http), and a best-effort auth-status guess (env vars present for stdio, an auth-looking header for http). A failed server shows its error truncated to 160 chars;/mcp details <name>prints the full untruncated text./mcp retry <name>closes any existing connection and attempts a freshconnectMcpServercall, merging the server's tools into the live tool-calling set on success. The default/mcp(no args) does not open any new connections — it only reads state already loaded at session start./context— read-only breakdown of what is occupying the current session's context window (Claude Code/contextparity), rendered as proportional bars + token counts + percent in one static report (not a live view). Six buckets: system prompt & persona (the seeded system message — Cyberine persona + planner instructions + the active/output-styleoverlay), instruction files (AGENTS.md/CLAUDE.md — always 0 tok here, since the chat planner's own context never injects these, only dispatched fan-out tasks do viaLoopAgentAdapter; the row still probes the repo and reports what it WOULD add, purely informational), memory preamble (the Memory-W3 preamble + anySessionStarthookadditionalContext, both prepended at mount), tool schemas (the exacttoolsarray sent this turn —ALL_PLANNER_TOOLS+ the skill tool + connected MCP tools), todo list (update_plantool-call/result pairs, carved out of the transcript bucket so nothing double-counts), and transcript (everything else). Token counts use the same chars/4 heuristicTruncationHistoryCompressorbudgets against — not the provider's real tokenizer — so a "Context window" section below the buckets cross-references the real, API-measuredlast-turn usage(the same numberChatStatusLine's context bar reads) againstconfig.chatContextWindowand the T530CONTEXT_BARshow/warn thresholds./vim— toggle vim-style modal (NORMAL/INSERT) editing in the input box; persists to.cyberine/config.json(chat.vimMode/CYBERINE_CHAT_VIM_MODE). Off by default. See "vim mode (T525)" above for the full motion/operator reference./help,/exit(or Ctrl-D) — help / quit. The help list is generated from the command registry, so new commands appear automatically.
Shell escapes: type !<cmd> to run a command in the chat's cwd via sh -c, streaming stdout/stderr
into the transcript as it runs and injecting the (truncated) output into the planner's context so the
model can react to it in the same turn — e.g. !npm run typecheck. Type !&<cmd> instead to run it
IN THE BACKGROUND: the child is spawned detached (it survives the chat process exiting) with its
stdout/stderr captured to a log file rather than streamed, and the chat returns immediately with the
job's id, log path, and the /bg tail / /bg kill commands to follow up — e.g. !&npm run build.
Use ! for anything you want to watch land inline; use !& for anything slow enough that you don't
want to block the chat on it. See /bg above to list, tail, or kill every !& job (and every
detached run) from this session.
Running-agents footer: while any run you dispatched in this project is live, a strip just above the
input lists it (animated spinner + rotating verb + short runId + step progress + the task summary),
Claude-Code style — so you watch a /run progress without leaving the chat. It shows up to 3 runs
(then "+N more") and disappears when nothing is live.
Input keys: Up/Down history, Ctrl+R reverse history search, Ctrl+Z undo, Ctrl+V paste, Ctrl+O copy,
Ctrl+Q select-all, Ctrl+U delete to line start, Home/End and Ctrl+A/E cursor moves, Shift+Enter or
+Enter newline, PgUp/PgDn transcript scroll, double-Esc clear. On an empty input, press < or
Left-arrow to return to the fleet TUI.
Completed replies render as markdown: headings, bold/italic/bold-italic/strikethrough,
inline code, links + bare autolinks, fenced code (per-language syntax highlighting for
ts/js/tsx/jsx/json/bash/python/go/rust/sql/yaml, CYBERINE_SYNTAX_HIGHLIGHT=0 to opt out + diff
highlighting), GFM tables, nested and task-list bullets, blockquotes, and rules. Leaked tool-call
protocol markup is stripped before display. Run /mdtest to see the full gallery on one screen.
paste-collapse
Pasting a long block or multiline text (>200 chars or contains a newline) collapses it to a compact
placeholder chip [Pasted text #N +K lines] in the input instead of expanding the input box. The
full original text is kept in memory and expanded back on submit — the model receives the complete
text. Short single-line pastes (<= 200 chars) are inserted verbatim.
To delete a chip, position the cursor immediately before or after it and press Backspace or Delete once — the whole chip is removed in one step. Multiple chips in one input are supported (N increments per paste within the same input session). Chips are also cleared when the input is cleared.
paste-assets (T417) — Ctrl+V image/file paste
Ctrl+V detects whether the OS clipboard holds an image or plain text:
- Image clipboard (e.g. a screenshot, Cmd+Ctrl+Shift+4 on macOS): the image is saved to a temp
PNG file and a compact chip
[Image #N]is inserted into the input. On submit the image is attached to the planner message as a multimodal content part (when the active model supports vision, detected viaCYBERINE_CHAT_MULTIMODALor model-id heuristic). For text-only models a file-path reference[Pasted asset: clipboard-image.png at /tmp/...]is appended instead. - Text clipboard: falls through to the existing paste-collapse text behavior.
Image chip deletion works the same as text chips: cursor adjacent + Backspace or Delete removes the whole chip atomically.
Cross-platform support:
- macOS: full image detection via
osascript+ PNG write. - Linux/Wayland: text only (TODO:
wl-paste --type image/png). - Linux/X11: text only (TODO:
xclip -t image/png). - Windows: text only (TODO: PowerShell Get-Clipboard image path).
Fleet dispatch also forwards pasted images (T417-multimodal-dispatch): /run, the
dispatch_run/dispatch_tasks planner tools, and the Ctrl+B streaming handoff all carry this
turn's pasted-image attachments to launchBackgroundRun, not just the planner's own in-chat
turn. Each attachment is copied into the dispatched run's own directory
(.local/runs/<runId>/attachments/, validated for type + size — see Known Limitations below)
and forwarded to the child process as a CYBERINE_TASK_IMAGES manifest (JSON TaskImage[],
{ path, detail }). The no-build-fleet single-step dispatch path (a plain QA/consensus
/run, without an active CYBERINE_BUILD_FLEET) attaches the manifest to Task.images, so
every default adapter sees it:
- Vision-capable loop workers (
multimodal: true, e.g.loop-deepseek-directpinned to a vision model) receive a realimage_urlcontent part (base64 data URL) — same mechanism the in-chat planner turn already used. - Text-only loop workers get the pre-existing fallback: a text note naming the image path
instead of the bytes (
[Note: this worker is text-only; N image attachment(s) were not forwarded: <path>]). - CLI workers with a native image mechanism (currently
codex, via its-i <path>flag) receive the real file path on the CLI's own argv. - Every other CLI worker (
claude,opencode,gemini, ...) gets a readable-path note appended to its prompt ([Image attachment: <path>]) so it at least knows the attachment exists and where to read it from disk.
Run recipe: paste an image (Ctrl+V) into the chat input, then either type /run <task> or let
the planner call dispatch_run/dispatch_tasks on its own — no extra flags needed.
Known Limitations and Deferred Work (T417-multimodal-dispatch):
- Only image MIME types
image/png,image/jpeg,image/gif,image/webpare accepted; anything else is dropped (logged, never thrown). Each attachment is capped at 8 MiB (MAX_ATTACHMENT_BYTESinsrc/store/attachments.ts) — no env override yet. - Only the no-build-fleet single-step dispatch (
cyberine-bg-run.ts's bare consensus path) and the isolate/swarm-chunk worker paths inWorkflowEngineforward images. The swarm lane-runner and search-fan-out paths (WorkflowStep.swarmUseLaneRunner,searchFanOut) do not forward images yet — a task routed there still loses the attachment silently./dispatch(task-manifest fan-out) and/workflow author(authored-workflow-script dispatch) do not forward images either — only/run, the planner'sdispatch_run/dispatch_taskstool calls, and the Ctrl+B streaming handoff do. - No new environment variable was added for this feature.
cmd-delete (kill to line start)
Ctrl+U deletes from the cursor back to the start of the current line (readline kill-to-line-start).
On macOS with iTerm2 you can map Cmd+Delete to the same action: in iTerm2 Preferences > Profiles >
Keys, add a key binding for Cmd+Delete that sends the hex code \x15 (Ctrl+U). There is no standard
terminal escape sequence for Cmd+Delete that works across all terminals without explicit iTerm2
profile mapping.
newline-in-chat
Shift+Enter inserts a newline on iTerm2 (and any terminal that sends a bare line-feed for Shift+Enter) without any special protocol. The portable fallback that always works on every terminal: end a line with a backslash then Enter, or use Ctrl+J.
For terminals that need the kitty keyboard protocol sequence (\x1b[13;2u) to distinguish
Shift+Enter from Enter, set CYBERINE_KITTY_KEYBOARD=1 to opt in. This is OFF by default because
terminals that do not fully decode CSI-u sequences (e.g. iTerm2 with Ink 5) will silently
break Ctrl+C when the protocol is active.
Sessions (Claude-Code style): every chat is persisted as a session under
<CYBERINE_HOME>/chat-sessions/<id>.json — saved continuously (debounced 500ms), never written while
empty. The title auto-derives from your first message (48 chars) until /rename overrides it; the
session also keeps the full API history and token usage, so resuming continues the conversation
with full context. From the shell:
cyberine chat sessions # list stored sessions for this folder (works piped)
cyberine chat --continue # reopen the most recent session for this folder
cyberine chat --resume <id> # reopen a specific session (id or unique prefix)A session stays filed under the folder it was created in (kwbgf). Resuming it from another folder, by --resume <id> or the in-chat /resume picker, keeps its stored cwd, so it still lists under its own project and --continue there still finds it; a note in the transcript names both folders, because tools, file reads and dispatches in that window run in the folder you launched from. /clear starts a new session filed under the current folder.
Every chat start sweeps what killed or crashed chats left behind (kpq4s): a session with no user or assistant message and no hub draft prompt, untouched for 10 minutes and with no live presence, is deleted along with its presence record, handle claim and empty bus inbox; half-written <id>.json.<pid>.<n>.tmp files of dead processes go too. A session holding any user or assistant message, a hub compose-bar draft, and a non-empty inbox are never touched. Guard: npm run selftest:chat-leftovers. Known limitation: the sweep runs only when a chat starts, not from the daemon's reaper, so leftovers sit until the next cyberine chat.
Slash-command autocomplete works exactly like Claude Code: type / and a popup lists the matching
commands (first one pre-selected). Up/Down (or Ctrl+P/Ctrl+N) move the highlight (wrapping), Tab
completes the highlighted command into the input (adds a trailing space, no submit), Enter accepts
AND runs it when the command takes no argument (commands with an argument hint like `/rename
While a reply is streaming you can keep typing: Enter QUEUES the prompt (it auto-dispatches when
the stream ends), and the input shows a dim N queued badge while prompts wait. Esc peels off the
most recently queued prompt; once the queue is empty, Esc INTERRUPTS the in-flight response
(Claude-Code-style stop). Ctrl+B instantly dispatches the pending TASK: as a background fleet run
without leaving the chat. When idle, quitting takes a double Ctrl+C (the first press shows a confirm
hint for 800ms); while streaming a single Ctrl+C still exits immediately.
Two more input affordances: as you type, a dim ghost suffix completes your text from your own
prompt history (most recent entry that extends the input; hidden for /commands, while streaming,
and during Ctrl+R search) — press Right-arrow at the end of the input to accept it. And while a
reply is streaming, Ctrl+B hands the turn off to the fleet: the in-flight stream is aborted
(the bubble closes with (handed off to background)), the SAME prompt is dispatched as a detached
background run (the capability router picks its fleet), and the chat posts
handed off to background run <id> — the run completion report comes back into the conversation
exactly like a /run.
When CYBERINE_CHAT_MODELS is set (comma-separated model ids), Ctrl+T cycles the planner model
mid-session: the status line shows the active model plus a ^T model affordance, a system line
notes each switch, and every subsequent turn (including /compact and run reports) uses the active
model. Unset or single-entry = no toggle, exact status quo.
Permission mode (Claude-Code-style dispatch gating)
cyberine chat has six permission modes that gate how (or whether) /run dispatches a fleet run.
Cycle the three interactive modes with Shift+Tab (a (mode) indicator appears in the status
line), or set a mode at startup with --permission-mode:
cyberine chat --permission-mode acceptEdits # auto-approve edits, no per-dispatch prompt
cyberine chat --permission-mode plan # planner only — never dispatches; writes a plan file first
cyberine chat --permission-mode dontAsk # only pre-approved actions (no dispatch at all)| Mode | Shift+Tab | Dispatch behavior |
|---|---|---|
default | yes | Ask (prints a note) before each /run that edits; planner answers freely |
acceptEdits | yes | Auto-approve file edits inside task.cwd (build agent + skip-permissions); non-edit destructive ops prompt; deny floor + host sandbox still block |
plan | yes | Planner only — never dispatches an editing run; /run writes a traceable plan file and prompts you to switch modes to dispatch. The planner can also call the present_plan tool to show a finalized plan and ask proceed / edit / cancel via the selection overlay; only proceed unlocks the next dispatch_run (one-time, cleared on dispatch or when leaving plan mode) |
auto | no | Full auto dispatch bounded by deny floor + host sandbox. Note: Cyberine has no per-tool classifier model (unlike CC) — this is documented as a gap; auto is treated as acceptEdits-for-all-tools, not CC's classifier |
bypassPermissions | no | Startup-only (--permission-mode bypassPermissions); auto-approve all except the deny floor |
dontAsk | no | Startup-only; only pre-approved actions — silently refuses dispatch |
Switch mid-session with /mode <name> (e.g. /mode acceptEdits) or Shift+Tab. The active mode is
persisted on the session record so --continue / --resume restores it.
Plan files (written when plan mode runs /run or when the model calls dispatch_run):
~/.cyberine/plans/<project-slug>/<YYYYMMDD-HHMMSS>__<topic-slug>.md (global default)
<cwd>/.cyberine/plans/<YYYYMMDD-HHMMSS>__<topic-slug>.md (when CYBERINE_PLANS_DIR is set)Each plan file has YAML frontmatter with session_id (back-links to the chat), run_ids[]
(appended as runs are dispatched from the plan), status (draft -> approved -> done),
and approved_mode (the mode chosen when plan mode was approved). The body has ## Goal,
## Context, ## Steps, ## Done-when, and ## Notes / Lessons sections.
Override the plans directory per-project:
export CYBERINE_PLANS_DIR=1 # stores plans in <cwd>/.cyberine/plans/ instead of ~/.cyberine/plans/<project>/Browse plan files with cyberine plans:
cyberine plans list # list plans for this project, newest-first (hides superseded)
cyberine plans list --all # include superseded plans
cyberine plans show <slug> # print frontmatter facts + body; accepts a unique prefixEach row in cyberine plans list shows: topic-slug status created-date description.
cyberine plans show resolves a partial prefix; errors with all candidates when ambiguous.
Both commands read the global ~/.cyberine/plans/<project-slug>/ directory and, when
CYBERINE_PLANS_DIR is set, also the per-project <cwd>/.cyberine/plans/ directory.
The dim status line under the input mirrors Claude Code's: model ^T affordance, provider, cwd,
the current git branch, live session usage (<total> tok compacted as 12.3k, plus $cost
when pricing is known), and a context bar (████░░░░ N%) showing how full the latest turn
left the model window (approximated at 65k tokens, override with CYBERINE_CHAT_CONTEXT_WINDOW).
The branch is polled every ~30s.
The context bar now matches Claude Code's threshold behavior instead of being always-on: it stays
hidden while usage is low and only appears once the window starts filling (default: at 60%
used, i.e. ~40% remaining), then flips from the accent color to a warn (amber) tone once usage
crosses 85% (~15% remaining). Both thresholds are one knob — CONTEXT_BAR in src/tui/theme.ts —
overridable via CYBERINE_CONTEXT_BAR_SHOW_PCT / CYBERINE_CONTEXT_BAR_WARN_PCT (used%, 0-100).
When auto-compaction is enabled (see /compact below), the bar also annotates the auto-compact
trigger point: the empty-track cell at that fill level renders in the warn color as a tick mark,
so you can see where auto-compact will fire before you get there — independent of the warn-color
threshold above, since CYBERINE_AUTOCOMPACT_THRESHOLD can be tuned separately.
If you step away and come back, the next keypress after ~5
minutes idle opens a Welcome back dialog instead of typing: press c to continue where you left
off, k to compact the conversation, or x to clear and start fresh.
Type @ to open a file/folder picker over the project (gitignore-aware, fuzzy-filtered as you
keep typing; @src/ style queries drill into subdirectories). Up/Down navigate, Tab or Enter
inserts the relative path at the cursor, Esc dismisses (the literal @ stays, and the popup
re-opens if you keep typing the query).
On send, every @-mention is EXPANDED the way Claude Code does it: the referenced file's contents
(or a directory listing) are read and injected into the turn as a synthetic <system-reminder>
Read-tool result, so the planner — which has no file tool of its own — actually sees the file
instead of asking you to paste it. Supports quoted paths (@"with space.md"), line ranges
(@src/x.ts#L10-20), and directories. Files over 256 KB are skipped and >2000-line files are
truncated; a missing path is dropped silently. A dim read N file(s): … line confirms what was
attached. The visible transcript keeps only your @mention.
The chat TUI keeps your shell scrollback on exit; set CYBERINE_ALTSCREEN=1 to run it in the terminal
alternate screen instead. The fleet TUI (cyberine agents) uses the alternate screen by default
(CYBERINE_ALTSCREEN=0 disables). Both TUIs respect CYBERINE_THEME and
compact themselves on narrow (<90 cols) or short (<24 rows) terminals.
Color schemes (CYBERINE_THEME / /config theme)
Five schemes ship; dark is the default. Switch from inside chat with /config theme <name> (saved to
config; applies on the next cyberine chat launch — theme tokens resolve at startup), or set CYBERINE_THEME=<name> in the env.
| Scheme | Accent | Look |
|---|---|---|
dark (default) | violet #c77dff | Plasma: violet accent and mint verdict (the website's palette until 2026-10-01; the web dashboard keeps it) on Claude dark-daltonized semantics — colorblind-safe (success is blue, diff blue/deep-red); inline code is pink |
cyan | cyan #30dede | Cyberine brand cyan accent + Claude standard-dark semantics (green success) |
claude-dark | orange #d77757 | Claude standard dark (non-daltonized): green success, standard brand orange |
light | violet #7a2bd1 | Plasma for light-background terminals: the Plasma light ring on Claude light-daltonized semantics |
ansi | magentaBright | 16-color fallback for terminals without truecolor (inline code is cyanBright) |
/config with no args prints the current theme (and fleet / permission / model / thinking).
Input editing keys (readline / Emacs, like Claude Code + opencode)
The cyberine chat prompt follows the standard GNU-readline / Emacs editing convention — the same
one Claude Code and opencode use. The core editing layer is fixed (not remappable):
| Action | Keys |
|---|---|
| Line start / end | Ctrl+A / Ctrl+E (or Home / End) |
| Char forward | Ctrl+F (or the right arrow) |
| Word back / forward | Alt+B / Alt+F, or Ctrl+Left / Ctrl+Right |
| Delete back to whitespace | Ctrl+W (bash unix-word-rubout: alpha foo/bar.baz becomes alpha ) |
| Delete word backward | Alt+Backspace, Ctrl+Backspace (Kitty terminals) |
| Delete word forward | Alt+D, Ctrl+Delete |
| Forward-delete one char | Ctrl+D, Fn+Delete |
| Kill to line start / end | Ctrl+U / Ctrl+K (yank back with Ctrl+Y, cycle older kills with Alt+Y) |
| Undo | Ctrl+Z or Ctrl+_ |
| Select all | Ctrl+Q (then type / Backspace to replace) |
| Clear whole input | Esc Esc (double), or Ctrl+C once (the text goes to the kill ring: Ctrl+Y brings it back) |
| Rewind picker | Esc Esc on an empty input once the session has a checkpoint (same picker as /rewind) |
| Recall previous prompt for editing | Esc Esc on an empty input before any checkpoint exists (repeat to step further back) |
| Exit | Ctrl+C twice within 800ms on an empty input; once while a response streams |
| Newline vs send | Ctrl+J / Shift+Enter / trailing \+Enter = newline; Enter = send |
| History | Up / Down (or Ctrl+P / Ctrl+N), Ctrl+R reverse-search |
Measured against Claude Code 2.1.273 on a real terminal (karmy, 2026-09-17; the per-key decision
record is .local/research/2026-09-17-input-keymap-decision.m3max.md): Ctrl+W deletes back to
whitespace, Ctrl+C on text discards it and on an empty box arms exit, and Esc Esc on an empty
box opens the rewind picker. Before karmy, Ctrl+C in cyberine chat exited on the FIRST press
whatever the buffer held: Ink's default exitOnCtrlC consumed the key before the chat's own
double-press guard ever saw it.
Double-Esc is one gesture with two meanings depending on the buffer: with text in the box it
clears it (first Esc arms a "(Esc again to clear)" hint for 800ms); with an empty box it opens
the rewind picker once a checkpoint exists, and before that it recalls your most recent submitted
prompt into the input for editing
— a "editing previous message" hint replaces the clear hint while it's showing, and sending just
submits the edited text as a new message (it never retroactively replaces the old one). Doing
Esc Esc again while the recalled text is untouched steps one prompt further back; typing
anything breaks the association and the next Esc Esc goes back to the normal clear gesture.
Note: Ctrl+B (readline char-backward) is intentionally NOT bound to cursor movement — the chat REPL
already uses it for F01/F02 (dispatch the pending task, or hand off a streaming turn to background).
Use the left arrow for char-backward instead.
macOS note: Alt/Option chords (Alt+B/F/D/Y, Alt+Backspace) only reach the app when your terminal
sends Option as Meta — enable "Use Option as Meta key" (Terminal.app) or "Left Option: Esc+" (iTerm2).
Cmd+* chords are intercepted by macOS and never reach a terminal app, so there is no in-app Cmd+A /
Cmd+Backspace — use Ctrl+Q / Ctrl+U instead. (Cmd+Backspace is separately remapped by
iTerm2/Terminal.app to Ctrl+U at the terminal level.)
Deliberately not adopted: Ctrl+G (open the prompt in an external editor) has no counterpart yet --
it needs Ink's render suspended around $EDITOR without losing the draft, and nothing does that
today. Ctrl+D stays forward-delete and does not exit on an empty box: two presses 1.5s apart did not
exit Claude Code 2.1.273 either. Claude Code's defaults for Ctrl+O / Ctrl+T / Ctrl+S, Cmd+P
and Cmd+T are not copied; rebind chat actions in ~/.cyberine/keybindings.json instead. Under
tmux with a Ctrl+A prefix, tmux swallows Ctrl+A: type the prefix twice, or use Home. Regression
guard: npm run selftest:keymap-tui-guard section 4. Known limitation: the Esc Esc rewind path
was verified by code and smoke, not on a real TTY, since reaching it needs a submitted turn.
vim mode (T525) — modal NORMAL/INSERT editing in the chat input
The chat InputBox supports optional vim-style modal editing, off by default. Toggle it with the
/vim slash command from inside cyberine chat:
/vim # flip vim mode on/off; persists to <cwd>/.cyberine/config.json (chat.vimMode)Toggling on always starts fresh in NORMAL mode; a fixed-width NORMAL (inverse-styled) /
INSERT badge appears in the top-right corner of the input frame whenever vim mode is on, so the
active sub-mode is always visible without shifting the input's layout. The preference persists
across sessions (see CYBERINE_CHAT_VIM_MODE above) — flip it back off the same way.
INSERT mode behaves exactly like the readline/Emacs editing above (every key in that table still
works); Esc switches to NORMAL. NORMAL mode implements a deliberately small, chat-box-sized
subset of vim's grammar — enough for quick edits to an in-progress message, not a full editor:
| Category | Keys | Effect |
|---|---|---|
| Motion | h l | cursor left / right one char |
| Motion | j k | cursor down / up one line (column-preserving) |
| Motion | w | start of next word |
| Motion | b | start of previous word |
| Motion | e | end of current/next word |
| Motion | 0 | start of line |
| Motion | $ | end of line |
| Operator | x | delete the character under the cursor |
| Operator | dd | delete the current line |
| Operator | dw | delete from cursor to the next word (incl. trailing space) |
| Operator | cw | change word: delete to end of word, drop into INSERT |
| Enter INSERT | i / a | insert before / after the cursor |
| Enter INSERT | A / I | insert at end / start of line |
| Enter INSERT | o / O | open a new line below / above, drop into INSERT |
| Count prefix | 3w, 2dd, 5x, ... | repeats the following motion/operator N times |
Word motions use the same word-boundary rule as the rest of the input (Alt+B/Alt+F above) — a
simple word-vs-non-word split, not vim's separate punctuation class, so results on punctuation-
heavy text are an approximation. Everything not listed (search /, marks, registers, visual mode,
. repeat, ...) is intentionally out of scope; unmapped NORMAL-mode keys are no-ops rather than
inserting themselves into the buffer. Special keys (arrows, PageUp/PageDown, Tab, Enter, Backspace)
always fall through to their normal chat behavior (history recall, autocomplete, submit, scroll)
regardless of vim mode.
mouse-in-chat (wheel scroll + drag-to-copy)
Mouse reporting is ON by default in cyberine chat: the wheel scrolls the transcript, and you can
drag-select transcript text to copy it — release the mouse and the selected characters land on the
OS clipboard with a ✓ Copied N chars toast on the mode line. The highlight is exactly what gets
copied (both come from one buffer), so there is no drift; while dragging, assistant messages show their
raw source (markdown styling returns the instant you release). Set CYBERINE_MOUSE=0 to disable mouse
reporting entirely and fall back to your terminal's native select/copy (you lose wheel-scroll and the
in-app copy; use PgUp/PgDn to scroll). The drag-select uses SGR button-event tracking (?1002+?1006),
restored on exit/crash so the terminal is never left with tracking stuck on.
The wheel scrolls 3 lines per notch for smooth reading; tune with CYBERINE_WHEEL_LINES=<1..10>
(smaller = finer, larger = faster). Keyboard PageUp/PageDown still jump a quarter-page.
Custom status line (Claude-Code-style)
The dim bar below the transcript is two rows by default:
◆ <model> [<agent>] · <provider> · <context bar> % · TASK ready: …
~/path/to/cwd · ⎇ <branch> · fleet: <mode> · <N> tok · $<cost> · pool: …Customize it with CYBERINE_STATUSLINE_CMD (or the equivalent statusline.command config.json key —
see "Config precedence" below), a shell command that reruns on a ~300ms debounce after the
session state changes (model, mode, streaming, token/cost, message count) — not a fixed poll. The
300ms window also acts as a floor: a burst of rapid changes (e.g. fast typing) collapses into one
rerun instead of a spawn storm. The output shape picks the mode:
-
Append a segment (plain text): the first stdout line is appended as one extra
· <text>segment.CYBERINE_STATUSLINE_CMD='echo "$(date +%H:%M) · $(whoami)"' cyberine chat -
Replace the whole bar (JSON): emit a JSON string array, or an object with a
linesarray, and those rows fully replace the built-in bar — you own every row (capped at 3, each truncated to the terminal width).# ~/.cyberine/statusline.sh — reads the injected JSON snapshot from stdin (see below) snapshot=$(cat) model=$(echo "$snapshot" | node -e 'process.stdin.once("data",d=>process.stdout.write(JSON.parse(d).model.id))') printf '{"lines":["◆ %s · node %s","%s · ⎇ %s"]}' \ "$model" "$(node -v)" "$(date +%H:%M)" "$(git branch --show-current)"CYBERINE_STATUSLINE_CMD='~/.cyberine/statusline.sh' cyberine chatStdin contract (like Claude Code's statusLine hook): the command's stdin receives a single JSON object, freshly built at each debounced rerun:
{ "model": { "id": "deepseek-v4-pro" }, "session": { "id": "018f...", "title": "fix login bug" }, "cwd": "/Users/you/project", "mode": { "permission": "default", "execution": "compete" }, "cost": { "totalUsd": 0.0123 }, "tokens": { "prompt": 1234, "completion": 456, "total": 1690 }, "elapsedMs": 45231 }A command that ignores stdin still works exactly as before (both examples above are valid with or without reading it). Invalid JSON stdout falls back to plain-text (first-line) mode, so a malformed command degrades to an appended segment instead of breaking the bar. An erroring or timing-out command (10s timeout) silently falls back to the built-in bar — the fallback is only ever applied once per failure streak (no repeated flicker between built-in and blank on every rerun).
This stdin/debounce upgrade applies to
cyberine chat's status line. The fleet hub (cyberine agents) still pollsCYBERINE_STATUSLINE_CMDon a fixed ~30s interval with no stdin payload — it has no single active session to snapshot.
Config precedence: statusline.command in .cyberine/config.json (or config.local.json /
~/.cyberine/config.json) resolves to the same CYBERINE_STATUSLINE_CMD env var, so the standard
precedence applies — shell env always wins over any config.json layer:
{ "statusline": { "command": "~/.cyberine/statusline.sh" } }User memory (honcho-style cross-session recall)
cyberine chat persists a per-project USER.md note under <CYBERINE_HOME>/user-memory/<project-slug>/USER.md.
Facts stated in one session are injected into the planner's system header on every subsequent turn,
so later sessions recall them automatically.
Three ways to add facts:
- Manual:
/remember <fact>appends a timestamped bullet. Example:/remember I prefer TypeScript over JavaScript. - Quick-append shortcut: typing
# <fact>(hash + space, as the very first characters of the input) routes straight to/rememberinstead of the planner — no round trip. Example:# I prefer TypeScript. A bare#foowith no space after the hash is NOT a shortcut and goes to the planner as normal text. The input box tints its caret/border and shows amemory notehint the moment the line starts with#, so you see the mode change while typing instead of only after theremembered: ...chip. - Planner-captured: the model has a
remember_user_facttool. When you state a durable preference, role, or workflow habit, it appends the fact for you.
Inspect the current note anytime with /memory. The file is plain markdown — you can also edit it
by hand. Disable the feature with CYBERINE_USER_MEMORY=0.
This is the standalone, no-Claude-Code path to go from "what should I even run?" to a dispatched fleet run.
Note: cyberine run uses the one-shot orchestrator (like npm run cli) and prints its result inline; it
does not create a workflow run, so it does not appear in cyberine agents / cyberine tui (which track
workflow-engine runs under .local/runs/).
Inline tool-call recovery (k4019)
DeepSeek, the default chat model, sometimes answers with no structured tool_calls and writes its native DSML call markup (<||DSML|| calls><||DSML|| invoke name="list_dir">...) into the reply text instead. The shared planner turn (src/chat/plannerTurn.ts, used by the chat TUI and the cyberine web chat) no longer drops that call silently:
- Well-formed markup on a request that offered tools is converted into real tool calls and handed to the same executor a structured call reaches, so plan mode,
dontAsk, execution-mode gates and argument validation all still apply. - Anything else (malformed markup, a tool name that was not offered, or markup written on the post-tool follow-up request, which offers no tools) executes nothing: the markup is stripped and the model gets one correction asking it to re-issue the call through the tool-calling interface, with tools offered on that one retry.
- Bounded: one recovery per turn, conversion or correction. Markup after that is only stripped. The persisted history carries the prose and real
tool_calls+toolpairs, never the DSML, so resume and fork keep a valid OpenAI-wire history. - Display: the chat's markup sanitizer now removes whole DSML call blocks, parameter values included, so a stripped call no longer leaves its argument (a lone
.) behind. - Streaming (kfn1p, 2026-10-01): a stream filter (
createDsmlStreamFilterinsrc/chat/inlineToolCalls.ts) holds tokens back from the first DSML marker, and briefly holds a trailing<,|or|until the next token shows it is not the start of one, so the web chat, the TUI and the readline REPL never print raw markup mid-stream; prose after a call block still appears at the end of the stream. - Readline REPL (kfn1p):
cyberine chatwith no TTY (src/cyberineChat.ts, its own turn loop) now makes the same decision as the planner turn: a well-formed call to an offered tool runs, anything else gets the one correction, then markup is only stripped.
Run: nothing to enable. Guard: npm run selftest:chat-dsml-recovery (no network: replays DSML through the parser, the decision and a fake-client planner turn).
Known Limitations and Deferred Work. Only DeepSeek's DSML shape is recovered; other vendors' inline formats are still only stripped for display. A reply whose markup is well-formed but repeats after the one recovery is stripped, not executed.
List + switch models — cyberine models, cyberine model
The catalog is an embedded offline seed (src/core/modelCatalog.ts) and the sole source of truth — there is no runtime cache to fall behind it. cyberine model use writes nothing but the one MODEL...
Task workflow inside cyberine chat — shared manifest with the nf-tasks skill
/run and the planner TASK: line dispatch ad-hoc text; the four slash commands below let the chat operate directly on the project's task manifest — the same .claude/TASKS.md / .codex/TASKS.md /...