Skip to content

Commit 094f485

Browse files
halfaipgclaude
andcommitted
feat: TUI polish, first-run setup, hardening
Theme system with dark/light switching, syntax highlighting for code blocks, auto-scroll that respects manual scrolling, slash commands (/help, /clear, /copy, /theme, /model, /session, /compact, /quit), contextual key hints, tool progress counter, and notification viewport fix. Hardening: transient HTTP retry with backoff, tool call ID matching for parallel tools, symlink-safe path resolution, file permission preservation, process group kill for shell timeouts, binary file detection, subagent shell sandboxing, ANSI-safe string truncation, streaming viewport debounce, and dangerous command blocklist. First-run experience: prompts for API key interactively when OPENAI_API_KEY is not set, saves to ~/.codebase/config.json. Session restore is now opt-in via --resume flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 01cd929 commit 094f485

20 files changed

Lines changed: 1488 additions & 384 deletions

.env.example

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# ── Required ─────────────────────────────────────────────────
2+
OPENAI_API_KEY=sk-... # Any OpenAI-compatible API key
3+
4+
# ── Optional: LLM Provider ──────────────────────────────────
5+
# OPENAI_BASE_URL=https://api.openai.com/v1 # Default; change for other providers
6+
# OPENAI_MODEL=gpt-4o # Default model
7+
8+
# ── Optional: Glue Sidecar (intent routing, narration) ──────
9+
# Falls back to OPENAI_* values if not set. Point at a fast/cheap
10+
# model (Groq, local ollama, etc.) to keep agent costs separate.
11+
# GLUE_API_KEY=gsk_...
12+
# GLUE_BASE_URL=https://api.groq.com/openai/v1
13+
# GLUE_SMART_MODEL=openai/gpt-oss-20b # Intent classification, planning
14+
# GLUE_FAST_MODEL=llama-3.1-8b-instant # Narration, titles, celebrations
15+
16+
# ── Optional: Web Search ────────────────────────────────────
17+
# Auto-detects from available keys. Falls back to DuckDuckGo (no key needed).
18+
# SEARCH_PROVIDER=tavily # tavily, brave, searxng, or duckduckgo
19+
# TAVILY_API_KEY=tvly-...
20+
# BRAVE_API_KEY=BSA...
21+
# SEARXNG_URL=http://localhost:8080 # Self-hosted SearXNG instance
22+
23+
# ── Optional: UI ─────────────────────────────────────────────
24+
# CODEBASE_THEME=dark # dark (default) or light
25+
# CODEBASE_NOBOOT=1 # Skip the boot animation

agent.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path/filepath"
88
"strings"
99
"sync"
10+
"time"
1011
)
1112

