Skip to content
Open
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
213 changes: 211 additions & 2 deletions desktop/src/components/memory/MemorySettings.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,219 @@
import { useState, useEffect, useCallback } from "react";
import { Save, RefreshCw, CheckCircle, AlertTriangle } from "lucide-react";
import { Save, RefreshCw, CheckCircle, AlertTriangle, Wifi, WifiOff, Monitor, Globe } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { fetchSettingsSchema, fetchMemorySettings, updateMemorySettings } from "@/lib/memory";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { fetchSettingsSchema, fetchMemorySettings, updateMemorySettings, fetchMemoryEndpoint, updateMemoryEndpoint, fetchBackendCapabilities } from "@/lib/memory";
import type { TaOSmdEndpoint } from "@/lib/memory";
import { fetchMemoryModel, setMemoryModel } from "@/lib/memory-api";
import { ModelPickerModal } from "@/components/ModelPickerModal";
import type { AgentModel } from "@/components/ModelPickerFlow";
import { SchemaFormRenderer } from "./SchemaFormRenderer";

/* ------------------------------------------------------------------ */
/* TaOSmdEndpointCard — running mode + switch-to-remote */
/* ------------------------------------------------------------------ */

const LOCAL_URL = "http://localhost:7900";

function TaOSmdEndpointCard() {
const [endpoint, setEndpoint] = useState<TaOSmdEndpoint | null>(null);
const [capabilities, setCapabilities] = useState<{ name: string; version: string } | null>(null);
const [loading, setLoading] = useState(true);
const [remoteUrl, setRemoteUrl] = useState("");
const [switching, setSwitching] = useState(false);
const [switchErr, setSwitchErr] = useState<string | null>(null);

const load = useCallback(async () => {
setLoading(true);
setSwitchErr(null);
try {
const [ep, caps] = await Promise.all([
fetchMemoryEndpoint(),
fetchBackendCapabilities(),
]);
setEndpoint(ep);
setCapabilities({ name: caps.name, version: caps.version });
setRemoteUrl((ep?.url && ep.url !== LOCAL_URL) ? ep.url : "");
} catch {
/* graceful — will show fallback */
}
setLoading(false);
}, []);

useEffect(() => { load(); }, [load]);

const handleSwitch = async () => {
const url = remoteUrl.trim();
if (!url) return;
setSwitching(true);
setSwitchErr(null);
try {
const result = await updateMemoryEndpoint(url);
setEndpoint(result);
if (!result.reachable && !result.is_local) {
setSwitchErr("Server is not reachable. Check the URL and try again.");
}
} catch (e: any) {
setSwitchErr(String(e?.message ?? e));
}
setSwitching(false);
};

const handleRevert = async () => {
setSwitching(true);
setSwitchErr(null);
try {
const result = await updateMemoryEndpoint(LOCAL_URL);
setEndpoint(result);
setRemoteUrl("");
} catch (e: any) {
setSwitchErr(String(e?.message ?? e));
}
setSwitching(false);
};

if (loading) {
return (
<Card className="bg-white/[0.02] border-white/8">
<CardContent className="p-4">
<p className="text-xs text-shell-text-tertiary" aria-label="Loading taOSmd status">
Loading taOSmd status…
</p>
</CardContent>
</Card>
);
}

if (!endpoint) return null;

const isLocal = endpoint.is_local;
const modeLabel = isLocal ? "LOCAL" : "REMOTE";
const ModeIcon = isLocal ? Monitor : Globe;
const modeClasses = isLocal
? "bg-green-500/15 text-green-400 border-green-500/30"
: "bg-blue-500/15 text-blue-400 border-blue-500/30";

return (
<Card className="bg-white/[0.02] border-white/8">
Comment on lines +94 to +99

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use semantic theme colors throughout the new endpoint controls.

  • desktop/src/components/memory/MemorySettings.tsx#L94-L99: replace fixed mode/status palettes and apply the same correction to the card’s remaining new color classes.
  • desktop/src/apps/SettingsApp.tsx#L421-L438: replace fixed input, focus, and error colors with theme tokens.
📍 Affects 2 files
  • desktop/src/components/memory/MemorySettings.tsx#L94-L99 (this comment)
  • desktop/src/apps/SettingsApp.tsx#L421-L438
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/components/memory/MemorySettings.tsx` around lines 94 - 99,
Replace the fixed mode/status and remaining new card color classes in
MemorySettings.tsx with the project’s semantic theme tokens, preserving the
local/remote state distinction. In desktop/src/apps/SettingsApp.tsx lines
421-438, replace the new input, focus, and error color classes with the
corresponding semantic theme tokens; update both sites as part of the same
endpoint-controls styling change.

<CardContent className="p-4 flex flex-col gap-3">
{/* Header */}
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2">
<p className="text-xs uppercase opacity-60">taOSmd Status</p>
<span
className={`text-[10px] font-semibold px-1.5 py-px rounded-full border ${modeClasses}`}
aria-label={`Mode: ${modeLabel}`}
>
<ModeIcon size={10} className="inline mr-0.5" aria-hidden="true" />
{modeLabel}
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={load}
disabled={loading}
aria-label="Refresh taOSmd status"
className="h-7 px-2.5 text-xs"
>
<RefreshCw size={13} className={loading ? "animate-spin" : ""} aria-hidden="true" />
Refresh
</Button>
</div>

{/* Reachability */}
<div className="flex items-center gap-2 text-xs">
{endpoint.reachable ? (
<Wifi size={13} className="text-green-400 shrink-0" aria-hidden="true" />
) : (
<WifiOff size={13} className="text-red-400 shrink-0" aria-hidden="true" />
)}
<span
className={endpoint.reachable ? "text-green-400" : "text-red-400"}
aria-label={endpoint.reachable ? "Reachable" : "Not reachable"}
>
{endpoint.reachable ? "Reachable" : "Not reachable"}
</span>
<span className="opacity-40" aria-label="Server URL">{endpoint.url || LOCAL_URL}</span>
</div>

{/* Capabilities / Tier */}
<div className="flex items-center gap-3 text-xs flex-wrap">
{capabilities && (
<span className="opacity-50" aria-label={`Backend ${capabilities.name} v${capabilities.version}`}>
{capabilities.name} v{capabilities.version}
</span>
)}
{endpoint.tier && (
<span
className="px-1.5 py-px rounded-full border border-white/10 bg-white/[0.03] text-[10px] font-medium opacity-60"
aria-label={`Memory tier: ${endpoint.tier}`}
>
Tier: {endpoint.tier}
</span>
)}
</div>

{/* Switch to remote */}
<div className="flex flex-col gap-2">
<Label htmlFor="taosmd-remote-url" className="text-xs font-normal opacity-60">
Switch to remote instance
</Label>
<div className="flex items-center gap-2">
<Input
id="taosmd-remote-url"
type="url"
placeholder="http://192.168.1.x:7900"
value={remoteUrl}
onChange={(e) => { setRemoteUrl(e.target.value); setSwitchErr(null); }}
disabled={switching}
className="h-8 text-xs bg-white/[0.04] border-white/10"
aria-label="Remote taOSmd URL"
/>
<Button
variant="outline"
size="sm"
onClick={handleSwitch}
disabled={switching || !remoteUrl.trim() || remoteUrl.trim() === (endpoint.url || LOCAL_URL)}
aria-label="Connect to remote taOSmd"
className="h-8 px-3 text-xs shrink-0"
>
{switching ? <RefreshCw size={12} className="animate-spin" aria-hidden="true" /> : "Connect"}
</Button>
</div>
</div>

{/* Revert to local */}
{!isLocal && (
<div className="flex">
<Button
variant="ghost"
size="sm"
onClick={handleRevert}
disabled={switching}
aria-label="Revert to local taOSmd"
className="h-7 px-2.5 text-xs opacity-70 hover:opacity-100"
>
<Monitor size={12} className="mr-1" aria-hidden="true" />
Revert to local
</Button>
</div>
)}

{/* Errors */}
{switchErr && (
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-red-500/10 border border-red-500/20" role="alert" aria-live="assertive">
<AlertTriangle size={13} className="text-red-400 shrink-0" aria-hidden="true" />
<p className="text-xs text-red-300">{switchErr}</p>
</div>
)}
</CardContent>
</Card>
);
}

/* ------------------------------------------------------------------ */
/* MemoryModelSection */
/* ------------------------------------------------------------------ */
Expand Down Expand Up @@ -189,6 +395,9 @@ export function MemorySettings() {
{/* System-wide memory model */}
<MemoryModelSection />

{/* taOSmd endpoint status */}
<TaOSmdEndpointCard />

<div className="flex items-center justify-between">
<h2 className="text-sm font-semibold text-shell-text">Backend Settings</h2>
<Button
Expand Down
25 changes: 24 additions & 1 deletion desktop/src/lib/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export interface BackendCapabilities {
capabilities: string[];
}

export interface TaOSmdEndpoint {
url: string;
is_local: boolean;
reachable: boolean;
tier?: string;
}

export interface CatalogSession {
id: number;
date: string;
Expand All @@ -40,7 +47,7 @@ export interface CatalogSession {
}

/* ------------------------------------------------------------------ */
/* Stats / capabilities */
/* Stats / capabilities / endpoint */
/* ------------------------------------------------------------------ */

export async function fetchMemoryStats(): Promise<Record<string, any>> {
Expand All @@ -67,6 +74,22 @@ export async function updateMemorySettings(settings: Record<string, any>): Promi
});
}

export async function fetchMemoryEndpoint(): Promise<TaOSmdEndpoint> {
const result = await fetchJson<TaOSmdEndpoint>(
`${API}/settings/memory-url`,
{ url: "", is_local: true, reachable: false },
);
return result;
}

export async function updateMemoryEndpoint(url: string): Promise<TaOSmdEndpoint> {
return fetchJson<TaOSmdEndpoint>(`${API}/settings/memory-url`, { url, is_local: true, reachable: false }, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
});
Comment on lines +77 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not fabricate endpoint state when requests fail.

fetchJson() converts HTTP errors, invalid JSON, and network failures into fallback objects. Consequently, a rejected PUT appears successful as {url, is_local: true}, and the card can display a fake local endpoint after a failed GET. These helpers must expose failure—preferably by throwing—so callers can retain current state and show an error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/lib/memory.ts` around lines 77 - 90, Update fetchMemoryEndpoint
and updateMemoryEndpoint to stop passing fabricated fallback endpoint objects to
fetchJson; use the failure-propagating request behavior so HTTP, parsing, and
network errors are thrown to callers. Preserve successful response handling
while ensuring failed GET and PUT requests do not return synthetic local
endpoint state.

}

/* ------------------------------------------------------------------ */
/* Catalog */
/* ------------------------------------------------------------------ */
Expand Down
Loading