Add Submit to Antigravity or Codex for Development modal with model picker and deep links
This commit is contained in:
1 parent
b5af03b628
commit
92c5039bcb
6 files changed
+752
-1
No files matched your search
@@ -416,3 +416,288 @@ export async function generateNameSuggestions({
|
||||
};
|
||||
}
|
||||
|
||||
export const DEV_HANDOFF_TARGETS = {
|
||||
antigravity: {
|
||||
id: "antigravity",
|
||||
label: "🛸 Google Antigravity",
|
||||
protocol: "antigravity://",
|
||||
defaultModel: "gemini-3.1-pro",
|
||||
models: [
|
||||
{ id: "gemini-3.1-pro", label: "Gemini 3.1 Pro (Recommended — Full-Stack & Architecture)" },
|
||||
{ id: "gemini-3.0-pro", label: "Gemini 3.0 Pro (Deep Reasoning & Complex Refactors)" },
|
||||
{ id: "gemini-3.1-flash", label: "Gemini 3.1 Flash (Fast Iteration & UI Prototyping)" },
|
||||
{ id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash-Lite (Lightweight & Rapid Tasks)" },
|
||||
{ id: "claude-sonnet-4.5", label: "Claude 4.5 Sonnet (via Antigravity Model Selector)" },
|
||||
{ id: "claude-opus-4.5", label: "Claude 4.5 Opus (via Antigravity Model Selector)" }
|
||||
]
|
||||
},
|
||||
codex: {
|
||||
id: "codex",
|
||||
label: "🤖 OpenAI Codex",
|
||||
protocol: "codex://threads/new",
|
||||
defaultModel: "gpt-5.6-sol",
|
||||
models: [
|
||||
{ id: "gpt-5.6-sol", label: "GPT-5.6-Sol (Recommended — Active Codex Default)" },
|
||||
{ id: "gpt-5.3-codex", label: "GPT-5.3-Codex (Specialized Agentic Coding)" },
|
||||
{ id: "gpt-5.4", label: "GPT-5.4 (Flagship Reasoning & Implementation)" },
|
||||
{ id: "o3", label: "OpenAI o3 (Deep Architecture & Debugging)" },
|
||||
{ id: "codex-mini-latest", label: "Codex Mini (Fast Lightweight Execution)" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export function slugifyIdeaTitle(title, id = "") {
|
||||
const base = String(title || "spark")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 36);
|
||||
return base || `spark-${id || "app"}`;
|
||||
}
|
||||
|
||||
export function buildDevelopmentSpecMarkdown({
|
||||
idea,
|
||||
targetApp = "antigravity",
|
||||
model = "",
|
||||
includeHomelab = true,
|
||||
extraInstructions = "",
|
||||
publicBaseUrl = "https://sparkvault.vloser.us",
|
||||
userBackground = DEFAULT_USER_BACKGROUND
|
||||
}) {
|
||||
const targetMeta = DEV_HANDOFF_TARGETS[targetApp] || DEV_HANDOFF_TARGETS.antigravity;
|
||||
const chosenModel = model || targetMeta.defaultModel;
|
||||
const slug = slugifyIdeaTitle(idea.title, idea.id);
|
||||
|
||||
const lines = [
|
||||
`# 🚀 Development Handoff Specification: ${idea.title}`,
|
||||
``,
|
||||
`- **Target Environment**: ${targetMeta.label}`,
|
||||
`- **Selected AI Model**: \`${chosenModel}\``,
|
||||
`- **Spark ID**: #${idea.id} (\`${slug}\`)`,
|
||||
`- **Category**: \`${idea.category}\` | **Current Stage**: \`${idea.status}\` | **Priority Score**: ${idea.composite_score}/100`,
|
||||
`- **Ratings**: 🔥 Excitement ${idea.rating_excitement}/5 • 🎯 Impact ${idea.rating_impact}/5 • ⚡ Ease ${idea.rating_ease}/5`,
|
||||
`- **Tags**: ${(idea.tags || []).map((t) => `#${t}`).join(", ") || "None"}`,
|
||||
``
|
||||
];
|
||||
|
||||
if (idea.elevator_pitch) {
|
||||
lines.push(`## 💡 Elevator Pitch`, idea.elevator_pitch, ``);
|
||||
}
|
||||
|
||||
if (idea.notes_markdown) {
|
||||
lines.push(`## 📝 Detailed Specs, Brainstorm & Notes`, idea.notes_markdown, ``);
|
||||
}
|
||||
|
||||
if (idea.checklist && idea.checklist.length > 0) {
|
||||
lines.push(
|
||||
`## ✅ Action Checklist & Requirements`,
|
||||
...idea.checklist.map((c) => `- [${c.done ? "x" : " "}] ${c.text}`),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if ((idea.pros && idea.pros.length > 0) || (idea.cons && idea.cons.length > 0)) {
|
||||
lines.push(`## ⚖️ Pros, Value Props & Constraints`);
|
||||
if (idea.pros?.length) {
|
||||
lines.push(`### Why It Works (Pros)`, ...idea.pros.map((p) => `- ✅ ${p}`), ``);
|
||||
}
|
||||
if (idea.cons?.length) {
|
||||
lines.push(`### Risks & Constraints (Cons)`, ...idea.cons.map((c) => `- ⚠️ ${c}`), ``);
|
||||
}
|
||||
}
|
||||
|
||||
if (idea.whack_history && idea.whack_history.length > 0) {
|
||||
lines.push(
|
||||
`## 🃏 Creative Direction & Lateral Twists`,
|
||||
...idea.whack_history.map(
|
||||
(w) => `- **Card #${w.cardNumber} (${w.cardTitle})**: ${w.reflection}`
|
||||
),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (idea.mockups && idea.mockups.length > 0) {
|
||||
lines.push(
|
||||
`## 🖼️ Visual Mockups & Design Assets`,
|
||||
...idea.mockups.map((m) => {
|
||||
const fullUrl = m.imageUrl?.startsWith("http")
|
||||
? m.imageUrl
|
||||
: `${publicBaseUrl}${m.imageUrl}`;
|
||||
return `- **${m.title}** (\`${m.template || "mockup"}\`, generated via \`${m.modelUsed || "AI"}\`): ${fullUrl}${m.prompt ? `\n - Prompt: *${m.prompt}*` : ""}`;
|
||||
}),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (idea.attachments && idea.attachments.length > 0) {
|
||||
lines.push(
|
||||
`## 📎 Attached Reference Notes & Images`,
|
||||
...idea.attachments.map((a) => {
|
||||
const imgPart = a.imageUrl
|
||||
? ` — Image: ${a.imageUrl.startsWith("http") ? a.imageUrl : `${publicBaseUrl}${a.imageUrl}`}`
|
||||
: "";
|
||||
const srcPart = a.sourceUrl ? ` (Source: ${a.sourceUrl})` : "";
|
||||
const txtPart = a.textContent ? `\n > ${a.textContent.replace(/\n/g, "\n > ")}` : "";
|
||||
return `- **[${a.type.toUpperCase()}] ${a.title}**${imgPart}${srcPart}${txtPart}`;
|
||||
}),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (idea.links && idea.links.length > 0) {
|
||||
lines.push(
|
||||
`## 🔗 Reference Links`,
|
||||
...idea.links.map((l) => `- [${l.title || l.url}](${l.url})`),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (userBackground) {
|
||||
lines.push(`## 👤 Creator Background & Preferences`, userBackground, ``);
|
||||
}
|
||||
|
||||
if (includeHomelab) {
|
||||
lines.push(
|
||||
`## 🛠️ Target Architecture & Homelab Deployment Requirements`,
|
||||
`1. Build a clean, self-contained full-stack web application (or service) for **${idea.title}** (\`${slug}\`).`,
|
||||
`2. Create/push a Gitea repository at \`https://gitea.vloser.us/chrisf/${slug}\`.`,
|
||||
`3. Deploy via Docker Compose on \`vloser\` (\`/home/chrisf/docker/${slug}\`) with persistent storage under \`/home/chrisf/docker/appdata/${slug}\`.`,
|
||||
`4. Expose via Traefik dynamic config at \`https://${slug}.vloser.us\` and add an entry with a custom SVG icon to Homepage (\`/home/chrisf/docker/appdata/homepage/services.yaml\`).`,
|
||||
`5. Include mobile-responsive UI styling and automated verification tests.`,
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (extraInstructions && extraInstructions.trim()) {
|
||||
lines.push(`## 🎯 Additional Development Instructions`, extraInstructions.trim(), ``);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function executeDevelopmentHandoff({
|
||||
idea,
|
||||
targetApp = "antigravity",
|
||||
model = "",
|
||||
customPrompt = "",
|
||||
includeHomelab = true,
|
||||
extraInstructions = "",
|
||||
executeServerCodex = false,
|
||||
publicBaseUrl = "https://sparkvault.vloser.us",
|
||||
userBackground = DEFAULT_USER_BACKGROUND,
|
||||
workspacesRootDir
|
||||
}) {
|
||||
const targetMeta = DEV_HANDOFF_TARGETS[targetApp] || DEV_HANDOFF_TARGETS.antigravity;
|
||||
const chosenModel = model || targetMeta.defaultModel;
|
||||
const slug = slugifyIdeaTitle(idea.title, idea.id);
|
||||
|
||||
const compiledSpec =
|
||||
customPrompt && customPrompt.trim()
|
||||
? customPrompt.trim()
|
||||
: buildDevelopmentSpecMarkdown({
|
||||
idea,
|
||||
targetApp,
|
||||
model: chosenModel,
|
||||
includeHomelab,
|
||||
extraInstructions,
|
||||
publicBaseUrl,
|
||||
userBackground
|
||||
});
|
||||
|
||||
let workspaceDir = "";
|
||||
if (workspacesRootDir) {
|
||||
workspaceDir = path.join(workspacesRootDir, `${idea.id}-${slug}`);
|
||||
fs.mkdirSync(workspaceDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(workspaceDir, "SPEC.md"), compiledSpec, "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(workspaceDir, "spark-context.json"),
|
||||
JSON.stringify({ idea, targetApp, model: chosenModel, createdAt: new Date().toISOString() }, null, 2),
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
const specUrl = `${publicBaseUrl}/api/ideas/${idea.id}/spec.md?target=${encodeURIComponent(targetApp)}&model=${encodeURIComponent(chosenModel)}`;
|
||||
|
||||
// Build OS Deep Link URL for the corresponding desktop application
|
||||
let deepLinkUrl = "";
|
||||
let cliCommand = "";
|
||||
|
||||
if (targetApp === "codex") {
|
||||
// Keep URL prompt within safe Windows protocol command-line limits (~1600 chars)
|
||||
const compactPrompt =
|
||||
compiledSpec.length <= 1500
|
||||
? compiledSpec
|
||||
: `${compiledSpec.slice(0, 1350)}\n\n[Full specification copied to clipboard & hosted at: ${specUrl}]`;
|
||||
const codexUrl = new URL("codex://threads/new");
|
||||
codexUrl.searchParams.set("mode", "codex");
|
||||
codexUrl.searchParams.set("prompt", compactPrompt);
|
||||
deepLinkUrl = codexUrl.toString();
|
||||
cliCommand = `codex -m ${chosenModel} "Read SPEC.md and implement ${idea.title}"`;
|
||||
} else {
|
||||
// Antigravity desktop deep link + CLI command
|
||||
deepLinkUrl = `antigravity://new?model=${encodeURIComponent(chosenModel)}&sparkId=${idea.id}`;
|
||||
cliCommand = `agy --model ${chosenModel}`;
|
||||
}
|
||||
|
||||
let codexExecution = null;
|
||||
if (targetApp === "codex" && executeServerCodex && isCodexConfigured()) {
|
||||
try {
|
||||
const reportFile = workspaceDir
|
||||
? path.join(workspaceDir, "CODEX_PLAN.md")
|
||||
: path.join(os.tmpdir(), `codex-plan-${idea.id}-${Date.now()}.md`);
|
||||
const args = [
|
||||
"exec",
|
||||
"-m",
|
||||
chosenModel,
|
||||
"--skip-git-repo-check",
|
||||
"-o",
|
||||
reportFile,
|
||||
`You are an autonomous senior engineer. Review the following SparkVault project specification and produce a complete implementation blueprint, file structure, and starter code plan:\n\n${compiledSpec}`
|
||||
];
|
||||
if (workspaceDir) {
|
||||
args.splice(4, 0, "-C", workspaceDir);
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn("codex", args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: { ...process.env }
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("close", () => resolve());
|
||||
});
|
||||
|
||||
if (fs.existsSync(reportFile)) {
|
||||
const planText = fs.readFileSync(reportFile, "utf8").trim();
|
||||
if (planText) {
|
||||
codexExecution = {
|
||||
ran: true,
|
||||
model: chosenModel,
|
||||
output: planText.slice(0, 6000)
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
codexExecution = {
|
||||
ran: false,
|
||||
error: err.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
targetApp,
|
||||
targetLabel: targetMeta.label,
|
||||
model: chosenModel,
|
||||
slug,
|
||||
specMarkdown: compiledSpec,
|
||||
specUrl,
|
||||
deepLinkUrl,
|
||||
cliCommand,
|
||||
workspaceDir,
|
||||
codexExecution,
|
||||
submittedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user