diff --git a/src/apps/officer-web/Screens/Dashboard/Browser/BrowserScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Browser/BrowserScreen.tsx index 5e4f38c0..936491a5 100644 --- a/src/apps/officer-web/Screens/Dashboard/Browser/BrowserScreen.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Browser/BrowserScreen.tsx @@ -1,6 +1,6 @@ import { useMemo, useCallback } from 'react'; import type { LayoutNode, PanelComponents } from 'officerdev'; -import { WorkspaceView } from 'officerdev'; +import { WorkspaceView, ChatPanelWrapper } from 'officerdev'; import { useDashboardState } from 'state/useDashboardState'; import { useIsMobile } from 'hooks/useIsMobile'; import { useGlobal } from 'hooks/useGlobal'; @@ -10,6 +10,9 @@ import { TabPreview } from './TabPreview'; const PROMPT_PREFIX = `You are a browser assistant. The user has connected Chrome tabs via the Officer Browser Relay. You have a 'browser' tool — use it to list tabs, take screenshots, navigate, evaluate JavaScript, and get page info. When asked about a page, take a screenshot first.`; +// This screen's prompt, so this screen's chat panel. See the note in `EmailScreen`. +const BrowserChatPanel = () => ; + export const BrowserScreen = () => { const isMobile = useIsMobile(); const [selectedId, setSelectedId] = useGlobal('BROWSER_SELECTED_TAB', null); @@ -19,6 +22,7 @@ export const BrowserScreen = () => { () => ({ 'browser-tabs': TabList, 'browser-preview': TabPreview, + 'browser-chat': BrowserChatPanel, }), [], ); @@ -32,7 +36,6 @@ export const BrowserScreen = () => { workspace={workspace} locked components={components} - promptPrefix={PROMPT_PREFIX} mobilePanelId={mobilePanelId} onMobilePanelChange={mobilePanelId ? () => onMobileBack() : undefined} /> diff --git a/src/apps/officer-web/Screens/Dashboard/Email/EmailScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Email/EmailScreen.tsx index 2ce7e454..364f8fb1 100644 --- a/src/apps/officer-web/Screens/Dashboard/Email/EmailScreen.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Email/EmailScreen.tsx @@ -1,7 +1,7 @@ import { useMemo, useCallback, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router'; import type { LayoutNode, PanelComponents } from 'officerdev'; -import { WorkspaceView } from 'officerdev'; +import { WorkspaceView, ChatPanelWrapper } from 'officerdev'; import { useDashboardState } from 'state/useDashboardState'; import { useIsMobile } from 'hooks/useIsMobile'; import { useGlobal } from 'hooks/useGlobal'; @@ -12,6 +12,11 @@ import { ComposeModal } from './Compose'; const PROMPT_PREFIX = `You are an email assistant. The user has a local SQLite email database available via the "email_db" tool — use it for all email queries (search, count, stats, aggregations, deletions) unless the user explicitly asks you to use Gmail. Do not use the Gmail integration for questions about existing emails.`; +// The prefix is this screen's, so this screen mounts the chat panel that gets it — rather than handing +// the string to the workspace to courier down to whichever app happens to read it. The panel id is the +// one in `defaultLayout`, and the layout is locked, so it is the same panel either way. +const EmailChatPanel = () => ; + export const EmailScreen = () => { const isMobile = useIsMobile(); const { emailId } = useParams<{ emailId?: string }>(); @@ -37,6 +42,7 @@ export const EmailScreen = () => { () => ({ 'email-list': EmailList, 'email-reader': EmailReader, + 'email-chat': EmailChatPanel, }), [], ); @@ -50,7 +56,6 @@ export const EmailScreen = () => { workspace={workspace} locked components={components} - promptPrefix={PROMPT_PREFIX} mobilePanelId={mobilePanelId} onMobilePanelChange={mobilePanelId ? () => onMobileBack() : undefined} /> diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx index 864b2bfe..b2e941d3 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx @@ -53,10 +53,13 @@ const ChatPanelInner = ({ ); }; -type ChatPanelWrapperProps = { panelId: string }; +// `promptPrefix` is a system prompt only this app understands, so the screen that wants one passes it +// here rather than through the workspace context — a screen supplies its own chat panel through +// `components`, which is what that prop exists for. The registry mounts this with no prefix. +type ChatPanelWrapperProps = { panelId: string; promptPrefix?: string }; -export const ChatPanelWrapper = ({ panelId }: ChatPanelWrapperProps) => { - const { workspace, cwd, root, promptPrefix } = useWorkspace(); +export const ChatPanelWrapper = ({ panelId, promptPrefix }: ChatPanelWrapperProps) => { + const { workspace, cwd, root } = useWorkspace(); const scoped = cwd !== '~'; // `contextId` stays the raw workspace key: it is the same string `agent_panels.dashboard_id` holds, and diff --git a/src/workspaces/officerdev/src/apps/Chat/index.ts b/src/workspaces/officerdev/src/apps/Chat/index.ts index 03f0854a..f1039ffe 100644 --- a/src/workspaces/officerdev/src/apps/Chat/index.ts +++ b/src/workspaces/officerdev/src/apps/Chat/index.ts @@ -10,6 +10,8 @@ export { QuestionActivity } from './components/QuestionActivity'; export { ModelSelector } from './components/ModelSelector'; export { InputArea } from './components/InputArea'; export { ChatLauncher } from './ChatLauncher'; +// Exported so a screen can mount the chat panel itself with a system prompt of its own. +export { ChatPanelWrapper } from './ChatPanelWrapper'; export { AttachmentList } from './components/AttachmentList'; export { AttachButton } from './components/AttachButton'; export { WebpageDialog } from './components/WebpageDialog'; diff --git a/src/workspaces/officerdev/src/components/Workspace/WorkspaceContext.ts b/src/workspaces/officerdev/src/components/Workspace/WorkspaceContext.ts index 77897c3a..86de39ca 100644 --- a/src/workspaces/officerdev/src/components/Workspace/WorkspaceContext.ts +++ b/src/workspaces/officerdev/src/components/Workspace/WorkspaceContext.ts @@ -9,7 +9,6 @@ type WorkspaceContextValue = { workspace: WorkspaceIdentity | null; cwd: string; root?: string; - promptPrefix?: string; // Per-panel app settings, by panel id. A map rather than a getter so a config change re-renders the // panel that owns it — apps read this through `usePanelConfig`, not directly. panelConfigs: Record; diff --git a/src/workspaces/officerdev/src/components/Workspace/WorkspaceLayout.tsx b/src/workspaces/officerdev/src/components/Workspace/WorkspaceLayout.tsx index 42cd9ffc..4ed3980d 100644 --- a/src/workspaces/officerdev/src/components/Workspace/WorkspaceLayout.tsx +++ b/src/workspaces/officerdev/src/components/Workspace/WorkspaceLayout.tsx @@ -11,7 +11,6 @@ type WorkspaceLayoutProps = { registry?: AppRegistryMap; components?: PanelComponents; cwd?: string; - promptPrefix?: string; noHeader?: boolean; isMobile?: boolean; mobilePanelId?: string; @@ -20,7 +19,7 @@ type WorkspaceLayoutProps = { const noop = () => {}; -export const WorkspaceLayout = ({ layout, onLayoutChange, registry: registryProp, components, cwd, promptPrefix, noHeader, isMobile, mobilePanelId, onMobileBack }: WorkspaceLayoutProps) => { +export const WorkspaceLayout = ({ layout, onLayoutChange, registry: registryProp, components, cwd, noHeader, isMobile, mobilePanelId, onMobileBack }: WorkspaceLayoutProps) => { const { registry: globalRegistry } = useAppRegistry(); const registry = registryProp ?? globalRegistry; @@ -37,7 +36,7 @@ export const WorkspaceLayout = ({ layout, onLayoutChange, registry: registryProp // dashboard previews. It is not a dashboard and not a screen, and a panel that asks should be told so // rather than handed something that parses. return ( - + {}; -export const WorkspaceView = ({ workspace, locked, cwd = '~', root, promptPrefix, components, ephemeral, mobilePanelId, onMobilePanelChange }: WorkspaceViewProps) => { +export const WorkspaceView = ({ workspace, locked, cwd = '~', root, components, ephemeral, mobilePanelId, onMobilePanelChange }: WorkspaceViewProps) => { const { registry } = useAppRegistry(); const isMobile = useIsMobile(); @@ -190,7 +189,6 @@ export const WorkspaceView = ({ workspace, locked, cwd = '~', root, promptPrefix workspace: identity, cwd, root, - promptPrefix, panelConfigs, setPanelConfig: locked ? noop : handleSetPanelConfig, swapSourceId, diff --git a/src/workspaces/officerdev/src/index.ts b/src/workspaces/officerdev/src/index.ts index 3748c0f8..ec9ce5d0 100644 --- a/src/workspaces/officerdev/src/index.ts +++ b/src/workspaces/officerdev/src/index.ts @@ -13,6 +13,7 @@ export { ModelSelector, InputArea, ChatLauncher, + ChatPanelWrapper, AttachmentList, AttachButton, WebpageDialog,