Merge branch 'main' into feat/extension-doctor
4 files · +46 -8
M README.md +1 -1
| @@ -302,7 +302,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 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 | 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`. | |
| 305 | - | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line) or `lines` (separator lines above and below the input). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | |
| 305 | + | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | |
| 306 | 306 | - **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. | |
| 307 | 307 | - **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). | |
| 308 | 308 | - **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. | |
| @@ -302,7 +302,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 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 | - **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`. | ||
| 305 | - | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line) or `lines` (separator lines above and below the input). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | 305 | + | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). |
| 306 | - **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. | 306 | - **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. | ||
| 307 | - **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). | 307 | - **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). | ||
| 308 | - **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. | 308 | - **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/config.go +1 -1
| @@ -76,7 +76,7 @@ type Config struct { | |||
| 76 | 76 | CompactMode *bool `json:"compact_mode,omitempty"` | |
| 77 | 77 | ||
| 78 | 78 | // TUIInputStyle controls the main input rendering. Supported values: | |
| 79 | - | // "plain" (default) and "lines". | |
| 79 | + | // "plain" (default), "lines", and "block". | |
| 80 | 80 | TUIInputStyle string `json:"tui_input_style,omitempty"` | |
| 81 | 81 | ||
| 82 | 82 | // TUIStatusPosition controls whether model, usage, and cwd information | |
| @@ -76,7 +76,7 @@ type Config struct { | |||||
| 76 | CompactMode *bool `json:"compact_mode,omitempty"` | 76 | CompactMode *bool `json:"compact_mode,omitempty"` | ||
| 77 | 77 | ||||
| 78 | // TUIInputStyle controls the main input rendering. Supported values: | 78 | // TUIInputStyle controls the main input rendering. Supported values: | ||
| 79 | - | // "plain" (default) and "lines". | 79 | + | // "plain" (default), "lines", and "block". |
| 80 | TUIInputStyle string `json:"tui_input_style,omitempty"` | 80 | TUIInputStyle string `json:"tui_input_style,omitempty"` | ||
| 81 | 81 | ||||
| 82 | // TUIStatusPosition controls whether model, usage, and cwd information | 82 | // TUIStatusPosition controls whether model, usage, and cwd information | ||
M packages/agent/modes/interactive.go +13 -5
| @@ -1251,7 +1251,8 @@ func (i *Interactive) redraw() { | |||
| 1251 | 1251 | Telegram: i.telegramBridge != nil && i.telegramBridge.Active(), | |
| 1252 | 1252 | Cols: cols, | |
| 1253 | 1253 | }) | |
| 1254 | - | if tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines { | |
| 1254 | + | inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle) | |
| 1255 | + | if inputStyle == tui.InputStyleLines || inputStyle == tui.InputStyleBlock { | |
| 1255 | 1256 | i.ed.Prompt = "" | |
| 1256 | 1257 | } else { | |
| 1257 | 1258 | i.ed.Prompt = i.cfg.Theme.AccentBar(i.cfg.Theme.Accent) | |
| @@ -1262,9 +1263,15 @@ func (i *Interactive) redraw() { | |||
| 1262 | 1263 | workingLines = []string{" " + busyPrefix} | |
| 1263 | 1264 | } | |
| 1264 | 1265 | inputCursorOffset := 0 | |
| 1265 | - | if tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines { | |
| 1266 | + | inputCursorColOffset := 0 | |
| 1267 | + | switch inputStyle { | |
| 1268 | + | case tui.InputStyleLines: | |
| 1266 | 1269 | edLines = tui.InputLines(i.cfg.Theme, edLines, cols) | |
| 1267 | 1270 | inputCursorOffset = 1 | |
| 1271 | + | case tui.InputStyleBlock: | |
| 1272 | + | edLines = tui.InputBlock(i.cfg.Theme, edLines, cols) | |
| 1273 | + | inputCursorOffset = 1 | |
| 1274 | + | inputCursorColOffset = 2 | |
| 1268 | 1275 | } | |
| 1269 | 1276 | ||
| 1270 | 1277 | // "Sliding in" chips for messages the user typed while a turn is | |
| @@ -1313,7 +1320,7 @@ func (i *Interactive) redraw() { | |||
| 1313 | 1320 | if !i.swarmDialog.Active() { | |
| 1314 | 1321 | bottom = append(bottom, suggest...) | |
| 1315 | 1322 | bottom = append(bottom, queue...) | |
| 1316 | - | lineInput := tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines | |
| 1323 | + | lineInput := inputStyle == tui.InputStyleLines | |
| 1317 | 1324 | statusBelow := statusPosition == tui.StatusPositionBelowInput | |
| 1318 | 1325 | workingBelow := workingPosition == tui.WorkingPositionBelowInput | |
| 1319 | 1326 | ||
| @@ -1506,7 +1513,7 @@ func (i *Interactive) redraw() { | |||
| 1506 | 1513 | dialogLead = 1 | |
| 1507 | 1514 | } | |
| 1508 | 1515 | cursorRow := inputStartRow + inputCursorOffset + curR | |
| 1509 | - | cursorCol := curC | |
| 1516 | + | cursorCol := curC + inputCursorColOffset | |
| 1510 | 1517 | if i.btwDialog.Active() { | |
| 1511 | 1518 | if r, c := i.btwDialog.CursorPos(cols); r >= 0 { | |
| 1512 | 1519 | cursorRow = dialogLead + r | |
| @@ -2957,6 +2964,7 @@ func (i *Interactive) openSettingsDialog() { | |||
| 2957 | 2964 | inputStyleOptions := []settingsOption{ | |
| 2958 | 2965 | {value: tui.InputStylePlain, label: "plain", desc: "render the input as the normal prompt line"}, | |
| 2959 | 2966 | {value: tui.InputStyleLines, label: "lines", desc: "draw separator lines above and below the input"}, | |
| 2967 | + | {value: tui.InputStyleBlock, label: "block", desc: "render the input as a user-bubble-style block"}, | |
| 2960 | 2968 | } | |
| 2961 | 2969 | inputStyleChoice := 0 | |
| 2962 | 2970 | for idx, opt := range inputStyleOptions { | |
| @@ -3032,7 +3040,7 @@ func (i *Interactive) openSettingsDialog() { | |||
| 3032 | 3040 | { | |
| 3033 | 3041 | key: "tui_input_style", | |
| 3034 | 3042 | label: "input style", | |
| 3035 | - | desc: "choose between the plain prompt and a lined input area", | |
| 3043 | + | desc: "choose between the plain prompt, lines, and a block input area", | |
| 3036 | 3044 | options: inputStyleOptions, | |
| 3037 | 3045 | choice: inputStyleChoice, | |
| 3038 | 3046 | }, | |
| @@ -1251,7 +1251,8 @@ func (i *Interactive) redraw() { | |||||
| 1251 | Telegram: i.telegramBridge != nil && i.telegramBridge.Active(), | 1251 | Telegram: i.telegramBridge != nil && i.telegramBridge.Active(), | ||
| 1252 | Cols: cols, | 1252 | Cols: cols, | ||
| 1253 | }) | 1253 | }) | ||
| 1254 | - | if tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines { | 1254 | + | inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle) |
| 1255 | + | if inputStyle == tui.InputStyleLines || inputStyle == tui.InputStyleBlock { | |||
| 1255 | i.ed.Prompt = "" | 1256 | i.ed.Prompt = "" | ||
| 1256 | } else { | 1257 | } else { | ||
| 1257 | i.ed.Prompt = i.cfg.Theme.AccentBar(i.cfg.Theme.Accent) | 1258 | i.ed.Prompt = i.cfg.Theme.AccentBar(i.cfg.Theme.Accent) | ||
| @@ -1262,9 +1263,15 @@ func (i *Interactive) redraw() { | |||||
| 1262 | workingLines = []string{" " + busyPrefix} | 1263 | workingLines = []string{" " + busyPrefix} | ||
| 1263 | } | 1264 | } | ||
| 1264 | inputCursorOffset := 0 | 1265 | inputCursorOffset := 0 | ||
| 1265 | - | if tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines { | 1266 | + | inputCursorColOffset := 0 |
| 1267 | + | switch inputStyle { | |||
| 1268 | + | case tui.InputStyleLines: | |||
| 1266 | edLines = tui.InputLines(i.cfg.Theme, edLines, cols) | 1269 | edLines = tui.InputLines(i.cfg.Theme, edLines, cols) | ||
| 1267 | inputCursorOffset = 1 | 1270 | inputCursorOffset = 1 | ||
| 1271 | + | case tui.InputStyleBlock: | |||
| 1272 | + | edLines = tui.InputBlock(i.cfg.Theme, edLines, cols) | |||
| 1273 | + | inputCursorOffset = 1 | |||
| 1274 | + | inputCursorColOffset = 2 | |||
| 1268 | } | 1275 | } | ||
| 1269 | 1276 | ||||
| 1270 | // "Sliding in" chips for messages the user typed while a turn is | 1277 | // "Sliding in" chips for messages the user typed while a turn is | ||
| @@ -1313,7 +1320,7 @@ func (i *Interactive) redraw() { | |||||
| 1313 | if !i.swarmDialog.Active() { | 1320 | if !i.swarmDialog.Active() { | ||
| 1314 | bottom = append(bottom, suggest...) | 1321 | bottom = append(bottom, suggest...) | ||
| 1315 | bottom = append(bottom, queue...) | 1322 | bottom = append(bottom, queue...) | ||
| 1316 | - | lineInput := tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines | 1323 | + | lineInput := inputStyle == tui.InputStyleLines |
| 1317 | statusBelow := statusPosition == tui.StatusPositionBelowInput | 1324 | statusBelow := statusPosition == tui.StatusPositionBelowInput | ||
| 1318 | workingBelow := workingPosition == tui.WorkingPositionBelowInput | 1325 | workingBelow := workingPosition == tui.WorkingPositionBelowInput | ||
| 1319 | 1326 | ||||
| @@ -1506,7 +1513,7 @@ func (i *Interactive) redraw() { | |||||
| 1506 | dialogLead = 1 | 1513 | dialogLead = 1 | ||
| 1507 | } | 1514 | } | ||
| 1508 | cursorRow := inputStartRow + inputCursorOffset + curR | 1515 | cursorRow := inputStartRow + inputCursorOffset + curR | ||
| 1509 | - | cursorCol := curC | 1516 | + | cursorCol := curC + inputCursorColOffset |
| 1510 | if i.btwDialog.Active() { | 1517 | if i.btwDialog.Active() { | ||
| 1511 | if r, c := i.btwDialog.CursorPos(cols); r >= 0 { | 1518 | if r, c := i.btwDialog.CursorPos(cols); r >= 0 { | ||
| 1512 | cursorRow = dialogLead + r | 1519 | cursorRow = dialogLead + r | ||
| @@ -2957,6 +2964,7 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 2957 | inputStyleOptions := []settingsOption{ | 2964 | inputStyleOptions := []settingsOption{ | ||
| 2958 | {value: tui.InputStylePlain, label: "plain", desc: "render the input as the normal prompt line"}, | 2965 | {value: tui.InputStylePlain, label: "plain", desc: "render the input as the normal prompt line"}, | ||
| 2959 | {value: tui.InputStyleLines, label: "lines", desc: "draw separator lines above and below the input"}, | 2966 | {value: tui.InputStyleLines, label: "lines", desc: "draw separator lines above and below the input"}, | ||
| 2967 | + | {value: tui.InputStyleBlock, label: "block", desc: "render the input as a user-bubble-style block"}, | |||
| 2960 | } | 2968 | } | ||
| 2961 | inputStyleChoice := 0 | 2969 | inputStyleChoice := 0 | ||
| 2962 | for idx, opt := range inputStyleOptions { | 2970 | for idx, opt := range inputStyleOptions { | ||
| @@ -3032,7 +3040,7 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 3032 | { | 3040 | { | ||
| 3033 | key: "tui_input_style", | 3041 | key: "tui_input_style", | ||
| 3034 | label: "input style", | 3042 | label: "input style", | ||
| 3035 | - | desc: "choose between the plain prompt and a lined input area", | 3043 | + | desc: "choose between the plain prompt, lines, and a block input area", |
| 3036 | options: inputStyleOptions, | 3044 | options: inputStyleOptions, | ||
| 3037 | choice: inputStyleChoice, | 3045 | choice: inputStyleChoice, | ||
| 3038 | }, | 3046 | }, | ||
M packages/tui/layout.go +31 -1
| @@ -5,6 +5,7 @@ import "strings" | |||
| 5 | 5 | const ( | |
| 6 | 6 | InputStylePlain = "plain" | |
| 7 | 7 | InputStyleLines = "lines" | |
| 8 | + | InputStyleBlock = "block" | |
| 8 | 9 | ||
| 9 | 10 | StatusPositionAboveInput = "above_input" | |
| 10 | 11 | StatusPositionBelowInput = "below_input" | |
| @@ -15,8 +16,10 @@ const ( | |||
| 15 | 16 | ||
| 16 | 17 | func NormalizeInputStyle(v string) string { | |
| 17 | 18 | switch strings.ToLower(strings.TrimSpace(v)) { | |
| 18 | - | case InputStyleLines, "line", "boxed", "box": | |
| 19 | + | case InputStyleLines, "line": | |
| 19 | 20 | return InputStyleLines | |
| 21 | + | case InputStyleBlock, "bubble", "boxed", "box": | |
| 22 | + | return InputStyleBlock | |
| 20 | 23 | default: | |
| 21 | 24 | return InputStylePlain | |
| 22 | 25 | } | |
| @@ -52,6 +55,33 @@ func InputLines(th Theme, lines []string, width int) []string { | |||
| 52 | 55 | return out | |
| 53 | 56 | } | |
| 54 | 57 | ||
| 58 | + | func InputBlock(th Theme, lines []string, width int) []string { | |
| 59 | + | if width < 1 { | |
| 60 | + | width = 1 | |
| 61 | + | } | |
| 62 | + | bubbleW := width - 2 | |
| 63 | + | if bubbleW < 1 { | |
| 64 | + | bubbleW = 1 | |
| 65 | + | } | |
| 66 | + | row := func(content string) string { | |
| 67 | + | visible := visibleWidth(content) | |
| 68 | + | if visible < bubbleW { | |
| 69 | + | content += strings.Repeat(" ", bubbleW-visible) | |
| 70 | + | } | |
| 71 | + | bg := sgrBGColor(th.UserBubbleBG) | |
| 72 | + | bar := bg + th.FG256(th.Accent, "▌ ") | |
| 73 | + | body := bg + strings.ReplaceAll(content, reset, reset+bg) + reset | |
| 74 | + | return bar + body | |
| 75 | + | } | |
| 76 | + | out := make([]string, 0, len(lines)+2) | |
| 77 | + | out = append(out, row("")) | |
| 78 | + | for _, line := range lines { | |
| 79 | + | out = append(out, row(line)) | |
| 80 | + | } | |
| 81 | + | out = append(out, row("")) | |
| 82 | + | return out | |
| 83 | + | } | |
| 84 | + | ||
| 55 | 85 | func CursorColor256(index int) string { | |
| 56 | 86 | r, g, b := xterm256RGB(index) | |
| 57 | 87 | return "\x1b]12;rgb:" + hexByte(r) + "/" + hexByte(g) + "/" + hexByte(b) + "\x07" | |
| @@ -5,6 +5,7 @@ import "strings" | |||||
| 5 | const ( | 5 | const ( | ||
| 6 | InputStylePlain = "plain" | 6 | InputStylePlain = "plain" | ||
| 7 | InputStyleLines = "lines" | 7 | InputStyleLines = "lines" | ||
| 8 | + | InputStyleBlock = "block" | |||
| 8 | 9 | ||||
| 9 | StatusPositionAboveInput = "above_input" | 10 | StatusPositionAboveInput = "above_input" | ||
| 10 | StatusPositionBelowInput = "below_input" | 11 | StatusPositionBelowInput = "below_input" | ||
| @@ -15,8 +16,10 @@ const ( | |||||
| 15 | 16 | ||||
| 16 | func NormalizeInputStyle(v string) string { | 17 | func NormalizeInputStyle(v string) string { | ||
| 17 | switch strings.ToLower(strings.TrimSpace(v)) { | 18 | switch strings.ToLower(strings.TrimSpace(v)) { | ||
| 18 | - | case InputStyleLines, "line", "boxed", "box": | 19 | + | case InputStyleLines, "line": |
| 19 | return InputStyleLines | 20 | return InputStyleLines | ||
| 21 | + | case InputStyleBlock, "bubble", "boxed", "box": | |||
| 22 | + | return InputStyleBlock | |||
| 20 | default: | 23 | default: | ||
| 21 | return InputStylePlain | 24 | return InputStylePlain | ||
| 22 | } | 25 | } | ||
| @@ -52,6 +55,33 @@ func InputLines(th Theme, lines []string, width int) []string { | |||||
| 52 | return out | 55 | return out | ||
| 53 | } | 56 | } | ||
| 54 | 57 | ||||
| 58 | + | func InputBlock(th Theme, lines []string, width int) []string { | |||
| 59 | + | if width < 1 { | |||
| 60 | + | width = 1 | |||
| 61 | + | } | |||
| 62 | + | bubbleW := width - 2 | |||
| 63 | + | if bubbleW < 1 { | |||
| 64 | + | bubbleW = 1 | |||
| 65 | + | } | |||
| 66 | + | row := func(content string) string { | |||
| 67 | + | visible := visibleWidth(content) | |||
| 68 | + | if visible < bubbleW { | |||
| 69 | + | content += strings.Repeat(" ", bubbleW-visible) | |||
| 70 | + | } | |||
| 71 | + | bg := sgrBGColor(th.UserBubbleBG) | |||
| 72 | + | bar := bg + th.FG256(th.Accent, "▌ ") | |||
| 73 | + | body := bg + strings.ReplaceAll(content, reset, reset+bg) + reset | |||
| 74 | + | return bar + body | |||
| 75 | + | } | |||
| 76 | + | out := make([]string, 0, len(lines)+2) | |||
| 77 | + | out = append(out, row("")) | |||
| 78 | + | for _, line := range lines { | |||
| 79 | + | out = append(out, row(line)) | |||
| 80 | + | } | |||
| 81 | + | out = append(out, row("")) | |||
| 82 | + | return out | |||
| 83 | + | } | |||
| 84 | + | ||||
| 55 | func CursorColor256(index int) string { | 85 | func CursorColor256(index int) string { | ||
| 56 | r, g, b := xterm256RGB(index) | 86 | r, g, b := xterm256RGB(index) | ||
| 57 | return "\x1b]12;rgb:" + hexByte(r) + "/" + hexByte(g) + "/" + hexByte(b) + "\x07" | 87 | return "\x1b]12;rgb:" + hexByte(r) + "/" + hexByte(g) + "/" + hexByte(b) + "\x07" | ||