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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.9.17] - 2026-07-31

### Changed

- 🤖 **Agent chats remember more when they start.** Codex and Claude Code now get helpful recent context at the beginning of a session, so follow-up requests are less likely to feel disconnected.
- 💬 **Open WebUI conversations carry over more naturally.** When a chat starts through Open WebUI, Computer now brings along earlier messages before answering the latest one.

### Fixed

- 🔔 **Task notifications are clearer when Computer needs you.** Notifications now say when a task is waiting for approval or an answer, with wording in every supported language.

## [0.9.16] - 2026-07-31

### Added
Expand Down
3 changes: 3 additions & 0 deletions cptr/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ def _env_int(name: str, default: int) -> int:
CHAT_TOOL_MAX_CHARS = int(os.environ.get("CHAT_TOOL_MAX_CHARS", "50000"))
CHAT_TOOL_COMMAND_MAX_CHARS = int(os.environ.get("CHAT_TOOL_COMMAND_MAX_CHARS", "8000"))
CHAT_COMPACT_TOKEN_THRESHOLD = int(os.environ.get("CHAT_COMPACT_TOKEN_THRESHOLD", "80000"))
AGENT_SEED_TRANSCRIPT_MAX_CHARS = max(
0, _env_int("CHAT_AGENT_SEED_TRANSCRIPT_MAX_CHARS", 12000)
)
# Claude SDK stdout JSON buffer; chat/tool output caps apply later after parsing.
CLAUDE_CODE_MAX_BUFFER_SIZE = _env_int("CPTR_CLAUDE_CODE_MAX_BUFFER_SIZE", 128 * 1024 * 1024)

Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "Antworten vorlesen",
"chat.stopSpeaking": "Vorlesen stoppen",
"chat.fallbackTitle": "Unterhaltung",
"chat.notification.approvalTitle": "Genehmigung erforderlich: {{title}}",
"chat.notification.inputTitle": "Eingabe erforderlich: {{title}}",
"chat.notification.approvalBody": "Erlaube oder verweigere {{tool}}, um fortzufahren.",
"chat.notification.inputBody": "Beantworte die Eingabeaufforderung, um fortzufahren.",
"chat.notification.finishedBody": "Aufgabe abgeschlossen.",
"chat.notification.thisTool": "dieses Tool",
"chat.greeting": "Wie kann ich Ihnen helfen?",
"admin.toolServers": "Tool-Server",
"toolServers.title": "Tool-Server",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,12 @@
"chat.speakResponses": "Speak responses",
"chat.stopSpeaking": "Stop speaking",
"chat.fallbackTitle": "Chat",
"chat.notification.approvalTitle": "Approval needed: {{title}}",
"chat.notification.inputTitle": "Input needed: {{title}}",
"chat.notification.approvalBody": "Allow or deny {{tool}} to continue.",
"chat.notification.inputBody": "Answer the prompt to continue.",
"chat.notification.finishedBody": "Task finished.",
"chat.notification.thisTool": "this tool",
"chat.greeting": "What can I help you with?",
"admin.toolServers": "Tool Servers",
"toolServers.title": "Tool Servers",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "Leer respuestas",
"chat.stopSpeaking": "Detener lectura",
"chat.fallbackTitle": "Conversación",
"chat.notification.approvalTitle": "Aprobación necesaria: {{title}}",
"chat.notification.inputTitle": "Entrada necesaria: {{title}}",
"chat.notification.approvalBody": "Permite o deniega {{tool}} para continuar.",
"chat.notification.inputBody": "Responde la solicitud para continuar.",
"chat.notification.finishedBody": "Tarea finalizada.",
"chat.notification.thisTool": "esta herramienta",
"chat.greeting": "¿En qué puedo ayudarte?",
"admin.toolServers": "Servidores de herramientas",
"toolServers.title": "Servidores de herramientas",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "Lire les réponses",
"chat.stopSpeaking": "Arrêter la lecture",
"chat.fallbackTitle": "Discussion",
"chat.notification.approvalTitle": "Approbation requise : {{title}}",
"chat.notification.inputTitle": "Saisie requise : {{title}}",
"chat.notification.approvalBody": "Autorisez ou refusez {{tool}} pour continuer.",
"chat.notification.inputBody": "Répondez à la demande pour continuer.",
"chat.notification.finishedBody": "Tâche terminée.",
"chat.notification.thisTool": "cet outil",
"chat.greeting": "Comment puis-je vous aider ?",
"admin.toolServers": "Serveurs d'outils",
"toolServers.title": "Serveurs d'outils",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "応答を読み上げる",
"chat.stopSpeaking": "読み上げを停止",
"chat.fallbackTitle": "チャット",
"chat.notification.approvalTitle": "承認が必要: {{title}}",
"chat.notification.inputTitle": "入力が必要: {{title}}",
"chat.notification.approvalBody": "続行するには {{tool}} を許可または拒否してください。",
"chat.notification.inputBody": "続行するにはプロンプトに回答してください。",
"chat.notification.finishedBody": "タスクが完了しました。",
"chat.notification.thisTool": "このツール",
"chat.greeting": "何かお手伝いできますか?",
"admin.toolServers": "ツールサーバー",
"toolServers.title": "ツールサーバー",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "응답 읽기",
"chat.stopSpeaking": "읽기 중지",
"chat.fallbackTitle": "채팅",
"chat.notification.approvalTitle": "승인 필요: {{title}}",
"chat.notification.inputTitle": "입력 필요: {{title}}",
"chat.notification.approvalBody": "계속하려면 {{tool}}을(를) 허용하거나 거부하세요.",
"chat.notification.inputBody": "계속하려면 프롬프트에 답하세요.",
"chat.notification.finishedBody": "작업이 완료되었습니다.",
"chat.notification.thisTool": "이 도구",
"chat.greeting": "무엇을 도와드릴까요?",
"admin.toolServers": "도구 서버",
"toolServers.title": "도구 서버",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "Ler respostas",
"chat.stopSpeaking": "Parar leitura",
"chat.fallbackTitle": "Conversa",
"chat.notification.approvalTitle": "Aprovação necessária: {{title}}",
"chat.notification.inputTitle": "Entrada necessária: {{title}}",
"chat.notification.approvalBody": "Permita ou negue {{tool}} para continuar.",
"chat.notification.inputBody": "Responda à solicitação para continuar.",
"chat.notification.finishedBody": "Tarefa concluída.",
"chat.notification.thisTool": "esta ferramenta",
"chat.greeting": "Como posso ajudar?",
"admin.toolServers": "Servidores de ferramentas",
"toolServers.title": "Servidores de ferramentas",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "Озвучивать ответы",
"chat.stopSpeaking": "Остановить озвучивание",
"chat.fallbackTitle": "Чат",
"chat.notification.approvalTitle": "Требуется подтверждение: {{title}}",
"chat.notification.inputTitle": "Требуется ввод: {{title}}",
"chat.notification.approvalBody": "Разрешите или запретите {{tool}}, чтобы продолжить.",
"chat.notification.inputBody": "Ответьте на запрос, чтобы продолжить.",
"chat.notification.finishedBody": "Задача завершена.",
"chat.notification.thisTool": "этот инструмент",
"chat.greeting": "Чем могу помочь?",
"admin.toolServers": "Серверы инструментов",
"toolServers.title": "Серверы инструментов",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "朗读响应",
"chat.stopSpeaking": "停止朗读",
"chat.fallbackTitle": "聊天",
"chat.notification.approvalTitle": "需要批准:{{title}}",
"chat.notification.inputTitle": "需要输入:{{title}}",
"chat.notification.approvalBody": "允许或拒绝 {{tool}} 以继续。",
"chat.notification.inputBody": "回答提示以继续。",
"chat.notification.finishedBody": "任务已完成。",
"chat.notification.thisTool": "此工具",
"chat.greeting": "有什么可以帮您的?",
"admin.toolServers": "工具服务器",
"toolServers.title": "工具服务器",
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,12 @@
"chat.speakResponses": "朗讀回應",
"chat.stopSpeaking": "停止朗讀",
"chat.fallbackTitle": "聊天",
"chat.notification.approvalTitle": "需要批准:{{title}}",
"chat.notification.inputTitle": "需要輸入:{{title}}",
"chat.notification.approvalBody": "允許或拒絕 {{tool}} 以繼續。",
"chat.notification.inputBody": "回答提示以繼續。",
"chat.notification.finishedBody": "任務已完成。",
"chat.notification.thisTool": "此工具",
"chat.greeting": "有什麼可以幫您的?",
"admin.toolServers": "工具伺服器",
"toolServers.title": "工具伺服器",
Expand Down
23 changes: 21 additions & 2 deletions cptr/frontend/src/lib/stores/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fetchJSON } from '$lib/apis';
import { socketStore } from '$lib/stores/socket.svelte';
import { activeTab } from '$lib/stores';
import { getPathDisplayName, isSupportedWorkspacePath } from '$lib/utils/paths';
import { i18next } from '$lib/i18n';

