@myrialabs/clopen 0.1.1 → 0.1.3

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 (51) hide show
  1. package/backend/index.ts +4 -1
  2. package/backend/lib/engine/adapters/opencode/message-converter.ts +53 -2
  3. package/backend/lib/engine/adapters/opencode/stream.ts +89 -5
  4. package/backend/lib/project/status-manager.ts +221 -181
  5. package/frontend/lib/components/chat/message/ChatMessages.svelte +16 -4
  6. package/frontend/lib/components/chat/tools/AgentTool.svelte +12 -11
  7. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +3 -3
  8. package/frontend/lib/components/chat/tools/BashTool.svelte +4 -4
  9. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +3 -1
  10. package/frontend/lib/components/chat/tools/EditTool.svelte +6 -6
  11. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +1 -1
  12. package/frontend/lib/components/chat/tools/GlobTool.svelte +12 -12
  13. package/frontend/lib/components/chat/tools/GrepTool.svelte +5 -5
  14. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +1 -1
  15. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +6 -6
  16. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +2 -2
  17. package/frontend/lib/components/chat/tools/ReadTool.svelte +4 -4
  18. package/frontend/lib/components/chat/tools/TaskStopTool.svelte +1 -1
  19. package/frontend/lib/components/chat/tools/TaskTool.svelte +1 -1
  20. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +4 -4
  21. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +1 -1
  22. package/frontend/lib/components/chat/tools/WriteTool.svelte +3 -3
  23. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +3 -3
  24. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +2 -2
  25. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +1 -1
  26. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +2 -2
  27. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +1 -1
  28. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +5 -5
  29. package/frontend/lib/components/common/Button.svelte +1 -1
  30. package/frontend/lib/components/common/Card.svelte +3 -3
  31. package/frontend/lib/components/common/Input.svelte +3 -3
  32. package/frontend/lib/components/common/LoadingSpinner.svelte +1 -1
  33. package/frontend/lib/components/common/Select.svelte +6 -6
  34. package/frontend/lib/components/common/Textarea.svelte +3 -3
  35. package/frontend/lib/components/files/FileViewer.svelte +1 -1
  36. package/frontend/lib/components/git/ChangesSection.svelte +2 -4
  37. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +9 -29
  38. package/frontend/lib/components/preview/browser/components/Container.svelte +17 -0
  39. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +2 -2
  40. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +0 -6
  41. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +15 -15
  42. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +2 -2
  43. package/frontend/lib/components/workspace/DesktopNavigator.svelte +380 -383
  44. package/frontend/lib/components/workspace/MobileNavigator.svelte +391 -395
  45. package/frontend/lib/components/workspace/PanelHeader.svelte +115 -4
  46. package/frontend/lib/components/workspace/ViewMenu.svelte +9 -25
  47. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +29 -4
  48. package/frontend/lib/services/notification/global-stream-monitor.ts +77 -86
  49. package/frontend/lib/services/project/status.service.ts +160 -159
  50. package/frontend/lib/stores/ui/workspace.svelte.ts +326 -283
  51. package/package.json +1 -1
package/backend/index.ts CHANGED
@@ -81,7 +81,10 @@ if (!isDevelopment) {
81
81
  if (filePath.startsWith(distDir)) {
82
82
  try {
83
83
  if (statSync(filePath).isFile()) {
84
- return new Response(Bun.file(filePath));
84
+ const file = Bun.file(filePath);
85
+ return new Response(file, {
86
+ headers: { 'Content-Type': file.type || 'application/octet-stream' }
87
+ });
85
88
  }
86
89
  } catch {}
87
90
  }
@@ -127,6 +127,8 @@ const TOOL_NAME_MAP: Record<string, string> = {
127
127
  'todo_write': 'TodoWrite',
128
128
  'todowrite': 'TodoWrite',
129
129
  'todoread': 'TodoWrite',
130
+ // Agent / sub-agent
131
+ 'task': 'Agent',
130
132
  // User interaction
131
133
  'question': 'AskUserQuestion',
132
134
  // Code intelligence & utilities
@@ -385,6 +387,7 @@ interface AssistantMessageParams {
385
387
  sessionId: string;
386
388
  stopReason: string | null;
387
389
  uuid: string;
390
+ parentToolUseId?: string | null;
388
391
  }
389
392
 
390
393
  /** Build a single SDKAssistantMessage from content blocks */
@@ -402,7 +405,7 @@ function buildAssistantSDKMessage(params: AssistantMessageParams): SDKMessage {
402
405
  ...(params.usage && { usage: params.usage }),
403
406
  context_management: null
404
407
  },
405
- parent_tool_use_id: null,
408
+ parent_tool_use_id: params.parentToolUseId ?? null,
406
409
  session_id: params.sessionId,
407
410
  uuid: params.uuid
408
411
  } as unknown as SDKMessage;
@@ -495,6 +498,20 @@ export function convertAssistantMessages(
495
498
 
496
499
  allBlocks.push(block);
497
500
  }
501
+ else if (part.type === 'subtask') {
502
+ // Subtask = Agent/sub-agent invocation
503
+ const subtaskPart = part as any;
504
+ allBlocks.push({
505
+ type: 'tool_use',
506
+ id: subtaskPart.id || crypto.randomUUID(),
507
+ name: 'Agent',
508
+ input: {
509
+ prompt: subtaskPart.prompt || '',
510
+ description: subtaskPart.description || '',
511
+ subagent_type: subtaskPart.agent || 'general-purpose',
512
+ } as NormalizedToolInput,
513
+ });
514
+ }
498
515
  // Skip: reasoning, step-start, step-finish, snapshot, patch, agent, retry, compaction
499
516
  }
