Documentation menu
On this page
Pinned technical reference

Read-only GitHub workspace

Canonical read-only github workspace from the v0.20.0 published source.

Status: read-only, included in v0.18.0 as /github, /prs and /issues. There is no remote mutation of any kind; the merge preview only describes what a merge would do. Physical terminal QA is still pending.

Boundary: GitHub owns remote PR and issue truth. Git owns local checkouts (see #337). NMSh owns presentation, navigation and safe interaction. This is a terminal control surface, not an IDE or a GitHub replacement.

Try it

From the isolated worktree:

cd /private/tmp/notMyShell-github-workspace
npx tsx scripts/dev/github-workspace-preview.ts
ModeCommand
Fixtures (offline, default)npx tsx scripts/dev/github-workspace-preview.ts
Fixture error scenario… --scenario=auth (ok empty auth rate_limit offline slow partial flaky)
Live, read-only… --live --repo=raiseCatError/notMyShell (uses your authenticated gh)
Printed frames (no TTY)… --static --width=40 --height=20 --keys=down,enter,tab,m
Safe glyphs / no color… --safe, … --no-color (NO_COLOR=1 is honored)

Keys that only exist in the preview:

KeyAction
Ctrl-ENext fixture scenario
Ctrl-GSwitch between Safe and Nerd glyphs
Ctrl-KColor on/off
Ctrl-WCycle the simulated width: 30/40/50/80/120/200/terminal
Ctrl-C or QQuit; the terminal is restored

The preview uses the alternate screen and restores the terminal on exit, on a signal and on an uncaught error. It is not imported by src/ and does not change NMSh startup.

Modules (src/githubWorkspace/)

FileRole
model.tsTyped facts and LIMITS. Fields GitHub did not report stay undefined or 'unknown'.
sanitize.tsUntrusted text goes through here: control and escape sequences and bidi overrides are stripped, and length is bounded. Also holds the owner/repo allowlist and https://github.com-only URLs.
query.tsGitHub search query passthrough, view scoping, honest notices, bounded history.
transport.tsRead-only gh api transport: argv only, bounded by time and bytes, credential-free errors.
queries.tsBounded GraphQL documents that request totalCount, so truncation is reported.
source.tsGithubSource interface, GraphQL/REST to model mapping, GhSource.
service.tsBounded LRU caches, request de-duplication, 2-request concurrency limit. Patches are keyed by head SHA.
diff.tsBounded read-only diff model, compact summary, external-review intent.
mergePlan.tsMerge plan contract, the always-refusing executeRemoteWrite, and the OpenRelatedWorktree intent.
controller.tsKeyboard-first state machine with one focus owner. Fetches only on explicit actions.
render.tsPure renderer: state to exactly rows lines of at most columns cells. No I/O.
fixtures.tsDeterministic raw GitHub-shaped data, including hostile entries. It runs through the real mappers.

Data sources (factual)

DataSource
List and searchGraphQL search(type: ISSUE), 25 results per page, cursor pagination, issueCount, rateLimit. Summary fields only: state, draft, head/base, review decision, mergeable, head commit statusCheckRollup, additions/deletions/files.
PR detailGraphQL repository.pullRequest, fetched lazily when you press Enter on an item. It includes headRefOid, mergeStateStatus, individual CheckRuns and StatusContexts (with isRequired), the last 100 commits, issue comments, reviews and inline review comments (kept distinct), the first 100 files, and repository merge policy (mergeCommitAllowed, squashMergeAllowed, rebaseMergeAllowed, deleteBranchOnMerge, viewerPermission).
PatchesREST GET repos/{o}/{r}/pulls/{n}/files, fetched lazily the first time the Diff tab opens. Up to 3 pages of 100, cached per head SHA.
Issue detailGraphQL repository.issue: labels, assignees, comments. Related links come only from closedByPullRequestsReferences and CROSS_REFERENCED_EVENT. Nothing is inferred from titles or branch names.

Partial GraphQL errors keep the data that did arrive and show it labelled “Partial results” or “Partial data”. Collections GitHub reports as larger than what NMSh holds are labelled “Showing N of M”.

Authentication boundary

  • Authentication belongs to gh (keyring or config). NMSh never reads, prints, stores or passes a token, and never puts credentials in argv.
  • execFile('gh', argv) runs without a shell. The query, owner, repo and number are GraphQL variables: strings go through -f (a raw field, never expanded as @file), and validated integers go through -F. The working directory is $HOME, so the repository you are in has no effect.
  • Each call has a 20 s timeout and an 8 MiB output limit. The environment sets GH_PROMPT_DISABLED, NO_COLOR and GH_PAGER=cat, and removes GH_DEBUG and DEBUG.
  • Error text has token-shaped strings redacted and is sanitized. Failures are classified as auth, rate_limit, network, not_found, invalid_query, timeout, too_large, unavailable (gh missing) or unknown. All are recoverable with R. Results already on screen stay visible and are labelled with their age.
  • Network access only happens on explicit actions: submitting a query, Tab to a view that has never loaded, N for more results, Enter on an item, opening the Diff tab, or R.

Read-only policy

GhCliTransport has exactly two methods: graphql and restGet. graphql rejects any document that is not a single query. restGet always sends --method GET and accepts only repos/<owner>/<repo>/… paths built from validated parts. executeRemoteWrite (merge, deleteBranch, submitReview, closeIssue) always throws RemoteWriteDisabledError. Tests assert all of this. No background agent may merge.

Query semantics

The text you type is kept as the original query. The effective query is exactly what GitHub receives, and it is displayed. GitHub’s search remains the authority on what a query means.

  • PRs view: adds is:pr when the query does not specify a type. A query that asks only for issues is refused with an explanation.
  • Issues view: the same, with is:issue.
  • Search view: the query is passed through unchanged and can return both PRs and issues.
  • Local checks reject: empty queries, more than 256 characters, control characters, an unbalanced ", and more than 5 AND/OR/NOT operators (a GitHub limit).
  • An undocumented qualifier: produces a warning that GitHub may treat it as text. It is never silently dropped.
  • Queries run only when submitted with Enter, never per keystroke. Search history keeps at most 20 entries.
  • Results are capped at 8 pages and 200 items. GitHub search itself navigates at most 1000.
  • Selection identity is kind:owner/repo#number. A refresh keeps the selected item when it is still present, and says so when it is gone.

Keyboard UX

Exactly one region owns the keyboard: list, query, detail or merge. The header names it ([focus: …]). The active tab is shown in brackets, and the selected row has a marker (> or ›) and is also shown inverse when color is on. None of this depends on color.

ContextKeys
ListUp/Down (j/k) to select, PgUp/PgDn, Home/End, Enter to inspect, / to search, Tab/Shift-Tab to switch PRs/Issues/Search, R to refresh, N for more, O to open on GitHub (intent), Q/Esc to quit
QueryType, Backspace, Enter to submit, Esc to cancel, Up/Down for history
DetailTab/Shift-Tab between sections, Up/Down/PgUp/PgDn to scroll, N/P for next/previous file, Enter on Files to open its diff, D for compact/review depth, R to refresh, M for merge preview, W for worktree intent, O to open, Esc to go back
Merge previewTab for method, B to toggle branch deletion, Up/Down to scroll, R to refresh and replan, Esc to close. Enter only explains that merging is disabled.

The footer shows only the actions available right now. At narrow widths it falls back to keys only (Esc back Tab ^v Pg R M W O).

Diff behavior and limitations

The diff has three depths:

  • Compact: files changed, +/- totals and the largest files.
  • Review: one file at a time, with hunks, old/new line numbers and context lines. N/P move between files.
  • External: an OpenExternalReview intent pointing at the PR’s /files URL on GitHub.

Every diff view is headed with its source: “Remote GitHub PR diff”, the repository and PR number, base ← head, and the head SHA. Each file is labelled with how complete its patch is:

LabelMeaning
completePatch present, and its +/- counts match what GitHub reported.
incompletePatch shorter than GitHub’s counts; GitHub abbreviated it.
truncatedNMSh’s 1,500-lines-per-file bound cut the patch.
omittedGitHub sent no patch (binary or too large). A rename without content changes is labelled as such.
skippedNMSh’s 20,000-line total budget was already used.

When GitHub reports more files than were loaded (the 300-file bound), the diff says it is NOT complete. Patch lines are sanitized, tabs are expanded, and each line is limited to 1,000 characters. Nothing in a patch is evaluated. In color mode, long lines are truncated rather than wrapped. There is no syntax highlighting and no side-by-side view; those are for #332.

Unknown and stale states

  • Mergeability: GitHub UNKNOWN displays as “unknown (GitHub has not computed it)”.
  • Merge state: “unknown (not reported)” when GitHub did not report it.
  • Checks: the rollup, plus each individual check with its status and conclusion. Required is shown as (required), (required: unknown), or not shown when GitHub says it is not required.
  • Review decision: a null decision is shown as “no review decision reported”.
  • Data older than 2 minutes is labelled STALE with its age. A failed refresh keeps the earlier data with an explicit “Refresh failed; showing … from Xm ago”.

Secure future merge transaction (not implemented)

planMerge(detail, {method, deleteBranch}) builds the preview: repository, PR number, exact head SHA, head → base, per-method support (allowed, disabled_by_repository or unknown), mergeability, merge state, and conditions (blockers, unknowns, warnings: draft, conflicts, blocked or behind, requested changes, required checks that failed or are pending, permission, stale data, truncated checks). It also carries a warning that this is a remote, shared action, and a separate destructive warning when branch deletion is requested (noting a cross-repository head when there is one). executable is always false.

A future integration must:

  1. Re-fetch the PR from GitHub immediately before acting and rebuild the plan.
  2. Show the plan and require explicit confirmation from a human.
  3. Call the merge with expectedHeadOid (GraphQL) or sha (REST) set to the previewed head SHA, so a push in between makes the merge fail.
  4. Delete the branch as a separate, separately confirmed step, and report partial success (merged, branch kept) truthfully.
  5. Never bypass branch protection, and never infer readiness from local Git.

Live routing (v0.18.0)

/github and /prs open the PRs tab, /issues the Issues tab, for the repository of the shell’s origin remote when it is on github.com (https, ssh or scp form; anything else opens nothing and says why). TerminalApp constructs GhSource, GithubWorkspaceService and GithubWorkspaceController and renders in the shared panel slot; NMSh’s decoded keys map onto WorkspaceKey (src/githubWorkspace/host.ts), with Tab and Shift+Tab switching tabs. Intents: OpenExternal and OpenExternalReview open only safeGithubUrl URLs with the system opener; OpenRelatedWorktree opens /worktrees on the PR’s head branch (same-repository PRs only); Exit closes the panel.

Future integrations

  • #336 (semantic actions): availableActions() becomes the action list. Mouse actions call the same handlers.
  • #331 (sidebar): reuse SearchItem, itemKey and checksLabel for quick links. Deep management stays here.
  • #332 (detail and diff panel): consume DiffModel and detailLines. The right panel replaces the inline review depth.

Verification

Automated: tests/githubWorkspace.test.ts covers queries, identity, pagination, refresh, stale and error states, transport safety, hostile content, layout at widths from 30 to 200, Safe and NO_COLOR rendering, the diff, checks, merge planning, disabled writes, the keyboard, intents, bounds, and render purity.

Human physical QA in real terminals is pending.

Reviewed against v0.20.0 · source 65e4de55 · 2026-10-18
Sources: github-workspace.md
Suggest a correction ↗

Documentation

Try “sessions” or “Theme Bridge”. Search runs in your browser.