Workspaces in Workspaces all around
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user