Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@

## CD Contract

- This repository does not currently define a formal release or deployment pipeline
- If tag releases, Homebrew distribution, installers, or automated publishing are added later, update this file and the related workflows together
- Tag pushes matching `v*` trigger `.github/workflows/release.yml`, which runs GoReleaser on `ubuntu-latest`
- Release packaging publishes macOS archives plus `checksums.txt` for installer and Homebrew consumers
- Homebrew distribution is published through the separate tap repository `carlisle0615/homebrew-openagentcleaner`
- Any change to archive names, release assets, installer behavior, or tap publishing must update this file, `.goreleaser.yaml`, `docs/RELEASING.md`, and `scripts/generate-homebrew-formula.sh` together

## Runbook

Expand Down
7 changes: 4 additions & 3 deletions docs/HOMEBREW_TAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Recommended Repository

Create this repository under the same GitHub account:
Use this repository under the same GitHub account:

```text
carlisle0615/homebrew-openagentcleaner
Expand All @@ -29,7 +29,8 @@ homebrew-openagentcleaner/
After a successful release, download `checksums.txt` from the GitHub Release page and run:

```bash
./scripts/generate-homebrew-formula.sh v0.1.1 checksums.txt > Formula/oac.rb
gh release download v0.1.2 -p checksums.txt -D dist/release-artifacts
./scripts/generate-homebrew-formula.sh v0.1.2 dist/release-artifacts/checksums.txt > Formula/oac.rb
```

The generated formula will point to:
Expand All @@ -45,7 +46,7 @@ Inside the tap repository:
mkdir -p Formula
cp /path/to/generated/oac.rb Formula/oac.rb
git add Formula/oac.rb
git commit -m "feat: add oac v0.1.1 formula"
git commit -m "feat: add oac v0.1.2 formula"
git push origin main
```

Expand Down
20 changes: 14 additions & 6 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# Releasing

This project ships macOS release archives through GitHub Releases.
This project ships macOS release archives through GitHub Releases and publishes a matching Homebrew formula through a separate tap repository.

## Tag a Release

1. Merge the release-ready branch into `main`.
2. Create and push a new semantic version tag.

```bash
git tag v0.1.0
git push origin v0.1.0
git checkout main
git pull --ff-only
git tag v0.1.2
git push origin v0.1.2
```

The release workflow will build:
Expand All @@ -15,6 +20,8 @@ The release workflow will build:
- `oac_<version>_darwin_amd64.tar.gz`
- `checksums.txt`

The workflow definition lives in `.github/workflows/release.yml` and runs on `ubuntu-latest`. Because `.goreleaser.yaml` executes `go test ./...` in a `before.hooks` step, all release-gating tests must be portable to Linux even if the shipped binaries are macOS-only.

## One-Line Install

Users can install the latest release with:
Expand Down Expand Up @@ -42,13 +49,14 @@ That command expects a tap repository named `homebrew-openagentcleaner` under th
Generate the formula after a release:

```bash
./scripts/generate-homebrew-formula.sh v0.1.0 dist/checksums.txt > Formula/oac.rb
gh release download v0.1.2 -p checksums.txt -D dist/release-artifacts
./scripts/generate-homebrew-formula.sh v0.1.2 dist/release-artifacts/checksums.txt > Formula/oac.rb
```

Then commit `Formula/oac.rb` into the tap repository.
Then commit `Formula/oac.rb` into the tap repository `carlisle0615/homebrew-openagentcleaner`.

## Notes

- The current installer supports macOS only.
- The current Homebrew packaging plan targets a separate tap repo, not this source repository.
- The Homebrew formula should be updated only after the GitHub Release assets are visible and checksums are final.
- `oac version` reads the release version injected at build time.
1 change: 1 addition & 0 deletions docs/current-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Last updated: 2026-03-09
| Add `GEMINI.md` symlink | Completed (verified) | `docs/current-plans/gemini-md-symlink.md` |
| Collaboration doc baseline migration | Completed (verified) | `docs/current-plans/context-doc-bootstrap.md` |
| Refactor Analyze TUI | Completed (verified) | `docs/current-plans/refactor-analyze-tui.md` |
| Refresh release and Homebrew tap | In progress | `docs/current-plans/release-brew-refresh.md` |

## Notes

Expand Down
26 changes: 26 additions & 0 deletions docs/current-plans/release-brew-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Release And Homebrew Refresh

## Goal

Push the current staged repository changes through the protected-branch workflow, cut a new GitHub Release from a green `main`, and update the Homebrew tap formula to the newly published version.

## Touched Files

- `docs/current-plan.md`
- `docs/current-plans/release-brew-refresh.md`
- Release-related code or tests only if required to unblock CI
- Homebrew formula files in this repo or `carlisle0615/homebrew-openagentcleaner`

## Validation Steps

