view
What the DotBot panel says, apart from how it is drawn.
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).
Interface
What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.
conforms to
zero/source-extract/v2What 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()`.
| arg | type | description |
|---|---|---|
| availability | any |
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.
| arg | type | description |
|---|---|---|
| agent | any | |
| usage | any |
scalarText(v: any) → string
| arg | type | description |
|---|---|---|
| v | any |
isArray(t: any) → boolean
| arg | type | description |
|---|---|---|
| t | any |
summaryOf(v: any) → string
What a table is worth saying when there is no room to open it: a count, never a stringified table.
| arg | type | description |
|---|---|---|
| v | any |
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.
| arg | type | description |
|---|---|---|
| value | any | |
| depth | number | |
| out | { DataLine } | |
| label | string? |
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.
| arg | type | description |
|---|---|---|
| value | any |
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.
| arg | type | description |
|---|---|---|
| agent | any | |
| usage | any | |
| 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.
| arg | type | description |
|---|---|---|
| availability | any | |
| agent | any | |
| usage | any |
problemNotice(agent: any) → Notice
The banner a problem puts above the composer, or nil when there is none.
| arg | type | description |
|---|---|---|
| agent | any |
metaLine(meta: any) → string
The per-turn meta line's left half: model, thinking level, tokens, tool calls and duration, separated by middle dots.
| arg | type | description |
|---|---|---|
| meta | any |
metaWho(meta: any) → string
Who paid for a turn — the right half of the meta line.
| arg | type | description |
|---|---|---|
| meta | any |
pickerSubtitle(agent: any) → string
What the picker shows under an agent's name.
| arg | type | description |
|---|---|---|
| agent | any |
pickerAction(agent: any) → string
The word on an agent's picker row when it needs an action, or nil.
| arg | type | description |
|---|---|---|
| agent | any |
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.
| arg | type | description |
|---|---|---|
| url | any |
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.
| arg | type | description |
|---|---|---|
| id | string | |
| label | string | |
| 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.
| arg | type | description |
|---|---|---|
| text | string | |
| size | number | |
| spacing | string | |
| pad | number |
connectSheet(agent: any, flow: any, link: any?) → any
| arg | type | description |
|---|---|---|
| agent | any | |
| flow | any | |
| link | any? |
connectedNotice(agent: any, account: any) → string
The line a freshly-connected agent shows above its first turn, or nil.
| arg | type | description |
|---|---|---|
| agent | any | |
| account | any |
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.
| arg | type | description |
|---|---|---|
| row | any |
historyIndex(history: { any }) → void
| arg | type | description |
|---|---|---|
| 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.
| arg | type | description |
|---|---|---|
| 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.
| arg | type | description |
|---|---|---|
| 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`.
| arg | type | description |
|---|---|---|
| row | any | |
| now | number? |
finishThinking(row: any, now: number) → void
Close a thinking row: stop it streaming and fix how long it took.
| arg | type | description |
|---|---|---|
| row | any | |
| now | number |
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.
| arg | type | description |
|---|---|---|
| state | { [string]: boolean } | |
| key | string |
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.
| arg | type | description |
|---|---|---|
| rows | { any } | |
| from | number? |
trimmed(line: string) → string
| arg | type | description |
|---|---|---|
| line | string |
isTableLine(line: string) → boolean
| arg | type | description |
|---|---|---|
| line | string |
isTableSeparator(line: string) → boolean
A table's separator line: `|---|:---:|`. Only dashes, colons, pipes and spaces, with at least one dash.
| arg | type | description |
|---|---|---|
| line | string |
tableCells(line: string) → void
The cells of one table line, without the outer pipes.
| arg | type | description |
|---|---|---|
| line | string |
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.
| arg | type | description |
|---|---|---|
| md | string |
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.
| arg | type | description |
|---|---|---|
| header | { string }? | |
| rows | { { string } } |
see(cells: { string }?) → void
| arg | type | description |
|---|---|---|
| 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.
| arg | type | description |
|---|---|---|
| P | any | |
| header | { string }? | |
| rows | { { string } } | |
| inline | (string |
rowNode(cells: { string }, isHeader: boolean, striped: boolean) → any
| arg | type | description |
|---|---|---|
| cells | { string } | |
| isHeader | boolean | |
| striped | boolean |
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.
| arg | type | description |
|---|---|---|
| node | any |
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.
| arg | type | description |
|---|---|---|
| P | any | |
| text | string |
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.
| arg | type | description |
|---|---|---|
| P | any | |
| key | string | |
| path | string | |
| exists | boolean |
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.
Problems
Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.
agent_score is exposed.+ 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".
Scoped to this part · feeds back into the world's score.