Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofplugin dotbot.plugin·originates fromworld abf1a532-5…

view

What the DotBot panel says, apart from how it is drawn.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 20d ago
What it does

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.

FunctionAnswers
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).

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

What the DotBot panel says, apart from how it is drawn. Every function here is pure: it takes readings from the panel's data source and returns the words and the state the panel renders. The hint line under the composer, the usage notice above it, whether the field takes typing and what its placeholder says, the per-turn meta line, and what the panel says when there is no data source at all — each is a value a test can read, rather than a string buried in a widget tree.

emptyState(availability: any) → void

What the panel shows in place of a conversation when no DotBot surface is present. `availability` is the source's `available()`.

argtypedescription
availabilityany

usageLine(agent: any, usage: any) → string

**How much is left, permanently, under the composer.** The AMOUNT and nothing else. It used to open with "Runs on your ChatGPT plan", which every turn's own meta line already says, so who pays was on screen twice; this carries the figure and lets the meta line carry the payer. Removing the whole row instead was the wrong half to cut: the figure is the part somebody wants standing there, and the duplicate was the sentence around it. Nil when there is nothing honest to say: an agent nobody connected, or one with a problem in front of it, has no reading to report.

argtypedescription
agentany
usageany

scalarText(v: any) → string

argtypedescription
vany

isArray(t: any) → boolean

argtypedescription
tany

summaryOf(v: any) → string

What a table is worth saying when there is no room to open it: a count, never a stringified table.

argtypedescription
vany

describeInto(value: any, depth: number, out: { DataLine }, label: string?) → void

Render a decoded value as lines a person reads. **JSON pretty-printed is still JSON.** The braces, the quotes and the commas are three quarters of the characters and none of the content, and a grey block of them is unreadable however neatly it is indented. So nothing here prints punctuation: a record is its name and then its fields, a list is its items and then how many were not shown, and a nested table too deep to open says how big it is instead of being flattened into a string.

argtypedescription
valueany
depthnumber
out{ DataLine }
labelstring?

describeValue(value: any) → void

A decoded tool answer as lines a person reads. The panel's entry point: one call, one list of lines. Exported from here rather than kept in the panel so it can be tested against the shapes the engine really answers with, which is the only way a claim about how a result READS can be checked at all.

argtypedescription
valueany

usageNotice(agent: any, usage: any, models: { any }?, alternatives: { any }?) → Notice

The banner above the composer, or nil when the reading warrants none. `models` is the agent's model list; a cheaper model is offered only when it is one of them, so the panel never points at a model it cannot run. `alternatives` are the connected accounts with room left (the source's `alternativesTo`): a spent window names them and offers the first.

argtypedescription
agentany
usageany
models{ any }?
alternatives{ any }?

composerState(availability: any, agent: any, usage: any) → Composer

Whether the composer takes typing, what it says while empty, and whether it carries the model and thinking chips.

argtypedescription
availabilityany
agentany
usageany

problemNotice(agent: any) → Notice

The banner a problem puts above the composer, or nil when there is none.

argtypedescription
agentany

metaLine(meta: any) → string

The per-turn meta line's left half: model, thinking level, tokens, tool calls and duration, separated by middle dots.

argtypedescription
metaany

metaWho(meta: any) → string

Who paid for a turn — the right half of the meta line.

argtypedescription
metaany

pickerSubtitle(agent: any) → string

What the picker shows under an agent's name.

argtypedescription
agentany

pickerAction(agent: any) → string

The word on an agent's picker row when it needs an action, or nil.

argtypedescription
agentany

linkPlace(url: any) → string

The sheet a disconnected or blocked agent shows in place of a conversation: a heading, the paragraphs under it, and the button that moves it on. Nil when the agent has a conversation to show instead. Where a link code is entered, as a person would type it: the URL's host and path, with no scheme and no query.

argtypedescription
urlany

