mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
feat: render background-task index.html output in a sandboxed iframe
The task output pane now prefers bg-tasks/<slug>/index.html when present and non-empty, rendering it full-bleed via HtmlFileViewer (app://workspace protocol) so CSS, layout, and scripts render faithfully. Falls back to the markdown index.md note when there is no HTML artifact. The viewer remounts on refreshKey so a re-run's updated HTML reloads. The Source/Rendered toggle works for both formats. The runner agent is instructed to choose index.md (default, notes) vs a self-contained index.html (visual/styled output) per run, written via the existing file-writeText tool. The Copilot background-task skill notes the HTML option so visual asks are steered toward it.
This commit is contained in:
parent
c48ef5ac0c
commit
344d30c951
3 changed files with 66 additions and 31 deletions
|
|
@ -19,6 +19,7 @@ import type { ConversationItem } from '@/lib/chat-conversation'
|
|||
import { runLogToConversation } from '@/lib/run-to-conversation'
|
||||
import { CompactConversation } from '@/components/compact-conversation'
|
||||
import { RichMarkdownViewer } from '@/components/rich-markdown-viewer'
|
||||
import { HtmlFileViewer } from '@/components/html-file-viewer'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trigger helpers (inlined; extract to shared <TriggersEditor> as a follow-up)
|
||||
|
|
@ -502,15 +503,22 @@ function SectionRegion({ label, children }: { label?: string; children: React.Re
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output pane — index.md (main pane content)
|
||||
// Output pane — index.html (preferred) or index.md (main pane content)
|
||||
//
|
||||
// Renders the task's `index.md` like a note: max-width 720px centered, same
|
||||
// typography (~16px, 1.5 line-height, generous padding) as the note editor's
|
||||
// ProseMirror rule in `editor.css`. No chrome above the body — just the
|
||||
// markdown, with a small floating Source ⇄ Rendered toggle in the top-right.
|
||||
// A task's agent-owned artifact is either:
|
||||
// - `index.html` — a self-contained, styled web page. Rendered full-bleed in
|
||||
// a sandboxed iframe (via `HtmlFileViewer` / the `app://workspace`
|
||||
// protocol) so CSS, layout, and scripts render faithfully. Preferred when
|
||||
// present and non-empty.
|
||||
// - `index.md` — a note. Rendered like the note editor: max-width 720px
|
||||
// centered, same typography as `editor.css`, via `RichMarkdownViewer`.
|
||||
//
|
||||
// In both cases a small floating Source ⇄ Rendered toggle in the top-right
|
||||
// swaps the rendered view for the raw file source.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function OutputPane({ slug, taskName, refreshKey }: { slug: string; taskName: string; refreshKey: number }) {
|
||||
const [mode, setMode] = useState<'md' | 'html'>('md')
|
||||
const [body, setBody] = useState<string>('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [viewSource, setViewSource] = useState(false)
|
||||
|
|
@ -519,21 +527,33 @@ function OutputPane({ slug, taskName, refreshKey }: { slug: string; taskName: st
|
|||
let cancelled = false
|
||||
setLoading(true)
|
||||
void (async () => {
|
||||
// Prefer index.html when it exists and has content; otherwise fall
|
||||
// back to index.md (the default seeded artifact).
|
||||
try {
|
||||
const result = await window.ipc.invoke('workspace:readFile', {
|
||||
const html = await window.ipc.invoke('workspace:readFile', {
|
||||
path: `bg-tasks/${slug}/index.html`,
|
||||
})
|
||||
if (html.data.trim()) {
|
||||
if (!cancelled) { setMode('html'); setBody(html.data) }
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// No index.html — fall through to markdown.
|
||||
}
|
||||
try {
|
||||
const md = await window.ipc.invoke('workspace:readFile', {
|
||||
path: `bg-tasks/${slug}/index.md`,
|
||||
})
|
||||
if (!cancelled) setBody(result.data)
|
||||
if (!cancelled) { setMode('md'); setBody(md.data) }
|
||||
} catch {
|
||||
if (!cancelled) setBody('')
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
if (!cancelled) { setMode('md'); setBody('') }
|
||||
}
|
||||
})()
|
||||
})().finally(() => { if (!cancelled) setLoading(false) })
|
||||
return () => { cancelled = true }
|
||||
}, [slug, refreshKey])
|
||||
|
||||
const isEmpty = !body.trim() || body.trim() === `# ${taskName}`
|
||||
const isEmpty = mode === 'md' && (!body.trim() || body.trim() === `# ${taskName}`)
|
||||
const showHtml = mode === 'html' && !viewSource
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 overflow-hidden bg-background">
|
||||
|
|
@ -542,29 +562,35 @@ function OutputPane({ slug, taskName, refreshKey }: { slug: string; taskName: st
|
|||
type="button"
|
||||
onClick={() => setViewSource(v => !v)}
|
||||
className="absolute right-4 top-3 z-10 rounded-md bg-background/70 px-2 py-0.5 text-[11px] text-muted-foreground backdrop-blur hover:bg-accent hover:text-foreground"
|
||||
aria-label={viewSource ? 'Show rendered output' : 'Show source markdown'}
|
||||
aria-label={viewSource ? 'Show rendered output' : 'Show source'}
|
||||
>
|
||||
{viewSource ? 'Rendered' : 'Source'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="mx-auto max-w-[720px] px-16 py-8">
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="size-3 animate-spin" /> Loading…
|
||||
</div>
|
||||
) : isEmpty ? (
|
||||
<p className="text-sm italic text-muted-foreground">
|
||||
No output yet. Click <span className="font-medium text-foreground">Run now</span> in the sidebar, or wait for a trigger to fire.
|
||||
</p>
|
||||
) : viewSource ? (
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap font-mono text-[13px] leading-relaxed">{body}</pre>
|
||||
) : (
|
||||
<RichMarkdownViewer content={body} />
|
||||
)}
|
||||
{showHtml ? (
|
||||
// Full-bleed: the iframe fills the pane and scrolls internally.
|
||||
// Remount on refreshKey so a re-run's updated index.html reloads.
|
||||
<HtmlFileViewer key={`${slug}-${refreshKey}`} path={`bg-tasks/${slug}/index.html`} />
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="mx-auto max-w-[720px] px-16 py-8">
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="size-3 animate-spin" /> Loading…
|
||||
</div>
|
||||
) : isEmpty ? (
|
||||
<p className="text-sm italic text-muted-foreground">
|
||||
No output yet. Click <span className="font-medium text-foreground">Run now</span> in the sidebar, or wait for a trigger to fire.
|
||||
</p>
|
||||
) : viewSource ? (
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap font-mono text-[13px] leading-relaxed">{body}</pre>
|
||||
) : (
|
||||
<RichMarkdownViewer content={body} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ export const skill = String.raw`
|
|||
A *background task* is a persistent agent the user configures once and the framework keeps firing — on a schedule, inside time-of-day windows, and/or in response to matching incoming events (Gmail threads, calendar changes). Each task lives at \`bg-tasks/<slug>/\` and owns two artifacts:
|
||||
|
||||
- \`task.yaml\` — the spec (the user's **instructions**, triggers, runtime state). You and the user both treat this as the source of truth.
|
||||
- \`index.md\` — the agent-owned body. The runtime never writes here; the bg-task agent does, each run.
|
||||
- \`index.md\` — the agent-owned body (a note). The runtime never writes here; the bg-task agent does, each run.
|
||||
|
||||
For **visual** output — a dashboard, a styled report, a metrics table with conditional colors, a chart — the agent may instead write a self-contained \`index.html\`, which the task view renders full-screen in a sandboxed iframe with CSS and layout preserved. The agent picks the format per run from the instructions; you don't set it, but when the ask is inherently visual, say so in the instructions (e.g. "…rendered as a styled HTML dashboard") so the agent leans that way.
|
||||
|
||||
A task is one of two shapes — the agent decides per run from the verbs in \`instructions\`:
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ You are running with **no user present** to clarify, approve, or watch.
|
|||
|
||||
Your task folder is \`bg-tasks/<slug>/\` (the path is given in the run message). It contains:
|
||||
- \`task.yaml\` — the spec. **Never touch this.** The runtime owns it.
|
||||
- \`index.md\` — agent-owned. You read and write this freely via \`file-readText\` / \`file-editText\`.
|
||||
- \`index.md\` — the default agent-owned artifact (a note). You read and write it freely via \`file-readText\` / \`file-editText\`.
|
||||
- \`index.html\` — optional agent-owned artifact for **visual** output (see OUTPUT MODE). When it exists and is non-empty it is shown to the user instead of \`index.md\`.
|
||||
- \`runs/\` — your own run logs (jsonl). You don't write to it directly; the runtime does.
|
||||
|
||||
You can also read and write anywhere else under the workspace (\`knowledge/\`, etc.) when your instructions call for it.
|
||||
|
|
@ -28,6 +29,12 @@ Use when instructions imply a **current state** artifact:
|
|||
- "Keep me posted on …" / "What's the latest on …"
|
||||
On every run: \`file-readText\` \`index.md\`, decide the smallest patch that brings it into alignment with the instructions, apply with \`file-editText\`. Patch-style discipline: edit one region, re-read, then edit the next. Avoid one-shot rewrites.
|
||||
|
||||
Pick the artifact format from what the output needs:
|
||||
- **\`index.md\`** (default) — prose, lists, summaries, digests, briefs. Rendered as a styled note. Use patch-style edits as above.
|
||||
- **\`index.html\`** — when the output is inherently **visual**: a dashboard, a metrics table with conditional colors, a chart, a styled report — anything where layout/CSS carry meaning that a plain note would lose. Write a single **self-contained** file with \`file-writeText\` (inline all CSS and JS; avoid external/CDN dependencies as they may be blocked; reference only assets you save next to it in the task folder — relative paths resolve against the folder). It renders full-screen in a sandboxed iframe. HTML is typically regenerated wholesale each run, so a one-shot \`file-writeText\` is fine here.
|
||||
|
||||
Use ONE format per task — don't maintain both. \`index.html\` wins when present and non-empty. If you move a task from HTML back to a plain note, blank out \`index.html\` (\`file-writeText\` with \`""\`) so \`index.md\` shows again.
|
||||
|
||||
ACTION MODE — perform a side-effect, append a journal entry.
|
||||
Use when instructions imply a **recurring action**:
|
||||
- "Send / draft / post / notify / file / reply / publish / call / forward …"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue