thepopebot 1.2.72 → 1.2.73-beta.1
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.
- package/config/index.js +1 -1
- package/drizzle/0003_rename_code_workspaces.sql +5 -0
- package/drizzle/meta/0003_snapshot.json +419 -0
- package/drizzle/meta/_journal.json +7 -0
- package/lib/ai/index.js +3 -3
- package/lib/ai/tools.js +8 -5
- package/lib/chat/actions.js +16 -13
- package/lib/chat/components/chat-page.js +2 -2
- package/lib/chat/components/chat-page.jsx +2 -2
- package/lib/chat/components/chats-page.js +4 -4
- package/lib/chat/components/chats-page.jsx +4 -4
- package/lib/chat/components/code-mode-toggle.js +3 -3
- package/lib/chat/components/code-mode-toggle.jsx +3 -3
- package/lib/chat/components/sidebar-history-item.js +4 -4
- package/lib/chat/components/sidebar-history-item.jsx +4 -4
- package/lib/code/actions.js +26 -26
- package/lib/code/code-page.js +5 -5
- package/lib/code/code-page.jsx +5 -5
- package/lib/code/ws-proxy.js +6 -6
- package/lib/db/chats.js +4 -4
- package/lib/db/{claude-workspaces.js → code-workspaces.js} +29 -27
- package/lib/db/schema.js +4 -3
- package/lib/tools/docker.js +9 -4
- package/package.json +1 -1
- package/templates/app/code/{[claudeWorkspaceId] → [codeWorkspaceId]}/page.js +2 -2
- package/templates/docker/{claude-workspace → claude-code-workspace}/Dockerfile +7 -7
- package/templates/docker/{claude-workspace → claude-code-workspace}/entrypoint.sh +2 -2
- /package/templates/docker/{claude-workspace → claude-code-workspace}/.tmux.conf +0 -0
package/config/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export function withThepopebot(nextConfig = {}) {
|
|
|
15
15
|
distDir: process.env.NEXT_BUILD_DIR || '.next',
|
|
16
16
|
env: {
|
|
17
17
|
...nextConfig.env,
|
|
18
|
-
|
|
18
|
+
NEXT_PUBLIC_CODE_WORKSPACE: process.env.CLAUDE_CODE_OAUTH_TOKEN && process.env.BETA ? 'true' : '',
|
|
19
19
|
},
|
|
20
20
|
serverExternalPackages: [
|
|
21
21
|
...(nextConfig.serverExternalPackages || []),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
ALTER TABLE `claude_workspaces` RENAME TO `code_workspaces`;--> statement-breakpoint
|
|
2
|
+
DROP INDEX IF EXISTS `claude_workspaces_container_name_unique`;--> statement-breakpoint
|
|
3
|
+
CREATE UNIQUE INDEX `code_workspaces_container_name_unique` ON `code_workspaces` (`container_name`);--> statement-breakpoint
|
|
4
|
+
ALTER TABLE `code_workspaces` ADD `coding_agent` text NOT NULL DEFAULT 'claude-code';--> statement-breakpoint
|
|
5
|
+
ALTER TABLE `chats` RENAME COLUMN `claude_workspace_id` TO `code_workspace_id`;
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
5
|
+
"prevId": "393d6739-2717-4020-bedb-8a9e1ed756da",
|
|
6
|
+
"tables": {
|
|
7
|
+
"chats": {
|
|
8
|
+
"name": "chats",
|
|
9
|
+
"columns": {
|
|
10
|
+
"id": {
|
|
11
|
+
"name": "id",
|
|
12
|
+
"type": "text",
|
|
13
|
+
"primaryKey": true,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": false
|
|
16
|
+
},
|
|
17
|
+
"user_id": {
|
|
18
|
+
"name": "user_id",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"title": {
|
|
25
|
+
"name": "title",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"autoincrement": false,
|
|
30
|
+
"default": "'New Chat'"
|
|
31
|
+
},
|
|
32
|
+
"starred": {
|
|
33
|
+
"name": "starred",
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"primaryKey": false,
|
|
36
|
+
"notNull": true,
|
|
37
|
+
"autoincrement": false,
|
|
38
|
+
"default": 0
|
|
39
|
+
},
|
|
40
|
+
"code_workspace_id": {
|
|
41
|
+
"name": "code_workspace_id",
|
|
42
|
+
"type": "text",
|
|
43
|
+
"primaryKey": false,
|
|
44
|
+
"notNull": false,
|
|
45
|
+
"autoincrement": false
|
|
46
|
+
},
|
|
47
|
+
"created_at": {
|
|
48
|
+
"name": "created_at",
|
|
49
|
+
"type": "integer",
|
|
50
|
+
"primaryKey": false,
|
|
51
|
+
"notNull": true,
|
|
52
|
+
"autoincrement": false
|
|
53
|
+
},
|
|
54
|
+
"updated_at": {
|
|
55
|
+
"name": "updated_at",
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"primaryKey": false,
|
|
58
|
+
"notNull": true,
|
|
59
|
+
"autoincrement": false
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"indexes": {},
|
|
63
|
+
"foreignKeys": {},
|
|
64
|
+
"compositePrimaryKeys": {},
|
|
65
|
+
"uniqueConstraints": {},
|
|
66
|
+
"checkConstraints": {}
|
|
67
|
+
},
|
|
68
|
+
"code_workspaces": {
|
|
69
|
+
"name": "code_workspaces",
|
|
70
|
+
"columns": {
|
|
71
|
+
"id": {
|
|
72
|
+
"name": "id",
|
|
73
|
+
"type": "text",
|
|
74
|
+
"primaryKey": true,
|
|
75
|
+
"notNull": true,
|
|
76
|
+
"autoincrement": false
|
|
77
|
+
},
|
|
78
|
+
"user_id": {
|
|
79
|
+
"name": "user_id",
|
|
80
|
+
"type": "text",
|
|
81
|
+
"primaryKey": false,
|
|
82
|
+
"notNull": true,
|
|
83
|
+
"autoincrement": false
|
|
84
|
+
},
|
|
85
|
+
"container_name": {
|
|
86
|
+
"name": "container_name",
|
|
87
|
+
"type": "text",
|
|
88
|
+
"primaryKey": false,
|
|
89
|
+
"notNull": false,
|
|
90
|
+
"autoincrement": false
|
|
91
|
+
},
|
|
92
|
+
"repo": {
|
|
93
|
+
"name": "repo",
|
|
94
|
+
"type": "text",
|
|
95
|
+
"primaryKey": false,
|
|
96
|
+
"notNull": false,
|
|
97
|
+
"autoincrement": false
|
|
98
|
+
},
|
|
99
|
+
"branch": {
|
|
100
|
+
"name": "branch",
|
|
101
|
+
"type": "text",
|
|
102
|
+
"primaryKey": false,
|
|
103
|
+
"notNull": false,
|
|
104
|
+
"autoincrement": false
|
|
105
|
+
},
|
|
106
|
+
"title": {
|
|
107
|
+
"name": "title",
|
|
108
|
+
"type": "text",
|
|
109
|
+
"primaryKey": false,
|
|
110
|
+
"notNull": true,
|
|
111
|
+
"autoincrement": false,
|
|
112
|
+
"default": "'Code Workspace'"
|
|
113
|
+
},
|
|
114
|
+
"coding_agent": {
|
|
115
|
+
"name": "coding_agent",
|
|
116
|
+
"type": "text",
|
|
117
|
+
"primaryKey": false,
|
|
118
|
+
"notNull": true,
|
|
119
|
+
"autoincrement": false,
|
|
120
|
+
"default": "'claude-code'"
|
|
121
|
+
},
|
|
122
|
+
"starred": {
|
|
123
|
+
"name": "starred",
|
|
124
|
+
"type": "integer",
|
|
125
|
+
"primaryKey": false,
|
|
126
|
+
"notNull": true,
|
|
127
|
+
"autoincrement": false,
|
|
128
|
+
"default": 0
|
|
129
|
+
},
|
|
130
|
+
"created_at": {
|
|
131
|
+
"name": "created_at",
|
|
132
|
+
"type": "integer",
|
|
133
|
+
"primaryKey": false,
|
|
134
|
+
"notNull": true,
|
|
135
|
+
"autoincrement": false
|
|
136
|
+
},
|
|
137
|
+
"updated_at": {
|
|
138
|
+
"name": "updated_at",
|
|
139
|
+
"type": "integer",
|
|
140
|
+
"primaryKey": false,
|
|
141
|
+
"notNull": true,
|
|
142
|
+
"autoincrement": false
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"indexes": {
|
|
146
|
+
"code_workspaces_container_name_unique": {
|
|
147
|
+
"name": "code_workspaces_container_name_unique",
|
|
148
|
+
"columns": [
|
|
149
|
+
"container_name"
|
|
150
|
+
],
|
|
151
|
+
"isUnique": true
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"foreignKeys": {},
|
|
155
|
+
"compositePrimaryKeys": {},
|
|
156
|
+
"uniqueConstraints": {},
|
|
157
|
+
"checkConstraints": {}
|
|
158
|
+
},
|
|
159
|
+
"messages": {
|
|
160
|
+
"name": "messages",
|
|
161
|
+
"columns": {
|
|
162
|
+
"id": {
|
|
163
|
+
"name": "id",
|
|
164
|
+
"type": "text",
|
|
165
|
+
"primaryKey": true,
|
|
166
|
+
"notNull": true,
|
|
167
|
+
"autoincrement": false
|
|
168
|
+
},
|
|
169
|
+
"chat_id": {
|
|
170
|
+
"name": "chat_id",
|
|
171
|
+
"type": "text",
|
|
172
|
+
"primaryKey": false,
|
|
173
|
+
"notNull": true,
|
|
174
|
+
"autoincrement": false
|
|
175
|
+
},
|
|
176
|
+
"role": {
|
|
177
|
+
"name": "role",
|
|
178
|
+
"type": "text",
|
|
179
|
+
"primaryKey": false,
|
|
180
|
+
"notNull": true,
|
|
181
|
+
"autoincrement": false
|
|
182
|
+
},
|
|
183
|
+
"content": {
|
|
184
|
+
"name": "content",
|
|
185
|
+
"type": "text",
|
|
186
|
+
"primaryKey": false,
|
|
187
|
+
"notNull": true,
|
|
188
|
+
"autoincrement": false
|
|
189
|
+
},
|
|
190
|
+
"created_at": {
|
|
191
|
+
"name": "created_at",
|
|
192
|
+
"type": "integer",
|
|
193
|
+
"primaryKey": false,
|
|
194
|
+
"notNull": true,
|
|
195
|
+
"autoincrement": false
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
"indexes": {},
|
|
199
|
+
"foreignKeys": {},
|
|
200
|
+
"compositePrimaryKeys": {},
|
|
201
|
+
"uniqueConstraints": {},
|
|
202
|
+
"checkConstraints": {}
|
|
203
|
+
},
|
|
204
|
+
"notifications": {
|
|
205
|
+
"name": "notifications",
|
|
206
|
+
"columns": {
|
|
207
|
+
"id": {
|
|
208
|
+
"name": "id",
|
|
209
|
+
"type": "text",
|
|
210
|
+
"primaryKey": true,
|
|
211
|
+
"notNull": true,
|
|
212
|
+
"autoincrement": false
|
|
213
|
+
},
|
|
214
|
+
"notification": {
|
|
215
|
+
"name": "notification",
|
|
216
|
+
"type": "text",
|
|
217
|
+
"primaryKey": false,
|
|
218
|
+
"notNull": true,
|
|
219
|
+
"autoincrement": false
|
|
220
|
+
},
|
|
221
|
+
"payload": {
|
|
222
|
+
"name": "payload",
|
|
223
|
+
"type": "text",
|
|
224
|
+
"primaryKey": false,
|
|
225
|
+
"notNull": true,
|
|
226
|
+
"autoincrement": false
|
|
227
|
+
},
|
|
228
|
+
"read": {
|
|
229
|
+
"name": "read",
|
|
230
|
+
"type": "integer",
|
|
231
|
+
"primaryKey": false,
|
|
232
|
+
"notNull": true,
|
|
233
|
+
"autoincrement": false,
|
|
234
|
+
"default": 0
|
|
235
|
+
},
|
|
236
|
+
"created_at": {
|
|
237
|
+
"name": "created_at",
|
|
238
|
+
"type": "integer",
|
|
239
|
+
"primaryKey": false,
|
|
240
|
+
"notNull": true,
|
|
241
|
+
"autoincrement": false
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
"indexes": {},
|
|
245
|
+
"foreignKeys": {},
|
|
246
|
+
"compositePrimaryKeys": {},
|
|
247
|
+
"uniqueConstraints": {},
|
|
248
|
+
"checkConstraints": {}
|
|
249
|
+
},
|
|
250
|
+
"settings": {
|
|
251
|
+
"name": "settings",
|
|
252
|
+
"columns": {
|
|
253
|
+
"id": {
|
|
254
|
+
"name": "id",
|
|
255
|
+
"type": "text",
|
|
256
|
+
"primaryKey": true,
|
|
257
|
+
"notNull": true,
|
|
258
|
+
"autoincrement": false
|
|
259
|
+
},
|
|
260
|
+
"type": {
|
|
261
|
+
"name": "type",
|
|
262
|
+
"type": "text",
|
|
263
|
+
"primaryKey": false,
|
|
264
|
+
"notNull": true,
|
|
265
|
+
"autoincrement": false
|
|
266
|
+
},
|
|
267
|
+
"key": {
|
|
268
|
+
"name": "key",
|
|
269
|
+
"type": "text",
|
|
270
|
+
"primaryKey": false,
|
|
271
|
+
"notNull": true,
|
|
272
|
+
"autoincrement": false
|
|
273
|
+
},
|
|
274
|
+
"value": {
|
|
275
|
+
"name": "value",
|
|
276
|
+
"type": "text",
|
|
277
|
+
"primaryKey": false,
|
|
278
|
+
"notNull": true,
|
|
279
|
+
"autoincrement": false
|
|
280
|
+
},
|
|
281
|
+
"created_by": {
|
|
282
|
+
"name": "created_by",
|
|
283
|
+
"type": "text",
|
|
284
|
+
"primaryKey": false,
|
|
285
|
+
"notNull": false,
|
|
286
|
+
"autoincrement": false
|
|
287
|
+
},
|
|
288
|
+
"created_at": {
|
|
289
|
+
"name": "created_at",
|
|
290
|
+
"type": "integer",
|
|
291
|
+
"primaryKey": false,
|
|
292
|
+
"notNull": true,
|
|
293
|
+
"autoincrement": false
|
|
294
|
+
},
|
|
295
|
+
"updated_at": {
|
|
296
|
+
"name": "updated_at",
|
|
297
|
+
"type": "integer",
|
|
298
|
+
"primaryKey": false,
|
|
299
|
+
"notNull": true,
|
|
300
|
+
"autoincrement": false
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
"indexes": {},
|
|
304
|
+
"foreignKeys": {},
|
|
305
|
+
"compositePrimaryKeys": {},
|
|
306
|
+
"uniqueConstraints": {},
|
|
307
|
+
"checkConstraints": {}
|
|
308
|
+
},
|
|
309
|
+
"subscriptions": {
|
|
310
|
+
"name": "subscriptions",
|
|
311
|
+
"columns": {
|
|
312
|
+
"id": {
|
|
313
|
+
"name": "id",
|
|
314
|
+
"type": "text",
|
|
315
|
+
"primaryKey": true,
|
|
316
|
+
"notNull": true,
|
|
317
|
+
"autoincrement": false
|
|
318
|
+
},
|
|
319
|
+
"platform": {
|
|
320
|
+
"name": "platform",
|
|
321
|
+
"type": "text",
|
|
322
|
+
"primaryKey": false,
|
|
323
|
+
"notNull": true,
|
|
324
|
+
"autoincrement": false
|
|
325
|
+
},
|
|
326
|
+
"channel_id": {
|
|
327
|
+
"name": "channel_id",
|
|
328
|
+
"type": "text",
|
|
329
|
+
"primaryKey": false,
|
|
330
|
+
"notNull": true,
|
|
331
|
+
"autoincrement": false
|
|
332
|
+
},
|
|
333
|
+
"created_at": {
|
|
334
|
+
"name": "created_at",
|
|
335
|
+
"type": "integer",
|
|
336
|
+
"primaryKey": false,
|
|
337
|
+
"notNull": true,
|
|
338
|
+
"autoincrement": false
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
"indexes": {},
|
|
342
|
+
"foreignKeys": {},
|
|
343
|
+
"compositePrimaryKeys": {},
|
|
344
|
+
"uniqueConstraints": {},
|
|
345
|
+
"checkConstraints": {}
|
|
346
|
+
},
|
|
347
|
+
"users": {
|
|
348
|
+
"name": "users",
|
|
349
|
+
"columns": {
|
|
350
|
+
"id": {
|
|
351
|
+
"name": "id",
|
|
352
|
+
"type": "text",
|
|
353
|
+
"primaryKey": true,
|
|
354
|
+
"notNull": true,
|
|
355
|
+
"autoincrement": false
|
|
356
|
+
},
|
|
357
|
+
"email": {
|
|
358
|
+
"name": "email",
|
|
359
|
+
"type": "text",
|
|
360
|
+
"primaryKey": false,
|
|
361
|
+
"notNull": true,
|
|
362
|
+
"autoincrement": false
|
|
363
|
+
},
|
|
364
|
+
"password_hash": {
|
|
365
|
+
"name": "password_hash",
|
|
366
|
+
"type": "text",
|
|
367
|
+
"primaryKey": false,
|
|
368
|
+
"notNull": true,
|
|
369
|
+
"autoincrement": false
|
|
370
|
+
},
|
|
371
|
+
"role": {
|
|
372
|
+
"name": "role",
|
|
373
|
+
"type": "text",
|
|
374
|
+
"primaryKey": false,
|
|
375
|
+
"notNull": true,
|
|
376
|
+
"autoincrement": false,
|
|
377
|
+
"default": "'admin'"
|
|
378
|
+
},
|
|
379
|
+
"created_at": {
|
|
380
|
+
"name": "created_at",
|
|
381
|
+
"type": "integer",
|
|
382
|
+
"primaryKey": false,
|
|
383
|
+
"notNull": true,
|
|
384
|
+
"autoincrement": false
|
|
385
|
+
},
|
|
386
|
+
"updated_at": {
|
|
387
|
+
"name": "updated_at",
|
|
388
|
+
"type": "integer",
|
|
389
|
+
"primaryKey": false,
|
|
390
|
+
"notNull": true,
|
|
391
|
+
"autoincrement": false
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"indexes": {
|
|
395
|
+
"users_email_unique": {
|
|
396
|
+
"name": "users_email_unique",
|
|
397
|
+
"columns": [
|
|
398
|
+
"email"
|
|
399
|
+
],
|
|
400
|
+
"isUnique": true
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
"foreignKeys": {},
|
|
404
|
+
"compositePrimaryKeys": {},
|
|
405
|
+
"uniqueConstraints": {},
|
|
406
|
+
"checkConstraints": {}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
"views": {},
|
|
410
|
+
"enums": {},
|
|
411
|
+
"_meta": {
|
|
412
|
+
"schemas": {},
|
|
413
|
+
"tables": {},
|
|
414
|
+
"columns": {}
|
|
415
|
+
},
|
|
416
|
+
"internal": {
|
|
417
|
+
"indexes": {}
|
|
418
|
+
}
|
|
419
|
+
}
|
package/lib/ai/index.js
CHANGED
|
@@ -116,8 +116,8 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
116
116
|
|
|
117
117
|
if (!existingChat) {
|
|
118
118
|
// First message: create workspace row, then chat, then link them
|
|
119
|
-
const {
|
|
120
|
-
const workspace =
|
|
119
|
+
const { createCodeWorkspace } = await import('../db/code-workspaces.js');
|
|
120
|
+
const workspace = createCodeWorkspace(options.userId || 'unknown', {
|
|
121
121
|
repo: options.repo,
|
|
122
122
|
branch: options.branch,
|
|
123
123
|
});
|
|
@@ -125,7 +125,7 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
125
125
|
createChat(options.userId || 'unknown', 'New Chat', threadId);
|
|
126
126
|
linkChatToWorkspace(threadId, workspaceId);
|
|
127
127
|
} else {
|
|
128
|
-
workspaceId = existingChat.
|
|
128
|
+
workspaceId = existingChat.codeWorkspaceId;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
agent = await getCodeAgent({
|
package/lib/ai/tools.js
CHANGED
|
@@ -194,12 +194,15 @@ function createStartCodingTool({ repo, branch, workspaceId }) {
|
|
|
194
194
|
async ({ task_description }) => {
|
|
195
195
|
try {
|
|
196
196
|
const { randomUUID } = await import('crypto');
|
|
197
|
-
const containerName = `
|
|
197
|
+
const containerName = `code-workspace-${randomUUID().slice(0, 8)}`;
|
|
198
198
|
|
|
199
|
-
const {
|
|
200
|
-
|
|
199
|
+
const { getCodeWorkspaceById, updateContainerName } = await import('../db/code-workspaces.js');
|
|
200
|
+
const workspace = getCodeWorkspaceById(workspaceId);
|
|
201
|
+
const codingAgent = workspace?.codingAgent || 'claude-code';
|
|
202
|
+
|
|
203
|
+
const { createCodeWorkspaceContainer } = await import('../tools/docker.js');
|
|
204
|
+
await createCodeWorkspaceContainer({ containerName, repo, branch, codingAgent });
|
|
201
205
|
|
|
202
|
-
const { updateContainerName } = await import('../db/claude-workspaces.js');
|
|
203
206
|
updateContainerName(workspaceId, containerName);
|
|
204
207
|
|
|
205
208
|
return JSON.stringify({
|
|
@@ -218,7 +221,7 @@ function createStartCodingTool({ repo, branch, workspaceId }) {
|
|
|
218
221
|
{
|
|
219
222
|
name: 'start_coding',
|
|
220
223
|
description:
|
|
221
|
-
'Launch a live
|
|
224
|
+
'Launch a live code workspace in a Docker container. Only call this when the user explicitly says they are ready to start coding (e.g. "let\'s start coding", "okay let\'s get started", "launch it"). Returns a link to the live workspace.',
|
|
222
225
|
schema: z.object({
|
|
223
226
|
task_description: z
|
|
224
227
|
.string()
|
package/lib/chat/actions.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function getChats() {
|
|
|
36
36
|
const user = await requireAuth();
|
|
37
37
|
const { or, eq, desc } = await import('drizzle-orm');
|
|
38
38
|
const { getDb } = await import('../db/index.js');
|
|
39
|
-
const { chats,
|
|
39
|
+
const { chats, codeWorkspaces } = await import('../db/schema.js');
|
|
40
40
|
const db = getDb();
|
|
41
41
|
return db
|
|
42
42
|
.select({
|
|
@@ -44,13 +44,13 @@ export async function getChats() {
|
|
|
44
44
|
userId: chats.userId,
|
|
45
45
|
title: chats.title,
|
|
46
46
|
starred: chats.starred,
|
|
47
|
-
|
|
48
|
-
containerName:
|
|
47
|
+
codeWorkspaceId: chats.codeWorkspaceId,
|
|
48
|
+
containerName: codeWorkspaces.containerName,
|
|
49
49
|
createdAt: chats.createdAt,
|
|
50
50
|
updatedAt: chats.updatedAt,
|
|
51
51
|
})
|
|
52
52
|
.from(chats)
|
|
53
|
-
.leftJoin(
|
|
53
|
+
.leftJoin(codeWorkspaces, eq(chats.codeWorkspaceId, codeWorkspaces.id))
|
|
54
54
|
.where(or(eq(chats.userId, user.id), eq(chats.userId, 'telegram')))
|
|
55
55
|
.orderBy(desc(chats.updatedAt))
|
|
56
56
|
.all();
|
|
@@ -205,10 +205,13 @@ export async function getAppVersion() {
|
|
|
205
205
|
await requireAuth();
|
|
206
206
|
const { getInstalledVersion } = await import('../cron.js');
|
|
207
207
|
const { getAvailableVersion, getReleaseNotes } = await import('../db/update-check.js');
|
|
208
|
+
const version = getInstalledVersion();
|
|
209
|
+
const available = getAvailableVersion();
|
|
210
|
+
const isNewer = available && available !== version;
|
|
208
211
|
return {
|
|
209
|
-
version
|
|
210
|
-
updateAvailable:
|
|
211
|
-
changelog: getReleaseNotes(),
|
|
212
|
+
version,
|
|
213
|
+
updateAvailable: isNewer ? available : null,
|
|
214
|
+
changelog: isNewer ? getReleaseNotes() : null,
|
|
212
215
|
};
|
|
213
216
|
}
|
|
214
217
|
|
|
@@ -313,9 +316,9 @@ export async function getBranches(repoFullName) {
|
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
/**
|
|
316
|
-
* Get chat metadata (
|
|
319
|
+
* Get chat metadata (codeWorkspaceId) for an existing chat.
|
|
317
320
|
* @param {string} chatId
|
|
318
|
-
* @returns {Promise<{
|
|
321
|
+
* @returns {Promise<{codeWorkspaceId: string|null}|null>}
|
|
319
322
|
*/
|
|
320
323
|
export async function getChatMeta(chatId) {
|
|
321
324
|
const user = await requireAuth();
|
|
@@ -323,7 +326,7 @@ export async function getChatMeta(chatId) {
|
|
|
323
326
|
if (!chat || (chat.userId !== user.id && chat.userId !== 'telegram')) {
|
|
324
327
|
return null;
|
|
325
328
|
}
|
|
326
|
-
return {
|
|
329
|
+
return { codeWorkspaceId: chat.codeWorkspaceId || null };
|
|
327
330
|
}
|
|
328
331
|
|
|
329
332
|
/**
|
|
@@ -334,10 +337,10 @@ export async function getChatMeta(chatId) {
|
|
|
334
337
|
export async function getWorkspace(workspaceId) {
|
|
335
338
|
await requireAuth();
|
|
336
339
|
try {
|
|
337
|
-
const {
|
|
338
|
-
const ws =
|
|
340
|
+
const { getCodeWorkspaceById } = await import('../db/code-workspaces.js');
|
|
341
|
+
const ws = getCodeWorkspaceById(workspaceId);
|
|
339
342
|
if (!ws) return null;
|
|
340
|
-
return { id: ws.id, repo: ws.repo, branch: ws.branch, containerName: ws.containerName };
|
|
343
|
+
return { id: ws.id, repo: ws.repo, branch: ws.branch, containerName: ws.containerName, codingAgent: ws.codingAgent };
|
|
341
344
|
} catch (err) {
|
|
342
345
|
console.error('Failed to get workspace:', err);
|
|
343
346
|
return null;
|
|
@@ -65,8 +65,8 @@ function ChatPage({ session, needsSetup, chatId }) {
|
|
|
65
65
|
setInitialMessages(uiMessages);
|
|
66
66
|
try {
|
|
67
67
|
const meta = await getChatMeta(activeChatId);
|
|
68
|
-
if (meta?.
|
|
69
|
-
const ws = await getWorkspace(meta.
|
|
68
|
+
if (meta?.codeWorkspaceId) {
|
|
69
|
+
const ws = await getWorkspace(meta.codeWorkspaceId);
|
|
70
70
|
setWorkspace(ws);
|
|
71
71
|
} else {
|
|
72
72
|
setWorkspace(null);
|
|
@@ -82,8 +82,8 @@ export function ChatPage({ session, needsSetup, chatId }) {
|
|
|
82
82
|
// Check if this is a code chat
|
|
83
83
|
try {
|
|
84
84
|
const meta = await getChatMeta(activeChatId);
|
|
85
|
-
if (meta?.
|
|
86
|
-
const ws = await getWorkspace(meta.
|
|
85
|
+
if (meta?.codeWorkspaceId) {
|
|
86
|
+
const ws = await getWorkspace(meta.codeWorkspaceId);
|
|
87
87
|
setWorkspace(ws);
|
|
88
88
|
} else {
|
|
89
89
|
setWorkspace(null);
|
|
@@ -187,7 +187,7 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
187
187
|
return /* @__PURE__ */ jsxs(
|
|
188
188
|
"a",
|
|
189
189
|
{
|
|
190
|
-
href: chat.
|
|
190
|
+
href: chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`,
|
|
191
191
|
className: "relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md",
|
|
192
192
|
style: { textDecoration: "inherit", color: "inherit" },
|
|
193
193
|
onMouseEnter: () => setHovered(true),
|
|
@@ -198,14 +198,14 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
200
|
e.preventDefault();
|
|
201
|
-
if (chat.
|
|
202
|
-
window.location.href = `/code/${chat.
|
|
201
|
+
if (chat.codeWorkspaceId && chat.containerName) {
|
|
202
|
+
window.location.href = `/code/${chat.codeWorkspaceId}`;
|
|
203
203
|
} else {
|
|
204
204
|
onNavigate(chat.id);
|
|
205
205
|
}
|
|
206
206
|
},
|
|
207
207
|
children: [
|
|
208
|
-
chat.
|
|
208
|
+
chat.codeWorkspaceId && chat.containerName ? /* @__PURE__ */ jsx(CodeIcon, { size: 16 }) : /* @__PURE__ */ jsx(MessageIcon, { size: 16 }),
|
|
209
209
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
210
210
|
editing ? /* @__PURE__ */ jsx(
|
|
211
211
|
"input",
|
|
@@ -229,7 +229,7 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
229
229
|
|
|
230
230
|
return (
|
|
231
231
|
<a
|
|
232
|
-
href={chat.
|
|
232
|
+
href={chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`}
|
|
233
233
|
className="relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md"
|
|
234
234
|
style={{ textDecoration: 'inherit', color: 'inherit' }}
|
|
235
235
|
onMouseEnter={() => setHovered(true)}
|
|
@@ -237,14 +237,14 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
237
237
|
onClick={(e) => {
|
|
238
238
|
if (editing) { e.preventDefault(); return; }
|
|
239
239
|
e.preventDefault();
|
|
240
|
-
if (chat.
|
|
241
|
-
window.location.href = `/code/${chat.
|
|
240
|
+
if (chat.codeWorkspaceId && chat.containerName) {
|
|
241
|
+
window.location.href = `/code/${chat.codeWorkspaceId}`;
|
|
242
242
|
} else {
|
|
243
243
|
onNavigate(chat.id);
|
|
244
244
|
}
|
|
245
245
|
}}
|
|
246
246
|
>
|
|
247
|
-
{chat.
|
|
247
|
+
{chat.codeWorkspaceId && chat.containerName ? <CodeIcon size={16} /> : <MessageIcon size={16} />}
|
|
248
248
|
<div className="flex-1 min-w-0">
|
|
249
249
|
{editing ? (
|
|
250
250
|
<input
|
|
@@ -52,7 +52,7 @@ function CodeModeToggle({
|
|
|
52
52
|
setLoadingBranches(false);
|
|
53
53
|
}).catch(() => setLoadingBranches(false));
|
|
54
54
|
}, [repo]);
|
|
55
|
-
if (!process.env.
|
|
55
|
+
if (!process.env.NEXT_PUBLIC_CODE_WORKSPACE) return null;
|
|
56
56
|
if (locked && enabled) {
|
|
57
57
|
return /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2.5 text-sm text-muted-foreground", children: [
|
|
58
58
|
repo && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -76,7 +76,7 @@ function CodeModeToggle({
|
|
|
76
76
|
className: "inline-flex items-center gap-2 group",
|
|
77
77
|
role: "switch",
|
|
78
78
|
"aria-checked": enabled,
|
|
79
|
-
"aria-label": "Toggle
|
|
79
|
+
"aria-label": "Toggle Code mode",
|
|
80
80
|
children: [
|
|
81
81
|
/* @__PURE__ */ jsx(
|
|
82
82
|
"span",
|
|
@@ -99,7 +99,7 @@ function CodeModeToggle({
|
|
|
99
99
|
/* @__PURE__ */ jsx("span", { className: cn(
|
|
100
100
|
"text-xs font-medium transition-colors",
|
|
101
101
|
enabled ? "text-foreground" : "text-muted-foreground group-hover:text-foreground"
|
|
102
|
-
), children: "
|
|
102
|
+
), children: "Code" })
|
|
103
103
|
]
|
|
104
104
|
}
|
|
105
105
|
),
|
|
@@ -73,7 +73,7 @@ export function CodeModeToggle({
|
|
|
73
73
|
}).catch(() => setLoadingBranches(false));
|
|
74
74
|
}, [repo]);
|
|
75
75
|
|
|
76
|
-
if (!process.env.
|
|
76
|
+
if (!process.env.NEXT_PUBLIC_CODE_WORKSPACE) return null;
|
|
77
77
|
|
|
78
78
|
// Locked mode: show as centered inline label
|
|
79
79
|
if (locked && enabled) {
|
|
@@ -109,7 +109,7 @@ export function CodeModeToggle({
|
|
|
109
109
|
className="inline-flex items-center gap-2 group"
|
|
110
110
|
role="switch"
|
|
111
111
|
aria-checked={enabled}
|
|
112
|
-
aria-label="Toggle
|
|
112
|
+
aria-label="Toggle Code mode"
|
|
113
113
|
>
|
|
114
114
|
{/* Track */}
|
|
115
115
|
<span
|
|
@@ -131,7 +131,7 @@ export function CodeModeToggle({
|
|
|
131
131
|
'text-xs font-medium transition-colors',
|
|
132
132
|
enabled ? 'text-foreground' : 'text-muted-foreground group-hover:text-foreground'
|
|
133
133
|
)}>
|
|
134
|
-
|
|
134
|
+
Code
|
|
135
135
|
</span>
|
|
136
136
|
</button>
|
|
137
137
|
|
|
@@ -27,20 +27,20 @@ function SidebarHistoryItem({ chat, isActive, onDelete, onStar, onRename }) {
|
|
|
27
27
|
/* @__PURE__ */ jsxs(
|
|
28
28
|
SidebarMenuButton,
|
|
29
29
|
{
|
|
30
|
-
href: chat.
|
|
30
|
+
href: chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`,
|
|
31
31
|
className: "pr-8",
|
|
32
32
|
isActive,
|
|
33
33
|
onClick: (e) => {
|
|
34
34
|
e.preventDefault();
|
|
35
|
-
if (chat.
|
|
36
|
-
window.location.href = `/code/${chat.
|
|
35
|
+
if (chat.codeWorkspaceId && chat.containerName) {
|
|
36
|
+
window.location.href = `/code/${chat.codeWorkspaceId}`;
|
|
37
37
|
} else {
|
|
38
38
|
navigateToChat(chat.id);
|
|
39
39
|
}
|
|
40
40
|
setOpenMobile(false);
|
|
41
41
|
},
|
|
42
42
|
children: [
|
|
43
|
-
chat.
|
|
43
|
+
chat.codeWorkspaceId && chat.containerName ? /* @__PURE__ */ jsx(CodeIcon, { size: 14 }) : /* @__PURE__ */ jsx(MessageIcon, { size: 14 }),
|
|
44
44
|
/* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: chat.title })
|
|
45
45
|
]
|
|
46
46
|
}
|
|
@@ -27,20 +27,20 @@ export function SidebarHistoryItem({ chat, isActive, onDelete, onStar, onRename
|
|
|
27
27
|
onMouseLeave={() => setHovered(false)}
|
|
28
28
|
>
|
|
29
29
|
<SidebarMenuButton
|
|
30
|
-
href={chat.
|
|
30
|
+
href={chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`}
|
|
31
31
|
className="pr-8"
|
|
32
32
|
isActive={isActive}
|
|
33
33
|
onClick={(e) => {
|
|
34
34
|
e.preventDefault();
|
|
35
|
-
if (chat.
|
|
36
|
-
window.location.href = `/code/${chat.
|
|
35
|
+
if (chat.codeWorkspaceId && chat.containerName) {
|
|
36
|
+
window.location.href = `/code/${chat.codeWorkspaceId}`;
|
|
37
37
|
} else {
|
|
38
38
|
navigateToChat(chat.id);
|
|
39
39
|
}
|
|
40
40
|
setOpenMobile(false);
|
|
41
41
|
}}
|
|
42
42
|
>
|
|
43
|
-
{chat.
|
|
43
|
+
{chat.codeWorkspaceId && chat.containerName ? <CodeIcon size={14} /> : <MessageIcon size={14} />}
|
|
44
44
|
<span className="truncate flex-1">
|
|
45
45
|
{chat.title}
|
|
46
46
|
</span>
|
package/lib/code/actions.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { auth } from '../auth/index.js';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from '../db/
|
|
5
|
+
createCodeWorkspace as dbCreateCodeWorkspace,
|
|
6
|
+
getCodeWorkspaceById,
|
|
7
|
+
getCodeWorkspacesByUser,
|
|
8
|
+
updateCodeWorkspaceTitle,
|
|
9
|
+
toggleCodeWorkspaceStarred,
|
|
10
|
+
deleteCodeWorkspace as dbDeleteCodeWorkspace,
|
|
11
|
+
} from '../db/code-workspaces.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Get the authenticated user or throw.
|
|
@@ -22,67 +22,67 @@ async function requireAuth() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Get all
|
|
25
|
+
* Get all code workspaces for the authenticated user.
|
|
26
26
|
* @returns {Promise<object[]>}
|
|
27
27
|
*/
|
|
28
|
-
export async function
|
|
28
|
+
export async function getCodeWorkspaces() {
|
|
29
29
|
const user = await requireAuth();
|
|
30
|
-
return
|
|
30
|
+
return getCodeWorkspacesByUser(user.id);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Create a new
|
|
34
|
+
* Create a new code workspace.
|
|
35
35
|
* @param {string} containerName - Docker container DNS name
|
|
36
|
-
* @param {string} [title='
|
|
36
|
+
* @param {string} [title='Code Workspace']
|
|
37
37
|
* @returns {Promise<object>}
|
|
38
38
|
*/
|
|
39
|
-
export async function
|
|
39
|
+
export async function createCodeWorkspace(containerName, title = 'Code Workspace') {
|
|
40
40
|
const user = await requireAuth();
|
|
41
|
-
return
|
|
41
|
+
return dbCreateCodeWorkspace(user.id, { containerName, title });
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Rename a
|
|
45
|
+
* Rename a code workspace (with ownership check).
|
|
46
46
|
* @param {string} id
|
|
47
47
|
* @param {string} title
|
|
48
48
|
* @returns {Promise<{success: boolean}>}
|
|
49
49
|
*/
|
|
50
|
-
export async function
|
|
50
|
+
export async function renameCodeWorkspace(id, title) {
|
|
51
51
|
const user = await requireAuth();
|
|
52
|
-
const workspace =
|
|
52
|
+
const workspace = getCodeWorkspaceById(id);
|
|
53
53
|
if (!workspace || workspace.userId !== user.id) {
|
|
54
54
|
return { success: false };
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
updateCodeWorkspaceTitle(id, title);
|
|
57
57
|
return { success: true };
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* Toggle a
|
|
61
|
+
* Toggle a code workspace's starred status (with ownership check).
|
|
62
62
|
* @param {string} id
|
|
63
63
|
* @returns {Promise<{success: boolean, starred?: number}>}
|
|
64
64
|
*/
|
|
65
|
-
export async function
|
|
65
|
+
export async function starCodeWorkspace(id) {
|
|
66
66
|
const user = await requireAuth();
|
|
67
|
-
const workspace =
|
|
67
|
+
const workspace = getCodeWorkspaceById(id);
|
|
68
68
|
if (!workspace || workspace.userId !== user.id) {
|
|
69
69
|
return { success: false };
|
|
70
70
|
}
|
|
71
|
-
const starred =
|
|
71
|
+
const starred = toggleCodeWorkspaceStarred(id);
|
|
72
72
|
return { success: true, starred };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Delete a
|
|
76
|
+
* Delete a code workspace (with ownership check).
|
|
77
77
|
* @param {string} id
|
|
78
78
|
* @returns {Promise<{success: boolean}>}
|
|
79
79
|
*/
|
|
80
|
-
export async function
|
|
80
|
+
export async function deleteCodeWorkspace(id) {
|
|
81
81
|
const user = await requireAuth();
|
|
82
|
-
const workspace =
|
|
82
|
+
const workspace = getCodeWorkspaceById(id);
|
|
83
83
|
if (!workspace || workspace.userId !== user.id) {
|
|
84
84
|
return { success: false };
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
dbDeleteCodeWorkspace(id);
|
|
87
87
|
return { success: true };
|
|
88
88
|
}
|
package/lib/code/code-page.js
CHANGED
|
@@ -13,7 +13,7 @@ import { ChatHeader } from "../chat/components/chat-header.js";
|
|
|
13
13
|
import "@xterm/xterm/css/xterm.css";
|
|
14
14
|
const STATUS = { connected: "#22c55e", connecting: "#eab308", disconnected: "#ef4444" };
|
|
15
15
|
const RECONNECT_INTERVAL = 3e3;
|
|
16
|
-
function CodePage({ session,
|
|
16
|
+
function CodePage({ session, codeWorkspaceId }) {
|
|
17
17
|
const containerRef = useRef(null);
|
|
18
18
|
const termRef = useRef(null);
|
|
19
19
|
const fitAddonRef = useRef(null);
|
|
@@ -39,7 +39,7 @@ function CodePage({ session, claudeWorkspaceId }) {
|
|
|
39
39
|
if (!term) return;
|
|
40
40
|
setStatus(STATUS.connecting);
|
|
41
41
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
42
|
-
const ws = new WebSocket(`${protocol}//${window.location.host}/code/${
|
|
42
|
+
const ws = new WebSocket(`${protocol}//${window.location.host}/code/${codeWorkspaceId}/ws`);
|
|
43
43
|
wsRef.current = ws;
|
|
44
44
|
ws.binaryType = "arraybuffer";
|
|
45
45
|
ws.onopen = () => {
|
|
@@ -56,7 +56,7 @@ function CodePage({ session, claudeWorkspaceId }) {
|
|
|
56
56
|
term.write(payload);
|
|
57
57
|
break;
|
|
58
58
|
case "1":
|
|
59
|
-
document.title = payload || "
|
|
59
|
+
document.title = payload || "Code Workspace";
|
|
60
60
|
break;
|
|
61
61
|
case "2":
|
|
62
62
|
break;
|
|
@@ -69,7 +69,7 @@ function CodePage({ session, claudeWorkspaceId }) {
|
|
|
69
69
|
ws.onerror = () => {
|
|
70
70
|
ws.close();
|
|
71
71
|
};
|
|
72
|
-
}, [
|
|
72
|
+
}, [codeWorkspaceId, setStatus]);
|
|
73
73
|
useEffect(() => {
|
|
74
74
|
const term = new Terminal({
|
|
75
75
|
cursorBlink: true,
|
|
@@ -129,7 +129,7 @@ function CodePage({ session, claudeWorkspaceId }) {
|
|
|
129
129
|
} }, children: /* @__PURE__ */ jsxs(SidebarProvider, { children: [
|
|
130
130
|
/* @__PURE__ */ jsx(AppSidebar, { user: session.user }),
|
|
131
131
|
/* @__PURE__ */ jsx(SidebarInset, { children: /* @__PURE__ */ jsxs("div", { className: "flex h-svh flex-col overflow-hidden", children: [
|
|
132
|
-
/* @__PURE__ */ jsx(ChatHeader, { workspaceId:
|
|
132
|
+
/* @__PURE__ */ jsx(ChatHeader, { workspaceId: codeWorkspaceId }),
|
|
133
133
|
/* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, minHeight: 0 }, children: [
|
|
134
134
|
/* @__PURE__ */ jsx("div", { ref: containerRef, className: "mx-4", style: { height: "100%", borderRadius: 6, overflow: "hidden" } }),
|
|
135
135
|
!connected && /* @__PURE__ */ jsx("div", { style: {
|
package/lib/code/code-page.jsx
CHANGED
|
@@ -15,7 +15,7 @@ import '@xterm/xterm/css/xterm.css';
|
|
|
15
15
|
const STATUS = { connected: '#22c55e', connecting: '#eab308', disconnected: '#ef4444' };
|
|
16
16
|
const RECONNECT_INTERVAL = 3000;
|
|
17
17
|
|
|
18
|
-
export default function CodePage({ session,
|
|
18
|
+
export default function CodePage({ session, codeWorkspaceId }) {
|
|
19
19
|
const containerRef = useRef(null);
|
|
20
20
|
const termRef = useRef(null);
|
|
21
21
|
const fitAddonRef = useRef(null);
|
|
@@ -45,7 +45,7 @@ export default function CodePage({ session, claudeWorkspaceId }) {
|
|
|
45
45
|
setStatus(STATUS.connecting);
|
|
46
46
|
|
|
47
47
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
48
|
-
const ws = new WebSocket(`${protocol}//${window.location.host}/code/${
|
|
48
|
+
const ws = new WebSocket(`${protocol}//${window.location.host}/code/${codeWorkspaceId}/ws`);
|
|
49
49
|
wsRef.current = ws;
|
|
50
50
|
|
|
51
51
|
ws.binaryType = 'arraybuffer';
|
|
@@ -67,7 +67,7 @@ export default function CodePage({ session, claudeWorkspaceId }) {
|
|
|
67
67
|
term.write(payload);
|
|
68
68
|
break;
|
|
69
69
|
case '1': // title
|
|
70
|
-
document.title = payload || '
|
|
70
|
+
document.title = payload || 'Code Workspace';
|
|
71
71
|
break;
|
|
72
72
|
case '2': // prefs (ignored)
|
|
73
73
|
break;
|
|
@@ -82,7 +82,7 @@ export default function CodePage({ session, claudeWorkspaceId }) {
|
|
|
82
82
|
ws.onerror = () => {
|
|
83
83
|
ws.close();
|
|
84
84
|
};
|
|
85
|
-
}, [
|
|
85
|
+
}, [codeWorkspaceId, setStatus]);
|
|
86
86
|
|
|
87
87
|
useEffect(() => {
|
|
88
88
|
const term = new Terminal({
|
|
@@ -160,7 +160,7 @@ export default function CodePage({ session, claudeWorkspaceId }) {
|
|
|
160
160
|
<AppSidebar user={session.user} />
|
|
161
161
|
<SidebarInset>
|
|
162
162
|
<div className="flex h-svh flex-col overflow-hidden">
|
|
163
|
-
<ChatHeader workspaceId={
|
|
163
|
+
<ChatHeader workspaceId={codeWorkspaceId} />
|
|
164
164
|
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
|
165
165
|
<div ref={containerRef} className="mx-4" style={{ height: '100%', borderRadius: 6, overflow: 'hidden' }} />
|
|
166
166
|
{!connected && (
|
package/lib/code/ws-proxy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
2
|
import { decode } from 'next-auth/jwt';
|
|
3
|
-
import {
|
|
3
|
+
import { getCodeWorkspaceById } from '../db/code-workspaces.js';
|
|
4
4
|
|
|
5
5
|
async function isAuthenticated(req) {
|
|
6
6
|
const cookies = req.headers.cookie || '';
|
|
@@ -37,16 +37,16 @@ export function attachCodeProxy(server) {
|
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
43
|
-
console.log(`[ws-proxy] rejected: unknown workspace ${
|
|
40
|
+
const codeWorkspaceId = match[1];
|
|
41
|
+
const codeWorkspace = getCodeWorkspaceById(codeWorkspaceId);
|
|
42
|
+
if (!codeWorkspace) {
|
|
43
|
+
console.log(`[ws-proxy] rejected: unknown workspace ${codeWorkspaceId}`);
|
|
44
44
|
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
45
45
|
socket.destroy();
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const container =
|
|
49
|
+
const container = codeWorkspace.containerName;
|
|
50
50
|
|
|
51
51
|
wss.handleUpgrade(req, socket, head, (clientWs) => {
|
|
52
52
|
const backendWs = new WebSocket(`ws://${container}:7681/ws`, 'tty');
|
package/lib/db/chats.js
CHANGED
|
@@ -50,13 +50,13 @@ export function getChatById(chatId) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Get a single chat by its
|
|
53
|
+
* Get a single chat by its code_workspace_id.
|
|
54
54
|
* @param {string} workspaceId
|
|
55
55
|
* @returns {object|undefined}
|
|
56
56
|
*/
|
|
57
57
|
export function getChatByWorkspaceId(workspaceId) {
|
|
58
58
|
const db = getDb();
|
|
59
|
-
return db.select().from(chats).where(eq(chats.
|
|
59
|
+
return db.select().from(chats).where(eq(chats.codeWorkspaceId, workspaceId)).get();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
@@ -132,14 +132,14 @@ export function getMessagesByChatId(chatId) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
* Link a chat to a
|
|
135
|
+
* Link a chat to a code workspace.
|
|
136
136
|
* @param {string} chatId
|
|
137
137
|
* @param {string} workspaceId
|
|
138
138
|
*/
|
|
139
139
|
export function linkChatToWorkspace(chatId, workspaceId) {
|
|
140
140
|
const db = getDb();
|
|
141
141
|
db.update(chats)
|
|
142
|
-
.set({
|
|
142
|
+
.set({ codeWorkspaceId: workspaceId, updatedAt: Date.now() })
|
|
143
143
|
.where(eq(chats.id, chatId))
|
|
144
144
|
.run();
|
|
145
145
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { eq, desc } from 'drizzle-orm';
|
|
3
3
|
import { getDb } from './index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { codeWorkspaces } from './schema.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Create a new
|
|
7
|
+
* Create a new code workspace.
|
|
8
8
|
* @param {string} userId
|
|
9
9
|
* @param {object} options
|
|
10
10
|
* @param {string} [options.containerName] - Docker container DNS name (null until launched)
|
|
11
11
|
* @param {string} [options.repo] - GitHub repo full name (e.g. "owner/repo")
|
|
12
12
|
* @param {string} [options.branch] - Git branch name
|
|
13
|
-
* @param {string} [options.title='
|
|
13
|
+
* @param {string} [options.title='Code Workspace']
|
|
14
|
+
* @param {string} [options.codingAgent='claude-code'] - Coding agent identifier
|
|
14
15
|
* @param {string} [options.id] - Optional ID (UUID). Generated if not provided.
|
|
15
16
|
* @returns {object} The created workspace
|
|
16
17
|
*/
|
|
17
|
-
export function
|
|
18
|
+
export function createCodeWorkspace(userId, { containerName = null, repo = null, branch = null, title = 'Code Workspace', codingAgent = 'claude-code', id = null } = {}) {
|
|
18
19
|
const db = getDb();
|
|
19
20
|
const now = Date.now();
|
|
20
21
|
const workspace = {
|
|
@@ -24,10 +25,11 @@ export function createClaudeWorkspace(userId, { containerName = null, repo = nul
|
|
|
24
25
|
repo,
|
|
25
26
|
branch,
|
|
26
27
|
title,
|
|
28
|
+
codingAgent,
|
|
27
29
|
createdAt: now,
|
|
28
30
|
updatedAt: now,
|
|
29
31
|
};
|
|
30
|
-
db.insert(
|
|
32
|
+
db.insert(codeWorkspaces).values(workspace).run();
|
|
31
33
|
return workspace;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -38,71 +40,71 @@ export function createClaudeWorkspace(userId, { containerName = null, repo = nul
|
|
|
38
40
|
*/
|
|
39
41
|
export function updateContainerName(id, containerName) {
|
|
40
42
|
const db = getDb();
|
|
41
|
-
db.update(
|
|
43
|
+
db.update(codeWorkspaces)
|
|
42
44
|
.set({ containerName, updatedAt: Date.now() })
|
|
43
|
-
.where(eq(
|
|
45
|
+
.where(eq(codeWorkspaces.id, id))
|
|
44
46
|
.run();
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
|
-
* Get a single
|
|
50
|
+
* Get a single code workspace by ID.
|
|
49
51
|
* @param {string} id
|
|
50
52
|
* @returns {object|undefined}
|
|
51
53
|
*/
|
|
52
|
-
export function
|
|
54
|
+
export function getCodeWorkspaceById(id) {
|
|
53
55
|
const db = getDb();
|
|
54
|
-
return db.select().from(
|
|
56
|
+
return db.select().from(codeWorkspaces).where(eq(codeWorkspaces.id, id)).get();
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
|
-
* Get all
|
|
60
|
+
* Get all code workspaces for a user, ordered by most recently updated.
|
|
59
61
|
* @param {string} userId
|
|
60
62
|
* @returns {object[]}
|
|
61
63
|
*/
|
|
62
|
-
export function
|
|
64
|
+
export function getCodeWorkspacesByUser(userId) {
|
|
63
65
|
const db = getDb();
|
|
64
66
|
return db
|
|
65
67
|
.select()
|
|
66
|
-
.from(
|
|
67
|
-
.where(eq(
|
|
68
|
-
.orderBy(desc(
|
|
68
|
+
.from(codeWorkspaces)
|
|
69
|
+
.where(eq(codeWorkspaces.userId, userId))
|
|
70
|
+
.orderBy(desc(codeWorkspaces.updatedAt))
|
|
69
71
|
.all();
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
|
-
* Update a
|
|
75
|
+
* Update a code workspace's title.
|
|
74
76
|
* @param {string} id
|
|
75
77
|
* @param {string} title
|
|
76
78
|
*/
|
|
77
|
-
export function
|
|
79
|
+
export function updateCodeWorkspaceTitle(id, title) {
|
|
78
80
|
const db = getDb();
|
|
79
|
-
db.update(
|
|
81
|
+
db.update(codeWorkspaces)
|
|
80
82
|
.set({ title, updatedAt: Date.now() })
|
|
81
|
-
.where(eq(
|
|
83
|
+
.where(eq(codeWorkspaces.id, id))
|
|
82
84
|
.run();
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
/**
|
|
86
|
-
* Toggle a
|
|
88
|
+
* Toggle a code workspace's starred status.
|
|
87
89
|
* @param {string} id
|
|
88
90
|
* @returns {number} The new starred value (0 or 1)
|
|
89
91
|
*/
|
|
90
|
-
export function
|
|
92
|
+
export function toggleCodeWorkspaceStarred(id) {
|
|
91
93
|
const db = getDb();
|
|
92
|
-
const workspace = db.select({ starred:
|
|
94
|
+
const workspace = db.select({ starred: codeWorkspaces.starred }).from(codeWorkspaces).where(eq(codeWorkspaces.id, id)).get();
|
|
93
95
|
const newValue = workspace?.starred ? 0 : 1;
|
|
94
|
-
db.update(
|
|
96
|
+
db.update(codeWorkspaces)
|
|
95
97
|
.set({ starred: newValue })
|
|
96
|
-
.where(eq(
|
|
98
|
+
.where(eq(codeWorkspaces.id, id))
|
|
97
99
|
.run();
|
|
98
100
|
return newValue;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/**
|
|
102
|
-
* Delete a
|
|
104
|
+
* Delete a code workspace.
|
|
103
105
|
* @param {string} id
|
|
104
106
|
*/
|
|
105
|
-
export function
|
|
107
|
+
export function deleteCodeWorkspace(id) {
|
|
106
108
|
const db = getDb();
|
|
107
|
-
db.delete(
|
|
109
|
+
db.delete(codeWorkspaces).where(eq(codeWorkspaces.id, id)).run();
|
|
108
110
|
}
|
package/lib/db/schema.js
CHANGED
|
@@ -14,7 +14,7 @@ export const chats = sqliteTable('chats', {
|
|
|
14
14
|
userId: text('user_id').notNull(),
|
|
15
15
|
title: text('title').notNull().default('New Chat'),
|
|
16
16
|
starred: integer('starred').notNull().default(0),
|
|
17
|
-
|
|
17
|
+
codeWorkspaceId: text('code_workspace_id'),
|
|
18
18
|
createdAt: integer('created_at').notNull(),
|
|
19
19
|
updatedAt: integer('updated_at').notNull(),
|
|
20
20
|
});
|
|
@@ -42,13 +42,14 @@ export const subscriptions = sqliteTable('subscriptions', {
|
|
|
42
42
|
createdAt: integer('created_at').notNull(),
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
export const
|
|
45
|
+
export const codeWorkspaces = sqliteTable('code_workspaces', {
|
|
46
46
|
id: text('id').primaryKey(),
|
|
47
47
|
userId: text('user_id').notNull(),
|
|
48
48
|
containerName: text('container_name').unique(),
|
|
49
49
|
repo: text('repo'),
|
|
50
50
|
branch: text('branch'),
|
|
51
|
-
title: text('title').notNull().default('
|
|
51
|
+
title: text('title').notNull().default('Code Workspace'),
|
|
52
|
+
codingAgent: text('coding_agent').notNull().default('claude-code'),
|
|
52
53
|
starred: integer('starred').notNull().default(0),
|
|
53
54
|
createdAt: integer('created_at').notNull(),
|
|
54
55
|
updatedAt: integer('updated_at').notNull(),
|
package/lib/tools/docker.js
CHANGED
|
@@ -50,16 +50,21 @@ async function detectNetwork() {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Create and start a
|
|
53
|
+
* Create and start a code workspace Docker container.
|
|
54
54
|
* @param {object} options
|
|
55
55
|
* @param {string} options.containerName - Docker container name
|
|
56
56
|
* @param {string} options.repo - GitHub repo full name (e.g. "owner/repo")
|
|
57
57
|
* @param {string} options.branch - Git branch name
|
|
58
|
+
* @param {string} [options.codingAgent='claude-code'] - Coding agent identifier
|
|
58
59
|
* @returns {Promise<{containerId: string}>}
|
|
59
60
|
*/
|
|
60
|
-
async function
|
|
61
|
+
async function createCodeWorkspaceContainer({ containerName, repo, branch, codingAgent = 'claude-code' }) {
|
|
62
|
+
if (codingAgent !== 'claude-code') {
|
|
63
|
+
throw new Error(`Unsupported coding agent: ${codingAgent}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
const version = process.env.THEPOPEBOT_VERSION || 'latest';
|
|
62
|
-
const image = `stephengpope/thepopebot:claude-workspace-${version}`;
|
|
67
|
+
const image = `stephengpope/thepopebot:claude-code-workspace-${version}`;
|
|
63
68
|
const network = await detectNetwork();
|
|
64
69
|
|
|
65
70
|
const env = [
|
|
@@ -97,4 +102,4 @@ async function createClaudeWorkspaceContainer({ containerName, repo, branch }) {
|
|
|
97
102
|
return { containerId };
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
export {
|
|
105
|
+
export { createCodeWorkspaceContainer };
|
package/package.json
CHANGED
|
@@ -3,6 +3,6 @@ import { CodePage } from 'thepopebot/code';
|
|
|
3
3
|
|
|
4
4
|
export default async function CodeRoute({ params }) {
|
|
5
5
|
const session = await auth();
|
|
6
|
-
const {
|
|
7
|
-
return <CodePage session={session}
|
|
6
|
+
const { codeWorkspaceId } = await params;
|
|
7
|
+
return <CodePage session={session} codeWorkspaceId={codeWorkspaceId} />;
|
|
8
8
|
}
|
|
@@ -41,18 +41,18 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
|
|
41
41
|
RUN npm install -g @anthropic-ai/claude-code
|
|
42
42
|
|
|
43
43
|
# Non-root user
|
|
44
|
-
RUN useradd -m -s /bin/bash claude \
|
|
45
|
-
&& mkdir -p /home/claude/
|
|
46
|
-
&& chown claude:claude /home/claude/
|
|
44
|
+
RUN useradd -m -s /bin/bash claude-code \
|
|
45
|
+
&& mkdir -p /home/claude-code/workspace \
|
|
46
|
+
&& chown claude-code:claude-code /home/claude-code/workspace
|
|
47
47
|
|
|
48
|
-
COPY .tmux.conf /home/claude/.tmux.conf
|
|
49
|
-
RUN chown claude:claude /home/claude/.tmux.conf
|
|
48
|
+
COPY .tmux.conf /home/claude-code/.tmux.conf
|
|
49
|
+
RUN chown claude-code:claude-code /home/claude-code/.tmux.conf
|
|
50
50
|
|
|
51
51
|
COPY entrypoint.sh /entrypoint.sh
|
|
52
52
|
RUN chmod +x /entrypoint.sh
|
|
53
53
|
|
|
54
|
-
USER claude
|
|
55
|
-
WORKDIR /home/claude/
|
|
54
|
+
USER claude-code
|
|
55
|
+
WORKDIR /home/claude-code/workspace
|
|
56
56
|
|
|
57
57
|
ENV REPO=""
|
|
58
58
|
ENV BRANCH=main
|
|
@@ -11,10 +11,10 @@ git config --global user.email "$GH_USER_EMAIL"
|
|
|
11
11
|
|
|
12
12
|
# Clone repo
|
|
13
13
|
if [ -n "$REPO" ]; then
|
|
14
|
-
git clone --branch "$BRANCH" "https://github.com/$REPO" /home/claude/
|
|
14
|
+
git clone --branch "$BRANCH" "https://github.com/$REPO" /home/claude-code/workspace
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
|
-
cd /home/claude/
|
|
17
|
+
cd /home/claude-code/workspace
|
|
18
18
|
WORKSPACE_DIR=$(pwd)
|
|
19
19
|
|
|
20
20
|
# Claude Code auth — use OAuth token, not API key
|
|
File without changes
|