linkText(id: string, label: string, onClick: () → void

Text that looks like a link and acts like one. There is one of these rather than an `edui.text` in link blue at each site, because the two halves drifted apart the moment they were written separately: the sign-in sheet painted "Open sign-in again" in `#3794ff` and attached no handler, so the only way back out of a stale paste step was a blue word that did nothing. Taking the callback as a required argument is what stops that shape existing: there is no way to ask for the colour without saying what the click does. `flexShrink = 0` so it keeps its width beside a growing sibling — a link crushed to "Open sig…" is the other way this reads as broken.

argtypedescription
idstring
labelstring
onClick(

codeBox(text: string, size: number, spacing: string, pad: number) → any

The box a sign-in code is drawn in. Selectable, because the code is copied into another page: highlighted and copied with Ctrl+C.

argtypedescription
textstring
sizenumber
spacingstring
padnumber

connectSheet(agent: any, flow: any, link: any?) → any

argtypedescription
agentany
flowany
linkany?

connectedNotice(agent: any, account: any) → string

The line a freshly-connected agent shows above its first turn, or nil.

argtypedescription
agentany
accountany

rowKey(row: any) → string

One row's identity, for deciding whether two lists hold the same row. **A row is not its text.** The key was the kind, the tool's name and the trimmed text, which for a tool row, a meta line and a thinking row is nothing at all: two `tool_exec` calls, or two turns' usage lines, had the same key. Merged against the stored history they collapsed into one, so the second turn's tool rows and usage line simply did not appear. A row that carries an identity of its own is keyed by it: * a tool row by the call's id, which the live event and the stored `tool_use` block both carry; * a live-only row (thinking, the usage line) by the `uid` it was given when it was made. Everything else is what a person would call the same message: its kind and its text.

argtypedescription
rowany

historyIndex(history: { any }) → void

argtypedescription
history{ any }

mergeRows(history: { any }, live: { any }) → void

What the plugin has stored, then what this session watched run, WITHOUT showing the overlap twice. History keeps its order, and a live row is shown only if history does not already hold one with the same identity (`rowKey`). Two identical messages a person really did send both survive, because both are in the history by then.

argtypedescription
history{ any }
live{ any }

groupCarrierRows(messages: { any }, toolsRunIn: (any) → void

Collapse runs of carrier calls into one row that names what actually ran. **Six rows saying `tool_exec` tell a person nothing.** Each one is a script that called real tools, and the answer carries the trace of which. So a run of consecutive carrier rows becomes a single row carrying every tool those calls ran, in order, and every other row is passed through untouched. A carrier call that ran no tools, or whose answer cannot be read, keeps its own row and its own name: there is nothing better to call it than what the model called it, and inventing a label for an unreadable answer would be inventing the contents of the answer. `toolsRunIn` is the source's reader of an answer's trace, passed in so this stays pure.

argtypedescription
messages{ any }
toolsRunIn(any

thinkingLabel(row: any, now: number?) → string

What a thinking row's header says. **Thinking is never hidden, only folded.** While the model is thinking the row says so (`Thinking…`, with how long it has been going, when the row knows when it started); once it has finished it says how long it thought. The text itself is behind the chevron either way. `now` is a clock reading in seconds from the same clock as `row.startedAt`.

argtypedescription
rowany
nownumber?

finishThinking(row: any, now: number) → void

Close a thinking row: stop it streaming and fix how long it took.

argtypedescription
rowany
nownumber

toggleOpen(state: { [string]: boolean }, key: string) → boolean

Flip one entry of an open/closed table and answer the new state. The panel keeps which rows are open OUTSIDE the rows, keyed by a stable identity, because a transcript is recomposed from history plus live events on every redraw and a flag stored on a row is lost the moment anything streams.

argtypedescription
state{ [string]: boolean }
keystring

foldThinking(rows: { any }, from: number?) → void

A turn's thinking rows folded into ONE, once the turn has ended. What it thought is worth watching while it thinks and worth little afterwards: a turn with eight tool calls has up to eight thinking phases, and eight "Thought for 2 s" rows between the question and the answer are clutter. They become a single row at the place the first one stood, carrying ALL of the thoughts (joined with a blank line) and the total time spent thinking, so folding loses nothing and the row stays unfoldable. It keeps the first row's `uid`, so a row the reader had open is still the one that is open. **Only the turn that ended is folded.** The live rows hold every turn this session watched, one after another, so folding "the thinking rows" folded every turn's thinking into the first turn's row and left the later turns with none. `from` is where the ended turn's rows begin (the row after the previous turn's usage line); rows before it are left exactly as they were. Returns the new list and the uids of the rows that were merged away.

argtypedescription
rows{ any }
fromnumber?

trimmed(line: string) → string

argtypedescription
linestring

isTableLine(line: string) → boolean

argtypedescription
linestring

isTableSeparator(line: string) → boolean

A table's separator line: `|---|:---:|`. Only dashes, colons, pipes and spaces, with at least one dash.

argtypedescription
linestring

tableCells(line: string) → void

The cells of one table line, without the outer pipes.

argtypedescription
linestring

splitMarkdown(md: string) → void

An answer's markdown split into the parts the panel draws itself and the prose the shared renderer draws. **The shared renderer cannot be left to draw these two.** It draws a table as raw `| a | b |` lines in monospace that run past the panel's right edge, and a fenced code block as one unwrapped run that is clipped or overflows. Neither can wrap, and neither can be selected. So they are lifted out here: * `{ kind = "prose", text }`: handed to the renderer as it was; * `{ kind = "code", lang, text }`: a fenced block, an unterminated one (an answer still streaming) included; * `{ kind = "table", header = {cells}?, rows = {{cells}} }`: a run of lines that start with a pipe. The header is the first row when the second is a separator; a run with no separator is all rows.

argtypedescription
mdstring

flushProse( ) → void

tableWeights(header: { string }?, rows: { { string } }) → void

How much of a table's width each column asks for. Proportional to the longest cell in it, within bounds: a column of one-word labels does not take as much as a column of sentences, and a column of long sentences cannot take the table whole. Every column can shrink to nothing, so the table is never wider than the panel whatever its cells hold.

argtypedescription
header{ string }?
rows{ { string } }

see(cells: { string }?) → void

argtypedescription
cells{ string }?

tableNode(P: any, header: { string }?, rows: { { string } }, inline: (string) → void

A table as a widget: one wrapping row per table row, every cell a box that may grow and may shrink and starts from nothing (`flexBasis = 0`), so the row is exactly the panel's width however long a cell's text is and a long cell wraps inside its own column.

argtypedescription
Pany
header{ string }?
rows{ { string } }
inline(string

rowNode(cells: { string }, isHeader: boolean, striped: boolean) → any

argtypedescription
cells{ string }
isHeaderboolean
stripedboolean

markSelectable(node: any) → void

Mark every piece of text under `node` selectable: styled runs (`richText`) and plain labels (headings, quotes) alike. The shared markdown renderer builds an answer out of those widgets, and each takes part in text selection only when it says so with `props.selectable`. Selection then reaches every piece of an answer — paragraphs, list items, headings — the way it reaches a plain label, and one drag can cross from one to the next. Walks `children`, so a list item's run inside its row is marked too.

argtypedescription
nodeany

codeNode(P: any, text: string) → any

A fenced code block as a widget: monospace text that WRAPS at the panel's edge and can be selected and copied. A block of code is the thing a person most often copies out of an answer.

argtypedescription
Pany
textstring

imageNode(P: any, key: string, path: string, exists: boolean) → any

What a tool row shows for a picture its answer named. The picture is a file, and the file may be gone: a capture from an earlier session, written under a scratch folder that was cleared since. Drawn as an image the transcript showed a broken tile and said nothing. When the file is not there the row says so instead.

argtypedescription
Pany
keystring
pathstring
existsboolean
⌬ Types
Notice = {Composer = {DataLine = { depth: number, label: string?, text: string, dim: boolean }

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

8+items
▣
module · born here
❒asset
# 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
·
plugin · born here
❒asset
# dotbot What this plugin does and who it is for, in a paragraph. ## Use ```luau local dotbot = require("dotbot") local answer = await(dotbot.doSomething("x")) ``` ## Methods - `doSomething(input)`: what it does, and what it answers.
▲ 0↑ born
backing path · dotbot.plugin/view.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬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

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.