Skip to content
Merged
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Keep pull requests focused. One feature or fix per PR is easier to review.

Documentation improvements are always welcome — typo fixes, clarifications, better examples. Open a PR directly.

The taOS agent manual is compiled: edit `docs/agent-manual/` and run `python3 scripts/build-agent-manual.py` to regenerate `docs/taos-agent-manual.md`.

---

## Adding an App to the Catalog
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ Search across agents, apps, messages, and files from a single endpoint. Finds an
- **Notifications.** Health alerts, backend up/down, worker join/leave, webhook forwarding (Slack/Discord/Telegram). Toast notifications appear top-right. The welcome notification is gated on a `localStorage` flag so it fires once per install, not on every page load.
- **Agent Logs.** Real-time log viewer with auto-refresh
- **Backup & Restore.** Downloadable config backup, one-click restore, scheduled auto-backup (daily/weekly)
- **System Updates.** Pull latest from GitHub via Settings page
- **System Updates.** Pull latest from GitHub via Settings page. taOS periodically checks for updates and reports an anonymous install count (a daily aggregate estimate, no identifiers); disable with `TAOS_NO_UPDATE_PING=1` or in Settings.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Documentation claims a Settings UI opt-out that this PR does not add.

The backend now checks update_ping_enabled, but the diff does not update desktop/src/apps/SettingsApp/UpdatesPanel.tsx or /api/settings/update-status to expose or persist that setting. Users will not have the documented Settings control unless this PR includes additional UI/API changes.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

- **Provider Management.** Add/test/remove inference providers with live connectivity checks. The Providers desktop app manages cloud LLM credentials; the model browser reflects configured providers automatically.

## App Catalog (108 Catalog Apps + 36 Desktop Apps + 47 MCP Plugins)
Expand Down
57 changes: 38 additions & 19 deletions desktop/src/apps/AgentMessagesPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from "react";
import { Send, Loader2, ArrowRightLeft } from "lucide-react";
import { Send, Loader2, ArrowRightLeft, Copy, Check } from "lucide-react";
import { Button, Card, Input, Textarea, Label } from "@/components/ui";

interface AgentMessageRaw {
Expand Down Expand Up @@ -55,6 +55,39 @@ function formatTime(ts: number): string {
return new Date(ts * 1000).toLocaleDateString();
}

function PreBlock({ content, label }: { content: unknown; label: string }) {
const [copied, setCopied] = useState(false);
const text = JSON.stringify(content, null, 2);

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// ignore
}
};

return (
<div className="mt-2">
<div className="flex items-center justify-between mb-0.5">
<p className="text-[10px] text-shell-text-tertiary uppercase tracking-wide">{label}</p>
<button
onClick={(e) => { e.stopPropagation(); handleCopy(); }}
aria-label={copied ? "Copied" : `Copy ${label.toLowerCase()}`}
className="p-0.5 rounded text-shell-text-tertiary hover:text-shell-text focus:outline-none focus:ring-1 focus:ring-accent/50 transition-colors"
>
{copied ? <Check size={10} /> : <Copy size={10} />}
</button>
</div>
<pre className="text-[10px] text-shell-text-secondary p-2 rounded bg-shell-bg-deep border border-white/5 overflow-x-auto select-text whitespace-pre-wrap break-all">
{text}
</pre>
</div>
);
}

