Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cbb6f98
feat: add in-chat computer use toggle
null-crafter Jul 7, 2026
796088d
feat: enable remote AI chat + MCP tools for computer use
null-crafter Jul 8, 2026
bfe6dd3
feat: Computer Use no longer requires a local model
null-crafter Jul 8, 2026
d9d3e89
chore: bump @iblai/iblai-js to 1.22.5-computer-use-1.12
null-crafter Jul 9, 2026
70712e8
feat: move local llms to llm tab
null-crafter Jul 20, 2026
a826cc0
chore: merge main
null-crafter Jul 22, 2026
9304e57
feat: improve local model handling
null-crafter Jul 22, 2026
a77df3c
feat(mentor): on-device models in the LLM picker — canonical provider…
null-crafter Jul 23, 2026
6a0c9f1
chore: fix deps
null-crafter Jul 23, 2026
6ad3ef4
chore: merge main
null-crafter Jul 23, 2026
cbe8d31
feat: ACP integration
null-crafter Jul 24, 2026
cc09d02
fix: update tests
null-crafter Jul 27, 2026
cfbb189
chore: merge main
null-crafter Jul 27, 2026
e6bcf1b
chore: update lock files
null-crafter Jul 28, 2026
2c85392
chore: merge main
null-crafter Jul 28, 2026
2d5e67d
chore: add translations
null-crafter Jul 28, 2026
be6422f
chore: update lockfile
null-crafter Jul 28, 2026
7a4a318
chore: fix deps
null-crafter Jul 28, 2026
a6e4fdf
chore: fix deps
null-crafter Jul 28, 2026
a3fcee5
chore: fix deps
null-crafter Jul 28, 2026
0bf6639
feat: add ACP implementation
null-crafter Jul 28, 2026
751cc99
chore: merge main
null-crafter Jul 28, 2026
791df32
chore: exclude tauri e2e tests in unit tests
null-crafter Jul 29, 2026
0135366
tests: fix tests
null-crafter Jul 29, 2026
3ef33bd
Merge branch 'main' of https://github.com/iblai/os
null-crafter Jul 29, 2026
a068298
Merge branch 'feat/acp' (ACP / opencode Coding Mode)
null-crafter Jul 30, 2026
27fc2c9
feat: merge in acp support
null-crafter Jul 30, 2026
5e7b97b
feat: cowork mcp updates
null-crafter Jul 30, 2026
41cedd8
chore: merge main
null-crafter Jul 30, 2026
7ed5963
feat: parallel opencode acp for code mode
null-crafter Jul 31, 2026
9bf0133
chore: update pnpm lock
null-crafter Jul 31, 2026
02eaf94
chore: merge main
null-crafter Jul 31, 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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,9 @@ e2e/playwright-report
e2e/test-results.json
playwright/.auth
e2e/playwright/.auth


# harnesses
.omo
.omp
.pi
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,32 @@ vi.mock('@/components/modals/auth-modal', () => ({
}));

