SessionTask UI System
Badges: Core · Server-Driven UI
Build reactive, AI-powered interfaces using pure Kotlin. Stream content, manage complex tasks, and create interactive human-in-the-loop workflows.
Reality Check: Kotlin to Browser
Source (analyze_task.kt):
fun analyze(task: SessionTask) {
task.header("Analysis Results", level = 2)
val results = task.newTask() // Sub-task
task.add("Scanning repository...")
// Stream findings
findings.forEach { f ->
results.add("<div class='finding'>${f.msg}</div>")
}
results.complete()
task.add("✓ Done")
task.complete()
}
Live Preview (rendered):
- Heading: "Analysis Results"
- Finding card:
src/main/App.kt— "Unused import detected" - Status: "Scanning repository..."
- Status: "✓ Done" (accent color)
UI Component Library
- 📋 SessionTask — The primary canvas. Manages HTML buffers, unique message IDs, and lifecycle states (spinner to complete).
- 📑 TabbedDisplay — Dynamic tabbed interface. Supports programmatically adding, updating, or streaming content into tabs.
- 💬 Discussable — Human-in-the-loop workflow. Generate, review, and revise content with a blocking acceptance loop.
- ♻️ Retryable — Extends TabbedDisplay for LLM operations. Adds a "Recycle" button to re-run generations in new tabs.
- 🛠️ Agent Patterns — Utilities like
displayMapInTabsandAddApplyFileDiffLinksfor rapid agent UI development. - 📁 File Management — Save files to session directories, generate download links, and create live-updating log streams.
API Reference
Rendering
// Basic content
task.add("Hello **World**!", markdown = true)
task.header("Title", level = 2)
task.echo("User prompt style")
// Specialized output
task.hideable("Dismissible message")
task.verbose("Debug info (hidden by default)")
task.error(exception) // Renders red box + stack trace
task.image(bufferedImage) // Auto-saves and renders <img>
// Markdown & Mermaid
val html = MarkdownUtil.renderMarkdown(raw, ui = task.ui)
task.add(html)
Interaction
// Server-side link triggers
val link = task.ui.hrefLink("Click Me") {
task.add("Link clicked!")
}
task.add("Action: $link")
// User input
val input = task.ui.textInput { response ->
task.add("You typed: $response")
}
task.add(input)
// Expandable sections
task.expandable("Logs", "<pre>...</pre>")
Layout & Tasks
// Sub-task management
val sub = task.newTask() // Reserves spot in stream
val detached = task.ui.newTask(false) // Manual placement
task.add("<div>${detached.placeholder}</div>")
// Tabbed Layouts
val tabs = TabbedDisplay(task)
tabs["Summary"] = "Static content"
val liveTab = tabs.newTask("Live") // Stream into tab
liveTab.add("Updating...")
// Agent Utilities
task.add(AgentPatterns.displayMapInTabs(fileMap))
Dynamic Updates
// Dynamic content updates
val statusBuffer = task.add("Starting process...")
for (i in 1..5) {
Thread.sleep(500)
statusBuffer?.setLength(0)
statusBuffer?.append("Processing step $i/5...")
task.update() // Push changes to client
}
statusBuffer?.setLength(0)
statusBuffer?.append("<strong>Done!</strong>")
task.update()
task.complete()
Best Practices
- Lifecycle Management — Always call
task.complete(). Failing to do so leaves the loading spinner active indefinitely, degrading user experience. - Thread Safety — SessionTask methods are thread-safe. Use
task.ui.poolfor heavy computations to avoid blocking the UI thread. - Dynamic Updates — Use the
StringBuilderreturned byadd()and calltask.update()to refresh existing elements. - Blocking Operations —
Discussableis blocking. Usetask.ui.scheduledThreadPoolExecutorfor delayed or periodic execution.
Advanced Workflow Patterns
Discussable: Human-in-the-Loop
val finalResult = Discussable(
task = task,
heading = "Drafting Email",
userMessage = { "Draft an email" },
initialResponse = { prompt ->
MyObject(llm.generate(prompt))
},
outputFn = { design ->
design.toHtml()
},
reviseResponse = { history ->
llm.chat(history)
}
).call() // Blocks until Accept
Flow: Generate → Review → Revise → Accept
Retryable: LLM Regeneration
val retryable = Retryable(task) { buffer ->
// Runs on init and each recycle
val result = performExpensiveOperation()
result // Return HTML content
}
// Each retry creates a new tab
// User clicks ♻ to regenerate
Use case: LLM generations that may need multiple attempts