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