1213
// ──────────────────────────────────────────────────────────────
@@ -34,6 +35,7 @@ type AgentEvent struct {
3435
Type EventType
3536
Text string // EventTextDelta
3637
Tool string // EventToolStart / EventToolResult — tool name
38+
ToolID string // EventToolStart / EventToolResult — tool call ID
3739
Args map[string]any // EventToolStart — parsed arguments
3840
Output string // EventToolResult — tool output
3941
Success bool // EventToolResult
@@ -101,7 +103,8 @@ func strPtr(s string) *string { return &s }
101103
func buildSystemPrompt(workDir string) string {
102104
var sb strings.Builder
103105
sb.WriteString(systemPrompt)
104-
sb.WriteString(fmt.Sprintf("\n\nWorking directory: %s\n", workDir))
106+
sb.WriteString(fmt.Sprintf("\n\nCurrent date: %s\n", time.Now().Format("2006-01-02")))
107+
sb.WriteString(fmt.Sprintf("Working directory: %s\n", workDir))
105108

106109
// Load project instructions if available
107110
projectInstructions := loadProjectInstructions(workDir)
@@ -267,7 +270,7 @@ func (a *Agent) Run(prompt string) {
267270
}
268271

269272
case StreamError:
270-
a.events <- AgentEvent{Type: EventError, Error: evt.Error}
273+
a.events <- AgentEvent{Type: EventError, Error: fmt.Errorf("%s", humanizeError(evt.Error))}
271274
a.events <- AgentEvent{Type: EventDone, Text: "Error occurred."}
272275
return
273276

@@ -310,7 +313,7 @@ func (a *Agent) Run(prompt string) {
310313
// Loop back for next turn
311314
}
312315

313-
a.events <- AgentEvent{Type: EventDone, Text: "Reached maximum turns."}
316+
a.events <- AgentEvent{Type: EventDone, Text: fmt.Sprintf("Reached maximum turns (%d). You can continue with a follow-up prompt.", maxTurns)}
314317
}
315318

316319
// executeToolCalls runs tool calls with parallel execution for read-only tools.
@@ -341,12 +344,15 @@ func (a *Agent) executeToolCalls(toolCalls []ToolCall, consecutiveErrors *int) {
341344

342345
for i, tc := range parallel {
343346
var argsMap map[string]any
344-
json.Unmarshal([]byte(tc.Function.Arguments), &argsMap)
347+
if err := json.Unmarshal([]byte(tc.Function.Arguments), &argsMap); err != nil {
348+
argsMap = map[string]any{"_raw": tc.Function.Arguments}
349+
}
345350

346351
a.events <- AgentEvent{
347-
Type: EventToolStart,
348-
Tool: tc.Function.Name,
349-
Args: argsMap,
352+
Type: EventToolStart,
353+
Tool: tc.Function.Name,
354+
ToolID: tc.ID,
355+
Args: argsMap,
350356
}
351357

352358
wg.Add(1)
@@ -384,6 +390,7 @@ func (a *Agent) executeToolCalls(toolCalls []ToolCall, consecutiveErrors *int) {
384390
a.events <- AgentEvent{
385391
Type: EventToolResult,
386392
Tool: r.tc.Function.Name,
393+
ToolID: r.tc.ID,
387394
Args: r.args,
388395
Output: r.output,
389396
Success: r.success,
@@ -401,12 +408,15 @@ func (a *Agent) executeToolCalls(toolCalls []ToolCall, consecutiveErrors *int) {
401408
// Run mutating tools sequentially
402409
for _, tc := range sequential {
403410
var argsMap map[string]any
404-
json.Unmarshal([]byte(tc.Function.Arguments), &argsMap)
411+
if err := json.Unmarshal([]byte(tc.Function.Arguments), &argsMap); err != nil {
412+
argsMap = map[string]any{"_raw": tc.Function.Arguments}
413+
}
405414

406415
a.events <- AgentEvent{
407-
Type: EventToolStart,
408-
Tool: tc.Function.Name,
409-
Args: argsMap,
416+
Type: EventToolStart,
417+
Tool: tc.Function.Name,
418+
ToolID: tc.ID,
419+
Args: argsMap,
410420
}
411421

412422
var output string
@@ -438,6 +448,7 @@ func (a *Agent) executeToolCalls(toolCalls []ToolCall, consecutiveErrors *int) {
438448
a.events <- AgentEvent{
439449
Type: EventToolResult,
440450
Tool: tc.Function.Name,
451+
ToolID: tc.ID,
441452
Args: argsMap,
442453
Output: output,
443454
Success: success,

app.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"os"
5+
46
tea "github.com/charmbracelet/bubbletea"
57
)
68

@@ -23,15 +25,22 @@ type appModel struct {
2325
}
2426

2527
func newAppModel(cfg *Config) appModel {
28+
startScreen := screenBoot
29+
if os.Getenv("CODEBASE_NOBOOT") != "" {
30+
startScreen = screenChat
31+
}
2632
return appModel{
27-
screen: screenBoot,
33+
screen: startScreen,
2834
boot: newBootModel(cfg),
2935
chat: newChatModel(cfg),
3036
config: cfg,
3137
}
3238
}
3339

3440
func (m appModel) Init() tea.Cmd {
41+
if m.screen == screenChat {
42+
return m.chat.Init()
43+
}
3544
return m.boot.Init()
3645
}
3746

boot.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"time"
1010

1111
tea "github.com/charmbracelet/bubbletea"
12-
"github.com/charmbracelet/lipgloss"
1312
)
1413

1514
// ──────────────────────────────────────────────────────────────
@@ -866,5 +865,3 @@ func iabs(x int) int {
866865
}
867866
return x
868867
}
869-
870-
var _ = lipgloss.NewStyle

0 commit comments

Comments
 (0)