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
21 changes: 21 additions & 0 deletions .cursor/rules/caveman.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
description: "Caveman mode — terse communication, ~75% fewer tokens, full technical accuracy"
alwaysApply: true
---

Respond terse like smart caveman. All technical substance stay. Only fluff die.

Rules:

- Drop: articles (a/an/the), filler (just/really/basically), pleasantries, hedging
- Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
- Pattern: [thing] [action] [reason]. [next step].
- Not: "Sure! I'd be happy to help you with that."
- Yes: "Bug in auth middleware. Fix:"

Switch level: /caveman lite|full|ultra|wenyan
Stop: "stop caveman" or "normal mode"

Auto-Clarity: drop caveman for security warnings, irreversible actions, user confused. Resume after.

Boundaries: code/commits/PRs written normal.
72 changes: 72 additions & 0 deletions .cursor/skills/caveman/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
wenyan-lite, wenyan-full, wenyan-ultra.
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
---

Respond terse like smart caveman. All technical substance stay. Only fluff die.

## Persistence

ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".

Default: **full**. Switch: `/caveman lite|full|ultra`.

## Rules

Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.

Pattern: `[thing] [action] [reason]. [next step].`

Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"

## Intensity

| Level | What change |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |

Example — "Why React component re-render?"

- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"

Example — "Explain database connection pooling."

- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"

## Auto-Clarity

Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.

Example — destructive op:

> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
>
> ```sql
> DROP TABLE users;
> ```
>
> Caveman resume. Verify backup exist first.

