@creezio/tasks 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +279 -0
  2. package/dist/.creezio-src-hash.json +4 -0
  3. package/dist/ai-task-agent.d.ts +47 -0
  4. package/dist/ai-task-agent.d.ts.map +1 -0
  5. package/dist/ai-task-agent.js +558 -0
  6. package/dist/ai-task-agent.js.map +1 -0
  7. package/dist/ai-task-runner.d.ts +78 -0
  8. package/dist/ai-task-runner.d.ts.map +1 -0
  9. package/dist/ai-task-runner.js +645 -0
  10. package/dist/ai-task-runner.js.map +1 -0
  11. package/dist/api-mount.d.ts +4 -0
  12. package/dist/api-mount.d.ts.map +1 -0
  13. package/dist/api-mount.js +91 -0
  14. package/dist/api-mount.js.map +1 -0
  15. package/dist/assistant-adapter.d.ts +7 -0
  16. package/dist/assistant-adapter.d.ts.map +1 -0
  17. package/dist/assistant-adapter.js +96 -0
  18. package/dist/assistant-adapter.js.map +1 -0
  19. package/dist/brand/config.d.ts +165 -0
  20. package/dist/brand/config.d.ts.map +1 -0
  21. package/dist/brand/config.js +39 -0
  22. package/dist/brand/config.js.map +1 -0
  23. package/dist/env-bridge.d.ts +12 -0
  24. package/dist/env-bridge.d.ts.map +1 -0
  25. package/dist/env-bridge.js +33 -0
  26. package/dist/env-bridge.js.map +1 -0
  27. package/dist/hono-routes.d.ts +8 -0
  28. package/dist/hono-routes.d.ts.map +1 -0
  29. package/dist/hono-routes.js +563 -0
  30. package/dist/hono-routes.js.map +1 -0
  31. package/dist/index.d.ts +29 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +21 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/kanban-service.d.ts +136 -0
  36. package/dist/kanban-service.d.ts.map +1 -0
  37. package/dist/kanban-service.js +580 -0
  38. package/dist/kanban-service.js.map +1 -0
  39. package/dist/mcp-host-tools.d.ts +59 -0
  40. package/dist/mcp-host-tools.d.ts.map +1 -0
  41. package/dist/mcp-host-tools.js +326 -0
  42. package/dist/mcp-host-tools.js.map +1 -0
  43. package/dist/mcp-workspace-tools.d.ts +15 -0
  44. package/dist/mcp-workspace-tools.d.ts.map +1 -0
  45. package/dist/mcp-workspace-tools.js +338 -0
  46. package/dist/mcp-workspace-tools.js.map +1 -0
  47. package/dist/memory-store.d.ts +3 -0
  48. package/dist/memory-store.d.ts.map +1 -0
  49. package/dist/memory-store.js +75 -0
  50. package/dist/memory-store.js.map +1 -0
  51. package/dist/sqlite-driver.d.ts +13 -0
  52. package/dist/sqlite-driver.d.ts.map +1 -0
  53. package/dist/sqlite-driver.js +13 -0
  54. package/dist/sqlite-driver.js.map +1 -0
  55. package/dist/sqlite-store.d.ts +15 -0
  56. package/dist/sqlite-store.d.ts.map +1 -0
  57. package/dist/sqlite-store.js +113 -0
  58. package/dist/sqlite-store.js.map +1 -0
  59. package/dist/task-runs.d.ts +108 -0
  60. package/dist/task-runs.d.ts.map +1 -0
  61. package/dist/task-runs.js +427 -0
  62. package/dist/task-runs.js.map +1 -0
  63. package/dist/types.d.ts +35 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +15 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist-cjs/ai-task-agent.js +569 -0
  68. package/dist-cjs/ai-task-agent.js.map +1 -0
  69. package/dist-cjs/ai-task-runner.js +660 -0
  70. package/dist-cjs/ai-task-runner.js.map +1 -0
  71. package/dist-cjs/api-mount.js +94 -0
  72. package/dist-cjs/api-mount.js.map +1 -0
  73. package/dist-cjs/assistant-adapter.js +99 -0
  74. package/dist-cjs/assistant-adapter.js.map +1 -0
  75. package/dist-cjs/brand/config.js +47 -0
  76. package/dist-cjs/brand/config.js.map +1 -0
  77. package/dist-cjs/env-bridge.js +36 -0
  78. package/dist-cjs/env-bridge.js.map +1 -0
  79. package/dist-cjs/hono-routes.js +566 -0
  80. package/dist-cjs/hono-routes.js.map +1 -0
  81. package/dist-cjs/index.js +117 -0
  82. package/dist-cjs/index.js.map +1 -0
  83. package/dist-cjs/kanban-service.js +601 -0
  84. package/dist-cjs/kanban-service.js.map +1 -0
  85. package/dist-cjs/mcp-host-tools.js +333 -0
  86. package/dist-cjs/mcp-host-tools.js.map +1 -0
  87. package/dist-cjs/mcp-workspace-tools.js +342 -0
  88. package/dist-cjs/mcp-workspace-tools.js.map +1 -0
  89. package/dist-cjs/memory-store.js +81 -0
  90. package/dist-cjs/memory-store.js.map +1 -0
  91. package/dist-cjs/package.json +3 -0
  92. package/dist-cjs/sqlite-driver.js +16 -0
  93. package/dist-cjs/sqlite-driver.js.map +1 -0
  94. package/dist-cjs/sqlite-store.js +119 -0
  95. package/dist-cjs/sqlite-store.js.map +1 -0
  96. package/dist-cjs/task-runs.js +457 -0
  97. package/dist-cjs/task-runs.js.map +1 -0
  98. package/dist-cjs/types.js +18 -0
  99. package/dist-cjs/types.js.map +1 -0
  100. package/package.json +79 -0
  101. package/scripts/port-from-tempoflow.mjs +405 -0
  102. package/ui/ai-activity-panel.tsx +559 -0
  103. package/ui/index.ts +7 -0
  104. package/ui/task-detail-sheet.tsx +668 -0
  105. package/ui/tasks-kanban-client.tsx +647 -0
  106. package/ui/tasks-types.ts +120 -0
