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 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-22 14:13:58 +02:00
parent a52abd9f96
commit 8b5fc974b0
6 changed files with 23 additions and 15 deletions
+1 -1
View File
@@ -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<void> {
+2 -2
View File
@@ -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.
*
+1 -1
View File
@@ -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+$/, '');
}
+11 -10
View File
@@ -208,6 +208,17 @@ const SEARCH_GROUP_LABELS: Record<SearchSourceType, string> = {
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 <em dash> /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<SearchSourceType, string> = {
* 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
+7
View File
@@ -40,6 +40,7 @@ const TOP_LEVEL: Record<string, string[]> = {
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);
+1 -1
View File
@@ -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', () => {