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:
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user