500
517
 
@@ -682,6 +699,7 @@ export function convertToolUseOnly(
682
699
  toolPart: ToolPart,
683
700
  ocMessage: OCMessage,
684
701
  sessionId: string,
702
+ parentToolUseId: string | null = null,
685
703
  ): SDKMessage {
686
704
  const claudeName = mapToolName(toolPart.tool || 'unknown');
687
705
  const resolvedInput = getToolInput(toolPart);
@@ -703,6 +721,7 @@ export function convertToolUseOnly(
703
721
  sessionId,
704
722
  stopReason: 'tool_use',
705
723
  uuid: crypto.randomUUID(),
724
+ parentToolUseId,
706
725
  });
707
726
  }
708
727
 
@@ -783,6 +802,37 @@ export function convertReasoningStreamStop(sessionId: string): EngineSDKMessage
783
802
  } as unknown as EngineSDKMessage;
784
803
  }
785
804
 
805
+ /**
806
+ * Convert a subtask part → assistant message with Agent tool_use.
807
+ * Used for progressive rendering when OpenCode emits subtask parts.
808
+ */
809
+ export function convertSubtaskToolUseOnly(
810
+ subtaskPart: { id: string; prompt: string; description: string; agent: string },
811
+ ocMessage: OCMessage,
812
+ sessionId: string,
813
+ ): SDKMessage {
814
+ const assistantMsg = ocMessage.role === 'assistant' ? ocMessage as AssistantMessage : null;
815
+ const modelId = assistantMsg ? `${assistantMsg.providerID}/${assistantMsg.modelID}` : '';
816
+
817
+ return buildAssistantSDKMessage({
818
+ content: [{
819
+ type: 'tool_use',
820
+ id: subtaskPart.id || crypto.randomUUID(),
821
+ name: 'Agent',
822
+ input: {
823
+ prompt: subtaskPart.prompt || '',
824
+ description: subtaskPart.description || '',
825
+ subagent_type: subtaskPart.agent || 'general-purpose',
826
+ } as NormalizedToolInput,
827
+ }],
828
+ ocMessage,
829
+ modelId,
830
+ sessionId,
831
+ stopReason: 'tool_use',
832
+ uuid: crypto.randomUUID(),
833
+ });
834
+ }
835
+
786
836
  /**
787
837
  * Convert a completed/errored tool part → user message with tool_result.
788
838
  * Sent after the tool finishes executing, matching Claude Code's pattern
@@ -791,6 +841,7 @@ export function convertReasoningStreamStop(sessionId: string): EngineSDKMessage
791
841
  export function convertToolResultOnly(
792
842
  toolPart: ToolPart,
793
843
  sessionId: string,
844
+ parentToolUseId: string | null = null,
794
845
  ): SDKMessage {
795
846
  const toolUseId = toolPart.callID || toolPart.id || crypto.randomUUID();
796
847
 
@@ -807,7 +858,7 @@ export function convertToolResultOnly(
807
858
  type: 'user',
808
859
  uuid: crypto.randomUUID(),
809
860
  session_id: sessionId,
810
- parent_tool_use_id: null,
861
+ parent_tool_use_id: parentToolUseId,
811
862
  message: {
812
863
  role: 'user',
813
864
  content: [{
@@ -37,6 +37,7 @@ import {
37
37
  convertPartialReasoningDelta,
38
38
  convertReasoningStreamStart,
39
39
  convertReasoningStreamStop,
40
+ convertSubtaskToolUseOnly,
40
41
  getToolInput,
41
42
  } from './message-converter';
42
43
  import { ensureClient, getClient } from './server';
@@ -236,6 +237,13 @@ export class OpenCodeEngine implements AIEngine {
236
237
  let reasoningStreamActive = false; // Whether reasoning is currently streaming
237
238
  let reasoningText = ''; // Accumulated reasoning text
238
239
 
240
+ // Child session tracking (for Agent tool sub-messages)
241
+ const childSessionToAgentTool = new Map<string, string>(); // child sessionID → agent tool callID
242
+ const childAssistantMessages = new Map<string, OCMessage>(); // child msgID → message
243
+ const childEmittedToolParts = new Set<string>();
244
+ const childCompletedToolParts = new Set<string>();
245
+ let lastAgentToolCallId: string | null = null;
246
+
239
247
  /**
240
248
  * Flush active reasoning stream: stop reasoning stream, emit final reasoning message.
241
249
  */
