detached 2158c27

fix ci: portable syscall.Select via x/sys/unix; gofmt pass

- rewrite resize_unix.go on top of golang.org/x/sys/unix so the
  peek-stdin helper compiles on linux (Select returns (int, error),
  Timeval.Usec is int64) as well as darwin (int32, error-only)
- promote golang.org/x/sys to a direct dep
- gofmt -w . (11 files of alignment drift from recent edits)
- install.sh / install.ps1: accept $GITHUB_TOKEN so the installers
  work against the repo while it's private; no-op on public repos
- README: document the private-repo install paths (PAT for curl|bash
  and powershell, GOPRIVATE for go install)
commit
2158c272afd7bbc914c7f5157518d0e7a8c0990d
author
patriceckhart · · 2026-04-18 08:55 UTC
parent
4712f5e
signature
unsigned
17 files · +159 -109
M README.md +23 -2
@@ -12,10 +12,20 @@ yet another coding agent harness, lightweight and written (vibe-slopped) in go.
1212
1313 ## install
1414
15+> the repo and prebuilt artifacts are **private at the moment**. until they go public, every download command below needs a github personal access token (PAT) with `contents:read` scope on `patriceckhart/zot`. create one at <https://github.com/settings/tokens> and export it before running any installer:
16+>
17+> ```bash
18+> export GITHUB_TOKEN=ghp_xxx
19+> ```
20+>
21+> once the repo is public, the token becomes optional.
22+
1523 ### one-liner (macos + linux)
1624
1725 ```bash
18-curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
26+curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" \
27+ https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh \
28+ | GITHUB_TOKEN=$GITHUB_TOKEN bash
1929 ```
2030
2131 detects your os/arch, downloads the latest release from github, verifies the sha256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin` — whichever is writable first. pass a version or prefix to pin:
@@ -27,23 +37,34 @@ curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh |
2737 ### one-liner (windows, powershell)
2838
2939 ```powershell
30-iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
40+$env:GITHUB_TOKEN = "ghp_xxx"
41+$h = @{ Authorization = "Bearer $env:GITHUB_TOKEN" }
42+(Invoke-WebRequest -UseBasicParsing -Headers $h `
43+ https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1).Content | iex
3144 ```
3245
3346 drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards.
3447
3548 ### homebrew (macos + linux)
3649
50+available once the repo is public. the tap lives at `patriceckhart/homebrew-tap`:
51+
3752 ```bash
3853 brew install patriceckhart/tap/zot
3954 ```
4055
4156 ### go install
4257
58+while the repo is private, `go install` needs to bypass the public go module proxy:
59+
4360 ```bash
61+export GOPRIVATE=github.com/patriceckhart/*
62+export GIT_TERMINAL_PROMPT=0
4463 go install github.com/patriceckhart/zot/cmd/zot@latest
4564 ```
4665
66+your git must already be authenticated to github (ssh key or https PAT in `~/.netrc`). once the repo is public, `go install github.com/patriceckhart/zot/cmd/zot@latest` is enough.
67+
4768 ### from source
4869
4970 ```bash
@@ -12,10 +12,20 @@ yet another coding agent harness, lightweight and written (vibe-slopped) in go.
12 12
13 ## install13 ## install
14 14
15+> the repo and prebuilt artifacts are **private at the moment**. until they go public, every download command below needs a github personal access token (PAT) with `contents:read` scope on `patriceckhart/zot`. create one at <https://github.com/settings/tokens> and export it before running any installer:
16+>
17+> ```bash
18+> export GITHUB_TOKEN=ghp_xxx
19+> ```
20+>
21+> once the repo is public, the token becomes optional.
22+
15 ### one-liner (macos + linux)23 ### one-liner (macos + linux)
16 24
17 ```bash25 ```bash
18-curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash26+curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" \
27+ https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh \
28+ | GITHUB_TOKEN=$GITHUB_TOKEN bash
19 ```29 ```
20 30
21 detects your os/arch, downloads the latest release from github, verifies the sha256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin` — whichever is writable first. pass a version or prefix to pin:31 detects your os/arch, downloads the latest release from github, verifies the sha256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin` — whichever is writable first. pass a version or prefix to pin:
@@ -27,23 +37,34 @@ curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh |
27 ### one-liner (windows, powershell)37 ### one-liner (windows, powershell)
28 38
29 ```powershell39 ```powershell
30-iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex40+$env:GITHUB_TOKEN = "ghp_xxx"
41+$h = @{ Authorization = "Bearer $env:GITHUB_TOKEN" }
42+(Invoke-WebRequest -UseBasicParsing -Headers $h `
43+ https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1).Content | iex
31 ```44 ```
32 45
33 drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards.46 drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards.
34 47
35 ### homebrew (macos + linux)48 ### homebrew (macos + linux)
36 49
50+available once the repo is public. the tap lives at `patriceckhart/homebrew-tap`:
51+
37 ```bash52 ```bash
38 brew install patriceckhart/tap/zot53 brew install patriceckhart/tap/zot
39 ```54 ```
40 55
41 ### go install56 ### go install
42 57
58+while the repo is private, `go install` needs to bypass the public go module proxy:
59+
43 ```bash60 ```bash
61+export GOPRIVATE=github.com/patriceckhart/*
62+export GIT_TERMINAL_PROMPT=0
44 go install github.com/patriceckhart/zot/cmd/zot@latest63 go install github.com/patriceckhart/zot/cmd/zot@latest
45 ```64 ```
46 65
66+your git must already be authenticated to github (ssh key or https PAT in `~/.netrc`). once the repo is public, `go install github.com/patriceckhart/zot/cmd/zot@latest` is enough.
67+
47 ### from source68 ### from source
48 69
49 ```bash70 ```bash
M go.mod +2 -2
@@ -3,14 +3,14 @@ module github.com/patriceckhart/zot
33 go 1.22
44
55 require (
6+ github.com/alecthomas/chroma/v2 v2.23.1
67 github.com/google/uuid v1.6.0
78 github.com/mattn/go-runewidth v0.0.16
9+ golang.org/x/sys v0.26.0
810 golang.org/x/term v0.25.0
911 )
1012
1113 require (
12- github.com/alecthomas/chroma/v2 v2.23.1 // indirect
1314 github.com/dlclark/regexp2 v1.11.5 // indirect
1415 github.com/rivo/uniseg v0.2.0 // indirect
15- golang.org/x/sys v0.26.0 // indirect
1616 )
@@ -3,14 +3,14 @@ module github.com/patriceckhart/zot
3 go 1.223 go 1.22
4 4
5 require (5 require (
6+ github.com/alecthomas/chroma/v2 v2.23.1
6 github.com/google/uuid v1.6.07 github.com/google/uuid v1.6.0
7 github.com/mattn/go-runewidth v0.0.168 github.com/mattn/go-runewidth v0.0.16
9+ golang.org/x/sys v0.26.0
8 golang.org/x/term v0.25.010 golang.org/x/term v0.25.0
9 )11 )
10 12
11 require (13 require (
12- github.com/alecthomas/chroma/v2 v2.23.1 // indirect
13 github.com/dlclark/regexp2 v1.11.5 // indirect14 github.com/dlclark/regexp2 v1.11.5 // indirect
14 github.com/rivo/uniseg v0.2.0 // indirect15 github.com/rivo/uniseg v0.2.0 // indirect
15- golang.org/x/sys v0.26.0 // indirect
16 )16 )
M go.sum +6 -4 generated file: collapsed
M install.ps1 +22 -7
@@ -12,6 +12,11 @@
1212 # release, verifies the sha256 against checksums.txt, extracts zot.exe,
1313 # and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH
1414 # via the User environment if missing).
15+#
16+# While the repo is private, set $env:GITHUB_TOKEN to a PAT with
17+# `contents:read` scope; the script uses it for every download. Once
18+# the repo goes public, the token becomes optional.
19+
1520
1621 [CmdletBinding()]
1722 param(
@@ -25,6 +30,11 @@ $owner = "patriceckhart"
2530 $repo = "zot"
2631 $binary = "zot"
2732
33+# Build Authorization header list once; used on every HTTP call so the
34+# script works against private repos when $env:GITHUB_TOKEN is set.
35+$headers = @{}
36+if ($env:GITHUB_TOKEN) { $headers["Authorization"] = "Bearer $($env:GITHUB_TOKEN)" }
37+
2838 if (-not $Version) { $Version = "latest" }
2939 if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }
3040
@@ -50,12 +60,17 @@ if ($arch -eq "arm64") {
5060 # ---- resolve version ----
5161
5262 if ($Version -eq "latest") {
53- $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `
54- "https://github.com/$owner/$repo/releases/latest"
55- if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {
56- $Version = $Matches[1]
63+ if ($headers.ContainsKey("Authorization")) {
64+ $api = Invoke-RestMethod -Headers $headers "https://api.github.com/repos/$owner/$repo/releases/latest"
65+ $Version = $api.tag_name
5766 } else {
58- Die "could not resolve latest version"
67+ $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `
68+ "https://github.com/$owner/$repo/releases/latest"
69+ if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {
70+ $Version = $Matches[1]
71+ } else {
72+ Die "could not resolve latest version (set `$env:GITHUB_TOKEN if the repo is private)"
73+ }
5974 }
6075 }
6176
@@ -73,10 +88,10 @@ $tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zot-install-" +
7388
7489 try {
7590 Msg "downloading $archive"
76- Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
91+ Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
7792
7893 Msg "verifying checksum"
79- $checksums = Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl | Select-Object -ExpandProperty Content
94+ $checksums = Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $checksumUrl | Select-Object -ExpandProperty Content
8095 $expected = ($checksums -split "`n" | Where-Object { $_ -match [regex]::Escape($archive) + "$" } | Select-Object -First 1)
8196 if (-not $expected) { Die "no checksum for $archive in checksums.txt" }
8297 $expectedHash = ($expected -split "\s+")[0]
@@ -12,6 +12,11 @@
12 # release, verifies the sha256 against checksums.txt, extracts zot.exe,12 # release, verifies the sha256 against checksums.txt, extracts zot.exe,
13 # and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH13 # and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH
14 # via the User environment if missing).14 # via the User environment if missing).
15+#
16+# While the repo is private, set $env:GITHUB_TOKEN to a PAT with
17+# `contents:read` scope; the script uses it for every download. Once
18+# the repo goes public, the token becomes optional.
19+
15 20
16 [CmdletBinding()]21 [CmdletBinding()]
17 param(22 param(
@@ -25,6 +30,11 @@ $owner = "patriceckhart"
25 $repo = "zot"30 $repo = "zot"
26 $binary = "zot"31 $binary = "zot"
27 32
33+# Build Authorization header list once; used on every HTTP call so the
34+# script works against private repos when $env:GITHUB_TOKEN is set.
35+$headers = @{}
36+if ($env:GITHUB_TOKEN) { $headers["Authorization"] = "Bearer $($env:GITHUB_TOKEN)" }
37+
28 if (-not $Version) { $Version = "latest" }38 if (-not $Version) { $Version = "latest" }
29 if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }39 if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }
30 40
@@ -50,12 +60,17 @@ if ($arch -eq "arm64") {
50 # ---- resolve version ----60 # ---- resolve version ----
51 61
52 if ($Version -eq "latest") {62 if ($Version -eq "latest") {
53- $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `63+ if ($headers.ContainsKey("Authorization")) {
54- "https://github.com/$owner/$repo/releases/latest"64+ $api = Invoke-RestMethod -Headers $headers "https://api.github.com/repos/$owner/$repo/releases/latest"
55- if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {65+ $Version = $api.tag_name
56- $Version = $Matches[1]
57 } else {66 } else {
58- Die "could not resolve latest version"67+ $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `
68+ "https://github.com/$owner/$repo/releases/latest"
69+ if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {
70+ $Version = $Matches[1]
71+ } else {
72+ Die "could not resolve latest version (set `$env:GITHUB_TOKEN if the repo is private)"
73+ }
59 }74 }
60 }75 }
61 76
@@ -73,10 +88,10 @@ $tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zot-install-" +
73 88
74 try {89 try {
75 Msg "downloading $archive"90 Msg "downloading $archive"
76- Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)91+ Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
77 92
78 Msg "verifying checksum"93 Msg "verifying checksum"
79- $checksums = Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl | Select-Object -ExpandProperty Content94+ $checksums = Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $checksumUrl | Select-Object -ExpandProperty Content
80 $expected = ($checksums -split "`n" | Where-Object { $_ -match [regex]::Escape($archive) + "$" } | Select-Object -First 1)95 $expected = ($checksums -split "`n" | Where-Object { $_ -match [regex]::Escape($archive) + "$" } | Select-Object -First 1)
81 if (-not $expected) { Die "no checksum for $archive in checksums.txt" }96 if (-not $expected) { Die "no checksum for $archive in checksums.txt" }
82 $expectedHash = ($expected -split "\s+")[0]97 $expectedHash = ($expected -split "\s+")[0]
M install.sh +26 -10
@@ -16,6 +16,9 @@
1616 # Environment overrides:
1717 # ZOT_VERSION same as $1
1818 # ZOT_PREFIX same as $2
19+# GITHUB_TOKEN personal access token — required while the repo is
20+# private, ignored once it goes public. Must have at
21+# least `contents:read` scope on the zot repository.
1922 #
2023 # The script detects your OS and architecture, downloads the matching
2124 # archive from the GitHub release, verifies the sha256 against the
@@ -39,6 +42,14 @@ die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
3942 command -v curl >/dev/null 2>&1 || die "curl is required"
4043 command -v tar >/dev/null 2>&1 || die "tar is required"
4144
45+# CURL_AUTH is prepended to every curl invocation so private-repo
46+# downloads work while $GITHUB_TOKEN is set. Empty string when the repo
47+# becomes public.
48+CURL_AUTH=()
49+if [ -n "${GITHUB_TOKEN:-}" ]; then
50+ CURL_AUTH=(-H "Authorization: Bearer $GITHUB_TOKEN")
51+fi
52+
4253 # ---- detect OS + arch ----
4354
4455 uname_s=$(uname -s)
@@ -62,13 +73,18 @@ esac
6273 # ---- resolve version ----
6374
6475 if [ "$VERSION" = "latest" ]; then
65- # GitHub's /releases/latest endpoint redirects to /releases/tag/<tag>.
66- # We follow the redirect and grab the tag from the final URL without
67- # needing jq.
68- VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
69- "https://github.com/${OWNER}/${REPO}/releases/latest" \
70- | sed -E 's|.*/tag/([^/]+).*|\1|')
71- [ -n "$VERSION" ] || die "could not resolve latest version"
76+ # Private-repo friendly: hit the api, grab tag_name. Falls back to
77+ # following the /releases/latest redirect on public repos.
78+ if [ ${#CURL_AUTH[@]} -gt 0 ]; then
79+ VERSION=$(curl -fsSL "${CURL_AUTH[@]}" \
80+ "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \
81+ | sed -nE 's/.*"tag_name": *"([^"]+)".*/\1/p' | head -n1)
82+ else
83+ VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
84+ "https://github.com/${OWNER}/${REPO}/releases/latest" \
85+ | sed -E 's|.*/tag/([^/]+).*|\1|')
86+ fi
87+ [ -n "$VERSION" ] || die "could not resolve latest version (set GITHUB_TOKEN if the repo is private)"
7288 fi
7389
7490 case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac
@@ -110,11 +126,11 @@ TMP=$(mktemp -d)
110126 trap 'rm -rf "$TMP"' EXIT
111127
112128 msg "downloading ${ARCHIVE}"
113-curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
114- || die "download failed: $ARCHIVE_URL"
129+curl -fsSL "${CURL_AUTH[@]}" -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
130+ || die "download failed: $ARCHIVE_URL (set GITHUB_TOKEN if the repo is private)"
115131
116132 msg "verifying checksum"
117-curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
133+curl -fsSL "${CURL_AUTH[@]}" -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
118134 || die "download failed: $CHECKSUMS_URL"
119135
120136 expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)
@@ -16,6 +16,9 @@
16 # Environment overrides:16 # Environment overrides:
17 # ZOT_VERSION same as $117 # ZOT_VERSION same as $1
18 # ZOT_PREFIX same as $218 # ZOT_PREFIX same as $2
19+# GITHUB_TOKEN personal access token — required while the repo is
20+# private, ignored once it goes public. Must have at
21+# least `contents:read` scope on the zot repository.
19 #22 #
20 # The script detects your OS and architecture, downloads the matching23 # The script detects your OS and architecture, downloads the matching
21 # archive from the GitHub release, verifies the sha256 against the24 # archive from the GitHub release, verifies the sha256 against the
@@ -39,6 +42,14 @@ die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
39 command -v curl >/dev/null 2>&1 || die "curl is required"42 command -v curl >/dev/null 2>&1 || die "curl is required"
40 command -v tar >/dev/null 2>&1 || die "tar is required"43 command -v tar >/dev/null 2>&1 || die "tar is required"
41 44
45+# CURL_AUTH is prepended to every curl invocation so private-repo
46+# downloads work while $GITHUB_TOKEN is set. Empty string when the repo
47+# becomes public.
48+CURL_AUTH=()
49+if [ -n "${GITHUB_TOKEN:-}" ]; then
50+ CURL_AUTH=(-H "Authorization: Bearer $GITHUB_TOKEN")
51+fi
52+
42 # ---- detect OS + arch ----53 # ---- detect OS + arch ----
43 54
44 uname_s=$(uname -s)55 uname_s=$(uname -s)
@@ -62,13 +73,18 @@ esac
62 # ---- resolve version ----73 # ---- resolve version ----
63 74
64 if [ "$VERSION" = "latest" ]; then75 if [ "$VERSION" = "latest" ]; then
65- # GitHub's /releases/latest endpoint redirects to /releases/tag/<tag>.76+ # Private-repo friendly: hit the api, grab tag_name. Falls back to
66- # We follow the redirect and grab the tag from the final URL without77+ # following the /releases/latest redirect on public repos.
67- # needing jq.78+ if [ ${#CURL_AUTH[@]} -gt 0 ]; then
68- VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \79+ VERSION=$(curl -fsSL "${CURL_AUTH[@]}" \
69- "https://github.com/${OWNER}/${REPO}/releases/latest" \80+ "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \
70- | sed -E 's|.*/tag/([^/]+).*|\1|')81+ | sed -nE 's/.*"tag_name": *"([^"]+)".*/\1/p' | head -n1)
71- [ -n "$VERSION" ] || die "could not resolve latest version"82+ else
83+ VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
84+ "https://github.com/${OWNER}/${REPO}/releases/latest" \
85+ | sed -E 's|.*/tag/([^/]+).*|\1|')
86+ fi
87+ [ -n "$VERSION" ] || die "could not resolve latest version (set GITHUB_TOKEN if the repo is private)"
72 fi88 fi
73 89
74 case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac90 case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac
@@ -110,11 +126,11 @@ TMP=$(mktemp -d)
110 trap 'rm -rf "$TMP"' EXIT126 trap 'rm -rf "$TMP"' EXIT
111 127
112 msg "downloading ${ARCHIVE}"128 msg "downloading ${ARCHIVE}"
113-curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \129+curl -fsSL "${CURL_AUTH[@]}" -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
114- || die "download failed: $ARCHIVE_URL"130+ || die "download failed: $ARCHIVE_URL (set GITHUB_TOKEN if the repo is private)"
115 131
116 msg "verifying checksum"132 msg "verifying checksum"
117-curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \133+curl -fsSL "${CURL_AUTH[@]}" -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
118 || die "download failed: $CHECKSUMS_URL"134 || die "download failed: $CHECKSUMS_URL"
119 135
120 expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)136 expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)
M internal/agent/cli.go +15 -15
@@ -200,21 +200,21 @@ func runInteractive(ctx context.Context, args Args, version string) error {
200200
201201 term := tui.NewProcTerm()
202202 iv := modes.NewInteractive(modes.InteractiveConfig{
203- Terminal: term,
204- Theme: tui.Dark,
205- Model: r.Model,
206- Provider: r.Provider,
207- AuthMethod: r.AuthMethod,
208- BaseURL: r.BaseURL,
209- Reasoning: r.Reasoning,
210- SystemPrompt: r.SystemPrompt,
211- Tools: r.ToolRegistry,
212- MaxSteps: r.MaxSteps,
213- CWD: r.CWD,
214- ZotHome: ZotHome(),
215- Sandbox: sharedSandbox,
216- Agent: ag,
217- InitialInput: args.Prompt,
203+ Terminal: term,
204+ Theme: tui.Dark,
205+ Model: r.Model,
206+ Provider: r.Provider,
207+ AuthMethod: r.AuthMethod,
208+ BaseURL: r.BaseURL,
209+ Reasoning: r.Reasoning,
210+ SystemPrompt: r.SystemPrompt,
211+ Tools: r.ToolRegistry,
212+ MaxSteps: r.MaxSteps,
213+ CWD: r.CWD,
214+ ZotHome: ZotHome(),
215+ Sandbox: sharedSandbox,
216+ Agent: ag,
217+ InitialInput: args.Prompt,
218218 AuthManager: mgr,
219219 BuildAgent: buildAgent,
220220 BuildAgentFor: buildAgentFor,
@@ -200,21 +200,21 @@ func runInteractive(ctx context.Context, args Args, version string) error {
200 200
201 term := tui.NewProcTerm()201 term := tui.NewProcTerm()
202 iv := modes.NewInteractive(modes.InteractiveConfig{202 iv := modes.NewInteractive(modes.InteractiveConfig{
203- Terminal: term,203+ Terminal: term,
204- Theme: tui.Dark,204+ Theme: tui.Dark,
205- Model: r.Model,205+ Model: r.Model,
206- Provider: r.Provider,206+ Provider: r.Provider,
207- AuthMethod: r.AuthMethod,207+ AuthMethod: r.AuthMethod,
208- BaseURL: r.BaseURL,208+ BaseURL: r.BaseURL,
209- Reasoning: r.Reasoning,209+ Reasoning: r.Reasoning,
210- SystemPrompt: r.SystemPrompt,210+ SystemPrompt: r.SystemPrompt,
211- Tools: r.ToolRegistry,211+ Tools: r.ToolRegistry,
212- MaxSteps: r.MaxSteps,212+ MaxSteps: r.MaxSteps,
213- CWD: r.CWD,213+ CWD: r.CWD,
214- ZotHome: ZotHome(),214+ ZotHome: ZotHome(),
215- Sandbox: sharedSandbox,215+ Sandbox: sharedSandbox,
216- Agent: ag,216+ Agent: ag,
217- InitialInput: args.Prompt,217+ InitialInput: args.Prompt,
218 AuthManager: mgr,218 AuthManager: mgr,
219 BuildAgent: buildAgent,219 BuildAgent: buildAgent,
220 BuildAgentFor: buildAgentFor,220 BuildAgentFor: buildAgentFor,
M internal/agent/modes/dialog_frame.go +1 -1
@@ -10,7 +10,7 @@ import (
1010 // Matches the thin rule style used for code blocks and tool results so
1111 // every dialog in the TUI looks the same.
1212 //
13-// ─── title ────────────────────────────────
13+// ─── title ────────────────────────────────
1414 func frameHeader(th tui.Theme, title string, width int) string {
1515 label := "── " + title + " "
1616 if width <= 0 {
@@ -10,7 +10,7 @@ import (
10 // Matches the thin rule style used for code blocks and tool results so10 // Matches the thin rule style used for code blocks and tool results so
11 // every dialog in the TUI looks the same.11 // every dialog in the TUI looks the same.
12 //12 //
13-// ─── title ────────────────────────────────13+// ─── title ────────────────────────────────
14 func frameHeader(th tui.Theme, title string, width int) string {14 func frameHeader(th tui.Theme, title string, width int) string {
15 label := "── " + title + " "15 label := "── " + title + " "
16 if width <= 0 {16 if width <= 0 {
M internal/agent/modes/interactive.go +4 -4
@@ -120,10 +120,10 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
120120 Theme: cfg.Theme,
121121 ImageProto: tui.DetectImageProtocol(),
122122 },
123- ed: tui.NewEditor(cfg.Theme.FG256(cfg.Theme.Accent, "▌ ")),
124- rend: tui.NewRenderer(cfg.Terminal),
125- toolCalls: map[string]*tui.ToolCallView{},
126- dirty: make(chan struct{}, 8),
123+ ed: tui.NewEditor(cfg.Theme.FG256(cfg.Theme.Accent, "▌ ")),
124+ rend: tui.NewRenderer(cfg.Terminal),
125+ toolCalls: map[string]*tui.ToolCallView{},
126+ dirty: make(chan struct{}, 8),
127127 dialog: newLoginDialog(),
128128 modelDialog: newModelDialog(),
129129 sessionDialog: newSessionDialog(),
@@ -120,10 +120,10 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
120 Theme: cfg.Theme,120 Theme: cfg.Theme,
121 ImageProto: tui.DetectImageProtocol(),121 ImageProto: tui.DetectImageProtocol(),
122 },122 },
123- ed: tui.NewEditor(cfg.Theme.FG256(cfg.Theme.Accent, "▌ ")),123+ ed: tui.NewEditor(cfg.Theme.FG256(cfg.Theme.Accent, "▌ ")),
124- rend: tui.NewRenderer(cfg.Terminal),124+ rend: tui.NewRenderer(cfg.Terminal),
125- toolCalls: map[string]*tui.ToolCallView{},125+ toolCalls: map[string]*tui.ToolCallView{},
126- dirty: make(chan struct{}, 8),126+ dirty: make(chan struct{}, 8),
127 dialog: newLoginDialog(),127 dialog: newLoginDialog(),
128 modelDialog: newModelDialog(),128 modelDialog: newModelDialog(),
129 sessionDialog: newSessionDialog(),129 sessionDialog: newSessionDialog(),
M internal/agent/modes/session_dialog.go +3 -3
@@ -11,9 +11,9 @@ import (
1111
1212 // sessionDialog is the inline picker shown when the user runs /sessions.
1313 type sessionDialog struct {
14- active bool
15- sessions []core.SessionSummary
16- cursor int
14+ active bool
15+ sessions []core.SessionSummary
16+ cursor int
1717 }
1818
1919 // sessionDialogAction is returned by HandleKey.
@@ -11,9 +11,9 @@ import (
11 11
12 // sessionDialog is the inline picker shown when the user runs /sessions.12 // sessionDialog is the inline picker shown when the user runs /sessions.
13 type sessionDialog struct {13 type sessionDialog struct {
14- active bool14+ active bool
15- sessions []core.SessionSummary15+ sessions []core.SessionSummary
16- cursor int16+ cursor int
17 }17 }
18 18
19 // sessionDialogAction is returned by HandleKey.19 // sessionDialogAction is returned by HandleKey.
M internal/agent/modes/spinner.go +4 -4
@@ -9,11 +9,11 @@ import (
99 // turn is streaming. It rotates through a list of playful status
1010 // messages and a small frame animation.
1111 type spinner struct {
12- frames []string
13- messages []string
12+ frames []string
13+ messages []string
1414 startedAt time.Time
15- msgIdx int
16- lastSwap time.Time
15+ msgIdx int
16+ lastSwap time.Time
1717
1818 // fixedMsg overrides the rotating funnyWorkingLines message when
1919 // set. Used for auto-compaction so the spinner clearly says what's
@@ -9,11 +9,11 @@ import (
9 // turn is streaming. It rotates through a list of playful status9 // turn is streaming. It rotates through a list of playful status
10 // messages and a small frame animation.10 // messages and a small frame animation.
11 type spinner struct {11 type spinner struct {
12- frames []string12+ frames []string
13- messages []string13+ messages []string
14 startedAt time.Time14 startedAt time.Time
15- msgIdx int15+ msgIdx int
16- lastSwap time.Time16+ lastSwap time.Time
17 17
18 // fixedMsg overrides the rotating funnyWorkingLines message when18 // fixedMsg overrides the rotating funnyWorkingLines message when
19 // set. Used for auto-compaction so the spinner clearly says what's19 // set. Used for auto-compaction so the spinner clearly says what's
M internal/provider/openai.go +1 -1
@@ -308,7 +308,7 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R
308308 }
309309 var (
310310 blocks []*blockEntry
311- currentText *blockEntry // most-recent text block, nil if none
311+ currentText *blockEntry // most-recent text block, nil if none
312312 toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block
313313 usage Usage
314314 stop StopReason = StopEnd
@@ -308,7 +308,7 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R
308 }308 }
309 var (309 var (
310 blocks []*blockEntry310 blocks []*blockEntry
311- currentText *blockEntry // most-recent text block, nil if none311+ currentText *blockEntry // most-recent text block, nil if none
312 toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block312 toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block
313 usage Usage313 usage Usage
314 stop StopReason = StopEnd314 stop StopReason = StopEnd
M internal/provider/openai_codex.go +14 -14
@@ -114,15 +114,15 @@ type codexTool struct {
114114 }
115115
116116 type codexRequest struct {
117- Model string `json:"model"`
118- Store bool `json:"store"`
119- Stream bool `json:"stream"`
120- Instructions string `json:"instructions,omitempty"`
121- Input []any `json:"input"`
122- Tools []codexTool `json:"tools,omitempty"`
123- ToolChoice string `json:"tool_choice,omitempty"`
124- ParallelToolCalls bool `json:"parallel_tool_calls"`
125- Include []string `json:"include,omitempty"`
117+ Model string `json:"model"`
118+ Store bool `json:"store"`
119+ Stream bool `json:"stream"`
120+ Instructions string `json:"instructions,omitempty"`
121+ Input []any `json:"input"`
122+ Tools []codexTool `json:"tools,omitempty"`
123+ ToolChoice string `json:"tool_choice,omitempty"`
124+ ParallelToolCalls bool `json:"parallel_tool_calls"`
125+ Include []string `json:"include,omitempty"`
126126 }
127127
128128 // ---- Request building ----
@@ -305,11 +305,11 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re
305305 announced bool
306306 }
307307 var (
308- items = map[int]*itemState{}
309- order []int
310- usage Usage
311- stop StopReason = StopEnd
312- finalErr error
308+ items = map[int]*itemState{}
309+ order []int
310+ usage Usage
311+ stop StopReason = StopEnd
312+ finalErr error
313313 )
314314
315315 assemble := func() Message {
@@ -114,15 +114,15 @@ type codexTool struct {
114 }114 }
115 115
116 type codexRequest struct {116 type codexRequest struct {
117- Model string `json:"model"`117+ Model string `json:"model"`
118- Store bool `json:"store"`118+ Store bool `json:"store"`
119- Stream bool `json:"stream"`119+ Stream bool `json:"stream"`
120- Instructions string `json:"instructions,omitempty"`120+ Instructions string `json:"instructions,omitempty"`
121- Input []any `json:"input"`121+ Input []any `json:"input"`
122- Tools []codexTool `json:"tools,omitempty"`122+ Tools []codexTool `json:"tools,omitempty"`
123- ToolChoice string `json:"tool_choice,omitempty"`123+ ToolChoice string `json:"tool_choice,omitempty"`
124- ParallelToolCalls bool `json:"parallel_tool_calls"`124+ ParallelToolCalls bool `json:"parallel_tool_calls"`
125- Include []string `json:"include,omitempty"`125+ Include []string `json:"include,omitempty"`
126 }126 }
127 127
128 // ---- Request building ----128 // ---- Request building ----
@@ -305,11 +305,11 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re
305 announced bool305 announced bool
306 }306 }
307 var (307 var (
308- items = map[int]*itemState{}308+ items = map[int]*itemState{}
309- order []int309+ order []int
310- usage Usage310+ usage Usage
311- stop StopReason = StopEnd311+ stop StopReason = StopEnd
312- finalErr error312+ finalErr error
313 )313 )
314 314
315 assemble := func() Message {315 assemble := func() Message {
M internal/tui/highlight.go +3 -3
@@ -58,9 +58,9 @@ func HighlightCode(src, lang string) []string {
5858 // order). That's good enough since tool_result text doesn't change once
5959 // emitted, so cache hits are frequent and evictions rare.
6060 type highlightResultCache struct {
61- mu sync.Mutex
62- max int
63- data map[string][]string
61+ mu sync.Mutex
62+ max int
63+ data map[string][]string
6464 order []string
6565 }
6666
@@ -58,9 +58,9 @@ func HighlightCode(src, lang string) []string {
58 // order). That's good enough since tool_result text doesn't change once58 // order). That's good enough since tool_result text doesn't change once
59 // emitted, so cache hits are frequent and evictions rare.59 // emitted, so cache hits are frequent and evictions rare.
60 type highlightResultCache struct {60 type highlightResultCache struct {
61- mu sync.Mutex61+ mu sync.Mutex
62- max int62+ max int
63- data map[string][]string63+ data map[string][]string
64 order []string64 order []string
65 }65 }
66 66
M internal/tui/image.go +5 -4
@@ -28,10 +28,11 @@ const (
2828 // The default is to auto-detect: if the terminal advertises iTerm2 or
2929 // Kitty-graphics support, we use it. The ZOT_INLINE_IMAGES env var
3030 // overrides the default:
31-// ZOT_INLINE_IMAGES=off -> force text fallback
32-// ZOT_INLINE_IMAGES=iterm -> force iTerm2 protocol
33-// ZOT_INLINE_IMAGES=kitty -> force Kitty protocol
34-// ZOT_INLINE_IMAGES=auto -> explicit auto-detect (same as default)
31+//
32+// ZOT_INLINE_IMAGES=off -> force text fallback
33+// ZOT_INLINE_IMAGES=iterm -> force iTerm2 protocol
34+// ZOT_INLINE_IMAGES=kitty -> force Kitty protocol
35+// ZOT_INLINE_IMAGES=auto -> explicit auto-detect (same as default)
3536 func DetectImageProtocol() ImageProtocol {
3637 switch strings.ToLower(os.Getenv("ZOT_INLINE_IMAGES")) {
3738 case "off", "none", "false", "0":
@@ -28,10 +28,11 @@ const (
28 // The default is to auto-detect: if the terminal advertises iTerm2 or28 // The default is to auto-detect: if the terminal advertises iTerm2 or
29 // Kitty-graphics support, we use it. The ZOT_INLINE_IMAGES env var29 // Kitty-graphics support, we use it. The ZOT_INLINE_IMAGES env var
30 // overrides the default:30 // overrides the default:
31-// ZOT_INLINE_IMAGES=off -> force text fallback31+//
32-// ZOT_INLINE_IMAGES=iterm -> force iTerm2 protocol32+// ZOT_INLINE_IMAGES=off -> force text fallback
33-// ZOT_INLINE_IMAGES=kitty -> force Kitty protocol33+// ZOT_INLINE_IMAGES=iterm -> force iTerm2 protocol
34-// ZOT_INLINE_IMAGES=auto -> explicit auto-detect (same as default)34+// ZOT_INLINE_IMAGES=kitty -> force Kitty protocol
35+// ZOT_INLINE_IMAGES=auto -> explicit auto-detect (same as default)
35 func DetectImageProtocol() ImageProtocol {36 func DetectImageProtocol() ImageProtocol {
36 switch strings.ToLower(os.Getenv("ZOT_INLINE_IMAGES")) {37 switch strings.ToLower(os.Getenv("ZOT_INLINE_IMAGES")) {
37 case "off", "none", "false", "0":38 case "off", "none", "false", "0":
M internal/tui/resize_unix.go +13 -19
@@ -7,6 +7,8 @@ import (
77 "os/signal"
88 "syscall"
99 "time"
10+
11+ "golang.org/x/sys/unix"
1012 )
1113
1214 func (p *ProcTerm) installResizeHandler() {
@@ -27,22 +29,24 @@ func (p *ProcTerm) SetNonblock(enable bool) error {
2729
2830 // peekStdin polls the stdin fd for up to d; if a byte is ready, reads
2931 // and returns it. Returns (0, false, nil) on timeout.
32+//
33+// Uses golang.org/x/sys/unix so the syscall signatures line up on both
34+// linux (syscall.Select returns (int, error), Timeval.Usec is int64)
35+// and darwin (Select returns error only, Timeval.Usec is int32) —
36+// pulling this through the x/sys wrappers makes both builds happy.
3037 func peekStdin(in *os.File, d time.Duration) (byte, bool, error) {
3138 fd := int(in.Fd())
32- var rset syscall.FdSet
33- fdSet(&rset, fd)
34- tv := syscall.Timeval{
35- Sec: int64(d / time.Second),
36- Usec: int32((d % time.Second) / time.Microsecond),
37- }
38- err := syscall.Select(fd+1, &rset, nil, nil, &tv)
39+ var rset unix.FdSet
40+ rset.Set(fd)
41+ tv := unix.NsecToTimeval(int64(d))
42+ _, err := unix.Select(fd+1, &rset, nil, nil, &tv)
3943 if err != nil {
40- if err == syscall.EINTR {
44+ if err == unix.EINTR {
4145 return 0, false, nil
4246 }
4347 return 0, false, err
4448 }
45- if !fdIsSet(&rset, fd) {
49+ if !rset.IsSet(fd) {
4650 return 0, false, nil
4751 }
4852 var b [1]byte
@@ -52,13 +56,3 @@ func peekStdin(in *os.File, d time.Duration) (byte, bool, error) {
5256 }
5357 return b[0], true, nil
5458 }
55-
56-// fdSet sets bit fd in the fd_set. Plain Go implementation so we don't
57-// need platform-specific helpers from golang.org/x/sys.
58-func fdSet(set *syscall.FdSet, fd int) {
59- set.Bits[fd/64] |= 1 << (uint(fd) % 64)
60-}
61-
62-func fdIsSet(set *syscall.FdSet, fd int) bool {
63- return set.Bits[fd/64]&(1<<(uint(fd)%64)) != 0
64-}
@@ -7,6 +7,8 @@ import (
7 "os/signal"7 "os/signal"
8 "syscall"8 "syscall"
9 "time"9 "time"
10+
11+ "golang.org/x/sys/unix"
10 )12 )
11 13
12 func (p *ProcTerm) installResizeHandler() {14 func (p *ProcTerm) installResizeHandler() {
@@ -27,22 +29,24 @@ func (p *ProcTerm) SetNonblock(enable bool) error {
27 29
28 // peekStdin polls the stdin fd for up to d; if a byte is ready, reads30 // peekStdin polls the stdin fd for up to d; if a byte is ready, reads
29 // and returns it. Returns (0, false, nil) on timeout.31 // and returns it. Returns (0, false, nil) on timeout.
32+//
33+// Uses golang.org/x/sys/unix so the syscall signatures line up on both
34+// linux (syscall.Select returns (int, error), Timeval.Usec is int64)
35+// and darwin (Select returns error only, Timeval.Usec is int32) —
36+// pulling this through the x/sys wrappers makes both builds happy.
30 func peekStdin(in *os.File, d time.Duration) (byte, bool, error) {37 func peekStdin(in *os.File, d time.Duration) (byte, bool, error) {
31 fd := int(in.Fd())38 fd := int(in.Fd())
32- var rset syscall.FdSet39+ var rset unix.FdSet
33- fdSet(&rset, fd)40+ rset.Set(fd)
34- tv := syscall.Timeval{41+ tv := unix.NsecToTimeval(int64(d))
35- Sec: int64(d / time.Second),42+ _, err := unix.Select(fd+1, &rset, nil, nil, &tv)
36- Usec: int32((d % time.Second) / time.Microsecond),
37- }
38- err := syscall.Select(fd+1, &rset, nil, nil, &tv)
39 if err != nil {43 if err != nil {
40- if err == syscall.EINTR {44+ if err == unix.EINTR {
41 return 0, false, nil45 return 0, false, nil
42 }46 }
43 return 0, false, err47 return 0, false, err
44 }48 }
45- if !fdIsSet(&rset, fd) {49+ if !rset.IsSet(fd) {
46 return 0, false, nil50 return 0, false, nil
47 }51 }
48 var b [1]byte52 var b [1]byte
@@ -52,13 +56,3 @@ func peekStdin(in *os.File, d time.Duration) (byte, bool, error) {
52 }56 }
53 return b[0], true, nil57 return b[0], true, nil
54 }58 }
55-
56-// fdSet sets bit fd in the fd_set. Plain Go implementation so we don't
57-// need platform-specific helpers from golang.org/x/sys.
58-func fdSet(set *syscall.FdSet, fd int) {
59- set.Bits[fd/64] |= 1 << (uint(fd) % 64)
60-}
61-
62-func fdIsSet(set *syscall.FdSet, fd int) bool {
63- return set.Bits[fd/64]&(1<<(uint(fd)%64)) != 0
64-}
M internal/tui/theme.go +13 -13
@@ -8,18 +8,18 @@ import "strings"
88 // ANSI 256-color palette used by zot. Defined as numeric codes so we
99 // can swap themes without changing any render code.
1010 type Theme struct {
11- FG int
12- Muted int
13- Accent int
14- User int // label color for the user role
15- Assistant int // label color for the zot role
16- Tool int
17- ToolOut int
18- Error int
19- Warning int
20- Spinner int // spinner + funny working line
21- SelectionBG int // background for highlighted rows
22- SelectionFG int // foreground for highlighted rows
11+ FG int
12+ Muted int
13+ Accent int
14+ User int // label color for the user role
15+ Assistant int // label color for the zot role
16+ Tool int
17+ ToolOut int
18+ Error int
19+ Warning int
20+ Spinner int // spinner + funny working line
21+ SelectionBG int // background for highlighted rows
22+ SelectionFG int // foreground for highlighted rows
2323 }
2424
2525 var Dark = Theme{
@@ -47,7 +47,7 @@ var Light = Theme{
4747 ToolOut: 240,
4848 Error: 160,
4949 Warning: 166,
50- Spinner: 91, // purple
50+ Spinner: 91, // purple
5151 SelectionBG: 153, // light blue
5252 SelectionFG: 232, // near-black
5353 }
@@ -8,18 +8,18 @@ import "strings"
8 // ANSI 256-color palette used by zot. Defined as numeric codes so we8 // ANSI 256-color palette used by zot. Defined as numeric codes so we
9 // can swap themes without changing any render code.9 // can swap themes without changing any render code.
10 type Theme struct {10 type Theme struct {
11- FG int11+ FG int
12- Muted int12+ Muted int
13- Accent int13+ Accent int
14- User int // label color for the user role14+ User int // label color for the user role
15- Assistant int // label color for the zot role15+ Assistant int // label color for the zot role
16- Tool int16+ Tool int
17- ToolOut int17+ ToolOut int
18- Error int18+ Error int
19- Warning int19+ Warning int
20- Spinner int // spinner + funny working line20+ Spinner int // spinner + funny working line
21- SelectionBG int // background for highlighted rows21+ SelectionBG int // background for highlighted rows
22- SelectionFG int // foreground for highlighted rows22+ SelectionFG int // foreground for highlighted rows
23 }23 }
24 24
25 var Dark = Theme{25 var Dark = Theme{
@@ -47,7 +47,7 @@ var Light = Theme{
47 ToolOut: 240,47 ToolOut: 240,
48 Error: 160,48 Error: 160,
49 Warning: 166,49 Warning: 166,
50- Spinner: 91, // purple50+ Spinner: 91, // purple
51 SelectionBG: 153, // light blue51 SelectionBG: 153, // light blue
52 SelectionFG: 232, // near-black52 SelectionFG: 232, // near-black
53 }53 }
M internal/tui/view.go +4 -3
@@ -43,7 +43,7 @@ type View struct {
4343 // toolPaths maps tool_use_id to the "path" argument of the call, if
4444 // any, so tool_result rendering can pick the right syntax language.
4545 // Rebuilt on each Build().
46- toolPaths map[string]string
46+ toolPaths map[string]string
4747 Streaming string // current assistant text delta
4848 StreamingActive bool
4949 ToolCalls []ToolCallView // tool calls in flight or completed
@@ -582,8 +582,9 @@ func truncateLines(s string, n int) string {
582582 // StatusBar builds the single-line status shown above the editor.
583583 //
584584 // Layout:
585-// [busyPrefix] provider-model tokens-cost ctrl+c hint cwd
586-// ↑ right-aligned
585+//
586+// [busyPrefix] provider-model tokens-cost ctrl+c hint cwd
587+// ↑ right-aligned
587588 //
588589 // cols is the terminal width; when > 0 the cwd is placed flush-right
589590 // with spaces. busyPrefix, if non-empty, is injected at the far left.
@@ -43,7 +43,7 @@ type View struct {
43 // toolPaths maps tool_use_id to the "path" argument of the call, if43 // toolPaths maps tool_use_id to the "path" argument of the call, if
44 // any, so tool_result rendering can pick the right syntax language.44 // any, so tool_result rendering can pick the right syntax language.
45 // Rebuilt on each Build().45 // Rebuilt on each Build().
46- toolPaths map[string]string46+ toolPaths map[string]string
47 Streaming string // current assistant text delta47 Streaming string // current assistant text delta
48 StreamingActive bool48 StreamingActive bool
49 ToolCalls []ToolCallView // tool calls in flight or completed49 ToolCalls []ToolCallView // tool calls in flight or completed
@@ -582,8 +582,9 @@ func truncateLines(s string, n int) string {
582 // StatusBar builds the single-line status shown above the editor.582 // StatusBar builds the single-line status shown above the editor.
583 //583 //
584 // Layout:584 // Layout:
585-// [busyPrefix] provider-model tokens-cost ctrl+c hint cwd585+//
586-// ↑ right-aligned586+// [busyPrefix] provider-model tokens-cost ctrl+c hint cwd
587+// ↑ right-aligned
587 //588 //
588 // cols is the terminal width; when > 0 the cwd is placed flush-right589 // cols is the terminal width; when > 0 the cwd is placed flush-right
589 // with spaces. busyPrefix, if non-empty, is injected at the far left.590 // with spaces. busyPrefix, if non-empty, is injected at the far left.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close