stop couriering a system prompt through the framework

`promptPrefix` was a workspace-context field: the email and browser screens set it, `WorkspaceView`
put it on the context, and `ChatPanelWrapper` read it back off. Only the chat app has ever understood
what the string is, so the framework was carrying an app's vocabulary between two places that both
know each other.

`components` already exists for this — a screen supplies its own component for a panel id, and
`PanelSlot` prefers it over the registry while still taking header and provider from the registry
entry, so a screen-mounted chat panel keeps its normal chrome. Both screens now do that, and pass the
prefix as a prop. `ChatPanelWrapper` is exported from the barrel for it.

Also removes the same prop from `WorkspaceLayout`, where it had no callers at all: every preview and
settings pane rendering through it was already handing its chat panels an undefined prefix.
This commit is contained in:
2026-08-07 09:40:47 +00:00
parent b1e0bac1e0
commit d3922bd9d3
8 changed files with 24 additions and 14 deletions
@@ -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
@@ -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';
@@ -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<string, PanelConfig>;
@@ -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 (
<WorkspaceProvider value={{ workspace: null, cwd: cwd ?? '~', promptPrefix, panelConfigs: collectPanelConfigs(layout), setPanelConfig: noop, swapSourceId: null, setSwapSourceId: noop, onSwap: noop, dragSourceId: null, setDragSourceId: noop, onMove: noop, onSetZoom: noop, maximizedPanelId: null, setMaximizedPanelId: noop, transitioningPanelId: null, isMobile: isMobile ?? false, onMobileBack: onMobileBack ?? null }}>
<WorkspaceProvider value={{ workspace: null, cwd: cwd ?? '~', panelConfigs: collectPanelConfigs(layout), setPanelConfig: noop, swapSourceId: null, setSwapSourceId: noop, onSwap: noop, dragSourceId: null, setDragSourceId: noop, onMove: noop, onSetZoom: noop, maximizedPanelId: null, setMaximizedPanelId: noop, transitioningPanelId: null, isMobile: isMobile ?? false, onMobileBack: onMobileBack ?? null }}>
<WorkspaceRenderer
layout={layout}
registry={registry}
@@ -16,7 +16,6 @@ type WorkspaceViewProps = {
locked?: boolean;
cwd?: string;
root?: string;
promptPrefix?: string;
components?: PanelComponents;
ephemeral?: EphemeralPanels | null;
mobilePanelId?: string;
@@ -25,7 +24,7 @@ type WorkspaceViewProps = {
const noop = () => {};
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,
+1
View File
@@ -13,6 +13,7 @@ export {
ModelSelector,
InputArea,
ChatLauncher,
ChatPanelWrapper,
AttachmentList,
AttachButton,
WebpageDialog,