Skip to content

Commit 71d1fd3

Browse files
jkyberneeesclaude
andcommitted
feat: sessions, models, cancel, sandbox UX + full API coverage, CI/release
Surface the rest of odek serve's API in the TUI and harden the project. TUI / endpoints (all odek serve APIs now used): - /api/sessions + /api/sessions/{id}: session browser (^R) to resume, replay transcript, and delete past conversations - /api/models: model switcher (^O) - /api/cancel: Esc aborts the running turn - per-session auth tokens persisted in ~/.bodek/sessions.json so resume/ cancel/delete work across runs (internal/tokens) - sandbox status shown in the header (🛡 sandboxed / ⚠ host access) - context-aware progress messages derived from the running tool/command (🧪 running tests, 📖 reading X, 🚀 pushing) with live elapsed timer Tests & quality: - comprehensive unit + integration tests against an in-process odek serve stand-in; internal-package coverage ~99% (client 100, tui 99, tokens 98, server 95 — remainder is unreachable OS-error handling) - golangci-lint v2 config (clean), Makefile test/cover/lint targets - GitHub Actions: CI (build, vet, race tests + coverage, lint) and a GoReleaser release workflow on version tags - README updated with new features, key bindings, badges, and dev docs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012rWqu7pktbd2ejfNw3a7Vf
1 parent 7145829 commit 71d1fd3

25 files changed

Lines changed: 2802 additions & 55 deletions

.github/workflows/ci.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
build-test:
13+
name: Build & Test
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Set up Go
19+
uses: actions/setup-go@v5
20+
with:
21+
go-version-file: go.mod
22+
cache: true
23+
24+
- name: Build
25+
run: go build ./...
26+
27+
- name: Vet
28+
run: go vet ./...
29+
30+
- name: Test (race + coverage)
31+
run: go test -race -coverpkg=./internal/... -coverprofile=coverage.out ./internal/...
32+
33+
- name: Coverage summary
34+
run: go tool cover -func=coverage.out | tail -1
35+
36+
- name: Upload coverage profile
37+
uses: actions/upload-artifact@v4
38+
with:
39+
name: coverage
40+
path: coverage.out
41+
42+
lint:
43+
name: Lint
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v4
47+
48+
- name: Set up Go
49+
uses: actions/setup-go@v5
50+
with:
51+
go-version-file: go.mod
52+
cache: true
53+
54+
- name: golangci-lint
55+
uses: golangci/golangci-lint-action@v6
56+
with:
57+
version: v2.5.0

.github/workflows/release.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
goreleaser:
13+
name: GoReleaser
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Set up Go
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version-file: go.mod
24+
cache: true
25+
26+
- name: Run GoReleaser
27+
uses: goreleaser/goreleaser-action@v6
28+
with:
29+
distribution: goreleaser
30+
version: '~> v2'
31+
args: release --clean
32+
env:
33+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.golangci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: "2"
2+
3+
run:
4+
timeout: 5m
5+
6+
linters:
7+
default: standard # errcheck, govet, ineffassign, staticcheck, unused
8+
exclusions:
9+
rules:
10+
# Test helpers freely ignore errors on HTTP test handlers and Close().
11+
- path: _test\.go
12+
linters:
13+
- errcheck
14+
15+
formatters:
16+
enable:
17+
- gofmt
18+
- goimports

.goreleaser.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
version: 2
2+
3+
project_name: bodek
4+
5+
before:
6+
hooks:
7+
- go mod tidy
8+
9+
builds:
10+
- id: bodek
11+
main: ./cmd/bodek
12+
binary: bodek
13+
env:
14+
- CGO_ENABLED=0
15+
goos:
16+
- linux
17+
- darwin
18+
- windows
19+
goarch:
20+
- amd64
21+
- arm64
22+
ldflags:
23+
- -s -w
24+
25+
archives:
26+
- id: bodek
27+
formats: [tar.gz]
28+
name_template: >-
29+
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
30+
format_overrides:
31+
- goos: windows
32+
formats: [zip]
33+
34+
checksum:
35+
name_template: checksums.txt
36+
37+
snapshot:
38+
version_template: '{{ incpatch .Version }}-next'
39+
40+
changelog:
41+
sort: asc
42+
filters:
43+
exclude:
44+
- '^docs:'
45+
- '^test:'
46+
- '^ci:'
47+
- '^chore:'
48+
49+
release:
50+
draft: false
51+
prerelease: auto

