Skip to content

Commit 54b4c25

Browse files
cevianclaude
andcommitted
feat: launch dev UI directly from init, add terminal spinner, remove start_dev_ui tool
Replace subprocess spawn with direct startDevServer() call in init wizard, auto-open browser on launch, add loading spinner to terminal, add --verbose flag to dev server, pass initial prompt to Claude via PTY args, and remove the start_dev_ui MCP tool since the dev UI is now launched by init/dev CLI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b9bffa4 commit 54b4c25

File tree

11 files changed

+131
-225
lines changed

11 files changed

+131
-225
lines changed

packages/core/dev-ui/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function App() {
4040
attachTo={terminal.attachTo}
4141
fit={terminal.fit}
4242
ptyAlive={terminal.ptyAlive}
43+
hasData={terminal.hasData}
4344
restart={terminal.restart}
4445
/>
4546
),

packages/core/dev-ui/src/components/ClaudeTerminal.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ interface ClaudeTerminalProps {
55
attachTo: (el: HTMLDivElement | null) => void;
66
fit: () => void;
77
ptyAlive: boolean;
8+
hasData: boolean;
89
restart: () => void;
910
}
1011

11-
export function ClaudeTerminal({ attachTo, fit, ptyAlive, restart }: ClaudeTerminalProps) {
12+
export function ClaudeTerminal({ attachTo, fit, ptyAlive, hasData, restart }: ClaudeTerminalProps) {
1213
const containerRef = useRef<HTMLDivElement>(null);
1314

1415
// Attach terminal to the container div
@@ -52,8 +53,19 @@ export function ClaudeTerminal({ attachTo, fit, ptyAlive, restart }: ClaudeTermi
5253
}, [ptyAlive, restart]);
5354

5455
return (
55-
<div className="h-full w-full overflow-hidden bg-[#1a1a1a] px-4 py-2">
56+
<div className="relative h-full w-full overflow-hidden bg-[#1a1a1a] px-4 py-2">
5657
<div ref={containerRef} className="h-full w-full" />
58+
{!hasData && (
59+
<div className="absolute inset-0 flex items-center justify-center">
60+
<div className="flex items-center gap-3 text-zinc-500">
61+
<svg className="h-5 w-5 animate-spin" viewBox="0 0 24 24" fill="none">
62+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" />
63+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
64+
</svg>
65+
<span>Starting Claude Code...</span>
66+
</div>
67+
</div>
68+
)}
5769
</div>
5870
);
5971
}

packages/core/dev-ui/src/hooks/useTerminal.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ function createTerminal(sendRef: React.MutableRefObject<(msg: object) => void>)
5555

