From 7bc8816c4356f5f2c64647363df637537386dd58 Mon Sep 17 00:00:00 2001 From: fujitani sora Date: Fri, 3 Jul 2026 14:41:55 +0900 Subject: [PATCH] perf: skip non-message preview lines --- internal/preview/preview.go | 27 +++++++- internal/preview/preview_test.go | 102 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/internal/preview/preview.go b/internal/preview/preview.go index 52c1f32..7ed8b85 100644 --- a/internal/preview/preview.go +++ b/internal/preview/preview.go @@ -356,7 +356,7 @@ func loadMessages(path string, limit int) ([]session.Message, time.Time, int, er ) for { line, err := readJSONLLine(r, previewLineCap) - if line != "" { + if line != "" && mightBeMessage(line) { if item, ts, ok := parseMessageLine(line); ok { if startedAt.IsZero() && !ts.IsZero() { startedAt = ts @@ -394,6 +394,31 @@ func collectRing(ring []session.Message, total int) []session.Message { return out } +func mightBeMessage(line string) bool { + for offset := 0; ; { + i := strings.Index(line[offset:], `"type"`) + if i < 0 { + return false + } + i += offset + len(`"type"`) + for i < len(line) && (line[i] == ' ' || line[i] == '\t') { + i++ + } + if i >= len(line) || line[i] != ':' { + offset = i + continue + } + i++ + for i < len(line) && (line[i] == ' ' || line[i] == '\t') { + i++ + } + if strings.HasPrefix(line[i:], `"user"`) || strings.HasPrefix(line[i:], `"assistant"`) { + return true + } + offset = i + } +} + type colors struct { bold string reset string diff --git a/internal/preview/preview_test.go b/internal/preview/preview_test.go index 53321c3..e25b2c3 100644 --- a/internal/preview/preview_test.go +++ b/internal/preview/preview_test.go @@ -403,6 +403,70 @@ func TestRender_FutureSessionHeaderSaysFuture(t *testing.T) { } } +func TestMightBeMessage(t *testing.T) { + cases := []struct { + name string + line string + want bool + }{ + { + name: "user compact", + line: `{"type":"user","message":{"role":"user","content":"hi"}}`, + want: true, + }, + { + name: "assistant spaced", + line: `{"type" : "assistant","message":{"role":"assistant","content":"hi"}}`, + want: true, + }, + { + name: "tool result", + line: `{"type":"tool_result","content":"hi"}`, + want: false, + }, + { + name: "false positive body", + line: `{"type":"tool_result","content":{"type":"user"}}`, + want: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := mightBeMessage(tc.line); got != tc.want { + t.Fatalf("mightBeMessage() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestLoadMessages_PrefilterFalsePositiveToolResult(t *testing.T) { + tmp := t.TempDir() + jsonl := filepath.Join(tmp, "false-positive.jsonl") + body := strings.Join([]string{ + `{"type":"tool_result","content":{"type":"user","content":"not a top-level message"}}`, + `{"type":"user","timestamp":"2026-05-26T10:00:00Z","message":{"role":"user","content":"real message"}}`, + }, "\n") + "\n" + if err := os.WriteFile(jsonl, []byte(body), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + msgs, startedAt, total, err := loadMessages(jsonl, 30) + if err != nil { + t.Fatalf("loadMessages: %v", err) + } + if total != 1 || len(msgs) != 1 { + t.Fatalf("messages = total:%d len:%d, want 1/1", total, len(msgs)) + } + if msgs[0].Body != "real message" { + t.Fatalf("message body = %q", msgs[0].Body) + } + want := time.Date(2026, 5, 26, 10, 0, 0, 0, time.UTC) + if !startedAt.Equal(want) { + t.Fatalf("startedAt = %v, want %v", startedAt, want) + } +} + // B-11: a line longer than the byte cap used to be returned as a // bufio.Scanner error that aborted the whole scan, leaving the preview // half-rendered. The Reader-based loader skips the oversize line and @@ -678,6 +742,16 @@ func BenchmarkRenderTextHighlightLargeTranscript(b *testing.B) { } } +func BenchmarkRenderTextToolHeavyTranscript(b *testing.B) { + s := benchmarkToolHeavyPreviewSession(b, 100, 900) + b.ResetTimer() + for range b.N { + if err := render(s, ioDiscard{}, Options{}); err != nil { + b.Fatalf("render: %v", err) + } + } +} + func benchmarkPreviewSession(b *testing.B, messages int) *session.Session { b.Helper() tmp := b.TempDir() @@ -701,6 +775,34 @@ func benchmarkPreviewSession(b *testing.B, messages int) *session.Session { } } +func benchmarkToolHeavyPreviewSession(b *testing.B, messages, toolLines int) *session.Session { + b.Helper() + tmp := b.TempDir() + jsonl := filepath.Join(tmp, "tool-heavy.jsonl") + var body strings.Builder + base := time.Date(2026, 5, 26, 10, 0, 0, 0, time.UTC) + toolBody := strings.Repeat("tool output ", 256) + for i := range messages + toolLines { + ts := base.Add(time.Duration(i) * time.Second).Format(time.RFC3339) + if i%10 == 0 { + body.WriteString(`{"type":"user","timestamp":"` + ts + `","message":{"role":"user","content":"message ` + itoa(i) + `"}}` + "\n") + continue + } + body.WriteString(`{"type":"tool_result","timestamp":"` + ts + `","content":"` + toolBody + itoa(i) + `"}` + "\n") + } + if err := os.WriteFile(jsonl, []byte(body.String()), 0o644); err != nil { + b.Fatalf("write: %v", err) + } + return &session.Session{ + ID: "bench-tool-heavy", + Source: "claude", + JSONLPath: jsonl, + CWD: tmp, + CWDExists: true, + LastTime: base.Add(time.Duration(messages+toolLines-1) * time.Second), + } +} + type fakePreviewSource struct{} func (fakePreviewSource) Name() string { return "claude" }