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
5 changes: 5 additions & 0 deletions coworker/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,11 @@ def settings_set_context_bar(body: dict) -> dict[str, Any]:
# Composer: show the context-window fill bar, or just the popover (owner ask).
return manager.set_context_bar((body or {}).get("context_bar", True))

@app.post("/v1/settings/hide-live-thinking")
def settings_set_hide_live_thinking(body: dict) -> dict[str, Any]:
# Hide live thinking steps while running (#611).
return manager.set_hide_live_thinking((body or {}).get("hide_live_thinking", False))

@app.post("/v1/settings/auto-approve")
def settings_set_auto_approve(body: dict) -> dict[str, Any]:
# Auto-Approve feature flag (spec §1.5): when on, Mode.AUTO_APPROVE gets an LLM
Expand Down
10 changes: 10 additions & 0 deletions coworker/server/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3442,6 +3442,7 @@ def _selectable(m: str) -> bool:
"nav_layout": self._nav_layout(),
"sessions_peek": self.sessions_peek(),
"context_bar": self.context_bar(),
"hide_live_thinking": self.hide_live_thinking(),
# Auto-Approve feature flag + its shadow-eval sibling (spec §1.5). Drive the
# Settings toggles and gate the composer's Auto-Approve mode entry.
"auto_approve": self.auto_approve(),
Expand Down Expand Up @@ -3515,6 +3516,15 @@ def set_context_bar(self, shown: Any) -> dict[str, Any]:
self._save_prefs()
return {"ok": True, "context_bar": self.context_bar()}

def hide_live_thinking(self) -> bool:
"""Hide/collapse live reasoning steps while running (#611)."""
return bool(self._prefs.get("hide_live_thinking", False))

def set_hide_live_thinking(self, hide: Any) -> dict[str, Any]:
self._prefs["hide_live_thinking"] = bool(hide)
self._save_prefs()
return {"ok": True, "hide_live_thinking": self.hide_live_thinking()}

# -- Auto-Approve (spec §1.5, Part 6 step 3) --------------------------------
# The feature flag and its shadow-eval sibling live in prefs (GUI-writable), falling
# back to the config.toml value a power user may have hand-set. Prefs is user-global,
Expand Down
15 changes: 14 additions & 1 deletion surfaces/gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
deleteMemory,
updateMemory,
getSettings,
setHideLiveThinking as apiSetHideLiveThinking,
getPersonas,
getInbox,
getUnattended,
Expand Down Expand Up @@ -210,6 +211,8 @@ export function App() {
// Settings: show the composer's context-window fill bar. OFF by default (owner ask),
// so an older backend without the field also shows the session total.
const [contextBar, setContextBar] = useState(false);
// Option to hide/collapse live thinking/reasoning steps while running (#611).
const [hideLiveThinking, setHideLiveThinkingState] = useState(false);
// Per-session token usage (OPE-42): rebuilt from the transcript on session load,
// accumulated live from assistant_message events, reset with the transcript.
const [usage, setUsage] = useState<SessionUsage>(emptyUsage());
Expand Down Expand Up @@ -616,6 +619,7 @@ export function App() {
setModelLabels(s.model_labels || {});
setModelContextWindows(s.model_context_windows || {});
setContextBar(s.context_bar === true);
setHideLiveThinkingState(s.hide_live_thinking === true);
setModelReady(s.model_ready);
if (s.surfaces) setSurfaces(s.surfaces);
})
Expand Down Expand Up @@ -1979,7 +1983,16 @@ export function App() {
the message finalizes. */}
{running && reasoningStream && !streaming && (
<div className="transcript">
<ThinkingBlock text={reasoningStream} live />
<ThinkingBlock
text={reasoningStream}
live
hidden={hideLiveThinking}
onToggleHidden={() => {
const next = !hideLiveThinking;
setHideLiveThinkingState(next);
void apiSetHideLiveThinking(next);
}}
/>
</div>
)}
{/* Compaction runs between provider turns (nothing streams during it), so
Expand Down
14 changes: 14 additions & 0 deletions surfaces/gui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,8 @@ export interface ModelSettings {
// Composer: show the context-window fill bar (default FALSE; absent → the chip shows
// the session total). The usage popover keeps both numbers regardless.
context_bar?: boolean;
// Option to hide/collapse live thinking/reasoning steps while running (#611).
hide_live_thinking?: boolean;
// Auto-Approve mode (spec §1.5): the feature flag that offers the reviewer mode, and its
// shadow-eval sibling. Both default FALSE and are absent on older backends — the composer
// hides the Auto-Approve mode entry unless auto_approve is explicitly true.
Expand Down Expand Up @@ -1075,6 +1077,18 @@ export async function setNavLayout(
return res.json();
}

/** Persist preference to hide live thinking steps while a task is running (#611). */
export async function setHideLiveThinking(
hide: boolean,
): Promise<{ ok: boolean; hide_live_thinking?: boolean; error?: string }> {
const res = await fetch(`${httpBase()}/v1/settings/hide-live-thinking`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hide_live_thinking: hide }),
});
return res.json();
}

