remove the email residue the extraction left in the platform
a sweep for remnants after 2026-08-18. nothing was broken - no dangling import, symbol, route or permission key anywhere in src/ - but the platform was still speaking the plugin's vocabulary in a dozen places. the one that mattered is live code rather than prose: ChatPanelWrapper hardcodes workspace.id === 'email' to send context: 'email', which is the frontend half of the pair whose backend half (resolveEmailCwd) carries a fifteen-line justification. the two ends now both say so, including that adding a SECOND id there is the signal the platform needs a real way for a plugin to declare a chat context. the rest: two dead imports left by removing the settings section, an empty 'Email' type-section header, a protocol.ts section header with no members naming the plugin's SSE design, and a queue comment pointing at sidecar/email/sync-runner.ts - a path in another repository now. CLAUDE.md, AGENTS.md and working-on-officer.md described email as a platform feature, an app permission and a pm2 process. those are the files a fresh agent reads first, so they were the highest-risk staleness in the tree. TODO.md's whole Email section moved to the plugin's own repository. it held OPEN items pointing at sidecar/email/store.ts, which is exactly how a leftover list sends someone to edit files that do not exist. the delete-account dialog promised it would remove email accounts and playlists. both tables belong to plugins now, so that cascade only happens where the plugin is installed - it now says 'anything an installed plugin keeps for them' instead of naming two it cannot guarantee. imapflow and mailparser STAY in package.json. the plugin has no package.json of its own and resolves from the platform's node_modules, so removing them would break the installed plugin's sidecar. the fix is giving plugins their own dependency manifest, not deleting these. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ strict subset of it**. Roles are `Admin` / `Member` / `Developer`; what each may
|
|||||||
per-role permission grants, resolved on every request.
|
per-role permission grants, resolved on every request.
|
||||||
|
|
||||||
If a design question turns on "which user", the answer depends on the surface: real for the **app**
|
If a design question turns on "which user", the answer depends on the surface: real for the **app**
|
||||||
permissions (gitea, music, photos, email, calendar…), and still always **the owner** for anything
|
permissions (gitea, music, photos, calendar…), and still always **the owner** for anything
|
||||||
that executes code or touches the disk — terminal, chat, tasks, files, desktop, browser are
|
that executes code or touches the disk — terminal, chat, tasks, files, desktop, browser are
|
||||||
`kind: 'execution'` and can never be granted. `src/servers/permissions/registry.ts` is the authority.
|
`kind: 'execution'` and can never be granted. `src/servers/permissions/registry.ts` is the authority.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Officer is a self-hosted platform built around one person — the server owner — which since 2026-08-07
|
Officer is a self-hosted platform built around one person — the server owner — which since 2026-08-07
|
||||||
also admits **additional accounts holding a strict subset of it**. It bundles an AI agent, a terminal,
|
also admits **additional accounts holding a strict subset of it**. It bundles an AI agent, a terminal,
|
||||||
a file browser, a code editor, email, a bitcoin wallet, a remote desktop and customisable dashboards
|
a file browser, a code editor, a bitcoin wallet, a remote desktop and customisable dashboards
|
||||||
behind a single web app.
|
behind a single web app.
|
||||||
|
|
||||||
**The owner/member split, and where the line falls.** This file said "single-user is a hard invariant,
|
**The owner/member split, and where the line falls.** This file said "single-user is a hard invariant,
|
||||||
@@ -30,7 +30,7 @@ written: `users` holds six rows. The accurate statement is narrower and more use
|
|||||||
since `getOwnerHomeDir` ignores the email it is passed. That rule lives there once and covers the
|
since `getOwnerHomeDir` ignores the email it is passed. That rule lives there once and covers the
|
||||||
HTTP routes, the websocket doors and the dock together.
|
HTTP routes, the websocket doors and the dock together.
|
||||||
|
|
||||||
So "which user is this" has a real answer for the **app** surface (gitea, music, photos, email,
|
So "which user is this" has a real answer for the **app** surface (gitea, music, photos,
|
||||||
calendar…) and for the **confined** one (terminal, chat, files), and is still always "the owner" for
|
calendar…) and for the **confined** one (terminal, chat, files), and is still always "the owner" for
|
||||||
anything under `execution`.
|
anything under `execution`.
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ One Bun process (`src/server.tsx`) serves everything:
|
|||||||
Long-running and privileged work lives in **sidecars**: separate processes that dial back in over
|
Long-running and privileged work lives in **sidecars**: separate processes that dial back in over
|
||||||
`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them
|
`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them
|
||||||
(the generated `ecosystem.config.cjs` — see below): `officer` (the server), `officer-anthropic-proxy`, `officer-claude-code`,
|
(the generated `ecosystem.config.cjs` — see below): `officer` (the server), `officer-anthropic-proxy`, `officer-claude-code`,
|
||||||
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
|
`officer-opencode`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
|
||||||
`officer-slskd`, `officer-headscale`, `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`,
|
`officer-slskd`, `officer-headscale`, `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`,
|
||||||
`officer-photos`, `officer-notify`, `officer-caldav`, `officer-memos`, `officer-jellyfin`, `officer-gitea`
|
`officer-photos`, `officer-notify`, `officer-caldav`, `officer-memos`, `officer-jellyfin`, `officer-gitea`
|
||||||
— twenty as of 2026-08-06, and a list that goes stale every time a sidecar lands. `pm2 jlist` is the
|
— twenty as of 2026-08-06, and a list that goes stale every time a sidecar lands. `pm2 jlist` is the
|
||||||
@@ -125,7 +125,7 @@ imported by their package name (`officerdev`, `hooks`, `state`, `types`, `helper
|
|||||||
Two stores, and the split matters:
|
Two stores, and the split matters:
|
||||||
|
|
||||||
**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards,
|
**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards,
|
||||||
email accounts, queue and pipeline jobs. Since 2026-08-16 there is a SECOND database,
|
queue and pipeline jobs. Since 2026-08-16 there is a SECOND database,
|
||||||
`officer_marketplace` (`src/databases/marketplace_db`), and the split is deliberate: it is a public
|
`officer_marketplace` (`src/databases/marketplace_db`), and the split is deliberate: it is a public
|
||||||
catalogue served by a process with no login, no session and no user, so sharing a connection would put
|
catalogue served by a process with no login, no session and no user, so sharing a connection would put
|
||||||
an unauthenticated surface one query away from every private table. It runs only on the host serving
|
an unauthenticated surface one query away from every private table. It runs only on the host serving
|
||||||
@@ -135,9 +135,9 @@ with the plugin ones commented out. Types inferred from the schema in `src/types
|
|||||||
|
|
||||||
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` (`$OFFICER_ROOT/capabilities`)
|
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` (`$OFFICER_ROOT/capabilities`)
|
||||||
contains one directory per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no
|
contains one directory per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no
|
||||||
database rows, no scope tiers. `DATA_PATH/<email>/` holds the managed home, attachments and the
|
database rows, no scope tiers. `DATA_PATH/<email>/` holds the managed home and attachments. It also holds
|
||||||
per-account email SQLite stores — those are the **email sidecar's**, and nothing in the platform opens
|
`email_accounts/`, which is the **email plugin's** — the platform pre-creates the directory and never opens
|
||||||
them.
|
what is inside it.
|
||||||
|
|
||||||
**None of those paths is configured.** Since 2026-08-13 `src/servers/data-path.ts` derives the install
|
**None of those paths is configured.** Since 2026-08-13 `src/servers/data-path.ts` derives the install
|
||||||
root as `resolve(process.cwd(), '..')` and hangs `data/`, `permissions/` and `dockers/` off it. That
|
root as `resolve(process.cwd(), '..')` and hangs `data/`, `permissions/` and `dockers/` off it. That
|
||||||
@@ -372,7 +372,7 @@ link-focusable). Half the app still does this; none of the new code should.
|
|||||||
copy it. A disabled entry renders as a `<span>`; a disabled `<a>` is not a thing. A control that
|
copy it. A disabled entry renders as a `<span>`; a disabled `<a>` is not a thing. A control that
|
||||||
_mutates_ rather than navigates stays a `<button>`.
|
_mutates_ rather than navigates stays a `<button>`.
|
||||||
- **Route pairs.** A bare screen route plus a param route rendering the same component: `/chat` +
|
- **Route pairs.** A bare screen route plus a param route rendering the same component: `/chat` +
|
||||||
`/chat/:sessionId`, `/jobs` + `/jobs/:id`, `/email` + `/email/:emailId`, `/headscale` +
|
`/chat/:sessionId`, `/jobs` + `/jobs/:id`, `/headscale` +
|
||||||
`/headscale/:section`. One `<Navigate … replace />` guard in the screen, placed after all hooks,
|
`/headscale/:section`. One `<Navigate … replace />` guard in the screen, placed after all hooks,
|
||||||
canonicalises both the bare route and a bogus param.
|
canonicalises both the bare route and a bogus param.
|
||||||
- **Master list + live preview uses `?selected=<id>`**, not the detail route — linking rows straight
|
- **Master list + live preview uses `?selected=<id>`**, not the detail route — linking rows straight
|
||||||
|
|||||||
@@ -132,49 +132,11 @@ the owner's OS user and can never be granted. Indirection there really is accide
|
|||||||
|
|
||||||
## Email
|
## Email
|
||||||
|
|
||||||
- [x] **Gmail-style search operators** (done 2026-07-24, `0e5b91e`). `parseEmailQuery` +
|
Email became a plugin on 2026-08-18 (`plugins/email/`, `gitea.officer.dev/plugins/email`). Its work queue
|
||||||
`searchEmails` in `sidecar/email/store.ts` parse: `from:`/`to:`/`subject:`/`body:` → FTS5 column
|
went with it — the open items pointed at `sidecar/email/store.ts`, a path in another repository, which is
|
||||||
filters; `has:attachment`, `is:unread`/`is:read`, `label:X`, `before:`/`after:YYYY-MM-DD` → SQL
|
exactly the way a leftover list sends someone to edit files that are not there.
|
||||||
`WHERE` on `emails`; quoted values → exact phrases; free text → prefix-AND full-text; unknown
|
|
||||||
`op:val` falls back to free text. Structured-filters-only queries skip the FTS join. Validated
|
|
||||||
on the real 18k-mail DB. Search box placeholder hints at operators.
|
|
||||||
- [x] **`OR`** (done 2026-07-24, `85d38fb`). Query splits on top-level uppercase `OR` into branches;
|
|
||||||
each branch is a self-contained condition (`id IN (FTS subquery)` + its SQL filters) and
|
|
||||||
branches are OR'd — so OR works across full-text _and_ structured filters. Verified on the 18k
|
|
||||||
DB (`from:github OR from:deepgram` = 90+7 = 97 exactly).
|
|
||||||
- [ ] **Remaining:** parenthesised grouping (nesting `(a OR b) c`), `in:sent/inbox/spam/trash`
|
|
||||||
(folder scope), and relative dates (`newer_than:7d`). Grouping needs a recursive parser.
|
|
||||||
- [ ] **Group search results into threads too.** Folder views group by conversation (`ee11c94`), but
|
|
||||||
`/email/search` still returns one row per message. Apply the same `thread_id` collapse to the
|
|
||||||
FTS result set so search matches Gmail's grouped results.
|
|
||||||
|
|
||||||
- [x] **Conversation threading** (done 2026-07-24, `ee11c94`). `thread_id` column on `emails`:
|
Anything still owed to the mail client belongs in that repository's own TODO.
|
||||||
header-based for new mail (`id` = `sha1(Message-Id)`, so `References[0]` hashes to the root's
|
|
||||||
id — `computeThreadId` in `sidecar/email/store.ts`), subject+counterpart backfill for already-synced mail.
|
|
||||||
`/messages` collapses to one row per thread (window fn) with count/unread; `/thread/:id` +
|
|
||||||
`/thread/:id/read`; reader renders a collapsible stack. Verified on synthetic + 18k real DB.
|
|
||||||
- [ ] **Upgrade old mail to exact threading.** One-time full re-fetch to capture `References` for
|
|
||||||
already-synced mail — replaces the subject+counterpart fallback (which can over-merge recurring
|
|
||||||
same-subject mail from one sender). Heavy/network-bound; opt-in.
|
|
||||||
- [ ] **Store the RFC `Message-Id` header** on ingest so replies can set a real `In-Reply-To`
|
|
||||||
(currently reply threading leans on subject/participants; `id` is a local hash, not the header).
|
|
||||||
|
|
||||||
- [x] **Compose/reply/send** (done 2026-07-24). Gmail SMTP send with contacts autocomplete, rich
|
|
||||||
contenteditable body (inline images at the caret via paste/drag-drop, Bcc), single close button.
|
|
||||||
- [x] **`busy_timeout` on the email db** (done 2026-07-24, `9527e0b`). API + email sidecar share
|
|
||||||
`emails.db`; wait out a concurrent writer instead of 500-ing with "database is locked".
|
|
||||||
|
|
||||||
- [ ] **Multiple views in the Email screen (tabs).** The screen grows beyond the current mailbox into
|
|
||||||
several switchable views:
|
|
||||||
- [ ] **Sender/domain management view.** Left panel: controls to group mail **by sender** or **by
|
|
||||||
domain** (room for more grouping axes later). From a group, run bulk actions on that
|
|
||||||
sender/domain: **unsubscribe + mark irrelevant**, **delete all mail** from it, **block/ignore
|
|
||||||
future incoming**, etc. Think "inbox cleanup / triage" — operate on a whole sender at once. - Needs: aggregate query (count/size per sender + per domain), a block/ignore list the
|
|
||||||
sync/IDLE path honors on incoming, an unsubscribe action (List-Unsubscribe header / link),
|
|
||||||
and bulk delete over a sender/domain.
|
|
||||||
- [ ] **Chat view.** The existing AI email-assistant flow — likely stays roughly as-is, just lives
|
|
||||||
as one of the tabs (main view).
|
|
||||||
- Open question: tabs vs. some other view switcher; which view is default.
|
|
||||||
|
|
||||||
## Transmission
|
## Transmission
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ None of those paths is configured. `src/servers/data-path.ts` derives the root a
|
|||||||
`resolve(process.cwd(), '..')` and hangs the rest off it, which is why the pm2 `cwd` pin matters and
|
`resolve(process.cwd(), '..')` and hangs the rest off it, which is why the pm2 `cwd` pin matters and
|
||||||
why `assertInstallLayout` refuses to boot from the wrong directory.
|
why `assertInstallLayout` refuses to boot from the wrong directory.
|
||||||
|
|
||||||
Officer is a self-hosted platform: an AI agent, a terminal, a file browser, a code editor, email, a
|
Officer is a self-hosted platform: an AI agent, a terminal, a file browser, a code editor, a
|
||||||
bitcoin wallet, a remote desktop and dashboards, behind one web app. **It is built around one owner**
|
bitcoin wallet, a remote desktop and dashboards, behind one web app. **It is built around one owner**
|
||||||
— user id 1, role `Super Admin`, who bypasses every permission check — and since 2026-08-07 also
|
— user id 1, role `Super Admin`, who bypasses every permission check — and since 2026-08-07 also
|
||||||
admits **additional accounts holding a strict subset of it**, governed by per-role permission grants.
|
admits **additional accounts holding a strict subset of it**, governed by per-role permission grants.
|
||||||
|
|
||||||
So "which user" has three answers depending on the surface. For the **app** permissions (gitea,
|
So "which user" has three answers depending on the surface. For the **app** permissions (gitea,
|
||||||
music, photos, email, calendar…) it is a real question with a real answer. For **confined** ones —
|
music, photos, calendar…) it is a real question with a real answer. For **confined** ones —
|
||||||
terminal, chat, files — it is also real, because the account has its own Linux user and the kernel
|
terminal, chat, files — it is also real, because the account has its own Linux user and the kernel
|
||||||
enforces the boundary; a grant there means nothing without that user, and `authorize.ts` drops it.
|
enforces the boundary; a grant there means nothing without that user, and `authorize.ts` drops it.
|
||||||
For **execution** — tasks, items, desktop, browser — it is still always the owner, and those can
|
For **execution** — tasks, items, desktop, browser — it is still always the owner, and those can
|
||||||
@@ -71,7 +71,7 @@ does, renaming a category — none of that needs a code change or a restart.
|
|||||||
**`platform/` — only when the mechanism itself is missing.** If a task needs a form control that
|
**`platform/` — only when the mechanism itself is missing.** If a task needs a form control that
|
||||||
doesn't exist, or an endpoint that isn't there, that's platform work. Adding a _permission_ is not.
|
doesn't exist, or an endpoint that isn't there, that's platform work. Adding a _permission_ is not.
|
||||||
|
|
||||||
**`data/` — never edit by hand.** `DATA_PATH`. Holds the owner's managed home, per-account email
|
**`data/` — never edit by hand.** `DATA_PATH`. Holds the owner's managed home, per-account plugin
|
||||||
SQLite stores, job logs, the queue, sidecar state. It is not backed up by git; deleting things here
|
SQLite stores, job logs, the queue, sidecar state. It is not backed up by git; deleting things here
|
||||||
destroys the only copy.
|
destroys the only copy.
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ Commit messages: simple lowercase, no prefixes, explaining _why_.
|
|||||||
## Running and checking your work
|
## Running and checking your work
|
||||||
|
|
||||||
The server runs under pm2 as `officer`, plus sidecars (`officer-anthropic-proxy`, `officer-claude-code`,
|
The server runs under pm2 as `officer`, plus sidecars (`officer-anthropic-proxy`, `officer-claude-code`,
|
||||||
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
|
`officer-opencode`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
|
||||||
`officer-slskd`, `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`, and a process per
|
`officer-slskd`, `officer-transmission`, `officer-invoiceshelf`, `officer-wallet`, and a process per
|
||||||
installed plugin — `officer-offscale`, `officer-music`).
|
installed plugin — `officer-offscale`, `officer-music`).
|
||||||
`pm2 list` shows them; `pm2 logs officer` follows.
|
`pm2 list` shows them; `pm2 logs officer` follows.
|
||||||
@@ -107,7 +107,7 @@ and proxies API traffic; `officer-claude-code` is the process that actually runs
|
|||||||
**Which process to restart.** A change under `src/servers/sidecar/<name>/` needs that sidecar restarted;
|
**Which process to restart.** A change under `src/servers/sidecar/<name>/` needs that sidecar restarted;
|
||||||
a change anywhere else needs `officer`. Both, if you changed the wire between them. Restarting `officer`
|
a change anywhere else needs `officer`. Both, if you changed the wire between them. Restarting `officer`
|
||||||
no longer costs you a running agent session or a terminal — every sidecar is a PM2 peer, and the terminal
|
no longer costs you a running agent session or a terminal — every sidecar is a PM2 peer, and the terminal
|
||||||
and email sidecars serve their own listeners, so `officer` is not in their data path at all.
|
and pty sidecars serve their own listeners, so `officer` is not in their data path at all.
|
||||||
|
|
||||||
**Don't restart the owner's server to test.** Boot your own on a spare port instead — the running
|
**Don't restart the owner's server to test.** Boot your own on a spare port instead — the running
|
||||||
instance holds `PORT` from `.env` (9010):
|
instance holds `PORT` from `.env` (9010):
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { Puzzle, KeyRound, UserCircle, Globe, Wrench, CalendarDays } from 'lucide-react';
|
import { Puzzle, KeyRound, Globe, Wrench, CalendarDays } from 'lucide-react';
|
||||||
import type { LayoutNode, PanelComponents } from 'officerdev';
|
import type { LayoutNode, PanelComponents } from 'officerdev';
|
||||||
import { WorkspaceLayout } from 'officerdev';
|
import { WorkspaceLayout } from 'officerdev';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
|
||||||
import { SettingsRoute, SettingsSidebar, SettingsContent, type SettingsSection } from '../SettingsPanel';
|
import { SettingsRoute, SettingsSidebar, SettingsContent, type SettingsSection } from '../SettingsPanel';
|
||||||
@@ -70,7 +69,7 @@ const personalSections: SettingsSection[] = [
|
|||||||
|
|
||||||
// The Enterprise/Personal tab is not a second thing to remember — it is a fact about which section is
|
// The Enterprise/Personal tab is not a second thing to remember — it is a fact about which section is
|
||||||
// open, so it is derived from the URL rather than kept in its own global. That also fixes the deep link:
|
// open, so it is derived from the URL rather than kept in its own global. That also fixes the deep link:
|
||||||
// with the tab held separately, `/settings/integrations/email-accounts` would have arrived with the tab
|
// with the tab held separately, `/settings/integrations/apify` would have arrived with the tab
|
||||||
// still on Enterprise and the content pane showing "Select a section".
|
// still on Enterprise and the content pane showing "Select a section".
|
||||||
const allSections = [...enterpriseSections, ...personalSections];
|
const allSections = [...enterpriseSections, ...personalSections];
|
||||||
const tabForSection = (section: string | undefined) =>
|
const tabForSection = (section: string | undefined) =>
|
||||||
|
|||||||
@@ -273,15 +273,19 @@ export const UsersSection = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Removal cascades — passkeys, dashboards, screens, email accounts, playlists — and there is no
|
{/* Removal cascades — passkeys, dashboards, screens — and there is no undo, so it is worth one
|
||||||
undo, so it is worth one deliberate confirmation. */}
|
deliberate confirmation.
|
||||||
|
|
||||||
|
It used to enumerate email accounts and playlists too. Both tables belong to plugins now, so the
|
||||||
|
cascade reaches them only where that plugin is installed — and promising it unconditionally is a
|
||||||
|
promise the platform cannot keep. */}
|
||||||
<AlertDialog open={!!confirmDelete} onOpenChange={(open) => !open && setConfirmDelete(null)}>
|
<AlertDialog open={!!confirmDelete} onOpenChange={(open) => !open && setConfirmDelete(null)}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Remove {confirmDelete?.email}?</AlertDialogTitle>
|
<AlertDialogTitle>Remove {confirmDelete?.email}?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This deletes the account and everything belonging to it — passkeys, dashboards, saved layouts, email
|
This deletes the account and everything belonging to it — passkeys, dashboards, saved layouts, and
|
||||||
accounts and playlists. It cannot be undone.
|
anything an installed plugin keeps for them. It cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ export type DashboardInsert = typeof Schema.dashboards.$inferInsert;
|
|||||||
export type ScreenSelect = typeof Schema.screens.$inferSelect;
|
export type ScreenSelect = typeof Schema.screens.$inferSelect;
|
||||||
export type ScreenInsert = typeof Schema.screens.$inferInsert;
|
export type ScreenInsert = typeof Schema.screens.$inferInsert;
|
||||||
|
|
||||||
// ── Email ──
|
|
||||||
|
|
||||||
// ── Server ──
|
// ── Server ──
|
||||||
|
|
||||||
export type ServerConfigSelect = typeof Schema.serverConfig.$inferSelect;
|
export type ServerConfigSelect = typeof Schema.serverConfig.$inferSelect;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Empty on purpose.
|
// Empty on purpose.
|
||||||
//
|
//
|
||||||
// Both handlers that lived here were email syncs, and they moved into the officer-email sidecar, which
|
// Both handlers that lived here were email syncs, and they moved into the email plugin's sidecar, which
|
||||||
// schedules its own work now (sidecar/email/sync-runner.ts). The queue itself is untouched and still
|
// schedules its own work now (the email plugin). The queue itself is untouched and still
|
||||||
// serves the Jobs screen; it simply no longer has anything to do with mail.
|
// serves the Jobs screen; it simply no longer has anything to do with mail.
|
||||||
//
|
//
|
||||||
// New handlers register here by side-effect import, as before.
|
// New handlers register here by side-effect import, as before.
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ export type SidecarEvent =
|
|||||||
| { type: 'vnc:password'; id: string; password: string }
|
| { type: 'vnc:password'; id: string; password: string }
|
||||||
| { type: 'vnc:stopped'; id: string }
|
| { type: 'vnc:stopped'; id: string }
|
||||||
| { type: 'vnc:error'; id: string; error: string }
|
| { type: 'vnc:error'; id: string; error: string }
|
||||||
// Email — new mail no longer crosses this socket; the sidecar owns the SSE stream and pushes directly
|
|
||||||
// OpenCode — the sidecar reports where its `opencode serve` is listening (random port) on connect
|
// OpenCode — the sidecar reports where its `opencode serve` is listening (random port) on connect
|
||||||
| { type: 'opencode:server'; port: number }
|
| { type: 'opencode:server'; port: number }
|
||||||
// OpenCode turn streaming (analog of claude:*): spawned ack, per-message stream, session id report
|
// OpenCode turn streaming (analog of claude:*): spawned ack, per-message stream, session id report
|
||||||
|
|||||||
@@ -122,6 +122,17 @@ export const ChatPanelWrapper = ({ panelId, promptPrefix }: ChatPanelWrapperProp
|
|||||||
// `contextId` stays the raw workspace key: it is the same string `agent_panels.dashboard_id` holds, and
|
// `contextId` stays the raw workspace key: it is the same string `agent_panels.dashboard_id` holds, and
|
||||||
// a stored address should not be two different strings depending on which feature wrote it. The bare id
|
// a stored address should not be two different strings depending on which feature wrote it. The bare id
|
||||||
// is for deciding, which is what `kind` and `id` are doing above it.
|
// is for deciding, which is what `kind` and `id` are doing above it.
|
||||||
|
// `'email'` is a PLUGIN's screen id, hardcoded in a shared platform package, and it is the one piece of
|
||||||
|
// plugin vocabulary the extraction on 2026-08-18 could not remove.
|
||||||
|
//
|
||||||
|
// It is the frontend half of a pair: this sends `context: 'email'`, and the chat socket answers it by
|
||||||
|
// running the turn in that mailbox's directory (`resolveEmailCwd`). The backend half carries its own
|
||||||
|
// justification; this line is recorded here so the coupling is visible from both ends rather than only
|
||||||
|
// one. If the email plugin is not installed, no workspace has this id and the branch is simply never
|
||||||
|
// taken — it fails to nothing rather than to an error.
|
||||||
|
//
|
||||||
|
// The general fix is a way for a plugin to declare a chat context, which does not exist. Until it does,
|
||||||
|
// adding a SECOND id here is the signal that it needs to.
|
||||||
const chatContext =
|
const chatContext =
|
||||||
workspace?.kind === 'screen' && workspace.id === 'email'
|
workspace?.kind === 'screen' && workspace.id === 'email'
|
||||||
? { context: 'email' as const }
|
? { context: 'email' as const }
|
||||||
|
|||||||
Reference in New Issue
Block a user