ZFMOUTHEMHYYEAGXRQ3L427FVZPNBC7CX6QATVXU2KPDJOVEH2EQC SDBVLSDDRPQF62XXKJKM2RQLMXOKKHOYRVUF6DIUDFRYCGL2DW3QC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC 3JA7HYRMHV57SIMGMGPDOMKQ3NBQS2SKOX3EKDHRBQRP7ZPZGFTQC DBOROCRFD6A5SJBMFYFEJI5S5M77X4EFEK6KDQWA5QDMQJKIHRWQC EW7VBNMGWFBC73ZUDLB4LIK2HWFKA74ZUTUDG4J575ZQHEFHW4UQC U6JEEU5O477ZOJ5UMRMOJSGPEJEU6Q7KMPKUSDF56CYVUJWL7QBQC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC package utilsimport ("image""image/color""strings""testing")func TestWriteKittyImage_SmallImage(t *testing.T) {// 2x2 image produces small base64 payload — single chunk, no m= keyimg := image.NewGray(image.Rect(0, 0, 2, 2))img.SetGray(0, 0, color.Gray{Y: 128})var buf strings.Builderif err := WriteKittyImage(img, &buf); err != nil {t.Fatalf("WriteKittyImage: %v", err)}out := buf.String()if !strings.HasPrefix(out, "\x1b_Gf=100,a=T;") {t.Error("expected single-chunk header with f=100,a=T")}if strings.Contains(out, "m=") {t.Error("small image should not use chunked m= key")}if !strings.HasSuffix(out, "\x1b\\") {t.Error("expected escape sequence terminator")}}func TestWriteKittyImage_LargeImage_Chunked(t *testing.T) {// 448x448 grayscale image produces >4096 bytes of base64img := image.NewGray(image.Rect(0, 0, 448, 448))for y := range 448 {for x := range 448 {img.SetGray(x, y, color.Gray{Y: uint8((x + y) % 256)})}}var buf strings.Builderif err := WriteKittyImage(img, &buf); err != nil {t.Fatalf("WriteKittyImage: %v", err)}out := buf.String()// Should have multiple escape sequenceschunks := strings.Split(out, "\x1b\\")// Last element is empty after final terminatorchunks = chunks[:len(chunks)-1]if len(chunks) < 2 {t.Fatalf("expected multiple chunks, got %d", len(chunks))}// First chunk should have f=100,a=T,m=1if !strings.Contains(chunks[0], "f=100,a=T,m=1") {t.Errorf("first chunk missing f=100,a=T,m=1: %s", chunks[0][:min(80, len(chunks[0]))])}// Last chunk should have m=0last := chunks[len(chunks)-1]if !strings.Contains(last, "\x1b_Gm=0;") {t.Errorf("last chunk missing m=0: %s", last[:min(80, len(last))])}// Middle chunks should have m=1for i := 1; i < len(chunks)-1; i++ {if !strings.Contains(chunks[i], "\x1b_Gm=1;") {t.Errorf("middle chunk %d missing m=1", i)}}}func TestClearKittyImages(t *testing.T) {var buf strings.BuilderClearKittyImages(&buf)expected := "\x1b_Ga=d\x1b\\"if buf.String() != expected {t.Errorf("got %q, want %q", buf.String(), expected)}}
// Write Kitty protocol: ESC _ G f=100,a=T;{base64_data} ESC \// f=100 = PNG format// a=T = transmit and displayfmt.Fprintf(w, "\x1b_Gf=100,a=T;%s\x1b\\", base64Data)
// Small payload: send in a single escape sequence (no m= key needed)const chunkSize = 4096if len(base64Data) <= chunkSize {fmt.Fprintf(w, "\x1b_Gf=100,a=T;%s\x1b\\", base64Data)return nil}// Chunked transmission for large payloadsfor offset := 0; offset < len(base64Data); offset += chunkSize {end := offset + chunkSizeif end > len(base64Data) {end = len(base64Data)}chunk := base64Data[offset:end]if offset == 0 {// First chunk: include format and action, m=1 means more chunks followfmt.Fprintf(w, "\x1b_Gf=100,a=T,m=1;%s\x1b\\", chunk)} else if end < len(base64Data) {// Middle chunk: m=1 means more chunks followfmt.Fprintf(w, "\x1b_Gm=1;%s\x1b\\", chunk)} else {// Last chunk: m=0 means no more chunksfmt.Fprintf(w, "\x1b_Gm=0;%s\x1b\\", chunk)}}
**Bug fix:** Spectrogram display upgraded from 224x224 to 448x448 pixels. Old image artifacts persisted between segment navigations at the larger size.- `utils/kitty_image.go` — Chunked Kitty protocol transmission (4096-byte chunks) per spec; small images still sent as single payload- `tui/classify.go` — Return `tea.ClearScreen` on navigation keys (`,`, `.`, bindings) to force full redraw and reliable image clearing- `tui/classify.go` — `ResizeImage` call updated from 224x224 to 448x448- `utils/kitty_image_test.go` — Tests for single-chunk, multi-chunk, and clear behavior