@myrialabs/clopen 0.1.7 → 0.1.9

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 (28) hide show
  1. package/backend/lib/database/migrations/023_create_user_unread_sessions_table.ts +32 -0
  2. package/backend/lib/database/migrations/index.ts +7 -0
  3. package/backend/lib/database/queries/session-queries.ts +37 -0
  4. package/backend/lib/git/git-service.ts +1 -0
  5. package/backend/ws/sessions/crud.ts +34 -2
  6. package/backend/ws/user/crud.ts +8 -4
  7. package/bun.lock +34 -12
  8. package/frontend/lib/components/common/MonacoEditor.svelte +6 -6
  9. package/frontend/lib/components/common/xterm/XTerm.svelte +27 -108
  10. package/frontend/lib/components/common/xterm/terminal-config.ts +2 -2
  11. package/frontend/lib/components/common/xterm/types.ts +1 -0
  12. package/frontend/lib/components/common/xterm/xterm-service.ts +69 -20
  13. package/frontend/lib/components/files/FileTree.svelte +4 -6
  14. package/frontend/lib/components/files/FileViewer.svelte +45 -101
  15. package/frontend/lib/components/git/CommitForm.svelte +1 -1
  16. package/frontend/lib/components/git/GitLog.svelte +141 -101
  17. package/frontend/lib/components/preview/browser/components/Toolbar.svelte +81 -72
  18. package/frontend/lib/components/settings/SettingsModal.svelte +1 -8
  19. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +3 -3
  20. package/frontend/lib/components/terminal/Terminal.svelte +1 -1
  21. package/frontend/lib/components/terminal/TerminalTabs.svelte +28 -26
  22. package/frontend/lib/components/workspace/PanelHeader.svelte +639 -623
  23. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +3 -2
  24. package/frontend/lib/components/workspace/panels/GitPanel.svelte +34 -92
  25. package/frontend/lib/stores/core/app.svelte.ts +46 -0
  26. package/frontend/lib/stores/core/sessions.svelte.ts +24 -3
  27. package/frontend/lib/stores/ui/workspace.svelte.ts +14 -14
  28. package/package.json +8 -6
@@ -0,0 +1,32 @@
1
+ import type { DatabaseConnection } from '$shared/types/database/connection';
2
+ import { debug } from '$shared/utils/logger';
3
+
4
+ export const description = 'Create user_unread_sessions table for persisting per-user unread session state';
5
+
6
+ export const up = (db: DatabaseConnection): void => {
7
+ debug.log('migration', 'Creating user_unread_sessions table...');
8
+
9
+ db.exec(`
10
+ CREATE TABLE IF NOT EXISTS user_unread_sessions (
11
+ user_id TEXT NOT NULL,
12
+ session_id TEXT NOT NULL,
13
+ project_id TEXT NOT NULL,
14
+ marked_at TEXT NOT NULL,
15
+ PRIMARY KEY (user_id, session_id),
16
+ FOREIGN KEY (session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE
17
+ )
18
+ `);
19
+
20
+ db.exec(`
21
+ CREATE INDEX IF NOT EXISTS idx_user_unread_sessions_user_project
22
+ ON user_unread_sessions(user_id, project_id)
23
+ `);
24
+
25
+ debug.log('migration', 'user_unread_sessions table created');
26
+ };
27
+
28
+ export const down = (db: DatabaseConnection): void => {
29
+ debug.log('migration', 'Dropping user_unread_sessions table...');
30
+ db.exec('DROP TABLE IF EXISTS user_unread_sessions');
31
+ debug.log('migration', 'user_unread_sessions table dropped');
32
+ };
@@ -21,6 +21,7 @@ import * as migration019 from './019_add_claude_account_to_sessions';
21
21
  import * as migration020 from './020_add_snapshot_tree_hash';
22
22
  import * as migration021 from './021_drop_prompt_templates_table';
23
23
  import * as migration022 from './022_add_snapshot_changes_column';
24
+ import * as migration023 from './023_create_user_unread_sessions_table';
24
25
 
