From 2925002c5d1aa195982892042c126ea2332f3156 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 12:18:36 -0400 Subject: [PATCH 1/2] fix(app): hide Issues and Pull requests when a project has no GitHub repository Why: apps/host/src/github.ts#function found | gh pr list --search returns [] for a repository GitHub can't find, so only gh api's 404 can tell an empty list from a missing repository --- ...F4P375REE7-all-projects-and-project-vie.md | 6 +- ...S1RP81MQCE0-a-hosted-channel-s-location.md | 6 +- ...MC0KH3Q6BN-a-hosted-channel-must-be-rea.md | 6 +- ...ZKM0B3AWRVX-a-move-to-a-cloud-workspace.md | 6 +- ...3EYAFM3YC4-worker-requests-worker-never.md | 6 +- ...XJSWZK588G-every-project-op-that-change.md | 6 +- ...Z5RK9MS1BN-gh-pr-list-search-returns-fo.md | 8 +++ apps/app/src/app.tsx | 21 +++++-- apps/app/src/navigation.tsx | 56 ++++++++++--------- apps/app/src/palette.tsx | 28 ++++++---- apps/host/src/gateway.ts | 7 ++- apps/host/src/github.ts | 15 +++++ 12 files changed, 109 insertions(+), 62 deletions(-) create mode 100644 .lane/memory/apps/host/src/github.ts/01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md diff --git a/.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md b/.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md index 348be767..bb1e1719 100644 --- a/.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md +++ b/.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md @@ -4,9 +4,9 @@ anchor: function App created: 2026-10-15T03:38:04Z norm: '1' sig: 063d0f00fea02511 -body_hash: 70b144740681d82c -raw_hash: 2962395e6c4e618a -vouched: 2026-10-22T15:45:21Z +body_hash: 825caa43ddeedb08 +raw_hash: a06c00e058bd3a88 +vouched: 2026-10-22T16:18:52Z lines: 235-824 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md b/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md index 4644a5ac..c715a588 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-15T04:08:49Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 7148e9daddb2ba29 -raw_hash: 1839c37c0e9b7635 -vouched: 2026-10-22T14:33:27Z +body_hash: 3d892bdf2a5a0db7 +raw_hash: f681a895851ad4fb +vouched: 2026-10-22T16:18:52Z lines: 266-519 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md b/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md index 35ae0bad..60b9a23b 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-15T06:00:20Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 7148e9daddb2ba29 -raw_hash: 1839c37c0e9b7635 -vouched: 2026-10-22T14:33:27Z +body_hash: 3d892bdf2a5a0db7 +raw_hash: f681a895851ad4fb +vouched: 2026-10-22T16:18:52Z lines: 281-561 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md b/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md index 3fafa5e9..1e9bbb96 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-19T21:09:57Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 7148e9daddb2ba29 -raw_hash: 1839c37c0e9b7635 -vouched: 2026-10-22T14:33:27Z +body_hash: 3d892bdf2a5a0db7 +raw_hash: f681a895851ad4fb +vouched: 2026-10-22T16:18:52Z lines: 384-700 supersedes: 01M5APK9YCQH467YNMKAC8BT5B --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md index 84d3c060..8527a79b 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-20T17:04:56Z norm: '1' sig: 1acdc0f6a0895ac5 -body_hash: d40ee870a4451138 -raw_hash: 07401839125f7e0e -vouched: 2026-10-22T15:28:03Z +body_hash: 308268d80c20b68b +raw_hash: 417f8cf07e0e37ad +vouched: 2026-10-22T16:18:52Z lines: 1-1153 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md b/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md index 6061e6f1..ba8349f1 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-22T14:00:47Z norm: '1' sig: 1acdc0f6a0895ac5 -body_hash: d40ee870a4451138 -raw_hash: 07401839125f7e0e -vouched: 2026-10-22T15:28:03Z +body_hash: 308268d80c20b68b +raw_hash: 417f8cf07e0e37ad +vouched: 2026-10-22T16:18:52Z lines: 1-1309 --- diff --git a/.lane/memory/apps/host/src/github.ts/01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md b/.lane/memory/apps/host/src/github.ts/01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md new file mode 100644 index 00000000..acf178eb --- /dev/null +++ b/.lane/memory/apps/host/src/github.ts/01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md @@ -0,0 +1,8 @@ +--- +id: 01M5J6GGQMK69MKRZ5RK9MS1BN +anchor: function found +created: 2026-10-22T16:18:36Z +norm: '1' +--- + +gh pr list --search returns [] for a repository GitHub can't find, so only gh api's 404 can tell an empty list from a missing repository diff --git a/apps/app/src/app.tsx b/apps/app/src/app.tsx index 217d0859..346036e1 100644 --- a/apps/app/src/app.tsx +++ b/apps/app/src/app.tsx @@ -41,7 +41,7 @@ import { useCompute } from "./compute"; import { Dashboard } from "./dashboard"; import { DeleteChannel } from "./delete-channel"; import { Github, GithubSidebar } from "./github"; -import { useGithubRefresh } from "./github-cache"; +import { useGithub, useGithubRefresh } from "./github-cache"; import { desktop, here, titlebar } from "./desktop"; import { host } from "./host"; import { type Invite, parse } from "./invite"; @@ -312,7 +312,7 @@ export function App() { const people = useSyncExternalStore(host.subscribe, () => host.people); const presence = useSyncExternalStore(host.subscribe, () => host.presence); const [hello, setHello] = useState({ user: "", host: "" }); - const [page, setPage] = useWindowStorage("ace:page", "dashboard"); + const [saved, setPage] = useWindowStorage("ace:page", "dashboard"); const [project, setProject] = useWindowStorage("ace:project-id", undefined); const [all, setAll] = useWindowStorage("ace:all-projects", false); const [selected, setSelected] = useWindowStorage>( @@ -428,7 +428,6 @@ export function App() { hello.host, ]); useGithubRefresh(available); - const allProjects = all && page === "channels"; const listings = new Map(channels.map((channel) => [channel.id, channel])); const byId = useMemo(() => { const values = new Map(); @@ -440,6 +439,16 @@ export function App() { return values; }, [available]); const current = (project && byId.get(project)) || available[0]; + const repo = useGithub( + current && { op: "project-repo", project: current.path, host: current.host }, + ); + // Only a definite answer hides GitHub, so the lists can still say why a lookup failed. + const linked = repo.value !== null; + const page = !linked && (saved === "issues" || saved === "prs") ? "channels" : saved; + function go(next: Page) { + if (linked || (next !== "issues" && next !== "prs")) setPage(next); + } + const allProjects = all && page === "channels"; const visible = current ? channels.filter((channel) => channelProject(channel) === current.id) : []; @@ -840,7 +849,8 @@ export function App() { page={page} user={hello.user} github={people[hello.user]} - onPage={setPage} + repo={linked} + onPage={go} onSettings={() => setSettings(true)} /> } @@ -850,7 +860,7 @@ export function App() { onOpen={() => void choose()} onClone={cloneUrl} onSettings={() => setSettings(true)} - onPage={setPage} + onPage={go} connected={connected} /> {page !== "dashboard" && current && ( @@ -1086,6 +1096,7 @@ export function App() { projects={available} host={hello.host} project={current?.id} + github={linked} attention={attention} onChannel={select} onCreate={current && creatable && connected diff --git a/apps/app/src/navigation.tsx b/apps/app/src/navigation.tsx index 0b476e57..7811eb1a 100644 --- a/apps/app/src/navigation.tsx +++ b/apps/app/src/navigation.tsx @@ -16,11 +16,13 @@ import { share } from "./window-storage"; export type Page = "dashboard" | "channels" | "issues" | "prs"; -export function Navigation({ page, user, github, onPage, onSettings }: { +export function Navigation({ page, user, github, repo, onPage, onSettings }: { page: Page; user: string; /** Shows the owner's GitHub avatar; initials without one. */ github?: string; + /** Whether the current project has a GitHub repository for Issues and Pull requests. */ + repo: boolean; onPage: (page: Page) => void; onSettings: () => void; }) { @@ -55,30 +57,34 @@ export function Navigation({ page, user, github, onPage, onSettings }: { > Channels - } - active={page === "issues"} - shortcut="⌘3" - onClick={(event) => { - event.preventDefault(); - onPage("issues"); - }} - > - Issues - - } - active={page === "prs"} - shortcut="⌘4" - onClick={(event) => { - event.preventDefault(); - onPage("prs"); - }} - > - PRs - + {repo && ( + <> + } + active={page === "issues"} + shortcut="⌘3" + onClick={(event) => { + event.preventDefault(); + onPage("issues"); + }} + > + Issues + + } + active={page === "prs"} + shortcut="⌘4" + onClick={(event) => { + event.preventDefault(); + onPage("prs"); + }} + > + PRs + + + )} ); diff --git a/apps/app/src/palette.tsx b/apps/app/src/palette.tsx index eedd0bc2..ad52ebbe 100644 --- a/apps/app/src/palette.tsx +++ b/apps/app/src/palette.tsx @@ -87,17 +87,21 @@ function tabItems(window: WindowState): CommandPaletteItem[] { } /** Mod+K searches channels in every project, the open channel's tabs, and window actions. */ -export function Palette({ channels, projects, host, project, attention, onChannel, onCreate }: { - channels: Listing[]; - projects: AppProject[]; - host: string; - /** The current project's id. */ - project?: string; - attention: Map; - onChannel: (channel: Listing) => void; - /** Starts a channel in the current project, when one can start there. */ - onCreate?: { name: string; run: () => void }; -}) { +export function Palette( + { channels, projects, host, project, github, attention, onChannel, onCreate }: { + channels: Listing[]; + projects: AppProject[]; + host: string; + /** The current project's id. */ + project?: string; + /** Whether the current project has a GitHub repository for Issues and Pull requests. */ + github: boolean; + attention: Map; + onChannel: (channel: Listing) => void; + /** Starts a channel in the current project, when one can start there. */ + onCreate?: { name: string; run: () => void }; + }, +) { const [open, setOpen] = useState(false); // Tabs are read once on open: they only change through the window the palette covers. const [tabs, setTabs] = useState([]); @@ -119,7 +123,7 @@ export function Palette({ channels, projects, host, project, attention, onChanne if (!open) return null; const items: CommandPaletteItem[] = [ ...(onCreate ? [action(CREATE, `New channel in ${onCreate.name}`, "Channel")] : []), - ...ACTIONS, + ...(github ? ACTIONS : ACTIONS.filter(({ id }) => id !== "issues" && id !== "prs")), ...tabs, ...channelItems(channels, projects, host, project, attention), ]; diff --git a/apps/host/src/gateway.ts b/apps/host/src/gateway.ts index 02c02ffb..94876138 100644 --- a/apps/host/src/gateway.ts +++ b/apps/host/src/gateway.ts @@ -642,7 +642,7 @@ async function handle( } case "project-repo": { if (request.host && request.host !== name && !client.peer) { - return (await remote(client, request.host)).request( + const repo = await (await remote(client, request.host)).request( { op: "project-repo", project: request.project, @@ -650,6 +650,7 @@ async function handle( undefined, trace, ); + return repo && await github.found(repo) ? repo : null; } const known = client.peer ? (await Promise.all(catalog.list().map((channel) => projects.checkout(channel.project)))) @@ -661,7 +662,9 @@ async function handle( if (client.peer && client.user !== catalog.user && !await access.canAccess(client.user, id)) { throw new Error("You don't have access to this project"); } - return repo; + // A peer's host answers only what the remote names; GitHub reads stay on the asking host. + if (client.peer) return repo; + return repo && await github.found(repo) ? repo : null; } case "github-list": return github.list(request); diff --git a/apps/host/src/github.ts b/apps/host/src/github.ts index 1f1cb7da..00690760 100644 --- a/apps/host/src/github.ts +++ b/apps/host/src/github.ts @@ -105,6 +105,21 @@ export async function readable(repo: string, login?: string): Promise { return permission !== "none"; } +/** + * Whether GitHub finds `repo` for this host's gh account. It answers not found for private + * repositories the account can't read, and `gh pr list --search` returns nothing for them instead + * of failing. Other failures throw, so the lists can show them. + */ +export async function found(repo: string): Promise { + return gh(["api", `repos/${repository(repo)}`, "--jq", ".id"]).then( + () => true, + (error: Error) => { + if (MISSING.test(error.message)) return false; + throw error; + }, + ); +} + /** A GitHub repository URL, SSH remote, or owner/name, as a valid owner/name. */ export function parse(value: string): string { const repo = githubRepo(value); From afe0635ea8b70c24bb0dcca05643b3f2b1929125 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 12:19:07 -0400 Subject: [PATCH 2/2] lane: sync fix-gh-repo memory --- ...01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.lane/memory/apps/host/src/github.ts/01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md b/.lane/memory/apps/host/src/github.ts/01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md index acf178eb..e0062b0d 100644 --- a/.lane/memory/apps/host/src/github.ts/01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md +++ b/.lane/memory/apps/host/src/github.ts/01M5J6GGQMK69MKRZ5RK9MS1BN-gh-pr-list-search-returns-fo.md @@ -3,6 +3,10 @@ id: 01M5J6GGQMK69MKRZ5RK9MS1BN anchor: function found created: 2026-10-22T16:18:36Z norm: '1' +sig: 82ae12c684fe72fe +body_hash: 4e01c124fe60d876 +raw_hash: 8456424d5a04b515 +lines: 113-121 --- gh pr list --search returns [] for a repository GitHub can't find, so only gh api's 404 can tell an empty list from a missing repository