2026-03-22 19:07:07 +00:00
|
|
|
import type { KeyboardEvent } from "react";
|
2026-03-31 10:04:52 +00:00
|
|
|
import type { OAuthStatus } from "../../api/client";
|
2026-03-22 19:07:07 +00:00
|
|
|
import { ProjectPathInput } from "./ProjectPathInput.tsx";
|
|
|
|
|
import { RecentProjectsList } from "./RecentProjectsList.tsx";
|
|
|
|
|
|
|
|
|
|
export interface RecentProjectMatch {
|
|
|
|
|
name: string;
|
|
|
|
|
path: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SelectionScreenProps {
|
|
|
|
|
knownProjects: string[];
|
|
|
|
|
onOpenProject: (path: string) => void;
|
|
|
|
|
onForgetProject: (path: string) => void;
|
|
|
|
|
pathInput: string;
|
|
|
|
|
homeDir?: string | null;
|
|
|
|
|
onPathInputChange: (value: string) => void;
|
|
|
|
|
onPathInputKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;
|
|
|
|
|
isOpening: boolean;
|
|
|
|
|
suggestionTail: string;
|
|
|
|
|
matchList: RecentProjectMatch[];
|
|
|
|
|
selectedMatch: number;
|
|
|
|
|
onSelectMatch: (index: number) => void;
|
|
|
|
|
onAcceptMatch: (path: string) => void;
|
|
|
|
|
onCloseSuggestions: () => void;
|
|
|
|
|
completionError: string | null;
|
|
|
|
|
currentPartial: string;
|
2026-03-31 10:04:52 +00:00
|
|
|
oauthStatus?: OAuthStatus | null;
|
2026-03-22 19:07:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SelectionScreen({
|
|
|
|
|
knownProjects,
|
|
|
|
|
onOpenProject,
|
|
|
|
|
onForgetProject,
|
|
|
|
|
pathInput,
|
|
|
|
|
homeDir,
|
|
|
|
|
onPathInputChange,
|
|
|
|
|
onPathInputKeyDown,
|
|
|
|
|
isOpening,
|
|
|
|
|
suggestionTail,
|
|
|
|
|
matchList,
|
|
|
|
|
selectedMatch,
|
|
|
|
|
onSelectMatch,
|
|
|
|
|
onAcceptMatch,
|
|
|
|
|
onCloseSuggestions,
|
|
|
|
|
completionError,
|
|
|
|
|
currentPartial,
|
2026-03-31 10:04:52 +00:00
|
|
|
oauthStatus = null,
|
2026-03-22 19:07:07 +00:00
|
|
|
}: SelectionScreenProps) {
|
|
|
|
|
const resolvedHomeDir = homeDir
|
|
|
|
|
? homeDir.endsWith("/")
|
|
|
|
|
? homeDir
|
|
|
|
|
: `${homeDir}/`
|
|
|
|
|
: "";
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="selection-screen"
|
|
|
|
|
style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}
|
|
|
|
|
>
|
2026-04-03 16:12:52 +01:00
|
|
|
<h1>Huskies</h1>
|
2026-03-22 19:07:07 +00:00
|
|
|
<p>Paste or complete a project path to start.</p>
|
|
|
|
|
|
2026-03-31 10:04:52 +00:00
|
|
|
{oauthStatus !== null && (
|
|
|
|
|
<div style={{ marginBottom: "1rem" }}>
|
|
|
|
|
{!oauthStatus.authenticated || oauthStatus.expired ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
2026-03-31 14:52:18 +00:00
|
|
|
window.open(
|
|
|
|
|
"/oauth/authorize",
|
|
|
|
|
"_blank",
|
|
|
|
|
"noopener,noreferrer",
|
|
|
|
|
);
|
2026-03-31 10:04:52 +00:00
|
|
|
}}
|
|
|
|
|
style={{
|
|
|
|
|
padding: "8px 16px",
|
|
|
|
|
borderRadius: "6px",
|
|
|
|
|
border: "1px solid #1a3a5c",
|
|
|
|
|
backgroundColor: "#1a3a5c",
|
|
|
|
|
color: "#7eb8f7",
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
fontSize: "0.9em",
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-03-31 14:52:18 +00:00
|
|
|
{oauthStatus.expired
|
|
|
|
|
? "Re-authenticate with Claude"
|
|
|
|
|
: "Login with Claude"}
|
2026-03-31 10:04:52 +00:00
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<span
|
|
|
|
|
title="Authenticated with Claude via OAuth"
|
|
|
|
|
style={{ color: "#4caf50", fontSize: "0.9em" }}
|
|
|
|
|
>
|
|
|
|
|
✓ Authenticated with Claude
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-22 19:07:07 +00:00
|
|
|
{knownProjects.length > 0 && (
|
|
|
|
|
<RecentProjectsList
|
|
|
|
|
projects={knownProjects}
|
|
|
|
|
onOpenProject={onOpenProject}
|
|
|
|
|
onForgetProject={onForgetProject}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<ProjectPathInput
|
|
|
|
|
value={pathInput}
|
|
|
|
|
onChange={onPathInputChange}
|
|
|
|
|
onKeyDown={onPathInputKeyDown}
|
|
|
|
|
suggestionTail={suggestionTail}
|
|
|
|
|
matchList={matchList}
|
|
|
|
|
selectedMatch={selectedMatch}
|
|
|
|
|
onSelectMatch={onSelectMatch}
|
|
|
|
|
onAcceptMatch={onAcceptMatch}
|
|
|
|
|
onCloseSuggestions={onCloseSuggestions}
|
|
|
|
|
currentPartial={currentPartial}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
gap: "8px",
|
|
|
|
|
marginTop: "8px",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onOpenProject(pathInput)}
|
|
|
|
|
disabled={isOpening}
|
|
|
|
|
>
|
|
|
|
|
{isOpening ? "Opening..." : "Open Project"}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onPathInputChange(resolvedHomeDir);
|
|
|
|
|
onCloseSuggestions();
|
|
|
|
|
}}
|
|
|
|
|
disabled={isOpening}
|
|
|
|
|
>
|
|
|
|
|
New Project
|
|
|
|
|
</button>
|
|
|
|
|
<div style={{ fontSize: "0.85em", color: "#666" }}>
|
|
|
|
|
Press Tab to complete the next path segment
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{completionError && (
|
|
|
|
|
<div style={{ color: "red", marginTop: "8px" }}>{completionError}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|