Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 32 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2569,6 +2569,31 @@ Homepage: https://github.com/vimeo/player.js

Copyright (c) 2016 [Vimeo](https://vimeo.com)

### `@xterm/addon-fit@0.11.0`
Homepage: https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit

Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)

### `@xterm/addon-unicode11@0.9.0`
Homepage: https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode11

Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)

### `@xterm/addon-web-links@0.12.0`
Homepage: https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-links

Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)

### `@xterm/addon-webgl@0.19.0`
Homepage: https://github.com/xtermjs/xterm.js/tree/master/addons/addon-webgl

Copyright (c) 2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)

### `@xterm/xterm@6.0.0`
Homepage: https://github.com/xtermjs/xterm.js

Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com) Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)

### `accepts@2.0.0`
Homepage: https://github.com/jshttp/accepts

Expand Down Expand Up @@ -4390,6 +4415,13 @@ Homepage: https://github.com/prebuild/node-gyp-build

Copyright (c) 2017 Mathias Buus

### `node-pty@1.1.0`
Homepage: https://github.com/microsoft/node-pty

Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)
Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com)
Copyright (c) 2018 - present Microsoft Corporation

### `node-releases@2.0.37`
Homepage: https://github.com/chicoxyzzy/node-releases

Expand Down
18 changes: 18 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,14 @@ export default {
'src/main/index.ts',
'src/preload/index.ts',
'src/utility/server-entry.ts',
'src/utility/pty-host.ts',
'src/**/*.test.ts',
'electron.vite.config.ts',
'scripts/*.mjs',
'tests/**/*.test.ts',
'tests/**/*.test.mjs',
],
ignoreUnresolved: [/utility\/pty-host\.js$/],
project: 'src/**',
},
},
Expand Down
9 changes: 7 additions & 2 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@
{
"name": "main app bundle (gzipped)",
"path": "dist/assets/index-*.js",
"limit": "407 kB",
"limit": "415 kB",
"gzip": true,
"running": false
},
{
"name": "all JS chunks combined (gzipped)",
"path": "dist/assets/*.js",
"limit": "2.85 MB",
"limit": "3 MB",
"gzip": true,
"running": false
},
Expand Down Expand Up @@ -115,6 +115,11 @@
"@tiptap/y-tiptap": "^3.0.3",
"@u-wave/react-vimeo": "^0.9.12",
"@uiw/codemirror-theme-basic": "^4.25.9",
"@xterm/addon-fit": "0.11.0",
"@xterm/addon-unicode11": "0.9.0",
"@xterm/addon-web-links": "0.12.0",
"@xterm/addon-webgl": "0.19.0",
"@xterm/xterm": "6.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/App.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ mock.module('@/lib/config-provider', () => ({
),
}));

mock.module('@/lib/config-context', () => ({
useConfigContext: () => ({ merged: null }),
}));

mock.module('@/lib/api-config', () => ({
fetchApiConfig: (...args: Parameters<typeof fetchApiConfigMock>) => fetchApiConfigMock(...args),
}));
Expand Down
39 changes: 31 additions & 8 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { CreateProjectMenuTrigger } from '@/components/CreateProjectMenuTrigger'
import { EditorPane } from '@/components/EditorPane';
import { FileSidebar } from '@/components/FileSidebar';
import { defaultInitialDir } from '@/components/file-tree-utils';
import {
type TerminalLaunchContextValue,
TerminalLaunchProvider,
} from '@/components/handoff/TerminalLaunchContext';
import { requestTerminalLaunch } from '@/components/handoff/terminal-launch-events';
import { selectScopedPrompt } from '@/components/handoff/useHandoffDispatch';
import { InstallInClaudeDesktopDialog } from '@/components/InstallInClaudeDesktopDialog';
import { McpConsentDialog } from '@/components/McpConsentDialog';
import { isNewItemShortcut, NewItemDialog } from '@/components/NewItemDialog';
Expand All @@ -24,6 +30,7 @@ import {
useDocumentTransition,
} from '@/editor/DocumentContext';
import { fetchApiConfig } from '@/lib/api-config';
import { useConfigContext } from '@/lib/config-context';
import { ConfigProvider } from '@/lib/config-provider';
import { assetPathFromHash, docNameFromHash, isContentRootHash } from '@/lib/doc-hash';
import { mark, ProfilerBoundary } from '@/lib/perf';
Expand Down Expand Up @@ -316,6 +323,16 @@ function AppBody() {
const isElectronHost = typeof window !== 'undefined' && window.okDesktop != null;
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
const singleFile = useSingleFileMode();
const { merged } = useConfigContext();
const autoOpen = merged?.appearance?.preview?.autoOpen ?? true;

const terminalLaunch: TerminalLaunchContextValue | null = desktopBridge
? {
launchInTerminal: (input) => {
requestTerminalLaunch(selectScopedPrompt(input, 'claude-code', autoOpen));
},
}
: null;

return (
<>
Expand Down Expand Up @@ -363,14 +380,20 @@ function AppBody() {
className="pointer-events-none fixed inset-x-0 top-0 z-50 h-2 [-webkit-app-region:drag]"
/>
)}
<SidebarProvider className="h-screen overflow-hidden">
{/* No-project single-file mode drops the file sidebar (file tree +
project switcher); the editor inset takes the full width. */}
{!singleFile && <FileSidebar onOpenSearch={() => setCommandPaletteOpen(true)} />}
<SidebarInset className="overflow-hidden h-[calc(100vh-var(--layout-inset-offset))]">
<EditorPane onOpenSearch={() => setCommandPaletteOpen(true)} />
</SidebarInset>
</SidebarProvider>
{/* The "Open in terminal" entry point spans both the FileSidebar
menus and the EditorHeader/EditorPane, which are siblings here —
so the provider wraps both. Its value is desktop-gated; the docked
terminal that consumes the launch lives in EditorPane. */}
<TerminalLaunchProvider value={terminalLaunch}>
<SidebarProvider className="h-screen overflow-hidden">
{/* No-project single-file mode drops the file sidebar (file tree +
project switcher); the editor inset takes the full width. */}
{!singleFile && <FileSidebar onOpenSearch={() => setCommandPaletteOpen(true)} />}
<SidebarInset className="overflow-hidden h-[calc(100vh-var(--layout-inset-offset))]">
<EditorPane onOpenSearch={() => setCommandPaletteOpen(true)} />
</SidebarInset>
</SidebarProvider>
</TerminalLaunchProvider>
</PageListProvider>
</>
);
Expand Down
128 changes: 128 additions & 0 deletions packages/app/src/components/ClaudeReadinessBanner.dom.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { ClaudeReadiness, OkDesktopBridge } from '@/lib/desktop-bridge-types';

