mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 08:56:22 +02:00
Refactor RowboatX configuration and enhance editor features
- Updated `next.config.ts` to scope Turbopack to the app's directory. - Modified `package.json` and `package-lock.json` to include new dependencies for Tiptap and markdown processing. - Removed deprecated chat API and added new agent and config routes for file management. - Introduced `JsonEditor` and `MarkdownViewer` components for improved content editing and display. - Enhanced `TiptapMarkdownEditor` with additional toolbar options and markdown parsing capabilities. - Updated layout and page components to integrate new editors and improve user experience.
This commit is contained in:
parent
da20e280f4
commit
c637cb49ac
26 changed files with 2552 additions and 540 deletions
|
|
@ -1,72 +0,0 @@
|
|||
import { cliClient, RunEvent } from '@/lib/cli-client';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* POST /api/chat
|
||||
* Creates a new conversation or sends a message to existing one
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { message, runId, agentId } = body;
|
||||
|
||||
if (!message || typeof message !== 'string') {
|
||||
return Response.json(
|
||||
{ error: 'Message is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let currentRunId = runId;
|
||||
|
||||
// Create new run if no runId provided
|
||||
if (!currentRunId) {
|
||||
const run = await cliClient.createRun({
|
||||
agentId: agentId || 'copilot',
|
||||
});
|
||||
currentRunId = run.id;
|
||||
}
|
||||
|
||||
// Always send the message (this triggers the agent runtime)
|
||||
await cliClient.sendMessage(currentRunId, message);
|
||||
|
||||
// Return the run ID
|
||||
return Response.json({ runId: currentRunId });
|
||||
} catch (error) {
|
||||
console.error('Chat API error:', error);
|
||||
return Response.json(
|
||||
{ error: 'Failed to process message' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/chat?runId=xxx
|
||||
* Get a specific run's details
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const runId = searchParams.get('runId');
|
||||
|
||||
if (!runId) {
|
||||
// List all runs
|
||||
const result = await cliClient.listRuns();
|
||||
return Response.json(result);
|
||||
}
|
||||
|
||||
// Get specific run
|
||||
const run = await cliClient.getRun(runId);
|
||||
return Response.json(run);
|
||||
} catch (error) {
|
||||
console.error('Chat API error:', error);
|
||||
return Response.json(
|
||||
{ error: 'Failed to fetch run' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
64
apps/rowboatx/app/api/rowboat/agent/route.ts
Normal file
64
apps/rowboatx/app/api/rowboat/agent/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
const AGENTS_ROOT = path.join(os.homedir(), ".rowboat", "agents");
|
||||
|
||||
function resolveAgentPath(fileParam: string): { target: string; relative: string } {
|
||||
// Normalize and strip any attempted path traversal.
|
||||
const normalized = path.normalize(fileParam).replace(/^(\.\.(\/|\\|$))+/, "");
|
||||
const target = path.join(AGENTS_ROOT, normalized);
|
||||
if (!target.startsWith(AGENTS_ROOT)) {
|
||||
throw new Error("Invalid path");
|
||||
}
|
||||
return { target, relative: normalized };
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const fileParam = req.nextUrl.searchParams.get("file");
|
||||
if (!fileParam) {
|
||||
return Response.json({ error: "file param required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { target, relative } = resolveAgentPath(fileParam);
|
||||
const content = await fs.readFile(target, "utf8");
|
||||
return Response.json({ file: relative, content, raw: content });
|
||||
} catch (error: unknown) {
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
if (err?.code === "ENOENT") {
|
||||
return Response.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
if (err instanceof Error && err.message === "Invalid path") {
|
||||
return Response.json({ error: "Invalid file path" }, { status: 400 });
|
||||
}
|
||||
console.error("Failed to read agent file", error);
|
||||
return Response.json({ error: "Failed to read agent file" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const fileParam = req.nextUrl.searchParams.get("file");
|
||||
if (!fileParam) {
|
||||
return Response.json({ error: "file param required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { target, relative } = resolveAgentPath(fileParam);
|
||||
const content = await req.text();
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
await fs.writeFile(target, content, "utf8");
|
||||
return Response.json({ file: relative, success: true });
|
||||
} catch (error: unknown) {
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
if (err?.code === "ENOENT") {
|
||||
return Response.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
if (err instanceof Error && err.message === "Invalid path") {
|
||||
return Response.json({ error: "Invalid file path" }, { status: 400 });
|
||||
}
|
||||
console.error("Failed to write agent file", error);
|
||||
return Response.json({ error: "Failed to write agent file" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
74
apps/rowboatx/app/api/rowboat/config/route.ts
Normal file
74
apps/rowboatx/app/api/rowboat/config/route.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
const CONFIG_ROOT = path.join(os.homedir(), ".rowboat", "config");
|
||||
|
||||
function resolveConfigPath(fileParam: string): { target: string; relative: string } {
|
||||
const normalized = path.normalize(fileParam).replace(/^(\.\.(\/|\\|$))+/, "");
|
||||
const target = path.join(CONFIG_ROOT, normalized);
|
||||
if (!target.startsWith(CONFIG_ROOT)) {
|
||||
throw new Error("Invalid path");
|
||||
}
|
||||
return { target, relative: normalized };
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const fileParam = req.nextUrl.searchParams.get("file");
|
||||
if (!fileParam) {
|
||||
return Response.json({ error: "file param required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { target, relative } = resolveConfigPath(fileParam);
|
||||
const content = await fs.readFile(target, "utf8");
|
||||
return Response.json({ file: relative, content, raw: content });
|
||||
} catch (error: unknown) {
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
if (err?.code === "ENOENT") {
|
||||
return Response.json(
|
||||
{ error: "File not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error && error.message === "Invalid path") {
|
||||
return Response.json(
|
||||
{ error: "Invalid file path" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
console.error("Failed to read config file", error);
|
||||
return Response.json(
|
||||
{ error: "Failed to read config file" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const fileParam = req.nextUrl.searchParams.get("file");
|
||||
if (!fileParam) {
|
||||
return Response.json({ error: "file param required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { target, relative } = resolveConfigPath(fileParam);
|
||||
const content = await req.text();
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
await fs.writeFile(target, content, "utf8");
|
||||
return Response.json({ file: relative, success: true });
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && error.message === "Invalid path") {
|
||||
return Response.json(
|
||||
{ error: "Invalid file path" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
console.error("Failed to write config file", error);
|
||||
return Response.json(
|
||||
{ error: "Failed to write config file" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,26 +5,38 @@ import { promises as fs } from "fs";
|
|||
|
||||
const ROWBOAT_ROOT = path.join(os.homedir(), ".rowboat", "runs");
|
||||
|
||||
function resolveRunPath(fileParam: string): { target: string; relative: string } {
|
||||
const normalized = path.normalize(fileParam).replace(/^(\.\.(\/|\\|$))+/, "");
|
||||
const target = path.join(ROWBOAT_ROOT, normalized);
|
||||
if (!target.startsWith(ROWBOAT_ROOT)) {
|
||||
throw new Error("Invalid path");
|
||||
}
|
||||
return { target, relative: normalized };
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const fileParam = req.nextUrl.searchParams.get("file");
|
||||
if (!fileParam) {
|
||||
return Response.json({ error: "file param required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prevent path traversal: only allow basenames.
|
||||
const safeName = path.basename(fileParam);
|
||||
const target = path.join(ROWBOAT_ROOT, safeName);
|
||||
|
||||
try {
|
||||
const { target, relative } = resolveRunPath(fileParam);
|
||||
const content = await fs.readFile(target, "utf8");
|
||||
let parsed: any = null;
|
||||
let parsed: unknown = null;
|
||||
try {
|
||||
parsed = JSON.parse(content);
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
return Response.json({ file: safeName, parsed, raw: content });
|
||||
} catch (error: any) {
|
||||
return Response.json({ file: relative, parsed, raw: content });
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && error.message === "Invalid path") {
|
||||
return Response.json(
|
||||
{ error: "Invalid file path" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
console.error("Failed to read run file", error);
|
||||
return Response.json(
|
||||
{ error: "Failed to read run file" },
|
||||
|
|
|
|||
|
|
@ -1,24 +1,39 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
const ROWBOAT_ROOT = path.join(os.homedir(), ".rowboat");
|
||||
|
||||
async function safeList(dir: string): Promise<string[]> {
|
||||
const full = path.join(ROWBOAT_ROOT, dir);
|
||||
try {
|
||||
const entries = await fs.readdir(full, { withFileTypes: true });
|
||||
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
async function listRecursive(dir: string): Promise<string[]> {
|
||||
const root = path.join(ROWBOAT_ROOT, dir);
|
||||
|
||||
const walk = async (current: string, prefix = ""): Promise<string[]> => {
|
||||
const results: string[] = [];
|
||||
try {
|
||||
const entries = await fs.readdir(current, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...(await walk(path.join(current, entry.name), relPath)));
|
||||
} else if (entry.isFile()) {
|
||||
results.push(relPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
return walk(root);
|
||||
}
|
||||
|
||||
export async function GET(_req: NextRequest) {
|
||||
const agents = await safeList("agents");
|
||||
const config = await safeList("config");
|
||||
const runs = await safeList("runs");
|
||||
export async function GET() {
|
||||
const agents = await listRecursive("agents");
|
||||
const config = await listRecursive("config");
|
||||
const runs = await listRecursive("runs");
|
||||
|
||||
return Response.json({
|
||||
agents,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export async function GET(request: NextRequest) {
|
|||
reader?.cancel();
|
||||
try {
|
||||
controller.close();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Already closed, ignore
|
||||
}
|
||||
});
|
||||
|
|
@ -43,10 +43,11 @@ export async function GET(request: NextRequest) {
|
|||
throw new Error(`Failed to connect to backend: ${response.statusText}`);
|
||||
}
|
||||
|
||||
reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
const body = response.body;
|
||||
if (!body) {
|
||||
throw new Error('No response body');
|
||||
}
|
||||
reader = body.getReader();
|
||||
|
||||
// Read and forward stream
|
||||
while (!isClosed) {
|
||||
|
|
@ -60,15 +61,15 @@ export async function GET(request: NextRequest) {
|
|||
if (!isClosed) {
|
||||
try {
|
||||
controller.enqueue(value);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Controller closed, stop reading
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
// Only log non-abort errors
|
||||
if (error.name !== 'AbortError') {
|
||||
if ((error as Error).name !== 'AbortError') {
|
||||
console.error('Stream error:', error);
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const errorMessage = `data: ${JSON.stringify({ type: 'error', error: String(error) })}\n\n`;
|
||||
controller.enqueue(encoder.encode(errorMessage));
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Controller already closed, ignore
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +87,7 @@ export async function GET(request: NextRequest) {
|
|||
if (reader) {
|
||||
try {
|
||||
await reader.cancel();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore cancel errors
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +95,7 @@ export async function GET(request: NextRequest) {
|
|||
if (!isClosed) {
|
||||
try {
|
||||
controller.close();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Already closed, ignore
|
||||
}
|
||||
}
|
||||
|
|
@ -110,4 +111,3 @@ export async function GET(request: NextRequest) {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue