feat: add directory autocomplete with validation for new session form

This commit is contained in:
weishu
2025-12-28 16:31:53 +08:00
parent ce40de900f
commit 05deb6fff8
9 changed files with 292 additions and 37 deletions
+20 -14
View File
@@ -133,16 +133,8 @@ export type RpcReadFileResponse = {
error?: string
}
export type SlashCommand = {
name: string
description?: string
source: 'builtin' | 'user'
}
export type RpcSlashCommandsResponse = {
success: boolean
commands?: SlashCommand[]
error?: string
export type RpcPathExistsResponse = {
exists: Record<string, boolean>
}
export type SyncEventType =
@@ -697,6 +689,24 @@ export class SyncEngine {
}
}
async checkPathsExist(machineId: string, paths: string[]): Promise<Record<string, boolean>> {
const result = await this.machineRpc(machineId, 'path-exists', { paths }) as RpcPathExistsResponse | unknown
if (!result || typeof result !== 'object') {
throw new Error('Unexpected path-exists result')
}
const existsValue = (result as RpcPathExistsResponse).exists
if (!existsValue || typeof existsValue !== 'object') {
throw new Error('Unexpected path-exists result')
}
const exists: Record<string, boolean> = {}
for (const [key, value] of Object.entries(existsValue)) {
exists[key] = value === true
}
return exists
}
async getGitStatus(sessionId: string, cwd?: string): Promise<RpcCommandResponse> {
return await this.sessionRpc(sessionId, 'git-status', { cwd }) as RpcCommandResponse
}
@@ -717,10 +727,6 @@ export class SyncEngine {
return await this.sessionRpc(sessionId, 'ripgrep', { args, cwd }) as RpcCommandResponse
}
async listSlashCommands(sessionId: string, agent: string): Promise<RpcSlashCommandsResponse> {
return await this.sessionRpc(sessionId, 'listSlashCommands', { agent }) as RpcSlashCommandsResponse
}
private async sessionRpc(sessionId: string, method: string, params: unknown): Promise<unknown> {
return await this.rpcCall(`${sessionId}:${method}`, params)
}
+35
View File
@@ -11,6 +11,10 @@ const spawnBodySchema = z.object({
worktreeName: z.string().optional()
})
const pathsExistsSchema = z.object({
paths: z.array(z.string().min(1)).max(1000)
})
export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Hono<WebAppEnv> {
const app = new Hono<WebAppEnv>()
@@ -53,5 +57,36 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho
return c.json(result)
})
app.post('/machines/:id/paths/exists', async (c) => {
const engine = getSyncEngine()
if (!engine) {
return c.json({ error: 'Not connected' }, 503)
}
const machineId = c.req.param('id')
const machine = engine.getMachine(machineId)
if (!machine) {
return c.json({ error: 'Machine not found' }, 404)
}
const body = await c.req.json().catch(() => null)
const parsed = pathsExistsSchema.safeParse(body)
if (!parsed.success) {
return c.json({ error: 'Invalid body' }, 400)
}
const uniquePaths = Array.from(new Set(parsed.data.paths.map((path) => path.trim()).filter(Boolean)))
if (uniquePaths.length === 0) {
return c.json({ exists: {} })
}
try {
const exists = await engine.checkPathsExist(machineId, uniquePaths)
return c.json({ exists })
} catch (error) {
return c.json({ error: error instanceof Error ? error.message : 'Failed to check paths' }, 500)
}
})
return app
}
+2
View File
@@ -7,6 +7,7 @@ import { requireSessionFromParam, requireSyncEngine } from './guards'
type SessionSummaryMetadata = {
name?: string
path: string
machineId?: string
summary?: { text: string }
flavor?: string | null
worktree?: {
@@ -35,6 +36,7 @@ function toSessionSummary(session: Session): SessionSummary {
const metadata: SessionSummaryMetadata | null = session.metadata ? {
name: session.metadata.name,
path: session.metadata.path,
machineId: session.metadata.machineId ?? undefined,
summary: session.metadata.summary ? { text: session.metadata.summary.text } : undefined,
flavor: session.metadata.flavor ?? null,
worktree: session.metadata.worktree