feat(codex-link): AITURK work routing setting and beta.40
- Default routing rule (user decision, 15 Oct 2026): repeating/scheduled work goes to the AITURK IDE bot as a cron job; coding stays in Codex (itself or its subagents); 'Codex' or 'sen yap' keeps everything in Codex. - Settings > Codex Bağlama: choice (recommended / send large work / never / custom text) with a text box and restore-default; written to bridge.json aiturk_hat. - Bridge re-reads aiturk_hat when bridge.json changes: no restart. IDE apply keeps aiturk_hat instead of wiping it. - Bridge version 20, IDE 1.0.0-beta.40. Jev untouched.
This commit is contained in:
1 parent
a7ba300448
commit
557e619499
16 files changed
+388
-26
No files matched your search
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
|
||||
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
|
||||
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
|
||||
// another version is restarted so an updated IDE never keeps an old bridge alive.
|
||||
export const CODEX_BRIDGE_VERSION = '19'
|
||||
export const CODEX_BRIDGE_VERSION = '20'
|
||||
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
|
||||
|
||||
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
|
||||
@@ -352,8 +352,21 @@ export function codexBridgeConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeCodexBridgeConfig(): string {
|
||||
return `${JSON.stringify(codexBridgeConfig(), null, 2)}\n`
|
||||
/** Keeps the user's own choices (Settings → AITURK iş hattı) across the rewrite. */
|
||||
export function serializeCodexBridgeConfig(previous?: null | string): string {
|
||||
const config: Record<string, unknown> = codexBridgeConfig()
|
||||
|
||||
try {
|
||||
const old = previous ? JSON.parse(previous) : null
|
||||
|
||||
if (old && typeof old === 'object' && old.aiturk_hat && typeof old.aiturk_hat === 'object') {
|
||||
config.aiturk_hat = old.aiturk_hat
|
||||
}
|
||||
} catch {
|
||||
// An unreadable file has nothing worth keeping.
|
||||
}
|
||||
|
||||
return `${JSON.stringify(config, null, 2)}\n`
|
||||
}
|
||||
|
||||
// ── Bridge command line ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -286,7 +286,16 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
|
||||
}
|
||||
|
||||
try {
|
||||
deps.writeTextAtomic(path.join(paths.bridgeHome, 'bridge.json'), serializeCodexBridgeConfig())
|
||||
const bridgeJson = path.join(paths.bridgeHome, 'bridge.json')
|
||||
let previous: null | string = null
|
||||
|
||||
try {
|
||||
previous = deps.readText(bridgeJson)
|
||||
} catch {
|
||||
previous = null
|
||||
}
|
||||
|
||||
deps.writeTextAtomic(bridgeJson, serializeCodexBridgeConfig(previous))
|
||||
} catch (error) {
|
||||
fail('bridge-config-failed', error)
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { readIsHatti, writeIsHatti } from './is-hatti-settings'
|
||||
|
||||
describe('AITURK iş hattı settings', () => {
|
||||
it('defaults to the recommended rule and reads older files the way the bridge does', () => {
|
||||
expect(readIsHatti(null)).toEqual({ secim: 'onerilen', talimat: '' })
|
||||
expect(readIsHatti('{"aiturk_hat":{"enabled":false}}').secim).toBe('kapali')
|
||||
expect(readIsHatti('{"aiturk_hat":{"talimat":"x"}}')).toEqual({ secim: 'ozel', talimat: 'x' })
|
||||
expect(readIsHatti('bozuk').secim).toBe('onerilen')
|
||||
})
|
||||
|
||||
it('writes only aiturk_hat and keeps everything else', () => {
|
||||
const raw = '{"port":8795,"aiturk_hat":{"enabled":false,"komut":"k"}}'
|
||||
const out = JSON.parse(writeIsHatti(raw, { secim: 'buyuk', talimat: '' }))
|
||||
expect(out).toEqual({ port: 8795, aiturk_hat: { komut: 'k', secim: 'buyuk', talimat: '' } })
|
||||
})
|
||||
|
||||
it('rejects an empty custom text and unknown choices', () => {
|
||||
expect(() => writeIsHatti(null, { secim: 'ozel', talimat: ' ' })).toThrow()
|
||||
expect(() => writeIsHatti(null, { secim: 'x', talimat: '' })).toThrow()
|
||||
expect(() => writeIsHatti('{bozuk', { secim: 'onerilen', talimat: '' })).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
/** "AITURK iş hattı" choice, stored in the Codex bridge's `bridge.json` under `aiturk_hat`.
|
||||
* The bridge re-reads the field when the file changes, so no restart is needed. */
|
||||
export type IsHattiSecim = 'onerilen' | 'buyuk' | 'kapali' | 'ozel'
|
||||
|
||||
export interface IsHattiSettings {
|
||||
secim: IsHattiSecim
|
||||
talimat: string
|
||||
}
|
||||
|
||||
const SECIMLER: IsHattiSecim[] = ['onerilen', 'buyuk', 'kapali', 'ozel']
|
||||
const MAX_TALIMAT = 20000
|
||||
|
||||
export function readIsHatti(raw: null | string): IsHattiSettings {
|
||||
try {
|
||||
const hat = raw ? JSON.parse(raw)?.aiturk_hat : null
|
||||
|
||||
if (!hat || typeof hat !== 'object') {
|
||||
return { secim: 'onerilen', talimat: '' }
|
||||
}
|
||||
|
||||
const talimat = typeof hat.talimat === 'string' ? hat.talimat : ''
|
||||
|
||||
if (SECIMLER.includes(hat.secim)) {
|
||||
return { secim: hat.secim, talimat }
|
||||
}
|
||||
|
||||
// Written before this screen existed: same meaning the bridge gives it.
|
||||
return { secim: hat.enabled === false ? 'kapali' : talimat.trim() ? 'ozel' : 'onerilen', talimat }
|
||||
} catch {
|
||||
return { secim: 'onerilen', talimat: '' }
|
||||
}
|
||||
}
|
||||
|
||||
export function writeIsHatti(raw: null | string, value: unknown): string {
|
||||
const v = value as Partial<IsHattiSettings> | null
|
||||
|
||||
if (!v || !SECIMLER.includes(v.secim as IsHattiSecim)) throw new Error('Invalid choice')
|
||||
const talimat = typeof v.talimat === 'string' ? v.talimat : ''
|
||||
|
||||
if (talimat.length > MAX_TALIMAT) throw new Error('Instruction too long')
|
||||
if (v.secim === 'ozel' && !talimat.trim()) throw new Error('Custom instruction is empty')
|
||||
let config: Record<string, unknown> = {}
|
||||
|
||||
try {
|
||||
const parsed = raw ? JSON.parse(raw) : {}
|
||||
config = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
} catch {
|
||||
throw new Error('bridge.json is not valid JSON')
|
||||
}
|
||||
|
||||
const old = config.aiturk_hat && typeof config.aiturk_hat === 'object' ? (config.aiturk_hat as Record<string, unknown>) : {}
|
||||
const hat: Record<string, unknown> = { ...old, secim: v.secim, talimat }
|
||||
delete hat.enabled
|
||||
|
||||
return `${JSON.stringify({ ...config, aiturk_hat: hat }, null, 2)}\n`
|
||||
}
|
||||
|
||||
export function createIsHattiStore(directory: string) {
|
||||
const file = path.join(directory, 'bridge.json')
|
||||
const read = () => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null)
|
||||
|
||||
return {
|
||||
get: (): IsHattiSettings => readIsHatti(read()),
|
||||
save(value: unknown): IsHattiSettings {
|
||||
const text = writeIsHatti(read(), value)
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
const tmp = `${file}.${process.pid}.tmp`
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tmp, text, 'utf8')
|
||||
fs.renameSync(tmp, file)
|
||||
} finally {
|
||||
if (fs.existsSync(tmp)) fs.unlinkSync(tmp)
|
||||
}
|
||||
|
||||
return readIsHatti(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,6 +380,7 @@ import { collectSshConfigHosts, parseSshGOutput } from './ssh-config'
|
||||
import { createSshProbeConnection, pickLocalPort, redactSecrets, SshConnection } from './ssh-connection'
|
||||
import { createStagedTransitionStore } from './staged-transition'
|
||||
import { createCompressionStore } from './compression-settings'
|
||||
import { createIsHattiStore } from './is-hatti-settings'
|
||||
import { createStreamThrottle } from './stream-throttle'
|
||||
import { registerTerminalIpc } from './terminal-ipc'
|
||||
import { nativeOverlayWidth as computeNativeOverlayWidth, macTitleBarOverlayHeight } from './titlebar-overlay-width'
|
||||
@@ -17195,6 +17196,7 @@ ipcMain.handle('hermes:quick-entry:settings:set', async (_event, patch) => {
|
||||
// a Run-key value). All decisions live in electron/codex-link.ts.
|
||||
const AITURK_IDE_DATA_ROOT = path.dirname(HERMES_HOME)
|
||||
const compressionStore = createCompressionStore(path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'))
|
||||
const isHattiStore = createIsHattiStore(path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'))
|
||||
const stagedTransition = createStagedTransitionStore(
|
||||
path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'),
|
||||
path.join(app.getPath('home'), '.typesafe', 'typesafe.env'),
|
||||
@@ -17307,6 +17309,16 @@ ipcMain.handle('aiturk:compression:set', async (event, settings) => {
|
||||
return compressionState()
|
||||
})
|
||||
|
||||
ipcMain.handle('aiturk:is-hatti:get', async event => {
|
||||
assertCodexLinkSender(event)
|
||||
return isHattiStore.get()
|
||||
})
|
||||
|
||||
ipcMain.handle('aiturk:is-hatti:set', async (event, settings) => {
|
||||
assertCodexLinkSender(event)
|
||||
return isHattiStore.save(settings)
|
||||
})
|
||||
|
||||
ipcMain.handle('aiturk:staged-transition:set', async (event, patch) => {
|
||||
assertCodexLinkSender(event)
|
||||
stagedTransition.save(patch)
|
||||
|
||||
@@ -297,6 +297,8 @@ contextBridge.exposeInMainWorld('hermesDesktop', {
|
||||
codexLink: {
|
||||
getCompression: () => ipcRenderer.invoke('aiturk:compression:get'),
|
||||
setCompression: settings => ipcRenderer.invoke('aiturk:compression:set', settings),
|
||||
getIsHatti: () => ipcRenderer.invoke('aiturk:is-hatti:get'),
|
||||
setIsHatti: settings => ipcRenderer.invoke('aiturk:is-hatti:set', settings),
|
||||
getStagedTransition: () => ipcRenderer.invoke('aiturk:staged-transition:get'),
|
||||
setStagedTransition: patch => ipcRenderer.invoke('aiturk:staged-transition:set', patch),
|
||||
get: () => ipcRenderer.invoke('aiturk:codex-link:get'),
|
||||
|
||||
Reference in new issue
Block a user