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:
tusharmagar 2025-12-22 09:14:09 +05:30 committed by Ramnique Singh
parent da20e280f4
commit c637cb49ac
26 changed files with 2552 additions and 540 deletions

View file

@ -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 }
);
}
}

View 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 });
}
}

View 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 }
);
}
}

View file

@ -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" },

View 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,

View file

@ -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) {
},
});
}