Skip to content
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,7 @@ and CLI without export or conversion.
| Attach a file | Use the composer attachment | `@path/to/file` |
| Change the next Turn's model | Composer model picker | `/model` |
| Adjust Thinking effort | Composer effort picker | `/effort` |
| Cap the Session context window | Model picker context control | `/context` |
| Choose tool access | Composer access picker | `/permissions` |
| Load Skills for the next Turn | Composer Skills control | `/skill <name>` |
| Create a reusable Skill | **Skills → Create Skill** | `$skill-creator` |
Expand All @@ -918,6 +919,9 @@ permanent deletion removes the Session records but never repository files.
Desktop provides connection setup and verification under **Settings → AI
providers**. In the CLI, `/model` changes the connection and model for future
Turns, while `/effort` selects a Thinking level supported by that model.
Use the model picker's context control, or `/context 64k` in the TUI, to make
future Turns compact history sooner; `/context auto` restores the model's
published window.

Model changes never rewrite earlier history or alter an active Turn. Thinking
effort controls the request sent to the provider; transcript detail controls
Expand Down
3 changes: 3 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,7 @@ Project,或者从项目目录启动 `deepcode`,然后创建新 Session 或
| 附加文件 | 使用输入框附件 | `@文件路径` |
| 修改下一个 Turn 的模型 | 输入框模型选择器 | `/model` |
| 调整 Thinking 档位 | 输入框 Thinking 选择器 | `/effort` |
| 限制 Session 上下文窗口 | 模型选择器上下文控件 | `/context` |
| 选择工具权限 | 输入框权限选择器 | `/permissions` |
| 为下一个 Turn 加载 Skills | 输入框 Skills 控件 | `/skill <名称>` |
| 创建可复用 Skill | **Skills → Create Skill** | `$skill-creator` |
Expand All @@ -749,6 +750,8 @@ Session,不删除历史;永久删除只移除 Session 记录,不会删除
Desktop 在 **Settings → AI providers** 中提供连接配置与验证。CLI 使用
`/model` 修改后续 Turn 的连接和模型,使用 `/effort` 选择该模型支持的
Thinking 档位。
在模型选择器中设置上下文上限,或在 TUI 中输入 `/context 64k`,可以让
后续 Turn 更早压缩历史;`/context auto` 会恢复模型公布的上下文窗口。

模型切换不会改写已有历史,也不会改变正在运行的 Turn。Thinking 档位决定
发送给 Provider 的请求;transcript 详细程度只影响界面展示。DeepCode 只在
Expand Down
55 changes: 53 additions & 2 deletions app_server/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@
AutomationActivationStatus,
AutomationScheduleKind,
)
from core.domain.execution_profile import ExecutionSelection
from core.domain.execution_profile import (
MAX_CONTEXT_WINDOW_TOKENS,
MIN_CONTEXT_WINDOW_TOKENS,
ExecutionSelection,
)
from core.domain.execution_security import ExecutionAccessPreset
from core.domain.message_provenance import ClientSurface
from core.domain.project import TrustState
Expand Down Expand Up @@ -170,6 +174,28 @@ def nullable_string(self, name: str) -> str | None:
raise InvalidParams(f"{name} must be a non-empty string or null")
return value

def nullable_integer(
self,
name: str,
*,
minimum: int = 0,
maximum: int | None = None,
) -> int | None:
if name not in self.values:
raise InvalidParams(f"{name} is required")
value = self.values[name]
if value is None:
return None
if isinstance(value, bool) or not isinstance(value, int):
raise InvalidParams(f"{name} must be an integer or null")
if value < minimum or (maximum is not None and value > maximum):
if maximum is None:
raise InvalidParams(f"{name} must be at least {minimum} or null")
raise InvalidParams(
f"{name} must be between {minimum} and {maximum}, or null"
)
return value


Handler = Callable[[Params], Any]

