Finish settings TODO cleanup

This commit is contained in:
2026-05-18 10:05:26 +01:00
parent cadd4c8f64
commit f10598601f
8 changed files with 131 additions and 86 deletions

View File

@@ -6,6 +6,21 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- Auto-summarization settings now save as soon as a changed row is
applied, including cadence/provider/toggle changes and model edits,
without requiring a separate save step.
- Auto-summarization setting rows now visually separate grey labels
from regular-colour values.
- The active-thread summary in the tab bar is now constrained to the
active tab's width instead of spanning the whole top row.
- Sidebar summary text now wraps from the full summary text instead of
using an ellipsized single-line value.
### Fixed
- Removed the redundant "Back to Settings" row from the
Agents / Auto-summarization settings screen.
## [0.0.6] - 2026-05-15
### Changed

View File

@@ -1,61 +0,0 @@
claude + new │ Processes
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━───────│ ─────────────────────────
- abc1234 if no tag exists yet
4. Wire version into the release workflow
Update .gitea/workflows/release.yml lines 31-35 to inject the pushed tag:
go build -trimpath \
-ldflags="-s -w -X main.version=${{ github.ref_name }}" \
-o dist/patterm-${{ github.ref_name }}-linux-amd64 \
./cmd/patterm
github.ref_name is the tag name (e.g. v0.0.1) because the workflow only
triggers on tags: ['v*'].
5. Update inline doc comment
cmd/patterm/main.go header comment (lines 5-11) — add the --version form
to the usage block. SPEC.md/CLAUDE.md already use --, no change needed there.
Out of scope
- Surfacing version in MCP whoami (the hardcoded "version": "0.1.0" in
internal/mcp/protocol.go:27 is the MCP protocol version, not the patterm
binary version — leave it).
- Renaming any existing flags.
- Adding short forms like -p for --project.
Critical files
- cmd/patterm/main.go — import swap, --version wiring, version var, header comment
- cmd/patterm/debug_harness.go — import swap
- Makefile lines 38-39 — VERSION var + ldflags
- .gitea/workflows/release.yml lines 31-35 — ldflags
- go.mod / go.sum — add github.com/spf13/pflag
Verification
1. go build -o ./bin/patterm ./cmd/patterm (without Makefile) → still builds, version reports dev.
2. make patterm → ./bin/patterm --version prints patterm v0.0.1 (commit <sha>, built <date>).
3. ./bin/patterm -h → help text shows --project string and --version lines.
4. ./bin/patterm -project /tmp → pflag rejects with usage error (confirms -- is enforced).
5. ./bin/patterm --project /tmp → starts normally.
6. ./bin/patterm mcp-stdio --socket /tmp/s --identity x → existing path still works (will fail to connect, but should parse flags fine).
7. ./bin/patterm debug-harness --scenario internal/harness/scenarios/spawn_process_via_palette.json → harness still runs.
8. go test ./... and go test ./internal/harness/... — both green.
9. Push a temporary tag locally and inspect git describe output; confirm release workflow's ${{ github.ref_name }} substitution matches the tag.
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
Claude has written up a plan and is ready to execute. Would you like to proceed?
1. Yes, and use auto mode
2. Yes, manually approve edits
3. No, refine with Ultraplan on Claude Code on the web
4. Tell Claude what to change
shift+tab to approve with this feedback
ctrl-g to edit in VS Code · ~/.claude/plans/flags-in-this-project-vectorized-gosling.md
claude · you have control Ctrl-A/D · tabs · Ctrl-W/S · tree · Ctrl-K · palette

View File