const toastErrors: string[] = [];
mock.module('sonner', () => ({
toast: { error: (message: string) => toastErrors.push(message) },
}));

const { ClaudeReadinessBanner } = await import('./ClaudeReadinessBanner');

function makeBridge(rewireResult: ClaudeReadiness = { claude: 'present', mcp: 'wired' }) {
const openExternal = mock(async (_url: string) => {});
const rewireClaudeMcp = mock(async () => rewireResult);
const bridge = {
shell: { openExternal },
terminal: { rewireClaudeMcp },
} as unknown as OkDesktopBridge;
return { bridge, openExternal, rewireClaudeMcp };
}

beforeEach(() => {
toastErrors.length = 0;
});
afterEach(() => cleanup());

describe('ClaudeReadinessBanner', () => {
test('not-found: offers a help affordance that opens the Claude Code docs', () => {
const { bridge, openExternal, rewireClaudeMcp } = makeBridge();
render(
<ClaudeReadinessBanner
readiness={{ claude: 'not-found', mcp: 'needs-rewire' }}
bridge={bridge}
onDismiss={() => {}}
/>,
);

expect(screen.getByText(/isn't installed or on your PATH/)).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Get Claude Code' }));
expect(openExternal).toHaveBeenCalledTimes(1);
expect(openExternal.mock.calls[0]?.[0]).toContain('claude-code');
expect(rewireClaudeMcp).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: 'Connect tools' })).toBeNull();
});

test('present + needs-rewire: offers a re-wire affordance and dismisses on success', async () => {
const onDismiss = mock(() => {});
const { bridge, rewireClaudeMcp, openExternal } = makeBridge();
render(
<ClaudeReadinessBanner
readiness={{ claude: 'present', mcp: 'needs-rewire' }}
bridge={bridge}
onDismiss={onDismiss}
/>,
);

expect(screen.getByText(/aren't connected to it yet/)).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Connect tools' }));
expect(rewireClaudeMcp).toHaveBeenCalledTimes(1);
await waitFor(() => expect(onDismiss).toHaveBeenCalledTimes(1));
expect(openExternal).not.toHaveBeenCalled();
});

test('present + needs-rewire: a rewire that reports an error surfaces a toast and keeps the banner', async () => {
const onDismiss = mock(() => {});
const { bridge } = makeBridge({
claude: 'present',
mcp: 'needs-rewire',
rewireError: 'consent dialog failed to arm',
});
render(
<ClaudeReadinessBanner
readiness={{ claude: 'present', mcp: 'needs-rewire' }}
bridge={bridge}
onDismiss={onDismiss}
/>,
);

await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Connect tools' }));
await Promise.resolve();
});
await waitFor(() => expect(toastErrors.length).toBe(1));
expect(onDismiss).not.toHaveBeenCalled();
});

test('present + wired: renders nothing', () => {
const { bridge } = makeBridge();
const { container } = render(
<ClaudeReadinessBanner
readiness={{ claude: 'present', mcp: 'wired' }}
bridge={bridge}
onDismiss={() => {}}
/>,
);
expect(container.firstChild).toBeNull();
expect(screen.queryByRole('status')).toBeNull();
});

test('unknown probe verdict renders nothing (no false "not installed")', () => {
const { bridge } = makeBridge();
const { container } = render(
<ClaudeReadinessBanner
readiness={{ claude: 'unknown', mcp: 'needs-rewire' }}
bridge={bridge}
onDismiss={() => {}}
/>,
);
expect(container.firstChild).toBeNull();
});

test('exposes a status live region and an accessible dismiss control', () => {
const onDismiss = mock(() => {});
const { bridge } = makeBridge();
render(
<ClaudeReadinessBanner
readiness={{ claude: 'not-found', mcp: 'needs-rewire' }}
bridge={bridge}
onDismiss={onDismiss}
/>,
);

expect(screen.getByRole('status')).toBeTruthy();
const dismiss = screen.getByRole('button', { name: 'Dismiss' });
fireEvent.click(dismiss);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
});
Loading