Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
set -euo pipefail
lua5.4 scripts/test_settings.lua
lua5.4 scripts/test_history.lua
lua5.4 scripts/test_fold.lua
python3 scripts/test_parse_refs.py
python3 scripts/test_json_extract.py
WEZAI_COMPOSER_TEST=1 python3 plugin/composer.py --self-test
5 changes: 4 additions & 1 deletion GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Practical examples for every major surface: Ask, the palette, `@git`, `@kube`, `
| Pane | Role |
|------|------|
| **Left (shell)** | Your real terminal. cwd, git repo, commands you run. Stay focused here. |
| **Right (wezai)** | Output only — answers, diffs, `git status`, terraform validate, docker ps, weather, progress. Not a shell. While Ask/Edit or a catalog command waits, this pane scrolls a spinner / timed status so it does not look frozen. |
| **Right (wezai)** | Output only — the question you asked, answers, diffs, `git status`, terraform validate, docker ps, weather, progress. Not a shell. While Ask/Edit or a catalog command waits, this pane scrolls a spinner / timed status so it does not look frozen. |

| Entry point | When to use it |
|-------------|----------------|
Expand All @@ -32,6 +32,8 @@ CTRL+I splits a **composer** under your shell. The right-hand wezai pane stays v

Pinned `@` / `#` files stay in context for follow-up questions until you **Clear**. **Compact** (palette, or type `compact`) shrinks the conversation and sticky selections but keeps those file refs.

Each Ask / `#` edit turn prints a **`you`** block with the line you typed (including `@` / `#` refs) so the log is readable later. Long pastes fold to a short preview plus `… folded N more lines`; palette **Show last question** reprints the full text.

### Plain questions

