From 5feec4b5c7e1db778c1bd358144afe8296cbe792 Mon Sep 17 00:00:00 2001 From: Muhammad Zakir Ramadhan <61570975+zakirkun@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:31:56 +0700 Subject: [PATCH] Print resume instructions on the way out Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/farewell.ts | 35 +++++++++++++++++++++++++++++++++ test/farewell.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/farewell.ts create mode 100644 test/farewell.test.ts diff --git a/src/farewell.ts b/src/farewell.ts new file mode 100644 index 0000000..99423e8 --- /dev/null +++ b/src/farewell.ts @@ -0,0 +1,35 @@ +/** What the session was, for the line printed as shiro exits. */ +export type Farewell = { + id: string; + messages: number; + title: string; +}; + +/** Long enough to be unique in practice, short enough to retype from the screen. */ +const PREFIX = 8; + +const clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 3)}...` : s); + +/** + * The exit message: good bye, and how to pick this session up again. + * + * A session's id is a UUIDv7 nobody retypes, so the resume line shows the prefix + * `resolveId` accepts alongside `-c`, which needs no id at all. An empty session was + * never persisted, so it gets no resume command — pointing someone at `-c` that finds + * nothing is worse than saying nothing. + */ +export function farewell({ id, messages, title }: Farewell): string { + if (messages === 0) return 'Good bye. Nothing to save from this session.'; + + const count = `${messages} message${messages === 1 ? '' : 's'}`; + const named = title && title !== 'untitled' ? `: "${clip(title, 52)}"` : ''; + + return [ + 'Good bye.', + `Saved ${count}${named}`, + '', + 'Resume it with:', + ` shiro -c newest session in this directory`, + ` shiro -r ${id.slice(0, PREFIX)} this session by id`, + ].join('\n'); +} diff --git a/test/farewell.test.ts b/test/farewell.test.ts new file mode 100644 index 0000000..8530e73 --- /dev/null +++ b/test/farewell.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from 'bun:test'; +import { farewell } from '../src/farewell'; + +const record = { id: '0193ab2c-7f31-4c9a-b8e1-6d2f9a4c1e58', messages: 8, title: 'why does the pagination test fail?' }; + +test('a saved session says good bye and both ways to resume it', () => { + const text = farewell(record); + + expect(text).toContain('Good bye'); + // The short prefix is what a user retypes, so it is what gets shown. + expect(text).toContain('shiro -c'); + expect(text).toContain('shiro -r 0193ab2c'); + expect(text).not.toContain(record.id); +}); + +test('the session is identified by its title, so the right one is recognisable', () => { + expect(farewell(record)).toContain('why does the pagination test fail?'); + expect(farewell({ ...record, messages: 8 })).toContain('8 messages'); +}); + +test('a single message reads as one message', () => { + expect(farewell({ ...record, messages: 1 })).toContain('1 message'); + expect(farewell({ ...record, messages: 1 })).not.toContain('1 messages'); +}); + +test('an empty session promises no resume command that would fail', () => { + // Nothing is persisted when no message was exchanged, so `-c` would find + // nothing: claiming otherwise sends the user to an error. + const text = farewell({ ...record, messages: 0 }); + + expect(text).toContain('Good bye'); + expect(text).not.toContain('shiro -c'); + expect(text).not.toContain('-r '); +}); + +test('a long title is cut rather than wrapped across the farewell', () => { + const text = farewell({ ...record, title: 'x'.repeat(200) }); + for (const line of text.split('\n')) expect(line.length).toBeLessThanOrEqual(80); +}); + +test('an untitled session still resumes, without an empty quote', () => { + const text = farewell({ ...record, title: 'untitled' }); + expect(text).toContain('shiro -c'); + expect(text).not.toContain('""'); +});