From 8b5fc974b071fe9cb6eb50a1af307f5f2ba222e9 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 16 Aug 2026 21:21:02 +0200 Subject: [PATCH] fix: cover tui in the CLI inventory and drop the em-dashes it printed The inventory test predates the `tui` command, so a rename or an accidental removal would have gone unnoticed: it now asserts the command, its `-l`/`--list` flag and its optional position operand. The digest and search-result lines joined their halves with an em-dash, which the repo's own convention rules out, so both now use the middle dot the surrounding lines already use. The one em-dash left in `src/tui/` is load-bearing: `search-service.ts` builds a session snippet with it, and the pattern that strips the repeated label has to match it. Also moves `buildSearchEntries`'s doc comment back onto `buildSearchEntries`; it had ended up stacked above a helper. Co-Authored-By: Claude Fable 5 --- src/tui/tui-app.ts | 2 +- src/tui/tui-client.ts | 4 ++-- src/tui/tui-digest.ts | 2 +- src/tui/tui-model.ts | 21 +++++++++++---------- test/cli-commands.test.ts | 7 +++++++ test/tui/tui-digest.test.ts | 2 +- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/tui/tui-app.ts b/src/tui/tui-app.ts index e893fb83..c600acb3 100644 --- a/src/tui/tui-app.ts +++ b/src/tui/tui-app.ts @@ -1399,7 +1399,7 @@ class TuiApp { } // Nothing to switch to (a history or file hit), so the row's own facts are // the answer; resuming one is phase 3. - this.model.updateSearch({ note: [entry.text, entry.detail].filter((part) => part).join(' — ') }); + this.model.updateSearch({ note: [entry.text, entry.detail].filter((part) => part).join(' · ') }); } private async openDigest(): Promise { diff --git a/src/tui/tui-client.ts b/src/tui/tui-client.ts index 981d992b..e9261c1d 100644 --- a/src/tui/tui-client.ts +++ b/src/tui/tui-client.ts @@ -6,7 +6,7 @@ * API the web UI uses, so the two surfaces can never disagree. This module is * the only place in `src/tui/` that does IO. It covers four jobs: * - * 1. **Discovery + auth** — find the instance's server (`CODEMAN_API_URL`, else + * 1. **Discovery + auth**: find the instance's server (`CODEMAN_API_URL`, else * loopback on `CODEMAN_PORT`), accepting the self-signed cert `--https` * generates, and read credentials the way `codeman attach` already does * (env, then the data dir's `.env`). @@ -15,7 +15,7 @@ * 3. **Live updates** over SSE, decoded by `tui-sse.ts`, with a staleness * watchdog and capped backoff. The TUI does not patch rows from payloads: an * interesting event means "resync", and the app layer debounces the refetch. - * 4. **Degraded mode** — when nothing answers, sessions are enumerated straight + * 4. **Degraded mode**: when nothing answers, sessions are enumerated straight * from tmux plus a read-only peek at `state.json`, which keeps the "the * server died, get me to my sessions" path that `sc` has today. * diff --git a/src/tui/tui-digest.ts b/src/tui/tui-digest.ts index e93c4327..2ad04da2 100644 --- a/src/tui/tui-digest.ts +++ b/src/tui/tui-digest.ts @@ -47,7 +47,7 @@ function ageColumn(item: AwayDigestItem, now: number): string { function itemLine(item: AwayDigestItem, now: number): string { const who = item.sessionName ?? item.sessionId?.slice(0, 8) ?? ''; - const what = [item.title, item.detail].filter((part) => part && part.trim() !== '').join(' — '); + const what = [item.title, item.detail].filter((part) => part && part.trim() !== '').join(' · '); return ` ${ageColumn(item, now)} ${[who, what].filter((part) => part !== '').join(' ')}`.replace(/\s+$/, ''); } diff --git a/src/tui/tui-model.ts b/src/tui/tui-model.ts index 59c81c18..4d6957be 100644 --- a/src/tui/tui-model.ts +++ b/src/tui/tui-model.ts @@ -208,6 +208,17 @@ const SEARCH_GROUP_LABELS: Record = { file: 'FILES', }; +/** + * A session snippet opens with the session's own name, which the row already + * shows in its first column (`search-service.ts` builds it as + * `w1-alpha /tmp/alpha`, hence the separator in the pattern). + * Dropping the repeat is what keeps a result row from reading as a stutter. + */ +function withoutLabelPrefix(snippet: string, label: string): string { + const rest = snippet.startsWith(label) ? snippet.slice(label.length) : snippet; + return rest === snippet ? snippet : rest.replace(/^\s*(?:[—:-]\s*)?/, ''); +} + /** * Flatten `GET /api/search`'s typed groups into the overlay's lines: a header * per group, then its results. Only a result row carries a session id, which is @@ -217,16 +228,6 @@ const SEARCH_GROUP_LABELS: Record = { * has a session id too, but selecting it would move the cursor to a row that is * not on the list. */ -/** - * A session snippet opens with the session's own name (`w1-alpha — /tmp/alpha`), - * which the row already shows in its first column. Dropping the repeat is what - * keeps a result row from reading as a stutter. - */ -function withoutLabelPrefix(snippet: string, label: string): string { - const rest = snippet.startsWith(label) ? snippet.slice(label.length) : snippet; - return rest === snippet ? snippet : rest.replace(/^\s*(?:[—:-]\s*)?/, ''); -} - export function buildSearchEntries( groups: readonly SearchResultGroup[], isLive: (sessionId: string) => boolean diff --git a/test/cli-commands.test.ts b/test/cli-commands.test.ts index 93a010ed..9946a961 100644 --- a/test/cli-commands.test.ts +++ b/test/cli-commands.test.ts @@ -40,6 +40,7 @@ const TOP_LEVEL: Record = { reset: [], start: [], list: ['ls'], + tui: [], web: [], service: [], users: [], @@ -137,6 +138,10 @@ describe('registered options', () => { expect(flagsOf(find(program, 'doctor')!)).toEqual(expect.arrayContaining(['--json', '--category'])); }); + it('keeps the two `tui` fast paths, which stand in for `sc -l` and `sc 2`', () => { + expect(flagsOf(find(program, 'tui')!)).toEqual(expect.arrayContaining(['-l', '--list'])); + }); + it('keeps the escape hatches that scripts depend on', () => { expect(flagsOf(find(program, 'reset')!)).toEqual(expect.arrayContaining(['-f', '--force'])); expect(flagsOf(find(program, 'status')!)).toEqual(expect.arrayContaining(['--url'])); @@ -164,6 +169,8 @@ describe('registered arguments', () => { it.each([ ['attach', ['path']], ['start', []], + // Optional: bare `codeman tui` opens the dashboard. + ['tui', ['n']], ])('declares the operands of `%s`', (name, expected) => { const args = find(program, name)!.registeredArguments.map((arg) => arg.name()); expect(args).toEqual(expected); diff --git a/test/tui/tui-digest.test.ts b/test/tui/tui-digest.test.ts index 2ac2bee5..1efac8af 100644 --- a/test/tui/tui-digest.test.ts +++ b/test/tui/tui-digest.test.ts @@ -75,7 +75,7 @@ describe('formatAwayDigest', () => { { now: NOW } ); expect(lines).toContain('NEEDS ATTENTION (1)'); - expect(lines).toContain(' 2m w4-api permission prompt — Bash(git push)'); + expect(lines).toContain(' 2m w4-api permission prompt · Bash(git push)'); }); it('caps a long section instead of burying the next one', () => {