Commit graph

149 commits

Author SHA1 Message Date
patriceckhart
b2fa0e3a2f feat: remove default step limit
Agent loop now runs unlimited by default. The model decides when to stop. --max-steps is still available as an optional safety cap.
2026-04-26 13:32:46 +02:00
patriceckhart
e52a560c6a fix(tui): compact empty tool call bodies
Tool calls with no result content now render just the header without a bottom rule, eliminating blank spacing for no-output or queued tool calls.
2026-04-26 12:34:34 +02:00
patriceckhart
0194ca6293 style(tui): indent and space sliding-in chips
Adds 2-space indent to match status bar alignment and an empty line after the chips for visual separation.
2026-04-26 11:56:48 +02:00
patriceckhart
9afd1238a3 fix(tui): atomic image scroll skip + live tool overlay indent 2026-04-26 11:40:46 +02:00
patriceckhart
a81472cc65 fix(tui): stabilize inline image scrolling 2026-04-26 11:20:25 +02:00
patriceckhart
487b097da4 fix(tui): VS Code ghost highlight on selection navigation
Forces full attribute reset before clearing each row when selection highlights are present. VS Code's xterm.js doesn't reliably clear background colors on row overwrite without an explicit reset.
2026-04-25 20:49:41 +02:00
patriceckhart
6237dd2e80 feat: session rename with r key in /sessions
Press r to rename the selected session. Uses append-based rename lines so the active session is not corrupted. Native terminal cursor in rename input. Title shown in picker if set.
2026-04-25 20:01:05 +02:00
patriceckhart
205e362ed4 feat: provider labels in login/logout, graceful unknown provider fallback, model picker filtering
Login and logout dialogs show descriptive labels (Anthropic Claude Pro/Max, OpenAI ChatGPT Plus/Pro). Unknown saved providers fall back to an available one instead of crashing. Model picker only shows models from logged-in providers.
2026-04-25 19:25:51 +02:00
patriceckhart
d53fbf4f2d fix(changelog): strip commit/date suffix from version strings
Version strings like '0.1.12 (25b2bd4, ...)' were used as-is for GitHub API lookups and comparisons, causing changelog to never show. Now strips to semver only.
2026-04-25 17:50:35 +02:00
patriceckhart
25b2bd4c96 feat: changelog on update, full-width highlights, @ file picker docs
Changelog dialog now shows only the changelog section from release notes with headings in accent color. Works for local 0.0.0 builds (fetches latest release). Full-width highlight bars fixed everywhere via erase-to-EOL and trailing ANSI preservation in truncateToWidth. Session ops dialog fixed. README documents the @ file picker.
2026-04-25 11:24:09 +02:00
patriceckhart
353da72d28 feat(tui): @ file picker with directory navigation
Type @ to browse files in the working directory. Up/down navigate, right arrow opens directories, left arrow goes back, enter selects. Files insert as [file:name], directories as [dir:name/]. Drag-dropped folders also show as [dir:] chips. Separate counters for file and dir chips.
2026-04-25 10:52:20 +02:00
patriceckhart
8a38171cc8 feat(tui): collapse long file paths to [file:basename] chips
Drag-dropped or pasted file paths are shown as compact [file:name] chips in the editor instead of the full path. Expanded back to the full quoted path on submit. URLs are never collapsed.
2026-04-25 10:20:30 +02:00
patriceckhart
8841800acd feat: auto-refresh OAuth tokens before each API call
Wraps OAuth clients with a RefreshingClient that checks token expiry before every Stream call. Refreshes transparently and rebuilds the underlying client with the fresh token. Fixes sessions silently dying after the 1-hour token lifetime.
2026-04-24 19:37:44 +02:00
patriceckhart
cb9de10ec6 feat: add ollama as first-class provider
Adds --provider ollama with auto-detection of local ollama at localhost:11434. No API key required for local models. Optional --api-key and --base-url for remote/authenticated instances. Uses the OpenAI chat completions client internally. Unknown models are accepted without catalog entries. Updated README with ollama documentation.
2026-04-24 19:13:45 +02:00
patriceckhart
b3a935fbe7 fix: reorder /jail and /unjail together in slash catalog 2026-04-24 15:30:41 +02:00
patriceckhart
9cc2fd6e8a fix(tui): force repaint on layout-shifting events
Calls Invalidate() on tool result, assistant message commit, and turn end to prevent stale code fragments from bleeding into the status bar when the diff renderer misses row changes.
2026-04-24 15:00:01 +02:00
patriceckhart
b25a2bc854 feat: custom models with baseUrl + domain migration to www.zot.sh
Adds baseUrl support in models.json for local models (ollama, vLLM, etc). Migrates all install URLs and references from zot.patriceckhart.com to www.zot.sh.
2026-04-24 14:00:31 +02:00
patriceckhart
dcc9ba05fc feat(models): add gpt-5.5 and gpt-5.5-mini to catalog
Speculative entries for the next OpenAI GPT-5.5 family. Will activate automatically once OpenAI ships them.
2026-04-24 08:01:00 +02:00
patriceckhart
b245be02e5 feat(models): support user-defined models via models.json
Reads $ZOT_HOME/models.json at startup and merges user-defined models into the active catalog with highest precedence. Provider keys like openai-codex are normalized. Documented in README.
2026-04-23 23:09:32 +02:00
patriceckhart
47c154950e fix(tui): strip leading newlines from assistant text
Removes leading newlines from assistant message content before rendering so the text always starts immediately after the zot header.
2026-04-23 19:47:41 +02:00
patriceckhart
e486f77579 style(tui): tighten code panel gutters and expand tabs
Reduces gutter padding between line numbers and code, replaces tab characters with 4 spaces in code panels so Go and other tab-indented languages render at consistent width.
2026-04-23 19:39:54 +02:00
patriceckhart
ba3f3287e9 fix(bash): kill process group immediately on cancel
Watches for context cancellation in a goroutine and kills the entire process group plus closes the pipe immediately, instead of waiting for cmd.Wait() which deadlocks when child processes hold the pipe open.
2026-04-23 18:47:44 +02:00
patriceckhart
daaa062c68 feat(compact): silent compaction with status line and orphan repair
Compaction no longer streams the summary into the chat. The spinner shows while compacting and users can queue prompts that fire after completion. The compaction info appears in the status line with ctrl+o to expand. Orphaned tool_result blocks in the preserved tail are stripped to prevent Anthropic rejection.
2026-04-23 14:20:15 +02:00
patriceckhart
b6529cf5c4 fix(bash): kill entire process group on cancel
Sets Setpgid on bash commands and sends SIGTERM/SIGKILL to the negative pgid so backgrounded children are cleaned up on esc. Also splits the status bar onto multiple lines on narrow terminals when a spinner is active.
2026-04-23 09:43:57 +02:00
patriceckhart
fcd6d7c9a2 ci(release): roll patch after .99
Makes the release workflow bump 0.0.99 to 0.1.0, 0.1.99 to 0.2.0, and so on instead of continuing to 0.0.100.
2026-04-22 21:11:34 +02:00
patriceckhart
07b539b05c fix(cli): adapt help layout for terminal width
Switches zot --help to a stacked layout on narrow terminals while keeping the wider two-column presentation on larger terminals.
2026-04-22 21:06:55 +02:00
patriceckhart
f5a88db223 style(cli): restyle help output
Makes zot --help use zot's TUI palette and section layout, pads columns consistently across terminals, expands section rules to terminal width, and adds extension install guidance in the help output.
2026-04-22 21:03:40 +02:00
patriceckhart
a5fad05fa3 docs(ext): refresh examples and help text
Adds the todo panel example under examples/extensions, updates example manifests and READMEs to match the current extension API, and surfaces extension install/load commands in zot --help.
2026-04-22 20:50:55 +02:00
patriceckhart
6b03aa3320 feat(auth): headless OAuth with paste-code input
Runs the local callback server and the manual copy-code flow in parallel. Displays a real input field (with blinking cursor) for pasting the authorization code / redirect URL / code#state. Anthropic manual variant uses the console copy-code redirect URI to bypass localhost.
2026-04-22 17:49:11 +02:00
patriceckhart
9cf29463b8 fix(tui): match streaming wrap to finalized layout 2026-04-22 10:22:13 +02:00
patriceckhart
6ecaeda598 fix(core): keep streamed assistant text on cancel 2026-04-22 09:45:24 +02:00
patriceckhart
569d28dbdb fix(tui): wrap ANSI-rendered assistant body on narrow terminals 2026-04-22 09:40:16 +02:00
patriceckhart
fdcdeb5eb1 feat(ext): interactive extension panels + persistence
Add open_panel / panel_render / panel_close / panel_key to the extension protocol, expose extension_dir + data_dir in hello_ack, wire panel rendering and key routing through the interactive TUI, extend the Go SDK, and document the new capability. Also fix doubled user-message indent and redundant assistant wrap.
2026-04-22 08:53:21 +02:00
patriceckhart
8d3b7ff155 fix(openai): pass image tool results to vision models
Mirror image-bearing tool output into an OpenAI-only user message so GPT vision models receive image bytes, and hide that synthetic message from the TUI transcript.
2026-04-21 21:45:26 +02:00
patriceckhart
2cf21d7bda fix(tui): suppress clipped images near status bar 2026-04-21 21:32:41 +02:00
patriceckhart
a739ef9a8b fix(tui): repaint fully while scrolling chat 2026-04-21 21:29:08 +02:00
patriceckhart
43aca0b9b2 fix(tui): keep multiple inline images visible
Also route up/down arrows to chat scrolling even when the editor has text, while preserving slash-popup navigation.
2026-04-21 21:24:11 +02:00
patriceckhart
c16e929f32 fix(core): repair orphan tool_use rows on session load
Resuming a session whose transcript contains an assistant
tool_use block without a matching tool_result in the next
message caused Anthropic (and OpenAI's responses API) to
refuse the first request with:

  http 400: messages.N: `tool_use` ids were found without
  `tool_result` blocks immediately after: toolu_...

Two ways the corrupt state gets onto disk:

  - Older zot builds persisted the assistant tool_use row
    before the tool_result row, then crashed or were killed
    between the two writes.
  - Earlier abort paths didn't drop the mid-turn assistant
    message cleanly before it reached the session file.

OpenSession now passes the hydrated message slice through
repairToolUseResultPairs before returning it. For every
assistant tool_use whose id isn't covered by a tool_result in
the next message, the repair injects a stub

  ToolResultBlock{
    CallID:  <id>,
    Content: [TextBlock{"tool call was aborted; no result recorded."}],
    IsError: true,
  }

The stub is merged into the following tool-role message if
one exists (preserves row count), otherwise a new tool-role
message is inserted right after the assistant. Model sees
the aborted context and decides whether to retry.

Runs once per OpenSession call; the hot runtime path is
untouched. Live abort handling already drops partial
assistant messages, so this is purely a safety net for
legacy-corrupted files and the crash-between-writes case.

Tests in session_repair_test.go cover:

  - stub appended when no tool-role message follows
  - stub merged into partial tool-role message
  - valid transcripts pass through unchanged
  - nil/empty input handled safely
2026-04-21 19:36:47 +02:00
patriceckhart
dad49e9b7a tweak(tui): image-block spacing + indent
Two small fixes to how inline-image tool results render.

1) Extra blank row under the image.

   RowsForInlineImage estimates the on-screen height from a
   fixed cell aspect ratio. Real terminals (iTerm2, Ghostty,
   Kitty, WezTerm) don't all match that ratio exactly, so the
   image often paints one row past the estimate and the info
   line below it ("image - image/png - 612x904 - 99 KB") gets
   overwritten by the image's bottom edge. Reserving one extra
   blank row between the image and the info line absorbs that
   spillover on every terminal I tried.

   Same symmetry for the trailing blank: the info line no
   longer sits flush against the tool-block closing rule or
   the next content below.

