Commit Graph
6 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 c538086f2b the chat says which mailbox it is in, and reads a brief instead of a prefix
the same arrangement proxy-manager got, with one difference that turned out to
matter: an email chat does not run in the plugin's chats directory, it runs in
the ACCOUNT's, beside emails.db.

three things.

the panel declares its chat context. it declared none, and it did not need to
while email was part of the platform — ChatPanelWrapper recognised the screen by
`workspace.id === 'email'` and supplied it. a plugin's workspace key is
`screens/plugin/email`, so that id became `plugin/email` on extraction day and
the branch silently stopped matching. every email chat since has run somewhere
with no mailbox anywhere near it. declaring it here is what that branch's own
comment asked for, and it carries the mailbox too, which a hardcoded screen id
never could — with two accounts the chat follows the picker.

the prompt prefix is now chat/CLAUDE.md. a prefix rides every message: paid per
turn forever, invisible as a design artefact because it lives in a typescript
constant, and visible in the one place it should not be. the file costs nothing
per turn and the person it is about can open it. it says more than the prefix
did, because it can afford to: answer from the local db and not from gmail, the
screen already shows what you would paste back, and a bulk delete over 19k rows
is not reversible from here.

the panel gets a header, so the conversation picker is scoped to this mailbox's
directory rather than absent. panelId is a prop now instead of the hardcoded
"email-chat" — two of these could never have held different conversations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-26 18:42:59 +00:00
pastilhasandClaude Opus 5 778d7d3f12 the reader reads the param the platform actually passes
clicking a message did nothing. the reading pane stayed on "Select an email to
read" whatever you opened, and it had been that way since email became a plugin.

the reader asks the URL which message is open. it asked for `emailId`, which was
the param name back when email was part of the platform and declared its own
`/email/:emailId` route. a plugin declares no routes: the platform mounts every
one at `<route>` and `<route>/:section`, so the id arrives as `section`. asking
for `emailId` got undefined, `selectedId` was null on every render, and the pane
rendered its empty state.

nothing else was wrong, which is why this took so long to place — the list
showed, the link navigated, the URL changed, and both API routes return full
data (verified against the real 19k-message store: /messages carries every
snippet, /thread/:id returns 55KB of html). one undefined param, and the whole
right-hand side of the screen looked dead.

still accepts `emailId`, so this keeps working if email ever gets its own route
again. the underlying smell is the platform naming a plugin's param for it —
worth fixing there, not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-26 18:32:29 +00:00
pastilhasandClaude Opus 5 c1c367456b the mailbox picker, so the multi-account backend is reachable
c60e3d6 made every route able to name its mailbox and left the UI unable to.
`?account=<id>` names one and `?account=all` spans them; nothing on screen
passed either, so the list showed whichever account sorted first enabled.

The selection is a QUERY PARAM, the same as the folder beside it and for the
same reason — it is addressable state, so a reload, a shared URL and a
cmd-click all land where they were meant to. The pills are real links built from
the live params, so the folder and any open email survive a mailbox switch.

`account` is in the react-query key. Without it, switching mailbox would serve
the previous one's page from cache under the new selection, which reads as the
filter not working rather than as a caching bug.

Absent means `all` in the UI and only in the UI. The routes keep treating absent
as "first enabled" — mail-scope.ts does that deliberately, because the mobile app
and the agent's email_db tool call them and know nothing about accounts. That
default is right for a caller that cannot choose and wrong for a person looking
at a screen: three accounts and you want all of it, not whichever sorts first.

Hidden with fewer than two accounts. A control that can say one thing is noise in
a toolbar that is already tight on a phone.

NOT visually verified: this machine has no email accounts configured, so the
picker cannot render here. tsgo clean, prettier clean, the 16 mail-scope tests
pass. It needs a look on a host with two accounts before it is trusted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-23 23:34:08 +00:00
pastilhasandClaude Opus 5 3db9534614 compose and add-account are real forms, so enter submits them
Compose's To/Cc/Bcc/Subject fields, and each of the three add-account
forms, sat in a plain div — enter did nothing and the button was the
only way in. Wrapped each in a <form onSubmit>, marked every button
that isn't the submit action type="button" (attach, cancel, cc/bcc
toggles, the recipient-suggestion picks), so none of them accidentally
submit now that a form exists around them. The compose body stays a
contenteditable div, which never triggers native enter-to-submit, so
Cmd/Ctrl+Enter wasn't needed to keep newlines working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:45:21 +00:00
pastilhasandClaude Opus 5 91b978c043 mount the accounts modal in the empty state, so a fresh install can add its first account
Clicking "Add Account" on a new install did nothing at all — no modal, no
error, nothing in the console.

EmailList early-returns an onboarding empty state when there is no mail, and
that branch carries the "Add Account" button. But <AccountsModal /> was only
mounted in the OTHER return — the one that renders when mail exists. The button
flipped the shared EMAIL_ACCOUNTS_OPEN global and nothing was listening.

A catch-22, and it closed the only door in: the sole other trigger is in the
toolbar, which is also in the mail-exists branch. So an install with no account
could never reach the dialog that adds one, and the failure was silent in both
directions — nothing threw, and the state flag really did flip.

Mount it in both branches. The empty state is exactly when it is needed most.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:50:46 +00:00
Claude Opus 5 980a9d3c38 email, extracted from the officer platform
IMAP and Gmail sync into a per-account SQLite store, with the mail read and
composed in the browser.

moved whole from src/servers/sidecar/email (14 files), src/servers/api/email,
officer_db/src/email, and the Screens/Dashboard/Email tree. what changed on the
way out:

- credentials are encrypted. the column was plaintext jsonb holding an IMAP app
  password that the SMTP transport read straight out of the row; it is now
  AES-256-GCM under this plugin's own 'email' secret-store purpose, sealed and
  opened in queries.ts alone so nothing above it can hold ciphertext by
  accident. no plaintext fallback on read - a value that does not verify is an
  error.
- the router derives its prefix from mountPrefix() instead of the literal
  '/api/email', which was only correct for a first-party publisher.
- the frontend became panels.ts + layout.ts. the two panel appTypes that were
  null are now declared, because the platform screen used to override them by
  panel id and a plugin has no such hook.
- account management became a modal. it was a section in the platform's
  Settings, which a plugin cannot contribute to, and a panel does not work
  either: PluginScreen renders the workspace locked, so a panel outside the
  default layout is unreachable. it follows Compose, which had already solved
  this.
- 17 palette classes replaced with semantic tokens.

not verified: this has never run. it was switched off in the platform on
2026-08-13 and its table was never created, so nothing has exercised sync,
sending or the UI since. the first install is the first test.
2026-08-18 00:29:50 +00:00