GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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
Contributor
The chat page only renders once the route has a chat ID ( This narrows those parameters to Tests that passed
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 methodologyPlan: finding misplaced nullability in
|
| 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:
if (!chatID) repeated in many helpers).!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.
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?"
tsc confirm.site/src).undefined). Resolved by the decision above: count only production inflows.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.
chat once in AgentChatPage, and pass a non-null Chat to the whole subtree").useState, useMemo, context, hook return objects, spreads ({...props}), and callbacks, and precision drops at each of these.Build one graph and run both queries on it.
useState).?., ??), and whether it's an origin.deadNullability means a slot with zero nullable inflows from production code.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.| 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 |
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.
site/src in about 3.5s; about 13s total.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:
T | undefined whose callers immediately use ??).{...spread} props, useState, context, and generics aren't modeled.options?: {...}) are idiomatic and should be allowlisted./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:
dead means zero nullable production inflows. Also list the test files that pass nullable values, since those need updating.T | undefined where every production caller immediately uses ??, ?., or a guard, or where the function never actually returns absent.falsy section: !x or x && on number | undefined or string | undefined slots.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).dead, chains, rewiden, falsy), each item with file:line, inflow counts, and the upstream origin. --focus <slot> prints a Mermaid subgraph for one chain.dead before using it to drive fixes. Tune the exclusions as needed.dead slot:
undefined or null so they match production usage.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.chatID cluster (useChatStore, rightPanelTabStorage, sidebarTabStorage, draftStorage, useChatDraftAttachments, useGitWatcher, useDesktopConnection).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.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.Tool: /home/coder/nullflow/nullflow.mts, report at /home/coder/nullflow/out/report.md (about 25s per run).
Fixes made after validation:
component: Foo) no longer mark props as "unknown". This had hidden 296 slots.?., ??/||, ternaries, destructuring, and dot access on Record types.a && x && ... chains, and object-literal implementations of interface methods.ReactNode.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):
ParsedMessageEntry.mergedFrom, omitted in an untyped literal) is caught by tsc when narrowing.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.
main)useChatStore, chatStore, rightPanelTabStorage, sidebarTabStorage, useChatDraftAttachments, chatDraftAttachmentStorage, useConversationEditingState, useChatToolInvalidations, useWorkspaceWatch, useGitWatcher, chatQueueReconciliation. Reference PR, done by me.ConversationTimeline, MessageBlocks, UserMessageContent, AttachmentBlocks, AssistantOutput, messageHelpers, ChatStatusCallout, ChatElements/tools/*.AgentChatInput, ChatMessageInput, SkillsTriggerMenu, AttachmentPreview, ChatPageContent, AgentChatPageView.ChatsSidebar/*, ChatSummary, ChatActionsMenuItems (flags only).DiffViewer/*, GitPanel, TerminalPanel, DesktopPanel, RightPanelAddTabControl, DebugPanel/*.UserCompactionThresholdSettings, WebPushButton, ChimeButton, AgentSetupNotice, CompactOrgSelector, UsageIndicator, modelOptions, diffColors, useAgentsPageKeybindings.Rules for every cluster:
tsc is the source of truth for what to update.disabled/error.None.
This PR was generated by Coder Agents on behalf of @DanielleMaywood.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
…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.
f7814ee to
20445d9
Compare
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.