v1.9: settings pane + per-project defaults + bulk archive + themes lift
Adds a singleton, ephemeral 'settings' pane kind to the workspace. Opened via a new bottom-pinned button in ProjectSidebar (emits an open_settings_pane event when a session is mounted; navigates to /settings otherwise). Pane has three sections — Session, Project, Theme — and a maximize toggle that hides sibling pane columns via display:none on desktop only. Settings panes don't count toward MAX_PANES and are filtered out of the localStorage persistence layer so reload always restores a clean workspace. Schema (additive): - projects.default_system_prompt TEXT NOT NULL DEFAULT '' - projects.default_web_search_enabled BOOLEAN NOT NULL DEFAULT false - sessions.web_search_enabled BOOLEAN (nullable; null = inherit) Inference resolves user_prompt = session.system_prompt.trim() || project.default_system_prompt.trim() — empty/whitespace at either layer means "no override". Keeps the columns NOT NULL and matches the existing inherit semantics. Server routes: - GET /api/projects/:id (new; settings pane refetches on project_updated) - PATCH /api/projects/:id accepts default_system_prompt, default_web_search_enabled - PATCH /api/sessions/:id accepts web_search_enabled (tri-state) - POST /api/projects/:id/sessions/archive-all + GET /api/projects/:id/sessions/open-count - POST /api/sessions/:id/chats/archive-all + GET /api/sessions/:id/chats/open-count - PATCH /api/sessions/:id now broadcasts session_updated on every successful PATCH (was rename-only). Lets SettingsPane open in another tab pick up edits without a refetch. Bulk-archive publishes one session_archived / chat_archived frame per affected id so useSidebar's existing reducer cases handle them incrementally — no new frame type, no payload widening. ModelPicker refactored: shared ModelList inside a responsive shell. Desktop = labeled trigger + DropdownMenu, mobile = icon-only Cpu button + BottomSheet. Header in Session.tsx drops the pill wrap on mobile since the new trigger is the visual. ChatInput gains an icon-only '+' DropdownMenu next to AgentPicker when sessionId + webSearchEnabled props are provided. One item for now — Web search — with a checkmark reflecting the stored value (true), not the effective one. Click PATCHes the override; to restore inherit-from-project the user opens SettingsPane. ThemePicker lifted out of pages/Settings.tsx into a reusable component. The standalone /settings route is now a thin wrapper that mounts <ThemePicker /> with a Back button on top (navigate(-1) with fallback to '/'); the SettingsPane Theme tab renders the same picker bare. Project section delete-flow removed (button + confirm dialog + handler). Replaced with "Archive all sessions" using the same two-step count → confirm → fire pattern as "Archive all chats" in the Session section. api.projects.remove() stays in the client because useProjects.ts still uses it. Hand-rolled Switch primitive in SettingsPane (no shadcn switch in the project; spec said no new deps). Section nav is plain buttons (no shadcn Tabs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,9 +116,32 @@ function SessionInner({ sessionId }: { sessionId: string }) {
|
||||
event.session_id === sessionId
|
||||
) {
|
||||
navigate(`/project/${event.project_id}`);
|
||||
return;
|
||||
}
|
||||
// v1.9: any session_updated for this session triggers a full refetch so
|
||||
// SettingsPane (mounted in a workspace pane) picks up system_prompt /
|
||||
// web_search_enabled / model edits made from another tab.
|
||||
if (event.type === 'session_updated' && event.session_id === sessionId) {
|
||||
void api.sessions.get(sessionId).then((s) => {
|
||||
setSession(s);
|
||||
setName((prev) => (editingName ? prev : s.name));
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
// v1.9: project_updated → refetch project so the Project section in
|
||||
// SettingsPane reflects the new defaults.
|
||||
if (event.type === 'project_updated' && project && event.project_id === project.id) {
|
||||
void api.projects.get(project.id).then(setProject).catch(() => {});
|
||||
return;
|
||||
}
|
||||
// v1.9: sidebar Settings button broadcasts this when a session is
|
||||
// mounted; we own the workspace pane state, so we open/focus the
|
||||
// singleton settings pane here.
|
||||
if (event.type === 'open_settings_pane') {
|
||||
panesHook.openOrFocusSettingsPane();
|
||||
}
|
||||
});
|
||||
}, [sessionId, editingName, navigate]);
|
||||
}, [sessionId, editingName, navigate, project, panesHook]);
|
||||
|
||||
// v1.8: URL ?pane= sync (mobile only). Lifted from Workspace.tsx so
|
||||
// MobileTabSwitcher's onSwitchPane can push the same URL state and the
|
||||
@@ -211,15 +234,13 @@ function SessionInner({ sessionId }: { sessionId: string }) {
|
||||
</div>
|
||||
|
||||
{session && (
|
||||
<div className="inline-flex items-center rounded-full bg-muted/40 hover:bg-muted/70 px-1 shrink-0">
|
||||
<ModelPicker
|
||||
value={session.model}
|
||||
onChange={async (model) => {
|
||||
const updated = await api.sessions.update(session.id, { model });
|
||||
setSession(updated);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ModelPicker
|
||||
value={session.model}
|
||||
onChange={async (model) => {
|
||||
const updated = await api.sessions.update(session.id, { model });
|
||||
setSession(updated);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
@@ -337,6 +358,8 @@ function SessionInner({ sessionId }: { sessionId: string }) {
|
||||
}}
|
||||
panesHook={panesHook}
|
||||
chatsHook={chatsHook}
|
||||
session={session}
|
||||
project={project}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user