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 6eb5da72..31a9e928 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: cf62711c75c66568 -raw_hash: 38568af4c890e786 -vouched: 2026-10-22T20:27:44Z +body_hash: b0a39d06b7ed2900 +raw_hash: 2b2e4ccf7e3ee6f2 +vouched: 2026-10-22T20:29:47Z lines: 235-824 --- diff --git a/.lane/memory/apps/app/src/app.tsx/01M5JMVG9G87F31FGVV571Y6BJ-the-project-list-is-built-on.md b/.lane/memory/apps/app/src/app.tsx/01M5JMVG9G87F31FGVV571Y6BJ-the-project-list-is-built-on.md new file mode 100644 index 00000000..43d32bee --- /dev/null +++ b/.lane/memory/apps/app/src/app.tsx/01M5JMVG9G87F31FGVV571Y6BJ-the-project-list-is-built-on.md @@ -0,0 +1,13 @@ +--- +id: 01M5JMVG9G87F31FGVV571Y6BJ +anchor: function App +created: 2026-10-22T20:29:16Z +norm: '1' +sig: 063d0f00fea02511 +body_hash: b0a39d06b7ed2900 +raw_hash: 2b2e4ccf7e3ee6f2 +vouched: 2026-10-22T20:29:47Z +lines: 330-1237 +--- + +the project list is built only once hello names this host and its projects frame arrives; earlier, its own channels read as a teammate's diff --git a/.lane/memory/apps/host/src/gateway-client.ts/01M4YQRMKVKT45GACS27J2TXWY-channel-listings-must-outliv.md b/.lane/memory/apps/host/src/gateway-client.ts/01M4YQRMKVKT45GACS27J2TXWY-channel-listings-must-outliv.md index fde304a4..00b23d32 100644 --- a/.lane/memory/apps/host/src/gateway-client.ts/01M4YQRMKVKT45GACS27J2TXWY-channel-listings-must-outliv.md +++ b/.lane/memory/apps/host/src/gateway-client.ts/01M4YQRMKVKT45GACS27J2TXWY-channel-listings-must-outliv.md @@ -4,9 +4,9 @@ anchor: class GatewayClient created: 2026-10-15T02:55:20Z norm: '1' sig: 41366b9e88f0e890 -body_hash: 37f27c37825b3457 -raw_hash: c990e819f8c5c8e5 -vouched: 2026-10-22T14:33:27Z +body_hash: f327ecd85b1a1ced +raw_hash: eb40263f67d2ffcb +vouched: 2026-10-22T20:29:22Z lines: 33-240 --- diff --git a/apps/app/src/app.tsx b/apps/app/src/app.tsx index 7ec76f20..4a329190 100644 --- a/apps/app/src/app.tsx +++ b/apps/app/src/app.tsx @@ -444,11 +444,12 @@ export function App() { host.request({ op: "hello" }).then(setHello, () => {}); }, [status]); - const available = useMemo(() => projects(opened, channels, hello.host), [ - opened, - channels, - hello.host, - ]); + // Until this host names itself and lists its projects, its own channels read as a teammate's. + const loaded = !!opened && !!hello.host; + const available = useMemo( + () => opened && hello.host ? projects(opened, channels, hello.host) : [], + [opened, channels, hello.host], + ); useGithubRefresh(available); const listings = new Map(channels.map((channel) => [channel.id, channel])); const byId = useMemo(() => { @@ -464,9 +465,10 @@ export function App() { 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; + // Only a definite answer hides GitHub, so the lists can still say why a lookup failed; Issues and + // PRs appear once the lookup answers either way, so they never show and then vanish. + const linked = !!repo.value || (status === "open" && !!repo.error); + const page = repo.value === null && (saved === "issues" || saved === "prs") ? "channels" : saved; function go(next: Page) { if (linked || (next !== "issues" && next !== "prs")) setPage(next); } @@ -613,7 +615,7 @@ export function App() { // Declared, not a const: the picker and All Projects groups above call it during render. function removable(value: AppProject) { - return connected && opened.some((checkout) => isLocal(checkout, value)); + return connected && !!opened?.some((checkout) => isLocal(checkout, value)); } function remove(value: AppProject) { @@ -623,7 +625,7 @@ export function App() { async function forget(value: AppProject) { try { - for (const checkout of opened.filter((checkout) => isLocal(checkout, value))) { + for (const checkout of opened?.filter((checkout) => isLocal(checkout, value)) || []) { await host.request({ op: "project-remove", path: checkout.path }); } } catch (error) { @@ -872,7 +874,7 @@ export function App() { page={page} user={hello.user} github={people[hello.user]} - repo={linked} + repo={linked || page === "issues" || page === "prs"} onPage={go} onSettings={() => setSettings(true)} /> @@ -999,7 +1001,7 @@ export function App() {

)} - {!current && !connected + {!current && (!connected || !loaded) ? null : !current ? ( diff --git a/apps/host/src/gateway-client.ts b/apps/host/src/gateway-client.ts index f886eb0a..00bfc76f 100644 --- a/apps/host/src/gateway-client.ts +++ b/apps/host/src/gateway-client.ts @@ -60,7 +60,8 @@ export class GatewayClient { */ stalled = false; channels: Listing[] = []; - projects: Project[] = []; + /** Undefined until the host first sends them, so an app can tell loading from having none. */ + projects?: Project[]; people: People = {}; /** Asks to join the team waiting for this host's owner; only the local app receives them. */ asks: TeamAsk[] = [];