GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — refactor(site/src/pages/AgentsPage): require chat IDs in chat store helpers by DanielleMaywood · Pull Request #29888 · coder/coder

Conversation

@DanielleMaywood

@DanielleMaywood DanielleMaywood commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

The chat page only renders once the route has a chat ID (KeyedAgentChatPage shows the not-found view otherwise), and every production caller passes chat.id or that route param. Chat-scoped hooks and storage helpers still accepted string | undefined and re-checked it, so those !chatID guards were dead code.

This narrows those parameters to string and deletes the guards. The same applies to chat store methods that only ever receive arrays or states (replaceMessages, setQueuedMessages, applyPromoteRefetchQueuedMessages, applyServerChatStatus, setRetryState, setReconnectState). useChatStore's chatRecordUpdatedAt becomes required, because omitting it silently disabled the status resync.

Tests that passed undefined or "" for values production never sends are removed. The useChatStore tests now pass chatRecordUpdatedAt.

tsc reported no production errors after narrowing, which confirms that no production caller passes a nullable value. aiGatewayDisabled stays optional: it defaults to false, and requiring it would churn 75 test call sites for no safety gain.

This is the first of several AgentsPage PRs. The findings came from a TypeScript compiler API script (kept out of the repo) that builds a graph of values flowing into nullable params and props, and flags slots that never receive null or undefined from production code. This PR removes 35 of the 112 dead data slots it found.

Plan and methodology

Plan: finding misplaced nullability in site/src/pages/AgentsPage

Status: brainstorm plus feasibility spike. No repo changes made.

Decisions

  • The analysis tool lives outside the repo, in the workspace (/home/coder/nullflow/). It is never committed.
  • Scope is site/src/pages/AgentsPage only. Slots are only reported if declared there. Callers anywhere in site/src still count as inflows, so a slot used from outside AgentsPage isn't narrowed by mistake.
  • Test and story call sites don't justify nullability. If only tests pass undefined or null, the slot is dead: narrow it and update the tests.
  • This is a one-off cleanup: no CI ratchet, no new lint rule, no guideline doc.

1. Problem framing

A null | undefined value goes through three roles:

Role What it is Examples in AgentsPage
Origin Where absence is genuinely introduced. useQuery().data, useParams(), optional API fields (chat.last_model_config_id), Map.get, .find(), str.match()
Carrier A param, prop, hook option, or return type that only forwards the possibility. 24 signatures take chatID: string | undefined; resolveCompactionThreshold(modelID?, thresholds?, models?: T[] | null | undefined)
Sink Where absence is finally handled: if (!x) return, ?., ??, conditional render. rightPanelTabStorage.ts guards if (!chatID) return [] in 7 functions

A check at the origin is healthy. The smells are about carriers:

  1. Dead nullability. A slot is declared nullable, but nothing nullable ever flows into it. Its guard is dead code.
  2. Long carry distance. A value is nullable at the origin, crosses N carriers, and is only narrowed deep inside. It should be narrowed at or near the origin, for example by gating render on data being loaded and then passing the non-null value down.
  3. Re-widening. A value that is already narrowed (non-null) goes into a nullable slot and gets checked again. This is dead nullability seen from the caller's side.
  4. Guard fan-out. One origin produces many guards downstream (the same if (!chatID) repeated in many helpers).
  5. Side finding: falsy checks on nullable values. !x on number | undefined or string | undefined also treats 0 and "" as absent. This is sometimes a real bug.

Grep baseline (non-test, non-story files): 377 nullable-typed annotations, 911 optional ?: members, 118 if (!x) return null/undefined guards, 399 ??, 640 ?.. Grep can't tell origins from carriers. For example, most if (!match) return undefined hits in thinkingTitle.ts are legitimate origins. So a syntactic approach alone will mostly produce noise.

2. Approaches

A. Syntactic heuristics (grep, a Biome rule, an agent that "just looks")

  • Pros: zero setup, fast, easy to explain.
  • Cons: no types and no cross-function view. It can't tell origin from carrier, and it can't see whether callers ever pass null. It has a high false-positive rate and depends on the agent's judgment, which is the thing we don't trust.
  • Verdict: fine for a baseline count, not a method.

B. Bottom-up (start at sinks and carriers, walk toward callers)

Start from every nullable slot (param, prop, destructured prop, hook option field) and use the type checker to answer one question: "Does any call site actually pass a nullable value?"

  • Pros:
    • Every finding is local and mechanically fixable: narrow the signature, delete the guard, and let tsc confirm.
    • Low risk, small diffs, easy to review. Agents do this well when given an exact target.
    • It converges iteratively. Narrowing a leaf can make its caller's slot dead too, so rerun until nothing changes and chains collapse upward.
    • Cheap to compute (see spike: about 13s over all of site/src).
  • Cons:
    • When the inflow really is nullable, bottom-up only says "push the guard up one level." It doesn't point at the root cause or the right place to narrow.
    • Can produce churn: the same chain gets touched once per iteration.
    • Sensitive to test call sites (tests often pass literals or undefined). Resolved by the decision above: count only production inflows.

