diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae46b8..1fb69cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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.8.4] - 2026-07-07 + +### Fixed + +- 💬 **Slash commands at the start.** New chats no longer block slash commands before the first regular message. +- 📱 **Better mobile typing.** Pressing Enter on phones and tablets now behaves like normal typing instead of choosing a suggestion or sending too soon. +- 🤖 **Larger Claude Code replies.** Claude Code can now handle bigger answers before chat display limits are applied. + ## [0.8.3] - 2026-07-07 ### Fixed diff --git a/README.md b/README.md index 48d2294..b1d43ee 100644 --- a/README.md +++ b/README.md @@ -214,4 +214,4 @@ This is safe when you are the only user and you control the network. It is not s ## License -Open Use License. Source available. All rights reserved. See [LICENSE](LICENSE). [Enterprise licenses available](mailto:sales@openwebui.com). +Open Use License. Source available. All rights reserved. See [LICENSE](LICENSE). [Commercial licenses](https://openwebui.com/computer/license) and [enterprise licenses](mailto:sales@openwebui.com) available. diff --git a/cptr/env.py b/cptr/env.py index b71f82b..fd9f47a 100644 --- a/cptr/env.py +++ b/cptr/env.py @@ -64,6 +64,8 @@ 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")) +# 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) # ── Execute timeout ───────────────────────────────────────── # Default wait (seconds) for run_command / check_task when the caller diff --git a/cptr/frontend/src/lib/components/chat/ChatInput.svelte b/cptr/frontend/src/lib/components/chat/ChatInput.svelte index bb3bc9f..d4e8d32 100644 --- a/cptr/frontend/src/lib/components/chat/ChatInput.svelte +++ b/cptr/frontend/src/lib/components/chat/ChatInput.svelte @@ -132,6 +132,13 @@ : $t('chat.voiceModeOn') ); + function isMobileInput(): boolean { + return ( + typeof window !== 'undefined' && + window.matchMedia('(hover: none) and (pointer: coarse)').matches + ); + } + // ── Lowlight setup ────────────────────────────── const lowlight = createLowlight(all); const _origHighlight = lowlight.highlight.bind(lowlight); @@ -313,6 +320,7 @@ return true; } if (event.key === 'Enter') { + if (isMobileInput()) return false; const item = currentItems[selectedIndex]; if (item && command) command(item); return true; @@ -477,6 +485,7 @@ return true; } if (event.key === 'Enter') { + if (isMobileInput()) return false; const item = currentItems[selectedIndex]; if (item && command) command(item); return true; @@ -554,6 +563,8 @@ spellcheck: 'true' }, handleKeyDown: (view, event) => { + if (event.key === 'Enter' && isMobileInput()) return false; + if (showSlashCommands) { if (event.key === 'ArrowDown') { event.preventDefault(); @@ -939,7 +950,7 @@ } }); - const slashCommandQuery = $derived(inputText.trim().toLowerCase()); + const slashCommandQuery = $derived(inputText.trimStart().toLowerCase()); const slashCommandIds = $derived.by(() => { if (!slashCommandQuery.startsWith('/')) return []; const ids: string[] = []; @@ -979,7 +990,7 @@ $effect(() => { const query = slashCommandQuery; const requestId = ++slashSkillsRequestId; - if (!query.startsWith('/') || !workspace) { + if (!query.startsWith('/') || /\s/.test(query.slice(1)) || !workspace) { slashSkillSuggestions = []; return; } diff --git a/cptr/frontend/src/lib/components/chat/ChatPanel.svelte b/cptr/frontend/src/lib/components/chat/ChatPanel.svelte index 68e4a10..1d1d4a0 100644 --- a/cptr/frontend/src/lib/components/chat/ChatPanel.svelte +++ b/cptr/frontend/src/lib/components/chat/ChatPanel.svelte @@ -299,9 +299,7 @@ const streaming = $derived(allMessages.some((m) => m.role === 'assistant' && !m.done)); const isLanding = $derived(allMessages.length === 0 && !chatId); const hasChatContent = $derived( - activePath.some( - ({ msg }) => msg.role === 'user' && msg.content.trim() && !msg.content.trim().startsWith('/') - ) + activePath.some(({ msg }) => msg.role === 'user' && msg.content.trim()) ); const workspaceDisplayName = $derived(getPathDisplayName(workspace, 'workspace')); const displayChatTitle = $derived(chatTitle || firstUserMessageTitle() || workspaceDisplayName); @@ -722,10 +720,6 @@ let text = inputText.trim(); if (!text || !selectedModel) return; if (sending) return; - if (!hasChatContent && text.startsWith('/') && text !== '/plan') { - toast.error('Only /plan is available before this chat has content'); - return; - } if (hasChatContent && text === '/compact') { await handleManualCompact(); return; diff --git a/cptr/utils/agents/claude_code.py b/cptr/utils/agents/claude_code.py index ecc8792..e0e4a18 100644 --- a/cptr/utils/agents/claude_code.py +++ b/cptr/utils/agents/claude_code.py @@ -7,6 +7,7 @@ import shlex from typing import Any, AsyncIterator +from cptr.env import CLAUDE_CODE_MAX_BUFFER_SIZE from cptr.utils.agents.attachments import PreparedAgentAttachments from cptr.utils.agents.events import ( AgentDone, @@ -128,6 +129,7 @@ async def run_claude_code_agent( "permission_mode": permission_mode, "env": env, "include_partial_messages": True, + "max_buffer_size": CLAUDE_CODE_MAX_BUFFER_SIZE, } if workspace: options_kwargs["cwd"] = workspace diff --git a/cptr/utils/chat_task.py b/cptr/utils/chat_task.py index ad19611..9d596ce 100644 --- a/cptr/utils/chat_task.py +++ b/cptr/utils/chat_task.py @@ -172,8 +172,6 @@ def _build_skill_create_prompt(user_request: str) -> str: def _message_has_real_content(message: dict) -> bool: text = _plain_message_text(message.get("content")).strip() - if message.get("role") == "user" and text.startswith("/"): - return False return bool( text or message.get("tool_calls") or message.get("reasoning_items") or message.get("output") ) diff --git a/pyproject.toml b/pyproject.toml index 1286aad..68acd71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cptr" -version = "0.8.3" +version = "0.8.4" description = "Your computer, from anywhere. Code, manage, and control your machine from the web." license = {file = "LICENSE"} readme = "README.md" diff --git a/uv.lock b/uv.lock index 541b6f8..865902d 100644 --- a/uv.lock +++ b/uv.lock @@ -284,7 +284,7 @@ wheels = [ [[package]] name = "cptr" -version = "0.8.3" +version = "0.8.4" source = { editable = "." } dependencies = [ { name = "aiosqlite" },