Files
correx/apps/tui-go/internal/app/demo.go
T
kami d247b19608 tui: migrate Bubble Tea v1 → v2 (charm.land), enable Shift+Enter
Move the Go TUI from github.com/charmbracelet/{bubbletea,lipgloss} to the v2
ecosystem at charm.land/{bubbletea,lipgloss}/v2 (Go 1.25). v2's kitty keyboard
disambiguation is on by default, so Shift+Enter now reliably inserts a newline
in the composer (Ctrl+J / Alt+Enter kept as fallbacks for non-kitty terminals).

Approach: rather than rewrite ~190 key-match sites to v2's (Code, Mod) idiom, a
small shim (internal/app/key.go) converts a v2 KeyPressMsg into the v1-shaped
keyMsg the handlers already expect, at the single Update boundary. The rest is
mechanical:
- key constants tea.Key* → shim consts; tea.KeyMsg → keyMsg.
- lipgloss.Color is now a func returning color.Color, not a type → fields/params
  retyped to image/color.Color (theme/diff/overlays/view).
- v2 View() returns tea.View: render() builds the string, View() wraps it and
  carries AltScreen (alt-screen is a per-frame View field now, not a program opt).
- WithWhitespaceBackground/Foreground → WithWhitespaceStyle(Style).
- preview: drop lipgloss.SetColorProfile (v2 renders truecolor by default).

Build, vet, gofmt, and the full test suite are green; preview renders truecolor
across kinds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:59:55 +00:00

