Skip to content
This repository was archived by the owner on Jun 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/claude-progress.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# Claude Progress Log
# Newest entries first. Agents: append your entry at the top after the header.

---
## 2026-05-13 | Session: Fix function list not updating on GitHub user switch
Worked on: Bug fix for PAT switching via Avatar button not refreshing the function list
Completed:
- Root cause: useEffect in useFunctionListPage had no dependency that changed on PAT switch (isConnectedToForge stayed true), and GithubService kept stale repo cache from previous user
- Added connectionId counter to ForgeConnectionContext, incremented on every connectToForge call
- useFunctionListPage uses connectionId as useEffect dependency and render-time state reset trigger
- GithubService clears #cachedFunctionRepos and #lastCommitSha when token changes in the #octokit getter
- Reworked usePatModal per PR review: replaced wasOpen render-time tracking with explicit cleanup at two exit points (setPat('') after successful connect, handleClose callback on cancel/dismiss)
- Clear stale functionItems on user switch so previous user's data doesn't linger during re-fetch
- Added tests for PAT modal cleanup (cleared on connect, cleared on cancel)
- 13 suites, 112 tests, all passing, zero lint errors
Left off: Branch chng-gh-user, PR #24 open. Reviewer requested dropping connectionId in favor of user.name, but keeping connectionId because it handles same-user different-PAT scenario.
Blockers: None

---
## 2026-04-30 | Session: Set KUBECONFIG GitHub secret for CI/CD deployment
Worked on: Encrypt and set KUBECONFIG secret on GitHub repos so GH Actions can deploy to the cluster
Expand Down
1 change: 1 addition & 0 deletions src/components/CreateFunctionForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const testUser: ForgeUser = { name: 'testuser' };
const forgeContext = {
isActive: true,
user: testUser,
connectionId: 0,
connectToForge: vi.fn(),
};

Expand Down
45 changes: 44 additions & 1 deletion src/components/UserAvatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const testUser = { name: 'twoGiants' };

