Repository navigation
Let a person see every task by what it needs from them - #918
Draft
jerelvelarde wants to merge 70 commits into
Draft
jerelvelarde wants to merge 70 commits into
jerelvelarde wants to merge 70 commits into
Conversation
The last two places still drawing boring-avatars' gradient circles now draw the orb in the colour the Bot's id picks, so a Bot looks the same in the sidebar, the conversation header and its profile. Nothing imports boring-avatars any more, so it leaves the app's dependencies.
…ll when asked A Bot's orb is a block, so a group conversation's 12px faces no longer sit on the header's text baseline and drop half out of their circles. It resizes its canvas when the screen's pixel density changes, so dragging a window to another screen or zooming no longer leaves it soft, and it follows the reduced-motion setting live rather than only when it first draws. The changelog and the stylesheet now describe what this step draws, not what later steps will.
The skins import the orb's state type from agent-orb now that ai-core is gone, OpenBot's mark keeps its agent-orb class for them, and their tests check the orb's colour rather than boring-avatars' SVG. The Appearance test's preferences carry the new skin field.
AgentOrb read "100%" as 100px, so with no Bot yet the header drew a 100px orb in a 44 or 52px slot. With a Bot, the classes that used to rescale boring-avatars' SVG squeezed the avatar's circle to 44px below 900px wide while the orb inside stayed 52px, clipped and off centre. The frame now picks 44 or 52px from the window's width and draws the face at that size, the selectors that matched only the old SVG are gone here and from OpenDots' Bot rows, and AgentOrb takes its size only in px and refuses anything else.
Every orb on screen redrew on every animation frame, uncapped, so a sidebar of conversations kept a few dozen canvases repainting at the screen's full rate while the app sat idle: a small orb's colour still drifted, so no two frames were ever the same. A small orb's colour now holds still, an orb skips any frame that would draw what its canvas already shows in the same theme and at the same density, and a small orb draws at most about thirty frames a second. A face at rest with the pointer still now draws only to blink.
…violet A Bot's face was one of nine colours and nothing else, so a team of four had about an even chance of two identical faces. In the Bots OpenBot ships with, three were the same sky blue, and release notes wore violet, OpenBot's own mark. A Bot is now drawn from its seed: one of the eight colours that aren't violet, with its colour masses turned, its hue nudged and its eyes spaced by the same hash, so two Bots in one colour still look apart. BotOrb takes the seed rather than a colour and draws OpenBot's violet when it has none. The OpenDots avatar test checked one colour in nine, so it passed or failed on the hash; it now checks that the seed reaches the orb.
GET /api/setup says, row by row, whether Intelligence, the model, the Bot computer, more apps and Slack and Teams work here, from their own answers rather than from a variable being set. POST /api/setup/check asks them again. An admin, or somebody running OpenBot alone, is told the page or the variable that closes a gap; everybody else is told only that it works or that their admin sets it up, and their response carries no variable name.
Onboarding is the only page somebody who has not finished it may open, so a consent started on its Connect your apps step returns to that step, worked or not, for OAuth and brokered apps alike. A consent cancelled at the vendor now returns to the screen it started on too, instead of the Connections list. The returnTo name is still narrowed to a closed set before it is sealed or used.
A step-by-step onboarding frame that works on a desktop screen too. One DOM, laid out by CSS: from lg up, the steps as a list (the ones reached can be picked) with the step's heading, sentence and Continue/Back on the left, and its picture filling the right; below lg, a dash for each step, a round Back and the buttons pinned to the bottom. Focus moves to each new heading. A demo's clock runs only while it is on screen in a shown tab, and holds still with reduced motion.
Welcome types a request under OpenBot's listening orb, a computer of its own shows the Bot's desktop with its pointer on an errand, the team shows three Bots' orbs taking turns at work, and an approval card waits for Allow once before it sends. Somebody running OpenBot alone also sees it on a laptop, with the agents they have, and for their organization. Each is a picture, read by its label, with nothing inside to click.
Connect your apps lists the apps the person's default Bot uses as them and connects the ones with a sign-in page right there, coming back to the step; a brokered consent is asked of the vendor on return, so the row turns Connected only when it is. Get notified previews the notification OpenBot really sends and asks the browser, sharing its answer with a Bot's page. Check your setup draws the server's checklist with a status pill per row, and Check again.
First run is now welcome, a browser and files of its own, the team of Bots, asking before it acts, running it yourself (only for somebody trying OpenBot alone), connect apps, get notified, check your setup, and the name step. Where somebody is lives in the address and is saved on every move, so a reload, browser Back and a consent's return keep their place. The name step stays last, under the orb: Get started saves the name, completes onboarding and plays the orb's finish, and a checklist link carries its page through it, since nothing else opens until onboarding is done.
…ws now The root's drop guard named the onboarding poster's composer, which is now a drawing inside onboarding's picture frame. The changelog says the computer check also runs once at start-up.
Found running the steps against a real install. Connect apps told an admin to wait for an admin; it now offers Plugins, carried through the name step like the checklist's links. The checklist's "Required" tag ran into each title, and the notification preview cut "General Assistant needs you" off at the Bot's name.
…s written out Setting OPENAI_BASE_URL to https://api.openai.com/v1 counted as a custom endpoint, so the check was skipped and the setup checklist said the model was not checked. The provider's own host is now checked as if no address were set.
…the box The step promised that a Bot asks before it sends, buys or deletes anything. Ask before making changes starts off, so out of the box a Bot asks only where a rule says to, and paying or changing account security is handed to the person. The step now says that, and where to turn asking on.
With AGENT_COMPUTER_URL set and no computer running, the usual state and always true on a first run, the row told an admin Bots can't browse the web and to set the variables and restart, for something already set. Not checked now says it is set up and none is running to check, and opens Computers.
Intelligence, model and computer answers were asked at boot and then served for as long as the server ran. An Intelligence slower to start than OpenBot read as a red required gap until somebody pressed Check again, and a key refused days after boot still read Ready. A read now asks again, in the background and under the shared 10-second limit, when an answer failed or is five minutes old, so the next read has it. Reads are answered from the last answers that are all in, so none waits on a slow fleet.
The changelog said the intro was replaced by another product's steps, and two comments cited it, which reads as a skin being added. They now say what OpenBot does. The changelog also said a cancelled connect returns to where it started from anywhere; only Connect your apps changed, so it says that.
At 1280 by 800 the self-hosting step and the name step ran past the bottom of the window, cutting Continue and Back in half; at 700 tall, four steps did. The left pane stacked the nine-step list, the step's words and its buttons, and the page was left to scroll. On a desktop the frame is now one screen tall and the step list's row is the only one that can shrink, so the step's words and its buttons always keep their full size. When the screen is too short for all of it, the list scrolls within itself, fades at the edge it scrolls under, and keeps the step on screen in view. Its rows and the space under it are a little tighter, so every step still fits whole at 1280 by 800. Only a window too short for the words and buttons themselves scrolls the page. The picture pane now stretches to the screen instead of being set to its height: a set height spanning every row gave that height to the empty last row before the list got any. It drops the phone's margin on a desktop, which would stop it short of the bottom on the name step.
Rows, destinations and the header's controls lift to the card's fill and shadow under the pointer and while open, with a visible focus ring. Row text is on type-sub and type-caption, and the unread dot is the brand colour. OpenBot's orb sits beside the product name, and the + button's tooltip names its shortcut. A busy conversation's orb works, and a still dot in the working colour says so as well: an orb under 40px only blinks, and reduced motion stills it. ChannelAvatar takes an optional state; its props are otherwise unchanged. Each conversation row has a "…" button (SidebarMenuAction showOnHover) with the Pin and Delete that were on right-click alone. It shows under the pointer and with focus in the row, and comes straight after the row in tab order. NavigationItem gains an optional badge, a count the sidebar draws beside the entry, set by useNavigationItems, which the sidebar now reads. Primary entries draw in list order, so a new destination and its count are added in lib/navigation.ts alone. ⌘K or Ctrl+K opens the sidebar, or its sheet on a narrow window, and focuses its search box. Settings lists it from the hotkey registry.
A Bot's reply is drawn one bubble per thought (split-bubbles.ts), each
bubble memoised so a streamed reply parses only the part still arriving.
A bubble holding a code block or a table takes the column's width from md
up; the rest keep an 82% cap. The person's messages sit in a tinted bubble,
and Thinking is three dots in the Bot's bubble, still with reduced motion.
New --bubble, --bubble-user and --radius-bubble tokens carry the colours
and the shape, and their words clear 4.5:1 in both themes.
Tool lines draw a hook, an outcome mark and a Tabler icon, and only a line
that worked carries the done mark. A call that was refused, failed or
declined is named for what it would have done, a declined one reads "You
declined" (toolResultDeclined) and is not counted as an issue, a group
where nothing worked never claims it did, and a step held on its approval
card reads "Waiting for you". Mail and calendar calls through any app are
named for what happened to the person's own ("Searched your email"), by
their words rather than a table of one Gmail plugin's tools.
The Bot's bubble stays the ghost variant, which the skins find a Bot's
words by, and OpenDots takes the bubble off again to keep its plain prose.
A Bot's page names its tools by what they do ("Send your email").
A conversation with one Bot shows that Bot's orb in its header, at 40px,
with a line under the conversation's name. The orb works while the Bot has
the turn, listens while the person types, and lands an answer (not a
failure or a Stop). The line says "Working · Searching the web" while a
step runs, "Needs you" while an approval waits in that conversation, and
nothing at rest. The conversation list's busy flag counts as working, so a
run the server resumes after Allow once shows as "Working"; the flag of
the tab's own turn does not outlast the turn. Groups keep their own header.
The state lives in components/channels/conversation-presence.tsx, a store
the conversation route provides and ConversationView writes. Each reader
subscribes to the fields it uses, so a keystroke reaches the orb and not
the line, and a new step reaches the line and not the orb. Reports carry
the conversation they came from, so the route, which stays mounted from
one conversation to the next, never shows the last one's turn.
The step comes from lib/channels/step.ts (stepFor, stepPhrase) in the
running words the step's own tool line uses. An app's call it cannot name
by what it does reads "Using <app>"; a question for the person and a
drawn interface are passed over.
The compact composer is a pill: 62px on a soft shadow around 44px
controls, the editor at the bubble's 17px, and the focus ring kept.
Its border stays, clear, so the skins still colour it. It takes a
placeholder ("Message Ana" in a conversation with one Bot, "Ask anything"
elsewhere) and an onTextChange. Every data-slot hook is where it was.
… tool calls it has TanStack Query polls once for every component reading a query, and fetches only merge while one is in flight. Every tool call row read the inbox twice, mounted at different moments as a reply streamed, so thirty calls asked for it sixty times every ten seconds. The rows and cards now read the cache without a poll of their own. The conversation polls it once, and so does the Bot page, whose cards used to keep themselves fresh.
A turn that stopped for an approval counted as answered. The browser sends a gated click to its card and stops the run with no failure and no Stop, and a call the server holds ends its turn the same way, so the orb played its landing while the line said "Needs you". A turn that ends on a call still waiting for the person no longer counts as answered, and an approval waiting in the conversation holds off any landing.
…itself The check read declined[1]?.output under toBeUndefined, which also passes when the step is missing; the assertion-shape guard refuses that shape.
The changelog still said the box names the site the value goes to. Since the password box stopped naming a host, which it could not vouch for, it says the value goes straight to the page and the Bot never sees it; the changelog now says the same.
What a task's conversation needs from the queue, with nothing in OpenBot
using it yet:
- `claim({ serialBy })` runs one item at a time per group. An item waits
while another of its group holds a live lease, or while an older one is
unfinished with attempts left. A payload field's name groups one kind by
that field; a `SerialOrder` lets several kinds share one order, each
with its own attempt cap and its own way of naming its group, so the
queue never needs to know what a group is.
- `release({ unstarted })` gives back the attempt a claim spent when the
work never began.
- `renew`, `finish` and `release` act only on a live lease. An expired
lease on its last attempt keeps its owner, and a late heartbeat, finish
or refund from that owner could otherwise revive it beside the sibling
now running in its place.
`ThreadLockBusyError` names a lock refused on a turn's first acquisition,
the one conflict that is safe to defer. `pendingPersonQuestion` is the
person's waiting questions, less any a reply has already answered; the
inbox and `respond` use it, which changes nothing until replies carry
`questionIds`.
A task is background work a Bot does in a conversation of its own. This adds where tasks are kept and how they are read; nothing opens one yet. The `tasks` table holds what only the person or the task's start can say: its Bot, its conversation, its title and why, where it came from, and `resolved_at`. There is no status column. Every read works the status out from the ledgers that already know it: the task's turns on the work queue, approvals waiting in the inbox, questions its Bot asked, and the last thing said in its conversation. A turn runs at most twice; one out of runs reads as failed, with the reason in plain words. `/api/tasks` serves the person's own tasks: `GET /`, `GET /:id`, and `POST /:id/resolve` and `/reopen` for Mark done and Reopen. Another person's task is the same as no task. Migration 0070 creates the table.
The thread lock refused when a turn first asks for it now throws `ThreadLockBusyError`, with the platform's 409 as its cause. That one conflict means the turn never ran, so a queue can put it back without spending an attempt. The same 409 later in a run, or on a renewal, may follow a tool call or a model's spend, and is raised as it comes. A turn also takes `systemMessages`: instructions the server resolved for it, such as the skills picked for a reply to a task. They are kept as history rows and given to the model as context, since a built-in Bot's model leaves system rows out. `keepLease` gives up exactly one interval before the lock lapses, not one tick after: with renewals failing, the `>` let the lock expire on the tick it should have stopped.
Started work used to run in the conversation the suggestion was delivered
to, so that conversation filled with the work. Start now opens a task: a
new conversation with the Bot and the record that names it, written in one
transaction so a lookup never sees one without the other. Its turn runs
there, queued and picked up straight away.
The proactive engine runs a task's turn at most twice: once, and once more
after a throw or a restart. A turn refused the conversation's lock on its
first ask is given back without spending a run. Out of runs, it stays on
the queue with its reason, which is what reads as failed, and the Bot says
so in the conversation. A turn queued before retries existed is never run
a second time. Try again (`again`) reruns the failed turn, keyed so two at
once run once. A reply (`reply`) is taken onto the queue before it is
answered, with the questions it answers and any files and skills checked
and kept, so it survives a restart. A task's turns, its replies and the
answers to question cards asked in it share one order (`TASK_ORDER`), so
none overtakes another.
A task's conversation stays out of the person's own: `direct()` never
picks one, the roster marks it with `taskId`, research never adopts it,
and its title comes from the ask inside the task frame. While a task's turn
holds its lease, the conversation's work list says so; the engine says it
is no longer busy once the turn is off the queue, so that list is read
after the turn has left it.
`POST /api/proactive/suggestions/:id/start` answers `{ suggestion, task }`,
`GET /api/proactive/suggestions/:id` reads one suggestion whatever its
status, and `/api/tasks/:id/reply` and `/again` steer a task.
A Bot OpenBot runs itself can now hand anything bigger than a quick
answer to a task of its own with `start_task`, and answer "On it" where it
was asked, so that conversation stays one thread and the work happens in
the task's. The tool opens the task through the engine's `startAsked`,
the path a started suggestion takes, so both run, wait and finish alike.
It is offered only to built-in Bots (`agents.type = 'built_in'`). A Bot
behind a remote agent framework composes its own turn at somebody else's
server, so it is not told to hand work on; its tasks start from its
suggestions and from the +.
`POST /api/tasks` is the + on Tasks: `{ botId, instruction, title? }`
opens a task with any of the person's Bots and starts it, titled from the
ask's first sentence when no title is given. A Bot the person cannot
reach is the same as no Bot.
`GET /api/tasks/:id/report` reads a task's conversation back as the report the task page shows above it: the person's decisions on approvals asked there, their replies, and the Bot's update as each turn ended, in the order they happened. Nothing is stored. The conversation comes from Intelligence, as the runtime holds it; its messages carry no times, so each time comes from whichever ledger saw that moment, the approval inbox or a turn's work item, or there is none. A turn still running is left out until its own completion is on the queue, and a rerun of an accepted reply adds an update without adding the person's reply twice. The report has no list of the emails and events a task read: that needs connector-specific readers OpenBot does not have.
The settle test now checks the queue at the moment the conversation is told it is no longer busy: the turn must already be finished there, which is the reason the signal moved off the turn. Its assertions also keep `?.` out of their subjects, as the assertion-shape guard requires. The accepted-answer fixtures ended the earlier turn on the host's clock just before a reply stamped on the database's. A host clock a few milliseconds ahead put that end after the reply's failure, and the task read as ready instead of failed. They now end it a minute earlier.
…n it must The frame told the Bot to "end with what you did and anything still waiting on them", and gpt-5.4 wrote that as literal "What I did:" and "Still waiting on you:" labels; the second became the task's next-step line on Tasks. Every task turn now carries a system message saying how to end. A question there reads as Needs you, so the Bot asks one only when it can't go on without an answer. The frame itself stays word for word, so tasks opened before this are still read as tasks.
… of what isn't waiting The first wording came back as "Nothing is still waiting on them." The guidance now asks for the ending written to the person as "you", and to leave out what waits on them when nothing does.
A turn stopped for Allow once with no words of its own recorded the toolkit's "The turn is waiting for human input." as the task's next step. It now reads "Waiting for you to allow a step.", or "Waiting for your answer." for anything else, and a message of the gate's own is kept as it was.
start_task was offered to every turn of a built-in Bot, and the task's turn always runs as the person. A routine, responsibility, wake-up or hand-off that read an injected email could start a task that ran past the rules written for unattended runs, credited to the person on the trail. It is now offered only to turns a person started, never to a task's own turn (built by its own runner, which tells the tools so), and one turn starts at most five tasks.
A task's status read whether its last reply asked from the line its conversation keeps, which is cut to 200 characters with an ellipsis. Any closing question longer than that lost its question mark there, and the task read Ready for review instead. Whether the reply asks is now read from the whole reply as the turn ends, and kept on the turn's work item as it finishes (`finish` can now merge what the work came to into its payload). Status uses it when the last line is that reply; a reply from before this, or anything else said there, is read from the line as before.
Nothing purges task turns or the answers to a task's questions, and the reads of them had no index: a task's status, its report and the question inbox's check for answers already taken each scanned every task turn ever written, for every person, and the inbox runs on every page that counts what is waiting. `work_items_owner_task_idx` indexes work by kind, person and task, folded into the unpublished 0070_tasks migration and its snapshot. The status read now names the person too, as every task turn does, so each of those reads is a lookup by person. A test plans them over a queue holding many people's turns. A task with no turns on the queue read as never started however old it was, so clearing old turns would have failed every old task. One whose conversation has had anything said in it did start, and now reads from what was said.
A task's turn that stops for Allow once has finished, and its resume was outside the order a task's turns take. So a reply sent before the person allowed the step could run after the resume, or one sent after it before, and the thread lock kept them apart but not in order: the resume carries on from the conversation as it was when the turn stopped. The approval resume sweep now claims under the task's order, which finds an approval's task through the thread it was raised in, as it finds a question card's. A task's work runs in the order the person gave it.
Each start launched its own drain, so task turns ran side by side without limit: one reply starts at most five tasks, but many replies or suggestions could still run dozens of headless model turns at once for one person. A claim can now leave an item queued while its value already has so many running (`runningAtMost`), counted again under a lock so replicas racing for one person cannot pass it. Task turns use it to run at most three per person, across replicas, and the engine runs at most ten in one process. A turn held back reads Working and starts as soon as one finishes, from the turn that made room, not at the next sweep.
…turn When Allow once, a question card's answer or a sign-in carried a turn on, its Bot was rebuilt through the generic approval runner, even in a task's own conversation. So the task's resumed turn was offered start_task again, which a task's own turn never is. Those three now carry a turn on through one runner that builds it as a task's turn when its thread is a task's conversation, and as before anywhere else.
The roster's summary carried a channel's task, but `GET /api/channels/:id` did not, so a link that opened a task's conversation by its channel had no way to know it was a task's. The browser drew it as an ordinary conversation, where a reply runs outside the task's queue. The channel read and the channel a create answers with now carry `taskId`, null for an ordinary conversation.
A recording showed a task's report giving the Bot's own reason for making a task as its why: "This is a multi-step planning request that benefits from a dedicated task and a polished itinerary." The field was described only as "why it matters or by when", which a model read as its own reasoning. start_task now says why holds only what the person said about why it matters or by when, in their words, is left out when they said neither, and is never the Bot's reason for making a task. Source and link had the same gap: source is never what kind of request it is, and link is the email or event it came from as read, never a page to use for the work or one made up. A model following a description can't be unit-tested; the test pins that the JSON schema the model is given says these things.
The app has a data layer for the server's tasks now: queries for the
list, one task and its report, the writes for starting one, Mark done,
Reopen, Try again and replying, and the grouping Tasks draws (Needs you,
Working, Failed, Ready for review, Done) with each task's next-step
line. The sidebar's count is that grouping's Needs you, so the number
and the list can never disagree.
A task runs in a conversation of its own, and the conversation list
marks it with `taskId`. Those conversations are left out of every list
a person picks a conversation from: the sidebar, the skins' menus, and
the pickers for responsibilities, research and reaching you.
A task's turn running on the server reads as the Bot working on it
("Scout is working on it…"), where before it would have read as a
routine.
The list is read every few seconds only while a task is working, and
now and then otherwise: the sidebar keeps it mounted on every screen,
and a task can wait on the person all day. An answer given here, to an
approval or a question, reads the tasks again at once, so the task it
was asked in moves off Needs you without waiting for the next read. A
task's card reads the approval inbox its conversation already polls,
with no poll of its own.
Tasks lists every task by what it needs from the person: Needs you, Working, Failed and Ready for review, then Done, folded. Each row is the task's Bot, its title and one line of what comes next: the question it stopped on, what an approval would do, why it couldn't finish, or the Bot's own last line. The list sits in a column beside the open task once the space Tasks is given is wide enough for both, and on a narrower one it is the list, then the task with Back. ↓ and ↑, or j and k, move through it, Enter opens a task and Escape closes it with the focus back on its row. In the sidebar, Tasks comes above Bots with how many tasks need the person. A task's page is its report above its conversation, drawn with the same transcript and approval cards as any conversation, with Mark done or Reopen, and Try again when it couldn't finish. Its turns run on the server, so the conversation has a task mode: a reply is sent to the server and drawn at once under the id the turn records it with, the replies the server holds show as Queued, and the Bot reads as working while its turn runs there. A reply on a failed task tries it again with the person's words; if the task was already being tried again, the reply waits its turn instead. The ask the server frames for the model reads as the person's own words, once however many times the turn runs. A refresh that fails keeps the task's page and the list on screen, under a line that says they couldn't refresh, so a failed poll never loses a reply being typed; the error takes their place only when nothing was read yet. `/channel/$channelId` sends a task's conversation to the task's page, replacing its own entry so Back still goes where the link was, and a Bot's activity links what was raised in a task to the task. A reply to a task always goes through the task's queue, never beside its own turn.
When a Bot hands work to a task with `start_task`, the conversation now draws the task as a card under the Bot's "On it": its title and where it stands, kept live, opening the task. The card goes to the end of the Bot's turn, under its words, wherever in the turn the call was made. A refused call draws nothing; the Bot is told what to fix. The + on Tasks opens New task: which Bot, the person's default one until they choose another, and what it should do. Start opens the task it started. Under the list, Up next holds the Bots' open suggestions with Start and Dismiss, and Start opens the task the server made of it. Start on the Bot profile's suggestions opens the task too, since a suggestion no longer runs in the person's own conversation.
The OpenDuet skin had a Tasks screen of its own that only rearranged OpenBot's lists: approvals and questions waiting, responsibilities, routines and suggestions. Its Tasks tab now opens OpenBot's Tasks, and stays lit with a task open. Its count is the tasks that need the person, counted the way Tasks lists them under Needs you, where before it was every approval and question waiting anywhere. The skin's own screen and its count are gone. The test of a skin switched while on a screen only the old skin has now uses the OpenMuse skin's screens, since the OpenDuet skin has none left.
The Unreleased entry for tasks said what the server does. It now says what a person sees too: Tasks in the sidebar, the list beside the open task and its keys, the task's page, New task and Up next, the card a Bot leaves in a conversation, and the OpenDuet skin's Tasks tab.
This was referenced Oct 31, 2026
9 tasks done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends on #916 and #917. Its own change is the last five commits; the seventeen before them are #917's: compare.
The problem
With #917, started work runs as a task in a conversation of its own, and the app has nowhere to show one. Start on a suggestion opens a task that shows only as one more conversation in the sidebar, where a reply runs outside the task's order. A Bot's
start_taskdraws as a generic tool call. Nothing says which tasks wait on the person, which are working, and which couldn't finish.The OpenDuet skin's Tasks tab only rearranges OpenBot's lists of approvals, questions, responsibilities, routines and suggestions, and counts every approval and question waiting anywhere.
The approach
Tasks lists every task by what it needs. Needs you, Working, Failed and Ready for review, then Done, folded. Each row is the task's Bot, its title and one line of what comes next, such as the question it stopped on. Tasks sits above Bots in the sidebar, and its count is the Needs you group itself, so the number and the list can't disagree. The list sits beside the open task when there is room, and before it on a narrow screen. ↓ and ↑, or j and k, move through it; Enter opens a task and Escape closes it.
A task's page is its report above its conversation, drawn with the same transcript and approval cards, with Mark done, Reopen, and Try again when it couldn't finish. The turns run on the server, so the conversation has a task mode: a reply goes to the server and is drawn at once under the id the turn records it with, held replies show as Queued, and the Bot reads as working while its turn runs there. A reply on a failed task tries it again with the person's words.
A task's conversation is the task's. It is left out of every list a person picks a conversation from, the skins' menus and pickers included.
/channel/$channelIdsends a task's conversation to the task's page, replacing its own entry so Back still goes where the link was, and a Bot's activity links what was raised in a task to the task.Starting one. When a Bot hands work on with
start_task, the conversation draws the task as a card under its "On it", live, opening the task; a refused call draws nothing. The + opens New task: which Bot, the default one first, and what to do. Start on a suggestion opens a task, from the Bot's page or from Up next under the list, which shows the Bots' open suggestions only.Reads stay cheap. The sidebar keeps the list mounted on every screen and a task can wait on the person all day, so the list is read every three seconds only while a task is working, and every thirty otherwise. An answer given here reads them again at once. A failed refresh keeps the page and the list on screen under a line saying so, so a reply being typed is never lost.
The OpenDuet skin's Tasks tab now opens Tasks, and its "waiting" became Needs you: its count is the tasks that need the person. Its own screen is gone.
The cost: every page now polls the task list.
What is not covered
/api/bots/attentionto leave task threads out.mainshows the same on a reply resumed after Allow once.Verification
typecheck, lint, format:check and the assertion-shape guard are clean. The full app suite is 2453 pass, 0 fail, against 2416 on #916 and 2129 on upstream
main. The server suite is 6333 pass, 20 fail, the same 20 that fail on #917 and upstream.app/tests/task-groups.test.ts: the groups' order; a task's own approval or question makes it Needs you; the sidebar's count is that group; fast reads only while one is working.app/tests/tasks-screen.test.tsx: the grouped list; a row opens beside it; the keys; Up next; New task; task conversations out of the sidebar.app/tests/task-thread.test.tsx: a reply goes through the server; Queued replies; Try again with words; a failed refresh keeps what was typed; a task's conversation opens as the task;start_task's card.app/tests/skins-openduet-frame.test.tsxandskins-openduet-model.test.tsx, reworked: the tab opens Tasks and counts Needs you.Merge notes
The seventeen commits under this branch have the same server, toolkit and shared files as #917, including migration
0070_tasks, so a renumbering there has to be repeated here.app/src/routeTree.gen.tsgains the/tasksroutes.