feat(codex-link): AITURK work routing setting and beta.40
Server CI and Windows package / verify (push) Successful in 2m42s
Server CI and Windows package / windows-package (push) Successful in 6m21s

- 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:
yilsem committed 2026-10-15 02:18:18 +03:00
1 parent a7ba300448
commit 557e619499
16 files changed
+388 -26

No files matched your search

+16 -3
View File
@@ -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 ─────────────────────────────────────────────────────
+10 -1
View File
@@ -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)
}
}
}
+12
View File
@@ -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)
+2
View File
@@ -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'),