feat(backend,frontend): tâches planifiées projet — tool MCP idea_scheduled_task_* et menu utilisateur #305, fix transcript lié indisponible après reboot #314 (QA verte)
#305: nouvel agrégat durable ScheduledProjectTask dans domain (scheduled_task.rs, ids, ports ScheduledProjectTaskStore), source de vérité <app_data_dir>/scheduled-project-tasks/<projectId>.json, schedules bornés V1 interval/daily/weekly avec fuseau, cycle À approuver/Active/Suspendue avec approbation obligatoire des tâches demandées par agent; use cases application/scheduled_task (list/create/update/delete/run_now/reconcile/fire) réutilisant BackgroundTask pour les runs avec distinction planifié/manuel; store infrastructure dédié + scheduler reconcile au boot sans rattrapage en rafale, cwd confiné au projet; 7 tools MCP idea_scheduled_task_* (list/read lecture seule, create/update/delete/run_now/set_enabled write-actions soumises aux permissions natives); commandes Tauri + routes HTTP aux contrats DTO identiques (camelCase nextRunAtMs/lastRunAtMs/lastTaskId/createdBy/updatedBy); frontend: gateways Tauri/HTTP/mock + types ports/domain, surface projet 'Tâches planifiées' (liste utilisateur+agent unifiée, création/édition avec récapitulatif avant activation, approbation/refus, exécuter maintenant, suspendre/reprendre, suppression), catalogage surface web (webSurfaceCatalog/viewPlacement/ViewPanelBody/WebWorkspace); docs/scheduled-project-tasks.md et sdk docs/services.md (047c38b: pas un service plugin V1, pas d'émulation via cron système). #314: correction de 'Conversation liée — transcript indisponible' après reboot: restauration du transcript lié côté application (orchestrator service, lifecycle, session_limit, background sink, terminal registry) et côté frontend (adapters agent/http frames/stream/requestResponse, workStateNormalization, CustomAgentChatView avec état et action utile), tests dto_chat/agent_lifecycle/structured_registry_d1/background_completion_sink étendus. QA verte rapportée par QA sur feature/305-scheduled-project-tasks-mcp, sans bloquant, pour #305 et #314. Hors périmètre worktree: tickets non liés, attachments/, scripts/; aucun push.
This commit is contained in:
@ -43,7 +43,11 @@ import {
|
||||
markProjectCliInteractionStopped,
|
||||
} from "@/features/projects/projectCliInteractionActivity";
|
||||
import { Button, EntryTimestamp, Spinner, cn, normalizeTimestamp } from "@/shared";
|
||||
import type { ChatAttachmentInput, ChatDraftScope } from "@/ports";
|
||||
import type {
|
||||
ChatAttachmentInput,
|
||||
ChatDraftScope,
|
||||
ConversationTranscriptUnavailableReason,
|
||||
} from "@/ports";
|
||||
|
||||
export interface CustomAgentChatViewProps {
|
||||
projectId: string;
|
||||
@ -968,6 +972,9 @@ export function CustomAgentChatView({
|
||||
useState<ProfileCommandDialogState | null>(null);
|
||||
const [opening, setOpening] = useState(Boolean(sessionId || conversationId));
|
||||
const [openRetryNonce, setOpenRetryNonce] = useState(0);
|
||||
const [transcriptUnavailableReason, setTranscriptUnavailableReason] = useState<
|
||||
ConversationTranscriptUnavailableReason | "conversationNotFound" | null
|
||||
>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [stopping, setStopping] = useState(false);
|
||||
const [error, setError] = useState<ChatErrorState | null>(null);
|
||||
@ -1373,6 +1380,7 @@ export function CustomAgentChatView({
|
||||
|
||||
const receive = useCallback(
|
||||
(chunk: ReplyChunk) => {
|
||||
setTranscriptUnavailableReason(null);
|
||||
scrollbackChunkCountRef.current += 1;
|
||||
const activeInteractionId = activeInteractionIdRef.current;
|
||||
if (
|
||||
@ -1604,6 +1612,11 @@ export function CustomAgentChatView({
|
||||
applyChatViewState(replayed);
|
||||
setBusy(turnsIndicateBusy(replayed));
|
||||
}
|
||||
if (reattached.scrollback.length > 0) {
|
||||
setTranscriptUnavailableReason(null);
|
||||
} else if (reattached.transcriptUnavailableReason) {
|
||||
setTranscriptUnavailableReason(reattached.transcriptUnavailableReason);
|
||||
}
|
||||
return reattached.sessionId;
|
||||
},
|
||||
[agent, agentId, applyChatViewState, projectId, publishSessionId, receive],
|
||||
@ -1700,6 +1713,9 @@ export function CustomAgentChatView({
|
||||
if (launched.assignedConversationId) {
|
||||
onConversationIdRef.current(launched.assignedConversationId);
|
||||
}
|
||||
if (launched.transcriptUnavailableReason) {
|
||||
setTranscriptUnavailableReason(launched.transcriptUnavailableReason);
|
||||
}
|
||||
try {
|
||||
const recoveredSessionId = await reattachStructuredSession(launched.sessionId, {
|
||||
applyScrollback: options.applyScrollback,
|
||||
@ -1771,6 +1787,7 @@ export function CustomAgentChatView({
|
||||
});
|
||||
setOpening(true);
|
||||
setError(null);
|
||||
setTranscriptUnavailableReason(null);
|
||||
try {
|
||||
const localSession = sessionRef.current;
|
||||
if (!externalSessionId && localSession) {
|
||||
@ -1875,7 +1892,11 @@ export function CustomAgentChatView({
|
||||
sessionId: externalSessionId,
|
||||
error: instrumentationError(e),
|
||||
});
|
||||
if (!cancelled) {
|
||||
if (!cancelled && conversationId && isNotFound(e)) {
|
||||
setTranscriptUnavailableReason("conversationNotFound");
|
||||
setCurrentSession(null);
|
||||
publishSessionId(null);
|
||||
} else if (!cancelled) {
|
||||
setError({ message: describeOpenFailure(e), openFailure: true });
|
||||
}
|
||||
} finally {
|
||||
@ -1909,10 +1930,37 @@ export function CustomAgentChatView({
|
||||
);
|
||||
const hasLinkedConversation = Boolean(
|
||||
conversationId ||
|
||||
transcriptUnavailableReason === "missingTranscript" ||
|
||||
transcriptUnavailableReason === "conversationNotFound" ||
|
||||
requesterDelegation.currentAgentBusy ||
|
||||
restart ||
|
||||
delegationActive,
|
||||
);
|
||||
const unavailableLinkedTranscript =
|
||||
displayTurns.length === 0 &&
|
||||
hasLinkedConversation &&
|
||||
transcriptUnavailableReason !== "emptyConversation"
|
||||
? transcriptUnavailableReason ?? "missingTranscript"
|
||||
: null;
|
||||
|
||||
function unlinkUnavailableConversation() {
|
||||
onConversationIdRef.current(null);
|
||||
setTranscriptUnavailableReason("emptyConversation");
|
||||
setError(null);
|
||||
}
|
||||
|
||||
function retryUnavailableTranscript() {
|
||||
const sid = sessionRef.current;
|
||||
setTranscriptUnavailableReason(null);
|
||||
setError(null);
|
||||
if (sid) {
|
||||
// Force the next user-triggered pass through the attach path. Keeping the
|
||||
// local session here would trip the #307/idempotency short-circuit.
|
||||
setCurrentSession(null);
|
||||
setExternalSessionId(sid);
|
||||
}
|
||||
setOpenRetryNonce((value) => value + 1);
|
||||
}
|
||||
|
||||
async function openProfileDialog() {
|
||||
const availableProfiles = (await profileGateway.listProfiles()).filter((candidate) =>
|
||||
@ -2490,10 +2538,30 @@ export function CustomAgentChatView({
|
||||
{externalSessionId ? "Reprise en cours..." : "Ouverture de la session structurée…"}
|
||||
</span>
|
||||
</div>
|
||||
) : displayTurns.length === 0 && hasLinkedConversation ? (
|
||||
<p className="m-auto text-xs text-muted">
|
||||
Conversation liée — transcript indisponible.
|
||||
</p>
|
||||
) : unavailableLinkedTranscript ? (
|
||||
<div className="m-auto flex max-w-md flex-col items-center gap-3 rounded-md border border-warning/40 bg-warning/10 p-4 text-center">
|
||||
<p className="text-sm font-medium text-content">
|
||||
La conversation liée n’a pas pu être restaurée.
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
{unavailableLinkedTranscript === "conversationNotFound"
|
||||
? "Cette conversation est introuvable dans le stockage disponible."
|
||||
: "Son transcript n’est plus disponible après la reprise."}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
disabled={opening}
|
||||
onClick={retryUnavailableTranscript}
|
||||
>
|
||||
Réessayer
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={unlinkUnavailableConversation}>
|
||||
Délier la conversation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : displayTurns.length === 0 ? (
|
||||
<p className="m-auto text-xs text-muted">
|
||||
Envoyez un message pour démarrer la conversation structurée.
|
||||
|
||||
Reference in New Issue
Block a user