awel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +200 -0
  2. package/README.md +98 -0
  3. package/babel-plugin-awel-source.cjs +79 -0
  4. package/bin/awel.js +2 -0
  5. package/dist/cli/agent.d.ts +6 -0
  6. package/dist/cli/agent.js +266 -0
  7. package/dist/cli/babel-setup.d.ts +1 -0
  8. package/dist/cli/babel-setup.js +180 -0
  9. package/dist/cli/comment-popup.d.ts +2 -0
  10. package/dist/cli/comment-popup.js +206 -0
  11. package/dist/cli/config.d.ts +14 -0
  12. package/dist/cli/config.js +29 -0
  13. package/dist/cli/devserver.d.ts +17 -0
  14. package/dist/cli/devserver.js +43 -0
  15. package/dist/cli/index.d.ts +1 -0
  16. package/dist/cli/index.js +34 -0
  17. package/dist/cli/inspector.d.ts +2 -0
  18. package/dist/cli/inspector.js +117 -0
  19. package/dist/cli/logger.d.ts +10 -0
  20. package/dist/cli/logger.js +40 -0
  21. package/dist/cli/plan-store.d.ts +14 -0
  22. package/dist/cli/plan-store.js +18 -0
  23. package/dist/cli/providers/registry.d.ts +17 -0
  24. package/dist/cli/providers/registry.js +112 -0
  25. package/dist/cli/providers/types.d.ts +17 -0
  26. package/dist/cli/providers/types.js +1 -0
  27. package/dist/cli/providers/vercel.d.ts +4 -0
  28. package/dist/cli/providers/vercel.js +483 -0
  29. package/dist/cli/proxy.d.ts +5 -0
  30. package/dist/cli/proxy.js +72 -0
  31. package/dist/cli/server.d.ts +7 -0
  32. package/dist/cli/server.js +104 -0
  33. package/dist/cli/session.d.ts +32 -0
  34. package/dist/cli/session.js +77 -0
  35. package/dist/cli/skills/react-best-practices.md +2934 -0
  36. package/dist/cli/skills/skills/react-best-practices.md +2934 -0
  37. package/dist/cli/sse.d.ts +17 -0
  38. package/dist/cli/sse.js +51 -0
  39. package/dist/cli/subprocess.d.ts +30 -0
  40. package/dist/cli/subprocess.js +163 -0
  41. package/dist/cli/tools/ask-user.d.ts +11 -0
  42. package/dist/cli/tools/ask-user.js +28 -0
  43. package/dist/cli/tools/bash.d.ts +4 -0
  44. package/dist/cli/tools/bash.js +30 -0
  45. package/dist/cli/tools/code-search.d.ts +4 -0
  46. package/dist/cli/tools/code-search.js +70 -0
  47. package/dist/cli/tools/edit.d.ts +6 -0
  48. package/dist/cli/tools/edit.js +37 -0
  49. package/dist/cli/tools/glob.d.ts +4 -0
  50. package/dist/cli/tools/glob.js +29 -0
  51. package/dist/cli/tools/grep.d.ts +5 -0
  52. package/dist/cli/tools/grep.js +146 -0
  53. package/dist/cli/tools/index.d.ts +86 -0
  54. package/dist/cli/tools/index.js +41 -0
  55. package/dist/cli/tools/ls.d.ts +3 -0
  56. package/dist/cli/tools/ls.js +31 -0
  57. package/dist/cli/tools/multi-edit.d.ts +8 -0
  58. package/dist/cli/tools/multi-edit.js +53 -0
  59. package/dist/cli/tools/propose-plan.d.ts +4 -0
  60. package/dist/cli/tools/propose-plan.js +21 -0
  61. package/dist/cli/tools/react-best-practices.d.ts +3 -0
  62. package/dist/cli/tools/react-best-practices.js +55 -0
  63. package/dist/cli/tools/read.d.ts +3 -0
  64. package/dist/cli/tools/read.js +24 -0
  65. package/dist/cli/tools/restart-dev-server.d.ts +3 -0
  66. package/dist/cli/tools/restart-dev-server.js +18 -0
  67. package/dist/cli/tools/todo.d.ts +8 -0
  68. package/dist/cli/tools/todo.js +59 -0
  69. package/dist/cli/tools/web-fetch.d.ts +5 -0
  70. package/dist/cli/tools/web-fetch.js +116 -0
  71. package/dist/cli/tools/web-search.d.ts +5 -0
  72. package/dist/cli/tools/web-search.js +74 -0
  73. package/dist/cli/tools/write.d.ts +4 -0
  74. package/dist/cli/tools/write.js +26 -0
  75. package/dist/cli/types.d.ts +16 -0
  76. package/dist/cli/types.js +2 -0
  77. package/dist/cli/undo.d.ts +49 -0
  78. package/dist/cli/undo.js +212 -0
  79. package/dist/cli/verbose.d.ts +7 -0
  80. package/dist/cli/verbose.js +60 -0
  81. package/dist/dashboard/assets/index-Bk--q3wu.js +313 -0
  82. package/dist/dashboard/assets/index-DkWV03So.css +1 -0
  83. package/dist/dashboard/index.html +16 -0
  84. package/dist/host/host.js +274 -0
  85. package/package.json +67 -0
