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:
Steven Enamakel
2026-06-01 22:02:46 -07:00
committed by GitHub
co-authored by Steven Enamakel's Droid oxoxDev sanil-23 Claude Opus 4.8
parent 3136ef5483
commit 97ff4f53d3
51 changed files with 1399 additions and 5167 deletions
@@ -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();
});
});
+52 -139
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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': 'нормальный',
+15 -8
View File
@@ -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': '正常',
+10 -24
View File
@@ -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}
/>
)}
+1 -39
View File
@@ -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', () => {
+3
View File
@@ -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>;
+6 -167
View File
@@ -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 }>> {