Add compact transcript mode setting
- commit
- 0e72ca708fb60c2649fa8703e2ddabe0a9a70162
- author
- patriceckhart · · 2026-06-29 18:34 UTC
- parent
- adfda69
- signature
- unsigned
11 files · +184 -32
M README.md +1 -0
| @@ -301,6 +301,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 301 | 301 | ||
| 302 | 302 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | |
| 303 | 303 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | |
| 304 | + | - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`. | |
| 304 | 305 | - **thinking level** — choose reasoning for supported models: off (default; no reasoning), minimum (~1k tokens), low (~2k), medium (~8k), high (~16k), maximum (~32k). The change is persisted to `config.json` and applied to the running agent's next model call. | |
| 305 | 306 | - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md). | |
| 306 | 307 | - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running. | |
| @@ -301,6 +301,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 301 | 301 | ||||
| 302 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | 302 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | ||
| 303 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | 303 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | ||
| 304 | + | - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`. | |||
| 304 | - **thinking level** — choose reasoning for supported models: off (default; no reasoning), minimum (~1k tokens), low (~2k), medium (~8k), high (~16k), maximum (~32k). The change is persisted to `config.json` and applied to the running agent's next model call. | 305 | - **thinking level** — choose reasoning for supported models: off (default; no reasoning), minimum (~1k tokens), low (~2k), medium (~8k), high (~16k), maximum (~32k). The change is persisted to `config.json` and applied to the running agent's next model call. | ||
| 305 | - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md). | 306 | - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md). | ||
| 306 | - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running. | 307 | - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running. | ||
M packages/agent/cli.go +1 -0
| @@ -965,6 +965,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 965 | 965 | QuickModelShortcuts: quickModelShortcuts, | |
| 966 | 966 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | |
| 967 | 967 | RespectGitignore: initialCfg.RespectGitignore, | |
| 968 | + | CompactMode: initialCfg.CompactMode, | |
| 968 | 969 | ThemeName: initialCfg.Theme, | |
| 969 | 970 | FlatTools: initialCfg.FlatToolRender(), | |
| 970 | 971 | CompactUser: initialCfg.CompactUserInput(), | |
| @@ -965,6 +965,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 965 | QuickModelShortcuts: quickModelShortcuts, | 965 | QuickModelShortcuts: quickModelShortcuts, | ||
| 966 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | 966 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | ||
| 967 | RespectGitignore: initialCfg.RespectGitignore, | 967 | RespectGitignore: initialCfg.RespectGitignore, | ||
| 968 | + | CompactMode: initialCfg.CompactMode, | |||
| 968 | ThemeName: initialCfg.Theme, | 969 | ThemeName: initialCfg.Theme, | ||
| 969 | FlatTools: initialCfg.FlatToolRender(), | 970 | FlatTools: initialCfg.FlatToolRender(), | ||
| 970 | CompactUser: initialCfg.CompactUserInput(), | 971 | CompactUser: initialCfg.CompactUserInput(), | ||
M packages/agent/config.go +6 -0
| @@ -69,6 +69,12 @@ type Config struct { | |||
| 69 | 69 | // which is on; false shows ignored entries. Toggle from /settings. | |
| 70 | 70 | RespectGitignore *bool `json:"respect_gitignore,omitempty"` | |
| 71 | 71 | ||
| 72 | + | // CompactMode renders the interactive transcript with less chrome: | |
| 73 | + | // tool calls use flat headers instead of bordered panels, and sent | |
| 74 | + | // user messages render without padded background bubbles. Off by | |
| 75 | + | // default; nil/missing means disabled. Toggle from /settings. | |
| 76 | + | CompactMode *bool `json:"compact_mode,omitempty"` | |
| 77 | + | ||
| 72 | 78 | // Insecure skips TLS verification for custom inference endpoints. | |
| 73 | 79 | Insecure bool `json:"insecure,omitempty"` | |
| 74 | 80 | ||
| @@ -69,6 +69,12 @@ type Config struct { | |||||
| 69 | // which is on; false shows ignored entries. Toggle from /settings. | 69 | // which is on; false shows ignored entries. Toggle from /settings. | ||
| 70 | RespectGitignore *bool `json:"respect_gitignore,omitempty"` | 70 | RespectGitignore *bool `json:"respect_gitignore,omitempty"` | ||
| 71 | 71 | ||||
| 72 | + | // CompactMode renders the interactive transcript with less chrome: | |||
| 73 | + | // tool calls use flat headers instead of bordered panels, and sent | |||
| 74 | + | // user messages render without padded background bubbles. Off by | |||
| 75 | + | // default; nil/missing means disabled. Toggle from /settings. | |||
| 76 | + | CompactMode *bool `json:"compact_mode,omitempty"` | |||
| 77 | + | ||||
| 72 | // Insecure skips TLS verification for custom inference endpoints. | 78 | // Insecure skips TLS verification for custom inference endpoints. | ||
| 73 | Insecure bool `json:"insecure,omitempty"` | 79 | Insecure bool `json:"insecure,omitempty"` | ||
| 74 | 80 | ||||
M packages/agent/modes/btw_dialog.go +24 -10
| @@ -30,12 +30,13 @@ type btwTurn struct { | |||
| 30 | 30 | // Cancellation: esc cancels an in-flight call when one is running, | |
| 31 | 31 | // otherwise closes the dialog. | |
| 32 | 32 | type btwDialog struct { | |
| 33 | - | mu sync.Mutex | |
| 34 | - | active bool | |
| 35 | - | turns []btwTurn | |
| 36 | - | editor *tui.Editor | |
| 37 | - | loading bool | |
| 38 | - | cancel context.CancelFunc | |
| 33 | + | mu sync.Mutex | |
| 34 | + | active bool | |
| 35 | + | turns []btwTurn | |
| 36 | + | editor *tui.Editor | |
| 37 | + | loading bool | |
| 38 | + | compactMode bool | |
| 39 | + | cancel context.CancelFunc | |
| 39 | 40 | ||
| 40 | 41 | // spin drives the same braille animation + rotating funny-line | |
| 41 | 42 | // shown in the main status bar. Owned by the dialog so its clock | |
| @@ -100,10 +101,11 @@ func (d *btwDialog) Loading() bool { | |||
| 100 | 101 | // auto-submitted (so /btw <text> starts a conversation right away). | |
| 101 | 102 | // invalidate, if non-nil, is called after each state change so the | |
| 102 | 103 | // host redraw loop can pick up the update without polling. | |
| 103 | - | func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, cwd, seed string, invalidate func()) { | |
| 104 | + | func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, cwd, seed string, compactMode bool, invalidate func()) { | |
| 104 | 105 | d.mu.Lock() | |
| 105 | 106 | d.active = true | |
| 106 | 107 | d.theme = th | |
| 108 | + | d.compactMode = compactMode | |
| 107 | 109 | d.turns = nil | |
| 108 | 110 | d.loading = false | |
| 109 | 111 | d.cancel = nil | |
| @@ -319,7 +321,7 @@ func (d *btwDialog) Render(th tui.Theme, width int) []string { | |||
| 319 | 321 | ||
| 320 | 322 | for _, t := range d.turns { | |
| 321 | 323 | out = append(out, "") | |
| 322 | - | out = append(out, btwUserBubbleRows(th, t.User, width-2)...) | |
| 324 | + | out = append(out, btwUserBubbleRows(th, t.User, width-2, d.compactMode)...) | |
| 323 | 325 | if t.Assistant != "" { | |
| 324 | 326 | out = append(out, "") | |
| 325 | 327 | md := tui.RenderMarkdown(t.Assistant, th, width-4) | |
| @@ -390,7 +392,7 @@ func (d *btwDialog) CursorPos(width int) (row, col int) { | |||
| 390 | 392 | } | |
| 391 | 393 | for _, t := range d.turns { | |
| 392 | 394 | editorOffset++ // blank | |
| 393 | - | editorOffset += len(btwUserBubbleRows(d.theme, t.User, width-2)) | |
| 395 | + | editorOffset += len(btwUserBubbleRows(d.theme, t.User, width-2, d.compactMode)) | |
| 394 | 396 | if t.Assistant != "" { | |
| 395 | 397 | editorOffset++ // blank | |
| 396 | 398 | editorOffset += len(strings.Split(tui.RenderMarkdown(t.Assistant, d.theme, width-4), "\n")) | |
| @@ -413,7 +415,7 @@ func (d *btwDialog) CursorPos(width int) (row, col int) { | |||
| 413 | 415 | // panel, left-edge ▌ bar, padding rows above and below). The frame | |
| 414 | 416 | // padding is the caller's job; bubbleWidth is the available row | |
| 415 | 417 | // width inside the frame. | |
| 416 | - | func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int) []string { | |
| 418 | + | func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int, compactMode bool) []string { | |
| 417 | 419 | const leftGutter = 0 | |
| 418 | 420 | const rightGutter = 2 | |
| 419 | 421 | innerWidth := bubbleWidth - 2 - leftGutter - rightGutter // 2 = bar's two cells | |
| @@ -425,6 +427,15 @@ func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int) []string { | |||
| 425 | 427 | inner := strings.Repeat(" ", leftGutter) + content | |
| 426 | 428 | return " " + bar + th.UserBubble(inner, bubbleWidth-2) | |
| 427 | 429 | } | |
| 430 | + | if compactMode { | |
| 431 | + | innerWidth = bubbleWidth - 2 | |
| 432 | + | if innerWidth < 1 { | |
| 433 | + | innerWidth = 1 | |
| 434 | + | } | |
| 435 | + | row = func(content string) string { | |
| 436 | + | return " " + th.FG256(th.Accent, "▌ ") + th.FG256(th.Muted, content) | |
| 437 | + | } | |
| 438 | + | } | |
| 428 | 439 | var bubble []string | |
| 429 | 440 | for _, l := range strings.Split(text, "\n") { | |
| 430 | 441 | for _, w := range tui.WrapANSILine(l, innerWidth) { | |
| @@ -434,6 +445,9 @@ func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int) []string { | |||
| 434 | 445 | if len(bubble) == 0 { | |
| 435 | 446 | return nil | |
| 436 | 447 | } | |
| 448 | + | if compactMode { | |
| 449 | + | return bubble | |
| 450 | + | } | |
| 437 | 451 | out := make([]string, 0, len(bubble)+2) | |
| 438 | 452 | out = append(out, row("")) | |
| 439 | 453 | out = append(out, bubble...) | |
| @@ -30,12 +30,13 @@ type btwTurn struct { | |||||
| 30 | // Cancellation: esc cancels an in-flight call when one is running, | 30 | // Cancellation: esc cancels an in-flight call when one is running, | ||
| 31 | // otherwise closes the dialog. | 31 | // otherwise closes the dialog. | ||
| 32 | type btwDialog struct { | 32 | type btwDialog struct { | ||
| 33 | - | mu sync.Mutex | 33 | + | mu sync.Mutex |
| 34 | - | active bool | 34 | + | active bool |
| 35 | - | turns []btwTurn | 35 | + | turns []btwTurn |
| 36 | - | editor *tui.Editor | 36 | + | editor *tui.Editor |
| 37 | - | loading bool | 37 | + | loading bool |
| 38 | - | cancel context.CancelFunc | 38 | + | compactMode bool |
| 39 | + | cancel context.CancelFunc | |||
| 39 | 40 | ||||
| 40 | // spin drives the same braille animation + rotating funny-line | 41 | // spin drives the same braille animation + rotating funny-line | ||
| 41 | // shown in the main status bar. Owned by the dialog so its clock | 42 | // shown in the main status bar. Owned by the dialog so its clock | ||
| @@ -100,10 +101,11 @@ func (d *btwDialog) Loading() bool { | |||||
| 100 | // auto-submitted (so /btw <text> starts a conversation right away). | 101 | // auto-submitted (so /btw <text> starts a conversation right away). | ||
| 101 | // invalidate, if non-nil, is called after each state change so the | 102 | // invalidate, if non-nil, is called after each state change so the | ||
| 102 | // host redraw loop can pick up the update without polling. | 103 | // host redraw loop can pick up the update without polling. | ||
| 103 | - | func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, cwd, seed string, invalidate func()) { | 104 | + | func (d *btwDialog) Open(th tui.Theme, agent *core.Agent, system, model, cwd, seed string, compactMode bool, invalidate func()) { |
| 104 | d.mu.Lock() | 105 | d.mu.Lock() | ||
| 105 | d.active = true | 106 | d.active = true | ||
| 106 | d.theme = th | 107 | d.theme = th | ||
| 108 | + | d.compactMode = compactMode | |||
| 107 | d.turns = nil | 109 | d.turns = nil | ||
| 108 | d.loading = false | 110 | d.loading = false | ||
| 109 | d.cancel = nil | 111 | d.cancel = nil | ||
| @@ -319,7 +321,7 @@ func (d *btwDialog) Render(th tui.Theme, width int) []string { | |||||
| 319 | 321 | ||||
| 320 | for _, t := range d.turns { | 322 | for _, t := range d.turns { | ||
| 321 | out = append(out, "") | 323 | out = append(out, "") | ||
| 322 | - | out = append(out, btwUserBubbleRows(th, t.User, width-2)...) | 324 | + | out = append(out, btwUserBubbleRows(th, t.User, width-2, d.compactMode)...) |
| 323 | if t.Assistant != "" { | 325 | if t.Assistant != "" { | ||
| 324 | out = append(out, "") | 326 | out = append(out, "") | ||
| 325 | md := tui.RenderMarkdown(t.Assistant, th, width-4) | 327 | md := tui.RenderMarkdown(t.Assistant, th, width-4) | ||
| @@ -390,7 +392,7 @@ func (d *btwDialog) CursorPos(width int) (row, col int) { | |||||
| 390 | } | 392 | } | ||
| 391 | for _, t := range d.turns { | 393 | for _, t := range d.turns { | ||
| 392 | editorOffset++ // blank | 394 | editorOffset++ // blank | ||
| 393 | - | editorOffset += len(btwUserBubbleRows(d.theme, t.User, width-2)) | 395 | + | editorOffset += len(btwUserBubbleRows(d.theme, t.User, width-2, d.compactMode)) |
| 394 | if t.Assistant != "" { | 396 | if t.Assistant != "" { | ||
| 395 | editorOffset++ // blank | 397 | editorOffset++ // blank | ||
| 396 | editorOffset += len(strings.Split(tui.RenderMarkdown(t.Assistant, d.theme, width-4), "\n")) | 398 | editorOffset += len(strings.Split(tui.RenderMarkdown(t.Assistant, d.theme, width-4), "\n")) | ||
| @@ -413,7 +415,7 @@ func (d *btwDialog) CursorPos(width int) (row, col int) { | |||||
| 413 | // panel, left-edge ▌ bar, padding rows above and below). The frame | 415 | // panel, left-edge ▌ bar, padding rows above and below). The frame | ||
| 414 | // padding is the caller's job; bubbleWidth is the available row | 416 | // padding is the caller's job; bubbleWidth is the available row | ||
| 415 | // width inside the frame. | 417 | // width inside the frame. | ||
| 416 | - | func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int) []string { | 418 | + | func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int, compactMode bool) []string { |
| 417 | const leftGutter = 0 | 419 | const leftGutter = 0 | ||
| 418 | const rightGutter = 2 | 420 | const rightGutter = 2 | ||
| 419 | innerWidth := bubbleWidth - 2 - leftGutter - rightGutter // 2 = bar's two cells | 421 | innerWidth := bubbleWidth - 2 - leftGutter - rightGutter // 2 = bar's two cells | ||
| @@ -425,6 +427,15 @@ func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int) []string { | |||||
| 425 | inner := strings.Repeat(" ", leftGutter) + content | 427 | inner := strings.Repeat(" ", leftGutter) + content | ||
| 426 | return " " + bar + th.UserBubble(inner, bubbleWidth-2) | 428 | return " " + bar + th.UserBubble(inner, bubbleWidth-2) | ||
| 427 | } | 429 | } | ||
| 430 | + | if compactMode { | |||
| 431 | + | innerWidth = bubbleWidth - 2 | |||
| 432 | + | if innerWidth < 1 { | |||
| 433 | + | innerWidth = 1 | |||
| 434 | + | } | |||
| 435 | + | row = func(content string) string { | |||
| 436 | + | return " " + th.FG256(th.Accent, "▌ ") + th.FG256(th.Muted, content) | |||
| 437 | + | } | |||
| 438 | + | } | |||
| 428 | var bubble []string | 439 | var bubble []string | ||
| 429 | for _, l := range strings.Split(text, "\n") { | 440 | for _, l := range strings.Split(text, "\n") { | ||
| 430 | for _, w := range tui.WrapANSILine(l, innerWidth) { | 441 | for _, w := range tui.WrapANSILine(l, innerWidth) { | ||
| @@ -434,6 +445,9 @@ func btwUserBubbleRows(th tui.Theme, text string, bubbleWidth int) []string { | |||||
| 434 | if len(bubble) == 0 { | 445 | if len(bubble) == 0 { | ||
| 435 | return nil | 446 | return nil | ||
| 436 | } | 447 | } | ||
| 448 | + | if compactMode { | |||
| 449 | + | return bubble | |||
| 450 | + | } | |||
| 437 | out := make([]string, 0, len(bubble)+2) | 451 | out := make([]string, 0, len(bubble)+2) | ||
| 438 | out = append(out, row("")) | 452 | out = append(out, row("")) | ||
| 439 | out = append(out, bubble...) | 453 | out = append(out, bubble...) | ||
M packages/agent/modes/interactive.go +35 -1
| @@ -71,6 +71,10 @@ type InteractiveConfig struct { | |||
| 71 | 71 | // compact_input config / ZOT_COMPACT_INPUT env at startup. | |
| 72 | 72 | CompactUser bool | |
| 73 | 73 | ||
| 74 | + | // CompactMode mirrors the persisted compact_mode flag at startup. | |
| 75 | + | // When true, it enables both flat tool calls and compact user turns. | |
| 76 | + | CompactMode *bool | |
| 77 | + | ||
| 74 | 78 | // QuickModelShortcuts maps slots 1-9 to provider/model pairs. The | |
| 75 | 79 | // shortcuts are Ctrl+1..9. Cmd+1..9 may also work when the terminal | |
| 76 | 80 | // forwards Command/Super keypresses, but Ctrl is the displayed chord. | |
| @@ -265,6 +269,7 @@ type SettingsStore interface { | |||
| 265 | 269 | SetAutoSwarm(enabled bool) error | |
| 266 | 270 | SetRecursiveFileSuggest(enabled bool) error | |
| 267 | 271 | SetRespectGitignore(enabled bool) error | |
| 272 | + | SetCompactMode(enabled bool) error | |
| 268 | 273 | SetReasoning(level string) error | |
| 269 | 274 | SetTheme(name string) error | |
| 270 | 275 | } | |
| @@ -487,6 +492,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 487 | 492 | ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled), | |
| 488 | 493 | FlatTools: cfg.FlatTools, | |
| 489 | 494 | CompactUser: cfg.CompactUser, | |
| 495 | + | CompactMode: cfg.CompactMode != nil && *cfg.CompactMode, | |
| 490 | 496 | }, | |
| 491 | 497 | // Prompt is the standard half-block accent bar used by chat | |
| 492 | 498 | // speaker labels too, so the input gutter matches the rest | |
| @@ -2818,6 +2824,7 @@ func (i *Interactive) openSettingsDialog() { | |||
| 2818 | 2824 | ||
| 2819 | 2825 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | |
| 2820 | 2826 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | |
| 2827 | + | compactMode := i.compactModeEnabled() | |
| 2821 | 2828 | quickItems := i.quickModelSettingItems() | |
| 2822 | 2829 | ||
| 2823 | 2830 | reasoningOptions := []settingsOption{ | |
| @@ -2895,6 +2902,12 @@ func (i *Interactive) openSettingsDialog() { | |||
| 2895 | 2902 | desc: "skip files and directories matched by the project's root .gitignore (and .git) when picking files with @", | |
| 2896 | 2903 | value: respectGitignore, | |
| 2897 | 2904 | }, | |
| 2905 | + | { | |
| 2906 | + | key: "compact_mode", | |
| 2907 | + | label: "compact transcript rendering", | |
| 2908 | + | desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles", | |
| 2909 | + | value: compactMode, | |
| 2910 | + | }, | |
| 2898 | 2911 | { | |
| 2899 | 2912 | key: "reasoning", | |
| 2900 | 2913 | label: "thinking level", | |
| @@ -2966,6 +2979,10 @@ func (i *Interactive) quickModelSettingItem(slot int) settingsItem { | |||
| 2966 | 2979 | } | |
| 2967 | 2980 | } | |
| 2968 | 2981 | ||
| 2982 | + | func (i *Interactive) compactModeEnabled() bool { | |
| 2983 | + | return i.cfg.CompactMode != nil && *i.cfg.CompactMode | |
| 2984 | + | } | |
| 2985 | + | ||
| 2969 | 2986 | func quickModelShortcutSlot(k tui.Key) int { | |
| 2970 | 2987 | if k.Kind != tui.KeyRune || k.Rune < '1' || k.Rune > '9' { | |
| 2971 | 2988 | return 0 | |
| @@ -3184,6 +3201,23 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 3184 | 3201 | i.statusOK = "hide gitignored files in @-picker " + onOff(value) | |
| 3185 | 3202 | i.statusErr = "" | |
| 3186 | 3203 | i.mu.Unlock() | |
| 3204 | + | case "compact_mode": | |
| 3205 | + | val := value | |
| 3206 | + | i.cfg.CompactMode = &val | |
| 3207 | + | if i.cfg.SettingsStore != nil { | |
| 3208 | + | if err := i.cfg.SettingsStore.SetCompactMode(value); err != nil { | |
| 3209 | + | i.mu.Lock() | |
| 3210 | + | i.statusErr = "settings: " + err.Error() | |
| 3211 | + | i.mu.Unlock() | |
| 3212 | + | return | |
| 3213 | + | } | |
| 3214 | + | } | |
| 3215 | + | i.mu.Lock() | |
| 3216 | + | i.view.CompactMode = value | |
| 3217 | + | i.view.InvalidateRenderCache() | |
| 3218 | + | i.statusOK = "compact transcript rendering " + onOff(value) | |
| 3219 | + | i.statusErr = "" | |
| 3220 | + | i.mu.Unlock() | |
| 3187 | 3221 | } | |
| 3188 | 3222 | } | |
| 3189 | 3223 | ||
| @@ -4052,7 +4086,7 @@ func (i *Interactive) openBtwDialog(args []string) { | |||
| 4052 | 4086 | return | |
| 4053 | 4087 | } | |
| 4054 | 4088 | seed := strings.TrimSpace(strings.Join(args, " ")) | |
| 4055 | - | i.btwDialog.Open(i.cfg.Theme, i.agent, i.agent.System, i.cfg.Model, i.cfg.CWD, seed, i.invalidate) | |
| 4089 | + | i.btwDialog.Open(i.cfg.Theme, i.agent, i.agent.System, i.cfg.Model, i.cfg.CWD, seed, i.compactModeEnabled(), i.invalidate) | |
| 4056 | 4090 | i.invalidate() | |
| 4057 | 4091 | } | |
| 4058 | 4092 | ||
| @@ -71,6 +71,10 @@ type InteractiveConfig struct { | |||||
| 71 | // compact_input config / ZOT_COMPACT_INPUT env at startup. | 71 | // compact_input config / ZOT_COMPACT_INPUT env at startup. | ||
| 72 | CompactUser bool | 72 | CompactUser bool | ||
| 73 | 73 | ||||
| 74 | + | // CompactMode mirrors the persisted compact_mode flag at startup. | |||
| 75 | + | // When true, it enables both flat tool calls and compact user turns. | |||
| 76 | + | CompactMode *bool | |||
| 77 | + | ||||
| 74 | // QuickModelShortcuts maps slots 1-9 to provider/model pairs. The | 78 | // QuickModelShortcuts maps slots 1-9 to provider/model pairs. The | ||
| 75 | // shortcuts are Ctrl+1..9. Cmd+1..9 may also work when the terminal | 79 | // shortcuts are Ctrl+1..9. Cmd+1..9 may also work when the terminal | ||
| 76 | // forwards Command/Super keypresses, but Ctrl is the displayed chord. | 80 | // forwards Command/Super keypresses, but Ctrl is the displayed chord. | ||
| @@ -265,6 +269,7 @@ type SettingsStore interface { | |||||
| 265 | SetAutoSwarm(enabled bool) error | 269 | SetAutoSwarm(enabled bool) error | ||
| 266 | SetRecursiveFileSuggest(enabled bool) error | 270 | SetRecursiveFileSuggest(enabled bool) error | ||
| 267 | SetRespectGitignore(enabled bool) error | 271 | SetRespectGitignore(enabled bool) error | ||
| 272 | + | SetCompactMode(enabled bool) error | |||
| 268 | SetReasoning(level string) error | 273 | SetReasoning(level string) error | ||
| 269 | SetTheme(name string) error | 274 | SetTheme(name string) error | ||
| 270 | } | 275 | } | ||
| @@ -487,6 +492,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 487 | ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled), | 492 | ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled), | ||
| 488 | FlatTools: cfg.FlatTools, | 493 | FlatTools: cfg.FlatTools, | ||
| 489 | CompactUser: cfg.CompactUser, | 494 | CompactUser: cfg.CompactUser, | ||
| 495 | + | CompactMode: cfg.CompactMode != nil && *cfg.CompactMode, | |||
| 490 | }, | 496 | }, | ||
| 491 | // Prompt is the standard half-block accent bar used by chat | 497 | // Prompt is the standard half-block accent bar used by chat | ||
| 492 | // speaker labels too, so the input gutter matches the rest | 498 | // speaker labels too, so the input gutter matches the rest | ||
| @@ -2818,6 +2824,7 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 2818 | 2824 | ||||
| 2819 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | 2825 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | ||
| 2820 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | 2826 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | ||
| 2827 | + | compactMode := i.compactModeEnabled() | |||
| 2821 | quickItems := i.quickModelSettingItems() | 2828 | quickItems := i.quickModelSettingItems() | ||
| 2822 | 2829 | ||||
| 2823 | reasoningOptions := []settingsOption{ | 2830 | reasoningOptions := []settingsOption{ | ||
| @@ -2895,6 +2902,12 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 2895 | desc: "skip files and directories matched by the project's root .gitignore (and .git) when picking files with @", | 2902 | desc: "skip files and directories matched by the project's root .gitignore (and .git) when picking files with @", | ||
| 2896 | value: respectGitignore, | 2903 | value: respectGitignore, | ||
| 2897 | }, | 2904 | }, | ||
| 2905 | + | { | |||
| 2906 | + | key: "compact_mode", | |||
| 2907 | + | label: "compact transcript rendering", | |||
| 2908 | + | desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles", | |||
| 2909 | + | value: compactMode, | |||
| 2910 | + | }, | |||
| 2898 | { | 2911 | { | ||
| 2899 | key: "reasoning", | 2912 | key: "reasoning", | ||
| 2900 | label: "thinking level", | 2913 | label: "thinking level", | ||
| @@ -2966,6 +2979,10 @@ func (i *Interactive) quickModelSettingItem(slot int) settingsItem { | |||||
| 2966 | } | 2979 | } | ||
| 2967 | } | 2980 | } | ||
| 2968 | 2981 | ||||
| 2982 | + | func (i *Interactive) compactModeEnabled() bool { | |||
| 2983 | + | return i.cfg.CompactMode != nil && *i.cfg.CompactMode | |||
| 2984 | + | } | |||
| 2985 | + | ||||
| 2969 | func quickModelShortcutSlot(k tui.Key) int { | 2986 | func quickModelShortcutSlot(k tui.Key) int { | ||
| 2970 | if k.Kind != tui.KeyRune || k.Rune < '1' || k.Rune > '9' { | 2987 | if k.Kind != tui.KeyRune || k.Rune < '1' || k.Rune > '9' { | ||
| 2971 | return 0 | 2988 | return 0 | ||
| @@ -3184,6 +3201,23 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||||
| 3184 | i.statusOK = "hide gitignored files in @-picker " + onOff(value) | 3201 | i.statusOK = "hide gitignored files in @-picker " + onOff(value) | ||
| 3185 | i.statusErr = "" | 3202 | i.statusErr = "" | ||
| 3186 | i.mu.Unlock() | 3203 | i.mu.Unlock() | ||
| 3204 | + | case "compact_mode": | |||
| 3205 | + | val := value | |||
| 3206 | + | i.cfg.CompactMode = &val | |||
| 3207 | + | if i.cfg.SettingsStore != nil { | |||
| 3208 | + | if err := i.cfg.SettingsStore.SetCompactMode(value); err != nil { | |||
| 3209 | + | i.mu.Lock() | |||
| 3210 | + | i.statusErr = "settings: " + err.Error() | |||
| 3211 | + | i.mu.Unlock() | |||
| 3212 | + | return | |||
| 3213 | + | } | |||
| 3214 | + | } | |||
| 3215 | + | i.mu.Lock() | |||
| 3216 | + | i.view.CompactMode = value | |||
| 3217 | + | i.view.InvalidateRenderCache() | |||
| 3218 | + | i.statusOK = "compact transcript rendering " + onOff(value) | |||
| 3219 | + | i.statusErr = "" | |||
| 3220 | + | i.mu.Unlock() | |||
| 3187 | } | 3221 | } | ||
| 3188 | } | 3222 | } | ||
| 3189 | 3223 | ||||
| @@ -4052,7 +4086,7 @@ func (i *Interactive) openBtwDialog(args []string) { | |||||
| 4052 | return | 4086 | return | ||
| 4053 | } | 4087 | } | ||
| 4054 | seed := strings.TrimSpace(strings.Join(args, " ")) | 4088 | seed := strings.TrimSpace(strings.Join(args, " ")) | ||
| 4055 | - | i.btwDialog.Open(i.cfg.Theme, i.agent, i.agent.System, i.cfg.Model, i.cfg.CWD, seed, i.invalidate) | 4089 | + | i.btwDialog.Open(i.cfg.Theme, i.agent, i.agent.System, i.cfg.Model, i.cfg.CWD, seed, i.compactModeEnabled(), i.invalidate) |
| 4056 | i.invalidate() | 4090 | i.invalidate() | ||
| 4057 | } | 4091 | } | ||
| 4058 | 4092 | ||||
M packages/agent/modes/swarm_dialog.go +13 -8
| @@ -32,10 +32,11 @@ import ( | |||
| 32 | 32 | // ↑/↓/PgUp/PgDn/Home/End scroll | |
| 33 | 33 | // esc / q return to the list | |
| 34 | 34 | type swarmDialog struct { | |
| 35 | - | active bool | |
| 36 | - | snapshot func() []swarm.AgentSnapshot | |
| 37 | - | stop func(id string) error | |
| 38 | - | remove func(id string) error | |
| 35 | + | active bool | |
| 36 | + | compactMode bool | |
| 37 | + | snapshot func() []swarm.AgentSnapshot | |
| 38 | + | stop func(id string) error | |
| 39 | + | remove func(id string) error | |
| 39 | 40 | // spawn accepts an optional model + provider override (empty | |
| 40 | 41 | // strings mean "let the child resolve its own default"). The cli | |
| 41 | 42 | // adapter forwards these to swarm.Swarm.SpawnReq. | |
| @@ -119,6 +120,10 @@ func newSwarmDialog() *swarmDialog { return &swarmDialog{} } | |||
| 119 | 120 | // run on whatever the user last picked via /model. Pass empty | |
| 120 | 121 | // strings to clear the override (the child then resolves its own | |
| 121 | 122 | // default the same way a bare `zot` invocation does). | |
| 123 | + | func (d *swarmDialog) SetCompactMode(enabled bool) { | |
| 124 | + | d.compactMode = enabled | |
| 125 | + | } | |
| 126 | + | ||
| 122 | 127 | func (d *swarmDialog) SetCurrentModel(model, providerID string) { | |
| 123 | 128 | d.pendingModel = model | |
| 124 | 129 | d.pendingProvider = providerID | |
| @@ -274,7 +279,7 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse | |||
| 274 | 279 | // Full body, no scroll window — the host's outer scrollback | |
| 275 | 280 | // owns overflow now. Cursor row math just counts every | |
| 276 | 281 | // rendered line. | |
| 277 | - | body := renderSwarmTranscriptBlocks(raw, tui.Theme{}, width) | |
| 282 | + | body := renderSwarmTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode) | |
| 278 | 283 | row += len(body) | |
| 279 | 284 | } | |
| 280 | 285 | ||
| @@ -1107,7 +1112,7 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string { | |||
| 1107 | 1112 | // That gives the user the chat UX they expect (transcript grows | |
| 1108 | 1113 | // with the window, arrow keys belong to the editor, no "↑ N | |
| 1109 | 1114 | // more above" cliff). | |
| 1110 | - | body := renderSwarmTranscriptBlocks(raw, th, width) | |
| 1115 | + | body := renderSwarmTranscriptBlocks(raw, th, width, d.compactMode) | |
| 1111 | 1116 | ||
| 1112 | 1117 | out := header | |
| 1113 | 1118 | out = append(out, body...) | |
| @@ -1218,7 +1223,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool { | |||
| 1218 | 1223 | // whole block together — essential for markdown, which needs the | |
| 1219 | 1224 | // full message body in one piece to detect fenced code spans, | |
| 1220 | 1225 | // bullet lists, and so on. | |
| 1221 | - | func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int) []string { | |
| 1226 | + | func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compactMode bool) []string { | |
| 1222 | 1227 | type blockKind int | |
| 1223 | 1228 | const ( | |
| 1224 | 1229 | kindAssistant blockKind = iota | |
| @@ -1289,7 +1294,7 @@ func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int) []stri | |||
| 1289 | 1294 | switch b.kind { | |
| 1290 | 1295 | case kindUser: | |
| 1291 | 1296 | text := strings.Join(b.body, "\n") | |
| 1292 | - | out = append(out, btwUserBubbleRows(th, text, bubbleWidth)...) | |
| 1297 | + | out = append(out, btwUserBubbleRows(th, text, bubbleWidth, compactMode)...) | |
| 1293 | 1298 | case kindAssistant: | |
| 1294 | 1299 | text := strings.Join(b.body, "\n") | |
| 1295 | 1300 | md := tui.RenderMarkdown(strings.TrimLeft(text, "\n"), th, innerMD) | |
| @@ -32,10 +32,11 @@ import ( | |||||
| 32 | // ↑/↓/PgUp/PgDn/Home/End scroll | 32 | // ↑/↓/PgUp/PgDn/Home/End scroll | ||
| 33 | // esc / q return to the list | 33 | // esc / q return to the list | ||
| 34 | type swarmDialog struct { | 34 | type swarmDialog struct { | ||
| 35 | - | active bool | 35 | + | active bool |
| 36 | - | snapshot func() []swarm.AgentSnapshot | 36 | + | compactMode bool |
| 37 | - | stop func(id string) error | 37 | + | snapshot func() []swarm.AgentSnapshot |
| 38 | - | remove func(id string) error | 38 | + | stop func(id string) error |
| 39 | + | remove func(id string) error | |||
| 39 | // spawn accepts an optional model + provider override (empty | 40 | // spawn accepts an optional model + provider override (empty | ||
| 40 | // strings mean "let the child resolve its own default"). The cli | 41 | // strings mean "let the child resolve its own default"). The cli | ||
| 41 | // adapter forwards these to swarm.Swarm.SpawnReq. | 42 | // adapter forwards these to swarm.Swarm.SpawnReq. | ||
| @@ -119,6 +120,10 @@ func newSwarmDialog() *swarmDialog { return &swarmDialog{} } | |||||
| 119 | // run on whatever the user last picked via /model. Pass empty | 120 | // run on whatever the user last picked via /model. Pass empty | ||
| 120 | // strings to clear the override (the child then resolves its own | 121 | // strings to clear the override (the child then resolves its own | ||
| 121 | // default the same way a bare `zot` invocation does). | 122 | // default the same way a bare `zot` invocation does). | ||
| 123 | + | func (d *swarmDialog) SetCompactMode(enabled bool) { | |||
| 124 | + | d.compactMode = enabled | |||
| 125 | + | } | |||
| 126 | + | ||||
| 122 | func (d *swarmDialog) SetCurrentModel(model, providerID string) { | 127 | func (d *swarmDialog) SetCurrentModel(model, providerID string) { | ||
| 123 | d.pendingModel = model | 128 | d.pendingModel = model | ||
| 124 | d.pendingProvider = providerID | 129 | d.pendingProvider = providerID | ||
| @@ -274,7 +279,7 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse | |||||
| 274 | // Full body, no scroll window — the host's outer scrollback | 279 | // Full body, no scroll window — the host's outer scrollback | ||
| 275 | // owns overflow now. Cursor row math just counts every | 280 | // owns overflow now. Cursor row math just counts every | ||
| 276 | // rendered line. | 281 | // rendered line. | ||
| 277 | - | body := renderSwarmTranscriptBlocks(raw, tui.Theme{}, width) | 282 | + | body := renderSwarmTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode) |
| 278 | row += len(body) | 283 | row += len(body) | ||
| 279 | } | 284 | } | ||
| 280 | 285 | ||||
| @@ -1107,7 +1112,7 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string { | |||||
| 1107 | // That gives the user the chat UX they expect (transcript grows | 1112 | // That gives the user the chat UX they expect (transcript grows | ||
| 1108 | // with the window, arrow keys belong to the editor, no "↑ N | 1113 | // with the window, arrow keys belong to the editor, no "↑ N | ||
| 1109 | // more above" cliff). | 1114 | // more above" cliff). | ||
| 1110 | - | body := renderSwarmTranscriptBlocks(raw, th, width) | 1115 | + | body := renderSwarmTranscriptBlocks(raw, th, width, d.compactMode) |
| 1111 | 1116 | ||||
| 1112 | out := header | 1117 | out := header | ||
| 1113 | out = append(out, body...) | 1118 | out = append(out, body...) | ||
| @@ -1218,7 +1223,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool { | |||||
| 1218 | // whole block together — essential for markdown, which needs the | 1223 | // whole block together — essential for markdown, which needs the | ||
| 1219 | // full message body in one piece to detect fenced code spans, | 1224 | // full message body in one piece to detect fenced code spans, | ||
| 1220 | // bullet lists, and so on. | 1225 | // bullet lists, and so on. | ||
| 1221 | - | func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int) []string { | 1226 | + | func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compactMode bool) []string { |
| 1222 | type blockKind int | 1227 | type blockKind int | ||
| 1223 | const ( | 1228 | const ( | ||
| 1224 | kindAssistant blockKind = iota | 1229 | kindAssistant blockKind = iota | ||
| @@ -1289,7 +1294,7 @@ func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int) []stri | |||||
| 1289 | switch b.kind { | 1294 | switch b.kind { | ||
| 1290 | case kindUser: | 1295 | case kindUser: | ||
| 1291 | text := strings.Join(b.body, "\n") | 1296 | text := strings.Join(b.body, "\n") | ||
| 1292 | - | out = append(out, btwUserBubbleRows(th, text, bubbleWidth)...) | 1297 | + | out = append(out, btwUserBubbleRows(th, text, bubbleWidth, compactMode)...) |
| 1293 | case kindAssistant: | 1298 | case kindAssistant: | ||
| 1294 | text := strings.Join(b.body, "\n") | 1299 | text := strings.Join(b.body, "\n") | ||
| 1295 | md := tui.RenderMarkdown(strings.TrimLeft(text, "\n"), th, innerMD) | 1300 | md := tui.RenderMarkdown(strings.TrimLeft(text, "\n"), th, innerMD) | ||
M packages/agent/modes/swarm_dialog_test.go +3 -3
| @@ -753,7 +753,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) { | |||
| 753 | 753 | "stderr: warning thing", | |
| 754 | 754 | "error: boom", | |
| 755 | 755 | } | |
| 756 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80) | |
| 756 | + | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 757 | 757 | joined := strings.Join(out, "\n") | |
| 758 | 758 | ||
| 759 | 759 | // The user echo should appear as a bubble row, which contains | |
| @@ -790,7 +790,7 @@ func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) { | |||
| 790 | 790 | "- **bold** item", | |
| 791 | 791 | "- plain item", | |
| 792 | 792 | } | |
| 793 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80) | |
| 793 | + | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 794 | 794 | joined := strings.Join(out, "\n") | |
| 795 | 795 | ||
| 796 | 796 | // ANSI bold on/off. Markdown rendering uses CSI 1 / 22. | |
| @@ -821,7 +821,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) | |||
| 821 | 821 | "```", | |
| 822 | 822 | "Done.", | |
| 823 | 823 | } | |
| 824 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80) | |
| 824 | + | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 825 | 825 | joined := strings.Join(out, "\n") | |
| 826 | 826 | ||
| 827 | 827 | // The literal fence markers should be consumed by the markdown | |
| @@ -753,7 +753,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) { | |||||
| 753 | "stderr: warning thing", | 753 | "stderr: warning thing", | ||
| 754 | "error: boom", | 754 | "error: boom", | ||
| 755 | } | 755 | } | ||
| 756 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80) | 756 | + | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) |
| 757 | joined := strings.Join(out, "\n") | 757 | joined := strings.Join(out, "\n") | ||
| 758 | 758 | ||||
| 759 | // The user echo should appear as a bubble row, which contains | 759 | // The user echo should appear as a bubble row, which contains | ||
| @@ -790,7 +790,7 @@ func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) { | |||||
| 790 | "- **bold** item", | 790 | "- **bold** item", | ||
| 791 | "- plain item", | 791 | "- plain item", | ||
| 792 | } | 792 | } | ||
| 793 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80) | 793 | + | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) |
| 794 | joined := strings.Join(out, "\n") | 794 | joined := strings.Join(out, "\n") | ||
| 795 | 795 | ||||
| 796 | // ANSI bold on/off. Markdown rendering uses CSI 1 / 22. | 796 | // ANSI bold on/off. Markdown rendering uses CSI 1 / 22. | ||
| @@ -821,7 +821,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) | |||||
| 821 | "```", | 821 | "```", | ||
| 822 | "Done.", | 822 | "Done.", | ||
| 823 | } | 823 | } | ||
| 824 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80) | 824 | + | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) |
| 825 | joined := strings.Join(out, "\n") | 825 | joined := strings.Join(out, "\n") | ||
| 826 | 826 | ||||
| 827 | // The literal fence markers should be consumed by the markdown | 827 | // The literal fence markers should be consumed by the markdown | ||
M packages/agent/modes/swarm_slash.go +1 -0
| @@ -69,6 +69,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||
| 69 | 69 | // wants a different model for the next swarm agent, they pick it | |
| 70 | 70 | // via /model first (globally), or, while inside the spawn | |
| 71 | 71 | // editor, by typing /model on its own line to pop the picker. | |
| 72 | + | i.swarmDialog.SetCompactMode(i.compactModeEnabled()) | |
| 72 | 73 | i.swarmDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider) | |
| 73 | 74 | if i.cfg.LoggedInProviders != nil { | |
| 74 | 75 | i.swarmDialog.SetLoggedInProviders(i.cfg.LoggedInProviders()) | |
| @@ -69,6 +69,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||||
| 69 | // wants a different model for the next swarm agent, they pick it | 69 | // wants a different model for the next swarm agent, they pick it | ||
| 70 | // via /model first (globally), or, while inside the spawn | 70 | // via /model first (globally), or, while inside the spawn | ||
| 71 | // editor, by typing /model on its own line to pop the picker. | 71 | // editor, by typing /model on its own line to pop the picker. | ||
| 72 | + | i.swarmDialog.SetCompactMode(i.compactModeEnabled()) | |||
| 72 | i.swarmDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider) | 73 | i.swarmDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider) | ||
| 73 | if i.cfg.LoggedInProviders != nil { | 74 | if i.cfg.LoggedInProviders != nil { | ||
| 74 | i.swarmDialog.SetLoggedInProviders(i.cfg.LoggedInProviders()) | 75 | i.swarmDialog.SetLoggedInProviders(i.cfg.LoggedInProviders()) | ||
M packages/agent/settings_store.go +9 -0
| @@ -65,6 +65,15 @@ func (configSettingsStore) SetRespectGitignore(enabled bool) error { | |||
| 65 | 65 | return SaveConfig(cfg) | |
| 66 | 66 | } | |
| 67 | 67 | ||
| 68 | + | func (configSettingsStore) SetCompactMode(enabled bool) error { | |
| 69 | + | cfg, err := LoadConfig() | |
| 70 | + | if err != nil { | |
| 71 | + | return err | |
| 72 | + | } | |
| 73 | + | cfg.CompactMode = &enabled | |
| 74 | + | return SaveConfig(cfg) | |
| 75 | + | } | |
| 76 | + | ||
| 68 | 77 | func (configSettingsStore) SetReasoning(level string) error { | |
| 69 | 78 | cfg, err := LoadConfig() | |
| 70 | 79 | if err != nil { | |
| @@ -65,6 +65,15 @@ func (configSettingsStore) SetRespectGitignore(enabled bool) error { | |||||
| 65 | return SaveConfig(cfg) | 65 | return SaveConfig(cfg) | ||
| 66 | } | 66 | } | ||
| 67 | 67 | ||||
| 68 | + | func (configSettingsStore) SetCompactMode(enabled bool) error { | |||
| 69 | + | cfg, err := LoadConfig() | |||
| 70 | + | if err != nil { | |||
| 71 | + | return err | |||
| 72 | + | } | |||
| 73 | + | cfg.CompactMode = &enabled | |||
| 74 | + | return SaveConfig(cfg) | |||
| 75 | + | } | |||
| 76 | + | ||||
| 68 | func (configSettingsStore) SetReasoning(level string) error { | 77 | func (configSettingsStore) SetReasoning(level string) error { | ||
| 69 | cfg, err := LoadConfig() | 78 | cfg, err := LoadConfig() | ||
| 70 | if err != nil { | 79 | if err != nil { | ||
M packages/tui/view.go +19 -10
| @@ -144,6 +144,12 @@ type View struct { | |||
| 144 | 144 | // padded, background-tinted bubble. | |
| 145 | 145 | CompactUser bool | |
| 146 | 146 | ||
| 147 | + | // CompactMode reduces visual chrome in the transcript. Tool calls | |
| 148 | + | // render with a flat header and no bordered panel, and sent user | |
| 149 | + | // messages render without padded background bubbles. False keeps | |
| 150 | + | // the current spacious rendering. | |
| 151 | + | CompactMode bool | |
| 152 | + | ||
| 147 | 153 | // ExpandAll forces every long tool result to render in full. | |
| 148 | 154 | // Toggled from the tui by ctrl+o. When false, results longer than | |
| 149 | 155 | // ToolCollapseLines collapse to ToolCollapsePreview lines plus a | |
| @@ -647,7 +653,7 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||
| 647 | 653 | bar := v.Theme.BG(v.Theme.UserBubbleBG, v.Theme.FG256(v.Theme.Accent, "▌ ")) | |
| 648 | 654 | return bar + padded | |
| 649 | 655 | } | |
| 650 | - | if v.CompactUser { | |
| 656 | + | if v.CompactUser || v.CompactMode { | |
| 651 | 657 | // Compact: no tinted bubble background and no padding rows. | |
| 652 | 658 | // Each wrapped row is a quiet "▌ text" gutter line, matching | |
| 653 | 659 | // the flat tool-call header so user turns still segment the | |
| @@ -657,6 +663,9 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||
| 657 | 663 | innerWidth = 1 | |
| 658 | 664 | } | |
| 659 | 665 | row = func(content string) string { | |
| 666 | + | if v.CompactMode { | |
| 667 | + | content = v.Theme.FG256(v.Theme.Muted, content) | |
| 668 | + | } | |
| 660 | 669 | return v.Theme.FG256(v.Theme.Accent, "▌ ") + content | |
| 661 | 670 | } | |
| 662 | 671 | } | |
| @@ -675,7 +684,7 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||
| 675 | 684 | } | |
| 676 | 685 | } | |
| 677 | 686 | if len(bubble) > 0 { | |
| 678 | - | if v.CompactUser { | |
| 687 | + | if v.CompactUser || v.CompactMode { | |
| 679 | 688 | // No tinted padding rows in compact mode; the inter-message | |
| 680 | 689 | // blank from Build() already gives the turn breathing room. | |
| 681 | 690 | lines = append(lines, bubble...) | |
| @@ -737,7 +746,7 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||
| 737 | 746 | // from the matching ToolCallBlock so multiple calls | |
| 738 | 747 | // in one assistant message render as N adjacent boxes | |
| 739 | 748 | // instead of stacking unclosed top edges. | |
| 740 | - | if v.FlatTools { | |
| 749 | + | if v.FlatTools || v.CompactMode { | |
| 741 | 750 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | |
| 742 | 751 | if tr.IsError { | |
| 743 | 752 | lines = append(lines, flatToolBody(v.Theme, v.Theme.FG256(color, " error"))) | |
| @@ -816,7 +825,7 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string { | |||
| 816 | 825 | } | |
| 817 | 826 | v.liveBodyHigh[tc.ID] = high | |
| 818 | 827 | } | |
| 819 | - | if v.FlatTools { | |
| 828 | + | if v.FlatTools || v.CompactMode { | |
| 820 | 829 | for len(body) < high { | |
| 821 | 830 | body = append(body, "") | |
| 822 | 831 | } | |
| @@ -840,7 +849,7 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string { | |||
| 840 | 849 | // directly closed by the bottom. Avoids a blank interior row | |
| 841 | 850 | // for no-output tools. | |
| 842 | 851 | if tc.Result == "" { | |
| 843 | - | if v.FlatTools { | |
| 852 | + | if v.FlatTools || v.CompactMode { | |
| 844 | 853 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | |
| 845 | 854 | return lines | |
| 846 | 855 | } | |
| @@ -858,7 +867,7 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string { | |||
| 858 | 867 | if tc.Error { | |
| 859 | 868 | color = v.Theme.Error | |
| 860 | 869 | } | |
| 861 | - | if v.FlatTools { | |
| 870 | + | if v.FlatTools || v.CompactMode { | |
| 862 | 871 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | |
| 863 | 872 | body := toolResultBlock(v.Theme, tc.Result, flatToolBodyRenderWidth(width), color) | |
| 864 | 873 | for _, l := range v.collapseToolBody(body, false) { | |
| @@ -926,7 +935,7 @@ func (v *View) renderLiveToolBody(tc ToolCallView, width int) []string { | |||
| 926 | 935 | ||
| 927 | 936 | func (v *View) renderLiveBashCommand(command string, width int) []string { | |
| 928 | 937 | inner := toolBoxBodyRenderWidth(width) | |
| 929 | - | if v.FlatTools { | |
| 938 | + | if v.FlatTools || v.CompactMode { | |
| 930 | 939 | inner = flatToolBodyRenderWidth(width) | |
| 931 | 940 | } | |
| 932 | 941 | prompt := v.Theme.FG256(v.Theme.Muted, "$ ") | |
| @@ -962,7 +971,7 @@ func (v *View) renderLiveBashCommand(command string, width int) []string { | |||
| 962 | 971 | func (v *View) wrapLiveBody(body []string, width int) []string { | |
| 963 | 972 | body = v.collapseToolBody(body, false) | |
| 964 | 973 | out := make([]string, 0, len(body)) | |
| 965 | - | if v.FlatTools { | |
| 974 | + | if v.FlatTools || v.CompactMode { | |
| 966 | 975 | for _, l := range body { | |
| 967 | 976 | _, stripped := parseImageFootprint(l) | |
| 968 | 977 | out = append(out, flatToolBody(v.Theme, stripped)) | |
| @@ -1080,7 +1089,7 @@ func flatToolHeader(th Theme, label string, width int) string { | |||
| 1080 | 1089 | label = oneLineToolLabel(label) | |
| 1081 | 1090 | name, rest := splitToolLabel(label) | |
| 1082 | 1091 | margin := strings.Repeat(" ", toolBoxOuterMargin) | |
| 1083 | - | gutter := th.FG256(th.Accent, "▌") + " " | |
| 1092 | + | gutter := th.FG256(th.Assistant, "▌") + " " | |
| 1084 | 1093 | // Budget the visible text to the content width so a very long | |
| 1085 | 1094 | // argument summary doesn't run off the right edge; truncate with | |
| 1086 | 1095 | // an ellipsis like the box header does. | |
| @@ -1332,7 +1341,7 @@ func (v *View) renderToolResultContent(blocks []provider.Content, width, color i | |||
| 1332 | 1341 | switch bb := b.(type) { | |
| 1333 | 1342 | case provider.TextBlock: | |
| 1334 | 1343 | bodyWidth := toolBoxBodyRenderWidth(width) | |
| 1335 | - | if v.FlatTools { | |
| 1344 | + | if v.FlatTools || v.CompactMode { | |
| 1336 | 1345 | bodyWidth = flatToolBodyRenderWidth(width) | |
| 1337 | 1346 | } | |
| 1338 | 1347 | body = append(body, v.renderToolText(bb.Text, bodyWidth, color, sourcePath, startLine)...) | |
| @@ -144,6 +144,12 @@ type View struct { | |||||
| 144 | // padded, background-tinted bubble. | 144 | // padded, background-tinted bubble. | ||
| 145 | CompactUser bool | 145 | CompactUser bool | ||
| 146 | 146 | ||||
| 147 | + | // CompactMode reduces visual chrome in the transcript. Tool calls | |||
| 148 | + | // render with a flat header and no bordered panel, and sent user | |||
| 149 | + | // messages render without padded background bubbles. False keeps | |||
| 150 | + | // the current spacious rendering. | |||
| 151 | + | CompactMode bool | |||
| 152 | + | ||||
| 147 | // ExpandAll forces every long tool result to render in full. | 153 | // ExpandAll forces every long tool result to render in full. | ||
| 148 | // Toggled from the tui by ctrl+o. When false, results longer than | 154 | // Toggled from the tui by ctrl+o. When false, results longer than | ||
| 149 | // ToolCollapseLines collapse to ToolCollapsePreview lines plus a | 155 | // ToolCollapseLines collapse to ToolCollapsePreview lines plus a | ||
| @@ -647,7 +653,7 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||||
| 647 | bar := v.Theme.BG(v.Theme.UserBubbleBG, v.Theme.FG256(v.Theme.Accent, "▌ ")) | 653 | bar := v.Theme.BG(v.Theme.UserBubbleBG, v.Theme.FG256(v.Theme.Accent, "▌ ")) | ||
| 648 | return bar + padded | 654 | return bar + padded | ||
| 649 | } | 655 | } | ||
| 650 | - | if v.CompactUser { | 656 | + | if v.CompactUser || v.CompactMode { |
| 651 | // Compact: no tinted bubble background and no padding rows. | 657 | // Compact: no tinted bubble background and no padding rows. | ||
| 652 | // Each wrapped row is a quiet "▌ text" gutter line, matching | 658 | // Each wrapped row is a quiet "▌ text" gutter line, matching | ||
| 653 | // the flat tool-call header so user turns still segment the | 659 | // the flat tool-call header so user turns still segment the | ||
| @@ -657,6 +663,9 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||||
| 657 | innerWidth = 1 | 663 | innerWidth = 1 | ||
| 658 | } | 664 | } | ||
| 659 | row = func(content string) string { | 665 | row = func(content string) string { | ||
| 666 | + | if v.CompactMode { | |||
| 667 | + | content = v.Theme.FG256(v.Theme.Muted, content) | |||
| 668 | + | } | |||
| 660 | return v.Theme.FG256(v.Theme.Accent, "▌ ") + content | 669 | return v.Theme.FG256(v.Theme.Accent, "▌ ") + content | ||
| 661 | } | 670 | } | ||
| 662 | } | 671 | } | ||
| @@ -675,7 +684,7 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||||
| 675 | } | 684 | } | ||
| 676 | } | 685 | } | ||
| 677 | if len(bubble) > 0 { | 686 | if len(bubble) > 0 { | ||
| 678 | - | if v.CompactUser { | 687 | + | if v.CompactUser || v.CompactMode { |
| 679 | // No tinted padding rows in compact mode; the inter-message | 688 | // No tinted padding rows in compact mode; the inter-message | ||
| 680 | // blank from Build() already gives the turn breathing room. | 689 | // blank from Build() already gives the turn breathing room. | ||
| 681 | lines = append(lines, bubble...) | 690 | lines = append(lines, bubble...) | ||
| @@ -737,7 +746,7 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||||
| 737 | // from the matching ToolCallBlock so multiple calls | 746 | // from the matching ToolCallBlock so multiple calls | ||
| 738 | // in one assistant message render as N adjacent boxes | 747 | // in one assistant message render as N adjacent boxes | ||
| 739 | // instead of stacking unclosed top edges. | 748 | // instead of stacking unclosed top edges. | ||
| 740 | - | if v.FlatTools { | 749 | + | if v.FlatTools || v.CompactMode { |
| 741 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | 750 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | ||
| 742 | if tr.IsError { | 751 | if tr.IsError { | ||
| 743 | lines = append(lines, flatToolBody(v.Theme, v.Theme.FG256(color, " error"))) | 752 | lines = append(lines, flatToolBody(v.Theme, v.Theme.FG256(color, " error"))) | ||
| @@ -816,7 +825,7 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string { | |||||
| 816 | } | 825 | } | ||
| 817 | v.liveBodyHigh[tc.ID] = high | 826 | v.liveBodyHigh[tc.ID] = high | ||
| 818 | } | 827 | } | ||
| 819 | - | if v.FlatTools { | 828 | + | if v.FlatTools || v.CompactMode { |
| 820 | for len(body) < high { | 829 | for len(body) < high { | ||
| 821 | body = append(body, "") | 830 | body = append(body, "") | ||
| 822 | } | 831 | } | ||
| @@ -840,7 +849,7 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string { | |||||
| 840 | // directly closed by the bottom. Avoids a blank interior row | 849 | // directly closed by the bottom. Avoids a blank interior row | ||
| 841 | // for no-output tools. | 850 | // for no-output tools. | ||
| 842 | if tc.Result == "" { | 851 | if tc.Result == "" { | ||
| 843 | - | if v.FlatTools { | 852 | + | if v.FlatTools || v.CompactMode { |
| 844 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | 853 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | ||
| 845 | return lines | 854 | return lines | ||
| 846 | } | 855 | } | ||
| @@ -858,7 +867,7 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string { | |||||
| 858 | if tc.Error { | 867 | if tc.Error { | ||
| 859 | color = v.Theme.Error | 868 | color = v.Theme.Error | ||
| 860 | } | 869 | } | ||
| 861 | - | if v.FlatTools { | 870 | + | if v.FlatTools || v.CompactMode { |
| 862 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | 871 | lines = append(lines, flatToolHeader(v.Theme, label, width)) | ||
| 863 | body := toolResultBlock(v.Theme, tc.Result, flatToolBodyRenderWidth(width), color) | 872 | body := toolResultBlock(v.Theme, tc.Result, flatToolBodyRenderWidth(width), color) | ||
| 864 | for _, l := range v.collapseToolBody(body, false) { | 873 | for _, l := range v.collapseToolBody(body, false) { | ||
| @@ -926,7 +935,7 @@ func (v *View) renderLiveToolBody(tc ToolCallView, width int) []string { | |||||
| 926 | 935 | ||||
| 927 | func (v *View) renderLiveBashCommand(command string, width int) []string { | 936 | func (v *View) renderLiveBashCommand(command string, width int) []string { | ||
| 928 | inner := toolBoxBodyRenderWidth(width) | 937 | inner := toolBoxBodyRenderWidth(width) | ||
| 929 | - | if v.FlatTools { | 938 | + | if v.FlatTools || v.CompactMode { |
| 930 | inner = flatToolBodyRenderWidth(width) | 939 | inner = flatToolBodyRenderWidth(width) | ||
| 931 | } | 940 | } | ||
| 932 | prompt := v.Theme.FG256(v.Theme.Muted, "$ ") | 941 | prompt := v.Theme.FG256(v.Theme.Muted, "$ ") | ||
| @@ -962,7 +971,7 @@ func (v *View) renderLiveBashCommand(command string, width int) []string { | |||||
| 962 | func (v *View) wrapLiveBody(body []string, width int) []string { | 971 | func (v *View) wrapLiveBody(body []string, width int) []string { | ||
| 963 | body = v.collapseToolBody(body, false) | 972 | body = v.collapseToolBody(body, false) | ||
| 964 | out := make([]string, 0, len(body)) | 973 | out := make([]string, 0, len(body)) | ||
| 965 | - | if v.FlatTools { | 974 | + | if v.FlatTools || v.CompactMode { |
| 966 | for _, l := range body { | 975 | for _, l := range body { | ||
| 967 | _, stripped := parseImageFootprint(l) | 976 | _, stripped := parseImageFootprint(l) | ||
| 968 | out = append(out, flatToolBody(v.Theme, stripped)) | 977 | out = append(out, flatToolBody(v.Theme, stripped)) | ||
| @@ -1080,7 +1089,7 @@ func flatToolHeader(th Theme, label string, width int) string { | |||||
| 1080 | label = oneLineToolLabel(label) | 1089 | label = oneLineToolLabel(label) | ||
| 1081 | name, rest := splitToolLabel(label) | 1090 | name, rest := splitToolLabel(label) | ||
| 1082 | margin := strings.Repeat(" ", toolBoxOuterMargin) | 1091 | margin := strings.Repeat(" ", toolBoxOuterMargin) | ||
| 1083 | - | gutter := th.FG256(th.Accent, "▌") + " " | 1092 | + | gutter := th.FG256(th.Assistant, "▌") + " " |
| 1084 | // Budget the visible text to the content width so a very long | 1093 | // Budget the visible text to the content width so a very long | ||
| 1085 | // argument summary doesn't run off the right edge; truncate with | 1094 | // argument summary doesn't run off the right edge; truncate with | ||
| 1086 | // an ellipsis like the box header does. | 1095 | // an ellipsis like the box header does. | ||
| @@ -1332,7 +1341,7 @@ func (v *View) renderToolResultContent(blocks []provider.Content, width, color i | |||||
| 1332 | switch bb := b.(type) { | 1341 | switch bb := b.(type) { | ||
| 1333 | case provider.TextBlock: | 1342 | case provider.TextBlock: | ||
| 1334 | bodyWidth := toolBoxBodyRenderWidth(width) | 1343 | bodyWidth := toolBoxBodyRenderWidth(width) | ||
| 1335 | - | if v.FlatTools { | 1344 | + | if v.FlatTools || v.CompactMode { |
| 1336 | bodyWidth = flatToolBodyRenderWidth(width) | 1345 | bodyWidth = flatToolBodyRenderWidth(width) | ||
| 1337 | } | 1346 | } | ||
| 1338 | body = append(body, v.renderToolText(bb.Text, bodyWidth, color, sourcePath, startLine)...) | 1347 | body = append(body, v.renderToolText(bb.Text, bodyWidth, color, sourcePath, startLine)...) | ||
A packages/tui/view_compact_mode_test.go +72 -0
| @@ -0,0 +1,72 @@ | |||
| 1 | + | package tui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/json" | |
| 5 | + | "strings" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/patriceckhart/zot/packages/provider" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func compactModeUserMsg(text string) provider.Message { | |
| 12 | + | return provider.Message{ | |
| 13 | + | Role: provider.RoleUser, | |
| 14 | + | Content: []provider.Content{provider.TextBlock{Text: text}}, | |
| 15 | + | } | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func countGutterRows(plain string) int { | |
| 19 | + | n := 0 | |
| 20 | + | for _, line := range strings.Split(plain, "\n") { | |
| 21 | + | if strings.Contains(line, "▌") { | |
| 22 | + | n++ | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | return n | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func TestCompactModeDropsUserBubblePadding(t *testing.T) { | |
| 29 | + | spacious := View{Theme: Dark, Messages: []provider.Message{compactModeUserMsg("hello")}} | |
| 30 | + | spaciousPlain := stripANSI(strings.Join(spacious.Build(80), "\n")) | |
| 31 | + | if got := countGutterRows(spaciousPlain); got != 3 { | |
| 32 | + | t.Fatalf("default user bubble should be 3 gutter rows, got %d:\n%s", got, spaciousPlain) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | compact := View{Theme: Dark, CompactMode: true, Messages: []provider.Message{compactModeUserMsg("hello")}} | |
| 36 | + | compactPlain := stripANSI(strings.Join(compact.Build(80), "\n")) | |
| 37 | + | if got := countGutterRows(compactPlain); got != 1 { | |
| 38 | + | t.Fatalf("compact mode user message should be 1 gutter row, got %d:\n%s", got, compactPlain) | |
| 39 | + | } | |
| 40 | + | if !strings.Contains(compactPlain, "▌ hello") { | |
| 41 | + | t.Fatalf("compact mode lost user text or gutter:\n%s", compactPlain) | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func TestCompactModeDropsToolBoxBorders(t *testing.T) { | |
| 46 | + | args := json.RawMessage(`{"command":"echo hi"}`) | |
| 47 | + | compact := View{ | |
| 48 | + | Theme: Dark, | |
| 49 | + | CompactMode: true, | |
| 50 | + | ToolCalls: []ToolCallView{ | |
| 51 | + | {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Result: "hi\n"}, | |
| 52 | + | }, | |
| 53 | + | } | |
| 54 | + | compactPlain := stripANSI(strings.Join(compact.Build(80), "\n")) | |
| 55 | + | if strings.ContainsAny(compactPlain, "┌└│┐┘") { | |
| 56 | + | t.Fatalf("compact mode tool render still contains box glyphs:\n%s", compactPlain) | |
| 57 | + | } | |
| 58 | + | if !strings.Contains(compactPlain, "bash") || !strings.Contains(compactPlain, "hi") { | |
| 59 | + | t.Fatalf("compact mode tool render lost header or output:\n%s", compactPlain) | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | spacious := View{ | |
| 63 | + | Theme: Dark, | |
| 64 | + | ToolCalls: []ToolCallView{ | |
| 65 | + | {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Result: "hi\n"}, | |
| 66 | + | }, | |
| 67 | + | } | |
| 68 | + | spaciousPlain := stripANSI(strings.Join(spacious.Build(80), "\n")) | |
| 69 | + | if !strings.ContainsAny(spaciousPlain, "┌└│┐┘") { | |
| 70 | + | t.Fatalf("default tool render should contain box glyphs:\n%s", spaciousPlain) | |
| 71 | + | } | |
| 72 | + | } | |
| @@ -0,0 +1,72 @@ | |||||
| 1 | + | package tui | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/json" | |||
| 5 | + | "strings" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/patriceckhart/zot/packages/provider" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func compactModeUserMsg(text string) provider.Message { | |||
| 12 | + | return provider.Message{ | |||
| 13 | + | Role: provider.RoleUser, | |||
| 14 | + | Content: []provider.Content{provider.TextBlock{Text: text}}, | |||
| 15 | + | } | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func countGutterRows(plain string) int { | |||
| 19 | + | n := 0 | |||
| 20 | + | for _, line := range strings.Split(plain, "\n") { | |||
| 21 | + | if strings.Contains(line, "▌") { | |||
| 22 | + | n++ | |||
| 23 | + | } | |||
| 24 | + | } | |||
| 25 | + | return n | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func TestCompactModeDropsUserBubblePadding(t *testing.T) { | |||
| 29 | + | spacious := View{Theme: Dark, Messages: []provider.Message{compactModeUserMsg("hello")}} | |||
| 30 | + | spaciousPlain := stripANSI(strings.Join(spacious.Build(80), "\n")) | |||
| 31 | + | if got := countGutterRows(spaciousPlain); got != 3 { | |||
| 32 | + | t.Fatalf("default user bubble should be 3 gutter rows, got %d:\n%s", got, spaciousPlain) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | compact := View{Theme: Dark, CompactMode: true, Messages: []provider.Message{compactModeUserMsg("hello")}} | |||
| 36 | + | compactPlain := stripANSI(strings.Join(compact.Build(80), "\n")) | |||
| 37 | + | if got := countGutterRows(compactPlain); got != 1 { | |||
| 38 | + | t.Fatalf("compact mode user message should be 1 gutter row, got %d:\n%s", got, compactPlain) | |||
| 39 | + | } | |||
| 40 | + | if !strings.Contains(compactPlain, "▌ hello") { | |||
| 41 | + | t.Fatalf("compact mode lost user text or gutter:\n%s", compactPlain) | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func TestCompactModeDropsToolBoxBorders(t *testing.T) { | |||
| 46 | + | args := json.RawMessage(`{"command":"echo hi"}`) | |||
| 47 | + | compact := View{ | |||
| 48 | + | Theme: Dark, | |||
| 49 | + | CompactMode: true, | |||
| 50 | + | ToolCalls: []ToolCallView{ | |||
| 51 | + | {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Result: "hi\n"}, | |||
| 52 | + | }, | |||
| 53 | + | } | |||
| 54 | + | compactPlain := stripANSI(strings.Join(compact.Build(80), "\n")) | |||
| 55 | + | if strings.ContainsAny(compactPlain, "┌└│┐┘") { | |||
| 56 | + | t.Fatalf("compact mode tool render still contains box glyphs:\n%s", compactPlain) | |||
| 57 | + | } | |||
| 58 | + | if !strings.Contains(compactPlain, "bash") || !strings.Contains(compactPlain, "hi") { | |||
| 59 | + | t.Fatalf("compact mode tool render lost header or output:\n%s", compactPlain) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | spacious := View{ | |||
| 63 | + | Theme: Dark, | |||
| 64 | + | ToolCalls: []ToolCallView{ | |||
| 65 | + | {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Result: "hi\n"}, | |||
| 66 | + | }, | |||
| 67 | + | } | |||
| 68 | + | spaciousPlain := stripANSI(strings.Join(spacious.Build(80), "\n")) | |||
| 69 | + | if !strings.ContainsAny(spaciousPlain, "┌└│┐┘") { | |||
| 70 | + | t.Fatalf("default tool render should contain box glyphs:\n%s", spaciousPlain) | |||
| 71 | + | } | |||
| 72 | + | } | |||