mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
feat(tui): add compact user-input rendering
Sent user messages render as a padded, background-tinted bubble by default — a blank tinted row above and below the text plus a left accent bar — so even a one-line prompt occupies three rows. Add an opt-in compact mode that collapses each turn to a single quiet gutter line per wrapped row: no padding rows, no background tint. Gated behind a non-breaking toggle (default stays the bubble): - config.json "compact_input": true - ZOT_COMPACT_INPUT env var overrides the config for a single run Sibling to the flat tool-render toggle; same TUI-layer-only scope.
This commit is contained in:
parent
cf4ee29d79
commit
9484dbcb8a
6 changed files with 164 additions and 6 deletions
21
README.md
21
README.md
|
|
@ -587,6 +587,27 @@ ZOT_FLAT_TOOLS=0 zot # boxes, even if config.json says "flat"
|
|||
|
||||
Either way, theme colors still drive the rendering (the header uses your accent/foreground, output uses the tool-output color) and `ctrl+o` still expands a truncated result.
|
||||
|
||||
## Compact input
|
||||
|
||||
By default a message you send renders as a padded, background-tinted bubble: a blank tinted row above and below the text, with a `▌` accent bar down the left. So even a one-line prompt occupies three rows. Set `compact_input` to collapse it to a single quiet `▌ your text` gutter line per wrapped row — no padding rows, no background tint.
|
||||
|
||||
```json
|
||||
{
|
||||
"compact_input": true
|
||||
}
|
||||
```
|
||||
|
||||
| Value | Effect |
|
||||
|---|---|
|
||||
| unset / `false` (default) | Padded, background-tinted user bubble. |
|
||||
| `true` | One quiet gutter line per wrapped row. |
|
||||
|
||||
The `ZOT_COMPACT_INPUT` env var overrides the config for a single run (`1`/`true`/`on`/`compact` force compact; `0`/`false`/`off`/`bubble` force the bubble):
|
||||
|
||||
```sh
|
||||
ZOT_COMPACT_INPUT=1 zot # compact, just this run
|
||||
```
|
||||
|
||||
## Queued messages
|
||||
|
||||
You can keep typing while the agent is working. Pressing `enter` during a turn queues the message instead of interrupting: it shows up above the status bar as `sliding in: <text>` and is delivered as the next user turn the moment the current one finishes. Queue as many as you want; they run in order. `esc` cancels the active turn and drops the queue so a runaway turn doesn't flood you with stale follow-ups; `ctrl+c` while busy arms the exit hint instead of interrupting, a second `ctrl+c` within two seconds exits zot.
|
||||
|
|
|
|||
|
|
@ -967,6 +967,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
|
|||
RespectGitignore: initialCfg.RespectGitignore,
|
||||
ThemeName: initialCfg.Theme,
|
||||
FlatTools: initialCfg.FlatToolRender(),
|
||||
CompactUser: initialCfg.CompactUserInput(),
|
||||
ExtensionThemes: extMgr.ThemeOptions,
|
||||
AutoSwarmSystemAddendum: AutoSwarmSystemAddendum,
|
||||
SettingsStore: configSettingsStore{},
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ type Config struct {
|
|||
// set ("1"/"true" forces flat, "0"/"false" forces box).
|
||||
ToolRender string `json:"tool_render,omitempty"`
|
||||
|
||||
// CompactInput renders sent user messages as a single quiet gutter
|
||||
// line instead of a padded, background-tinted bubble. nil/false
|
||||
// (the default) keeps the bubble. The ZOT_COMPACT_INPUT env var
|
||||
// overrides this when set.
|
||||
CompactInput *bool `json:"compact_input,omitempty"`
|
||||
|
||||
// QuickModelShortcuts maps slots 1-9 to provider/model pairs used by
|
||||
// Ctrl+1..9. Cmd+1..9 may also work on terminals that forward Super.
|
||||
QuickModelShortcuts []QuickModelShortcut `json:"quick_model_shortcuts,omitempty"`
|
||||
|
|
@ -121,6 +127,24 @@ func (c Config) FlatToolRender() bool {
|
|||
return strings.EqualFold(strings.TrimSpace(c.ToolRender), "flat")
|
||||
}
|
||||
|
||||
// CompactUserInput reports whether sent user messages should render as
|
||||
// a single quiet gutter line instead of a padded, background-tinted
|
||||
// bubble. The ZOT_COMPACT_INPUT env var takes precedence over the
|
||||
// config when set: "1"/"true"/"yes"/"on" force compact, "0"/"false"/
|
||||
// "no"/"off" force the bubble. Otherwise the config's compact_input
|
||||
// is consulted (nil/false means the bubble).
|
||||
func (c Config) CompactUserInput() bool {
|
||||
if v := strings.TrimSpace(strings.ToLower(os.Getenv("ZOT_COMPACT_INPUT"))); v != "" {
|
||||
switch v {
|
||||
case "1", "true", "yes", "on", "compact":
|
||||
return true
|
||||
case "0", "false", "no", "off", "bubble":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return c.CompactInput != nil && *c.CompactInput
|
||||
}
|
||||
|
||||
// AuthPath returns the path to auth.json.
|
||||
func AuthPath() string { return filepath.Join(ZotHome(), "auth.json") }
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ type InteractiveConfig struct {
|
|||
// resolved tool_render config / ZOT_FLAT_TOOLS env at startup.
|
||||
FlatTools bool
|
||||
|
||||
// CompactUser renders sent user messages as a single quiet gutter
|
||||
// line instead of a padded, tinted bubble. Mirrors the resolved
|
||||
// compact_input config / ZOT_COMPACT_INPUT env at startup.
|
||||
CompactUser bool
|
||||
|
||||
// QuickModelShortcuts maps slots 1-9 to provider/model pairs. The
|
||||
// shortcuts are Ctrl+1..9. Cmd+1..9 may also work when the terminal
|
||||
// forwards Command/Super keypresses, but Ctrl is the displayed chord.
|
||||
|
|
@ -478,9 +483,10 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
|
|||
i := &Interactive{
|
||||
cfg: cfg,
|
||||
view: &tui.View{
|
||||
Theme: cfg.Theme,
|
||||
ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled),
|
||||
FlatTools: cfg.FlatTools,
|
||||
Theme: cfg.Theme,
|
||||
ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled),
|
||||
FlatTools: cfg.FlatTools,
|
||||
CompactUser: cfg.CompactUser,
|
||||
},
|
||||
// Prompt is the standard half-block accent bar used by chat
|
||||
// speaker labels too, so the input gutter matches the rest
|
||||
|
|
|
|||
|
|
@ -138,6 +138,12 @@ type View struct {
|
|||
// False (the default) keeps the bordered box.
|
||||
FlatTools bool
|
||||
|
||||
// CompactUser renders sent user messages as a single quiet gutter
|
||||
// line per wrapped row instead of a tinted bubble with a blank
|
||||
// padding row above and below. False (the default) keeps the
|
||||
// padded, background-tinted bubble.
|
||||
CompactUser bool
|
||||
|
||||
// ExpandAll forces every long tool result to render in full.
|
||||
// Toggled from the tui by ctrl+o. When false, results longer than
|
||||
// ToolCollapseLines collapse to ToolCollapsePreview lines plus a
|
||||
|
|
@ -641,6 +647,19 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str
|
|||
bar := v.Theme.BG(v.Theme.UserBubbleBG, v.Theme.FG256(v.Theme.Accent, "▌ "))
|
||||
return bar + padded
|
||||
}
|
||||
if v.CompactUser {
|
||||
// Compact: no tinted bubble background and no padding rows.
|
||||
// Each wrapped row is a quiet "▌ text" gutter line, matching
|
||||
// the flat tool-call header so user turns still segment the
|
||||
// chat without the loud panel.
|
||||
innerWidth = width - 2
|
||||
if innerWidth < 1 {
|
||||
innerWidth = 1
|
||||
}
|
||||
row = func(content string) string {
|
||||
return v.Theme.FG256(v.Theme.Accent, "▌ ") + content
|
||||
}
|
||||
}
|
||||
var bubble []string
|
||||
for _, c := range m.Content {
|
||||
switch b := c.(type) {
|
||||
|
|
@ -656,9 +675,15 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str
|
|||
}
|
||||
}
|
||||
if len(bubble) > 0 {
|
||||
lines = append(lines, row(""))
|
||||
lines = append(lines, bubble...)
|
||||
lines = append(lines, row(""))
|
||||
if v.CompactUser {
|
||||
// No tinted padding rows in compact mode; the inter-message
|
||||
// blank from Build() already gives the turn breathing room.
|
||||
lines = append(lines, bubble...)
|
||||
} else {
|
||||
lines = append(lines, row(""))
|
||||
lines = append(lines, bubble...)
|
||||
lines = append(lines, row(""))
|
||||
}
|
||||
}
|
||||
case provider.RoleAssistant:
|
||||
// Assistant rows: no speaker label either. Prose still gets a
|
||||
|
|
|
|||
81
packages/tui/view_compact_user_test.go
Normal file
81
packages/tui/view_compact_user_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/patriceckhart/zot/packages/provider"
|
||||
)
|
||||
|
||||
func userMsg(text string) provider.Message {
|
||||
return provider.Message{
|
||||
Role: provider.RoleUser,
|
||||
Content: []provider.Content{provider.TextBlock{Text: text}},
|
||||
}
|
||||
}
|
||||
|
||||
// userBubbleRows counts the rows that carry the user gutter bar "▌".
|
||||
func userBubbleRows(plain string) int {
|
||||
n := 0
|
||||
for _, l := range strings.Split(plain, "\n") {
|
||||
if strings.Contains(l, "▌") {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// A one-line prompt is a 3-row bubble by default (blank pad, text,
|
||||
// blank pad); compact mode collapses it to a single text row.
|
||||
func TestCompactUserDropsPaddingRows(t *testing.T) {
|
||||
bubble := View{Theme: Dark, Messages: []provider.Message{userMsg("hello")}}
|
||||
bubblePlain := stripANSI(strings.Join(bubble.Build(80), "\n"))
|
||||
if got := userBubbleRows(bubblePlain); got != 3 {
|
||||
t.Fatalf("default bubble should be 3 gutter rows (pad+text+pad), got %d:\n%s", got, bubblePlain)
|
||||
}
|
||||
|
||||
compact := View{Theme: Dark, CompactUser: true, Messages: []provider.Message{userMsg("hello")}}
|
||||
compactPlain := stripANSI(strings.Join(compact.Build(80), "\n"))
|
||||
if got := userBubbleRows(compactPlain); got != 1 {
|
||||
t.Fatalf("compact user should be a single gutter row, got %d:\n%s", got, compactPlain)
|
||||
}
|
||||
if !strings.Contains(compactPlain, "▌ hello") {
|
||||
t.Fatalf("compact user lost the gutter or text:\n%s", compactPlain)
|
||||
}
|
||||
}
|
||||
|
||||
// Compact mode must never paint the bubble background tint. The tint
|
||||
// is an SGR background sequence (ESC[48;...m); the compact path uses
|
||||
// only a foreground-colored gutter, so no 48; should appear on the
|
||||
// user rows.
|
||||
func TestCompactUserHasNoBackgroundTint(t *testing.T) {
|
||||
compact := View{Theme: Dark, CompactUser: true, Messages: []provider.Message{userMsg("hi there")}}
|
||||
raw := strings.Join(compact.Build(80), "\n")
|
||||
if strings.Contains(raw, "[48;") {
|
||||
t.Fatalf("compact user should not paint a background tint:\n%q", raw)
|
||||
}
|
||||
|
||||
// The default bubble does tint, so this is a meaningful assertion.
|
||||
bubble := View{Theme: Dark, Messages: []provider.Message{userMsg("hi there")}}
|
||||
if rawB := strings.Join(bubble.Build(80), "\n"); !strings.Contains(rawB, "[48;") {
|
||||
t.Fatalf("default bubble was expected to paint a background tint:\n%q", rawB)
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-line prompts keep every wrapped row in compact mode, just
|
||||
// without the surrounding padding rows.
|
||||
func TestCompactUserKeepsAllWrappedRows(t *testing.T) {
|
||||
long := strings.Repeat("word ", 60) // forces several wrapped rows
|
||||
compact := View{Theme: Dark, CompactUser: true, Messages: []provider.Message{userMsg(long)}}
|
||||
plain := stripANSI(strings.Join(compact.Build(40), "\n"))
|
||||
rows := userBubbleRows(plain)
|
||||
if rows < 3 {
|
||||
t.Fatalf("expected multiple wrapped gutter rows, got %d:\n%s", rows, plain)
|
||||
}
|
||||
// No empty gutter rows (that would be leftover padding).
|
||||
for _, l := range strings.Split(plain, "\n") {
|
||||
if strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(l), "▌")) == "" && strings.Contains(l, "▌") {
|
||||
t.Fatalf("compact user emitted an empty padding row:\n%s", plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue