diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts index f6e5a2b..13a7c8a 100644 --- a/src/client/src/components/PiWebApp.ts +++ b/src/client/src/components/PiWebApp.ts @@ -305,8 +305,8 @@ export class PiWebApp extends LitElement {
${this.renderNavigationPanel(true)}
${state.selectedSession ? html` 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())}> - 0} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => this.sessions.send(text, streamingBehavior)} .onStop=${() => this.sessions.stopActiveWork()}> - { void this.openModelDialog(); }} .onCycleModel=${(direction: "forward" | "backward") => { void this.sessions.cycleModel(direction); }} .onSelectThinking=${() => { void this.openThinkingDialog(); }} .onCycleThinking=${() => { void this.sessions.cycleThinkingLevel(); }}> + 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(); }}> + ${state.commandDialog !== undefined ? html` this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}>` : null} ${state.modelDialog !== undefined ? html` { void this.pickModel(value); }} .onCancel=${() => { this.setState({ modelDialog: undefined }); }}>` : null} ${state.thinkingDialog !== undefined ? html` { void this.pickThinking(value); }} .onCancel=${() => { this.setState({ thinkingDialog: undefined }); }}>` : null} diff --git a/src/client/src/components/PromptEditor.ts b/src/client/src/components/PromptEditor.ts index ea5cdd8..df53d43 100644 --- a/src/client/src/components/PromptEditor.ts +++ b/src/client/src/components/PromptEditor.ts @@ -1,6 +1,6 @@ import { LitElement, html, type PropertyValues } from "lit"; 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 { promptEditorStyles, type CompletionItem } from "./shared"; import "./AutocompleteMenu"; @@ -13,8 +13,13 @@ export class PromptEditor extends LitElement { @property({ type: Boolean }) canSteer = false; @property({ type: Boolean }) isCompacting = false; @property({ type: Boolean }) canStop = false; + @property({ attribute: false }) status?: SessionStatus; @property({ attribute: false }) onSend?: (text: string, streamingBehavior?: "steer" | "followUp") => 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; @state() private draft = ""; @state() private completions: CompletionItem[] = []; @@ -55,6 +60,7 @@ export class PromptEditor extends LitElement { { this.pick(item); }}>
+ ${this.renderCompactStatus()} ${this.canSteer && !this.isCompacting ? html`` : null} @@ -67,6 +73,24 @@ export class PromptEditor extends LitElement { 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` +
+ + + + + + + +
+ `; + } + private resizeTextarea() { const textarea = this.textarea; if (!textarea) return; diff --git a/src/client/src/components/StatusBar.ts b/src/client/src/components/StatusBar.ts index 220a1cd..0c7ea55 100644 --- a/src/client/src/components/StatusBar.ts +++ b/src/client/src/components/StatusBar.ts @@ -8,16 +8,10 @@ import { statusBarStyles } from "./shared"; export class StatusBar extends LitElement { @property({ attribute: false }) status?: SessionStatus; @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() { const status = this.status; if (status === undefined) return html`
No session status yet
`; - const model = status.model?.id ?? "no model"; - const provider = status.model?.provider !== undefined && status.model.provider !== "" ? `${status.model.provider}/` : ""; const context = status.contextUsage; const contextText = context ? context.percent == null @@ -28,15 +22,6 @@ export class StatusBar extends LitElement { return html`
${this.workspace?.label ?? "workspace"} - - - - - - - - - ↑${formatTokenCount(tokens.input)} ↓${formatTokenCount(tokens.output)} ${contextText} diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts index 248231e..605feea 100644 --- a/src/client/src/components/shared.ts +++ b/src/client/src/components/shared.ts @@ -224,9 +224,6 @@ 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-top: 1px solid #30363d; background: #0d1117; white-space: nowrap; overflow: hidden; } 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; } .activity { display: inline-flex; align-items: center; gap: 6px; color: #8b949e; } .activity.active { color: #3fb950; } @@ -288,6 +285,11 @@ export const promptEditorStyles = css` footer.shell-mode { border-top-color: #3fb950; background: #0f1b12; } .editor-wrap { position: relative; min-width: 0; } .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; } .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; } @@ -295,7 +297,17 @@ export const promptEditorStyles = css` button:disabled, textarea:disabled { opacity: .5; cursor: not-allowed; } @media (max-width: 640px) { 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; } } `;