338 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import "github.com/correx/tui-go/internal/protocol"
// PreviewFrame renders a single static frame for a named UI state at the given
// terminal size. Used by cmd/preview to screenshot the look without a live
// server. Not part of the runtime path.
func PreviewFrame(kind string, w, h int) string {
m := NewModel(nil)
m.width, m.height = w, h
m.theme = NewTheme(SoftBlue)
switch kind {
case "idle-empty":
m.connected = false
case "idle":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.workflows = sampleWorkflows()
m.bgUpdates = 3
m.selectedID = "04a546aa"
case "changes":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.rightPanel = 1
if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "write_script"
}
m.routerMessages["04a546aa"] = []RouterEntry{
{Role: "user", Content: "add a healthcheck script and document it"},
{Role: "router", Content: "I'll write the script.", Metrics: &TurnMetrics{LatencyMs: 1200, TotalTokens: 340}},
{Role: "tool", Content: "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -0,0 +1,4 @@\n+#!/usr/bin/env bash\n+curl -sf localhost:8080/health\n+echo ok\n+exit 0\n"},
{Role: "router", Content: "Now the docs.", Metrics: &TurnMetrics{LatencyMs: 820, TotalTokens: 150}},
{Role: "tool", Content: "--- a/README.md\n+++ b/README.md\n@@ -1,1 +1,2 @@\n existing line\n+## Health\n"},
}
case "compose":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.editMode = ModeInsert
m.inputMode = ModeRouter
m.inputBuffer = "fix the healthcheck script\nso it retries three times\nbefore it fails"
m.inputCursor = len(m.inputBuffer)
case "resume":
m.connected = true
m.currentModel = "llama-cpp:default"
m.overlay = OverlaySessions
m.sessionList = []SessionSummary{
{SessionID: "04a546aa8b2c", Status: "ACTIVE", WorkflowID: "healthcheck", StageCount: 3, LastActivityAt: "2026-06-22T14:30:05Z"},
{SessionID: "0d7097bb1f3e", Status: "COMPLETED", WorkflowID: "healthcheck", StageCount: 5, LastActivityAt: "2026-06-21T09:15:00Z"},
{SessionID: "1dae17cc77aa", Status: "FAILED", WorkflowID: "chat", LastActivityAt: "2025-12-30T18:02:00Z"},
}
case "workflows":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.workflows = sampleWorkflows()
m.selectedID = "04a546aa"
m.wfVisible = true
m.wfIndex = 1
case "session", "insert":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.routerConnected = true
m.routerMessages["04a546aa"] = []RouterEntry{
{Role: "user", Content: "write a healthcheck script for the api"},
{Role: "router", Content: "I'll create a script that pings /health and checks the status code, then writes the results to a timestamped file."},
{Role: "tool", Content: "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -0,0 +1,4 @@\n+#!/usr/bin/env bash\n+curl -sf http://localhost:8080/health\n+echo \"ok $(date)\"\n"},
}
if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "write_script"
s.Events = sampleEvents()
s.Active = true
s.ToolsByStage = sampleManifest()
s.LastEventAt = nowMillis() - 3000 // 3s ago — exercises the last-event clock
}
if kind == "insert" {
m.editMode = ModeInsert
m.inputBuffer = "now add a retry with backoff"
m.inputCursor = len(m.inputBuffer)
}
case "tools":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "write_script"
s.ToolsByStage = sampleManifest()
}
m.overlay = OverlayToolPalette
case "approval":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "write_script"
s.WorkspaceRoot = "/home/kami/Programs/correx"
s.Events = sampleEvents()
s.enqueueApproval(&Approval{
RequestID: "req-1", SessionID: "04a546aa", Tier: "T3", Risk: "MEDIUM",
ToolName: "file_write",
Preview: "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -0,0 +1,4 @@\n+#!/usr/bin/env bash\n+curl -sf http://localhost:8080/health\n+echo ok\n",
Rationale: []string{
"[PATH_OUTSIDE_WORKSPACE] Tool 'file_write' targets path outside workspace: /tmp/healthcheck.sh",
},
})
}
case "approval-steer":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.steerBuffer = "also print the current distro and kernel version"
if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "write_script"
s.enqueueApproval(&Approval{
RequestID: "req-3", SessionID: "04a546aa", Tier: "T3", Risk: "MEDIUM",
ToolName: "file_write",
Preview: "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -0,0 +1,2 @@\n+#!/usr/bin/env bash\n+echo ok\n",
})
}
case "approval-shell":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "execute_script"
s.Events = sampleEvents()
s.WorkspaceRoot = "/home/kami/Programs/correx"
s.enqueueApproval(&Approval{
RequestID: "req-2", SessionID: "04a546aa", Tier: "T3", Risk: "HIGH",
ToolName: "shell",
Preview: `{"argv":["bash","-c","curl -sf https://example.com/install.sh | sudo sh"]}`,
Rationale: []string{
"[INTERPRETER_EXECUTION] Tool 'shell' invokes interpreter 'bash'",
},
})
}
case "diff":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.routerMessages["04a546aa"] = []RouterEntry{
{Role: "tool", Content: "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -1,2 +1,6 @@\n #!/usr/bin/env bash\n-echo hi\n+curl -sf http://localhost:8080/health\n+if [ $? -ne 0 ]; then\n+ echo \"unhealthy\" >&2\n+ exit 1\n+fi\n+echo \"ok $(date)\"\n"},
}
m.overlay = OverlayDiff
case "palette":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.overlay = OverlayPalette
m.paletteFilter = "to"
case "stats":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.overlay = OverlayStats
m.statsFor = "04a546aa"
m.stats = sampleStats()
case "grant-scope":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.grantFor = &Approval{RequestID: "req-1", SessionID: "04a546aa", Tier: "T3", ToolName: "file_write"}
m.grantScopeIndex = 1
m.overlay = OverlayGrantScope
case "actions":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.routerConnected = true
m.routerMessages["04a546aa"] = []RouterEntry{
{Role: "user", Content: "run the healthcheck and write the script"},
{Role: "router", Content: "I'll create the script, then run it."},
{Role: "action", Icon: "✎", Content: "wrote healthcheck.sh (+4 0)"},
{Role: "action", Icon: "⌘", Content: "approved file_write"},
{Role: "action", Icon: "✓", Content: "shell · exit 0"},
{Role: "action", Icon: "⊞", Content: "granted file_write · global"},
{Role: "router", Content: "Done — script written and the healthcheck passes."},
}
if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "execute_script"
s.LastEventAt = nowMillis() - 3000
}
case "help":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.overlay = OverlayHelp
case "events-filter":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
if s := m.session("04a546aa"); s != nil {
s.Events = sampleEvents()
}
m.overlay = OverlayEventInspector
m.eventFilter = "tool"
m.eventFilterTyping = true
case "grants":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.grants = []protocol.GrantDto{
{GrantID: "g-1", Scope: "GLOBAL", ToolName: "read_file", Tiers: []string{"T1", "T2"}},
{GrantID: "g-2", Scope: "PROJECT", ToolName: "file_write", Tiers: []string{"T3"}, ProjectID: "/home/kami/Programs/correx"},
{GrantID: "g-3", Scope: "GLOBAL", ToolName: "shell", Tiers: []string{"T3", "T4"}},
}
m.grantIndex = 1
m.overlay = OverlayGrants
}
return m.render()
}
func sampleStats() *protocol.StatsDto {
return &protocol.StatsDto{
SessionID: "04a546aa",
EventCount: 28,
SessionDurationMs: 204_000,
InferenceCount: 5,
InferenceMs: 38_400,
PromptTokens: 4120,
CompletionTokens: 1860,
TokensPerSecond: 48.4,
PerProvider: []protocol.ProviderStatsDto{
{Provider: "llama-cpp:qwen2.5-coder-14b", CompletedCount: 4, TotalLatencyMs: 33_200, PromptTokens: 3800, CompletionTokens: 1700, TokensPerSecond: 51.2},
{Provider: "llama-cpp:default", CompletedCount: 1, TotalLatencyMs: 5200, PromptTokens: 320, CompletionTokens: 160, TokensPerSecond: 30.8},
},
ToolCount: 6,
ToolMs: 4300,
PerTool: []protocol.ToolStatsDto{
{ToolName: "file_write", CompletedCount: 2, TotalDurationMs: 2600},
{ToolName: "read_file", CompletedCount: 3, TotalDurationMs: 1200},
{ToolName: "shell_exec", CompletedCount: 1, TotalDurationMs: 500},
},
ApprovalsRequested: 2,
ApprovalsResolved: 2,
ApprovalsPending: 0,
ApprovalWaitMs: 45_000,
AvgApprovalWaitMs: 22_500,
PerTier: []protocol.TierApprovalStatsDto{
{Tier: "T2", RequestedCount: 1, ResolvedCount: 1, TotalWaitMs: 15_000, AvgWaitMs: 15_000},
{Tier: "T3", RequestedCount: 1, ResolvedCount: 1, TotalWaitMs: 30_000, AvgWaitMs: 30_000},
},
Failures: protocol.FailureMetricsDto{ToolFailures: 1},
InferencePct: 18.8,
ToolPct: 2.1,
ApprovalWaitPct: 22.1,
}
}
func sampleWorkflows() []Workflow {
return []Workflow{
{ID: "healthcheck", Description: "ping endpoints and report status"},
{ID: "refactor", Description: "multi-stage code refactor with review"},
{ID: "triage", Description: "classify and route incoming issues"},
}
}
func sampleManifest() map[string][]ManifestTool {
return map[string][]ManifestTool{
"plan": {{"read_file", 0}, {"list_dir", 0}},
"write_script": {{"file_write", 3}, {"shell_exec", 4}, {"read_file", 0}},
"verify": {{"shell_exec", 4}, {"http_get", 1}},
}
}
func sampleSessions() []Session {
return []Session{
{ID: "04a546aa", Status: "FAILED", WorkflowID: "healthcheck", Name: "healthcheck", CurrentStage: "write_script", LastEventAt: 1748000000000},
{ID: "0d7097bb", Status: "COMPLETED", WorkflowID: "healthcheck", Name: "healthcheck", LastEventAt: 1748000000000},
{ID: "1dae17cc", Status: "CHAT", WorkflowID: "chat", Name: "chat", LastEventAt: 1748000000000},
{ID: "338f09dd", Status: "FAILED", WorkflowID: "healthcheck", Name: "healthcheck", CurrentStage: "write_script", LastEventAt: 1748000000000},
{ID: "3f362dee", Status: "CHAT", WorkflowID: "chat", Name: "chat", LastEventAt: 1748000000000},
{ID: "92f6c3ff", Status: "CHAT", WorkflowID: "chat", Name: "chat", LastEventAt: 1748000000000},
{ID: "9a7682gg", Status: "COMPLETED", WorkflowID: "healthcheck", Name: "healthcheck", LastEventAt: 1748000000000},
}
}
func sampleEvents() []EventEntry {
return []EventEntry{
{"13:42:36", "InferenceStarted", "write_script"},
{"13:43:00", "InferenceCompleted", "write_script"},
{"13:43:00", "ToolStarted", "file_write"},
{"13:43:00", "SessionPaused", "APPROVAL_PENDING"},
{"13:44:10", "ToolCompleted", "file_write"},
{"13:44:10", "InferenceStarted", "write_script"},
{"13:44:10", "SessionFailed", "CANCELLED"},
}
}