Commit Graph
448 Commits
Author SHA1 Message Date
brunorezioandClaude Opus 5 0e71d87f75 rewrite CLAUDE.md against the current codebase
The old version described a repo that no longer exists: apps/dashboard and
apps/editor, a tracking server on port 5001, an ephemeral_db, and a dashboard
and API running as separate processes. None of that is true.

Replaces it with what the code actually does — one Bun process serving the SPA,
the API and eight WebSocket providers; sidecars for the privileged work; the
split between Postgres and the file-backed items store; and the single-user
invariant stated as an invariant rather than a migration in progress.

Also records two things that are easy to get wrong from reading alone: the
database is maintained with db:push and has never had a migration applied, and
agents run unsandboxed with permissions bypassed on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 0d67e2af26 clear the remaining type errors
- DiscordAccount seeded DiscordStatus without its two nullable fields.
- bug-report typed reporter.name as string, but users.name is nullable; and the
  Discord upload wrapped a Buffer directly in a Blob.
- Lucide icons take no `title` prop, so the sync spinner's tooltip moved to a
  wrapping span.
- DesktopView cast its dynamic import to a type that included `| null`.
- dock PUT cast the request body straight to string[]; it now rejects anything
  that is not an array of strings instead of writing it to the database.
- buildZodSchema assembles a mutable record, since z.ZodRawShape is readonly in
  zod v4.
- The dev-server proxy forwards Bun's `string | Buffer` frames through a helper
  that satisfies WebSocket.send without copying.

bunx tsgo is now clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 0041fcbd47 make useClient actually return its type parameter
Every verb ended in `return data as T | any`, and `T | any` collapses to `any` —
so client.get<Foo>() handed back `any` and no annotation downstream meant
anything. That was the source of most of the implicit-any errors: the callbacks
had nothing to infer from.

Returning `as T` drops the whole class (19 errors to 9) rather than annotating
each parameter. Two calls in useAuth were relying on the looseness and now
declare their response shapes.

signin also no longer stores `undefined` as the bearer token when the server
withholds one; that path returns early.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 1fa3a659bc restore the code editor screen, drop the dead theme picker, unshadow AppRegistry
Three unrelated type errors that each pointed at something actually broken:

- Dashboard.CodeEditor was deleted in ab03b17 ("Projects") while the
  /code-editor route and the dock's Editor item kept pointing at it, so the
  route rendered undefined. Screen restored.

- Appearance.tsx imported 'themes', a workspace deleted in 0746844. Nothing
  reads settings.appearance.colorTheme and no theme CSS survives, so the picker
  was writing a value with no consumer. Removed it and the setting; colorMode
  stays, it is live.

- officerdev exported both a component and a type named AppRegistry, so
  `import { AppRegistry }` resolved to the type and <AppRegistry /> failed to
  typecheck. Renamed the Record type to AppRegistryMap.

Also declares "*.css" so side-effect stylesheet imports resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 78130f21ce require a separator when checking a path is inside its root
resolveUserPath and five sibling checks used startsWith(rootDir), which also
accepts a sibling directory whose name begins with the root's: from a root of
/home/br, "../br-backup/secret" resolves to /home/br-backup/secret and passed.
Compare against root + sep (or the root itself) via a shared isInside helper.

Verified the escape cases now deny while "", ".", and ordinary relative paths
still resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 c13875cef8 return 400 for an unparseable body, and cover origin validation
bodyParser caught parse failures and did nothing — the throw was commented out
and `body` was never set, so handlers destructured undefined and the client got
a 500 for a malformed request. Throw BAD_REQUEST instead.

Also adds the regression tests for the Host suffix match fixed in 2ca850c.

Verified against a running server: malformed JSON now 400, valid credentials
path still 401, spoofed Host still 403.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 1eb8cfd273 compare the Host header against the origin authority, not as a suffix
The no-Origin branch asked whether the configured origin ends with the
client-supplied Host, so `Host: dev` matched https://rezio.pastilhas.dev — as
did `pastilhas.dev` and `o.pastilhas.dev`. Match the URL authority exactly
instead. Officer always runs behind an HTTPS reverse proxy, so the forwarded
Host is expected to equal PUBLIC_URL's authority.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 8163f04420 fail closed when PUBLIC_BUILD_ENV is unset
Origin validation, every rate limiter and the password-strength check each
treated an unset PUBLIC_BUILD_ENV as "relaxed", so a deployment that forgot the
variable silently ran with CORS reflecting any origin, no brute-force limit on
the sole account, and no password rules. setup.sh writes it, but .env.example
never mentioned it.

