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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ 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.16] - 2026-07-31

### Added

- 🧰 **Tool servers can now carry their own environment settings.** Add extra environment values for local tool servers from Admin, with matching translations across supported languages.
- 🌐 **OpenCode over Docker is easier to set up.** The README now shows how to connect a host-running OpenCode server from a Docker install.

### Changed

- 🤖 **Claude Code conversations continue more smoothly.** Computer keeps Claude Code sessions warm between turns when the setup is unchanged, so follow-up prompts feel more natural.
- 🧭 **Agent model choices are less fussy.** Saved agent models and newly detected models are combined, so custom setups are less likely to disappear from the picker.
- 💬 **OpenAI-style chat requests return a full answer unless streaming is requested.** This better matches clients that expect a single response by default.

### Fixed

- 🔐 **Trusted-header sign-in is more dependable.** Computer now recognizes the configured header during session checks and keeps signed-in users attached to their account.
- 🧹 **Deleted agent profiles stay deleted after saving.** Removing a saved agent no longer gets skipped just because no new profiles were added.
- 🏠 **Workspace names are preserved when workspace details update.** Updating tabs or layout no longer quietly falls back to the folder name.
- 📝 **Lists appear correctly while writing.** Bulleted and numbered lists now show their markers in chat and rich text editors.
- 🗂️ **Git paths are easier to read.** File names with non-English characters now appear as themselves instead of escaped text.
- 🧩 **Quoted tool-server commands work more reliably.** Local tool servers can start even when the saved command was wrapped in quotes.
- 📦 **Optional tool-server support stays on the supported version range.** Fresh installs avoid incompatible future releases.

## [0.9.15] - 2026-07-23

### Fixed
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ The default image includes Git and GitHub CLI (`gh`). Use `ghcr.io/open-webui/co

If you bind-mount a host directory to `/data`, make sure that directory is writable by the container user. SQLite needs to create and update `/data/app.db`, and host directory permissions take precedence over the image's built-in `/data` ownership.

To connect a host-running OpenCode server from Docker, start OpenCode on a host-reachable interface and set the OpenCode agent Server URL to the Docker host address:

```bash
opencode serve --hostname 0.0.0.0 --port 4096
```

Use `http://host.docker.internal:4096` on Docker Desktop. On Linux, add `--add-host=host.docker.internal:host-gateway` to `docker run` if that hostname is not already available.

The `:dev` image is also available and tracks the `main` branch.

## Air-gapped installation
Expand Down
2 changes: 1 addition & 1 deletion cptr/frontend/src/lib/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function fetchJSON<T = unknown>(path: string, init?: RequestInit):
const res = await fetchHandler(path, init);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new ApiError(res.status, data.detail || data.error || res.statusText);
throw new ApiError(res.status, data.detail || data.error || data.message || res.statusText);
}
return res.json();
}
Expand Down
9 changes: 7 additions & 2 deletions cptr/frontend/src/lib/components/Admin/Agents.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
let available = $state<Record<string, boolean>>({});
let saved = $state<Record<string, boolean>>({});
let modal = $state<ModalState | null>(null);
let deletedProfile = $state(false);

const statusLabelKey: Record<string, string> = {
ready: 'admin.agentsStatusReady',
Expand Down Expand Up @@ -62,6 +63,7 @@
detected = Object.fromEntries(data.profiles.map((entry) => [entry.id, entry.detected]));
available = Object.fromEntries(data.profiles.map((entry) => [entry.id, entry.available]));
saved = Object.fromEntries(data.profiles.map((entry) => [entry.id, !entry.implicit]));
deletedProfile = false;
}

function newProfile(agent: AgentProfile['agent'] = 'codex'): AgentProfile {
Expand Down Expand Up @@ -176,6 +178,7 @@
if (profile) {
const { [profile.id]: _removed, ...rest } = saved;
saved = rest;
deletedProfile = true;
}
modal = null;
}
Expand All @@ -189,8 +192,10 @@
}

