UI polish: skill encryption key, disconnect fix, settings layout cleanup (#330)

* feat: add overlay process launcher for OpenHuman (#328)

- Introduced a new `overlay` module to manage the lifecycle of the `openhuman-overlay` Tauri application.
- Implemented the `spawn_overlay` function to discover and launch the overlay as a child process, enhancing the user experience with a transparent debug/voice panel.
- Updated the `jsonrpc.rs` file to invoke the overlay process when the core server starts, ensuring seamless integration.
- Enhanced module organization by adding the `overlay` module to the `openhuman` namespace.

* feat: add animated mesh gradient background to App component

- Introduced a new `MeshGradient` component that renders an animated WebGL mesh gradient background, enhancing the visual appeal of the application.
- Updated the `App` component to include the `MeshGradient`, ensuring it renders behind the dotted canvas overlay for improved aesthetics.
- Modified the CSS for the `app-dotted-canvas` to use a transparent background and adjusted the gradient styling for better integration with the new component.
- Added TypeScript definitions and implementation for the `Gradient` class to manage the mesh gradient functionality.

* style: update MeshGradient component colors and opacity

- Changed the opacity of the mesh gradient from 20% to 10% for a more subtle effect.
- Updated gradient colors to enhance visual appeal, replacing previous colors with a new palette for better integration with the overall design.

* feat: enhance SkillSetupWizard with encryption key support

- Added a new input field for client-side encryption key in the OAuthLoginView component, improving security during manual integration.
- Implemented validation to ensure the encryption key is provided before submission, enhancing error handling.
- Updated the layout to accommodate both integration ID and encryption key inputs, improving user experience in development mode.
- Refactored related logic to streamline the handling of integration ID and encryption key during OAuth completion.

* feat: integrate skill installation check in SkillSetupWizard

- Updated the OAuthLoginView component to ensure skills are installed before starting the runtime, enhancing reliability during the setup process.
- Added error handling to manage cases where the skill may already be installed, improving user experience and reducing potential runtime errors.

* fix: improve base directory resolution in skill runtime

- Updated the base directory resolution logic in both `jsonrpc.rs` and `skills_cli.rs` to filter out empty workspace paths, enhancing robustness.
- Added debug logging in `qjs_engine.rs` to provide better visibility into skill startup processes, including checks for manifest existence and workspace directory status.

* feat: enhance SkillManager with skill stopping and client key cleanup

- Implemented the `stopSkill` method to halt the frontend-side runtime and added a call to `rpcStopSkill` for stopping the core sidecar skill process.
- Introduced `removePersistedClientKey` function to clear the client-side encryption key, ensuring better security and resource management during skill revocation.
- Added error handling and debug logging for both stopping the skill and cleaning up the client key, improving observability and reliability.

* style: update OnboardingOverlay background opacity and refine settings warning message

- Changed the background opacity of the OnboardingOverlay from 95% to 50% for a more subtle appearance.
- Revised the warning message in SettingsHome to clarify the local data deletion process, improving user understanding of the consequences of signing out.

* refactor: simplify layout structure in settings components

- Removed unnecessary overflow properties from the main container divs in SettingsHome, SettingsSectionPage, and SettingsPanelLayout components, improving layout clarity.
- Adjusted child divs to ensure proper content display without overflow, enhancing user experience in the settings interface.

* style: adjust padding and spacing in SettingsSectionPage component

- Updated padding in the main container from `p-4` to `py-4` for improved vertical spacing.
- Increased horizontal padding of the description text from `px-1` to `px-5`, enhancing readability and visual appeal.

* style: refine layout and spacing in settings components

- Adjusted padding and spacing in SettingsSectionPage, SettingsBackButton, and SettingsHeader components for improved visual consistency.
- Updated font sizes and margins to enhance readability and user experience across the settings interface.

* style: simplify layout and improve spacing in settings panels

- Removed unnecessary flex properties and adjusted padding in various settings panels for a cleaner layout.
- Standardized spacing across panels to enhance visual consistency and user experience.
- Updated component structures to eliminate redundant overflow properties, improving content display.

* style: simplify section layouts and improve spacing in settings panels

- Removed unnecessary border and background properties from multiple sections in the Accessibility, Screen Intelligence, and other settings panels for a cleaner appearance.
- Standardized section spacing and padding across various panels to enhance visual consistency and user experience.
- Updated font sizes for better readability and adjusted button styles for improved interaction.

* style: standardize panel headings from text-lg to text-sm

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: update heading for connected skills section to improve clarity

- Changed the section title from "Connected Skills" to "3rd Party Skills" for better representation of the content.

* style: improve code formatting and spacing in various components

- Enhanced readability by adjusting spacing and formatting in SettingsBackButton, MemoryDebugPanel, WebhooksDebugPanel, and other components.
- Standardized the layout of JSX elements for better visual consistency across the application.
- Cleaned up log messages in Rust files for improved clarity and debugging.

* fix: enhance MeshGradient cleanup and improve settings warning message

- Added a call to `gradient.disconnect()` in the MeshGradient component to ensure proper cleanup on unmount.
- Updated the warning message in SettingsHome to use a list format for better clarity regarding local data deletion consequences.
- Removed unused 'tauri-commands' route from settings navigation for cleaner code.
- Added validation for the encryption key in SkillSetupWizard to prevent invalid characters.
- Updated the Gradient class to include additional checks for canvas element and WebGL context during initialization.
- Improved error handling in SkillManager's `stopSkill` method for better debugging and reliability.

* style: correct comment formatting in meshGradient.js

- Updated comment syntax in the Gradient class to ensure proper code documentation and readability.

* fix: add encryption key validation and remove double transition in dev setup

- Validate devEncryptionKey for invalid characters (quotes, backslashes,
  control chars) before using as X-Encryption-Key header
- Remove explicit onManualComplete() call — let the snapshot effect handle
  the connected→setup transition to avoid double-firing startSkill/startSetup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: improve encryption key validation in SkillSetupWizard

- Enhanced validation for the devEncryptionKey to check for quotes, backslashes, and control characters, ensuring only valid characters are accepted.
- This change prevents potential issues when using the encryption key in the application.

* fix: resolve lint errors in MeshGradient, meshGradient.js, and SkillSetupWizard

- Fix canvasRef.current stale ref warning by capturing to local variable
- Add eslint-disable for browser globals in vendored meshGradient.js
- Replace control character regex with printable-range check
- Remove unused onManualComplete from dependency array

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Steven Enamakel
2026-04-05 17:21:05 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent a0abda956a
commit 7bce9d2bbe
51 changed files with 1523 additions and 776 deletions
+8 -4
View File
@@ -8,6 +8,7 @@ import BottomTabBar from './components/BottomTabBar';
import ServiceBlockingGate from './components/daemon/ServiceBlockingGate';
import ErrorFallbackScreen from './components/ErrorFallbackScreen';
import LocalAIDownloadSnackbar from './components/LocalAIDownloadSnackbar';
import MeshGradient from './components/MeshGradient';
import OnboardingOverlay from './components/OnboardingOverlay';
import CoreStateProvider from './providers/CoreStateProvider';
import SocketProvider from './providers/SocketProvider';
@@ -29,11 +30,14 @@ function App() {
<SocketProvider>
<Router>
<ServiceBlockingGate>
<div className="app-dotted-canvas relative h-screen flex flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto pb-16">
<AppRoutes />
<div className="relative h-screen flex flex-col overflow-hidden">
<MeshGradient />
<div className="app-dotted-canvas relative z-10 flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto pb-16">
<AppRoutes />
</div>
<BottomTabBar />
</div>
<BottomTabBar />
</div>
<OnboardingOverlay />
<LocalAIDownloadSnackbar />
+45
View File
@@ -0,0 +1,45 @@
import { useEffect, useRef } from 'react';
import { Gradient } from '../lib/meshGradient';
/**
* Animated WebGL mesh gradient background (Stripe-style).
* Renders behind the dotted-canvas overlay so dots remain visible on top.
*/
export default function MeshGradient() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const gradient = new Gradient();
gradient.initGradient('#mesh-gradient');
return () => {
gradient.disconnect();
gradient.pause();
if (canvas) {
const gl = canvas.getContext('webgl') || canvas.getContext('webgl2');
if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext();
}
}
};
}, []);
return (
<canvas
ref={canvasRef}
id="mesh-gradient"
data-transition-in
className="absolute inset-0 w-full h-full opacity-10"
style={
{
'--gradient-color-1': '#0019d9',
'--gradient-color-2': '#b5d5ff', // primary-50
'--gradient-color-3': '#ffffff', // primary-100
'--gradient-color-4': '#4fa4ff', // primary-200
} as React.CSSProperties
}
/>
);
}
+1 -1
View File
@@ -53,7 +53,7 @@ const OnboardingOverlay = () => {
if (!shouldShow) return null;
return createPortal(
<div className="fixed inset-0 z-[9999] bg-white/95 backdrop-blur-md flex items-center justify-center">
<div className="fixed inset-0 z-[9999] bg-white/50 backdrop-blur-md flex items-center justify-center">
<Onboarding onComplete={handleDone} onDefer={handleDone} />
</div>,
document.body
+12 -10
View File
@@ -180,10 +180,10 @@ const SettingsHome = () => {
];
return (
<div className="overflow-hidden h-full flex flex-col z-10 relative">
<div className="z-10 relative">
<SettingsHeader />
<div className="flex-1 overflow-y-auto">
<div>
{/* Grouped Settings */}
{groupedMenuItems.map((item, index) => (
<SettingsMenuItem
@@ -238,14 +238,16 @@ const SettingsHome = () => {
</div>
<div className="mb-6">
<p className="text-stone-700 text-sm leading-relaxed">
This will sign you out and permanently delete local app data including: App
settings and conversations Email data from Gmail Chat history from Telegram
Cached files from Notion All other skills data
<br />
<br />
This action cannot be undone and may take a few moments to complete.
</p>
<div className="text-stone-700 text-sm leading-relaxed">
<p>This will sign you out and permanently delete local app data including:</p>
<ul className="list-disc pl-5 mt-2 space-y-1">
<li>App settings and conversations</li>
<li>All skills data</li>
<li>Workspace data</li>
<li>All other local data</li>
</ul>
<p className="mt-3">This action cannot be undone.</p>
</div>
{error && (
<div className="mt-3 p-3 rounded-lg bg-coral-100 border border-coral-500/20">
@@ -22,26 +22,24 @@ const SettingsSectionPage = ({ title, description, items }: SettingsSectionPageP
const { navigateBack, navigateToSettings } = useSettingsNavigation();
return (
<div className="overflow-hidden h-full flex flex-col z-10 relative">
<div className="z-10 relative">
<SettingsHeader title={title} showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div className="p-4 space-y-4">
{description && <p className="text-sm text-stone-500 px-1">{description}</p>}
<div>
{description && <p className="mt-1 text-xs text-stone-500 px-5 pb-3">{description}</p>}
<div>
{items.map((item, index) => (
<SettingsMenuItem
key={item.id}
icon={item.icon}
title={item.title}
description={item.description}
onClick={() => navigateToSettings(item.route)}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
))}
</div>
<div>
{items.map((item, index) => (
<SettingsMenuItem
key={item.id}
icon={item.icon}
title={item.title}
description={item.description}
onClick={() => navigateToSettings(item.route)}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
))}
</div>
</div>
</div>
@@ -10,15 +10,19 @@ const SettingsBackButton = ({
className = '',
}: SettingsBackButtonProps) => {
return (
<div className={`bg-stone-50 border-b border-stone-200 p-6 ${className}`}>
<div className={`px-5 pt-5 pb-3 ${className}`}>
<button
onClick={onClick}
className="flex items-center space-x-3 text-stone-900 hover:text-stone-700 transition-colors duration-150"
className="flex items-center space-x-2 text-stone-900 hover:text-stone-700 transition-colors duration-150"
aria-label="Go back">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg
className="w-4 h-4 text-stone-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-lg font-semibold">{title}</span>
<span className="text-sm font-semibold">{title}</span>
</button>
</div>
);
@@ -12,16 +12,16 @@ const SettingsHeader = ({
onBack,
}: SettingsHeaderProps) => {
return (
<div className={`bg-stone-50 border-b border-stone-200 p-3 relative ${className}`}>
<div className={`px-5 pt-5 pb-3 ${className}`}>
<div className="flex items-center">
{/* Back button */}
{showBackButton && onBack && (
<button
onClick={onBack}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-200 transition-colors mr-3"
className="w-6 h-6 flex items-center justify-center rounded-full hover:bg-stone-100 transition-colors mr-2"
aria-label="Go back">
<svg
className="w-5 h-5 opacity-70"
className="w-4 h-4 text-stone-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@ -36,7 +36,7 @@ const SettingsHeader = ({
)}
{/* Title */}
<h2 className="text-lg font-semibold text-stone-900">{title}</h2>
<h2 className="text-sm font-semibold text-stone-900">{title}</h2>
</div>
</div>
);
@@ -16,9 +16,9 @@ const SettingsPanelLayout = ({
className = '',
}: SettingsPanelLayoutProps) => {
return (
<div className={`glass rounded-3xl overflow-hidden h-[600px] flex flex-col ${className}`}>
<div className={`glass rounded-3xl overflow-hidden ${className}`}>
<SettingsBackButton onClick={onBack} title={title} />
<div className="flex-1 overflow-y-auto">{children}</div>
<div>{children}</div>
</div>
);
};
@@ -23,7 +23,6 @@ export type SettingsRoute =
| 'skills'
| 'ai'
| 'local-model'
| 'tauri-commands'
| 'memory-debug'
| 'recovery-phrase'
| 'webhooks-debug'
@@ -81,7 +80,6 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
if (path.includes('/settings/skills')) return 'skills';
if (path.includes('/settings/ai')) return 'ai';
if (path.includes('/settings/local-model')) return 'local-model';
if (path.includes('/settings/tauri-commands')) return 'tauri-commands';
if (path.includes('/settings/memory-debug')) return 'memory-debug';
if (path.includes('/settings/webhooks-debug')) return 'webhooks-debug';
if (path.includes('/settings/recovery-phrase')) return 'recovery-phrase';
@@ -74,12 +74,12 @@ const AIPanel = () => {
};
return (
<div className="h-full flex flex-col">
<div>
<SettingsHeader title="AI Configuration" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<div className="p-4 space-y-4">
<section className="space-y-4">
<h3 className="text-lg font-semibold text-stone-900">AI System Overview</h3>
<h3 className="text-sm font-semibold text-stone-900">AI System Overview</h3>
<p className="text-sm text-stone-500">
Prompt and markdown orchestration is handled in Rust runtime.
</p>
@@ -110,7 +110,7 @@ const AIPanel = () => {
<section className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-stone-900">Local Model Runtime</h3>
<h3 className="text-sm font-semibold text-stone-900">Local Model Runtime</h3>
<div className="flex items-center gap-4">
<button
onClick={() => navigateToSettings('local-model')}
@@ -153,7 +153,7 @@ const AIPanel = () => {
<section className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-stone-900">SOUL Persona Configuration</h3>
<h3 className="text-sm font-semibold text-stone-900">SOUL Persona Configuration</h3>
<button
onClick={() => refreshConfig('soul')}
className="text-sm text-primary-500 hover:text-primary-600 transition-colors disabled:opacity-50"
@@ -218,7 +218,7 @@ const AIPanel = () => {
<section className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-stone-900">TOOLS Configuration</h3>
<h3 className="text-sm font-semibold text-stone-900">TOOLS Configuration</h3>
<button
onClick={() => refreshConfig('tools')}
className="text-sm text-primary-500 hover:text-primary-600 transition-colors disabled:opacity-50"
@@ -108,15 +108,15 @@ const AccessibilityPanel = () => {
const stopDisabled = isStoppingSession || !status?.session.active;
return (
<div className="overflow-hidden h-full flex flex-col z-10 relative">
<div className="z-10 relative">
<SettingsHeader
title="Accessibility Automation"
showBackButton={true}
onBack={navigateBack}
/>
<div className="flex-1 overflow-y-auto max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<div className="max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Permissions</h3>
<PermissionBadge
label="Accessibility"
@@ -183,7 +183,7 @@ const AccessibilityPanel = () => {
)}
</section>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<section className="space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Features</h3>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
@@ -229,7 +229,7 @@ const AccessibilityPanel = () => {
</label>
</section>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<section className="space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Session</h3>
<div className="text-sm text-stone-700 space-y-1">
<div>Status: {status?.session.active ? 'Active' : 'Stopped'}</div>
@@ -281,7 +281,7 @@ const AccessibilityPanel = () => {
</div>
</section>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<section className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-stone-900">Vision Summaries</h3>
<button
@@ -5,42 +5,32 @@ const AdvancedPanel = () => {
const { navigateBack } = useSettingsNavigation();
return (
<div className="overflow-hidden h-full flex flex-col">
<div className="z-10 relative">
<SettingsHeader title="Advanced" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div className="p-4 h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-stone-100 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-stone-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-stone-900 mb-2">Advanced Settings</h3>
<p className="text-stone-500 text-sm max-w-sm mx-auto">
Configure advanced features, developer options, and system-level settings.
</p>
<div className="mt-6">
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-100 text-stone-600 border-stone-200">
Coming Soon
</span>
</div>
<div className="py-10 flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 mx-auto mb-3 bg-stone-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-stone-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<p className="text-xs text-stone-500">Coming soon</p>
</div>
</div>
</div>
@@ -72,12 +72,12 @@ const AgentChatPanel = () => {
};
return (
<div className="h-full flex flex-col">
<div>
<SettingsHeader title="Agent Chat" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<div className="p-4 space-y-4">
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Overrides</h3>
<h3 className="text-sm font-semibold text-stone-900">Overrides</h3>
<p className="text-sm text-stone-400">
Inference uses your OpenHuman backend (config API URL and session). Optional model and
temperature override the defaults for this panel only.
@@ -105,7 +105,7 @@ const AgentChatPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Conversation</h3>
<h3 className="text-sm font-semibold text-stone-900">Conversation</h3>
{error && (
<div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
{error}
@@ -323,10 +323,10 @@ const AutocompletePanel = () => {
}, []);
return (
<div className="overflow-hidden h-full flex flex-col z-10 relative">
<div className="z-10 relative">
<SettingsHeader title="Inline Autocomplete" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto max-w-2xl mx-auto w-full p-4 space-y-4">
<div className="max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Runtime</h3>
<div className="text-sm text-stone-700 space-y-1">
@@ -388,7 +388,7 @@ const BillingPanel = () => {
// ── JSX ─────────────────────────────────────────────────────────────
return (
<div className="overflow-hidden flex flex-col">
<div>
<SettingsHeader
title={teamName ? `Billing — ${teamName}` : 'Billing & Subscription'}
showBackButton={true}
@@ -188,11 +188,11 @@ const ConnectionsPanel = () => {
};
return (
<div className="overflow-hidden h-full flex flex-col">
<div>
<SettingsHeader title="Connections" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div className="p-4 space-y-6">
<div>
<div className="p-4 space-y-4">
{/* Connection Options */}
<div>
{connectOptions.map((option, index) => (
@@ -304,12 +304,12 @@ const CronJobsPanel = () => {
};
return (
<div className="h-full flex flex-col">
<div>
<SettingsHeader title="Cron Jobs" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<section className="rounded-xl border border-stone-200 bg-white p-4 space-y-2">
<h3 className="text-lg font-semibold text-stone-900">Scheduled Jobs</h3>
<div className="p-4 space-y-4">
<section className="space-y-1">
<h3 className="text-sm font-semibold text-stone-900">Scheduled Jobs</h3>
<p className="text-xs text-stone-400">
Manage cron jobs from both the core scheduler and runtime skills.
</p>
@@ -35,22 +35,6 @@ const developerItems = [
</svg>
),
},
{
id: 'tauri-commands',
title: 'Tauri Command Console',
description: 'Run OpenHuman Tauri commands for quick testing',
route: 'tauri-commands',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m2 8H7a2 2 0 01-2-2V6a2 2 0 012-2h6l6 6v8a2 2 0 01-2 2z"
/>
</svg>
),
},
{
id: 'webhooks-debug',
title: 'Webhooks',
@@ -89,25 +73,21 @@ const DeveloperOptionsPanel = () => {
const { navigateToSettings, navigateBack } = useSettingsNavigation();
return (
<div className="overflow-hidden h-full flex flex-col z-10 relative">
<div className="z-10 relative">
<SettingsHeader title="Developer Options" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto max-w-md mx-auto">
<div className="p-4">
<div>
{developerItems.map((item, index) => (
<SettingsMenuItem
key={item.id}
icon={item.icon}
title={item.title}
description={item.description}
onClick={() => navigateToSettings(item.route)}
isFirst={index === 0}
isLast={index === developerItems.length - 1}
/>
))}
</div>
</div>
<div>
{developerItems.map((item, index) => (
<SettingsMenuItem
key={item.id}
icon={item.icon}
title={item.title}
description={item.description}
onClick={() => navigateToSettings(item.route)}
isFirst={index === 0}
isLast={index === developerItems.length - 1}
/>
))}
</div>
</div>
);
@@ -367,13 +367,13 @@ const LocalModelPanel = () => {
};
return (
<div className="h-full flex flex-col">
<div>
<SettingsHeader title="Local Model" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<div className="p-4 space-y-4">
{/* --- Model Tier Selection --- */}
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Model Tier</h3>
<h3 className="text-sm font-semibold text-stone-900">Model Tier</h3>
{/* Loading / error states */}
{presetsLoading && !presetsData && (
@@ -497,7 +497,7 @@ const LocalModelPanel = () => {
<>
<section className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-stone-900">Runtime Status</h3>
<h3 className="text-sm font-semibold text-stone-900">Runtime Status</h3>
<button
onClick={() => void loadStatus()}
className="text-sm text-primary-500 hover:text-primary-600 transition-colors">
@@ -688,7 +688,7 @@ const LocalModelPanel = () => {
<section className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-stone-900">Ollama Diagnostics</h3>
<h3 className="text-sm font-semibold text-stone-900">Ollama Diagnostics</h3>
<button
onClick={async () => {
setIsDiagnosticsLoading(true);
@@ -848,7 +848,7 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Capability Assets</h3>
<h3 className="text-sm font-semibold text-stone-900">Capability Assets</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<div className="text-xs text-stone-500">
Quantization preference: {assets?.quantization ?? 'q4'}
@@ -883,7 +883,7 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Summarization</h3>
<h3 className="text-sm font-semibold text-stone-900">Test Summarization</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={summaryInput}
@@ -911,7 +911,7 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Suggested Prompts</h3>
<h3 className="text-sm font-semibold text-stone-900">Test Suggested Prompts</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={suggestInput}
@@ -949,7 +949,7 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Custom Prompt</h3>
<h3 className="text-sm font-semibold text-stone-900">Test Custom Prompt</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={promptInput}
@@ -994,7 +994,7 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Vision Prompt</h3>
<h3 className="text-sm font-semibold text-stone-900">Test Vision Prompt</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={visionPromptInput}
@@ -1025,7 +1025,7 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Embeddings</h3>
<h3 className="text-sm font-semibold text-stone-900">Test Embeddings</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={embeddingInput}
@@ -1050,7 +1050,7 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Voice Input (STT)</h3>
<h3 className="text-sm font-semibold text-stone-900">Test Voice Input (STT)</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<input
value={audioPathInput}
@@ -1077,7 +1077,7 @@ const LocalModelPanel = () => {
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold text-stone-900">Test Voice Output (TTS)</h3>
<h3 className="text-sm font-semibold text-stone-900">Test Voice Output (TTS)</h3>
<div className="bg-stone-50 rounded-lg border border-stone-200 p-4 space-y-3">
<textarea
value={ttsInput}
@@ -13,8 +13,6 @@ import {
import { MemoryTextWithEntities } from '../../intelligence/MemoryTextWithEntities';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
import { PrimaryButton } from './components/ActionPanel';
import SectionCard from './components/SectionCard';
import { normalizeMemoryDocuments } from './memoryDebugUtils';
const MemoryDebugPanel = () => {
@@ -149,7 +147,7 @@ const MemoryDebugPanel = () => {
if (!ns) return;
const confirmed = window.confirm(
`This will permanently delete ALL documents in namespace "${ns}". This action cannot be undone.\n\nContinue?`
`This will permanently delete ALL documents in namespace "${ns}". Continue?`
);
if (!confirmed) return;
@@ -159,9 +157,9 @@ const MemoryDebugPanel = () => {
try {
const result = await memoryClearNamespace(ns);
if (result.cleared) {
setClearSuccess(`Namespace "${result.namespace}" cleared successfully.`);
setClearSuccess(`Namespace "${result.namespace}" cleared.`);
} else {
setClearSuccess(`Clear request completed for "${result.namespace}" (nothing to clear).`);
setClearSuccess(`Nothing to clear in "${result.namespace}".`);
}
await refreshAll();
} catch (error) {
@@ -172,261 +170,205 @@ const MemoryDebugPanel = () => {
}, [clearNamespaceInput, refreshAll]);
return (
<div className="overflow-hidden h-full flex flex-col">
<div>
<SettingsHeader title="Memory Debug" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<SectionCard
title="Documents"
priority="tools"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m2 8H7a2 2 0 01-2-2V6a2 2 0 012-2h6l6 6v8a2 2 0 01-2 2z"
/>
</svg>
}>
<div className="space-y-3">
<PrimaryButton onClick={() => void loadDocuments()} loading={documentsLoading}>
Refresh Documents
</PrimaryButton>
<label className="block text-xs text-stone-300">
Namespace Filter (optional)
<input
value={documentsNamespaceFilter}
onChange={e => setDocumentsNamespaceFilter(e.target.value)}
className="mt-1 w-full rounded border border-stone-600 bg-black/30 px-3 py-2 text-sm text-white"
placeholder="e.g. conversations"
/>
</label>
{documentsError && (
<div className="text-xs text-coral-300 border border-coral-500/30 bg-coral-500/10 rounded p-2">
{documentsError}
</div>
)}
{documents.length === 0 && !documentsLoading ? (
<div className="text-xs text-stone-400">
No structured documents found. Raw response is shown below.
</div>
) : (
<div className="space-y-2">
{documents.map(doc => (
<div
key={`${doc.namespace}:${doc.documentId}`}
className="rounded border border-stone-700 bg-black/20 p-2">
<div className="text-xs text-white break-all">ID: {doc.documentId}</div>
<div className="text-xs text-stone-300 break-all">
Namespace: {doc.namespace}
</div>
{doc.title ? (
<div className="text-xs text-stone-400">Title: {doc.title}</div>
) : null}
<div className="pt-2">
<PrimaryButton
variant="outline"
loading={deleteLoadingId === doc.documentId}
disabled={Boolean(deleteLoadingId)}
onClick={() => void handleDelete(doc)}
className="px-3 py-1.5 text-xs">
Delete
</PrimaryButton>
<div className="p-4 space-y-5">
{/* Documents */}
<section className="space-y-2">
<h3 className="text-sm font-semibold text-stone-900">Documents</h3>
<div className="flex gap-2">
<input
value={documentsNamespaceFilter}
onChange={e => setDocumentsNamespaceFilter(e.target.value)}
className="flex-1 rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs text-stone-700 placeholder:text-stone-400"
placeholder="Filter by namespace..."
/>
<button
type="button"
onClick={() => void loadDocuments()}
disabled={documentsLoading}
className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs font-medium text-stone-700 hover:bg-stone-100 disabled:opacity-50">
{documentsLoading ? '...' : 'Refresh'}
</button>
</div>
{documentsError && (
<div className="rounded-lg border border-coral-200 bg-coral-50 px-3 py-2 text-xs text-coral-700">
{documentsError}
</div>
)}
{documents.length === 0 && !documentsLoading ? (
<p className="text-xs text-stone-400">No documents found.</p>
) : (
<div className="space-y-1">
{documents.map(doc => (
<div
key={`${doc.namespace}:${doc.documentId}`}
className="flex items-start justify-between gap-2 rounded-lg border border-stone-200 bg-stone-50 p-2">
<div className="min-w-0">
<div className="text-xs font-medium text-stone-900 break-all">
{doc.documentId}
</div>
<div className="text-[11px] text-stone-500 break-all">{doc.namespace}</div>
{doc.title && <div className="text-[11px] text-stone-400">{doc.title}</div>}
</div>
))}
</div>
)}
<details className="text-xs">
<summary className="cursor-pointer text-stone-300">Raw documents response</summary>
<pre className="mt-2 rounded border border-stone-700 bg-black/20 p-2 overflow-auto text-[11px] leading-5">
{JSON.stringify(documentsRaw, null, 2)}
</pre>
</details>
</div>
</SectionCard>
<SectionCard
title="Namespaces"
priority="tools"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 7h16M4 12h16M4 17h16"
/>
</svg>
}>
<div className="space-y-3">
<PrimaryButton onClick={() => void loadNamespaces()} loading={namespacesLoading}>
Refresh Namespaces
</PrimaryButton>
{namespacesError && (
<div className="text-xs text-coral-300 border border-coral-500/30 bg-coral-500/10 rounded p-2">
{namespacesError}
</div>
)}
<div className="rounded border border-stone-700 bg-black/20 p-2 text-xs">
{namespaces.length > 0 ? namespaces.join('\n') : 'No namespaces found.'}
<button
type="button"
disabled={Boolean(deleteLoadingId)}
onClick={() => void handleDelete(doc)}
className="shrink-0 rounded border border-stone-200 px-2 py-1 text-[10px] text-stone-500 hover:bg-stone-100 disabled:opacity-50">
{deleteLoadingId === doc.documentId ? '...' : 'Delete'}
</button>
</div>
))}
</div>
)}
<details className="text-xs">
<summary className="cursor-pointer text-stone-400">Raw response</summary>
<pre className="mt-1 max-h-32 overflow-auto rounded-lg border border-stone-200 bg-stone-950 p-2 text-[11px] text-stone-100 whitespace-pre-wrap break-words">
{JSON.stringify(documentsRaw, null, 2)}
</pre>
</details>
</section>
{/* Namespaces */}
<section className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-stone-900">Namespaces</h3>
<button
type="button"
onClick={() => void loadNamespaces()}
disabled={namespacesLoading}
className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-1 text-[11px] font-medium text-stone-600 hover:bg-stone-100 disabled:opacity-50">
{namespacesLoading ? '...' : 'Refresh'}
</button>
</div>
</SectionCard>
<SectionCard
title="Clear Namespace"
priority="tools"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
}>
<div className="space-y-3">
<p className="text-xs text-stone-400">
Delete all documents within a namespace. This is a destructive operation and cannot be
undone.
</p>
<label className="block text-xs text-stone-300">
Namespace
{namespaces.length > 0 ? (
<select
value={clearNamespaceInput}
onChange={e => setClearNamespaceInput(e.target.value)}
className="mt-1 w-full rounded border border-stone-600 bg-black/30 px-3 py-2 text-sm text-white">
<option value="">-- select a namespace --</option>
{namespaces.map(ns => (
<option key={ns} value={ns}>
{ns}
</option>
))}
</select>
) : (
<input
value={clearNamespaceInput}
onChange={e => setClearNamespaceInput(e.target.value)}
className="mt-1 w-full rounded border border-stone-600 bg-black/30 px-3 py-2 text-sm text-white"
placeholder="e.g. skill:gmail:user@example.com"
/>
)}
</label>
<PrimaryButton
variant="outline"
onClick={() => void handleClearNamespace()}
loading={clearLoading}
disabled={!clearNamespaceInput.trim()}
className="border-coral-500/50 text-coral-300 hover:bg-coral-500/10">
Clear Namespace
</PrimaryButton>
{clearSuccess && (
<div className="text-xs text-sage-300 border border-sage-500/30 bg-sage-500/10 rounded p-2">
{clearSuccess}
</div>
)}
{clearError && (
<div className="text-xs text-coral-300 border border-coral-500/30 bg-coral-500/10 rounded p-2">
{clearError}
</div>
)}
</div>
</SectionCard>
<SectionCard
title="Query & Recall"
priority="tools"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 10h.01M12 10h.01M16 10h.01M9 16h6M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
}>
<div className="space-y-3">
<label className="block text-xs text-stone-300">
Namespace
<input
value={namespaceInput}
onChange={e => setNamespaceInput(e.target.value)}
className="mt-1 w-full rounded border border-stone-600 bg-black/30 px-3 py-2 text-sm text-white"
placeholder="e.g. conversations"
/>
</label>
<label className="block text-xs text-stone-300">
Query
<textarea
value={queryInput}
onChange={e => setQueryInput(e.target.value)}
className="mt-1 w-full rounded border border-stone-600 bg-black/30 px-3 py-2 text-sm text-white"
rows={3}
placeholder="What do I remember about..."
/>
</label>
<label className="block text-xs text-stone-300">
Max Chunks
<input
value={maxChunksInput}
onChange={e => setMaxChunksInput(e.target.value)}
className="mt-1 w-full rounded border border-stone-600 bg-black/30 px-3 py-2 text-sm text-white"
/>
</label>
<div className="flex flex-wrap gap-2">
<PrimaryButton
onClick={() => void handleQuery()}
loading={queryLoading}
disabled={!namespaceInput.trim() || !queryInput.trim()}>
Run Query
</PrimaryButton>
<PrimaryButton
variant="secondary"
onClick={() => void handleRecall()}
loading={recallLoading}
disabled={!namespaceInput.trim()}>
Run Recall
</PrimaryButton>
{namespacesError && (
<div className="rounded-lg border border-coral-200 bg-coral-50 px-3 py-2 text-xs text-coral-700">
{namespacesError}
</div>
)}
{namespaces.length > 0 ? (
<div className="flex flex-wrap gap-1">
{namespaces.map(ns => (
<span
key={ns}
className="rounded-full bg-stone-100 px-2 py-0.5 text-[11px] text-stone-600">
{ns}
</span>
))}
</div>
) : (
<p className="text-xs text-stone-400">No namespaces found.</p>
)}
</section>
{queryError && (
<div className="text-xs text-coral-300 border border-coral-500/30 bg-coral-500/10 rounded p-2">
Query error: {queryError}
</div>
)}
{recallError && (
<div className="text-xs text-coral-300 border border-coral-500/30 bg-coral-500/10 rounded p-2">
Recall error: {recallError}
</div>
)}
{/* Query & Recall */}
<section className="space-y-2">
<h3 className="text-sm font-semibold text-stone-900">Query & Recall</h3>
<input
value={namespaceInput}
onChange={e => setNamespaceInput(e.target.value)}
className="w-full rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs text-stone-700 placeholder:text-stone-400"
placeholder="Namespace"
/>
<textarea
value={queryInput}
onChange={e => setQueryInput(e.target.value)}
className="w-full rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs text-stone-700 placeholder:text-stone-400"
rows={2}
placeholder="Query text..."
/>
<div className="flex items-center gap-2">
<input
value={maxChunksInput}
onChange={e => setMaxChunksInput(e.target.value)}
className="w-16 rounded-lg border border-stone-200 bg-stone-50 px-2 py-1.5 text-xs text-stone-700"
placeholder="10"
/>
<span className="text-[11px] text-stone-400">max chunks</span>
<div className="flex-1" />
<button
type="button"
onClick={() => void handleQuery()}
disabled={queryLoading || !namespaceInput.trim() || !queryInput.trim()}
className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs font-medium text-stone-700 hover:bg-stone-100 disabled:opacity-50">
{queryLoading ? '...' : 'Query'}
</button>
<button
type="button"
onClick={() => void handleRecall()}
disabled={recallLoading || !namespaceInput.trim()}
className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs font-medium text-stone-700 hover:bg-stone-100 disabled:opacity-50">
{recallLoading ? '...' : 'Recall'}
</button>
</div>
{queryError && <div className="text-xs text-coral-600">Query: {queryError}</div>}
{recallError && <div className="text-xs text-coral-600">Recall: {recallError}</div>}
{(queryResult || recallResult) && (
<div className="space-y-2">
<div className="text-xs text-stone-400">Query response</div>
<MemoryTextWithEntities
text={queryResult?.text ?? ''}
entities={queryResult?.entities}
className="rounded border border-stone-700 bg-black/20 p-2 overflow-auto text-[11px] leading-5 min-h-16 whitespace-pre-wrap"
/>
<div className="text-xs text-stone-400">Recall response</div>
<MemoryTextWithEntities
text={recallResult?.text ?? ''}
entities={recallResult?.entities}
className="rounded border border-stone-700 bg-black/20 p-2 overflow-auto text-[11px] leading-5 min-h-16 whitespace-pre-wrap"
/>
{queryResult && (
<div>
<div className="text-[11px] font-medium text-stone-500 mb-1">Query result</div>
<MemoryTextWithEntities
text={queryResult.text ?? ''}
entities={queryResult.entities}
className="rounded-lg border border-stone-200 bg-stone-50 p-2 text-[11px] leading-5 min-h-12 whitespace-pre-wrap"
/>
</div>
)}
{recallResult && (
<div>
<div className="text-[11px] font-medium text-stone-500 mb-1">Recall result</div>
<MemoryTextWithEntities
text={recallResult.text ?? ''}
entities={recallResult.entities}
className="rounded-lg border border-stone-200 bg-stone-50 p-2 text-[11px] leading-5 min-h-12 whitespace-pre-wrap"
/>
</div>
)}
</div>
)}
</section>
{/* Clear Namespace */}
<section className="space-y-2">
<h3 className="text-sm font-semibold text-stone-900">Clear Namespace</h3>
<p className="text-xs text-stone-400">
Permanently delete all documents within a namespace.
</p>
<div className="flex gap-2">
{namespaces.length > 0 ? (
<select
value={clearNamespaceInput}
onChange={e => setClearNamespaceInput(e.target.value)}
className="flex-1 rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs text-stone-700">
<option value="">Select namespace...</option>
{namespaces.map(ns => (
<option key={ns} value={ns}>
{ns}
</option>
))}
</select>
) : (
<input
value={clearNamespaceInput}
onChange={e => setClearNamespaceInput(e.target.value)}
className="flex-1 rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 text-xs text-stone-700 placeholder:text-stone-400"
placeholder="e.g. skill:gmail:user@example.com"
/>
)}
<button
type="button"
onClick={() => void handleClearNamespace()}
disabled={clearLoading || !clearNamespaceInput.trim()}
className="rounded-lg border border-coral-200 bg-coral-50 px-3 py-1.5 text-xs font-medium text-coral-600 hover:bg-coral-100 disabled:opacity-50">
{clearLoading ? '...' : 'Clear'}
</button>
</div>
</SectionCard>
{clearSuccess && <div className="text-xs text-sage-600">{clearSuccess}</div>}
{clearError && <div className="text-xs text-coral-600">{clearError}</div>}
</section>
</div>
</div>
);
@@ -257,10 +257,10 @@ const MessagingPanel = () => {
const displayError = error || loadError;
return (
<div className="overflow-hidden h-full flex flex-col">
<div>
<SettingsHeader title="Messaging" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="p-4 space-y-4">
{/* Default channel selector */}
<section className="rounded-xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Default Messaging Channel</h3>
@@ -17,11 +17,11 @@ const PrivacyPanel = () => {
};
return (
<div className="overflow-hidden h-full flex flex-col">
<div>
<SettingsHeader title="Privacy & Security" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div className="p-4 space-y-6">
<div>
<div className="p-4 space-y-4">
{/* Analytics Section */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-3 px-1">
@@ -5,36 +5,26 @@ const ProfilePanel = () => {
const { navigateBack } = useSettingsNavigation();
return (
<div className="overflow-hidden h-full flex flex-col">
<div>
<SettingsHeader title="Profile" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div className="p-4 h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-stone-100 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-stone-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-stone-900 mb-2">Profile Settings</h3>
<p className="text-stone-500 text-sm max-w-sm mx-auto">
Update your profile information, avatar, and personal preferences.
</p>
<div className="mt-6">
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-100 text-stone-600 border-stone-200">
Coming Soon
</span>
</div>
<div className="py-10 flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 mx-auto mb-3 bg-stone-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-stone-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
<p className="text-xs text-stone-500">Coming soon</p>
</div>
</div>
</div>
@@ -200,10 +200,10 @@ const RecoveryPhrasePanel = () => {
const canSave = mode === 'generate' ? confirmed : isImportComplete;
return (
<div className="overflow-hidden h-full flex flex-col">
<div>
<SettingsHeader title="Recovery Phrase" showBackButton onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div>
<div className="p-4 max-w-md mx-auto">
{success ? (
<div className="flex flex-col items-center justify-center gap-3 py-12">
@@ -161,11 +161,11 @@ const ScreenIntelligencePanel = () => {
};
return (
<div className="overflow-hidden h-full flex flex-col z-10 relative">
<div className="z-10 relative">
<SettingsHeader title="Screen Intelligence" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<div className="max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Permissions</h3>
<PermissionBadge
label="Screen Recording"
@@ -237,7 +237,7 @@ const ScreenIntelligencePanel = () => {
)}
</section>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<section className="space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Screen Intelligence Policy</h3>
<label className="flex items-center justify-between rounded-xl border border-stone-200 bg-stone-50 px-3 py-2">
@@ -351,7 +351,7 @@ const ScreenIntelligencePanel = () => {
</label>
</section>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<section className="space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Session</h3>
<div className="text-sm text-stone-600 space-y-1">
<div>Status: {status?.session.active ? 'Active' : 'Stopped'}</div>
@@ -403,7 +403,7 @@ const ScreenIntelligencePanel = () => {
</div>
</section>
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<section className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-stone-900">Vision Summaries</h3>
<button
@@ -457,7 +457,7 @@ const DebugSection = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<section className="space-y-3">
<button
type="button"
onClick={() => setIsOpen(prev => !prev)}
@@ -68,13 +68,13 @@ const SkillsPanel = () => {
}, [integrations]);
return (
<div className="h-full flex flex-col">
<div>
<SettingsHeader title="Skills" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
<section className="rounded-xl border border-stone-200 bg-white p-4 space-y-3">
<div className="p-4 space-y-4">
<section className="space-y-3">
<div>
<h3 className="text-lg font-semibold text-stone-900">Browser Access</h3>
<h3 className="text-sm font-semibold text-stone-900">Browser Access</h3>
<p className="text-xs text-stone-400">
Allow the browser tool to visit any public domain (private and file URLs are still
blocked).
@@ -538,10 +538,10 @@ const TauriCommandsPanel = () => {
// };
return (
<div className="h-full flex flex-col">
<div>
<SettingsHeader title="Tauri Command Console" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto px-6 pb-12 space-y-10">
<div className="p-4 space-y-4">
{!tauriAvailable && (
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-200">
Tauri runtime not detected. Commands will fail in browser mode.
@@ -101,10 +101,10 @@ const TeamInvitesPanel = () => {
};
return (
<div className="overflow-hidden flex flex-col h-full">
<div>
<SettingsHeader title="Invites" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div>
<div className="max-w-md mx-auto p-4 space-y-4">
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3">
@@ -333,7 +333,7 @@ const TeamInvitesPanel = () => {
{inviteToRevoke && (
<div className="fixed inset-0 bg-stone-900/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Revoke Invite Code</h3>
<h3 className="text-sm font-semibold text-stone-900 mb-4">Revoke Invite Code</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -88,7 +88,7 @@ const TeamManagementPanel = () => {
if (!teamEntry) {
return (
<div className="overflow-hidden flex flex-col h-full">
<div className="">
<SettingsHeader title="Team Management" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-stone-500">Team not found</p>
@@ -99,7 +99,7 @@ const TeamManagementPanel = () => {
if (!isAdmin) {
return (
<div className="overflow-hidden flex flex-col h-full">
<div className="">
<SettingsHeader title="Team Management" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-stone-500">Access denied</p>
@@ -111,10 +111,10 @@ const TeamManagementPanel = () => {
const { team } = teamEntry;
return (
<div className="overflow-hidden flex flex-col h-full">
<div className="">
<SettingsHeader title={`Manage ${team.name}`} showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div>
<div className="max-w-md mx-auto p-4 space-y-4">
{/* Team Info */}
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
@@ -125,7 +125,7 @@ const TeamManagementPanel = () => {
</span>
</div>
<div>
<h3 className="text-lg font-semibold text-stone-900">{team.name}</h3>
<h3 className="text-sm font-semibold text-stone-900">{team.name}</h3>
<p className="text-xs text-stone-500">
{team.subscription.plan} Plan Created{' '}
{new Date(team.createdAt).toLocaleDateString()}
@@ -291,7 +291,7 @@ const TeamManagementPanel = () => {
{isEditModalOpen && (
<div className="fixed inset-0 bg-stone-900/40 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Edit Team Settings</h3>
<h3 className="text-sm font-semibold text-stone-900 mb-4">Edit Team Settings</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -337,7 +337,7 @@ const TeamManagementPanel = () => {
{isDeleteModalOpen && (
<div className="fixed inset-0 bg-stone-900/40 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Delete Team</h3>
<h3 className="text-sm font-semibold text-stone-900 mb-4">Delete Team</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -113,10 +113,10 @@ const TeamMembersPanel = () => {
};
return (
<div className="overflow-hidden flex flex-col h-full">
<div>
<SettingsHeader title="Members" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div>
<div className="max-w-md mx-auto p-4 space-y-3">
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3">
@@ -257,7 +257,7 @@ const TeamMembersPanel = () => {
{memberToRemove && (
<div className="fixed inset-0 bg-stone-900/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Remove Team Member</h3>
<h3 className="text-sm font-semibold text-stone-900 mb-4">Remove Team Member</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -300,7 +300,7 @@ const TeamMembersPanel = () => {
{roleChangeConfirmation && (
<div className="fixed inset-0 bg-stone-900/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Change Member Role</h3>
<h3 className="text-sm font-semibold text-stone-900 mb-4">Change Member Role</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -231,10 +231,10 @@ const TeamPanel = () => {
};
return (
<div className="overflow-hidden flex flex-col h-full">
<div>
<SettingsHeader title="Team" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto">
<div>
<div className="max-w-md mx-auto p-4 space-y-4">
{/* Error banner */}
{error && (
@@ -331,7 +331,7 @@ const TeamPanel = () => {
{teamToLeave && (
<div className="fixed inset-0 bg-stone-900/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md border border-stone-200">
<h3 className="text-lg font-semibold text-stone-900 mb-4">Leave Team</h3>
<h3 className="text-sm font-semibold text-stone-900 mb-4">Leave Team</h3>
{error && (
<div className="rounded-xl bg-coral-500/10 border border-coral-500/20 p-3 mb-4">
@@ -13,8 +13,6 @@ import {
} from '../../../utils/tauriCommands';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
import { PrimaryButton } from './components/ActionPanel';
import SectionCard from './components/SectionCard';
const LOG_LIMIT = 100;
@@ -141,215 +139,135 @@ const WebhooksDebugPanel = () => {
}, [loadData]);
return (
<div className="overflow-hidden h-full flex flex-col">
<div>
<SettingsHeader title="Webhooks Debug" showBackButton={true} onBack={navigateBack} />
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<SectionCard
title="Overview"
priority="development"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 9l4-4 4 4m0 6l-4 4-4-4M3 12h18"
/>
</svg>
}>
<div className="flex flex-wrap items-center gap-3">
<PrimaryButton onClick={() => void loadData()} loading={loading} variant="secondary">
Refresh
</PrimaryButton>
<PrimaryButton
onClick={() => void handleClearLogs()}
loading={clearing}
disabled={logs.length === 0}
variant="outline">
Clear Logs
</PrimaryButton>
<div className="rounded-lg border border-stone-200 bg-white px-3 py-2 text-sm text-stone-700">
Registered:{' '}
<span className="font-semibold text-stone-900">{registrations.length}</span>
</div>
<div className="rounded-lg border border-stone-200 bg-white px-3 py-2 text-sm text-stone-700">
Captured: <span className="font-semibold text-stone-900">{logs.length}</span>
</div>
<div className="rounded-lg border border-stone-200 bg-white px-3 py-2 text-sm text-stone-700">
Live:{' '}
<span
className={isLive ? 'font-semibold text-sage-700' : 'font-semibold text-stone-500'}>
{isLive ? 'connected' : 'disconnected'}
</span>
</div>
<div className="p-4 space-y-5">
{/* Status bar */}
<div className="flex flex-wrap items-center gap-2 text-xs">
<button
type="button"
onClick={() => void loadData()}
disabled={loading}
className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 font-medium text-stone-700 hover:bg-stone-100 disabled:opacity-50">
{loading ? 'Loading...' : 'Refresh'}
</button>
<button
type="button"
onClick={() => void handleClearLogs()}
disabled={clearing || logs.length === 0}
className="rounded-lg border border-stone-200 bg-stone-50 px-3 py-1.5 font-medium text-stone-700 hover:bg-stone-100 disabled:opacity-50">
{clearing ? 'Clearing...' : 'Clear Logs'}
</button>
<span className="text-stone-500">
{registrations.length} registered &middot; {logs.length} captured &middot;{' '}
<span className={isLive ? 'text-sage-600' : 'text-stone-400'}>
{isLive ? 'live' : 'disconnected'}
</span>
</span>
</div>
{error && (
<div className="rounded-lg border border-coral-200 bg-coral-50 px-3 py-2 text-xs text-coral-700">
{error}
</div>
)}
{lastEvent && (
<div className="rounded-lg border border-stone-200 bg-white px-3 py-2 text-xs text-stone-600">
Last event: <span className="font-medium text-stone-900">{lastEvent.event_type}</span>{' '}
at {formatDateTime(lastEvent.timestamp)}
</div>
)}
{lastEvent && (
<div className="text-xs text-stone-500">
Last event: <span className="font-medium text-stone-700">{lastEvent.event_type}</span>{' '}
at {formatDateTime(lastEvent.timestamp)}
</div>
)}
{error && (
<div className="rounded-lg border border-coral-500/40 bg-coral-50 px-3 py-2 text-sm text-coral-700">
{error}
</div>
)}
</SectionCard>
<SectionCard
title="Registered Webhooks"
priority="tools"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13.828 10.172a4 4 0 010 5.656l-2 2a4 4 0 01-5.656-5.656l1-1m5-5a4 4 0 015.656 5.656l-1 1m-5 5l5-5"
/>
</svg>
}>
{/* Registrations */}
<section className="space-y-2">
<h3 className="text-sm font-semibold text-stone-900">Registered Webhooks</h3>
{registrations.length === 0 ? (
<div className="rounded-lg border border-dashed border-stone-300 bg-white px-4 py-6 text-sm text-stone-500">
No webhook registrations are active in the runtime.
</div>
<p className="text-xs text-stone-400">No active registrations.</p>
) : (
<div className="space-y-3">
<div className="space-y-2">
{registrations.map(registration => (
<div
key={registration.tunnel_uuid}
className="rounded-xl border border-stone-200 bg-white p-4">
className="rounded-xl border border-stone-200 bg-stone-50 p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<div className="text-sm font-semibold text-stone-900">
{registration.tunnel_name || registration.tunnel_uuid}
</div>
<div className="text-xs text-stone-500">{registration.tunnel_uuid}</div>
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="rounded-full bg-stone-100 px-3 py-1 font-medium text-stone-700">
<span className="text-xs font-semibold text-stone-900">
{registration.tunnel_name || registration.tunnel_uuid}
</span>
<div className="flex gap-1 text-[10px]">
<span className="rounded-full bg-stone-200 px-2 py-0.5 text-stone-600">
{registration.target_kind}
</span>
<span className="rounded-full bg-stone-100 px-3 py-1 font-medium text-stone-700">
<span className="rounded-full bg-stone-200 px-2 py-0.5 text-stone-600">
{registration.skill_id}
</span>
</div>
</div>
<div className="mt-3 grid gap-2 text-xs text-stone-600">
<div>
Target URL:{' '}
<span className="font-mono text-stone-900">
{tunnelsApi.ingressUrl(fallbackBackendUrl, registration.tunnel_uuid)}
</span>
</div>
{registration.backend_tunnel_id && (
<div>
Backend tunnel ID:{' '}
<span className="font-mono text-stone-900">
{registration.backend_tunnel_id}
</span>
</div>
)}
<div className="mt-1 text-[11px] text-stone-500 font-mono break-all">
{tunnelsApi.ingressUrl(fallbackBackendUrl, registration.tunnel_uuid)}
</div>
</div>
))}
</div>
)}
</SectionCard>
</section>
<SectionCard
title="Captured Requests"
priority="development"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
}>
{/* Captured Requests */}
<section className="space-y-2">
<h3 className="text-sm font-semibold text-stone-900">Captured Requests</h3>
{logs.length === 0 ? (
<div className="rounded-lg border border-dashed border-stone-300 bg-white px-4 py-6 text-sm text-stone-500">
No webhook requests captured yet.
</div>
<p className="text-xs text-stone-400">No webhook requests captured yet.</p>
) : (
<div className="grid gap-4 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.25fr)]">
<div className="space-y-2">
{logs.map(entry => (
<button
key={entry.correlation_id}
type="button"
onClick={() => setSelectedCorrelationId(entry.correlation_id)}
className={`w-full rounded-xl border p-3 text-left transition-colors ${
selectedLog?.correlation_id === entry.correlation_id
? 'border-primary-300 bg-primary-50'
: 'border-stone-200 bg-white hover:bg-stone-50'
}`}>
<div className="flex items-center justify-between gap-2">
<div className="text-sm font-semibold text-stone-900">
{entry.method} {entry.path}
</div>
<div className="text-xs font-medium text-stone-500">
{entry.status_code ?? '...'}
</div>
</div>
<div className="mt-1 text-xs text-stone-500">
{entry.tunnel_name} {entry.skill_id ? `${entry.skill_id}` : '• unrouted'}
</div>
<div className="mt-1 text-xs text-stone-500">
{formatDateTime(entry.updated_at)}
</div>
</button>
))}
</div>
<div className="space-y-2">
{logs.map(entry => (
<button
key={entry.correlation_id}
type="button"
onClick={() => setSelectedCorrelationId(entry.correlation_id)}
className={`w-full rounded-xl border p-3 text-left transition-colors ${
selectedLog?.correlation_id === entry.correlation_id
? 'border-primary-300 bg-primary-50'
: 'border-stone-200 bg-stone-50 hover:bg-stone-100'
}`}>
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-semibold text-stone-900">
{entry.method} {entry.path}
</span>
<span className="text-[10px] text-stone-500">{entry.status_code ?? '...'}</span>
</div>
<div className="mt-1 text-[11px] text-stone-500">
{entry.tunnel_name} {entry.skill_id ? `· ${entry.skill_id}` : '· unrouted'} ·{' '}
{formatDateTime(entry.updated_at)}
</div>
</button>
))}
{selectedLog && (
<div className="rounded-xl border border-stone-200 bg-white p-4 space-y-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<div className="text-lg font-semibold text-stone-900">
{selectedLog.method} {selectedLog.path}
</div>
<div className="text-xs text-stone-500">{selectedLog.correlation_id}</div>
<div className="rounded-xl border border-stone-200 bg-stone-50 p-3 space-y-3">
<div>
<div className="text-xs font-semibold text-stone-900">
{selectedLog.method} {selectedLog.path}
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="rounded-full bg-stone-100 px-3 py-1 font-medium text-stone-700">
stage: {selectedLog.stage}
</span>
<span className="rounded-full bg-stone-100 px-3 py-1 font-medium text-stone-700">
status: {selectedLog.status_code ?? 'pending'}
</span>
<div className="text-[10px] text-stone-400 font-mono">
{selectedLog.correlation_id}
</div>
</div>
<div className="grid gap-2 text-sm text-stone-700">
<div>
Tunnel:{' '}
<span className="font-medium text-stone-900">{selectedLog.tunnel_name}</span>
</div>
<div>
Tunnel UUID:{' '}
<span className="font-mono text-xs text-stone-900">
{selectedLog.tunnel_uuid}
</span>
</div>
<div>
Skill:{' '}
<span className="font-medium text-stone-900">
{selectedLog.skill_id || 'unrouted'}
</span>
</div>
<div>Received: {formatDateTime(selectedLog.timestamp)}</div>
<div>Updated: {formatDateTime(selectedLog.updated_at)}</div>
<div className="flex flex-wrap gap-1 text-[10px]">
<span className="rounded-full bg-stone-200 px-2 py-0.5 text-stone-600">
{selectedLog.stage}
</span>
<span className="rounded-full bg-stone-200 px-2 py-0.5 text-stone-600">
{selectedLog.status_code ?? 'pending'}
</span>
<span className="rounded-full bg-stone-200 px-2 py-0.5 text-stone-600">
{selectedLog.skill_id || 'unrouted'}
</span>
</div>
{selectedLog.error_message && (
<div className="rounded-lg border border-coral-500/40 bg-coral-50 px-3 py-2 text-sm text-coral-700">
<div className="rounded-lg border border-coral-200 bg-coral-50 px-3 py-2 text-xs text-coral-700">
{selectedLog.error_message}
</div>
)}
@@ -381,7 +299,7 @@ const WebhooksDebugPanel = () => {
)}
</div>
)}
</SectionCard>
</section>
</div>
</div>
);
@@ -389,14 +307,14 @@ const WebhooksDebugPanel = () => {
function PayloadBlock({ title, value }: { title: string; value: string }) {
return (
<div>
<div className="mb-1 text-xs font-semibold uppercase tracking-wide text-stone-500">
<details className="text-xs">
<summary className="cursor-pointer font-semibold text-stone-500 uppercase tracking-wide text-[10px]">
{title}
</div>
<pre className="max-h-56 overflow-auto rounded-lg border border-stone-200 bg-stone-950 p-3 text-xs text-stone-100 whitespace-pre-wrap break-words">
</summary>
<pre className="mt-1 max-h-40 overflow-auto rounded-lg border border-stone-200 bg-stone-950 p-2 text-[11px] text-stone-100 whitespace-pre-wrap break-words">
{value}
</pre>
</div>
</details>
);
}
+37 -30
View File
@@ -15,7 +15,7 @@
import { useState, useEffect, useCallback } from "react";
import { useSkillSnapshot } from "../../lib/skills/hooks.ts";
import { skillManager } from "../../lib/skills/manager.ts";
import { getSkillSnapshot, listAvailable, setSetupComplete, startSkill } from "../../lib/skills/skillsApi.ts";
import { getSkillSnapshot, installSkill, listAvailable, setSetupComplete, startSkill } from "../../lib/skills/skillsApi.ts";
import { callCoreRpc } from "../../services/coreRpcClient.ts";
import { apiClient } from "../../services/apiClient.ts";
import { openUrl } from "../../utils/openUrl.ts";
@@ -664,32 +664,32 @@ function OAuthLoginView({
onCancel,
waiting,
skillId,
onManualComplete,
onManualComplete: _onManualComplete,
}: OAuthLoginViewProps) {
const providerName = formatProviderName(provider);
const [devIntegrationId, setDevIntegrationId] = useState("");
const [devEncryptionKey, setDevEncryptionKey] = useState("");
const [devSubmitting, setDevSubmitting] = useState(false);
const [devError, setDevError] = useState("");
const handleDevManualComplete = useCallback(async () => {
const id = devIntegrationId.trim();
const encKey = devEncryptionKey.trim();
if (!id || !skillId) return;
if (!encKey) { setDevError("Encryption key is required"); return; }
if (/["\\]/.test(encKey) || encKey !== encKey.replace(/[^\x20-\x7e]/g, "")) { setDevError("Encryption key contains invalid characters"); return; }
setDevSubmitting(true);
setDevError("");
try {
// 1. Fetch client key share (one-time handoff)
let clientKeyShare: string | undefined;
try {
const result = await callCoreRpc<{ result: { clientKey: string } }>({
method: "openhuman.auth.oauth_fetch_client_key",
params: { integrationId: id },
});
clientKeyShare = result?.result?.clientKey;
} catch {
// May be plaintext OAuth — continue without key
}
// 1. Use the manually-provided client-side encryption key
const clientKeyShare: string | undefined = encKey || undefined;
// 2. Start skill runtime
// 2. Ensure skill is installed, then start the runtime
try {
await installSkill(skillId);
} catch {
// May already be installed
}
try {
await startSkill(skillId);
} catch (startErr) {
@@ -710,19 +710,17 @@ function OAuthLoginView({
}
if (!running) throw new Error("Skill did not reach running state within timeout");
// 4. Notify skill of OAuth completion
// 4. Notify skill of OAuth completion — the snapshot effect will
// detect the connected state and transition to setup automatically.
await skillManager.notifyOAuthComplete(skillId, id, provider, {
clientKeyShare,
});
// 5. Advance wizard
onManualComplete?.();
} catch (err) {
setDevError(err instanceof Error ? err.message : String(err));
} finally {
setDevSubmitting(false);
}
}, [devIntegrationId, skillId, provider, onManualComplete]);
}, [devIntegrationId, devEncryptionKey, skillId, provider]);
return (
<div className="py-6">
@@ -781,26 +779,35 @@ function OAuthLoginView({
Open login page again
</button>
{/* Dev-mode: manual integration ID entry */}
{/* Dev-mode: manual integration ID + encryption key entry */}
{IS_DEV && skillId && (
<div className="mt-4 pt-4 border-t border-stone-700/50">
<p className="text-[10px] text-stone-500 uppercase tracking-wider mb-2">Dev mode</p>
<div className="flex gap-2">
<div className="flex flex-col gap-2">
<input
type="text"
placeholder="Integration ID (24-char hex)"
value={devIntegrationId}
onChange={(e) => setDevIntegrationId(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") handleDevManualComplete(); }}
className="flex-1 px-3 py-1.5 text-xs bg-stone-800 border border-stone-700 rounded-lg text-white placeholder:text-stone-500 focus:outline-none focus:border-primary-500"
className="w-full px-3 py-1.5 text-xs bg-stone-800 border border-stone-700 rounded-lg text-white placeholder:text-stone-500 focus:outline-none focus:border-primary-500"
/>
<button
onClick={handleDevManualComplete}
disabled={devSubmitting || !devIntegrationId.trim()}
className="px-3 py-1.5 text-xs font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-500 disabled:opacity-40 transition-colors"
>
{devSubmitting ? "..." : "Go"}
</button>
<div className="flex gap-2">
<input
type="password"
placeholder="Client-side encryption key"
value={devEncryptionKey}
onChange={(e) => setDevEncryptionKey(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") handleDevManualComplete(); }}
className="flex-1 px-3 py-1.5 text-xs bg-stone-800 border border-stone-700 rounded-lg text-white placeholder:text-stone-500 focus:outline-none focus:border-primary-500"
/>
<button
onClick={handleDevManualComplete}
disabled={devSubmitting || !devIntegrationId.trim() || !devEncryptionKey.trim()}
className="px-3 py-1.5 text-xs font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-500 disabled:opacity-40 transition-colors"
>
{devSubmitting ? "..." : "Go"}
</button>
</div>
</div>
{devError && (
<p className="mt-1.5 text-[11px] text-coral-400">{devError}</p>
+2 -2
View File
@@ -153,8 +153,8 @@
}
.app-dotted-canvas {
background-color: #f8f7f2;
background-image: radial-gradient(circle at center, #cdcdcd 1px, transparent 1px);
background-color: transparent;
background-image: radial-gradient(circle at center, rgba(0, 0, 0, 0.2) 1px, transparent 1px);
background-size: 18px 18px;
background-position: 0 0;
}
+14
View File
@@ -0,0 +1,14 @@
export interface GradientConfig {
playing: boolean;
}
export class Gradient {
el?: HTMLCanvasElement;
conf?: GradientConfig;
play(): void;
pause(): void;
disconnect(): void;
initGradient(selector: string): this;
toggleColor(index: number): void;
updateFrequency(freq: number): void;
}
File diff suppressed because one or more lines are too long
+22
View File
@@ -12,8 +12,10 @@ import { emitSkillStateChange } from "./skillEvents";
import {
getSkillSnapshot,
setSetupComplete as rpcSetSetupComplete,
stopSkill as rpcStopSkill,
revokeOAuth as rpcRevokeOAuth,
removePersistedOAuthCredential,
removePersistedClientKey,
revokeAuth as rpcRevokeAuth,
removePersistedAuthCredential,
} from "./skillsApi";
@@ -429,7 +431,20 @@ class SkillManager {
}
try {
// Stop the frontend-side runtime (if any)
await this.stopSkill(skillId);
// Stop the core sidecar skill process
try {
await rpcStopSkill(skillId);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes("not found") || msg.includes("not registered")) {
console.debug("[SkillManager] core skills_stop: already stopped");
} else {
console.error("[SkillManager] core skills_stop failed:", err);
throw err;
}
}
} finally {
// Host-side fallback cleanup if RPC revoke failed
if (!oauthRevokeSucceeded) {
@@ -448,6 +463,13 @@ class SkillManager {
);
});
}
// Always clear the client-side encryption key
await removePersistedClientKey(skillId).catch((err) => {
console.debug(
"[SkillManager] host-side client key cleanup failed:",
err,
);
});
}
await rpcSetSetupComplete(skillId, false).catch(() => {});
+8
View File
@@ -178,6 +178,14 @@ export async function removePersistedAuthCredential(skillId: string): Promise<vo
});
}
/** Host-side fallback: clear client_key.json from the skill's data dir. */
export async function removePersistedClientKey(skillId: string): Promise<void> {
await callCoreRpc({
method: 'openhuman.skills_data_write',
params: { skill_id: skillId, filename: 'client_key.json', content: '' },
});
}
export async function disableSkill(skillId: string): Promise<void> {
await callCoreRpc({
method: 'openhuman.skills_disable',
+2 -6
View File
@@ -17,7 +17,6 @@ import ProfilePanel from '../components/settings/panels/ProfilePanel';
import RecoveryPhrasePanel from '../components/settings/panels/RecoveryPhrasePanel';
import ScreenIntelligencePanel from '../components/settings/panels/ScreenIntelligencePanel';
import SkillsPanel from '../components/settings/panels/SkillsPanel';
import TauriCommandsPanel from '../components/settings/panels/TauriCommandsPanel';
import TeamInvitesPanel from '../components/settings/panels/TeamInvitesPanel';
import TeamManagementPanel from '../components/settings/panels/TeamManagementPanel';
import TeamMembersPanel from '../components/settings/panels/TeamMembersPanel';
@@ -229,10 +228,8 @@ const aiSettingsItems = [
const Settings = () => {
return (
<div className="h-full overflow-y-auto p-4 pt-6">
<div
className="max-w-lg mx-auto bg-white rounded-2xl shadow-soft border border-stone-200 overflow-hidden flex flex-col min-h-0"
style={{ maxHeight: 'calc(100vh - 7rem)' }}>
<div className="p-4 pt-6">
<div className="max-w-lg mx-auto bg-white rounded-2xl shadow-soft border border-stone-200 overflow-hidden">
<Routes>
<Route index element={<SettingsHome />} />
<Route
@@ -286,7 +283,6 @@ const Settings = () => {
<Route path="team/members" element={<TeamMembersPanel />} />
<Route path="team/invites" element={<TeamInvitesPanel />} />
<Route path="developer-options" element={<DeveloperOptionsPanel />} />
<Route path="tauri-commands" element={<TauriCommandsPanel />} />
<Route path="webhooks-debug" element={<WebhooksDebugPanel />} />
<Route path="memory-debug" element={<MemoryDebugPanel />} />
<Route path="recovery-phrase" element={<RecoveryPhrasePanel />} />
+1 -1
View File
@@ -408,7 +408,7 @@ export default function Skills() {
{/* Header */}
<div className="mb-3 flex items-start justify-between gap-3">
<div>
<h2 className="text-sm font-semibold text-stone-900">Connected Skills</h2>
<h2 className="text-sm font-semibold text-stone-900">3rd Party Skills</h2>
<p className="mt-1 text-xs text-stone-500">
Third-party integrations and external data sources.
</p>
+16 -6
View File
@@ -522,13 +522,21 @@ pub async fn run_server(
tokio::spawn(async {
match crate::openhuman::config::Config::load_or_init().await {
Ok(config) if config.local_ai.enabled => {
let service = crate::openhuman::local_ai::global(&config);
service.bootstrap(&config).await;
Ok(config) => {
if config.local_ai.enabled {
let service = crate::openhuman::local_ai::global(&config);
service.bootstrap(&config).await;
}
// Launch the overlay Tauri app (transparent debug/voice panel) as a child process.
if config.overlay_enabled {
crate::openhuman::overlay::spawn_overlay();
} else {
log::info!("[overlay] overlay disabled by config (overlay_enabled = false)");
}
}
Ok(_) => {}
Err(err) => {
log::warn!("[core] local-ai bootstrap skipped: {err}");
log::warn!("[core] config load failed, skipping local-ai and overlay: {err}");
}
}
});
@@ -545,8 +553,10 @@ pub async fn bootstrap_skill_runtime() {
// Resolve the base directory (~/.openhuman or $OPENHUMAN_WORKSPACE).
let base_dir = std::env::var("OPENHUMAN_WORKSPACE")
.ok()
.filter(|s| !s.is_empty())
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| {
.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".openhuman")
+13 -8
View File
@@ -239,8 +239,10 @@ fn run_interactive(rt: &tokio::runtime::Runtime, verbose: bool) -> anyhow::Resul
fn history_file_path() -> PathBuf {
let base = std::env::var("OPENHUMAN_WORKSPACE")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|_| {
.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".openhuman")
@@ -622,13 +624,16 @@ fn print_schema(namespace: Option<&str>) {
}
fn print_env() {
let workspace = std::env::var("OPENHUMAN_WORKSPACE").unwrap_or_else(|_| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".openhuman")
.to_string_lossy()
.to_string()
});
let workspace = std::env::var("OPENHUMAN_WORKSPACE")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".openhuman")
.to_string_lossy()
.to_string()
});
let cwd = std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "(unknown)".to_string());
+3 -1
View File
@@ -111,8 +111,10 @@ async fn bootstrap_skills_runtime(
// Resolve the base directory (~/.openhuman or $OPENHUMAN_WORKSPACE).
let base_dir = std::env::var("OPENHUMAN_WORKSPACE")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|_| {
.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".openhuman")
+9 -74
View File
@@ -682,85 +682,20 @@ impl Config {
}
}
if let Ok(flag) = std::env::var("OPENHUMAN_OVERLAY_ENABLED") {
let normalized = flag.trim().to_ascii_lowercase();
match normalized.as_str() {
"1" | "true" | "yes" | "on" => self.overlay_enabled = true,
"0" | "false" | "no" | "off" => self.overlay_enabled = false,
_ => {}
}
}
if self.proxy.enabled && self.proxy.scope == ProxyScope::Environment {
self.proxy.apply_to_process_env();
}
set_runtime_proxy_config(self.proxy.clone());
// Learning subsystem configuration
if let Ok(flag) = std::env::var("OPENHUMAN_LEARNING_ENABLED") {
let normalized = flag.trim().to_ascii_lowercase();
match normalized.as_str() {
"1" | "true" | "yes" | "on" => self.learning.enabled = true,
"0" | "false" | "no" | "off" => self.learning.enabled = false,
_ => {}
}
}
if let Ok(flag) = std::env::var("OPENHUMAN_LEARNING_REFLECTION_ENABLED") {
let normalized = flag.trim().to_ascii_lowercase();
match normalized.as_str() {
"1" | "true" | "yes" | "on" => self.learning.reflection_enabled = true,
"0" | "false" | "no" | "off" => self.learning.reflection_enabled = false,
_ => {}
}
}
if let Ok(flag) = std::env::var("OPENHUMAN_LEARNING_USER_PROFILE_ENABLED") {
let normalized = flag.trim().to_ascii_lowercase();
match normalized.as_str() {
"1" | "true" | "yes" | "on" => self.learning.user_profile_enabled = true,
"0" | "false" | "no" | "off" => self.learning.user_profile_enabled = false,
_ => {}
}
}
if let Ok(flag) = std::env::var("OPENHUMAN_LEARNING_TOOL_TRACKING_ENABLED") {
let normalized = flag.trim().to_ascii_lowercase();
match normalized.as_str() {
"1" | "true" | "yes" | "on" => self.learning.tool_tracking_enabled = true,
"0" | "false" | "no" | "off" => self.learning.tool_tracking_enabled = false,
_ => {}
}
}
if let Ok(flag) = std::env::var("OPENHUMAN_LEARNING_SKILL_CREATION_ENABLED") {
let normalized = flag.trim().to_ascii_lowercase();
match normalized.as_str() {
"1" | "true" | "yes" | "on" => self.learning.skill_creation_enabled = true,
"0" | "false" | "no" | "off" => self.learning.skill_creation_enabled = false,
_ => {}
}
}
if let Ok(source_str) = std::env::var("OPENHUMAN_LEARNING_REFLECTION_SOURCE") {
let normalized = source_str.trim().to_ascii_lowercase();
match normalized.as_str() {
"local" => self.learning.reflection_source = super::ReflectionSource::Local,
"cloud" => self.learning.reflection_source = super::ReflectionSource::Cloud,
_ => {
tracing::warn!(
source = %source_str,
"Ignoring invalid OPENHUMAN_LEARNING_REFLECTION_SOURCE (valid: local, cloud)"
);
}
}
}
if let Ok(max_str) = std::env::var("OPENHUMAN_LEARNING_MAX_REFLECTIONS_PER_SESSION") {
if let Ok(max_val) = max_str.parse::<usize>() {
if max_val > 0 {
self.learning.max_reflections_per_session = max_val;
}
}
}
if let Ok(min_str) = std::env::var("OPENHUMAN_LEARNING_MIN_TURN_COMPLEXITY") {
if let Ok(min_val) = min_str.parse::<usize>() {
self.learning.min_turn_complexity = min_val;
}
}
}
pub async fn save(&self) -> Result<()> {
+1
View File
@@ -8,6 +8,7 @@ mod autocomplete;
mod autonomy;
mod channels;
mod defaults;
pub(crate) use defaults::default_true;
mod hardware;
mod heartbeat_cron;
mod identity_cost;
+6
View File
@@ -118,6 +118,11 @@ pub struct Config {
#[serde(default)]
pub orchestrator: OrchestratorConfig,
/// Whether to launch the overlay Tauri app (floating debug/voice panel)
/// when the core RPC server starts. Defaults to `true`.
#[serde(default = "default_true")]
pub overlay_enabled: bool,
/// Whether the user has completed the onboarding flow.
#[serde(default)]
pub onboarding_completed: bool,
@@ -167,6 +172,7 @@ impl Default for Config {
query_classification: QueryClassificationConfig::default(),
learning: LearningConfig::default(),
orchestrator: OrchestratorConfig::default(),
overlay_enabled: true,
onboarding_completed: false,
}
}
+1
View File
@@ -32,6 +32,7 @@ pub mod learning;
pub mod local_ai;
pub mod memory;
pub mod migration;
pub mod overlay;
pub mod providers;
pub mod screen_intelligence;
pub mod security;
+7
View File
@@ -0,0 +1,7 @@
//! Overlay process launcher — discovers and spawns the `openhuman-overlay`
//! Tauri application as a child process so the floating debug/voice panel
//! appears automatically when the core RPC server is running.
mod process;
pub use process::spawn_overlay;
+134
View File
@@ -0,0 +1,134 @@
//! Discovery and lifecycle management for the `openhuman-overlay` binary.
//!
//! The overlay is a separate Tauri application that provides a transparent
//! floating panel with voice transcription, autocomplete debug info, and
//! Globe/Fn hotkey toggling. The core process launches it as a fire-and-forget
//! child so that it appears automatically when the core RPC server starts.
use std::path::PathBuf;
use std::process::{Command, Stdio};
/// Attempt to find and spawn the overlay binary.
///
/// This is best-effort: if the binary is not found or fails to launch, a
/// warning is logged and the core continues normally.
pub fn spawn_overlay() {
let Some(overlay_bin) = find_overlay_binary() else {
log::debug!("[overlay] openhuman-overlay binary not found — skipping overlay launch");
return;
};
log::info!("[overlay] launching overlay: {}", overlay_bin.display());
match Command::new(&overlay_bin)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => {
log::info!("[overlay] overlay process spawned (pid={})", child.id());
}
Err(err) => {
log::warn!(
"[overlay] failed to spawn overlay at {}: {err}",
overlay_bin.display()
);
}
}
}
/// Search for the `openhuman-overlay` binary in standard locations.
///
/// Search order:
/// 1. `OPENHUMAN_OVERLAY_BIN` env var (explicit override)
/// 2. Next to the current executable (`openhuman-overlay` / `openhuman-overlay.exe`)
/// 3. macOS: inside the `.app` bundle Resources directory
/// 4. Dev builds: `overlay/src-tauri/target/debug/openhuman-overlay`
fn find_overlay_binary() -> Option<PathBuf> {
// 1. Explicit env var override
if let Ok(path) = std::env::var("OPENHUMAN_OVERLAY_BIN") {
let candidate = PathBuf::from(&path);
if candidate.exists() {
log::debug!(
"[overlay] found via OPENHUMAN_OVERLAY_BIN: {}",
candidate.display()
);
return Some(candidate);
}
log::debug!("[overlay] OPENHUMAN_OVERLAY_BIN set but path does not exist: {path}");
}
let exe = std::env::current_exe().ok()?;
let exe_dir = exe.parent()?;
// 2. Next to the current executable
let standalone = exe_dir.join(overlay_binary_name());
if standalone.exists() {
log::debug!("[overlay] found next to exe: {}", standalone.display());
return Some(standalone);
}
// 3. macOS: Resources directory inside the .app bundle
#[cfg(target_os = "macos")]
{
if let Some(resources_dir) = exe_dir.parent().map(|p| p.join("Resources")) {
let in_resources = resources_dir.join(overlay_binary_name());
if in_resources.exists() {
log::debug!("[overlay] found in Resources: {}", in_resources.display());
return Some(in_resources);
}
}
}
// 4. Dev builds: walk up from current exe to find overlay/src-tauri/target
if cfg!(debug_assertions) {
// Try relative to CARGO_MANIFEST_DIR at compile time
let dev_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("overlay")
.join("src-tauri")
.join("target")
.join("debug")
.join(overlay_binary_name());
if dev_path.exists() {
log::debug!("[overlay] found dev build: {}", dev_path.display());
return Some(dev_path);
}
// Also check for the macOS .app bundle in dev
#[cfg(target_os = "macos")]
{
let dev_app_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("overlay")
.join("src-tauri")
.join("target")
.join("debug")
.join("bundle")
.join("macos")
.join("openhuman-overlay.app")
.join("Contents")
.join("MacOS")
.join("openhuman-overlay");
if dev_app_path.exists() {
log::debug!(
"[overlay] found dev .app bundle: {}",
dev_app_path.display()
);
return Some(dev_app_path);
}
}
}
None
}
fn overlay_binary_name() -> &'static str {
#[cfg(windows)]
{
"openhuman-overlay.exe"
}
#[cfg(not(windows))]
{
"openhuman-overlay"
}
}
+17
View File
@@ -357,17 +357,34 @@ impl RuntimeEngine {
let skills_dir = self.get_skills_source_dir()?;
let mut skill_dir = skills_dir.join(skill_id);
let mut manifest_path = skill_dir.join("manifest.json");
log::debug!(
"[runtime] start_skill '{}': manifest_path={:?}, exists={}",
skill_id,
manifest_path,
manifest_path.exists()
);
if !manifest_path.exists() {
// Try workspace skills directory
if let Some(workspace_dir) = self.workspace_dir.read().as_ref() {
let ws_skill_dir = workspace_dir.join("skills").join(skill_id);
let ws_manifest = ws_skill_dir.join("manifest.json");
log::debug!(
"[runtime] start_skill '{}': workspace check {:?}, exists={}",
skill_id,
ws_manifest,
ws_manifest.exists()
);
if ws_manifest.exists() {
log::info!("[runtime] Found skill '{}' in workspace dir", skill_id);
skill_dir = ws_skill_dir;
manifest_path = ws_manifest;
}
} else {
log::warn!(
"[runtime] start_skill '{}': workspace_dir is not set",
skill_id
);
}
}
+28 -3
View File
@@ -7122,7 +7122,16 @@ streamx@^2.15.0, streamx@^2.21.0:
fast-fifo "^1.3.2"
text-decoder "^1.1.0"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -7221,7 +7230,14 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -7995,7 +8011,7 @@ workerpool@^6.5.1:
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -8013,6 +8029,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"