Commit Graph
104 Commits
Author SHA1 Message Date
tben 28053e690f Narrow the files browser's folders column to 180px 2026-10-22 10:21:58 +01:00
tben eacbe4ab87 Lead the files browser's folders with a tinted type icon
Each folder row in /admin/files now opens with its type as an icon in a
box tinted with its own hue (Bots ember, Workspaces teal, Computers
cobalt, Media violet, MCP servers moss, Database amber), so the picker
reads at a glance and the row's width goes to the name. The box carries
the type as its title, so hovering still names it.
2026-10-22 10:18:35 +01:00
tben aebd6b8ccc Serve every media file privately at /media/<id>
There is no public media any more. `/media/public/<id>` served a media
row with no session at all, so anyone with the id could read a file.
The public/private split is gone: the path is `/media/<id>` (and
`/media/<id>/thumbnail`), every request is signed in first, and
`grabUserMedia` decides whether the user may see it: their own upload,
a bot's or workspace's picture or file they reach, or a file on a task
they reach. Anything else is a 404 before a path is built.

The `media.is_private` column and the two per-user directories went
with it: uploads live in `media/<user_id>/` and there is nothing to
pick between. The files browser's media volume is now the viewer's
own folder whatever their role.

Bots also say which of the two things they mean. A `[file:ID]` ref is a
saved file and its chip opens the bytes; a folder is a new
`[folder:volume:owner:path]` ref (`src/utils/folder-ref.ts`) whose chip
opens the files browser there and downloads nothing. The old `[path:...]`
ref and the extension sniffing that guessed file from folder are gone,
so `import_file` names the folder holding a file it copied rather than
the file itself.

The prompts say what the Assistant got wrong: list before importing or
saving, one copy per artifact, never split a file into parts unless
asked, never claim the store is empty without a fresh list, and never
invent a size cap. A re-import now says the path already existed
instead of silently overwriting and reporting a fresh copy.
2026-10-22 10:10:53 +01:00
tben ffd32c8b27 Paste another site's text without replaying it forever
view.pasteText(text, event) re-asks every paste handler with the event it was given, so the composer's own handler replayed the paste until the stack overflowed (any rich paste from VS Code, Slack or a browser page). It now passes no event, and a ref keeps it from re-entering either way.

The plain-text decision moves to shouldPastePlainText with its own tests.
2026-10-22 08:35:39 +01:00
tben 9af5323530 Square off the rail header and add a back button
The rail header now sits in the standard 64px row with a hairline under
it, matching the pane headers, and shows the full logo off the chat
pages instead of the bare stone. A back button joins the conversation
and settings headers (workspace pages only for the conversation). The
rail's own back link and the dividers around it are switched off for now.

Also renames the bot and workspace Delete group to Danger Zone, lifts the
step budget to 50, and lets Logo render without its home link.
2026-10-22 08:30:51 +01:00
tben 96735de48f Let a bot and a workspace set their own envs
Envs had three scopes, general, workspace and computer. A bot had no
scope of its own, so the only way to give one worker a key was to set it
on a computer it happened to share.

The envs table gets a bot_id, and resolution now layers bot over
computer over workspace over general, so a bot's own key wins wherever it
runs. Every computer call site passes the run's bot_id, and grabRunEnvs
does the same for the {{env.KEY}} a custom tool or an MCP server's headers
read.

A bot's and a workspace's settings page can now edit their own envs, in
place, through the same save-env and delete-env routes: sending `scoped`
makes them answer with just that scope instead of the whole install's
overview. Super admins only, like the Environment page.
2026-10-22 08:30:40 +01:00
tben 7a3d2df213 Document the files browser, import streaming and the FK trap
Records why import_file must not read bytes through the shell (its
30,047-character stdout cap truncated every file), why a conversation's
file props have to carry the whole folder tree, and why any helper that
deletes a parent row must delete its children explicitly instead of
trusting the schema's cascade.
2026-10-22 07:37:29 +01:00
tben 80b6b6c1d7 Let a bot's reply link to the files it saved
Both runners now pass the files a run wrote back into the reply, and a
computer file ref renders as a chip like a bot file ref does, so a
reply can point at what it produced instead of naming a path.
2026-10-22 07:37:27 +01:00
tben 43e70de983 Delete a bot file's chunks with the file
ON DELETE CASCADE only fires when PRAGMA foreign_keys is on, and only
src/server.ts turns it on. A script or CLI process deleting a bot file
therefore left every bot_file_chunks row behind, and the next
`bun run db:schema` failed with

    Foreign key violations after schema sync: bot_file_chunks -> bot_files