// Fired after a cloud sign-in/out completes so the account row (§26) refreshes without
// waiting for the next window focus.
export const CLOUD_CHANGED = "coworker:cloud-changed";
Expand Down
41 changes: 41 additions & 0 deletions surfaces/gui/src/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
setAutoApproveShadow,
setCompactionSettings,
setContextBar,
setHideLiveThinking,
setOnboarded,
setPdfSettings,
setScratchBase,
Expand Down Expand Up @@ -473,6 +474,8 @@ function AppearanceSection() {

<ContextBarCard />

<LiveThinkingCard />

<AutoApproveCard />

<FilesCard />
Expand Down Expand Up @@ -875,6 +878,44 @@ function ContextBarCard() {
);
}

// Live Thinking steps toggle (#611): allows collapsing or hiding reasoning text while
// running so long agent turns do not produce a fast-scrolling wall of thinking text.
function LiveThinkingCard() {
const { t } = useTranslation();
const [hide, setHide] = useState<boolean | null>(null);

useEffect(() => {
getSettings()
.then((s) => setHide(s.hide_live_thinking === true))
.catch(() => setHide(false));
}, []);

const save = async (next: boolean) => {
setHide(next);
await setHideLiveThinking(next);
};

if (hide === null) return null;
return (
<div className={CARD + " p-4 mb-4"} data-testid="live-thinking-card">
<div className={FIELD_LABEL}>{t("settings.thinking_section")}</div>
<label className="flex items-start gap-3 py-2">
<input
type="checkbox"
className="mt-0.5"
data-testid="hide-live-thinking-toggle"
checked={hide}
onChange={(e) => save(e.target.checked)}
/>
<span>
<span className="block text-[13px] text-ink">{t("settings.hide_live_thinking_title")}</span>
<span className="block text-[12px] text-muted">{t("settings.hide_live_thinking_desc")}</span>
</span>
</label>
</div>
);
}

