|
1 | | -import { Show, For, createSignal, onMount, onCleanup } from 'solid-js'; |
| 1 | +import { Show, For, createSignal, createEffect, onMount, onCleanup } from 'solid-js'; |
2 | 2 |
|
3 | 3 | import { |
4 | 4 | store, |
@@ -27,13 +27,38 @@ interface TaskAITerminalProps { |
27 | 27 | task: Task; |
28 | 28 | isActive: boolean; |
29 | 29 | promptHandle: PromptInputHandle | undefined; |
| 30 | + /** Receives a function that scrolls the AI terminal to the moment a given step |
| 31 | + * index was recorded. Called with `undefined` when the terminal unmounts. */ |
| 32 | + onStepJumpReady?: (jump: ((stepIndex: number) => boolean) | undefined) => void; |
30 | 33 | } |
31 | 34 |
|
32 | 35 | export function TaskAITerminal(props: TaskAITerminalProps) { |
33 | 36 | onCleanup(() => unregisterFocusFn(`${props.task.id}:ai-terminal`)); |
34 | 37 |
|
35 | 38 | const dockerOverlayLabel = () => getTaskDockerOverlayLabel(props.task.dockerSource); |
36 | 39 |
|
| 40 | + // Step bookmarks — TerminalView hands us a mark/jump API once the xterm |
| 41 | + // instance is ready. We mark each newly arrived step at the current scrollback |
| 42 | + // position, and expose `jump` upward so the steps panel can scroll back to it. |
| 43 | + // On agent restart the inner TerminalView remounts (keyed Show), so the API |
| 44 | + // reference must be reset to undefined and lastMarkedLen back to 0 — the new |
| 45 | + // mount will then re-backfill markers for all existing steps. |
| 46 | + let stepNav: { mark: (i: number) => void; jump: (i: number) => boolean } | undefined; |
| 47 | + let lastMarkedLen = 0; |
| 48 | + onCleanup(() => props.onStepJumpReady?.(undefined)); |
| 49 | + |
| 50 | + createEffect(() => { |
| 51 | + const len = props.task.stepsContent?.length ?? 0; |
| 52 | + if (!stepNav) return; // Don't advance lastMarkedLen until a terminal is ready. |
| 53 | + if (len <= lastMarkedLen) { |
| 54 | + lastMarkedLen = len; |
| 55 | + return; |
| 56 | + } |
| 57 | + for (let i = lastMarkedLen; i < len; i++) stepNav.mark(i); |
| 58 | + lastMarkedLen = len; |
| 59 | + }); |
| 60 | + |
| 61 | + // --- Markdown file viewer --- |
37 | 62 | const [mdViewerContent, setMdViewerContent] = createSignal(''); |
38 | 63 | const [mdViewerFileName, setMdViewerFileName] = createSignal(''); |
39 | 64 | const [mdViewerOpen, setMdViewerOpen] = createSignal(false); |
@@ -200,6 +225,24 @@ export function TaskAITerminal(props: TaskAITerminalProps) { |
200 | 225 | onFileLink={handleFileLink} |
201 | 226 | onPromptDetected={(text) => setLastPrompt(props.task.id, text)} |
202 | 227 | onReady={(focusFn) => registerFocusFn(`${props.task.id}:ai-terminal`, focusFn)} |
| 228 | + onStepNavReady={(api) => { |
| 229 | + if (!api) { |
| 230 | + // TerminalView is unmounting (agent restart). Drop the stale API |
| 231 | + // and reset the watermark so the next mount's backfill marks every step. |
| 232 | + stepNav = undefined; |
| 233 | + lastMarkedLen = 0; |
| 234 | + props.onStepJumpReady?.(undefined); |
| 235 | + return; |
| 236 | + } |
| 237 | + stepNav = api; |
| 238 | + // Backfill markers for steps that already exist when the terminal mounts. |
| 239 | + // They all anchor to the current line — best-effort, since we can't know |
| 240 | + // where each historical step was originally written. |
| 241 | + const len = props.task.stepsContent?.length ?? 0; |
| 242 | + for (let i = 0; i < len; i++) api.mark(i); |
| 243 | + lastMarkedLen = len; |
| 244 | + props.onStepJumpReady?.(api.jump); |
| 245 | + }} |
203 | 246 | fontSize={14} |
204 | 247 | /> |
205 | 248 | </Show> |
|
0 commit comments