Skip to content

Commit 406899a

Browse files
cevianclaude
andcommitted
fix terminal sizing, add welcome prompt with session resume, improve dev UI
Defer PTY spawn until the first client sends pty-resize with actual terminal dimensions, fixing line overflow on initial load. Move welcome prompt into pty.ts with auto-resume for existing sessions. Improve empty-state UX in canvas/dashboard. Restructure Dockerfile for better layer caching by installing template deps before runcrayon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 553d740 commit 406899a

File tree

11 files changed

+212
-65
lines changed

11 files changed

+212
-65
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { DashboardPage } from "./components/DashboardPage";
1818

1919
export function App() {
2020
const { state, connected, sendMessage, ptyEvents } = useDAGSocket();
21-
const terminal = useTerminal({ sendMessage, ptyEvents });
21+
const terminal = useTerminal({ sendMessage, ptyEvents, connected });
2222
const connectionsApi = useConnections();
2323
const router = useHashRouter();
2424
const sidebar = useSidebarState();
@@ -209,8 +209,16 @@ export function App() {
209209
</div>
210210
</ReactFlowProvider>
211211
) : (
212-
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
213-
Waiting for workflow files...
212+
<div className="flex flex-col items-center justify-center h-full text-muted-foreground text-sm gap-4">
213+
<p className="font-medium">Create a workflow with Claude Code</p>
214+
<ul className="list-none space-y-1.5 text-xs text-center">
215+
<li>"Create workflow to qualify inbound leads and route to sales reps"</li>
216+
<li>"Create workflow to track competitor pricing and alert on changes"</li>
217+
<li>"Create workflow to generate weekly pipeline reports from CRM data"</li>
218+
</ul>
219+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="mt-2 animate-bounce">
220+
<polyline points="6 9 12 15 18 9" />
221+
</svg>
214222
</div>
215223
)}
216224

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function DashboardPage({ workflows, parseErrors, onSelectWorkflow }: Dash
6969
</div>
7070
<p className="text-[15px] text-[#1a1a1a] font-medium">No workflows yet</p>
7171
<p className="text-[13px] text-[#a8a099] mt-1.5 max-w-sm">
72-
Create a workflow file in your project to get started. Use Claude Code to generate one.
72+
Go to the <a href="#/canvas" className="underline text-[#1a1a1a] hover:text-[#000]">Canvas</a> tab to create workflows.
7373
</p>
7474
</div>
7575
) : (

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function WorkflowSelector({
2020
</h2>
2121

2222
{workflows.length === 0 && parseErrors.length === 0 && (
23-
<p className="text-xs text-[#a8a099] italic">No workflows found</p>
23+
<p className="text-xs text-[#a8a099] italic">No workflows yet — create one with Claude Code</p>
2424
)}
2525

2626
{workflows.map((w) => (

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { WebLinksAddon } from "@xterm/addon-web-links";
66
export interface UseTerminalOptions {
77
sendMessage: (msg: object) => void;
88
ptyEvents: EventTarget;
9+
connected: boolean;
910
}
1011

1112
function createTerminal(sendRef: React.MutableRefObject<(msg: object) => void>) {
@@ -53,7 +54,7 @@ function createTerminal(sendRef: React.MutableRefObject<(msg: object) => void>)
5354
return { term, fitAddon };
5455
}
5556

56-
export function useTerminal({ sendMessage, ptyEvents }: UseTerminalOptions) {
57+
export function useTerminal({ sendMessage, ptyEvents, connected }: UseTerminalOptions) {
5758
const [ptyAlive, setPtyAlive] = useState(false);
5859
const [hasData, setHasData] = useState(false);
5960

@@ -117,6 +118,18 @@ export function useTerminal({ sendMessage, ptyEvents }: UseTerminalOptions) {
117118
};
118119
}, [ptyEvents, term]);
119120

121+
// When WebSocket connects (or reconnects), ensure PTY is spawned with correct size.
122+
// The initial pty-resize from attachTo may have been lost if the WS wasn't open yet.
123+
useEffect(() => {
124+
if (connected && !ptyAlive && openedRef.current) {
125+
fitAddon.fit();
126+
sendRef.current({
127+
type: "pty-resize",
128+
data: { cols: term.cols, rows: term.rows },
129+
});
130+
}
131+
}, [connected, ptyAlive, term, fitAddon]);
132+
120133
const attachTo = useCallback(
121134
(container: HTMLDivElement | null) => {
122135
if (!container) return;

packages/core/docker/Dockerfile.cloud-dev

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,39 @@ RUN apt-get update && apt-get install -y python3 make g++ git curl openssh-serve
1111
# This means users can run `npm install` without extra flags and still hit the cache.
1212
RUN npm config set cache /npm-cache --global
1313

14+
# Install Claude Code CLI early so this layer is cached independently of crayon changes.
15+
# The _setup user is reused later by `crayon install` to register the plugin.
16+
RUN groupadd -f devs && \
17+
useradd -m -s /bin/bash -g devs _setup && \
18+
su -s /bin/bash _setup -c "curl -fsSL https://claude.ai/install.sh | bash" && \
19+
ln -sf /home/_setup/.local/bin/claude /usr/local/bin/claude
20+
21+
# Pre-install app template node_modules at /node_modules so Node.js finds them
22+
# via standard parent-directory resolution from /data/app (no NODE_PATH needed).
23+
# The template package.json has Handlebars placeholders (e.g. {{crayon_version}})
24+
# that must be resolved before npm install can parse it.
25+
#
26+
# Step 1: Get the template package.json into /tmp/cache-build/.
27+
# For local builds, build-dev.sh copies it into the build context.
28+
# For npm builds, download the tarball and extract just the template.
29+
COPY entrypoint.sh template-package.json* /tmp/
30+
RUN mkdir -p /tmp/cache-build && \
31+
if [ "$CRAYON_SOURCE" = "local" ]; then \
32+
mv /tmp/template-package.json /tmp/cache-build/package.json; \
33+
else \
34+
npm pack runcrayon@dev --pack-destination /tmp && \
35+
tar -xzf /tmp/runcrayon-*.tgz -C /tmp/cache-build --strip-components=3 package/templates/app/package.json && \
36+
rm -f /tmp/runcrayon-*.tgz; \
37+
fi
38+
39+
# Step 2: Install template deps. This layer caches when deps don't change.
40+
RUN cd /tmp/cache-build && \
41+
sed -i 's/{{crayon_version}}/dev/g' package.json && \
42+
sed -i 's/{{app_name}}/cache-app/g' package.json && \
43+
npm install && \
44+
mv node_modules / && \
45+
rm -rf /tmp/cache-build
46+
1447
# runcrayon — install from local tarball or npm depending on build arg.
1548
# In dev mode, run: npm pack --pack-destination docker/ (from packages/core)
1649
# The wildcard COPY uses a dot file as fallback so it never fails when no .tgz exists.
@@ -23,36 +56,19 @@ RUN if [ "$CRAYON_SOURCE" = "local" ] && ls /tmp/build/runcrayon-*.tgz 1>/dev/nu
2356
else \
2457
npm install -g runcrayon@dev; \
2558
fi && \
26-
rm -rf /tmp/build
27-
28-
# Pre-install app template node_modules at /node_modules so Node.js finds them
29-
# via standard parent-directory resolution from /data/app (no NODE_PATH needed).
30-
# The template package.json has Handlebars placeholders (e.g. {{crayon_version}})
31-
# that must be resolved before npm install can parse it.
32-
RUN GLOBAL_MODULES="$(npm root -g)" && \
33-
TEMPLATE_DIR="$GLOBAL_MODULES/runcrayon/templates/app" && \
34-
mkdir -p /tmp/cache-build && cp "$TEMPLATE_DIR/package.json" /tmp/cache-build/ && \
35-
cd /tmp/cache-build && sed -i 's/{{crayon_version}}/dev/g' package.json && \
36-
sed -i 's/{{app_name}}/cache-app/g' package.json && \
37-
npm install && \
38-
mv node_modules / && \
39-
rm -rf /tmp/cache-build && \
59+
rm -rf /tmp/build && \
4060
rm -rf /node_modules/runcrayon && \
41-
ln -s "$GLOBAL_MODULES/runcrayon" /node_modules/runcrayon
61+
ln -s "$(npm root -g)/runcrayon" /node_modules/runcrayon
4262

4363
# /node_modules found by Node.js via parent-directory resolution from /data/app.
4464
# /npm-cache is pre-populated during build; world-writable so DEV_USER can use it.
4565
RUN chmod -R a+rwx /npm-cache
4666
ENV PATH=/node_modules/.bin:$PATH
4767

48-
# Pre-run claude install + crayon install as a temp user, then save to /etc/skel.
68+
# Register crayon plugin with the _setup user created earlier, then bake into /etc/skel.
4969
# useradd -m copies /etc/skel into new home dirs, so runtime users get Claude Code
5070
# + plugin/marketplace registration without the slow setup on every boot.
51-
RUN groupadd -f devs && \
52-
useradd -m -s /bin/bash -g devs _setup && \
53-
su -s /bin/bash _setup -c "curl -fsSL https://claude.ai/install.sh | bash" && \
54-
ln -sf /home/_setup/.local/bin/claude /usr/local/bin/claude && \
55-
CRAYON="$(npm prefix -g)/bin/crayon" && \
71+
RUN CRAYON="$(npm prefix -g)/bin/crayon" && \
5672
su -s /bin/bash _setup -c "$CRAYON install --force" && \
5773
test -f /home/_setup/.claude/settings.json || { echo "ERROR: crayon plugin not registered"; exit 1; } && \
5874
cp -a /home/_setup/. /etc/skel/ && \

packages/core/docker/build-dev.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ echo "==> Packing tarball..."
3131
cd "$CORE_DIR"
3232
rm -f docker/runcrayon-*.tgz
3333
npm pack --pack-destination docker/
34+
cp templates/app/package.json docker/template-package.json
3435

3536
echo "==> Building and pushing Docker image (tag: $TAG)..."
3637
cd "$SCRIPT_DIR"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "{{app_name}}",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"preinstall": "[ ! -f /tmp/node_modules.lock ] || flock /tmp/node_modules.lock true",
8+
"postinstall": "rm -rf node_modules/runcrayon",
9+
"prebuild": "crayon build",
10+
"build": "next build",
11+
"check": "biome check .",
12+
"check:unsafe": "biome check --write --unsafe .",
13+
"check:write": "biome check --write .",
14+
"db:generate": "drizzle-kit generate",
15+
"db:migrate": "drizzle-kit migrate",
16+
"db:push": "drizzle-kit push",
17+
"db:studio": "drizzle-kit studio",
18+
"predev": "crayon build",
19+
"dev": "next dev --turbo",
20+
"crayon": "crayon",
21+
"preview": "next build && next start",
22+
"start": "next start",
23+
"typecheck": "tsc --noEmit"
24+
},
25+
"dependencies": {
26+
"runcrayon": "{{crayon_version}}",
27+
"@nangohq/node": "^0.45.0",
28+
"@dbos-inc/otel": "latest",
29+
"@t3-oss/env-nextjs": "^0.13.10",
30+
"@tanstack/react-query": "^5.90.19",
31+
"@trpc/client": "^11.8.1",
32+
"@trpc/react-query": "^11.8.1",
33+
"@trpc/server": "^11.8.1",
34+
"drizzle-orm": "^0.41.0",
35+
"next": "^16.1.4",
36+
"postgres": "^3.4.8",
37+
"react": "^19.2.3",
38+
"react-dom": "^19.2.3",
39+
"server-only": "^0.0.1",
40+
"superjson": "^2.2.6",
41+
"zod": "^4.3.5"
42+
},
43+
"devDependencies": {
44+
"@biomejs/biome": "^2.3.11",
45+
"@tailwindcss/postcss": "^4.1.18",
46+
"@types/node": "^25.0.9",
47+
"@types/react": "^19.2.9",
48+
"@types/react-dom": "^19.2.3",
49+
"drizzle-kit": "^0.31.8",
50+
"postcss": "^8.5.6",
51+
"tailwindcss": "^4.1.18",
52+
"tsx": "^4.21.0",
53+
"typescript": "^5.9.3"
54+
},
55+
"ct3aMetadata": {
56+
"initVersion": "7.40.0"
57+
},
58+
"packageManager": "npm@11.7.0"
59+
}

packages/core/src/cli/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -602,9 +602,8 @@ program
602602
.option("-p, --port <number>", "Port to serve on", "4173")
603603
.option("--host", "Expose to network")
604604
.option("--dangerously-skip-permissions", "Pass --dangerously-skip-permissions to Claude Code")
605-
.option("--claude-prompt <text>", "Initial prompt to send to Claude Code")
606605
.option("--verbose", "Show detailed startup information")
607-
.action(async (options: { port: string; host?: boolean; dangerouslySkipPermissions?: boolean; claudePrompt?: string; verbose?: boolean }) => {
606+
.action(async (options: { port: string; host?: boolean; dangerouslySkipPermissions?: boolean; verbose?: boolean }) => {
608607
// Load .env for DATABASE_URL and NANGO_SECRET_KEY
609608
try {
610609
resolveEnv();
@@ -627,7 +626,6 @@ program
627626
nangoSecretKey: process.env.NANGO_SECRET_KEY,
628627
claudePluginDir: pluginDir,
629628
claudeSkipPermissions: options.dangerouslySkipPermissions,
630-
claudePrompt: options.claudePrompt,
631629
verbose: options.verbose,
632630
});
633631
});

packages/core/src/cli/run.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,6 @@ function discoverProjects(): ProjectInfo[] {
207207
return projects;
208208
}
209209

210-
const WELCOME_PROMPT =
211-
"Welcome to your crayon project! What workflow would you like to create? Here are some ideas:\n\n" +
212-
'- "Enrich leads from a CSV file with company data"\n' +
213-
'- "Monitor website uptime and send Slack alerts"\n' +
214-
'- "Sync Salesforce contacts to our database nightly"\n' +
215-
'- "Score and route inbound leads based on firmographics"\n\n' +
216-
"Describe what you'd like to automate and I'll help you build it with /create-workflow.";
217-
218210
async function launchExistingProject(projectPath: string): Promise<void> {
219211
// Check if database is paused and start it if needed
220212
try {
@@ -284,7 +276,6 @@ async function launchDevServer(cwd: string, { yolo = false }: { yolo?: boolean }
284276
nangoSecretKey: process.env.NANGO_SECRET_KEY,
285277
claudePluginDir: pluginDir,
286278
claudeSkipPermissions: yolo,
287-
claudePrompt: WELCOME_PROMPT,
288279
});
289280

290281
// Open browser

packages/core/src/dev-ui/dev-server.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export interface DevServerOptions {
5353
nangoSecretKey?: string;
5454
claudePluginDir?: string;
5555
claudeSkipPermissions?: boolean;
56-
claudePrompt?: string;
5756
}
5857

5958
export async function startDevServer(options: DevServerOptions) {
@@ -156,16 +155,27 @@ export async function startDevServer(options: DevServerOptions) {
156155
let ptyManager: PtyManager | null = null;
157156

158157
const onClientMessage = (_ws: import("ws").WebSocket, msg: WSClientMessage) => {
159-
if (!ptyManager) return;
158+
if (!ptyManager) {
159+
console.log(`[dev-server] Ignoring ${msg.type}: ptyManager is null (node-pty not available)`);
160+
return;
161+
}
160162
switch (msg.type) {
161163
case "pty-input":
162164
ptyManager.write(msg.data);
163165
break;
164166
case "pty-resize":
165-
ptyManager.resize(msg.data.cols, msg.data.rows);
167+
if (!ptyManager.isAlive()) {
168+
// First resize from client — spawn PTY with correct dimensions
169+
console.log(`[dev-server] First pty-resize, spawning PTY (${msg.data.cols}x${msg.data.rows})`);
170+
const pid = ptyManager.spawn(msg.data.cols, msg.data.rows);
171+
broadcast({ type: "pty-spawned", data: { pid } });
172+
} else {
173+
ptyManager.resize(msg.data.cols, msg.data.rows);
174+
}
166175
break;
167176
case "pty-spawn":
168177
if (!ptyManager.isAlive()) {
178+
console.log("[dev-server] pty-spawn received, spawning PTY");
169179
const pid = ptyManager.spawn();
170180
broadcast({ type: "pty-spawned", data: { pid } });
171181
}
@@ -183,12 +193,12 @@ export async function startDevServer(options: DevServerOptions) {
183193
claudeArgs: [
184194
...(options.claudePluginDir ? ["--plugin-dir", options.claudePluginDir] : []),
185195
...(options.claudeSkipPermissions ? ["--dangerously-skip-permissions"] : []),
186-
...(options.claudePrompt ? ["--", options.claudePrompt] : []),
187196
],
188197
onData: (data) => broadcast({ type: "pty-data", data }),
189198
onExit: (code) => broadcast({ type: "pty-exit", data: { code } }),
190199
});
191-
} catch {
200+
} catch (err) {
201+
console.log(`[dev-server] Failed to load node-pty: ${err}`);
192202
if (!options.quiet) {
193203
console.log(" Terminal: unavailable (node-pty not installed)\n");
194204
}
@@ -200,20 +210,7 @@ export async function startDevServer(options: DevServerOptions) {
200210
onMessage: (msg) => broadcast(msg),
201211
});
202212

203-
// Auto-spawn Claude Code PTY
204-
if (ptyManager) {
205-
try {
206-
const pid = ptyManager.spawn();
207-
if (options.verbose) {
208-
console.log(` Terminal: Claude Code running (PID ${pid})\n`);
209-
}
210-
} catch (err) {
211-
if (!options.quiet) {
212-
console.log(` Terminal: failed to spawn claude (${err instanceof Error ? err.message : err})\n`);
213-
}
214-
ptyManager = null;
215-
}
216-
}
213+
// PTY spawns on first client pty-resize so it starts with correct dimensions
217214

218215
// On new WS connection, wait for initial scan then send full state
219216
wss.on("connection", async (ws) => {

0 commit comments

Comments
 (0)