1. Run `make fmt`.
2. Run `make build`.
3. Run `make test`.
4. Confirm the release workflow succeeds for the new tag.
5. Confirm the Homebrew formula points to the new tag and checksum.

## Doc-Sync Steps

- Keep `docs/current-plan.md` aligned with task status.
- Update `docs/repo-map.md` or `AGENTS.md` only if release or tap workflow changes materially.
- Add or update a handoff note under `docs/handoffs/` if the release/tap work leaves follow-up items.
2 changes: 2 additions & 0 deletions docs/handoffs/refactor-analyze-tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
- 对过长的 Session Label 执行了截断 `trimForDisplay(str, 22)` 处理,以此消灭了因终端窄宽或长 ID 名字引发的布局彻底崩散(挤出多行的 Bug)。
- 所有 List 使用 `❯` 作为激活高亮,仅改变前景强调色,消除长短不一的色块。
- Header 与 Footer 转变为简单统一的暗色提示序列和面包屑,整体对齐在代码或详情上。
- **对话详情预览层(Session Preview)**:
- 加载了 `openclaw_sessions.go` 解析 `.jsonl` 时抽取 `message` 内容的提取逻辑(`previewOpenClawSession`),支持在列表按 `Enter` 切入完整的对话预览页面查阅最近几条的互动内容,防止误删会话。

## 验证结论

Expand Down
2 changes: 1 addition & 1 deletion docs/repo-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Last updated: 2026-03-09
- `manual` is the last safety boundary and should not be downgraded just because a path looks cache-like
- The non-interactive behavior in `cleanReport` depends on `--yes` / `--dry-run`; this is a core agent-mode guardrail
- `openclaw_sessions.go` edits `sessions.json` after transcript deletions; mismatches there can create orphaned metadata or confusing UX
- CI runs on `macos-latest`, but `go test ./...` should remain independent from local machine state whenever possible
- CI runs on `macos-latest`, while the release workflow runs GoReleaser plus `go test ./...` on `ubuntu-latest`; tests must stay cross-platform unless they are explicitly guarded
- The release installer and Homebrew formula generator assume archive names shaped like `oac_<version>_darwin_<arch>.tar.gz`
- `gofmt -w ... && git diff --exit-code` modifies files before validating cleanliness, so any format target changes must stay in sync across CI and `Makefile`

Expand Down
62 changes: 60 additions & 2 deletions internal/cleaner/analyze_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
screenAssistants analyzeScreen = iota
screenOpenClawMenu
screenSessions
screenSessionPreview
screenCandidates
)

Expand Down Expand Up @@ -84,6 +85,7 @@ type analyzeModel struct {
confirmItem Candidate
status string
lastErr error
previewText string
}