@@ -0,0 +1,483 @@
1
+ import { streamText, stepCountIs } from 'ai';
2
+ import { createOpenAI } from '@ai-sdk/openai';
3
+ import { createGoogleGenerativeAI } from '@ai-sdk/google';
4
+ import { createAnthropic } from '@ai-sdk/anthropic';
5
+ import { claudeCode } from 'ai-sdk-provider-claude-code';
6
+ import { createQwen } from 'qwen-ai-provider';
7
+ import { createMinimax } from 'vercel-minimax-ai-provider';
8
+ import { pauseDevServer, resumeDevServer } from '../devserver.js';
9
+ import { addToHistory, writeSSEEvent } from '../sse.js';
10
+ import { awelTools } from '../tools/index.js';
11
+ import { storePlan } from '../plan-store.js';
12
+ import { startUndoSession, endUndoSession, pushSnapshot, getCurrentSessionStats } from '../undo.js';
13
+ import { resolve } from 'path';
14
+ import { logEvent } from '../verbose.js';
15
+ const SYSTEM_PROMPT = `You are Awel, an expert AI coding assistant. You help users build, modify, and understand their code projects.
16
+
17
+ You have access to these tools:
18
+ - Read: Read file contents
19
+ - Write: Create or overwrite files (creates parent directories automatically)
20
+ - Edit: Find-and-replace edits in files
21
+ - Bash: Execute shell commands
22
+ - Glob: Find files by glob pattern
23
+ - Ls: List directory contents
24
+ - ProposePlan: Propose a structured implementation plan before executing complex tasks
25
+ - AskUser: Ask the user clarifying questions with selectable options
26
+ - ReactBestPractices: Get React/Next.js performance best practices (40+ rules). Call with a section name or "all".
27
+ - Grep: Search file contents for a regex pattern (find function definitions, variable usage, string matches)
28
+ - MultiEdit: Apply multiple find-and-replace edits to a single file in one call
29
+ - WebSearch: Search the web for real-time information (documentation, error messages, APIs, libraries)
30
+ - WebFetch: Fetch content from a URL and return it as markdown, plain text, or raw HTML
31
+ - CodeSearch: Search the web for code examples, API docs, and SDK references
32
+ - TodoRead: Read the current task list to check progress
33
+ - TodoWrite: Create or update the task list to track multi-step work
34
+ - RestartDevServer: Restart the user's dev server if it has crashed, is unresponsive, or needs a restart after config changes
35
+
36
+ React Best Practices:
37
+ - When writing, reviewing, or refactoring React/Next.js code, use the ReactBestPractices tool to consult the performance guide. Request a specific section (e.g. "bundle", "rerender") when you know the area, or "all" for the full guide.
38
+
39
+ Guidelines:
40
+ - Always read a file before editing it
41
+ - Use relative paths when possible
42
+ - Be concise in explanations
43
+ - When making changes, explain what you did and why
44
+ - If a task requires multiple steps, work through them methodically
45
+
46
+ Plan Mode:
47
+ - When a user's request involves changes to 2 or more files, or any non-trivial multi-step work, you MUST use the ProposePlan tool FIRST before making any changes.
48
+ - Write a detailed markdown plan (up to 600-700 words for complex projects) covering: an overview of the approach, step-by-step implementation details, which files will be modified or created, and critical considerations or trade-offs.
49
+ - After calling ProposePlan, STOP and wait for the user to approve the plan or provide feedback.
50
+ - Do NOT begin executing file changes until the user has approved your plan.
51
+ - If the user provides feedback, revise your plan and call ProposePlan again with the updated plan.
52
+
53
+ Clarifying Questions:
54
+ - When a user's request is ambiguous or has multiple valid approaches, use the AskUser tool to ask clarifying questions before proceeding.
55
+ - Present 1-4 questions, each with 2-4 concrete options. Use multiSelect when choices are not mutually exclusive.
56
+ - Keep header labels short (max 12 chars). Put the recommended option first with "(Recommended)" in the label.
57
+ - CRITICAL: All fields in the AskUser tool must be plain text. Do NOT use markdown formatting (no **, no ##, no \`code\`, no bullet points, no lists). The UI renders these strings directly in a structured card — markdown will display as raw characters.
58
+ - After calling AskUser, STOP and wait for the user's answers before continuing.
59
+
60
+ Inspector Context:
61
+ - When a prompt includes [Inspector Context], the user selected a specific HTML tag using the visual inspector.
62
+ - The context has two sections: "Selected Tag" (the exact element clicked) and "Parent Component Context" (the surrounding component for reference).
63
+ - CRITICAL: Focus your changes on the specific selected tag, NOT the entire parent component. The rendered HTML attributes help you locate the exact JSX element in the source code.
64
+ - Use the parent component source code only as context to find and modify the specific tag.
65
+ - Prioritize addressing what the user sees: if props are undefined/null, investigate why.
66
+ - Reference the specific line numbers from the context when making edits.`;
67
+ /** Detects files Claude Code uses for plan output (.claude/plans/*.md, plan.md) */
68
+ function isPlanFile(filePath) {
69
+ const normalized = filePath.replace(/\\/g, '/');
70
+ if (/\.claude\/plans\/[^/]+\.md$/.test(normalized))
71
+ return true;
72
+ const basename = normalized.split('/').pop()?.toLowerCase() || '';
73
+ return /^\.?plan\.md$/.test(basename);
74
+ }
75
+ /** Extracts a title and body from plan markdown content */
76
+ function parsePlanContent(raw) {
77
+ const lines = raw.split('\n');
78
+ const headingIdx = lines.findIndex(l => /^#\s+/.test(l));
79
+ if (headingIdx !== -1) {
80
+ const title = lines[headingIdx].replace(/^#\s+/, '').trim();
81
+ const content = [...lines.slice(0, headingIdx), ...lines.slice(headingIdx + 1)].join('\n').trim();
82
+ return { title, content };
83
+ }
84
+ return { title: lines[0]?.trim() || 'Plan', content: lines.slice(1).join('\n').trim() };
85
+ }
86
+ /** Tool names that require pausing the stream for user interaction.
87
+ * Covers both custom tool names (non-Anthropic) and Claude Code SDK names. */
88
+ const ASK_USER_TOOLS = new Set(['AskUser', 'AskUserQuestion']);
89
+ const PLAN_TOOLS = new Set(['ProposePlan', 'EnterPlanMode', 'ExitPlanMode']);
90
+ const INTERACTIVE_TOOLS = new Set([...ASK_USER_TOOLS, ...PLAN_TOOLS]);
91
+ function createModel(modelId, providerType, cwd) {
92
+ if (providerType === 'claude-code') {
93
+ return claudeCode(modelId, {
94
+ allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Ls', 'ProposePlan', 'AskUser'],
95
+ cwd,
96
+ permissionMode: 'acceptEdits',
97
+ streamingInput: 'always',
98
+ maxTurns: 25,
99
+ });
100
+ }
101
+ else if (providerType === 'anthropic') {
102
+ const anthropic = createAnthropic({});
103
+ return anthropic(modelId);
104
+ }
105
+ else if (providerType === 'openai') {
106
+ const openai = createOpenAI({});
107
+ return openai(modelId);
108
+ }
109
+ else if (providerType === 'google-ai') {
110
+ const google = createGoogleGenerativeAI({});
111
+ return google(modelId);
112
+ }
113
+ else if (providerType === 'qwen') {
114
+ const qwen = createQwen({});
115
+ // qwen-ai-provider returns LanguageModelV1; AI SDK v6 handles v1 models
116
+ // at runtime but the type signature expects v2/v3 — cast to satisfy tsc.
117
+ return qwen(modelId);
118
+ }
119
+ else if (providerType === 'minimax') {
120
+ const minimax = createMinimax({});
121
+ return minimax(modelId);
122
+ }
123
+ else {
124
+ // vercel-gateway: pass model ID string directly to streamText
125
+ // ai v6 routes through the gateway using AI_GATEWAY_API_KEY env var
126
+ return modelId;
127
+ }
128
+ }
129
+ export function createVercelProvider(modelId, providerType) {
130
+ return {
131
+ async streamResponse(stream, messages, config) {
132
+ const PROVIDER_LABELS = {
133
+ 'claude-code': 'Claude Code',
134
+ anthropic: 'Anthropic',
135
+ openai: 'OpenAI',
136
+ 'google-ai': 'Google AI',
137
+ 'vercel-gateway': 'Vercel AI Gateway',
138
+ qwen: 'Qwen',
139
+ minimax: 'MiniMax',
140
+ };
141
+ const providerLabel = PROVIDER_LABELS[providerType];
142
+ await writeSSEEvent(stream, 'status', {
143
+ type: 'status',
144
+ message: `Connecting to ${providerLabel}...`
145
+ });
146
+ // Extract the last user message text for use in storePlan's originalPrompt
147
+ const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
148
+ const lastUserPrompt = typeof lastUserMsg?.content === 'string'
149
+ ? lastUserMsg.content
150
+ : '';
151
+ // Self-contained providers have built-in tools, system prompt, and execution
152
+ // loop via the `cwd` config — they don't need Awel's tools or system prompt.
153
+ const isSelfContained = providerType === 'claude-code';
154
+ const model = createModel(modelId, providerType, config.projectCwd);
155
+ const tools = isSelfContained ? undefined : awelTools(config.projectCwd);
156
+ pauseDevServer(config.targetPort);
157
+ const startTime = Date.now();
158
+ let numTurns = 0;
159
+ // State for intercepting Claude Code native plan mode
160
+ let pendingPlanContent = null;
161
+ let accumulatedText = '';
162
+ let planEmitted = false;
163
+ let inPlanMode = false;
164
+ let waitingForUserInput = false;
165
+ let reasoningActive = false;
166
+ const suppressedToolCallIds = new Set();
167
+ const abortController = new AbortController();
168
+ // Propagate external cancellation (e.g. new chat request) to the internal controller
169
+ if (config.signal) {
170
+ if (config.signal.aborted) {
171
+ abortController.abort();
172
+ }
173
+ else {
174
+ config.signal.addEventListener('abort', () => abortController.abort(), { once: true });
175
+ }
176
+ }
177
+ // Start undo session to group all file changes from this agent run
178
+ startUndoSession();
179
+ let responseMessages = [];
180
+ try {
181
+ // Self-contained providers handle their own system prompt and tools internally.
182
+ // All other providers get our system prompt + tools.
183
+ const systemPrompt = isSelfContained
184
+ ? undefined
185
+ : `${SYSTEM_PROMPT}\n\nThe user's project directory is: ${config.projectCwd}`;
186
+ const streamTextArgs = {
187
+ model,
188
+ ...(systemPrompt && { system: systemPrompt }),
189
+ messages,
190
+ tools,
191
+ ...(!isSelfContained && { stopWhen: stepCountIs(25) }),
192
+ abortSignal: abortController.signal,
193
+ };
194
+ logEvent('stream:start', `model=${modelId} provider=${providerType} messages=${messages.length}`);
195
+ const result = streamText(streamTextArgs);
196
+ try {
197
+ for await (const part of result.fullStream) {
198
+ switch (part.type) {
199
+ case 'text-delta': {
200
+ logEvent('text-delta', part.text);
201
+ accumulatedText += part.text;
202
+ if (inPlanMode)
203
+ break;
204
+ const textData = JSON.stringify({
205
+ type: 'text',
206
+ text: part.text,
207
+ model: modelId
208
+ });
209
+ addToHistory('text', textData);
210
+ await stream.writeSSE({ event: 'text', data: textData });
211
+ break;
212
+ }
213
+ case 'tool-call': {
214
+ logEvent('tool-call', `${part.toolName} ${JSON.stringify(part.input).slice(0, 200)}`);
215
+ // Intercept ProposePlan — emit as a plan SSE event
216
+ if (part.toolName === 'ProposePlan') {
217
+ const input = part.input;
218
+ const planId = crypto.randomUUID();
219
+ storePlan({
220
+ planId,
221
+ plan: { title: input.title, content: input.content },
222
+ originalPrompt: lastUserPrompt,
223
+ modelId,
224
+ approved: false,
225
+ });
226
+ const planData = JSON.stringify({
227
+ type: 'plan',
228
+ planId,
229
+ planTitle: input.title,
230
+ planContent: input.content,
231
+ });
232
+ addToHistory('plan', planData);
233
+ await stream.writeSSE({ event: 'plan', data: planData });
234
+ waitingForUserInput = true;
235
+ break;
236
+ }
237
+ // Intercept AskUser / AskUserQuestion — emit as a question SSE event
238
+ if (ASK_USER_TOOLS.has(part.toolName)) {
239
+ const input = part.input;
240
+ const questionId = crypto.randomUUID();
241
+ const questionData = JSON.stringify({
242
+ type: 'question',
243
+ questionId,
244
+ questions: input.questions,
245
+ });
246
+ addToHistory('question', questionData);
247
+ await stream.writeSSE({ event: 'question', data: questionData });
248
+ waitingForUserInput = true;
249
+ break;
250
+ }
251
+ // Intercept Claude Code native plan mode tools
252
+ if (part.toolName === 'EnterPlanMode') {
253
+ inPlanMode = true;
254
+ accumulatedText = '';
255
+ break;
256
+ }
257
+ if (part.toolName === 'ExitPlanMode') {
258
+ inPlanMode = false;
259
+ // Deduplicate — Claude Code may call ExitPlanMode multiple times
260
+ if (planEmitted) {
261
+ break;
262
+ }
263
+ // Prefer Write-captured content, fall back to accumulated text-deltas
264
+ const planContent = pendingPlanContent || accumulatedText;
265
+ if (planContent) {
266
+ const parsed = parsePlanContent(planContent);
267
+ const planId = crypto.randomUUID();
268
+ storePlan({
269
+ planId,
270
+ plan: { title: parsed.title, content: parsed.content || planContent },
271
+ originalPrompt: lastUserPrompt,
272
+ modelId,
273
+ approved: false,
274
+ });
275
+ const planData = JSON.stringify({
276
+ type: 'plan',
277
+ planId,
278
+ planTitle: parsed.title,
279
+ planContent: parsed.content || planContent,
280
+ });
281
+ addToHistory('plan', planData);
282
+ await stream.writeSSE({ event: 'plan', data: planData });
283
+ planEmitted = true;
284
+ waitingForUserInput = true;
285
+ }
286
+ pendingPlanContent = null;
287
+ break;
288
+ }
289
+ // Snapshot files before Write/Edit execution for undo support.
290
+ // This is essential for CLI providers (Claude Code, Gemini CLI) whose
291
+ // built-in tools bypass Awel's tool implementations.
292
+ if (part.toolName === 'Write' || part.toolName === 'Edit') {
293
+ const input = part.input;
294
+ const filePath = (input.file_path || input.filePath || '');
295
+ if (filePath) {
296
+ const fullPath = filePath.startsWith('/') ? filePath : resolve(config.projectCwd, filePath);
297
+ pushSnapshot(fullPath);
298
+ }
299
+ }
300
+ // Capture Write calls to plan files for ExitPlanMode interception
301
+ if (part.toolName === 'Write') {
302
+ const input = part.input;
303
+ const filePath = (input.file_path || input.filePath || '');
304
+ if (isPlanFile(filePath) && typeof input.content === 'string') {
305
+ pendingPlanContent = input.content;
306
+ suppressedToolCallIds.add(part.toolCallId);
307
+ break;
308
+ }
309
+ }
310
+ const toolData = JSON.stringify({
311
+ type: 'tool_use',
312
+ tool: part.toolName,
313
+ input: part.input,
314
+ id: part.toolCallId
315
+ });
316
+ addToHistory('tool_use', toolData);
317
+ await stream.writeSSE({ event: 'tool_use', data: toolData });
318
+ break;
319
+ }
320
+ case 'tool-result': {
321
+ logEvent('tool-result', `${part.toolName} ${typeof part.output === 'string' ? part.output.slice(0, 120) : JSON.stringify(part.output).slice(0, 120)}`);
322
+ // Suppress tool results for intercepted tools
323
+ if (INTERACTIVE_TOOLS.has(part.toolName)
324
+ || suppressedToolCallIds.delete(part.toolCallId))
325
+ break;
326
+ const resultData = JSON.stringify({
327
+ type: 'tool_result',
328
+ tool_use_id: part.toolCallId,
329
+ tool: part.toolName,
330
+ content: part.output,
331
+ is_error: false
332
+ });
333
+ addToHistory('tool_result', resultData);
334
+ await stream.writeSSE({ event: 'tool_result', data: resultData });
335
+ break;
336
+ }
337
+ case 'finish-step': {
338
+ numTurns++;
339
+ logEvent('finish-step', `turn=${numTurns}`);
340
+ if (inPlanMode) {
341
+ accumulatedText = '';
342
+ }
343
+ break;
344
+ }
345
+ case 'reasoning-start': {
346
+ logEvent('reasoning', 'start');
347
+ reasoningActive = true;
348
+ // Emit a status so the UI shows progress
349
+ const reasoningStatus = JSON.stringify({ type: 'status', message: 'Reasoning...' });
350
+ addToHistory('status', reasoningStatus);
351
+ await stream.writeSSE({ event: 'status', data: reasoningStatus });
352
+ break;
353
+ }
354
+ case 'reasoning-delta': {
355
+ // Some providers surface reasoning text; capture if present
356
+ const rp = part;
357
+ if (rp.text) {
358
+ logEvent('reasoning-delta', rp.text.slice(0, 120));
359
+ }
360
+ break;
361
+ }
362
+ case 'reasoning-end': {
363
+ logEvent('reasoning', 'end');
364
+ reasoningActive = false;
365
+ break;
366
+ }
367
+ case 'tool-input-start':
368
+ case 'tool-input-delta':
369
+ case 'tool-input-end': {
370
+ // Intermediate tool-input streaming; the aggregated tool-call
371
+ // event is what we act on — silently ignore these.
372
+ break;
373
+ }
374
+ case 'tool-error': {
375
+ const te = part;
376
+ const toolErrMsg = te.error instanceof Error
377
+ ? te.error.message
378
+ : typeof te.error === 'string'
379
+ ? te.error
380
+ : JSON.stringify(te.error);
381
+ logEvent('tool-error', `${te.toolName ?? 'unknown'} ${toolErrMsg}`);
382
+ const toolErrData = JSON.stringify({
383
+ type: 'tool_result',
384
+ tool_use_id: '',
385
+ content: toolErrMsg,
386
+ is_error: true
387
+ });
388
+ addToHistory('tool_result', toolErrData);
389
+ await stream.writeSSE({ event: 'tool_result', data: toolErrData });
390
+ break;
391
+ }
392
+ case 'error': {
393
+ const errorMsg = part.error instanceof Error
394
+ ? part.error.message
395
+ : typeof part.error === 'string'
396
+ ? part.error
397
+ : JSON.stringify(part.error);
398
+ logEvent('error', errorMsg);
399
+ const errorData = JSON.stringify({
400
+ type: 'error',
401
+ message: errorMsg
402
+ });
403
+ addToHistory('error', errorData);
404
+ await stream.writeSSE({ event: 'error', data: errorData });
405
+ break;
406
+ }
407
+ // Known stream lifecycle events — no action needed
408
+ case 'start':
409
+ case 'start-step':
410
+ case 'text-start':
411
+ case 'text-end':
412
+ case 'source':
413
+ case 'file':
414
+ case 'finish':
415
+ case 'raw':
416
+ break;
417
+ default:
418
+ logEvent('stream:unknown', `type=${part.type}`);
419
+ break;
420
+ }
421
+ // Stop consuming the stream when waiting for user input
422
+ if (waitingForUserInput) {
423
+ logEvent('abort', 'waiting for user input');
424
+ abortController.abort();
425
+ break;
426
+ }
427
+ }
428
+ }
429
+ catch (err) {
430
+ // Ignore abort errors from user-input pauses or external cancellation
431
+ const externallyAborted = config.signal?.aborted;
432
+ if (!waitingForUserInput && !externallyAborted)
433
+ throw err;
434
+ }
435
+ // Capture response messages for multi-turn accumulation.
436
+ // Skip only when externally aborted (new request cancelled this one).
437
+ // For waitingForUserInput, we still try to capture the partial response
438
+ // (e.g. the assistant's plan/question tool call) so the session history
439
+ // stays consistent and avoids orphan user messages that cause 400 errors.
440
+ const externallyAborted = config.signal?.aborted;
441
+ if (!externallyAborted) {
442
+ try {
443
+ const response = await result.response;
444
+ responseMessages = response.messages;
445
+ }
446
+ catch {
447
+ // If awaiting response fails (e.g. abort race), leave empty
448
+ }
449
+ }
450
+ if (externallyAborted) {
451
+ logEvent('abort', 'externally cancelled');
452
+ }
453
+ // When externally cancelled, skip result events — no client is listening
454
+ if (!externallyAborted) {
455
+ const durationMs = Date.now() - startTime;
456
+ const resultSubtype = waitingForUserInput ? 'waiting_for_input' : 'success';
457
+ const fileStats = getCurrentSessionStats(config.projectCwd);
458
+ const resultData = JSON.stringify({
459
+ type: 'result',
460
+ subtype: resultSubtype,
461
+ duration_ms: durationMs,
462
+ num_turns: numTurns,
463
+ result: waitingForUserInput ? 'waiting_for_input' : 'completed',
464
+ is_error: false,
465
+ ...(fileStats && fileStats.length > 0 ? { file_stats: fileStats } : {}),
466
+ });
467
+ logEvent('stream:end', `duration=${durationMs}ms turns=${numTurns} result=${resultSubtype}`);
468
+ addToHistory('result', resultData);
469
+ await stream.writeSSE({ event: 'result', data: resultData });
470
+ }
471
+ }
472
+ finally {
473
+ // End undo session so all file changes are grouped together
474
+ endUndoSession();
475
+ resumeDevServer(config.targetPort);
476
+ }
477
+ if (!config.signal?.aborted) {
478
+ await writeSSEEvent(stream, 'done', { type: 'done', message: 'Agent completed' });
479
+ }
480
+ return responseMessages;
481
+ }
482
+ };
483
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Creates proxy middleware that forwards requests to the target app
3
+ * and injects the Awel host script into HTML responses.
4
+ */
5
+ export declare function createProxyMiddleware(targetPort: number, projectCwd?: string): (c: any, _next: () => Promise<void>) => Promise<any>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Creates proxy middleware that forwards requests to the target app
3
+ * and injects the Awel host script into HTML responses.
4
+ */
5
+ export function createProxyMiddleware(targetPort, projectCwd) {
6
+ return async (c, _next) => {
7
+ const url = new URL(c.req.url);
8
+ const targetUrl = `http://localhost:${targetPort}${url.pathname}${url.search}`;
9
+ try {
10
+ // Clone headers and remove Accept-Encoding to get uncompressed response
11
+ const headers = new Headers(c.req.raw.headers);
12
+ headers.delete('accept-encoding');
13
+ const response = await fetch(targetUrl, {
14
+ method: c.req.method,
15
+ headers,
16
+ body: c.req.method !== 'GET' && c.req.method !== 'HEAD'
17
+ ? await c.req.raw.arrayBuffer()
18
+ : undefined,
19
+ });
20
+ const contentType = response.headers.get('content-type') || '';
21
+ // If it's HTML, inject our script
22
+ if (contentType.includes('text/html')) {
23
+ let html = await response.text();
24
+ // Inject project CWD (for source-map resolution) and the host script
25
+ const cwdScript = projectCwd
26
+ ? `<script>window.__AWEL_PROJECT_CWD__=${JSON.stringify(projectCwd)}</script>`
27
+ : '';
28
+ // Constrain Next.js error overlay stacking context below Awel's UI.
29
+ // Covers both legacy and modern Next.js error overlay custom elements.
30
+ const awelOverlayStyle = `<style id="awel-overlay-fix">nextjs-portal, nextjs-portal-root, next-error-overlay, nextjs-dev-tools { position: relative !important; z-index: 999997 !important; }</style>`;
31
+ const scriptTag = `${awelOverlayStyle}${cwdScript}<script src="/_awel/host.js"></script>`;
32
+ // Inject into <head> so the script loads early — even on error pages
33
+ // where the body might be minimal or replaced by Next.js error recovery.
34
+ // Fall back to </body> then </html> if <head> isn't found.
35
+ if (html.includes('</head>')) {
36
+ html = html.replace('</head>', `${scriptTag}</head>`);
37
+ }
38
+ else if (html.includes('</body>')) {
39
+ html = html.replace('</body>', `${scriptTag}</body>`);
40
+ }
41
+ else if (html.includes('</html>')) {
42
+ html = html.replace('</html>', `${scriptTag}</html>`);
43
+ }
44
+ else {
45
+ html += scriptTag;
46
+ }
47
+ const responseHeaders = new Headers(response.headers);
48
+ responseHeaders.delete('content-encoding');
49
+ responseHeaders.delete('content-length');
50
+ responseHeaders.set('content-type', 'text/html; charset=utf-8');
51
+ return new Response(html, {
52
+ status: response.status,
53
+ headers: responseHeaders,
54
+ });
55
+ }
56
+ // For non-HTML responses, pass through as-is but clean up encoding headers
57
+ const body = await response.arrayBuffer();
58
+ const responseHeaders = new Headers(response.headers);
59
+ // Remove content-encoding since we've already decoded the response
60
+ responseHeaders.delete('content-encoding');
61
+ responseHeaders.delete('content-length');
62
+ return new Response(body, {
63
+ status: response.status,
64
+ headers: responseHeaders,
65
+ });
66
+ }
67
+ catch (error) {
68
+ // Target app might not be ready yet
69
+ return c.text('Waiting for target app to start...', 503);
70
+ }
71
+ };
72
+ }
@@ -0,0 +1,7 @@
1
+ interface ServerOptions {
2
+ awelPort: number;
3
+ targetPort: number;
4
+ projectCwd: string;
5
+ }
6
+ export declare function startServer({ awelPort, targetPort, projectCwd }: ServerOptions): Promise<void>;
7
+ export {};