botschat 0.1.4 → 0.1.6
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.
- package/README.md +2 -2
- package/package.json +4 -1
- package/packages/api/package.json +2 -1
- package/packages/api/src/do/connection-do.ts +128 -33
- package/packages/api/src/index.ts +103 -6
- package/packages/api/src/routes/auth.ts +123 -29
- package/packages/api/src/routes/pairing.ts +14 -1
- package/packages/api/src/routes/setup.ts +70 -24
- package/packages/api/src/routes/upload.ts +12 -8
- package/packages/api/src/utils/auth.ts +212 -43
- package/packages/api/src/utils/id.ts +30 -14
- package/packages/api/src/utils/rate-limit.ts +73 -0
- package/packages/plugin/dist/src/channel.js +9 -3
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/package.json +2 -2
- package/packages/web/dist/assets/{index-DuGeoFJT.css → index-BST9bfvT.css} +1 -1
- package/packages/web/dist/assets/index-Da18EnTa.js +851 -0
- package/packages/web/dist/botschat-icon.svg +4 -0
- package/packages/web/dist/index.html +23 -3
- package/packages/web/dist/manifest.json +24 -0
- package/packages/web/dist/sw.js +40 -0
- package/packages/web/index.html +21 -1
- package/packages/web/src/App.tsx +241 -96
- package/packages/web/src/api.ts +63 -3
- package/packages/web/src/components/ChatWindow.tsx +11 -11
- package/packages/web/src/components/ConnectionSettings.tsx +475 -0
- package/packages/web/src/components/CronDetail.tsx +475 -235
- package/packages/web/src/components/CronSidebar.tsx +1 -1
- package/packages/web/src/components/DebugLogPanel.tsx +116 -3
- package/packages/web/src/components/IconRail.tsx +56 -16
- package/packages/web/src/components/JobList.tsx +2 -6
- package/packages/web/src/components/LoginPage.tsx +126 -103
- package/packages/web/src/components/MobileLayout.tsx +480 -0
- package/packages/web/src/components/OnboardingPage.tsx +7 -16
- package/packages/web/src/components/ResizeHandle.tsx +34 -0
- package/packages/web/src/components/Sidebar.tsx +1 -1
- package/packages/web/src/components/TaskBar.tsx +2 -2
- package/packages/web/src/components/ThreadPanel.tsx +2 -5
- package/packages/web/src/hooks/useIsMobile.ts +27 -0
- package/packages/web/src/index.css +59 -0
- package/packages/web/src/main.tsx +9 -0
- package/packages/web/src/store.ts +12 -5
- package/packages/web/src/ws.ts +2 -0
- package/scripts/dev.sh +13 -13
- package/wrangler.toml +3 -1
- package/packages/web/dist/assets/index-DyzTR_Y4.js +0 -847
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React, { useEffect, useCallback, useState, useRef } from "react";
|
|
2
2
|
import ReactMarkdown from "react-markdown";
|
|
3
3
|
import remarkGfm from "remark-gfm";
|
|
4
|
+
import { Group, Panel, useDefaultLayout } from "react-resizable-panels";
|
|
4
5
|
import { useAppState, useAppDispatch } from "../store";
|
|
5
6
|
import { jobsApi, tasksApi } from "../api";
|
|
6
7
|
import { ModelSelect } from "./ModelSelect";
|
|
7
8
|
import { ScheduleEditor, ScheduleDisplay } from "./ScheduleEditor";
|
|
9
|
+
import { ResizeHandle } from "./ResizeHandle";
|
|
10
|
+
import { useIsMobile } from "../hooks/useIsMobile";
|
|
8
11
|
import { dlog } from "../debug-log";
|
|
9
12
|
|
|
10
13
|
function relativeTime(ts: number): string {
|
|
@@ -49,6 +52,7 @@ function statusColors(status: string): { bg: string; fg: string } {
|
|
|
49
52
|
export function CronDetail() {
|
|
50
53
|
const state = useAppState();
|
|
51
54
|
const dispatch = useAppDispatch();
|
|
55
|
+
const isMobile = useIsMobile();
|
|
52
56
|
|
|
53
57
|
const task = state.cronTasks.find((t) => t.id === state.selectedCronTaskId);
|
|
54
58
|
|
|
@@ -302,7 +306,7 @@ export function CronDetail() {
|
|
|
302
306
|
|
|
303
307
|
if (!task) {
|
|
304
308
|
return (
|
|
305
|
-
<div className="flex-1 flex items-center justify-center" style={{ background: "var(--bg-surface)" }}>
|
|
309
|
+
<div className="flex-1 h-full flex items-center justify-center" style={{ background: "var(--bg-surface)" }}>
|
|
306
310
|
<div className="text-center">
|
|
307
311
|
<svg
|
|
308
312
|
className="w-16 h-16 mx-auto mb-4"
|
|
@@ -329,15 +333,15 @@ export function CronDetail() {
|
|
|
329
333
|
const channel = state.channels.find((c) => c.id === task.channelId);
|
|
330
334
|
|
|
331
335
|
return (
|
|
332
|
-
<div className="flex-1 flex flex-col min-w-0" style={{ background: "var(--bg-surface)" }}>
|
|
336
|
+
<div className="flex-1 h-full flex flex-col min-w-0" style={{ background: "var(--bg-surface)" }}>
|
|
333
337
|
{/* ---- Header ---- */}
|
|
334
338
|
<div
|
|
335
|
-
className="flex items-center justify-between px-5 py-3"
|
|
339
|
+
className="flex flex-wrap items-center justify-between gap-2 px-4 sm:px-5 py-2 sm:py-3 flex-shrink-0"
|
|
336
340
|
style={{ borderBottom: "1px solid var(--border)" }}
|
|
337
341
|
>
|
|
338
|
-
<div className="flex items-center gap-3 min-w-0">
|
|
342
|
+
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
|
339
343
|
<svg
|
|
340
|
-
className="w-5 h-5 flex-shrink-0"
|
|
344
|
+
className="w-5 h-5 flex-shrink-0 hidden sm:block"
|
|
341
345
|
fill="none"
|
|
342
346
|
viewBox="0 0 24 24"
|
|
343
347
|
stroke="currentColor"
|
|
@@ -348,18 +352,18 @@ export function CronDetail() {
|
|
|
348
352
|
</svg>
|
|
349
353
|
|
|
350
354
|
{editingField === "name" ? (
|
|
351
|
-
<div className="flex items-center gap-2">
|
|
355
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
352
356
|
<input
|
|
353
357
|
ref={editRef as React.RefObject<HTMLInputElement>}
|
|
354
358
|
value={editValue}
|
|
355
359
|
onChange={(e) => setEditValue(e.target.value)}
|
|
356
360
|
onKeyDown={handleKeyDown}
|
|
357
|
-
className="text-h2 font-bold px-2 py-0.5 rounded-sm focus:outline-none"
|
|
361
|
+
className="text-h2 font-bold px-2 py-0.5 rounded-sm focus:outline-none min-w-0 w-full"
|
|
358
362
|
style={{
|
|
359
363
|
background: "var(--bg-hover)",
|
|
360
364
|
color: "var(--text-primary)",
|
|
361
365
|
border: "1px solid var(--bg-active)",
|
|
362
|
-
|
|
366
|
+
maxWidth: 280,
|
|
363
367
|
}}
|
|
364
368
|
/>
|
|
365
369
|
<SaveCancelButtons saving={saving} onSave={saveEdit} onCancel={cancelEdit} />
|
|
@@ -385,12 +389,12 @@ export function CronDetail() {
|
|
|
385
389
|
)}
|
|
386
390
|
</div>
|
|
387
391
|
|
|
388
|
-
<div className="flex items-center gap-2 flex-shrink-0">
|
|
392
|
+
<div className="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
|
|
389
393
|
{/* Run Now button */}
|
|
390
394
|
<button
|
|
391
395
|
onClick={handleRunNow}
|
|
392
396
|
disabled={running}
|
|
393
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded-sm transition-colors disabled:opacity-50"
|
|
397
|
+
className="flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1 sm:py-1.5 text-caption rounded-sm transition-colors disabled:opacity-50"
|
|
394
398
|
style={{
|
|
395
399
|
background: "rgba(29,155,209,0.15)",
|
|
396
400
|
color: "var(--text-link)",
|
|
@@ -400,13 +404,13 @@ export function CronDetail() {
|
|
|
400
404
|
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
|
|
401
405
|
<path d="M8 5v14l11-7z" />
|
|
402
406
|
</svg>
|
|
403
|
-
{running ? "Running..." : "Run Now"}
|
|
407
|
+
<span className="hidden sm:inline">{running ? "Running..." : "Run Now"}</span>
|
|
404
408
|
</button>
|
|
405
409
|
|
|
406
410
|
{/* Delete button */}
|
|
407
411
|
<button
|
|
408
412
|
onClick={() => setShowDeleteConfirm(true)}
|
|
409
|
-
className="p-1.5 rounded-sm transition-colors hover:bg-[--bg-hover]"
|
|
413
|
+
className="p-1 sm:p-1.5 rounded-sm transition-colors hover:bg-[--bg-hover]"
|
|
410
414
|
style={{ color: "var(--text-muted)" }}
|
|
411
415
|
title="Delete task"
|
|
412
416
|
>
|
|
@@ -418,14 +422,14 @@ export function CronDetail() {
|
|
|
418
422
|
{/* Enable/Disable toggle */}
|
|
419
423
|
<button
|
|
420
424
|
onClick={handleToggleEnabled}
|
|
421
|
-
className="flex items-center gap-2 px-3 py-1.5 text-caption rounded-sm transition-colors"
|
|
425
|
+
className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1 sm:py-1.5 text-caption rounded-sm transition-colors"
|
|
422
426
|
style={{
|
|
423
427
|
background: task.enabled ? "rgba(43,172,118,0.15)" : "rgba(107,111,118,0.15)",
|
|
424
428
|
color: task.enabled ? "var(--accent-green)" : "var(--text-muted)",
|
|
425
429
|
}}
|
|
426
430
|
>
|
|
427
431
|
<div
|
|
428
|
-
className="w-7 h-4 rounded-full relative transition-colors"
|
|
432
|
+
className="w-7 h-4 rounded-full relative transition-colors flex-shrink-0"
|
|
429
433
|
style={{ background: task.enabled ? "var(--accent-green)" : "var(--text-muted)" }}
|
|
430
434
|
>
|
|
431
435
|
<div
|
|
@@ -433,7 +437,7 @@ export function CronDetail() {
|
|
|
433
437
|
style={{ left: task.enabled ? 14 : 2 }}
|
|
434
438
|
/>
|
|
435
439
|
</div>
|
|
436
|
-
{task.enabled ? "Enabled" : "Disabled"}
|
|
440
|
+
<span className="hidden sm:inline">{task.enabled ? "Enabled" : "Disabled"}</span>
|
|
437
441
|
</button>
|
|
438
442
|
</div>
|
|
439
443
|
</div>
|
|
@@ -441,15 +445,15 @@ export function CronDetail() {
|
|
|
441
445
|
{/* ---- Delete confirmation ---- */}
|
|
442
446
|
{showDeleteConfirm && (
|
|
443
447
|
<div
|
|
444
|
-
className="px-5 py-3 flex items-center justify-between"
|
|
448
|
+
className="px-4 sm:px-5 py-2 sm:py-3 flex flex-wrap items-center justify-between gap-2"
|
|
445
449
|
style={{ background: "rgba(224,30,90,0.08)", borderBottom: "1px solid var(--border)" }}
|
|
446
450
|
>
|
|
447
|
-
<div className="flex items-center gap-2">
|
|
448
|
-
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} style={{ color: "var(--accent-red)" }}>
|
|
451
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
452
|
+
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} style={{ color: "var(--accent-red)" }}>
|
|
449
453
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
|
450
454
|
</svg>
|
|
451
455
|
<span className="text-caption" style={{ color: "var(--accent-red)" }}>
|
|
452
|
-
Delete "{task.name}"?
|
|
456
|
+
Delete "{task.name}"?
|
|
453
457
|
</span>
|
|
454
458
|
</div>
|
|
455
459
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
@@ -472,248 +476,484 @@ export function CronDetail() {
|
|
|
472
476
|
</div>
|
|
473
477
|
)}
|
|
474
478
|
|
|
475
|
-
{/* ---- Info section
|
|
476
|
-
<
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
479
|
+
{/* ---- Info section + Content area: vertically resizable on desktop, stacked on mobile ---- */}
|
|
480
|
+
<CronInfoAndContent
|
|
481
|
+
task={task}
|
|
482
|
+
channel={channel}
|
|
483
|
+
state={state}
|
|
484
|
+
isMobile={isMobile}
|
|
485
|
+
infoExpanded={infoExpanded}
|
|
486
|
+
setInfoExpanded={setInfoExpanded}
|
|
487
|
+
editingField={editingField}
|
|
488
|
+
editValue={editValue}
|
|
489
|
+
setEditValue={setEditValue}
|
|
490
|
+
editRef={editRef}
|
|
491
|
+
handleKeyDown={handleKeyDown}
|
|
492
|
+
saving={saving}
|
|
493
|
+
saveEdit={saveEdit}
|
|
494
|
+
cancelEdit={cancelEdit}
|
|
495
|
+
startEdit={startEdit}
|
|
496
|
+
handleModelSelectChange={handleModelSelectChange}
|
|
497
|
+
handleSelectJob={handleSelectJob}
|
|
498
|
+
/>
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
493
502
|
|
|
494
|
-
|
|
495
|
-
<div className="px-5 pb-4 space-y-4">
|
|
496
|
-
{/* Row 1: Schedule (wider) + Model + Channel + Status */}
|
|
497
|
-
{editingField === "schedule" ? (
|
|
498
|
-
/* Schedule editing takes full width */
|
|
499
|
-
<div>
|
|
500
|
-
<InfoField label="Schedule">
|
|
501
|
-
<ScheduleEditor
|
|
502
|
-
value={editValue}
|
|
503
|
-
onChange={setEditValue}
|
|
504
|
-
onSave={saveEdit}
|
|
505
|
-
onCancel={cancelEdit}
|
|
506
|
-
saving={saving}
|
|
507
|
-
/>
|
|
508
|
-
</InfoField>
|
|
509
|
-
</div>
|
|
510
|
-
) : (
|
|
511
|
-
<div className="grid grid-cols-4 gap-4">
|
|
512
|
-
<InfoField label="Schedule">
|
|
513
|
-
<ScheduleDisplay
|
|
514
|
-
schedule={task.schedule}
|
|
515
|
-
onClick={() => startEdit("schedule")}
|
|
516
|
-
/>
|
|
517
|
-
</InfoField>
|
|
518
|
-
|
|
519
|
-
<InfoField label="Model">
|
|
520
|
-
<ModelSelect
|
|
521
|
-
value={task.model ?? ""}
|
|
522
|
-
onChange={handleModelSelectChange}
|
|
523
|
-
models={state.models}
|
|
524
|
-
placeholder="Default"
|
|
525
|
-
/>
|
|
526
|
-
</InfoField>
|
|
503
|
+
// ---- Info section + Content area (vertical resizable split) ----
|
|
527
504
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
505
|
+
function CronInfoAndContent({
|
|
506
|
+
task,
|
|
507
|
+
channel,
|
|
508
|
+
state,
|
|
509
|
+
isMobile,
|
|
510
|
+
infoExpanded,
|
|
511
|
+
setInfoExpanded,
|
|
512
|
+
editingField,
|
|
513
|
+
editValue,
|
|
514
|
+
setEditValue,
|
|
515
|
+
editRef,
|
|
516
|
+
handleKeyDown,
|
|
517
|
+
saving,
|
|
518
|
+
saveEdit,
|
|
519
|
+
cancelEdit,
|
|
520
|
+
startEdit,
|
|
521
|
+
handleModelSelectChange,
|
|
522
|
+
handleSelectJob,
|
|
523
|
+
}: {
|
|
524
|
+
task: any;
|
|
525
|
+
channel: any;
|
|
526
|
+
state: any;
|
|
527
|
+
isMobile: boolean;
|
|
528
|
+
infoExpanded: boolean;
|
|
529
|
+
setInfoExpanded: (v: boolean) => void;
|
|
530
|
+
editingField: "name" | "schedule" | "instructions" | null;
|
|
531
|
+
editValue: string;
|
|
532
|
+
setEditValue: (v: string) => void;
|
|
533
|
+
editRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>;
|
|
534
|
+
handleKeyDown: (e: React.KeyboardEvent) => void;
|
|
535
|
+
saving: boolean;
|
|
536
|
+
saveEdit: () => void;
|
|
537
|
+
cancelEdit: () => void;
|
|
538
|
+
startEdit: (field: "name" | "schedule" | "instructions") => void;
|
|
539
|
+
handleModelSelectChange: (modelId: string) => Promise<void>;
|
|
540
|
+
handleSelectJob: (jobId: string) => void;
|
|
541
|
+
}) {
|
|
542
|
+
const cronDetailVertLayout = useDefaultLayout({ id: "botschat-cron-detail-v" });
|
|
547
543
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
{task.openclawCronJobId ?? "N/A"}
|
|
553
|
-
</span>
|
|
554
|
-
</InfoField>
|
|
544
|
+
// Mobile drag-to-resize state
|
|
545
|
+
const [mobileInfoPct, setMobileInfoPct] = useState(40);
|
|
546
|
+
const mobileContainerRef = useRef<HTMLDivElement>(null);
|
|
547
|
+
const dragRef = useRef({ startY: 0, startPct: 40 });
|
|
555
548
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
549
|
+
const onDragHandleTouchStart = useCallback((e: React.TouchEvent) => {
|
|
550
|
+
e.preventDefault();
|
|
551
|
+
dragRef.current.startY = e.touches[0].clientY;
|
|
552
|
+
dragRef.current.startPct = mobileInfoPct;
|
|
553
|
+
const onMove = (ev: TouchEvent) => {
|
|
554
|
+
ev.preventDefault();
|
|
555
|
+
if (!mobileContainerRef.current) return;
|
|
556
|
+
const h = mobileContainerRef.current.getBoundingClientRect().height;
|
|
557
|
+
const delta = ((ev.touches[0].clientY - dragRef.current.startY) / h) * 100;
|
|
558
|
+
setMobileInfoPct(Math.max(10, Math.min(70, dragRef.current.startPct + delta)));
|
|
559
|
+
};
|
|
560
|
+
const onEnd = () => {
|
|
561
|
+
document.removeEventListener("touchmove", onMove);
|
|
562
|
+
document.removeEventListener("touchend", onEnd);
|
|
563
|
+
};
|
|
564
|
+
document.addEventListener("touchmove", onMove, { passive: false });
|
|
565
|
+
document.addEventListener("touchend", onEnd);
|
|
566
|
+
}, [mobileInfoPct]);
|
|
561
567
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
568
|
+
const onDragHandleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
569
|
+
e.preventDefault();
|
|
570
|
+
dragRef.current.startY = e.clientY;
|
|
571
|
+
dragRef.current.startPct = mobileInfoPct;
|
|
572
|
+
const onMove = (ev: MouseEvent) => {
|
|
573
|
+
if (!mobileContainerRef.current) return;
|
|
574
|
+
const h = mobileContainerRef.current.getBoundingClientRect().height;
|
|
575
|
+
const delta = ((ev.clientY - dragRef.current.startY) / h) * 100;
|
|
576
|
+
setMobileInfoPct(Math.max(10, Math.min(70, dragRef.current.startPct + delta)));
|
|
577
|
+
};
|
|
578
|
+
const onEnd = () => {
|
|
579
|
+
document.removeEventListener("mousemove", onMove);
|
|
580
|
+
document.removeEventListener("mouseup", onEnd);
|
|
581
|
+
};
|
|
582
|
+
document.addEventListener("mousemove", onMove);
|
|
583
|
+
document.addEventListener("mouseup", onEnd);
|
|
584
|
+
}, [mobileInfoPct]);
|
|
585
|
+
|
|
586
|
+
// Shared info section content
|
|
587
|
+
const infoSection = (
|
|
588
|
+
<div className="h-full flex flex-col">
|
|
589
|
+
<button
|
|
590
|
+
className="w-full flex items-center gap-2 px-4 sm:px-5 py-2 text-tiny uppercase tracking-wider hover:bg-[--bg-hover] transition-colors flex-shrink-0"
|
|
591
|
+
style={{ color: "var(--text-muted)" }}
|
|
592
|
+
onClick={() => setInfoExpanded(!infoExpanded)}
|
|
593
|
+
>
|
|
594
|
+
<svg
|
|
595
|
+
className={`w-3 h-3 transition-transform ${infoExpanded ? "rotate-0" : "-rotate-90"}`}
|
|
596
|
+
fill="none"
|
|
597
|
+
viewBox="0 0 24 24"
|
|
598
|
+
stroke="currentColor"
|
|
599
|
+
strokeWidth={2}
|
|
600
|
+
>
|
|
601
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
602
|
+
</svg>
|
|
603
|
+
Task Details
|
|
604
|
+
</button>
|
|
605
|
+
|
|
606
|
+
{infoExpanded && (
|
|
607
|
+
<div className="flex-1 overflow-y-auto px-4 sm:px-5 pb-4 space-y-4">
|
|
608
|
+
{/* Row 1: Schedule (wider) + Model + Channel + Status */}
|
|
609
|
+
{editingField === "schedule" ? (
|
|
610
|
+
<div>
|
|
611
|
+
<InfoField label="Schedule">
|
|
612
|
+
<ScheduleEditor
|
|
613
|
+
value={editValue}
|
|
614
|
+
onChange={setEditValue}
|
|
615
|
+
onSave={saveEdit}
|
|
616
|
+
onCancel={cancelEdit}
|
|
617
|
+
saving={saving}
|
|
618
|
+
/>
|
|
566
619
|
</InfoField>
|
|
567
620
|
</div>
|
|
621
|
+
) : (
|
|
622
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
|
|
623
|
+
<InfoField label="Schedule">
|
|
624
|
+
<ScheduleDisplay
|
|
625
|
+
schedule={task.schedule}
|
|
626
|
+
onClick={() => startEdit("schedule")}
|
|
627
|
+
/>
|
|
628
|
+
</InfoField>
|
|
568
629
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
630
|
+
<InfoField label="Model">
|
|
631
|
+
<ModelSelect
|
|
632
|
+
value={task.model ?? ""}
|
|
633
|
+
onChange={handleModelSelectChange}
|
|
634
|
+
models={state.models}
|
|
635
|
+
placeholder="Default"
|
|
636
|
+
/>
|
|
637
|
+
</InfoField>
|
|
638
|
+
|
|
639
|
+
<InfoField label="Channel">
|
|
640
|
+
<span className="text-body" style={{ color: "var(--text-primary)" }}>
|
|
641
|
+
{channel?.name ?? "Default"}
|
|
574
642
|
</span>
|
|
575
|
-
|
|
576
|
-
<button
|
|
577
|
-
onClick={() => startEdit("instructions")}
|
|
578
|
-
className="text-tiny px-2 py-0.5 rounded-sm transition-colors hover:bg-[--bg-hover]"
|
|
579
|
-
style={{ color: "var(--text-link)" }}
|
|
580
|
-
>
|
|
581
|
-
Edit
|
|
582
|
-
</button>
|
|
583
|
-
)}
|
|
584
|
-
</div>
|
|
643
|
+
</InfoField>
|
|
585
644
|
|
|
586
|
-
|
|
587
|
-
<div>
|
|
588
|
-
<
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
onChange={(e) => setEditValue(e.target.value)}
|
|
592
|
-
onKeyDown={(e) => {
|
|
593
|
-
if (e.key === "Escape") cancelEdit();
|
|
594
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
595
|
-
e.preventDefault();
|
|
596
|
-
saveEdit();
|
|
597
|
-
}
|
|
598
|
-
}}
|
|
599
|
-
placeholder="Enter the prompt or instructions for this cron task..."
|
|
600
|
-
rows={6}
|
|
601
|
-
className="w-full text-caption p-3 rounded-md resize-y focus:outline-none"
|
|
602
|
-
style={{
|
|
603
|
-
background: "var(--bg-hover)",
|
|
604
|
-
color: "var(--text-primary)",
|
|
605
|
-
border: "1px solid var(--bg-active)",
|
|
606
|
-
minHeight: 80,
|
|
607
|
-
maxHeight: 300,
|
|
608
|
-
}}
|
|
645
|
+
<InfoField label="Status">
|
|
646
|
+
<div className="flex items-center gap-2">
|
|
647
|
+
<div
|
|
648
|
+
className="w-2 h-2 rounded-full"
|
|
649
|
+
style={{ background: task.enabled ? "var(--accent-green)" : "var(--accent-yellow)" }}
|
|
609
650
|
/>
|
|
610
|
-
<
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
</span>
|
|
614
|
-
<SaveCancelButtons saving={saving} onSave={saveEdit} onCancel={cancelEdit} />
|
|
615
|
-
</div>
|
|
651
|
+
<span className="text-body" style={{ color: "var(--text-primary)" }}>
|
|
652
|
+
{task.enabled ? "Active" : "Paused"}
|
|
653
|
+
</span>
|
|
616
654
|
</div>
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
655
|
+
</InfoField>
|
|
656
|
+
</div>
|
|
657
|
+
)}
|
|
658
|
+
|
|
659
|
+
{/* Row 2: Cron Job ID + Created + Updated */}
|
|
660
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
|
661
|
+
<InfoField label="Cron Job ID">
|
|
662
|
+
<span className="text-caption font-mono" style={{ color: "var(--text-secondary)" }}>
|
|
663
|
+
{task.openclawCronJobId ?? "N/A"}
|
|
664
|
+
</span>
|
|
665
|
+
</InfoField>
|
|
666
|
+
|
|
667
|
+
<InfoField label="Created">
|
|
668
|
+
<span className="text-caption" style={{ color: "var(--text-secondary)" }}>
|
|
669
|
+
{task.createdAt ? formatTimestamp(task.createdAt) : "N/A"}
|
|
670
|
+
</span>
|
|
671
|
+
</InfoField>
|
|
672
|
+
|
|
673
|
+
<InfoField label="Updated">
|
|
674
|
+
<span className="text-caption" style={{ color: "var(--text-secondary)" }}>
|
|
675
|
+
{task.updatedAt ? formatTimestamp(task.updatedAt) : "N/A"}
|
|
676
|
+
</span>
|
|
677
|
+
</InfoField>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
{/* Row 3: Prompt / Instructions (full width) */}
|
|
681
|
+
<div>
|
|
682
|
+
<div className="flex items-center justify-between mb-1">
|
|
683
|
+
<span className="text-tiny uppercase tracking-wider" style={{ color: "var(--text-muted)" }}>
|
|
684
|
+
Prompt / Instructions
|
|
685
|
+
</span>
|
|
686
|
+
{editingField !== "instructions" && (
|
|
687
|
+
<button
|
|
626
688
|
onClick={() => startEdit("instructions")}
|
|
627
|
-
|
|
689
|
+
className="text-tiny px-2 py-0.5 rounded-sm transition-colors hover:bg-[--bg-hover]"
|
|
690
|
+
style={{ color: "var(--text-link)" }}
|
|
628
691
|
>
|
|
629
|
-
|
|
630
|
-
</
|
|
692
|
+
Edit
|
|
693
|
+
</button>
|
|
631
694
|
)}
|
|
632
695
|
</div>
|
|
696
|
+
|
|
697
|
+
{editingField === "instructions" ? (
|
|
698
|
+
<div>
|
|
699
|
+
<textarea
|
|
700
|
+
ref={editRef as React.RefObject<HTMLTextAreaElement>}
|
|
701
|
+
value={editValue}
|
|
702
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
703
|
+
onKeyDown={(e) => {
|
|
704
|
+
if (e.key === "Escape") cancelEdit();
|
|
705
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
706
|
+
e.preventDefault();
|
|
707
|
+
saveEdit();
|
|
708
|
+
}
|
|
709
|
+
}}
|
|
710
|
+
placeholder="Enter the prompt or instructions for this cron task..."
|
|
711
|
+
rows={6}
|
|
712
|
+
className="w-full text-caption p-3 rounded-md resize-y focus:outline-none"
|
|
713
|
+
style={{
|
|
714
|
+
background: "var(--bg-hover)",
|
|
715
|
+
color: "var(--text-primary)",
|
|
716
|
+
border: "1px solid var(--bg-active)",
|
|
717
|
+
minHeight: 80,
|
|
718
|
+
maxHeight: 300,
|
|
719
|
+
}}
|
|
720
|
+
/>
|
|
721
|
+
<div className="flex items-center justify-between mt-2">
|
|
722
|
+
<span className="text-tiny" style={{ color: "var(--text-muted)" }}>
|
|
723
|
+
Cmd/Ctrl+Enter to save, Esc to cancel
|
|
724
|
+
</span>
|
|
725
|
+
<SaveCancelButtons saving={saving} onSave={saveEdit} onCancel={cancelEdit} />
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
) : (
|
|
729
|
+
<div
|
|
730
|
+
className="text-caption p-3 rounded-md whitespace-pre-wrap cursor-pointer hover:border-[--text-muted] transition-colors"
|
|
731
|
+
style={{
|
|
732
|
+
background: "var(--bg-hover)",
|
|
733
|
+
color: task.instructions ? "var(--text-primary)" : "var(--text-muted)",
|
|
734
|
+
border: "1px solid transparent",
|
|
735
|
+
minHeight: 48,
|
|
736
|
+
}}
|
|
737
|
+
onClick={() => startEdit("instructions")}
|
|
738
|
+
title="Click to edit"
|
|
739
|
+
>
|
|
740
|
+
{task.instructions || "No instructions set. Click to add a prompt for this cron task."}
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
633
743
|
</div>
|
|
634
|
-
|
|
635
|
-
|
|
744
|
+
</div>
|
|
745
|
+
)}
|
|
746
|
+
</div>
|
|
747
|
+
);
|
|
636
748
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
749
|
+
// Mobile: vertical stack with draggable divider between info and content
|
|
750
|
+
if (isMobile) {
|
|
751
|
+
return (
|
|
752
|
+
<div ref={mobileContainerRef} className="flex-1 min-h-0 flex flex-col">
|
|
753
|
+
{/* Info section — draggable height when expanded, auto when collapsed */}
|
|
640
754
|
<div
|
|
641
|
-
className="overflow-y-auto
|
|
755
|
+
className="flex-shrink-0 overflow-y-auto"
|
|
642
756
|
style={{
|
|
643
|
-
|
|
644
|
-
|
|
757
|
+
...(infoExpanded ? { height: `${mobileInfoPct}%` } : {}),
|
|
758
|
+
borderBottom: "1px solid var(--border)",
|
|
645
759
|
}}
|
|
646
760
|
>
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
761
|
+
{infoSection}
|
|
762
|
+
</div>
|
|
763
|
+
{/* Drag handle — only shown when info section is expanded */}
|
|
764
|
+
{infoExpanded && (
|
|
765
|
+
<div
|
|
766
|
+
className="flex-shrink-0 flex items-center justify-center touch-none select-none"
|
|
767
|
+
style={{
|
|
768
|
+
height: 20,
|
|
769
|
+
cursor: "row-resize",
|
|
770
|
+
background: "var(--bg-surface)",
|
|
771
|
+
borderBottom: "1px solid var(--border)",
|
|
772
|
+
}}
|
|
773
|
+
onTouchStart={onDragHandleTouchStart}
|
|
774
|
+
onMouseDown={onDragHandleMouseDown}
|
|
775
|
+
>
|
|
776
|
+
<div
|
|
777
|
+
className="rounded-full"
|
|
778
|
+
style={{
|
|
779
|
+
width: 36,
|
|
780
|
+
height: 4,
|
|
781
|
+
background: "var(--text-muted)",
|
|
782
|
+
opacity: 0.4,
|
|
783
|
+
}}
|
|
784
|
+
/>
|
|
654
785
|
</div>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}`}
|
|
676
|
-
style={{
|
|
677
|
-
borderBottom: "1px solid var(--border)",
|
|
678
|
-
...(isSelected ? { borderLeft: "3px solid var(--bg-active)" } : {}),
|
|
679
|
-
}}
|
|
680
|
-
>
|
|
681
|
-
<div className="flex items-center justify-between">
|
|
682
|
-
<span className="text-tiny font-mono" style={{ color: "var(--text-muted)" }}>
|
|
683
|
-
#{displayNum}
|
|
684
|
-
</span>
|
|
685
|
-
<span
|
|
686
|
-
className="text-tiny px-1.5 py-0.5 rounded-sm font-bold flex items-center gap-1"
|
|
687
|
-
style={{ background: colors.bg, color: colors.fg }}
|
|
688
|
-
>
|
|
689
|
-
{job.status === "running" && (
|
|
690
|
-
<span className="inline-block w-1.5 h-1.5 rounded-full animate-pulse" style={{ background: "var(--text-link)" }} />
|
|
691
|
-
)}
|
|
692
|
-
{statusLabel(job.status)}
|
|
693
|
-
</span>
|
|
694
|
-
</div>
|
|
695
|
-
<div className="text-tiny mt-0.5" style={{ color: "var(--text-muted)" }}>
|
|
696
|
-
{job.time}
|
|
697
|
-
{job.durationMs != null && (
|
|
698
|
-
<span className="ml-1">({(job.durationMs / 1000).toFixed(1)}s)</span>
|
|
699
|
-
)}
|
|
700
|
-
</div>
|
|
701
|
-
{job.summary && (
|
|
702
|
-
<div className="text-caption mt-1 truncate" style={{ color: "var(--text-secondary)" }}>
|
|
703
|
-
{job.summary}
|
|
704
|
-
</div>
|
|
705
|
-
)}
|
|
706
|
-
</button>
|
|
707
|
-
);
|
|
708
|
-
})
|
|
709
|
-
)}
|
|
786
|
+
)}
|
|
787
|
+
{/* History + output — takes remaining space */}
|
|
788
|
+
<CronContentPanels cronJobs={state.cronJobs} selectedCronJobId={state.selectedCronJobId} handleSelectJob={handleSelectJob} mobile />
|
|
789
|
+
</div>
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Desktop: vertical resizable split between info and content
|
|
794
|
+
return (
|
|
795
|
+
<Group
|
|
796
|
+
orientation="vertical"
|
|
797
|
+
defaultLayout={cronDetailVertLayout.defaultLayout}
|
|
798
|
+
onLayoutChanged={cronDetailVertLayout.onLayoutChanged}
|
|
799
|
+
id="botschat-cron-detail-v"
|
|
800
|
+
className="flex-1 min-h-0"
|
|
801
|
+
>
|
|
802
|
+
{/* Info section panel */}
|
|
803
|
+
<Panel id="cron-info" defaultSize="40%" minSize="5%" maxSize="70%">
|
|
804
|
+
<div className="h-full" style={{ borderBottom: "1px solid var(--border)" }}>
|
|
805
|
+
{infoSection}
|
|
710
806
|
</div>
|
|
807
|
+
</Panel>
|
|
711
808
|
|
|
712
|
-
|
|
713
|
-
|
|
809
|
+
<ResizeHandle direction="vertical" />
|
|
810
|
+
|
|
811
|
+
{/* Content area panel (job history + output) */}
|
|
812
|
+
<Panel id="cron-content-area">
|
|
813
|
+
<CronContentPanels cronJobs={state.cronJobs} selectedCronJobId={state.selectedCronJobId} handleSelectJob={handleSelectJob} mobile={false} />
|
|
814
|
+
</Panel>
|
|
815
|
+
</Group>
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---- Cron content panels (split component for hook usage) ----
|
|
820
|
+
|
|
821
|
+
function CronContentPanels({
|
|
822
|
+
cronJobs,
|
|
823
|
+
selectedCronJobId,
|
|
824
|
+
handleSelectJob,
|
|
825
|
+
mobile,
|
|
826
|
+
}: {
|
|
827
|
+
cronJobs: Array<{ id: string; number: number; status: string; startedAt: number; finishedAt: number | null; durationMs: number | null; summary: string; time: string; sessionKey?: string }>;
|
|
828
|
+
selectedCronJobId: string | null;
|
|
829
|
+
handleSelectJob: (jobId: string) => void;
|
|
830
|
+
mobile: boolean;
|
|
831
|
+
}) {
|
|
832
|
+
const cronLayout = useDefaultLayout({ id: "botschat-cron-content" });
|
|
833
|
+
const [mobileShowOutput, setMobileShowOutput] = useState(false);
|
|
834
|
+
|
|
835
|
+
// On mobile, tapping a job → show output; navigate back to list via header
|
|
836
|
+
const handleMobileSelectJob = (jobId: string) => {
|
|
837
|
+
handleSelectJob(jobId);
|
|
838
|
+
setMobileShowOutput(true);
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// --- Shared job list content ---
|
|
842
|
+
const jobListContent = (
|
|
843
|
+
<div className="overflow-y-auto h-full">
|
|
844
|
+
<div className="px-3 py-2" style={{ borderBottom: "1px solid var(--border)" }}>
|
|
845
|
+
<span className="text-tiny uppercase tracking-wider font-bold" style={{ color: "var(--text-muted)" }}>
|
|
846
|
+
Execution History
|
|
847
|
+
</span>
|
|
848
|
+
<span className="text-tiny ml-1" style={{ color: "var(--text-muted)" }}>
|
|
849
|
+
({cronJobs.length})
|
|
850
|
+
</span>
|
|
714
851
|
</div>
|
|
852
|
+
{cronJobs.length === 0 ? (
|
|
853
|
+
<div className="px-4 py-6 text-center">
|
|
854
|
+
<p className="text-tiny" style={{ color: "var(--text-muted)" }}>
|
|
855
|
+
No runs yet.
|
|
856
|
+
</p>
|
|
857
|
+
<p className="text-tiny mt-1" style={{ color: "var(--text-muted)" }}>
|
|
858
|
+
Waiting for schedule...
|
|
859
|
+
</p>
|
|
860
|
+
</div>
|
|
861
|
+
) : (
|
|
862
|
+
cronJobs.map((job, idx) => {
|
|
863
|
+
const colors = statusColors(job.status);
|
|
864
|
+
const displayNum = job.number || cronJobs.length - idx;
|
|
865
|
+
const isSelected = selectedCronJobId === job.id;
|
|
866
|
+
return (
|
|
867
|
+
<button
|
|
868
|
+
key={job.id}
|
|
869
|
+
onClick={() => mobile ? handleMobileSelectJob(job.id) : handleSelectJob(job.id)}
|
|
870
|
+
className={`w-full text-left px-3 py-2 hover:bg-[--bg-hover] transition-colors ${
|
|
871
|
+
isSelected ? "bg-[--bg-hover]" : ""
|
|
872
|
+
}`}
|
|
873
|
+
style={{
|
|
874
|
+
borderBottom: "1px solid var(--border)",
|
|
875
|
+
...(isSelected ? { borderLeft: "3px solid var(--bg-active)" } : {}),
|
|
876
|
+
}}
|
|
877
|
+
>
|
|
878
|
+
<div className="flex items-center justify-between">
|
|
879
|
+
<span className="text-tiny font-mono" style={{ color: "var(--text-muted)" }}>
|
|
880
|
+
#{displayNum}
|
|
881
|
+
</span>
|
|
882
|
+
<span
|
|
883
|
+
className="text-tiny px-1.5 py-0.5 rounded-sm font-bold flex items-center gap-1"
|
|
884
|
+
style={{ background: colors.bg, color: colors.fg }}
|
|
885
|
+
>
|
|
886
|
+
{job.status === "running" && (
|
|
887
|
+
<span className="inline-block w-1.5 h-1.5 rounded-full animate-pulse" style={{ background: "var(--text-link)" }} />
|
|
888
|
+
)}
|
|
889
|
+
{statusLabel(job.status)}
|
|
890
|
+
</span>
|
|
891
|
+
</div>
|
|
892
|
+
<div className="text-tiny mt-0.5" style={{ color: "var(--text-muted)" }}>
|
|
893
|
+
{job.time}
|
|
894
|
+
{job.durationMs != null && (
|
|
895
|
+
<span className="ml-1">({(job.durationMs / 1000).toFixed(1)}s)</span>
|
|
896
|
+
)}
|
|
897
|
+
</div>
|
|
898
|
+
{job.summary && (
|
|
899
|
+
<div className="text-caption mt-1 truncate" style={{ color: "var(--text-secondary)" }}>
|
|
900
|
+
{job.summary}
|
|
901
|
+
</div>
|
|
902
|
+
)}
|
|
903
|
+
</button>
|
|
904
|
+
);
|
|
905
|
+
})
|
|
906
|
+
)}
|
|
715
907
|
</div>
|
|
716
908
|
);
|
|
909
|
+
|
|
910
|
+
// --- Mobile: stack-based navigation (list ↔ output) ---
|
|
911
|
+
if (mobile) {
|
|
912
|
+
if (mobileShowOutput && selectedCronJobId) {
|
|
913
|
+
return (
|
|
914
|
+
<div className="flex-1 flex flex-col min-h-0">
|
|
915
|
+
{/* Back to list */}
|
|
916
|
+
<button
|
|
917
|
+
onClick={() => setMobileShowOutput(false)}
|
|
918
|
+
className="flex items-center gap-1 px-4 py-2 text-caption flex-shrink-0"
|
|
919
|
+
style={{ borderBottom: "1px solid var(--border)", color: "var(--text-link)" }}
|
|
920
|
+
>
|
|
921
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
922
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
|
923
|
+
</svg>
|
|
924
|
+
Back to History
|
|
925
|
+
</button>
|
|
926
|
+
<div className="flex-1 min-h-0">
|
|
927
|
+
<JobOutputPanel jobs={cronJobs} selectedJobId={selectedCronJobId} />
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return <div className="flex-1 min-h-0">{jobListContent}</div>;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// --- Desktop: resizable horizontal panels ---
|
|
936
|
+
return (
|
|
937
|
+
<Group
|
|
938
|
+
orientation="horizontal"
|
|
939
|
+
defaultLayout={cronLayout.defaultLayout}
|
|
940
|
+
onLayoutChanged={cronLayout.onLayoutChanged}
|
|
941
|
+
id="botschat-cron-content"
|
|
942
|
+
className="flex-1 min-h-0"
|
|
943
|
+
>
|
|
944
|
+
{/* Job list panel */}
|
|
945
|
+
<Panel id="cron-jobs" defaultSize="20%" minSize="8%" maxSize="40%">
|
|
946
|
+
{jobListContent}
|
|
947
|
+
</Panel>
|
|
948
|
+
|
|
949
|
+
<ResizeHandle />
|
|
950
|
+
|
|
951
|
+
{/* Job output detail */}
|
|
952
|
+
<Panel id="cron-output">
|
|
953
|
+
<JobOutputPanel jobs={cronJobs} selectedJobId={selectedCronJobId} />
|
|
954
|
+
</Panel>
|
|
955
|
+
</Group>
|
|
956
|
+
);
|
|
717
957
|
}
|
|
718
958
|
|
|
719
959
|
// ---- Job output panel ----
|
|
@@ -771,7 +1011,7 @@ function JobOutputPanel({
|
|
|
771
1011
|
|
|
772
1012
|
if (!job) {
|
|
773
1013
|
return (
|
|
774
|
-
<div className="flex-1 flex items-center justify-center" style={{ background: "var(--bg-surface)" }}>
|
|
1014
|
+
<div className="flex-1 h-full flex items-center justify-center" style={{ background: "var(--bg-surface)" }}>
|
|
775
1015
|
<p className="text-caption" style={{ color: "var(--text-muted)" }}>
|
|
776
1016
|
{jobs.length > 0 ? "Select a run to view output" : "No execution history yet"}
|
|
777
1017
|
</p>
|
|
@@ -784,7 +1024,7 @@ function JobOutputPanel({
|
|
|
784
1024
|
const blocks = job.summary ? parseMessageBlocks(job.summary) : [];
|
|
785
1025
|
|
|
786
1026
|
return (
|
|
787
|
-
<div className="flex-1 flex flex-col min-w-0" style={{ background: "var(--bg-surface)" }}>
|
|
1027
|
+
<div className="flex-1 h-full flex flex-col min-w-0" style={{ background: "var(--bg-surface)" }}>
|
|
788
1028
|
{/* Job header bar */}
|
|
789
1029
|
<div
|
|
790
1030
|
className="flex items-center gap-3 px-5 py-2.5 flex-shrink-0"
|