func runAnalyzeTUI(assistants []string, before time.Time, stdout, stderr io.Writer) error {
Expand Down Expand Up @@ -373,6 +375,18 @@ func (m *analyzeModel) activateSelection() error {
}
m.screen = screenCandidates
return m.reloadCandidates("openclaw")
case screenSessions:
if len(m.sessions) == 0 {
return nil
}
session := m.sessions[m.sessionIndex]
preview, err := previewOpenClawSession(session.TranscriptPath)
if err != nil {
return err
}
m.previewText = preview
m.screen = screenSessionPreview
return nil
}
return nil
}
Expand Down Expand Up @@ -460,6 +474,9 @@ func (m *analyzeModel) clearConfirm() {

func (m *analyzeModel) navigateBack() {
switch m.screen {
case screenSessionPreview:
m.screen = screenSessions
return
case screenCandidates:
if m.activeAssistant == "openclaw" {
m.screen = screenOpenClawMenu
Expand Down Expand Up @@ -601,14 +618,17 @@ func (m analyzeModel) View() string {
func (m analyzeModel) renderHeader() string {
breadcrumbs := []string{"Analyze"}
switch m.screen {
case screenOpenClawMenu, screenSessions, screenCandidates:
case screenOpenClawMenu, screenSessions, screenSessionPreview, screenCandidates:
if m.activeAssistant != "" {
breadcrumbs = append(breadcrumbs, displayAssistant(m.activeAssistant))
}
}
if m.screen == screenSessions {
if m.screen == screenSessions || m.screen == screenSessionPreview {
breadcrumbs = append(breadcrumbs, "Conversations")
}
if m.screen == screenSessionPreview {
breadcrumbs = append(breadcrumbs, "Preview")
}
if m.screen == screenCandidates {
breadcrumbs = append(breadcrumbs, "Items")
}
Expand Down Expand Up @@ -644,6 +664,8 @@ func (m analyzeModel) renderBody() (string, string) {
return m.renderOpenClawMenu(), m.renderOpenClawDetail()
case screenSessions:
return m.renderSessionsList(), m.renderSessionDetail()
case screenSessionPreview:
return m.renderSessionsList(), m.renderSessionPreview()
case screenCandidates:
return m.renderCandidateList(), m.renderCandidateDetail()
default:
Expand All @@ -654,6 +676,8 @@ func (m analyzeModel) renderBody() (string, string) {
func (m analyzeModel) renderFooter() string {
keys := []string{"↑/↓ Navigate", "Enter Select", "q Back"}
switch m.screen {
case screenSessionPreview:
keys = []string{"q Back to conversations"}
case screenSessions:
keys = append(keys, "d Delete item", "f Filter date", "x Bulk delete", "c Clear filter")
case screenCandidates:
Expand Down Expand Up @@ -785,6 +809,40 @@ func (m analyzeModel) renderSessionDetail() string {
return strings.Join(lines, "\n")
}

func (m analyzeModel) renderSessionPreview() string {
if m.previewText == "" {
return m.styles.muted.Render("Unable to load preview data.")
}

lines := []string{
m.styles.header.Render("Conversation Preview"),
"",
}

// Truncate the preview text to fit reasonably well
previewLines := strings.Split(m.previewText, "\n")
maxLines := m.height - 12 // Leave space for headers, footers
if maxLines < 10 {
maxLines = 10
}

if len(previewLines) > maxLines {
// Just take the tail part
lines = append(lines, m.styles.muted.Render(fmt.Sprintf("... (%d truncated lines) ...", len(previewLines)-maxLines)))
previewLines = previewLines[len(previewLines)-maxLines:]
}

for _, l := range previewLines {
if strings.HasPrefix(l, "== User ==") || strings.HasPrefix(l, "== Assistant ==") || strings.HasPrefix(l, "== System ==") {
lines = append(lines, m.styles.accent.Render(strings.TrimSpace(l)))
} else {
lines = append(lines, l)
}
}

return strings.Join(lines, "\n")
}

func (m analyzeModel) renderCandidateList() string {
lines := []string{m.styles.header.Render(displayAssistant(m.activeAssistant) + " items"), ""}
if len(m.candidates) == 0 {
Expand Down
5 changes: 5 additions & 0 deletions internal/cleaner/analyze_tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cleaner

import (
"path/filepath"
"runtime"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -391,6 +392,10 @@ func TestAnalyzeRenderingHelpers(t *testing.T) {
}

func TestRunAnalyzeTUIQuit(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("Bubble Tea quit smoke test is only reliable on macOS in this project")
}

home := setTestHome(t)
writeTestFile(t, filepath.Join(home, ".ollama", "logs", "server.log"), "log")

Expand Down
69 changes: 69 additions & 0 deletions internal/cleaner/openclaw_sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cleaner
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -48,6 +49,18 @@ type openClawSessionEvent struct {
Timestamp string `json:"timestamp"`
}

type openClawMessageEvent struct {
Type string `json:"type"`
Message struct {
Role string `json:"role"`
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
ErrorMessage string `json:"errorMessage,omitempty"`
} `json:"message"`
}

func discoverOpenClawSessions() ([]OpenClawSession, error) {
home, err := os.UserHomeDir()
if err != nil {
Expand Down Expand Up @@ -181,6 +194,62 @@ func scanOpenClawTranscript(path string) (time.Time, int, error) {
return startedAt, lines, nil
}

func previewOpenClawSession(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()

scanner := bufio.NewScanner(file)
buffer := make([]byte, 0, 64*1024)
scanner.Buffer(buffer, 2*1024*1024)

var messages []string
for scanner.Scan() {
var event openClawMessageEvent
if err := json.Unmarshal(scanner.Bytes(), &event); err == nil && event.Type == "message" {
role := event.Message.Role
if role == "" {
continue
}

var textParts []string
if event.Message.ErrorMessage != "" {
textParts = append(textParts, "[Error] "+event.Message.ErrorMessage)
}
for _, c := range event.Message.Content {
if c.Type == "text" && strings.TrimSpace(c.Text) != "" {
textParts = append(textParts, strings.TrimSpace(c.Text))
}
}

content := strings.Join(textParts, "\n")
if content != "" {
prefix := "User"
if role == "assistant" {
prefix = "Assistant"
} else if role == "system" {
prefix = "System"
}
messages = append(messages, fmt.Sprintf("== %s ==\n%s", prefix, content))
}
}
}
if err := scanner.Err(); err != nil {
return "", err
}
if len(messages) == 0 {
return "No text messages found in this conversation.", nil
}

// Keep only the last 4 messages to avoid overwhelming the terminal
if len(messages) > 4 {
messages = messages[len(messages)-4:]
}
return strings.Join(messages, "\n\n"), nil
}

func deleteOpenClawSessions(sessions []OpenClawSession) error {
type sessionKey struct {
Key string
Expand Down