The DotBot panel on edui: one dock tab, in the right dock under the Agents
menu beside the terminal agents. It carries a toolbar, a transcript of user
turns, thinking, tool calls, the answer and a per-turn meta line, a
composer with the account, model and thinking chips, the connected
accounts view, the connect flows, the usage notice / warning / blocked
states, and the failure states.
Which account a conversation runs on is a setting of the conversation,
shown on the composer's account chip and changed from it: OrigoZero
credits, or any Claude or ChatGPT account connected to DotBot. Each account
is one of the plugin's agents (`claude` on the first Claude account,
`claude~2` on the second), and moving a conversation to another account is
sending its next turn as that account's agent.
There is deliberately NO permanent line under the composer. It read
"Runs on your ChatGPT plan - 4% of the weekly window left", and both
halves were already on screen: every turn's meta line names who paid, and
the usage banner names what is left. Two permanent copies of one fact is a
fixture a person learns to read past.
Everything the panel shows comes from `modules.dotbot.source`, and every
word it puts on screen for a reading comes from `modules.dotbot.view`.
The panel holds only what is on screen right now — which view, which popup
is open, how far a connect flow has run, and the unsent draft.
shortCount(n: number) → string
A count a person reads at a glance: `9829`, `43.2k`, `1.4M`.
| arg | type | description |
|---|
| n | number | |
rowsFor(tabId: string) → void
| arg | type | description |
|---|
| tabId | string | |
freezeHistory(tabId: string, conversationId: string?, agentId: string?) → void
Take the history snapshot this tab's transcript sits on for the next turn.
Called when a turn STARTS, which is the only moment the prefix is allowed
to move.
| arg | type | description |
|---|
| tabId | string | |
| conversationId | string? | |
| agentId | string? | |
storedRows(tabId: string, conversationId: string, agentId: string) → void
The stored rows the transcript draws under the live ones: the snapshot while
a turn is running on this conversation, and whatever the plugin holds
otherwise.
| arg | type | description |
|---|
| tabId | string | |
| conversationId | string | |
| agentId | string | |
startNewChat(tabId: string, agentId: string) → void
Start a fresh conversation in `tabId`: the ONE place that says what "new
chat" means, so the button and the menu item cannot mean different things.
The live rows are the part that was being missed. They are the tail the
transcript draws under whatever history it loaded, so clearing the
conversation id without clearing them left the last turn's messages sitting
in the "new" chat — the button looked broken, and the rows it stranded then
merged against the next conversation's history, which is what put half a
transcript on screen.
@param agentId the agent whose conversation is being replaced
| arg | type | description |
|---|
| tabId | string | |
| agentId | string | |
agentForAccount(account: string) → string
The panel id of the agent that runs on `account`, an account key
(`anthropic-claude~2` -> `claude~2`), or nil when the plugin lists no agent
of that provider.
| arg | type | description |
|---|
| account | string | |
agentNamed(agentId: string) → any
The agent `agentId` names, also when it is an account the plugin no longer
lists (one disconnected while a conversation was on it): that is drawn as
the provider's account, not connected, so the panel asks for one rather
than moving the conversation somewhere nobody chose.
| arg | type | description |
|---|
| agentId | string | |
currentAgentId(tabId: string) → string
The agent `tabId`'s conversation runs on: the one the person chose (or the
conversation's own turns ran on) in this panel, else the one the plugin
recorded for the conversation, else the first connected agent, else the
first the plugin lists.
| arg | type | description |
|---|
| tabId | string | |
switchAgent(tabId: string, agentId: string) → void
Put `tabId`'s conversation on `agentId`. The turn running now finishes
where it is; the next one runs on the new account, and the conversation
says so where the next turn will appear. A conversation keeps its history:
the agent it moves to reads it as it stands.
| arg | type | description |
|---|
| tabId | string | |
| agentId | string | |
closeRow(row: any) → void
Stop a row streaming. A thinking row is timed from its first token to here,
which is the time the model spent thinking and not the time the whole turn
took.
appendText(tabId: string, kind: string, text: string) → void
| arg | type | description |
|---|
| tabId | string | |
| kind | string | |
| text | string | |
closeStream(tabId: string) → void
| arg | type | description |
|---|
| tabId | string | |
lastRowLength(tabId: string) → number
How long the last transcript row's text is, for the panel's memo.
| arg | type | description |
|---|
| tabId | string | |
tabForTurn(payload: any) → string
Which tab a turn event belongs to.
By the turn id first — that is the direct link between the `send` the tab
started and everything that turn says. A tab that has a send in flight but
has not yet read the id back matches next, because an event can arrive in
the same drain that learns it. An event belonging to no tab is dropped: it
is a turn this panel did not start.
| arg | type | description |
|---|
| payload | any | |
foldTurnEvent(payload: any) → void
Fold one `turn` event into the tab's rows.
Only the shapes the transcript draws are read. An event this does not know
is dropped rather than rendered as its own JSON: a panel that prints
`pipeline_hook_start` at a person is noise, and a turn that gains a new
event type must not make the transcript unreadable.
| arg | type | description |
|---|
| payload | any | |
awaitProvider(f: any, answer: any) → void
Put a flow onto the provider's own page: the code to type, or the page to
approve on and paste back from, as the job answered it.
| arg | type | description |
|---|
| f | any | |
| answer | any | |
flowOf(provider: string) → any
How far the sign-in of `account` (an account key) has run.
| arg | type | description |
|---|
| provider | string | |
popKey(tabId: string, which: string) → string
| arg | type | description |
|---|
| tabId | string | |
| which | string | |
togglePop(tabId: string, which: string) → void
| arg | type | description |
|---|
| tabId | string | |
| which | string | |
drainEvents( ) → void
Drain everything the plugin has said since the last draw.
The panel is the only drainer, and one cursor serves every tab: the ring is
the person's, not the tab's, and a second cursor would mean one tab's draw
eating the events another tab had not read yet.
pump( ) → void
Run every job a flow is waiting on one step further.
**Called from `build`, BEFORE the panel's own memo check** — never from
`buildTab`. `build` answers `nil` when nothing it draws has changed, and a
pump behind that check is a pump that only runs once something else has
already moved. Nothing else does: a turn's whole life is a job this has to
poll and a ring it has to drain, and neither changes the signature until this
has run. The panel froze on "Sending…" while the turn it was waiting for had
already finished and been paid for.
Jobs first, then the ring: polling a send is what learns the turn id, and
`tabForTurn` needs it to file the events that same drain is about to hand it.
startJob(provider: string, kind: string, ...: ?) → void
| arg | type | description |
|---|
| provider | string | |
| kind | string | |
| ... | ? | |
startAddAccount(tabId: string, provider: string) → void
Connect a further account of `provider` and put `tabId`'s conversation on
it once its sign-in starts. The accounts already connected stay connected.
| arg | type | description |
|---|
| tabId | string | |
| provider | string | |
pill(text: string, color: string, borderColor: string) → any
A pill naming a state in its own hue, the shape edui's type badge uses.
| arg | type | description |
|---|
| text | string | |
| color | string | |
| borderColor | string | |
banner(o: { [string]: any }) → any
| arg | type | description |
|---|
| o | { [string]: any } | |
richMenu(o: { [string]: any }) → any
A popup whose rows carry a title over a subtitle, which `edui.menu` has no
shape for. The frame matches edui's own menu so the two read as one language.
| arg | type | description |
|---|
| o | { [string]: any } | |
sheet(children: { any }) → any
The card a connect flow, a failure or the accounts view puts in the body.
| arg | type | description |
|---|
| children | { any } | |
chip(o: { [string]: any }) → any
| arg | type | description |
|---|
| o | { [string]: any } | |
sendDraft(tabId: string, agent: any, how: string?) → void
Send whatever is in the composer. The ONE send path: the button and the
Enter key both call this, so a message cannot behave differently depending
on which one a person used.
While a turn runs on the tab the message does not start a second one beside
it: by default it waits in the tab's queue and starts the next turn when
this one ends, and `how = "interrupt"` stops the running turn and runs the
message as soon as that turn has ended.
| arg | type | description |
|---|
| tabId | string | |
| agent | any | |
| how | string? | |
composerButton(id: string, icon: string, tooltip: string, onClick: (() → void
A small square icon button in the composer row.
| arg | type | description |
|---|
| id | string | |
| icon | string | |
| tooltip | string | |
| onClick | (( | |
queueRows(tabId: string, agent: any) → void
The messages waiting on a tab, above its composer: each one can be taken
back into the composer to edit, dropped, or (once a Stop held it) sent.
| arg | type | description |
|---|
| tabId | string | |
| agent | any | |
onEnter(v: ?) → void
Enter sends. The field still reports every keystroke, so the
Send button beside it keeps working; this is the same send,
reached by the key people actually press.
sheetBody(tabId: string, agent: any, spec: any) → any
| arg | type | description |
|---|
| tabId | string | |
| agent | any | |
| spec | any | |
act(kind: string) → void
| arg | type | description |
|---|
| kind | string | |
providersListed( ) → void
The subscription providers the plugin lists, once each, in its order, with
the name a person reads for each.
connectAccount(tabId: string, acc: any) → void
Put `tabId`'s conversation on the account `acc` and start its sign-in, in
place: the conversation shows the sign-in's own sheet.
| arg | type | description |
|---|
| tabId | string | |
| acc | any | |
accountsBody(tabId: string) → any
| arg | type | description |
|---|
| tabId | string | |
accountPickerRows(tabId: string, agent: any) → void
The account picker: every account DotBot can run a conversation on, the
one it is on now ticked, then a further account of each provider, then the
accounts view.
| arg | type | description |
|---|
| tabId | string | |
| agent | any | |
unavailable(av: any) → any
buildTab(tabId: string, agentId: string) → any
| arg | type | description |
|---|
| tabId | string | |
| agentId | string | |
signature(tabId: string, agentId: string) → string
Everything a tab draws, in one string. Same string, same picture.
| arg | type | description |
|---|
| tabId | string | |
| agentId | string | |
menuHint( ) → void
The short, right-aligned note the Agents menu shows beside the entry:
the state of the account the open conversation runs on.