25
26
  // Export all migrations in order
26
27
  export const migrations = [
@@ -155,6 +156,12 @@ export const migrations = [
155
156
  description: migration022.description,
156
157
  up: migration022.up,
157
158
  down: migration022.down
159
+ },
160
+ {
161
+ id: '023',
162
+ description: migration023.description,
163
+ up: migration023.up,
164
+ down: migration023.down
158
165
  }
159
166
  ];
160
167
 
@@ -267,5 +267,42 @@ export const sessionQueries = {
267
267
  SET head_message_id = ?
268
268
  WHERE session_id = ? AND branch_name = ?
269
269
  `).run(newHeadMessageId, sessionId, branchName);
270
+ },
271
+
272
+ // ==================== PER-USER UNREAD SESSION TRACKING ====================
273
+
274
+ /**
275
+ * Mark a session as unread for a specific user
276
+ */
277
+ markUnread(userId: string, sessionId: string, projectId: string): void {
278
+ const db = getDatabase();
279
+ const now = new Date().toISOString();
280
+ db.prepare(`
281
+ INSERT OR IGNORE INTO user_unread_sessions (user_id, session_id, project_id, marked_at)
282
+ VALUES (?, ?, ?, ?)
283
+ `).run(userId, sessionId, projectId, now);
284
+ },
285
+
286
+ /**
287
+ * Mark a session as read for a specific user
288
+ */
289
+ markRead(userId: string, sessionId: string): void {
290
+ const db = getDatabase();
291
+ db.prepare(`
292
+ DELETE FROM user_unread_sessions
293
+ WHERE user_id = ? AND session_id = ?
294
+ `).run(userId, sessionId);
295
+ },
296
+
297
+ /**
298
+ * Get all unread session IDs for a user within a project
299
+ * Returns array of { sessionId, projectId }
300
+ */
301
+ getUnreadSessions(userId: string, projectId: string): { session_id: string; project_id: string }[] {
302
+ const db = getDatabase();
303
+ return db.prepare(`
304
+ SELECT session_id, project_id FROM user_unread_sessions
305
+ WHERE user_id = ? AND project_id = ?
306
+ `).all(userId, projectId) as { session_id: string; project_id: string }[];
270
307
  }
271
308
  };
@@ -283,6 +283,7 @@ export class GitService {
283
283
 
284
284
  const args = [
285
285
  'log',
286
+ '--topo-order',
286
287
  `--format=${format}`,
287
288
  `--max-count=${limit + 1}`, // +1 to check if there are more
288
289
  `--skip=${skip}`
@@ -33,7 +33,11 @@ export const crudHandler = createRouter()
33
33
  started_at: t.String(),
34
34
  ended_at: t.Optional(t.String())
35
35
  })),
36
- currentSessionId: t.Optional(t.String())
36
+ currentSessionId: t.Optional(t.String()),
37
+ unreadSessionIds: t.Array(t.Object({
38
+ sessionId: t.String(),
39
+ projectId: t.String()
40
+ }))
37
41
  })
38
42
  }, async ({ conn }) => {
39
43
  const projectId = ws.getProjectId(conn);
@@ -43,6 +47,10 @@ export const crudHandler = createRouter()
43
47
  // Get the user's saved current session for this project
44
48
  const currentSessionId = projectQueries.getCurrentSessionId(userId, projectId);
45
49
 
50
+ // Get unread sessions for this user/project
51
+ const unreadRows = sessionQueries.getUnreadSessions(userId, projectId);
52
+ debug.log('session', `[unread] sessions:list — user=${userId}, project=${projectId}, unreadCount=${unreadRows.length}`, unreadRows);
53
+
46
54
  // Convert null to undefined for TypeScript optional fields
47
55
  return {
48
56
  sessions: sessions.map(session => ({
@@ -54,7 +62,8 @@ export const crudHandler = createRouter()
54
62
  current_head_message_id: session.current_head_message_id ?? undefined,
55
63
  ended_at: session.ended_at ?? undefined
56
64
  })),
57
- currentSessionId: currentSessionId ?? undefined
65
+ currentSessionId: currentSessionId ?? undefined,
66
+ unreadSessionIds: unreadRows.map(r => ({ sessionId: r.session_id, projectId: r.project_id }))
58
67
  };
59
68
  })
60
69
 
@@ -324,4 +333,27 @@ export const crudHandler = createRouter()
324
333
  const userId = ws.getUserId(conn);
325
334
  projectQueries.setCurrentSessionId(userId, projectId, data.sessionId);
326
335
  debug.log('session', `User ${userId} set current session to ${data.sessionId} in project ${projectId}`);
336
+ })
337
+
338
+ // Mark a session as read for the current user
339
+ .on('sessions:mark-read', {
340
+ data: t.Object({
341
+ sessionId: t.String()
342
+ })
343
+ }, async ({ data, conn }) => {
344
+ const userId = ws.getUserId(conn);
345
+ sessionQueries.markRead(userId, data.sessionId);
346
+ debug.log('session', `[unread] Marked session ${data.sessionId} as READ for user ${userId}`);
347
+ })
348
+
349
+ // Mark a session as unread for the current user
350
+ .on('sessions:mark-unread', {
351
+ data: t.Object({
352
+ sessionId: t.String(),
353
+ projectId: t.String()
354
+ })
355
+ }, async ({ data, conn }) => {
356
+ const userId = ws.getUserId(conn);
357
+ sessionQueries.markUnread(userId, data.sessionId, data.projectId);
358
+ debug.log('session', `[unread] Marked session ${data.sessionId} as UNREAD for user ${userId} in project ${data.projectId}`);
327
359
  });
@@ -108,7 +108,8 @@ export const crudHandler = createRouter()
108
108
  response: t.Object({
109
109
  currentProjectId: t.Union([t.String(), t.Null()]),
110
110
  lastView: t.Union([t.String(), t.Null()]),
111
- settings: t.Union([t.Any(), t.Null()])
111
+ settings: t.Union([t.Any(), t.Null()]),
112
+ unreadSessions: t.Union([t.Any(), t.Null()])
112
113
  })
113
114
  }, async ({ conn }) => {
114
115
  const userId = ws.getUserId(conn);
@@ -116,17 +117,20 @@ export const crudHandler = createRouter()
116
117
  const currentProjectId = getUserState(userId, 'currentProjectId') as string | null;
117
118
  const lastView = getUserState(userId, 'lastView') as string | null;
118
119
  const userSettings = getUserState(userId, 'settings');
120
+ const unreadSessions = getUserState(userId, 'unreadSessions');
119
121
 
120
122
  debug.log('user', `Restored state for ${userId}:`, {
121
123
  currentProjectId,
122
124
  lastView,
123
- hasSettings: !!userSettings
125
+ hasSettings: !!userSettings,
126
+ unreadSessionsCount: unreadSessions ? Object.keys(unreadSessions).length : 0
124
127
  });
125
128
 
126
129
  return {
127
130
  currentProjectId: currentProjectId ?? null,
128
131
  lastView: lastView ?? null,
129
- settings: userSettings ?? null
132
+ settings: userSettings ?? null,
133
+ unreadSessions: unreadSessions ?? null
130
134
  };
131
135
  })
132
136
 
@@ -143,7 +147,7 @@ export const crudHandler = createRouter()
143
147
  const userId = ws.getUserId(conn);
144
148
 
145
149
  // Validate allowed keys to prevent arbitrary data storage
146
- const allowedKeys = ['currentProjectId', 'lastView', 'settings'];
150
+ const allowedKeys = ['currentProjectId', 'lastView', 'settings', 'unreadSessions'];
147
151
  if (!allowedKeys.includes(data.key)) {
148
152
  throw new Error(`Invalid state key: ${data.key}. Allowed: ${allowedKeys.join(', ')}`);
149
153
  }
package/bun.lock CHANGED
@@ -13,8 +13,12 @@
13
13
  "@modelcontextprotocol/sdk": "^1.26.0",
14
14
  "@monaco-editor/loader": "^1.5.0",
15
15
  "@opencode-ai/sdk": "^1.2.15",
16
- "@xterm/addon-fit": "^0.10.0",
17
- "@xterm/addon-web-links": "^0.11.0",
16
+ "@xterm/addon-clipboard": "^0.2.0",
17
+ "@xterm/addon-fit": "^0.11.0",
18
+ "@xterm/addon-ligatures": "^0.10.0",
19
+ "@xterm/addon-unicode11": "^0.9.0",
20
+ "@xterm/addon-web-links": "^0.12.0",
21
+ "@xterm/xterm": "^6.0.0",
18
22
  "bun-pty": "^0.4.2",
19
23
  "cloudflared": "^0.7.1",
20
24
  "elysia": "^1.4.19",
@@ -26,7 +30,6 @@
26
30
  "puppeteer": "^24.33.0",
27
31
  "puppeteer-cluster": "^0.25.0",
28
32
  "qrcode": "^1.5.4",
29
- "xterm": "^5.3.0",
30
33
  },
31
34
  "devDependencies": {
32
35
  "@eslint/js": "^9.31.0",
@@ -35,7 +38,6 @@
35
38
  "@types/bun": "^1.2.18",
36
39
  "@types/node": "^24.0.14",
37
40
  "@types/qrcode": "^1.5.6",
38
- "@types/xterm": "^3.0.0",
39
41
  "concurrently": "^9.2.1",
40
42
  "eslint": "^9.31.0",
41
43
  "eslint-plugin-svelte": "^3.10.1",
@@ -310,8 +312,6 @@
310
312
 
311
313
  "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
312
314
 
313
- "@types/xterm": ["@types/xterm@3.0.0", "", { "dependencies": { "xterm": "*" } }, "sha512-+VaAJQmE7E1d1ebkIh/Zdc2mbXBVwxZGGSgqwzDPpk/HKo0mNT+iX5ZrnswztHSV+CDV+bURl7Yg7PWF7IZfXQ=="],
314
-
315
315
  "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
316
316
 
317
317
  "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
@@ -334,11 +334,17 @@
334
334
 
335
335
  "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
336
336
 
337
- "@xterm/addon-fit": ["@xterm/addon-fit@0.10.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ=="],
337
+ "@xterm/addon-clipboard": ["@xterm/addon-clipboard@0.2.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-Dl31BCtBhLaUEECUbEiVcCLvLBbaeGYdT7NofB8OJkGTD3MWgBsaLjXvfGAD4tQNHhm6mbKyYkR7XD8kiZsdNg=="],
338
+
339
+ "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="],
340
+
341
+ "@xterm/addon-ligatures": ["@xterm/addon-ligatures@0.10.0", "", { "dependencies": { "font-finder": "^1.1.0", "font-ligatures": "^1.4.1" } }, "sha512-/Few8ZSHMib7sGjRJoc5l7bCtEB9XJfkNofvPpOcWADxKaUl8og8P172j67OoACSNJAXqeCLIuvj8WFCBkcTxg=="],
342
+
343
+ "@xterm/addon-unicode11": ["@xterm/addon-unicode11@0.9.0", "", {}, "sha512-FxDnYcyuXhNl+XSqGZL/t0U9eiNb/q3EWT5rYkQT/zuig8Gz/VagnQANKHdDWFM2lTMk9ly0EFQxxxtZUoRetw=="],
338
344
 
339
- "@xterm/addon-web-links": ["@xterm/addon-web-links@0.11.0", "", { "peerDependencies": { "@xterm/xterm": "^5.0.0" } }, "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q=="],
345
+ "@xterm/addon-web-links": ["@xterm/addon-web-links@0.12.0", "", {}, "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw=="],
340
346
 
341
- "@xterm/xterm": ["@xterm/xterm@5.5.0", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],
347
+ "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],
342
348
 
343
349
  "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
344
350
 
@@ -562,6 +568,10 @@
562
568
 
563
569
  "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
564
570
 
571
+ "font-finder": ["font-finder@1.1.0", "", { "dependencies": { "get-system-fonts": "^2.0.0", "promise-stream-reader": "^1.0.1" } }, "sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw=="],
572
+
573
+ "font-ligatures": ["font-ligatures@1.4.1", "", { "dependencies": { "font-finder": "^1.0.3", "lru-cache": "^6.0.0", "opentype.js": "^0.8.0" } }, "sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw=="],
574
+
565
575
  "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
566
576
 
567
577
  "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
@@ -578,6 +588,8 @@
578
588
 
579
589
  "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
580
590
 
591
+ "get-system-fonts": ["get-system-fonts@2.0.2", "", {}, "sha512-zzlgaYnHMIEgHRrfC7x0Qp0Ylhw/sHpM6MHXeVBTYIsvGf5GpbnClB+Q6rAPdn+0gd2oZZIo6Tj3EaWrt4VhDQ=="],
592
+
581
593
  "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
582
594
 
583
595
  "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -638,6 +650,8 @@
638
650
 
639
651
  "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
640
652
 
653
+ "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="],
654
+
641
655
  "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
642
656
 
643
657
  "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
@@ -694,7 +708,7 @@
694
708
 
695
709
  "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
696
710
 
697
- "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
711
+ "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
698
712
 
699
713
  "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
700
714
 
@@ -742,6 +756,8 @@
742
756
 
743
757
  "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
744
758
 
759
+ "opentype.js": ["opentype.js@0.8.0", "", { "dependencies": { "tiny-inflate": "^1.0.2" }, "bin": { "ot": "./bin/ot" } }, "sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg=="],
760
+
745
761
  "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
746
762
 
747
763
  "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -790,6 +806,8 @@
790
806
 
791
807
  "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
792
808
 
809
+ "promise-stream-reader": ["promise-stream-reader@1.0.1", "", {}, "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg=="],
810
+
793
811
  "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
794
812
 
795
813
  "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
@@ -904,6 +922,8 @@
904
922
 
905
923
  "text-extensions": ["text-extensions@3.1.0", "", {}, "sha512-anOjtXr8OT5w4vc/2mP4AYTCE0GWc/21icGmaHtBHnI7pN7o01a/oqG9m06/rGzoAsDm/WNzggBpqptuCmRlZQ=="],
906
924
 
925
+ "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
926
+
907
927
  "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
908
928
 
909
929
  "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
@@ -958,10 +978,10 @@
958
978
 
959
979
  "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
960
980
 
961
- "xterm": ["xterm@5.3.0", "", {}, "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="],
962
-
963
981
  "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
964
982
 
983
+ "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
984
+
965
985
  "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
966
986
 
967
987
  "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -1012,6 +1032,8 @@
1012
1032
 
1013
1033
  "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
1014
1034
 
1035
+ "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
1036
+
1015
1037
  "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
1016
1038
 
1017
1039
  "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@@ -58,9 +58,9 @@
58
58
  indentGuide: '#21262d',
59
59
  indentGuideActive: '#30363d',
60
60
  ruler: '#21262d',
61
- scrollbar: '#21262d60',
62
- scrollbarHover: '#30363d80',
63
- scrollbarActive: '#6e7681'
61
+ scrollbar: '#6e768140',
62
+ scrollbarHover: '#6e768180',
63
+ scrollbarActive: '#8b949e'
64
64
  },
65
65
  tokens: {
66
66
  comment: '6A9955',
@@ -87,9 +87,9 @@
87
87
  indentGuide: '#e3e3e3',
88
88
  indentGuideActive: '#d3d3d3',
89
89
  ruler: '#e3e3e3',
90
- scrollbar: '#cccccc60',
91
- scrollbarHover: '#999999a0',
92
- scrollbarActive: '#666666'
90
+ scrollbar: '#92929240',
91
+ scrollbarHover: '#92929280',
92
+ scrollbarActive: '#555555'
93
93
  },
94
94
  tokens: {
95
95
  comment: '008000',
@@ -14,7 +14,7 @@
14
14
  import { settings } from '$frontend/lib/stores/features/settings.svelte';
15
15
 
16
16
  // Import CSS directly - Vite will handle it properly
17
- import 'xterm/css/xterm.css';
17
+ import '@xterm/xterm/css/xterm.css';
18
18
 
19
19
  // Props
20
20
  const {
@@ -183,127 +183,39 @@
183
183
  };
184
184
  }
185
185
 
186
- // Handle right-click copy/paste functionality
187
- function setupRightClickCopy() {
186
+ // Handle right-click copy/paste via clipboard addon
187
+ function setupClipboardHandling() {
188
188
  if (!terminalContainer || !xtermService.terminal) return;
189
189
 
190
- const handleRightClick = async (event: MouseEvent) => {
191
- event.preventDefault(); // Prevent default context menu
192
-
193
- // Get selected text from xterm.js
190
+ const handleContextMenu = async (event: MouseEvent) => {
191
+ event.preventDefault();
192
+
194
193
  const selectedText = xtermService.getSelectedText();
195
-
196
- if (selectedText && selectedText.trim()) {
194
+
195
+ if (selectedText?.trim()) {
197
196
  // Copy selected text to clipboard
198
197
  try {
199
198
  await navigator.clipboard.writeText(selectedText);
200
-
201
- // Clear selection after copy (like most terminals do)
202
199
  xtermService.clearSelection();
203
-
204
- // Show brief visual feedback
205
- showCopyFeedback();
206
- } catch (err) {
207
- }
200
+ } catch { /* clipboard not available */ }
208
201
  } else {
209
- // No text selected - paste from clipboard instead
202
+ // No text selected - paste from clipboard
210
203
  try {
211
- const clipboardText = await navigator.clipboard.readText();
212
- if (clipboardText && clipboardText.trim() && xtermService.isReady) {
213
-
214
- // Use terminal's built-in paste functionality
215
- // This simulates typing each character through the input handler
216
- if ((xtermService as any).inputHandler) {
217
- // Process each character through the input handler
218
- for (const char of clipboardText) {
219
- // Skip newlines - let user decide when to execute
220
- if (char !== '\n' && char !== '\r') {
221
- (xtermService as any).inputHandler(char);
222
- }
223
- }
224
- }
225
-
226
- // Show brief visual feedback
227
- showPasteFeedback();
204
+ const text = await navigator.clipboard.readText();
205
+ if (text?.trim()) {
206
+ xtermService.pasteText(text);
228
207
  }
229
- } catch {
230
- // paste not supported
231
- }
208
+ } catch { /* clipboard not available */ }
232
209
  }
233
210
  };
234
211
 
235
- // Add right-click event listener to terminal container
236
- terminalContainer.addEventListener('contextmenu', handleRightClick);
237
-
212
+ terminalContainer.addEventListener('contextmenu', handleContextMenu);
213
+
238
214
  return () => {
239
- terminalContainer?.removeEventListener('contextmenu', handleRightClick);
215
+ terminalContainer?.removeEventListener('contextmenu', handleContextMenu);
240
216
  };
241
217
  }
242
218
 
243
- // Show brief visual feedback for copy action
244
- function showCopyFeedback() {
245
- if (!terminalContainer) return;
246
-
247
- // Create temporary feedback element
248
- const feedback = document.createElement('div');
249
- feedback.textContent = 'Copied!';
250
- feedback.style.cssText = `
251
- position: absolute;
252
- top: 10px;
253
- right: 10px;
254
- background: rgb(34 197 94 / 0.9);
255
- color: white;
256
- padding: 4px 8px;
257
- border-radius: 4px;
258
- font-size: 12px;
259
- font-family: system-ui, sans-serif;
260
- z-index: 1000;
261
- pointer-events: none;
262
- `;
263
-
264
- terminalContainer.style.position = 'relative';
265
- terminalContainer.appendChild(feedback);
266
-
267
- // Remove feedback after 1 second
268
- setTimeout(() => {
269
- if (feedback.parentNode) {
270
- feedback.parentNode.removeChild(feedback);
271
- }
272
- }, 1000);
273
- }
274
-
275
- // Show brief visual feedback for paste action
276
- function showPasteFeedback() {
277
- if (!terminalContainer) return;
278
-
279
- // Create temporary feedback element
280
- const feedback = document.createElement('div');
281
- feedback.textContent = 'Pasted!';
282
- feedback.style.cssText = `
283
- position: absolute;
284
- top: 10px;
285
- right: 10px;
286
- background: rgba(59, 130, 246, 0.9);
287
- color: white;
288
- padding: 4px 8px;
289
- border-radius: 4px;
290
- font-size: 12px;
291
- font-family: system-ui, sans-serif;
292
- z-index: 1000;
293
- pointer-events: none;
294
- `;
295
-
296
- terminalContainer.style.position = 'relative';
297
- terminalContainer.appendChild(feedback);
298
-
299
- // Remove feedback after 1 second
300
- setTimeout(() => {
301
- if (feedback.parentNode) {
302
- feedback.parentNode.removeChild(feedback);
303
- }
304
- }, 1000);
305
- }
306
-
307
219
  // Handle theme changes
308
220
  function setupThemeHandling() {
309
221
  xtermService.updateTheme();
@@ -404,7 +316,9 @@
404
316
  $effect(() => {
405
317
  const size = settings.fontSize;
406
318
  if (isInitialized) {
407
- xtermService.updateFontSize(size, session?.id);
319
+ const fontSize = Math.round(size * 0.9);
320
+ const lineHeight = Math.round(size * 0.9);
321
+ xtermService.updateFontSize(fontSize, lineHeight, session?.id);
408
322
  }
409
323
  });
410
324
 
@@ -656,12 +570,12 @@
656
570
 
657
571
  const cleanupResize = setupResizeHandling();
658
572
  const cleanupTheme = setupThemeHandling();
659
- const cleanupRightClick = setupRightClickCopy();
573
+ const cleanupClipboard = setupClipboardHandling();
660
574
 
661
575
  return () => {
662
576
  cleanupResize();
663
577
  cleanupTheme();
664
- cleanupRightClick?.();
578
+ cleanupClipboard?.();
665
579
  };
666
580
  });
667
581
 
@@ -711,6 +625,11 @@
711
625
  export function clearSelection() {
712
626
  xtermService.clearSelection();
713
627
  }
628
+
629
+ export function pasteText(text: string) {
630
+ xtermService.pasteText(text);
631
+ }
632
+
714
633
  </script>
715
634
 
716
635
  <!-- Pure xterm.js terminal container -->
@@ -4,7 +4,7 @@
4
4
  * Centralized xterm.js configuration and utilities
5
5
  */
6
6
 
7
- import type { ITerminalOptions } from 'xterm';
7
+ import type { ITerminalOptions } from '@xterm/xterm';
8
8
 
9
9
  // Terminal theme configuration
10
10
  export const terminalConfig: ITerminalOptions = {
@@ -40,7 +40,7 @@ export const terminalConfig: ITerminalOptions = {
40
40
  convertEol: true,
41
41
  scrollback: 1000,
42
42
  tabStopWidth: 4,
43
- allowProposedApi: false,
43
+ allowProposedApi: true,
44
44
  altClickMovesCursor: true,
45
45
  disableStdin: false, // ✅ ENABLED for interactive PTY mode - stdin forwards to backend
46
46
  allowTransparency: false
@@ -23,6 +23,7 @@ export interface XTermMethods {
23
23
  writeData(data: string): void;
24
24
  getSelectedText(): string;
25
25
  clearSelection(): void;
26
+ pasteText(text: string): void;
26
27
  }
27
28
 
28
29
  export interface TerminalSize {