feat(backend,frontend): lot utilisateur #310 #311 #312 #313 — horodatage conversation, stop agent robuste, préférence terminal par défaut, paramètres projet (QA verte)

- #310: horodatage de tous les messages et événements de conversation visibles — composant partagé EntryTimestamp, timestamps propagés dans le transcript chat, les annonces inter-agent et le viewer de conversation, normalisation côté domain/adapters (conversation_log, paginate).
- #311: stop d'agent robuste pour PTY non monté, sessions headless et CLI custom — lifecycle/orchestrator durcis, ponts chat app-tauri/web-server alignés, arrêt fiable sans session orpheline.
- #312: préférence globale d'interface terminal par défaut — nouveau store settings (domain/application/infrastructure + tests), DTO et commandes backend, adapter frontend userSettings, panneau TerminalSettings dans les paramètres globaux.
- #313: Paramètres du projet — contexte de projet éditable et couleur de thème par projet (ProjectSettingsPanel + projectTheme), teinte des onglets projet, persistance via DTO projet.

QA verte (GO QA sur feature/batch-310-311-312-313-chat-stop-prefs-theme) — vérifications Git: git diff --cached --check OK; staging borné à crates/ et frontend/ (74 fichiers, +2105/-531), commit unique de lot conforme à la convention des lots (précédents batch-151/158/159, batch-210/211/212) car les fichiers partagés (dto.rs, commands.rs, CustomAgentChatView) entrelacent les 4 tickets.

fichiers .ideai/* (agents/test.md, chat-drafts.json, system-permissions.json, tickets/counter.json, tickets/index.json, tickets/308-313/), attachments/ et scripts/ hors périmètre et préservés en worktree ; aucun push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 21:26:16 +02:00
parent fc45594d9a
commit 402a60f213
74 changed files with 2105 additions and 531 deletions

View File

@ -81,6 +81,8 @@ import { useProjectActivity } from "./useProjectActivity";
import { ProjectTabs } from "./ProjectTabs";
import { ProjectActionsMenu } from "./ProjectActionsMenu";
import { RemoveProjectDialog } from "./RemoveProjectDialog";
import { PROJECT_THEME_CHANGED_EVENT } from "./ProjectSettingsPanel";
import { projectThemeStyle, validThemeColor } from "./projectTheme";
import { ViewPanelBody, type ViewPanelId } from "./ViewPanelBody";
import {
PANEL_TITLE,
@ -99,6 +101,7 @@ import {
const PANEL_SIZE: Record<PanelId, FloatingWindowSize> = {
projects: "md",
context: "lg",
projectSettings: "lg",
work: "lg",
tickets: "lg",
agents: "lg",
@ -235,6 +238,7 @@ export function ProjectsView() {
system,
window: windowGateway,
focusedProject,
project,
git,
agent,
} = useGateways();
@ -273,8 +277,15 @@ export function ProjectsView() {
const [removalPending, setRemovalPending] = useState(false);
const [removalError, setRemovalError] = useState<string | null>(null);
const [removalNotice, setRemovalNotice] = useState<Project | null>(null);
const [projectThemeColors, setProjectThemeColors] = useState<
Record<string, string | null>
>({});
const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null;
const openProjectThemeKey = vm.openTabs.map((tab) => tab.id).join("\u0000");
const activeThemeStyle = projectThemeStyle(
active ? projectThemeColors[active.id] : null,
);
const activeViewerConversation =
viewerConversation?.projectId === active?.id ? viewerConversation : null;
const showRestoreNotification =
@ -301,6 +312,40 @@ export function ProjectsView() {
useEffect(() => {
if (vm.restoreFailures.length === 0) setRestoreDetailsOpen(false);
}, [vm.restoreFailures.length]);
useEffect(() => {
let cancelled = false;
void Promise.all(
vm.openTabs.map(async (tab) => {
try {
const settings = await project.readProjectSettings(tab.id);
return [tab.id, validThemeColor(settings.themeColor)] as const;
} catch {
return [tab.id, null] as const;
}
}),
).then((entries) => {
if (!cancelled) setProjectThemeColors(Object.fromEntries(entries));
});
return () => {
cancelled = true;
};
}, [project, openProjectThemeKey]);
useEffect(() => {
const update = (event: Event) => {
const detail = (
event as CustomEvent<{ projectId: string; themeColor: string | null }>
).detail;
if (!detail?.projectId) return;
setProjectThemeColors((current) => ({
...current,
[detail.projectId]: validThemeColor(detail.themeColor),
}));
};
window.addEventListener(PROJECT_THEME_CHANGED_EVENT, update);
return () => window.removeEventListener(PROJECT_THEME_CHANGED_EVENT, update);
}, []);
const projectNameById = useMemo(() => {
const names = new Map<string, string>();
for (const project of vm.projects) names.set(project.id, project.name);
@ -601,6 +646,7 @@ export function ProjectsView() {
// placement is marked in the submenu (and the parent entry shows ● when the
// panel is open somewhere). Order per UX: content panels first, Projects last.
const panelOrder: PanelId[] = [
"projectSettings",
"context",
"work",
"tickets",
@ -659,6 +705,11 @@ export function ProjectsView() {
const nativePanelItems: MenuBarItem[] = panelOrder.map((panel) => ({
id: panel,
label: PANEL_TITLE[panel],
disabled: panel === "projectSettings" && !active,
title:
panel === "projectSettings" && !active
? "Ouvrez ou sélectionnez un projet pour accéder à ses paramètres."
: undefined,
active: placementOf(placements, panel) !== "closed",
onSelect: () => {},
submenu: placementSubmenu(panel),
@ -954,6 +1005,7 @@ export function ProjectsView() {
{/* ── Project tab bar (#26): tabs = open projects, "+" opens Projects ── */}
<ProjectTabs
items={vm.openTabs.map((t) => ({ id: t.id, label: t.name }))}
themeColorByProjectId={projectThemeColors}
activeTabId={vm.activeTabId}
onSelect={(id) => vm.activateTab(id)}
onClose={(id) => void vm.closeTab(id)}
@ -965,6 +1017,11 @@ export function ProjectsView() {
}}
/>
<div
data-testid="active-project-theme-scope"
className="flex min-h-0 flex-1 flex-col overflow-hidden bg-canvas"
style={activeThemeStyle}
>
{/* ── Menu bar (replaces the former left sidebar) ── */}
<PluginMenuRenderBoundary
resetKey={pluginMenuResetKey}
@ -1204,6 +1261,7 @@ export function ProjectsView() {
))}
</div>
)}
</div>
</div>
);
}