C. Top-down (start at origins, taint forward)

List origins (query data, route params, optional API fields, Map.get/find/match, context values) and follow each one forward to find how many hops it travels before it's narrowed, and how many guards it creates.

  • Pros:
    • Finds root causes and the architectural fix ("narrow chat once in AgentChatPage, and pass a non-null Chat to the whole subtree").
    • Groups findings per origin, so one decision removes many guards. This matches the goal of handling absence earlier.
    • Surfaces guard fan-out and carry distance directly.
  • Cons:
    • Forward dataflow through React is hard. It goes through useState, useMemo, context, hook return objects, spreads ({...props}), and callbacks, and precision drops at each of these.
    • Large fan-out, and many origins are legitimately optional end to end (for example, an optional API field shown as "n/a"). This needs an allowlist of "OK to stay optional" facts, which is domain knowledge.
    • Fixes are bigger (restructuring component boundaries, loading gates) and need human judgment. They're less suited to autonomous agents.

D. Hybrid: a nullability flow graph, queried both ways (recommended)

Build one graph and run both queries on it.

  • Nodes: nullable slots (params, prop signatures, destructured props, hook option and return fields; later also context values and useState).
  • Node facts: declared nullability, count of nullable vs non-null inflows (production vs test counted separately), whether the body narrows it (guard, ?., ??), and whether it's an origin.
  • Edges: value flow at call arguments, object-literal arguments (hook options), and JSX attributes. Later also return values to caller variables.
  • Queries:
    • Bottom-up: deadNullability means a slot with zero nullable inflows from production code.
    • Top-down: chains means for each origin, the longest path to its first narrowing, plus the number of guards along the way.
    • rewiden means a non-null argument flowing into a nullable slot, reported at the caller.
  • Pros: reuses one analysis. Bottom-up gives safe mechanical wins now, and top-down gives a ranked list of architectural refactors. The graph can also be rendered (DOT or Mermaid) for a chosen origin, which helps reviewers.
  • Cons: the most engineering effort, and the tool needs maintenance. Precision depends on how many React flow patterns get modeled.
Criterion A: grep B: bottom-up C: top-down D: graph (B+C)
Tells origin from carrier no partly yes yes
Actionable, small fixes weak strong weak strong
Finds root cause no no yes yes
False positives high low medium low/medium
Implementation cost ~0 low high medium
Agent-friendly (exact targets) no yes partly yes

3. Feasibility spike (done)

Script: /tmp/nullspike/spike.ts (about 150 lines, TypeScript compiler API, uses the existing [email protected] dependency). It covers call args, object-literal args, JSX attributes, and destructured props, and tracks pass-through edges between slots.

  • Builds the program for all of site/src in about 3.5s; about 13s total.
  • 308 nullable slots in AgentsPage have at least one observed inflow.
  • 81 never receive a nullable value (dead nullability candidates).
  • 91 receive nullable only by pass-through from another nullable slot (carrier chains).
  • Deepest chains are 3 to 4 hops, for example modelOptions.ts ref <- storedRef <- storedRef <- modelDisplayName.lastModelConfigID, and chatQueueReconciliation.ts promotedHeadID/queuedTail.

Manually verified: useChatStore({ chatID: string | undefined }) has one production caller (AgentChatPage.tsx:371), which passes agentId, typed string (AgentChatPage.tsx:103). The other 65 inflows are tests. This is a real finding, but it shows the tool must separate production inflows from test inflows.

Known gaps in the spike:

  • Tests and stories are mixed into the counts.
  • Return values aren't tracked (functions returning T | undefined whose callers immediately use ??).
  • Callbacks passed by reference, {...spread} props, useState, context, and generics aren't modeled.
  • Optional "options bag" params (options?: {...}) are idiomatic and should be allowlisted.
  • Falsy-check detection isn't implemented yet.

