- Fix memory-embeddings-llm-gateway tests: add queryInstruction field to
expected config objects after loadGatewayConfigFromEnv was updated to
return it
- Add STYLEGUIDE.md: SF code standards adapted from ace-coder patterns
(purpose doctrine, principles, anti-patterns STY001-012, thresholds,
naming, patterns, documentation sections)
- Phase 2 /sf prefix removal: update all web components, browser dispatch,
and tests to use direct commands (/autonomous, /stop, /next, /discuss,
/init, /new-milestone) instead of /sf-prefixed forms
- workflow-actions.ts: all command strings updated
- chat-mode.tsx: SF_ACTIONS array updated
- project-welcome.tsx: primaryCommand values updated
- command-surface.tsx: fallback display updated
- remaining-command-panels.tsx: usage examples updated
- browser-slash-command-dispatch.ts: add stop/new-milestone/init to
SF_PASSTHROUGH_COMMANDS so they route correctly to the extension
- recovery-diagnostics-service.ts: suggestion commands updated
- welcome-screen.ts: hint text updated
- All affected tests updated to match new command strings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
278 lines
8.2 KiB
TypeScript
278 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
ArrowRight,
|
|
ArrowUpCircle,
|
|
FileCode,
|
|
Folder,
|
|
FolderOpen,
|
|
GitBranch,
|
|
Package,
|
|
Sparkles,
|
|
} from "lucide-react";
|
|
import type { ProjectDetection } from "@/lib/sf-workspace-store";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
// ─── Variant Config ─────────────────────────────────────────────────────────
|
|
|
|
interface WelcomeVariant {
|
|
icon: React.ReactNode;
|
|
headline: string;
|
|
body: string;
|
|
detail?: string;
|
|
primaryLabel: string;
|
|
primaryCommand: string;
|
|
secondary?: {
|
|
label: string;
|
|
action: "files-view" | "command";
|
|
command?: string;
|
|
};
|
|
}
|
|
|
|
function getVariant(detection: ProjectDetection): WelcomeVariant {
|
|
switch (detection.kind) {
|
|
case "brownfield":
|
|
return {
|
|
icon: (
|
|
<FolderOpen className="h-8 w-8 text-foreground" strokeWidth={1.5} />
|
|
),
|
|
headline: "Existing project detected",
|
|
body: "SF will map your codebase and ask a few questions about what you want to build. From there it generates structured milestones and deliverable slices.",
|
|
primaryLabel: "Map & Initialize",
|
|
primaryCommand: "/init",
|
|
secondary: {
|
|
label: "Browse files first",
|
|
action: "files-view",
|
|
},
|
|
};
|
|
|
|
case "v1-legacy":
|
|
return {
|
|
icon: (
|
|
<ArrowUpCircle
|
|
className="h-8 w-8 text-foreground"
|
|
strokeWidth={1.5}
|
|
/>
|
|
),
|
|
headline: "SF v1 project found",
|
|
body: "This project has a .planning/ folder from an earlier SF version. Migration converts your existing planning data into the new .sf/ format.",
|
|
detail:
|
|
"Your original files will be preserved — migration creates the new structure alongside them.",
|
|
primaryLabel: "Migrate to v2",
|
|
primaryCommand: "/migrate",
|
|
secondary: {
|
|
label: "Start fresh instead",
|
|
action: "command",
|
|
command: "/init",
|
|
},
|
|
};
|
|
|
|
case "blank":
|
|
return {
|
|
icon: (
|
|
<Sparkles className="h-8 w-8 text-foreground" strokeWidth={1.5} />
|
|
),
|
|
headline: "Start a new project",
|
|
body: "This folder is empty. SF will ask what you want to build, then generate a structured plan — milestones broken into deliverable slices with risk-ordered execution.",
|
|
primaryLabel: "Start Project Setup",
|
|
primaryCommand: "/init",
|
|
};
|
|
|
|
// active-sf and empty-sf shouldn't reach here, but handle gracefully
|
|
default:
|
|
return {
|
|
icon: <Folder className="h-8 w-8 text-foreground" strokeWidth={1.5} />,
|
|
headline: "Set up your project",
|
|
body: "Run the SF wizard to get started.",
|
|
primaryLabel: "Get Started",
|
|
primaryCommand: "/init",
|
|
};
|
|
}
|
|
}
|
|
|
|
// ─── Signal Chips ───────────────────────────────────────────────────────────
|
|
|
|
function SignalChip({ icon, label }: { icon: React.ReactNode; label: string }) {
|
|
return (
|
|
<span className="inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1 text-xs text-muted-foreground">
|
|
{icon}
|
|
{label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function SignalChips({ signals }: { signals: ProjectDetection["signals"] }) {
|
|
const chips: { icon: React.ReactNode; label: string }[] = [];
|
|
|
|
if (signals.hasGitRepo) {
|
|
chips.push({
|
|
icon: <GitBranch className="h-3 w-3" />,
|
|
label: "Git repository",
|
|
});
|
|
}
|
|
if (signals.hasPackageJson) {
|
|
chips.push({
|
|
icon: <Package className="h-3 w-3" />,
|
|
label: "Node.js project",
|
|
});
|
|
}
|
|
if (signals.fileCount > 0) {
|
|
chips.push({
|
|
icon: <FileCode className="h-3 w-3" />,
|
|
label: `${signals.fileCount} file${signals.fileCount === 1 ? "" : "s"}`,
|
|
});
|
|
}
|
|
|
|
if (chips.length === 0) return null;
|
|
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
{chips.map((chip) => (
|
|
<SignalChip key={chip.label} icon={chip.icon} label={chip.label} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Main Component ─────────────────────────────────────────────────────────
|
|
|
|
interface ProjectWelcomeProps {
|
|
detection: ProjectDetection;
|
|
onCommand: (command: string) => void;
|
|
onSwitchView: (view: string) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function ProjectWelcome({
|
|
detection,
|
|
onCommand,
|
|
onSwitchView,
|
|
disabled = false,
|
|
}: ProjectWelcomeProps) {
|
|
const variant = getVariant(detection);
|
|
const showSignals =
|
|
detection.kind === "brownfield" || detection.kind === "v1-legacy";
|
|
|
|
return (
|
|
<div className="flex h-full items-center justify-center p-8">
|
|
<div className="w-full max-w-lg">
|
|
{/* Icon */}
|
|
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-xl border border-border bg-card">
|
|
{variant.icon}
|
|
</div>
|
|
|
|
{/* Headline */}
|
|
<h2 className="text-2xl font-bold tracking-tight text-foreground">
|
|
{variant.headline}
|
|
</h2>
|
|
|
|
{/* Body */}
|
|
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
|
|
{variant.body}
|
|
</p>
|
|
|
|
{/* Detail note */}
|
|
{variant.detail && (
|
|
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
|
|
{variant.detail}
|
|
</p>
|
|
)}
|
|
|
|
{/* Detected signals */}
|
|
{showSignals && (
|
|
<div className="mt-5">
|
|
<SignalChips signals={detection.signals} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="mt-8 flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => onCommand(variant.primaryCommand)}
|
|
disabled={disabled}
|
|
className={cn(
|
|
"inline-flex items-center gap-2 rounded-md bg-foreground px-5 py-2.5 text-sm font-medium text-background transition-colors hover:bg-foreground/90",
|
|
disabled && "cursor-not-allowed opacity-50",
|
|
)}
|
|
>
|
|
{variant.primaryLabel}
|
|
<ArrowRight className="h-3.5 w-3.5" />
|
|
</button>
|
|
|
|
{variant.secondary && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (variant.secondary!.action === "files-view") {
|
|
onSwitchView("files");
|
|
} else if (variant.secondary!.command) {
|
|
onCommand(variant.secondary!.command);
|
|
}
|
|
}}
|
|
disabled={disabled}
|
|
className={cn(
|
|
"inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-4 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-accent",
|
|
disabled && "cursor-not-allowed opacity-50",
|
|
)}
|
|
>
|
|
{variant.secondary.label}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* What happens next — for blank projects */}
|
|
{detection.kind === "blank" && (
|
|
<div className="mt-8 rounded-lg border border-border/50 bg-card/50 p-4">
|
|
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
What happens next
|
|
</p>
|
|
<ul className="mt-2.5 space-y-2">
|
|
{[
|
|
"A few questions about what you're building",
|
|
"Codebase analysis and context gathering",
|
|
"Structured milestone and slice generation",
|
|
].map((step, i) => (
|
|
<li
|
|
key={i}
|
|
className="flex items-start gap-2.5 text-xs text-muted-foreground"
|
|
>
|
|
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] font-medium text-muted-foreground">
|
|
{i + 1}
|
|
</span>
|
|
{step}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* What happens next — for brownfield */}
|
|
{detection.kind === "brownfield" && (
|
|
<div className="mt-8 rounded-lg border border-border/50 bg-card/50 p-4">
|
|
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
What happens next
|
|
</p>
|
|
<ul className="mt-2.5 space-y-2">
|
|
{[
|
|
"SF scans your codebase and asks about your goals",
|
|
"You discuss scope, constraints, and priorities",
|
|
"A milestone with risk-ordered slices is generated",
|
|
].map((step, i) => (
|
|
<li
|
|
key={i}
|
|
className="flex items-start gap-2.5 text-xs text-muted-foreground"
|
|
>
|
|
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] font-medium text-muted-foreground">
|
|
{i + 1}
|
|
</span>
|
|
{step}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|