·
plugin · drop-in viewer
dotbot
What this plugin does and who it is for, in a paragraph.
byzero-proxy @ DESKTOP-DB3UJOJ·posted 20d ago
What it does
dotbot
What this plugin does and who it is for, in a paragraph.
Use
local dotbot = require("dotbot")
local answer = await(dotbot.doSomething("x"))
Methods
doSomething(input): what it does, and what it answers.
Interface
What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.
This asset doesn't declare a schema or structured payload.
⌬ Metadata
description
import{"at":3667.888404852,"importer":"aef4a205-7820-4ed5-ba55-8183353199c4","iteration":24,"source":{"guid":"ecffa096-bb76-42e4-bf4b-ffa9707fa705","path":"/zero/source/dotbot.plugin/plugin.wasm"}}source_checksum58b3bb52c2ca71abf5c6728027b6df34a50bfbf6fd391cbe3b5ffe6cb6b57b44Sub-parts
Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.
28items
# dotbot (editor panel)
The DotBot panel: one dock tab, DotBot, in the right dock under the Agents menu
beside the terminal agents' tabs.
A conversation runs on an account, and the account is a setting of the
conversation, shown on the composer's account chip and changed from it:
| Account | Pays |
|---------|------|
| OrigoZero credits | the agent included with OrigoZero |
| each connected Claude account | that Claude plan |
| each connected ChatGPT account | that ChatGPT plan |
Several accounts of one provider connect side by side. Each is one of the
plugin's agents: `dotbot-claude` runs on the first Claude account and
`dotbot-claude~2` on the second, so moving a conversation to another account is
sending its next turn as that account's agent. A conversation keeps its
history when it moves.
- **Toolbar**: DotBot, then conversations, new chat, whether DotBot reacts to
engine events, and overflow.
- **Conversations**: every conversation, whichever account it ran on, named
with that account. Opening one opens it on the account it ran on.
- **Transcript**: user bubbles, the thinking row, tool cards, the answer, and a
meta line per turn: model, thinking level, tokens, tool calls, duration, and
which account paid. A switch of account says so in the conversation where the
next turn will appear.
- *Thinking* is folded, never hidden: a row that says `Thinking… 4 s` while
the model thinks and `Thought for 4.2 s` once it has, with a chevron that
opens the thoughts (streaming live while it thinks). A turn's thinking
phases fold into one row when the turn ends, and a reloaded conversation
shows the thinking it stored.
- *Tool cards* name what a `tool_exec` actually ran, from the trace in its
answer, for a live answer (cut at 2000 characters by the host) as well as a
stored one. A picture whose file is gone says so.
- *Answers* wrap: a markdown table is drawn as a table whose cells wrap, and a
fenced code block wraps at the panel's edge instead of running past it.
- *Selection*: text can be highlighted with the mouse and copied with Ctrl+C:
user messages, thinking, tool details, error cards, code blocks, and (where
the renderer supports `selectable` on a `richText`) every styled paragraph,
list item and table cell of an answer.
- **Composer**: the account chip, a model chip and a thinking chip offering only
the levels the chosen model accepts, attach, send, and a line naming what is
left of the account's usage.
- *While a turn runs* (one the person sent, or one DotBot woke into on its
own) the Working row shows and Send becomes Stop. Enter queues the message
for when the turn ends; the interrupt button stops the turn and runs the
message once it has ended. Queued messages sit above the composer, each one
editable and droppable. Stop holds them: a stopped conversation starts
nothing until the person sends a held message.
- **Account picker** (the account chip): every account DotBot can run the
conversation on, with its state and usage, then "Connect another Claude
account", "Connect another ChatGPT account" and "Connected accounts…".
- **Connected accounts**: one card per account with its plan, its own usage
windows, and Use, Rename, Sign in again and Disconnect; Connect on an account
that is not connected starts its sign-in in place.
- **Connect flows**: ChatGPT's device code, and Claude's sign-in then paste,
for the first account of a provider and for each further one.
- **Usage states**: a notice under 25% of a window left, a warning under 10%
offering a cheaper model only when that model is one this account can run,
and a spent window naming the connected accounts that still have room, with
a button that moves the conversation to the first of them. Nothing moves on
its own.
- **Failure states**: an expired sign-in, an expired code, a provider not
issuing codes, an engine not linked to an OrigoZero account, and an account
disconnected while a conversation was on it, which then asks for an account
rather than sending anywhere.
Everything it shows comes from the plugin through `source.module`, and every
word it puts on screen for a reading comes from `view.module`. The panel holds
only what is on screen right now: which view, which popup is open, which
account the open conversation runs on, how far a connect flow has run, and the
unsent draft.
▲ 0↑ born
# dotbot_service
The DotBot bridge, minus its privileges.
`src/lua/trusted/dotbot/` is the layer between the DotBot panel (user-VM Luau,
untrusted) and the DotBot plugin (trusted scope, holds a person's ChatGPT /
Claude sign-in). Everything in that layer that is a **rule** rather than a
**capability** lives in this module: the job table, the consent choreography,
the event ring, the mirror the read-only exports answer from, and the scrub
every value passes through on its way out.
```lua
local Service = require("@builtin::modules.dotbot_service")
local service = Service.new({
plugin = function() return rawget(_G, "dotbot") end,
await = task.await,
spawn = task.spawn,
consentRequest = function(o) return consent.request(o) end,
consentGranted = function(o) return consent.granted(o) end,
openUrl = function(url) return system.openUrl(url) end,
newJobId = function() return __crypto.uuid_v4() end,
nowMs = function() return os.clock() * 1000 end,
})
```
## Why the split
It is what makes the layer testable. The module is a **factory**: it holds no
state of its own and no privileged global, so a test can construct an instance
over fakes and drive every rule in it. Being a builtin module, a world script
can construct one too — and wins nothing, because its instance talks to its own
fakes. The one instance that talks to the real consent gate, the real plugin
namespace and the real `system.openUrl` is built in the trusted VM, out of
globals only the trusted VM holds, and is reachable only as
`trusted.call("dotbot.<method>", …)`.
## The shape, and where it comes from
| Constraint | Consequence |
|---|---|
| A `trusted.call` handler must not yield (`trusted/exports.luau`) | Nothing that awaits is answered in the frame. Awaiting verbs are start/poll **jobs**; reads are answered from a **mirror** a spawned task fills, which stays current until a change marks it stale, so an open panel asks the plugin nothing while nothing changes. |
| There is no push from the trusted VM into a user VM | The panel **drains a cursor** (`events(cursor)`) rather than registering a callback — so no trusted code ever calls into a user VM, and a world script cannot install itself as the panel's event handler. |
`stop` is the one verb answered in the frame: stopping is always allowed and
always cheap, and a stop that needed a confirmation would be a stop that lands
after the turn it was meant to end. Its outcome arrives as a turn event.
## Signing in
A person's plan is reached by one of two flows, and **the plugin says which**.
`connectStart` reads it from the plugin's own account state (`M.signInFlow`)
and, when a logged-out account names none, asks the plugin — never the caller,
whose choice of flow would be a choice of which endpoint the person is sent to.
| Flow | `connect.start` answers | Then | Finished by |
|---|---|---|---|
| `deviceCode` (ChatGPT) | `{ flow = "deviceCode", state = { userCode, verificationUrl, … } }` | the person types the code on the provider's page | `checkSignIn` until the account connects |
| `pasteCode` (Claude) | `{ flow = "pasteCode", state = { authorizeUrl } }` | the person signs in and the browser ends on an address carrying the code, which they paste | `completeSignIn(provider, pasted)` |
`cancelSignIn` abandons either.
### Several accounts of one provider
Every flow takes an **account key**: the provider's key (`anthropic-claude`)
for its first account, the provider and a slot (`anthropic-claude~2`) for each
further one. `addAccountStart(provider)` connects a further account beside the
ones already connected: behind the same `connect` confirmation, it asks the
plugin for the key the new account signs in under (`newAccount`) and starts
that account's sign-in, answering what `connect` answers plus the `account` it
started. `renameAccountStart(account, label)` keeps a person's name for an
account, as they wrote it (one line, at most 64 characters), and asks nobody. A
dialog that names an account names which one, by the email its provider stated,
once a provider has more than one: the name a person gives an account is set by
an ungated call, so the dialog's words stay the plugin's own.
Only `connect` asks for a confirmation. The three that continue it do not, for
`stop`'s reason: they carry out a `connect` the person has already confirmed and
is at that moment completing on the provider's own page, they spend nothing, and
`completeSignIn` cannot connect anything on its own — the plugin holds a
single-use verifier from the gated `connect` and answers "no pending login" to
anything else. A second dialog inside one flow is how a person learns to stop
reading the first.
## Spending
`sendStart` and `commandStart` both ask for the `spend` grant, keyed by action,
plugin and the agent's display name, remembered for the world session. A slash
command is gated because a slash command can run a turn — `/compact` summarises
a conversation with the model — so it spends the plan exactly as a message does.
## What it will not do
- Return a token, a credential id or a header. `M.scrub` drops every
credential-named key at every depth, on every value leaving the layer —
results and event payloads alike. It is the second line: DotBot's account and
event types carry no token by construction. The names it drops are exact, not
a substring match on `token`, so the usage counters a person is shown
(`tokens`) and a turn's `turnId` survive.
- Take a URL. `openSignInStart(provider)` reads the verification URL out of the
plugin's own account state for the provider named, checks it is an http(s)
page, shows a person its **host** in the dialog, and only then opens it.
Account state naming none errors the job.
- Answer, dismiss or count a consent dialog. There is nothing here that settles
one; the gate is the engine's, and only a person at the keyboard answers it.
- Put a caller's own words on a dialog. `detail` is resolved through the
plugin's own agent and account lists and the flow **errors when it cannot be
named** rather than falling back to the id it was handed. The two arguments a
caller does supply beyond a message — a pasted sign-in code and a command
line — are shape-checked (`M.pastedCode`, `M.commandLine`) and reach no
dialog: the pasted code's flow draws none, and a command's dialog names the
agent.
- Read a plugin's refusal as an answer. The plugin host **raises** one where
the call is awaited, so `call` reports it as a failure and a job never settles
`done` on a sign-in that did not happen.
## Constants
| Name | Meaning |
|---|---|
| `RING_CAPACITY` | 512 — events held. A cursor older than the tail is answered `dropped = true`. |
| `JOB_LIMIT` | 8 — jobs live at once, across every caller. A ninth `start` is `{ ok = false, reason = "busy" }`. |
| `JOB_TTL_MS` | 600000 — a job nobody polls is dropped after ten minutes. |
| `RETRY_MS` | 2000 — how long a read the plugin could not answer waits before it is asked again. A read that has an answer stays current until a change marks it stale: an `account`, `usage` or turn-ending `turn` event, a turn in a conversation the list does not hold, a job this service ran, or `refresh(name)`. A sign-in, a sign-out and an `account` event mark the accounts, the agents, the models and the usage. A new conversation list marks the history of each conversation whose message count or last update it shows moved. |
| `MIRRORED` | The reads the mirror holds: `agents`, `accounts`, `models`, `usage`, `conversations`, `history`. `refresh(name)` takes one of them. |
| `TURN_ENDS` | The turn event types that end a turn (`end`, `done`, `error`), after which the conversation list, the history, usage, accounts and agents are asked again. |
| `PLUGIN_NAME` | `"dotbot"` — the name the consent gate resolves the dialog's display name from. |
| `DEVICE_CODE` / `PASTE_CODE` | `"deviceCode"` / `"pasteCode"` — the two sign-in flows, as `connect.start` names them back. |
Tested by `src/lua/lib/tests/suites/dotbot_exports.testSuite`.
▲ 0↑ born
# source
The DotBot panel's data source — the one seam between the editor panel and the
DotBot agent surface.
**This implementation is a fixture.** Every reading it hands back is a constant
written into its `init.luau`. It talks to no plugin, no provider and no network,
spends nothing, and holds no credential. It exists so the panel can be built and
looked at while the real surface is built elsewhere.
The contract is shaped after the `dotbot.*` trusted exports, so swapping the
fixture for the real surface is a change to this one file:
| Half | Members |
|------|---------|
| Read-only, answered in the frame | `available`, `agents`, `agent`, `terminalAgents`, `accounts`, `accountsFooter`, `models`, `model`, `levelsFor`, `level`, `usage`, `conversations`, `currentConversation`, `agentOfConversation`, `alternativesTo`, `history`, `sendable`, `events` |
| Asking a read again | `refreshConversations`: the conversation list is asked again on its next read. `conversations` and `history` translate each plugin answer once and hand back the same list until the plugin answers again. |
| Start / poll, because a trusted handler may not yield | `connect.start` / `.poll`, `openSignIn.start` / `.poll`, `disconnect.start` / `.poll`, `addAccount.start` / `.poll`, `renameAccount.start` / `.poll`, `send.start` / `.poll` |
| Read-write but unpriced | `setModel`, `setReasoning`, `modelOf`, `levelOf`, `stop` |
| The turn each tab runs, and what waits for it | `submit`, `turnOn`, `turnRunning`, `queued`, `stopTurn`, `unqueue`, `release`, `forgetTurn`, `pollTurns`, `observeTurn` |
| The half only a fixture has | `fixture.enable`, `fixture.scenario`, `fixture.approve`, `fixture.setConnected`, `fixture.dropEventsBefore`, `fixture.reset` |
A `start` answers `{ ok = true, id }` or `{ ok = false, reason = "busy" }`. A
`poll` answers `pending`, `awaitingPerson`, `done`, `refused` or `error`. A
device-code flow carries its code, its page and its expiry on the
`awaitingPerson` answer, because that is what a person has to be shown while the
job waits.
Two `done` answers carry a flag that says the fixture did not do the thing:
`openSignIn` reports `opened = false` with the address it would have opened, and
`send` reports `delivered = false` with the reason. The panel shows both rather
than leaving a button that looks like it worked.
`fixture.enable(false)` takes the data away, and the panel says
"DotBot is not available in this build" instead of drawing a conversation
nobody had.
## One turn per tab
A tab runs one turn at a time, whoever started it: a send from the composer, or
a turn the plugin woke into on its own. `turnOn(tabId)` is that turn's record,
and the Working row, the Stop button and the composer all read it.
`submit` starts a turn on an idle tab; while one runs it queues the message
(`"queue"`, the default) or stops the running turn and runs the message once
that turn has ended (`"interrupt"`). A turn's `end` starts the next queued
message. `stopTurn` holds the queue, so a stopped tab stays stopped until the
person sends again (`release`). `unqueue` takes a queued message back to edit
or drop.
## Scenarios
`fixture.scenario(name)` puts the readings into one state at a time:
`normal`, `fresh`, `notice`, `warning`, `blocked`, `signInExpired`,
`codeExpired`, `codesUnavailable`, `notLinked`.
▲ 0↑ born
# surface
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this module provides and who
consumes it, then delete both comment lines. -->
TODO: describe this module.
▲ 0↑ born
# transcript
The DotBot panel's transcript: a conversation's rows as the scroll area one tab
draws.
```luau
local Transcript = require(".transcript")
local transcript = Transcript.new({
source = Source, -- traceOf / toolsRunIn, to read a tool answer
icons = { down = ..., right = ..., alert = ..., bot = ... },
expanded = expandedRows, -- which rows are open, by row key
onToggle = function() expandEpoch += 1 end,
})
local node = transcript.render(tabId, messages, { conversation = convId })
```
| Member | What it does |
|--------|--------------|
| `PAGE` | 40: the rows a transcript draws before the "Show earlier messages" row, and how many more each press of it draws. |
| `new(deps)` | A transcript over `deps.source`, `deps.icons`, `deps.expanded` and `deps.onToggle`; `deps.pageSize` overrides `PAGE`. |
| `render(tabId, messages, opts?)` | The virtual scroll area holding the newest rows of `messages`, with a "Show earlier messages" row above them when there are more. `opts.conversation` names the conversation (a tab on a new one starts again at one page); `opts.now` is the clock a thinking row's seconds are read against. |
| `showEarlier(tabId)` | Draws one page more of the earlier rows on the next render. |
| `stats(tabId)` | What the last render did: `{ rows, drawn, built, kept, hidden }`. |
## What a redraw costs
Each row is drawn from what it shows: its text, its tool call and answer,
whether it is open, a thinking row's seconds. A row that shows the same as it
did last time keeps the subtree it already has, and its callbacks are carried
into the new build (`edui.app` `collectHandlers` / `adoptHandlers`), so a delta
streaming into the last answer builds that answer and nothing above it. A row
holding a callback with no key is built fresh every time, because an unkeyed
callback is numbered by the order a build reaches it.
Only the newest rows are drawn, so what a redraw costs is set by what is on
screen rather than by how long the conversation is. The earlier rows are one
press away and nothing is dropped.
Tested by `src/lua/lib/tests/suites/dotbot_panel.testSuite`.
▲ 0↑ born
# view
What the DotBot panel says, apart from how it is drawn.
Every function here is pure: it takes readings from `modules.dotbot.source` and
returns the words and the state the panel renders.
| Function | Answers |
|----------|---------|
| `emptyState(availability)` | The title and detail shown when no DotBot surface is present. |
| `hintLine(agent, usage)` | The line under the composer naming what pays and what is left, or nil when there is nothing honest to say. |
| `usageNotice(agent, usage, models)` | The banner above the composer: a notice under 25% left, a warning under 10% offering a cheaper model only when that model is one the agent can run, and a blocked state naming the reset. |
| `problemNotice(agent)` | The banner a failed sign-in puts above the composer. |
| `composerState(availability, agent, usage)` | Whether the field takes typing, what it says while empty, and whether it carries the model and thinking chips. |
| `connectSheet(agent, flow)` | The sheet a disconnected, expired or unlinked agent shows in place of a conversation. |
| `connectedNotice(agent, account)` | The line a freshly-connected agent shows above its first turn. |
| `metaLine(meta)` / `metaWho(meta)` | The per-turn meta line: model, thinking level, tokens, tool calls, duration — and who paid. |
| `pickerSubtitle(agent)` / `pickerAction(agent)` | What an agent's row in the picker says, and the word on its button when it needs one. |
| `rowKey(row)` / `mergeRows(history, live)` | A transcript row's identity (a tool row is its call, a thinking row or usage line is itself) and the merge of stored history with the live rows, which shows the overlap once. A history list is keyed once, when it first reaches `mergeRows`, so a merge on every redraw costs the live rows. |
| `groupCarrierRows(rows, toolsRunIn)` | Runs of `tool_exec` calls collapsed into one row naming the tools they ran. |
| `thinkingLabel(row, now)` / `finishThinking(row, now)` / `foldThinking(rows, from)` / `thinkingRow(...)` / `toggleOpen(state, key)` | Thinking as a folded row: its header, its clock, the fold of one turn's phases into one row, and the open/closed state. |
| `splitMarkdown(md)` / `tableNode(...)` / `tableWeights(...)` / `codeNode(...)` / `markSelectable(node)` | An answer's tables and fenced code lifted out of the markdown and drawn so they wrap, and every piece of an answer marked selectable. |
| `imageNode(P, key, path, exists)` | A tool's picture, or the line saying it is no longer available. |
| `linkText(id, label, onClick)` | Text that looks like a link and acts like one, its callback keyed by `id`. |
The thresholds the usage ladder turns on are `noticeThreshold` (25) and
`warnThreshold` (10).
▲ 0↑ born
backing path · dotbot.plugin
Problems
Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.
0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up
agent_score is exposed.usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor
+ quality × 0.35
+ performance × 0.25
± compat factor
Usability ratings
Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".
—%no reports yet
Sign in to report whether this part worked for you.
Discussion
Scoped to this part · feeds back into the world's score.
0comments
Sign in to post.sign in
No comments yet. Be the first.