Makefile

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ BINARY := bodek
22
PKG := ./cmd/bodek
33
GOBIN ?= $(shell go env GOPATH)/bin
44

5-
.PHONY: all build install run fmt vet tidy clean
5+
.PHONY: all build install run fmt vet lint test cover tidy clean
66

77
all: build
88

@@ -27,6 +27,23 @@ fmt:
2727
vet:
2828
go vet ./...
2929

30+
## lint: run golangci-lint if available
31+
lint:
32+
@if command -v golangci-lint >/dev/null 2>&1; then \
33+
golangci-lint run ./...; \
34+
else \
35+
echo "golangci-lint not installed; skipping (see https://golangci-lint.run)"; \
36+
fi
37+
38+
## test: run the race-enabled test suite
39+
test:
40+
go test -race ./...
41+
42+
## cover: print internal-package coverage
43+
cover:
44+
go test -coverpkg=./internal/... -coverprofile=coverage.out ./internal/...
45+
go tool cover -func=coverage.out | tail -1
46+
3047
## tidy: tidy module dependencies
3148
tidy:
3249
go mod tidy

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# bodek
22

3+
[![CI](https://github.com/BackendStack21/bodek/actions/workflows/ci.yml/badge.svg)](https://github.com/BackendStack21/bodek/actions/workflows/ci.yml)
4+
[![Release](https://github.com/BackendStack21/bodek/actions/workflows/release.yml/badge.svg)](https://github.com/BackendStack21/bodek/actions/workflows/release.yml)
5+
[![Go Reference](https://pkg.go.dev/badge/github.com/BackendStack21/bodek.svg)](https://pkg.go.dev/github.com/BackendStack21/bodek)
6+
[![Go Report Card](https://goreportcard.com/badge/github.com/BackendStack21/bodek)](https://goreportcard.com/report/github.com/BackendStack21/bodek)
7+
38
**A beautiful [Bubble Tea](https://github.com/charmbracelet/bubbletea) terminal interface for the [odek](https://github.com/BackendStack21/odek) agent.**
49

510
```
@@ -78,9 +83,12 @@ by `odek serve` from its usual chain — `~/.odek/config.json` → `./odek.json`
7883
|-----|--------|
7984
| `` | Send the prompt |
8085
| `@` | Open file/session reference completion (see below) |
81-
| `^J` | Insert a newline in the input |
86+
| `^R` | Browse & resume saved sessions |
87+
| `^O` | Switch the model |
8288
| `^T` | Toggle extended thinking for the next turn |
89+
| `^J` | Insert a newline in the input |
8390
| `^L` | Clear the conversation |
91+
| `Esc` | Cancel the running turn |
8492
| `PgUp` / `PgDn` / wheel | Scroll the transcript |
8593
| `^C` | Quit |
8694

@@ -123,6 +131,14 @@ When the agent requests approval for a dangerous operation, answer inline:
123131
- **Live reasoning** — the model's pre-tool thinking streams in dimmed text,
124132
with a running elapsed timer and cycling status while it works.
125133
- **`@` autocomplete** — a live, navigable popup of files and sessions.
134+
- **Context-aware progress** — while the agent works, the status badge shows
135+
what it's actually doing (`🧪 running tests`, `📖 reading client.go`,
136+
`🚀 pushing`) with a live elapsed timer.
137+
- **Session browser** (`^R`) — resume, replay, or delete past conversations.
138+
- **Model switcher** (`^O`) — change the model for the next turn.
139+
- **Cancellation** (`Esc`) — abort a running turn via odek's cancel API.
140+
- **Sandbox aware** — the header shows `🛡 sandboxed` or `⚠ host access`; pass
141+
`--sandbox` to run tool calls inside odek's Docker isolation.
126142
- **Telemetry** — session token totals and last-turn latency in the chrome.
127143
- **Fluent by default** — gradient wordmark and hairline, smooth braille
128144
spinner, smart autoscroll that never yanks you while you read history, and a
@@ -137,18 +153,35 @@ When the agent requests approval for a dangerous operation, answer inline:
137153
```bash
138154
make build # → bin/bodek
139155
make run # build and launch
156+
make test # go test -race ./...
157+
make cover # coverage report for internal packages
158+
make lint # golangci-lint (if installed)
140159
make vet
141160
make tidy
142161
```
143162

163+
Continuous integration runs build, `go vet`, `golangci-lint`, and the race-
164+
enabled test suite on every push (see [`.github/workflows`](.github/workflows)).
165+
Tagged releases (`vX.Y.Z`) are built and published automatically by
166+
[GoReleaser](https://goreleaser.com).
167+
144168
Project layout:
145169

146170
| Path | Responsibility |
147171
|------|----------------|
148172
| `cmd/bodek` | CLI entry point: flags, lifecycle, wiring |
149173
| `internal/server` | Launch / attach to `odek serve`, resolve the auth token |
150-
| `internal/client` | odek serve WebSocket protocol (transport + event decoding) |
151-
| `internal/tui` | The Bubble Tea model, update loop, and view |
174+
| `internal/client` | odek serve WebSocket protocol (transport + REST + decoding) |
175+
| `internal/tokens` | Local persistence of per-session auth tokens |
176+
| `internal/tui` | The Bubble Tea model, update loop, panels, and view |
177+
178+
### Architecture & testing
179+
180+
bodek is a pure client, so it is highly testable: the WebSocket protocol, REST
181+
endpoints, token store, and the full Bubble Tea update/view loop are exercised
182+
by unit and integration tests against an in-process `odek serve` stand-in.
183+
Internal-package statement coverage is **~99%** (client 100%, tui 99%, tokens
184+
98%, server 95% — the remainder is unreachable OS-error handling).
152185

153186
---
154187

internal/client/client.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Event struct {
3434

3535
// session
3636
SessionID string `json:"session_id"`
37+
AuthToken string `json:"auth_token"`
3738
Model string `json:"model"`
3839
Sandbox bool `json:"sandbox"`
3940

@@ -117,7 +118,7 @@ func (c *Client) Resources(query string, limit int) ([]Resource, error) {
117118
if err != nil {
118119
return nil, err
119120
}
120-
defer resp.Body.Close()
121+
defer func() { _ = resp.Body.Close() }()
121122
if resp.StatusCode != http.StatusOK {
122123
return nil, fmt.Errorf("resources: status %s", resp.Status)
123124
}
@@ -147,22 +148,32 @@ func (c *Client) readLoop() {
147148

148149
// prompt is the client→server prompt message.
149150
type prompt struct {
150-
Type string `json:"type"`
151-
Content string `json:"content"`
152-
Thinking string `json:"thinking,omitempty"`
153-
Model string `json:"model,omitempty"`
151+
Type string `json:"type"`
152+
Content string `json:"content"`
153+
Thinking string `json:"thinking,omitempty"`
154+
Model string `json:"model,omitempty"`
155+
SessionID string `json:"session_id,omitempty"`
156+
AuthToken string `json:"auth_token,omitempty"`
154157
}
155158

156-
// SendPrompt submits a task. thinking is "enabled" to force reasoning for this
157-
// turn, or "" for the server default. model switches the active model when set.
158-
// Session continuity is automatic: the server keeps one conversation per
159-
// connection.
160-
func (c *Client) SendPrompt(content, thinking, model string) error {
159+
// PromptOpts are optional parameters for a prompt turn.
160+
type PromptOpts struct {
161+
Thinking string // "enabled" to force reasoning this turn, "" for default
162+
Model string // switch the active model when set
163+
SessionID string // resume/continue a specific session
164+
AuthToken string // session-scoped token, required when SessionID is set
165+
}
166+
167+
// SendPrompt submits a task. Session continuity is automatic on a single
168+
// connection; SessionID+AuthToken resume a saved conversation.
169+
func (c *Client) SendPrompt(content string, opts PromptOpts) error {
161170
return ws.JSON.Send(c.conn, prompt{
162-
Type: "prompt",
163-
Content: content,
164-
Thinking: thinking,
165-
Model: model,
171+
Type: "prompt",
172+
Content: content,
173+
Thinking: opts.Thinking,
174+
Model: opts.Model,
175+
SessionID: opts.SessionID,
176+
AuthToken: opts.AuthToken,
166177
})
167178
}
168179

0 commit comments

Comments
 (0)