@myrialabs/clopen 0.2.12 → 0.2.14

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 (36) hide show
  1. package/backend/chat/stream-manager.ts +3 -0
  2. package/backend/engine/adapters/claude/stream.ts +2 -1
  3. package/backend/engine/types.ts +9 -0
  4. package/backend/mcp/config.ts +32 -6
  5. package/backend/snapshot/snapshot-service.ts +9 -7
  6. package/backend/terminal/stream-manager.ts +106 -155
  7. package/backend/ws/projects/crud.ts +3 -3
  8. package/backend/ws/snapshot/timeline.ts +6 -2
  9. package/backend/ws/terminal/persistence.ts +19 -33
  10. package/backend/ws/terminal/session.ts +37 -19
  11. package/bin/clopen.ts +376 -99
  12. package/bun.lock +6 -0
  13. package/frontend/components/chat/input/ChatInput.svelte +8 -0
  14. package/frontend/components/chat/input/components/LoadingIndicator.svelte +2 -2
  15. package/frontend/components/chat/input/composables/use-animations.svelte.ts +127 -111
  16. package/frontend/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -1
  17. package/frontend/components/chat/widgets/FloatingTodoList.svelte +2 -2
  18. package/frontend/components/checkpoint/TimelineModal.svelte +3 -1
  19. package/frontend/components/common/overlay/Dialog.svelte +2 -2
  20. package/frontend/components/git/ChangesSection.svelte +104 -13
  21. package/frontend/components/preview/browser/BrowserPreview.svelte +7 -0
  22. package/frontend/components/preview/browser/components/Canvas.svelte +8 -0
  23. package/frontend/components/settings/engines/AIEnginesSettings.svelte +1 -1
  24. package/frontend/components/settings/general/AuthModeSettings.svelte +2 -2
  25. package/frontend/components/terminal/Terminal.svelte +5 -1
  26. package/frontend/components/tunnel/TunnelInactive.svelte +4 -4
  27. package/frontend/services/chat/chat.service.ts +52 -11
  28. package/frontend/services/terminal/project.service.ts +4 -60
  29. package/frontend/services/terminal/terminal.service.ts +18 -27
  30. package/frontend/stores/core/sessions.svelte.ts +6 -0
  31. package/frontend/stores/ui/settings-modal.svelte.ts +1 -1
  32. package/frontend/stores/ui/theme.svelte.ts +11 -11
  33. package/frontend/stores/ui/workspace.svelte.ts +1 -1
  34. package/index.html +2 -2
  35. package/package.json +4 -2
  36. package/shared/utils/anonymous-user.ts +4 -4
@@ -15,7 +15,7 @@
15
15
  // Info box visibility - load from localStorage
16
16
  let showInfoBox = $state(
17
17
  typeof window !== 'undefined'
18
- ? localStorage.getItem('tunnel-info-dismissed') !== 'true'
18
+ ? localStorage.getItem('clopen-tunnel-info-dismissed') !== 'true'
19
19
  : true
20
20
  );
21
21
 
@@ -32,7 +32,7 @@
32
32
 
33
33
  // Save "don't show again" preference
34
34
  if (dontShowWarningAgain) {
35
- localStorage.setItem('tunnel-warning-dismissed', 'true');
35
+ localStorage.setItem('clopen-tunnel-warning-dismissed', 'true');
36
36
  }
37
37
 
38
38
  // Close modal immediately
@@ -59,7 +59,7 @@
59
59
  warningDismissed = false;
60
60
 
61
61
  // Check if user has dismissed warning permanently