2) Info line indented to 4 spaces.

   Every other tool-output row (renderRawFile's gutter,
   renderBashResult, diff rows, the "... N more lines, M total"
   collapse footer) starts at column 4. The image info line
   used to start at column 2, which looked off next to the
   other rows. Bumped it to 4 so the metadata line aligns with
   the file content above and below it.

Text-only fallback (terminals without inline image support)
also picks up the trailing blank + 4-space indent so the two\nrender paths behave consistently.
2026-04-21 19:12:04 +02:00
patriceckhart
112341ca3d fix(tui): slash popup + transient overlays swallow esc before busy-cancel
Two rules for how esc resolves while a turn is running got
implemented this round:

1) Let the user open the slash popup during a busy turn.

   The suggest render path used to short-circuit on i.busy, so
   typing / while the agent was working did nothing. The
   dispatcher in runSlash already handles the busy-state routing
   per command (safe ones run immediately, destructive ones
   cancel first), so dropping the guard was safe. Now / opens
   the popup whether or not a turn is in flight.

2) Esc dismisses overlays before it cancels the turn.

   The global key switch used to fire the busy-cancel
   unconditionally on esc. That meant three common patterns
   silently ripped the active turn away:

     - Open the slash popup, press esc to dismiss it ->
       turn cancelled.
     - Run /help to see the key bindings while a turn was
       running, press esc when done -> turn cancelled.
     - An extension pushed a notify/display line, user pressed
       esc to clear it -> turn cancelled.

   The esc case now checks, in order:
     - slash popup active -> break out of the switch, let the
       popup's own esc handler (later in handleKey) close it
     - helpBlock or extNotes non-empty -> clear them, invalidate,
       return (turn keeps running)
     - busy + cancelable -> cancel the turn (old behaviour)
     - idle -> fall through to the editor which clears itself

