fix(frontend,application): lot utilisateur #315 — détachement Paramètres projet, couleurs projet fiables, restauration conversation custom CLI, images post-aller-retour projet, retrait menus Projects/Project Context (QA verte)

- détachement: le panneau Paramètres du projet devient détachable (mapping historique panel=context → projectSettings dans parseViewWindowParams, DETACHABLE/ViewPanelBody alignés, ProjectsView ne court-circuitant plus le refus); Retry d'échec de détachement préservé
- couleurs projet: projectTheme publie idea:project-theme-changed localement + BroadcastChannel inter-webviews (fenêtres OS détachées), garde de révision par projet dans ProjectsView/ViewWindow contre les lectures settings en course — plus de flash/retour au bleu par défaut
- conversation custom CLI: transcript_restore_expected ne considère plus l'absence de scrollback volatile comme une restauration échouée quand la session rouvre en SessionPlan::None (continuation vide valide); CustomAgentChatView n'affiche plus le faux « conversation liée n'a pas pu être restaurée »
- images: conservation des images de conversation après changement de projet puis retour
- menus Panneaux: retrait des entrées Projects et Project Context (panelOrder WebWorkspace, création/switch via tab-bar +, contexte projet fusionné dans Paramètres du projet)
- QA verte: tests frontend ciblés window/ViewWindow/ProjectSettingsPanel/ProjectsView.detach/projects/CustomAgentChatView/WebDesktopWorkspace + cargo test -p application lifecycle transcript_restore_expected; fichiers .ideai/* (tickets 308-315, index/counter, agents/test, chat-drafts, system-permissions, project-settings.json, attachments/) et scripts/ hors périmètre préservés en worktree; aucun push
This commit is contained in:
2026-09-11 11:27:09 +02:00
parent 352ec2d8b7
commit 45f819e1e8
15 changed files with 434 additions and 82 deletions

View File

@ -19,13 +19,18 @@
* and crucially no `openProject()` — opening a project is the main window's job.
*/
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { CustomPluginLayoutCell } from "@/domain";
import type { FocusedProject } from "@/ports";
import { Panel } from "@/shared";
import { PANEL_TITLE, type PanelId } from "@/features/projects";
import { ViewPanelBody, type ViewPanelId } from "@/features/projects";
import {
onProjectThemeChanged,
projectThemeStyle,
validThemeColor,
} from "@/features/projects/projectTheme";
import {
PluginLayoutCellView,
PluginRuntimeProvider,
@ -98,7 +103,8 @@ export function parseViewWindowParams(
};
}
const panel = params.get("panel");
const rawPanel = params.get("panel");
const panel = rawPanel === "context" ? "projectSettings" : rawPanel;
if (!panel || !DETACHABLE.has(panel)) return null;
return { kind: "panel", panel: panel as ViewPanelId };
}
@ -108,12 +114,14 @@ export interface ViewWindowProps {
}
export function ViewWindow({ panel }: ViewWindowProps) {
const { focusedProject } = useGateways();
const { focusedProject, project } = useGateways();
// The focused project this window follows. `undefined` = not yet resolved
// (initial read in flight); `null` = resolved, no project focused.
const [focus, setFocus] = useState<FocusedProject | null | undefined>(
undefined,
);
const [themeColors, setThemeColors] = useState<Record<string, string | null>>({});
const themeRevisionRef = useRef<Record<string, number>>({});
// Read the current focus at mount, then track changes. Never opens a project:
// the window is a pure follower of the main window's focus (#47).
@ -142,8 +150,42 @@ export function ViewWindow({ panel }: ViewWindowProps) {
};
}, [focusedProject]);
useEffect(() => {
if (!focus) return;
let cancelled = false;
const revision = themeRevisionRef.current[focus.id] ?? 0;
void project.readProjectSettings(focus.id).then((settings) => {
if (
!cancelled &&
(themeRevisionRef.current[focus.id] ?? 0) === revision
) {
setThemeColors((current) => ({
...current,
[focus.id]: validThemeColor(settings.themeColor),
}));
}
}).catch(() => {
// Keep the last known colour; settings content owns its own retry state.
});
return () => {
cancelled = true;
};
}, [focus?.id, project]);
useEffect(() => onProjectThemeChanged(({ projectId, themeColor }) => {
themeRevisionRef.current[projectId] =
(themeRevisionRef.current[projectId] ?? 0) + 1;
setThemeColors((current) => ({
...current,
[projectId]: validThemeColor(themeColor),
}));
}), []);
return (
<div className="flex h-full flex-col bg-canvas text-content">
<div
className="flex h-full flex-col bg-canvas text-content"
style={projectThemeStyle(focus ? themeColors[focus.id] : null)}
>
<header className="flex shrink-0 items-center gap-2 border-b border-border px-4 py-2.5">
<h1 className="text-sm font-semibold text-content">
{PANEL_TITLE[panel]}