Archived
Improve compaction session UX
This commit is contained in:
@@ -27,6 +27,8 @@ export class ChatView extends LitElement {
|
||||
@property({ type: Number }) messageTotal = 0;
|
||||
@property({ type: Boolean }) hasMore = false;
|
||||
@property({ type: Boolean }) loadingMore = false;
|
||||
@property({ type: Boolean }) isCompacting = false;
|
||||
@property({ type: Number }) pendingMessageCount = 0;
|
||||
@property({ attribute: false }) onLoadMore?: () => void;
|
||||
@query(".chat") private chat?: HTMLDivElement;
|
||||
@state() private pinnedToBottom = true;
|
||||
@@ -60,11 +62,23 @@ export class ChatView extends LitElement {
|
||||
${groupChatMessages(this.messages, this.messageStart).map((group) => group.kind === "message"
|
||||
? this.renderMessage(group.message, group.index)
|
||||
: this.renderMessageGroup(group.messages, group.startIndex))}
|
||||
${this.renderSessionActivity()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSessionActivity() {
|
||||
if (!this.isCompacting) return null;
|
||||
return html`
|
||||
<aside class="session-activity compacting" aria-live="polite">
|
||||
<strong>Compacting history…</strong>
|
||||
<span>The agent is summarizing earlier context. New prompts will be queued until compaction finishes.</span>
|
||||
${this.pendingMessageCount > 0 ? html`<small>${this.pendingMessageCount} queued ${this.pendingMessageCount === 1 ? "message" : "messages"}</small>` : null}
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderHistoryIndicator() {
|
||||
if (!this.messages.length || this.messageTotal <= 0) return null;
|
||||
const loadedCount = this.messages.length;
|
||||
|
||||
@@ -114,8 +114,8 @@ export class PiWebApp extends LitElement {
|
||||
<main>
|
||||
${state.error ? html`<div class="error">${state.error}</div>` : null}
|
||||
${state.selectedSession ? html`
|
||||
<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} .canSteer=${state.status?.isStreaming === true} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => this.sessions.send(text, streamingBehavior)} .onStopSession=${() => this.sessions.stopSession()}></prompt-editor>
|
||||
<chat-view .sessionId=${state.selectedSession.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
|
||||
<prompt-editor .sessionId=${state.selectedSession.id} .cwd=${state.selectedWorkspace?.path} .canSteer=${state.status?.isStreaming === true} .isCompacting=${state.status?.isCompacting === 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>`}
|
||||
|
||||
@@ -11,6 +11,7 @@ export class PromptEditor extends LitElement {
|
||||
@property() sessionId?: string;
|
||||
@property() cwd?: string;
|
||||
@property({ type: Boolean }) canSteer = false;
|
||||
@property({ type: Boolean }) isCompacting = false;
|
||||
@property({ attribute: false }) onSend?: (text: string, streamingBehavior?: "steer" | "followUp") => void;
|
||||
@property({ attribute: false }) onStopSession?: () => void;
|
||||
@query("textarea") private textarea?: HTMLTextAreaElement;
|
||||
@@ -35,6 +36,7 @@ export class PromptEditor extends LitElement {
|
||||
override render() {
|
||||
const inputMode = inputModeForDraft(this.draft);
|
||||
const shellMode = inputMode.kind === "shell";
|
||||
const queuesInput = this.canSteer || this.isCompacting;
|
||||
return html`
|
||||
<footer class=${shellMode ? "shell-mode" : ""}>
|
||||
<div class="editor-wrap">
|
||||
@@ -48,11 +50,12 @@ export class PromptEditor extends LitElement {
|
||||
placeholder="Message pi... Use / for commands, @ for files"
|
||||
></textarea>
|
||||
${shellMode ? html`<div class="mode-hint">Shell command${inputMode.excludeFromContext ? " · excluded from context" : ""}</div>` : null}
|
||||
${this.isCompacting && !shellMode ? html`<div class="mode-hint">Compacting history · message will be queued</div>` : null}
|
||||
<autocomplete-menu .items=${this.completions} .selectedIndex=${this.selectedIndex} .onPick=${(item: CompletionItem) => { this.pick(item); }}></autocomplete-menu>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<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=${queuesInput ? "Queue until the current activity finishes" : "Send message"} @click=${() => { this.send("followUp"); }}>${queuesInput ? "Queue" : "Send"}</button>
|
||||
${this.canSteer && !this.isCompacting ? 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>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -144,7 +147,7 @@ export class PromptEditor extends LitElement {
|
||||
}
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
this.send(this.canSteer ? "followUp" : undefined);
|
||||
this.send(this.canSteer || this.isCompacting ? "followUp" : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +163,7 @@ export class PromptEditor extends LitElement {
|
||||
this.draft = "";
|
||||
if (this.sessionId !== undefined && this.sessionId !== "") clearDraft(this.sessionId);
|
||||
this.completions = [];
|
||||
this.onSend?.(text, this.canSteer ? streamingBehavior : undefined);
|
||||
this.onSend?.(text, this.canSteer || this.isCompacting ? streamingBehavior : undefined);
|
||||
}
|
||||
|
||||
static override styles = promptEditorStyles;
|
||||
|
||||
@@ -69,6 +69,10 @@ export const chatStyles = css`
|
||||
.group-msg.system { color: #ff7b72; }
|
||||
.group-msg.bash { color: #3fb950; }
|
||||
.history-boundary { display: grid; gap: 3px; margin: 0 0 14px; color: #8b949e; font-size: 12px; text-align: center; }
|
||||
.session-activity { display: grid; gap: 4px; margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; color: #e6edf3; }
|
||||
.session-activity.compacting { border-color: #a371f7; background: #21132f; }
|
||||
.session-activity strong { color: #d2a8ff; }
|
||||
.session-activity span, .session-activity small { color: #8b949e; }
|
||||
.history-boundary small { color: #6e7681; }
|
||||
.label { display: block; margin-bottom: 8px; color: #8b949e; font-size: 12px; text-transform: uppercase; }
|
||||
formatted-text.part { display: block; }
|
||||
|
||||
@@ -105,8 +105,8 @@ export class PiSessionService {
|
||||
|
||||
async prompt(sessionId: string, text: string, streamingBehavior?: "steer" | "followUp"): Promise<void> {
|
||||
const session = await this.getOrOpen(sessionId);
|
||||
const behavior = session.isStreaming ? streamingBehavior ?? "followUp" : undefined;
|
||||
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
||||
const behavior = session.isStreaming || session.isCompacting ? streamingBehavior ?? "followUp" : undefined;
|
||||
this.publishActivity(session, session.isCompacting ? "message queued during compaction" : 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);
|
||||
|
||||
Reference in New Issue
Block a user