## Boundaries

Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ repos:
- owner/repo-b
poll_interval_seconds: 20
group_by_repo: false
dashboard_view: auto # auto, full, compact
theme:
selected: auto # auto, neon-dark, neon-light, or a user theme id
directory: ~/.config/github-butler/themes
Expand All @@ -119,6 +120,7 @@ theme:
| `repos` | list | `[]` | List of `owner/repo` slugs to track |
| `poll_interval_seconds` | int | `20` | How often the app polls GitHub (min `2`, max `3600`) |
| `group_by_repo` | bool | `false` | When `true`, PRs are grouped under per-repo section headers instead of one flat updated list |
| `dashboard_view` | str | `auto` | Dashboard layout: `auto`, `full`, or `compact` |
| `theme.selected` | str | `auto` | Theme id to use (`auto`, `neon-dark`, `neon-light`, or a user theme filename without suffix) |
| `theme.directory` | str | XDG dir | Directory scanned for `*.theme.yaml` user themes |
| `theme.colors` | map | `{}` | Optional config-level color overrides applied after the selected theme |
Expand All @@ -127,6 +129,12 @@ Repositories and settings are editable from inside the app (`m` → **Settings**

On first launch with no config, the app opens to an empty dashboard; press `m` → Repositories → `a` to add one.

### Dashboard layouts

`dashboard_view: auto` keeps the full table on wide terminals and switches to a compact reduced-column table on narrower screens or split panes. Use **Settings** → **Dashboard view** to pick `auto`, `full`, or `compact`.

Compact mode stays one row per PR and keeps only `REPO`, `#`, `TITLE`, `TAGS`, and a dense `STATUS` summary. `STATUS` combines CI (`PASS`, `FAIL×2`, `RUN×1`), review/required state (`OK`, `CHG`, `REQ 1/2`, `PEND`), and unresolved threads (`UNRES 3` when non-zero).

### Themes

`auto` uses Lipgloss/termenv to detect whether your terminal background is dark or light, then chooses the built-in `neon-dark` or `neon-light` theme. Detection depends on terminal support, so you can force either built-in theme from **Settings** → **Theme**.
Expand Down
6 changes: 6 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ const (
DefaultPollInterval = 20 * time.Second
MinPollIntervalSeconds = 2
MaxPollIntervalSeconds = 3600

DashboardViewAuto = "auto"
DashboardViewFull = "full"
DashboardViewCompact = "compact"
)

// Config is the user-facing configuration persisted as YAML.
type Config struct {
Repos []string `yaml:"repos"`
PollIntervalSeconds int `yaml:"poll_interval_seconds"`
GroupByRepo bool `yaml:"group_by_repo"`
DashboardView string `yaml:"dashboard_view"`
Theme Theme `yaml:"theme"`

// Path is the file this config was loaded from (or should be saved to).
Expand Down Expand Up @@ -50,6 +55,7 @@ func Default() Config {
return Config{
Repos: []string{},
PollIntervalSeconds: int(DefaultPollInterval / time.Second),
DashboardView: DashboardViewAuto,
Theme: Theme{
Selected: "auto",
Directory: themeDir,
Expand Down
22 changes: 22 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,34 @@ func TestLoadDefaultsThemeConfig(t *testing.T) {
if cfg.Theme.Selected != "auto" {
t.Fatalf("Theme.Selected = %q, want auto", cfg.Theme.Selected)
}
if cfg.DashboardView != DashboardViewAuto {
t.Fatalf("DashboardView = %q, want auto", cfg.DashboardView)
}
wantDir := filepath.Join(xdg, "github-butler", "themes")
if cfg.Theme.Directory != wantDir {
t.Fatalf("Theme.Directory = %q, want %q", cfg.Theme.Directory, wantDir)
}
}

func TestValidateDashboardView(t *testing.T) {
cfg := Default()
for _, view := range []string{DashboardViewAuto, DashboardViewFull, DashboardViewCompact} {
cfg.DashboardView = view
if err := Validate(cfg); err != nil {
t.Fatalf("Validate(%q) error: %v", view, err)
}
}

cfg.DashboardView = "tiny"
err := Validate(cfg)
if err == nil {
t.Fatal("Validate succeeded, want error")
}
if !strings.Contains(err.Error(), "dashboard_view") {
t.Fatalf("Validate error = %q, want dashboard_view path", err)
}
}

func TestValidateThemeColors(t *testing.T) {
cfg := Default()
cfg.Theme.Colors = map[string]string{"accent": "not-a-color"}
Expand Down
3 changes: 3 additions & 0 deletions internal/config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ func Load(path string) (Config, error) {
if cfg.Repos == nil {
cfg.Repos = []string{}
}
if cfg.DashboardView == "" {
cfg.DashboardView = DashboardViewAuto
}
if cfg.Theme.Selected == "" {
cfg.Theme.Selected = "auto"
}
Expand Down
6 changes: 6 additions & 0 deletions internal/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ func Validate(cfg Config) error {
}
seen[key] = struct{}{}
}
switch cfg.DashboardView {
case "", DashboardViewAuto, DashboardViewFull, DashboardViewCompact:
default:
return fmt.Errorf("dashboard_view must be one of %q, %q, or %q (got %q)",
DashboardViewAuto, DashboardViewFull, DashboardViewCompact, cfg.DashboardView)
}
if strings.TrimSpace(cfg.Theme.Selected) == "" {
return fmt.Errorf("theme.selected must not be empty")
}
Expand Down
6 changes: 5 additions & 1 deletion internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
screenConfirmRemove
screenSettings
screenEditInterval
screenDashboardViews
screenThemes
)

Expand Down Expand Up @@ -50,6 +51,7 @@ type Model struct {
menuCursor int
reposCursor int
settingsCursor int
viewCursor int
themeCursor int
themeChoices []theme.Choice

Expand Down Expand Up @@ -182,6 +184,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateSettings(msg)
case screenEditInterval:
return m.updateEditInterval(msg)
case screenDashboardViews:
return m.updateDashboardViews(msg)
case screenThemes:
return m.updateThemes(msg)
}
Expand All @@ -202,7 +206,7 @@ func (m Model) View() string {
body = m.viewMenu()
case screenRepos, screenAddRepo, screenConfirmRemove:
body = m.viewRepos()
case screenSettings, screenEditInterval, screenThemes:
case screenSettings, screenEditInterval, screenDashboardViews, screenThemes:
body = m.viewSettings()
}

Expand Down
Loading
Loading