fix: scope instance-served state to the connected instance
Linear and GitHub logins, quotas, MCP status, skills and agent memory are served by whichever instance is connected, but each was cached globally or by directory alone — which two instances can share. Switching instances left the previous instance's answers on screen and its Linear login usable against a runtime that has no Linear. Reset them all through runtimeEndpointReset, each store guarding its in-flight requests with a generation so a response for the previous instance cannot land in the new one. The Linear team filter is now persisted per instance: a team belongs to one workspace, so carrying it across filtered the new instance's issue list down to nothing. Usage also waits for the instance to report itself initialised before loading. Providers report themselves as configured only once the instance can read their credentials, so a fetch fired at mount answered "nothing configured" for every provider and cached it — which is why Usage stayed missing from the work-status panel until Settings -> Usage forced a fresh fetch.
This commit is contained in:
@@ -347,6 +347,28 @@ the matching header dropdown:
|
||||
discovered relative to the active project. It does not wrap the call in
|
||||
`runBackgroundNetworkTask`: the store already gates its own fetch.
|
||||
|
||||
Usage waits for the instance to say it is initialised. Quota providers report
|
||||
themselves as configured only once the instance can read their credentials,
|
||||
which on a remote instance is not true when the UI mounts — a fetch fired at
|
||||
mount gets "nothing configured" for every provider, and since each one then has
|
||||
a result, nothing asks again until the three-minute refresh. That is why Usage
|
||||
could stay missing from the panel until Settings -> Usage forced a fresh fetch.
|
||||
`useQuotaStore.ensureLoadedForRuntime` owns both the readiness rule and the
|
||||
once-per-instance bookkeeping, so every caller can ask on each connection
|
||||
change.
|
||||
|
||||
### These readouts belong to the connected instance
|
||||
|
||||
Quotas, MCP status, skills, agent memory and the Linear/GitHub logins are all
|
||||
served by whichever OpenChamber instance is connected, and each was cached
|
||||
globally or by directory alone — which two instances can share. A switch left
|
||||
the previous instance's answers on screen, and its Linear login usable against
|
||||
a runtime that has no Linear. `apps/runtimeEndpointReset.ts` now drops all of
|
||||
them, each store guarding its own in-flight requests with a generation so a
|
||||
response for the previous instance cannot land in the new one. The MCP and
|
||||
skills effects take `isConnected` as a dependency — not a gate — because
|
||||
`directory` alone does not change when both instances hold the same path.
|
||||
|
||||
The panel now performs these itself, silently and through the
|
||||
background-network gate, so it cannot compete with chat bootstrap traffic for
|
||||
sockets. Usage additionally provides an explicit refresh action in its section
|
||||
|
||||
@@ -15,6 +15,7 @@ import { resolveProjectContextId } from '@/lib/projectContextApi';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useMobileAppActions } from '@/apps/mobileAppContext';
|
||||
import { useLinearAuthStore } from '@/stores/useLinearAuthStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { WorkStatusCollapsibleSection, WorkStatusRow, WorkStatusValue } from './WorkStatusPrimitives';
|
||||
import { useReportWorkStatusPresence } from './presenceContext';
|
||||
@@ -61,9 +62,15 @@ export const WorkStatusContextSection: React.FC<Props> = ({ sessionId, directory
|
||||
// here: `loadSkills` already gates its own fetch, and wrapping it again
|
||||
// would hold a second slot idle for the length of the first.
|
||||
const loadSkills = useSkillsStore((state) => state.loadSkills);
|
||||
// `isConnected` is a dependency, not a gate: skills are discovered on the
|
||||
// connected instance and their caches are dropped when instances switch, so
|
||||
// the count has to be asked for again once the new instance is up. Two
|
||||
// instances can hold the same project path, which leaves `directory`
|
||||
// unchanged across a switch.
|
||||
const isConnected = useConfigStore((state) => state.isConnected);
|
||||
React.useEffect(() => {
|
||||
void loadSkills();
|
||||
}, [directory, loadSkills]);
|
||||
}, [directory, isConnected, loadSkills]);
|
||||
|
||||
/**
|
||||
* What this session carries. Read from the server
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useMcpStore } from '@/stores/useMcpStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { McpIcon } from '@/components/icons/McpIcon';
|
||||
import { runBackgroundNetworkTask } from '@/lib/background-network';
|
||||
import { toast } from 'sonner';
|
||||
@@ -28,6 +29,7 @@ export const WorkStatusMcpSection: React.FC<Props> = ({ directory }) => {
|
||||
const ensureMcpFresh = useMcpStore((state) => state.ensureFresh);
|
||||
const connect = useMcpStore((state) => state.connect);
|
||||
const disconnect = useMcpStore((state) => state.disconnect);
|
||||
const isConnected = useConfigStore((state) => state.isConnected);
|
||||
const [busyServer, setBusyServer] = React.useState<string | null>(null);
|
||||
|
||||
// The panel must not depend on the header dropdown having been mounted or
|
||||
@@ -35,9 +37,12 @@ export const WorkStatusMcpSection: React.FC<Props> = ({ directory }) => {
|
||||
// compete with chat bootstrap traffic for sockets. The section remounts on
|
||||
// every session switch, so it only asks for a status that is missing or
|
||||
// older than a minute; connect/disconnect/auth refresh on their own.
|
||||
// `isConnected` is a dependency, not a gate: MCP status is cached by
|
||||
// directory alone and dropped on an instance switch, and two instances can
|
||||
// hold the same project path — so the switch itself has to trigger the ask.
|
||||
React.useEffect(() => {
|
||||
void runBackgroundNetworkTask(() => ensureMcpFresh({ directory, silent: true, maxAgeMs: MCP_STATUS_MAX_AGE_MS }));
|
||||
}, [directory, ensureMcpFresh]);
|
||||
}, [directory, ensureMcpFresh, isConnected]);
|
||||
|
||||
const mcpServers = React.useMemo(
|
||||
() => Object.entries(mcpStatus ?? {}).sort(([left], [right]) => left.localeCompare(right)),
|
||||
|
||||
@@ -43,9 +43,10 @@ export const WorkStatusUsageSection: React.FC = () => {
|
||||
const groups = useUsageProviderGroups();
|
||||
const displayMode = useQuotaStore((state) => state.displayMode);
|
||||
const isLoading = useQuotaStore((state) => state.isLoading);
|
||||
const quotaResults = useQuotaStore((state) => state.results);
|
||||
const dropdownProviderIds = useQuotaStore((state) => state.dropdownProviderIds);
|
||||
const fetchQuotas = useQuotaStore((state) => state.fetchQuotas);
|
||||
const ensureQuotasLoadedForRuntime = useQuotaStore((state) => state.ensureLoadedForRuntime);
|
||||
const isInitialized = useConfigStore((state) => state.isInitialized);
|
||||
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||
const currentProviderId = useConfigStore((state) => state.currentProviderId);
|
||||
|
||||
@@ -54,17 +55,13 @@ export const WorkStatusUsageSection: React.FC = () => {
|
||||
|
||||
// `useQuotaAutoRefresh` only schedules an interval — it never performs the
|
||||
// first fetch. That was owned by the header dropdown's open handler, so the
|
||||
// panel stayed empty until the user opened it. Kick off the initial load for
|
||||
// any enabled provider that has not reported yet, background-gated so it
|
||||
// cannot compete with chat bootstrap traffic.
|
||||
// panel stayed empty until the user opened it. `ensureLoadedForRuntime` owns
|
||||
// the once-per-instance load and its readiness rule; asking again is a no-op,
|
||||
// so this is safe to run on every connection change.
|
||||
React.useEffect(() => {
|
||||
if (isLoading || dropdownProviderIds.length === 0) return;
|
||||
const missingProvider = dropdownProviderIds.some(
|
||||
(providerId) => !quotaResults.some((result) => result.providerId === providerId),
|
||||
);
|
||||
if (!missingProvider) return;
|
||||
void runBackgroundNetworkTask(() => fetchQuotas(dropdownProviderIds));
|
||||
}, [dropdownProviderIds, fetchQuotas, isLoading, quotaResults]);
|
||||
if (!isInitialized) return;
|
||||
void runBackgroundNetworkTask(() => ensureQuotasLoadedForRuntime());
|
||||
}, [ensureQuotasLoadedForRuntime, isInitialized]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (groups.length === 0) return;
|
||||
|
||||
@@ -251,6 +251,14 @@ export const LinearIssuesView: React.FC = () => {
|
||||
const setListPriority = useUIStore((state) => state.setLinearIssueListPriority);
|
||||
const resetListFilters = useUIStore((state) => state.resetLinearIssueListFilters);
|
||||
const setLinearIssueFocus = useUIStore((state) => state.setLinearIssueFocus);
|
||||
const applyLinearFiltersForRuntime = useUIStore((state) => state.applyLinearIssueListFiltersForRuntime);
|
||||
|
||||
// The team filter is stored per instance, and rehydration can run before the
|
||||
// runtime endpoint is known. Reading it here means the view always opens on
|
||||
// the filter belonging to the instance it is about to query.
|
||||
React.useEffect(() => {
|
||||
applyLinearFiltersForRuntime();
|
||||
}, [applyLinearFiltersForRuntime]);
|
||||
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [searchOpen, setSearchOpen] = React.useState(false);
|
||||
|
||||
Reference in New Issue
Block a user