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:
@@ -1,9 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
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 { WorkspaceLayout } from 'officerdev';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
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
|
||||
// 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".
|
||||
const allSections = [...enterpriseSections, ...personalSections];
|
||||
const tabForSection = (section: string | undefined) =>
|
||||
|
||||
@@ -273,15 +273,19 @@ export const UsersSection = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Removal cascades — passkeys, dashboards, screens, email accounts, playlists — and there is no
|
||||
undo, so it is worth one deliberate confirmation. */}
|
||||
{/* Removal cascades — passkeys, dashboards, screens — and there is no undo, so it is worth one
|
||||
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)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove {confirmDelete?.email}?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This deletes the account and everything belonging to it — passkeys, dashboards, saved layouts, email
|
||||
accounts and playlists. It cannot be undone.
|
||||
This deletes the account and everything belonging to it — passkeys, dashboards, saved layouts, and
|
||||
anything an installed plugin keeps for them. It cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -60,8 +60,6 @@ export type DashboardInsert = typeof Schema.dashboards.$inferInsert;
|
||||
export type ScreenSelect = typeof Schema.screens.$inferSelect;
|
||||
export type ScreenInsert = typeof Schema.screens.$inferInsert;
|
||||
|
||||
// ── Email ──
|
||||
|
||||
// ── Server ──
|
||||
|
||||
export type ServerConfigSelect = typeof Schema.serverConfig.$inferSelect;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Empty on purpose.
|
||||
//
|
||||
// Both handlers that lived here were email syncs, and they moved into the officer-email sidecar, which
|
||||
// schedules its own work now (sidecar/email/sync-runner.ts). The queue itself is untouched and still
|
||||
// Both handlers that lived here were email syncs, and they moved into the email plugin's sidecar, which
|
||||
// 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.
|
||||
//
|
||||
// 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:stopped'; id: 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
|
||||
| { type: 'opencode:server'; port: number }
|
||||
// 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
|
||||
// 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.
|
||||
// `'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 =
|
||||
workspace?.kind === 'screen' && workspace.id === 'email'
|
||||
? { context: 'email' as const }
|
||||
|
||||
Reference in New Issue
Block a user