async function save() {
const savedProfiles = profiles.filter((profile) => saved[profile.id]);
if (savedProfiles.length === 0) {
const savedProfiles = deletedProfile
? profiles
: profiles.filter((profile) => saved[profile.id]);
if (savedProfiles.length === 0 && !deletedProfile) {
toast.success($t('settings.saved'));
return;
}
Expand Down
76 changes: 73 additions & 3 deletions cptr/frontend/src/lib/components/Admin/ToolServers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,54 @@
let formCommand = $state('');
let formArgs = $state('');
let formCwd = $state('');
let formEnv = $state('');

let saving = $state(false);
let verifying = $state(false);

// Verify result
let verifyResult = $state<{ ok: boolean; tools?: any[]; message?: string } | null>(null);

function splitArgs(value: string): string[] {
const args: string[] = [];
let current = '';
let quote: string | null = null;
let started = false;

const input = value.trim();
for (let i = 0; i < input.length; i += 1) {
const ch = input[i];
const next = input[i + 1];
if (quote) {
started = true;
if (ch === quote) quote = null;
else if (ch === '\\' && (next === quote || next === '\\')) current += input[++i];
else current += ch;
} else if (ch === '"' || ch === "'") {
started = true;
quote = ch;
} else if (/\s/.test(ch)) {
if (started) {
args.push(current);
current = '';
started = false;
}
} else {
started = true;
current += ch;
}
}

if (started) args.push(current);
return args;
}

function joinArgs(args: string[]): string {
return args
.map((arg) => (!arg || /\s|["']/.test(arg) ? `"${arg.replace(/(["\\])/g, '\\$1')}"` : arg))
.join(' ');
}

async function load() {
try {
servers = await listToolServers();
Expand All @@ -67,6 +108,7 @@
formCommand = '';
formArgs = '';
formCwd = '';
formEnv = '';

verifyResult = null;
showModal = true;
Expand All @@ -84,8 +126,9 @@
formDescription = s.description;
formHeaders = s.headers ? JSON.stringify(s.headers, null, 2) : '';
formCommand = s.command || '';
formArgs = (s.args || []).join(' ');
formArgs = joinArgs(s.args || []);
formCwd = s.cwd || '';
formEnv = s.env ? JSON.stringify(s.env, null, 2) : '';

verifyResult = null;
showModal = true;
Expand Down Expand Up @@ -119,6 +162,17 @@
return;
}
}
let parsedEnv: Record<string, string> | null = null;
if (isStdio && formEnv.trim()) {
try {
const v = JSON.parse(formEnv);
if (typeof v !== 'object' || v === null || Array.isArray(v)) throw 0;
parsedEnv = v;
} catch {
toast.error($t('toolServers.envInvalid'));
return;
}
}
saving = true;
try {
const data: Record<string, unknown> = {
Expand All @@ -140,8 +194,9 @@
description: formDescription.trim(),
headers: parsedHeaders || null,
command: formCommand.trim(),
args: formArgs.trim() ? formArgs.trim().split(/\s+/) : [],
cwd: formCwd.trim() || null
args: splitArgs(formArgs),
cwd: formCwd.trim() || null,
env: parsedEnv
};
if (editServer) {
if (formKey.trim()) data.key = formKey.trim();
Expand Down Expand Up @@ -360,6 +415,21 @@
spellcheck="false"
class="block w-full bg-transparent text-[0.8125rem] text-gray-700 dark:text-gray-300 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none py-0.5 font-mono"
/>
<label for="tool-server-env" class="text-[0.625rem] text-gray-400 dark:text-gray-600 mt-2"
>{$t('toolServers.env')}</label
>
<p class="text-[0.625rem] text-gray-300 dark:text-gray-700 mb-0.5">
{$t('toolServers.envHint')}
</p>
<textarea
id="tool-server-env"
placeholder={'{"MAIL_TOKEN": "xxxx"}'}
bind:value={formEnv}
autocomplete="off"
spellcheck="false"
rows="2"
class="block w-full bg-transparent text-[0.8125rem] text-gray-700 dark:text-gray-300 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none py-0.5 font-mono resize-none"
></textarea>
{/if}

<!-- Spec path (OpenAPI only) -->
Expand Down
6 changes: 6 additions & 0 deletions cptr/frontend/src/lib/components/chat/ChatInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -1607,6 +1607,12 @@
.chat-editor-mount :global(.chat-prosemirror ol) {
@apply mb-1 pl-4.5 text-sm;
}
.chat-editor-mount :global(.chat-prosemirror ul) {
list-style-type: disc;
}
.chat-editor-mount :global(.chat-prosemirror ol) {
list-style-type: decimal;
}
.chat-editor-mount :global(.chat-prosemirror li) {
@apply my-0.5;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@
padding-left: 1.25rem;
}

.rte-container :global(.rte-prosemirror ul) {
list-style-type: disc;
}

.rte-container :global(.rte-prosemirror ol) {
list-style-type: decimal;
}

.rte-container :global(.rte-prosemirror li) {
margin: 0.25rem 0;
}
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "Argumente",
"toolServers.cwd": "Arbeitsverzeichnis",
"toolServers.cwdPlaceholder": "Optional (standardmäßig System)",
"toolServers.env": "Umgebung",
"toolServers.envHint": "Zusätzliche Umgebungsvariablen als JSON-Objekt. Werden mit dem geerbten PATH beim Start des Prozesses zusammengeführt.",
"toolServers.envInvalid": "Umgebung muss ein gültiges JSON-Objekt sein",
"toolServers.commandRequired": "Befehl ist für stdio-Server erforderlich",
"toolServers.connectionFailed": "Verbindung fehlgeschlagen",
"toolServers.apiKeyKeep": "•••••••• (leer lassen zum Beibehalten)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,9 @@
"toolServers.args": "Arguments",
"toolServers.cwd": "Working Directory",
"toolServers.cwdPlaceholder": "Optional (defaults to system)",
"toolServers.env": "Environment",
"toolServers.envHint": "Extra environment variables as JSON object. Merged with the inherited PATH when spawning the process.",
"toolServers.envInvalid": "Environment must be a valid JSON object",
"toolServers.commandRequired": "Command is required for stdio servers",
"toolServers.connectionFailed": "Connection failed",
"toolServers.apiKeyKeep": "•••••••• (leave blank to keep)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "Argumentos",
"toolServers.cwd": "Directorio de trabajo",
"toolServers.cwdPlaceholder": "Opcional (predeterminado del sistema)",
"toolServers.env": "Entorno",
"toolServers.envHint": "Variables de entorno adicionales como objeto JSON. Se combinan con el PATH heredado al lanzar el proceso.",
"toolServers.envInvalid": "El entorno debe ser un objeto JSON válido",
"toolServers.commandRequired": "El comando es obligatorio para servidores stdio",
"toolServers.connectionFailed": "Conexión fallida",
"toolServers.apiKeyKeep": "•••••••• (dejar vacío para mantener)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "Paramètres",
"toolServers.cwd": "Répertoire de travail",
"toolServers.cwdPlaceholder": "Facultatif (système par défaut)",
"toolServers.env": "Environnement",
"toolServers.envHint": "Variables d'environnement supplémentaires en JSON. Fusionnées avec le PATH hérité au lancement du processus.",
"toolServers.envInvalid": "L'environnement doit être un objet JSON valide",
"toolServers.commandRequired": "La commande est requise pour les serveurs stdio",
"toolServers.connectionFailed": "Échec de la connexion",
"toolServers.apiKeyKeep": "•••••••• (laisser vide pour conserver)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "引数",
"toolServers.cwd": "作業ディレクトリ",
"toolServers.cwdPlaceholder": "任意(既定はシステム)",
"toolServers.env": "環境変数",
"toolServers.envHint": "JSON オブジェクト形式の追加環境変数。プロセス起動時に継承された PATH とマージされます。",
"toolServers.envInvalid": "環境変数は有効なJSONオブジェクトである必要があります",
"toolServers.commandRequired": "stdio サーバーにはコマンドが必要です",
"toolServers.connectionFailed": "接続に失敗しました",
"toolServers.apiKeyKeep": "••••••••(空のままで保持)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "인수",
"toolServers.cwd": "작업 디렉터리",
"toolServers.cwdPlaceholder": "선택 사항(기본값은 시스템)",
"toolServers.env": "환경 변수",
"toolServers.envHint": "추가 환경 변수를 JSON 객체로 입력합니다. 프로세스를 시작할 때 상속된 PATH 와 병합됩니다.",
"toolServers.envInvalid": "환경 변수는 유효한 JSON 객체여야 합니다",
"toolServers.commandRequired": "stdio 서버에는 명령이 필요합니다",
"toolServers.connectionFailed": "연결 실패",
"toolServers.apiKeyKeep": "•••••••• (유지하려면 비워두세요)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "Argumentos",
"toolServers.cwd": "Diretório de trabalho",
"toolServers.cwdPlaceholder": "Opcional (padrão do sistema)",
"toolServers.env": "Ambiente",
"toolServers.envHint": "Variáveis de ambiente adicionais como objeto JSON. Mescladas com o PATH herdado ao iniciar o processo.",
"toolServers.envInvalid": "O ambiente deve ser um objeto JSON válido",
"toolServers.commandRequired": "Comando é obrigatório para servidores stdio",
"toolServers.connectionFailed": "Falha na conexão",
"toolServers.apiKeyKeep": "•••••••• (deixe vazio para manter)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "Аргументы",
"toolServers.cwd": "Рабочий каталог",
"toolServers.cwdPlaceholder": "Необязательно (по умолчанию системный)",
"toolServers.env": "Окружение",
"toolServers.envHint": "Дополнительные переменные окружения в виде JSON-объекта. Объединяются с унаследованным PATH при запуске процесса.",
"toolServers.envInvalid": "Окружение должно быть корректным JSON-объектом",
"toolServers.commandRequired": "Команда обязательна для stdio-серверов",
"toolServers.connectionFailed": "Ошибка подключения",
"toolServers.apiKeyKeep": "•••••••• (оставьте пустым для сохранения)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "参数",
"toolServers.cwd": "工作目录",
"toolServers.cwdPlaceholder": "可选(默认为系统)",
"toolServers.env": "环境变量",
"toolServers.envHint": "以 JSON 对象形式提供的附加环境变量。启动进程时会与继承的 PATH 合并。",
"toolServers.envInvalid": "环境变量必须是有效的 JSON 对象",
"toolServers.commandRequired": "stdio 服务器需要命令",
"toolServers.connectionFailed": "连接失败",
"toolServers.apiKeyKeep": "••••••••(留空以保持不变)",
Expand Down
3 changes: 3 additions & 0 deletions cptr/frontend/src/lib/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,9 @@
"toolServers.args": "引數",
"toolServers.cwd": "工作目錄",
"toolServers.cwdPlaceholder": "選填(預設為系統)",
"toolServers.env": "環境變數",
"toolServers.envHint": "以 JSON 物件形式提供的附加環境變數。啟動進程時會與繼承的 PATH 合併。",
"toolServers.envInvalid": "環境變數必須是有效的 JSON 物件",
"toolServers.commandRequired": "stdio 伺服器需要命令",
"toolServers.connectionFailed": "連線失敗",
"toolServers.apiKeyKeep": "••••••••(留空以保持不變)",
Expand Down
14 changes: 12 additions & 2 deletions cptr/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
get_or_create_user,
has_any_user,
hash_password,
load_config,
now_ms,
pam_authenticate,
record_attempt,
Expand Down Expand Up @@ -54,9 +55,18 @@ async def get_auth(request: Request):

client_host = request.client.host if request.client else "127.0.0.1"
token = request.cookies.get(COOKIE_NAME)
auth = check_access(client_host=client_host, jwt_token=token)
remote_user = None
if get_auth_mode() == AuthMode.TRUSTED_HEADER:
header_name = load_config().get("auth", {}).get("header", "Remote-User")
remote_user = request.headers.get(header_name)
auth = check_access(client_host=client_host, jwt_token=token, remote_user_header=remote_user)

if auth is not None and auth.username and not auth.user_id:
auth.user_id = await get_or_create_user(auth.username)

if auth is not None and auth.user_id:
if not auth.exp:
auth.exp = time.time() + SESSION_MAX_AGE
user = await User.get_by_id(auth.user_id)
if user is None:
from starlette.responses import JSONResponse as StarletteJSONResponse
Expand All @@ -77,7 +87,7 @@ async def get_auth(request: Request):

# Sliding session: refresh token if past halfway to expiry
remaining = auth.exp - time.time()
if remaining < SESSION_MAX_AGE / 2:
if remote_user or remaining < SESSION_MAX_AGE / 2:
new_token = create_token(auth.user_id, auth.username, user.role)
return _ok_with_cookie(new_token, data)

Expand Down
2 changes: 1 addition & 1 deletion cptr/routers/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class ChatCompletionMessage(BaseModel):
class ChatCompletionRequest(BaseModel):
model: str
messages: list[dict]
stream: bool = True
stream: bool = False
# Other OpenAI params are accepted but ignored
temperature: float | None = None
max_tokens: int | None = None
Expand Down
Loading
Loading