function renderWithContext(
ui: ReactNode,
contextValue = { isActive: false, user: testUser, connectToForge: vi.fn() },
contextValue = { isActive: false, user: testUser, connectionId: 0, connectToForge: vi.fn() },
) {
return render(
<ForgeConnectionContext.Provider value={contextValue}>{ui}</ForgeConnectionContext.Provider>,
Expand Down Expand Up @@ -120,6 +120,7 @@ describe('UserAvatar', () => {
renderWithContext(<UserAvatar enableReconnect />, {
isActive: false,
user: testUser,
connectionId: 0,
connectToForge,
});

Expand Down Expand Up @@ -160,6 +161,48 @@ describe('UserAvatar', () => {
expect(screen.queryByText('Personal Access Token')).not.toBeInTheDocument();
});

it('clears PAT input after successful connect', async () => {
const user = userEvent.setup();
const connectToForge = vi.fn();
mockFetchUserInfo.mockResolvedValue(testUser);

renderWithContext(<UserAvatar enableReconnect />, {
isActive: false,
user: testUser,
connectionId: 0,
connectToForge,
});

await user.type(screen.getByLabelText('Personal Access Token'), 'ghp_valid');
await user.click(screen.getByRole('button', { name: 'Connect' }));

await waitFor(() => {
expect(screen.getByText('twoGiants')).toBeInTheDocument();
});

await user.click(screen.getByRole('button', { name: 'twoGiants' }));

expect(screen.getByLabelText('Personal Access Token')).toHaveValue('');
});

it('clears PAT input and error on cancel', async () => {
const user = userEvent.setup();
mockFetchUserInfo.mockRejectedValue(new Error('Bad credentials'));

renderWithContext(<UserAvatar enableReconnect />);

await user.type(screen.getByLabelText('Personal Access Token'), 'ghp_bad');
await user.click(screen.getByRole('button', { name: 'Connect' }));

expect(await screen.findByText('Bad credentials')).toBeInTheDocument();

await user.click(screen.getByRole('button', { name: 'Cancel' }));
await user.click(screen.getByRole('button', { name: 'Connect to GitHub' }));

expect(screen.getByLabelText('Personal Access Token')).toHaveValue('');
expect(screen.queryByText('Bad credentials')).not.toBeInTheDocument();
});

it('disables Cancel button while validating', async () => {
const user = userEvent.setup();
let resolveConnect: () => void;
Expand Down
20 changes: 15 additions & 5 deletions src/components/UserAvatar.tsx
Comment thread
matejvasek marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,13 @@ interface PatModalProps {

function PatModal({ isOpen, onClose, onConnect }: PatModalProps) {
const { t } = useTranslation('plugin__console-functions-plugin');
const { pat, isValidating, error, setPat, handleConnect } = usePatModal(onConnect);
const { pat, isValidating, error, setPat, handleConnect, handleClose } = usePatModal(
onClose,
onConnect,
);

return (
<Modal isOpen={isOpen} onClose={isValidating ? undefined : onClose} variant="small">
<Modal isOpen={isOpen} onClose={isValidating ? undefined : handleClose} variant="small">
<ModalHeader title={t('Connect to GitHub')} />
<ModalBody>
{error && (
Expand All @@ -120,15 +123,15 @@ function PatModal({ isOpen, onClose, onConnect }: PatModalProps) {
>
{t('Connect')}
</Button>
<Button variant="link" onClick={onClose} isDisabled={isValidating}>
<Button variant="link" onClick={handleClose} isDisabled={isValidating}>
{t('Cancel')}
</Button>
</ModalFooter>
</Modal>
);
}

function usePatModal(onConnect: (pat: string) => Promise<void>) {
function usePatModal(onClose: () => void, onConnect: (pat: string) => Promise<void>) {
const [pat, setPat] = useState('');
const [isValidating, setIsValidating] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand All @@ -138,12 +141,19 @@ function usePatModal(onConnect: (pat: string) => Promise<void>) {
setError(null);
try {
await onConnect(pat);
setPat('');
Comment thread
twoGiants marked this conversation as resolved.
Resolved
} catch (err) {
setError(errorMessage(err));
} finally {
setIsValidating(false);
}
};

return { pat, isValidating, error, setPat, handleConnect };
const handleClose = () => {
setPat('');
setError(null);
onClose();
};

return { pat, isValidating, error, setPat, handleConnect, handleClose };
}
6 changes: 5 additions & 1 deletion src/context/ForgeConnectionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,30 @@ import { ForgeUser, PAT_KEY, USER_KEY } from '../services/types';
interface ForgeConnection {
isActive: boolean;
user: ForgeUser;
connectionId: number;
connectToForge: (user: ForgeUser) => void;
}

export const ForgeConnectionContext = createContext<ForgeConnection>({
isActive: false,
user: { name: '' },
connectionId: 0,
Comment thread
matejvasek marked this conversation as resolved.
Resolved
connectToForge: () => {},
});

export function ForgeConnectionProvider({ children }: Readonly<{ children: ReactNode }>) {
const [isActive, setIsActive] = useState<boolean>(() => !!sessionStorage.getItem(PAT_KEY));
const [user, setUser] = useState<ForgeUser>(readStoredUser);
const [connectionId, setConnectionId] = useState(0);

const connectToForge = (forgeUser: ForgeUser) => {
setUser(forgeUser);
setIsActive(true);
setConnectionId((id) => id + 1);
};

return (
<ForgeConnectionContext.Provider value={{ isActive, user, connectToForge }}>
<ForgeConnectionContext.Provider value={{ isActive, user, connectionId, connectToForge }}>
{children}
</ForgeConnectionContext.Provider>
);
Expand Down
2 changes: 2 additions & 0 deletions src/services/source-control/GithubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export class GithubService implements SourceControlService {
if (token !== this.#cachedToken) {
this.#cachedToken = token;
this.#cachedOctokit = new Octokit({ auth: token });
this.#cachedFunctionRepos = [];
this.#lastCommitSha.clear();
}
return this.#cachedOctokit!;
}
Expand Down
20 changes: 8 additions & 12 deletions src/views/FunctionsListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,26 +92,22 @@ function useFunctionListPage(): {
isConnectedToForge: boolean;
error: string;
} {
const isConnectedToForge = useContext(ForgeConnectionContext).isActive;
const { isActive: isConnectedToForge, connectionId } = useContext(ForgeConnectionContext);
const sourceControl = useSourceControlService();
const navigate = useNavigate();

const [functionItems, setFunctionItems] = useState<FunctionTableItem[]>([]);
const [reposLoaded, setReposLoaded] = useState(!isConnectedToForge);
const [wasConnectedToForge, setWasConnectedToForge] = useState(isConnectedToForge);
const [prevConnectionId, setPrevConnectionId] = useState(connectionId);

const [error, setError] = useState<string>('');

// Reset state when authentication status changes (render-time adjustment)
if (isConnectedToForge !== wasConnectedToForge) {
setWasConnectedToForge(isConnectedToForge);
// Reset state when connection changes (initial connect or user switch)
if (connectionId !== prevConnectionId) {
setPrevConnectionId(connectionId);
setFunctionItems([]);
setError('');
if (isConnectedToForge) {
setReposLoaded(false);
} else {
setFunctionItems([]);
setReposLoaded(true);
}
setReposLoaded(false);
}

useEffect(() => {
Expand Down Expand Up @@ -149,7 +145,7 @@ function useFunctionListPage(): {
return () => {
ignore = true;
};
}, [sourceControl, isConnectedToForge]);
}, [sourceControl, isConnectedToForge, connectionId]);

const functionNames = useMemo(() => functionItems.map((item) => item.name), [functionItems]);

Expand Down