Archived
Refine mobile model controls
This commit is contained in:
@@ -305,8 +305,8 @@ export class PiWebApp extends LitElement {
|
|||||||
<div class="mobile-navigation-panel">${this.renderNavigationPanel(true)}</div>
|
<div class="mobile-navigation-panel">${this.renderNavigationPanel(true)}</div>
|
||||||
${state.selectedSession ? html`
|
${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} .isReceivingPartialStream=${state.isReceivingPartialStream} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .status=${state.status} .activity=${state.activity} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
|
<chat-view .sessionId=${state.selectedSession.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .isReceivingPartialStream=${state.isReceivingPartialStream} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .status=${state.status} .activity=${state.activity} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
|
||||||
<prompt-editor .sessionId=${state.selectedSession.id} .cwd=${state.selectedWorkspace?.path} .disabled=${state.selectedSession.archived === true} .canSteer=${state.status?.isStreaming === true} .isCompacting=${state.status?.isCompacting === true} .canStop=${state.status?.isStreaming === true || state.status?.isBashRunning === true || state.status?.isCompacting === true || (state.status?.pendingMessageCount ?? 0) > 0} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => this.sessions.send(text, streamingBehavior)} .onStop=${() => this.sessions.stopActiveWork()}></prompt-editor>
|
<prompt-editor .sessionId=${state.selectedSession.id} .cwd=${state.selectedWorkspace?.path} .disabled=${state.selectedSession.archived === true} .canSteer=${state.status?.isStreaming === true} .isCompacting=${state.status?.isCompacting === true} .canStop=${state.status?.isStreaming === true || state.status?.isBashRunning === true || state.status?.isCompacting === true || (state.status?.pendingMessageCount ?? 0) > 0} .status=${state.status} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => this.sessions.send(text, streamingBehavior)} .onStop=${() => this.sessions.stopActiveWork()} .onSelectModel=${() => { void this.openModelDialog(); }} .onCycleModel=${(direction: "forward" | "backward") => { void this.sessions.cycleModel(direction); }} .onSelectThinking=${() => { void this.openThinkingDialog(); }} .onCycleThinking=${() => { void this.sessions.cycleThinkingLevel(); }}></prompt-editor>
|
||||||
<status-bar .status=${state.status} .workspace=${state.selectedWorkspace} .onSelectModel=${() => { void this.openModelDialog(); }} .onCycleModel=${(direction: "forward" | "backward") => { void this.sessions.cycleModel(direction); }} .onSelectThinking=${() => { void this.openThinkingDialog(); }} .onCycleThinking=${() => { void this.sessions.cycleThinkingLevel(); }}></status-bar>
|
<status-bar .status=${state.status} .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}
|
${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}
|
||||||
${state.modelDialog !== undefined ? html`<command-picker title=${state.modelDialog.title} .searchable=${true} .options=${state.modelDialog.options} .selectedValue=${state.modelDialog.selectedValue} .onPick=${(value: string) => { void this.pickModel(value); }} .onCancel=${() => { this.setState({ modelDialog: undefined }); }}></command-picker>` : null}
|
${state.modelDialog !== undefined ? html`<command-picker title=${state.modelDialog.title} .searchable=${true} .options=${state.modelDialog.options} .selectedValue=${state.modelDialog.selectedValue} .onPick=${(value: string) => { void this.pickModel(value); }} .onCancel=${() => { this.setState({ modelDialog: undefined }); }}></command-picker>` : null}
|
||||||
${state.thinkingDialog !== undefined ? html`<command-picker title=${state.thinkingDialog.title} .options=${state.thinkingDialog.options} .selectedValue=${state.thinkingDialog.selectedValue} .onPick=${(value: string) => { void this.pickThinking(value); }} .onCancel=${() => { this.setState({ thinkingDialog: undefined }); }}></command-picker>` : null}
|
${state.thinkingDialog !== undefined ? html`<command-picker title=${state.thinkingDialog.title} .options=${state.thinkingDialog.options} .selectedValue=${state.thinkingDialog.selectedValue} .onPick=${(value: string) => { void this.pickThinking(value); }} .onCancel=${() => { this.setState({ thinkingDialog: undefined }); }}></command-picker>` : null}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LitElement, html, type PropertyValues } from "lit";
|
import { LitElement, html, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators.js";
|
import { customElement, property, query, state } from "lit/decorators.js";
|
||||||
import { api, type FileSuggestion, type SlashCommand } from "../api";
|
import { api, type FileSuggestion, type SessionStatus, type SlashCommand } from "../api";
|
||||||
import { inputModeForDraft } from "../inputModes";
|
import { inputModeForDraft } from "../inputModes";
|
||||||
import { promptEditorStyles, type CompletionItem } from "./shared";
|
import { promptEditorStyles, type CompletionItem } from "./shared";
|
||||||
import "./AutocompleteMenu";
|
import "./AutocompleteMenu";
|
||||||
@@ -13,8 +13,13 @@ export class PromptEditor extends LitElement {
|
|||||||
@property({ type: Boolean }) canSteer = false;
|
@property({ type: Boolean }) canSteer = false;
|
||||||
@property({ type: Boolean }) isCompacting = false;
|
@property({ type: Boolean }) isCompacting = false;
|
||||||
@property({ type: Boolean }) canStop = false;
|
@property({ type: Boolean }) canStop = false;
|
||||||
|
@property({ attribute: false }) status?: SessionStatus;
|
||||||
@property({ attribute: false }) onSend?: (text: string, streamingBehavior?: "steer" | "followUp") => void;
|
@property({ attribute: false }) onSend?: (text: string, streamingBehavior?: "steer" | "followUp") => void;
|
||||||
@property({ attribute: false }) onStop?: () => void;
|
@property({ attribute: false }) onStop?: () => void;
|
||||||
|
@property({ attribute: false }) onSelectModel?: () => void;
|
||||||
|
@property({ attribute: false }) onCycleModel?: (direction: "forward" | "backward") => void;
|
||||||
|
@property({ attribute: false }) onSelectThinking?: () => void;
|
||||||
|
@property({ attribute: false }) onCycleThinking?: () => void;
|
||||||
@query("textarea") private textarea?: HTMLTextAreaElement;
|
@query("textarea") private textarea?: HTMLTextAreaElement;
|
||||||
@state() private draft = "";
|
@state() private draft = "";
|
||||||
@state() private completions: CompletionItem[] = [];
|
@state() private completions: CompletionItem[] = [];
|
||||||
@@ -55,6 +60,7 @@ export class PromptEditor extends LitElement {
|
|||||||
<autocomplete-menu .items=${this.completions} .selectedIndex=${this.selectedIndex} .onPick=${(item: CompletionItem) => { this.pick(item); }}></autocomplete-menu>
|
<autocomplete-menu .items=${this.completions} .selectedIndex=${this.selectedIndex} .onPick=${(item: CompletionItem) => { this.pick(item); }}></autocomplete-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
${this.renderCompactStatus()}
|
||||||
<button ?disabled=${this.disabled} title=${queuesInput ? "Queue until the current activity finishes" : "Send message"} @click=${() => { this.send("followUp"); }}>${queuesInput ? "Queue" : "Send"}</button>
|
<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}
|
${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 || !this.canStop} title=${this.canStop ? "Stop current work and clear queued messages" : "Nothing running"} @click=${() => this.onStop?.()}>Stop</button>
|
<button ?disabled=${this.disabled || !this.canStop} title=${this.canStop ? "Stop current work and clear queued messages" : "Nothing running"} @click=${() => this.onStop?.()}>Stop</button>
|
||||||
@@ -67,6 +73,24 @@ export class PromptEditor extends LitElement {
|
|||||||
this.textarea?.focus();
|
this.textarea?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderCompactStatus() {
|
||||||
|
const status = this.status;
|
||||||
|
if (status === undefined) return null;
|
||||||
|
const model = status.model?.id ?? "no model";
|
||||||
|
const provider = status.model?.provider !== undefined && status.model.provider !== "" ? `${status.model.provider}/` : "";
|
||||||
|
return html`
|
||||||
|
<div class="compact-status" aria-label="Session status">
|
||||||
|
<span class="model-controls">
|
||||||
|
<button class="cycle-model" title="Previous model" @click=${() => this.onCycleModel?.("backward")}>‹</button>
|
||||||
|
<button class="select-model" title="Select model" @click=${() => this.onSelectModel?.()}>${provider}${model}</button>
|
||||||
|
<button class="cycle-model" title="Next model" @click=${() => this.onCycleModel?.("forward")}>›</button>
|
||||||
|
</span>
|
||||||
|
<button class="thinking-cycle" title="Cycle thinking level" @click=${() => this.onCycleThinking?.()}>think ${status.thinkingLevel ?? "off"}</button>
|
||||||
|
<button class="thinking-select" title="Select thinking level" @click=${() => this.onSelectThinking?.()}>⌄</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private resizeTextarea() {
|
private resizeTextarea() {
|
||||||
const textarea = this.textarea;
|
const textarea = this.textarea;
|
||||||
if (!textarea) return;
|
if (!textarea) return;
|
||||||
|
|||||||
@@ -8,16 +8,10 @@ import { statusBarStyles } from "./shared";
|
|||||||
export class StatusBar extends LitElement {
|
export class StatusBar extends LitElement {
|
||||||
@property({ attribute: false }) status?: SessionStatus;
|
@property({ attribute: false }) status?: SessionStatus;
|
||||||
@property({ attribute: false }) workspace?: Workspace;
|
@property({ attribute: false }) workspace?: Workspace;
|
||||||
@property({ attribute: false }) onSelectModel?: () => void;
|
|
||||||
@property({ attribute: false }) onCycleModel?: (direction: "forward" | "backward") => void;
|
|
||||||
@property({ attribute: false }) onSelectThinking?: () => void;
|
|
||||||
@property({ attribute: false }) onCycleThinking?: () => void;
|
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const status = this.status;
|
const status = this.status;
|
||||||
if (status === undefined) return html`<div class="bar muted">No session status yet</div>`;
|
if (status === undefined) return html`<div class="bar muted">No session status yet</div>`;
|
||||||
const model = status.model?.id ?? "no model";
|
|
||||||
const provider = status.model?.provider !== undefined && status.model.provider !== "" ? `${status.model.provider}/` : "";
|
|
||||||
const context = status.contextUsage;
|
const context = status.contextUsage;
|
||||||
const contextText = context
|
const contextText = context
|
||||||
? context.percent == null
|
? context.percent == null
|
||||||
@@ -28,15 +22,6 @@ export class StatusBar extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
<span title=${this.workspace?.path ?? ""}>${this.workspace?.label ?? "workspace"}</span>
|
<span title=${this.workspace?.path ?? ""}>${this.workspace?.label ?? "workspace"}</span>
|
||||||
<span class="control-group">
|
|
||||||
<button title="Previous model" @click=${() => this.onCycleModel?.("backward")}>‹</button>
|
|
||||||
<button title="Select model" @click=${() => this.onSelectModel?.()}>${provider}${model}</button>
|
|
||||||
<button title="Next model" @click=${() => this.onCycleModel?.("forward")}>›</button>
|
|
||||||
</span>
|
|
||||||
<span class="control-group">
|
|
||||||
<button title="Cycle thinking level" @click=${() => this.onCycleThinking?.()}>thinking ${status.thinkingLevel ?? "off"}</button>
|
|
||||||
<button title="Select thinking level" @click=${() => this.onSelectThinking?.()}>⌄</button>
|
|
||||||
</span>
|
|
||||||
<span>↑${formatTokenCount(tokens.input)}</span>
|
<span>↑${formatTokenCount(tokens.input)}</span>
|
||||||
<span>↓${formatTokenCount(tokens.output)}</span>
|
<span>↓${formatTokenCount(tokens.output)}</span>
|
||||||
<span>${contextText}</span>
|
<span>${contextText}</span>
|
||||||
|
|||||||
@@ -224,9 +224,6 @@ export const statusBarStyles = css`
|
|||||||
:host { display: block; color: #8b949e; font: 12px system-ui, sans-serif; }
|
: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-top: 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; }
|
span { overflow: hidden; text-overflow: ellipsis; }
|
||||||
button { border: 0; border-radius: 4px; background: transparent; color: inherit; padding: 1px 3px; font: inherit; cursor: pointer; }
|
|
||||||
button:hover, button:focus { background: #21262d; color: #e6edf3; }
|
|
||||||
.control-group { display: inline-flex; align-items: center; gap: 2px; flex: 0 1 auto; min-width: 0; }
|
|
||||||
.bar > span:first-child { flex: 1 1 auto; min-width: 80px; }
|
.bar > span:first-child { flex: 1 1 auto; min-width: 80px; }
|
||||||
.activity { display: inline-flex; align-items: center; gap: 6px; color: #8b949e; }
|
.activity { display: inline-flex; align-items: center; gap: 6px; color: #8b949e; }
|
||||||
.activity.active { color: #3fb950; }
|
.activity.active { color: #3fb950; }
|
||||||
@@ -288,6 +285,11 @@ export const promptEditorStyles = css`
|
|||||||
footer.shell-mode { border-top-color: #3fb950; background: #0f1b12; }
|
footer.shell-mode { border-top-color: #3fb950; background: #0f1b12; }
|
||||||
.editor-wrap { position: relative; min-width: 0; }
|
.editor-wrap { position: relative; min-width: 0; }
|
||||||
.actions { display: flex; gap: 8px; align-items: start; white-space: nowrap; }
|
.actions { display: flex; gap: 8px; align-items: start; white-space: nowrap; }
|
||||||
|
.compact-status { display: flex; min-width: 0; align-items: center; gap: 6px; color: #8b949e; font-size: 12px; flex: 1 1 280px; }
|
||||||
|
.compact-status > button, .model-controls { flex: 0 1 auto; min-width: 0; }
|
||||||
|
.model-controls { display: inline-flex; align-items: center; gap: 2px; min-width: 0; }
|
||||||
|
.compact-status button { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.compact-status .model-controls button:nth-child(2) { max-width: min(36vw, 260px); }
|
||||||
textarea { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow-y: auto; border-radius: 8px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; padding: 8px; font: 16px/1.4 system-ui, sans-serif; }
|
textarea { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow-y: auto; border-radius: 8px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; padding: 8px; font: 16px/1.4 system-ui, sans-serif; }
|
||||||
.shell-mode textarea { border-color: #3fb950; box-shadow: 0 0 0 1px #3fb95055; }
|
.shell-mode textarea { border-color: #3fb950; box-shadow: 0 0 0 1px #3fb95055; }
|
||||||
.mode-hint { position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px); border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 2px 8px; font-size: 12px; pointer-events: none; }
|
.mode-hint { position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px); border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 2px 8px; font-size: 12px; pointer-events: none; }
|
||||||
@@ -295,7 +297,17 @@ export const promptEditorStyles = css`
|
|||||||
button:disabled, textarea:disabled { opacity: .5; cursor: not-allowed; }
|
button:disabled, textarea:disabled { opacity: .5; cursor: not-allowed; }
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
footer { grid-template-columns: minmax(0, 1fr); gap: 8px; padding: 8px; }
|
footer { grid-template-columns: minmax(0, 1fr); gap: 8px; padding: 8px; }
|
||||||
.actions { justify-content: flex-end; flex-wrap: wrap; }
|
.actions { justify-content: flex-end; flex-wrap: wrap; align-items: center; gap: 6px; }
|
||||||
|
.compact-status { flex: 1 1 220px; gap: 4px; }
|
||||||
|
.compact-status .model-controls button:nth-child(2) { max-width: min(50vw, 230px); }
|
||||||
|
button { padding: 6px 8px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.compact-status { flex-basis: 170px; font-size: 11px; }
|
||||||
|
.cycle-model, .thinking-select { display: none; }
|
||||||
|
.compact-status .model-controls button:nth-child(2) { max-width: 42vw; }
|
||||||
|
.thinking-cycle { max-width: 70px; }
|
||||||
|
button { padding: 5px 7px; }
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user