Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions components/ChatInput.streaming-thinking.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";

const source = await readFile(new URL("./ChatInput.tsx", import.meta.url), "utf8");
const controls = source.slice(source.indexOf("{isStreaming && onThinkingLevelChange &&"));

test("shows the reasoning level of the running turn without offering a control", () => {
const readOnly = controls.slice(0, controls.indexOf("{!isStreaming && onThinkingLevelChange &&"));
assert.match(readOnly, /<span\s+title=\{t\("chat\.currentReasoning", \{ level: thinkingDisplayLabel \}\)\}/);
assert.match(readOnly, /\{thinkingDisplayLabel\}<\/span>/);
assert.doesNotMatch(readOnly, /<button|onClick=/);
});

test("keeps the read-only level aligned with the neighbouring controls", () => {
const readOnly = controls.slice(0, controls.indexOf("{!isStreaming && onThinkingLevelChange &&"));
const dropdown = controls.slice(controls.indexOf("{!isStreaming && onThinkingLevelChange &&"));
const padding = /padding: isMobile \? "0 6px" : "8px 12px"/;
const height = /height: 32/;
for (const [label, markup] of [["read-only", readOnly], ["dropdown", dropdown]]) {
assert.match(markup, padding, `${label} horizontal padding`);
assert.match(markup, height, `${label} height`);
}
// The label collapses to the icon on mobile exactly like the editable control.
assert.match(readOnly, /\(!isMobile \|\| controlsMenuOpen\) &&/);
});

test("shows the level the runtime actually applies, not the selector placeholder", async () => {
const session = await readFile(new URL("../hooks/useAgentSession.ts", import.meta.url), "utf8");
const start = session.slice(session.indexOf('case "agent_start":'), session.indexOf('case "agent_end":'));
// Choosing "auto" leaves pi's setting untouched, so the selector alone cannot be trusted.
assert.match(session, /if \(level === "auto"\) return;/);
assert.match(start, /fetch\(`\/api\/agent\/\$\{encodeURIComponent\(sessionIdRef\.current\)\}`\)/);
assert.match(start, /if \(!agentRunningRef\.current \|\| !d\.state\?\.thinkingLevel\) return;\s*setThinkingLevel\(d\.state\.thinkingLevel as ThinkingLevelOption\);/);
});
20 changes: 20 additions & 0 deletions components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { FolderIcon, getFileIcon } from "./FileIcons";
import { useIsMobile } from "@/hooks/useIsMobile";
import { useI18n } from "@/hooks/useI18n";
import { useChatAppearance } from "@/hooks/useChatAppearance";
import { ThinkingIcon } from "./ThinkingIcon";
import type { ToolPreset } from "@/lib/tool-presets";
import { ModelSelector, type ModelSelectorOption } from "./ModelSelector";

Expand Down Expand Up @@ -2268,6 +2269,25 @@ export const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({
backdropFilter: "blur(10px)",
} : null),
}}>
{isStreaming && onThinkingLevelChange && (
// The level cannot change mid-turn, so this is read-only: a button here
// would invite clicks that do nothing. It still answers the question
// that matters while a turn runs, which budget is this one spending.
<span
title={t("chat.currentReasoning", { level: thinkingDisplayLabel })}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show the effective reasoning level instead of auto

When an existing session previously used an explicit level and the user selects “Use pi default,” handleThinkingLevelChange sets the client state to auto but returns without updating the runtime, so the agent retains the previous level. On the next prompt this new badge therefore reports auto as the current turn's level even though, for example, high is actually being used. Capture or synchronize the runtime's effective level when the turn starts rather than rendering the selector state directly.

Useful? React with 👍 / 👎.

style={{
display: "flex", alignItems: "center", gap: 5,
// The control row has no gap; each control pads itself, so match
// the neighbouring buttons or this sits flush against Stop.
padding: isMobile ? "0 6px" : "8px 12px",
height: 32,
color: "var(--text-dim)", fontSize: 12,
}}
>
<ThinkingIcon active={false} size={11} />
{(!isMobile || controlsMenuOpen) && <span style={{ whiteSpace: "nowrap" }}>{thinkingDisplayLabel}</span>}
</span>
)}
{!isStreaming && onThinkingLevelChange && (
<div ref={thinkingDropdownRef} style={{ position: "relative" }}>
<button
Expand Down
12 changes: 12 additions & 0 deletions hooks/useAgentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,18 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
setAgentRunning(true);
setAgentPhase({ kind: "waiting_model" });
dispatch({ type: "start" });
// "auto" is a client-side placeholder that leaves pi's setting untouched,
// so the selector can disagree with the runtime. Show the level this turn
// actually runs with.
if (sessionIdRef.current) {
fetch(`/api/agent/${encodeURIComponent(sessionIdRef.current)}`)
.then((r) => r.json())
.then((d: { state?: AgentStateResponse }) => {
if (!agentRunningRef.current || !d.state?.thinkingLevel) return;
setThinkingLevel(d.state.thinkingLevel as ThinkingLevelOption);
})
.catch(() => {});
}
break;
case "agent_end":
// One logical prompt can emit multiple agent_end events before retrying,
Expand Down
1 change: 1 addition & 0 deletions lib/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export const enLocale: LocalePlugin = {
"chat.allBuiltInTools": "All built-in tools",
"chat.changeReasoning": "Change reasoning level: {level}",
"chat.changeReasoningLabel": "Change reasoning level",
"chat.currentReasoning": "Reasoning level for this turn: {level}",
"chat.changeToolPreset": "Change tool preset",
"chat.stopCompaction": "Stop compaction",
"chat.compactContext": "Compact context",
Expand Down
1 change: 1 addition & 0 deletions lib/i18n/messages/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export const zhCNLocale: LocalePlugin = {
"chat.allBuiltInTools": "全部内置工具",
"chat.changeReasoning": "更改推理级别:{level}",
"chat.changeReasoningLabel": "更改推理级别",
"chat.currentReasoning": "本轮使用的推理级别:{level}",
"chat.changeToolPreset": "更改工具预设",
"chat.stopCompaction": "停止压缩",
"chat.compactContext": "压缩上下文",
Expand Down
1 change: 1 addition & 0 deletions lib/i18n/messages/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export const zhTWLocale: LocalePlugin = {
"chat.allBuiltInTools": "全部內建工具",
"chat.changeReasoning": "變更推理層級:{level}",
"chat.changeReasoningLabel": "變更推理層級",
"chat.currentReasoning": "本輪使用的推理層級:{level}",
"chat.changeToolPreset": "更改工具預設",
"chat.stopCompaction": "停止壓縮",
"chat.compactContext": "壓縮上下文",
Expand Down
Loading