// Auto-Approve (spec §1.5): the experimental feature flag that adds the "Auto-Approve" mode
// to the composer's mode picker, plus its shadow-evaluation sibling. Both default off and are
// user-global (a cloned repo can't turn either on). Shadow is nested under the main flag — it
Expand Down
65 changes: 54 additions & 11 deletions surfaces/gui/src/components/Transcript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,64 @@ function BubbleMeta({ text, ts, align }: { text: string; ts?: number; align: "le
// Reasoning-model thinking text (model-layer roadmap item 4): a quiet disclosure —
// collapsed by default, the trace one click away. `live` = still streaming (pulsing label);
// App renders that variant above the transcript, this one rides a finalized assistant item.
export function ThinkingBlock({ text, live }: { text: string; live?: boolean }) {
export function ThinkingBlock({
text,
live,
hidden = false,
onToggleHidden,
}: {
text: string;
live?: boolean;
hidden?: boolean;
onToggleHidden?: () => void;
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);

if (live && hidden) {
return (
<div className="thinking thinking-quiet" data-testid="thinking-quiet">
<span className="thinking-live">{t("transcript.thinking_live")}</span>
{onToggleHidden && (
<button
type="button"
className="thinking-toggle-btn ml-1"
onClick={onToggleHidden}
data-testid="show-live-thinking-btn"
>
({t("transcript.show_thinking")})
</button>
)}
</div>
);
}

return (
<div className="thinking">
<button
className="thinking-head"
onClick={() => setOpen((v) => !v)}
data-testid="thinking-toggle"
>
<Icon name="chevronDown" size={12} className={"thinking-caret" + (open ? " open" : "")} />
<span className={live ? "thinking-live" : undefined}>
{live ? t("transcript.thinking_live") : t("transcript.thinking_process")}
</span>
</button>
<div className="flex items-center justify-between">
<button
type="button"
className="thinking-head"
onClick={() => setOpen((v) => !v)}
data-testid="thinking-toggle"
>
<Icon name="chevronDown" size={12} className={"thinking-caret" + (open ? " open" : "")} />
<span className={live ? "thinking-live" : undefined}>
{live ? t("transcript.thinking_live") : t("transcript.thinking_process")}
</span>
</button>
{live && onToggleHidden && (
<button
type="button"
className="thinking-toggle-btn mr-1"
onClick={onToggleHidden}
title={t("transcript.hide_live_thinking")}
data-testid="hide-live-thinking-btn"
>
{t("transcript.hide_thinking")}
</button>
)}
</div>
{open && (
<div className="thinking-body" data-testid="thinking-body">
{text}
Expand Down
8 changes: 7 additions & 1 deletion surfaces/gui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@
"copied": "Copied",
"thinking_live": "Thinking…",
"thinking_process": "Thought process",
"show_thinking": "Show thinking",
"hide_thinking": "Hide thinking",
"hide_live_thinking": "Hide live thinking",
"who_assistant": "assistant",
"retry": "Retry",
"approval": {
Expand Down Expand Up @@ -393,7 +396,10 @@
"personas_desc": "Coworkers are agents specialized for a particular role or task. They come equipped with the tools and skills to be successful in that role. Enabling a coworker lets you pick it when starting a conversation.",
"composer_section": "Composer",
"context_bar_title": "Show the context window bar",
"context_bar_desc": "A small meter showing how full the model’s context window is. Turn it off to show the same thing as a number instead."
"context_bar_desc": "A small meter showing how full the model’s context window is. Turn it off to show the same thing as a number instead.",
"thinking_section": "Reasoning & Thinking",
"hide_live_thinking_title": "Hide live thinking steps while a task is running",
"hide_live_thinking_desc": "Suppress streaming reasoning text from the live transcript during execution. The full thought process remains available once the turn completes."
},
"cloud": {
"check_browser": "Check your browser…",
Expand Down
8 changes: 7 additions & 1 deletion surfaces/gui/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@
"copied": "已复制",
"thinking_live": "思考中…",
"thinking_process": "思考过程",
"show_thinking": "显示思考",
"hide_thinking": "隐藏思考",
"hide_live_thinking": "隐藏实时思考",
"who_assistant": "助手",
"retry": "重试",
"approval": {
Expand Down Expand Up @@ -385,7 +388,10 @@
"personas_desc": "同事是专精于特定角色或任务的 agent,自带胜任该角色所需的工具与技能。启用同事后,即可在开始会话时选择它。",
"composer_section": "输入框",
"context_bar_title": "显示上下文窗口占用条",
"context_bar_desc": "一个小指示条,显示模型上下文窗口的占用程度。关闭后将改为以数字形式显示。"
"context_bar_desc": "一个小指示条,显示模型上下文窗口的占用程度。关闭后将改为以数字形式显示。",
"thinking_section": "推理与思考",
"hide_live_thinking_title": "在任务运行时隐藏实时思考步骤",
"hide_live_thinking_desc": "在执行期间从实时记录中抑制流式传输的思考文本。回合完成后,完整的思考过程仍可查看。"
},
"cloud": {
"check_browser": "请查看你的浏览器…",
Expand Down
9 changes: 9 additions & 0 deletions surfaces/gui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,15 @@ html[data-platform="linux"] ::-webkit-scrollbar-thumb:hover { background-color:
border-left: 2px solid var(--line); padding: 4px 0 4px 10px; margin: 4px 0 6px 4px;
max-height: 280px; overflow-y: auto;
}
.thinking-quiet {
display: flex; align-items: center; gap: 8px;
font-size: 12px; color: var(--faint); padding: 2px 0;
}
.thinking-toggle-btn {
background: none; border: none; font-size: 11px;
color: var(--faint); cursor: pointer; padding: 0 4px;
}
.thinking-toggle-btn:hover { color: var(--ink); text-decoration: underline; }

/* -- BETA tag (quiet gray chip beside the wordmark; presentation-only, never in
bundle/artifact names — those feed the updater) --------------------------------- */
Expand Down
66 changes: 66 additions & 0 deletions tests/test_hide_live_thinking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Tests for option to hide live thinking steps (#611)."""

from __future__ import annotations

from fastapi.testclient import TestClient

from coworker.providers import ModelCapabilities, ProviderClient
from coworker.server import SessionManager, create_app


class EmptyProvider(ProviderClient):
def complete(self, *, model, messages, tools=None, **settings):
raise NotImplementedError

def capabilities(self, model):
return ModelCapabilities()


def test_hide_live_thinking_preference_default_and_toggle(tmp_path):
manager = SessionManager(workspace=tmp_path, provider=EmptyProvider())
# Default is False
assert manager.hide_live_thinking() is False
assert manager.get_settings()["hide_live_thinking"] is False

# Toggle to True
res = manager.set_hide_live_thinking(True)
assert res["ok"] is True
assert res["hide_live_thinking"] is True
assert manager.hide_live_thinking() is True
assert manager.get_settings()["hide_live_thinking"] is True

# Rebuilding manager on same workspace / prefs persists preference
manager2 = SessionManager(workspace=tmp_path, provider=EmptyProvider())
assert manager2.hide_live_thinking() is True

# Toggle back to False
res2 = manager2.set_hide_live_thinking(False)
assert res2["ok"] is True
assert res2["hide_live_thinking"] is False
assert manager2.hide_live_thinking() is False


def test_hide_live_thinking_rest_endpoints(tmp_path):
manager = SessionManager(workspace=tmp_path, provider=EmptyProvider())
client = TestClient(create_app(manager))

# Initial settings
resp = client.get("/v1/settings")
assert resp.status_code == 200
assert resp.json()["hide_live_thinking"] is False

# POST /v1/settings/hide-live-thinking
resp = client.post("/v1/settings/hide-live-thinking", json={"hide_live_thinking": True})
assert resp.status_code == 200
assert resp.json()["ok"] is True
assert resp.json()["hide_live_thinking"] is True

# Check updated settings
resp = client.get("/v1/settings")
assert resp.status_code == 200
assert resp.json()["hide_live_thinking"] is True

# Turn off
resp = client.post("/v1/settings/hide-live-thinking", json={"hide_live_thinking": False})
assert resp.status_code == 200
assert resp.json()["hide_live_thinking"] is False