@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,559 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { Bot, Loader2, Maximize2, MonitorPlay, Play, RotateCcw, Square } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import { Button } from "@creezio/shell-ui/ui/kit";
7
+ import { cn } from "@creezio/shell-ui";
8
+
9
+ type LogRow = {
10
+ id: string;
11
+ seq: number;
12
+ level: string;
13
+ event_type: string;
14
+ message: string;
15
+ created_at: string;
16
+ payload_json?: string | null;
17
+ };
18
+
19
+ type RunRow = {
20
+ id: string;
21
+ status: string;
22
+ step_count: number;
23
+ last_error?: string | null;
24
+ hitl_prompt?: string | null;
25
+ };
26
+
27
+ type Props = {
28
+ /** userId IA à observer, ou runId direct */
29
+ userId?: string;
30
+ runId?: string;
31
+ className?: string;
32
+ compact?: boolean;
33
+ title?: string;
34
+ };
35
+
36
+ function parseSseChunk(
37
+ buffer: string,
38
+ ): { events: { event: string; data: string }[]; rest: string } {
39
+ const events: { event: string; data: string }[] = [];
40
+ let rest = buffer;
41
+ let idx: number;
42
+ while ((idx = rest.indexOf("\n\n")) !== -1) {
43
+ const raw = rest.slice(0, idx);
44
+ rest = rest.slice(idx + 2);
45
+ let event = "message";
46
+ const dataLines: string[] = [];
47
+ for (const line of raw.split("\n")) {
48
+ if (line.startsWith("event:")) event = line.slice(6).trim();
49
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
50
+ }
51
+ if (dataLines.length) {
52
+ events.push({ event, data: dataLines.join("\n") });
53
+ }
54
+ }
55
+ return { events, rest };
56
+ }
57
+
58
+ /**
59
+ * Vue live (lecture seule) : frames JPEG du screencast de l'espace IA,
60
+ * reçues en SSE (/api/v1/tasks/screencast/:aiUserId/stream). Le flux desktop
61
+ * démarre au 1er spectateur et s'arrête au dernier — monter/démonter ce
62
+ * composant suffit.
63
+ */
64
+ function AiLiveView({ userId }: { userId: string }) {
65
+ const [src, setSrc] = useState<string | null>(null);
66
+ const [status, setStatus] = useState<string>("Connexion au flux…");
67
+ const [frames, setFrames] = useState(0);
68
+ const containerRef = useRef<HTMLDivElement>(null);
69
+
70
+ useEffect(() => {
71
+ setSrc(null);
72
+ setFrames(0);
73
+ setStatus("Connexion au flux…");
74
+ if (typeof window === "undefined" || !window.EventSource) {
75
+ setStatus("SSE non supporté par ce navigateur");
76
+ return;
77
+ }
78
+ const es = new EventSource(
79
+ `/api/v1/tasks/screencast/${encodeURIComponent(userId)}/stream`,
80
+ );
81
+ const onFrame = (ev: MessageEvent) => {
82
+ try {
83
+ const data = JSON.parse(String(ev.data)) as { data?: string };
84
+ if (data.data) {
85
+ setSrc(`data:image/jpeg;base64,${data.data}`);
86
+ setFrames((n) => n + 1);
87
+ setStatus("");
88
+ }
89
+ } catch {
90
+ /* ignore */
91
+ }
92
+ };
93
+ const onStatus = (ev: MessageEvent) => {
94
+ try {
95
+ const data = JSON.parse(String(ev.data)) as {
96
+ ok?: boolean;
97
+ error?: string;
98
+ };
99
+ if (data.ok === false) {
100
+ setStatus(data.error || "Screencast indisponible");
101
+ } else {
102
+ setStatus((prev) => (prev === "Connexion au flux…" ? "En attente d’image…" : prev));
103
+ }
104
+ } catch {
105
+ /* ignore */
106
+ }
107
+ };
108
+ es.addEventListener("frame", onFrame);
109
+ es.addEventListener("status", onStatus);
110
+ es.onerror = () => {
111
+ setStatus("Flux interrompu — reconnexion…");
112
+ };
113
+ return () => {
114
+ es.close();
115
+ };
116
+ }, [userId]);
117
+
118
+ function goFullscreen() {
119
+ const el = containerRef.current;
120
+ if (el && el.requestFullscreen) void el.requestFullscreen();
121
+ }
122
+
123
+ return (
124
+ <div className="flex flex-col">
125
+ <div
126
+ ref={containerRef}
127
+ className="relative flex min-h-48 items-center justify-center bg-slate-950"
128
+ data-creezio-tasks-ai-live-frames={frames}
129
+ >
130
+ {src ? (
131
+ // eslint-disable-next-line @next/next/no-img-element
132
+ <img
133
+ src={src}
134
+ alt={`Vue live de l’espace IA ${userId}`}
135
+ className="max-h-[min(70vh,32rem)] w-full object-contain"
136
+ />
137
+ ) : (
138
+ <p className="px-4 py-10 text-center text-xs text-slate-400">
139
+ {status || "En attente d’image…"}
140
+ </p>
141
+ )}
142
+ {src ? (
143
+ <Button
144
+ type="button"
145
+ size="sm"
146
+ variant="secondary"
147
+ className="absolute right-2 top-2 h-7 px-2 text-[11px] opacity-80"
148
+ onClick={goFullscreen}
149
+ >
150
+ <Maximize2 className="mr-1 h-3 w-3" />
151
+ Plein écran
152
+ </Button>
153
+ ) : null}
154
+ </div>
155
+ <p className="border-t border-slate-100 px-3 py-1.5 text-[11px] text-slate-500">
156
+ Lecture seule — l’IA garde la main. {status && src ? status : ""}
157
+ </p>
158
+ </div>
159
+ );
160
+ }
161
+
162
+ export function AiActivityPanel({
163
+ userId,
164
+ runId,
165
+ className,
166
+ compact,
167
+ title,
168
+ }: Props) {
169
+ const [view, setView] = useState<"logs" | "live">("logs");
170
+ const [logs, setLogs] = useState<LogRow[]>([]);
171
+ const [run, setRun] = useState<RunRow | null>(null);
172
+ const [loading, setLoading] = useState(true);
173
+ const [transport, setTransport] = useState<"sse" | "poll">("sse");
174
+ const [busy, setBusy] = useState(false);
175
+ const afterRef = useRef(0);
176
+ const scroller = useRef<HTMLDivElement>(null);
177
+ const prevStatus = useRef<string | null>(null);
178
+
179
+ const mergeLogs = useCallback((incoming: LogRow[]) => {
180
+ if (!incoming.length) return;
181
+ setLogs((prev) => {
182
+ const seen = new Set(prev.map((l) => l.id));
183
+ const merged = [...prev];
184
+ for (const l of incoming) {
185
+ if (!seen.has(l.id)) merged.push(l);
186
+ }
187
+ return merged.slice(-300);
188
+ });
189
+ afterRef.current = Math.max(
190
+ afterRef.current,
191
+ ...incoming.map((l) => l.seq),
192
+ );
193
+ }, []);
194
+
195
+ const applyRun = useCallback((next: RunRow | null | undefined) => {
196
+ if (!next) return;
197
+ setRun(next);
198
+ const prev = prevStatus.current;
199
+ if (prev && prev !== next.status) {
200
+ if (next.status === "succeeded") {
201
+ toast.success("Run IA terminé");
202
+ } else if (next.status === "failed") {
203
+ toast.error(next.last_error || "Run IA échoué");
204
+ } else if (next.status === "cancelled") {
205
+ toast.message("Run IA annulé");
206
+ }
207
+ }
208
+ if (next.hitl_prompt && prev !== "hitl" && next.status === "running") {
209
+ toast.message("Validation humaine requise", {
210
+ description: next.hitl_prompt,
211
+ });
212
+ }
213
+ prevStatus.current = next.hitl_prompt ? "hitl" : next.status;
214
+ }, []);
215
+
216
+ const poll = useCallback(async () => {
217
+ try {
218
+ const url = runId
219
+ ? `/api/v1/tasks/runs/${runId}/logs?after=${afterRef.current}`
220
+ : userId
221
+ ? `/api/v1/tasks/activity/${userId}?after=${afterRef.current}`
222
+ : null;
223
+ if (!url) return;
224
+ const res = await fetch(url);
225
+ if (!res.ok) return;
226
+ const data = (await res.json()) as {
227
+ run?: RunRow | null;
228
+ logs?: LogRow[];
229
+ };
230
+ applyRun(data.run);
231
+ mergeLogs(data.logs || []);
232
+ } finally {
233
+ setLoading(false);
234
+ }
235
+ }, [runId, userId, applyRun, mergeLogs]);
236
+
237
+ useEffect(() => {
238
+ afterRef.current = 0;
239
+ setLogs([]);
240
+ setRun(null);
241
+ setLoading(true);
242
+ prevStatus.current = null;
243
+
244
+ const streamUrl = runId
245
+ ? `/api/v1/tasks/runs/${runId}/stream?after=0`
246
+ : userId
247
+ ? `/api/v1/tasks/activity/${userId}/stream`
248
+ : null;
249
+
250
+ if (!streamUrl || typeof window === "undefined" || !window.EventSource) {
251
+ setTransport("poll");
252
+ void poll();
253
+ const id = window.setInterval(() => void poll(), 1000);
254
+ return () => window.clearInterval(id);
255
+ }
256
+
257
+ let closed = false;
258
+ let pollFallback: number | null = null;
259
+ const es = new EventSource(streamUrl);
260
+
261
+ const onReady = () => {
262
+ setTransport("sse");
263
+ setLoading(false);
264
+ };
265
+ const onSnapshot = (ev: MessageEvent) => {
266
+ try {
267
+ const data = JSON.parse(String(ev.data)) as {
268
+ run?: RunRow | null;
269
+ logs?: LogRow[];
270
+ };
271
+ applyRun(data.run);
272
+ if (data.logs?.length) {
273
+ setLogs(data.logs.slice(-300));
274
+ afterRef.current = Math.max(
275
+ 0,
276
+ ...data.logs.map((l) => l.seq),
277
+ );
278
+ }
279
+ } catch {
280
+ /* ignore */
281
+ }
282
+ setLoading(false);
283
+ };
284
+ const onLog = (ev: MessageEvent) => {
285
+ try {
286
+ const data = JSON.parse(String(ev.data)) as {
287
+ log?: LogRow;
288
+ run?: RunRow;
289
+ };
290
+ if (data.run) applyRun(data.run);
291
+ if (data.log) mergeLogs([data.log]);
292
+ } catch {
293
+ /* ignore */
294
+ }
295
+ setLoading(false);
296
+ };
297
+ const onRun = (ev: MessageEvent) => {
298
+ try {
299
+ const data = JSON.parse(String(ev.data)) as { run?: RunRow };
300
+ applyRun(data.run);
301
+ } catch {
302
+ /* ignore */
303
+ }
304
+ };
305
+
306
+ es.addEventListener("ready", onReady);
307
+ es.addEventListener("snapshot", onSnapshot);
308
+ es.addEventListener("log", onLog);
309
+ es.addEventListener("run", onRun);
310
+ es.onerror = () => {
311
+ if (closed) return;
312
+ es.close();
313
+ setTransport("poll");
314
+ void poll();
315
+ if (!pollFallback) {
316
+ pollFallback = window.setInterval(() => void poll(), 1000);
317
+ }
318
+ };
319
+
320
+ // Bootstrap immédiat au cas où le snapshot SSE tarde
321
+ void poll();
322
+
323
+ return () => {
324
+ closed = true;
325
+ es.close();
326
+ if (pollFallback) window.clearInterval(pollFallback);
327
+ };
328
+ }, [runId, userId, poll, applyRun, mergeLogs]);
329
+
330
+ useEffect(() => {
331
+ const el = scroller.current;
332
+ if (!el) return;
333
+ el.scrollTop = el.scrollHeight;
334
+ }, [logs]);
335
+
336
+ async function resumeHitl() {
337
+ if (!run?.id) return;
338
+ setBusy(true);
339
+ try {
340
+ const res = await fetch(`/api/v1/tasks/runs/${run.id}/resume`, {
341
+ method: "POST",
342
+ headers: { "Content-Type": "application/json" },
343
+ body: JSON.stringify({ response: "ok" }),
344
+ });
345
+ const data = (await res.json()) as { error?: string; run?: RunRow };
346
+ if (!res.ok) {
347
+ toast.error(data.error || "Reprise impossible");
348
+ return;
349
+ }
350
+ applyRun(data.run);
351
+ toast.success("Run repris");
352
+ } finally {
353
+ setBusy(false);
354
+ }
355
+ }
356
+
357
+ async function retryRun() {
358
+ if (!run?.id) return;
359
+ setBusy(true);
360
+ try {
361
+ const res = await fetch(`/api/v1/tasks/runs/${run.id}/retry`, {
362
+ method: "POST",
363
+ });
364
+ const data = (await res.json()) as { error?: string; run?: RunRow };
365
+ if (!res.ok) {
366
+ toast.error(data.error || "Retry impossible");
367
+ return;
368
+ }
369
+ toast.success("Nouveau run en file");
370
+ applyRun(data.run);
371
+ afterRef.current = 0;
372
+ setLogs([]);
373
+ } finally {
374
+ setBusy(false);
375
+ }
376
+ }
377
+
378
+ async function cancelRun() {
379
+ if (!run?.id) return;
380
+ setBusy(true);
381
+ try {
382
+ const res = await fetch(`/api/v1/tasks/runs/${run.id}/cancel`, {
383
+ method: "POST",
384
+ });
385
+ const data = (await res.json()) as { error?: string; run?: RunRow };
386
+ if (!res.ok) {
387
+ toast.error(data.error || "Annulation impossible");
388
+ return;
389
+ }
390
+ applyRun(data.run);
391
+ } finally {
392
+ setBusy(false);
393
+ }
394
+ }
395
+
396
+ const hitl = Boolean(run?.hitl_prompt && run.status === "running");
397
+ const canRetry = run?.status === "failed" || run?.status === "cancelled";
398
+ const canCancel = run?.status === "queued" || run?.status === "running";
399
+
400
+ return (
401
+ <aside
402
+ className={cn(
403
+ "flex flex-col border border-slate-200 bg-white",
404
+ compact ? "rounded-md" : "rounded-lg",
405
+ className,
406
+ )}
407
+ data-creezio-tasks-ai-activity={run?.id || ""}
408
+ >
409
+ <header className="flex items-center gap-2 border-b border-slate-100 px-3 py-2">
410
+ <Bot className="h-4 w-4 text-slate-600" />
411
+ <div className="min-w-0 flex-1">
412
+ <p className="truncate text-sm font-medium text-slate-900">
413
+ {title || "Activité IA"}
414
+ </p>
415
+ <p className="truncate text-xs text-slate-500">
416
+ {run
417
+ ? `Run ${run.status}${hitl ? " · pause HITL" : ""} · ${run.step_count} étapes · ${transport}`
418
+ : loading
419
+ ? "Chargement…"
420
+ : "Aucun run"}
421
+ </p>
422
+ </div>
423
+ {loading ? (
424
+ <Loader2 className="h-3.5 w-3.5 animate-spin text-slate-400" />
425
+ ) : null}
426
+ {userId ? (
427
+ <div className="flex gap-0.5 rounded-md border border-slate-200 p-0.5">
428
+ <button
429
+ type="button"
430
+ className={cn(
431
+ "rounded px-1.5 py-0.5 text-[11px]",
432
+ view === "logs"
433
+ ? "bg-slate-900 text-white"
434
+ : "text-slate-600 hover:bg-slate-100",
435
+ )}
436
+ onClick={() => setView("logs")}
437
+ >
438
+ Activité
439
+ </button>
440
+ <button
441
+ type="button"
442
+ className={cn(
443
+ "flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px]",
444
+ view === "live"
445
+ ? "bg-slate-900 text-white"
446
+ : "text-slate-600 hover:bg-slate-100",
447
+ )}
448
+ onClick={() => setView("live")}
449
+ data-creezio-tasks-aid="ai-live-view-tab"
450
+ >
451
+ <MonitorPlay className="h-3 w-3" />
452
+ Vue live
453
+ </button>
454
+ </div>
455
+ ) : null}
456
+ </header>
457
+
458
+ {userId && view === "live" ? <AiLiveView userId={userId} /> : null}
459
+
460
+ {view === "live" ? null : hitl || canRetry || canCancel ? (
461
+ <div className="flex flex-wrap gap-1.5 border-b border-slate-100 px-3 py-2">
462
+ {hitl ? (
463
+ <Button
464
+ type="button"
465
+ size="sm"
466
+ className="h-7 px-2 text-[11px]"
467
+ disabled={busy}
468
+ onClick={() => void resumeHitl()}
469
+ >
470
+ <Play className="mr-1 h-3 w-3" />
471
+ Reprendre
472
+ </Button>
473
+ ) : null}
474
+ {canRetry ? (
475
+ <Button
476
+ type="button"
477
+ size="sm"
478
+ variant="outline"
479
+ className="h-7 px-2 text-[11px]"
480
+ disabled={busy}
481
+ onClick={() => void retryRun()}
482
+ >
483
+ <RotateCcw className="mr-1 h-3 w-3" />
484
+ Retry
485
+ </Button>
486
+ ) : null}
487
+ {canCancel ? (
488
+ <Button
489
+ type="button"
490
+ size="sm"
491
+ variant="ghost"
492
+ className="h-7 px-2 text-[11px] text-red-600"
493
+ disabled={busy}
494
+ onClick={() => void cancelRun()}
495
+ >
496
+ <Square className="mr-1 h-3 w-3" />
497
+ Annuler
498
+ </Button>
499
+ ) : null}
500
+ </div>
501
+ ) : null}
502
+
503
+ {view !== "live" && hitl && run?.hitl_prompt ? (
504
+ <p className="border-b border-amber-100 bg-amber-50 px-3 py-2 text-xs text-amber-900">
505
+ {run.hitl_prompt}
506
+ </p>
507
+ ) : null}
508
+
509
+ <div
510
+ ref={scroller}
511
+ className={cn(
512
+ "space-y-1.5 overflow-y-auto px-3 py-2 font-mono text-[11px] leading-relaxed text-slate-700",
513
+ compact ? "max-h-56" : "max-h-[min(70vh,28rem)]",
514
+ view === "live" && "hidden",
515
+ )}
516
+ >
517
+ {logs.length === 0 ? (
518
+ <p className="font-sans text-xs text-slate-500">
519
+ En attente d’actions (tool calls, navigations, décisions)…
520
+ </p>
521
+ ) : (
522
+ logs.map((l) => (
523
+ <div
524
+ key={l.id}
525
+ className="border-b border-slate-50 pb-1.5 last:border-0"
526
+ data-creezio-tasks-ai-log-seq={l.seq}
527
+ data-creezio-tasks-ai-log-type={l.event_type}
528
+ >
529
+ <span
530
+ className={cn(
531
+ "mr-1.5 uppercase",
532
+ l.level === "error" && "text-red-600",
533
+ l.level === "warn" && "text-amber-700",
534
+ l.level === "tool" && "text-sky-700",
535
+ l.level === "nav" && "text-emerald-700",
536
+ l.level === "decision" && "text-violet-700",
537
+ )}
538
+ >
539
+ [{l.level}]
540
+ </span>
541
+ <span className="text-slate-400">{l.event_type}</span>
542
+ <div className="whitespace-pre-wrap break-words text-slate-800">
543
+ {l.message}
544
+ </div>
545
+ </div>
546
+ ))
547
+ )}
548
+ {run?.last_error ? (
549
+ <p className="font-sans text-xs text-red-600">{run.last_error}</p>
550
+ ) : null}
551
+ </div>
552
+ </aside>
553
+ );
554
+ }
555
+
556
+ /** Utilitaire testable : parse SSE (export pour scripts éventuels). */
557
+ export function __parseSseChunkForTests(buffer: string) {
558
+ return parseSseChunk(buffer);
559
+ }
package/ui/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @creezio/tasks/ui — kanban / détail / activité IA (O9, gold TF).
3
+ */
4
+ export * from "./tasks-types";
5
+ export * from "./tasks-kanban-client";
6
+ export * from "./task-detail-sheet";
7
+ export * from "./ai-activity-panel";