Expand Down Expand Up @@ -862,6 +888,7 @@ def _thread_start(self, params: Params) -> dict[str, Any]:
"connectionId",
"model",
"reasoningEffort",
"contextWindow",
"workspacePath",
"parentThreadId",
"agentPreset",
Expand All @@ -878,6 +905,11 @@ def _thread_start(self, params: Params) -> dict[str, Any]:
connection_id=params.string("connectionId", required=False),
model=params.string("model", required=False),
reasoning_effort=params.string("reasoningEffort", required=False),
context_window=params.optional_integer(
"contextWindow",
minimum=MIN_CONTEXT_WINDOW_TOKENS,
maximum=MAX_CONTEXT_WINDOW_TOKENS,
),
workspace_path=params.string("workspacePath", required=False),
parent_thread_id=params.string("parentThreadId", required=False),
agent_preset=params.string("agentPreset", required=False),
Expand Down Expand Up @@ -928,6 +960,7 @@ def _thread_model(self, params: Params) -> dict[str, Any]:
connection_id=connection_id,
model_id=model,
reasoning_effort=current.reasoning_effort,
context_window=current.context_window,
),
)
thread = self.application.threads.set_execution_selection(
Expand All @@ -944,6 +977,7 @@ def _thread_model(self, params: Params) -> dict[str, Any]:
connection_id=current.connection_id,
model_id=model,
reasoning_effort=current.reasoning_effort,
context_window=current.context_window,
),
)
thread = self.application.threads.set_model(
Expand All @@ -955,25 +989,42 @@ def _thread_model(self, params: Params) -> dict[str, Any]:
def _thread_execution_update(self, params: Params) -> dict[str, Any]:
"""Atomically validate and update the future-Turn execution choice."""

params.only("threadId", "connectionId", "model", "reasoningEffort")
params.only(
"threadId",
"connectionId",
"model",
"reasoningEffort",
"contextWindow",
)
thread_id = str(params.string("threadId"))
current = self.application.threads.read(thread_id)
connection_id = params.nullable_string("connectionId")
model = params.nullable_string("model")
reasoning_effort = params.nullable_string("reasoningEffort")
context_window = (
params.nullable_integer(
"contextWindow",
minimum=MIN_CONTEXT_WINDOW_TOKENS,
maximum=MAX_CONTEXT_WINDOW_TOKENS,
)
if "contextWindow" in params.values
else current.context_window
)
self.application.llm.resolve(
current.workspace_path,
ExecutionSelection(
connection_id=connection_id,
model_id=model,
reasoning_effort=reasoning_effort,
context_window=context_window,
),
)
thread = self.application.threads.set_execution_selection(
thread_id,
connection_id=connection_id,
model=model,
reasoning_effort=reasoning_effort,
context_window=context_window,
)
return {"thread": thread_view(thread)}

Expand Down
31 changes: 31 additions & 0 deletions cli/execution_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
from __future__ import annotations

import argparse
import re

from core.domain.execution_profile import (
MAX_CONTEXT_WINDOW_TOKENS,
MIN_CONTEXT_WINDOW_TOKENS,
)
from core.domain.execution_security import ExecutionAccessPreset

_CONTEXT_WINDOW = re.compile(r"^(\d+(?:\.\d+)?)\s*([km]?)$", re.IGNORECASE)


def add_reasoning_effort_argument(parser: argparse.ArgumentParser) -> None:
"""Add the model-aware reasoning override used by every CLI surface.
Expand All @@ -27,6 +34,29 @@ def add_reasoning_effort_argument(parser: argparse.ArgumentParser) -> None:
)


def parse_context_window(value: str) -> int | None:
"""Parse a human context cap (``32k``, ``1m``), or ``auto`` to inherit."""

clean = value.strip().lower()
if clean in {"auto", "default", "inherit"}:
return None
match = _CONTEXT_WINDOW.fullmatch(clean)
if match is None:
raise ValueError("context window must be auto or a token count such as 32k")
amount = float(match.group(1))
multiplier = {"": 1, "k": 1_000, "m": 1_000_000}[match.group(2).lower()]
tokens = amount * multiplier
if not tokens.is_integer():
raise ValueError("context window must resolve to a whole token count")
parsed = int(tokens)
if not MIN_CONTEXT_WINDOW_TOKENS <= parsed <= MAX_CONTEXT_WINDOW_TOKENS:
raise ValueError(
"context window must be between "
f"{MIN_CONTEXT_WINDOW_TOKENS} and {MAX_CONTEXT_WINDOW_TOKENS} tokens"
)
return parsed


def add_access_preset_argument(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--access",
Expand Down Expand Up @@ -61,4 +91,5 @@ def parse_access_preset(value: str | None) -> ExecutionAccessPreset | None:
"add_reasoning_effort_argument",
"add_workspace_trust_argument",
"parse_access_preset",
"parse_context_window",
]
24 changes: 23 additions & 1 deletion cli/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def __init__(
if reasoning_effort is not None
else None
)
self._requested_context_window: int | None = None
self.selected_skill_ids: list[str] = []
self._session_activity: FileLease | None = None
self._leased_session_id: str | None = None
Expand Down Expand Up @@ -132,6 +133,7 @@ def _sync_thread_state(self) -> None:
self._requested_connection = profile.connection_id
self._requested_model = profile.model_id
self._requested_reasoning_effort = thread.reasoning_effort
self._requested_context_window = thread.context_window
self.bridge = SessionBridge(
store=self.thread_client.store,
session_id=thread.id,
Expand Down Expand Up @@ -236,6 +238,7 @@ async def switch_model(
connection_id=connection_id,
model=model,
reasoning_effort=effort,
context_window=self._requested_context_window,
)
self.model = profile.model_id
self._requested_connection = profile.connection_id
Expand Down Expand Up @@ -271,10 +274,23 @@ async def switch_reasoning_effort(self, effort: str) -> None:
connection_id=self.thread_client.execution_profile.connection_id,
model=self.thread_client.execution_profile.model_id,
reasoning_effort=requested,
context_window=self._requested_context_window,
)
self.model = profile.model_id
self._requested_reasoning_effort = requested

async def switch_context_window(self, context_window: int | None) -> None:
"""Change the context cap for future Turns in this Session."""

profile = self.thread_client.switch_execution(
connection_id=self.thread_client.execution_profile.connection_id,
model=self.thread_client.execution_profile.model_id,
reasoning_effort=self._requested_reasoning_effort,
context_window=context_window,
)
self.model = profile.model_id
self._requested_context_window = context_window

def connection_views(self) -> list[dict]:
data = self.thread_client.application.llm.list_connections(
self.thread_client.project.id
Expand All @@ -290,7 +306,8 @@ def model_overview(self) -> str:
profile = self.thread_client.execution_profile
current = (
f"connection: {profile.connection_id} · model: {self.model} · "
f"effort: {self.requested_reasoning_effort}"
f"effort: {self.requested_reasoning_effort} · "
f"context: {self.requested_context_window}"
)
views = [
view
Expand Down Expand Up @@ -415,6 +432,11 @@ def requested_reasoning_effort(self) -> str:

return self._requested_reasoning_effort or "auto"

@property
def requested_context_window(self) -> str:
value = self._requested_context_window
return f"{value} tokens" if value is not None else "auto"

def clear_conversation(self) -> None:
self.goal_controller.close()
self.thread_client.clear_context()
Expand Down
36 changes: 36 additions & 0 deletions cli/tui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import Any

from cli.transcript import TranscriptMode
from cli.execution_options import parse_context_window
from cli.tui import theme
from cli.tui.picker import Picker, PickerItem, PickerScope, PickerVariant
from cli.tui.text import fit_head, short_path
Expand Down Expand Up @@ -132,6 +133,14 @@ def _effort_levels(app, prefix: str) -> list[str]:
return [level for level in levels if level.startswith(prefix)]


def _context_windows(app, prefix: str) -> list[str]:
return [
value
for value in ("auto", "32k", "64k", "128k", "256k", "512k", "1m")
if value.startswith(prefix.lower())
]


def _resolve_session_prefix(app, target: str) -> str | list[str]:
"""Resolve an id prefix to a full session id.

Expand Down Expand Up @@ -444,6 +453,26 @@ async def _cmd_effort(app, args: str) -> str | None:
)


async def _cmd_context(app, args: str) -> str | None:
wanted = args.strip()
profile = app.thread_client.execution_profile
if not wanted:
return (
f"context cap: {app.requested_context_window} · "
f"effective: {profile.context_window} tokens"
)
try:
requested = parse_context_window(wanted)
await app.switch_context_window(requested)
except (OSError, RuntimeError, ValueError) as exc:
return f"context switch failed: {exc}"
profile = app.thread_client.execution_profile
return (
f"context cap switched to {app.requested_context_window} "
f"(effective: {profile.context_window} tokens; history preserved)"
)


_PERMISSION_CHOICES: dict[str, str | None] = {
"ask": "ask",
"read-only": "read_only",
Expand Down Expand Up @@ -765,6 +794,13 @@ async def _cmd_exit(app, args: str) -> str | None:
_cmd_effort,
arguments=_effort_levels,
),
Command(
"context",
"/context [auto|tokens]",
"show or set this Session's context-window cap",
_cmd_context,
arguments=_context_windows,
),
Command(
"permissions",
"/permissions [preset]",
Expand Down
Loading
Loading