feat: v1.3.0 — plan-first workflow, OpenRouter provider, enhanced prompt engine

Major changes:
- Plan-first workflow: AI generates structured plan before code, with
  plan review card (Modify Plan / Start Coding / Skip to Code)
- Post-coding UX: Preview + Request Modifications buttons after code gen
- OpenRouter integration: 4th AI provider with 20+ model support
- Enhanced prompt engine: 9 strategies, 11+ intent patterns, modular
- PLAN MODE system prompt block in all 4 services
- Fixed stale React closure in approveAndGenerate with isApproval flag
- Fixed canvas auto-opening during plan phase with wasIdle gate
- Updated README, CHANGELOG, .env.example, version bump to 1.3.0
This commit is contained in:
admin
2026-03-18 18:45:37 +00:00
Unverified
parent cca11fe07a
commit a4b7a0d9e4
17 changed files with 3189 additions and 358 deletions

View File

@@ -54,6 +54,10 @@ export default function SettingsPanel() {
setApiKey("zai", keys.zai);
modelAdapter.updateZaiApiKey(keys.zai);
}
if (keys.openrouter) {
setApiKey("openrouter", keys.openrouter);
modelAdapter.updateOpenRouterApiKey(keys.openrouter);
}
} catch (e) {
console.error("Failed to load API keys:", e);
}
@@ -84,7 +88,7 @@ export default function SettingsPanel() {
}
};
const validateApiKey = async (provider: "qwen" | "ollama" | "zai") => {
const validateApiKey = async (provider: "qwen" | "ollama" | "zai" | "openrouter") => {
const key = apiKeys[provider];
if (!key || key.trim().length === 0) {
setApiValidationStatus(provider, { valid: false, error: "API key is required" });
@@ -149,6 +153,9 @@ export default function SettingsPanel() {
case "zai":
modelAdapter.updateZaiApiKey(value);
break;
case "openrouter":
modelAdapter.updateOpenRouterApiKey(value);
break;
}
// Debounce validation (500ms)
@@ -187,7 +194,7 @@ export default function SettingsPanel() {
}
};
const getStatusIndicator = (provider: "qwen" | "ollama" | "zai") => {
const getStatusIndicator = (provider: "qwen" | "ollama" | "zai" | "openrouter") => {
const status = apiValidationStatus[provider];
if (validating[provider]) {
@@ -439,6 +446,63 @@ export default function SettingsPanel() {
</div>
</div>
<div className="space-y-2 text-start">
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
OpenRouter API Key
</label>
<div className="relative">
<Input
type={showApiKey.openrouter ? "text" : "password"}
placeholder={t.enterKey("OpenRouter")}
value={apiKeys.openrouter || ""}
onChange={(e) => handleApiKeyChange("openrouter", e.target.value)}
className="font-mono text-xs lg:text-sm pr-24"
/>
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
{getStatusIndicator("openrouter")}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => setShowApiKey((prev) => ({ ...prev, openrouter: !prev.openrouter }))}
>
{showApiKey.openrouter ? (
<EyeOff className="h-3 w-3" />
) : (
<Eye className="h-3 w-3" />
)}
</Button>
</div>
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<p className="text-[10px] lg:text-xs text-muted-foreground">
{t.getApiKey}{" "}
<a
href="https://openrouter.ai/keys"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
openrouter.ai/keys
</a>
</p>
{apiKeys.openrouter && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-[9px] lg:text-[10px] w-fit"
onClick={() => validateApiKey("openrouter")}
disabled={validating.openrouter}
>
<RefreshCw className={`h-2.5 w-2.5 mr-1 ${validating.openrouter ? "animate-spin" : ""}`} />
Test
</Button>
)}
</div>
</div>
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
{t.saveKeys}
@@ -455,7 +519,7 @@ export default function SettingsPanel() {
</CardHeader>
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
<div className="grid gap-2 lg:gap-3">
{(["qwen", "ollama", "zai"] as const).map((provider) => (
{(["qwen", "ollama", "zai", "openrouter"] as const).map((provider) => (
<button
key={provider}
onClick={() => setSelectedProvider(provider)}
@@ -468,11 +532,12 @@ export default function SettingsPanel() {
<Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
<h3 className="font-medium capitalize text-sm lg:text-base">{provider === "openrouter" ? "OpenRouter" : provider}</h3>
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
{provider === "qwen" && t.qwenDesc}
{provider === "ollama" && t.ollamaDesc}
{provider === "zai" && t.zaiDesc}
{provider === "openrouter" && t.openrouterDesc}
</p>
</div>
{selectedProvider === provider && (