@@ -0,0 +1,668 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import {
5
+ Activity,
6
+ AlertCircle,
7
+ Bot,
8
+ CheckCircle2,
9
+ Clock3,
10
+ Eye,
11
+ FileText,
12
+ Loader2,
13
+ Play,
14
+ RefreshCw,
15
+ Sparkles,
16
+ } from "lucide-react";
17
+ import { formatDistanceToNow } from "date-fns";
18
+ import { fr } from "date-fns/locale";
19
+ import { toast } from "sonner";
20
+ import { Badge } from "@creezio/shell-ui/ui/kit";
21
+ import { Button } from "@creezio/shell-ui/ui/kit";
22
+ import { ScrollArea } from "@creezio/shell-ui/ui/kit";
23
+ import {
24
+ Sheet,
25
+ SheetContent,
26
+ SheetDescription,
27
+ SheetHeader,
28
+ SheetTitle,
29
+ } from "@creezio/shell-ui/ui/kit";
30
+ import { openAiWorkspaceView } from "@creezio/shell-ui/ui/kit";
31
+ import { cn } from "@creezio/shell-ui";
32
+ import { AiActivityPanel } from "./ai-activity-panel";
33
+ import {
34
+ EXECUTOR_META,
35
+ STATUS_META,
36
+ sourceLabel,
37
+ type TaskCard,
38
+ } from "./tasks-types";
39
+
40
+ type RunRow = {
41
+ id: string;
42
+ status: string;
43
+ step_count: number;
44
+ last_error?: string | null;
45
+ hitl_prompt?: string | null;
46
+ started_at?: string | null;
47
+ finished_at?: string | null;
48
+ created_at: string;
49
+ };
50
+
51
+ type HermesDetail = {
52
+ task: {
53
+ id: string;
54
+ title: string;
55
+ body?: string | null;
56
+ status: string;
57
+ result?: string | null;
58
+ last_failure_error?: string | null;
59
+ age_seconds?: {
60
+ created_age_seconds?: number | null;
61
+ time_to_complete_seconds?: number | null;
62
+ } | null;
63
+ };
64
+ comments: { id?: number | string; body?: string; author?: string }[];
65
+ events: {
66
+ id?: number;
67
+ kind?: string;
68
+ payload?: Record<string, unknown>;
69
+ created_at?: number;
70
+ }[];
71
+ runs: {
72
+ id?: string;
73
+ status?: string;
74
+ summary?: string | null;
75
+ error?: string | null;
76
+ started_at?: number | null;
77
+ finished_at?: number | null;
78
+ }[];
79
+ };
80
+
81
+ type DetailPayload = {
82
+ task: TaskCard;
83
+ hermes: HermesDetail | null;
84
+ hermesError: string | null;
85
+ runs: RunRow[];
86
+ subtasks: TaskCard[];
87
+ };
88
+
89
+ type TabId = "brief" | "activite" | "rapport" | "timeline";
90
+
91
+ function formatTs(isoOrUnix: string | number | null | undefined): string {
92
+ if (isoOrUnix == null || isoOrUnix === "") return "—";
93
+ const d =
94
+ typeof isoOrUnix === "number"
95
+ ? new Date(isoOrUnix * 1000)
96
+ : new Date(isoOrUnix);
97
+ if (Number.isNaN(d.getTime())) return "—";
98
+ return formatDistanceToNow(d, { addSuffix: true, locale: fr });
99
+ }
100
+
101
+ function eventLabel(kind?: string): string {
102
+ const map: Record<string, string> = {
103
+ created: "Créée",
104
+ status_changed: "Statut modifié",
105
+ claimed: "Prise en charge",
106
+ completed: "Terminée",
107
+ blocked: "Bloquée",
108
+ unblocked: "Débloquée",
109
+ comment: "Commentaire",
110
+ dispatched: "Dispatch",
111
+ };
112
+ return kind ? map[kind] || kind : "Événement";
113
+ }
114
+
115
+ export function TaskDetailSheet({
116
+ taskId,
117
+ open,
118
+ onOpenChange,
119
+ onStatusChange,
120
+ onChanged,
121
+ }: {
122
+ taskId: string | null;
123
+ open: boolean;
124
+ onOpenChange: (open: boolean) => void;
125
+ onStatusChange: (id: string, status: string) => Promise<void>;
126
+ onChanged?: () => void;
127
+ }) {
128
+ const [data, setData] = useState<DetailPayload | null>(null);
129
+ const [loading, setLoading] = useState(false);
130
+ const [tab, setTab] = useState<TabId>("brief");
131
+ const [acting, setActing] = useState(false);
132
+
133
+ async function load() {
134
+ if (!taskId) return;
135
+ setLoading(true);
136
+ try {
137
+ const res = await fetch(`/api/v1/tasks/${taskId}`);
138
+ const json = (await res.json()) as DetailPayload & { error?: string };
139
+ if (!res.ok) {
140
+ setData(null);
141
+ return;
142
+ }
143
+ setData(json);
144
+ } finally {
145
+ setLoading(false);
146
+ }
147
+ }
148
+
149
+ useEffect(() => {
150
+ if (open && taskId) void load();
151
+ if (!open) {
152
+ setData(null);
153
+ setTab("brief");
154
+ }
155
+ // eslint-disable-next-line react-hooks/exhaustive-deps
156
+ }, [open, taskId]);
157
+
158
+ const task = data?.task;
159
+ const hermes = data?.hermes;
160
+ const runs = data?.runs || [];
161
+ const latestRun = runs[0] || null;
162
+ const status = task ? STATUS_META[task.status] || STATUS_META.backlog : null;
163
+ const exec = task ? EXECUTOR_META[task.executor_kind] : null;
164
+
165
+ // À l'ouverture : onglet le plus pertinent selon l'exécutant et l'état.
166
+ useEffect(() => {
167
+ if (!task) return;
168
+ if (task.result || hermes?.task?.result) setTab("rapport");
169
+ else if (task.executor_kind === "ai" && latestRun) setTab("activite");
170
+ else setTab("brief");
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ }, [data?.task?.id]);
173
+
174
+ const reportText = useMemo(() => {
175
+ if (!task) return "";
176
+ const parts: string[] = [];
177
+ if (task.result) parts.push(task.result);
178
+ else if (hermes?.task?.result) parts.push(hermes.task.result);
179
+ for (const run of hermes?.runs || []) {
180
+ if (run.summary) parts.push(run.summary);
181
+ if (run.error) parts.push(`Erreur run : ${run.error}`);
182
+ }
183
+ for (const c of hermes?.comments || []) {
184
+ if (c.body) parts.push(c.body);
185
+ }
186
+ if (hermes?.task?.last_failure_error) {
187
+ parts.push(`Dernière erreur : ${hermes.task.last_failure_error}`);
188
+ }
189
+ return parts.filter(Boolean).join("\n\n---\n\n");
190
+ }, [task, hermes]);
191
+
192
+ const tabs = useMemo(() => {
193
+ const list: Array<{ id: TabId; label: string; icon: typeof FileText }> = [
194
+ { id: "brief", label: "Brief", icon: FileText },
195
+ ];
196
+ if (task?.executor_kind === "ai") {
197
+ list.push({ id: "activite", label: "Activité IA", icon: Activity });
198
+ }
199
+ list.push({ id: "rapport", label: "Rapport", icon: Bot });
200
+ if (task?.executor_kind === "hermes") {
201
+ list.push({ id: "timeline", label: "Timeline", icon: Clock3 });
202
+ }
203
+ return list;
204
+ }, [task?.executor_kind]);
205
+
206
+ async function act(statusNext: string) {
207
+ if (!task) return;
208
+ setActing(true);
209
+ try {
210
+ await onStatusChange(task.id, statusNext);
211
+ await load();
212
+ } finally {
213
+ setActing(false);
214
+ }
215
+ }
216
+
217
+ async function launchRun() {
218
+ if (!task) return;
219
+ setActing(true);
220
+ try {
221
+ const res = await fetch(`/api/v1/tasks/${task.id}/launch`, {
222
+ method: "POST",
223
+ });
224
+ const json = (await res.json()) as { error?: string };
225
+ if (!res.ok) {
226
+ toast.error(json.error || "Lancement impossible");
227
+ return;
228
+ }
229
+ toast.success("Run IA en file");
230
+ setTab("activite");
231
+ await load();
232
+ onChanged?.();
233
+ } finally {
234
+ setActing(false);
235
+ }
236
+ }
237
+
238
+ async function viewAsAi() {
239
+ if (!task?.assignee) return;
240
+ const result = await openAiWorkspaceView(
241
+ task.assignee.id,
242
+ task.assignee.username,
243
+ );
244
+ if (!result.ok) toast.error(result.error || "Workspace IA indisponible");
245
+ }
246
+
247
+ const activeRun = runs.find(
248
+ (r) => r.status === "queued" || r.status === "running",
249
+ );
250
+
251
+ return (
252
+ <Sheet open={open} onOpenChange={onOpenChange}>
253
+ <SheetContent
254
+ side="right"
255
+ className="w-full max-w-xl sm:max-w-xl md:max-w-2xl"
256
+ >
257
+ {loading && !task ? (
258
+ <div className="flex flex-1 items-center justify-center gap-2 text-sm text-slate-500">
259
+ <Loader2 className="h-4 w-4 animate-spin" /> Chargement du détail…
260
+ </div>
261
+ ) : !task ? (
262
+ <div className="flex flex-1 items-center justify-center text-sm text-slate-500">
263
+ Tâche introuvable
264
+ </div>
265
+ ) : (
266
+ <>
267
+ <SheetHeader className="border-b border-slate-100 bg-[linear-gradient(180deg,#f8fafc_0%,#fff_100%)]">
268
+ <div className="flex flex-wrap items-center gap-2">
269
+ {status ? (
270
+ <Badge className={cn("border-0 font-medium", status.badgeClass)}>
271
+ {status.label}
272
+ </Badge>
273
+ ) : null}
274
+ {exec ? (
275
+ <Badge className={cn("border-0 gap-1 font-medium", exec.badgeClass)}>
276
+ <Bot className="h-3 w-3" />
277
+ {task.assignee?.username
278
+ ? `${exec.short} · ${task.assignee.username}`
279
+ : exec.label}
280
+ </Badge>
281
+ ) : null}
282
+ {task.recurring_schedule ? (
283
+ <Badge variant="outline" className="gap-1 text-[11px]">
284
+ <RefreshCw className="h-3 w-3" />
285
+ {task.recurring_schedule}
286
+ </Badge>
287
+ ) : null}
288
+ <Badge variant="outline" className="text-[11px]">
289
+ {sourceLabel(task.source)}
290
+ </Badge>
291
+ </div>
292
+ <SheetTitle className="text-lg leading-snug tracking-tight">
293
+ {task.title}
294
+ </SheetTitle>
295
+ <SheetDescription className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
296
+ <span>Màj {formatTs(task.updated_at)}</span>
297
+ {task.executor_kind === "hermes" ? (
298
+ task.hermes_task_id ? (
299
+ <span className="font-mono text-slate-400">
300
+ {task.hermes_task_id}
301
+ </span>
302
+ ) : (
303
+ <span className="text-amber-700">Non liée à Hermes</span>
304
+ )
305
+ ) : null}
306
+ {activeRun ? (
307
+ <span className="inline-flex items-center gap-1 text-violet-700">
308
+ <Loader2 className="h-3 w-3 animate-spin" />
309
+ Run {activeRun.status}
310
+ </span>
311
+ ) : null}
312
+ </SheetDescription>
313
+ </SheetHeader>
314
+
315
+ <div className="flex shrink-0 gap-1 border-b border-slate-100 px-3 py-2">
316
+ {tabs.map((t) => {
317
+ const Icon = t.icon;
318
+ const active = tab === t.id;
319
+ const dot =
320
+ (t.id === "rapport" && Boolean(reportText)) ||
321
+ (t.id === "activite" && Boolean(activeRun));
322
+ return (
323
+ <button
324
+ key={t.id}
325
+ type="button"
326
+ onClick={() => setTab(t.id)}
327
+ className={cn(
328
+ "inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
329
+ active
330
+ ? "bg-slate-900 text-white"
331
+ : "text-slate-600 hover:bg-slate-100",
332
+ )}
333
+ >
334
+ <Icon className="h-3.5 w-3.5" />
335
+ {t.label}
336
+ {dot && !active ? (
337
+ <span className="ml-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500" />
338
+ ) : null}
339
+ </button>
340
+ );
341
+ })}
342
+ <button
343
+ type="button"
344
+ className="ml-auto inline-flex items-center gap-1 rounded-lg px-2 py-1.5 text-xs text-slate-500 hover:bg-slate-100"
345
+ onClick={() => void load()}
346
+ disabled={loading}
347
+ >
348
+ <RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
349
+ </button>
350
+ </div>
351
+
352
+ <ScrollArea className="min-h-0 flex-1 px-4 py-4">
353
+ {tab === "brief" ? (
354
+ <div className="space-y-4">
355
+ <section className="rounded-2xl border border-slate-200 bg-slate-50/70 p-4">
356
+ <div className="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
357
+ <Sparkles className="h-3.5 w-3.5 text-sky-600" />
358
+ Brief
359
+ </div>
360
+ {(hermes?.task?.body || task.body || "").trim() ? (
361
+ <pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-slate-800">
362
+ {(hermes?.task?.body || task.body).trim()}
363
+ </pre>
364
+ ) : (
365
+ <p className="text-sm text-slate-500">
366
+ Aucun brief détaillé — seul le titre est renseigné.
367
+ </p>
368
+ )}
369
+ </section>
370
+
371
+ <dl className="grid grid-cols-2 gap-3 text-xs">
372
+ <div className="rounded-xl border border-slate-100 p-3">
373
+ <dt className="text-slate-400">Exécutant</dt>
374
+ <dd className="mt-1 font-medium text-slate-800">
375
+ {exec?.label}
376
+ {task.assignee ? ` — ${task.assignee.username}` : ""}
377
+ </dd>
378
+ </div>
379
+ <div className="rounded-xl border border-slate-100 p-3">
380
+ <dt className="text-slate-400">Créée</dt>
381
+ <dd className="mt-1 font-medium text-slate-800">
382
+ {formatTs(task.created_at)}
383
+ </dd>
384
+ </div>
385
+ <div className="rounded-xl border border-slate-100 p-3">
386
+ <dt className="text-slate-400">Priorité</dt>
387
+ <dd className="mt-1 font-medium text-slate-800">
388
+ {task.priority}
389
+ </dd>
390
+ </div>
391
+ <div className="rounded-xl border border-slate-100 p-3">
392
+ <dt className="text-slate-400">Runs IA</dt>
393
+ <dd className="mt-1 font-medium text-slate-800">
394
+ {runs.length || "—"}
395
+ </dd>
396
+ </div>
397
+ </dl>
398
+
399
+ {(data?.subtasks?.length || 0) > 0 ? (
400
+ <section className="space-y-2">
401
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
402
+ Sous-tâches déléguées
403
+ </h3>
404
+ {data!.subtasks.map((st) => {
405
+ const stExec = EXECUTOR_META[st.executor_kind];
406
+ const stStatus = STATUS_META[st.status];
407
+ return (
408
+ <div
409
+ key={st.id}
410
+ className="flex items-center justify-between gap-2 rounded-xl border border-slate-100 px-3 py-2 text-xs"
411
+ >
412
+ <span className="min-w-0 truncate font-medium text-slate-800">
413
+ {st.title}
414
+ </span>
415
+ <span className="flex shrink-0 items-center gap-1.5">
416
+ <span
417
+ className={cn(
418
+ "rounded-md px-1.5 py-0.5 text-[10px] font-medium",
419
+ stExec.badgeClass,
420
+ )}
421
+ >
422
+ {stExec.short}
423
+ </span>
424
+ <span
425
+ className={cn(
426
+ "rounded-md px-1.5 py-0.5 text-[10px] font-medium",
427
+ stStatus?.badgeClass,
428
+ )}
429
+ >
430
+ {stStatus?.label || st.status}
431
+ </span>
432
+ </span>
433
+ </div>
434
+ );
435
+ })}
436
+ </section>
437
+ ) : null}
438
+
439
+ {data?.hermesError ? (
440
+ <div className="flex gap-2 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
441
+ <AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
442
+ Détail Hermes indisponible : {data.hermesError}
443
+ </div>
444
+ ) : null}
445
+ </div>
446
+ ) : null}
447
+
448
+ {tab === "activite" ? (
449
+ <div className="space-y-4">
450
+ {latestRun ? (
451
+ <AiActivityPanel
452
+ runId={activeRun?.id || latestRun.id}
453
+ title={`Run ${(activeRun || latestRun).status}`}
454
+ />
455
+ ) : (
456
+ <section className="flex flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-slate-200 bg-slate-50/50 px-6 py-12 text-center">
457
+ <Bot className="h-8 w-8 text-slate-300" />
458
+ <p className="text-sm font-medium text-slate-700">
459
+ Aucun run IA pour l&apos;instant
460
+ </p>
461
+ <p className="max-w-sm text-xs text-slate-500">
462
+ Lancez la tâche pour que le collaborateur IA travaille
463
+ dans son workspace.
464
+ </p>
465
+ </section>
466
+ )}
467
+
468
+ {runs.length > 1 ? (
469
+ <section className="space-y-2">
470
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
471
+ Historique des runs
472
+ </h3>
473
+ {runs.map((r) => (
474
+ <div
475
+ key={r.id}
476
+ className="flex items-center justify-between rounded-xl border border-slate-100 px-3 py-2 text-xs"
477
+ >
478
+ <span
479
+ className={cn(
480
+ "font-medium",
481
+ r.status === "succeeded" && "text-emerald-700",
482
+ r.status === "failed" && "text-rose-700",
483
+ (r.status === "running" || r.status === "queued") &&
484
+ "text-violet-700",
485
+ )}
486
+ >
487
+ {r.status} · {r.step_count} étapes
488
+ </span>
489
+ <span className="text-slate-400">
490
+ {formatTs(r.finished_at || r.started_at || r.created_at)}
491
+ </span>
492
+ </div>
493
+ ))}
494
+ </section>
495
+ ) : null}
496
+ </div>
497
+ ) : null}
498
+
499
+ {tab === "rapport" ? (
500
+ <div className="space-y-4">
501
+ {reportText ? (
502
+ <section className="rounded-2xl border border-emerald-200 bg-gradient-to-b from-emerald-50/80 to-white p-4">
503
+ <div className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-emerald-800">
504
+ <CheckCircle2 className="h-3.5 w-3.5" />
505
+ Rapport
506
+ </div>
507
+ <pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-slate-800">
508
+ {reportText}
509
+ </pre>
510
+ </section>
511
+ ) : (
512
+ <section className="flex flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-slate-200 bg-slate-50/50 px-6 py-12 text-center">
513
+ <Bot className="h-8 w-8 text-slate-300" />
514
+ <p className="text-sm font-medium text-slate-700">
515
+ Pas encore de rapport
516
+ </p>
517
+ <p className="max-w-sm text-xs text-slate-500">
518
+ {task.executor_kind === "hermes"
519
+ ? "Dès que Hermes termine la mission, le résultat apparaît ici."
520
+ : task.executor_kind === "ai"
521
+ ? "Le rapport du collaborateur IA apparaît ici quand il clôture la tâche."
522
+ : "Renseignez un résultat en terminant la tâche."}
523
+ </p>
524
+ </section>
525
+ )}
526
+
527
+ {(hermes?.runs?.length || 0) > 0 ? (
528
+ <section className="space-y-2">
529
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
530
+ Runs Hermes
531
+ </h3>
532
+ {hermes!.runs.map((run, i) => (
533
+ <div
534
+ key={run.id || i}
535
+ className="rounded-xl border border-slate-100 px-3 py-2 text-xs"
536
+ >
537
+ <div className="flex items-center justify-between gap-2">
538
+ <span className="font-medium text-slate-800">
539
+ {run.status || "run"}
540
+ </span>
541
+ <span className="text-slate-400">
542
+ {formatTs(run.finished_at || run.started_at)}
543
+ </span>
544
+ </div>
545
+ {run.summary ? (
546
+ <p className="mt-1 text-slate-600">{run.summary}</p>
547
+ ) : null}
548
+ {run.error ? (
549
+ <p className="mt-1 text-rose-600">{run.error}</p>
550
+ ) : null}
551
+ </div>
552
+ ))}
553
+ </section>
554
+ ) : null}
555
+ </div>
556
+ ) : null}
557
+
558
+ {tab === "timeline" ? (
559
+ <div className="space-y-0">
560
+ {(hermes?.events?.length || 0) === 0 ? (
561
+ <p className="text-sm text-slate-500">
562
+ Aucun événement Hermes pour l&apos;instant.
563
+ </p>
564
+ ) : (
565
+ <ol className="relative space-y-0 border-l border-slate-200 pl-4">
566
+ {[...(hermes?.events || [])]
567
+ .slice()
568
+ .reverse()
569
+ .map((ev, i) => (
570
+ <li key={ev.id ?? i} className="relative pb-4">
571
+ <span className="absolute -left-[21px] top-1 h-2.5 w-2.5 rounded-full border-2 border-white bg-sky-500 shadow" />
572
+ <p className="text-xs font-medium text-slate-800">
573
+ {eventLabel(ev.kind)}
574
+ </p>
575
+ <p className="text-[11px] text-slate-400">
576
+ {formatTs(ev.created_at)}
577
+ </p>
578
+ {ev.payload && Object.keys(ev.payload).length ? (
579
+ <pre className="mt-1 max-h-28 overflow-auto rounded-lg bg-slate-50 p-2 font-mono text-[10px] text-slate-600">
580
+ {JSON.stringify(ev.payload, null, 2)}
581
+ </pre>
582
+ ) : null}
583
+ </li>
584
+ ))}
585
+ </ol>
586
+ )}
587
+ </div>
588
+ ) : null}
589
+ </ScrollArea>
590
+
591
+ <div className="flex shrink-0 flex-wrap gap-2 border-t border-slate-100 bg-white px-4 py-3">
592
+ {task.executor_kind === "ai" ? (
593
+ <>
594
+ {!activeRun ? (
595
+ <Button size="sm" disabled={acting} onClick={() => void launchRun()}>
596
+ <Play className="mr-1.5 h-3.5 w-3.5" />
597
+ {runs.length ? "Relancer" : "Lancer"}
598
+ </Button>
599
+ ) : null}
600
+ {task.assignee ? (
601
+ <Button
602
+ size="sm"
603
+ variant="outline"
604
+ disabled={acting}
605
+ onClick={() => void viewAsAi()}
606
+ >
607
+ <Eye className="mr-1.5 h-3.5 w-3.5" />
608
+ Voir comme IA
609
+ </Button>
610
+ ) : null}
611
+ </>
612
+ ) : null}
613
+ {task.executor_kind === "hermes" &&
614
+ task.status !== "in_progress" &&
615
+ task.status !== "done" ? (
616
+ <Button
617
+ size="sm"
618
+ variant="outline"
619
+ disabled={acting}
620
+ onClick={() => void act("in_progress")}
621
+ >
622
+ <Play className="mr-1.5 h-3.5 w-3.5" />
623
+ Envoyer à Hermes
624
+ </Button>
625
+ ) : null}
626
+ {task.executor_kind === "human" && task.status === "backlog" ? (
627
+ <Button
628
+ size="sm"
629
+ variant="outline"
630
+ disabled={acting}
631
+ onClick={() => void act("in_progress")}
632
+ >
633
+ <Play className="mr-1.5 h-3.5 w-3.5" />
634
+ Commencer
635
+ </Button>
636
+ ) : null}
637
+ {task.status !== "done" ? (
638
+ <Button size="sm" disabled={acting} onClick={() => void act("done")}>
639
+ <CheckCircle2 className="mr-1.5 h-3.5 w-3.5" />
640
+ Marquer terminée
641
+ </Button>
642
+ ) : (
643
+ <Button
644
+ size="sm"
645
+ variant="outline"
646
+ disabled={acting}
647
+ onClick={() => void act("backlog")}
648
+ >
649
+ Rouvrir
650
+ </Button>
651
+ )}
652
+ {task.status !== "blocked" && task.status !== "done" ? (
653
+ <Button
654
+ size="sm"
655
+ variant="ghost"
656
+ disabled={acting}
657
+ onClick={() => void act("blocked")}
658
+ >
659
+ Bloquer
660
+ </Button>
661
+ ) : null}
662
+ </div>
663
+ </>
664
+ )}
665
+ </SheetContent>
666
+ </Sheet>
667
+ );
668
+ }