feat: add issue view with body cleaning and truncation (task 0003)
All checks were successful
CI / test (pull_request) Successful in 26s
CI / test (push) Successful in 23s

Add `issue view <n>` as the first detail command, introducing the
content-cleaning and truncation machinery (src/body.ts) that later issue
and PR slices reuse.

- Default output: number, title, state, author, created, body (truncated
  at 500), plus comment_count; --comments expands every comment (bodies
  truncated at 800); --full suppresses all truncation.
- cleanBody runs only when a body exceeds its limit: normalizes Gitea
  issue/PR URLs on the detected host to Issue#N/PR#N, strips image embeds
  and long URLs, and collapses quoted blocks.
- Type guard: a PR number fails with VALIDATION_ERROR and a `pr view <n>`
  hint, detected via the fetched object's pull_request field.
- renderDetail joins the detail entity, optional sub-blocks, and help.
This commit was merged in pull request #2.
This commit is contained in:
2026-07-11 19:32:31 -04:00
parent ae0f4c2675
commit 5e66b4d746
8 changed files with 680 additions and 16 deletions

View File

@@ -15,12 +15,26 @@ No `type` field and no sub-issue augmentation.
## Acceptance criteria
- [ ] `issue view <n>` renders the default detail fields plus `comment_count` via renderDetail
- [ ] Bodies over 500 chars are cleaned then truncated with the inline hint `"... (truncated, N chars total - use --full to see complete body)"`; bodies at or under the limit pass through untouched
- [ ] cleanBody normalizes issue/PR URLs using the detected hostname, strips image embeds and long URLs, and collapses quoted blocks
- [ ] `--comments` renders every comment (no cap), each body cleaned and truncated at 800 chars
- [ ] `--full` returns raw, untruncated body and comment bodies
- [ ] A PR number yields `VALIDATION_ERROR` (exit 2) with the "is a pull request" message and a `pr view <n>` help line, detected via the fetched object's `pull_request` field
- [ ] A nonexistent issue yields `ISSUE_NOT_FOUND` (exit 1)
- [ ] Single-entity next-step suggestions fill the actual issue number rather than a placeholder
- [ ] Fixture-server tests cover truncation boundaries, cleanBody transforms, `--comments`, `--full`, and the type guard
- [x] `issue view <n>` renders the default detail fields plus `comment_count` via renderDetail
- [x] Bodies over 500 chars are cleaned then truncated with the inline hint `"... (truncated, N chars total - use --full to see complete body)"`; bodies at or under the limit pass through untouched
- [x] cleanBody normalizes issue/PR URLs using the detected hostname, strips image embeds and long URLs, and collapses quoted blocks
- [x] `--comments` renders every comment (no cap), each body cleaned and truncated at 800 chars
- [x] `--full` returns raw, untruncated body and comment bodies
- [x] A PR number yields `VALIDATION_ERROR` (exit 2) with the "is a pull request" message and a `pr view <n>` help line, detected via the fetched object's `pull_request` field
- [x] A nonexistent issue yields `ISSUE_NOT_FOUND` (exit 1)
- [x] Single-entity next-step suggestions fill the actual issue number rather than a placeholder
- [x] Fixture-server tests cover truncation boundaries, cleanBody transforms, `--comments`, `--full`, and the type guard
## Implementation Notes
The cleaning/truncation machinery lives in a new `src/body.ts` (`cleanBody`, `truncateBody`, and the `BODY_TRUNCATE_LIMIT`/`COMMENT_TRUNCATE_LIMIT` constants) so the later issue/PR slices can reuse it.
`renderDetail` was added to `src/render.ts` alongside `renderList`, sharing a private `encodeRows` helper for the `(none)` empty-block form.
`comment_count` is emitted only in the default view; when `--comments` is passed, the full `comments` block replaces it rather than sitting alongside a redundant scalar.
This matches gh-axi's documented behaviour ("with `--comments`, the comments block is appended") — the spec lists `comment_count` as a default field but does not require it to persist under `--comments`.
When there are no comments, `comment_count` renders as the bare number `0` (no `use --comments` hint, since there is nothing to expand).
The `--full` next-step suggestion fires whenever the rendered body differs from the raw body — i.e. it was cleaned-under-limit *or* truncated — read directly off the rendered output rather than recomputing the over-limit threshold, so the suggestion can never drift from `truncateBody`'s own decision.
The default detail fields (`number`, `title`, `state`, `author`, `created`) reuse the shared `FieldDef`/`extractRow` extraction from the list path; only `body` and `comment_count` are handled bespokely.
As a consequence the displayed `number` comes straight from the fetched issue rather than falling back to the requested number, which is safe because the get-issue endpoint always returns it.