Add queue and steer prompt actions

This commit is contained in:
Federico Jaramillo Martinez
2026-05-07 15:55:36 +02:00
parent c7119c1197
commit 398fe3153d
8 changed files with 20 additions and 17 deletions
+1 -1
View File
@@ -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 }) }),
+2 -2
View File
@@ -114,9 +114,9 @@ export class PiWebApp extends LitElement {
<main>
${state.error ? html`<div class="error">${state.error}</div>` : null}
${state.selectedSession ? html`
<status-bar .status=${state.status} .activity=${state.activity} .workspace=${state.selectedWorkspace}></status-bar>
<chat-view .sessionId=${state.selectedSession.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
<prompt-editor .sessionId=${state.selectedSession.id} .cwd=${state.selectedWorkspace?.path} .onSend=${(text: string) => this.sessions.send(text)} .onStopSession=${() => this.sessions.stopSession()}></prompt-editor>
<prompt-editor .sessionId=${state.selectedSession.id} .cwd=${state.selectedWorkspace?.path} .canSteer=${state.status?.isStreaming === true} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => this.sessions.send(text, streamingBehavior)} .onStopSession=${() => this.sessions.stopSession()}></prompt-editor>
<status-bar .status=${state.status} .activity=${state.activity} .workspace=${state.selectedWorkspace}></status-bar>
${state.commandDialog !== undefined ? html`<command-picker .title=${state.commandDialog.title} .options=${state.commandDialog.options} .onPick=${(value: string) => this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}></command-picker>` : null}
` : html`<div class="empty">Select or start a session.</div>`}
</main>
+7 -5
View File
@@ -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`<div class="mode-hint">Shell command${inputMode.excludeFromContext ? " · excluded from context" : ""}</div>` : null}
<autocomplete-menu .items=${this.completions} .selectedIndex=${this.selectedIndex} .onPick=${(item: CompletionItem) => { this.pick(item); }}></autocomplete-menu>
</div>
<button ?disabled=${this.disabled} @click=${() => { this.send(); }}>Send</button>
<button ?disabled=${this.disabled} title=${this.canSteer ? "Queue after the current response" : "Send message"} @click=${() => { this.send("followUp"); }}>${this.canSteer ? "Queue" : "Send"}</button>
${this.canSteer ? html`<button ?disabled=${this.disabled} title="Steer the current response before the next model call" @click=${() => { this.send("steer"); }}>Steer</button>` : null}
<button ?disabled=${this.disabled} title="Stop only this Pi session from continuing" @click=${() => this.onStopSession?.()}>Stop session</button>
</footer>
`;
@@ -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;
+1 -1
View File
@@ -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; }
@@ -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) });
}