Files
zero/internal/tui/attachment_remove_test.go
KRATOS 4582727f95 tui: image-input UX — drag-drop attach, inline numbered chips, vision fallback, cursor + copy fixes (#247)
* tui: image-input UX — drag-drop attach, inline numbered chips, vision fallback, cursor + copy fixes

Improves the image/PDF attachment flow and two long-standing composer papercuts.

- Drag-and-drop: dropping an image/PDF onto the composer attaches it instead of
  erroring as an unknown "/…" command. The dropped path is detected on paste and
  on submit (absolute/escaped/quoted paths to an existing image or PDF).
- Inline numbered chips: staged attachments render as compact [Image #1] /
  [Doc #1] chips INSIDE the input box; the verbose "Attached … (image/png)"
  system line is dropped. Backspace on an empty composer removes the last chip
  (/image clear still drops all).
- Vision fallback: a real multimodal model absent from the curated catalog
  (custom / openai-compatible / local ids) now accepts images via a conservative
  name match; text-only ids stay refused, and the catalog stays authoritative.
- Blinking composer cursor, including when the box is empty (previously no caret
  was drawn for a focused, empty box).
- Ctrl+E releases/recaptures the mouse so native drag-select + copy works (mouse
  capture otherwise intercepts terminal text selection).

Tests: dropped-path detection, vision-by-name + unknown-model fallback,
mouse-release capture toggle, attachment removal, and updated chip-render tests.

* fix(vision): a "vision-less" model name must not match the bare-vision fallback

visionCapableByName treated any id containing "vision" as multimodal, so a model
named "...vision-less..." (i.e. WITHOUT vision) was wrongly classified vision-
capable — which silently skipped the non-vision image gate and broke
TestRunExecDropsImagesOnNonVisionModelWithWarning in internal/cli.

Add mentionsVision, which still matches the "vision" word but excludes the
negated forms (vision-less / visionless / no-vision / non-vision). Used for both
the grok-vision and the general open-multimodal fallback. Added negation cases to
the name table.

* fix(tui): drag-drop path detection corrupted Windows paths (backslash unescape)

unescapeDroppedPath stripped backslashes to undo Unix terminal drag-drop escaping
("\ " -> " "), but on Windows the backslash is the path separator, so a plain
dropped path like C:\Users\x\file.png became C:Usersxfile.png and never resolved
-- TestDroppedAttachmentPath failed on windows-latest.

No-op the unescape on Windows (dropped paths there arrive quoted or plain), and
gate the Unix-only backslash-escaped positive case behind runtime.GOOS.
2026-06-18 19:42:29 +05:30

36 lines
1.2 KiB
Go

package tui
import (
"testing"
"github.com/Gitlawb/zero/internal/zeroruntime"
)
func TestRemoveLastAttachment(t *testing.T) {
m := model{
pendingImages: []zeroruntime.ImageBlock{{MediaType: "image/png"}, {MediaType: "image/png"}},
pendingImageLabels: []string{"a.png", "b.png"},
pendingDocuments: []pendingDocument{{label: "spec.pdf"}},
}
// Documents render last, so a staged doc is removed first.
m, ok := m.removeLastAttachment()
if !ok || len(m.pendingDocuments) != 0 {
t.Fatalf("doc should be removed first: ok=%v docs=%d", ok, len(m.pendingDocuments))
}
// Then the last image (images + labels stay in lockstep).
m, ok = m.removeLastAttachment()
if !ok || len(m.pendingImages) != 1 || len(m.pendingImageLabels) != 1 || m.pendingImageLabels[0] != "a.png" {
t.Fatalf("last image should be removed: ok=%v imgs=%d labels=%v", ok, len(m.pendingImages), m.pendingImageLabels)
}
// Remove the final image.
m, ok = m.removeLastAttachment()
if !ok || len(m.pendingImages) != 0 || len(m.pendingImageLabels) != 0 {
t.Fatalf("all images should be removed: ok=%v imgs=%d", ok, len(m.pendingImages))
}
// Nothing left.
if _, ok := m.removeLastAttachment(); ok {
t.Fatal("removeLastAttachment on an empty set must report false")
}
}