Archived
Add model and thinking controls
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
export { api, filesApi, gitApi, projectsApi, sessionsApi, terminalsApi, workspacesApi } from "./api/clients";
|
export { api, filesApi, gitApi, projectsApi, sessionsApi, terminalsApi, workspacesApi } from "./api/clients";
|
||||||
export { globalSessionEvents, sessionEvents, terminalSocket } from "./api/sockets";
|
export { globalSessionEvents, sessionEvents, terminalSocket } from "./api/sockets";
|
||||||
export type { CommandOption, CommandResult, FileContentResponse, FileSuggestion, FileTreeEntry, FileTreeResponse, GitDiffResponse, GitFileState, GitStatusFile, GitStatusResponse, MessagePage, Project, QueuedSessionMessage, SessionActivity, SessionInfo, SessionStatus, SlashCommand, SessionUiEvent, TerminalInfo, Workspace } from "../../shared/apiTypes";
|
export type { CommandOption, CommandResult, FileContentResponse, FileSuggestion, FileTreeEntry, FileTreeResponse, GitDiffResponse, GitFileState, GitStatusFile, GitStatusResponse, MessagePage, ModelSelectionResponse, Project, QueuedSessionMessage, SessionActivity, SessionInfo, SessionModel, SessionStatus, SlashCommand, SessionUiEvent, TerminalInfo, ThinkingLevel, ThinkingLevelsResponse, Workspace } from "../../shared/apiTypes";
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
parseGitDiffResponse,
|
parseGitDiffResponse,
|
||||||
parseGitStatusResponse,
|
parseGitStatusResponse,
|
||||||
parseMessagePage,
|
parseMessagePage,
|
||||||
|
parseModelSelectionResponse,
|
||||||
parseProject,
|
parseProject,
|
||||||
parseRestored,
|
parseRestored,
|
||||||
parseSessionInfo,
|
parseSessionInfo,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
parseSlashCommand,
|
parseSlashCommand,
|
||||||
parseStopped,
|
parseStopped,
|
||||||
parseTerminalInfo,
|
parseTerminalInfo,
|
||||||
|
parseThinkingLevelsResponse,
|
||||||
parseWorkspace,
|
parseWorkspace,
|
||||||
} from "./parsers";
|
} from "./parsers";
|
||||||
import { gitDiffUrl, messageUrl } from "./urls";
|
import { gitDiffUrl, messageUrl } from "./urls";
|
||||||
@@ -43,6 +45,12 @@ export const sessionsApi = {
|
|||||||
startSession: (cwd: string) => request("/api/sessions", parseSessionInfo, { method: "POST", body: JSON.stringify({ cwd }) }),
|
startSession: (cwd: string) => request("/api/sessions", parseSessionInfo, { method: "POST", body: JSON.stringify({ cwd }) }),
|
||||||
messages: (sessionId: string, options?: { limit?: number; before?: number }) => request(messageUrl(sessionId, options), parseMessagePage),
|
messages: (sessionId: string, options?: { limit?: number; before?: number }) => request(messageUrl(sessionId, options), parseMessagePage),
|
||||||
status: (sessionId: string) => request(`/api/sessions/${sessionId}/status`, parseSessionStatus),
|
status: (sessionId: string) => request(`/api/sessions/${sessionId}/status`, parseSessionStatus),
|
||||||
|
models: (sessionId: string) => request(`/api/sessions/${sessionId}/models`, parseModelSelectionResponse),
|
||||||
|
setModel: (sessionId: string, provider: string, modelId: string) => request(`/api/sessions/${sessionId}/model`, parseSessionStatus, { method: "POST", body: JSON.stringify({ provider, modelId }) }),
|
||||||
|
cycleModel: (sessionId: string, direction: "forward" | "backward") => request(`/api/sessions/${sessionId}/model/cycle`, parseSessionStatus, { method: "POST", body: JSON.stringify({ direction }) }),
|
||||||
|
thinkingLevels: (sessionId: string) => request(`/api/sessions/${sessionId}/thinking-levels`, parseThinkingLevelsResponse),
|
||||||
|
setThinkingLevel: (sessionId: string, level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh") => request(`/api/sessions/${sessionId}/thinking-level`, parseSessionStatus, { method: "POST", body: JSON.stringify({ level }) }),
|
||||||
|
cycleThinkingLevel: (sessionId: string) => request(`/api/sessions/${sessionId}/thinking-level/cycle`, parseSessionStatus, { method: "POST" }),
|
||||||
commands: (sessionId: string) => request(`/api/sessions/${sessionId}/commands`, arrayOf(parseSlashCommand)),
|
commands: (sessionId: string) => request(`/api/sessions/${sessionId}/commands`, arrayOf(parseSlashCommand)),
|
||||||
prompt: (sessionId: string, text: string, streamingBehavior?: "steer" | "followUp") => request(`/api/sessions/${sessionId}/prompt`, parseAccepted, { method: "POST", body: JSON.stringify(streamingBehavior === undefined ? { text } : { text, streamingBehavior }) }),
|
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 }) }),
|
shell: (sessionId: string, text: string) => request(`/api/sessions/${sessionId}/shell`, parseAccepted, { method: "POST", body: JSON.stringify({ text }) }),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CommandOption, CommandResult, FileContentResponse, FileSuggestion, FileTreeEntry, FileTreeResponse, GitDiffResponse, GitFileState, GitStatusFile, GitStatusResponse, MessagePage, Project, QueuedSessionMessage, SessionInfo, SessionStatus, SlashCommand, TerminalInfo, Workspace } from "../../../shared/apiTypes";
|
import type { CommandOption, CommandResult, FileContentResponse, FileSuggestion, FileTreeEntry, FileTreeResponse, GitDiffResponse, GitFileState, GitStatusFile, GitStatusResponse, MessagePage, ModelSelectionResponse, Project, QueuedSessionMessage, SessionInfo, SessionModel, SessionStatus, SlashCommand, TerminalInfo, ThinkingLevel, ThinkingLevelsResponse, Workspace } from "../../../shared/apiTypes";
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null;
|
return typeof value === "object" && value !== null;
|
||||||
@@ -127,10 +127,29 @@ function parseTokens(value: unknown): SessionStatus["tokens"] {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSessionModel(value: unknown): SessionModel {
|
||||||
|
const record = requireRecord(value);
|
||||||
|
return { ...optionalField("provider", optionalString(record, "provider")), ...optionalField("id", optionalString(record, "id")), ...optionalField("name", optionalString(record, "name")), ...optionalField("contextWindow", optionalNumber(record, "contextWindow")), ...optionalField("reasoning", record["reasoning"]) };
|
||||||
|
}
|
||||||
|
|
||||||
function optionalModel(value: unknown): Pick<SessionStatus, "model"> | object {
|
function optionalModel(value: unknown): Pick<SessionStatus, "model"> | object {
|
||||||
if (value === undefined) return {};
|
if (value === undefined) return {};
|
||||||
|
return { model: parseSessionModel(value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseModelSelectionResponse(value: unknown): ModelSelectionResponse {
|
||||||
const record = requireRecord(value);
|
const record = requireRecord(value);
|
||||||
return { model: { ...optionalField("provider", optionalString(record, "provider")), ...optionalField("id", optionalString(record, "id")), ...optionalField("name", optionalString(record, "name")), ...optionalField("contextWindow", optionalNumber(record, "contextWindow")), ...optionalField("reasoning", record["reasoning"]) } };
|
return { models: arrayOf(parseSessionModel)(record["models"]) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseThinkingLevel(value: unknown): ThinkingLevel {
|
||||||
|
if (value !== "off" && value !== "minimal" && value !== "low" && value !== "medium" && value !== "high" && value !== "xhigh") throw new Error("Invalid thinking level");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseThinkingLevelsResponse(value: unknown): ThinkingLevelsResponse {
|
||||||
|
const record = requireRecord(value);
|
||||||
|
return { levels: arrayOf(parseThinkingLevel)(record["levels"]) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function optionalContextUsage(value: unknown): Pick<SessionStatus, "contextUsage"> | object {
|
function optionalContextUsage(value: unknown): Pick<SessionStatus, "contextUsage"> | object {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CommandResult, FileContentResponse, FileTreeEntry, GitDiffResponse, GitStatusResponse, Project, SessionActivity, SessionInfo, SessionStatus, Workspace } from "./api";
|
import type { CommandOption, CommandResult, FileContentResponse, FileTreeEntry, GitDiffResponse, GitStatusResponse, Project, SessionActivity, SessionInfo, SessionStatus, Workspace } from "./api";
|
||||||
import type { ChatLine } from "./components/shared";
|
import type { ChatLine } from "./components/shared";
|
||||||
import type { QualifiedContributionId } from "./plugins/types";
|
import type { QualifiedContributionId } from "./plugins/types";
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@ export interface AppState {
|
|||||||
sessionStatuses: Record<string, SessionStatus>;
|
sessionStatuses: Record<string, SessionStatus>;
|
||||||
sessionActivities: Record<string, SessionActivity>;
|
sessionActivities: Record<string, SessionActivity>;
|
||||||
commandDialog: Extract<CommandResult, { type: "select" }> | undefined;
|
commandDialog: Extract<CommandResult, { type: "select" }> | undefined;
|
||||||
|
modelDialog: { title: string; options: CommandOption[]; selectedValue?: string } | undefined;
|
||||||
|
thinkingDialog: { title: string; options: CommandOption[]; selectedValue?: string } | undefined;
|
||||||
actionPaletteOpen: boolean;
|
actionPaletteOpen: boolean;
|
||||||
projectDialogOpen: boolean;
|
projectDialogOpen: boolean;
|
||||||
workspaceTool: QualifiedContributionId;
|
workspaceTool: QualifiedContributionId;
|
||||||
@@ -54,6 +56,8 @@ export function initialAppState(): AppState {
|
|||||||
sessionStatuses: {},
|
sessionStatuses: {},
|
||||||
sessionActivities: {},
|
sessionActivities: {},
|
||||||
commandDialog: undefined,
|
commandDialog: undefined,
|
||||||
|
modelDialog: undefined,
|
||||||
|
thinkingDialog: undefined,
|
||||||
actionPaletteOpen: false,
|
actionPaletteOpen: false,
|
||||||
projectDialogOpen: false,
|
projectDialogOpen: false,
|
||||||
workspaceTool: "core:workspace.files",
|
workspaceTool: "core:workspace.files",
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ import { commandPickerStyles } from "./shared";
|
|||||||
@customElement("command-picker")
|
@customElement("command-picker")
|
||||||
export class CommandPicker extends LitElement {
|
export class CommandPicker extends LitElement {
|
||||||
@property() override title = "Select";
|
@property() override title = "Select";
|
||||||
|
@property({ type: Boolean }) searchable = false;
|
||||||
@property({ attribute: false }) options: CommandOption[] = [];
|
@property({ attribute: false }) options: CommandOption[] = [];
|
||||||
|
@property({ attribute: false }) selectedValue?: string;
|
||||||
@property({ attribute: false }) onPick?: (value: string) => void;
|
@property({ attribute: false }) onPick?: (value: string) => void;
|
||||||
@property({ attribute: false }) onCancel?: () => void;
|
@property({ attribute: false }) onCancel?: () => void;
|
||||||
@state() private selectedIndex = 0;
|
@state() private selectedIndex = 0;
|
||||||
|
@state() private query = "";
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
|
const options = this.filteredOptions();
|
||||||
return html`
|
return html`
|
||||||
<div class="backdrop" @mousedown=${() => this.onCancel?.()}>
|
<div class="backdrop" @mousedown=${() => this.onCancel?.()}>
|
||||||
<section @mousedown=${(event: MouseEvent) => { event.stopPropagation(); }}>
|
<section @mousedown=${(event: MouseEvent) => { event.stopPropagation(); }}>
|
||||||
@@ -20,13 +24,15 @@ export class CommandPicker extends LitElement {
|
|||||||
<strong>${this.title}</strong>
|
<strong>${this.title}</strong>
|
||||||
<button @click=${() => this.onCancel?.()}>×</button>
|
<button @click=${() => this.onCancel?.()}>×</button>
|
||||||
</header>
|
</header>
|
||||||
|
${this.searchable ? html`<input placeholder="Search" .value=${this.query} @input=${(event: Event) => { this.handleSearchInput(event); }} @keydown=${(event: KeyboardEvent) => { this.handleKeyDown(event); }}>` : null}
|
||||||
<div class="options" @keydown=${(event: KeyboardEvent) => { this.handleKeyDown(event); }} tabindex="0">
|
<div class="options" @keydown=${(event: KeyboardEvent) => { this.handleKeyDown(event); }} tabindex="0">
|
||||||
${this.options.map((option, index) => html`
|
${options.map((option, index) => html`
|
||||||
<button class=${index === this.selectedIndex ? "selected" : ""} ${scrollWhenSelected(index === this.selectedIndex, option.value)} @click=${() => this.onPick?.(option.value)}>
|
<button class=${index === this.selectedIndex ? "selected" : ""} ${scrollWhenSelected(index === this.selectedIndex, option.value)} @click=${() => this.onPick?.(option.value)}>
|
||||||
<span>${option.label}</span>
|
<span>${option.label}</span>
|
||||||
${option.description !== undefined && option.description !== "" ? html`<small>${option.description}</small>` : null}
|
${option.description !== undefined && option.description !== "" ? html`<small>${option.description}</small>` : null}
|
||||||
</button>
|
</button>
|
||||||
`)}
|
`)}
|
||||||
|
${options.length === 0 ? html`<div class="empty">No matching options</div>` : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,22 +40,43 @@ export class CommandPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override firstUpdated() {
|
override firstUpdated() {
|
||||||
this.renderRoot.querySelector<HTMLElement>(".options")?.focus();
|
this.selectInitialValue();
|
||||||
|
this.renderRoot.querySelector<HTMLElement>(this.searchable ? "input" : ".options")?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectInitialValue(): void {
|
||||||
|
if (this.selectedValue === undefined) return;
|
||||||
|
const index = this.filteredOptions().findIndex((option) => option.value === this.selectedValue);
|
||||||
|
if (index >= 0) this.selectedIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSearchInput(event: Event): void {
|
||||||
|
if (event.target instanceof HTMLInputElement) {
|
||||||
|
this.query = event.target.value;
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private filteredOptions(): CommandOption[] {
|
||||||
|
const query = this.query.trim().toLowerCase();
|
||||||
|
if (query === "") return this.options;
|
||||||
|
return this.options.filter((option) => `${option.label} ${option.description ?? ""} ${option.value}`.toLowerCase().includes(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleKeyDown(event: KeyboardEvent) {
|
private handleKeyDown(event: KeyboardEvent) {
|
||||||
|
const options = this.filteredOptions();
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.onCancel?.();
|
this.onCancel?.();
|
||||||
} else if (event.key === "ArrowDown") {
|
} else if (event.key === "ArrowDown") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.options.length > 0) this.selectedIndex = (this.selectedIndex + 1) % this.options.length;
|
if (options.length > 0) this.selectedIndex = (this.selectedIndex + 1) % options.length;
|
||||||
} else if (event.key === "ArrowUp") {
|
} else if (event.key === "ArrowUp") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.options.length > 0) this.selectedIndex = (this.selectedIndex - 1 + this.options.length) % this.options.length;
|
if (options.length > 0) this.selectedIndex = (this.selectedIndex - 1 + options.length) % options.length;
|
||||||
} else if (event.key === "Enter") {
|
} else if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const option = this.options[this.selectedIndex];
|
const option = options[this.selectedIndex];
|
||||||
if (option) this.onPick?.(option.value);
|
if (option) this.onPick?.(option.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, query, state } from "lit/decorators.js";
|
import { customElement, query, state } from "lit/decorators.js";
|
||||||
import type { Project, SessionInfo, Workspace } from "../api";
|
import type { Project, SessionInfo, ThinkingLevel, Workspace } from "../api";
|
||||||
import type { AppAction } from "../actions";
|
import type { AppAction } from "../actions";
|
||||||
import { initialAppState, type AppState } from "../appState";
|
import { initialAppState, type AppState } from "../appState";
|
||||||
import { FileExplorerController } from "../controllers/fileExplorerController";
|
import { FileExplorerController } from "../controllers/fileExplorerController";
|
||||||
@@ -246,6 +246,48 @@ export class PiWebApp extends LitElement {
|
|||||||
if (action !== undefined) void action.run();
|
if (action !== undefined) void action.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async openModelDialog() {
|
||||||
|
const models = await this.sessions.listModels();
|
||||||
|
const currentProvider = this.state.status?.model?.provider;
|
||||||
|
const currentId = this.state.status?.model?.id;
|
||||||
|
this.setState({
|
||||||
|
modelDialog: {
|
||||||
|
title: "Select Model",
|
||||||
|
...(currentProvider !== undefined && currentId !== undefined ? { selectedValue: `${currentProvider}/${currentId}` } : {}),
|
||||||
|
options: models.map((model) => {
|
||||||
|
const provider = model.provider ?? "";
|
||||||
|
const id = model.id ?? "";
|
||||||
|
const isCurrent = provider === currentProvider && id === currentId;
|
||||||
|
return { value: `${provider}/${id}`, label: `${id}${isCurrent ? " ✓ current" : ""}`, description: provider };
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pickModel(value: string) {
|
||||||
|
this.setState({ modelDialog: undefined });
|
||||||
|
const slash = value.indexOf("/");
|
||||||
|
if (slash <= 0) return;
|
||||||
|
await this.sessions.setModel(value.slice(0, slash), value.slice(slash + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openThinkingDialog() {
|
||||||
|
const levels = await this.sessions.listThinkingLevels();
|
||||||
|
const current = this.state.status?.thinkingLevel ?? "off";
|
||||||
|
this.setState({
|
||||||
|
thinkingDialog: {
|
||||||
|
title: "Select Thinking Level",
|
||||||
|
selectedValue: current,
|
||||||
|
options: levels.map((level) => ({ value: level, label: `${level}${level === current ? " ✓ current" : ""}`, description: thinkingDescription(level) })),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pickThinking(value: string) {
|
||||||
|
this.setState({ thinkingDialog: undefined });
|
||||||
|
if (isThinkingLevel(value)) await this.sessions.setThinkingLevel(value);
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
return html`
|
return html`
|
||||||
@@ -264,8 +306,10 @@ export class PiWebApp extends LitElement {
|
|||||||
${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} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => this.sessions.send(text, streamingBehavior)} .onStop=${() => this.sessions.stopActiveWork()}></prompt-editor>
|
||||||
<status-bar .status=${state.status} .workspace=${state.selectedWorkspace}></status-bar>
|
<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>
|
||||||
${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.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}
|
||||||
` : html`<div class="empty">Select or start a session.</div>`}
|
` : html`<div class="empty">Select or start a session.</div>`}
|
||||||
<div class="mobile-panel">${this.renderWorkspacePanel(true)}</div>
|
<div class="mobile-panel">${this.renderWorkspacePanel(true)}</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -292,3 +336,18 @@ function isActive(status: AppState["status"]): boolean {
|
|||||||
function nextFrame(): Promise<void> {
|
function nextFrame(): Promise<void> {
|
||||||
return new Promise((resolve) => requestAnimationFrame(() => { resolve(); }));
|
return new Promise((resolve) => requestAnimationFrame(() => { resolve(); }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isThinkingLevel(value: string): value is ThinkingLevel {
|
||||||
|
return value === "off" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
|
||||||
|
}
|
||||||
|
|
||||||
|
function thinkingDescription(level: ThinkingLevel): string {
|
||||||
|
switch (level) {
|
||||||
|
case "off": return "No reasoning";
|
||||||
|
case "minimal": return "Very brief reasoning (~1k tokens)";
|
||||||
|
case "low": return "Light reasoning (~2k tokens)";
|
||||||
|
case "medium": return "Moderate reasoning (~8k tokens)";
|
||||||
|
case "high": return "Deep reasoning (~16k tokens)";
|
||||||
|
case "xhigh": return "Maximum reasoning (~32k tokens)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +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;
|
||||||
@@ -24,8 +28,15 @@ 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>${provider}${model}</span>
|
<span class="control-group">
|
||||||
<span>thinking ${status.thinkingLevel ?? "off"}</span>
|
<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,6 +224,9 @@ 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; }
|
||||||
@@ -251,9 +254,12 @@ export const commandPickerStyles = css`
|
|||||||
.options { min-height: 0; overflow: auto; outline: none; }
|
.options { min-height: 0; overflow: auto; outline: none; }
|
||||||
button { border: 0; background: transparent; color: #e6edf3; cursor: pointer; }
|
button { border: 0; background: transparent; color: #e6edf3; cursor: pointer; }
|
||||||
header button { font-size: 20px; color: #8b949e; }
|
header button { font-size: 20px; color: #8b949e; }
|
||||||
|
input { margin: 10px 12px; border: 1px solid #30363d; border-radius: 8px; background: #0d1117; color: #e6edf3; font: 14px system-ui, sans-serif; padding: 8px 10px; outline: none; }
|
||||||
|
input:focus { border-color: #58a6ff; }
|
||||||
.options button { display: block; width: 100%; padding: 10px 12px; border-bottom: 1px solid #21262d; text-align: left; }
|
.options button { display: block; width: 100%; padding: 10px 12px; border-bottom: 1px solid #21262d; text-align: left; }
|
||||||
.options button.selected, .options button:hover { background: #0d2847; }
|
.options button.selected, .options button:hover { background: #0d2847; }
|
||||||
small { display: block; margin-top: 4px; color: #8b949e; }
|
small { display: block; margin-top: 4px; color: #8b949e; }
|
||||||
|
.empty { padding: 24px; color: #8b949e; text-align: center; }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const actionPaletteStyles = css`
|
export const actionPaletteStyles = css`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { api, type CommandResult, type SessionActivity, type SessionInfo, type SessionStatus } from "../api";
|
import { api, type CommandResult, type SessionActivity, type SessionInfo, type SessionStatus, type ThinkingLevel } from "../api";
|
||||||
|
|
||||||
const MESSAGE_PAGE_SIZE = 100;
|
const MESSAGE_PAGE_SIZE = 100;
|
||||||
import { normalizeMessages, textMessage } from "../chatMessages";
|
import { normalizeMessages, textMessage } from "../chatMessages";
|
||||||
@@ -205,6 +205,68 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listModels() {
|
||||||
|
const session = this.getState().selectedSession;
|
||||||
|
if (!session || session.archived === true) return [];
|
||||||
|
try {
|
||||||
|
return (await api.models(session.id)).models;
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error: String(error) });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setModel(provider: string, modelId: string) {
|
||||||
|
const session = this.getState().selectedSession;
|
||||||
|
if (!session || session.archived === true) return;
|
||||||
|
try {
|
||||||
|
this.applyStatus(await api.setModel(session.id, provider, modelId));
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cycleModel(direction: "forward" | "backward") {
|
||||||
|
const session = this.getState().selectedSession;
|
||||||
|
if (!session || session.archived === true) return;
|
||||||
|
try {
|
||||||
|
this.applyStatus(await api.cycleModel(session.id, direction));
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listThinkingLevels() {
|
||||||
|
const session = this.getState().selectedSession;
|
||||||
|
if (!session || session.archived === true) return [];
|
||||||
|
try {
|
||||||
|
return (await api.thinkingLevels(session.id)).levels;
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error: String(error) });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setThinkingLevel(level: ThinkingLevel) {
|
||||||
|
const session = this.getState().selectedSession;
|
||||||
|
if (!session || session.archived === true) return;
|
||||||
|
try {
|
||||||
|
this.applyStatus(await api.setThinkingLevel(session.id, level));
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cycleThinkingLevel() {
|
||||||
|
const session = this.getState().selectedSession;
|
||||||
|
if (!session || session.archived === true) return;
|
||||||
|
try {
|
||||||
|
this.applyStatus(await api.cycleThinkingLevel(session.id));
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async stopActiveWork() {
|
async stopActiveWork() {
|
||||||
const session = this.getState().selectedSession;
|
const session = this.getState().selectedSession;
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|||||||
@@ -18,21 +18,6 @@ export function registerSessionProxyRoutes(app: FastifyInstance, daemon = new Se
|
|||||||
|
|
||||||
app.get("/api/sessiond/health", (_request, reply) => proxy({ method: "GET", url: "/api/health" }, reply));
|
app.get("/api/sessiond/health", (_request, reply) => proxy({ method: "GET", url: "/api/health" }, reply));
|
||||||
|
|
||||||
app.get<{ Querystring: { cwd?: string } }>("/api/sessions", (request, reply) => proxy(request, reply));
|
|
||||||
app.post<{ Body: { cwd: string } }>("/api/sessions", (request, reply) => proxy(request, reply));
|
|
||||||
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; 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));
|
|
||||||
app.post<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/abort", (request, reply) => proxy(request, reply));
|
|
||||||
app.post<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/stop", (request, reply) => proxy(request, reply));
|
|
||||||
app.post<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/archive", (request, reply) => proxy(request, reply));
|
|
||||||
app.post<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/restore", (request, reply) => proxy(request, reply));
|
|
||||||
app.post<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/detach-parent", (request, reply) => proxy(request, reply));
|
|
||||||
|
|
||||||
app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/events", { websocket: true }, (socket, request) => {
|
app.get<{ Params: { sessionId: string } }>("/api/sessions/:sessionId/events", { websocket: true }, (socket, request) => {
|
||||||
bridgeSockets(socket, daemon.connectWebSocket(`/sessions/${request.params.sessionId}/events`));
|
bridgeSockets(socket, daemon.connectWebSocket(`/sessions/${request.params.sessionId}/events`));
|
||||||
});
|
});
|
||||||
@@ -40,6 +25,9 @@ export function registerSessionProxyRoutes(app: FastifyInstance, daemon = new Se
|
|||||||
app.get("/api/sessions/events", { websocket: true }, (socket) => {
|
app.get("/api/sessions/events", { websocket: true }, (socket) => {
|
||||||
bridgeSockets(socket, daemon.connectWebSocket("/sessions/events"));
|
bridgeSockets(socket, daemon.connectWebSocket("/sessions/events"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.all("/api/sessions", (request, reply) => proxy(request, reply));
|
||||||
|
app.all("/api/sessions/*", (request, reply) => proxy(request, reply));
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripApiPrefix(url: string): string {
|
function stripApiPrefix(url: string): string {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
type AgentSession,
|
type AgentSession,
|
||||||
type CreateAgentSessionRuntimeFactory,
|
type CreateAgentSessionRuntimeFactory,
|
||||||
} from "@earendil-works/pi-coding-agent";
|
} from "@earendil-works/pi-coding-agent";
|
||||||
import type { ClientCommand, ClientCommandResult, ClientMessagePage, ClientSession, ClientSessionStatus, SessionUiEvent } from "../types.js";
|
import type { ClientCommand, ClientCommandResult, ClientMessagePage, ClientSession, ClientSessionModel, ClientSessionStatus, ClientThinkingLevel, SessionUiEvent } from "../types.js";
|
||||||
import type { SessionEventHub } from "../realtime/sessionEventHub.js";
|
import type { SessionEventHub } from "../realtime/sessionEventHub.js";
|
||||||
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
||||||
import { SessionCommandService } from "./sessionCommandService.js";
|
import { SessionCommandService } from "./sessionCommandService.js";
|
||||||
@@ -151,6 +151,65 @@ export class PiSessionService {
|
|||||||
return this.statusFromSession(await this.getOrOpen(sessionId));
|
return this.statusFromSession(await this.getOrOpen(sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async availableModels(sessionId: string): Promise<ClientSessionModel[]> {
|
||||||
|
const session = await this.getOrOpen(sessionId);
|
||||||
|
session.modelRegistry.refresh();
|
||||||
|
const models = session.scopedModels.length > 0
|
||||||
|
? session.scopedModels.map((scoped) => scoped.model)
|
||||||
|
: session.modelRegistry.getAvailable();
|
||||||
|
return models.map(modelToClientModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setModel(sessionId: string, provider: string, modelId: string): Promise<ClientSessionStatus> {
|
||||||
|
await this.assertWritable(sessionId);
|
||||||
|
const session = await this.getOrOpen(sessionId);
|
||||||
|
session.modelRegistry.refresh();
|
||||||
|
const candidates = session.scopedModels.length > 0
|
||||||
|
? session.scopedModels.map((scoped) => scoped.model)
|
||||||
|
: session.modelRegistry.getAvailable();
|
||||||
|
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
||||||
|
?? session.modelRegistry.find(provider, modelId);
|
||||||
|
if (model === undefined) throw new Error(`Model not found: ${provider}/${modelId}`);
|
||||||
|
await session.setModel(model);
|
||||||
|
this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
|
||||||
|
this.publishStatus(session);
|
||||||
|
return this.statusFromSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cycleModel(sessionId: string, direction: "forward" | "backward"): Promise<ClientSessionStatus> {
|
||||||
|
await this.assertWritable(sessionId);
|
||||||
|
const session = await this.getOrOpen(sessionId);
|
||||||
|
const result = await session.cycleModel(direction);
|
||||||
|
if (result === undefined) throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
|
||||||
|
this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
|
||||||
|
this.publishStatus(session);
|
||||||
|
return this.statusFromSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
async availableThinkingLevels(sessionId: string): Promise<ClientThinkingLevel[]> {
|
||||||
|
const session = await this.getOrOpen(sessionId);
|
||||||
|
return session.getAvailableThinkingLevels();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setThinkingLevel(sessionId: string, level: ClientThinkingLevel): Promise<ClientSessionStatus> {
|
||||||
|
await this.assertWritable(sessionId);
|
||||||
|
const session = await this.getOrOpen(sessionId);
|
||||||
|
session.setThinkingLevel(level);
|
||||||
|
this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
|
||||||
|
this.publishStatus(session);
|
||||||
|
return this.statusFromSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cycleThinkingLevel(sessionId: string): Promise<ClientSessionStatus> {
|
||||||
|
await this.assertWritable(sessionId);
|
||||||
|
const session = await this.getOrOpen(sessionId);
|
||||||
|
const level = session.cycleThinkingLevel();
|
||||||
|
if (level === undefined) throw new Error("Current model does not support thinking");
|
||||||
|
this.publishActivity(session, `thinking: ${level}`, "idle");
|
||||||
|
this.publishStatus(session);
|
||||||
|
return this.statusFromSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
async commands(sessionId: string): Promise<ClientCommand[]> {
|
async commands(sessionId: string): Promise<ClientCommand[]> {
|
||||||
const session = await this.getOrOpen(sessionId);
|
const session = await this.getOrOpen(sessionId);
|
||||||
const commands: ClientCommand[] = [...BUILTIN_COMMANDS];
|
const commands: ClientCommand[] = [...BUILTIN_COMMANDS];
|
||||||
@@ -402,19 +461,7 @@ export class PiSessionService {
|
|||||||
|
|
||||||
private statusFromSession(session: AgentSession): ClientSessionStatus {
|
private statusFromSession(session: AgentSession): ClientSessionStatus {
|
||||||
const stats = session.getSessionStats();
|
const stats = session.getSessionStats();
|
||||||
const model = session.model === undefined
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
||||||
? undefined
|
|
||||||
: (() => {
|
|
||||||
const name = getString(session.model, "name");
|
|
||||||
const reasoning = getProperty(session.model, "reasoning");
|
|
||||||
return {
|
|
||||||
provider: session.model.provider,
|
|
||||||
id: session.model.id,
|
|
||||||
...(name === undefined ? {} : { name }),
|
|
||||||
contextWindow: session.model.contextWindow,
|
|
||||||
...(reasoning === undefined ? {} : { reasoning }),
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
const contextUsage = session.getContextUsage();
|
const contextUsage = session.getContextUsage();
|
||||||
return {
|
return {
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
@@ -432,6 +479,19 @@ export class PiSessionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modelToClientModel(model: AgentSession["model"]): ClientSessionModel {
|
||||||
|
if (model === undefined) return {};
|
||||||
|
const name = getString(model, "name");
|
||||||
|
const reasoning = getProperty(model, "reasoning");
|
||||||
|
return {
|
||||||
|
provider: model.provider,
|
||||||
|
id: model.id,
|
||||||
|
...(name === undefined ? {} : { name }),
|
||||||
|
contextWindow: model.contextWindow,
|
||||||
|
...(reasoning === undefined ? {} : { reasoning }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function clearParentSession(sessionFile: string): Promise<void> {
|
async function clearParentSession(sessionFile: string): Promise<void> {
|
||||||
const content = await readFile(sessionFile, "utf8");
|
const content = await readFile(sessionFile, "utf8");
|
||||||
const newlineIndex = content.indexOf("\n");
|
const newlineIndex = content.indexOf("\n");
|
||||||
@@ -445,8 +505,7 @@ async function clearParentSession(sessionFile: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearSessionQueue(session: AgentSession): void {
|
function clearSessionQueue(session: AgentSession): void {
|
||||||
const candidate = session as AgentSession & { clearQueue?: () => unknown };
|
session.clearQueue();
|
||||||
candidate.clearQueue?.();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasQueuedMessageText(session: AgentSession, text: string): boolean {
|
function hasQueuedMessageText(session: AgentSession, text: string): boolean {
|
||||||
@@ -454,10 +513,9 @@ function hasQueuedMessageText(session: AgentSession, text: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function queuedMessagesFromSession(session: AgentSession): { kind: "steer" | "followUp"; text: string }[] {
|
function queuedMessagesFromSession(session: AgentSession): { kind: "steer" | "followUp"; text: string }[] {
|
||||||
const candidate = session as AgentSession & { getSteeringMessages?: () => string[]; getFollowUpMessages?: () => string[] };
|
|
||||||
return [
|
return [
|
||||||
...(candidate.getSteeringMessages?.() ?? []).map((text) => ({ kind: "steer" as const, text })),
|
...session.getSteeringMessages().map((text) => ({ kind: "steer" as const, text })),
|
||||||
...(candidate.getFollowUpMessages?.() ?? []).map((text) => ({ kind: "followUp" as const, text })),
|
...session.getFollowUpMessages().map((text) => ({ kind: "followUp" as const, text })),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,54 @@ export function registerSessionRoutes(app: FastifyInstance, sessions: PiSessionS
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get<{ Params: { sessionId: string } }>(`${prefix}/sessions/:sessionId/models`, async (request, reply) => {
|
||||||
|
try {
|
||||||
|
return { models: await sessions.availableModels(request.params.sessionId) };
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post<{ Params: { sessionId: string }; Body: { provider: string; modelId: string } }>(`${prefix}/sessions/:sessionId/model`, async (request, reply) => {
|
||||||
|
try {
|
||||||
|
return await sessions.setModel(request.params.sessionId, request.body.provider, request.body.modelId);
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post<{ Params: { sessionId: string }; Body: { direction?: "forward" | "backward" } }>(`${prefix}/sessions/:sessionId/model/cycle`, async (request, reply) => {
|
||||||
|
try {
|
||||||
|
return await sessions.cycleModel(request.params.sessionId, request.body.direction ?? "forward");
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get<{ Params: { sessionId: string } }>(`${prefix}/sessions/:sessionId/thinking-levels`, async (request, reply) => {
|
||||||
|
try {
|
||||||
|
return { levels: await sessions.availableThinkingLevels(request.params.sessionId) };
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post<{ Params: { sessionId: string }; Body: { level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" } }>(`${prefix}/sessions/:sessionId/thinking-level`, async (request, reply) => {
|
||||||
|
try {
|
||||||
|
return await sessions.setThinkingLevel(request.params.sessionId, request.body.level);
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post<{ Params: { sessionId: string } }>(`${prefix}/sessions/:sessionId/thinking-level/cycle`, async (request, reply) => {
|
||||||
|
try {
|
||||||
|
return await sessions.cycleThinkingLevel(request.params.sessionId);
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get<{ Params: { sessionId: string } }>(`${prefix}/sessions/:sessionId/commands`, async (request, reply) => {
|
app.get<{ Params: { sessionId: string } }>(`${prefix}/sessions/:sessionId/commands`, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
return await sessions.commands(request.params.sessionId);
|
return await sessions.commands(request.params.sessionId);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export type {
|
|||||||
SessionInfo as ClientSession,
|
SessionInfo as ClientSession,
|
||||||
MessagePage as ClientMessagePage,
|
MessagePage as ClientMessagePage,
|
||||||
SessionStatus as ClientSessionStatus,
|
SessionStatus as ClientSessionStatus,
|
||||||
|
SessionModel as ClientSessionModel,
|
||||||
|
ThinkingLevel as ClientThinkingLevel,
|
||||||
SlashCommand as ClientCommand,
|
SlashCommand as ClientCommand,
|
||||||
FileSuggestion as ClientFileSuggestion,
|
FileSuggestion as ClientFileSuggestion,
|
||||||
CommandOption as ClientCommandOption,
|
CommandOption as ClientCommandOption,
|
||||||
|
|||||||
+19
-1
@@ -43,9 +43,27 @@ export interface QueuedSessionMessage {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionModel {
|
||||||
|
provider?: string;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
contextWindow?: number;
|
||||||
|
reasoning?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||||
|
|
||||||
|
export interface ModelSelectionResponse {
|
||||||
|
models: SessionModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThinkingLevelsResponse {
|
||||||
|
levels: ThinkingLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SessionStatus {
|
export interface SessionStatus {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
model?: { provider?: string; id?: string; name?: string; contextWindow?: number; reasoning?: unknown };
|
model?: SessionModel;
|
||||||
thinkingLevel?: string;
|
thinkingLevel?: string;
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
isCompacting: boolean;
|
isCompacting: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user