({ sessionId, taskId }: { sessionId: string; taskId: string })
| 186 | } |
| 187 | |
| 188 | function TaskOutput({ sessionId, taskId }: { sessionId: string; taskId: string }) { |
| 189 | // Progressive byte-window paging: fetch the first window on mount, then |
| 190 | // append subsequent windows on demand via the server-provided exact |
| 191 | // `nextOffset` cursor. Keeps arbitrarily large logs readable in full. |
| 192 | const [content, setContent] = useState(''); |
| 193 | const [cursor, setCursor] = useState(0); |
| 194 | const [size, setSize] = useState(0); |
| 195 | const [eof, setEof] = useState(false); |
| 196 | const [loading, setLoading] = useState(false); |
| 197 | const [err, setErr] = useState<string | null>(null); |
| 198 | const [started, setStarted] = useState(false); |
| 199 | |
| 200 | const loadFrom = useCallback( |
| 201 | async (offset: number) => { |
| 202 | setLoading(true); |
| 203 | setErr(null); |
| 204 | try { |
| 205 | const w = await api.getTaskOutput(sessionId, taskId, offset); |
| 206 | setContent((prev) => (offset === 0 ? w.content : prev + w.content)); |
| 207 | setCursor(w.nextOffset); |
| 208 | setSize(w.size); |
| 209 | setEof(w.eof); |
| 210 | } catch (error) { |
| 211 | setErr(error instanceof Error ? error.message : String(error)); |
| 212 | } finally { |
| 213 | setLoading(false); |
| 214 | } |
| 215 | }, |
| 216 | [sessionId, taskId], |
| 217 | ); |
| 218 | |
| 219 | useEffect(() => { |
| 220 | if (started) return; |
| 221 | setStarted(true); |
| 222 | void loadFrom(0); |
| 223 | }, [started, loadFrom]); |
| 224 | |
| 225 | return ( |
| 226 | <div className="border-t border-border bg-[var(--color-surface-0)]"> |
| 227 | <div className="flex items-center gap-2 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.1em] text-fg-3"> |
| 228 | <span>output.log</span> |
| 229 | <span className="tabular"> |
| 230 | {formatBytes(Math.min(cursor, size))} / {formatBytes(size)} |
| 231 | </span> |
| 232 | {!eof && cursor > 0 ? ( |
| 233 | <span className="text-[var(--color-sev-warning)]">· more below</span> |
| 234 | ) : null} |
| 235 | <span className="ml-auto"><CopyButton value={content} label="copy" /></span> |
| 236 | </div> |
| 237 | {err !== null ? ( |
| 238 | <div className="border-t border-border px-3 py-2 font-mono text-[11px] text-[var(--color-sev-error)]"> |
| 239 | {err} |
| 240 | </div> |
| 241 | ) : null} |
| 242 | <pre className="max-h-[480px] overflow-auto whitespace-pre-wrap break-words border-t border-border px-3 py-2 font-mono text-[11px] leading-[1.5] text-fg-1"> |
| 243 | {content || (loading ? 'loading log…' : '(empty)')} |
| 244 | </pre> |
| 245 | {!eof && cursor > 0 ? ( |
nothing calls this directly
no test coverage detected