diff --git a/.changeset/create-with-claude-cli.md b/.changeset/create-with-claude-cli.md
new file mode 100644
index 00000000..80a06264
--- /dev/null
+++ b/.changeset/create-with-claude-cli.md
@@ -0,0 +1,7 @@
+---
+"@inkeep/open-knowledge": patch
+---
+
+The empty-state create surface can now launch the `claude` CLI in a docked terminal. Alongside the existing app agents, the agent-picker dropdown now offers a **Terminal → Claude** option. Selecting it switches the primary button to **Create with Claude CLI**; clicking Create then opens the docked terminal with the same create-scope prompt built from your typed brief — bringing the new-file empty state to parity with the editor's "Open with AI" menu (desktop only; absent on the web host).
+
+The Open-in-Agent menus ("Open/Edit with AI", the file-tree right-click submenu, and the sidebar empty-space submenu) and the empty-state agent picker are now organized into labeled **Desktop** (app launches) and **Terminal** (CLI launch) sections, so the two launch modes are visually distinct. The CLI row's visible label is now "Claude" (its accessible name remains "Claude CLI"). Section labels render only for non-empty sections, and existing launch behavior is unchanged.
diff --git a/docs/content/features/editor.mdx b/docs/content/features/editor.mdx
index 99ab63ae..11198cf4 100644
--- a/docs/content/features/editor.mdx
+++ b/docs/content/features/editor.mdx
@@ -142,9 +142,13 @@ The Claude **claude.ai** web fallback row appears on file-scope dispatches only;
See the [per-integration pages](/docs/integrations/claude-code) for what each agent receives once dispatched.
+The menu is organized into two sections. **Desktop** lists the installed agent apps (Claude, Codex, Cursor); selecting one launches it via its deep link. **Terminal** (desktop app only) adds a single **Claude** row that runs the `claude` CLI in Open Knowledge's docked terminal with the same scoped prompt. Both a Desktop **Claude** (the app) and a Terminal **Claude** (the CLI) can therefore appear; screen readers tell them apart by the Terminal row's "Claude CLI" accessible name. Each section renders only when it has something to launch.
+
+The empty-state create composer on a fresh project carries the same Desktop / Terminal split: its Desktop rows pick the default agent the **Create** button uses, while its Terminal **Claude** row (desktop only, disabled until the workspace resolves) launches the `claude` CLI with the brief you typed.
+
### Open in Terminal
-Spawns `open -a Terminal.app
` at the right path: the folder for a folder row, the file's parent for a file row, the project root for the empty-space menu, the parent dir for an asset row. Terminal.app is hardcoded for v1; user-configurable terminal preference is on the roadmap.
+Spawns `open -a Terminal.app ` at the right path: the folder for a folder row, the file's parent for a file row, the project root for the empty-space menu, the parent dir for an asset row. Terminal.app is hardcoded for v1; user-configurable terminal preference is on the roadmap. This is distinct from the **Terminal** section inside **Open with AI**, which runs the `claude` CLI in Open Knowledge's docked terminal rather than opening Terminal.app.
## Sidebars and window width
diff --git a/packages/app/package.json b/packages/app/package.json
index 41d37222..88710db7 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -57,7 +57,7 @@
{
"name": "main CSS (gzipped)",
"path": "dist/assets/index-*.css",
- "limit": "49 kB",
+ "limit": "50 kB",
"gzip": true,
"running": false
}
diff --git a/packages/app/src/components/EditorArea.dom.test.tsx b/packages/app/src/components/EditorArea.dom.test.tsx
index 179d0a8e..487faa68 100644
--- a/packages/app/src/components/EditorArea.dom.test.tsx
+++ b/packages/app/src/components/EditorArea.dom.test.tsx
@@ -21,19 +21,44 @@ mock.module('@/components/PropertyContext', () => ({
useProperties: () => ({ requestAddProperty: () => {} }),
}));
+const FOLDER_DOC_CTX = {
+ activeDocName: 'folder/index',
+ activeProvider: null,
+ activeTarget: { kind: 'folder', target: 'folder', folderPath: 'folder' },
+ recycleDocument: () => {},
+ docPanelMode: 'timeline',
+ docPanelAgentId: null,
+ docPanelExpandSignal: 0,
+};
+const EMPTY_DOC_CTX = {
+ activeDocName: null,
+ activeProvider: null,
+ activeTarget: null,
+ recycleDocument: () => {},
+ docPanelMode: 'timeline',
+ docPanelAgentId: null,
+ docPanelExpandSignal: 0,
+};
+let docCtx: typeof FOLDER_DOC_CTX | typeof EMPTY_DOC_CTX = FOLDER_DOC_CTX;
mock.module('@/editor/DocumentContext', () => ({
- useDocumentContext: () => ({
- activeDocName: 'folder/index',
- activeProvider: null,
- activeTarget: { kind: 'folder', target: 'folder', folderPath: 'folder' },
- recycleDocument: () => {},
- docPanelMode: 'timeline',
- docPanelAgentId: null,
- docPanelExpandSignal: 0,
- }),
+ useDocumentContext: () => docCtx,
useDocumentTransition: () => ({ openDocumentTransition: null }),
}));
+mock.module('@/components/EmptyEditorState', () => ({
+ EmptyEditorState: ({ terminalVisible }: { terminalVisible?: boolean }) => (
+
+ ),
+}));
+
+mock.module('./TerminalDock', () => ({
+ TerminalDock: ({ children, visible }: { children: ReactNode; visible?: boolean }) => (
+
+ {children}
+
+ ),
+}));
+
mock.module('react-resizable-panels', () => ({
usePanelRef: () => ({
current: {
@@ -95,6 +120,7 @@ function renderEditorArea() {
describe('EditorArea SettingsDialogPortal runtime wiring', () => {
beforeEach(() => {
cleanup();
+ docCtx = FOLDER_DOC_CTX;
settingsRouteOpen = false;
closeSettingsRouteMock = mock(() => {});
shellProps = [];
@@ -118,3 +144,45 @@ describe('EditorArea SettingsDialogPortal runtime wiring', () => {
expect(closeSettingsRouteMock).toHaveBeenCalledTimes(1);
});
});
+
+describe('EditorArea empty-state terminal host', () => {
+ beforeEach(() => {
+ cleanup();
+ docCtx = EMPTY_DOC_CTX;
+ });
+
+ test('hosts the docked terminal on the empty state when a terminal bridge is present', () => {
+ render(
+ {}}
+ activeTab="timeline"
+ onActiveTabChange={() => {}}
+ terminalBridge={{} as never}
+ terminalVisible
+ onTerminalVisibleChange={() => {}}
+ terminalLaunch={null}
+ />,
+ );
+
+ const dock = screen.getByTestId('terminal-dock');
+ expect(dock.getAttribute('data-visible')).toBe('true');
+ const emptyState = dock.querySelector('[data-testid="empty-editor-state"]');
+ expect(emptyState).not.toBeNull();
+ expect(emptyState?.getAttribute('data-terminal-visible')).toBe('true');
+ });
+
+ test('renders the empty state with no terminal dock on the web host (no bridge)', () => {
+ render(
+ {}}
+ activeTab="timeline"
+ onActiveTabChange={() => {}}
+ />,
+ );
+
+ expect(screen.queryByTestId('terminal-dock')).toBeNull();
+ expect(screen.getByTestId('empty-editor-state')).toBeTruthy();
+ });
+});
diff --git a/packages/app/src/components/EditorArea.tsx b/packages/app/src/components/EditorArea.tsx
index 2c7f57f2..892da48c 100644
--- a/packages/app/src/components/EditorArea.tsx
+++ b/packages/app/src/components/EditorArea.tsx
@@ -368,6 +368,18 @@ function EditorAreaInner({
if (hashDoc !== null) {
return ;
}
+ if (terminalBridge != null) {
+ return (
+ {})}
+ launch={terminalLaunch}
+ >
+
+
+ );
+ }
return ;
}
diff --git a/packages/app/src/components/EmptyEditorState.tsx b/packages/app/src/components/EmptyEditorState.tsx
index c1450982..7e6a2777 100644
--- a/packages/app/src/components/EmptyEditorState.tsx
+++ b/packages/app/src/components/EmptyEditorState.tsx
@@ -7,6 +7,7 @@ import { CreatePromptComposer } from '@/components/empty-state/CreatePromptCompo
import { CreateView } from '@/components/empty-state/CreateView';
import { EmptyStateHeader } from '@/components/empty-state/EmptyStateHeader';
import { filterVisibleEntries } from '@/components/file-tree-utils';
+import { OkBlob } from '@/components/OkBlob';
import { PackCardGrid } from '@/components/PackCardGrid';
import { SeedDialog } from '@/components/SeedDialog';
import { Button } from '@/components/ui/button';
@@ -15,7 +16,7 @@ import { emitCreateTopLevelFile } from '@/lib/create-file-events';
import type { OkPackId } from '@/lib/desktop-bridge-types';
import { subscribeToDocumentsChanged } from '@/lib/documents-events';
-export function EmptyEditorState() {
+export function EmptyEditorState({ terminalVisible = false }: { terminalVisible?: boolean }) {
const [seedDialogOpen, setSeedDialogOpen] = useState(false);
const [seedDialogInitialPackId, setSeedDialogInitialPackId] = useState(
undefined,
@@ -84,6 +85,14 @@ export function EmptyEditorState() {
if (!next) setSeedDialogInitialPackId(undefined);
}
+ if (terminalVisible) {
+ return (
+