export const chatEnabled = writable<boolean>(false);

Expand Down Expand Up @@ -139,9 +140,11 @@ export function bindGlobalChatListener() {
(data: {
type?: string;
chat_id: string;
status?: string;
done?: boolean;
title?: string;
content?: string;
tool_name?: string;
workspace?: string;
workspace_name?: string;
active?: boolean;
Expand Down Expand Up @@ -187,8 +190,24 @@ export function bindGlobalChatListener() {
const workspacePath = isSupportedWorkspacePath(data.workspace) ? data.workspace : '';
const workspaceDisplayName = data.workspace_name || getPathDisplayName(workspacePath);
const workspaceTitlePrefix = workspaceDisplayName ? `[${workspaceDisplayName}] ` : '';
const title = `${workspaceTitlePrefix}${data.title || 'Chat'}`;
const body = data.content || '';
const chatTitle = data.title || i18next.t('chat.fallbackTitle');
const approvalRequired = data.status === 'approval_required';
const inputRequired = data.status === 'input_required';
const toolName = data.tool_name?.replace(/_/g, ' ');
const title = approvalRequired
? `${workspaceTitlePrefix}${i18next.t('chat.notification.approvalTitle', { title: chatTitle })}`
: inputRequired
? `${workspaceTitlePrefix}${i18next.t('chat.notification.inputTitle', { title: chatTitle })}`
: `${workspaceTitlePrefix}${chatTitle}`;
const body =
data.content ||
(approvalRequired
? i18next.t('chat.notification.approvalBody', {
tool: toolName || i18next.t('chat.notification.thisTool')
})
: inputRequired
? i18next.t('chat.notification.inputBody')
: i18next.t('chat.notification.finishedBody'));
const openChat = async () => {
const { goto } = await import('$app/navigation');
const workspaceQuery = workspacePath
Expand Down
33 changes: 32 additions & 1 deletion cptr/routers/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

from cptr.models import Chat, ChatMessage, Config
from cptr.models.workspaces import Workspace
from cptr.utils.agents.prompts import message_text
from cptr.utils.config import now_ms

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -281,11 +282,41 @@ async def _resolve_messages(
(m for m in reversed(messages) if m.get("role") == "user"),
None,
)
user_content = (last_user.get("content", "") if last_user else "") or ""
user_content = message_text(last_user).strip() if last_user else ""

# Determine parent for the user message
cptr_parent_id = chat.current_message_id if chat else None

# First time this OWUI chat reaches cptr, copy the request's existing
# transcript into the parent chain so the native agent can cold-start with it.
if chat and not chat.current_message_id and messages:
last_user_index = None
for index in range(len(messages) - 1, -1, -1):
if messages[index].get("role") == "user":
last_user_index = index
break
if last_user_index:
created_at = now_ms()
for offset, message in enumerate(messages[:last_user_index]):
role = message.get("role")
if role not in ("user", "assistant"):
continue
content = message_text(message).strip()
if not content:
continue
imported = await ChatMessage.create(
chat_id=chat_id,
role=role,
content=content,
parent_id=cptr_parent_id,
done=True,
created_at=created_at + offset,
)
cptr_parent_id = imported.id
owui_id = str(message.get("id") or "").strip()
if owui_id:
msg_map[owui_id] = imported.id

if owui_user_id and owui_user_id in msg_map:
# User message already exists in cptr (regeneration case)
existing_user_msg = await ChatMessage.get_by_id(msg_map[owui_user_id])
Expand Down
4 changes: 2 additions & 2 deletions cptr/utils/agents/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
AgentTextDelta,
AgentToolUpdate,
)
from cptr.utils.agents.prompts import latest_user_text
from cptr.utils.agents.prompts import session_turn_prompt_text


_claude_clients: dict[str, tuple[Any, tuple[Any, ...]]] = {}
Expand Down Expand Up @@ -125,7 +125,6 @@ async def run_claude_code_agent(
yield AgentError("Claude Code support requires the claude-agent-sdk Python package")
return

prompt = latest_user_text(messages)
env = os.environ.copy()
home = os.path.expanduser(str(profile["home"])) if profile.get("home") else None
if home:
Expand All @@ -143,6 +142,7 @@ async def run_claude_code_agent(
if resume_state:
value = resume_state.get("session_id")
session_id = value if isinstance(value, str) and value else None
prompt = session_turn_prompt_text(messages, resumed=bool(session_id))

options_kwargs: dict[str, Any] = {
"system_prompt": system_prompt or None,
Expand Down
4 changes: 2 additions & 2 deletions cptr/utils/agents/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
AgentToolOutputDelta,
AgentToolUpdate,
)
from cptr.utils.agents.prompts import latest_user_text
from cptr.utils.agents.prompts import session_turn_prompt_text

CODEX_STDOUT_CHUNK_SIZE = 64 * 1024
CODEX_MAX_WIRE_MESSAGE_CHARS = 16 * 1024 * 1024
Expand Down Expand Up @@ -375,7 +375,7 @@ async def run_codex_agent(
yield AgentError("Codex did not return a thread id")
return

prompt = latest_user_text(messages)
prompt = session_turn_prompt_text(messages, resumed=bool(_resume_thread_id(resume_state)))
turn_input: list[dict[str, Any]] = []
if prompt:
turn_input.append({"type": "text", "text": prompt})
Expand Down
61 changes: 54 additions & 7 deletions cptr/utils/agents/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from typing import Any

from cptr.env import AGENT_SEED_TRANSCRIPT_MAX_CHARS


def message_text(message: dict[str, Any]) -> str:
content = message.get("content", "")
Expand All @@ -14,17 +16,62 @@ def message_text(message: dict[str, Any]) -> str:
return str(content or "")


def latest_user_text(messages: list[dict[str, Any]]) -> str:
"""Return only the current user turn for native-session agents."""
def session_turn_prompt_text(messages: list[dict[str, Any]], resumed: bool) -> str:
"""Return the prompt for a native-session agent turn."""

last_user_index = None
for index in range(len(messages) - 1, -1, -1):
if messages[index].get("role") == "user":
last_user_index = index
break
if last_user_index is None:
return ""

prompt = message_text(messages[last_user_index]).strip()
if resumed or AGENT_SEED_TRANSCRIPT_MAX_CHARS <= 0:
return prompt

parts = []
for message in messages[:last_user_index]:
role = message.get("role")
if role not in ("user", "assistant"):
continue
text = message_text(message).strip()
if text:
parts.append(f"<{role}>\n{text}\n</{role}>")

for message in reversed(messages):
if message.get("role") == "user":
return message_text(message).strip()
return ""
selected = []
total = 0
truncated = False
for part in reversed(parts):
if len(part) > AGENT_SEED_TRANSCRIPT_MAX_CHARS:
selected.append(
"[Earlier conversation truncated.]\n"
+ part[-AGENT_SEED_TRANSCRIPT_MAX_CHARS:]
)
truncated = True
break
next_total = total + len(part) + (2 if selected else 0)
if selected and next_total > AGENT_SEED_TRANSCRIPT_MAX_CHARS:
truncated = True
break
selected.append(part)
total = next_total
if truncated and not selected[-1].startswith("[Earlier conversation truncated.]"):
selected.append("[Earlier conversation truncated.]")
transcript = "\n\n".join(reversed(selected))
if not transcript:
return prompt
return (
"The following is prior conversation context from before this native agent "
"session began. Use it for context, then answer only the latest user request.\n\n"
f"<prior_conversation>\n{transcript}\n</prior_conversation>\n\n"
f"<latest_user_request>\n{prompt}\n</latest_user_request>"
)


def turn_prompt_text(messages: list[dict[str, Any]], system_prompt: str, resumed: bool) -> str:
prompt = latest_user_text(messages)
prompt = session_turn_prompt_text(messages, resumed)
if system_prompt and not resumed:
return f"{system_prompt}\n\n{prompt}" if prompt else system_prompt
return prompt
Loading
Loading