@@ -505,7 +505,18 @@ func (st *uiState) dbgf(format string, args ...any) {
}
func (st *uiState) activeSummaryText(width int) string {
if width <= 0 || st.summaries == nil {
text := st.activeSummaryRaw()
if text == "" || width <= 0 {
return ""
}
if visibleLen(text) > width {
text = clipRunes(text, width-1) + "…"
}
return text
}
func (st *uiState) activeSummaryRaw() string {
if st.summaries == nil {
return ""
}
st.settingsMu.Lock()
@@ -525,9 +536,6 @@ func (st *uiState) activeSummaryText(width int) string {
if text == "" {
return ""
}
if visibleLen(text) > width {
text = clipRunes(text, width-1) + "…"
}
return text
}
@@ -1626,6 +1634,11 @@ func (st *uiState) processStdin(chunk []byte) {
adv = 1
}
i += adv
if action.kind == "settings-save" {
st.applySettingsAction(action)
st.renderPaletteLocked()
continue
}
if done {
a := action
pendingAction = &a

View File

@@ -1277,8 +1277,11 @@ func (p *paletteState) handleSettingsTextInput(chunk []byte, i int) (paletteActi
}
switch b {
case '\r', '\n':
p.applySettingsInput()
changed := p.applySettingsInput()
p.mode = paletteModeAutoSummary
if changed {
return p.settingsAction("settings-save"), false, 1
}
case 0x7f, 0x08:
if len(p.settingsInput.value) > 0 {
p.settingsInput.value = p.settingsInput.value[:len(p.settingsInput.value)-1]
@@ -1310,7 +1313,6 @@ func autoSummaryRows() []autoSummaryRow {
{key: "run_now", label: "Summarize current top-level agent now"},
{key: "save", label: "Save settings"},
{key: "cancel", label: "Cancel"},
{key: "back", label: "Back to Settings"},
}
}
@@ -1322,6 +1324,8 @@ func (p *paletteState) activateAutoSummaryRow() (paletteAction, bool, int) {
switch rows[p.cursor].key {
case "enabled":
p.settings.AutoSummary.Enabled = !p.settings.AutoSummary.Enabled
p.settings.normalize()
return p.settingsAction("settings-save"), false, 1
case "provider":
switch p.settings.AutoSummary.Provider {
case "codex":
@@ -1331,6 +1335,8 @@ func (p *paletteState) activateAutoSummaryRow() (paletteAction, bool, int) {
default:
p.settings.AutoSummary.Provider = "codex"
}
p.settings.normalize()
return p.settingsAction("settings-save"), false, 1
case "codex_model", "opencode_model", "claude_model":
provider := strings.TrimSuffix(rows[p.cursor].key, "_model")
p.settingsInput = &settingsInputForm{
@@ -1349,6 +1355,8 @@ func (p *paletteState) activateAutoSummaryRow() (paletteAction, bool, int) {
default:
p.settings.AutoSummary.Cadence = "15s"
}
p.settings.normalize()
return p.settingsAction("settings-save"), false, 1
case "test":
return p.settingsAction("settings-test"), true, 1
case "run_now":
@@ -1357,36 +1365,36 @@ func (p *paletteState) activateAutoSummaryRow() (paletteAction, bool, int) {
return p.settingsAction("settings-close"), true, 1
case "cancel":
return paletteAction{kind: "cancel"}, true, 1
case "back":
p.mode = paletteModeSettings
p.cursor = 0
p.query = nil
p.rebuildSettings()
}
p.settings.normalize()
return paletteAction{}, false, 1
}
func (p *paletteState) applySettingsInput() {
func (p *paletteState) applySettingsInput() bool {
if p.settingsInput == nil {
return
return false
}
val := strings.TrimSpace(string(p.settingsInput.value))
if val == "" {
return
return false
}
if p.settings.AutoSummary.Models == nil {
p.settings.AutoSummary.Models = defaultSummaryModels()
}
changed := false
switch p.settingsInput.field {
case "codex_model":
changed = p.settings.AutoSummary.Models["codex"] != val
p.settings.AutoSummary.Models["codex"] = val
case "opencode_model":
changed = p.settings.AutoSummary.Models["opencode"] != val
p.settings.AutoSummary.Models["opencode"] = val
case "claude_model":
changed = p.settings.AutoSummary.Models["claude"] != val
p.settings.AutoSummary.Models["claude"] = val
}
p.settings.normalize()
return changed
}
func (p *paletteState) settingsCloseAction() paletteAction {
@@ -1473,7 +1481,7 @@ func (p *paletteState) renderAutoSummary(out writeFlusher, cols, rows int) {
moveTo(&b, row, leftPad)
b.WriteString(styleBorder + "├" + strings.Repeat("─", width-2) + "┤" + styleReset)
row++
footer := styleHint + "↵ edit/toggle · cadence 15s/30s/1m · save row commits · esc cancel" + styleReset
footer := styleHint + "↵ edit/toggle · changes save when applied · esc cancel" + styleReset
if visibleLen(footer) > content {
footer = clipRunes(footer, content-1) + "…"
}
@@ -1503,7 +1511,7 @@ func (p *paletteState) autoSummaryDisplayRows() []string {
var out []string
for _, row := range autoSummaryRows() {
if v, ok := values[row.key]; ok {
out = append(out, row.label+": "+v)
out = append(out, styleHint+row.label+":"+styleReset+" "+v)
} else {
out = append(out, row.label)
}

View File

@@ -356,20 +356,71 @@ func TestAutoSummaryCadenceCyclesSoloValues(t *testing.T) {
if p.settings.AutoSummary.Cadence != "1m" {
t.Fatalf("initial cadence = %q", p.settings.AutoSummary.Cadence)
}
p.activateAutoSummaryRow()
action, done, _ := p.activateAutoSummaryRow()
if done || action.kind != "settings-save" {
t.Fatalf("first cycle action = %+v done=%v, want settings-save without close", action, done)
}
if p.settings.AutoSummary.Cadence != "15s" {
t.Fatalf("first cycle cadence = %q", p.settings.AutoSummary.Cadence)
}
p.activateAutoSummaryRow()
action, done, _ = p.activateAutoSummaryRow()
if done || action.kind != "settings-save" {
t.Fatalf("second cycle action = %+v done=%v, want settings-save without close", action, done)
}
if p.settings.AutoSummary.Cadence != "30s" {
t.Fatalf("second cycle cadence = %q", p.settings.AutoSummary.Cadence)
}
p.activateAutoSummaryRow()
action, done, _ = p.activateAutoSummaryRow()
if done || action.kind != "settings-save" {
t.Fatalf("third cycle action = %+v done=%v, want settings-save without close", action, done)
}
if p.settings.AutoSummary.Cadence != "1m" {
t.Fatalf("third cycle cadence = %q", p.settings.AutoSummary.Cadence)
}
}
func TestAutoSummaryScreenOmitsBackRow(t *testing.T) {
for _, row := range autoSummaryRows() {
if row.label == "Back to Settings" {
t.Fatal("auto-summary settings should not show Back to Settings")
}
}
}
func TestAutoSummaryValueRowsStyleLabelAndValueSeparately(t *testing.T) {
p := newPalette(nil, "", "", preset.Set{}, defaultSettings())
rows := p.autoSummaryDisplayRows()
for _, row := range rows {
if strings.Contains(row, "Cadence:") {
if !strings.HasPrefix(row, styleHint+"Cadence:"+styleReset+" ") {
t.Fatalf("cadence row styling = %q", row)
}
if strings.Contains(strings.TrimPrefix(row, styleHint+"Cadence:"+styleReset+" "), styleHint) {
t.Fatalf("cadence value should use regular text styling: %q", row)
}
return
}
}
t.Fatal("missing cadence display row")
}
func TestAutoSummaryTextInputSavesWhenSubmitted(t *testing.T) {
p := newPalette(nil, "", "", preset.Set{}, defaultSettings())
p.mode = paletteModeSettingsInput
p.settingsInput = &settingsInputForm{
title: "codex model",
field: "codex_model",
value: []rune("custom-model"),
}
action, done, _ := p.handleSettingsTextInput([]byte{'\r'}, 0)
if done || action.kind != "settings-save" {
t.Fatalf("submit action = %+v done=%v, want settings-save without close", action, done)
}
if got := p.settings.AutoSummary.modelFor("codex"); got != "custom-model" {
t.Fatalf("codex model = %q", got)
}
}
func TestPaletteFormCtrlRTogglesRelaunchFromCommandField(t *testing.T) {
p := newPalette(nil, "", "", preset.Set{})
p.mode = paletteModeSpawnForm

View File

@@ -331,7 +331,7 @@ func (st *uiState) drawSidebar() {
write(prefix + openStyle + nameCell + styleReset + suffix)
}
if summary := st.activeSummaryText(width - 4); summary != "" && row+2 <= maxRow {
if summary := st.activeSummaryRaw(); summary != "" && row+2 <= maxRow {
write("")
for _, line := range wrapSidebarSummary(summary, width-4) {
if row > maxRow {
@@ -417,7 +417,13 @@ func wrapSidebarSummary(s string, width int) []string {
out = append(out, cur)
cur = ""
}
out = append(out, clipRunes(word, width-1)+"…")
for visibleLen(word) > width {
out = append(out, clipRunes(word, width))
word = string([]rune(word)[width:])
}
if word != "" {
cur = word
}
continue
}
if cur == "" {

View File

@@ -42,8 +42,13 @@ func TestWrapSidebarSummaryKeepsWordBoundaries(t *testing.T) {
}
}
long := wrapSidebarSummary("supercalifragilistic short", 8)
if len(long) == 0 || !strings.HasSuffix(long[0], "…") {
t.Fatalf("long word should clip with ellipsis: %#v", long)
if len(long) == 0 || strings.Contains(strings.Join(long, ""), "…") {
t.Fatalf("long word should wrap without ellipsis: %#v", long)
}
for _, line := range long {
if visibleLen(line) > 8 {
t.Fatalf("line %q exceeds width", line)
}
}
}

View File

@@ -64,6 +64,7 @@ func (st *uiState) drawTabBar() {
label string
active bool
}
activeTab := -1
// Reserve space at the right edge for "+ new". If there are too
// many tabs to fit even at minTabWidth, drop tabs from the right
@@ -134,6 +135,9 @@ func (st *uiState) drawTabBar() {
label: label,
active: c.ID == focus,
})
if tabs[len(tabs)-1].active {
activeTab = len(tabs) - 1
}
col += w
}
}
@@ -195,8 +199,12 @@ func (st *uiState) drawTabBar() {
hintCol, styleBorder, strings.Repeat("─", newHintW), styleReset)
}
if summary := st.activeSummaryText(width - 2); summary != "" {
fmt.Fprintf(&b, "\x1b[2;1H %s%s%s", styleDim, summary, styleReset)
if activeTab >= 0 {
tab := tabs[activeTab]
summaryWidth := tab.width - 2
if summary := st.activeSummaryText(summaryWidth); summary != "" {
fmt.Fprintf(&b, "\x1b[2;%dH %s%s%s", tab.startCol, styleDim, summary, styleReset)
}
}
frame := b.String()