Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0857c98
refactor(subconscious): replace task evaluator with agent-per-tick model
senamakel May 31, 2026
c1780f6
refactor(ui): simplify subconscious tab for agent-per-tick model
senamakel May 31, 2026
0df4a8b
fix: update remaining test files for agent-per-tick model
senamakel May 31, 2026
e60a3d5
style: apply prettier + cargo fmt formatting
senamakel May 31, 2026
9b76fef
Merge remote-tracking branch 'upstream/main' into pr/3079
senamakel Jun 1, 2026
845a215
feat(subconscious): wire up real agent harness with tool access
senamakel Jun 1, 2026
776fca2
feat(subconscious): add tiered SubconsciousMode (off/simple/aggressive)
senamakel Jun 1, 2026
03b4676
feat(ui): add tiered mode selector to subconscious tab
senamakel Jun 1, 2026
a656b56
fix(subconscious): add subconscious_mode to heartbeat RPC schema
senamakel Jun 1, 2026
06c82c7
feat(ui): add frequency slider to subconscious tab
senamakel Jun 1, 2026
b296a03
fix(inference): surface actionable error when Managed route fails wit…
senamakel-droid Jun 1, 2026
aa91c66
fix(memory/safety): exclude bare-phone patterns from strict PII rejec…
oxoxDev Jun 1, 2026
ea1a5b8
test: green Rust Core Coverage (stale assertions + env-race serializa…
sanil-23 Jun 1, 2026
8a6a253
fix(composio): complete connection-disconnect cleanup (config entry +…
sanil-23 Jun 1, 2026
de85d1f
feat(analytics): wire product event tracking (#3123)
senamakel Jun 2, 2026
cb4b897
refactor(agent): unwire eager 7-day memory-tree digest from turn loop…
sanil-23 Jun 2, 2026
0257f09
fix: resolve merge conflict in subconscious store.rs
senamakel Jun 2, 2026
68ecb69
fix: address CodeRabbit review feedback
senamakel Jun 2, 2026
ad85d00
test: add useSubconscious hook tests for coverage gate
senamakel Jun 2, 2026
461d28b
fix: add missing afterEach import in useSubconscious test
senamakel Jun 2, 2026
3e33e12
style: format useSubconscious test with prettier
senamakel Jun 2, 2026
bfd73ee
fix(subconscious): track agent failures separately from empty ticks
senamakel Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
654 changes: 170 additions & 484 deletions app/src/components/intelligence/IntelligenceSubconsciousTab.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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(),
};
}

Expand All @@ -68,48 +48,41 @@ describe('IntelligenceSubconsciousTab', () => {
vi.restoreAllMocks();
});

it('on Act → dispatches setSelectedThread + navigates to /chat', () => {
it('renders three mode options', () => {
render(<IntelligenceSubconsciousTab {...baseProps()} />);
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');
expect(screen.getByText('Off')).toBeInTheDocument();
expect(screen.getByText('Simple')).toBeInTheDocument();
expect(screen.getByText('Aggressive')).toBeInTheDocument();
});

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,
}}
/>
);
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');
});

expect(screen.getByText('Subconscious is paused')).toBeInTheDocument();
expect(screen.getByText(/configure a local Subconscious provider/i)).toBeInTheDocument();
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();
});

const runNow = screen.getByRole('button', { name: /Run Now/i });
expect(runNow).toBeDisabled();
fireEvent.click(runNow);
expect(triggerTick).not.toHaveBeenCalled();
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();
});

fireEvent.click(screen.getByRole('button', { name: /AI settings/i }));
expect(mockNavigate).toHaveBeenCalledWith('/settings/llm');
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'));
expect(mockDispatch).toHaveBeenCalledWith(setSelectedThread('spawned-thread-42'));
expect(mockNavigate).toHaveBeenCalledWith('/chat');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function refl(overrides: Partial<Reflection> = {}): Reflection {
created_at: 1,
acted_on_at: null,
dismissed_at: null,
thread_id: null,
...overrides,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
128 changes: 128 additions & 0 deletions app/src/hooks/__tests__/useSubconscious.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading
Loading