651 lines
25 KiB
JavaScript
651 lines
25 KiB
JavaScript
// The three islands: ListsIsland, TasksIsland, DetailsIsland
|
||
const { useState, useEffect, useRef, useMemo } = React;
|
||
|
||
// ---------- Helpers ----------
|
||
const fmtDate = (iso) => {
|
||
if (!iso) return null;
|
||
const d = new Date(iso);
|
||
const now = new Date();
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||
const target = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||
const diff = Math.round((target - today) / 86400000);
|
||
if (diff === 0) return 'Today';
|
||
if (diff === 1) return 'Tomorrow';
|
||
if (diff === -1) return 'Yesterday';
|
||
if (diff < 0) return `${Math.abs(diff)}d overdue`;
|
||
if (diff < 7) return d.toLocaleDateString(undefined, { weekday: 'short' });
|
||
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||
};
|
||
const isToday = (iso) => {
|
||
if (!iso) return false;
|
||
const d = new Date(iso); const n = new Date();
|
||
return d.getFullYear() === n.getFullYear() && d.getMonth() === n.getMonth() && d.getDate() === n.getDate();
|
||
};
|
||
const isOverdue = (iso) => {
|
||
if (!iso) return false;
|
||
const d = new Date(iso); const n = new Date();
|
||
return d < new Date(n.getFullYear(), n.getMonth(), n.getDate());
|
||
};
|
||
|
||
const STATUS_LABEL = {
|
||
idle: 'Idle', queued: 'Queued', running: 'Running',
|
||
review: 'Review', done: 'Done', error: 'Error',
|
||
};
|
||
|
||
const relTime = (iso) => {
|
||
if (!iso) return '';
|
||
const diff = Math.max(0, Date.now() - new Date(iso).getTime());
|
||
const s = Math.floor(diff / 1000);
|
||
if (s < 60) return s + 's ago';
|
||
const m = Math.floor(s / 60);
|
||
if (m < 60) return m + 'm ago';
|
||
const h = Math.floor(m / 60);
|
||
if (h < 24) return h + 'h ago';
|
||
return Math.floor(h / 24) + 'd ago';
|
||
};
|
||
|
||
const logTime = (iso) => {
|
||
const d = new Date(iso);
|
||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||
};
|
||
|
||
// ---------- Checkbox ----------
|
||
const Checkbox = ({ done, onToggle, size }) => (
|
||
<div
|
||
className={`check ${done ? 'done' : ''}`}
|
||
style={size ? { width: size, height: size } : undefined}
|
||
onClick={(e) => { e.stopPropagation(); onToggle(); }}
|
||
role="checkbox"
|
||
aria-checked={done}
|
||
>
|
||
<svg viewBox="0 0 24 24"><path d="M5 12l4.5 4.5L19 7"/></svg>
|
||
</div>
|
||
);
|
||
|
||
// ---------- Lists Island ----------
|
||
const ListsIsland = ({ activeList, setActiveList, counts, search, setSearch }) => {
|
||
return (
|
||
<div className="island">
|
||
<div className="island-header">
|
||
<div className="island-eyebrow"><span className="dot"/><span>Navigator</span></div>
|
||
<h2 className="island-title">Lists</h2>
|
||
</div>
|
||
|
||
<div className="search-wrap">
|
||
<Icon name="search" size={14} />
|
||
<input
|
||
placeholder="Search tasks…"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
/>
|
||
<span className="kbd">⌘K</span>
|
||
</div>
|
||
|
||
<div className="island-body">
|
||
<div className="list-section-label">Smart lists</div>
|
||
{SEED_LISTS.map((l) => (
|
||
<div
|
||
key={l.id}
|
||
className={`list-item ${activeList === l.id ? 'active' : ''}`}
|
||
onClick={() => setActiveList(l.id)}
|
||
>
|
||
<div className="icon"><Icon name={l.icon} size={15} /></div>
|
||
<div className="label">{l.name}</div>
|
||
<div className="count">{counts[l.id] ?? ''}</div>
|
||
</div>
|
||
))}
|
||
|
||
<div className="list-section-label">My lists</div>
|
||
{SEED_USER_LISTS.map((l) => (
|
||
<div
|
||
key={l.id}
|
||
className={`list-item ${activeList === l.id ? 'active' : ''}`}
|
||
onClick={() => setActiveList(l.id)}
|
||
>
|
||
<div className="swatch" style={{ background: l.color }} />
|
||
<div className="label">{l.name}</div>
|
||
<div className="count">{counts[l.id] ?? ''}</div>
|
||
</div>
|
||
))}
|
||
|
||
<button className="new-list-btn">
|
||
<Icon name="plus" size={14} />
|
||
<span>New list</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="island-footer" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<div style={{
|
||
width: 28, height: 28, borderRadius: '50%',
|
||
background: 'linear-gradient(135deg, var(--moss), var(--sage))',
|
||
display: 'grid', placeItems: 'center',
|
||
fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--deep)', fontWeight: 600
|
||
}}>AK</div>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ fontSize: 12, color: 'var(--text)' }}>Aoife Kelly</div>
|
||
<div style={{ fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--text-faint)' }}>rider.island / local</div>
|
||
</div>
|
||
<button className="icon-btn" style={{ width: 26, height: 26 }} title="Settings">
|
||
<Icon name="more" size={14} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ---------- Tasks Island ----------
|
||
const TaskRow = ({ task, selected, onSelect, onToggle, onStar, leaving, entering }) => {
|
||
const [starPulse, setStarPulse] = useState(false);
|
||
const handleStar = (e) => {
|
||
e.stopPropagation();
|
||
setStarPulse(true);
|
||
setTimeout(() => setStarPulse(false), 400);
|
||
onStar();
|
||
};
|
||
const list = SEED_USER_LISTS.find((l) => l.id === task.list);
|
||
const overdue = isOverdue(task.due) && !task.done;
|
||
const today = isToday(task.due);
|
||
|
||
return (
|
||
<div
|
||
className={`task ${task.done ? 'done' : ''} ${selected ? 'selected' : ''} ${leaving ? 'leaving' : ''} ${entering ? 'entering' : ''}`}
|
||
onClick={onSelect}
|
||
>
|
||
<Checkbox done={task.done} onToggle={onToggle} />
|
||
<div className="task-body">
|
||
<div className="task-title">{task.title}</div>
|
||
<div className="task-meta">
|
||
{task.agent && (
|
||
<span className={`status-chip ${task.agent.status}`}>
|
||
<span className={`status-dot ${task.agent.status}`} />
|
||
{STATUS_LABEL[task.agent.status]}
|
||
</span>
|
||
)}
|
||
{list && (
|
||
<span className="chip" style={{ color: list.color }}>
|
||
<span style={{ width: 6, height: 6, borderRadius: 2, background: list.color, display: 'inline-block' }} />
|
||
{list.name}
|
||
</span>
|
||
)}
|
||
{task.agent?.branch && (
|
||
<span className="chip" title={task.agent.branch}>
|
||
<Icon name="branch" size={10} /> {task.agent.branch.replace('agent/', '')}
|
||
</span>
|
||
)}
|
||
{task.agent?.diff && task.agent.diff.files > 0 && (
|
||
<span className="chip">
|
||
<span className="diff-stats">
|
||
<span className="add">+{task.agent.diff.additions}</span>
|
||
<span className="del">−{task.agent.diff.deletions}</span>
|
||
</span>
|
||
</span>
|
||
)}
|
||
{task.due && !task.agent && (
|
||
<span className={`chip ${overdue ? 'overdue' : today ? 'due-today' : ''}`}>
|
||
<Icon name="calendar" size={10} /> {fmtDate(task.due)}
|
||
</span>
|
||
)}
|
||
{task.subtasks && task.subtasks.length > 0 && (
|
||
<span className="subcount">
|
||
{task.subtasks.filter((s) => s.done).length}/{task.subtasks.length} steps
|
||
</span>
|
||
)}
|
||
{task.tags && task.tags.map((t) => <span key={t} className="tag">{t}</span>)}
|
||
</div>
|
||
{task.agent && task.agent.status === 'running' && task.agent.log && task.agent.log.length > 0 && (() => {
|
||
const last = task.agent.log[task.agent.log.length - 1];
|
||
return (
|
||
<div className="task-agent-line">
|
||
<span className="prompt">›</span>
|
||
<span className="txt">{last.m}</span>
|
||
<span className="mini-cursor" />
|
||
</div>
|
||
);
|
||
})()}
|
||
</div>
|
||
<button
|
||
className={`star-btn ${task.starred ? 'on' : ''} ${starPulse ? 'pulse' : ''}`}
|
||
onClick={handleStar}
|
||
title={task.starred ? 'Unstar' : 'Mark important'}
|
||
>
|
||
<Icon name={task.starred ? 'star-filled' : 'star'} size={15} />
|
||
</button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const TasksIsland = ({
|
||
tasks, selectedId, setSelected,
|
||
onToggle, onStar, onAdd,
|
||
leavingIds, enteringIds,
|
||
activeList, showCompleted, setShowCompleted,
|
||
}) => {
|
||
const [newTitle, setNewTitle] = useState('');
|
||
const inputRef = useRef(null);
|
||
|
||
const now = new Date();
|
||
const dateLine = now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
|
||
|
||
const activeTasks = tasks.filter((t) => !t.done);
|
||
const doneTasks = tasks.filter((t) => t.done);
|
||
const overdueTasks = activeTasks.filter((t) => isOverdue(t.due));
|
||
const todayTasks = activeTasks.filter((t) => !isOverdue(t.due));
|
||
|
||
const listMeta = SEED_LISTS.find((l) => l.id === activeList) || SEED_USER_LISTS.find((l) => l.id === activeList);
|
||
const title = activeList === 'myday' ? 'My Day' : (listMeta?.name || 'Tasks');
|
||
const eyebrow = activeList === 'myday' ? dateLine : `${activeTasks.length} open · ${doneTasks.length} done`;
|
||
|
||
const handleSubmit = (e) => {
|
||
e.preventDefault();
|
||
if (newTitle.trim()) {
|
||
onAdd(newTitle.trim());
|
||
setNewTitle('');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="island">
|
||
<div className="tasks-head">
|
||
<div className="tasks-meta">
|
||
<div>
|
||
<div className="tasks-date">{activeList === 'myday' ? 'My Day' : 'List'}</div>
|
||
<h1 className="tasks-title">{title}</h1>
|
||
<div className="tasks-subtitle">
|
||
{activeList === 'myday' ? dateLine : eyebrow}
|
||
<span className="sep">·</span>
|
||
{activeTasks.length} open
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="tasks-actions">
|
||
<button className="icon-btn" title="Sort"><Icon name="sort" size={15} /></button>
|
||
<button className={`icon-btn ${showCompleted ? 'active' : ''}`} onClick={() => setShowCompleted((v) => !v)} title="Show completed">
|
||
<Icon name="eye" size={15} />
|
||
</button>
|
||
<button className="icon-btn" title="More"><Icon name="more" size={15} /></button>
|
||
</div>
|
||
</div>
|
||
|
||
<form className="add-task" onSubmit={handleSubmit}>
|
||
<div className="plus">+</div>
|
||
<input
|
||
ref={inputRef}
|
||
placeholder="Add a task…"
|
||
value={newTitle}
|
||
onChange={(e) => setNewTitle(e.target.value)}
|
||
/>
|
||
<span className="hint">ENTER</span>
|
||
</form>
|
||
|
||
<div className="island-body">
|
||
{overdueTasks.length > 0 && (
|
||
<>
|
||
<div className="tasks-group-label" style={{ color: 'var(--blood)' }}>Overdue</div>
|
||
{overdueTasks.map((t) => (
|
||
<TaskRow
|
||
key={t.id}
|
||
task={t}
|
||
selected={selectedId === t.id}
|
||
onSelect={() => setSelected(t.id)}
|
||
onToggle={() => onToggle(t.id)}
|
||
onStar={() => onStar(t.id)}
|
||
leaving={leavingIds.includes(t.id)}
|
||
entering={enteringIds.includes(t.id)}
|
||
/>
|
||
))}
|
||
</>
|
||
)}
|
||
|
||
{todayTasks.length > 0 && (
|
||
<>
|
||
{overdueTasks.length > 0 && <div className="tasks-group-label">Tasks</div>}
|
||
{todayTasks.map((t) => (
|
||
<TaskRow
|
||
key={t.id}
|
||
task={t}
|
||
selected={selectedId === t.id}
|
||
onSelect={() => setSelected(t.id)}
|
||
onToggle={() => onToggle(t.id)}
|
||
onStar={() => onStar(t.id)}
|
||
leaving={leavingIds.includes(t.id)}
|
||
entering={enteringIds.includes(t.id)}
|
||
/>
|
||
))}
|
||
</>
|
||
)}
|
||
|
||
{activeTasks.length === 0 && (
|
||
<div style={{ padding: '40px 24px', textAlign: 'center', color: 'var(--text-faint)' }}>
|
||
<div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase' }}>
|
||
All clear
|
||
</div>
|
||
<div style={{ fontSize: 12, marginTop: 6 }}>The harbor is calm. Add a task above.</div>
|
||
</div>
|
||
)}
|
||
|
||
{showCompleted && doneTasks.length > 0 && (
|
||
<>
|
||
<div className="tasks-group-label">Completed · {doneTasks.length}</div>
|
||
{doneTasks.map((t) => (
|
||
<TaskRow
|
||
key={t.id}
|
||
task={t}
|
||
selected={selectedId === t.id}
|
||
onSelect={() => setSelected(t.id)}
|
||
onToggle={() => onToggle(t.id)}
|
||
onStar={() => onStar(t.id)}
|
||
leaving={leavingIds.includes(t.id)}
|
||
entering={enteringIds.includes(t.id)}
|
||
/>
|
||
))}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ---------- Worktree + Terminal sub-components ----------
|
||
const WorktreeCard = ({ agent, onOpenDiff, onOpenWorktree }) => {
|
||
if (!agent) return null;
|
||
return (
|
||
<div className="worktree-card">
|
||
<div className="row">
|
||
<span className="k">Worktree</span>
|
||
<span className="v path" title={agent.worktree}>{agent.worktree}</span>
|
||
<button className="copy-btn" title="Copy path"><Icon name="copy" size={12} /></button>
|
||
</div>
|
||
<div className="row">
|
||
<span className="k">Branch</span>
|
||
<span className="v">
|
||
<span className="branch"><Icon name="branch" size={11} /> {agent.branch}</span>
|
||
<span style={{ color: 'var(--text-faint)', marginLeft: 8 }}>← {agent.baseBranch}</span>
|
||
</span>
|
||
</div>
|
||
<div className="row">
|
||
<span className="k">Diff</span>
|
||
<span className="v">
|
||
{agent.diff.files > 0 ? (
|
||
<span className="diff-stats">
|
||
<span>{agent.diff.files} files</span>
|
||
<span className="add">+{agent.diff.additions}</span>
|
||
<span className="del">−{agent.diff.deletions}</span>
|
||
<span className="bars">
|
||
{Array.from({ length: 5 }).map((_, i) => {
|
||
const total = agent.diff.additions + agent.diff.deletions || 1;
|
||
const addShare = Math.round((agent.diff.additions / total) * 5);
|
||
return <span key={i} className={i < addShare ? 'add' : 'del'} />;
|
||
})}
|
||
</span>
|
||
</span>
|
||
) : <span style={{ color: 'var(--text-faint)' }}>No changes yet</span>}
|
||
</span>
|
||
</div>
|
||
{agent.commits > 0 && (
|
||
<div className="row">
|
||
<span className="k">Commits</span>
|
||
<span className="v">{agent.commits} on branch</span>
|
||
</div>
|
||
)}
|
||
<div className="action-row">
|
||
<button className="btn primary grow" onClick={onOpenDiff} disabled={agent.diff.files === 0}>
|
||
<Icon name="diff" size={12} /> Open diff
|
||
</button>
|
||
<button className="btn" onClick={onOpenWorktree} title="Open worktree folder">
|
||
<Icon name="folder-open" size={12} /> Worktree
|
||
</button>
|
||
<button className="btn icon-only" title="Open in editor">
|
||
<Icon name="external" size={12} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const SessionTerminal = ({ agent, onInput }) => {
|
||
const bodyRef = useRef(null);
|
||
const [draft, setDraft] = useState('');
|
||
|
||
useEffect(() => {
|
||
if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
|
||
}, [agent?.log?.length]);
|
||
|
||
if (!agent) return null;
|
||
const running = agent.status === 'running';
|
||
const statusLabel = running ? 'LIVE' : STATUS_LABEL[agent.status];
|
||
|
||
return (
|
||
<div className="terminal">
|
||
<div className="terminal-head">
|
||
<div className="dots"><span className="r"/><span className="y"/><span className="g"/></div>
|
||
<span className="lbl">claude-session · {agent.branch}</span>
|
||
{running
|
||
? <span className="live"><span className="d"/>LIVE</span>
|
||
: <span className="live" style={{ color: 'var(--text-faint)' }}>{statusLabel}</span>}
|
||
</div>
|
||
<div className="terminal-body" ref={bodyRef}>
|
||
{(agent.log || []).map((l, i) => (
|
||
<div key={i} className={`log-line ${l.k}`}>
|
||
<span className="ts">{logTime(l.t)}</span>
|
||
<span className="tag">{l.k === 'msg' ? 'claude' : l.k === 'tool' ? 'tool' : l.k === 'sys' ? 'sys' : l.k === 'stdout' ? 'out' : l.k === 'stderr' ? 'err' : l.k}</span>
|
||
<span className="m">{l.m}</span>
|
||
</div>
|
||
))}
|
||
{running && (
|
||
<div className="log-line msg">
|
||
<span className="ts">{logTime(new Date().toISOString())}</span>
|
||
<span className="tag">claude</span>
|
||
<span className="m"><span className="cursor-block"/></span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 6, padding: '8px 10px', borderTop: '1px solid var(--line)', background: 'var(--surface-2)' }}>
|
||
<span style={{ fontFamily: 'var(--mono)', color: 'var(--accent)', fontSize: 11, alignSelf: 'center' }}>›</span>
|
||
<input
|
||
placeholder={running ? 'Send a message to the agent…' : 'Agent not running'}
|
||
value={draft}
|
||
onChange={(e) => setDraft(e.target.value)}
|
||
onKeyDown={(e) => { if (e.key === 'Enter' && draft.trim()) { onInput(draft); setDraft(''); } }}
|
||
disabled={!running}
|
||
style={{ flex: 1, fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--text)' }}
|
||
/>
|
||
<button className="btn icon-only" disabled={!draft.trim()}><Icon name="send" size={12} /></button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ---------- Details Island ----------
|
||
const DetailsIsland = ({ task, onUpdate, onDelete, onToggle, onStar, onAgentAction, onOpenDiff, onOpenWorktree, onAgentInput }) => {
|
||
if (!task) {
|
||
return (
|
||
<div className="island">
|
||
<div className="details-empty">
|
||
<div>
|
||
<div className="glyph"><Icon name="note" size={22} /></div>
|
||
<div className="label">No task selected</div>
|
||
<div className="hint">Pick a task from the middle<br/>to see its details here.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const list = SEED_USER_LISTS.find((l) => l.id === task.list);
|
||
const created = task.created ? new Date(task.created).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '—';
|
||
const due = task.due ? new Date(task.due).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }) : 'None';
|
||
|
||
const toggleSub = (sid) => {
|
||
const next = task.subtasks.map((s) => s.id === sid ? { ...s, done: !s.done } : s);
|
||
onUpdate({ ...task, subtasks: next });
|
||
};
|
||
const addSub = (title) => {
|
||
if (!title.trim()) return;
|
||
const next = [...(task.subtasks || []), { id: 's' + Date.now(), title: title.trim(), done: false }];
|
||
onUpdate({ ...task, subtasks: next });
|
||
};
|
||
const [subDraft, setSubDraft] = useState('');
|
||
|
||
return (
|
||
<div className="island">
|
||
<div className="island-header" style={{ paddingBottom: 10 }}>
|
||
<div className="island-eyebrow">
|
||
<span className="dot" />
|
||
<span>Logbook</span>
|
||
<span style={{ marginLeft: 'auto', color: 'var(--text-faint)' }}>#{task.id}</span>
|
||
</div>
|
||
<h2 className="island-title" style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-dim)' }}>
|
||
{task.agent ? 'Agent task' : 'Task details'}
|
||
</h2>
|
||
</div>
|
||
|
||
{task.agent && (
|
||
<div className="agent-strip">
|
||
<span className={`status-chip ${task.agent.status}`}>
|
||
<span className={`status-dot ${task.agent.status}`} />
|
||
{STATUS_LABEL[task.agent.status]}
|
||
</span>
|
||
<div className="meta">
|
||
<Icon name="cpu" size={10} /> {task.agent.model}
|
||
<span className="sep">·</span>
|
||
{task.agent.turns} turns
|
||
<span className="sep">·</span>
|
||
{(task.agent.tokens / 1000).toFixed(1)}k tok
|
||
{task.agent.startedAt && <><span className="sep">·</span>{relTime(task.agent.startedAt)}</>}
|
||
</div>
|
||
{task.agent.status === 'running' ? (
|
||
<button className="btn danger icon-only" onClick={() => onAgentAction(task.id, 'stop')} title="Stop agent"><Icon name="stop" size={12} /></button>
|
||
) : task.agent.status === 'idle' || task.agent.status === 'error' || task.agent.status === 'queued' ? (
|
||
<button className="btn primary" onClick={() => onAgentAction(task.id, 'start')}><Icon name="play" size={12} /> {task.agent.status === 'error' ? 'Retry' : 'Dispatch'}</button>
|
||
) : null}
|
||
</div>
|
||
)}
|
||
|
||
<div className="island-body">
|
||
<div className="details-title-row">
|
||
<Checkbox done={task.done} onToggle={() => onToggle(task.id)} />
|
||
<textarea
|
||
className="details-title"
|
||
value={task.title}
|
||
onChange={(e) => onUpdate({ ...task, title: e.target.value })}
|
||
rows={2}
|
||
/>
|
||
<button
|
||
className={`star-btn ${task.starred ? 'on' : ''}`}
|
||
style={{ opacity: 1 }}
|
||
onClick={() => onStar(task.id)}
|
||
>
|
||
<Icon name={task.starred ? 'star-filled' : 'star'} size={16} />
|
||
</button>
|
||
</div>
|
||
|
||
{task.agent && (
|
||
<div className="details-section">
|
||
<div className="details-section-label">Worktree</div>
|
||
<WorktreeCard
|
||
agent={task.agent}
|
||
onOpenDiff={() => onOpenDiff(task.id)}
|
||
onOpenWorktree={() => onOpenWorktree(task.id)}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{task.agent && (
|
||
<div className="details-section">
|
||
<div className="details-section-label">
|
||
Session output
|
||
<span style={{ marginLeft: 'auto', float: 'right', color: 'var(--text-mute)', fontFamily: 'var(--mono)', fontSize: 10 }}>
|
||
{(task.agent.log || []).length} lines
|
||
</span>
|
||
</div>
|
||
<SessionTerminal
|
||
agent={task.agent}
|
||
onInput={(msg) => onAgentInput(task.id, msg)}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{(task.subtasks || []).length > 0 && (
|
||
<div className="details-section">
|
||
<div className="details-section-label">Steps · {task.subtasks.filter(s => s.done).length}/{task.subtasks.length}</div>
|
||
{task.subtasks.map((s) => (
|
||
<div key={s.id} className={`subtask-row ${s.done ? 'done' : ''}`}>
|
||
<Checkbox done={s.done} onToggle={() => toggleSub(s.id)} />
|
||
<div className="label">{s.title}</div>
|
||
</div>
|
||
))}
|
||
<div className="subtask-add">
|
||
<div className="check" style={{ width: 16, height: 16, borderStyle: 'dashed' }} />
|
||
<input
|
||
placeholder="Add step"
|
||
value={subDraft}
|
||
onChange={(e) => setSubDraft(e.target.value)}
|
||
onKeyDown={(e) => { if (e.key === 'Enter') { addSub(subDraft); setSubDraft(''); } }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="details-section">
|
||
<div className="meta-row">
|
||
<span className="key">List</span>
|
||
<span className="val" style={{ color: list?.color || 'var(--text)' }}>
|
||
{list ? list.name : '—'}
|
||
</span>
|
||
</div>
|
||
<div className="meta-row">
|
||
<span className="key">Due</span>
|
||
<span className={`val ${isOverdue(task.due) && !task.done ? 'peat' : isToday(task.due) ? 'accent' : 'muted'}`}>{due}</span>
|
||
</div>
|
||
{!task.agent && (
|
||
<div className="meta-row">
|
||
<span className="key">Reminder</span>
|
||
<span className="val muted">{task.reminder || 'None'}</span>
|
||
</div>
|
||
)}
|
||
<div className="meta-row">
|
||
<span className="key">Important</span>
|
||
<span className={`val ${task.starred ? 'peat' : 'muted'}`}>{task.starred ? 'Starred' : 'No'}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="details-section">
|
||
<div className="details-section-label">Notes</div>
|
||
<textarea
|
||
className="notes-area"
|
||
placeholder="Add a note for the agent…"
|
||
value={task.notes || ''}
|
||
onChange={(e) => onUpdate({ ...task, notes: e.target.value })}
|
||
/>
|
||
</div>
|
||
|
||
{(task.tags || []).length > 0 && (
|
||
<div className="details-section" style={{ borderBottom: 0 }}>
|
||
<div className="details-section-label">Tags</div>
|
||
<div>
|
||
{task.tags.map((t) => <span key={t} className="tag-chip">{t}</span>)}
|
||
<span className="tag-chip" style={{ borderStyle: 'dashed', cursor: 'pointer' }}>+ add</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="island-footer" style={{ display: 'flex', gap: 8, justifyContent: 'space-between' }}>
|
||
<button className="icon-btn" title="Delete" onClick={() => onDelete(task.id)}>
|
||
<Icon name="trash" size={14} />
|
||
</button>
|
||
<div style={{ fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--text-faint)', alignSelf: 'center' }}>
|
||
Created {created}
|
||
</div>
|
||
<button className="icon-btn" title="Close">
|
||
<Icon name="x" size={14} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
window.ListsIsland = ListsIsland;
|
||
window.TasksIsland = TasksIsland;
|
||
window.DetailsIsland = DetailsIsland;
|