@@ -266,9 +274,10 @@ export class OpenCodeEngine implements AIEngine {
266
274
  yield convertStreamStop(sessionId);
267
275
 
268
276
  const parts = messageParts.get(msgId) || [];
269
- // Filter out tool parts and reasoning parts already emitted
277
+ // Filter out tool parts, subtask parts, and reasoning parts already emitted
270
278
  const remainingParts = parts.filter(p => {
271
279
  if (p.type === 'tool') return !emittedToolParts.has(p.id);
280
+ if (p.type === 'subtask') return !emittedToolParts.has(p.id);
272
281
  if (p.type === 'reasoning') return false; // Already emitted as reasoning message
273
282
  return true;
274
283
  });
@@ -312,6 +321,15 @@ export class OpenCodeEngine implements AIEngine {
312
321
  }
313
322
  currentAssistantId = info.id;
314
323
  }
324
+
325
+ // Track child session assistant messages (for Agent tool sub-messages)
326
+ if (info.role === 'assistant' && info.sessionID !== sessionId && lastAgentToolCallId) {
327
+ if (!childSessionToAgentTool.has(info.sessionID)) {
328
+ childSessionToAgentTool.set(info.sessionID, lastAgentToolCallId);
329
+ debug.log('engine', `[OC] child session detected: ${info.sessionID} → agent tool ${lastAgentToolCallId}`);
330
+ }
331
+ childAssistantMessages.set(info.id, info);
332
+ }
315
333
  break;
316
334
  }
317
335
 