deleteBotFile and purgeStorage now delete the chunks themselves, so the
cascade is no longer load-bearing and a maintenance script behaves like
the server.
2026-10-22 07:37:24 +01:00
tben de863618ba List a conversation's whole file tree, not just its top level
grab-conversation-files asked listBotFiles for the top level only, and a
bot saves into folders (reports/, seo/coderank/, tmp/...). The page
therefore carried none of the files a reply named, so every [file:ID]
chip rendered as dead plain text and the files could not be opened at
all: the bot had done the work and the user had no way to reach it.

The props now take the whole tree, and a chip that still isn't among
them (a message older than the page's first page of files) fetches its
one file by id, cached per id so a deleted one doesn't refetch on every
render.
2026-10-22 07:37:21 +01:00
tben f1b7ba535f Stream computer files instead of reading them through the shell
import_file sent a computer's bytes back with base64 over /v1/shell/exec,
whose stdout is capped at 30,047 characters. Every import was therefore
cut at 22,528 bytes and saved anyway, with the tool reporting success: a
1.12 MiB archive became a 22 KB file that would not open, and a crawler's
crawl.json lost half its pages.

The bytes now come from the sandbox's streaming GET /v1/file/download.
The size is read first so nothing over BotFileImportMaxBytes is
transferred, the download asks for change_policy=abort so a file that
changes mid-send is refused rather than half-saved, and the received
length is checked against the size read, so a cut-short transfer fails
loudly. Folders go the same way, as a gzipped tar unpacked into one row
per file.

Bots also name a path as [path:volume:owner:path] now, so what a bot
leaves on a computer is a link into the files browser instead of text.
2026-10-22 07:37:19 +01:00
tben 956942bba9 Add a files browser for every folder on the data volume
A bot's deliverables and the raw material behind them only existed as
paths in a message, so they couldn't be opened from the app at all. This
browses them read-only at /admin/files, one volume at a time: a bot's own
folder, a workspace's shared one, a computer's home (where a bot's ~/...
actually is), the media library, an MCP server's install folder and the
database.

Access is the whole gate: resolveStorageSpec checks the viewer against
the volume first (a bot through grabUserBot, a workspace through
grabUserWorkspace, a computer through grabUserComputer, media only their
own unless they manage the crew) and no path is built before it passes.
resolveStoragePath then applies the resolveBotPath rules to any root.
The picker lists folders through reachableBotsSql, so it never offers one
the resolver would refuse.

Reading is listStorageEntries (folders first, paginated, symlinks
skipped, catalog rows joined so a bot file still opens through its media
row) and grabStorageFile for bytes under a cap, both behind an ETag.
2026-10-22 07:37:16 +01:00
tben 07042a2288 Keep Tailwind's scan off the server-side folders
The task work added ref strings ([file:ID], [bot:3]) in new prompt and tool
files, so the warning about "file" not being a known CSS property came back:
Tailwind read them as arbitrary-property utilities and emitted `file: ID`.

The exclusion list now covers every server-side directory whole instead of
one file at a time (none of them holds a className, so nothing is lost), and
`dict` stays scanned for the static hue classes. The ref literals that must
live in scanned files are reworded in comments, and task-file-row builds its
chip through `fileRef` rather than its own template literal.
2026-10-22 06:06:25 +01:00
tben 7cb26c4e36 Add tasks: the crew's shared to-do list
Bots and workspaces each keep a list of work tracked until it's done, and
the bots move it along themselves as they work.

- tasks (one collection per bot or per workspace), task_notes (the
  progress log) and task_media (a deliverable or a reference file, its
  bytes a private media row served at /media/private/<id>)
- statuses todo/doing/blocked/done/cancelled, with blocked_reason while
  only the user can unblock it
- visibility as SQL in src/utils/task-access: a user's own bots and
  workspaces, a run's own list plus its workspaces, everything for the
  coordinator; every tool and endpoint resolves through it
- list_tasks, create_task and update_task on every run, so a chat reply,
  a task and a routine all read the list, log progress and file what they
  produced
- TasksNote on every run and TaskReminderNote in chat, so a greeting or a
  proposed next step brings pending work up in one measured line
- tasks group on the workspace dashboard and a tasks modal in the
  conversation header, sharing one form, row and detail panel
- a bot's or workspace's own chat uploads are read before the delete so
  none are stranded; bot files keep their folder

Also corrects the README's RAM guidance and table alignment.
2026-10-22 05:57:50 +01:00
tben 2d108b05bf Drop the nested form now twui guards a missing one
twui 0.1.5 makes Form's change handler tolerate a field with no form
ancestor, so the modal's own form is no longer needed to keep a change in
it from throwing.
2026-10-22 05:04:40 +01:00
tben 89101d0e05 Keep model settings changes out of the page's form
The modal renders in a portal at the end of the document, outside the
Models page's form, so twui's Form found no form element on a change there
and threw building FormData. The fields get a form of their own, nested
inside the page's, to give it something to read and to keep Enter from
submitting the page.
2026-10-22 04:59:29 +01:00
tben 8edff8988a Run models on sensible generation defaults out of the box
A fresh install had no generation settings until someone set one, so every
model ran on whatever its provider happened to use. Each harness now has a
default (text 0.3 temperature, vision 0.2, image the provider's own), shown
in the model settings modal so what a model runs on is never a guess.

The defaults are only sent to a model whose catalog lists the field, so a
provider that says nothing about its models still gets no parameter it
might reject; anything saved on a harness or bot wins over the default.
2026-10-22 04:56:10 +01:00
tben c2553a4746 Add per-model generation settings to the model selector
Each text and vision harness setting now carries the generation settings
its model takes (temperature, sampling limits, penalties, seed, reasoning
effort), edited in a modal under the model pick so the harness block stays
short. Nothing is set by default, so models keep their provider's own
values until a super admin or admin changes them.

A setting is only offered and only sent when the catalog says the model
takes it: OpenRouter's supported_parameters is now stored on
harness_models and refreshed by the hourly sync. Other providers list no
parameters, so their models get them all.
2026-10-22 04:52:55 +01:00
tben fe502b38fe Rework the README with current screenshots
The README was a bare file list. It now opens with the horizontal
lockup, a theme-aware hero and a workspace dashboard, both taken from
the running app, so GitHub's preview shows the product instead of
describing it.

AGENTS.md records that the design handoff is outdated: its screens and
briefs are history, while its brand assets (stones, coordinator, app
icon, wordmark) and their copies in src/data are still the foundation.
2026-10-22 04:15:43 +01:00
tben 72a150c2b2 Update user conversation bubble links 2026-10-21 19:08:58 +01:00
tben f644772944 Stop Tailwind scanning ref strings, colour links in the user bubble
Tailwind read ref literals like [file:12] in prompts and docs as arbitrary-property utilities and emitted invalid CSS; those files hold no className, so they are excluded from the scan. Links in the user's ink bubble get a readable blue instead of the bubble's own ink, and the bubble no longer needs its own CSS class.
2026-10-21 19:03:24 +01:00
tben d17a4a7bdd Add bots files handling logic 2026-10-21 17:56:09 +01:00
tben b0af4db1bf Open the composer's tools in a modal behind one counting button
The chips under the composer clogged it once a bot had a handful of MCP servers and tools. They are now one button reading "N tools" (with an alert icon when one is off or not working) that opens them in a modal, where any the bot was given can still be removed and more added.
2026-10-21 16:49:56 +01:00
tben 66903f7a00 Keep the conversation header's meta on one line with the workspace 2026-10-21 16:43:48 +01:00
tben 92ad95f743 Show the coordinator in the workspace rail under the workspace row 2026-10-21 16:10:06 +01:00
tben 40cf101b38 Fold the conversation header's icons into one menu
The header's sessions, new session, computers, models and settings icons
become rows in a single dropdown off an ellipsis on the right, so the
header keeps one control whatever width it has. A row that opens a modal
closes the menu first, so the modal never sits behind an open menu, and
each row shows what it acts on (the computer's name, the text model).
2026-10-21 16:06:21 +01:00
tben 361b8add9b Run development on the dev server's code, not a build's cache
A production deploy leaves its service worker on the device, and it
claims every tab. It serves /.bunext/public/* cache first without
knowing which build asked, while every cache-busting mechanism the app
has (the precache gate, /build-info.json, the loading page) is
production only. So switching to `bun run dev` kept serving the old
build, vendor files included, and wrote the dev files back into the
production build's cache.

Development now clears that worker and every cache it wrote
(`clear-development-service-worker.ts`, from ServiceWorkerRegistrar,
which did nothing in development before), reloads once into the
uncontrolled tab, and /sw.js answers with a stub that unregisters
itself so Web Push and the loading page can't register it back.
2026-10-21 15:57:29 +01:00
tben 8bf6bedec5 Serve plain HTTP only and drop the local certificate tooling
The app never terminates TLS: a reverse proxy or tunnel does, and
isSecureRequest still reads X-Forwarded-Proto so cookies stay Secure.
Removes the cert generator, TLS resolution and HTTP redirect, the
local CA download, and the TLS envs, and trims the installer, Dockerfile
health check and docs to match. Also softens the composer's loading row.
2026-10-21 15:35:27 +01:00
tbenandClaude Opus 5.5 f13fa863c5 Trim page props and page the conversation's messages
Bot and workspace lists in page props leave out instructions, memory and
workspace context, and carry the last message as a short preview. The
header avatar's edit form loads the bot's instructions when it opens.
The conversation renders its latest 30 messages and loads older ones as
the thread scrolls to its top.

Also: the composer shows a placeholder until its editor loads, and
`bun start` no longer builds first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 20:09:01 +01:00
tbenandClaude Opus 5.5 b3057119df Download the whole app build behind a loading page
In production, a page request without the running build's cookie gets a
small loading page instead of the app. It downloads every file in
.bunext/public (listed at /build-info.json) into that build's cache,
checks they are all there, hands over to a new service worker waiting
behind an older one, sets the cookie and reloads. The service worker
then serves the app's code from the cache, so later visits load it from
the device.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 19:33:15 +01:00
tben 9778d0b9ff Keep a bot's workspace memory from reading as the workspace's record
A workspace bot asked to "update the workspace record" wrote to its own
memory there, called it the workspace's record, and then, when the user
said nothing had changed, denied making any write at all: it checked
nothing and confessed to work it had done.

- Say in the prompt that the workspace's context is the user's, and that
  memory in a workspace is the bot's own notes, readable in its settings
- Say the same in update_memory and update_instructions results, with
  where the user reads it and that the workspace's own fields didn't
  change
- Have ActionCheckNote look at what actually happened before agreeing
  with a user that nothing was done, and never deny work that happened
2026-10-20 14:59:56 +01:00
tben b34a8fdaf1 Show the workspace picture in the conversation header 2026-10-20 14:50:24 +01:00
tben 8425631b3a Show sent messages before the server confirms them
The composer now puts the bubble and clears the input as soon as you send, so a slow connection never holds either. The bot's status row reads "<name> is getting your message..." until the saved message arrives (by response or push), then switches to thinking. A failed send removes the bubble and restores the text and files.
2026-10-20 14:50:20 +01:00
tben 4f7e0a6bf0 Finish the mobile performance pass
- Cache the model catalog once per user and each conversation's setting
  separately; clear both on logout and after model saves
- Compress hashed assets asynchronously with a byte budget and set Vary
- Memoize message parsing and throttle selection reads
- Debounce draft writes to localStorage
- Keep the composer editor on the page and focused, as before
- Add a bundle budget check and run it in CI
- Show a loading block while sessions load and a workspace avatar in the
  mobile conversation header
2026-10-20 14:37:11 +01:00
tbenandClaude Opus 5.5 c1a94fc9a5 Make pages load fast on mobile
- Compress responses: page scripts and styles once (brotli or gzip, in
  memory) and cached immutable; pages and API JSON gzipped as they stream
- Service worker serves content-named assets cache-first
- Load the models modal's data when it opens (cached in localStorage)
  instead of shipping the model catalog in every conversation page
- Lazy-load the Tiptap composer and memoize thread messages
- Import lucide icons directly; twui 0.1.4 drops by-name icons from Button
- bun run start builds the pages minified before serving

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 13:41:45 +01:00
tben 3b355d68bc Keep bot jobs and harness calls from hanging 2026-10-20 12:59:24 +01:00
tben 3e13052a25 Run the bots queue and cron with bun run start 2026-10-20 10:49:44 +01:00
tben 459eebf73d Use the certs/ fallback in every environment
TLS no longer depends on NODE_ENV: the TLS_* variables win, else certs/
is used whenever it exists (the container's /app/certs mount lands there
too), and TLS_DISABLED=1 is the only way to serve plain HTTP with a
certificate present. The plain-HTTP redirect listener now starts whenever
TLS is on, not only in development, and HTTP_REDIRECT_PORT replaces
DEV_HTTP_PORT. The installer writes TLS_DISABLED=1 for --no-https, and the
health check follows the same rule.
2026-10-20 10:29:37 +01:00
tben 8c20c9a7ed Ask about HTTPS on a fresh install
A fresh install asks whether to serve HTTPS, with the reasons, before
touching anything. --yes keeps the default, --no-https answers it, and an
update keeps the saved mode. The prompt reads /dev/tty so `curl | bash`
still works; without a terminal the default is explained instead.

Also state why a production process is serving plain HTTP (no TLS vars),
instead of only giving that hint in development.
2026-10-20 10:24:38 +01:00
tben e859bab294 Serve HTTPS by default with a local CA
The installer generates a CA and a 397-day certificate into
$COBBLE_HOME/certs (in the app image, with the host's addresses), mounts
the certificate, key and CA into the container and serves TLS from Bun on
the published port. --no-https opts out; --https-cert/--https-key use a
real certificate, --https-host adds names and --https-regen replaces the
CA. The installer and the image health check use curl -k when TLS is on.

The CA is served at /ca.crt and as an iOS/macOS profile at
/ca.mobileconfig for one-tap install. Development picks up certs/
automatically, `bun run dev:phone` generates and starts, and a plain-HTTP
listener on PORT + 1 redirects to HTTPS. Port sharing is off, so a stale
dev server fails instead of silently answering on the same port.
2026-10-20 10:20:01 +01:00
tbenandClaude Opus 5.5 535adb6e16 Make routines act on their own and check every bot action
Scheduled runs are pre-approved: they carry out their brief instead of
drafting for confirmation. A run that can't do its brief calls
report_run_failed and is retried 5 and 10 minutes later; only then does
the user get one "failed after N tries" message, and the routine keeps
why in last_error.

Every reply and task now gets ActionCheckNote: look for an existing
record before creating one, and verify an action took effect before
saying it's done. Memory and instructions saves are read back, and
create_bot, create_schedule, start_task and delegate_task refuse
duplicates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 08:45:49 +01:00
tben 6c57fae444 Use short install URL in installer and docs 2026-10-20 08:35:36 +01:00
tben e73eab3d41 Require the current container init setting on update
CI / check (push) Canceled after 0s
2026-10-20 07:45:06 +01:00
tben f20f20f8a2 Document the release and launch checklist
CI / check (push) Canceled after 0s
2026-10-20 07:43:55 +01:00
tben 2b2c41bebd Check the image architecture before starting
CI / check (push) Canceled after 0s
2026-10-20 07:34:16 +01:00
tben d0798a93a5 Add the self-hosted installer and release workflows
CI / check (push) Canceled after 0s
2026-10-20 07:02:58 +01:00
tbenandClaude Opus 5.5 1208eac6b7 Replace the composer textarea with a Tiptap markdown editor
The composer now writes markdown with bold, italic, strike, code,
links, quotes and lists, from shortcuts, markdown typing or a bubble
over the selection. @ mentions a bot and # a workspace, shown as chips
and saved as [bot:ID] / [workspace:ID]. Text selected in a message can
be quoted into the composer. User messages render as markdown, and
replies tell the bot who and what the user mentioned.

Adds an ESM stand-in for use-sync-external-store, since bunext keeps
React external in page bundles and the CommonJS original requires it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 05:48:23 +01:00
tben e2093b0df6 Update frontend UI/UX 2026-10-20 04:59:08 +01:00
tben 59f5acb7bc Update bot chat block 2026-10-19 22:09:02 +01:00
tbenandClaude Opus 5.5 100ef0eb89 Only offer reply and reactions on bot messages
The user's own messages no longer get the reply and react row. A bot's
reaction to one still shows, read-only, and react-message refuses a
reaction on the user's own message.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 22:04:29 +01:00