From 8ee34b0714eec88e9a33d23e9a31f56f29f7e3d2 Mon Sep 17 00:00:00 2001 From: yu-sugimoto Date: Wed, 4 Mar 2026 13:04:04 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E9=96=8B=E7=99=BA=E3=82=B5?= =?UTF-8?q?=E3=83=BC=E3=83=90=E3=83=BC=E3=81=AE=E4=BE=9D=E5=AD=98=E9=96=A2?= =?UTF-8?q?=E4=BF=82=E3=82=A4=E3=83=B3=E3=82=B9=E3=83=88=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E6=89=8B=E9=A0=86=E3=82=92=E6=98=8E=E7=A2=BA=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/worker-analyze.py | 3 ++- docker/worker-implement.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/worker-analyze.py b/docker/worker-analyze.py index 29dff0f..29fa8d5 100644 --- a/docker/worker-analyze.py +++ b/docker/worker-analyze.py @@ -105,7 +105,7 @@ async def take_before_screenshot(repo_dir: str, screenshot_output: str) -> None: Follow these steps: 1. Investigate how to start the dev server (check package.json scripts, README, etc.) -2. Install dependencies if needed +2. Install the project's dependencies if needed (e.g. `npm install` in the project directory) 3. Start the dev server in the background using Bash (e.g. `npm run dev &` or whatever is appropriate) 4. Wait for the server to become ready (poll with curl until it responds) 5. Once the server is ready, take a screenshot by running: @@ -114,6 +114,7 @@ async def take_before_screenshot(repo_dir: str, screenshot_output: str) -> None: 6. Verify the screenshot file was created at {screenshot_output} IMPORTANT: The screenshot is critical. Do your best to get the dev server running and take the screenshot. +NOTE: Playwright and Chromium are pre-installed globally in this container. Do NOT install playwright or run `npx playwright install`. Just run `node /usr/local/bin/take-screenshot.mjs ` directly. """ current_tool = None diff --git a/docker/worker-implement.py b/docker/worker-implement.py index bb7ffb8..e7efc9f 100644 --- a/docker/worker-implement.py +++ b/docker/worker-implement.py @@ -120,7 +120,7 @@ async def implement_proposal( ## Step 2: Start the dev server and take a screenshot After implementing the changes: 1. Investigate how to start the dev server for this project (check package.json scripts, README, etc.) -2. Install dependencies if needed +2. Install the project's dependencies if needed (e.g. `npm install` in the project directory) 3. Start the dev server in the background using Bash (e.g. `npm run dev &` or whatever is appropriate) 4. Wait for the server to become ready (poll with curl until it responds) 5. Once the server is ready, take a screenshot by running: @@ -129,6 +129,7 @@ async def implement_proposal( 6. Make sure the screenshot file was created at {screenshot_output} IMPORTANT: The screenshot is critical. Do your best to get the dev server running and take the screenshot. +NOTE: Playwright and Chromium are pre-installed globally in this container. Do NOT install playwright or run `npx playwright install`. Just run `node /usr/local/bin/take-screenshot.mjs ` directly. """ current_tool = None From 085f5c7291853aa9e6447f1527e99ea7ae2bdf59 Mon Sep 17 00:00:00 2001 From: yu-sugimoto Date: Wed, 4 Mar 2026 13:37:13 +0900 Subject: [PATCH 2/2] =?UTF-8?q?agent=20sdk=20=E3=82=92=20query=20->=20sdkc?= =?UTF-8?q?lient=20=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/worker-analyze.py | 91 ++++++++++++++--------- docker/worker-implement.py | 107 +++++++++++++++++++++------ frontend/src/components/LogPanel.tsx | 3 +- 3 files changed, 139 insertions(+), 62 deletions(-) diff --git a/docker/worker-analyze.py b/docker/worker-analyze.py index 29fa8d5..3a1bf86 100644 --- a/docker/worker-analyze.py +++ b/docker/worker-analyze.py @@ -14,7 +14,7 @@ import boto3 from botocore.config import Config -from claude_agent_sdk import ClaudeAgentOptions, query, AssistantMessage, TextBlock, ToolUseBlock +from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, query, AssistantMessage, TextBlock, ToolUseBlock from claude_agent_sdk.types import StreamEvent @@ -99,37 +99,12 @@ def s3_upload_text(s3, bucket, key, text, content_type="text/plain"): raise -async def take_before_screenshot(repo_dir: str, screenshot_output: str) -> None: - """Use Claude Agent SDK to start dev server and take a before screenshot.""" - prompt = f"""You need to start the dev server for the web application at {repo_dir} and take a screenshot. - -Follow these steps: -1. Investigate how to start the dev server (check package.json scripts, README, etc.) -2. Install the project's dependencies if needed (e.g. `npm install` in the project directory) -3. Start the dev server in the background using Bash (e.g. `npm run dev &` or whatever is appropriate) -4. Wait for the server to become ready (poll with curl until it responds) -5. Once the server is ready, take a screenshot by running: - `node /usr/local/bin/take-screenshot.mjs {screenshot_output}` - where is the URL the dev server is listening on (e.g. http://localhost:5173) -6. Verify the screenshot file was created at {screenshot_output} - -IMPORTANT: The screenshot is critical. Do your best to get the dev server running and take the screenshot. -NOTE: Playwright and Chromium are pre-installed globally in this container. Do NOT install playwright or run `npx playwright install`. Just run `node /usr/local/bin/take-screenshot.mjs ` directly. -""" - +async def _process_messages(client: ClaudeSDKClient, phase: str) -> None: + """Process messages from ClaudeSDKClient, emitting logs for a given phase.""" current_tool = None tool_input_chunks = "" - async for msg in query( - prompt=prompt, - options=ClaudeAgentOptions( - allowed_tools=["Read", "Bash", "Glob", "Grep"], - cwd=repo_dir, - max_turns=20, - max_budget_usd=1.0, - include_partial_messages=True, - ), - ): + async for msg in client.receive_response(): if isinstance(msg, StreamEvent): event = msg.event etype = event.get("type") @@ -144,7 +119,7 @@ async def take_before_screenshot(repo_dir: str, screenshot_output: str) -> None: tool_input_chunks += delta.get("partial_json", "") elif etype == "content_block_stop": if current_tool and tool_input_chunks: - _emit_tool_detail("screenshot", current_tool, tool_input_chunks) + _emit_tool_detail(phase, current_tool, tool_input_chunks) current_tool = None tool_input_chunks = "" continue @@ -152,9 +127,52 @@ async def take_before_screenshot(repo_dir: str, screenshot_output: str) -> None: if isinstance(msg, AssistantMessage): for block in msg.content: if isinstance(block, TextBlock): - emit_log("screenshot", block.text[:200], detail=block.text) + emit_log(phase, block.text[:200], detail=block.text) elif isinstance(block, ToolUseBlock): - _emit_tool_detail("screenshot", block.name, json.dumps(block.input)) + _emit_tool_detail(phase, block.name, json.dumps(block.input)) + + +async def launch_and_screenshot(repo_dir: str, screenshot_output: str) -> None: + """Launch dev server and take screenshot in a single session. + + Uses ClaudeSDKClient to keep the same conversation context across + two query() calls so the agent remembers the dev server URL. + """ + options = ClaudeAgentOptions( + allowed_tools=["Read", "Bash", "Glob", "Grep"], + cwd=repo_dir, + max_turns=20, + max_budget_usd=1.0, + include_partial_messages=True, + ) + + async with ClaudeSDKClient(options=options) as client: + # Phase 1: install deps + start dev server + emit_log("launching", "Launching project") + await client.query(f"""You need to launch the web application at {repo_dir}. + +Follow these steps: +1. Investigate how to start the dev server (check package.json scripts, README, etc.) +2. Install the project's dependencies if needed (e.g. `npm install` in the project directory) +3. Start the dev server in the background using Bash (e.g. `npm run dev &` or whatever is appropriate) +4. Wait for the server to become ready (poll with curl until it responds) + +IMPORTANT: Make sure the dev server is running and responding before finishing. +""") + await _process_messages(client, "launching") + + # Phase 2: take screenshot (same session, so dev server URL is remembered) + emit_log("screenshot", "Taking before screenshot") + await client.query(f"""Now take a screenshot of the running application. + +You already know the dev server URL from the previous step. Use it to take a screenshot: +1. Run: `node /usr/local/bin/take-screenshot.mjs {screenshot_output}` + where is the URL the dev server is listening on +2. Verify the screenshot file was created at {screenshot_output} + +NOTE: Playwright and Chromium are pre-installed globally in this container. Do NOT install playwright or run `npx playwright install`. Just run `node /usr/local/bin/take-screenshot.mjs ` directly. +""") + await _process_messages(client, "screenshot") def _extract_proposals_json(collected_text: list[str]) -> list[dict]: @@ -373,17 +391,16 @@ async def main() -> None: file=sys.stderr, ) - # Step 2: Take before screenshot - emit_log("screenshot", "Taking before screenshot") + # Step 2-3: Launch project and take before screenshot (single session) before_path = f"{tmp_dir}/before.png" - await take_before_screenshot(repo_dir, before_path) + await launch_and_screenshot(repo_dir, before_path) - # Step 3: Generate design proposals via Claude Agent SDK + # Step 4: Generate design proposals via Claude Agent SDK emit_log("analyzing", "Generating design proposals") proposals = await generate_proposals(repo_dir, instruction, num_proposals) emit_log("analyzing", f"Generated {len(proposals)} proposals") - # Step 4: Upload results to S3 + # Step 5: Upload results to S3 emit_log("uploading", "Uploading results to S3") # Upload before screenshot diff --git a/docker/worker-implement.py b/docker/worker-implement.py index e7efc9f..3a20a62 100644 --- a/docker/worker-implement.py +++ b/docker/worker-implement.py @@ -14,7 +14,7 @@ import boto3 from botocore.config import Config -from claude_agent_sdk import ClaudeAgentOptions, query, AssistantMessage, TextBlock, ToolUseBlock +from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, query, AssistantMessage, TextBlock, ToolUseBlock from claude_agent_sdk.types import StreamEvent @@ -99,37 +99,18 @@ def s3_upload_text(s3, bucket, key, text, content_type="text/plain"): raise -async def implement_proposal( - repo_dir: str, proposal_plan: str, screenshot_output: str -) -> None: - """Use Claude Agent SDK to implement the design proposal, start dev server, and take screenshot.""" +async def implement_changes(repo_dir: str, proposal_plan: str) -> None: + """Use Claude Agent SDK to implement the design proposal.""" prompt = f"""You are implementing UI changes to a web application located at {repo_dir}. Here is the specific design proposal to implement: {proposal_plan} -Follow these steps in order: - -## Step 1: Implement the changes - Implement all the changes described in the plan - All file modifications must be correct and complete - Follow existing code conventions and patterns - Do not leave any TODO comments or incomplete implementations - -## Step 2: Start the dev server and take a screenshot -After implementing the changes: -1. Investigate how to start the dev server for this project (check package.json scripts, README, etc.) -2. Install the project's dependencies if needed (e.g. `npm install` in the project directory) -3. Start the dev server in the background using Bash (e.g. `npm run dev &` or whatever is appropriate) -4. Wait for the server to become ready (poll with curl until it responds) -5. Once the server is ready, take a screenshot by running: - `node /usr/local/bin/take-screenshot.mjs {screenshot_output}` - where is the URL the dev server is listening on (e.g. http://localhost:5173) -6. Make sure the screenshot file was created at {screenshot_output} - -IMPORTANT: The screenshot is critical. Do your best to get the dev server running and take the screenshot. -NOTE: Playwright and Chromium are pre-installed globally in this container. Do NOT install playwright or run `npx playwright install`. Just run `node /usr/local/bin/take-screenshot.mjs ` directly. """ current_tool = None @@ -164,7 +145,6 @@ async def implement_proposal( tool_input_chunks = "" continue - # Log progress messages from completed AssistantMessage if isinstance(msg, AssistantMessage): for block in msg.content: if isinstance(block, TextBlock): @@ -173,6 +153,82 @@ async def implement_proposal( _emit_tool_detail("implementing", block.name, json.dumps(block.input)) +async def _process_messages(client: ClaudeSDKClient, phase: str) -> None: + """Process messages from ClaudeSDKClient, emitting logs for a given phase.""" + current_tool = None + tool_input_chunks = "" + + async for msg in client.receive_response(): + if isinstance(msg, StreamEvent): + event = msg.event + etype = event.get("type") + if etype == "content_block_start": + content_block = event.get("content_block", {}) + if content_block.get("type") == "tool_use": + current_tool = content_block.get("name") + tool_input_chunks = "" + elif etype == "content_block_delta": + delta = event.get("delta", {}) + if delta.get("type") == "input_json_delta": + tool_input_chunks += delta.get("partial_json", "") + elif etype == "content_block_stop": + if current_tool and tool_input_chunks: + _emit_tool_detail(phase, current_tool, tool_input_chunks) + current_tool = None + tool_input_chunks = "" + continue + + if isinstance(msg, AssistantMessage): + for block in msg.content: + if isinstance(block, TextBlock): + emit_log(phase, block.text[:200], detail=block.text) + elif isinstance(block, ToolUseBlock): + _emit_tool_detail(phase, block.name, json.dumps(block.input)) + + +async def launch_and_screenshot(repo_dir: str, screenshot_output: str) -> None: + """Launch dev server and take screenshot in a single session. + + Uses ClaudeSDKClient to keep the same conversation context across + two query() calls so the agent remembers the dev server URL. + """ + options = ClaudeAgentOptions( + allowed_tools=["Read", "Bash", "Glob", "Grep"], + cwd=repo_dir, + max_turns=20, + max_budget_usd=1.0, + include_partial_messages=True, + ) + + async with ClaudeSDKClient(options=options) as client: + # Phase 1: install deps + start dev server + emit_log("launching", "Launching project") + await client.query(f"""You need to launch the web application at {repo_dir}. + +Follow these steps: +1. Investigate how to start the dev server (check package.json scripts, README, etc.) +2. Install the project's dependencies if needed (e.g. `npm install` in the project directory) +3. Start the dev server in the background using Bash (e.g. `npm run dev &` or whatever is appropriate) +4. Wait for the server to become ready (poll with curl until it responds) + +IMPORTANT: Make sure the dev server is running and responding before finishing. +""") + await _process_messages(client, "launching") + + # Phase 2: take screenshot (same session, so dev server URL is remembered) + emit_log("screenshot", "Taking after screenshot") + await client.query(f"""Now take a screenshot of the running application. + +You already know the dev server URL from the previous step. Use it to take a screenshot: +1. Run: `node /usr/local/bin/take-screenshot.mjs {screenshot_output}` + where is the URL the dev server is listening on +2. Verify the screenshot file was created at {screenshot_output} + +NOTE: Playwright and Chromium are pre-installed globally in this container. Do NOT install playwright or run `npx playwright install`. Just run `node /usr/local/bin/take-screenshot.mjs ` directly. +""") + await _process_messages(client, "screenshot") + + async def main() -> None: # Read environment variables session_id = os.environ["SESSION_ID"] @@ -262,7 +318,10 @@ async def main() -> None: screenshot_path = f"{tmp_dir}/after.png" emit_log("implementing", "Implementing design proposal") - await implement_proposal(repo_dir, proposal_plan, screenshot_path) + await implement_changes(repo_dir, proposal_plan) + + # Launch + screenshot in a single session (shares dev server URL context) + await launch_and_screenshot(repo_dir, screenshot_path) # Step 4: Generate cumulative patch (squash everything since base_branch) emit_log("uploading", "Generating cumulative patch") diff --git a/frontend/src/components/LogPanel.tsx b/frontend/src/components/LogPanel.tsx index e738a79..7d292dc 100644 --- a/frontend/src/components/LogPanel.tsx +++ b/frontend/src/components/LogPanel.tsx @@ -3,8 +3,9 @@ import type { LogStreamState, JobLogState, LogEntry } from '../hooks/useLogStrea const PHASE_LABELS: Record = { sandbox: 'Creating Sandbox', - cloning: 'Cloning', + cloning: 'Cloning Project', patching: 'Applying Patch', + launching: 'Launching Project', screenshot: 'Taking Screenshot', analyzing: 'Analyzing', implementing: 'Implementing',