The three now share IS_DEV_BUILD, which is true only when PUBLIC_BUILD_ENV is
explicitly "dev" or "development". Anything else, including unset, is hardened.
Documented in .env.example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 85596da086 fix ReferenceError in the pipeline "View in Jobs" button
The handler called onOpenChange and navigate, neither of which is in scope in
PipelineRunner — they belong to TaskRunnerModal, declared further down. Clicking
the button after a pipeline finished threw instead of navigating.

PipelineRunner now takes an onClose prop and calls useNavigate itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 044aacf4d5 remove the dead multi-user surface
Officer is single-user: the server owner is the only account, created once by
/auth/bootstrap. Everything that existed to serve additional users was
unreachable, so it is gone rather than left looking like it does something.

Accounts: drop the invite / resend-invite / delete / list-users routes and the
Users settings screen, the inert /auth/signup handler, and the account
verification chain it fed (verify, resend-verification, VerifyScreen, the
UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token
survives for password resets only, and now requires a reset-password token
rather than accepting any signed JWT.

Roles: drop the users.role column and the four-value USER_ROLES enum. The
permissions table granted every role identical methods, and every
role === 'Super Admin' check was permanently true. The JWT no longer carries a
role claim.

Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected
only for non-Super-Admin users, so it never ran. It was also not a usable agent
jail as written — --share-net, the project root (with .env) bound read-only,
and runuser dropping to the server's own uid. Rebuilding it for agent
containment would be a different construction, and git history keeps this one.

getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the
owner's real login home, which is what terminals, chats and task runs use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
pastilhasandClaude Opus 4.8 92de996412 setup.sh: symlink claude into /usr/local/bin
The Claude sidecar execs /usr/local/bin/claude (claude-manager.ts), but the
Anthropic installer only puts the CLI in ~/.local/bin — so on a fresh host that
path doesn't exist and claude chat fails with
"ENOENT … posix_spawn '/usr/local/bin/claude'". Symlink ~/.local/bin/claude →
/usr/local/bin/claude after install (idempotent; tracks Claude's self-updates).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 17:22:26 +00:00
pastilhasandClaude Opus 4.8 f226542de3 setup.sh: prompt for OFFICER_ITEMS_DIR in .env generation
The item store location wasn't written to .env, so a fresh server fell back to
<repo>/officer-items and booted with an empty store. Prompt for it (default: a
sibling of the repo) and write it to .env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:45:59 +00:00
pastilhasandClaude Opus 4.8 f7b8cbe3f3 setup-desktop: install tigervnc-common for vncpasswd
The VNC sidecar builds its .vnc/passwd rfbauth file with `vncpasswd -f`
(vnc-manager.ts) — x11vnc alone doesn't ship vncpasswd. The GNOME-on-Xorg
rewrite dropped tigervnc, so the mirror couldn't create its password and
/desktop failed with "VNC password not configured". Add tigervnc-common back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:26:31 +00:00
pastilhasandClaude Opus 4.8 97f6d7fecc auth: single-step super-admin bootstrap (no verification email)
Collapse the two-phase bootstrap (email a verification link → verify screen) into
one direct step: the Bootstrap form collects name/email/username/password and posts
once to /bootstrap, which creates the first user directly as an active Super Admin
(+ provisions DATA_PATH/<email>). Still gated to an empty user table.

The invite flow (/verify, /verify-token) is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:11:37 +00:00
pastilhasandClaude Opus 4.8 d604b1e722 provisioning: drop multi-user remnants, provision the super admin on bootstrap
Remove the multi-user provisioning leftovers (the provision-existing-users.sh
migration was already deleted in the prior commit):
- gut provisionVncEnv from provision.ts (per-user startxfce4 virtual desktop, dead
  since the switch to mirroring :0 — vnc-manager.ts self-provisions its own passwd)
- drop the Pi `.pi/agent/sessions` seed and the now-orphaned `run` helper

Wire provisioning into bootstrapHandler: the super admin (first user) is created via
createUser, which never called provisionUserEnvironment — only the invite/verify flow
did. So the single user's DATA_PATH/<email> was never provisioned up front. Now it is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 15:32:53 +00:00
pastilhasandClaude Opus 4.8 1e789b4c44 setup: Ubuntu GNOME-on-Xorg desktop + setup.sh hardening
setup-desktop.sh now installs ubuntu-desktop + gdm3 + x11vnc and forces the
Xorg session (WaylandEnable=false) with auto-login — x11vnc can only mirror an
Xorg :0, not Wayland. vnc-manager.ts resolves the X authority from the GDM
per-session path (/run/user/<uid>/gdm/Xauthority) with a ~/.Xauthority fallback.

setup.sh fixes:
- desktop step gates on `dpkg -s ubuntu-desktop` (was the decommissioned
  officer-vnc service, which never matched so setup-desktop re-ran every time)
- remove Pi (install, --list-models validation, verification check)
- export GOPATH before the cliamp build so `go install` lands where it's checked
  even when Go was already present this run
- write PUBLIC_BUILD_ENV=production and quote all .env values
- guard the interactive .env block behind a TTY check so non-interactive runs
  skip cleanly instead of aborting on read EOF under set -e
- restart systemd-logind only when a key actually changed
- sed prefix-strip instead of `tr -d` (which deletes characters, not a prefix)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 15:32:28 +00:00
pastilhasandClaude Opus 4.8 5d077a4a54 route OpenCode chat through the officer-opencode sidecar
Turns now run in the sidecar via `opencode run --dir <cwd> --format json
--dangerously-skip-permissions [-s <ses_>]` instead of the serve's
`POST /session/{id}/message` path. That path was unreliable at reporting
tool completion — tools finished but the turn stayed status=running,
wedging the UI at "Working…". `run` re-anchors tools to the chat cwd via
--dir, reports completion faithfully, and exits when done.

- runner.ts (new): spawn `run`, map its JSON events (text/tool_use/
  step_finish) to ChatEvent, report the `ses_` id for resume, accumulate
  cost; inactivity (120s) + hard-cap (10min) watchdogs kill a hung turn
  and emit a clean error instead of hanging forever.
- protocol.ts: opencode:run-streaming/kill commands; opencode:spawned/
  event/session events; OpenCodeRunParams.
- sidecar index.ts: wire run/kill; sweepStaleServes() on startup kills
  only an `opencode serve` whose resolved /proc/<pid>/cwd == SERVE_CWD,
  so an unclean prior exit can't leave two.
- sidecar-registry.ts: spawnOpenCodeStreaming/killOpenCode/onOpenCodeEvent/
  onOpenCodeSession helpers.
- send-opencode.ts: rewritten to mirror send-claude-code (subscribe →
  resolve resume id → spawn → kill handle).
- sidecar-server.ts: persist reported ses_ id into state for resume.
- list-models/server-manager: route to the sidecar's reported serve URL.

The serve stays up only for read-only calls that never hung (model
listing, session history).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:54:52 +00:00
pastilhasandClaude Opus 4.8 dcb23b0a86 server: allowed origin from PUBLIC_URL; repoint web assets to the new domain
origin-validation: the production web origin now reads from PUBLIC_URL (.env), e.g.
https://officer.pastilhas.dev, instead of a hardcoded domain; drop alpha.officer.dev.
officer-web/index.html: point og:image/favicon/manifest/etc. at the new domain (served
locally from public/).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:43:57 +00:00
pastilhasandClaude Opus 4.8 203a8b0708 opencode: add the OpenCode sidecar (step 1 — serve lifecycle + port report)
New officer-opencode sidecar (same philosophy as officer-claude): a singleton that owns
an `opencode serve` running from DATA_PATH/opencode-sidecar (created if missing) on a
random port, registers with the API as capability 'opencode', and reports its port via a
new `opencode:server` protocol event. The API stores it (sidecar-server.ts, wired in
server.tsx via getOpenCodeServerUrl). ecosystem.config.cjs runs the sidecar instead of a
bare pm2 serve. Turn-running + API rewiring come in later steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:43:57 +00:00
pastilhasandClaude Opus 4.8 6fb48af402 chat: strengthen OpenCode cwd prompt — demand absolute paths on every tool call
The prior "treat this as your cwd" wording didn't stop the model from using bare
globs/relative paths, which OpenCode resolves against the server cwd. Reworded to
explicitly require absolute paths under the target dir on every tool call (and `cd`
for bash). Still a soft override; a stronger attempt before considering `opencode run --dir`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:46:02 +00:00
pastilhasandClaude Opus 4.8 d8e0dc79ef chat: send cwd on every message instead of reading it back from session metadata
Simpler + more efficient than the previous per-turn GET /session: the client already
has the selected cwd, so it now sends it on every message (it's constant for a session).
The server uses msg.cwd directly for OpenCode's per-turn working-directory system prompt,
and still tags the cwd at creation for listing. Drops the getSession round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:26:41 +00:00
pastilhasandClaude Opus 4.8 2a940de76a chat: keep OpenCode session cwd consistent across turns + strengthen the override
msg.cwd only rides the first message, but OpenCode's system prompt is rebuilt every
turn — so turn 2+ reverted to general_chat_sessions and the model fell back to the
server's real cwd. Fix: the cwd is bound at creation (metadata.officer.cwd), so for an
existing/resumed session read it back (new client.getSession) and reuse it for the
system prompt every turn. Also make the prompt explicit that it overrides any other
working directory the environment reports.

Verified: a session created with a cwd reads the same cwd back on a later turn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:21:28 +00:00
pastilhasandClaude Opus 4.8 88b74b2888 chat: OpenCode /chat cwd = general_chat_sessions (match Claude), drop home special-case
Both harnesses now use the resolved chat cwd as their working directory: Claude runs
in it natively, and OpenCode is told the same via its system prompt. Removes the
earlier special-case that pointed OpenCode at the user's home for /chat, so `workingDir`
collapses into `cwd` — which now both tags the session (metadata.officer.cwd) and drives
the Officer system prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:00:34 +00:00
pastilhasandClaude Opus 4.8 0c3f270419 chat: rename claude_sessions → general_chat_sessions; drop dead chat_sessions
The default /chat working directory is used by both the Claude and OpenCode harnesses
now, so its Claude-specific name was misleading.

- Rename the dir + accessors: getClaudeSessionsCwd → getGeneralChatSessionsCwd,
  ensureClaudeSessionsCwd → ensureGeneralChatSessionsCwd, path segment claude_sessions
  → general_chat_sessions (data-path on disk + code + UI labels/comments). No history
  migration — the old Claude transcript slug is orphaned (intentionally).

- Remove the vestigial chat_sessions dir (leftover from the retired session store):
  it only ever held empty claude/archived/ dirs, recreated by a signin hook. Drop that
  hook (+ its dead imports) and the 4 unused data-path accessors (getUserSessionsDir,
  getClaudeDir, getSessionDir, getArchivedSessionDir), and delete the dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:50:48 +00:00
pastilhasandClaude Opus 4.8 9a79a76b95 chat: inject an Officer system prompt telling OpenCode its working directory (step 2)
OpenCode can't set a real per-session cwd (every session runs in the fixed server's
dir), so we tell the model its working directory via a system prompt appended to
OpenCode's own — sent as a system message, so it never appears in the visible chat
(verified against source + live). Claude doesn't need this (it honors cwd natively).

- client.postMessage(…, system?) forwards a `system` string on the message.
- send-opencode builds the Officer prompt from `workingDir` and sends it every turn.
- websocket: workingDir = the resolved cwd, except the general /chat (whose cwd is the
  claude_sessions grouping placeholder) uses the user's home.

Verified live: with the prompt, the model reports the injected dir as its cwd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:06:17 +00:00
pastilhasandClaude Opus 4.8 7cd2be9fb9 chat: tag OpenCode sessions with officer metadata (cwd) + filter lists by it
OpenCode has no per-session directory (every session runs in the fixed server's cwd),
so sessions from every context (/chat pwd, email account, project) all landed in one
list. Now each session is tagged on creation with its logical cwd via the free-form
session `metadata`: { officer: { cwd } } — API-settable, round-trips on list+detail,
never touched by opencode core (confirmed by source dive + live test).

- client.createSession(metadata?) sends `metadata`; adds OfficerSessionMeta + officerMeta() helper.
- send-opencode tags new sessions with { officer: { cwd } } (the resolved chat cwd).
- websocket: pass the full resolved cwd for every context (not just non-/chat).
- listOpenCodeSessions(cwd?) filters by metadata.officer.cwd; chat.ts passes the request cwd.

Verified live: sessions tagged with distinct cwds list only under their own cwd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:50:48 +00:00
pastilhas bab0d1b7f8 Revert "chat: root the OpenCode server at the chat cwd (email chat now runs in the account dir)"
This reverts commit d60c73b3a3.
2026-07-25 10:08:48 +00:00
pastilhas 9b9938aaa9 Revert "chat: reap orphaned per-cwd opencode serves on startup"
This reverts commit 1f6eff3614.
2026-07-25 10:08:48 +00:00
pastilhasandClaude Opus 4.8 1f6eff3614 chat: reap orphaned per-cwd opencode serves on startup
Each pooled per-cwd server is a ~0.5GB process; across officer restarts the previous
instance's servers would orphan and pile up. On module load, kill any opencode serve on
a non-fixed port (the fixed OPENCODE_SERVER_URL port is preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:21:57 +00:00
pastilhasandClaude Opus 4.8 d60c73b3a3 chat: root the OpenCode server at the chat cwd (email chat now runs in the account dir)
OpenCode has no per-session `directory` — a session inherits the server's cwd (POST
/session ignores extra fields). So the previous "pass directory on create" was a no-op
and every OpenCode chat ran in the fixed server's dir (~), including the email chat.

Fix: hybrid server model. server-manager.ensureServer(cwd?, home?) returns the fixed
pm2 server (OPENCODE_SERVER_URL, :4096) for the general /chat, but for a context-scoped
cwd (email account dir, project dir) it spawns/pools an `opencode serve` rooted at that
directory — so the session's agent actually operates there. send-opencode passes the
resolved cwd + the user's home; createSession drops the ignored directory param.

Verified live: a cwd-scoped server reports the session directory as the target dir
(not ~), while /chat still uses :4096.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:20:15 +00:00
pastilhasandClaude Opus 4.8 cbe83f39a9 chat: run the /email chat from the email account's storage dir
The email chat's working directory now resolves to
DATA_PATH/<owner>/email_accounts/<accountEmail> (created if missing), so the agent
operates in the selected account's dir (emails.db, attachment_cache, …).

- websocket.ts: new resolveChatCwd — context 'email' → the account dir (via a new
  resolveEmailCwd), 'chat' → the pwd/claude_sessions dir, else the given cwd. Both the
  Claude and OpenCode handlers use it. The account defaults to the owner's first enabled
  account for now; the account selector will pass it as contextId later.
- OpenCode honors the cwd again: send-opencode passes it as the session `directory`
  (client.createSession(directory?)) for context-scoped chats; the general /chat still
  omits it and uses the fixed server's default project. Verified against the live server
  that directory-bound sessions create + list.

No frontend change — the /email panel already sends context:'email'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:37:48 +00:00
pastilhasandClaude Opus 4.8 7a60f2cb0e chat: fix chat WebSocket URL (double /chat) — was disconnecting every chat
useChat connected to `/api/chat/chat/ws` while the server listens on `/api/chat/ws`.
The double `chat` was a de-Pi rename artifact: the global `api/pi/` → `api/chat/` sed
rewrote `/api/pi/chat/ws` to `/api/chat/chat/ws`, and the targeted fix ran too late to
catch it. Result: the chat socket never connected, so the UI showed "Disconnected" and
the model selector — locked while disconnected — displayed only the active (Claude)
provider, hiding OpenCode. One-character path fix restores all chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:23:31 +00:00
pastilhasandClaude Opus 4.8 ab134aa02b chat: surface all OpenCode models (drop the curated allow-list)
Removes the Big Pickle + Claude Haiku allow-list; the picker now lists every model
the fixed server reports from GET /config/providers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:15:10 +00:00
pastilhasandClaude Opus 4.8 7b16f3bc4c chat: point OpenCode at a fixed pm2-managed server (fixes empty model picker)
The per-cwd `opencode serve` spawning is replaced by a single fixed server
(http://127.0.0.1:4096, OPENCODE_SERVER_URL) managed by pm2 — added as
`officer-opencode` in ecosystem.config.cjs (cwd = home).

Root-cause fix for the empty model selector: list-models shelled out to
`opencode models`, which failed at runtime on the deployed server (the picker got
only Claude tiers). It now reads the fixed server's GET /config/providers over HTTP —
11ms and reliable — so the curated OpenCode models (Big Pickle, Claude Haiku) show up.

- server-manager.ts — drops spawning; exposes OPENCODE_SERVER_URL + a health check.
- client.ts — createSession no longer binds a directory (sessions live in the one
  server's project).
- send-opencode.ts / opencode-sessions.ts / chat.ts — use the fixed server; drop the
  cwd/home plumbing. Session list/load/delete/rename now hit :4096.

Verified end-to-end against the live server: model list, streaming turn, session
list, and transcript load all work with no spawning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:12:53 +00:00
pastilhasandClaude Opus 4.8 059539a64a chat: unified session history across Claude + OpenCode (Phase 3)
/chat's session list, transcript load, delete, and rename now span both harnesses.

- opencode-sessions.ts — REST-backed reader (OpenCode's SQLite via its HTTP API, never
  the DB): listOpenCodeSessions / loadOpenCodeSession / delete / rename, returning the
  same shapes as the Claude reader, tagged harness:'opencode'. A serve is directory-
  scoped, so listing a cwd = asking the serve rooted there. Transcript rebuild maps
  user/assistant/tool parts and drops reasoning (parity with the delta filter).
- client.ts — adds listSessions/getMessages/deleteSession/renameSession over /session/*.
- chat.ts — /sessions merges both (newest first); /sessions/:id, DELETE, and
  /title route by id shape (ses_ = OpenCode). ClaudeSessionSummary gains an optional
  `harness` tag.
- send-opencode.ts — resuming from history: when the sessionKey is itself a ses_ id,
  reuse that OpenCode session instead of creating a new one.
- SessionList.tsx — shows an "OpenCode" badge for OpenCode sessions.

Verified end-to-end against a live serve: list (5 sessions, tagged), load (transcript
rebuilt, reasoning filtered), and rename all work. Phases 1-3 complete; needs a restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:56:20 +00:00
pastilhasandClaude Opus 4.8 3a2f3317b2 chat: curate OpenCode models to Big Pickle + Claude Haiku
The full `opencode models` catalog is ~58 entries; surface only opencode/big-pickle
and opencode/claude-haiku-4-5 in the picker for now via an allow-list. Easy to extend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:45:43 +00:00
pastilhasandClaude Opus 4.8 025c5dd38c chat: surface OpenCode models in the picker (Phase 2)
list-models.ts now merges the OpenCode catalog (from `opencode models`, cached;
ids are providerID/modelID) with the static Claude tiers, so /chat/models returns
both. invalidateModelCache clears the OpenCode cache for real now.

Adds the 'opencode' → 'OpenCode Zen' provider label in the /models response and the
ModelSelector's PROVIDER_DISPLAY. The existing useModels visibility gates already
pass non-claude-code providers through, so no gate changes are needed — Super Admin
sees all models. Selecting any non-claude-code model routes the turn to the OpenCode
harness (Phase 1).

Default model stays 'claude-code'. 58 OpenCode Zen models currently list; curating to
a flagship subset is an easy follow-up if the full catalog is unwieldy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:32:12 +00:00
pastilhasandClaude Opus 4.8 ad32c7516e chat: add OpenCode as a second harness — live turn (Phase 1)
Introduces an OpenCode chat harness alongside Claude, driven over HTTP + SSE against
a persistent `opencode serve`, emitting the same ChatEvent contract so the entire
chat UI and createEventHandler pipeline are unchanged.

New servers/api/chat/opencode/:
- server-manager.ts — one warm `opencode serve` per cwd (free port, health-gated,
  respawn on exit; HOME set so it reads the user's ~/.local/share/opencode auth).
  Binary pinned via OPENCODE_BIN (installed is 1.17.9; the 1.18.4 upgrade never landed).
- client.ts — per-server HTTP calls (/session create, /message, /abort) + a single
  reconnecting `/event` SSE stream demuxed to per-session listeners.
- event-mapper.ts — SSE → ChatEvent. Verified live against 1.17.9: message.part.delta
  → delta, tool parts → tool:start/tool:result, message.updated → cost, session.idle
  → result. Crucially, deltas are gated on partID being a `text` part (declared before
  its deltas) so the model's reasoning — which also streams as field:'text' — is
  dropped, matching the Claude harness hiding thinking.
- state.ts — sessionKey ↔ opencode ses_ id map for resume.

channels/send-opencode.ts — the OpenCode analog of send-claude-code: ensure serve,
create/reuse session, subscribe, post the message, forward mapped events; kill = abort.

websocket.ts — replaces the Claude-only coercion with harness routing:
provider 'claude-code' → Claude sidecar, everything else → handleOpenCodeChat.
handleStop aborts the right harness.

Verified end-to-end (streaming text, tool call/result, cost, abort) against a
throwaway serve using the free deepseek model — no prod restart involved. UI-level
model selection + session history follow in Phases 2–3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:29:55 +00:00
pastilhasandClaude Opus 4.8 669692355d chat: rename the pi-mono provider router + purge residual pi names (Stage 4b/2)
Renames the AI-harness/provider settings router into the chat namespace and
clears the remaining "pi" identifiers from the chat stack.

- server-settings/pi-mono.ts → chat-providers.ts; piMonoRouter → chatProvidersRouter;
  route /server-settings/pi-mono → /server-settings/chat-providers (+ all callers)
- piId → providerId (PROVIDERS map + AIHarnessesSection UI), PiProvider → ChatProvider,
  PI_MONO_* query keys → CHAT_PROVIDERS_*, installPiMono → installAgent
- data-path: PI_CONFIG_DIR → AGENT_CONFIG_DIR (path ~/.pi/agent unchanged);
  drop dead getPiMonoDir/getPiMonoSessionDir exports
- settings: flip the vestigial defaultProvider literal 'pi' → 'chat' (never read;
  only defaultModel drives behavior); access-policy config key 'pi-access-policy'
  → 'chat-access-policy'
- misc: ModelSelector fallback label, TaskDefaults model grouping, CapabilityPage
  chat var, a stale stream-parser comment

Intentionally left (genuine external `pi`/opencode references, not ours to rename):
the `pi` binary install/version flow (@mariozechner/pi-coding-agent, `which pi`),
the ~/.pi/agent config path, PI_TOOLS_DIRS/PI_SEARXNG_URL runtime env-var contract,
TOOL.md `targets: pi` metadata, and the "Pi Mono" installer UI label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:52:58 +00:00
pastilhasandClaude Opus 4.8 49df1c0b0c chat: rename the pi chat transport + model list to chat (Stage 4b/1)
Pure rename, no behavior change. Moves the misnamed "pi" chat harness into the
chat namespace:

- api/pi/{websocket,session-manager,types,logger,list-models} → api/chat/
- merge api/pi/rest.ts into api/chat/chat.ts (/pi/models → /chat/models,
  /pi/stt → /chat/stt); drop the piRestRouter mount
- PiEvent → ChatEvent, piWebsocket → chatWebsocket, listPiModels → listChatModels
- WS route /api/pi/chat/ws → /api/chat/ws, provider tag 'pi' → 'chat'
- frontend: useChat/useAudioRecording URLs, usePiModels→useModels /
  useVisiblePiModels→useVisibleModels / useEnabledPiModels→useEnabledModels,
  'PI_MODELS' query key → 'CHAT_MODELS', attachments provider 'pi-mono' → 'chat'

The /pi-mono provider/harness settings router is renamed separately (next commit).
Note: the WS route change requires the mobile app to point at /api/chat/ws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:47:40 +00:00
pastilhasandClaude Opus 4.8 70555c8d71 chat: retire the old saved-session store (Stage 4a)
Deletes all session persistence that isn't Claude's native transcript store, per
the "only harness-native session management survives" rule.

Backend: delete api/pi/storage.ts (meta.json+messages.json file store), the
api/saved-sessions router (+ unmount), the /pi/sessions REST endpoints, and the
storage.save/loadSession calls in the chat WS handler (in-memory session-manager
stays for live turns; no disk persistence — Claude's transcript is the record).
Also drops the Postgres saved_sessions layer: schema/chat.ts, queries/saved-sessions.ts,
its types and re-exports.

Frontend: delete state/useSavedSessions, ChatList, and the ChatHistory Widget
(all pure saved-session UI); slim ChatHeader to a label; strip the auto-load-latest
+ Save wiring from ChatPanelWrapper and ChatDetailPanel; drop the old resume path
from useChat and SessionListPage; remove the /chat/saved/:id route and the
useInitialData prefetch.

Behavior removed (intended): the Save-session button, email/project panels
auto-resuming the last chat, and /chat/saved/:id. /chat itself is unchanged —
already fully on Claude transcripts. The orphaned saved_sessions Postgres table
is dropped on the next `bun db:push`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:29:26 +00:00
pastilhasandClaude Opus 4.8 2e3c45ddfb chat: rename usePiChat → useChat, PiMonoInner → AgenticTaskRunner
Frontend de-Pi (Stage 3). Renames the chat hook usePiChat → useChat (+ UsePiChatType
→ UseChatType, file moved to hooks/useChat.ts) across all consumers, and renames
the TaskRunnerModal agentic runner PiMonoInner → AgenticTaskRunner (dropping the
dead defaultProvider === 'pi' check → always the Claude default). Pure rename, no
behavior change. The WS route stays /api/pi/chat/ws until Stage 4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:57:34 +00:00
pastilhasandClaude Opus 4.8 260d201731 chat: delete dead EmailChat.tsx
Orphaned standalone email-assistant chat, superseded by the officerdev/chat panel
(ChatPanelWrapper) that the email layout uses. Imported nowhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:47:09 +00:00
pastilhasandClaude Opus 4.8 f36b98db30 chat: heartbeat the chat WebSocket so long/idle turns don't drop the connection
Bun closes a WS idle for idleTimeout (60s) and only resets its timer on frames
received from the client — but during a chat turn the client only receives, so a
long tool call or a gap between turns tripped the timeout (clean close 1000,
mid-stream cuts, reconnects). Now each chat connection is pinged every 25s in the
WS open handler (cleared in close); the client auto-pongs at the protocol level,
resetting Bun's timer. Covers every client (web, mobile) with one server change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:43:42 +00:00
pastilhasandClaude Opus 4.8 6f7bf8df42 remove the Automation page
Deletes the Automation and NewAutomation screens, their /automation and
/new-automation routes, the Dashboard barrel exports, the dock item (+ default
dock path), and the page-title rule. TaskRunnerModal is kept — the file browser
still uses it to run tasks on files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:39:09 +00:00
pastilhasandClaude Opus 4.8 496f80dd5c header: fixed #1d2724 title color (mode-independent)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:58:25 +00:00
pastilhasandClaude Opus 4.8 746a7b84a8 header: page title in officer green, larger
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:55:49 +00:00
pastilhasandClaude Opus 4.8 f4cf20d378 header: per-route page titles + a centered, click-to-rename tab title
Each dashboard route now sets its own document.title (route→title map in
usePageTitleSync, mounted in DashboardLayout). The title also renders centered in
the top header; clicking it edits inline and updates the browser tab live —
per-tab only, reset on navigation/refresh (not persisted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:53:37 +00:00
pastilhasandClaude Opus 4.8 85b6a381e8 email: align MCP email_db account with the API (first enabled)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:38:52 +00:00
pastilhasandClaude Opus 4.8 33a0bb4578 email: store emails.db per account under email_accounts/<account>/
Reorganizes email storage: the DB moves from DATA_PATH/<user>/emails.db to
DATA_PATH/<user>/email_accounts/<accountEmail>/emails.db, with a shared
email_accounts/attachment_cache/ (was Gmail/emails/attachments). openEmailDb now
takes (owner, account); a new openUserEmailDb(owner, userId) resolves the user's
configured account (first enabled) for read paths. Threads the account through
email.ts, accounts, resync, queue sync, channel handlers, and the email_db MCP
tool path. Drops the dead getUserEmailDir helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:34:29 +00:00