62
- const securityWarningDismissed = localStorage.getItem('tunnel-warning-dismissed') === 'true';
62
+ const securityWarningDismissed = localStorage.getItem('clopen-tunnel-warning-dismissed') === 'true';
63
63
  if (securityWarningDismissed) {
64
64
  // Skip warning and start tunnel directly
65
65
  handleStartTunnel();
@@ -70,7 +70,7 @@
70
70
 
71
71
  function closeInfoBox() {
72
72
  showInfoBox = false;
73
- localStorage.setItem('tunnel-info-dismissed', 'true');
73
+ localStorage.setItem('clopen-tunnel-info-dismissed', 'true');
74
74
  }
75
75
 
76
76
  function dismissError() {
@@ -43,16 +43,14 @@ class ChatService {
43
43
 
44
44
  static loadingTexts: string[] = [
45
45
  'thinking', 'processing', 'analyzing', 'calculating', 'computing',
46
- 'strategizing', 'learningpatterns', 'updatingweights', 'finetuning',
47
- 'adaptingmodels', 'trainingnetworks', 'evaluatingoptions', 'planningactions',
48
- 'executingplans', 'simulatingscenarios', 'predictingoutcomes', 'scanningenvironment',
49
- 'monitoringsignals', 'processinginputs', 'adjustingparameters', 'optimizing',
50
- 'generatingresponses', 'refininglogic', 'recognizingpatterns', 'synthesizinginformation',
51
- 'runninginference', 'validatingoutputs', 'modulatingresponse', 'updatingmemory',
52
- 'switchingcontext', 'resolvingconflicts', 'allocatingresources', 'prioritizingtasks',
53
- 'developingawareness', 'buildingstrategies', 'assessingscenarios', 'integratingdata',
54
- 'bootingreasoning', 'activatingmodules', 'triggeringaction', 'deployinglogic',
55
- 'maintainingstate', 'clearingcache', 'updating', 'reflecting', 'syncinglogic',
46
+ 'strategizing', 'learningpatterns', 'adaptingmodels', 'evaluatingoptions',
47
+ 'executingplans', 'simulatingscenarios', 'predictingoutcomes', 'planningactions',
48
+ 'processinginputs', 'optimizing', 'generatingresponses', 'refininglogic',
49
+ 'validatingoutputs', 'modulatingresponse', 'updatingmemory', 'recognizingpatterns',
50
+ 'switchingcontext', 'allocatingresources', 'prioritizingtasks',
51
+ 'developingawareness', 'buildingstrategies', 'assessingscenarios',
52
+ 'bootingreasoning', 'triggeringaction', 'deployinglogic', 'synthesizinginformation',
53
+ 'maintainingstate', 'updating', 'reflecting', 'syncinglogic',
56
54
  'connectingdots', 'compilingideas', 'brainstorming', 'schedulingtasks'
57
55
  ].map(text => text + '...');
58
56
 
@@ -61,7 +59,50 @@ class ChatService {
61
59
  'Create a full-stack e-commerce platform with Next.js, Stripe, and PostgreSQL',
62
60
  'Build a real-time chat application using Socket.io with room support and typing indicators',
63
61
  'Create a SaaS dashboard with user management, billing, and analytics',
64
- // ... (keep the same placeholder texts as before)
62
+ 'Build a REST API with authentication, rate limiting, and Swagger documentation',
63
+ 'Create a CLI tool in TypeScript that scaffolds new projects with custom templates',
64
+ // Debugging & fixing
65
+ 'Debug a memory leak in a Node.js service causing it to crash every 24 hours',
66
+ 'Fix race conditions in a concurrent queue processor causing duplicate jobs',
67
+ 'Fix a CORS issue blocking requests between a frontend and backend on different origins',
68
+ 'Fix broken JWT refresh logic that logs users out unexpectedly',
69
+ 'Fix flaky tests that pass locally but fail randomly in CI',
70
+ // Code review & refactoring
71
+ 'Refactor a 1000-line monolithic function into clean, testable modules',
72
+ 'Convert a class-based React codebase to functional components with hooks',
73
+ 'Migrate a JavaScript project to TypeScript with strict mode enabled',
74
+ 'Refactor database queries to use an ORM with proper migrations',
75
+ 'Clean up and standardize error handling across an entire Express application',
76
+ // Writing tests
77
+ 'Write unit tests for a payment processing module with 100% coverage',
78
+ 'Add end-to-end tests using Playwright for a multi-step checkout flow',
79
+ 'Set up integration tests for a REST API using a real test database',
80
+ 'Write property-based tests to find edge cases in a data validation library',
81
+ 'Set up test coverage reporting and enforce a minimum threshold in CI',
82
+ // Performance & optimization
83
+ 'Optimize a slow PostgreSQL query that runs 10 seconds on a 5M-row table',
84
+ 'Implement Redis caching to reduce database load by 80%',
85
+ 'Reduce bundle size of a React app from 4MB to under 500KB',
86
+ 'Profile and optimize a Python data pipeline processing 1M records per hour',
87
+ 'Add lazy loading and virtualization to a list rendering 10,000 items',
88
+ // Architecture & design
89
+ 'Design a scalable event-driven architecture using Kafka for a high-traffic app',
90
+ 'Plan a migration from a monolith to microservices without downtime',
91
+ 'Design a multi-tenant SaaS architecture with data isolation per customer',
92
+ 'Create an authentication system supporting SSO, OAuth, and MFA',
93
+ 'Architect a real-time notification system using WebSockets and a message queue',
94
+ // DevOps & infrastructure
95
+ 'Write a Dockerfile and docker-compose setup for a full-stack app with hot reload',
96
+ 'Set up a GitHub Actions CI/CD pipeline with testing, linting, and auto-deploy',
97
+ 'Configure Nginx as a reverse proxy with SSL termination and load balancing',
98
+ 'Create Terraform scripts to provision a production-ready AWS infrastructure',
99
+ 'Set up monitoring and alerting using Prometheus and Grafana',
100
+ // AI & data
101
+ 'Build a RAG pipeline using LangChain, embeddings, and a vector database',
102
+ 'Create a sentiment analysis API using a fine-tuned transformer model',
103
+ 'Build a data scraper that extracts and structures product data at scale',
104
+ 'Implement a recommendation engine using collaborative filtering',
105
+ 'Create a real-time data dashboard ingesting from multiple streaming sources',
65
106
  ];
66
107
 
67
108
  constructor() {
@@ -262,66 +262,10 @@ class TerminalProjectManager {
262
262
  }
263
263
  }
264
264
 
265
- // Restore saved output for this session
266
- let baseOutput: any[] = [];
267
- let backgroundOutput: any[] = [];
268
-
269
- // First, restore base output from context (input/output sebelumnya)
270
- if (context.sessionOutputs.has(sessionId)) {
271
- const savedOutput = context.sessionOutputs.get(sessionId);
272
- if (savedOutput) {
273
- baseOutput = savedOutput.map(output => ({
274
- content: output.content,
275
- type: output.type as any,
276
- timestamp: output.timestamp
277
- }));
278
- // Restored base output lines for session
279
- }
280
- }
281
-
282
- // Second, get NEW output from server that was generated while we were away
283
- // We need to track when we saved the output to know what's new
284
- if (hasActiveStream && activeStreamInfo) {
285
- try {
286
- // Get the saved output count from context metadata
287
- // This tells us how much output we had when we switched away
288
- let savedOutputCount = 0;
289
- const savedMetadata = context.sessionOutputs.get(`${sessionId}-metadata`);
290
- if (savedMetadata && typeof savedMetadata === 'object' && 'outputCount' in savedMetadata) {
291
- savedOutputCount = (savedMetadata as any).outputCount || 0;
292
- } else {
293
- // Fallback: count actual output lines in baseOutput
294
- for (const line of baseOutput) {
295
- if (line.type === 'output' || line.type === 'error') {
296
- savedOutputCount++;
297
- }
298
- }
299
- }
300
-
301
- // Get only NEW output from server (skip what we already have)
302
- const data = await terminalService.getMissedOutput(
303
- sessionId,
304
- activeStreamInfo.streamId,
305
- savedOutputCount
306
- );
307
- if (data.success && data.output && data.output.length > 0) {
308
- // Convert server output to terminal lines
309
- backgroundOutput = data.output.map((content: string) => ({
310
- content: content,
311
- type: 'output',
312
- timestamp: new Date()
313
- }));
314
- debug.log('terminal', `Restored ${backgroundOutput.length} new output lines for session ${sessionId}`);
315
- }
316
- } catch (error) {
317
- debug.error('terminal', 'Failed to fetch missed output:', error);
318
- }
319
- }
320
-
321
- // Combine base output with background output from server
322
- // Base output contains previous input/output saved in context
323
- // Background output contains new output from server (if any)
324
- terminalSession.lines = [...baseOutput, ...backgroundOutput];
265
+ // Always start with empty lines.
266
+ // The create-session replay from headless xterm will provide the
267
+ // accurate current terminal state (respects clear, scrollback, etc.)
268
+ terminalSession.lines = [];
325
269
 
326
270
  // Restore command history from context (persisted) rather than manager (temporary)
327
271
  const savedCommandHistory = context.sessionCommandHistories.get(sessionId);
@@ -62,20 +62,6 @@ export class TerminalService {
62
62
  // Create unique stream ID for this connection
63
63
  const streamId = `${sessionId}-${Date.now()}`;
64
64
 
65
- // Get current output count to mark where new output starts
66
- let outputStartIndex = 0;
67
- if (typeof window !== 'undefined') {
68
- try {
69
- const terminalStoreModule = await import('$frontend/stores/features/terminal.svelte');
70
- const termSession = terminalStoreModule.terminalStore.getSession(sessionId);
71
- if (termSession && termSession.lines) {
72
- outputStartIndex = termSession.lines.length;
73
- }
74
- } catch {
75
- // Ignore error, use default 0
76
- }
77
- }
78
-
79
65
  // Setup WebSocket listeners for this session
80
66
  const listeners: Array<() => void> = [];
81
67
 
@@ -173,8 +159,7 @@ export class TerminalService {
173
159
  workingDirectory: session.workingDirectory,
174
160
  projectPath,
175
161
  cols: terminalSize?.cols || 80,
176
- rows: terminalSize?.rows || 24,
177
- outputStartIndex
162
+ rows: terminalSize?.rows || 24
178
163
  });
179
164
 
180
165
  debug.log('terminal', `✅ Terminal session created:`, response);
@@ -230,6 +215,17 @@ export class TerminalService {
230
215
  }
231
216
  }
232
217
 
218
+ /**
219
+ * Clear headless terminal on backend (sync with frontend clear)
220
+ */
221
+ async clearHeadlessTerminal(sessionId: string): Promise<void> {
222
+ try {
223
+ await ws.http('terminal:clear', { sessionId });
224
+ } catch {
225
+ // Silently handle - non-critical
226
+ }
227
+ }
228
+
233
229
  /**
234
230
  * Resize terminal for a specific session
235
231
  */
@@ -301,39 +297,34 @@ export class TerminalService {
301
297
  }
302
298
 
303
299
  /**
304
- * Get missed output for a session
300
+ * Get missed output for a session (serialized terminal state)
305
301
  */
306
302
  async getMissedOutput(
307
303
  sessionId: string,
308
- streamId?: string,
309
- fromIndex: number = 0
304
+ streamId?: string
310
305
  ): Promise<{
311
306
  success: boolean;
312
- output: string[];
313
- outputCount: number;
307
+ output: string;
314
308
  status: string;
315
309
  }> {
316
310
  try {
317
- const data = await ws.http('terminal:missed-output', { sessionId, streamId, fromIndex }, 5000);
311
+ const data = await ws.http('terminal:missed-output', { sessionId, streamId }, 5000);
318
312
  if (data.sessionId === sessionId) {
319
313
  return {
320
314
  success: true,
321
315
  output: data.output,
322
- outputCount: data.outputCount,
323
316
  status: data.status
324
317
  };
325
318
  }
326
319
  return {
327
320
  success: false,
328
- output: [],
329
- outputCount: 0,
321
+ output: '',
330
322
  status: 'invalid_session'
331
323
  };
332
324
  } catch {
333
325
  return {
334
326
  success: false,
335
- output: [],
336
- outputCount: 0,
327
+ output: '',
337
328
  status: 'timeout'
338
329
  };
339
330
  }
@@ -436,6 +436,12 @@ export async function initializeSessions() {
436
436
  setupCollaborativeListeners();
437
437
  setupEditModeListener();
438
438
 
439
+ // Skip loading if no project is active — both calls require WS project context
440
+ if (!projectState.currentProject) {
441
+ debug.log('session', 'No active project, skipping session load');
442
+ return;
443
+ }
444
+
439
445
  // Load sessions and restore edit mode in parallel
440
446
  // Both only need WS project context (already set by initializeProjects)
441
447
  await Promise.all([
@@ -50,7 +50,7 @@ export const settingsSections: SettingsSectionMeta[] = [
50
50
  },
51
51
  {
52
52
  id: 'account',
53
- label: 'Account',
53
+ label: 'User Profile',
54
54
  icon: 'lucide:user',
55
55
  description: 'Your profile and access'
56
56
  },
@@ -3,7 +3,7 @@ import type { Theme } from '$shared/types/ui';
3
3
  // Theme store using Svelte 5 runes
4
4
  export const themeStore = $state({
5
5
  current: {
6
- name: 'claude-modern',
6
+ name: 'clopen-modern',
7
7
  primary: '#D97757',
8
8
  secondary: '#4F46E5',
9
9
  background: '#F9FAFB',
@@ -26,7 +26,7 @@ export function isDarkMode() {
26
26
  // Theme presets
27
27
  export const themes: Theme[] = [
28
28
  {
29
- name: 'claude-modern',
29
+ name: 'clopen-modern',
30
30
  primary: '#D97757',
31
31
  secondary: '#4F46E5',
32
32
  background: '#F9FAFB',
@@ -34,7 +34,7 @@ export const themes: Theme[] = [
34
34
  mode: 'light'
35
35
  },
36
36
  {
37
- name: 'claude-dark',
37
+ name: 'clopen-dark',
38
38
  primary: '#D97757',
39
39
  secondary: '#4F46E5',
40
40
  background: '#111827',
@@ -83,7 +83,7 @@ export function setTheme(theme: Theme) {
83
83
  }
84
84
 
85
85
  // Save to localStorage
86
- localStorage.setItem('claude-theme', JSON.stringify(theme));
86
+ localStorage.setItem('clopen-theme', JSON.stringify(theme));
87
87
  }
88
88
 
89
89
  // Helper function to update theme color meta tag
@@ -105,12 +105,12 @@ export function toggleDarkMode() {
105
105
  const newMode: 'light' | 'dark' = themeStore.isDark ? 'light' : 'dark';
106
106
 
107
107
  // Use predefined themes for consistency
108
- const newTheme = newMode === 'dark' ? themes[1] : themes[0]; // claude-dark or claude-modern
108
+ const newTheme = newMode === 'dark' ? themes[1] : themes[0]; // clopen-dark or clopen-modern
109
109
 
110
110
  setTheme(newTheme);
111
111
 
112
112
  // Mark as manual theme choice
113
- localStorage.setItem('claude-theme-manual', 'true');
113
+ localStorage.setItem('clopen-theme-manual', 'true');
114
114
  }
115
115
 
116
116
  export function initializeTheme() {
@@ -121,8 +121,8 @@ export function initializeTheme() {
121
121
  themeStore.isSystemDark = isSystemDark;
122
122
 
123
123
  // Check for saved theme preference
124
- const savedTheme = localStorage.getItem('claude-theme');
125
- const isManualTheme = localStorage.getItem('claude-theme-manual') === 'true';
124
+ const savedTheme = localStorage.getItem('clopen-theme');
125
+ const isManualTheme = localStorage.getItem('clopen-theme-manual') === 'true';
126
126
 
127
127
  let initialTheme: Theme;
128
128
 
@@ -160,7 +160,7 @@ export function initializeTheme() {
160
160
  themeStore.isSystemDark = e.matches;
161
161
 
162
162
  // Only follow system if no manual theme was set
163
- if (!localStorage.getItem('claude-theme-manual')) {
163
+ if (!localStorage.getItem('clopen-theme-manual')) {
164
164
  const newTheme = e.matches ? themes[1] : themes[0];
165
165
  setTheme(newTheme);
166
166
  }
@@ -169,11 +169,11 @@ export function initializeTheme() {
169
169
 
170
170
  export function setManualTheme(theme: Theme) {
171
171
  setTheme(theme);
172
- localStorage.setItem('claude-theme-manual', 'true');
172
+ localStorage.setItem('clopen-theme-manual', 'true');
173
173
  }
174
174
 
175
175
  export function useSystemTheme() {
176
- localStorage.removeItem('claude-theme-manual');
176
+ localStorage.removeItem('clopen-theme-manual');
177
177
  const defaultTheme = themeStore.isSystemDark ? themes[1] : themes[0];
178
178
  setTheme(defaultTheme);
179
179
  }
@@ -751,7 +751,7 @@ export function setActiveMobilePanel(panelId: PanelId): void {
751
751
  // PERSISTENCE
752
752
  // ============================================
753
753
 
754
- const STORAGE_KEY = 'claude-workspace-layout';
754
+ const STORAGE_KEY = 'clopen-workspace-layout';
755
755
 
756
756
  export function saveWorkspaceState(): void {
757
757
  try {
package/index.html CHANGED
@@ -20,8 +20,8 @@
20
20
  (function() {
21
21
  // Check for saved theme preference
22
22
  try {
23
- const savedTheme = localStorage.getItem('claude-theme');
24
- const isManualTheme = localStorage.getItem('claude-theme-manual') === 'true';
23
+ const savedTheme = localStorage.getItem('clopen-theme');
24
+ const isManualTheme = localStorage.getItem('clopen-theme-manual') === 'true';
25
25
 
26
26
  let isDark = false;
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myrialabs/clopen",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
5
5
  "author": "Myria Labs",
6
6
  "license": "MIT",
@@ -77,17 +77,19 @@
77
77
  "dependencies": {
78
78
  "@anthropic-ai/claude-agent-sdk": "0.2.63",
79
79
  "@anthropic-ai/sdk": "0.78.0",
80
- "@opencode-ai/sdk": "1.2.15",
81
80
  "@elysiajs/cors": "^1.4.0",
82
81
  "@iconify-json/lucide": "^1.2.57",
83
82
  "@iconify-json/material-icon-theme": "^1.2.16",
84
83
  "@modelcontextprotocol/sdk": "^1.26.0",
85
84
  "@monaco-editor/loader": "^1.5.0",
85
+ "@opencode-ai/sdk": "1.2.15",
86
86
  "@xterm/addon-clipboard": "^0.2.0",
87
87
  "@xterm/addon-fit": "^0.11.0",
88
88
  "@xterm/addon-ligatures": "^0.10.0",
89
+ "@xterm/addon-serialize": "^0.14.0",
89
90
  "@xterm/addon-unicode11": "^0.9.0",
90
91
  "@xterm/addon-web-links": "^0.12.0",
92
+ "@xterm/headless": "^6.0.0",
91
93
  "@xterm/xterm": "^6.0.0",
92
94
  "bun-pty": "^0.4.2",
93
95
  "cloudflared": "^0.7.1",
@@ -56,7 +56,7 @@ export async function getOrCreateAnonymousUser(): Promise<AnonymousUser | null>
56
56
  return null;
57
57
  }
58
58
 
59
- const stored = localStorage.getItem('claude-anonymous-user');
59
+ const stored = localStorage.getItem('clopen-anonymous-user');
60
60
 
61
61
  if (stored) {
62
62
  try {
@@ -76,7 +76,7 @@ export async function getOrCreateAnonymousUser(): Promise<AnonymousUser | null>
76
76
  const newUser = await generateAnonymousUserFromServer();
77
77
 
78
78
  if (newUser) {
79
- localStorage.setItem('claude-anonymous-user', JSON.stringify(newUser));
79
+ localStorage.setItem('clopen-anonymous-user', JSON.stringify(newUser));
80
80
  return newUser;
81
81
  }
82
82
 
@@ -93,7 +93,7 @@ export function getCurrentAnonymousUser(): AnonymousUser | null {
93
93
  return null;
94
94
  }
95
95
 
96
- const stored = localStorage.getItem('claude-anonymous-user');
96
+ const stored = localStorage.getItem('clopen-anonymous-user');
97
97
 
98
98
  if (stored) {
99
99
  try {
@@ -146,7 +146,7 @@ export async function updateAnonymousUserName(newName: string): Promise<Anonymou
146
146
  });
147
147
 
148
148
  // Save to localStorage
149
- localStorage.setItem('claude-anonymous-user', JSON.stringify(response));
149
+ localStorage.setItem('clopen-anonymous-user', JSON.stringify(response));
150
150
  debug.log('user', '✅ Updated user name:', response.name);
151
151
  return response;
152
152
  } catch (error) {