Workspaces in Workspaces all around

This commit is contained in:
2026-02-19 01:49:15 +00:00
parent 72bca6cd42
commit dd8ab84df5
84 changed files with 3047 additions and 749 deletions
+3 -2
View File
@@ -135,7 +135,8 @@ async function handleChat({
const workingDir = state.cwd ?? homeDir;
const skillsAppend = await buildSkillsPrompt(ws.data.email);
if (skillsAppend) send(ws, { type: 'system:prompt', text: skillsAppend });
const contextAppend = `\n\nThe user's home directory is: ${homeDir}` + skillsAppend;
send(ws, { type: 'system:prompt', text: contextAppend });
// Build prompt: use AsyncIterable<SDKUserMessage> with image content blocks when images are present
let promptInput: string | AsyncIterable<SDKUserMessage> = prompt;
@@ -170,7 +171,7 @@ async function handleChat({
systemPrompt: {
type: 'preset',
preset: 'claude_code',
...(skillsAppend ? { append: skillsAppend } : {}),
append: contextAppend,
},
additionalDirectories: [],
includePartialMessages: true,
+65
View File
@@ -413,6 +413,71 @@ router.post('/git-clone', async (ctx) => {
return ctx.json({ ok: true });
});
// Download file or directory (directories are zipped on-the-fly)
router.get('/download', async (ctx) => {
const user = ctx.get('user');
const rootDir = getRootDir(user, ctx.req.query('root') ?? undefined);
const relPath = (ctx.req.query('path') || '').replace(/^\/+/, '');
if (!relPath) throw errors.BAD_REQUEST('path is required');
const absPath = resolveUserPath(rootDir, relPath);
const s = await stat(absPath);
const name = absPath.split('/').pop()!;
if (s.isDirectory()) {
const proc = Bun.spawn(['zip', '-r', '-', name], {
cwd: dirname(absPath),
stdout: 'pipe',
stderr: 'ignore',
});
return new Response(proc.stdout as ReadableStream, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="${name}.zip"`,
},
});
}
const file = Bun.file(absPath);
return new Response(file, {
headers: {
'Content-Type': file.type || 'application/octet-stream',
'Content-Disposition': `attachment; filename="${name}"`,
'Content-Length': String(s.size),
},
});
});
// Download multiple items as a single zip
router.post('/download', async (ctx) => {
const user = ctx.get('user');
const rootDir = getRootDir(user, ctx.req.query('root') ?? undefined);
const { paths } = ctx.get('body') as { paths: string[] };
if (!Array.isArray(paths) || paths.length === 0) throw errors.BAD_REQUEST('paths is required');
const items: string[] = [];
for (const p of paths) {
const relPath = p.replace(/^\/+/, '');
const absPath = resolveUserPath(rootDir, relPath);
await stat(absPath); // throws if not found
items.push(relPath);
}
const proc = Bun.spawn(['zip', '-r', '-', ...items], {
cwd: rootDir,
stdout: 'pipe',
stderr: 'ignore',
});
return new Response(proc.stdout as ReadableStream, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="download.zip"',
},
});
});
// Delete file or directory
router.delete('/rm', async (ctx) => {
const user = ctx.get('user');
+5 -3
View File
@@ -2,7 +2,7 @@ import type { ServerWebSocket } from 'bun';
import { mkdir, rename } from 'node:fs/promises';
import { join } from 'node:path';
import type { ClientMessage, ServerMessage, ImageData, TaskInfo } from '@@/api/chat-types';
import { getTmpAttachmentsDir, getAttachmentsDir, getOpencodeSessionDir, getNativeSkillsDir, getGlobalSkillsDir, getUserSkillsDir } from '@@/data-path';
import { getTmpAttachmentsDir, getAttachmentsDir, getOpencodeSessionDir, getHomeDir, getNativeSkillsDir, getGlobalSkillsDir, getUserSkillsDir } from '@@/data-path';
import { readSkillDirs, parseFrontmatter } from '@@/api/skills/skills';
import { createTaskLog, appendToLog, finalizeLog } from '@@/api/task-logger';
@@ -400,9 +400,11 @@ async function handleChat({ ws, prompt, sessionId, model, attachmentIds, images,
}
}
const homeDir = getHomeDir(ws.data.email);
const skillsAppend = await buildSkillsPrompt(ws.data.email);
if (skillsAppend) send(ws, { type: 'system:prompt', text: skillsAppend });
const fullPrompt = skillsAppend ? `<system>${skillsAppend}</system>\n\n${prompt}` : prompt;
const contextAppend = `\n\nThe user's home directory is: ${homeDir}` + skillsAppend;
send(ws, { type: 'system:prompt', text: contextAppend });
const fullPrompt = `<system>${contextAppend}</system>\n\n${prompt}`;
const parts: Record<string, unknown>[] = [];
if (images?.length) {
+17
View File
@@ -144,6 +144,23 @@ processesRouter.put('/:name/chat', async (ctx) => {
return ctx.json({ ok: true });
});
processesRouter.delete('/:name/chat', async (ctx) => {
const user = ctx.get('user');
const name = ctx.req.param('name');
const nativeProcesses = await readProcessDirs(getNativeProcessesDir());
const globalProcesses = await readProcessDirs(getGlobalProcessesDir());
const userProcesses = await readProcessDirs(getUserProcessesDir(user.email));
const resolved = resolveFile(name, nativeProcesses, globalProcesses, userProcesses);
if (!resolved) return ctx.text('Not found', 404);
const chatDir = join(dirname(resolved.filePath), 'chat');
await rm(chatDir, { recursive: true, force: true });
return ctx.json({ ok: true });
});
processesRouter.post('/', async (ctx) => {
const { name } = await ctx.req.json<{ name: string }>();
if (!name?.trim()) return ctx.text('Name is required', 400);
+17
View File
@@ -144,6 +144,23 @@ skillsRouter.put('/:name/chat', async (ctx) => {
return ctx.json({ ok: true });
});
skillsRouter.delete('/:name/chat', async (ctx) => {
const user = ctx.get('user');
const name = ctx.req.param('name');
const nativeSkills = await readSkillDirs(getNativeSkillsDir());
const globalSkills = await readSkillDirs(getGlobalSkillsDir());
const userSkills = await readSkillDirs(getUserSkillsDir(user.email));
const resolved = resolveFile(name, nativeSkills, globalSkills, userSkills);
if (!resolved) return ctx.text('Not found', 404);
const chatDir = join(dirname(resolved.filePath), 'chat');
await rm(chatDir, { recursive: true, force: true });
return ctx.json({ ok: true });
});
skillsRouter.post('/', async (ctx) => {
const { name } = await ctx.req.json<{ name: string }>();
if (!name?.trim()) return ctx.text('Name is required', 400);
+17
View File
@@ -171,6 +171,23 @@ tasksRouter.put('/:name/chat', async (ctx) => {
return ctx.json({ ok: true });
});
tasksRouter.delete('/:name/chat', async (ctx) => {
const user = ctx.get('user');
const name = ctx.req.param('name');
const nativeTasks = await readTaskDirs(getNativeTasksDir());
const globalTasks = await readTaskDirs(getGlobalTasksDir());
const userTasks = await readTaskDirs(getUserTasksDir(user.email));
const resolved = resolveFile(name, nativeTasks, globalTasks, userTasks);
if (!resolved) return ctx.text('Not found', 404);
const chatDir = join(dirname(resolved.filePath), 'chat');
await rm(chatDir, { recursive: true, force: true });
return ctx.json({ ok: true });
});
tasksRouter.post('/', async (ctx) => {
const { name } = await ctx.req.json<{ name: string }>();
if (!name?.trim()) return ctx.text('Name is required', 400);