The Influxx native chat panel used to leave review context out of sight — you'd finish a turn with an agent and go hunting through a separate sidebar to find out what actually changed. Influxx 1.2 replaces that gap with a single context strip pinned above the composer: repo and branch identity, a churn chip that opens a changed-files list, and a Create PR button — three controls where there used to be a scavenger hunt, and, more importantly, zero new code paths for opening a diff.
One Strip, Three Jobs
The NativeChatWorktreeChrome component sits directly above the chat composer and renders three things in a row: a pill with the worktree's display name and branch, a churn chip showing +added/-removed line counts (or a file count when there's no line diff to show), and a Create PR button whose label and disabled state track the same eligibility signals — uncommitted changes, upstream status, ahead/behind counts — that Source Control's own header action uses. None of this is decorative. Every value comes from the same gitStatusByWorktree and remoteStatusesByWorktree state the rest of the app already polls, read through a useShallow selector so the strip re-renders only when the underlying primitives actually move.
The chip's whole reason for existing is to answer one question without leaving the conversation: what did the agent just touch? Before this, that answer lived in a different panel entirely, which meant tabbing away from the exact context — the chat transcript describing what was supposed to change — that would help you judge whether the diff matches the ask.
Folding Git's Two Rows Into One
Git's status output is staging-area-aware: a file that's partially staged and partially still dirty in the working tree shows up as two separate entries, one per area. That's correct for git, but it's the wrong shape for a "what changed" summary — nobody wants to see CartDrawer.tsx listed twice because half its edits are staged and half aren't. buildNativeChatChangedFiles, in src/shared/native-chat-changed-files.ts, folds those rows into one entry per path, summing added/removed line counts across areas as it goes.
The interesting decision is which area "wins" when a path is dirty in more than one. The module ranks areas with an explicit priority — unstaged beats untracked, which beats staged — and keeps the winning area's status and area tag on the merged entry. The comment in the source is direct about why: a path dirty in both areas opens on its working-tree diff, because that's almost always the edit an agent just made. If you staged half a change and then kept iterating, the freshest work is unstaged, and that's what a click should show you — not a stale staged snapshot from three turns ago.
"Merging staged and unstaged rows sounds like a two-line dedupe until you ask which version wins. We picked unstaged-wins deliberately: the moment you're looking at this list, you almost certainly want to see the change you just made, not the one you already staged and moved past. Untracked comes next because a brand-new file has no staged counterpart to prefer. Staged is the fallback for the case where a file really is only staged and nothing else touched it since."
— Priya Ramaswami, Staff Engineer, Source Control at ETAPX
The Popover Is a List, Not a New Diff Surface
NativeChatChangedFilesPopover renders the folded list: a header with the total file count and aggregate +/- counts, then one row per file showing a status glyph, the file name, its clipped parent directory (rendered right-to-left so the meaningful tail of a deep path survives truncation instead of its uninformative head), and per-file added/removed counts. Clicking a row doesn't render a diff inline in the popover — it calls onSelect, which the worktree chrome wires straight to openChangedFile.
That handler closes the popover and calls useAppStore.getState().openDiff(worktreeId, filePath, file.path, detectLanguage(file.path), file.area === 'staged') — the exact same store action, called with the exact same staged/unstaged resolution logic (area === 'staged'), that Source Control's own file rows call when you click a changed file there. The native-chat popover is a second entry point into a diff, but it is not a second implementation of "open a diff."
Why openDiff() Has Exactly One Implementation
The openDiff action, defined in src/renderer/src/store/slices/editor.ts, does more than open a file: it builds a stable tab identity from the worktree, runtime environment, diff source (staged vs. unstaged), and relative path; it checks whether that exact tab already exists and, if so, reuses it and requests a content reload rather than spawning a duplicate; and it resolves which runtime environment owns the diff so content loads correctly even for remote or SSH-backed worktrees. None of that logic is native-chat-specific or Source-Control-specific — it's generic tab-and-content-resolution logic that both surfaces need identically.
If the native-chat popover had grown its own "open this file's diff" function instead of calling into the shared action, that logic would have had to be re-derived and kept in sync by hand: tab identity, staged/unstaged resolution, runtime environment lookup, and reuse-vs-create semantics all duplicated in a second place, with no compiler or test forcing the two copies to agree. The two entry points would drift the moment either one changed — a fix to how staged diffs resolve their runtime owner in Source Control simply wouldn't apply to the chat popover, and nobody would notice until a user reported that the same file opened differently depending on where they clicked.
Reusing one action instead means there is exactly one place that defines what "open this diff" means in Influxx. A change to tab reuse behavior, or to how staged/unstaged is resolved, or to how runtime-owned files get their environment stamped, propagates to every caller automatically. The native-chat chrome doesn't need to know any of those details — it just needs to know a file's path and whether it's staged, which is exactly what buildNativeChatChangedFiles already computed for it.
"I run three or four worktrees in parallel and I used to lose track of which one I'd actually reviewed. Now I just glance at the chip after the agent finishes a turn, click through the files right there in chat, and if something looks off I go straight to Source Control — same file, same diff, no re-orientation needed because it's literally the same tab. Small thing, but it removed a whole category of 'wait, did I already look at this' moments."
— Dana Okafor, engineering lead at a fintech infrastructure startup, Influxx user
Create PR Follows the Same Pattern
The strip's third control, Create PR, follows the same reuse principle from a different angle. Rather than duplicating Source Control's create-PR flow, clicking it calls requestSourceControlCreatePr(worktreeId) — a one-shot store request that Source Control's own header consumes to run its existing create/create-intent path. The chrome's job is limited to deciding when the button should be enabled and what it should say, using the same eligibility check (getHostedReviewCreationEligibility) and the same existing-review lookup that Source Control polls. The actual PR-creation logic lives in exactly one place.
Line count formatting, branch name display, and eligibility-state resolution are each pulled into their own small helper modules — native-chat-worktree-chrome-stats.ts and native-chat-create-pr-chrome.ts — rather than inlined into the component. That's a smaller version of the same discipline: presentation logic that multiple UI states depend on gets one testable definition instead of being recomputed inline wherever it's needed.
Why This Matters Beyond This One Feature
It's easy to treat "just call the existing function" as an obvious choice not worth writing about. It's obvious in hindsight, but it's also the decision that's easiest to skip under deadline pressure — writing a narrower, chat-scoped version of "open a diff" would have shipped just as fast, maybe faster, since it wouldn't need to satisfy Source Control's broader contract. The cost of skipping it doesn't show up on day one. It shows up months later, as two behaviors that were supposed to be identical quietly diverging, and a bug report that reads like a mystery until someone finds the second copy of the logic.
"Every surface we add to Influxx is another place a user can trigger the same underlying action — open a diff, create a PR, stage a file. The question we ask before shipping a new entry point isn't 'does this work,' it's 'does this call the same code the existing entry points call.' That discipline is invisible when it's followed and expensive when it isn't."
— Elena Voss, VP of Engineering at ETAPX
Frequently Asked Questions
Does the changed-files popover show a live diff, or just a file list?
It's a file list. Each row shows the file name, its directory, a status glyph, and per-file line counts. Clicking a row opens the actual diff in the editor area using the same openDiff action Source Control uses — the popover itself never renders diff content.
If a file is both staged and has unstaged edits, which version opens when I click it?
The unstaged, working-tree version. buildNativeChatChangedFiles explicitly prioritizes unstaged over untracked over staged when merging a path's split git-status rows, on the assumption that the working-tree edit is the most recent one.
Does clicking a file in the native-chat popover open a different tab than clicking the same file in Source Control?
No. Both call the same openDiff store action with the same worktree, path, and staged flag, which resolves to the same stable tab identity — so clicking either produces the same diff tab.
Does the Create PR button in the chat chrome duplicate Source Control's PR-creation logic?
No. It dispatches a one-shot request that Source Control's existing header action consumes, so PR creation runs through a single implementation regardless of which button triggered it.
What happens to the diff pill when there are no uncommitted changes?
If there's no line diff and no changed files, the pill doesn't render at all — the chrome only shows the repo/branch pill and Create PR button in that case.
Does the context strip poll git status separately from the rest of the app?
No. It reads from the same gitStatusByWorktree and remoteStatusesByWorktree store state that other surfaces like Source Control already populate, via a shallow-equal selector scoped to primitives to avoid unnecessary re-renders.
Is the churn chip's file count accurate for renamed files?
The chip reflects whatever git status reports for a path; rename detection is handled upstream by the git status pipeline the chip's data comes from, not by the chip itself.
The context strip is a small piece of UI, but the decision underneath it — one action, called from every surface that needs it — is the kind of restraint that keeps a codebase honest as it grows.