5656
export function useTerminal({ sendMessage, ptyEvents }: UseTerminalOptions) {
5757
const [ptyAlive, setPtyAlive] = useState(false);
58+
const [hasData, setHasData] = useState(false);
5859

5960
// Stable ref for sendMessage so Terminal.onData doesn't recreate
6061
const sendRef = useRef(sendMessage);
@@ -88,6 +89,7 @@ export function useTerminal({ sendMessage, ptyEvents }: UseTerminalOptions) {
8889
const data = (e as CustomEvent).detail as string;
8990
if (openedRef.current) {
9091
term.write(data);
92+
setHasData(true);
9193
} else {
9294
bufferRef.current.push(data);
9395
}
@@ -125,10 +127,13 @@ export function useTerminal({ sendMessage, ptyEvents }: UseTerminalOptions) {
125127
openedRef.current = true;
126128

127129
// Flush any data buffered before the terminal was opened
128-
for (const chunk of bufferRef.current) {
129-
term.write(chunk);
130+
if (bufferRef.current.length > 0) {
131+
for (const chunk of bufferRef.current) {
132+
term.write(chunk);
133+
}
134+
bufferRef.current = [];
135+
setHasData(true);
130136
}
131-
bufferRef.current = [];
132137
} else if (term.element) {
133138
// Re-attach: move existing xterm DOM into the new container
134139
container.appendChild(term.element);
@@ -157,5 +162,5 @@ export function useTerminal({ sendMessage, ptyEvents }: UseTerminalOptions) {
157162
sendRef.current({ type: "pty-spawn" });
158163
}, [term]);
159164

160-
return { attachTo, fit, ptyAlive, restart };
165+
return { attachTo, fit, ptyAlive, hasData, restart };
161166
}

packages/core/src/cli/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,9 @@ program
508508
.option("-p, --port <number>", "Port to serve on", "4173")
509509
.option("--host", "Expose to network")
510510
.option("--dangerously-skip-permissions", "Pass --dangerously-skip-permissions to Claude Code")
511-
.action(async (options: { port: string; host?: boolean; dangerouslySkipPermissions?: boolean }) => {
511+
.option("--claude-prompt <text>", "Initial prompt to send to Claude Code")
512+
.option("--verbose", "Show detailed startup information")
513+
.action(async (options: { port: string; host?: boolean; dangerouslySkipPermissions?: boolean; claudePrompt?: string; verbose?: boolean }) => {
512514
// Load .env for DATABASE_URL and NANGO_SECRET_KEY
513515
try {
514516
resolveEnv();
@@ -531,6 +533,8 @@ program
531533
nangoSecretKey: process.env.NANGO_SECRET_KEY,
532534
claudePluginDir: pluginDir,
533535
claudeSkipPermissions: options.dangerouslySkipPermissions,
536+
claudePrompt: options.claudePrompt,
537+
verbose: options.verbose,
534538
});
535539
});
536540

packages/core/src/cli/init.ts

Lines changed: 70 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { execSync, spawn } from "node:child_process";
1+
import { execSync } from "node:child_process";
22
import { existsSync, readFileSync, readdirSync } from "node:fs";
33
import { basename, join, resolve } from "node:path";
44
import * as p from "@clack/prompts";
@@ -8,14 +8,14 @@ import {
88
createDatabase,
99
setupAppSchema,
1010
} from "./mcp/lib/scaffolding.js";
11-
import { packageRoot } from "./mcp/config.js";
1211

13-
// Monorepo root (only valid in dev mode)
14-
const monorepoRoot = join(packageRoot, "..", "..");
15-
16-
function isDevMode(): boolean {
17-
const corePath = join(monorepoRoot, "packages", "core");
18-
return existsSync(corePath);
12+
function isClaudeAvailable(): boolean {
13+
try {
14+
execSync("claude --version", { stdio: "ignore" });
15+
return true;
16+
} catch {
17+
return false;
18+
}
1919
}
2020

2121
function isCwdEmpty(): boolean {
@@ -28,15 +28,6 @@ function isCwdEmpty(): boolean {
2828
}
2929
}
3030

31-
function isClaudeAvailable(): boolean {
32-
try {
33-
execSync("claude --version", { stdio: "ignore" });
34-
return true;
35-
} catch {
36-
return false;
37-
}
38-
}
39-
4031
/**
4132
* Poll Tiger Cloud until the service is ready (status != "creating").
4233
* Returns true if ready, false on timeout.
@@ -79,37 +70,62 @@ function isExisting0pflow(): boolean {
7970
}
8071
}
8172

82-
function launchClaude(cwd: string, { yolo = false }: { yolo?: boolean } = {}): void {
83-
const prompt =
84-
"Welcome to your 0pflow project! What workflow would you like to create? Here are some ideas:\n\n" +
85-
'- "Enrich leads from a CSV file with company data"\n' +
86-
'- "Monitor website uptime and send Slack alerts"\n' +
87-
'- "Sync Salesforce contacts to our database nightly"\n' +
88-
'- "Score and route inbound leads based on firmographics"\n\n' +
89-
"Describe what you'd like to automate and I'll help you build it with /create-workflow.";
90-
91-
const claudeArgs: string[] = [];
92-
if (isDevMode()) claudeArgs.push("--plugin-dir", monorepoRoot);
93-
if (yolo) claudeArgs.push("--dangerously-skip-permissions");
94-
claudeArgs.push(prompt);
95-
96-
const child = spawn("claude", claudeArgs, {
97-
cwd,
98-
stdio: "inherit",
73+
const WELCOME_PROMPT =
74+
"Welcome to your 0pflow project! What workflow would you like to create? Here are some ideas:\n\n" +
75+
'- "Enrich leads from a CSV file with company data"\n' +
76+
'- "Monitor website uptime and send Slack alerts"\n' +
77+
'- "Sync Salesforce contacts to our database nightly"\n' +
78+
'- "Score and route inbound leads based on firmographics"\n\n' +
79+
"Describe what you'd like to automate and I'll help you build it with /create-workflow.";
80+
81+
async function launchDevServer(cwd: string, { yolo = false }: { yolo?: boolean } = {}): Promise<void> {
82+
// Load .env for DATABASE_URL and NANGO_SECRET_KEY
83+
try {
84+
const { resolveEnv } = await import("./env.js");
85+
resolveEnv();
86+
} catch {
87+
// Dev UI can work without env
88+
}
89+
90+
// Detect dev mode (running from monorepo source) for --plugin-dir
91+
const { packageRoot } = await import("./mcp/config.js");
92+
const monorepoRoot = resolve(packageRoot, "..", "..");
93+
const pluginDir = existsSync(resolve(monorepoRoot, "packages", "core")) ? monorepoRoot : undefined;
94+
95+
const { startDevServer } = await import("../dev-ui/index.js");
96+
const { url } = await startDevServer({
97+
projectRoot: cwd,
98+
databaseUrl: process.env.DATABASE_URL,
99+
nangoSecretKey: process.env.NANGO_SECRET_KEY,
100+
claudePluginDir: pluginDir,
101+
claudeSkipPermissions: yolo,
102+
claudePrompt: WELCOME_PROMPT,
99103
});
100-
child.on("exit", (code) => process.exit(code ?? 0));
104+
105+
// Open browser
106+
try {
107+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
108+
execSync(`${cmd} ${url}`, { stdio: "ignore" });
109+
} catch {
110+
// Non-fatal — user can open manually
111+
}
101112
}
102113

103114
export async function runInit(): Promise<void> {
104115
p.intro(pc.red("0pflow") + pc.dim(" — create a new project"));
105116

117+
if (!isClaudeAvailable()) {
118+
p.log.error("Claude Code CLI not found. Install it from https://claude.ai/code");
119+
process.exit(1);
120+
}
121+
106122
// ── Existing project check ──────────────────────────────────────────
107123
if (isExisting0pflow()) {
108124
const action = await p.select({
109125
message: "This directory is already an 0pflow project.",
110126
options: [
111-
{ value: "claude" as const, label: "Launch Claude Code" },
112-
{ value: "claude-yolo" as const, label: "Launch Claude Code with --dangerously-skip-permissions" },
127+
{ value: "claude" as const, label: "Launch Dev UI" },
128+
{ value: "claude-yolo" as const, label: "Launch Dev UI with --dangerously-skip-permissions" },
113129
{ value: "new" as const, label: "Create a new project in a subdirectory" },
114130
],
115131
});
@@ -120,12 +136,8 @@ export async function runInit(): Promise<void> {
120136
}
121137

122138
if (action === "claude" || action === "claude-yolo") {
123-
if (!isClaudeAvailable()) {
124-
p.log.error("Claude Code CLI not found. Install it from https://claude.ai/code");
125-
process.exit(1);
126-
}
127-
p.outro(pc.green("Launching Claude Code..."));
128-
launchClaude(process.cwd(), { yolo: action === "claude-yolo" });
139+
p.outro(pc.green("Launching Dev UI..."));
140+
await launchDevServer(process.cwd(), { yolo: action === "claude-yolo" });
129141
return;
130142
}
131143
// fall through to normal wizard (cwdEmpty will be false, so directory defaults to ./<name>)
@@ -388,39 +400,28 @@ export async function runInit(): Promise<void> {
388400
}
389401
}
390402

391-
// ── Launch Claude? ──────────────────────────────────────────────────
392-
const hasClaude = isClaudeAvailable();
393-
394-
if (hasClaude) {
395-
const launchChoice = await p.select({
396-
message: "Launch Claude Code to design your first workflow?",
397-
options: [
398-
{ value: "claude" as const, label: "Yes" },
399-
{ value: "claude-yolo" as const, label: "Yes, with --dangerously-skip-permissions" },
400-
{ value: "no" as const, label: "No, I'll do it later" },
401-
],
402-
});
403+
// ── Launch Dev UI? ──────────────────────────────────────────────────
404+
const launchChoice = await p.select({
405+
message: "Launch Dev UI to design your first workflow?",
406+
options: [
407+
{ value: "claude" as const, label: "Yes" },
408+
{ value: "claude-yolo" as const, label: "Yes, with --dangerously-skip-permissions" },
409+
{ value: "no" as const, label: "No, I'll do it later" },
410+
],
411+
});
403412

404-
if (!p.isCancel(launchChoice) && launchChoice !== "no") {
405-
p.outro(pc.green("Launching Claude Code..."));
406-
launchClaude(resolve(appPath), { yolo: launchChoice === "claude-yolo" });
407-
return;
408-
}
413+
if (!p.isCancel(launchChoice) && launchChoice !== "no") {
414+
p.outro(pc.green("Launching Dev UI..."));
415+
await launchDevServer(resolve(appPath), { yolo: launchChoice === "claude-yolo" });
416+
return;
409417
}
410418

411419
// ── Done ────────────────────────────────────────────────────────────
412420
const cdCmd = directory === "." ? "" : `cd ${directory} && `;
413-
const pluginFlag = isDevMode() ? ` --plugin-dir ${monorepoRoot}` : "";
414-
const claudeCmd = `${cdCmd}claude${pluginFlag}`;
415421

416422
p.outro(pc.green("Project created!"));
417423
console.log();
418-
console.log(pc.bold(" To launch Claude Code later:"));
419-
console.log(pc.cyan(` ${claudeCmd}`));
420-
console.log();
421-
console.log(pc.bold(" Example prompts to get started:"));
422-
console.log(pc.dim(' "Create a workflow that enriches leads from a CSV file"'));
423-
console.log(pc.dim(' "Build a workflow that monitors website uptime and sends Slack alerts"'));
424-
console.log(pc.dim(' "Create a data pipeline that syncs Salesforce contacts to our database"'));
424+
console.log(pc.bold(" To launch the Dev UI later:"));
425+
console.log(pc.cyan(` ${cdCmd}0pflow dev`));
425426
console.log();
426427
}

packages/core/src/cli/mcp/tools/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createDatabaseFactory } from "./createDatabase.js";
33
import { setupAppSchemaFactory } from "./setupAppSchema.js";
44
import { listIntegrationsFactory } from "./listIntegrations.js";
55
import { getConnectionInfoFactory } from "./getConnectionInfo.js";
6-
import { startDevUiFactory } from "./startDevUi.js";
6+
77
import { listWorkflowsFactory } from "./listWorkflows.js";
88
import { runWorkflowFactory } from "./runWorkflow.js";
99
import { runNodeFactory } from "./runNode.js";
@@ -18,7 +18,7 @@ export async function getApiFactories() {
1818
setupAppSchemaFactory,
1919
listIntegrationsFactory,
2020
getConnectionInfoFactory,
21-
startDevUiFactory,
21+
2222
listWorkflowsFactory,
2323
runWorkflowFactory,
2424
runNodeFactory,

0 commit comments

Comments
 (0)