module view import src.events import src.fmt import src.history import src.snapshot import src.storage fn save(id string, message string, checkpoint bool) history.Save { return history.Save{ id: id message: message created_at: 1790000000 size_bytes: 1024 is_checkpoint: checkpoint } } fn line_map(ids []string) map[string]bool { mut out := map[string]bool{} for id in ids { out[id] = true } return out } fn plain(lines []string) []string { return lines.map(fmt.strip_ansi(it)) } // never_act stands in for the command layer where no action may happen yet. fn never_act(_ &storage.Store, action Action) string { panic('an action ran without the strict confirm: ${action.kind}') } fn list_session(items []history.Save) Session { return Session{ screen: Screen{ tab: .all items: items on_line: line_map(items.map(it.id)) rows: 30 cols: 80 } } } fn test_tabs_filter_by_what_is_on_the_line() { // A combine replays the local save `bbb` as `ddd`: the copy is on the line, // the original is not. on_the_line := line_map(['aaa', 'ccc', 'ddd']) mut copy := save('ddd', 'my work, replayed', false) copy.replayed_from = 'bbb' items := [save('aaa', 'first', false), save('bbb', 'my work', false), save('ccc', 'before things', true), copy] assert filter_saves(items, .all, on_the_line, '').len == 4 // The replayed-away original exists in history but is on neither live list; // the copy that replaced it is an ordinary save on the line. The two lists // never overlap: a checkpoint belongs to the checkpoints tab alone. assert filter_saves(items, .saves, on_the_line, '').map(it.id) == ['aaa', 'ddd'] assert filter_saves(items, .checkpoints, on_the_line, '').map(it.id) == ['ccc'] } fn test_search_is_case_insensitive_across_the_fields() { on_the_line := line_map(['aaa', 'bbb', 'ccc']) mut with_agent := save('bbb', 'fix chunk boundary', false) with_agent.agent = 'DeepSeek/Flash' mut with_session := save('ccc', 'other work', false) with_session.session = 'phase one' items := [save('aaa', 'add storage', false), with_agent, with_session] assert filter_saves(items, .all, on_the_line, 'STORAGE').map(it.id) == ['aaa'] assert filter_saves(items, .all, on_the_line, 'chunk').map(it.id) == ['bbb'] assert filter_saves(items, .all, on_the_line, 'deepseek').map(it.id) == ['bbb'] assert filter_saves(items, .all, on_the_line, 'PHASE').map(it.id) == ['ccc'] assert filter_saves(items, .all, on_the_line, 'nothing here').len == 0 } fn test_move_selection_clamps_at_both_ends() { assert move_selection(0, 1, 3) == 1 assert move_selection(2, 1, 3) == 2 assert move_selection(0, -1, 3) == 0 assert move_selection(0, 0, 0) == 0 assert move_selection(5, 1, 0) == 0 assert move_selection(3, -1, 3) == 2 } // A long message used to wrap, which scrolled the terminal and pushed the tab // bar off the top: every line of a frame must fit, and the frame must fill // the screen exactly. fn test_frame_fills_the_screen_and_never_wraps() { mut items := []history.Save{} for i in 0 .. 20 { items << save('id${i:09}', 'a very long message '.repeat(12), i % 3 == 0) } for tab in [Tab.all, .saves, .checkpoints] { mut session := list_session(items) session.screen.tab = tab session.screen.cols = 60 session.screen.selected = 7 lines := frame_lines(session) assert lines.len == 30 for line in lines { assert fmt.visible_width(line) <= 60 } assert plain(lines)[0].contains('checkpoints') } frame := render(frame_lines(list_session(items))) assert frame.starts_with('\x1b[H') assert frame.count('\x1b[2K') == 30 // No newline after the last row, or the terminal scrolls one line. assert !frame.ends_with('\n') } fn test_head_has_its_own_mark_and_every_other_save_a_dot() { items := [save('aaa', 'newest', false), save('bbb', 'a checkpoint', true), save('ccc', 'older', false)] mut session := list_session(items) session.screen.head = 'aaa' session.screen.selected = 2 text := plain(frame_lines(session)).join('\n') assert text.contains('▸ aaa HEAD') assert text.contains('· bbb') assert text.contains('· ccc') assert !text.contains('◐') } fn test_the_selected_save_adds_size_agent_and_session() { mut first := save('aaa', 'first', false) first.agent = 'claude-opus-5-5' first.session = 'history view' mut session := list_session([first, save('bbb', 'second', false)]) text := plain(frame_lines(session)).join('\n') assert text.contains('1.0 KiB · claude-opus-5-5 · history view') session.screen.selected = 1 unselected := plain(frame_lines(session)).join('\n') assert !unselected.contains('claude-opus-5-5') assert unselected.contains('unknown · no session') } fn test_flagged_and_replayed_away_saves_are_marked() { mut session := list_session([save('aaa', 'risky', false), save('bbb', 'my work', false)]) session.screen.flags['aaa'] = events.Flag{ save_id: 'aaa' flagged: true } session.screen.on_line = line_map(['aaa']) text := plain(frame_lines(session)).join('\n') assert text.contains('⚠ risky') assert text.contains('[flagged]') assert text.contains('bbb [replayed away]') } fn test_typing_searches_only_after_a_slash() { mut session := list_session([save('aaa', 'first', false)]) assert !handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'x' }, never_act) assert session.mode == .list assert session.screen.query == '' handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: '/' }, never_act) assert session.mode == .search handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'x' }, never_act) assert session.screen.query == 'x' } fn test_q_is_a_letter_while_searching_and_esc_is_the_way_out() { mut session := list_session([save('aaa', 'queue the saves', false)]) assert handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'q' }, never_act) session.mode = .search assert !handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'q' }, never_act) assert session.screen.query == 'q' handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act) assert session.mode == .list assert session.screen.query == '' assert handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'q' }, never_act) } fn test_the_search_bar_shows_whether_it_is_being_typed() { mut session := list_session([save('aaa', 'first', false)]) for key in ['/', 'f', 'i'] { handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: key }, never_act) } typing := plain(frame_lines(session)) assert typing[0].contains('/fi_') assert typing.last().contains('type to filter · enter keep · esc clear') handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act) kept := plain(frame_lines(session)) assert kept[0].contains('/fi') && !kept[0].contains('/fi_') assert kept.last().contains('/ search · tab mode') handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: '/' }, never_act) handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act) assert session.screen.query == '' } fn test_g_goes_to_the_oldest_and_shift_g_to_the_newest() { mut session := list_session([save('aaa', 'new', false), save('bbb', 'mid', false), save('ccc', 'old', false)]) handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'g' }, never_act) assert session.screen.selected == 2 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'G' }, never_act) assert session.screen.selected == 0 } fn test_tab_cycles_the_tabs_and_starts_at_the_top() { mut session := list_session([save('aaa', 'new', false), save('bbb', 'old', false)]) session.screen.selected = 1 handle_key(mut session, unsafe { nil }, Key{ kind: .tab }, never_act) assert session.screen.tab == .saves assert session.screen.selected == 0 handle_key(mut session, unsafe { nil }, Key{ kind: .tab }, never_act) handle_key(mut session, unsafe { nil }, Key{ kind: .tab }, never_act) assert session.screen.tab == .all } fn test_flagging_asks_the_strict_confirm_before_anything_happens() { mut session := list_session([save('aaa', 'risky', false)]) handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act) assert session.mode == .detail handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'f' }, never_act) assert session.mode == .reason // q is text here: a reason may say "quick hack". for key in ['q', 'u', 'i', 'c', 'k'] { handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: key }, never_act) } handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act) assert session.mode == .confirm assert session.pending.kind == .flag assert session.pending.reason == 'quick' assert plain(frame_lines(session)).last().contains('flag [aaa]? y yes · n/esc cancel') // Only a lowercase y goes ahead; anything else cancels. handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'Y' }, never_act) assert session.mode == .list assert session.pending.kind == .none } fn test_detail_lists_files_with_stats_and_notes_binaries() { mut target := save('aaa', 'change two files', false) target.parent = 'ppp' flag := events.Flag{ save_id: 'aaa' flagged: true reason: 'sketchy' } deltas := [ snapshot.Delta{ path: 'src/main.v' kind: 'modified' added: 4 removed: 2 }, snapshot.Delta{ path: 'logo.png' kind: 'added' binary: true }, ] text := plain(detail_lines(target, '', deltas, flag)).join('\n') assert text.contains('checksum aaa') assert text.contains('flag sketchy') assert text.contains('M src/main.v +4 -2') assert text.contains('A logo.png (binary)') assert text.contains('2 files changed') } fn test_detail_of_the_first_save_says_there_is_no_parent() { text := plain(detail_lines(save('aaa', 'first save', false), '', [], events.Flag{ save_id: 'aaa' })).join('\n') assert text.contains('initial save — no parent to diff against') } fn test_detail_fits_the_frame_too() { mut session := list_session([save('aaa', 'x'.repeat(300), false)]) session.mode = .detail session.target = session.screen.items[0] session.detail = detail_lines(session.target, '', [], events.Flag{}) lines := frame_lines(session) assert lines.len == 30 for line in lines { assert fmt.visible_width(line) <= 80 } } fn test_an_action_result_shows_even_on_a_narrow_screen() { mut session := list_session([save('aaa', 'first', false)]) session.screen.cols = 50 session.screen.status = 'flagged [70c78decf279]' last := plain(frame_lines(session)).last() assert last.ends_with('flagged [70c78decf279]') assert fmt.visible_width(last) <= 50 } // Koh 2 spreads six entries over the whole body. June asked for them 15% // tighter with no empty band left: seven entries, still filling the screen. fn test_entries_sit_fifteen_percent_tighter_and_still_fill_the_screen() { mut items := []history.Save{} for i in 0 .. 10 { items << save('id${i:09}', 'message ${i}', false) } mut session := list_session(items) session.screen.rows = 51 // a body of 48 rows: Koh 2's blocks are 8 tall lines := plain(frame_lines(session)) assert lines.len == 51 mut headers := []int{} mut separators := []int{} for i, line in lines { if line.starts_with(' · ') || line.starts_with(' ▸ ') { headers << i } if line.starts_with('─') { separators << i } } assert headers.len == 7 for i in 1 .. headers.len { assert headers[i] - headers[i - 1] in [6, 7] } // The last block ends at the bottom of the body, right above the status bar. assert lines.len - 2 - separators.last() <= 1 } // Esc always goes back one step, and from the list it leaves the view: first // it drops a kept search filter, then it exits, just as q does. fn test_esc_goes_back_or_exits_from_the_list() { mut session := list_session([save('aaa', 'first', false)]) session.screen.query = 'fir' assert !handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act) assert session.screen.query == '' assert handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act) } fn test_esc_steps_back_from_every_other_mode() { mut session := list_session([save('aaa', 'first', false)]) handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act) handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'f' }, never_act) assert session.mode == .reason handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act) assert session.mode == .detail handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'l' }, never_act) assert session.mode == .confirm handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act) assert session.mode == .list handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act) handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act) assert session.mode == .list } fn test_manual_pages_jump_between_sections_and_name_the_one_on_top() { mut lines := []ManualLine{len: 10, init: ManualLine{ text: 'body' }} lines[2] = ManualLine{ text: 'NAME' section: 'NAME' } lines[7] = ManualLine{ text: 'FILES' section: 'FILES' } // Pages of three: [0,1,2] [3,4,5] [6,7,8] [9]. assert section_page(lines, 0, 3, true) == 2 assert section_page(lines, 2, 3, true) == 2 assert section_page(lines, 3, 3, false) == 2 assert section_page(lines, 2, 3, false) == 0 frame := manual_frame(lines, 3, 3, 80) assert frame.len == 4 assert frame[0] == 'body' && frame[1] == '' assert fmt.strip_ansi(frame[3]).starts_with('FILES · page 4 of 4') }