feat: pin sessions to workspace revisions

This commit is contained in:
2026-08-04 05:05:20 +02:00
parent 90894176b6
commit 301db4bd85
14 changed files with 288 additions and 54 deletions
+22 -13
View File
@@ -1,13 +1,14 @@
// frontend/src/shell/SteerInput.tsx
import { useEffect, useRef, useState } from "react";
import { CornerDownLeft } from "lucide-react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { postSteer, createSession } from "../api/sessions";
import { ApiError } from "../api/client";
import { getSettings, putSettings, type Settings } from "../api/settings";
import { getSettings } from "../api/settings";
import { listWorkspaces } from "../api/workspaces";
import { listModels } from "../api/models";
import { useSessionStore } from "../store/sessionStore";
import { workspacePreferences, type WorkspacePreference } from "../workspaces/preferences";
const THINKING_LEVELS = ["low", "medium", "high"] as const;
@@ -151,25 +152,33 @@ export function SteerInput({
/**
* Status strip beneath the composer. Left: the active workspace selector.
* Right: live context usage plus model + thinking-level selectors (these replace
* the old Settings dialog and persist through PUT /settings).
* Right: live context usage plus browser-local workspace/model/thinking selectors.
*/
export function ComposerFooter() {
const qc = useQueryClient();
const { data: settings } = useQuery({ queryKey: ["settings"], queryFn: getSettings });
const { data: workspaces = [] } = useQuery({ queryKey: ["workspaces"], queryFn: listWorkspaces });
const { data: modelsData } = useQuery({ queryKey: ["models"], queryFn: listModels });
const models = modelsData?.models ?? [];
const tokenUsage = useSessionStore((state) => state.tokenUsage);
const [preferences, setPreferences] = useState<WorkspacePreference>(() => workspacePreferences.load());
const workspace = settings?.workspace ?? "";
const model = settings?.model ?? "";
const thinking = settings?.thinking ?? "medium";
useEffect(() => {
if (!settings) return;
setPreferences(workspacePreferences.migrate({
workspaceId: settings.workspace,
provider: settings.provider,
model: settings.model,
thinking: settings.thinking,
}));
}, [settings]);
// PUT /settings replaces the whole object, so always send the merged settings.
async function update(patch: Partial<Settings>) {
await putSettings({ workspace, provider: settings?.provider, model, thinking, ...patch });
qc.invalidateQueries({ queryKey: ["settings"] });
const workspace = preferences.workspaceId ?? settings?.workspace ?? "";
const model = preferences.model ?? settings?.model ?? "";
const thinking = preferences.thinking ?? settings?.thinking ?? "medium";
function update(patch: WorkspacePreference) {
const next = workspacePreferences.save({ ...preferences, ...patch });
setPreferences(next);
}
function onModelChange(id: string) {
@@ -185,7 +194,7 @@ export function ComposerFooter() {
return (
<div className="flex items-center justify-between gap-2 border-t border-border/60 px-3 py-1.5">
<div className="flex items-center gap-2">
<FooterSelect aria-label="Workspace" value={workspace} onChange={(v) => update({ workspace: v })}>
<FooterSelect aria-label="Workspace" value={workspace} onChange={(v) => update({ workspaceId: v })}>
{workspaces.length === 0 ? (
<option value="">— workspace —</option>
) : (