export function AgentMessagesPanel({ agentName }: Props) {
const [messages, setMessages] = useState<AgentMessage[]>([]);
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -158,7 +191,7 @@ export function AgentMessagesPanel({ agentName }: Props) {
</div>
</div>
<p
className={`text-sm text-shell-text whitespace-pre-wrap ${
className={`text-sm text-shell-text whitespace-pre-wrap select-text ${
isOpen ? "" : "line-clamp-3"
}`}
>
Expand All @@ -169,32 +202,18 @@ export function AgentMessagesPanel({ agentName }: Props) {
<p className="text-[10px] text-shell-text-tertiary uppercase tracking-wide mb-0.5">
Reasoning
</p>
<p className="text-xs text-shell-text-secondary pl-2 border-l border-white/10 whitespace-pre-wrap">
<p className="text-xs text-shell-text-secondary pl-2 border-l border-white/10 whitespace-pre-wrap select-text">
{msg.reasoning}
</p>
</div>
)}
{isOpen && msg.tool_calls && msg.tool_calls.length > 0 && (
<div className="mt-2">
<p className="text-[10px] text-shell-text-tertiary uppercase tracking-wide mb-0.5">
Tool Calls
</p>
<pre className="text-[10px] text-shell-text-secondary p-2 rounded bg-shell-bg-deep border border-white/5 overflow-x-auto">
{JSON.stringify(msg.tool_calls, null, 2)}
</pre>
</div>
<PreBlock content={msg.tool_calls} label="Tool Calls" />
)}
{isOpen &&
msg.tool_results &&
msg.tool_results.length > 0 && (
<div className="mt-2">
<p className="text-[10px] text-shell-text-tertiary uppercase tracking-wide mb-0.5">
Tool Results
</p>
<pre className="text-[10px] text-shell-text-secondary p-2 rounded bg-shell-bg-deep border border-white/5 overflow-x-auto">
{JSON.stringify(msg.tool_results, null, 2)}
</pre>
</div>
<PreBlock content={msg.tool_results} label="Tool Results" />
)}
</Card>
);
Expand Down
16 changes: 8 additions & 8 deletions desktop/src/apps/AgentsApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,9 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
const agent = agents.find((a) => a.name === detail.name);
if (agent) {
return (
<div className="flex flex-col h-full min-h-0 overflow-hidden bg-shell-bg text-shell-text select-none">
<div className="flex flex-col h-full min-h-0 overflow-hidden bg-shell-bg text-shell-text">
{/* Back header */}
<div className="flex items-center gap-2 px-3 py-2 border-b border-white/5 shrink-0">
<div className="flex items-center gap-2 px-3 py-2 border-b border-white/5 shrink-0 select-none">
<button
type="button"
aria-label="Back to agents"
Expand All @@ -401,7 +401,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
{agent.display_name || agent.name}
</span>
</div>
<div className="flex flex-1 min-h-0 flex-col overflow-hidden">
<div className="flex flex-1 min-h-0 flex-col overflow-hidden select-text">
<AgentDetailPanel
agent={agent}
initialTab={detail.tab}
Expand All @@ -422,9 +422,9 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
// Full-window detail view for the taOS system agent
if (taosDetailOpen) {
return (
<div className="flex flex-col h-full min-h-0 overflow-hidden bg-shell-bg text-shell-text select-none">
<div className="flex flex-col h-full min-h-0 overflow-hidden bg-shell-bg text-shell-text">
{/* Back header */}
<div className="flex items-center gap-2 px-3 py-2 border-b border-white/5 shrink-0">
<div className="flex items-center gap-2 px-3 py-2 border-b border-white/5 shrink-0 select-none">
<button
type="button"
aria-label="Back to agents"
Expand All @@ -435,7 +435,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
</button>
<span className="text-sm font-medium text-shell-text truncate">taOS agent</span>
</div>
<div className="flex flex-1 min-h-0 flex-col overflow-hidden">
<div className="flex flex-1 min-h-0 flex-col overflow-hidden select-text">
<TaosAgentDetailPanel onClose={() => setTaosDetailOpen(false)} fullHeight />
</div>
<DeployWizard open={wizardOpen} onClose={handleWizardClose} />
Expand All @@ -444,9 +444,9 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
}

return (
<div className="flex flex-col h-full min-h-0 overflow-hidden bg-shell-bg text-shell-text select-none relative">
<div className="flex flex-col h-full min-h-0 overflow-hidden bg-shell-bg text-shell-text relative">
{/* Toolbar */}
<div className="flex items-center justify-between gap-2 px-4 py-3 border-b border-white/5">
<div className="flex items-center justify-between gap-2 px-4 py-3 border-b border-white/5 select-none">
<div className="flex items-center gap-2 min-w-0">
<Bot size={18} className="text-accent shrink-0" />
<h1 className="text-sm font-semibold shrink-0">Agents</h1>
Expand Down
40 changes: 37 additions & 3 deletions desktop/src/apps/MessagesApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
import { displayAuthor } from "./chat/format-author";
import { useProcessStore } from "@/stores/process-store";
import { getApp } from "@/registry/app-registry";
import { CodeBlock } from "@/components/CodeBlock";

/* ------------------------------------------------------------------ */
/* Types */
Expand Down Expand Up @@ -212,6 +213,29 @@ function relativeTime(ts: number | string): string {
}

function renderContent(text: string) {
// Split on fenced code blocks first, then apply inline markdown to non-code segments.
const result: (string | React.ReactElement)[] = [];
const fenceRegex = /```(?:[^\n]*)?\n([\s\S]*?)```/g;
let lastFence = 0;
let fenceMatch: RegExpExecArray | null;
let seg = 0;

// Each segment gets a distinct key prefix so keys can never collide no
// matter how many inline elements one segment produces.
while ((fenceMatch = fenceRegex.exec(text)) !== null) {
if (fenceMatch.index > lastFence) {
result.push(...renderInline(text.slice(lastFence, fenceMatch.index), `s${seg++}`));
}
result.push(<CodeBlock key={`cb-${seg++}`} code={fenceMatch[1] ?? ""} />);
lastFence = fenceMatch.index + fenceMatch[0].length;
}
if (lastFence < text.length) {
result.push(...renderInline(text.slice(lastFence), `s${seg}`));
}
return result;
}

function renderInline(text: string, keyPrefix: string) {
// basic markdown: bold, italic, inline code
const parts: (string | React.ReactElement)[] = [];
const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g;
Expand All @@ -220,9 +244,9 @@ function renderContent(text: string) {
let key = 0;
while ((match = regex.exec(text)) !== null) {
if (match.index > last) parts.push(text.slice(last, match.index));
if (match[2]) parts.push(<strong key={key++} className="font-semibold">{match[2]}</strong>);
else if (match[3]) parts.push(<em key={key++} className="italic">{match[3]}</em>);
else if (match[4]) parts.push(<code key={key++} className="bg-white/10 px-1.5 py-0.5 rounded text-[13px] font-mono">{match[4]}</code>);
if (match[2]) parts.push(<strong key={`${keyPrefix}-${key++}`} className="font-semibold">{match[2]}</strong>);
else if (match[3]) parts.push(<em key={`${keyPrefix}-${key++}`} className="italic">{match[3]}</em>);
else if (match[4]) parts.push(<code key={`${keyPrefix}-${key++}`} className="bg-white/10 px-1.5 py-0.5 rounded text-[13px] font-mono">{match[4]}</code>);
last = match.index + match[0].length;
}
if (last < text.length) parts.push(text.slice(last));
Expand Down Expand Up @@ -1032,6 +1056,15 @@ export function MessagesApp({
} catch { /* ignore */ }
};

const handleCopyText = async (msgId: string) => {
setOverflowMenu(null);
const msg = messages.find((m) => m.id === msgId);
if (!msg) return;
try {
await navigator.clipboard.writeText(msg.content);
} catch { /* ignore */ }
};

const handlePin = async (msg: Message) => {
setOverflowMenu(null);
const isPinned = pinnedMessages.some((p) => p.id === msg.id);
Expand Down Expand Up @@ -2081,6 +2114,7 @@ export function MessagesApp({
onEdit={() => handleEdit(msg.id)}
onDelete={() => handleDelete(msg.id)}
onCopyLink={() => handleCopyLink(msg.id)}
onCopyText={() => handleCopyText(msg.id)}
onPin={() => handlePin(msg)}
onMarkUnread={() => handleMarkUnread(msg.id)}
onClose={() => setOverflowMenu(null)}
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/apps/chat/MessageOverflowMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ export interface MessageOverflowMenuProps {
onEdit: () => void;
onDelete: () => void;
onCopyLink: () => void;
onCopyText?: () => void;
onPin: () => void;
onMarkUnread: () => void;
onClose?: () => void;
}

export function MessageOverflowMenu({
isOwn, isHuman, isPinned = false,
onEdit, onDelete, onCopyLink, onPin, onMarkUnread, onClose,
onEdit, onDelete, onCopyLink, onCopyText, onPin, onMarkUnread, onClose,
}: MessageOverflowMenuProps) {
const containerRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -66,6 +67,8 @@ export function MessageOverflowMenu({
)}
<button role="menuitem" onClick={onCopyLink}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none">Copy link</button>
{onCopyText && <button role="menuitem" onClick={onCopyText}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none">Copy text</button>}
{isHuman && (
<button role="menuitem" onClick={onPin}
className="block w-full text-left px-3 py-1.5 hover:bg-white/5 focus:bg-white/5 focus:outline-none">
Expand Down
36 changes: 36 additions & 0 deletions desktop/src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from "react";
import { Check, Copy } from "lucide-react";

interface Props {
code: string;
language?: string;
}

export function CodeBlock({ code }: Props) {
const [copied, setCopied] = useState(false);

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// ignore
}
};

return (
<div className="relative group my-1.5 rounded-lg bg-shell-bg-deep border border-white/10 overflow-x-auto">
<button
onClick={handleCopy}
aria-label={copied ? "Copied" : "Copy code"}
className="absolute top-1.5 right-1.5 p-1 rounded opacity-0 group-hover:opacity-100 focus:opacity-100 bg-shell-surface border border-white/10 text-shell-text-secondary hover:text-shell-text transition-opacity"
>
{copied ? <Check size={12} /> : <Copy size={12} />}
</button>
<pre className="text-[12px] font-mono text-shell-text-secondary p-3 pr-8 whitespace-pre-wrap break-words select-text">
{code}
</pre>
</div>
);
}
48 changes: 45 additions & 3 deletions desktop/src/components/TaosAssistantPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
Paperclip,
Camera,
ExternalLink,
Copy,
Check,
} from "lucide-react";
import { CodeBlock } from "@/components/CodeBlock";
import { useTaosAgentStore } from "@/stores/taos-agent-store";
import { TaosAssistantSettings } from "./TaosAssistantSettings";
import {
Expand Down Expand Up @@ -497,6 +500,21 @@ function ToolbarButton({
/* Message bubble */
/* ------------------------------------------------------------------ */

function renderBubbleContent(text: string): (string | React.ReactElement)[] {
const result: (string | React.ReactElement)[] = [];
const fenceRegex = /```(?:[^\n]*)?\n([\s\S]*?)```/g;
let last = 0;
let match: RegExpExecArray | null;
let key = 0;
while ((match = fenceRegex.exec(text)) !== null) {
if (match.index > last) result.push(text.slice(last, match.index));
result.push(<CodeBlock key={`cb-${key++}`} code={match[1] ?? ""} />);
last = match.index + match[0].length;
}
if (last < text.length) result.push(text.slice(last));
return result;
}

function MessageBubble({
role,
content,
Expand All @@ -506,23 +524,47 @@ function MessageBubble({
content: string;
streaming?: boolean;
}) {
const [copied, setCopied] = useState(false);

if (role === "system") return null;

const isUser = role === "user";

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// ignore
}
};

return (
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
<div className={`flex ${isUser ? "justify-end" : "justify-start"} group`}>
<div
className={`max-w-[85%] rounded-xl px-3 py-2 text-sm whitespace-pre-wrap break-words ${
className={`relative max-w-[85%] rounded-xl px-3 py-2 text-sm break-words select-text ${
isUser ? "bg-accent text-white" : "bg-shell-surface-hover text-shell-text"
}`}
>
{content}
<div className="whitespace-pre-wrap">
{renderBubbleContent(content)}
</div>
{streaming && !content && (
<span className="inline-block w-2 h-3 bg-current opacity-60 animate-pulse ml-0.5 rounded-sm" />
)}
{streaming && content && (
<span className="inline-block w-1.5 h-3 bg-current opacity-60 animate-pulse ml-0.5 rounded-sm" />
)}
{!streaming && content && (
<button
onClick={handleCopy}
aria-label={copied ? "Copied" : "Copy message"}
className="absolute -top-2 -right-2 p-1 rounded opacity-0 group-hover:opacity-100 focus:opacity-100 bg-shell-surface border border-white/10 text-shell-text-secondary hover:text-shell-text transition-opacity select-none"
>
{copied ? <Check size={10} /> : <Copy size={10} />}
</button>
)}
</div>
</div>
);
Expand Down
Loading
Loading