Result: esc feels like a dismiss key that escalates. It nukes
the turn only when nothing else on screen wants it.

No change to dialog handlers \u2014 those already intercept esc in
their own return-false branches before the global switch ever
runs.
2026-04-21 18:32:22 +02:00
patriceckhart
6019404644 fix(tui): blinking cursor in /btw, proper idle redraws, tighter status bar
Three related tweaks to how the interactive mode drives its redraw
loop, the terminal cursor, and the status-bar layout.

1) Redraw-on-tick narrowed to things that actually animate.

   The render loop used to force a redraw every 120ms for every
   open dialog (model picker, jump, sessions, btw, etc). Most
   of those are static pickers \u2014 the repaint was wasted and had
   a visible side effect: the re-emitted hide-cursor / show-cursor
   pair at the start of each frame cancels the terminal's blink
   cycle for any dialog that hosts its own input field. Concretely
   the blinking cursor in /btw never blinked, it just sat as a
   steady reverse-video block.

   Tick-driven redraw is now only triggered when i.busy (main
   spinner animates) or btw.Loading() (side-chat spinner animates).
   Static dialogs rely on the dirty-channel invalidations that
   fire on key events, which is sufficient because nothing else
   on screen is moving.

2) btw side-chat redraws on completion.

   Consequence of (1): the btw goroutine that streams a model
   response used to depend on the 120ms tick to make the final
   answer visible. With the tick gone for idle dialogs, the
   answer landed in d.turns but the screen stayed blank until
   the user pressed a key.

   btwDialog.Open and submit now take an invalidate callback
   that the goroutine fires after completeTurn, including the
   early-error branch. While the request is in flight,
   btw.Loading() returns true so the tick keeps the spinner
   animating; once the answer lands, a single invalidate()
   redraws and the tick stops.

