Skip to content
Merged

0.8.1 #103

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

### Added

- ✅ **Visible task progress.** Chats can now show a live checklist so long-running work is easier to follow.
- 🕒 **Message timestamps.** Hover over message controls to see when a message was sent.

### Changed

- 🧰 **Better model tool control.** Admins can choose which built-in tool groups each model can use, from files and terminal access to web, memory, images, and sub-agents.
- 🚀 **More flexible sub-agents.** Sub-agent limits now support higher defaults and unlimited mode for teams that want more parallel work.

### Fixed

- 💬 **More reliable chat lists.** Chat history stays in the right order more consistently, even when a saved chat needs extra cleanup.
- ✨ **Cleaner skill suggestions.** Skill matches in chat now use a simpler, more consistent icon.

## [0.8.0] - 2026-07-06

### Added
Expand Down
1 change: 1 addition & 0 deletions cptr/frontend/src/lib/apis/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export interface ModelConfigEntry {
request_params?: Record<string, unknown>;
system_prompt?: string;
compact_token_threshold?: number;
builtin_tools?: Record<string, boolean> | null;
};
}

Expand Down
9 changes: 9 additions & 0 deletions cptr/frontend/src/lib/apis/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,18 @@ export interface ContextUsage {
source: 'estimated';
}

export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';

export interface ChatTask {
id: string;
content: string;
status: TaskStatus;
}

export interface ChatDetail {
chat: ChatInfo;
messages: ChatMessageRow[];
tasks?: ChatTask[];
context_usage?: ContextUsage | null;
}

Expand Down
187 changes: 180 additions & 7 deletions cptr/frontend/src/lib/components/Admin/Models.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import Icon from '../Icon.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import ModelSelector from '$lib/components/common/ModelSelector.svelte';
import ToggleSwitch from '$lib/components/common/ToggleSwitch.svelte';

type ParamRow = { key: string; value: string };
type BuiltinToolsConfig = Record<string, boolean>;
type ModelEntry = {
id: string;
name: string;
Expand All @@ -25,6 +27,7 @@
rows: ParamRow[];
systemPrompt: string;
compactTokenThreshold: number | null;
builtinTools: BuiltinToolsConfig;
dirty: boolean;
};

Expand All @@ -37,6 +40,7 @@
let globalRows = $state<ParamRow[]>([]);
let globalSystemPrompt = $state('');
let globalCompactTokenThreshold = $state<number | null>(null);
let globalBuiltinTools = $state<BuiltinToolsConfig>({});
let globalDirty = $state(false);
let globalExpanded = $state(false);
let showVariables = $state(false);
Expand Down Expand Up @@ -66,6 +70,41 @@
{ name: 'MODEL', desc: 'Model ID being used' }
];

const BUILTIN_TOOL_GROUPS = [
{ id: 'files', label: 'models.builtinTools.files', desc: 'models.builtinTools.filesDesc' },
{
id: 'terminal',
label: 'models.builtinTools.terminal',
desc: 'models.builtinTools.terminalDesc'
},
{ id: 'web', label: 'models.builtinTools.web', desc: 'models.builtinTools.webDesc' },
{
id: 'browser',
label: 'models.builtinTools.browser',
desc: 'models.builtinTools.browserDesc'
},
{ id: 'memory', label: 'models.builtinTools.memory', desc: 'models.builtinTools.memoryDesc' },
{ id: 'chats', label: 'models.builtinTools.chats', desc: 'models.builtinTools.chatsDesc' },
{ id: 'skills', label: 'models.builtinTools.skills', desc: 'models.builtinTools.skillsDesc' },
{ id: 'tasks', label: 'models.builtinTools.tasks', desc: 'models.builtinTools.tasksDesc' },
{
id: 'automations',
label: 'models.builtinTools.automations',
desc: 'models.builtinTools.automationsDesc'
},
{ id: 'images', label: 'models.builtinTools.images', desc: 'models.builtinTools.imagesDesc' },
{
id: 'subagents',
label: 'models.builtinTools.subagents',
desc: 'models.builtinTools.subagentsDesc'
},
{
id: 'notifications',
label: 'models.builtinTools.notifications',
desc: 'models.builtinTools.notificationsDesc'
}
];

