mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
refactor(subconscious): replace task system with agent-per-tick model (#3079)
Co-authored-by: Steven Enamakel's Droid <enamakel.agent@tinyhumans.ai> Co-authored-by: oxoxDev <164490987+oxoxDev@users.noreply.github.com> Co-authored-by: sanil-23 <sanil@tinyhumans.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Steven Enamakel's Droid
oxoxDev
sanil-23
Claude Opus 4.8
parent
3136ef5483
commit
97ff4f53d3
@@ -1,101 +1,72 @@
|
||||
import type { Dispatch, FormEvent, SetStateAction } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import { setSelectedThread } from '../../store/threadSlice';
|
||||
import type {
|
||||
SubconsciousEscalation,
|
||||
SubconsciousLogEntry,
|
||||
SubconsciousStatus,
|
||||
SubconsciousTask,
|
||||
} from '../../utils/tauriCommands/subconscious';
|
||||
import type { SubconsciousMode } from '../../utils/tauriCommands/heartbeat';
|
||||
import type { SubconsciousStatus } from '../../utils/tauriCommands/subconscious';
|
||||
import SubconsciousReflectionCards from './SubconsciousReflectionCards';
|
||||
|
||||
const SKILL_KEYWORDS =
|
||||
/\bskill\b|\boauth\b|\bnotion\b|\bgmail\b|\bintegration\b|\bdisconnect|\breconnect|\bre-?auth/i;
|
||||
|
||||
function isSkillRelated(title: string, description: string): boolean {
|
||||
return SKILL_KEYWORDS.test(title) || SKILL_KEYWORDS.test(description);
|
||||
interface ModeOption {
|
||||
id: SubconsciousMode;
|
||||
titleKey: string;
|
||||
descKey: string;
|
||||
}
|
||||
|
||||
function formatInterval(minutes: number, t: (key: string) => string): string {
|
||||
switch (minutes) {
|
||||
case 5:
|
||||
return t('subconscious.interval.fiveMinutes');
|
||||
case 10:
|
||||
return t('subconscious.interval.tenMinutes');
|
||||
case 15:
|
||||
return t('subconscious.interval.fifteenMinutes');
|
||||
case 30:
|
||||
return t('subconscious.interval.thirtyMinutes');
|
||||
case 60:
|
||||
return t('subconscious.interval.oneHour');
|
||||
case 360:
|
||||
return t('subconscious.interval.sixHours');
|
||||
case 720:
|
||||
return t('subconscious.interval.twelveHours');
|
||||
case 1440:
|
||||
return t('subconscious.interval.oneDay');
|
||||
default:
|
||||
return String(minutes);
|
||||
}
|
||||
const MODE_OPTIONS: ModeOption[] = [
|
||||
{ id: 'off', titleKey: 'subconscious.mode.off.title', descKey: 'subconscious.mode.off.desc' },
|
||||
{
|
||||
id: 'simple',
|
||||
titleKey: 'subconscious.mode.simple.title',
|
||||
descKey: 'subconscious.mode.simple.desc',
|
||||
},
|
||||
{
|
||||
id: 'aggressive',
|
||||
titleKey: 'subconscious.mode.aggressive.title',
|
||||
descKey: 'subconscious.mode.aggressive.desc',
|
||||
},
|
||||
];
|
||||
|
||||
const INTERVAL_STOPS = [5, 10, 15, 30, 60, 120, 360, 720, 1440];
|
||||
|
||||
function formatMinutes(minutes: number, t: (key: string) => string): string {
|
||||
if (minutes < 60) return t('subconscious.interval.minutes').replace('{n}', String(minutes));
|
||||
const hours = minutes / 60;
|
||||
if (hours === 1) return t('subconscious.interval.oneHour');
|
||||
if (hours === 24) return t('subconscious.interval.oneDay');
|
||||
return t('subconscious.interval.hours').replace('{n}', String(hours));
|
||||
}
|
||||
|
||||
function formatPriority(priority: string, t: (key: string) => string): string {
|
||||
switch (priority) {
|
||||
case 'critical':
|
||||
return t('subconscious.priority.critical');
|
||||
case 'important':
|
||||
return t('subconscious.priority.important');
|
||||
default:
|
||||
return t('subconscious.priority.normal');
|
||||
}
|
||||
function minutesToSlider(minutes: number): number {
|
||||
const idx = INTERVAL_STOPS.indexOf(minutes);
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
function formatDuration(durationMs: number, t: (key: string) => string): string {
|
||||
if (durationMs > 1000) {
|
||||
return t('subconscious.durationSeconds').replace('{seconds}', (durationMs / 1000).toFixed(1));
|
||||
}
|
||||
return t('subconscious.durationMilliseconds').replace('{milliseconds}', String(durationMs));
|
||||
function sliderToMinutes(value: number): number {
|
||||
return INTERVAL_STOPS[value] ?? 30;
|
||||
}
|
||||
|
||||
interface IntelligenceSubconsciousTabProps {
|
||||
addSubconsciousTask: (title: string) => Promise<void>;
|
||||
approveEscalation: (escalationId: string) => Promise<void>;
|
||||
dismissEscalation: (escalationId: string) => Promise<void>;
|
||||
expandedLogIds: Set<string>;
|
||||
logEntries: SubconsciousLogEntry[];
|
||||
newTaskTitle: string;
|
||||
removeSubconsciousTask: (taskId: string) => Promise<void>;
|
||||
setExpandedLogIds: Dispatch<SetStateAction<Set<string>>>;
|
||||
setNewTaskTitle: (value: string) => void;
|
||||
status: SubconsciousStatus | null;
|
||||
tasks: SubconsciousTask[];
|
||||
toggleSubconsciousTask: (taskId: string, enabled: boolean) => Promise<void>;
|
||||
mode: SubconsciousMode;
|
||||
intervalMinutes: number;
|
||||
triggerTick: () => Promise<void>;
|
||||
triggering: boolean;
|
||||
escalations: SubconsciousEscalation[];
|
||||
loading: boolean;
|
||||
settingMode: boolean;
|
||||
setMode: (mode: SubconsciousMode) => Promise<void>;
|
||||
setIntervalMinutes: (minutes: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function IntelligenceSubconsciousTab({
|
||||
addSubconsciousTask,
|
||||
approveEscalation,
|
||||
dismissEscalation,
|
||||
escalations,
|
||||
expandedLogIds,
|
||||
loading,
|
||||
logEntries,
|
||||
newTaskTitle,
|
||||
removeSubconsciousTask,
|
||||
setExpandedLogIds,
|
||||
setNewTaskTitle,
|
||||
status,
|
||||
tasks,
|
||||
toggleSubconsciousTask,
|
||||
mode,
|
||||
intervalMinutes,
|
||||
triggerTick,
|
||||
triggering,
|
||||
settingMode,
|
||||
setMode,
|
||||
setIntervalMinutes,
|
||||
}: IntelligenceSubconsciousTabProps) {
|
||||
const { t } = useT();
|
||||
const navigate = useNavigate();
|
||||
@@ -104,52 +75,35 @@ export default function IntelligenceSubconsciousTab({
|
||||
const providerUnavailableReason = providerUnavailable
|
||||
? (status?.provider_unavailable_reason ?? t('subconscious.providerUnavailableTitle'))
|
||||
: null;
|
||||
const isEnabled = mode !== 'off';
|
||||
|
||||
// Reflection "Act" callback — sets the freshly-spawned thread as the
|
||||
// selected one and navigates the user to the chat surface so they
|
||||
// land in the new conversation. Reflections never write into existing
|
||||
// threads (#623), so every act starts its own conversation.
|
||||
//
|
||||
// We dispatch `setSelectedThread` (NOT `setActiveThread`): the
|
||||
// Conversations page reads `selectedThreadId` from the thread slice on
|
||||
// mount and resumes that thread if present in the fetched list,
|
||||
// falling back to the most recent thread otherwise. `activeThreadId`
|
||||
// is a separate, runtime-only field used for in-flight chat-turn
|
||||
// routing — setting it without `selectedThreadId` would not affect
|
||||
// which thread the user lands on.
|
||||
//
|
||||
// Route is `/chat`, NOT `/conversations`. The repo's CLAUDE.md hash-
|
||||
// route list is stale — `BottomTabBar` and `OpenhumanLinkModal` both
|
||||
// navigate to `/chat`. Using `/conversations` falls through to a home
|
||||
// redirect so the user ends up on `/home` instead of the new thread.
|
||||
const handleNavigateToReflectionThread = (threadId: string) => {
|
||||
console.debug('[subconscious-ui] reflection navigate:thread', { threadId });
|
||||
const [localSlider, setLocalSlider] = useState(() => minutesToSlider(intervalMinutes));
|
||||
|
||||
// Keep the local slider in sync when the prop changes from outside (e.g. after a refresh).
|
||||
useEffect(() => {
|
||||
setLocalSlider(minutesToSlider(intervalMinutes));
|
||||
}, [intervalMinutes]);
|
||||
|
||||
const handleSliderChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = Number(e.target.value);
|
||||
setLocalSlider(val);
|
||||
}, []);
|
||||
|
||||
const handleSliderCommit = useCallback(() => {
|
||||
const minutes = sliderToMinutes(localSlider);
|
||||
if (minutes !== intervalMinutes) {
|
||||
void setIntervalMinutes(minutes);
|
||||
}
|
||||
}, [localSlider, intervalMinutes, setIntervalMinutes]);
|
||||
|
||||
const handleNavigateToThread = (threadId: string) => {
|
||||
dispatch(setSelectedThread(threadId));
|
||||
navigate('/chat');
|
||||
};
|
||||
|
||||
const handleAddTask = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const title = newTaskTitle.trim();
|
||||
if (!title) return;
|
||||
console.debug('[subconscious-ui] add task:start', { title });
|
||||
try {
|
||||
await addSubconsciousTask(title);
|
||||
setNewTaskTitle('');
|
||||
console.debug('[subconscious-ui] add task:success', { title });
|
||||
} catch (error) {
|
||||
console.debug('[subconscious-ui] add task:error', {
|
||||
title,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunTick = async () => {
|
||||
console.debug('[subconscious-ui] run tick:start', { triggering });
|
||||
try {
|
||||
await triggerTick();
|
||||
console.debug('[subconscious-ui] run tick:done');
|
||||
} catch (error) {
|
||||
console.debug('[subconscious-ui] run tick:error', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
@@ -157,136 +111,112 @@ export default function IntelligenceSubconsciousTab({
|
||||
}
|
||||
};
|
||||
|
||||
const handleApproveEscalation = async (escalationId: string) => {
|
||||
console.debug('[subconscious-ui] escalation approve:start', { escalationId });
|
||||
try {
|
||||
await approveEscalation(escalationId);
|
||||
console.debug('[subconscious-ui] escalation approve:success', { escalationId });
|
||||
} catch (error) {
|
||||
console.debug('[subconscious-ui] escalation approve:error', {
|
||||
escalationId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismissEscalation = async (escalationId: string) => {
|
||||
console.debug('[subconscious-ui] escalation dismiss:start', { escalationId });
|
||||
try {
|
||||
await dismissEscalation(escalationId);
|
||||
console.debug('[subconscious-ui] escalation dismiss:success', { escalationId });
|
||||
} catch (error) {
|
||||
console.debug('[subconscious-ui] escalation dismiss:error', {
|
||||
escalationId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFixInSkills = (escalationId: string) => {
|
||||
console.debug('[subconscious-ui] escalation fix in skills:navigate', { escalationId });
|
||||
navigate('/skills', { state: { subconsciousEscalationId: escalationId } });
|
||||
};
|
||||
|
||||
const handleToggleTask = async (taskId: string, enabled: boolean, title: string) => {
|
||||
console.debug('[subconscious-ui] task toggle:start', { taskId, enabled, title });
|
||||
try {
|
||||
await toggleSubconsciousTask(taskId, enabled);
|
||||
console.debug('[subconscious-ui] task toggle:success', { taskId, enabled, title });
|
||||
} catch (error) {
|
||||
console.debug('[subconscious-ui] task toggle:error', {
|
||||
taskId,
|
||||
enabled,
|
||||
title,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTask = async (taskId: string, title: string) => {
|
||||
console.debug('[subconscious-ui] task remove:start', { taskId, title });
|
||||
try {
|
||||
await removeSubconsciousTask(taskId);
|
||||
console.debug('[subconscious-ui] task remove:success', { taskId, title });
|
||||
} catch (error) {
|
||||
console.debug('[subconscious-ui] task remove:error', {
|
||||
taskId,
|
||||
title,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-up">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-stone-400 dark:text-neutral-500">
|
||||
{status && (
|
||||
<>
|
||||
<span>
|
||||
{status.task_count} {t('subconscious.tasks')}
|
||||
</span>
|
||||
<span className="text-stone-300 dark:text-neutral-600">|</span>
|
||||
<span>
|
||||
{status.total_ticks} {t('subconscious.ticks')}
|
||||
</span>
|
||||
{status.last_tick_at && (
|
||||
<>
|
||||
<span className="text-stone-300 dark:text-neutral-600">|</span>
|
||||
<span>
|
||||
{t('subconscious.last')}:{' '}
|
||||
{new Date(status.last_tick_at * 1000).toLocaleTimeString()}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{status.consecutive_failures > 0 && (
|
||||
<>
|
||||
<span className="text-stone-300 dark:text-neutral-600">|</span>
|
||||
<span className="text-coral-500">
|
||||
{status.consecutive_failures} {t('subconscious.failed')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="w-3 h-3 text-stone-400 dark:text-neutral-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
<div className="space-y-5 animate-fade-up">
|
||||
{/* Mode selector */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100 mb-2">
|
||||
{t('subconscious.mode.label')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{MODE_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
disabled={settingMode}
|
||||
onClick={() => void setMode(opt.id)}
|
||||
className={`flex flex-col items-center text-center rounded-lg border p-3 transition ${
|
||||
mode === opt.id
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-500/10'
|
||||
: 'border-stone-200 dark:border-neutral-800 hover:border-primary-300 dark:hover:border-primary-500/40'
|
||||
} ${settingMode ? 'opacity-60 cursor-wait' : ''}`}>
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded-full border-2 mb-1.5 ${
|
||||
mode === opt.id
|
||||
? 'bg-primary-500 border-primary-500'
|
||||
: 'border-stone-300 dark:border-neutral-600'
|
||||
}`}
|
||||
/>
|
||||
</svg>
|
||||
<select
|
||||
value={status?.interval_minutes ?? 5}
|
||||
onChange={() => {
|
||||
// Config update would require restart — show as read-only for now
|
||||
}}
|
||||
disabled
|
||||
title={t('subconscious.tickInterval')}
|
||||
className="text-xs bg-stone-50 dark:bg-neutral-800/60 border border-stone-200 dark:border-neutral-800 rounded px-1.5 py-0.5 text-stone-500 dark:text-neutral-400 cursor-not-allowed">
|
||||
<option value={5}>{formatInterval(5, t)}</option>
|
||||
<option value={10}>{formatInterval(10, t)}</option>
|
||||
<option value={15}>{formatInterval(15, t)}</option>
|
||||
<option value={30}>{formatInterval(30, t)}</option>
|
||||
<option value={60}>{formatInterval(60, t)}</option>
|
||||
<option value={360}>{formatInterval(360, t)}</option>
|
||||
<option value={720}>{formatInterval(720, t)}</option>
|
||||
<option value={1440}>{formatInterval(1440, t)}</option>
|
||||
</select>
|
||||
<span className="text-sm font-medium text-stone-900 dark:text-neutral-100">
|
||||
{t(opt.titleKey)}
|
||||
</span>
|
||||
<p className="mt-1 text-[11px] leading-tight text-stone-500 dark:text-neutral-400">
|
||||
{t(opt.descKey)}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{mode === 'aggressive' && (
|
||||
<p className="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
{t('subconscious.mode.aggressiveWarning')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Frequency slider */}
|
||||
{isEnabled && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="text-xs font-medium text-stone-700 dark:text-neutral-300">
|
||||
{t('subconscious.interval.label')}
|
||||
</label>
|
||||
<span className="text-xs text-stone-500 dark:text-neutral-400">
|
||||
{formatMinutes(sliderToMinutes(localSlider), t)}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={INTERVAL_STOPS.length - 1}
|
||||
step={1}
|
||||
value={localSlider}
|
||||
onChange={handleSliderChange}
|
||||
onMouseUp={handleSliderCommit}
|
||||
onTouchEnd={handleSliderCommit}
|
||||
className="w-full h-1.5 rounded-full appearance-none cursor-pointer bg-stone-200 dark:bg-neutral-700 accent-primary-500"
|
||||
/>
|
||||
<div className="flex justify-between mt-1 text-[10px] text-stone-400 dark:text-neutral-500">
|
||||
<span>5m</span>
|
||||
<span>1h</span>
|
||||
<span>24h</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status bar + Run Now */}
|
||||
{isEnabled && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-stone-400 dark:text-neutral-500">
|
||||
{status && (
|
||||
<>
|
||||
<span>
|
||||
{status.total_ticks} {t('subconscious.ticks')}
|
||||
</span>
|
||||
{status.last_tick_at && (
|
||||
<>
|
||||
<span className="text-stone-300 dark:text-neutral-600">|</span>
|
||||
<span>
|
||||
{t('subconscious.last')}:{' '}
|
||||
{new Date(status.last_tick_at * 1000).toLocaleTimeString()}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{status.consecutive_failures > 0 && (
|
||||
<>
|
||||
<span className="text-stone-300 dark:text-neutral-600">|</span>
|
||||
<span className="text-coral-500">
|
||||
{status.consecutive_failures} {t('subconscious.failed')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => void handleRunTick()}
|
||||
disabled={triggering || providerUnavailable}
|
||||
title={providerUnavailable ? t('subconscious.providerUnavailableTitle') : undefined}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-stone-50 dark:bg-neutral-800/60 hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 disabled:opacity-40 border border-stone-200 dark:border-neutral-800 rounded-lg text-stone-600 dark:text-neutral-300 transition-colors">
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-stone-50 dark:bg-neutral-800/60 hover:bg-stone-100 dark:hover:bg-neutral-800 disabled:opacity-40 border border-stone-200 dark:border-neutral-800 rounded-lg text-stone-600 dark:text-neutral-300 transition-colors">
|
||||
{triggering ? (
|
||||
<div className="w-3 h-3 border border-stone-400 border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
@@ -302,9 +232,9 @@ export default function IntelligenceSubconsciousTab({
|
||||
{t('subconscious.runNow')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{providerUnavailable && (
|
||||
{isEnabled && providerUnavailable && (
|
||||
<div className="rounded-lg border border-amber-200 dark:border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
@@ -325,256 +255,12 @@ export default function IntelligenceSubconsciousTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SubconsciousReflectionCards
|
||||
onNavigateToThread={handleNavigateToReflectionThread}
|
||||
pollIntervalMs={15_000}
|
||||
/>
|
||||
|
||||
{escalations.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100 mb-3 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" />
|
||||
{t('subconscious.approvalNeeded')}
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-300">
|
||||
{escalations.length}
|
||||
</span>
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{escalations.map(esc => (
|
||||
<div
|
||||
key={esc.id}
|
||||
className="bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 rounded-xl p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-stone-900 dark:text-neutral-100">
|
||||
{esc.title}
|
||||
</p>
|
||||
<p className="text-xs text-stone-500 dark:text-neutral-400 mt-1">
|
||||
{esc.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span
|
||||
className={`text-[10px] px-2 py-0.5 rounded-full ${
|
||||
esc.priority === 'critical'
|
||||
? 'bg-coral-100 dark:bg-coral-500/20 text-coral-700 dark:text-coral-300'
|
||||
: esc.priority === 'important'
|
||||
? 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-300'
|
||||
: 'bg-stone-100 dark:bg-neutral-800 text-stone-600 dark:text-neutral-300'
|
||||
}`}>
|
||||
{formatPriority(esc.priority, t)}
|
||||
</span>
|
||||
<span className="text-[10px] text-stone-400 dark:text-neutral-500">
|
||||
{t('subconscious.requiresApproval')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 ml-3 flex-shrink-0">
|
||||
{isSkillRelated(esc.title, esc.description) ? (
|
||||
<button
|
||||
onClick={() => handleFixInSkills(esc.id)}
|
||||
className="px-3 py-1.5 text-xs bg-primary-500 hover:bg-primary-600 text-white rounded-lg transition-colors">
|
||||
{t('subconscious.fixInConnections')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => void handleApproveEscalation(esc.id)}
|
||||
className="px-3 py-1.5 text-xs bg-sage-500 hover:bg-sage-600 text-white rounded-lg transition-colors">
|
||||
{t('subconscious.goAhead')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => void handleDismissEscalation(esc.id)}
|
||||
className="px-3 py-1.5 text-xs bg-stone-100 dark:bg-neutral-800 hover:bg-stone-200 text-stone-600 dark:text-neutral-300 rounded-lg transition-colors">
|
||||
{t('common.skip')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{isEnabled && (
|
||||
<SubconsciousReflectionCards
|
||||
onNavigateToThread={handleNavigateToThread}
|
||||
pollIntervalMs={15_000}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100 mb-3">
|
||||
{t('subconscious.activeTasks')}
|
||||
</h3>
|
||||
{loading && tasks.length === 0 ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="w-6 h-6 mx-auto border-2 border-stone-300 dark:border-neutral-700 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : tasks.filter(t => !t.completed).length === 0 ? (
|
||||
<p className="text-xs text-stone-400 dark:text-neutral-500 py-3">
|
||||
{t('subconscious.noActiveTasks')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{tasks
|
||||
.filter(t => !t.completed && t.source === 'system')
|
||||
.map(task => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="flex items-center py-2 px-3 bg-stone-50 dark:bg-neutral-800/60 rounded-lg">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-sage-400 flex-shrink-0 mr-2.5" />
|
||||
<span className="text-sm text-stone-900 dark:text-neutral-100 truncate flex-1">
|
||||
{task.title}
|
||||
</span>
|
||||
<span className="text-[10px] text-stone-400 dark:text-neutral-500 flex-shrink-0 px-1.5 py-0.5 rounded bg-stone-100 dark:bg-neutral-800">
|
||||
{t('subconscious.default')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{tasks
|
||||
.filter(t => !t.completed && t.source !== 'system')
|
||||
.map(task => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="flex items-center justify-between py-2 px-3 bg-stone-50 dark:bg-neutral-800/60 rounded-lg group">
|
||||
<div className="flex items-center gap-2.5 flex-1 min-w-0">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={task.enabled}
|
||||
aria-label={`${task.enabled ? t('common.disable') : t('common.enable')} ${task.title}`}
|
||||
onClick={() => void handleToggleTask(task.id, !task.enabled, task.title)}
|
||||
className={`relative w-7 h-4 rounded-full flex-shrink-0 transition-colors ${
|
||||
task.enabled ? 'bg-sage-500' : 'bg-stone-300'
|
||||
}`}>
|
||||
<span
|
||||
className={`absolute top-0.5 left-0.5 w-3 h-3 rounded-full bg-white dark:bg-neutral-900 shadow transition-transform ${
|
||||
task.enabled ? 'translate-x-3' : 'translate-x-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span
|
||||
className={`text-sm truncate ${task.enabled ? 'text-stone-900 dark:text-neutral-100' : 'text-stone-400 dark:text-neutral-500'}`}>
|
||||
{task.title}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${t('common.remove')} ${task.title}`}
|
||||
onClick={() => void handleRemoveTask(task.id, task.title)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 text-stone-400 dark:text-neutral-500 hover:text-coral-500 transition-all">
|
||||
<svg
|
||||
className="w-3.5 h-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={e => void handleAddTask(e)} className="flex gap-2 mt-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('subconscious.addTaskPlaceholder')}
|
||||
value={newTaskTitle}
|
||||
onChange={e => setNewTaskTitle(e.target.value)}
|
||||
className="flex-1 px-3 py-2 text-sm bg-white dark:bg-neutral-900 border border-stone-200 dark:border-neutral-800 rounded-lg text-stone-900 dark:text-neutral-100 placeholder-stone-400 focus:outline-none focus:border-primary-500/50 transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newTaskTitle.trim()}
|
||||
className="px-3 py-2 text-sm bg-primary-500 hover:bg-primary-600 disabled:opacity-40 text-white rounded-lg transition-colors">
|
||||
{t('common.add')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100 mb-3">
|
||||
{t('subconscious.activityLog')}
|
||||
</h3>
|
||||
{logEntries.length === 0 ? (
|
||||
<p className="text-xs text-stone-400 dark:text-neutral-500 py-3">
|
||||
{t('subconscious.noActivity')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1 max-h-64 overflow-y-auto">
|
||||
{logEntries.map(entry => (
|
||||
<div key={entry.id} className="flex items-start gap-2 py-1.5 px-2 text-xs">
|
||||
<span className="text-stone-400 dark:text-neutral-500 flex-shrink-0 w-14">
|
||||
{new Date(entry.tick_at * 1000).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
<span
|
||||
className={`flex-shrink-0 w-1.5 h-1.5 rounded-full mt-1.5 ${
|
||||
entry.decision === 'act'
|
||||
? 'bg-sage-400'
|
||||
: entry.decision === 'in_progress'
|
||||
? 'bg-primary-400 animate-pulse'
|
||||
: entry.decision === 'escalate'
|
||||
? 'bg-amber-400'
|
||||
: entry.decision === 'failed'
|
||||
? 'bg-coral-400'
|
||||
: entry.decision === 'cancelled'
|
||||
? 'bg-stone-300'
|
||||
: entry.decision === 'dismissed'
|
||||
? 'bg-stone-300'
|
||||
: 'bg-stone-200 dark:bg-neutral-800'
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`break-words min-w-0 ${
|
||||
entry.decision === 'in_progress'
|
||||
? 'text-stone-400 dark:text-neutral-500'
|
||||
: entry.decision === 'failed'
|
||||
? 'text-coral-500'
|
||||
: 'text-stone-600 dark:text-neutral-300'
|
||||
} ${entry.result && entry.result.length > 120 ? 'cursor-pointer hover:text-stone-900 dark:hover:text-neutral-100 dark:text-neutral-100' : ''}`}
|
||||
onClick={() => {
|
||||
if (entry.result && entry.result.length > 120) {
|
||||
setExpandedLogIds(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(entry.id)) next.delete(entry.id);
|
||||
else next.add(entry.id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{entry.result
|
||||
? expandedLogIds.has(entry.id)
|
||||
? entry.result
|
||||
: entry.result.length > 120
|
||||
? `${entry.result.substring(0, 120)}...`
|
||||
: entry.result
|
||||
: entry.decision === 'noop'
|
||||
? t('subconscious.decision.nothingNew')
|
||||
: entry.decision === 'act'
|
||||
? t('subconscious.decision.completed')
|
||||
: entry.decision === 'in_progress'
|
||||
? t('subconscious.decision.evaluating')
|
||||
: entry.decision === 'escalate'
|
||||
? t('subconscious.decision.waitingApproval')
|
||||
: entry.decision === 'failed'
|
||||
? t('subconscious.decision.failed')
|
||||
: entry.decision === 'cancelled'
|
||||
? t('subconscious.decision.cancelled')
|
||||
: entry.decision === 'dismissed'
|
||||
? t('subconscious.decision.skipped')
|
||||
: entry.decision}
|
||||
</span>
|
||||
{entry.duration_ms != null && (
|
||||
<span className="text-stone-300 dark:text-neutral-600 flex-shrink-0 ml-auto">
|
||||
{formatDuration(entry.duration_ms, t)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -259,6 +259,14 @@ export default function SubconsciousReflectionCards({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 flex-shrink-0">
|
||||
{r.thread_id && (
|
||||
<button
|
||||
data-testid={`reflection-view-${r.id}`}
|
||||
onClick={() => onNavigateToThread?.(r.thread_id!)}
|
||||
className="px-3 py-1.5 text-xs bg-stone-50 dark:bg-neutral-800/60 hover:bg-stone-100 dark:hover:bg-neutral-800 border border-stone-200 dark:border-neutral-700 text-stone-600 dark:text-neutral-300 rounded-lg transition-colors">
|
||||
{t('reflections.viewConversation')}
|
||||
</button>
|
||||
)}
|
||||
{r.proposed_action && (
|
||||
<button
|
||||
data-testid={`reflection-act-${r.id}`}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
/**
|
||||
* Vitest for the Intelligence Subconscious tab (#623).
|
||||
*
|
||||
* Covers `handleNavigateToReflectionThread` — the callback passed to
|
||||
* `SubconsciousReflectionCards`. The function is small but load-bearing:
|
||||
* it dispatches `setSelectedThread(threadId)` so `Conversations` resumes
|
||||
* the new thread on mount, then routes to `/chat` (the unified chat
|
||||
* surface; `/conversations` redirects to `/home`). Both dispatch and
|
||||
* navigate are mocked so we can assert the contract without spinning up
|
||||
* the full Redux/router stack.
|
||||
* Vitest for the Intelligence Subconscious tab.
|
||||
*/
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { ComponentProps } from 'react';
|
||||
@@ -23,10 +15,6 @@ vi.mock('react-redux', () => ({ useDispatch: () => mockDispatch, useSelector: ()
|
||||
|
||||
vi.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate }));
|
||||
|
||||
// Stub out the cards component so we can trigger the navigate callback
|
||||
// directly without exercising the RPC / polling path (already covered by
|
||||
// `SubconsciousReflectionCards.test.tsx`). The stub renders a button
|
||||
// that fires `onNavigateToThread` with a known thread id when clicked.
|
||||
vi.mock('../SubconsciousReflectionCards', () => ({
|
||||
default: ({ onNavigateToThread }: { onNavigateToThread?: (id: string) => void }) => (
|
||||
<button
|
||||
@@ -38,24 +26,16 @@ vi.mock('../SubconsciousReflectionCards', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
function baseProps() {
|
||||
function baseProps(): ComponentProps<typeof IntelligenceSubconsciousTab> {
|
||||
return {
|
||||
addSubconsciousTask: vi.fn(),
|
||||
approveEscalation: vi.fn(),
|
||||
dismissEscalation: vi.fn(),
|
||||
expandedLogIds: new Set<string>(),
|
||||
logEntries: [],
|
||||
newTaskTitle: '',
|
||||
removeSubconsciousTask: vi.fn(),
|
||||
setExpandedLogIds: vi.fn(),
|
||||
setNewTaskTitle: vi.fn(),
|
||||
status: null as ComponentProps<typeof IntelligenceSubconsciousTab>['status'],
|
||||
tasks: [],
|
||||
toggleSubconsciousTask: vi.fn(),
|
||||
status: null,
|
||||
mode: 'off',
|
||||
intervalMinutes: 30,
|
||||
triggerTick: vi.fn(),
|
||||
triggering: false,
|
||||
escalations: [],
|
||||
loading: false,
|
||||
settingMode: false,
|
||||
setMode: vi.fn(),
|
||||
setIntervalMinutes: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,48 +48,41 @@ describe('IntelligenceSubconsciousTab', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('on Act → dispatches setSelectedThread + navigates to /chat', () => {
|
||||
it('renders three mode options', () => {
|
||||
render(<IntelligenceSubconsciousTab {...baseProps()} />);
|
||||
expect(screen.getByText('Off')).toBeInTheDocument();
|
||||
expect(screen.getByText('Simple')).toBeInTheDocument();
|
||||
expect(screen.getByText('Aggressive')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking a mode option calls setMode', () => {
|
||||
const setMode = vi.fn();
|
||||
render(<IntelligenceSubconsciousTab {...baseProps()} setMode={setMode} />);
|
||||
fireEvent.click(screen.getByText('Simple'));
|
||||
expect(setMode).toHaveBeenCalledWith('simple');
|
||||
});
|
||||
|
||||
it('hides Run Now and reflections when mode is off', () => {
|
||||
render(<IntelligenceSubconsciousTab {...baseProps()} mode="off" />);
|
||||
expect(screen.queryByText('Run Now')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('cards-stub-trigger')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Run Now and reflections when mode is simple', () => {
|
||||
render(<IntelligenceSubconsciousTab {...baseProps()} mode="simple" />);
|
||||
expect(screen.getByText('Run Now')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('cards-stub-trigger')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows aggressive warning when mode is aggressive', () => {
|
||||
render(<IntelligenceSubconsciousTab {...baseProps()} mode="aggressive" />);
|
||||
expect(screen.getByText(/full tool access including writes/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('on Act → dispatches setSelectedThread + navigates to /chat', () => {
|
||||
render(<IntelligenceSubconsciousTab {...baseProps()} mode="simple" />);
|
||||
fireEvent.click(screen.getByTestId('cards-stub-trigger'));
|
||||
// Redux dispatch payload should match the slice's action creator
|
||||
// exactly — comparing the produced action keeps the assertion robust
|
||||
// if the slice path changes.
|
||||
expect(mockDispatch).toHaveBeenCalledWith(setSelectedThread('spawned-thread-42'));
|
||||
// Route must be `/chat` (the unified chat surface), not
|
||||
// `/conversations` — the latter falls through to a `/home` redirect
|
||||
// and the user lands somewhere unexpected.
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/chat');
|
||||
});
|
||||
|
||||
it('shows provider unavailable state and blocks manual ticks', () => {
|
||||
const triggerTick = vi.fn();
|
||||
render(
|
||||
<IntelligenceSubconsciousTab
|
||||
{...baseProps()}
|
||||
triggerTick={triggerTick}
|
||||
status={{
|
||||
enabled: true,
|
||||
provider_available: false,
|
||||
provider_unavailable_reason: 'Sign in or configure a local Subconscious provider.',
|
||||
interval_minutes: 5,
|
||||
last_tick_at: null,
|
||||
total_ticks: 0,
|
||||
task_count: 3,
|
||||
pending_escalations: 0,
|
||||
consecutive_failures: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Subconscious is paused')).toBeInTheDocument();
|
||||
expect(screen.getByText(/configure a local Subconscious provider/i)).toBeInTheDocument();
|
||||
|
||||
const runNow = screen.getByRole('button', { name: /Run Now/i });
|
||||
expect(runNow).toBeDisabled();
|
||||
fireEvent.click(runNow);
|
||||
expect(triggerTick).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /AI settings/i }));
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/settings/llm');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,7 @@ function refl(overrides: Partial<Reflection> = {}): Reflection {
|
||||
created_at: 1,
|
||||
acted_on_at: null,
|
||||
dismissed_at: null,
|
||||
thread_id: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ const baseHeartbeatSettings = {
|
||||
meeting_lookahead_minutes: 60,
|
||||
max_calendar_connections_per_tick: 2,
|
||||
reminder_lookahead_minutes: 30,
|
||||
subconscious_mode: 'off' as 'off' | 'simple' | 'aggressive',
|
||||
};
|
||||
|
||||
const baseUsage = {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useSubconscious } from '../useSubconscious';
|
||||
|
||||
const mockStatus = {
|
||||
result: {
|
||||
enabled: true,
|
||||
mode: 'simple',
|
||||
provider_available: true,
|
||||
provider_unavailable_reason: null,
|
||||
interval_minutes: 30,
|
||||
last_tick_at: null,
|
||||
total_ticks: 0,
|
||||
consecutive_failures: 0,
|
||||
},
|
||||
logs: [],
|
||||
};
|
||||
|
||||
const mockSettings = {
|
||||
result: {
|
||||
settings: {
|
||||
enabled: true,
|
||||
interval_minutes: 30,
|
||||
inference_enabled: true,
|
||||
notify_meetings: false,
|
||||
notify_reminders: false,
|
||||
notify_relevant_events: false,
|
||||
external_delivery_enabled: false,
|
||||
meeting_lookahead_minutes: 120,
|
||||
max_calendar_connections_per_tick: 2,
|
||||
reminder_lookahead_minutes: 30,
|
||||
subconscious_mode: 'simple' as const,
|
||||
},
|
||||
},
|
||||
logs: [],
|
||||
};
|
||||
|
||||
let currentMode = 'simple';
|
||||
|
||||
vi.mock('../../utils/tauriCommands', () => ({
|
||||
isTauri: () => true,
|
||||
subconsciousStatus: vi.fn(async () => mockStatus),
|
||||
subconsciousTrigger: vi.fn(async () => ({ result: { triggered: true }, logs: [] })),
|
||||
openhumanHeartbeatSettingsGet: vi.fn(async () => ({
|
||||
result: { settings: { ...mockSettings.result.settings, subconscious_mode: currentMode } },
|
||||
logs: [],
|
||||
})),
|
||||
openhumanHeartbeatSettingsSet: vi.fn(async (patch: Record<string, unknown>) => {
|
||||
if (patch.subconscious_mode) currentMode = patch.subconscious_mode as string;
|
||||
return {
|
||||
result: {
|
||||
settings: { ...mockSettings.result.settings, ...patch, subconscious_mode: currentMode },
|
||||
},
|
||||
logs: [],
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useSubconscious', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
currentMode = 'simple';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('loads status and mode on mount', async () => {
|
||||
const { result } = renderHook(() => useSubconscious());
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
expect(result.current.mode).toBe('simple');
|
||||
expect(result.current.intervalMinutes).toBe(30);
|
||||
expect(result.current.status).not.toBeNull();
|
||||
});
|
||||
|
||||
it('setMode calls heartbeat settings set', async () => {
|
||||
const { openhumanHeartbeatSettingsSet } = await import('../../utils/tauriCommands');
|
||||
const { result } = renderHook(() => useSubconscious());
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.setMode('aggressive');
|
||||
});
|
||||
|
||||
expect(openhumanHeartbeatSettingsSet).toHaveBeenCalledWith({ subconscious_mode: 'aggressive' });
|
||||
expect(result.current.mode).toBe('aggressive');
|
||||
});
|
||||
|
||||
it('setIntervalMinutes calls heartbeat settings set', async () => {
|
||||
const { openhumanHeartbeatSettingsSet } = await import('../../utils/tauriCommands');
|
||||
const { result } = renderHook(() => useSubconscious());
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.setIntervalMinutes(15);
|
||||
});
|
||||
|
||||
expect(openhumanHeartbeatSettingsSet).toHaveBeenCalledWith({ interval_minutes: 15 });
|
||||
});
|
||||
|
||||
it('triggerTick calls subconsciousTrigger', async () => {
|
||||
const { subconsciousTrigger } = await import('../../utils/tauriCommands');
|
||||
const { result } = renderHook(() => useSubconscious());
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.triggerTick();
|
||||
});
|
||||
|
||||
expect(subconsciousTrigger).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,62 +1,42 @@
|
||||
/**
|
||||
* useSubconscious — hook for the subconscious engine UI.
|
||||
*
|
||||
* Provides tasks, escalations, execution log, and actions for the
|
||||
* Provides status, mode control, and engine actions for the
|
||||
* subconscious tab on the Intelligence page.
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
isTauri,
|
||||
subconsciousEscalationsApprove,
|
||||
subconsciousEscalationsDismiss,
|
||||
subconsciousEscalationsList,
|
||||
subconsciousLogList,
|
||||
openhumanHeartbeatSettingsGet,
|
||||
openhumanHeartbeatSettingsSet,
|
||||
subconsciousStatus,
|
||||
subconsciousTasksAdd,
|
||||
subconsciousTasksList,
|
||||
subconsciousTasksRemove,
|
||||
subconsciousTasksUpdate,
|
||||
subconsciousTrigger,
|
||||
} from '../utils/tauriCommands';
|
||||
import type {
|
||||
SubconsciousEscalation,
|
||||
SubconsciousLogEntry,
|
||||
SubconsciousStatus,
|
||||
SubconsciousTask,
|
||||
} from '../utils/tauriCommands/subconscious';
|
||||
import type { SubconsciousMode } from '../utils/tauriCommands/heartbeat';
|
||||
import type { SubconsciousStatus } from '../utils/tauriCommands/subconscious';
|
||||
|
||||
export interface UseSubconsciousResult {
|
||||
// Data
|
||||
tasks: SubconsciousTask[];
|
||||
escalations: SubconsciousEscalation[];
|
||||
logEntries: SubconsciousLogEntry[];
|
||||
status: SubconsciousStatus | null;
|
||||
|
||||
// Loading states
|
||||
mode: SubconsciousMode;
|
||||
intervalMinutes: number;
|
||||
loading: boolean;
|
||||
triggering: boolean;
|
||||
|
||||
// Actions
|
||||
settingMode: boolean;
|
||||
refresh: () => Promise<void>;
|
||||
triggerTick: () => Promise<void>;
|
||||
addTask: (title: string) => Promise<void>;
|
||||
removeTask: (taskId: string) => Promise<void>;
|
||||
toggleTask: (taskId: string, enabled: boolean) => Promise<void>;
|
||||
approveEscalation: (escalationId: string) => Promise<void>;
|
||||
dismissEscalation: (escalationId: string) => Promise<void>;
|
||||
|
||||
// Error
|
||||
setMode: (mode: SubconsciousMode) => Promise<void>;
|
||||
setIntervalMinutes: (minutes: number) => Promise<void>;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useSubconscious(): UseSubconsciousResult {
|
||||
const [tasks, setTasks] = useState<SubconsciousTask[]>([]);
|
||||
const [escalations, setEscalations] = useState<SubconsciousEscalation[]>([]);
|
||||
const [logEntries, setLogEntries] = useState<SubconsciousLogEntry[]>([]);
|
||||
const [status, setStatus] = useState<SubconsciousStatus | null>(null);
|
||||
const [mode, setModeState] = useState<SubconsciousMode>('off');
|
||||
const [intervalMinutes, setIntervalState] = useState(30);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [triggering, setTriggering] = useState(false);
|
||||
const [settingMode, setSettingMode] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const fetchingRef = useRef(false);
|
||||
|
||||
@@ -66,25 +46,24 @@ export function useSubconscious(): UseSubconsciousResult {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Each RPC is bounded by RPC_TIMEOUT_MS so Promise.all is guaranteed
|
||||
// to settle. Without this, a single hung request (e.g. sidecar held
|
||||
// in a long-running tick) would leave fetchingRef.current === true
|
||||
// forever, and every subsequent 3s poll would silently no-op at the
|
||||
// early-return above — freezing the Intelligence page on a stale
|
||||
// snapshot. withTimeout returns null on timeout, matching the
|
||||
// existing `.catch(() => null)` failure contract, so downstream
|
||||
// setState calls just skip that slice for this tick.
|
||||
const [tasksRes, escalationsRes, logRes, statusRes] = await Promise.all([
|
||||
withTimeout(subconsciousTasksList()),
|
||||
withTimeout(subconsciousEscalationsList('pending')),
|
||||
withTimeout(subconsciousLogList(undefined, 30)),
|
||||
const [statusRes, settingsRes] = await Promise.all([
|
||||
withTimeout(subconsciousStatus()),
|
||||
withTimeout(openhumanHeartbeatSettingsGet()),
|
||||
]);
|
||||
|
||||
if (tasksRes) setTasks(unwrap(tasksRes) ?? []);
|
||||
if (escalationsRes) setEscalations(unwrap(escalationsRes) ?? []);
|
||||
if (logRes) setLogEntries(unwrap(logRes) ?? []);
|
||||
if (statusRes) setStatus(unwrap(statusRes) ?? null);
|
||||
const settings = settingsRes
|
||||
? unwrap<{ settings: { subconscious_mode: SubconsciousMode; interval_minutes: number } }>(
|
||||
settingsRes
|
||||
)
|
||||
: null;
|
||||
if (settings?.settings) {
|
||||
if (settings.settings.subconscious_mode) {
|
||||
setModeState(settings.settings.subconscious_mode);
|
||||
}
|
||||
if (settings.settings.interval_minutes) {
|
||||
setIntervalState(settings.settings.interval_minutes);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load subconscious data');
|
||||
} finally {
|
||||
@@ -105,85 +84,37 @@ export function useSubconscious(): UseSubconsciousResult {
|
||||
}
|
||||
}, [triggering]);
|
||||
|
||||
const addTask = useCallback(
|
||||
async (title: string) => {
|
||||
const setMode = useCallback(
|
||||
async (newMode: SubconsciousMode) => {
|
||||
if (!isTauri()) return;
|
||||
setSettingMode(true);
|
||||
setModeState(newMode);
|
||||
try {
|
||||
await subconsciousTasksAdd(title);
|
||||
await openhumanHeartbeatSettingsSet({ subconscious_mode: newMode });
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
console.warn('[subconscious] add task failed:', err);
|
||||
throw err;
|
||||
console.warn('[subconscious] setMode failed:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to update mode');
|
||||
} finally {
|
||||
setSettingMode(false);
|
||||
}
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const removeTask = useCallback(
|
||||
async (taskId: string) => {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
await subconsciousTasksRemove(taskId);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
console.warn('[subconscious] remove task failed:', err);
|
||||
}
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
const setIntervalMinutes = useCallback(async (minutes: number) => {
|
||||
if (!isTauri()) return;
|
||||
setIntervalState(minutes);
|
||||
try {
|
||||
await openhumanHeartbeatSettingsSet({ interval_minutes: minutes });
|
||||
} catch (err) {
|
||||
console.warn('[subconscious] setInterval failed:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleTask = useCallback(
|
||||
async (taskId: string, enabled: boolean) => {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
await subconsciousTasksUpdate(taskId, { enabled });
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
console.warn('[subconscious] toggle task failed:', err);
|
||||
}
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const approveEscalation = useCallback(
|
||||
async (escalationId: string) => {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
await subconsciousEscalationsApprove(escalationId);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
console.warn('[subconscious] approve failed:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const dismissEscalation = useCallback(
|
||||
async (escalationId: string) => {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
await subconsciousEscalationsDismiss(escalationId);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
console.warn('[subconscious] dismiss failed:', err);
|
||||
}
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
|
||||
// Poll every 3s while the hook is mounted (user is on Subconscious tab).
|
||||
// Picks up all state changes: in_progress → act/noop/escalate/failed,
|
||||
// new escalations, background tick completions, etc.
|
||||
//
|
||||
// On unmount we also clear fetchingRef — otherwise a request that times
|
||||
// out or resolves after the component has been torn down would leave the
|
||||
// ref stuck `true` for the next mount (React Strict Mode double-mount in
|
||||
// dev, or tab navigation back to Intelligence), silently wedging the
|
||||
// poller exactly as before.
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
const interval = setInterval(refresh, 3000);
|
||||
const interval = setInterval(refresh, 5000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
fetchingRef.current = false;
|
||||
@@ -191,36 +122,22 @@ export function useSubconscious(): UseSubconsciousResult {
|
||||
}, [refresh]);
|
||||
|
||||
return {
|
||||
tasks,
|
||||
escalations,
|
||||
logEntries,
|
||||
status,
|
||||
mode,
|
||||
intervalMinutes,
|
||||
loading,
|
||||
triggering,
|
||||
settingMode,
|
||||
refresh,
|
||||
triggerTick,
|
||||
addTask,
|
||||
removeTask,
|
||||
toggleTask,
|
||||
approveEscalation,
|
||||
dismissEscalation,
|
||||
setMode,
|
||||
setIntervalMinutes,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-RPC client-side timeout for the polling refresh. Must be strictly
|
||||
* less than the 3s poll interval so a hung call can't stack up across
|
||||
* ticks. 2500ms leaves a 500ms safety margin.
|
||||
*/
|
||||
const RPC_TIMEOUT_MS = 2500;
|
||||
|
||||
/**
|
||||
* Race a promise against a timeout. Resolves to `null` on timeout or
|
||||
* rejection — matching the prior `.catch(() => null)` contract used by
|
||||
* the refresh logic so downstream code can treat "no data this tick" and
|
||||
* "RPC failed this tick" identically.
|
||||
*/
|
||||
function withTimeout<T>(promise: Promise<T>, ms: number = RPC_TIMEOUT_MS): Promise<T | null> {
|
||||
return Promise.race<T | null>([
|
||||
promise.catch(() => null),
|
||||
@@ -228,13 +145,9 @@ function withTimeout<T>(promise: Promise<T>, ms: number = RPC_TIMEOUT_MS): Promi
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap a CommandResponse — callCoreRpc returns `{ result: T, logs: [...] }`.
|
||||
*/
|
||||
function unwrap<T>(response: unknown): T | null {
|
||||
if (!response || typeof response !== 'object') return null;
|
||||
const r = response as Record<string, unknown>;
|
||||
// CommandResponse shape: { result: T, logs: string[] }
|
||||
if ('result' in r) {
|
||||
return r.result as T;
|
||||
}
|
||||
|
||||
+16
-8
@@ -1882,6 +1882,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'الإجراء المقترح',
|
||||
'reflections.act': 'تنفيذ',
|
||||
'reflections.dismiss': 'تجاهل',
|
||||
'reflections.viewConversation': 'عرض',
|
||||
'subconscious.mode.label': 'وضع اللاوعي',
|
||||
'subconscious.mode.off.title': 'إيقاف',
|
||||
'subconscious.mode.off.desc': 'اللاوعي معطل.',
|
||||
'subconscious.mode.simple.title': 'بسيط',
|
||||
'subconscious.mode.simple.desc': 'مراقبة للقراءة فقط. الوصول للذاكرة والملفات فقط.',
|
||||
'subconscious.mode.aggressive.title': 'مكثف',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'وصول كامل للأدوات. يمكنه الكتابة وإنشاء وكلاء وتفويض المهام.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'الوضع المكثف يمنح اللاوعي وصولاً كاملاً للأدوات بما في ذلك الكتابة وإنشاء الوكلاء الفرعيين.',
|
||||
'subconscious.interval.label': 'التردد',
|
||||
'subconscious.interval.minutes': '{n} د',
|
||||
'subconscious.interval.hours': '{n} س',
|
||||
'subconscious.interval.oneHour': 'ساعة واحدة',
|
||||
'subconscious.interval.oneDay': '24 ساعة',
|
||||
'whatsapp.chatsSynced': 'محادثات مزامنة',
|
||||
'whatsapp.chatSynced': 'محادثة مزامنة',
|
||||
'sync.active': 'نشط',
|
||||
@@ -4061,14 +4077,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'التصفية حسب المصدر',
|
||||
'calls.comingSoonDescription': 'المكالمات بمساعدة الذكاء الاصطناعي قادمة قريباً. ابقَ على اطلاع.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 دقائق',
|
||||
'subconscious.interval.tenMinutes': '10 دقائق',
|
||||
'subconscious.interval.fifteenMinutes': '15 دقيقة',
|
||||
'subconscious.interval.thirtyMinutes': '30 دقيقة',
|
||||
'subconscious.interval.oneHour': 'ساعة واحدة',
|
||||
'subconscious.interval.sixHours': '6 ساعات',
|
||||
'subconscious.interval.twelveHours': '12 ساعة',
|
||||
'subconscious.interval.oneDay': 'يوم واحد',
|
||||
'subconscious.priority.critical': 'حرجة',
|
||||
'subconscious.priority.important': 'مهم',
|
||||
'subconscious.priority.normal': 'عادي',
|
||||
|
||||
+16
-8
@@ -1920,6 +1920,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'প্রস্তাবিত কাজ',
|
||||
'reflections.act': 'কাজ করুন',
|
||||
'reflections.dismiss': 'বাদ দিন',
|
||||
'reflections.viewConversation': 'দেখুন',
|
||||
'subconscious.mode.label': 'অবচেতন মোড',
|
||||
'subconscious.mode.off.title': 'বন্ধ',
|
||||
'subconscious.mode.off.desc': 'অবচেতন নিষ্ক্রিয়।',
|
||||
'subconscious.mode.simple.title': 'সরল',
|
||||
'subconscious.mode.simple.desc': 'শুধুমাত্র পঠনযোগ্য পর্যবেক্ষণ। শুধু মেমরি ও ফাইল অ্যাক্সেস।',
|
||||
'subconscious.mode.aggressive.title': 'আক্রমণাত্মক',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'সম্পূর্ণ টুল অ্যাক্সেস। লিখতে, এজেন্ট তৈরি ও কাজ অর্পণ করতে পারে।',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'আক্রমণাত্মক মোড অবচেতনকে লেখা ও সাব-এজেন্ট তৈরিসহ সম্পূর্ণ টুল অ্যাক্সেস দেয়।',
|
||||
'subconscious.interval.label': 'ফ্রিকোয়েন্সি',
|
||||
'subconscious.interval.minutes': '{n} মি',
|
||||
'subconscious.interval.hours': '{n} ঘ',
|
||||
'subconscious.interval.oneHour': '১ ঘণ্টা',
|
||||
'subconscious.interval.oneDay': '২৪ ঘণ্টা',
|
||||
'whatsapp.chatsSynced': 'চ্যাট সিঙ্ক হয়েছে',
|
||||
'whatsapp.chatSynced': 'চ্যাট সিঙ্ক হয়েছে',
|
||||
'sync.active': 'সক্রিয়',
|
||||
@@ -4129,14 +4145,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'উত্স দ্বারা ফিল্টার',
|
||||
'calls.comingSoonDescription': 'AI-সহায়তা কলগুলি শীঘ্রই আসছে৷ সাথে থাকুন।',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 মিনিট',
|
||||
'subconscious.interval.tenMinutes': '10 মিনিট',
|
||||
'subconscious.interval.fifteenMinutes': '15 মিনিট',
|
||||
'subconscious.interval.thirtyMinutes': '30 মিনিট',
|
||||
'subconscious.interval.oneHour': '1 ঘন্টা',
|
||||
'subconscious.interval.sixHours': '1 ঘন্টা',
|
||||
'subconscious.interval.twelveHours': '[[I18N_SEP_92731]] 12 ঘন্টা',
|
||||
'subconscious.interval.oneDay': '1 দিন',
|
||||
'subconscious.priority.critical': 'গুরুত্বপূর্ণ',
|
||||
'subconscious.priority.important': 'গুরুত্বপূর্ণ',
|
||||
'subconscious.priority.normal': 'স্বাভাবিক',
|
||||
|
||||
+16
-8
@@ -1968,6 +1968,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'Vorgeschlagene Aktion',
|
||||
'reflections.act': 'Handeln',
|
||||
'reflections.dismiss': 'Entlassen',
|
||||
'reflections.viewConversation': 'Ansehen',
|
||||
'subconscious.mode.label': 'Unterbewusstsein-Modus',
|
||||
'subconscious.mode.off.title': 'Aus',
|
||||
'subconscious.mode.off.desc': 'Unterbewusstsein ist deaktiviert.',
|
||||
'subconscious.mode.simple.title': 'Einfach',
|
||||
'subconscious.mode.simple.desc': 'Nur-Lese-Beobachtung. Nur Speicher- und Dateizugriff.',
|
||||
'subconscious.mode.aggressive.title': 'Aggressiv',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Voller Werkzeugzugriff. Kann schreiben, Agenten starten und Aufgaben delegieren.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'Aggressiver Modus gewährt dem Unterbewusstsein vollen Werkzeugzugriff einschließlich Schreiben und Sub-Agenten-Erstellung.',
|
||||
'subconscious.interval.label': 'Häufigkeit',
|
||||
'subconscious.interval.minutes': '{n} Min',
|
||||
'subconscious.interval.hours': '{n} Std',
|
||||
'subconscious.interval.oneHour': '1 Stunde',
|
||||
'subconscious.interval.oneDay': '24 Stunden',
|
||||
'whatsapp.chatsSynced': 'Chats synchronisiert',
|
||||
'whatsapp.chatSynced': 'Chat synchronisiert',
|
||||
'sync.active': 'Aktiv',
|
||||
@@ -4243,14 +4259,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'Nach Quelle filtern',
|
||||
'calls.comingSoonDescription': 'KI-unterstützte Anrufe folgen in Kürze. Bleiben Sie dran.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 Min.',
|
||||
'subconscious.interval.tenMinutes': '10 Min.',
|
||||
'subconscious.interval.fifteenMinutes': '15 Min.',
|
||||
'subconscious.interval.thirtyMinutes': '30 Min.',
|
||||
'subconscious.interval.oneHour': '1 Stunde',
|
||||
'subconscious.interval.sixHours': '6 Stunden',
|
||||
'subconscious.interval.twelveHours': '12 Stunden',
|
||||
'subconscious.interval.oneDay': '1 Tag',
|
||||
'subconscious.priority.critical': 'kritisch',
|
||||
'subconscious.priority.important': 'wichtig',
|
||||
'subconscious.priority.normal': 'normal',
|
||||
|
||||
+18
-8
@@ -2150,6 +2150,24 @@ const en: TranslationMap = {
|
||||
'reflections.proposedAction': 'Proposed Action',
|
||||
'reflections.act': 'Act',
|
||||
'reflections.dismiss': 'Dismiss',
|
||||
'reflections.viewConversation': 'View',
|
||||
|
||||
// Subconscious mode selector
|
||||
'subconscious.mode.label': 'Subconscious Mode',
|
||||
'subconscious.mode.off.title': 'Off',
|
||||
'subconscious.mode.off.desc': 'Subconscious is disabled.',
|
||||
'subconscious.mode.simple.title': 'Simple',
|
||||
'subconscious.mode.simple.desc': 'Read-only observation. Memory and file access only.',
|
||||
'subconscious.mode.aggressive.title': 'Aggressive',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Full tool access. Can write, spawn agents, and delegate tasks.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'Aggressive mode gives the subconscious full tool access including writes and sub-agent spawning.',
|
||||
'subconscious.interval.label': 'Frequency',
|
||||
'subconscious.interval.minutes': '{n} min',
|
||||
'subconscious.interval.hours': '{n}h',
|
||||
'subconscious.interval.oneHour': '1 hour',
|
||||
'subconscious.interval.oneDay': '24 hours',
|
||||
|
||||
// WhatsApp
|
||||
'whatsapp.chatsSynced': 'chats synced',
|
||||
@@ -4429,14 +4447,6 @@ const en: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'Filter by source',
|
||||
'calls.comingSoonDescription': 'AI-assisted calls are coming soon. Stay tuned.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 min',
|
||||
'subconscious.interval.tenMinutes': '10 min',
|
||||
'subconscious.interval.fifteenMinutes': '15 min',
|
||||
'subconscious.interval.thirtyMinutes': '30 min',
|
||||
'subconscious.interval.oneHour': '1 hour',
|
||||
'subconscious.interval.sixHours': '6 hours',
|
||||
'subconscious.interval.twelveHours': '12 hours',
|
||||
'subconscious.interval.oneDay': '1 day',
|
||||
'subconscious.priority.critical': 'critical',
|
||||
'subconscious.priority.important': 'important',
|
||||
'subconscious.priority.normal': 'normal',
|
||||
|
||||
+16
-8
@@ -1960,6 +1960,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'Acción propuesta',
|
||||
'reflections.act': 'Actuar',
|
||||
'reflections.dismiss': 'Descartar',
|
||||
'reflections.viewConversation': 'Ver',
|
||||
'subconscious.mode.label': 'Modo subconsciente',
|
||||
'subconscious.mode.off.title': 'Apagado',
|
||||
'subconscious.mode.off.desc': 'El subconsciente está desactivado.',
|
||||
'subconscious.mode.simple.title': 'Simple',
|
||||
'subconscious.mode.simple.desc': 'Observación de solo lectura. Solo acceso a memoria y archivos.',
|
||||
'subconscious.mode.aggressive.title': 'Agresivo',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Acceso completo a herramientas. Puede escribir, crear agentes y delegar tareas.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'El modo agresivo otorga al subconsciente acceso completo a herramientas, incluyendo escritura y creación de subagentes.',
|
||||
'subconscious.interval.label': 'Frecuencia',
|
||||
'subconscious.interval.minutes': '{n} min',
|
||||
'subconscious.interval.hours': '{n}h',
|
||||
'subconscious.interval.oneHour': '1 hora',
|
||||
'subconscious.interval.oneDay': '24 horas',
|
||||
'whatsapp.chatsSynced': 'chats sincronizados',
|
||||
'whatsapp.chatSynced': 'chat sincronizado',
|
||||
'sync.active': 'Activo',
|
||||
@@ -4209,14 +4225,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'Filtrar por fuente',
|
||||
'calls.comingSoonDescription': 'Las llamadas asistidas por IA llegarán pronto. Mantente atento.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 minutos',
|
||||
'subconscious.interval.tenMinutes': '10 minutos',
|
||||
'subconscious.interval.fifteenMinutes': '15 minutos',
|
||||
'subconscious.interval.thirtyMinutes': '30 minutos',
|
||||
'subconscious.interval.oneHour': '1 hora',
|
||||
'subconscious.interval.sixHours': '6 horas',
|
||||
'subconscious.interval.twelveHours': '12 horas',
|
||||
'subconscious.interval.oneDay': '1 dia',
|
||||
'subconscious.priority.critical': 'crítico',
|
||||
'subconscious.priority.important': 'importante',
|
||||
'subconscious.priority.normal': 'normales',
|
||||
|
||||
+17
-8
@@ -1966,6 +1966,23 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'Action proposée',
|
||||
'reflections.act': 'Agir',
|
||||
'reflections.dismiss': 'Ignorer',
|
||||
'reflections.viewConversation': 'Voir',
|
||||
'subconscious.mode.label': 'Mode subconscient',
|
||||
'subconscious.mode.off.title': 'Désactivé',
|
||||
'subconscious.mode.off.desc': 'Le subconscient est désactivé.',
|
||||
'subconscious.mode.simple.title': 'Simple',
|
||||
'subconscious.mode.simple.desc':
|
||||
'Observation en lecture seule. Accès mémoire et fichiers uniquement.',
|
||||
'subconscious.mode.aggressive.title': 'Agressif',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Accès complet aux outils. Peut écrire, créer des agents et déléguer des tâches.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
"Le mode agressif donne au subconscient un accès complet aux outils, y compris l'écriture et la création de sous-agents.",
|
||||
'subconscious.interval.label': 'Fréquence',
|
||||
'subconscious.interval.minutes': '{n} min',
|
||||
'subconscious.interval.hours': '{n}h',
|
||||
'subconscious.interval.oneHour': '1 heure',
|
||||
'subconscious.interval.oneDay': '24 heures',
|
||||
'whatsapp.chatsSynced': 'conversations synchronisées',
|
||||
'whatsapp.chatSynced': 'conversation synchronisée',
|
||||
'sync.active': 'Actif',
|
||||
@@ -4224,14 +4241,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'Filtrer par source',
|
||||
'calls.comingSoonDescription': "Les appels assistés par IA arrivent bientôt. Restez à l'écoute.",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 min',
|
||||
'subconscious.interval.tenMinutes': '10 min',
|
||||
'subconscious.interval.fifteenMinutes': '15 min',
|
||||
'subconscious.interval.thirtyMinutes': '30 min',
|
||||
'subconscious.interval.oneHour': '1 heure',
|
||||
'subconscious.interval.sixHours': '6 heures',
|
||||
'subconscious.interval.twelveHours': '12 heures',
|
||||
'subconscious.interval.oneDay': '1 jour',
|
||||
'subconscious.priority.critical': 'critique',
|
||||
'subconscious.priority.important': 'important',
|
||||
'subconscious.priority.normal': 'normal',
|
||||
|
||||
+16
-8
@@ -1920,6 +1920,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'प्रस्तावित एक्शन',
|
||||
'reflections.act': 'करें',
|
||||
'reflections.dismiss': 'हटाएं',
|
||||
'reflections.viewConversation': 'देखें',
|
||||
'subconscious.mode.label': 'अवचेतन मोड',
|
||||
'subconscious.mode.off.title': 'बंद',
|
||||
'subconscious.mode.off.desc': 'अवचेतन अक्षम है।',
|
||||
'subconscious.mode.simple.title': 'सरल',
|
||||
'subconscious.mode.simple.desc': 'केवल-पठन अवलोकन। केवल मेमोरी और फ़ाइल एक्सेस।',
|
||||
'subconscious.mode.aggressive.title': 'आक्रामक',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'पूर्ण टूल एक्सेस। लिख सकता है, एजेंट बना सकता है और कार्य सौंप सकता है।',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'आक्रामक मोड अवचेतन को लेखन और उप-एजेंट निर्माण सहित पूर्ण टूल एक्सेस देता है।',
|
||||
'subconscious.interval.label': 'आवृत्ति',
|
||||
'subconscious.interval.minutes': '{n} मि',
|
||||
'subconscious.interval.hours': '{n} घं',
|
||||
'subconscious.interval.oneHour': '1 घंटा',
|
||||
'subconscious.interval.oneDay': '24 घंटे',
|
||||
'whatsapp.chatsSynced': 'चैट्स सिंक हुईं',
|
||||
'whatsapp.chatSynced': 'चैट सिंक हुई',
|
||||
'sync.active': 'एक्टिव',
|
||||
@@ -4138,14 +4154,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'स्रोत के अनुसार फ़िल्टर करें',
|
||||
'calls.comingSoonDescription': 'एआई-सहायक कॉल जल्द ही आ रही हैं। बने रहें।',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 मिनट',
|
||||
'subconscious.interval.tenMinutes': '10 मिनट',
|
||||
'subconscious.interval.fifteenMinutes': '15 मि',
|
||||
'subconscious.interval.thirtyMinutes': '30 मि',
|
||||
'subconscious.interval.oneHour': '1 घंटा',
|
||||
'subconscious.interval.sixHours': '6 घंटे',
|
||||
'subconscious.interval.twelveHours': '12 घंटे',
|
||||
'subconscious.interval.oneDay': '1 दिन',
|
||||
'subconscious.priority.critical': 'आलोचनात्मक',
|
||||
'subconscious.priority.important': 'महत्वपूर्ण',
|
||||
'subconscious.priority.normal': 'सामान्य',
|
||||
|
||||
+16
-8
@@ -1923,6 +1923,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'Tindakan yang Diusulkan',
|
||||
'reflections.act': 'Tindakan',
|
||||
'reflections.dismiss': 'Abaikan',
|
||||
'reflections.viewConversation': 'Lihat',
|
||||
'subconscious.mode.label': 'Mode Alam Bawah Sadar',
|
||||
'subconscious.mode.off.title': 'Mati',
|
||||
'subconscious.mode.off.desc': 'Alam bawah sadar dinonaktifkan.',
|
||||
'subconscious.mode.simple.title': 'Sederhana',
|
||||
'subconscious.mode.simple.desc': 'Pengamatan hanya-baca. Hanya akses memori dan file.',
|
||||
'subconscious.mode.aggressive.title': 'Agresif',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Akses alat penuh. Dapat menulis, membuat agen, dan mendelegasikan tugas.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'Mode agresif memberikan alam bawah sadar akses alat penuh termasuk menulis dan membuat sub-agen.',
|
||||
'subconscious.interval.label': 'Frekuensi',
|
||||
'subconscious.interval.minutes': '{n} mnt',
|
||||
'subconscious.interval.hours': '{n} jam',
|
||||
'subconscious.interval.oneHour': '1 jam',
|
||||
'subconscious.interval.oneDay': '24 jam',
|
||||
'whatsapp.chatsSynced': 'obrolan disinkronkan',
|
||||
'whatsapp.chatSynced': 'obrolan disinkronkan',
|
||||
'sync.active': 'Aktif',
|
||||
@@ -4149,14 +4165,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'Filter berdasarkan sumber',
|
||||
'calls.comingSoonDescription': 'Panggilan dengan bantuan AI akan segera hadir. Pantau terus.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 menit',
|
||||
'subconscious.interval.tenMinutes': '10 menit',
|
||||
'subconscious.interval.fifteenMinutes': '15 menit',
|
||||
'subconscious.interval.thirtyMinutes': '30 menit',
|
||||
'subconscious.interval.oneHour': '1 jam',
|
||||
'subconscious.interval.sixHours': '6 jam',
|
||||
'subconscious.interval.twelveHours': '12 jam',
|
||||
'subconscious.interval.oneDay': '1 hari',
|
||||
'subconscious.priority.critical': 'kritis',
|
||||
'subconscious.priority.important': 'penting',
|
||||
'subconscious.priority.normal': 'normal',
|
||||
|
||||
+16
-8
@@ -1951,6 +1951,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'Azione proposta',
|
||||
'reflections.act': 'Agisci',
|
||||
'reflections.dismiss': 'Ignora',
|
||||
'reflections.viewConversation': 'Visualizza',
|
||||
'subconscious.mode.label': 'Modalità subconscio',
|
||||
'subconscious.mode.off.title': 'Disattivato',
|
||||
'subconscious.mode.off.desc': 'Il subconscio è disattivato.',
|
||||
'subconscious.mode.simple.title': 'Semplice',
|
||||
'subconscious.mode.simple.desc': 'Osservazione in sola lettura. Solo accesso a memoria e file.',
|
||||
'subconscious.mode.aggressive.title': 'Aggressivo',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Accesso completo agli strumenti. Può scrivere, creare agenti e delegare compiti.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'La modalità aggressiva concede al subconscio accesso completo agli strumenti, inclusa scrittura e creazione di sotto-agenti.',
|
||||
'subconscious.interval.label': 'Frequenza',
|
||||
'subconscious.interval.minutes': '{n} min',
|
||||
'subconscious.interval.hours': '{n}h',
|
||||
'subconscious.interval.oneHour': '1 ora',
|
||||
'subconscious.interval.oneDay': '24 ore',
|
||||
'whatsapp.chatsSynced': 'chat sincronizzate',
|
||||
'whatsapp.chatSynced': 'chat sincronizzata',
|
||||
'sync.active': 'Attivo',
|
||||
@@ -4201,14 +4217,6 @@ const messages: TranslationMap = {
|
||||
'calls.comingSoonDescription':
|
||||
"Le chiamate assistite dall'IA sono in arrivo. Resta sintonizzato.",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 minuti',
|
||||
'subconscious.interval.tenMinutes': '10 minuti',
|
||||
'subconscious.interval.fifteenMinutes': '15 minuti',
|
||||
'subconscious.interval.thirtyMinutes': '30 minuti',
|
||||
'subconscious.interval.oneHour': '1 ora',
|
||||
'subconscious.interval.sixHours': '6 ore',
|
||||
'subconscious.interval.twelveHours': '12 ore',
|
||||
'subconscious.interval.oneDay': '1 giorno',
|
||||
'subconscious.priority.critical': 'critico',
|
||||
'subconscious.priority.important': 'importante',
|
||||
'subconscious.priority.normal': 'normale',
|
||||
|
||||
+15
-8
@@ -1900,6 +1900,21 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': '제안된 작업',
|
||||
'reflections.act': '실행',
|
||||
'reflections.dismiss': '닫기',
|
||||
'reflections.viewConversation': '보기',
|
||||
'subconscious.mode.label': '잠재의식 모드',
|
||||
'subconscious.mode.off.title': '끔',
|
||||
'subconscious.mode.off.desc': '잠재의식이 비활성화되었습니다.',
|
||||
'subconscious.mode.simple.title': '심플',
|
||||
'subconscious.mode.simple.desc': '읽기 전용 관찰. 메모리 및 파일 접근만 가능.',
|
||||
'subconscious.mode.aggressive.title': '적극적',
|
||||
'subconscious.mode.aggressive.desc': '전체 도구 접근. 쓰기, 에이전트 생성, 작업 위임 가능.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'적극적 모드는 잠재의식에 쓰기 및 하위 에이전트 생성을 포함한 전체 도구 접근 권한을 부여합니다.',
|
||||
'subconscious.interval.label': '빈도',
|
||||
'subconscious.interval.minutes': '{n}분',
|
||||
'subconscious.interval.hours': '{n}시간',
|
||||
'subconscious.interval.oneHour': '1시간',
|
||||
'subconscious.interval.oneDay': '24시간',
|
||||
'whatsapp.chatsSynced': '채팅 동기화됨',
|
||||
'whatsapp.chatSynced': '채팅 동기화됨',
|
||||
'sync.active': '활성',
|
||||
@@ -4097,14 +4112,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': '소스별 필터링',
|
||||
'calls.comingSoonDescription': 'AI 지원 통화가 곧 제공됩니다. 기대해 주세요.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5분',
|
||||
'subconscious.interval.tenMinutes': '10분',
|
||||
'subconscious.interval.fifteenMinutes': '15분',
|
||||
'subconscious.interval.thirtyMinutes': '30분',
|
||||
'subconscious.interval.oneHour': '1시간',
|
||||
'subconscious.interval.sixHours': '6시간',
|
||||
'subconscious.interval.twelveHours': '12시간',
|
||||
'subconscious.interval.oneDay': '1일',
|
||||
'subconscious.priority.critical': '심각',
|
||||
'subconscious.priority.important': '중요',
|
||||
'subconscious.priority.normal': '정상',
|
||||
|
||||
+16
-8
@@ -1941,6 +1941,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'Proponowane działanie',
|
||||
'reflections.act': 'Wykonaj',
|
||||
'reflections.dismiss': 'Odrzuć',
|
||||
'reflections.viewConversation': 'Zobacz',
|
||||
'subconscious.mode.label': 'Tryb podświadomości',
|
||||
'subconscious.mode.off.title': 'Wyłączony',
|
||||
'subconscious.mode.off.desc': 'Podświadomość jest wyłączona.',
|
||||
'subconscious.mode.simple.title': 'Prosty',
|
||||
'subconscious.mode.simple.desc': 'Obserwacja tylko do odczytu. Tylko dostęp do pamięci i plików.',
|
||||
'subconscious.mode.aggressive.title': 'Agresywny',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Pełny dostęp do narzędzi. Może pisać, tworzyć agentów i delegować zadania.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'Tryb agresywny daje podświadomości pełny dostęp do narzędzi, w tym pisanie i tworzenie podagentów.',
|
||||
'subconscious.interval.label': 'Częstotliwość',
|
||||
'subconscious.interval.minutes': '{n} min',
|
||||
'subconscious.interval.hours': '{n} godz',
|
||||
'subconscious.interval.oneHour': '1 godzina',
|
||||
'subconscious.interval.oneDay': '24 godziny',
|
||||
'whatsapp.chatsSynced': 'rozmów zsynchronizowano',
|
||||
'whatsapp.chatSynced': 'rozmowa zsynchronizowana',
|
||||
'sync.active': 'Aktywna',
|
||||
@@ -4202,14 +4218,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'Filtruj po źródle',
|
||||
'calls.comingSoonDescription': 'Połączenia wspierane AI pojawią się wkrótce. Bądź na bieżąco.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 min',
|
||||
'subconscious.interval.tenMinutes': '10 min',
|
||||
'subconscious.interval.fifteenMinutes': '15 min',
|
||||
'subconscious.interval.thirtyMinutes': '30 min',
|
||||
'subconscious.interval.oneHour': '1 godz.',
|
||||
'subconscious.interval.sixHours': '6 godz.',
|
||||
'subconscious.interval.twelveHours': '12 godz.',
|
||||
'subconscious.interval.oneDay': '1 dzień',
|
||||
'subconscious.priority.critical': 'krytyczne',
|
||||
'subconscious.priority.important': 'ważne',
|
||||
'subconscious.priority.normal': 'normalne',
|
||||
|
||||
+17
-8
@@ -1957,6 +1957,23 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'Ação Proposta',
|
||||
'reflections.act': 'Agir',
|
||||
'reflections.dismiss': 'Dispensar',
|
||||
'reflections.viewConversation': 'Ver',
|
||||
'subconscious.mode.label': 'Modo subconsciente',
|
||||
'subconscious.mode.off.title': 'Desligado',
|
||||
'subconscious.mode.off.desc': 'O subconsciente está desativado.',
|
||||
'subconscious.mode.simple.title': 'Simples',
|
||||
'subconscious.mode.simple.desc':
|
||||
'Observação somente leitura. Apenas acesso a memória e arquivos.',
|
||||
'subconscious.mode.aggressive.title': 'Agressivo',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Acesso completo a ferramentas. Pode escrever, criar agentes e delegar tarefas.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'O modo agressivo concede ao subconsciente acesso completo a ferramentas, incluindo escrita e criação de subagentes.',
|
||||
'subconscious.interval.label': 'Frequência',
|
||||
'subconscious.interval.minutes': '{n} min',
|
||||
'subconscious.interval.hours': '{n}h',
|
||||
'subconscious.interval.oneHour': '1 hora',
|
||||
'subconscious.interval.oneDay': '24 horas',
|
||||
'whatsapp.chatsSynced': 'chats sincronizados',
|
||||
'whatsapp.chatSynced': 'chat sincronizado',
|
||||
'sync.active': 'Ativo',
|
||||
@@ -4201,14 +4218,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'Filtrar por origem',
|
||||
'calls.comingSoonDescription': 'Chamadas assistidas por IA chegam em breve. Fique ligado.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 minutos',
|
||||
'subconscious.interval.tenMinutes': '10 minutos',
|
||||
'subconscious.interval.fifteenMinutes': '15 minutos',
|
||||
'subconscious.interval.thirtyMinutes': '30 minutos',
|
||||
'subconscious.interval.oneHour': '1 hora',
|
||||
'subconscious.interval.sixHours': '6 horas',
|
||||
'subconscious.interval.twelveHours': '12 horas',
|
||||
'subconscious.interval.oneDay': '1 dia',
|
||||
'subconscious.priority.critical': 'crítico',
|
||||
'subconscious.priority.important': 'importante',
|
||||
'subconscious.priority.normal': 'normal',
|
||||
|
||||
+16
-8
@@ -1933,6 +1933,22 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': 'Предлагаемое действие',
|
||||
'reflections.act': 'Выполнить',
|
||||
'reflections.dismiss': 'Закрыть',
|
||||
'reflections.viewConversation': 'Просмотр',
|
||||
'subconscious.mode.label': 'Режим подсознания',
|
||||
'subconscious.mode.off.title': 'Выкл',
|
||||
'subconscious.mode.off.desc': 'Подсознание отключено.',
|
||||
'subconscious.mode.simple.title': 'Простой',
|
||||
'subconscious.mode.simple.desc': 'Наблюдение в режиме чтения. Только доступ к памяти и файлам.',
|
||||
'subconscious.mode.aggressive.title': 'Агрессивный',
|
||||
'subconscious.mode.aggressive.desc':
|
||||
'Полный доступ к инструментам. Может писать, создавать агентов и делегировать задачи.',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'Агрессивный режим даёт подсознанию полный доступ к инструментам, включая запись и создание подагентов.',
|
||||
'subconscious.interval.label': 'Частота',
|
||||
'subconscious.interval.minutes': '{n} мин',
|
||||
'subconscious.interval.hours': '{n} ч',
|
||||
'subconscious.interval.oneHour': '1 час',
|
||||
'subconscious.interval.oneDay': '24 часа',
|
||||
'whatsapp.chatsSynced': 'чатов синхронизировано',
|
||||
'whatsapp.chatSynced': 'чат синхронизирован',
|
||||
'sync.active': 'Активно',
|
||||
@@ -4169,14 +4185,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': 'Фильтровать по источнику',
|
||||
'calls.comingSoonDescription': 'Звонки с поддержкой ИИ скоро появятся. Следите за обновлениями.',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 минут',
|
||||
'subconscious.interval.tenMinutes': '10 минут',
|
||||
'subconscious.interval.fifteenMinutes': '15 минут',
|
||||
'subconscious.interval.thirtyMinutes': '30 минут',
|
||||
'subconscious.interval.oneHour': '1 час',
|
||||
'subconscious.interval.sixHours': '6 часов',
|
||||
'subconscious.interval.twelveHours': '12 часов',
|
||||
'subconscious.interval.oneDay': '1 день',
|
||||
'subconscious.priority.critical': 'критический',
|
||||
'subconscious.priority.important': 'важный',
|
||||
'subconscious.priority.normal': 'нормальный',
|
||||
|
||||
@@ -1823,6 +1823,21 @@ const messages: TranslationMap = {
|
||||
'reflections.proposedAction': '建议操作',
|
||||
'reflections.act': '执行',
|
||||
'reflections.dismiss': '忽略',
|
||||
'reflections.viewConversation': '查看',
|
||||
'subconscious.mode.label': '潜意识模式',
|
||||
'subconscious.mode.off.title': '关闭',
|
||||
'subconscious.mode.off.desc': '潜意识已禁用。',
|
||||
'subconscious.mode.simple.title': '简单',
|
||||
'subconscious.mode.simple.desc': '只读观察。仅可访问记忆和文件。',
|
||||
'subconscious.mode.aggressive.title': '积极',
|
||||
'subconscious.mode.aggressive.desc': '完整工具访问。可写入、创建代理和委派任务。',
|
||||
'subconscious.mode.aggressiveWarning':
|
||||
'积极模式赋予潜意识完整的工具访问权限,包括写入和创建子代理。',
|
||||
'subconscious.interval.label': '频率',
|
||||
'subconscious.interval.minutes': '{n}分钟',
|
||||
'subconscious.interval.hours': '{n}小时',
|
||||
'subconscious.interval.oneHour': '1小时',
|
||||
'subconscious.interval.oneDay': '24小时',
|
||||
'whatsapp.chatsSynced': '个对话已同步',
|
||||
'whatsapp.chatSynced': '个对话已同步',
|
||||
'sync.active': '活跃',
|
||||
@@ -3934,14 +3949,6 @@ const messages: TranslationMap = {
|
||||
'memory.sourceFilterAria': '按来源过滤',
|
||||
'calls.comingSoonDescription': '人工智能辅助通话即将推出。敬请关注。',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5分钟',
|
||||
'subconscious.interval.tenMinutes': '10分钟',
|
||||
'subconscious.interval.fifteenMinutes': '15分钟',
|
||||
'subconscious.interval.thirtyMinutes': '30分钟',
|
||||
'subconscious.interval.oneHour': '1小时',
|
||||
'subconscious.interval.sixHours': '6小时',
|
||||
'subconscious.interval.twelveHours': '12小时',
|
||||
'subconscious.interval.oneDay': '1天',
|
||||
'subconscious.priority.critical': '批评的',
|
||||
'subconscious.priority.important': '重要的',
|
||||
'subconscious.priority.normal': '正常',
|
||||
|
||||
@@ -42,21 +42,15 @@ export default function Intelligence() {
|
||||
|
||||
// Subconscious engine data
|
||||
const {
|
||||
tasks: subconsciousTasks,
|
||||
escalations,
|
||||
logEntries,
|
||||
status: subconsciousEngineStatus,
|
||||
loading: subconsciousLoading,
|
||||
mode: subconsciousMode,
|
||||
intervalMinutes: subconsciousInterval,
|
||||
triggering: subconsciousTriggering,
|
||||
settingMode: subconsciousSettingMode,
|
||||
triggerTick,
|
||||
addTask: addSubconsciousTask,
|
||||
removeTask: removeSubconsciousTask,
|
||||
toggleTask: toggleSubconsciousTask,
|
||||
approveEscalation,
|
||||
dismissEscalation,
|
||||
setMode: setSubconsciousMode,
|
||||
setIntervalMinutes: setSubconsciousInterval,
|
||||
} = useSubconscious();
|
||||
const [newTaskTitle, setNewTaskTitle] = useState('');
|
||||
const [expandedLogIds, setExpandedLogIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Socket integration
|
||||
const socketManager = useIntelligenceSocketManager();
|
||||
@@ -175,22 +169,14 @@ export default function Intelligence() {
|
||||
|
||||
{activeTab === 'subconscious' && (
|
||||
<IntelligenceSubconsciousTab
|
||||
addSubconsciousTask={addSubconsciousTask}
|
||||
approveEscalation={approveEscalation}
|
||||
dismissEscalation={dismissEscalation}
|
||||
escalations={escalations}
|
||||
expandedLogIds={expandedLogIds}
|
||||
loading={subconsciousLoading}
|
||||
logEntries={logEntries}
|
||||
newTaskTitle={newTaskTitle}
|
||||
removeSubconsciousTask={removeSubconsciousTask}
|
||||
setExpandedLogIds={setExpandedLogIds}
|
||||
setNewTaskTitle={setNewTaskTitle}
|
||||
status={subconsciousEngineStatus}
|
||||
tasks={subconsciousTasks}
|
||||
toggleSubconsciousTask={toggleSubconsciousTask}
|
||||
mode={subconsciousMode}
|
||||
intervalMinutes={subconsciousInterval}
|
||||
triggerTick={triggerTick}
|
||||
triggering={subconsciousTriggering}
|
||||
settingMode={subconsciousSettingMode}
|
||||
setMode={setSubconsciousMode}
|
||||
setIntervalMinutes={setSubconsciousInterval}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ import type { ChannelConnectionStatus, ChannelDefinition, ChannelType } from '..
|
||||
import type { ToastNotification } from '../types/intelligence';
|
||||
import { IS_DEV } from '../utils/config';
|
||||
import { isLocalSessionToken } from '../utils/localSession';
|
||||
import { openhumanComposioGetMode, subconsciousEscalationsDismiss } from '../utils/tauriCommands';
|
||||
import { openhumanComposioGetMode } from '../utils/tauriCommands';
|
||||
import SkillsDashboard from './SkillsDashboard';
|
||||
|
||||
function channelStatusLabel(status: ChannelConnectionStatus, t: (key: string) => string): string {
|
||||
@@ -426,43 +426,6 @@ export default function Skills() {
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
}, []);
|
||||
const pendingEscalationId =
|
||||
location.state &&
|
||||
typeof location.state === 'object' &&
|
||||
'subconsciousEscalationId' in location.state &&
|
||||
typeof location.state.subconsciousEscalationId === 'string'
|
||||
? location.state.subconsciousEscalationId
|
||||
: null;
|
||||
|
||||
const clearPendingEscalationState = useCallback(() => {
|
||||
navigate(location.pathname, { replace: true, state: null });
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
const dismissPendingEscalationIfResolved = useCallback(
|
||||
async (resolution: string) => {
|
||||
if (!pendingEscalationId) return;
|
||||
console.debug('[skills][subconscious] dismiss escalation:start', {
|
||||
escalationId: pendingEscalationId,
|
||||
resolution,
|
||||
});
|
||||
try {
|
||||
await subconsciousEscalationsDismiss(pendingEscalationId);
|
||||
console.debug('[skills][subconscious] dismiss escalation:success', {
|
||||
escalationId: pendingEscalationId,
|
||||
resolution,
|
||||
});
|
||||
} catch (error) {
|
||||
console.debug('[skills][subconscious] dismiss escalation:error', {
|
||||
escalationId: pendingEscalationId,
|
||||
resolution,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
clearPendingEscalationState();
|
||||
},
|
||||
[clearPendingEscalationState, pendingEscalationId]
|
||||
);
|
||||
|
||||
// Discover SKILL.md skills via the core RPC. Ignore failures — the rest of
|
||||
// the page still works when the sidecar is unreachable or no skills exist.
|
||||
@@ -1156,7 +1119,6 @@ export default function Skills() {
|
||||
}
|
||||
onChanged={() => {
|
||||
void refreshComposio();
|
||||
void dismissPendingEscalationIfResolved(`composio:${composioModalToolkit.slug}`);
|
||||
}}
|
||||
onClose={() => setComposioModalToolkit(null)}
|
||||
/>
|
||||
|
||||
@@ -59,11 +59,7 @@ vi.mock('../../utils/tauriCommands', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../utils/tauriCommands')>(
|
||||
'../../utils/tauriCommands'
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
openhumanComposioGetMode: vi.fn(async () => composioModeStatus),
|
||||
subconsciousEscalationsDismiss: vi.fn(),
|
||||
};
|
||||
return { ...actual, openhumanComposioGetMode: vi.fn(async () => composioModeStatus) };
|
||||
});
|
||||
|
||||
describe('Skills page — Composio catalog fallback', () => {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import { callCoreRpc } from '../../services/coreRpcClient';
|
||||
import { type CommandResponse, isTauri } from './common';
|
||||
|
||||
export type SubconsciousMode = 'off' | 'simple' | 'aggressive';
|
||||
|
||||
export interface HeartbeatSettings {
|
||||
enabled: boolean;
|
||||
interval_minutes: number;
|
||||
@@ -15,6 +17,7 @@ export interface HeartbeatSettings {
|
||||
meeting_lookahead_minutes: number;
|
||||
max_calendar_connections_per_tick: number;
|
||||
reminder_lookahead_minutes: number;
|
||||
subconscious_mode: SubconsciousMode;
|
||||
}
|
||||
|
||||
export type HeartbeatSettingsPatch = Partial<HeartbeatSettings>;
|
||||
|
||||
@@ -1,62 +1,26 @@
|
||||
/**
|
||||
* Subconscious engine commands — task management, escalations, execution log.
|
||||
* Subconscious engine commands — thoughts (reflections) and engine control.
|
||||
*/
|
||||
import { callCoreRpc } from '../../services/coreRpcClient';
|
||||
import { type CommandResponse, isTauri } from './common';
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SubconsciousTask {
|
||||
id: string;
|
||||
title: string;
|
||||
source: 'system' | 'user';
|
||||
recurrence: string;
|
||||
enabled: boolean;
|
||||
last_run_at: number | null;
|
||||
next_run_at: number | null;
|
||||
completed: boolean;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
export interface SubconsciousLogEntry {
|
||||
id: string;
|
||||
task_id: string;
|
||||
tick_at: number;
|
||||
decision: 'noop' | 'act' | 'escalate' | 'dismissed' | string;
|
||||
result: string | null;
|
||||
duration_ms: number | null;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
export interface SubconsciousEscalation {
|
||||
id: string;
|
||||
task_id: string;
|
||||
log_id: string | null;
|
||||
title: string;
|
||||
description: string;
|
||||
priority: 'critical' | 'important' | 'normal';
|
||||
status: 'pending' | 'approved' | 'dismissed';
|
||||
created_at: number;
|
||||
resolved_at: number | null;
|
||||
}
|
||||
|
||||
export interface SubconsciousStatus {
|
||||
enabled: boolean;
|
||||
mode: 'off' | 'simple' | 'aggressive';
|
||||
provider_available: boolean;
|
||||
provider_unavailable_reason: string | null;
|
||||
interval_minutes: number;
|
||||
last_tick_at: number | null;
|
||||
total_ticks: number;
|
||||
task_count: number;
|
||||
pending_escalations: number;
|
||||
consecutive_failures: number;
|
||||
}
|
||||
|
||||
export interface TickResult {
|
||||
tick_at: number;
|
||||
evaluations: Array<{ task_id: string; decision: string; reason: string }>;
|
||||
executed: number;
|
||||
escalated: number;
|
||||
thoughts_count: number;
|
||||
thread_id: string | null;
|
||||
duration_ms: number;
|
||||
}
|
||||
|
||||
@@ -76,101 +40,8 @@ export async function subconsciousTrigger(): Promise<CommandResponse<TickResult>
|
||||
});
|
||||
}
|
||||
|
||||
// ── Tasks CRUD ───────────────────────────────────────────────────────────────
|
||||
// ── Thoughts (Reflections) ──────────────────────────────────────────────────
|
||||
|
||||
export async function subconsciousTasksList(
|
||||
enabledOnly = false
|
||||
): Promise<CommandResponse<SubconsciousTask[]>> {
|
||||
if (!isTauri()) throw new Error('Not running in Tauri');
|
||||
return await callCoreRpc<CommandResponse<SubconsciousTask[]>>({
|
||||
method: 'openhuman.subconscious_tasks_list',
|
||||
params: { enabled_only: enabledOnly },
|
||||
});
|
||||
}
|
||||
|
||||
export async function subconsciousTasksAdd(
|
||||
title: string,
|
||||
source: 'user' | 'system' = 'user'
|
||||
): Promise<CommandResponse<SubconsciousTask>> {
|
||||
if (!isTauri()) throw new Error('Not running in Tauri');
|
||||
return await callCoreRpc<CommandResponse<SubconsciousTask>>({
|
||||
method: 'openhuman.subconscious_tasks_add',
|
||||
params: { title, source },
|
||||
});
|
||||
}
|
||||
|
||||
export async function subconsciousTasksUpdate(
|
||||
taskId: string,
|
||||
patch: { title?: string; enabled?: boolean }
|
||||
): Promise<CommandResponse<{ updated: string }>> {
|
||||
if (!isTauri()) throw new Error('Not running in Tauri');
|
||||
return await callCoreRpc<CommandResponse<{ updated: string }>>({
|
||||
method: 'openhuman.subconscious_tasks_update',
|
||||
params: { task_id: taskId, ...patch },
|
||||
});
|
||||
}
|
||||
|
||||
export async function subconsciousTasksRemove(
|
||||
taskId: string
|
||||
): Promise<CommandResponse<{ removed: string }>> {
|
||||
if (!isTauri()) throw new Error('Not running in Tauri');
|
||||
return await callCoreRpc<CommandResponse<{ removed: string }>>({
|
||||
method: 'openhuman.subconscious_tasks_remove',
|
||||
params: { task_id: taskId },
|
||||
});
|
||||
}
|
||||
|
||||
// ── Log ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function subconsciousLogList(
|
||||
taskId?: string,
|
||||
limit = 50
|
||||
): Promise<CommandResponse<SubconsciousLogEntry[]>> {
|
||||
if (!isTauri()) throw new Error('Not running in Tauri');
|
||||
return await callCoreRpc<CommandResponse<SubconsciousLogEntry[]>>({
|
||||
method: 'openhuman.subconscious_log_list',
|
||||
params: { task_id: taskId, limit },
|
||||
});
|
||||
}
|
||||
|
||||
// ── Escalations ──────────────────────────────────────────────────────────────
|
||||
|
||||
export async function subconsciousEscalationsList(
|
||||
status?: 'pending' | 'approved' | 'dismissed'
|
||||
): Promise<CommandResponse<SubconsciousEscalation[]>> {
|
||||
if (!isTauri()) throw new Error('Not running in Tauri');
|
||||
return await callCoreRpc<CommandResponse<SubconsciousEscalation[]>>({
|
||||
method: 'openhuman.subconscious_escalations_list',
|
||||
params: status ? { status } : {},
|
||||
});
|
||||
}
|
||||
|
||||
export async function subconsciousEscalationsApprove(
|
||||
escalationId: string
|
||||
): Promise<CommandResponse<{ approved: string }>> {
|
||||
if (!isTauri()) throw new Error('Not running in Tauri');
|
||||
return await callCoreRpc<CommandResponse<{ approved: string }>>({
|
||||
method: 'openhuman.subconscious_escalations_approve',
|
||||
params: { escalation_id: escalationId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function subconsciousEscalationsDismiss(
|
||||
escalationId: string
|
||||
): Promise<CommandResponse<{ dismissed: string }>> {
|
||||
if (!isTauri()) throw new Error('Not running in Tauri');
|
||||
return await callCoreRpc<CommandResponse<{ dismissed: string }>>({
|
||||
method: 'openhuman.subconscious_escalations_dismiss',
|
||||
params: { escalation_id: escalationId },
|
||||
});
|
||||
}
|
||||
|
||||
// ── #623: proactive reflection layer ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Categorisation of the underlying signal that produced the reflection.
|
||||
* Mirrors `subconscious::reflection::ReflectionKind` on the Rust side.
|
||||
*/
|
||||
export type ReflectionKind =
|
||||
| 'hotness_spike'
|
||||
| 'cross_source_pattern'
|
||||
@@ -179,45 +50,24 @@ export type ReflectionKind =
|
||||
| 'risk'
|
||||
| 'opportunity';
|
||||
|
||||
/**
|
||||
* One resolved chunk of memory-tree content the reflection LLM cited via
|
||||
* `source_refs`, snapshot at tick time. Mirrors `subconscious::SourceChunk`
|
||||
* on the Rust side. Powers the Intelligence-tab "Sources" disclosure for
|
||||
* transparency, and the orchestrator's memory-context injection into the
|
||||
* system prompt for any chat turn in a thread spawned from the reflection.
|
||||
*
|
||||
* Snapshot semantics — `content` is what the LLM saw at tick time, even
|
||||
* if the underlying entity/summary has since mutated.
|
||||
*/
|
||||
export interface SourceChunk {
|
||||
/** Original opaque ref like `"entity:phoenix"` or `"summary:abc123"`. */
|
||||
ref_id: string;
|
||||
/** Parsed kind portion of `ref_id`. `"entity"`, `"summary"`, etc. */
|
||||
kind: string;
|
||||
/** Resolved chunk preview at tick time. Empty if no resolver matched. */
|
||||
content: string;
|
||||
/** Optional per-kind metadata (display_name, hotness, sealed_at, etc). */
|
||||
metadata?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* One persisted observation about the user's state. Created by the
|
||||
* subconscious tick LLM. Reflections are observation-only — they live
|
||||
* exclusively on the Intelligence tab and never auto-post into any
|
||||
* conversation thread. Tapping the action button (when `proposed_action`
|
||||
* is non-null) spawns a *fresh* conversation thread via `actOnReflection`.
|
||||
*/
|
||||
export interface Reflection {
|
||||
id: string;
|
||||
kind: ReflectionKind;
|
||||
body: string;
|
||||
proposed_action: string | null;
|
||||
source_refs: string[];
|
||||
/** Resolved chunks captured at tick time. See {@link SourceChunk}. */
|
||||
source_chunks?: SourceChunk[];
|
||||
created_at: number;
|
||||
acted_on_at: number | null;
|
||||
dismissed_at: number | null;
|
||||
thread_id: string | null;
|
||||
}
|
||||
|
||||
export async function listReflections(
|
||||
@@ -233,17 +83,6 @@ export async function listReflections(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a fresh conversation thread and seed it with the reflection body
|
||||
* as the FIRST ASSISTANT message (proposed_action appended if present).
|
||||
* No LLM turn fires — the user lands in a thread that opens with the
|
||||
* observation from OpenHuman, ready for them to reply.
|
||||
*
|
||||
* Marks `acted_on_at`. Returns the new thread's id so the caller can
|
||||
* navigate the user into the new conversation. Reflections never write
|
||||
* into existing threads — every act gets its own conversation so the
|
||||
* user's main chat surface stays uncluttered.
|
||||
*/
|
||||
export async function actOnReflection(
|
||||
reflectionId: string
|
||||
): Promise<CommandResponse<{ reflection_id: string; thread_id: string }>> {
|
||||
|
||||
Reference in New Issue
Block a user