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:
@ -136,6 +136,7 @@ import type {
|
||||
OpenTerminalOptions,
|
||||
ProfileGateway,
|
||||
ProjectGateway,
|
||||
ProjectSettings,
|
||||
PermissionGateway,
|
||||
PluginConfigDocumentReadInput,
|
||||
PluginConfigDocumentUpdateInput,
|
||||
@ -178,6 +179,8 @@ import type {
|
||||
CreateTicketInput,
|
||||
UpdateTicketInput,
|
||||
UiPreferencesGateway,
|
||||
UserSettings,
|
||||
UserSettingsGateway,
|
||||
ViewWindowClosed,
|
||||
ViewWindowSnapshot,
|
||||
WindowGateway,
|
||||
@ -414,7 +417,6 @@ export class MockAgentGateway implements AgentGateway {
|
||||
/** Runtime kind per live agent (`agentId → kind`). */
|
||||
private liveKindByAgent = new Map<string, "pty" | "structured">();
|
||||
/** Last stopped runtime per agent for idempotent stop. */
|
||||
private stoppedByAgent = new Map<string, StoppedLiveAgent>();
|
||||
/** Retained structured reply chunks per live chat session. */
|
||||
private chatScrollback = new Map<string, ReplyChunk[]>();
|
||||
/** Persisted custom chat drafts keyed by project/layout/node. */
|
||||
@ -539,14 +541,9 @@ export class MockAgentGateway implements AgentGateway {
|
||||
}
|
||||
const sessionId = this.liveSessionByAgent.get(agentId);
|
||||
if (!sessionId) {
|
||||
const stopped = this.stoppedByAgent.get(agentId);
|
||||
if (stopped) return stopped;
|
||||
const err: GatewayError = {
|
||||
code: "NOT_FOUND",
|
||||
message: `agent ${agentId} has no live session`,
|
||||
};
|
||||
throw err;
|
||||
return { agentId, stopped: [], alreadyStopped: true };
|
||||
}
|
||||
const nodeId = this.liveByAgent.get(agentId) ?? "mock-node";
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) session.closed = true;
|
||||
this.sessions.delete(sessionId);
|
||||
@ -557,11 +554,14 @@ export class MockAgentGateway implements AgentGateway {
|
||||
this.liveKindByAgent.delete(agentId);
|
||||
const stopped: StoppedLiveAgent = {
|
||||
agentId,
|
||||
sessionId,
|
||||
kind,
|
||||
runtimeState: "stopped",
|
||||
stopped: [{
|
||||
sessionId,
|
||||
nodeId,
|
||||
kind,
|
||||
runtimeState: "stopped",
|
||||
}],
|
||||
alreadyStopped: false,
|
||||
};
|
||||
this.stoppedByAgent.set(agentId, stopped);
|
||||
return stopped;
|
||||
}
|
||||
|
||||
@ -842,7 +842,6 @@ export class MockAgentGateway implements AgentGateway {
|
||||
this.liveByAgent.set(agentId, options.nodeId);
|
||||
this.liveSessionByAgent.set(agentId, sessionId);
|
||||
this.liveKindByAgent.set(agentId, "pty");
|
||||
this.stoppedByAgent.delete(agentId);
|
||||
}
|
||||
const clearLive = () => {
|
||||
if (this.liveByAgent.get(agentId) === options.nodeId) {
|
||||
@ -910,7 +909,6 @@ export class MockAgentGateway implements AgentGateway {
|
||||
this.liveByAgent.set(agentId, options.nodeId);
|
||||
this.liveSessionByAgent.set(agentId, sessionId);
|
||||
this.liveKindByAgent.set(agentId, "structured");
|
||||
this.stoppedByAgent.delete(agentId);
|
||||
}
|
||||
return {
|
||||
sessionId,
|
||||
@ -1251,6 +1249,7 @@ export class MockTerminalGateway implements TerminalGateway {
|
||||
export class MockProjectGateway implements ProjectGateway {
|
||||
private projects: Project[] = [];
|
||||
private contexts = new Map<string, string>();
|
||||
private settings = new Map<string, ProjectSettings>();
|
||||
private restoreProjectIds: string[] = [];
|
||||
private restoreFailures = new Map<string, OpenProjectRestoreFailure>();
|
||||
|
||||
@ -1426,6 +1425,28 @@ export class MockProjectGateway implements ProjectGateway {
|
||||
this.contexts.set(projectId, content);
|
||||
}
|
||||
|
||||
async readProjectSettings(projectId: string): Promise<ProjectSettings> {
|
||||
if (!this.projects.some((project) => project.id === projectId)) {
|
||||
throw { code: "NOT_FOUND", message: `project ${projectId} not found` };
|
||||
}
|
||||
return this.settings.get(projectId) ?? { themeColor: null };
|
||||
}
|
||||
|
||||
async updateProjectSettings(
|
||||
projectId: string,
|
||||
settings: ProjectSettings,
|
||||
): Promise<ProjectSettings> {
|
||||
if (!this.projects.some((project) => project.id === projectId)) {
|
||||
throw { code: "NOT_FOUND", message: `project ${projectId} not found` };
|
||||
}
|
||||
if (settings.themeColor !== null && !/^#[0-9A-Fa-f]{6}$/.test(settings.themeColor)) {
|
||||
throw { code: "INVALID", message: "themeColor must use #RRGGBB" };
|
||||
}
|
||||
const stored = { themeColor: settings.themeColor?.toUpperCase() ?? null };
|
||||
this.settings.set(projectId, stored);
|
||||
return stored;
|
||||
}
|
||||
|
||||
async listProjectFiles(_projectId: string, path: string): Promise<ProjectFileListing> {
|
||||
return { path, entries: [] };
|
||||
}
|
||||
@ -4838,6 +4859,7 @@ export function createMockGateways(): Gateways {
|
||||
window: new MockWindowGateway(),
|
||||
focusedProject: new MockFocusedProjectGateway(),
|
||||
uiPreferences: new MockUiPreferencesGateway(),
|
||||
userSettings: new MockUserSettingsGateway(),
|
||||
plugin: new MockPluginGateway(),
|
||||
pluginWorkspace: new MockPluginWorkspaceGateway(),
|
||||
pluginTask: new MockPluginTaskGateway(),
|
||||
@ -4848,5 +4870,18 @@ export function createMockGateways(): Gateways {
|
||||
};
|
||||
}
|
||||
|
||||
export class MockUserSettingsGateway implements UserSettingsGateway {
|
||||
private settings: UserSettings = { defaultAgentTerminalInterface: null };
|
||||
|
||||
async readUserSettings(): Promise<UserSettings> {
|
||||
return { ...this.settings };
|
||||
}
|
||||
|
||||
async updateUserSettings(settings: UserSettings): Promise<UserSettings> {
|
||||
this.settings = { ...settings };
|
||||
return { ...this.settings };
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export GatewayError so tests can reference the thrown shape.
|
||||
export type { GatewayError };
|
||||
|
||||
Reference in New Issue
Block a user