SessionTask UI System
Server-driven reactive UI framework for building AI-powered interfaces. Write Kotlin, stream HTML, create interactive workflows.
Architecture Overview
The SessionTask UI system is fundamentally server-driven. Instead of writing JavaScript to manipulate the DOM, you write Kotlin code that generates HTML fragments. These fragments are pushed to the browser via WebSockets, where they're rendered in real-time.
SessionTask owns a unique messageID
that maps to a <div> in the DOM. When you call task.add(),
content is appended to that div. When you call task.update(), the entire div
is replaced with the current buffer state.
Reality Check: Code โ Output
fun analyzeCode(task: SessionTask) {
task.header("Code Analysis", level = 2)
// Create a sub-task for streaming results
val resultsTask = task.newTask()
task.add("Scanning repository...")
// Stream findings as they're discovered
for (finding in analyzer.scan()) {
resultsTask.add("""
<div class="finding">
<strong>${finding.file}</strong>
<p>${finding.message}</p>
</div>
""")
}
resultsTask.complete()
task.add("โ Analysis complete")
task.complete()
}
Code Analysis
Unused import detected
Consider extracting method
Scanning repository...
โ Analysis complete
Core Components
hrefLink and textInput APIs.
Rendering Methods
// Simple text (wrapped in div)
task.add("Hello, World!")
// Headers (H1-H6)
task.header("Analysis Results", level = 2)
// HTML with custom CSS classes
task.add("<b>Important</b>", additionalClasses = "alert alert-warning")
// User-style message (right-aligned)
task.echo("User prompt appears here")
// Dismissible message (has close button)
task.hideable("<b>Note:</b> Click X to dismiss")
// Verbose/debug output (hidden by default)
task.verbose("Detailed debug information...")
// Error display with expandable stack trace
try {
riskyOperation()
} catch (e: Exception) {
task.error(e)
}
// Mark task as complete (removes spinner)
task.complete()
// Clickable link that triggers server-side code
val linkHtml = task.ui.hrefLink("Click Me") {
log.info("Link was clicked!")
task.add("You clicked the link!")
}
task.add("Please $linkHtml to continue.")
// Text input field
val inputHtml = task.ui.textInput { userResponse: String ->
task.add("You typed: $userResponse")
}
task.add(inputHtml)
// Expandable content (collapsed by default)
task.expandable("Debug Logs", "<pre>Log content...</pre>")
// Expandable content (expanded by default)
task.expanded("Summary", "<p>Key findings here</p>")
Please Click Me to continue.
// Dynamic updates using buffer references
// add() returns a StringBuilder you can modify
val statusBuffer = task.add("Starting process...")
for (i in 1..5) {
Thread.sleep(500)
// Clear and update the buffer
statusBuffer?.setLength(0)
statusBuffer?.append("Processing step $i/5...")
// Push changes to the client
task.update()
}
// Finalize
statusBuffer?.setLength(0)
statusBuffer?.append("<strong>Done!</strong>")
task.update()
task.complete()
add() method returns a StringBuilder
reference. Modify this buffer and call task.update() to refresh the UI
without appending new elements.
// TabbedDisplay for organized content
val tabs = TabbedDisplay(task)
tabs["Summary"] = "Overview content here"
tabs["Details"] = "<ul><li>Detail 1</li></ul>"
// Embed a streaming task inside a tab
val workerTask = tabs.newTask("Live Progress")
workerTask.add("Step 1 complete...")
workerTask.add("Step 2 complete...")
workerTask.complete()
// Delete a tab
tabs.delete("Details")
// Sub-tasks for nested layouts
val subTask = task.newTask() // Reserves display order
task.add("This appears BELOW the subTask")
subTask.add("I appear in my reserved spot")
subTask.complete()
// Manual placement (detached task)
val innerTask = task.ui.newTask(false)
task.add("<div class='custom-box'>${innerTask.placeholder}</div>")
innerTask.add("Content inside the box")
innerTask.complete()
Human-in-the-Loop: Discussable
The Discussable component implements a powerful Generate โ Review โ Revise workflow.
It blocks execution until the user explicitly accepts the result.
Initial Generation
The initialResponse function generates content based on the user's prompt.
Review Interface
Content is displayed with a chat box for feedback and an "Accept" button.
Revision Loop
If the user provides feedback, reviseResponse generates a new version in a new tab.
Acceptance
When "Accept" is clicked, the function returns the final approved object.
val finalResult = Discussable(
task = task,
heading = "Drafting Email",
userMessage = { "Draft an email to the team about the release" },
initialResponse = { prompt ->
EmailDraft(llm.generate(prompt))
},
outputFn = { draft ->
// Render the draft as HTML for review
draft.toHtml()
},
reviseResponse = { history ->
// history is List<Pair<String, Role>>
// Contains user feedback + assistant responses
llm.chat(history)
}
).call() // Blocks until user clicks "Accept"
task.add("Final approved email: ${finalResult.subject}")
Best Practices
task.complete() when work is done. Otherwise, the spinner
spins forever, making the UI look unresponsive.
SessionTask methods are safe to call from background threads.
Use task.ui.pool for heavy computations.
SocketManager checks AuthorizationManager before
allowing reads/writes. Configure your auth interface properly.
Discussable is blocking. Retryable uses a thread pool.
Use task.ui.scheduledThreadPoolExecutor for delayed execution.
Quick API Reference
// SessionTask Methods
task.add(html: String, additionalClasses: String = ""): StringBuilder?
task.header(text: String, level: Int = 1)
task.echo(message: String)
task.hideable(html: String): StringBuilder?
task.verbose(message: String)
task.error(e: Throwable)
task.expandable(title: String, content: String)
task.expanded(title: String, content: String)
task.image(image: BufferedImage)
task.append(html: String, showSpinner: Boolean = true)
task.update()
task.complete()
task.newTask(): SessionTask
task.saveFile(path: String, data: ByteArray): String
task.newLogStream(name: String): OutputStream
task.linkedTask(label: String): SessionTask
// SocketManager Methods (via task.ui)
task.ui.hrefLink(text: String, handler: () -> Unit): String
task.ui.textInput(handler: (String) -> Unit): String
task.ui.newTask(root: Boolean = true, cancelable: Boolean = false): SessionTask
task.ui.linkToSession(text: String): String
task.ui.pool: ExecutorService
task.ui.scheduledThreadPoolExecutor: ScheduledExecutorService
// TabbedDisplay
val tabs = TabbedDisplay(task, closable: Boolean = true)
tabs[label: String] = content: String
tabs.newTask(label: String): SessionTask
tabs.delete(label: String)
tabs.clear()
// Retryable
Retryable.retryable(ui: SocketManager) { subTask -> ... }
// Discussable
Discussable(task, heading, userMessage, initialResponse, outputFn, reviseResponse).call()
Start Building
The SessionTask UI system powers all of Cognotik's interactive features. Explore the source code or dive into the documentation.