Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import { ChatBox, ChatBoxHandle } from "@/features/chat/components/chatBox";
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
import { ChatPaneDropzone } from "@/features/chat/components/chatBox/chatPaneDropzone";
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
import { LanguageModelInfo, RepoSearchScope } from "@/features/chat/types";
import { LanguageModelInfo, RepoSearchScope, SearchScope } from "@/features/chat/types";
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
import { DISABLED_MCP_SERVER_IDS_LOCAL_STORAGE_KEY } from "@/features/chat/constants";
import { getRepoImageSrc } from '@/lib/utils';
import { useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLocalStorage } from "usehooks-ts";
import type { AskCommandDefinition } from '@/features/chat/commands/types';
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";

const ASKGH_SELECTED_SEARCH_SCOPES_LOCAL_STORAGE_KEY = 'askGhSelectedSearchScopes';

interface LandingPageProps {
languageModels: LanguageModelInfo[];
Expand All @@ -24,6 +27,8 @@ interface LandingPageProps {
askCommands: AskCommandDefinition[];
isAuthenticated: boolean;
maxImageBytes: number;
repos: RepositoryQuery[];
searchContexts: SearchContextQuery[];
}

export const LandingPage = ({
Expand All @@ -35,21 +40,49 @@ export const LandingPage = ({
askCommands,
isAuthenticated,
maxImageBytes,
repos,
searchContexts,
}: LandingPageProps) => {
const { createNewChatThread, isLoading } = useCreateNewChatThread();
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
const [disabledMcpServerIds, setDisabledMcpServerIds] = useLocalStorage<string[]>(DISABLED_MCP_SERVER_IDS_LOCAL_STORAGE_KEY, [], { initializeWithValue: false });
const chatBoxRef = useRef<ChatBoxHandle>(null);
const isChatBoxDisabled = languageModels.length === 0;

const selectedSearchScopes = useMemo(() => [
{
type: 'repo',
name: repoDisplayName ?? repoName,
value: repoName,
codeHostType: 'github' as const,
} satisfies RepoSearchScope,
], [repoDisplayName, repoName]);
// Default scope for the current repo
const defaultRepoScope = useMemo(() => ({
type: 'repo' as const,
name: repoDisplayName ?? repoName,
value: repoName,
codeHostType: 'github' as const,
} satisfies RepoSearchScope), [repoDisplayName, repoName]);

// Use local storage for selected scopes, with the current repo as default
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>(
ASKGH_SELECTED_SEARCH_SCOPES_LOCAL_STORAGE_KEY,
[defaultRepoScope],
{ initializeWithValue: false }
);

// Ensure the current repo is always included in selected scopes when visiting this page
// This handles the case where the user visits a different repo's Ask GH page
const [hasInitialized, setHasInitialized] = useState(false);
useEffect(() => {
if (hasInitialized) {
return;
}
setHasInitialized(true);

// Check if the current repo is already in the selected scopes
const currentRepoIncluded = selectedSearchScopes.some(
(scope) => scope.type === 'repo' && scope.value === repoName
);

// If not, add it to the scopes
if (!currentRepoIncluded) {
setSelectedSearchScopes([defaultRepoScope, ...selectedSearchScopes]);
}
}, [hasInitialized, selectedSearchScopes, repoName, defaultRepoScope, setSelectedSearchScopes]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current repo never auto-added

High Severity

The one-shot hasInitialized effect runs against the pre-hydration default ([defaultRepoScope]), so the current repo always looks included and the add path never runs. After useLocalStorage hydrates with { initializeWithValue: false }, stored scopes without this repo win, and soft navigations between Ask GH pages also skip re-checking because hasInitialized stays true. Visiting another repo's Ask GH page therefore does not auto-include that repo in the search scope.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bc78eef. Configure here.

Comment on lines +60 to +85

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

ast-grep outline 'packages/web/src/app/(app)/askgh/[owner]/[repo]/components/landingPage.tsx' --items all
rg -n -C 8 'initializeWithValue|hasInitialized|selectedSearchScopes|setSelectedSearchScopes' \
  'packages/web/src/app/(app)/askgh/[owner]/[repo]/components/landingPage.tsx'
fd -a -i '^(package\.json|pnpm-lock\.yaml)$' . -x rg -n -C 2 'usehooks-ts'

Repository: sourcebot-dev/sourcebot

Length of output: 6774


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== package manager lock section for usehooks-ts =="
python3 - <<'PY'
from pathlib import Path
for p in Path('.').rglob('pnpm-lock.yaml'):
    print(f'--- {p} ---')
    lines = p.read_text(encoding='utf-8').splitlines()
    for i,l in enumerate(lines):
        if '"usehooks-ts"' in l or '- usehooks-ts' in l or 'usehooks-ts@' in l:
            for j in range(max(0,i-5), min(len(lines),i+15)):
                print(f'{j+1}: {lines[j]}')
PY

echo "== local storage usage around scope merging =="
sed -n '40,145p' 'packages/web/src/app/(app)/askgh/[owner]/[repo]/components/landingPage.tsx'

echo "== dependency files mentioning usehooks-ts =="
fd -a -i '^(package\.json|pnpm-lock\.yaml|package-lock.json|yarn.lock|bun.lockb)$' . -x bash -c 'echo "--- $1 ---"; rg -n "usehooks-ts" "$1" || true' sh {}

Repository: sourcebot-dev/sourcebot

Length of output: 6281


🌐 Web query:

usehooks-ts useLocalStorage initializeWithValue false behavior first returns initialValue then restores localStorage

💡 Result:

In the usehooks-ts library, the initializeWithValue: false option is designed primarily for Server-Side Rendering (SSR) environments to prevent hydration mismatches [1][2][3]. When initializeWithValue is set to false, the hook behaves as follows: 1. Initial Value Assignment: Upon the first render, the hook ignores any data currently in localStorage and instead initializes the state using the provided initialValue [4][5]. 2. Subsequent Synchronization: The hook includes a useEffect that runs after the initial mount [2]. This effect triggers a re-read of the localStorage key [2]. If data exists in localStorage, the state is updated to reflect that stored value, effectively "restoring" or synchronizing the hook with the persisted data once it is safe to access the browser environment [2]. This mechanism ensures that the server-rendered HTML matches the initial client-side render (using initialValue), avoiding errors where the server cannot access window.localStorage or produces a different initial value than the client [1][2][3]. Once the component is mounted on the client, the useEffect catches up and updates the state with the actual localStorage content [2].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== packages/web package.json "
sed -n '1,240p' packages/web/package.json

echo "== yarn.lock usehooks-ts resolved package info =="
sed -n '23430,23445p' yarn.lock

echo "== find installed usehooks-ts package.json =="
fd -a -i '^package.json$' . \
  -x sh -c 'if rg -q "\"usehooks-ts\"" "$1/yarn.lock" && rg -q "\"usehooks-ts\"" "$1/package.json"; then echo "--- $1 ---"; fi' sh {} \;

echo "== search useLocalStorage implementation in installed usehooks-ts =="
python3 - <<'PY'
from pathlib import Path
matches=[]
for f in Path('.').rglob('useLocalStorage.ts'):
    p=str(f)
    if 'node_modules' not in p:
        continue
    text=f.read_text(errors='ignore')
    if 'initializeWithValue' in text:
        matches.append(p)
for p in matches[:20]:
    print("---", p, "---")
    lines=open(p, errors='ignore').readlines()
    for i,l in enumerate(lines,1):
        if 'initializeWithValue' in l or 'useEffect' in l or 'addEventListener' in l:
            print(f'{i}: {l.rstrip()}')
PY

Repository: sourcebot-dev/sourcebot

Length of output: 11253


🌐 Web query:

github juliencrn usehooks-ts 3.1.1 packages use-local-storage source useLocalStorage initializeWithValue false

💡 Result:

In version 3.1.1 of the usehooks-ts library, the useLocalStorage hook includes an optional parameter called initializeWithValue within its options object to support Server-Side Rendering (SSR) [1][2]. When initializeWithValue is set to false, the hook will not attempt to read from localStorage during the initial render [1]. Instead, it returns the provided initial value, which prevents hydration mismatch errors in SSR environments where the server does not have access to the browser's localStorage API [1]. The signature of useLocalStorage in this version is: export function useLocalStorage( key: string, initialValue: T | ( => T), options: UseLocalStorageOptions = {},): [T, Dispatch<SetStateAction>, => void] By default, initializeWithValue is set to true [1]. To disable it, you can pass the option as follows: const [value, setValue] = useLocalStorage('my-key', initialValue, { initializeWithValue: false });

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for f in Path('.').rglob('useLocalStorage.ts'):
    p = str(f)
    if 'node_modules' not in p and 'dist' not in p:
        continue
    text = f.read_text(errors='ignore')
    lines = text.splitlines()
    ok = any('initializeWithValue' in line for line in lines)
    print(f'--- {p} ok={ok} line_count={len(lines)} ---')
    if ok:
        for i,l in enumerate(lines, 1):
            if any(k in l for k in ['useStorageState', 'localStorage', 'initializeWithValue', 'addEventListener', 'useEffect']):
                print(f'{i}: {l}')
        # Print focused region around hook definition/body
        for start in range(len(lines)):
            if 'function useLocalStorage' in lines[start] or 'const useLocalStorage' in lines[start]:
                end = min(len(lines), start + 160)
                for j in range(start, end):
                    print(f'{j+1}: {lines[j]}')
                break
PY

echo "== fetch upstream 3.1.1 useLocalStorage.ts =="
python3 - <<'PY'
import urllib.request
url='https://github.com/ghraw/juliencrn/usehooks-ts/v3.1.1/packages/use-local-storage/src/useLocalStorage.ts'
try:
    text=urllib.request.urlopen(url, timeout=20).read().decode()
    for i,l in enumerate(text.splitlines(),1):
        if any(s in l for s in ['initializeWithValue', 'useStorageState', 'localStorage', 'addEventListener', 'useEffect']):
            print(f'{i}: {l}')
except Exception as e:
    print('FETCH_ERROR', repr(e))
PY

Repository: sourcebot-dev/sourcebot

Length of output: 379


🌐 Web query:

github juliencrn usehooks-ts v3.1.1 packages use-local-storage src useLocalStorage.ts initializeWithValue useEffect JSON.parse

💡 Result:

In the usehooks-ts library version 3.1.1, the useLocalStorage hook is designed to persist state in the browser's localStorage using JSON serialization [1]. The implementation handles initialization, reading, and synchronization through the following key mechanisms: initializeWithValue: The hook accepts an optional initializeWithValue parameter (defaulting to true) [1]. When set to true, the hook reads from localStorage during the initial state initialization [1]. If false, it ignores localStorage during the initial render and uses the provided initialValue instead, which helps prevent hydration mismatches in Server-Side Rendering (SSR) environments [1][2]. JSON.parse and Serialization: By default, the hook uses JSON.stringify for serialization and JSON.parse for deserialization [1]. The deserializer function is wrapped in a try-catch block; if JSON.parse fails, it logs an error and returns the provided initialValue as a fallback [1]. It also explicitly handles the string 'undefined' by returning the value undefined [1]. Users can override these defaults by providing custom serializer and deserializer functions in the options object [1]. useEffect and State Management: The hook utilizes useState to hold the current value and uses the localStorage API to persist updates [1]. While the provided source reflects the core logic of readValue being called during initialization via useState, the hook ensures that localStorage operations are safely executed in client-side environments by checking for the existence of the window object (or an equivalent server-side check) to avoid errors during server-side builds [1]. [1]

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
curl -fsSL --connect-timeout 15 https://registry.npmjs.org/usehooks-ts/3.1.1 \
  | jq -r '.dist.tarball' \
  | xargs -r curl -fsSL --connect-timeout 15 \
  | tar -xzO --no-same-owner --wildcards '*useLocalStorage.ts' \
  | sed -n '1,220p'

Repository: sourcebot-dev/sourcebot

Length of output: 264


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
curl -fsSL --connect-timeout 15 https://registry.npmjs.org/usehooks-ts/3.1.1 \
  | jq -r '.dist.tarball' \
  | xargs -r curl -fsSL --connect-timeout 15 \
  | tar -tz \
  | grep -E 'use-hooks-ts|useLocalStorage|package' | head -80

Repository: sourcebot-dev/sourcebot

Length of output: 310


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
tmp="$(mktemp -d)"
curl -fsSL --connect-timeout 15 https://registry.npmjs.org/usehooks-ts/3.1.1 \
  | jq -r '.dist.tarball' \
  | xargs curl -fsSL --connect-timeout 15 -o "$tmp/package.tgz"

echo "== build/useLocalStorage references =="
python3 - <<'PY'
import tarfile, re, subprocess, tempfile, sys
path=sys.argv[1]
with tarfile.open(path, "r:gz") as t:
    names=[n for n in t.getnames() if "local" in n.lower() or n.endswith(("js","cts","d.ts","d.cts"))]
    for n in names:
        try:
            data=t.extractfile(n).read().decode(errors="ignore")
        except Exception as e:
            print(n, "ERR", repr(e))
            continue
        if "initializeWithValue" in data or "useLocalStorage" in data:
            print(f"--- {n} ---")
            for i,line in enumerate(data.splitlines(),1):
                if any(k in line for k in ["initializeWithValue", "useLocalStorage", "useEffect", "localStorage", "useStorageState"]):
                    print(f"{i}: {line}")
py "$tmp/package.tgz"

echo "== JS module snippet around useLocalStorage exports =="
python3 - <<'PY'
import tarfile, sys
path=sys.argv[1]
with tarfile.open(path, "r:gz") as t:
    for n in ["package/dist/index.js","package/dist/index.cts","package/dist/index.cjs","package/dist/index.d.ts","package/dist/index.d.cts"]:
        try:
            data=t.extractfile(n).read().decode(errors="ignore")
        except Exception:
            continue
        if "local" in data.lower():
            print(f"--- {n} ---")
            for i,line in enumerate(data.splitlines(),1):
                if "local" in line.lower() or "export" in line and ("useLocalStorage" in line or "useReadLocalStorage" in line):
                    print(f"{i}: {line}")
            text=' '.join(data.split())
            start=text.find("useLocalStorage")
            if start!=-1:
                print("--- snippet ---")
                print(text[max(0,start-800):start+1600])
py "$tmp/package.tgz"

Repository: sourcebot-dev/sourcebot

Length of output: 376


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
tmp="$(mktemp -d)"
curl -fsSL --	connect-timeout 15 https://registry.npmjs.org/usehooks-ts/3.1.1 \
  | jq -r '.dist.tarball' \
  | xargs curl -fsSL --connect-timeout 15 -o "$tmp/package.tgz"

echo "== build/useLocalStorage references =="
python3 <<'PY' "$tmp/package.tgz"
import tarfile, sys
path=sys.argv[1]
with tarfile.open(path, "r:gz") as t:
    for n in t.getnames():
        try:
            data=t.extractfile(n).read().decode(errors="ignore")
        except Exception as e:
            continue
        if "initializeWithValue" in data or "useLocalStorage" in data:
            print(f"--- {n} ---")
            for i,line in enumerate(data.splitlines(),1):
                if any(k in line for k in ["initializeWithValue", "useLocalStorage", "useEffect", "localStorage", "useStorageState"]):
                    print(f"{i}: {line}")
PY

echo "== JS module snippet around useLocalStorage exports =="
python3 <<'PY' "$tmp/package.tgz"
import tarfile, sys
path=sys.argv[1]
with tarfile.open(path, "r:gz") as t:
    for n in ["package/dist/index.js","package/dist/index.cts","package/dist/index.cjs","package/dist/index.d.ts","package/dist/index.d.cts"]:
        try:
            data=t.extractfile(n).read().decode(errors="ignore")
        except Exception:
            continue
        if "local" in data.lower():
            print(f"--- {n} ---")
            for i,line in enumerate(data.splitlines(),1):
                if "local" in line.lower() or ("export" in line and ("useLocalStorage" in line or "useReadLocalStorage" in line)):
                    print(f"{i}: {line}")
            text=' '.join(data.split())
            start=text.find("useLocalStorage")
            if start!=-1:
                print("--- snippet ---")
                print(text[max(0,start-800):start+1600])
PY

Repository: sourcebot-dev/sourcebot

Length of output: 153


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
tmp="$(mktemp -d)"
curl -L https://codeload.github.com/juliencrn/usehooks-ts/tar.gz/v3.1.1 -o "$tmp/tar.gz"
tar -xzO -f "$tmp/tar.gz" "usehooks-ts-v3.1.1/packages/use-local-storage/src/useLocalStorage.ts" | sed -n '1,240p'

echo "== installed usehooks source if present =="
if [ -f node_modules/usehooks-ts/usehooks-ts-v3.1.1/packages/use-local-storage/src/useLocalStorage.ts ]; then
  sed -n '1,240p' node_modules/usehooks-ts/usehooks-ts-v3.1.1/packages/use-local-storage/src/useLocalStorage.ts
else
  echo "not present"
fi

Repository: sourcebot-dev/sourcebot

Length of output: 577


Remove the initialization gate.

With useLocalStorage(..., { initializeWithValue: false }), the initial render uses the default scope before local storage restores. This effect then runs once on the default value and skips persisted scopes; after that, it cannot add the current repository when repoName changes. Run the merge on each repoName change with a functional setter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/`(app)/askgh/[owner]/[repo]/components/landingPage.tsx
around lines 60 - 85, Remove the hasInitialized state and initialization guard
from the effect. Update the selectedSearchScopes effect to run when repoName
changes and use the functional form of setSelectedSearchScopes, adding
defaultRepoScope only when the current repository is absent while preserving
restored scopes.


const imageSrc = imageUrl ? getRepoImageSrc(imageUrl, repoId) : undefined;
const displayName = repoDisplayName ?? repoName;
Expand Down Expand Up @@ -88,7 +121,7 @@ export const LandingPage = ({
className="min-h-[50px]"
isRedirecting={isLoading}
selectedSearchScopes={selectedSearchScopes}
searchContexts={[]}
searchContexts={searchContexts}
askCommands={askCommands}
isDisabled={isChatBoxDisabled}
isAuthenticated={isAuthenticated}
Expand All @@ -100,10 +133,10 @@ export const LandingPage = ({
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
<ChatBoxToolbar
languageModels={languageModels}
repos={[]}
searchContexts={[]}
repos={repos}
searchContexts={searchContexts}
selectedSearchScopes={selectedSearchScopes}
onSelectedSearchScopesChange={() => { }}
onSelectedSearchScopesChange={setSelectedSearchScopes}
isContextSelectorOpen={isContextSelectorOpen}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
disabledMcpServerIds={disabledMcpServerIds}
Expand Down
13 changes: 13 additions & 0 deletions packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { hasEntitlement } from "@/lib/entitlements";
import { ChatEntitlementMessage } from "@/features/chat/components/chatEntitlementMessage";
import { env } from "@sourcebot/shared";
import { listAgentSkillCommandsOrEmpty } from "@/ee/features/chat/skills/skillCommands.server";
import { getRepos, getSearchContexts } from "@/actions";

interface PageProps {
params: Promise<{ owner: string; repo: string }>;
Expand Down Expand Up @@ -70,11 +71,21 @@ export default async function GitHubRepoPage(props: PageProps) {
const askCommands = session?.user
? await listAgentSkillCommandsOrEmpty()
: [];
const allRepos = await getRepos();
const searchContexts = await getSearchContexts();

if (isServiceError(repoInfo)) {
throw new ServiceErrorException(repoInfo);
}

if (isServiceError(allRepos)) {
throw new ServiceErrorException(allRepos);
}

if (isServiceError(searchContexts)) {
throw new ServiceErrorException(searchContexts);
}

return (
<RepoIndexedGuard initialRepoInfo={repoInfo}>
<CustomSlateEditor>
Expand All @@ -87,6 +98,8 @@ export default async function GitHubRepoPage(props: PageProps) {
askCommands={askCommands}
isAuthenticated={!!session?.user}
maxImageBytes={env.SOURCEBOT_CHAT_ATTACHMENT_MAX_IMAGE_BYTES}
repos={allRepos}
searchContexts={searchContexts}
/>
</CustomSlateEditor>
</RepoIndexedGuard>
Expand Down
Loading