```
Expand Down Expand Up @@ -146,6 +148,7 @@ Type to fuzzy-filter. Labels start with namespaces so filtering is easy:
| **Edit file (`#path …`)** | Opens Ask so you can type an edit |
| **Undo last edit** | Restore last `#` write from its wezai backup (or in-memory prior text if backups are off) |
| **Copy last command** | Clipboard: last AI command, else shell history, else scrollback |
| **Show last question** | Reprint the last Ask/`#` line (unfolded) |
| **Re-ask last question (shorter)** | Same question, shorter answer |
| **Pick model…** | Switch model for next requests (`models` list / `WEZAI_MODELS`) |
| **Compact chat (keep @/# files)** | Shrink conversation + sticky selection; keep pinned files |
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Do **not** leave `update_all()` / `reload_configuration()` at config file scope
| `CTRL+ALT+W` | Palette scoped to `@weather` (not `CTRL+SHIFT+W` — that is WezTerm’s close tab) |
| `CTRL+SHIFT+H` | Palette scoped to `@history` (fuzzy unique commands; Delete like fish) |

Stay on your **shell** pane. The right split is **output only** (answers, diffs, git/kube/tf/docker/weather status). Catalog commands print immediately and spin `waiting…` until output arrives, so a slow `@weather:now` does not look frozen. Don’t run git from that pane — wezai always uses your shell’s cwd.
Stay on your **shell** pane. The right split is **output only** (your question, answers, diffs, git/kube/tf/docker/weather status). Ask/`#` turns echo a **`you`** block with what you typed; long pastes fold with a leftover-line note (palette **Show last question** reprints the full text). Catalog commands print immediately and spin `waiting…` until output arrives, so a slow `@weather:now` does not look frozen. Don’t run git from that pane — wezai always uses your shell’s cwd.

```
CTRL+SHIFT+P → type @git:status → Enter
Expand Down Expand Up @@ -171,7 +171,14 @@ wezai.apply_to_config(config, {
-- Dialect (fish/zsh/bash/…) is auto-appended from the active shell pane / $SHELL.
-- system_prompt = "You are a concise terminal assistant. …",

ai_pane = { enabled = true, direction = "Right", size_percent = 35, pad_cols = 2 },
ai_pane = {
enabled = true,
direction = "Right",
size_percent = 35,
pad_cols = 2,
question_fold_lines = 8,
question_fold_chars = 600,
},
history = {
max_shell = 20000, -- unique commands indexed (newest first)
palette_n = 200, -- unified CTRL+SHIFT+P
Expand Down Expand Up @@ -213,6 +220,7 @@ plugin/
init.lua -- entry, keybindings, ask/edit orchestration
palette.lua -- CTRL+SHIFT+P unified palette
ui.lua -- output pane, styling, InputSelector
fold.lua -- fold long Ask/Edit questions in the output pane
session.lua -- chat memory + pinned @/# files + drafts
history.lua -- shell/scrollback history (auto-detect fish/zsh/bash)
history_store.lua / history_edit.py -- unique index, fuzzy, fish-style delete
Expand Down
12 changes: 8 additions & 4 deletions SPECS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ plugin/
util.lua -- paths, files, JSON parse, run_cmd (pcall), resolve_executable, large-file read, version_label

ui.lua -- AI pane lifecycle, styling, InputSelector, usage banner, start_progress / start_busy
fold.lua -- fold long Ask/Edit question text for the write-only AI pane
session.lua -- per-tab chat memory + pinned @/# files + draft + last edit
shell.lua -- shell detect, OS platform hint, risk gate, clipboard, send_command
context.lua -- @ / # parsing (@@ alias), dir walk, token budget, redaction
Expand Down Expand Up @@ -180,7 +181,9 @@ Command labels print as `(fish/macos)` style when showing suggested commands.

### 5.1 Ask (`keybinding`, default often remapped to CTRL+I by users)

Flow: **composer pane** (split of the shell, AI log stays visible) → parse refs → `context.prepare_request` → `providers.ask` → print message/command → optional `shell.send_command`.
Flow: **composer pane** (split of the shell, AI log stays visible) → parse refs → `context.prepare_request` → `providers.ask` → print the **question** (`▶ you`, folded if long) then message/command → optional `shell.send_command`.

**Question echo:** Ask and `#` edit print whatever was typed (`source_line` when present, else `user_text`) under a `you` label after the turn rule, before progress. Long pastes fold to the first `ai_pane.question_fold_lines` lines / `question_fold_chars` (defaults 8 / 600); leftover lines show as `… folded N more lines (M chars)`. The write-only pane cannot interactively expand — palette **Show last question** reprints the full text. Secrets are redacted the same way as the prompt. Catalog AI helpers echo their `user_text` tag (`git:msg`, `kube:diagnose`, …).

If `composer.enabled = false` or python3/`composer.py` is missing, fall back to WezTerm `PromptInputLine` (full-window overlay). Esc in the composer **saves a draft** and restores it on the next CTRL+I.

Expand Down Expand Up @@ -233,7 +236,7 @@ Unified fuzzy `InputSelector` with scopes:
- `weather` (`CTRL+ALT+W` — not `CTRL+SHIFT+W`, which is WezTerm CloseCurrentTab)
- `history` (`CTRL+SHIFT+H`) and filtered history scopes

Core palette rows include: Ask, Ask+pane, Fix last error, Explain last command, Attach/Edit file (fuzzy), Undo edit, Copy last command, Shorter re-ask, Pick model, **Compact chat (keep @/# files)**, **Clear chat + file context**, **Show wezai install**, **Update wezai plugin**.
Core palette rows include: Ask, Ask+pane, Fix last error, Explain last command, Attach/Edit file (fuzzy), Undo edit, Copy last command, **Show last question**, Shorter re-ask, Pick model, **Compact chat (keep @/# files)**, **Clear chat + file context**, **Show wezai install**, **Update wezai plugin**.

### 5.3 History

Expand Down Expand Up @@ -349,7 +352,7 @@ Per-tab:
- Pinned `@` attach paths and `#` edit targets (re-read from disk each turn; survive Compact)
- Sticky selection / extra context (cleared by Compact)
- Composer draft (Esc in CTRL+I)
- Last question/command, last edit batch (for undo), history events
- Last question/command (the typed Ask/`#` line, used to echo `▶ you` and for **Show last question**), last edit batch (for undo), history events

**Compact** folds older turns into a recap and drops sticky selection text. **Clear** wipes pins, draft, turns, and extras.

Expand Down Expand Up @@ -556,7 +559,7 @@ WezTerm Lua has no `debug.getinfo`. `init.lua` locates the plugin dir by scannin
- [ ] Git/kube/tf/docker/weather/history always use shell cwd (not AI pane).
- [ ] `@history` / `CTRL+SHIFT+H` fuzzy-filters unique commands from the detected shell (fish native `history` or bash/zsh `HISTFILE`); row **Delete** removes all copies (fish `history delete`, bash/zsh flush+rewrite+reload).
- [ ] Local Ollama HTTP: with an unloaded large model, `timeout` ≥ load+warmup still returns JSON (not curl 28 / 0 bytes); second Ask is fast while model stays loaded.
- [ ] During a multi-minute Ask wait, AI pane scrolls progress with model/endpoint, elapsed vs timeout %, and rotating hints (not only a bare “thinking…” line).
- [ ] Ask / `#` edit print a `you` block with the typed question before the assistant reply; a 20-line paste folds with a leftover-line note. Palette **Show last question** reprints the full text.
- [ ] Chatty model wrapping JSON in prose still parses via `extract_json_object` when a single object is present. Unparseable Ask replies print as errors (not assistant turns).
- [ ] `#` / `@@` edit accepting `content` alias when `file` is missing still shows diff confirm (diff visible in the right pane; confirm split does not cover it).
- [ ] Gemini `type = "google"` `#` edits return `file` / `files` (schema allows those keys; Ask still uses message+command).
Expand Down Expand Up @@ -586,6 +589,7 @@ WezTerm Lua has no `debug.getinfo`. `init.lua` locates the plugin dir by scannin
| Apply/Cancel confirm (AI pane stays visible) | `plugin/confirm.lua`, `plugin/confirm.py` |
| `run_cmd` / `resolve_executable` | `plugin/util.lua` |
| Pane / UI | `plugin/ui.lua` |
| Fold long questions | `plugin/fold.lua` (`ui.print_question`) |
| Install version label | `plugin/version.lua`, `plugin/util.lua` (`version_label` / `brand_with_version` / `sync_plugin_git`) |
| Settings / env overlay | `plugin/settings.lua` (`wezai.env`, `WEZAI_*`) |
| Providers | `plugin/providers/` |
Expand Down
3 changes: 3 additions & 0 deletions plugin/context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ local function confirm_needed(tokens, config)
end

function M.prepare_request(window, pane, line, selection, config)
local source_line = line or ""
local cwd = util.get_pane_cwd(pane)
local max_bytes = config.max_file_bytes or 100000
local parsed = M.parse_at_refs(line or "")
Expand Down Expand Up @@ -966,6 +967,7 @@ function M.prepare_request(window, pane, line, selection, config)
original_content = targets[1] and targets[1].content,
is_new = is_new_any,
user_text = instruction,
source_line = source_line,
attach_labels = synth_labels,
omitted = omitted_all,
token_estimate = tokens,
Expand Down Expand Up @@ -1080,6 +1082,7 @@ function M.prepare_request(window, pane, line, selection, config)
prompt = prompt,
files = all,
user_text = question,
source_line = source_line,
attach_labels = synth_labels,
omitted = omitted_all,
token_estimate = tokens,
Expand Down
124 changes: 124 additions & 0 deletions plugin/fold.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
-- Pure helpers to fold long Ask/Edit text for the AI output pane.
-- No wezterm / subprocess. Used by ui.lua and scripts/test_fold.lua.

local M = {}

local DEFAULT_MAX_LINES = 8
local DEFAULT_MAX_CHARS = 600
local DEFAULT_MAX_LINE_CHARS = 160

local function to_int(v, fallback)
if v == nil then
return fallback
end
local n = tonumber(v)
if not n then
return fallback
end
return math.floor(n)
end

local function split_lines(text)
text = (text or ""):gsub("\r\n", "\n"):gsub("\r", "\n")
if text == "" then
return {}
end
local lines = {}
for line in (text .. "\n"):gmatch("(.-)\n") do
lines[#lines + 1] = line
end
-- Drop the empty match created by the extra "\n" we appended.
if #lines > 0 and lines[#lines] == "" then
table.remove(lines)
end
return lines
end

local function clip_line(line, max_line_chars)
if max_line_chars and max_line_chars > 0 and #line > max_line_chars then
return line:sub(1, max_line_chars) .. "…", true
end
return line, false
end

--- Fold long text for the write-only AI pane (no interactive expand).
--- opts: max_lines, max_chars, max_line_chars. Values <= 0 disable that limit.
--- Returns display_text, meta { folded, lines, chars, shown_lines }
function M.fold_text(text, opts)
opts = opts or {}
if type(text) ~= "string" then
text = text == nil and "" or tostring(text)
end
local max_lines = to_int(opts.max_lines, DEFAULT_MAX_LINES)
local max_chars = to_int(opts.max_chars, DEFAULT_MAX_CHARS)
local max_line_chars = to_int(opts.max_line_chars, DEFAULT_MAX_LINE_CHARS)

local lines = split_lines(text)
local total_lines = #lines
local total_chars = #text
if total_lines == 0 then
return "", { folded = false, lines = 0, chars = 0, shown_lines = 0 }
end

local shown = {}
local shown_chars = 0
local folded = false

for _, line in ipairs(lines) do
if max_lines > 0 and #shown >= max_lines then
folded = true
break
end
local piece, line_cut = clip_line(line, max_line_chars)
local room = (max_chars > 0) and (max_chars - shown_chars) or nil
if room and room <= 0 then
folded = true
break
end
if room and #piece > room then
piece = piece:sub(1, room) .. "…"
line_cut = true
end
shown[#shown + 1] = piece
shown_chars = shown_chars + #piece
if line_cut then
folded = true
if max_chars > 0 and shown_chars >= max_chars then
break
end
end
end

if #shown < total_lines then
folded = true
end

local out = table.concat(shown, "\n")
if folded then
local hidden = math.max(0, total_lines - #shown)
if hidden > 0 then
out = out
.. string.format(
"\n… folded %d more line%s (%d chars)",
hidden,
hidden == 1 and "" or "s",
total_chars
)
else
out = out .. string.format("\n… folded (%d chars)", total_chars)
end
end

return out, {
folded = folded,
lines = total_lines,
chars = total_chars,
shown_lines = #shown,
}
end

M.DEFAULT_MAX_LINES = DEFAULT_MAX_LINES
M.DEFAULT_MAX_CHARS = DEFAULT_MAX_CHARS
M.DEFAULT_MAX_LINE_CHARS = DEFAULT_MAX_LINE_CHARS

return M
34 changes: 26 additions & 8 deletions plugin/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ do
"composer.lua",
"confirm.lua",
"version.lua",
"fold.lua",
}

local function file_exists(path)
Expand Down Expand Up @@ -349,17 +350,20 @@ local function handle_ai_request(window, shell_pane, prompt, config, opts)
prompt = prepend_history(window, prompt, config)
prompt = context.redact(prompt)

if opts.user_text then
session.set_last_question(window, opts.user_text)
session.add_turn(window, "user", context.redact(opts.user_text), config.chat_max_turns)
local asked = opts.source_line or opts.user_text
if asked and tostring(asked):match("%S") then
asked = context.redact(asked)
session.set_last_question(window, asked)
session.add_turn(window, "user", asked, config.chat_max_turns)
session.push_history_event(
window,
{ kind = "ask", text = context.redact(opts.user_text) },
{ kind = "ask", text = asked },
(config.history and config.history.max_session) or 50
)
end

ui.begin_turn(ai_pane, os.date("%H:%M:%S") .. " ask")
ui.print_question(ai_pane, asked, config)
local progress = ui.start_progress(ai_pane, config)
local success, stdout, err, meta = providers.ask(req_config, prompt)
progress.stop()
Expand Down Expand Up @@ -508,18 +512,21 @@ local function handle_edit_request(window, shell_pane, request, config)
local prompt = prepend_history(window, request.prompt, config)
prompt = context.redact(prompt)

if request.user_text then
session.set_last_question(window, request.user_text)
session.add_turn(window, "user", "EDIT " .. request.target_path .. ": " .. request.user_text, config.chat_max_turns)
local asked = request.source_line or request.user_text
if asked and tostring(asked):match("%S") then
asked = context.redact(asked)
session.set_last_question(window, asked)
session.add_turn(window, "user", "EDIT " .. (request.target_path or "?") .. ": " .. asked, config.chat_max_turns)
session.push_history_event(window, {
kind = "edit",
text = request.user_text,
text = asked,
path = request.target_path,
instruction = request.user_text,
}, (config.history and config.history.max_session) or 50)
end

ui.begin_turn(ai_pane, os.date("%H:%M:%S") .. " edit")
ui.print_question(ai_pane, asked, config)
local progress = ui.start_progress(ai_pane, config)

local edit_config = util.copy_config(config)
Expand Down Expand Up @@ -697,6 +704,7 @@ local function dispatch_request(window, shell_pane, request, config, opts)
handle_ai_request(window, shell_pane, request.prompt, config, {
share_pane = opts.share_pane,
user_text = request.user_text,
source_line = request.source_line,
})
end

Expand Down Expand Up @@ -1220,6 +1228,16 @@ palette.handlers = {
ui.ai_print(ai_pane, "Could not copy to clipboard.", "error")
end
end,
show_question = function(win, p, cfg)
local ai_pane = ui.ensure_ai_pane(win, p, cfg)
local q = session.get_last_question(win)
if not q then
ui.ai_print(ai_pane, "No previous question.", "error")
return
end
ui.begin_turn(ai_pane, os.date("%H:%M:%S") .. " question")
ui.print_question(ai_pane, q, cfg, { unfold = true })
end,
shorter = function(win, p, cfg)
local ai_pane = ui.ensure_ai_pane(win, p, cfg)
local q = session.get_last_question(win)
Expand Down
5 changes: 5 additions & 0 deletions plugin/palette.lua
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ function M.show(window, pane, config, opts)
H.copy_cmd(win, p, cfg)
end
end)
add(choices, handlers, "core:show_question", "Show last question", function(win, p, cfg)
if H.show_question then
H.show_question(win, p, cfg)
end
end)
add(choices, handlers, "core:shorter", "Re-ask last question (shorter)", function(win, p, cfg)
if H.shorter then
H.shorter(win, p, cfg)
Expand Down
11 changes: 10 additions & 1 deletion plugin/settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,16 @@ local BASE = {
dir = nil,
},
backup_suffix = ".wezai.bak",
ai_pane = { enabled = true, direction = "Right", size_percent = 35, pad_cols = 2 },
ai_pane = {
enabled = true,
direction = "Right",
size_percent = 35,
pad_cols = 2,
-- Echo the Ask/Edit question; fold long pastes in the write-only pane.
question_fold_lines = 8,
question_fold_chars = 600,
question_fold_line_chars = nil, -- nil → fit the pane width when known
},
-- Ask composer (CTRL+I): split of the shell pane so the AI log stays visible.
composer = {
enabled = true,
Expand Down
Loading