feat(tui-go): inline action rows in the OUTPUT transcript

Surface side-effecting ("external feedback") events in the conversation
flow, opencode-style, instead of only in the EVENTS side log.

- New RouterEntry role "action" (glyph + text) appended at arrival so it
  interleaves with chat turns by order. Rendered dim with an accent gutter
  glyph; the EVENTS panel still carries the full stream.
- Surfaced: tool start (⏵), tool done (✓ / ✎ wrote <path> (+a −b) for a
  diff), tool failed (✗), rejected (✕ blocked), approval resolved
  (⌘ approved / ✕ rejected, noting "· via grant" on a grant auto-approve),
  and grant create/revoke (⊞ / ⊟ · scope). ApprovalResolved names no tool
  on the wire, so the tool is recovered from the pending queue before the
  gate is dropped.
- Toggle: "inline actions" palette command flips actionsHidden (rows are
  always recorded, gated at render, so toggling reveals history); persisted
  in tui-prefs.json. prefs.go refactored to load/mutate/save so the new
  field and statusbarHidden no longer clobber each other.
- demo.go: "actions" preview kind.

go build / vet / test green; rendered the flow via cmd/preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 23:00:18 +00:00
parent 3f4c45a7b0
commit 6a06f2ead5
6 changed files with 191 additions and 27 deletions
+44 -25
View File
@@ -9,7 +9,8 @@ import (
// prefs is the TUI's local, persisted preferences (display-only client state — kept out
// of the server config, which is shared/replayed). Stored as JSON in the correx config dir.
type prefs struct {
StatusbarHidden []string `json:"statusbarHidden"`
StatusbarHidden []string `json:"statusbarHidden"`
InlineActionsHidden bool `json:"inlineActionsHidden"`
}
// statusSegment is one toggleable status-bar segment. The always-on segments (correx label,
@@ -37,44 +38,62 @@ func prefsPath() string {
return filepath.Join(dir, "tui-prefs.json")
}
// loadStatusbarHidden reads the persisted hidden-segment set (best-effort: a missing or
// corrupt file yields an empty set, i.e. everything shown).
func loadStatusbarHidden() map[string]bool {
out := map[string]bool{}
// loadPrefs reads the whole prefs file (best-effort: a missing or corrupt file yields the
// zero value, i.e. everything shown / actions visible).
func loadPrefs() prefs {
var p prefs
path := prefsPath()
if path == "" {
return out
return p
}
data, err := os.ReadFile(path)
if err != nil {
return out
if data, err := os.ReadFile(path); err == nil {
_ = json.Unmarshal(data, &p)
}
var p prefs
if json.Unmarshal(data, &p) == nil {
for _, id := range p.StatusbarHidden {
out[id] = true
}
}
return out
return p
}
// saveStatusbarHidden writes the hidden set back to disk (best-effort; ignores IO errors so
// a read-only home never crashes the UI). Order follows statusSegments for a stable file.
func saveStatusbarHidden(hidden map[string]bool) {
// savePrefs writes the whole prefs file (best-effort; ignores IO errors so a read-only home
// never crashes the UI). Callers load, mutate one field, then save so nothing is clobbered.
func savePrefs(p prefs) {
path := prefsPath()
if path == "" {
return
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return
}
if data, err := json.MarshalIndent(p, "", " "); err == nil {
_ = os.WriteFile(path, data, 0o644)
}
}
// loadStatusbarHidden reads the persisted hidden-segment set (best-effort: a missing or
// corrupt file yields an empty set, i.e. everything shown).
func loadStatusbarHidden() map[string]bool {
out := map[string]bool{}
for _, id := range loadPrefs().StatusbarHidden {
out[id] = true
}
return out
}
// saveStatusbarHidden writes the hidden set back to disk, preserving the other prefs fields.
// Order follows statusSegments for a stable file.
func saveStatusbarHidden(hidden map[string]bool) {
var ids []string
for _, seg := range statusSegments {
if hidden[seg.id] {
ids = append(ids, seg.id)
}
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return
}
if data, err := json.MarshalIndent(prefs{StatusbarHidden: ids}, "", " "); err == nil {
_ = os.WriteFile(path, data, 0o644)
}
p := loadPrefs()
p.StatusbarHidden = ids
savePrefs(p)
}
// saveInlineActionsHidden persists the inline-action-rows toggle, preserving other prefs fields.
func saveInlineActionsHidden(hidden bool) {
p := loadPrefs()
p.InlineActionsHidden = hidden
savePrefs(p)
}