@@ -321,8 +339,36 @@ export class OpenCodeEngine implements AIEngine {
321
339
 
322
340
  debug.log('engine', `[OC] part.updated: type=${part.type}, partId=${part.id}, msgId=${part.messageID}, session=${part.sessionID === sessionId ? 'match' : 'skip'}`);
323
341
 
324
- // Only process parts belonging to our session
325
- if (part.sessionID !== sessionId) break;
342
+ // Handle child session parts (sub-agent tool activities)
343
+ if (part.sessionID !== sessionId) {
344
+ const agentCallId = childSessionToAgentTool.get(part.sessionID);
345
+ if (agentCallId && childAssistantMessages.has(part.messageID)) {
346
+ const childMsg = childAssistantMessages.get(part.messageID)!;
347
+
348
+ if (part.type === 'tool') {
349
+ const childToolPart = part as ToolPart;
350
+ if (!childEmittedToolParts.has(part.id)) {
351
+ const resolvedInput = getToolInput(childToolPart);
352
+ const hasInput = Object.keys(resolvedInput).length > 0
353
+ || childToolPart.state.status !== 'pending';
354
+ if (hasInput) {
355
+ childEmittedToolParts.add(part.id);
356
+ yield convertToolUseOnly(childToolPart, childMsg, sessionId, agentCallId);
357
+ if (childToolPart.state.status === 'completed' || childToolPart.state.status === 'error') {
358
+ childCompletedToolParts.add(part.id);
359
+ yield convertToolResultOnly(childToolPart, sessionId, agentCallId);
360
+ }
361
+ }
362
+ } else if (!childCompletedToolParts.has(part.id)) {
363
+ if (childToolPart.state.status === 'completed' || childToolPart.state.status === 'error') {
364
+ childCompletedToolParts.add(part.id);
365
+ yield convertToolResultOnly(childToolPart, sessionId, agentCallId);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ break;
371
+ }
326
372
 
327
373
  // Only process parts for tracked assistant messages (skip user message parts)
328
374
  if (!assistantMessages.has(part.messageID)) {
@@ -364,6 +410,11 @@ export class OpenCodeEngine implements AIEngine {
364
410
  if (hasInput) {
365
411
  emittedToolParts.add(part.id);
366
412
 
413
+ // Track agent tool callID for child session linking
414
+ if (toolPart.tool === 'task') {
415
+ lastAgentToolCallId = toolPart.callID || toolPart.id;
416
+ }
417
+
367
418
  yield convertStreamStop(sessionId);
368
419
  yield convertToolUseOnly(toolPart, msg, sessionId);
369
420
 
@@ -412,6 +463,25 @@ export class OpenCodeEngine implements AIEngine {
412
463
  }
413
464
  }
414
465
 
466
+ // Handle subtask parts — convert to Agent tool_use for progressive rendering
467
+ if (part.type === 'subtask') {
468
+ const subtaskPart = part as any;
469
+ const msg = assistantMessages.get(msgId);
470
+ if (msg && !emittedToolParts.has(part.id)) {
471
+ emittedToolParts.add(part.id);
472
+ // Track for child session linking
473
+ lastAgentToolCallId = part.id;
474
+ if (reasoningStreamActive) {
475
+ yield* flushReasoning(msg);
476
+ }
477
+ yield convertStreamStop(sessionId);
478
+ yield convertSubtaskToolUseOnly(subtaskPart, msg, sessionId);
479
+ yield convertStreamStart(sessionId);
480
+ streamingText = '';
481
+ }
482
+ break;
483
+ }
484
+
415
485
  // Skip non-text/non-reasoning parts (step-start, step-finish, etc.)
416
486
  if (part.type !== 'text') {
417
487
  debug.log('engine', `[OC] part.updated: skipped non-text type=${part.type}`);
@@ -455,6 +525,12 @@ export class OpenCodeEngine implements AIEngine {
455
525
  }
456
526
 
457
527
  case 'session.idle': {
528
+ // Only handle idle for our session (sub-agent sessions have different IDs)
529
+ const idleProps = (event as EventSessionIdle).properties;
530
+ if (idleProps.sessionID !== sessionId) {
531
+ debug.log('engine', `[OC] session.idle: ignored (session=${idleProps.sessionID}, ours=${sessionId})`);
532
+ break;
533
+ }
458
534
  // Session finished — flush reasoning and emit the last assistant message
459
535
  if (currentAssistantId && !emittedMessageIds.has(currentAssistantId)) {
460
536
  const msg = assistantMessages.get(currentAssistantId);
@@ -468,6 +544,7 @@ export class OpenCodeEngine implements AIEngine {
468
544
  // Filter out tool parts and reasoning parts already emitted
469
545
  const remainingParts = parts.filter(p => {
470
546
  if (p.type === 'tool') return !emittedToolParts.has(p.id);
547
+ if (p.type === 'subtask') return !emittedToolParts.has(p.id);
471
548
  if (p.type === 'reasoning') return false;
472
549
  return true;
473
550
  });
@@ -495,7 +572,10 @@ export class OpenCodeEngine implements AIEngine {
495
572
  }
496
573
 
497
574
  case 'session.status': {
498
- const { status } = (event as EventSessionStatus).properties;
575
+ const statusProps = (event as EventSessionStatus).properties;
576
+ // Only handle status for our session (sub-agent sessions have different IDs)
577
+ if (statusProps.sessionID !== sessionId) break;
578
+ const { status } = statusProps;
499
579
  if (status.type === 'idle') {
500
580
  // Same as session.idle
501
581
  if (currentAssistantId && !emittedMessageIds.has(currentAssistantId)) {
@@ -508,6 +588,7 @@ export class OpenCodeEngine implements AIEngine {
508
588
  const parts = messageParts.get(currentAssistantId) || [];
509
589
  const remainingParts = parts.filter(p => {
510
590
  if (p.type === 'tool') return !emittedToolParts.has(p.id);
591
+ if (p.type === 'subtask') return !emittedToolParts.has(p.id);
511
592
  if (p.type === 'reasoning') return false;
512
593
  return true;
513
594
  });
@@ -584,7 +665,10 @@ export class OpenCodeEngine implements AIEngine {
584
665
  }
585
666
 
586
667
  case 'session.error': {
587
- const { error } = (event as EventSessionError).properties;
668
+ const errorProps = (event as EventSessionError).properties;
669
+ // Only handle errors for our session (sessionID is optional on errors)
670
+ if (errorProps.sessionID && errorProps.sessionID !== sessionId) break;
671
+ const { error } = errorProps;
588
672
  // Extract a human-readable error message.
589
673
  // OpenCode SDK errors follow: { name: string, data: { message, statusCode?, providerID?, responseBody? } }
590
674
  // Don't prepend error class names (e.g. "APIError") — those are SDK