4. Methodology

  1. Harden the tool (approach D) in /home/coder/nullflow/ (outside the repo). It loads typescript from site/node_modules via createRequire, so nothing is copied into site/. Changes from the spike:
    • Count production inflows and test/story inflows separately. dead means zero nullable production inflows. Also list the test files that pass nullable values, since those need updating.
    • Track return values: a function returning T | undefined where every production caller immediately uses ??, ?., or a guard, or where the function never actually returns absent.
    • Track guards in the slot's body, so each finding names the dead guard to delete.
    • Add a falsy section: !x or x && on number | undefined or string | undefined slots.
    • Exclusion list: optional options?: bags, callbacks passed by reference that the tool can't follow, and slots that receive {...spread} props (report these as "unknown", never as dead).
    • Output: JSON graph plus a Markdown report (dead, chains, rewiden, falsy), each item with file:line, inflow counts, and the upstream origin. --focus <slot> prints a Mermaid subgraph for one chain.
  2. Validate precision. Hand-check about 30 findings across sections. Target at least 80% true positives for dead before using it to drive fixes. Tune the exclusions as needed.
  3. Bottom-up fix loop. For each dead slot:
    • Narrow the type and delete the now-dead guard.
    • Update tests that pass undefined or null so they match production usage.
    • Run pnpm lint:types (tsc), pnpm check (Biome), and the affected Vitest tests (pnpm test <path>), then rerun the tool. Callers that just became dead show up in the next pass.
    • Repeat until nothing changes. Start with the chatID cluster (useChatStore, rightPanelTabStorage, sidebarTabStorage, draftStorage, useChatDraftAttachments, useGitWatcher, useDesktopConnection).
  4. Top-down pass. Once the fixpoint is reached, the remaining chains are real nullable origins carried too far. Group them by origin, rank by number of guards downstream, and decide per origin where to narrow: a loading gate, a parent component, or a converter at the API boundary. These need judgment, so I'll report each proposed narrowing point before changing it.
  5. Delivery. If the total cleanup is small (roughly up to 30 files or about 600 changed lines), ship one PR. Otherwise, ship one PR per cluster or origin. Every PR touches only AgentsPage and its tests, verified with pnpm lint:types, pnpm check, pnpm lint:oxlint, pnpm lint:compiler, and the relevant Vitest files. The final tool report (before and after counts) goes in each PR description. The tool itself isn't committed.

5. Results so far

Tool: /home/coder/nullflow/nullflow.mts, report at /home/coder/nullflow/out/report.md (about 25s per run).

Fixes made after validation:

  • Story/test references (component: Foo) no longer mark props as "unknown". This had hidden 296 slots.
  • Unchecked index detection now follows local consts, ?., ??/||, ternaries, destructuring, and dot access on Record types.
  • Guards now include destructuring/param defaults, right operands in a && x && ... chains, and object-literal implementations of interface methods.
  • Returns check reuses the unchecked-index logic and ignores ReactNode.
  • Inflows that are non-null only via an as cast are flagged (e.g. useParams() as { agentId: string }, which is safe because KeyedAgentChatPage renders not-found when agentId is empty).

Precision (hand-verified by two read-only agents on the pre-fix report):

  • data: 63 TP, 9 FP, 3 skip (87.5%). 8 of 9 FPs were unchecked-index cases now caught. The remaining one (ParsedMessageEntry.mergedFrom, omitted in an untyped literal) is caught by tsc when narrowing.
  • callbacks: 20/20 TP. Flags: 28 TP, 3 skip (idiomatic leaf props).
  • Returns: 0 real findings (section now empty).
  • Falsy-risk: 15/15 sampled were intentional (empty string means absent). Not acted on.

Current dead slots, excluding idiomatic (options bags, signal, param defaults) and the skip list (chatFamilyAllowsArchive/chatStatusAllowsArchive documented fail-open, fileContentOptions, aiGatewayDisabled, CompactIntegerField.error/disabled, mergedFrom): 254 slots in 69 files (about 110 data, 73 callback, 70 flag). Well over the one-PR threshold, so one PR per cluster.