const DEFAULT_PROMPT_PLACEHOLDER = `You are Computer, a helpful assistant running inside the user's computer interface. You have access to tools to read, search, and modify files in the workspace, run commands, and use configured tools. Use them to help the user directly. Approach hard requests with initiative and persistence: make the best possible attempt, adapt as needed, and keep going unless a real constraint prevents progress.

{{CPTR_CONTEXT}}
Expand Down Expand Up @@ -95,6 +134,60 @@ Files:
return Number.isFinite(threshold) && threshold > 0 ? Math.floor(threshold) : null;
}

function parseBuiltinTools(config: ModelConfigEntry | undefined): BuiltinToolsConfig {
const value = config?.params?.builtin_tools;
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
return Object.fromEntries(
Object.entries(value).filter(([, enabled]) => typeof enabled === 'boolean')
);
}

function builtinEnabled(
tools: BuiltinToolsConfig,
group: string,
inherited: BuiltinToolsConfig | null = null
): boolean {
if (tools[group] !== undefined) return tools[group] !== false;
if (inherited?.[group] !== undefined) return inherited[group] !== false;
return true;
}

function setBuiltinEnabled(
tools: BuiltinToolsConfig,
group: string,
enabled: boolean,
inherited: BuiltinToolsConfig | null = null
): BuiltinToolsConfig {
const next = { ...tools };
const inheritedEnabled = inherited?.[group] !== undefined ? inherited[group] !== false : true;
if (enabled === inheritedEnabled) {
delete next[group];
} else {
next[group] = enabled;
}
return next;
}

function serializeBuiltinTools(
tools: BuiltinToolsConfig,
inherited: BuiltinToolsConfig | null = null
): BuiltinToolsConfig | null {
const result: BuiltinToolsConfig = {};
for (const group of BUILTIN_TOOL_GROUPS) {
if (tools[group.id] === undefined) continue;
const inheritedEnabled =
inherited?.[group.id] !== undefined ? inherited[group.id] !== false : true;
if (tools[group.id] !== inheritedEnabled) {
result[group.id] = tools[group.id];
}
}
return Object.keys(result).length ? result : null;
}

function configuredRowCount(rows: ParamRow[]): number {
return rows.filter((row) => row.key.trim()).length;
}

function rowsToRequestParams(rows: ParamRow[]): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const { key, value } of rows) {
Expand All @@ -119,8 +212,12 @@ Files:
globalRows = parseRows(config['*']);
globalSystemPrompt = config['*']?.params?.system_prompt || '';
globalCompactTokenThreshold = parseCompactTokenThreshold(config['*']);
globalBuiltinTools = parseBuiltinTools(config['*']);
globalExpanded =
globalRows.length > 0 || !!globalSystemPrompt || globalCompactTokenThreshold !== null;
globalRows.length > 0 ||
!!globalSystemPrompt ||
globalCompactTokenThreshold !== null ||
Object.keys(globalBuiltinTools).length > 0;
}

models = data.models.map((m) => {
Expand All @@ -136,6 +233,7 @@ Files:
rows: parseRows(mc),
systemPrompt: (mc?.params as any)?.system_prompt || '',
compactTokenThreshold: parseCompactTokenThreshold(mc),
builtinTools: parseBuiltinTools(mc),
dirty: false
};
});
Expand Down Expand Up @@ -204,13 +302,17 @@ Files:
function buildParams(
rows: ParamRow[],
systemPrompt: string,
compactThreshold: number | null
compactThreshold: number | null,
builtinTools: BuiltinToolsConfig,
inheritedBuiltinTools: BuiltinToolsConfig | null = null
): Record<string, unknown> {
const params: Record<string, unknown> = {};
const rp = rowsToRequestParams(rows);
const bt = serializeBuiltinTools(builtinTools, inheritedBuiltinTools);
if (Object.keys(rp).length) params.request_params = rp;
if (systemPrompt.trim()) params.system_prompt = systemPrompt.trim();
if (compactThreshold && compactThreshold > 0) params.compact_token_threshold = compactThreshold;
if (bt) params.builtin_tools = bt;
return params;
}

Expand All @@ -221,15 +323,26 @@ Files:
if (globalDirty) {
promises.push(
updateModelConfig('*', {
params: buildParams(globalRows, globalSystemPrompt, globalCompactTokenThreshold)
params: buildParams(
globalRows,
globalSystemPrompt,
globalCompactTokenThreshold,
globalBuiltinTools
)
})
);
}
for (const model of models) {
if (model.dirty) {
promises.push(
updateModelConfig(model.id, {
params: buildParams(model.rows, model.systemPrompt, model.compactTokenThreshold)
params: buildParams(
model.rows,
model.systemPrompt,
model.compactTokenThreshold,
model.builtinTools,
globalBuiltinTools
)
})
);
}
Expand Down Expand Up @@ -380,6 +493,47 @@ Files:
</div>
{/snippet}

{#snippet builtinToolsField(
tools: BuiltinToolsConfig,
inherited: BuiltinToolsConfig | null,
onChange: (tools: BuiltinToolsConfig) => void,
resetLabel: string
)}
<div class="mb-2">
<div class="flex items-center justify-between mb-1">
<span class="text-[0.625rem] text-gray-400 dark:text-gray-600 uppercase tracking-wide"
>{$t('models.builtinTools')}</span
>
{#if Object.keys(tools).length > 0}
<button
class="text-[0.625rem] text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400 transition-colors duration-75"
onclick={() => onChange({})}
>
{resetLabel}
</button>
{/if}
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-1">
{#each BUILTIN_TOOL_GROUPS as group}
<div class="flex items-center justify-between gap-2 min-h-7">
<div class="min-w-0">
<div class="text-[0.75rem] text-gray-600 dark:text-gray-400 truncate">
{$t(group.label)}
</div>
<div class="text-[0.625rem] text-gray-400 dark:text-gray-600 truncate">
{$t(group.desc)}
</div>
</div>
<ToggleSwitch
value={builtinEnabled(tools, group.id, inherited)}
onchange={(enabled) => onChange(setBuiltinEnabled(tools, group.id, enabled, inherited))}
/>
</div>
{/each}
</div>
</div>
{/snippet}

<div class="flex flex-col h-full">
{#if loading}
<div class="flex justify-center py-8"><Spinner size={16} /></div>
Expand Down Expand Up @@ -448,11 +602,11 @@ Files:
<span class="flex-1 text-[0.8125rem] text-gray-500 dark:text-gray-400"
>{$t('models.defaults')}</span
>
{#if globalRows.filter((r) => r.key.trim()).length > 0 || globalSystemPrompt.trim()}
{#if configuredRowCount(globalRows) > 0 || globalSystemPrompt.trim()}
<span class="text-[0.625rem] text-gray-400 dark:text-gray-600">
{#if globalSystemPrompt.trim()}prompt{/if}
{#if globalRows.filter((r) => r.key.trim()).length > 0}
{globalRows.filter((r) => r.key.trim()).length} params
{#if configuredRowCount(globalRows) > 0}
{configuredRowCount(globalRows)} params
{/if}
</span>
{/if}
Expand Down Expand Up @@ -480,6 +634,15 @@ Files:
},
DEFAULT_PROMPT_PLACEHOLDER
)}
{@render builtinToolsField(
globalBuiltinTools,
null,
(tools) => {
globalBuiltinTools = tools;
globalDirty = true;
},
$t('models.resetToDefault')
)}
{@render paramRows(
globalRows,
() => (globalDirty = true),
Expand Down Expand Up @@ -546,6 +709,16 @@ Files:
},
$t('models.systemPromptInherited')
)}
{@render builtinToolsField(
model.builtinTools,
globalBuiltinTools,
(tools) => {
model.builtinTools = tools;
model.dirty = true;
models = [...models];
},
$t('models.resetToDefault')
)}
{@render paramRows(
model.rows,
() => (model.dirty = true),
Expand Down
14 changes: 6 additions & 8 deletions cptr/frontend/src/lib/components/Admin/Subagents.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

let enabled = $state(false);
let backgroundEnabled = $state(false);
let maxConcurrent = $state(3);
let maxAsync = $state(3);
let maxConcurrent = $state(20);
let maxAsync = $state(20);
let maxIterations = $state(30);
let maxOutput = $state(30000);
let systemPrompt = $state('');
Expand All @@ -24,8 +24,8 @@
backgroundEnabled =
config['subagents.background_enabled'] === true ||
config['subagents.background_enabled'] === 'true';
maxConcurrent = Number(config['subagents.max_concurrent']) || 3;
maxAsync = Number(config['subagents.max_async']) || 3;
maxConcurrent = Number(config['subagents.max_concurrent']) || 20;
maxAsync = Number(config['subagents.max_async']) || 20;
maxIterations = Number(config['subagents.max_iterations']) || 30;
maxOutput = Number(config['subagents.max_output']) || 30000;
systemPrompt = (config['subagents.system_prompt'] as string) || '';
Expand Down Expand Up @@ -86,8 +86,7 @@
id="sa-concurrent"
type="number"
bind:value={maxConcurrent}
min="1"
max="10"
min="-1"
class="w-16 h-7 px-2 rounded-lg text-xs bg-gray-100 dark:bg-white/6 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-white/8 outline-none focus:border-blue-400 dark:focus:border-blue-500 transition-colors"
/>
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600"
Expand Down Expand Up @@ -123,8 +122,7 @@
id="sa-async"
type="number"
bind:value={maxAsync}
min="1"
max="10"
min="-1"
class="w-16 h-7 px-2 rounded-lg text-xs bg-gray-100 dark:bg-white/6 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-white/8 outline-none focus:border-blue-400 dark:focus:border-blue-500 transition-colors"
/>
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600"
Expand Down
4 changes: 4 additions & 0 deletions cptr/frontend/src/lib/components/Icon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
<path d="M16 9.2C15.2 9.2 14.5 9.7 14.2 10.4" />
<path d="M8.6 14.2C9.4 14.2 10.1 13.8 10.5 13.1" />
<path d="M15.4 14.2C14.6 14.2 13.9 13.8 13.5 13.1" />
{:else if name === 'spark'}
<path
d="M3 12C9.26752 12 12 9.36306 12 3C12 9.36306 14.7134 12 21 12C14.7134 12 12 14.7134 12 21C12 14.7134 9.26752 12 3 12Z"
/>
{:else if name === 'plus'}
<path d="M6 12H12M18 12H12M12 12V6M12 12V18" />
{:else if name === 'xmark'}
Expand Down
Loading
Loading