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
82 changes: 82 additions & 0 deletions electron/aiTabMetadata/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type AiTabMetadataTestMock = {
models?: AiTabMetadataModel[]
noteResult?: string
titleResult?: string
promptResult?: string
}

type ShellEnv = Record<string, string>
Expand Down Expand Up @@ -757,4 +758,85 @@ export class AiTabMetadataService {
await rm(tempDir, { force: true, recursive: true })
}
}

/**
* Run a single non-interactive prompt and return the raw model text. Unlike
* {@link generate}, this does not shape the prompt or post-process the result,
* so callers (e.g. Quick Push) can ask the model for structured output.
*/
async runPrompt(request: { provider: AiTabMetadataProvider; model: string; prompt: string; cwd: string }): Promise<string> {
if (request.provider !== 'codex' && request.provider !== 'claudeCode') {
throw new Error(`Unsupported AI provider: ${request.provider}`)
}

if (!request.model.trim()) {
throw new Error('Choose an AI model before running the AI provider.')
}

const isUsingMock =
process.env.TERMINAY_TEST === '1' &&
((request.provider === 'codex' && process.env.TERMINAY_TEST_USE_REAL_CODEX !== '1') ||
(request.provider === 'claudeCode' && process.env.TERMINAY_TEST_USE_REAL_CLAUDE_CODE !== '1'))

if (isUsingMock) {
if (this.testMock.error) {
throw new Error(this.testMock.error)
}

return this.testMock.promptResult ?? '{}'
}

if (request.provider === 'claudeCode') {
try {
const providerEnv = await getProviderEnv()
return await runClaudeCodePrint(request.prompt, {
cwd: request.cwd,
env: providerEnv,
model: request.model,
timeout: PROVIDER_TIMEOUT_MS,
})
} catch (error) {
throw normalizeProviderError(error, 'Unable to run Claude Code.', 'Claude Code')
}
}

const tempDir = await mkdtemp(path.join(os.tmpdir(), 'terminay-codex-'))
const outputPath = path.join(tempDir, 'last-message.txt')

try {
const providerEnv = await getProviderEnv()
await runCodexExec(
[
'exec',
'--model',
request.model,
'-c',
'model_reasoning_effort="low"',
'--sandbox',
'read-only',
'--skip-git-repo-check',
'--ephemeral',
'--ignore-rules',
'--color',
'never',
'--cd',
request.cwd,
'-o',
outputPath,
request.prompt,
],
{
cwd: request.cwd,
env: providerEnv,
timeout: PROVIDER_TIMEOUT_MS,
},
)

return await readFile(outputPath, 'utf8')
} catch (error) {
throw normalizeProviderError(error, 'Unable to run Codex.', 'Codex')
} finally {
await rm(tempDir, { force: true, recursive: true })
}
}
}
8 changes: 8 additions & 0 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { defaultTerminalSettings, normalizeTerminalSettings } from '../src/termi
import { findCommandForKeyboardEvent, getCommandShortcut, isReservedSystemAccelerator } from '../src/keyboardShortcuts'
import { registerAiTabMetadataIpcHandlers } from './aiTabMetadata/ipc'
import { AiTabMetadataService, warmAiTabMetadataProviderEnv } from './aiTabMetadata/service'
import { registerQuickPushIpcHandlers } from './quickPush/ipc'
import { QuickPushService } from './quickPush/service'
import { TerminalRecordingService } from './recording/service'
import type { MacroDefinition } from '../src/types/macros'
import type { TerminalSettings } from '../src/types/settings'
Expand Down Expand Up @@ -215,6 +217,7 @@ const fileWatchService = new FileWatchService(fileBufferService)
const fileExplorerWatchService = new FileExplorerWatchService(() => app.getPath('home'))
const gitDiffService = new GitDiffService(fileBufferService)
const aiTabMetadataService = new AiTabMetadataService(app.getPath('home'))
const quickPushService = new QuickPushService(aiTabMetadataService)
warmAiTabMetadataProviderEnv()
let cachedAppUpdateStatus: AppUpdateStatus | null = null
let appUpdateFetchPromise: Promise<AppUpdateStatus> | null = null
Expand Down Expand Up @@ -2726,6 +2729,11 @@ registerAiTabMetadataIpcHandlers({
ipcMain,
})

registerQuickPushIpcHandlers({
quickPushService,
ipcMain,
})

app.on('window-all-closed', () => {
mainWindow = null
if (process.platform !== 'darwin') {
Expand Down
8 changes: 8 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import type {
ControlRendererResponseMessage,
ProjectEditWindowDraft,
ProjectEditWindowResult,
QuickPushApplyRequest,
QuickPushApplyResult,
QuickPushGenerateRequest,
QuickPushPlan,
FileExplorerWatchEvent,
RemoteAccessStatus,
SettingsChangeMessage,
Expand Down Expand Up @@ -124,6 +128,10 @@ contextBridge.exposeInMainWorld('terminay', {
ipcRenderer.invoke('ai-tab-metadata:list-models', { provider }) as Promise<AiTabMetadataModel[]>,
generateAiTabMetadata: (payload: AiTabMetadataGenerateRequest) =>
ipcRenderer.invoke('ai-tab-metadata:generate', payload) as Promise<AiTabMetadataGenerateResult>,
generateQuickPushPlan: (payload: QuickPushGenerateRequest) =>
ipcRenderer.invoke('quick-push:generate-plan', payload) as Promise<QuickPushPlan>,
applyQuickPush: (payload: QuickPushApplyRequest) =>
ipcRenderer.invoke('quick-push:apply', payload) as Promise<QuickPushApplyResult>,

getMacros: () => ipcRenderer.invoke('macros:get') as Promise<MacroDefinition[]>,
updateMacros: (macros: MacroDefinition[]) => ipcRenderer.invoke('macros:update', macros) as Promise<MacroDefinition[]>,
Expand Down
18 changes: 18 additions & 0 deletions electron/quickPush/ipc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { IpcMain } from 'electron'
import type { QuickPushApplyRequest, QuickPushGenerateRequest } from '../../src/types/terminay'
import type { QuickPushService } from './service'

type RegisterQuickPushIpcOptions = {
quickPushService: QuickPushService
ipcMain: IpcMain
}

export function registerQuickPushIpcHandlers({ quickPushService, ipcMain }: RegisterQuickPushIpcOptions): void {
ipcMain.handle('quick-push:generate-plan', async (_event, payload: QuickPushGenerateRequest) => {
return quickPushService.generatePlan(payload)
})

ipcMain.handle('quick-push:apply', async (_event, payload: QuickPushApplyRequest) => {
return quickPushService.apply(payload)
})
}
Loading
Loading