diff --git a/packages/opencode-config/plugin/codenomad.ts b/packages/opencode-config/plugin/codenomad.ts index 08515dd8..ec4a2af2 100644 --- a/packages/opencode-config/plugin/codenomad.ts +++ b/packages/opencode-config/plugin/codenomad.ts @@ -1,6 +1,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import { createCodeNomadClient, getCodeNomadConfig } from "./lib/client" import { createBackgroundProcessTools } from "./lib/background-process" +import { createSessionTools } from "./lib/session" let voiceModeEnabled = false @@ -8,6 +9,7 @@ export async function CodeNomadPlugin(input: PluginInput) { const config = getCodeNomadConfig() const client = createCodeNomadClient(config) const backgroundProcessTools = createBackgroundProcessTools(config, { baseDir: input.directory }) + const sessionTools = createSessionTools(config) await client.startEvents((event) => { if (event.type === "codenomad.ping") { @@ -29,6 +31,7 @@ export async function CodeNomadPlugin(input: PluginInput) { return { tool: { ...backgroundProcessTools, + ...sessionTools, }, async "chat.message"(_input: { sessionID: string }, output: { message: { system?: string } }) { if (!voiceModeEnabled) { diff --git a/packages/opencode-config/plugin/lib/session.ts b/packages/opencode-config/plugin/lib/session.ts new file mode 100644 index 00000000..e1777d1e --- /dev/null +++ b/packages/opencode-config/plugin/lib/session.ts @@ -0,0 +1,49 @@ +import { tool } from "@opencode-ai/plugin/tool" +import { createCodeNomadRequester, type CodeNomadConfig } from "./request" + +type SessionRenameResponse = { + sessionID: string + title: string +} + +export function createSessionTools(config: CodeNomadConfig) { + const requester = createCodeNomadRequester(config) + + const request = async (path: string, init?: RequestInit): Promise => { + return requester.requestJson(path, init) + } + + return { + rename_session: tool({ + description: + "Rename the current session when the user asks to change the chat title. Use a short descriptive title that reflects the current task.", + args: { + title: tool.schema + .string() + .describe("New session title, kept short and descriptive, for example 'Fix login bug' or 'Add session rename tool'"), + }, + async execute(args, context) { + const sessionID = context.sessionID + if (!sessionID) { + return "Error: No active session is available for renaming." + } + + const trimmedTitle = args.title.trim() + if (!trimmedTitle) { + return "Error: Session title cannot be empty." + } + + const result = await request("/session/title", { + method: "POST", + body: JSON.stringify({ + sessionID, + directory: context.directory, + title: trimmedTitle, + }), + }) + + return `Renamed session ${result.sessionID} to \"${result.title}\".` + }, + }), + } +} diff --git a/packages/server/src/server/routes/plugin.ts b/packages/server/src/server/routes/plugin.ts index daa7630e..5f15de41 100644 --- a/packages/server/src/server/routes/plugin.ts +++ b/packages/server/src/server/routes/plugin.ts @@ -27,6 +27,12 @@ const VoiceModeStateSchema = z.object({ connectionId: z.string().trim().min(1), }) +const SessionTitleUpdateSchema = z.object({ + sessionID: z.string().trim().min(1), + directory: z.string().trim().min(1).optional(), + title: z.string().trim().min(1), +}) + export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) { app.get<{ Params: { id: string } }>("/workspaces/:id/plugin/events", (request, reply) => { const workspace = deps.workspaceManager.get(request.params.id) @@ -92,6 +98,49 @@ export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) { return } + if (normalized === "session/title" && request.method === "POST") { + const parsed = SessionTitleUpdateSchema.parse(request.body ?? {}) + const port = deps.workspaceManager.getInstancePort(workspaceId) + if (!port) { + reply.code(502).send({ error: "Workspace instance is not ready" }) + return + } + + const params = new URLSearchParams() + if (parsed.directory) { + params.set("directory", parsed.directory) + } + + const targetUrl = `http://127.0.0.1:${port}/session/${encodeURIComponent(parsed.sessionID)}${params.size > 0 ? `?${params.toString()}` : ""}` + const headers: Record = { + "content-type": "application/json", + } + + const authorization = deps.workspaceManager.getInstanceAuthorizationHeader(workspaceId) + if (authorization) { + headers.authorization = authorization + } + + const response = await fetch(targetUrl, { + method: "PATCH", + headers, + body: JSON.stringify({ title: parsed.title }), + }) + + if (!response.ok) { + const message = await response.text().catch(() => "") + reply.code(response.status).send({ error: message || `Session update failed with ${response.status}` }) + return + } + + const payload = (await response.json().catch(() => null)) as { id?: string; title?: string } | null + reply.send({ + sessionID: payload?.id ?? parsed.sessionID, + title: payload?.title ?? parsed.title, + }) + return + } + reply.code(404).send({ error: "Unknown plugin endpoint" }) }