6. Clusters (one PR each, separate branches from main)

  1. Chat ID and chat store: useChatStore, chatStore, rightPanelTabStorage, sidebarTabStorage, useChatDraftAttachments, chatDraftAttachmentStorage, useConversationEditingState, useChatToolInvalidations, useWorkspaceWatch, useGitWatcher, chatQueueReconciliation. Reference PR, done by me.
  2. Conversation timeline and tools: ConversationTimeline, MessageBlocks, UserMessageContent, AttachmentBlocks, AssistantOutput, messageHelpers, ChatStatusCallout, ChatElements/tools/*.
  3. Chat input and page shell: AgentChatInput, ChatMessageInput, SkillsTriggerMenu, AttachmentPreview, ChatPageContent, AgentChatPageView.
  4. Sidebar: ChatsSidebar/*, ChatSummary, ChatActionsMenuItems (flags only).
  5. Right panel: DiffViewer/*, GitPanel, TerminalPanel, DesktopPanel, RightPanelAddTabControl, DebugPanel/*.
  6. Settings and misc: settings page views, UserCompactionThresholdSettings, WebPushButton, ChimeButton, AgentSetupNotice, CompactOrgSelector, UsageIndicator, modelOptions, diffColors, useAgentsPageKeybindings.

Rules for every cluster:

  • Narrow the type, delete the guards/defaults the report lists, update tests and stories that pass or omit the value. tsc is the source of truth for what to update.
  • Flags: narrow on feature components; skip generic leaf-control props such as disabled/error.
  • A prop whose every production caller passes the same constant can be deleted instead of narrowed.
  • No behavior change. Anything that turns out to be reachable with null/undefined stays as is and is reported.
  • Top-down pass (step 4) happens after these land, with proposals first.

7. Open questions

None.


This PR was generated by Coder Agents on behalf of @DanielleMaywood.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

chatgpt-codex-connector[bot] reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@github-actions github-actions Bot assigned DanielleMaywood Sep 25, 2026
@DanielleMaywood
DanielleMaywood marked this pull request as ready for review September 25, 2026 10:23
@DanielleMaywood
DanielleMaywood requested a review from a team as a code owner September 25, 2026 10:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T10:25:24.486276Z f7814ee Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

This was referenced Sep 25, 2026
refactor(site/src/pages/AgentsPage): require always-passed sidebar props #29889
Closed
refactor(site/src/pages/AgentsPage): require always-passed chat input props #29890
Merged
refactor(site/src/pages/AgentsPage): require always-passed timeline and tool props #29891
Merged
refactor(site/src/pages/AgentsPage): require always-passed settings and misc props #29892
Merged
refactor(site/src/pages/AgentsPage): require always-passed right panel props #29893
Merged
refactor(site/src/pages/AgentsPage): require org, MCP servers and URL transform in timeline #29894
Merged
refactor(site/src/pages/AgentsPage): split ChatMessageItem into live and persisted rows #29900
Closed
refactor(site/src/pages/AgentsPage): make UploadState a discriminated union #29901
Closed
refactor(site/src/pages/AgentsPage): split ChatTopBar into chatless frame and loaded bar #29902
Closed
refactor(site/src/pages/AgentsPage): return normalized line stats from getChatDisplayConfig #29903
Closed
refactor(site/src/pages/AgentsPage): carry diff status in git panel PR tab #29904
Closed
refactor(site/src/pages/AgentsPage): require non-null context usage in the indicator #29905
Merged
refactor(site/src/pages/AgentsPage): drop dead chat page null checks #29907
Merged
refactor(site/src/pages/AgentsPage): remove unused tool call props #29909
Merged
@DanielleMaywood
refactor(site/src/pages/AgentsPage): require chat IDs in chat store h…
20445d9
…elpers

The chat page only renders once the route has a chat ID, and every
production caller passes a non-empty `chat.id`. Chat-scoped hooks and
storage helpers still accepted `string | undefined` and re-checked it,
so their `!chatID` guards were dead.

Narrow `chatID`/`chatId`/`agentId`/`organizationId` to `string` in
`useChatStore`, `useConversationEditingState`, `useChatToolInvalidations`,
`useWorkspaceWatch`, `useGitWatcher`, `useChatDraftAttachments`,
`runPromoteQueuedMessage`, and the right-panel, sidebar and draft
attachment storage helpers, then delete the guards. Chat store methods
that only ever receive arrays or states (`replaceMessages`,
`setQueuedMessages`, `applyPromoteRefetchQueuedMessages`,
`applyServerChatStatus`, `setRetryState`, `setReconnectState`) drop
`undefined`/`null` from their parameters. `chatRecordUpdatedAt` becomes
required so a caller cannot silently disable the status resync.

Tests that passed `undefined` or `""` for values production never
sends are removed or updated.
@DanielleMaywood
DanielleMaywood force-pushed the nullability/chat-id-store branch from f7814ee to 20445d9 Compare September 29, 2026 14:28
@DanielleMaywood
DanielleMaywood requested a review from ibetitsmike September 29, 2026 14:45
@linear-code

linear-code Bot commented Sep 30, 2026

Copy link
Copy Markdown

CODAGT-1258

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

jakehwll
jakehwll approved these changes Oct 6, 2026
View reviewed changes
@DanielleMaywood
DanielleMaywood merged commit deff49f into main Oct 6, 2026
27 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@DanielleMaywood
DanielleMaywood deleted the nullability/chat-id-store branch October 6, 2026 07:36
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

@jakehwll jakehwll jakehwll approved these changes
@ibetitsmike ibetitsmike Awaiting requested review from ibetitsmike

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@DanielleMaywood DanielleMaywood

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@DanielleMaywood @jakehwll
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.