1module view23import src.events4import src.fmt5import src.history6import src.snapshot7import src.storage89fn save(id string, message string, checkpoint bool) history.Save {10 return history.Save{11 id: id12 message: message13 created_at: 179000000014 size_bytes: 102415 is_checkpoint: checkpoint16 }17}1819fn line_map(ids []string) map[string]bool {20 mut out := map[string]bool{}21 for id in ids {22 out[id] = true23 }24 return out25}2627fn plain(lines []string) []string {28 return lines.map(fmt.strip_ansi(it))29}303132fn never_act(_ &storage.Store, action Action) string {33 panic('an action ran without the strict confirm: ${action.kind}')34}3536fn list_session(items []history.Save) Session {37 return Session{38 screen: Screen{39 tab: .all40 items: items41 on_line: line_map(items.map(it.id))42 rows: 3043 cols: 8044 }45 }46}4748fn test_tabs_filter_by_what_is_on_the_line() {49 50 51 on_the_line := line_map(['aaa', 'ccc', 'ddd'])52 mut copy := save('ddd', 'my work, replayed', false)53 copy.replayed_from = 'bbb'54 items := [save('aaa', 'first', false), save('bbb', 'my work', false),55 save('ccc', 'before things', true), copy]5657 assert filter_saves(items, .all, on_the_line, '').len == 458 59 60 61 assert filter_saves(items, .saves, on_the_line, '').map(it.id) == ['aaa', 'ddd']62 assert filter_saves(items, .checkpoints, on_the_line, '').map(it.id) == ['ccc']63}6465fn test_search_is_case_insensitive_across_the_fields() {66 on_the_line := line_map(['aaa', 'bbb', 'ccc'])67 mut with_agent := save('bbb', 'fix chunk boundary', false)68 with_agent.agent = 'DeepSeek/Flash'69 mut with_session := save('ccc', 'other work', false)70 with_session.session = 'phase one'71 items := [save('aaa', 'add storage', false), with_agent, with_session]7273 assert filter_saves(items, .all, on_the_line, 'STORAGE').map(it.id) == ['aaa']74 assert filter_saves(items, .all, on_the_line, 'chunk').map(it.id) == ['bbb']75 assert filter_saves(items, .all, on_the_line, 'deepseek').map(it.id) == ['bbb']76 assert filter_saves(items, .all, on_the_line, 'PHASE').map(it.id) == ['ccc']77 assert filter_saves(items, .all, on_the_line, 'nothing here').len == 078}7980fn test_move_selection_clamps_at_both_ends() {81 assert move_selection(0, 1, 3) == 182 assert move_selection(2, 1, 3) == 283 assert move_selection(0, -1, 3) == 084 assert move_selection(0, 0, 0) == 085 assert move_selection(5, 1, 0) == 086 assert move_selection(3, -1, 3) == 287}8889909192fn test_frame_fills_the_screen_and_never_wraps() {93 mut items := []history.Save{}94 for i in 0 .. 20 {95 items << save('id${i:09}', 'a very long message '.repeat(12), i % 3 == 0)96 }97 for tab in [Tab.all, .saves, .checkpoints] {98 mut session := list_session(items)99 session.screen.tab = tab100 session.screen.cols = 60101 session.screen.selected = 7102 lines := frame_lines(session)103 assert lines.len == 30104 for line in lines {105 assert fmt.visible_width(line) <= 60106 }107 assert plain(lines)[0].contains('checkpoints')108 }109 frame := render(frame_lines(list_session(items)))110 assert frame.starts_with('\x1b[H')111 assert frame.count('\x1b[2K') == 30112 113 assert !frame.ends_with('\n')114}115116fn test_head_has_its_own_mark_and_every_other_save_a_dot() {117 items := [save('aaa', 'newest', false), save('bbb', 'a checkpoint', true),118 save('ccc', 'older', false)]119 mut session := list_session(items)120 session.screen.head = 'aaa'121 session.screen.selected = 2122 text := plain(frame_lines(session)).join('\n')123 assert text.contains('▸ aaa HEAD')124 assert text.contains('· bbb')125 assert text.contains('· ccc')126 assert !text.contains('◐')127}128129fn test_the_selected_save_adds_size_agent_and_session() {130 mut first := save('aaa', 'first', false)131 first.agent = 'claude-opus-5-5'132 first.session = 'history view'133 mut session := list_session([first, save('bbb', 'second', false)])134 text := plain(frame_lines(session)).join('\n')135 assert text.contains('1.0 KiB · claude-opus-5-5 · history view')136 session.screen.selected = 1137 unselected := plain(frame_lines(session)).join('\n')138 assert !unselected.contains('claude-opus-5-5')139 assert unselected.contains('unknown · no session')140}141142fn test_flagged_and_replayed_away_saves_are_marked() {143 mut session := list_session([save('aaa', 'risky', false), save('bbb', 'my work', false)])144 session.screen.flags['aaa'] = events.Flag{145 save_id: 'aaa'146 flagged: true147 }148 session.screen.on_line = line_map(['aaa'])149 text := plain(frame_lines(session)).join('\n')150 assert text.contains('⚠ risky')151 assert text.contains('[flagged]')152 assert text.contains('bbb [replayed away]')153}154155fn test_typing_searches_only_after_a_slash() {156 mut session := list_session([save('aaa', 'first', false)])157 assert !handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'x' }, never_act)158 assert session.mode == .list159 assert session.screen.query == ''160 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: '/' }, never_act)161 assert session.mode == .search162 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'x' }, never_act)163 assert session.screen.query == 'x'164}165166fn test_q_is_a_letter_while_searching_and_esc_is_the_way_out() {167 mut session := list_session([save('aaa', 'queue the saves', false)])168 assert handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'q' }, never_act)169 session.mode = .search170 assert !handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'q' }, never_act)171 assert session.screen.query == 'q'172 handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act)173 assert session.mode == .list174 assert session.screen.query == ''175 assert handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'q' }, never_act)176}177178fn test_the_search_bar_shows_whether_it_is_being_typed() {179 mut session := list_session([save('aaa', 'first', false)])180 for key in ['/', 'f', 'i'] {181 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: key }, never_act)182 }183 typing := plain(frame_lines(session))184 assert typing[0].contains('/fi_')185 assert typing.last().contains('type to filter · enter keep · esc clear')186 handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act)187 kept := plain(frame_lines(session))188 assert kept[0].contains('/fi') && !kept[0].contains('/fi_')189 assert kept.last().contains('/ search · tab mode')190 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: '/' }, never_act)191 handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act)192 assert session.screen.query == ''193}194195fn test_g_goes_to_the_oldest_and_shift_g_to_the_newest() {196 mut session := list_session([save('aaa', 'new', false), save('bbb', 'mid', false),197 save('ccc', 'old', false)])198 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'g' }, never_act)199 assert session.screen.selected == 2200 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'G' }, never_act)201 assert session.screen.selected == 0202}203204fn test_tab_cycles_the_tabs_and_starts_at_the_top() {205 mut session := list_session([save('aaa', 'new', false), save('bbb', 'old', false)])206 session.screen.selected = 1207 handle_key(mut session, unsafe { nil }, Key{ kind: .tab }, never_act)208 assert session.screen.tab == .saves209 assert session.screen.selected == 0210 handle_key(mut session, unsafe { nil }, Key{ kind: .tab }, never_act)211 handle_key(mut session, unsafe { nil }, Key{ kind: .tab }, never_act)212 assert session.screen.tab == .all213}214215fn test_flagging_asks_the_strict_confirm_before_anything_happens() {216 mut session := list_session([save('aaa', 'risky', false)])217 handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act)218 assert session.mode == .detail219 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'f' }, never_act)220 assert session.mode == .reason221 222 for key in ['q', 'u', 'i', 'c', 'k'] {223 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: key }, never_act)224 }225 handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act)226 assert session.mode == .confirm227 assert session.pending.kind == .flag228 assert session.pending.reason == 'quick'229 assert plain(frame_lines(session)).last().contains('flag [aaa]? y yes · n/esc cancel')230 handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act)231 assert session.mode == .list232 assert session.pending.kind == .none233}234235fn test_detail_lists_files_with_stats_and_notes_binaries() {236 mut target := save('aaa', 'change two files', false)237 target.parent = 'ppp'238 flag := events.Flag{239 save_id: 'aaa'240 flagged: true241 reason: 'sketchy'242 }243 deltas := [244 snapshot.Delta{245 path: 'src/main.v'246 kind: 'modified'247 added: 4248 removed: 2249 },250 snapshot.Delta{251 path: 'logo.png'252 kind: 'added'253 binary: true254 },255 snapshot.Delta{256 path: 'new.txt'257 kind: 'added'258 added: 3259 },260 snapshot.Delta{261 path: 'old.txt'262 kind: 'removed'263 removed: 5264 },265 ]266 text := plain(detail_lines(target, '', deltas, flag)).join('\n')267 assert text.contains('checksum aaa')268 assert text.contains('flag sketchy')269 assert text.contains('M src/main.v +4 -2')270 assert text.contains('A logo.png (binary)')271 assert text.contains('4 files changed')272 assert text.ends_with('\n\n total +7 -7')273}274275fn test_detail_of_an_unchanged_checkpoint_shows_zero_line_totals() {276 mut target := save('aaa', 'checkpoint', true)277 target.parent = 'ppp'278 text := plain(detail_lines(target, '', [], events.Flag{})).join('\n')279 assert text.contains('0 files changed')280 assert text.ends_with('\n\n total +0 -0')281}282283fn test_detail_of_the_first_save_says_there_is_no_parent() {284 text := plain(detail_lines(save('aaa', 'first save', false), '', [], events.Flag{285 save_id: 'aaa'286 })).join('\n')287 assert text.contains('initial save — no parent to diff against')288}289290fn test_detail_fits_the_frame_too() {291 mut session := list_session([save('aaa', 'x'.repeat(300), false)])292 session.mode = .detail293 session.target = session.screen.items[0]294 session.detail = detail_lines(session.target, '', [], events.Flag{})295 lines := frame_lines(session)296 assert lines.len == 30297 for line in lines {298 assert fmt.visible_width(line) <= 80299 }300}301302fn test_an_action_result_shows_even_on_a_narrow_screen() {303 mut session := list_session([save('aaa', 'first', false)])304 session.screen.cols = 50305 session.screen.status = 'flagged [70c78decf279]'306 last := plain(frame_lines(session)).last()307 assert last.ends_with('flagged [70c78decf279]')308 assert fmt.visible_width(last) <= 50309}310311312313fn test_entries_sit_fifteen_percent_tighter_and_still_fill_the_screen() {314 mut items := []history.Save{}315 for i in 0 .. 10 {316 items << save('id${i:09}', 'message ${i}', false)317 }318 mut session := list_session(items)319 session.screen.rows = 51 320 lines := plain(frame_lines(session))321 assert lines.len == 51322 mut headers := []int{}323 mut separators := []int{}324 for i, line in lines {325 if line.starts_with(' · ') || line.starts_with(' ▸ ') {326 headers << i327 }328 if line.starts_with('─') {329 separators << i330 }331 }332 assert headers.len == 7333 for i in 1 .. headers.len {334 assert headers[i] - headers[i - 1] in [6, 7]335 }336 337 assert lines.len - 2 - separators.last() <= 1338}339340341342fn test_esc_goes_back_or_exits_from_the_list() {343 mut session := list_session([save('aaa', 'first', false)])344 session.screen.query = 'fir'345 assert !handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act)346 assert session.screen.query == ''347 assert handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act)348}349350fn test_esc_steps_back_from_every_other_mode() {351 mut session := list_session([save('aaa', 'first', false)])352 handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act)353 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'f' }, never_act)354 assert session.mode == .reason355 handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act)356 assert session.mode == .detail357 handle_key(mut session, unsafe { nil }, Key{ kind: .rune, text: 'l' }, never_act)358 assert session.mode == .confirm359 handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act)360 assert session.mode == .list361 handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act)362 handle_key(mut session, unsafe { nil }, Key{ kind: .escape }, never_act)363 assert session.mode == .list364}365366fn test_manual_pages_jump_between_sections_and_name_the_one_on_top() {367 mut lines := []ManualLine{len: 10, init: ManualLine{368 text: 'body'369 }}370 lines[2] = ManualLine{371 text: 'NAME'372 section: 'NAME'373 }374 lines[7] = ManualLine{375 text: 'FILES'376 section: 'FILES'377 }378 379 assert section_page(lines, 0, 3, true) == 2380 assert section_page(lines, 2, 3, true) == 2381 assert section_page(lines, 3, 3, false) == 2382 assert section_page(lines, 2, 3, false) == 0383 frame := manual_frame(lines, 3, 3, 80)384 assert frame.len == 4385 assert frame[0] == 'body' && frame[1] == ''386 assert fmt.strip_ansi(frame[3]).starts_with('FILES · page 4 of 4')387}388389fn confirmed_action(_ &storage.Store, action Action) string {390 return 'confirmed ${action.save_id}'391}392393fn test_both_cases_of_y_confirm_view_actions() {394 for answer in ['y', 'Y'] {395 mut session := Session{ mode: .confirm, pending: Action{ kind: .load, save_id: 'aaa' } }396 confirm_key(mut session, unsafe { nil }, Key{ kind: .rune, text: answer }, confirmed_action)397 assert session.screen.status == 'confirmed aaa'398 assert session.mode == .list399 }400}