let lastCreditBalanceProps: any = null;
// On-device selection, driven per-test; default OFF so existing tests see the
// pre-badge nav-bar. One catalog model lets the badge resolve a display name.
let mockLocalEnabled = false;
let mockLocalModelId: string | null = null;
vi.mock('@iblai/iblai-js/web-containers', () => ({
// The toggle now ships from the SDK; tests don't exercise its internals,
// so a no-render stub is enough — it also blocks the SDK's transitive
// axios chain from being pulled into the test's module graph.
ChatPrivacyToggle: () => null,
// On-device model helpers read by `useSelectedLocalModel` (the top-left
// badge). Default to local mode OFF so the nav-bar renders exactly as it did
// before the badge existed (cloud selector shown, no badge).
isLocalLLMEnabled: () => mockLocalEnabled,
getLocalLLMModel: () => mockLocalModelId,
// The hook reconciles the tool-support cache against the catalog on read.
getLocalLLMToolSupport: () => true,
setLocalLLMToolSupport: () => {},
LOCAL_MODELS: [
{
name: 'Llama 3.2',
provider: 'Meta',
id: 'llama3.2',
size: '2.0 GB',
tool_support: true,
},
],
NotificationDropdown: () => (
<div data-testid="notification-dropdown">Notifications</div>
),
Expand Down Expand Up @@ -540,6 +561,8 @@ describe('NavBar', () => {
};
mockAllTenants = [{ key: 'test-tenant' }];
lastCreditBalanceProps = null;
mockLocalEnabled = false;
mockLocalModelId = null;
// Suppress console.log during tests
vi.spyOn(console, 'log').mockImplementation(() => {});
});
Expand Down Expand Up @@ -619,6 +642,51 @@ describe('NavBar', () => {
expect(screen.getByLabelText('LLM Model Selector')).toBeInTheDocument();
});

it('shows the on-device model badge and hides the cloud selector when local mode is on', () => {
mockIsAdmin = true;
mockUserIsStudent = false;
mockPathname = '/platform/tenant123/mentor456';
mockLocalEnabled = true;
mockLocalModelId = 'llama3.2';
const store = createTestStore();

render(
<Provider store={store}>
<NavBar />
</Provider>,
);

const badge = screen.getByTestId('local-model-indicator');
expect(badge).toHaveTextContent('Llama 3.2');
expect(badge).toHaveTextContent('On-device');
// The on-device badge replaces the cloud model selector while local is on.
expect(
screen.queryByLabelText('LLM Model Selector'),
).not.toBeInTheDocument();
});

it('opens the model picker when the on-device badge is clicked, so an admin can still choose an LLM while local is on', () => {
mockIsAdmin = true;
mockUserIsStudent = false;
mockPathname = '/platform/tenant123/mentor456';
mockLocalEnabled = true;
mockLocalModelId = 'llama3.2';
const store = createTestStore();

render(
<Provider store={store}>
<NavBar />
</Provider>,
);

// Picker is closed until the badge is clicked.
expect(
screen.queryByTestId('llm-provider-modal'),
).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('local-model-indicator'));
expect(screen.getByTestId('llm-provider-modal')).toBeInTheDocument();
});

it('hides the LLM model selector on the tenant-scoped projects index page', () => {
mockIsAdmin = true;
mockUserIsStudent = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Menu,
User,
Bot,
HardDrive,
GitFork,
} from 'lucide-react';

Expand Down Expand Up @@ -84,6 +85,10 @@ import { config } from '@/lib/config';
import { MentorVisibilityEnum } from '@iblai/iblai-api';
import { toast } from 'sonner';
import { useModelDownload } from '@/hooks/use-model-download';
import {
useSelectedLocalModel,
LOCAL_LLM_CHANGED_EVENT,
} from '@/hooks/use-selected-local-model';
import {
useMentorSegments,
MENTOR_SEGMENT_NAV_CATEGORIES,
Expand Down Expand Up @@ -244,6 +249,47 @@ export function NavBar() {
onSelectFoundryModel,
} = useModelDownload();

// Active on-device (local) model, if any. Shown top-left in place of the cloud
// model while local mode is on (chat routes to the local model then, so the
// cloud model would be misleading). Reactive to picks + the master toggle.
const selectedLocal = useSelectedLocalModel();
const localModelLogo = selectedLocal.model
? getLLMProviderDetails(selectedLocal.model.provider, selectedLocal.model.name)
.logo
: '';
const localModelName =
selectedLocal.model?.name ?? selectedLocal.modelId ?? '';
// Admins (non-students) can switch the mentor's LLM; for them the on-device
// badge doubles as the entry point to the model picker (mirrors the cloud
// selector's gate). Others get a plain, non-interactive indicator.
const canChooseLlm = isAdmin && !userIsStudent;
const localModelBadgeInner = (
<>
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-white">
{localModelLogo ? (
<Image
src={localModelLogo}
alt={`${localModelName} model logo`}
className="h-5 w-5 object-contain"
height={32}
width={32}
loading="lazy"
/>
) : (
<HardDrive className="h-4 w-4 text-[#646464]" />
)}
</div>
<span className="hidden max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap lowercase sm:block">
{localModelName}
</span>
<span className="hidden flex-shrink-0 items-center gap-1 rounded-full bg-[#F5F8FF] px-2 py-0.5 text-[11px] font-medium text-[#38A1E5] md:inline-flex">
<HardDrive className="h-3 w-3" aria-hidden="true" />
On-device
</span>
{canChooseLlm && <ChevronDown className="h-4 w-4 text-gray-500" />}
</>
);

console.log('[NavBar] After useModelDownload:', {
isLocalLLMAvailable,
foundryStatus,
Expand Down Expand Up @@ -491,7 +537,46 @@ export function NavBar() {
)}

<div className="flex items-center pl-2 md:pl-4">
{isOnChatPage && isAdmin && !userIsStudent && (
{/* On-device (local) model indicator. Shown while local mode is on;
it replaces the cloud model selector below (hidden via the same
`selectedLocal.isLocal` condition). For users who can switch LLMs
it is ALSO the entry point to the model picker — click to open it
and choose a different model (cloud or local) without first
disabling local mode. */}
{isOnChatPage && selectedLocal.isLocal && (
<Tooltip>
<TooltipTrigger asChild>
{canChooseLlm ? (
<button
type="button"
className="mr-2 flex cursor-pointer items-center gap-1.5 text-sm font-medium text-[#646464] transition-colors hover:text-[#484848]"
onClick={() =>
!userIsVisiting && setIsProviderSelectionOpen(true)
}
aria-label={`Change model, on-device model in use: ${localModelName}`}
data-testid="local-model-indicator"
>
{localModelBadgeInner}
</button>
) : (
<div
className="mr-2 flex items-center gap-1.5 text-sm font-medium text-[#646464]"
aria-label={`On-device model in use: ${localModelName}`}
data-testid="local-model-indicator"
>
{localModelBadgeInner}
</div>
)}
</TooltipTrigger>
<TooltipContent className="ibl-tooltip-content" side="bottom">
{canChooseLlm
? `On-device: ${localModelName} — click to change model`
: `On-device model in use — ${localModelName}`}
</TooltipContent>
</Tooltip>
)}

{isOnChatPage && isAdmin && !userIsStudent && !selectedLocal.isLocal && (
<Tooltip>
<TooltipTrigger asChild>
<Button
Expand Down Expand Up @@ -708,7 +793,12 @@ export function NavBar() {
{isUserProfileOpen && (
<UserProfileModal
isOpen={isUserProfileOpen}
onClose={() => setIsUserProfileOpen(false)}
onClose={() => {
setIsUserProfileOpen(false);
// Profile → Advanced hosts the Local Models master toggle; re-read on
// close so the nav-bar on-device badge reflects an enable/disable.
window.dispatchEvent(new Event(LOCAL_LLM_CHANGED_EVENT));
}}
params={{
tenantKey,
mentorId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
'use client';

import {
useEffect,
useState,
useCallback,
useRef,
type ComponentProps,
} from 'react';
import { useEffect, useState, useCallback, useRef } from 'react';
import {
useParams,
useRouter,
Expand Down Expand Up @@ -44,6 +38,7 @@ import {
} from '@/features/rbac/rbac-slice';
import { useModelDownload } from '@/hooks/use-model-download';
import { useLockedTenant } from '@/hooks/use-tenant-lock';
import { LOCAL_LLM_CHANGED_EVENT } from '@/hooks/use-selected-local-model';

export function UserProfile() {
const username = useUsername();
Expand Down Expand Up @@ -107,6 +102,10 @@ export function UserProfile() {
if (!open) {
// Set flag to prevent useEffect from reopening
isClosingRef.current = true;
// This modal (Profile → Advanced) hosts the Local Models master toggle.
// Notify the nav-bar on-device badge to re-read so it reverts to the
// cloud model indicator when the user disables local models here.
window.dispatchEvent(new Event(LOCAL_LLM_CHANGED_EVENT));
// Clear profileTab from URL when modal closes
const params = new URLSearchParams(searchParams.toString());
params.delete('profileTab');
Expand Down Expand Up @@ -368,15 +367,6 @@ export function UserProfile() {
onResetState: resetState,
onSelectFoundryModel,
}}
// System Control (Computer Assistant): configure the size gate to 13 GB.
// The dropdown self-wires the rest of systemControlProps via useGhostOs;
// the SDK types the object as fully required, but each field falls back at
// runtime, so we intentionally pass only the gate.
systemControlProps={
{ requiredSizeGb: 13 } as unknown as NonNullable<
ComponentProps<typeof UserProfileDropdown>['systemControlProps']
>
}
// Controlled modal state for URL sync
isModalOpen={isProfileModalOpen}
onModalOpenChange={handleModalOpenChange}
Expand Down
66 changes: 64 additions & 2 deletions components/chat-input-form/inside-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,35 @@ import {
PopoverAnchor,
PopoverContent,
} from '@/components/ui/popover';
import { X, BookOpen, Archive, Check, Terminal } from 'lucide-react';
import { X, BookOpen, Archive, Check, Terminal, Monitor } from 'lucide-react';
import { toast } from 'sonner';
import { DeepSearchIcon, CanvasIcon } from '@/components/icons/svg-icons';
import { TOOLS } from '@iblai/iblai-js/web-utils';
import { TOOLS, hasRemoteAiConfig } from '@iblai/iblai-js/web-utils';
import {
useGhostOs,
isSystemControlEnabled,
setSystemControlEnabled,
isLocalLLMEnabled,
getLocalLLMModel,
modelSupportsSystemControl,
} from '@iblai/iblai-js/web-containers';
import { MemoryButton } from './memory-button';
import { MemoryMenu } from './memory-menu';

// Computer Use is macOS-only in prod; the env flag bypasses the OS check so the
// toggle can be exercised on Linux/Windows desktop builds during testing.
const isMacOS = () => {
if (typeof navigator === 'undefined') return false;
return /mac/i.test(navigator.userAgent || '');
};
const allowNonMacOSComputerUse = () =>
process.env.NEXT_PUBLIC_ALLOW_NON_MACOS_COMPUTER_USE_TOGGLE === 'true';

// 12GB floor, matching the SDK default (DEFAULT_SYSTEM_CONTROL_REQUIRED_SIZE_GB)
// and the Local Models tab's "supported" indicator. modelSupportsSystemControl
// gates size <= gb (strictly greater), so a model of exactly 12GB is also off.
const COMPUTER_USE_MIN_MODEL_GB = 12;

interface InsideButtonsProps {
activeOptions: string[];
onOptionClick: (optionName: string) => Promise<void>;
Expand Down Expand Up @@ -61,7 +84,46 @@ export const InsideButtons = ({
username,
}: InsideButtonsProps) => {
const t = useTranslations('chatInputFormInsideButtons');

// Computer Use = the Tauri GhostOS assistant. Same calls as the old profile
// "Computer Assistant" toggle (useGhostOs install/stop + localStorage pref),
// no backend round-trip. Reads the pref on mount; cross-tab sync not polled.
const ghostOs = useGhostOs();
const [computerUseEnabled, setComputerUseEnabled] = useState(isSystemControlEnabled);
const toggleComputerUse = () => {
const next = !computerUseEnabled;
// Guard only when turning on. Computer Use runs on EITHER a large local model
// or the remote AI (DM OpenAI-compatible endpoint) — allow enabling when
// either backend is ready, so a local model is not required. The chatbox has
// no inline notice space, so remind via toast instead of failing silently.
if (next) {
const localReady =
isLocalLLMEnabled() &&
modelSupportsSystemControl(getLocalLLMModel(), COMPUTER_USE_MIN_MODEL_GB);
if (!localReady && !hasRemoteAiConfig()) {
toast.warning(
isLocalLLMEnabled()
? t('computerUseModelTooSmall')
: t('computerUseNeedsLocalModel'),
);
return;
}
}
setComputerUseEnabled(next);
setSystemControlEnabled(next);
if (next) ghostOs.install();
else ghostOs.stop();
};

const allInsideButtons = [
{
name: 'Computer Use',
label: t('computerUse'),
icon: <Monitor className="h-4 w-4" />,
isActive: computerUseEnabled,
action: toggleComputerUse,
isEnabled: ghostOs.isAvailable && (isMacOS() || allowNonMacOSComputerUse()),
},
{
name: 'Canvas',
label: t('canvas'),
Expand Down
Loading
Loading