3) Cursor routed into btw while it's open.

   btwDialog already had a CursorPos(width) method that returns
   where the side-chat editor's caret sits within the dialog's
   rendered rows. The host never used it \u2014 the real terminal
   cursor stayed on the main editor below. Now when btw is
   active the host picks up CursorPos and points the terminal
   cursor there, so the blink shows in the correct input and
   the main editor has no cursor.

4) Status bar idle-path spacing.

   Idle row used to render as "<pad><pad>(provider) model" =
   4 spaces before the model, so the line started at column 4
   while busy and idle didn't match. Dropped one pad in the
   idle branch; both paths now start at column 2, aligned with
   the conversation column ('  you' / '  zot' message markers).

No semantic change to transcripts or provider calls. All tests
pass.
2026-04-21 18:09:10 +02:00
patriceckhart
fc47398717 tweak(tui): status bar spacing + muted busy-segment dash
Two small polishes.

- StatusBar: the busy branch used to emit three pad groups
  before the provider/model block (pad + busyPrefix + pad +
  pad). On screen that read as six spaces between the elapsed
  counter and "(openai) gpt-5.4", which looked like a layout
  bug. Collapsed to two pads so the gap matches the idle path
  exactly: 2 spaces before the model block, 2 spaces between
  the busy segment and the model block.

