diff --git a/src/client/src/api.ts b/src/client/src/api.ts index ddb5f32..f2296ae 100644 --- a/src/client/src/api.ts +++ b/src/client/src/api.ts @@ -102,7 +102,7 @@ export const api = { status: (sessionId: string) => request(`/api/sessions/${sessionId}/status`, parseSessionStatus), commands: (sessionId: string) => request(`/api/sessions/${sessionId}/commands`, arrayOf(parseSlashCommand)), files: (cwd: string, query: string, kind?: FileSuggestion["kind"]) => request(`/api/files?cwd=${encodeURIComponent(cwd)}&q=${encodeURIComponent(query)}${kind !== undefined ? `&kind=${encodeURIComponent(kind)}` : ""}`, arrayOf(parseFileSuggestion)), - prompt: (sessionId: string, text: string) => request(`/api/sessions/${sessionId}/prompt`, parseAccepted, { method: "POST", body: JSON.stringify({ text }) }), + prompt: (sessionId: string, text: string, streamingBehavior?: "steer" | "followUp") => request(`/api/sessions/${sessionId}/prompt`, parseAccepted, { method: "POST", body: JSON.stringify(streamingBehavior === undefined ? { text } : { text, streamingBehavior }) }), shell: (sessionId: string, text: string) => request(`/api/sessions/${sessionId}/shell`, parseAccepted, { method: "POST", body: JSON.stringify({ text }) }), runCommand: (sessionId: string, text: string) => request(`/api/sessions/${sessionId}/commands/run`, parseCommandResult, { method: "POST", body: JSON.stringify({ text }) }), respondToCommand: (sessionId: string, requestId: string, value: string) => request(`/api/sessions/${sessionId}/commands/respond`, parseCommandResult, { method: "POST", body: JSON.stringify({ requestId, value }) }), diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts index 7f9c43b..c050ddc 100644 --- a/src/client/src/components/PiWebApp.ts +++ b/src/client/src/components/PiWebApp.ts @@ -114,9 +114,9 @@ export class PiWebApp extends LitElement {
${state.error ? html`
${state.error}
` : null} ${state.selectedSession ? html` - 0} .loadingMore=${state.isLoadingEarlierMessages} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}> - this.sessions.send(text)} .onStopSession=${() => this.sessions.stopSession()}> + this.sessions.send(text, streamingBehavior)} .onStopSession=${() => this.sessions.stopSession()}> + ${state.commandDialog !== undefined ? html` this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}>` : null} ` : html`
Select or start a session.
`}
diff --git a/src/client/src/components/PromptEditor.ts b/src/client/src/components/PromptEditor.ts index 5297f6e..0e4fce7 100644 --- a/src/client/src/components/PromptEditor.ts +++ b/src/client/src/components/PromptEditor.ts @@ -10,7 +10,8 @@ export class PromptEditor extends LitElement { @property({ type: Boolean }) disabled = false; @property() sessionId?: string; @property() cwd?: string; - @property({ attribute: false }) onSend?: (text: string) => void; + @property({ type: Boolean }) canSteer = false; + @property({ attribute: false }) onSend?: (text: string, streamingBehavior?: "steer" | "followUp") => void; @property({ attribute: false }) onStopSession?: () => void; @query("textarea") private textarea?: HTMLTextAreaElement; @state() private draft = ""; @@ -49,7 +50,8 @@ export class PromptEditor extends LitElement { ${shellMode ? html`
Shell command${inputMode.excludeFromContext ? " ยท excluded from context" : ""}
` : null} { this.pick(item); }}> - + + ${this.canSteer ? html`` : null} `; @@ -140,7 +142,7 @@ export class PromptEditor extends LitElement { } if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); - this.send(); + this.send(this.canSteer ? "followUp" : undefined); } } @@ -150,13 +152,13 @@ export class PromptEditor extends LitElement { this.completions = []; } - private send() { + private send(streamingBehavior?: "steer" | "followUp") { const text = this.draft.trim(); if (text === "" || this.disabled) return; this.draft = ""; if (this.sessionId !== undefined && this.sessionId !== "") clearDraft(this.sessionId); this.completions = []; - this.onSend?.(text); + this.onSend?.(text, this.canSteer ? streamingBehavior : undefined); } static override styles = promptEditorStyles; diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts index 762b566..ee90b2e 100644 --- a/src/client/src/components/shared.ts +++ b/src/client/src/components/shared.ts @@ -104,7 +104,7 @@ export const formattedTextStyles = css` export const statusBarStyles = css` :host { display: block; color: #8b949e; font: 12px system-ui, sans-serif; } - .bar { display: flex; gap: 12px; align-items: center; min-width: 0; padding: 7px 12px; border-bottom: 1px solid #30363d; background: #0d1117; white-space: nowrap; overflow: hidden; } + .bar { display: flex; gap: 12px; align-items: center; min-width: 0; padding: 7px 12px; border-top: 1px solid #30363d; background: #0d1117; white-space: nowrap; overflow: hidden; } span { overflow: hidden; text-overflow: ellipsis; } .bar > span:first-child { flex: 1 1 auto; min-width: 80px; } .activity { display: inline-flex; align-items: center; gap: 6px; color: #8b949e; } diff --git a/src/client/src/controllers/sessionController.ts b/src/client/src/controllers/sessionController.ts index 068cc64..a6b2230 100644 --- a/src/client/src/controllers/sessionController.ts +++ b/src/client/src/controllers/sessionController.ts @@ -81,7 +81,7 @@ export class SessionController { } } - async send(text: string) { + async send(text: string, streamingBehavior?: "steer" | "followUp") { const trimmed = text.trim(); if (trimmed.startsWith("/")) return this.runCommand(text); if (isShellInput(text)) return this.runShell(text); @@ -89,7 +89,7 @@ export class SessionController { if (!session) return; this.setState({ messages: [...this.getState().messages, textMessage("user", text)] }); try { - await api.prompt(session.id, text); + await api.prompt(session.id, text, streamingBehavior); } catch (error) { this.setState({ error: String(error) }); } diff --git a/src/server/sessiond/sessionProxyRoutes.ts b/src/server/sessiond/sessionProxyRoutes.ts index 640de7b..b47cd29 100644 --- a/src/server/sessiond/sessionProxyRoutes.ts +++ b/src/server/sessiond/sessionProxyRoutes.ts @@ -21,7 +21,7 @@ export function registerSessionProxyRoutes(app: FastifyInstance, daemon = new Se app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/messages", (request, reply) => proxy(request, reply)); app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/status", (request, reply) => proxy(request, reply)); app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/commands", (request, reply) => proxy(request, reply)); - app.post<{ Params: { sessionId: string }; Body: { text: string } }>("/api/sessions/:sessionId/prompt", (request, reply) => proxy(request, reply)); + app.post<{ Params: { sessionId: string }; Body: { text: string; streamingBehavior?: "steer" | "followUp" } }>("/api/sessions/:sessionId/prompt", (request, reply) => proxy(request, reply)); app.post<{ Params: { sessionId: string }; Body: { text: string } }>("/api/sessions/:sessionId/shell", (request, reply) => proxy(request, reply)); app.post<{ Params: { sessionId: string }; Body: { text: string } }>("/api/sessions/:sessionId/commands/run", (request, reply) => proxy(request, reply)); app.post<{ Params: { sessionId: string }; Body: { requestId: string; value: string } }>("/api/sessions/:sessionId/commands/respond", (request, reply) => proxy(request, reply)); diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index d4e9774..5d988a6 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -103,10 +103,11 @@ export class PiSessionService { return commands.sort((a, b) => a.name.localeCompare(b.name)); } - async prompt(sessionId: string, text: string): Promise { + async prompt(sessionId: string, text: string, streamingBehavior?: "steer" | "followUp"): Promise { const session = await this.getOrOpen(sessionId); - this.publishActivity(session, "prompt accepted", "active"); - void session.prompt(text).catch((error: unknown) => { + const behavior = session.isStreaming ? streamingBehavior ?? "followUp" : undefined; + this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active"); + void session.prompt(text, behavior === undefined ? undefined : { streamingBehavior: behavior }).catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); this.publishActivity(session, "error", "error", message); this.events.publish(sessionId, { type: "session.error", message }); diff --git a/src/server/sessions/sessionRoutes.ts b/src/server/sessions/sessionRoutes.ts index 849658e..563ca83 100644 --- a/src/server/sessions/sessionRoutes.ts +++ b/src/server/sessions/sessionRoutes.ts @@ -41,9 +41,9 @@ export function registerSessionRoutes(app: FastifyInstance, sessions: PiSessionS } }); - app.post<{ Params: { sessionId: string }; Body: { text: string } }>(`${prefix}/sessions/:sessionId/prompt`, async (request, reply) => { + app.post<{ Params: { sessionId: string }; Body: { text: string; streamingBehavior?: "steer" | "followUp" } }>(`${prefix}/sessions/:sessionId/prompt`, async (request, reply) => { try { - await sessions.prompt(request.params.sessionId, request.body.text); + await sessions.prompt(request.params.sessionId, request.body.text, request.body.streamingBehavior); return { accepted: true }; } catch (error) { return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });