mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
feat(agent-manager): match new worktree dialog prompt pills to sidebar (#8713)
The New Worktree dialog prompt input now mirrors the sidebar chat exactly: mode selector, model selector, thinking effort (reasoning), and a reset button — in the same order with the same defaults. - Extract ThinkingSelectorBase from ThinkingSelector for reuse outside session context (same pattern as ModelSelectorBase/ModeSwitcherBase) - Initialize model/variant/agent from session defaults instead of null - Show reset button only when model differs from config default - Hide model/thinking/reset pills in compare mode (each session uses its model's default variant) - Extract message-files and continue-worktree helpers from KiloProvider to fix pre-existing lint cap violations - Extract onRequestState from AgentManagerProvider.onMessage to fix pre-existing complexity violation - Remove orphaned open-sessions.ts (dead code flagged by knip)
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
/* eslint-disable max-lines -- TODO: refactor to reduce file size and remove this disable */
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
import { buildPreviewPath, getPreviewCommand, getPreviewDir, parseImage, trimEntries } from "./image-preview"
|
||||
import { isAbsolutePath } from "./path-utils"
|
||||
import type {
|
||||
@@ -42,6 +40,8 @@ import { resolveProjectDirectory } from "./project-directory"
|
||||
import { getBusySessionCount, seedSessionStatuses } from "./session-status"
|
||||
import { retry } from "./services/cli-backend/retry"
|
||||
import { slimPart, slimParts } from "./kilo-provider/slim-metadata"
|
||||
import { handleContinueInWorktree } from "./kilo-provider/continue-worktree"
|
||||
import { parseMessageFiles } from "./kilo-provider/message-files"
|
||||
import { matchFollowup, recordFollowup, type Followup } from "./kilo-provider/followup-session"
|
||||
import { childID } from "./kilo-provider/task-session"
|
||||
import { retryable, backoff, MAX_RETRIES } from "./util/retry"
|
||||
@@ -527,7 +527,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
*/
|
||||
private setupWebviewMessageHandler(webview: vscode.Webview): void {
|
||||
this.webviewMessageDisposable?.dispose()
|
||||
// eslint-disable-next-line complexity -- TODO: refactor to reduce complexity and remove this disable
|
||||
this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => {
|
||||
// Run interceptor if attached (e.g., AgentManagerProvider worktree logic)
|
||||
if (this.onBeforeMessage) {
|
||||
@@ -551,17 +550,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.readyResolvers.splice(0).forEach((r) => r())
|
||||
break
|
||||
case "sendMessage": {
|
||||
const files = z
|
||||
.array(
|
||||
z.object({
|
||||
mime: z.string(),
|
||||
url: z.string().refine((u) => u.startsWith("file://") || u.startsWith("data:")),
|
||||
filename: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.parse(message.files)
|
||||
const files = parseMessageFiles(message.files)
|
||||
await this.handleSendMessage(
|
||||
message.text,
|
||||
typeof message.messageID === "string" ? message.messageID : undefined,
|
||||
@@ -576,17 +565,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
break
|
||||
}
|
||||
case "sendCommand": {
|
||||
const files = z
|
||||
.array(
|
||||
z.object({
|
||||
mime: z.string(),
|
||||
url: z.string().refine((u) => u.startsWith("file://") || u.startsWith("data:")),
|
||||
filename: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.parse(message.files)
|
||||
const files = parseMessageFiles(message.files)
|
||||
await this.handleSendCommand(
|
||||
message.command,
|
||||
message.arguments,
|
||||
@@ -667,9 +646,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
await handleRefreshProfile(this.authCtx)
|
||||
break
|
||||
case "openExternal":
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
}
|
||||
this.openExternal(message.url)
|
||||
break
|
||||
case "openSettingsPanel":
|
||||
vscode.commands.executeCommand("kilo-code.new.settingsButtonClicked", message.tab)
|
||||
@@ -684,30 +661,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
vscode.commands.executeCommand("kilo-code.new.showChanges")
|
||||
break
|
||||
case "openDiffVirtual":
|
||||
if (this.diffVirtualProvider && message.diff) {
|
||||
this.diffVirtualProvider.open(message.diff)
|
||||
}
|
||||
this.openDiffVirtual(message.diff)
|
||||
break
|
||||
case "continueInWorktree":
|
||||
if (message.sessionId && this.continueInWorktreeHandler) {
|
||||
this.continueInWorktreeHandler(message.sessionId, (status: string, detail?: string, error?: string) => {
|
||||
this.postMessage({ type: "continueInWorktreeProgress", status, detail, error })
|
||||
}).catch((err: unknown) => {
|
||||
console.error("[Kilo New] continueInWorktree failed:", err)
|
||||
this.postMessage({
|
||||
type: "continueInWorktreeProgress",
|
||||
status: "error",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
} else if (message.sessionId) {
|
||||
console.error("[Kilo New] continueInWorktree: no handler registered")
|
||||
this.postMessage({
|
||||
type: "continueInWorktreeProgress",
|
||||
status: "error",
|
||||
error: "Continue in Worktree is not available",
|
||||
})
|
||||
}
|
||||
handleContinueInWorktree({
|
||||
sessionId: message.sessionId,
|
||||
handler: this.continueInWorktreeHandler ?? undefined,
|
||||
post: (msg) => this.postMessage(msg),
|
||||
})
|
||||
break
|
||||
case "retryConnection":
|
||||
console.log("[Kilo New] KiloProvider: 🔄 Retrying connection...")
|
||||
@@ -909,16 +870,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
void handleRequestCloudSessionData(this.cloudSessionCtx, message.sessionId)
|
||||
break
|
||||
case "importAndSend": {
|
||||
const files = z
|
||||
.array(
|
||||
z.object({
|
||||
mime: z.string(),
|
||||
url: z.string().refine((u) => u.startsWith("file://") || u.startsWith("data:")),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.parse(message.files)
|
||||
const files = parseMessageFiles(message.files)
|
||||
void handleImportAndSend(
|
||||
this.cloudSessionCtx,
|
||||
message.cloudSessionId,
|
||||
@@ -1069,6 +1021,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
})
|
||||
}
|
||||
|
||||
private openExternal(url: unknown): void {
|
||||
if (typeof url !== "string") return
|
||||
void vscode.env.openExternal(vscode.Uri.parse(url))
|
||||
}
|
||||
|
||||
private openDiffVirtual(diff: unknown): void {
|
||||
if (!this.diffVirtualProvider || !diff) return
|
||||
this.diffVirtualProvider.open(diff as import("./DiffVirtualProvider").DiffVirtualFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize connection to the CLI backend server.
|
||||
* Subscribes to the shared KiloConnectionService.
|
||||
|
||||
@@ -381,28 +381,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
|
||||
private onStateMessage(m: AgentManagerInMessage): Record<string, unknown> | null | undefined {
|
||||
if (m.type === "agentManager.requestState") {
|
||||
void this.stateReady
|
||||
?.then(() => {
|
||||
if (!this.state) {
|
||||
this.pushEmptyState()
|
||||
return
|
||||
}
|
||||
this.pushState()
|
||||
if (this.cachedWorktreeStats) this.postToWebview(this.cachedWorktreeStats)
|
||||
if (this.cachedLocalStats) this.postToWebview(this.cachedLocalStats)
|
||||
this.prBridge.replay()
|
||||
if (this.state.getSessions().length > 0) {
|
||||
this.panel?.sessions.refreshSessions()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log("initializeState failed, pushing partial state:", err)
|
||||
if (!this.state) {
|
||||
this.pushEmptyState()
|
||||
return
|
||||
}
|
||||
this.pushState()
|
||||
})
|
||||
this.onRequestState()
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -416,7 +395,6 @@ export class AgentManagerProvider implements Disposable {
|
||||
}
|
||||
if (m.type === "agentManager.setWorktreeOrder") {
|
||||
this.state?.setWorktreeOrder(m.order)
|
||||
this.pushState()
|
||||
return null
|
||||
}
|
||||
if (m.type === "agentManager.setSessionsCollapsed") {
|
||||
@@ -500,6 +478,41 @@ export class AgentManagerProvider implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private onRequestState(): void {
|
||||
void this.stateReady
|
||||
?.then(() => {
|
||||
// When the folder is not a git repo (or has no folder open),
|
||||
// this.state is never created. pushState() silently returns in that
|
||||
// case, so re-send the empty/non-git state explicitly.
|
||||
if (!this.state) {
|
||||
this.pushEmptyState()
|
||||
return
|
||||
}
|
||||
this.pushState()
|
||||
// Re-send cached stats so the webview gets them even if the poller
|
||||
// already emitted before the webview was ready to receive messages.
|
||||
if (this.cachedWorktreeStats) this.postToWebview(this.cachedWorktreeStats)
|
||||
if (this.cachedLocalStats) this.postToWebview(this.cachedLocalStats)
|
||||
this.prBridge.replay()
|
||||
// Refresh sessions after pushState so the webview's sessionsLoaded
|
||||
// handler is guaranteed to be registered (requestState fires from
|
||||
// onMount). Without this, the initial refreshSessions() in
|
||||
// initializeState() can race ahead of webview mount, causing
|
||||
// sessionsLoaded to never flip to true.
|
||||
if (this.state.getSessions().length > 0) {
|
||||
this.panel?.sessions.refreshSessions()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log("initializeState failed, pushing partial state:", err)
|
||||
if (!this.state) {
|
||||
this.pushEmptyState()
|
||||
return
|
||||
}
|
||||
this.pushState()
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1022,7 +1035,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
}
|
||||
|
||||
// Phase 2: Send the initial prompt to all sessions, or clear busy state if no text.
|
||||
const messages = buildInitialMessages(created, models, { providerID, modelID }, text, agent, files)
|
||||
const messages = buildInitialMessages(created, models, { providerID, modelID }, text, agent, msg.variant, files)
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]!
|
||||
if (text) {
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface InitialMessage {
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
agent?: string
|
||||
variant?: string
|
||||
files?: Array<{ mime: string; url: string }>
|
||||
}
|
||||
|
||||
@@ -81,6 +82,7 @@ export function buildInitialMessages(
|
||||
fallback: { providerID?: string; modelID?: string },
|
||||
prompt?: string,
|
||||
agent?: string,
|
||||
variant?: string,
|
||||
files?: Array<{ mime: string; url: string }>,
|
||||
): InitialMessage[] {
|
||||
return created.map((entry) => {
|
||||
@@ -96,6 +98,7 @@ export function buildInitialMessages(
|
||||
if (prompt) {
|
||||
msg.text = prompt
|
||||
msg.agent = agent
|
||||
msg.variant = variant
|
||||
msg.files = files
|
||||
}
|
||||
return msg
|
||||
|
||||
@@ -169,6 +169,7 @@ interface SendInitialMessage {
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
agent?: string
|
||||
variant?: string
|
||||
files?: Array<{ mime: string; url: string }>
|
||||
}
|
||||
|
||||
@@ -365,6 +366,7 @@ interface CreateMultiVersionIn {
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
agent?: string
|
||||
variant?: string
|
||||
files?: Array<{ mime: string; url: string }>
|
||||
baseBranch?: string
|
||||
branchName?: string
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
type Progress = (status: string, detail?: string, error?: string) => void
|
||||
|
||||
type Ctx = {
|
||||
sessionId?: string
|
||||
handler?: (sessionId: string, progress: Progress) => Promise<void>
|
||||
post: (message: { type: "continueInWorktreeProgress"; status: string; detail?: string; error?: string }) => void
|
||||
}
|
||||
|
||||
export function handleContinueInWorktree(ctx: Ctx): void {
|
||||
if (ctx.sessionId && ctx.handler) {
|
||||
ctx
|
||||
.handler(ctx.sessionId, (status, detail, error) => {
|
||||
ctx.post({ type: "continueInWorktreeProgress", status, detail, error })
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error("[Kilo New] continueInWorktree failed:", err)
|
||||
ctx.post({
|
||||
type: "continueInWorktreeProgress",
|
||||
status: "error",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!ctx.sessionId) return
|
||||
console.error("[Kilo New] continueInWorktree: no handler registered")
|
||||
ctx.post({
|
||||
type: "continueInWorktreeProgress",
|
||||
status: "error",
|
||||
error: "Continue in Worktree is not available",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const file = z.object({
|
||||
mime: z.string(),
|
||||
url: z.string().refine((url) => url.startsWith("file://") || url.startsWith("data:")),
|
||||
filename: z.string().optional(),
|
||||
})
|
||||
|
||||
export function parseMessageFiles(value: unknown) {
|
||||
return z.array(file).optional().catch(undefined).parse(value)
|
||||
}
|
||||
@@ -313,20 +313,16 @@ describe("Agent Manager Provider — onMessage routing", () => {
|
||||
* call pushEmptyState() instead — otherwise the webview stays stuck on
|
||||
* loading skeletons forever.
|
||||
*/
|
||||
it("requestState handler calls pushEmptyState when state is falsy", () => {
|
||||
const text = body("onStateMessage")
|
||||
const start = text.indexOf('"agentManager.requestState"')
|
||||
expect(start, "requestState branch must exist").toBeGreaterThan(-1)
|
||||
const snippet = text.slice(start, start + 700)
|
||||
expect(snippet, "must call pushEmptyState when state is absent").toContain("pushEmptyState")
|
||||
expect(snippet, "must guard on this.state being falsy").toMatch(/!this\.state/)
|
||||
it("requestState handler calls pushEmptyState when this.state is falsy", () => {
|
||||
// onStateMessage delegates to onRequestState; verify the actual handler
|
||||
const text = body("onRequestState")
|
||||
expect(text, "must call pushEmptyState when state is absent").toContain("pushEmptyState")
|
||||
expect(text, "must guard on this.state being falsy").toMatch(/!this\.state/)
|
||||
})
|
||||
|
||||
it("requestState handler calls pushState when state is truthy", () => {
|
||||
const text = body("onStateMessage")
|
||||
const start = text.indexOf('"agentManager.requestState"')
|
||||
const snippet = text.slice(start, start + 700)
|
||||
expect(snippet, "must call pushState for the normal path").toContain("this.pushState()")
|
||||
it("requestState handler calls pushState when this.state is truthy", () => {
|
||||
const text = body("onRequestState")
|
||||
expect(text, "must call pushState for the normal path").toContain("this.pushState()")
|
||||
})
|
||||
|
||||
it("worktree diff behavior lives in the cohesive diff controller", () => {
|
||||
|
||||
@@ -170,16 +170,11 @@ describe("Extension — KiloProvider handler wiring", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("KiloProvider — continueInWorktree error fallback", () => {
|
||||
const provider = fs.readFileSync(KILO_PROVIDER_FILE, "utf-8")
|
||||
const helper = fs.readFileSync(path.join(ROOT, "src/kilo-provider/continue-worktree.ts"), "utf-8")
|
||||
|
||||
it("sends error progress when handler is missing", () => {
|
||||
const caseStart = provider.indexOf('case "continueInWorktree"')
|
||||
expect(caseStart, "continueInWorktree case must exist").toBeGreaterThan(-1)
|
||||
const caseEnd = provider.indexOf("break", caseStart)
|
||||
const block = provider.slice(caseStart, caseEnd)
|
||||
|
||||
expect(block, "must have else branch for missing handler").toContain("else if")
|
||||
expect(block, "must send error status back to webview").toContain('"error"')
|
||||
expect(block, "must use continueInWorktreeProgress message type").toContain("continueInWorktreeProgress")
|
||||
expect(helper, "must send error status back to webview").toContain('"error"')
|
||||
expect(helper, "must use continueInWorktreeProgress message type").toContain("continueInWorktreeProgress")
|
||||
expect(helper, "must handle missing handler case").toContain("no handler registered")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1311,6 +1311,7 @@ const AgentManagerContent: Component = () => {
|
||||
providerID: ev.providerID,
|
||||
modelID: ev.modelID,
|
||||
agent: ev.agent,
|
||||
variant: ev.variant,
|
||||
files: ev.files,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { useVSCode } from "../src/context/vscode"
|
||||
import { useServer } from "../src/context/server"
|
||||
import { useSession } from "../src/context/session"
|
||||
import { useProvider } from "../src/context/provider"
|
||||
import { ModelSelectorBase } from "../src/components/shared/ModelSelector"
|
||||
import { ModeSwitcherBase } from "../src/components/shared/ModeSwitcher"
|
||||
import { ThinkingSelectorBase } from "../src/components/shared/ThinkingSelector"
|
||||
import {
|
||||
MultiModelSelector,
|
||||
type ModelAllocations,
|
||||
@@ -60,6 +62,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
|
||||
const vscode = useVSCode()
|
||||
const server = useServer()
|
||||
const session = useSession()
|
||||
const provider = useProvider()
|
||||
|
||||
const [tab, setTab] = createSignal<DialogTab>("new")
|
||||
|
||||
@@ -74,7 +77,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
|
||||
const cached = vscode.getState<Record<string, unknown>>()
|
||||
const [prompt, setPrompt] = createSignal((cached?.advancedDialogPrompt as string) ?? "")
|
||||
const [versions, setVersions] = createSignal<VersionCount>(1)
|
||||
const [model, setModel] = createSignal<{ providerID: string; modelID: string } | null>(null)
|
||||
const [model, setModel] = createSignal<{ providerID: string; modelID: string } | null>(session.selected())
|
||||
const [compareMode, setCompareMode] = createSignal(false)
|
||||
const [modelAllocations, setModelAllocations] = createSignal<ModelAllocations>(new Map())
|
||||
const [agent, setAgent] = createSignal(session.selectedAgent())
|
||||
@@ -85,6 +88,43 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
|
||||
const [baseBranchOpen, setBaseBranchOpen] = createSignal(false)
|
||||
const [compareOpen, setCompareOpen] = createSignal(false)
|
||||
const [highlightedIndex, setHighlightedIndex] = createSignal(0)
|
||||
const [variant, setVariant] = createSignal<string | undefined>(session.currentVariant())
|
||||
|
||||
// Variant list for the currently selected model
|
||||
const variants = createMemo(() => {
|
||||
const sel = model()
|
||||
if (!sel) return []
|
||||
const found = provider.findModel(sel)
|
||||
if (!found?.variants) return []
|
||||
return Object.keys(found.variants)
|
||||
})
|
||||
|
||||
// Current effective variant — falls back to first available if stored value is invalid
|
||||
const effectiveVariant = createMemo(() => {
|
||||
const list = variants()
|
||||
if (list.length === 0) return undefined
|
||||
const stored = variant()
|
||||
return stored && list.includes(stored) ? stored : list[0]
|
||||
})
|
||||
|
||||
// True when the user has changed the model from the session/config default
|
||||
const overridden = createMemo(() => {
|
||||
const sel = model()
|
||||
const cfg = session.selected()
|
||||
if (!sel || !cfg) return false
|
||||
return sel.providerID !== cfg.providerID || sel.modelID !== cfg.modelID
|
||||
})
|
||||
|
||||
// Reset variant when model changes and stored variant is not in new list
|
||||
createEffect(() => {
|
||||
const list = variants()
|
||||
if (list.length === 0) {
|
||||
setVariant(undefined)
|
||||
return
|
||||
}
|
||||
const stored = variant()
|
||||
if (!stored || !list.includes(stored)) setVariant(list[0])
|
||||
})
|
||||
|
||||
const imageAttach = useImageAttachments()
|
||||
imageAttach.setFilePathDropHandler((paths) => {
|
||||
@@ -172,6 +212,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
|
||||
providerID: sel?.providerID,
|
||||
modelID: sel?.modelID,
|
||||
agent: selectedAgent,
|
||||
variant: isCompare ? undefined : effectiveVariant(),
|
||||
baseBranch: advanced ? (baseBranch() ?? undefined) : undefined,
|
||||
branchName: customBranch,
|
||||
modelAllocations: allocations,
|
||||
@@ -333,17 +374,32 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
|
||||
</div>
|
||||
<div class="prompt-input-hint">
|
||||
<div class="prompt-input-hint-selectors">
|
||||
<Show when={session.agents().length > 1}>
|
||||
<ModeSwitcherBase agents={session.agents()} value={agent()} onSelect={setAgent} />
|
||||
</Show>
|
||||
<Show when={!compareMode()}>
|
||||
<ModelSelectorBase
|
||||
value={model()}
|
||||
onSelect={(pid, mid) => setModel(pid && mid ? { providerID: pid, modelID: mid } : null)}
|
||||
onSelect={(pid, mid) => {
|
||||
if (pid && mid) setModel({ providerID: pid, modelID: mid })
|
||||
}}
|
||||
placement="top-start"
|
||||
allowClear
|
||||
clearLabel="Default"
|
||||
/>
|
||||
</Show>
|
||||
<Show when={session.agents().length > 1}>
|
||||
<ModeSwitcherBase agents={session.agents()} value={agent()} onSelect={setAgent} />
|
||||
<ThinkingSelectorBase variants={variants()} value={effectiveVariant()} onSelect={setVariant} />
|
||||
<Show when={overridden()}>
|
||||
<Tooltip value={t("prompt.action.resetModel")} placement="top">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setModel(session.selected())}
|
||||
aria-label={t("prompt.action.resetModel")}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
|
||||
</svg>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="prompt-input-hint-actions" />
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* ThinkingSelector component
|
||||
* Popover-based dropdown for choosing a thinking effort variant.
|
||||
* Only rendered when the selected model supports reasoning variants.
|
||||
*
|
||||
* ThinkingSelectorBase — reusable core that accepts variants/value/onSelect props.
|
||||
* ThinkingSelector — thin wrapper wired to session context for chat usage.
|
||||
*/
|
||||
|
||||
import { Component, createSignal, For, Show } from "solid-js"
|
||||
@@ -9,26 +12,35 @@ import { Popover } from "@kilocode/kilo-ui/popover"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { useSession } from "../../context/session"
|
||||
|
||||
export const ThinkingSelector: Component = () => {
|
||||
const session = useSession()
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reusable base component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ThinkingSelectorBaseProps {
|
||||
/** Available variant names (e.g. ["low","medium","high"]) */
|
||||
variants: string[]
|
||||
/** Currently selected variant */
|
||||
value: string | undefined
|
||||
/** Called when the user picks a variant */
|
||||
onSelect: (value: string) => void
|
||||
}
|
||||
|
||||
export const ThinkingSelectorBase: Component<ThinkingSelectorBaseProps> = (props) => {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
const variants = () => session.variantList()
|
||||
const current = () => session.currentVariant()
|
||||
|
||||
function pick(value: string) {
|
||||
session.selectVariant(value)
|
||||
props.onSelect(value)
|
||||
setOpen(false)
|
||||
requestAnimationFrame(() => window.dispatchEvent(new Event("focusPrompt")))
|
||||
}
|
||||
|
||||
const triggerLabel = () => {
|
||||
const v = current()
|
||||
const label = () => {
|
||||
const v = props.value
|
||||
return v ? v.charAt(0).toUpperCase() + v.slice(1) : ""
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={variants().length > 0}>
|
||||
<Show when={props.variants.length > 0}>
|
||||
<Popover
|
||||
placement="top-start"
|
||||
open={open()}
|
||||
@@ -37,7 +49,7 @@ export const ThinkingSelector: Component = () => {
|
||||
triggerProps={{ variant: "ghost", size: "small" }}
|
||||
trigger={
|
||||
<>
|
||||
<span class="thinking-selector-trigger-label">{triggerLabel()}</span>
|
||||
<span class="thinking-selector-trigger-label">{label()}</span>
|
||||
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" style={{ "flex-shrink": "0" }}>
|
||||
<path d="M8 4l4 5H4l4-5z" />
|
||||
</svg>
|
||||
@@ -45,12 +57,12 @@ export const ThinkingSelector: Component = () => {
|
||||
}
|
||||
>
|
||||
<div class="thinking-selector-list" role="listbox">
|
||||
<For each={variants()}>
|
||||
<For each={props.variants}>
|
||||
{(v) => (
|
||||
<div
|
||||
class={`thinking-selector-item${current() === v ? " selected" : ""}`}
|
||||
class={`thinking-selector-item${props.value === v ? " selected" : ""}`}
|
||||
role="option"
|
||||
aria-selected={current() === v}
|
||||
aria-selected={props.value === v}
|
||||
onClick={() => pick(v)}
|
||||
>
|
||||
<span class="thinking-selector-item-name">{v.charAt(0).toUpperCase() + v.slice(1)}</span>
|
||||
@@ -62,3 +74,19 @@ export const ThinkingSelector: Component = () => {
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chat-specific wrapper (backwards-compatible)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ThinkingSelector: Component = () => {
|
||||
const session = useSession()
|
||||
|
||||
return (
|
||||
<ThinkingSelectorBase
|
||||
variants={session.variantList()}
|
||||
value={session.currentVariant()}
|
||||
onSelect={(value) => session.selectVariant(value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1118,6 +1118,7 @@ export interface AgentManagerSendInitialMessage {
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
agent?: string
|
||||
variant?: string
|
||||
files?: Array<{ mime: string; url: string }>
|
||||
}
|
||||
|
||||
@@ -2004,6 +2005,7 @@ export interface CreateMultiVersionRequest {
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
agent?: string
|
||||
variant?: string
|
||||
files?: FileAttachment[]
|
||||
baseBranch?: string
|
||||
branchName?: string
|
||||
|
||||
Reference in New Issue
Block a user