- The separator between the spinner message and the elapsed
  counter was rendered as a plain unstyled "-", which the
  terminal painted in the default foreground colour (white on
  my dark theme) and stood out against the muted counter next
  to it. Pulled the dash into its own FG256(Muted, ...) call so
  the whole tail ("- 15s") shares the same grey tone.
2026-04-21 17:53:11 +02:00
patriceckhart
1f28b62a47 style: replace middle-dot separators with ascii hyphens
Swept the TUI strings and README for the stray U+00B7 MIDDLE DOT
(\u00b7) separators left over from earlier UI iterations. They read
fine on terminals that render the glyph as a small bullet, but
on some fonts (especially the telegram desktop client, a few
linux terminal fonts) it renders as an off-center dot that
looks like a smudge or a broken pipe. Plain ' - ' is universally
readable and matches every other separator already in the
status bar and dialogs.

Touched:
  README.md                    paragraph separators
  modes/btw_dialog.go          header joiner
  modes/help.go                table row separators
  modes/interactive.go         status bar tags, telegram mirror
  modes/jump_dialog.go         row separators
  modes/login_dialog.go        header joiners, status line
  modes/model_dialog.go        model + source joiner
  modes/slash_suggest.go       commands list
  tui/view.go                  assorted tui separators

No functional change. go test ./... still passes.
2026-04-21 17:39:08 +02:00
patriceckhart
c4150ce630 tweak(tui): two-threshold paste collapse with line-vs-char shape
Before: any paste with >= 2 newlines (3+ lines) collapsed to
[pasted text #N +L lines]. That fired too eagerly on short
three-line snippets the user wants to see inline, and never
fired on a 2000-character single-line dump that bloats the
editor just as badly.

New rule matches the shape widely used in other TUIs:

  collapse when
    lines > 10         OR
    chars > 1000

Both triggers produce distinct placeholder shapes so you can
see at a glance which dimension tripped it:

  [pasted text #1 +12 lines]   line trigger
  [pasted text #1 1500 chars]  char trigger

When both triggers hit (e.g. 12 lines of 400 chars each) the
line-count shape wins \u2014 "+12 lines" reads more informatively
than a raw character count for multi-line content.

Implementation:

  - pasteCollapseLineThreshold  = 10
  - pasteCollapseCharThreshold  = 1000
  - pasteShouldCollapse checks countLines(s) > 10 || len(s) > 1000
  - formatPastePlaceholder picks the shape; line trigger wins
    on ties
  - pastePlaceholderRE widened with a non-capturing alternation
    (\+\d+ lines?|\d+ chars?) so expansion works for both
    shapes; capture group 1 is still just the id
  - the strings.Contains fast-path in SubmitValue still matches
    both shapes because both start with the same prefix

Tests rewritten around the new thresholds:

  - TestPasteCollapseLineTrigger    11 lines -> +11 lines marker
  - TestPasteCollapseCharTrigger    1500 chars, 1 line -> N chars
  - TestPasteCollapseLinePrecedence 12 lines of 400 chars each ->
                                    line shape wins
  - TestPasteCollapseFallthrough    1 line / 2 lines / 10 lines /
                                    1000 chars / 5 lines under
                                    caps all stay inline
  - TestPasteCollapseSequentialIDs  mixed-shape placeholders
                                    coexist, both expand
  - TestPasteCollapseClearResetsMap unchanged logic, updated body
                                    to trigger the new threshold
2026-04-21 17:13:23 +02:00
patriceckhart
755ca7ccdf feat(tui): /login shows current status for each provider
Running /login used to drop straight into the method / provider
pickers with no indication of what's already logged in. Easy to
accidentally log out a working subscription because you
forgot which provider you'd authenticated with last.

The dialog now takes a snapshot of auth.json when Open() runs
and renders it as a two-line header above both the method step
and the provider step:

  login
    \u2713 anthropic: subscription
    \u2713 openai: api key

  choose login method (\u2191/\u2193, enter, esc to cancel):
    api key
    subscription (claude pro/max \u00b7 chatgpt plus/pro)

Logged-in providers get a green check + their method
("api key" or "subscription"); providers with no credentials
get a muted \u2013 dash + "not logged in". When NEITHER provider is
logged in (first-run, fresh box) the status block is
suppressed entirely \u2014 a pair of "not logged in" rows there is
just noise when the user is already seconds away from picking
a method.

On the provider step each row also gets an inline method tag
so picking it implicitly replaces the existing credential:

  login \u00b7 oauth
    \u2713 anthropic: subscription
    \u2713 openai: api key

  choose provider:
    anthropic  (subscription)
    openai     (api key)

Implementation:

  - New d.status map on loginDialog, populated by Open() from
    auth.Credentials.Method(provider) which already returns
    exactly the three states we care about: "apikey", "oauth",
    or "".
  - Open() gained a zotHome string arg so the dialog can
    compute auth.json's path without importing the agent
    package (which would be a cyclic import; modes is inside
    agent). Both callers in interactive.go now pass
    i.cfg.ZotHome.
  - renderStatusLines() centralises the two-row block so
    loginStepMethod and loginStepProvider share it.

Tests: the whole package compiles and vet-clean; go test
./... still passes. No new tests because the dialog is pure
rendering off captured state and the captured-state path is a
one-liner.
2026-04-21 16:56:49 +02:00
patriceckhart
cb8526c061 fix(tui): up/down arrows move the cursor through visual rows, not history
Two related changes to how the input handles the arrow keys.

1) History recall is gone.

   The editor used to maintain a 200-entry ring of previously
   submitted prompts and swap the current draft for a history
   entry when up/down hit the top/bottom edge of the buffer.
   In practice this mis-fired more than it helped: pressing up
   on a single-line draft silently replaced it with an older
   prompt. Users expect history recall (when they want it) on
   a dedicated key, not as a side effect of cursor navigation.

   Dropped: Editor.History, histIdx, savedDraft; PushHistory,
   historyPrev, historyNext, findInHistory; every e.histIdx
   assignment sprinkled across the mutating methods; the
   PushHistory call sites in interactive.go (submit handler
   and the slash-selector enter path). Clear() already wiped
   the visible buffer, it now also doesn't touch history
   because there's no history to wipe.

2) Up/Down navigate VISUAL rows, not logical buffer lines.

   The TUI wraps a long single-line input across several
   visual rows. Before this patch, up/down only moved between
   entries in e.Lines, so when the cursor was on the second
   visual row of a wrapped single line, up did nothing
   (CursorR was already 0). Users saw their cursor stuck at
   the bottom of a wrapped draft with no way to move up
   except arrow-left until they wrapped around.

   Added moveCursorVisual(dir int) which rebuilds the same
   wrapped layout Render produces, records which visual row
   the cursor currently occupies, computes the target visual
   row = current + dir, then maps the current visual column
   onto the rune index inside the target row's slice of its
   logical line. Subsumes the multi-line logical-line case:
   if a wrapped row straddles a logical-line boundary, the
   mapping naturally advances CursorR.

   The editor now records the latest width passed to Render so
   moveCursorVisual can walk the same layout. Fallback
   moveCursorLogical covers the edge case where Render hasn't
   run yet (shouldn't happen in practice; kept for safety).

Tests: existing TestEditorCursorAfterMultilinePaste and
TestEditorCursorAfterLongPasteWithWrap still pass because they
test Editor.Insert followed by Render, not up/down traversal;
the new navigation leaves Render's cursor-position math
unchanged.
2026-04-21 09:22:54 +02:00
patriceckhart
ce806272e0 feat(tui): /study slash command to prime the agent on the current project
New built-in /study command that runs a single canned prompt:
"Read and understand everything in the current directory." The
first thing most sessions need is project context, and typing
the full sentence every time is friction; /study turns that
into one keystroke-saving shortcut.

Dispatched through the same queue-or-start path as a typed
prompt, so it behaves identically:

  - idle  -> startTurn(studyPrompt)
  - busy  -> queued behind the running turn, delivered next

Also added to the README slash-commands table so /help output
and the top-level docs stay in sync with slashCatalog.
2026-04-21 08:59:53 +02:00
patriceckhart
51fd11fce6 feat(tui): collapse multi-line pastes to a placeholder token
A 200-line paste (log, stack trace, config blob) used to expand
the editor to 200 visible rows, burying the rest of the tui and
making the prompt awkward to edit. Now a paste of 3+ lines is
replaced in the editor with a short token like

  [pasted text #1 +56 lines]

while the full body is stashed behind the scenes and expanded
back in right before the turn goes to the agent. Single-line
and two-line pastes fall through to the old inline insert path
(drag-dropped file paths, short snippets) so those still work
as before.

Implementation

  Editor gained two private fields:
    - pastes   map[int]string   full bodies keyed by id
    - pasteSeq int               monotonic id counter

  KeyPaste branch: on content with >= 2 newlines, allocates the
  next id, stores the raw body, inserts the placeholder token
  at the cursor. Everything else stays on the existing
  quotePastedFilePaths path.

  Editor.Value()       returns what's visible (placeholder).
  Editor.SubmitValue() new method: runs pastePlaceholderRE over
                       the visible text and swaps each match
                       for its stored body. Called once at
                       submit time; non-destructive so history
                       recall (up-arrow) still shows the
                       placeholder form, not the replay.

  Editor.Clear()       drops the pastes map + resets pasteSeq
                       so ids from a previous turn can't leak.

  SetValue()           same reset: pastes map is tied to the
                       visible text and a SetValue replaces it.

  interactive.go       the one caller that reads the editor to
                       build a prompt now reads Value() for the
                       history entry and SubmitValue() for the
                       string that goes to the agent.

Tests

  paste_collapse_test.go covers:
    - placeholder shape + SubmitValue expansion
    - single-line and two-line pastes skip the collapse path
    - two separate pastes get distinct ids, both expand
    - Clear() resets the map + counter

  Tweaked the pre-existing TestEditorCursorAfterMultilinePaste
  and TestEditorCursorAfterLongPasteWithWrap to use Editor.Insert
  directly so they keep testing wrap / cursor math rather than
  accidentally exercising the new collapse path.
2026-04-21 08:47:36 +02:00
patriceckhart
916a6f71d1 fix(update): re-check every launch when the cache says up-to-date
CheckForUpdate used to trust any cache entry less than 12h old
whose CurrentAt matched the running binary. Problem: if a user
installs v0.0.72 at 16:05 (cache: up-to-date) and a v0.0.73
release ships at 18:27, relaunching zot at 18:29 hits the
fresh cache and never calls the github api \u2014 the update
banner stays hidden until either 12h pass or the binary is
rebuilt. Noticed in the wild: v0.0.73 latest, zot 0.0.72
running, no banner.

Refined logic: only short-circuit on a cached entry when it
already reports Available=true. If the cache says "up to
date" we let the flow fall through to fetchLatestRelease and
reconcile. The api call is a single ~4s timeout request;
cheap enough to do on every up-to-date launch, and the common
"cache already shows available" path still skips the network
entirely.

Also: manually cleared /Users/pat/Library/Application
Support/zot/update-check.json on my local box so the next
launch sees the new v0.0.73 release without waiting; that's
user state, not something to commit.
2026-04-20 18:31:51 +02:00
patriceckhart
13b8947fba tweak(tui): ctrl+c no longer interrupts a running turn
A single ctrl+c during a busy turn used to cancel the turn
(same as esc). That misfired a lot in practice because ctrl+c
is reflex muscle-memory ("be quiet" in a shell) rather than a
deliberate decision to kill a multi-minute model call you have
already paid tokens for. Users kept aborting expensive turns by
accident.

New behavior:

- busy + first ctrl+c  -> arms the exit hint, status line
                          reads "press ctrl+c again to exit,
                          esc to cancel the turn"; the turn
                          keeps running.
- busy + second ctrl+c (within ctrlCExitWindow = 2s)
                       -> exits zot.
- busy + esc           -> cancels the running turn (unchanged).
- idle + ctrl+c        -> clears editor/queue as before;
                          second press within 2s exits.

The double-tap-to-exit pattern now works the same from busy and
idle, which also matches the habits from python repls and
similar tools.

Also:
- assistant body keeps a 4-cell right gutter that mirrors the
  4-space left indent so wrapped prose sits in a symmetric
  column instead of kissing the terminal edge on ultra-wide
  windows. The prose cap itself is gone; the new
  assistantBodyRightPad constant replaces maxAssistantWidth.

- README Keys table + Queued messages paragraph updated to
  describe the new ctrl+c / esc split so the docs match the
  code.
2026-04-20 18:23:59 +02:00