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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ release/
cli/dist/
cli/node_modules/
cli/bun.lock

# Pen.dev local design file (binary, not for repo)
design.pen
16 changes: 16 additions & 0 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ export function registerIpcHandlers(): void {

ipcMain.handle(CHANNELS.harnessList, () => HARNESS_CATALOG)

ipcMain.handle(CHANNELS.libraryList, async () => {
const installed = await discoverAll(HARNESS_CATALOG)
return HARNESS_CATALOG.map((entry) => {
const found = installed.find((i) => i.spec.id === entry.id)
const status = found?.path
? 'installed'
: (entry.status === 'installed' ? 'available' : entry.status)
return {
...entry,
status,
exec: found?.path ?? null,
version: found?.version ?? null,
}
})
})

ipcMain.handle(CHANNELS.harnessDiscover, async () => {
const installed = await discoverAll(HARNESS_CATALOG)
return installed.reduce<Record<string, InstalledTool>>((acc, item) => {
Expand Down
42 changes: 37 additions & 5 deletions src/main/providers/harnesses.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,54 @@
import type { ToolInstallSpec } from '../../shared/types'

export const HARNESS_CATALOG: ToolInstallSpec[] = [
export type HarnessStatus = 'installed' | 'installing' | 'available' | 'failed' | 'deprecated'

export interface HarnessCatalogEntry extends ToolInstallSpec {
avatar: string
models: string[]
features: string[]
status: HarnessStatus
statusNote?: string
}

export const HARNESS_CATALOG: HarnessCatalogEntry[] = [
{
id: 'claude-code',
name: 'Claude Code',
description: 'Anthropic\'s official agentic coding CLI.',
avatar: 'CC',
description: "Anthropic's agent harness for the terminal. Plans changes, edits files, runs commands, and reports back. Works on any codebase Claude can read.",
models: ['anthropic', 'opus-4', 'opus-4.1', 'sonnet-4'],
features: [
'Plan + edit + execute in one session',
'Inline diff review in the terminal',
'Permissions model per command type',
'Slash commands for repeated workflows',
'CLAUDE.md project context files',
],
status: 'installed',
installMethods: [
{ type: 'npm', package: '@anthropic-ai/claude-code', binary: 'claude' },
],
},
{
id: 'opencode',
name: 'OpenCode',
description: 'Open-source AI coding agent with a TUI.',
avatar: 'OC',
description: 'Open-source AI coding agent with a TUI. Multi-provider, configuration-light.',
models: ['anthropic', 'openai', 'gemini'],
features: [],
status: 'installed',
installMethods: [
{ type: 'npm', package: 'opencode-ai', binary: 'opencode' },
],
},
{
id: 'codex',
name: 'Codex',
description: 'OpenAI\'s terminal coding agent.',
name: 'Codex CLI',
avatar: 'CX',
description: "OpenAI's terminal coding agent. Background-safe via login shell sessions.",
models: ['openai'],
features: ['GPT-5.1 · v0.46.0'],
status: 'installed',
installMethods: [
{ type: 'npm', package: '@openai/codex', binary: 'codex' },
],
Expand All @@ -30,3 +58,7 @@ export const HARNESS_CATALOG: ToolInstallSpec[] = [
export function findHarness(id: string): ToolInstallSpec | undefined {
return HARNESS_CATALOG.find((h) => h.id === id)
}

export function findHarnessCatalog(id: string): HarnessCatalogEntry | undefined {
return HARNESS_CATALOG.find((h) => h.id === id)
}
16 changes: 16 additions & 0 deletions src/preload/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ export interface HoistAPI {
* the main process never invokes this opportunistically. */
read: () => Promise<ClipboardReadResponse>
}
library: {
/** Returns the catalog of harnesses with `discover()`-resolved status fields. */
list: () => Promise<LibraryEntry[]>
}
}

export interface LibraryEntry {
id: string
name: string
avatar: string
desc: string
models: string[]
features: string[]
status: 'installed' | 'installing' | 'available' | 'failed' | 'deprecated'
exec: string | null
version: string | null
}

export interface ClipboardReadResponse {
Expand Down
3 changes: 3 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const api: HoistAPI = {
clipboard: {
read: () => ipcRenderer.invoke(CHANNELS.clipboardRead),
},
library: {
list: () => ipcRenderer.invoke(CHANNELS.libraryList),
},
}

contextBridge.exposeInMainWorld('hoist', api)
Loading
Loading