HivemindOS manual
Managing a computer from the phone or the web: the screens
For whoever builds this on the phone (React Native) and on hivemindos.app/chat (React, glass). Both draw
from the same view models in core/computer-manager.ts (desktop canonical src/shared/hive/core, phone
shared/hive/core, website src/shared/hive/core). If both builders follow this page, a person moving between
their phone and their browser meets the same sections, the same rows, the same buttons and the same words.
0. Rules for both builders
- No wording in app code. Every title, row line, status, button label, confirm question, notice and
error comes from the view models or
MANAGE_COPY. The only words an app may add are plain chrome: “Manage”, “Refresh”, “Cancel”, “Done”, “Back”, “Send”, “Try again”, “Ask again”, “Sign in”. If a screen needs a new sentence, add it tocomputer-manager.ts(and its test), then sync. - One loop per call. Collect what
callDeviceyields, catch what it throws, then:const table = { scopes: DEVICE_ACCESS_SCOPES, actions: DEVICE_ACTIONS, messages: DEVICE_CALL_MESSAGES }; let access = computerAccess(grant.scopes, table, grant.desktop.name); // once per grant const outcome = readManageAnswer(access, chunks, error); // every call access = narrowAccess(access, outcome, action); // every call const view = automationsView(access, outcome, clock); // per sectionnarrowAccessmatters: a “not allowed” lowers that one scope (its buttons turn off with the computer’s reason); a removal or ending locks everything and shows the lost state. - Loading. Load a section when it first comes on screen, never all eight at once: at most
MANAGE_PARALLEL_LOADS(3) calls in flight per computer. The computer refuses a device’s fifth call at once as busy. Keep the last good view while refreshing. Show a placeholder only when there is no view yet (view.loading). - Refresh. While a section is on screen and the app is in front, ask again every
MANAGE_REFRESH_MS[id](null: only on open and pull). Phone: pull to refresh. Web: a Refresh button in the section header. Stop timers when the screen is hidden. - Buttons are
ManageButtons. Draw every button the view gives, in its order.enabled: falsewith areason: draw it visibly off. Phone: a tap shows the reason in a short line under the row (or a small toast). Web: the reason is the tooltip and, on click, a line under the row.enabled: falsewith no reason: it is held while another call on that row runs. Draw it off.busy: keep the label, add a small spinner, block taps.confirm: ask first (section 4), call only after the confirm button.- After a row action answers, show the matching done notice (
automationDoneText,serviceDoneText) for about 3 s under the row, then reload that section.
- Notices (
ManageNotice) sit inline at the top of the section they belong to, never as a toast. One button, fromnext:retry“Try again”,open-computer“Try again”,sign-in“Sign in”,relink“Ask again” (startsrequestDeviceAccessagain),wait(no button; try again after 30 s),settings,check-walletandnone(no button; the words already say what to do). - Tones.
infomuted text,goodsuccess color,warnhoney,errordanger. A small dot or icon before the text is fine. Never a left accent bar, never sparkles or emoji. - Time. Pass
{ now: Date.now(), hour12 }, wherehour12follows the device’s own 12/24-hour setting. LeaveutcOffsetMinutesout (the device’s own zone). - Never show model servers, app plumbing or ids:
host,providerLabel,sourceId,selection,confirmationId,uploadId,runIdand keys are for calls, not for people.
1. Where it lives
- Phone: from a computer’s own screen (the place its apps are listed today), a “Manage” row opens the manager: one stack screen (the overview) and one screen per section. Sheets for forms and confirms.
- Web: hivemindos.app/chat, from the account menu: “Your computers”, then a computer, then the manager. At 900 px and wider: the sections list on the left (260 px), the open section on the right. Narrower: the same stack as the phone.
- Only computers with a grant on this device appear. One computer at a time.
2. The overview
- Header:
access.desktopNameas the title. Under it, whether that computer is online (fromlistDesktops):MANAGE_COPY.onlinewith a success dot, or theno-devicemessage fromDEVICE_CALL_MESSAGES. - Lost access (
access.lostset): the whole screen is one empty state:MANAGE_COPY.lostTitle, thenaccess.lost.text, then one button foraccess.lost.next(“Ask again”). No sections. - Sections (
access.sections, in that order): one row each.- Title:
section.title. - Second line: the section view’s
summaryonce loaded, elsesection.line. - When
section.canChangeis false: a small muted pill withsection.levelLabel(“See”). - A warn dot when the section’s view has a
noticewith tone warn or error, or Copy trading hasattentionitems.
- Title:
- Locked line:
access.lockedLineunder the list, small and muted, no icon. Omit when empty. - Phone: inset grouped list on
surface, hairline dividers, 14 pt row padding, 44 pt touch targets. Web: the same rows in the left column; the selected row gets asurfaceAltfill, no border.
3. The sections
Every section screen: title (section.title), section.line muted under it, then the notice if any,
then the content below, then view.empty as a centered muted line when the list is empty.
3.1 AI models (models.list; also media.list when Pictures and video is allowed)
modelsView(access, { text, media }). Passmediaonly whenmanageMay(access, 'media.list').- One group per
view.groupsitem, headed bygroup.title(“Chat models”, “Picture models”, “Video models”). Each row:name(primary),whereandnote(muted, under it),stateTexton the right with a dot (success for running and ready, muted for idle, honey for not ready, none for off). - Chat models have
row.chat: a compact “Chat” button. It opens the app’s own chat with that model; the turn goes throughmodels.chatwithmodelChatArgs(...), and the reply streams throughmodelChatStep. WhenmodelChatArgsreportsdropped > 0, showMANAGE_COPY.modelChatDroppedonce, muted, above the reply. When the call’s loop ends, every time, pass the state (and the error it threw, if any) throughmanageStreamEnded(access, state, error): a reply cut off part way keeps what arrived and ends in plain words instead of sitting at “answering”.
3.2 Pictures and video (media.list, media.generate, media.status)
- Form (
mediaForm(access, outcome)): a segmented control fromform.kinds(Picture | Video). Under a kind that is notready, show itsnoteand disable Make.- Model picker (a row that opens a list):
form.models[kind]; Automatic first. Each choice:label,wheremuted,notewhen not ready (drawn off). - Prompt: multiline field, placeholder
MANAGE_COPY.mediaPrompt[kind](the same wordscheckMediaRequestuses when it is empty). - Start pictures: an
MANAGE_COPY.addPicturecontrol only whenform.maxPictures[kind] > 0, up to that many, PNG, JPEG or WebP, sent as data URLs. One sealed call carries under 96 KB in all, so every picture is scaled down before it is added:mediaPictureRoom(form, request, count)is the longest data URL each ofcountpictures may be. Re-encode as JPEG (quality 0.7) at 768 px on the long side, then 512, 384, 256, until the data URL fits. With four pictures for a video expect about 256 px each. If even 256 px does not fit, showMANAGE_COPY.pictureTooBigand do not add it. - Make button:
form.make. On tap:checkMediaRequest(form, { kind, prompt, modelKey, pictures, runId })withrunId = manageId(random, 'run_'). A{ ok: false, field, message }puts the message under that field. Otherwise save aMediaRunlocally, then callmedia.generatewithargs.
- Model picker (a row that opens a list):
- Runs (
mediaRecentView(access, runs, clock)), newest first, under the form. The device keeps its own runs (the computer does not list past ones to other devices): phone in its app storage, web inhost.store. Each row:title,whenmuted,line, and whilepollMsis set callmedia.status { runId }after that long and store the outcome on the run. - Ready results: each
resultis a thumbnail placeholder until fetched: callmedia.statuswithresult.fetchArgs, feed every chunk tomediaFileStep, showmediaFileLine(progress)over the placeholder (and end the loop withmanageStreamEnded(access, progress, error), as for a model reply), and whenprogress.done, build the file fromprogress.parts.join('')(base64) andprogress.mimeType. Phone: write it to the cache and offer Save to Photos and Share. Web: a blob URL with Download.
3.3 Chats (chats.list, chats.read, chats.continue)
chatsView(access, outcome, clock). Rows like a messages list:title(one line),agentandwhenon the second line,previewmuted (two lines at most). Whenrunning, a small “Working…” (stateText) instead ofwhen.- Tap a row (
row.open): the chat screen.chats.read { sessionId }first, then{ sessionId, sinceSeq: page.maxSeq }every 3 s whilepage.running, else every 20 s; feed each answer tochatPageStep(access, page, outcome). Drawitemsin order: user messages right, assistant messages as plain text, tool steps as one muted line (step, thentext) with a dot forstepState. Whenpage.waitinghas lines, show each as an info line above the composer: this device cannot answer those; the person answers on the computer. - Reply (
row.reply): a composer at the bottom, only when enabled; otherwise its reason sits there instead, muted. A mode choice beside Send, fromCHAT_MODES(label, withlineunder each choice), “Ask first” by default. Send callschats.continuewithchatContinueArgs(...); then read again at once. - Who started it. Every listed row carries
origin("person","phone"or"automation"), plusoriginKind(queen-bee,work-board,company,swarm,capability,script,subagent,cron,import) andgroupId(one fan-out of the same prompt to several agents) when known.chats.list { origin: ["person", "phone"] }leaves automations out on the computer; withoutoriginevery chat is listed, as before. A desktop from before this sends noorigin: treat a missing one as a person’s. - The computer’s app chats (Claude Code, Codex, Hermes), read only:
chats.list { source: "apps", runtime?, limit?, cursor?, origin? }andchats.read { source: "apps", runtime, sessionId, sinceSeq?, before?, limit? }. The answer carriessource: "apps"; one without it came from a desktop that predates this and listed its own chats instead. Rows carryorigin("person"or"automation") andoriginKind. A refusal with codechat-history-not-shared:askorchat-history-not-shared:denyis the computer’s chat history setting. Continuing an app chat this way answersnot-availablefor now.
3.4 Files (HiveDrop to the computer: files.send)
hiveDropView(access, sends). A drop area (web: drag and drop plus a picker; phone: Photos, Files and paste) labelledMANAGE_COPY.fileDrop(phone: a “Send files” button,view.send.label), withview.destinationunder it (“Downloads on Studio Mac”).view.sendgates it.- Per file:
hiveDropStart(file, manageId(random, 'upload_')). A failure shows its message in the queue without sending. Send one file at a time and one piece at a time:hiveDropNext(send)says which bytes to read, base64 them, callfiles.sendwithhiveDropArgs(send, data), thenhiveDropStep(send, outcome); repeat untilhiveDropNextreturns null. The step retries a lost answer and resumes where the computer says, so the loop needs no logic of its own. - Rows:
name,sizemuted, a thin progress bar frompercent,lineunder it (success when done, danger when failed). Summary line above the queue:view.line.
3.5 Automations (automations.list, .run, .pause, .resume, .delete)
automationsView(access, outcome, clock, pending), wherependingmaps a row id to the action whose call is running.- Row:
name(primary); second linenext(withstateTextwhen it is not “On” and differs fromnext); third line, muted:last, andagentandwherewhen present. Afailedrow gets the warn dot. The buttons sit in a row under it on the phone (compact, secondary style), and on the right on the web. - Tap the row: a detail sheet from
automations.list { id }throughautomationDetailView:schedule,next, thepromptas plain text, and the recentruns(line,summarymuted). - Remove asks first (section 4), with the row’s own
confirm.body: for a job an agent runs on its own timer and that cannot be paused from here, it says removing may not stop it. After any action:automationDoneText(...), then reload. - Making a new automation is not offered from other devices yet (see section 6).
3.6 Copy trading (copytrader.status, .pause, .resume)
copytraderView(access, outcome, clock, pending).- When
view.elsewhere: one info notice with it at the top; every button is off with that reason. - Top:
view.headlineas a short line. Then, whenview.attentionis not empty, a group headedMANAGE_COPY.copyAttentionTitle: each itemlabelandtext. - Rows (already sorted, attention first):
label(primary) and a mode pillmodeText(Live: neutral fill; Paper: outline) withmodeLineas its accessible description; second linestateText·network·target; third lineresultsinresultsTonecolor andactivitymuted. Whenattention: show it under the row, thendetailmuted and smaller (the computer’s own words). - One button per row: Pause or Resume. Resuming a Live copy asks first (section 4).
3.7 Background apps (services.list, .start, .stop, .restart)
servicesView(access, outcome, pending). Rows:nameandversionmuted;stateTexton the right with a dot (success when running); buttons Restart and Stop when running, Start when stopped.note(the computer’s own detail) only in an expanded row (tap to expand), muted.view.moreas a muted line under the list. Installing is done on the computer.- After an action answers:
servicesAfter(list, outcome)to update the row at once, thenserviceDoneText(...).
3.8 Wallets (wallets.balances, wallets.send)
walletsView(access, outcome). Top:view.totalLinelarge. Rows:name,networkpill,shortAddressmuted (tap to copy the fulladdress),totalon the right, then up to fiveassets(symbol,amount,value).notemuted when set. Each row hasrow.send.- Send sheet (from
row.send):- Form: From (the wallet, fixed), To (address field with paste), Amount with a “$ | asset” switch (the
asset choices are the row’s
assets; pass the chosen one’ssymbolasassetand itstokenAddress, so two tokens with one symbol are never mixed up). Check withcheckWalletSend(access, balances, input); a failure puts its message underfield. “$” and “,” are read only as dollar writing (“$1,000”); “1,5” is asked about, never guessed. - Review:
review.titlelarge,review.linesas label and value rows (the To line shows the whole address, never shortened),review.footnotemuted. Primary button “Send” (chrome word), Cancel. - Status:
state = walletSendStart(Date.now()), callwallets.sendwithargs, thenwalletSendAsked(state, outcome). WhilewalletSendView(...).pollMsis set, callwallets.sendwithwalletSendCheckArgs(state)after that long and applywalletSendChecked(state, outcome, Date.now()). Drawtitle,line, andtimeLeftwhile waiting. The sheet cannot cancel a send: the line already says to tap No on the computer. - When
final: one button.next“retry”: “Try again” (back to the form).check-walletandnone: “Done” (and reload balances). “unknown” means it may have gone out: never offer to send it again from here.
- Form: From (the wallet, fixed), To (address field with paste), Amount with a “$ | asset” switch (the
asset choices are the row’s
- Keep a waiting send alive while the sheet is closed (it lives with the section, not the sheet); reopening shows it.
4. Confirm sheets and dialogs
A ManageConfirm is drawn the same everywhere: title as the heading, body under it, two buttons:
Cancel (secondary) and confirm (primary; the danger color when danger is true). Phone: a bottom sheet
sized to its content. Web: a centered dialog on .glass (dom/tokens.module.css), focus starts on Cancel,
Escape cancels.
5. Look and feel
- Phone: the app’s palette (
constants/palette.ts), cards only where the design guidelines allow, hairline dividers, accent at most once per screen. Sheets via the app’s sheet presence hook. - Web: section content on the solid surface tokens (
--hive-surface,--hive-surface-2). Floating things (dialogs, popovers, the send sheet, tooltips) use the liquid glass class. Light and dark from the host theme. RespectreducedMotion. - Numbers right-aligned with tabular figures. Money is already formatted; never format it again.
6. Not offered from other devices yet (draw nothing for these)
- Making an automation: the computer offers
automations.create, but no device action lists the agents to pick from. - Swapping, installing an app, changing a copy’s settings, the terminal: the computer answers these as not available yet.
- Answering a chat’s waiting question: only on the computer for now (
page.waitingsays so). - Brain search: it has its own sealed path, not this manager.
7. What both builders check before calling it done
- The same fixtures as
tests/computer-manager.test.tsdrawn on screen, light and dark, phone width and 1280 px wide, with: everything allowed, “Standard” (buttons off with reasons), “Look only” (locked line), and a removed device (lost state). - A send walked through every phase, including “unknown” (the confirmation answered on the computer and then gone).
- A 150 KB file sent with one answer lost on the way: it still arrives once. When the lost answer is the last
piece’s, the row says to check Downloads (
MANAGE_COPY.fileUnconfirmed) and nothing is sent again. - A busy computer: open the overview with all sections allowed and confirm no “busy” notice appears.