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

+1 -1
View File
@@ -35,7 +35,7 @@ import gecis_context
import terminal_stream
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "19" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
SURUM = "20" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
try:
with open(os.path.abspath(__file__), "rb") as _f:
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
+44 -8
View File
@@ -11,7 +11,9 @@ isteklere, istek gecerken ekler. Istemci `originator` basligindan ayirt edilir (
Hat, Codex Baglama'nin parcasidir: ayri bir dugmesi yoktur, bu bilgisayarda isi gonderebilen bir AITURK komutu
(`aiturk is`) bulundugu surece aciktir. Komut yoksa talimat eklenmez: gonderme yolu olmayan bir "AITURK'e gonder"
talimati modeli yaniltir. Ayar (bridge.json, istege bagli): "aiturk_hat": {"enabled": false} kapatir;
{"komut": "..."} komutu elle verir; {"talimat": "..."} metni degistirir.
{"komut": "..."} komutu elle verir; {"talimat": "..."} metni degistirir ({komut}, {zamanla} yer tutuculari);
{"secim": "onerilen"|"buyuk"|"kapali"|"ozel"} AITURK IDE ayarlar ekranindan yazilir. Kopru ayari her istekte
dosyadan tazeler (kopru.py hat_ayari), yeniden baslatma gerekmez.
"""
import os
import shutil
@@ -19,7 +21,7 @@ import subprocess
MASAUSTU = {"codex desktop"}
ISARET = "[AITURK iş hattı]"
VARSAYILAN = (
ESKI = (
ISARET + " Bu Codex, AITURK'e bağlı. Şu kurala göre çalış:\n"
"1. Basit soruyu, bilgi isteğini ya da tek küçük düzeltmeyi kendin yap.\n"
"2. Kullanıcı \"sen yap\" derse işi kendin yap.\n"
@@ -28,6 +30,31 @@ VARSAYILAN = (
"bitmesini bekle ve sonucu kullanıcıya kısaca aktar:\n{komut}\n"
"Komut \"işi sen yap\" derse ya da hata verirse işi kendin yap. Emin değilsen işi küçük say ve kendin yap."
)
# Kullanici karari, 15 Ekim 2026: tekrarli/zamanli isler AITURK IDE botuna, kodlama Codex'te kalir.
VARSAYILAN = (
ISARET + " Bu Codex, AITURK'e bağlı. İş dağıtımı kuralı:\n"
"1. Tekrarlı, zamanlanmış ya da sürekli açık kalacak işleri (her gün şu saatte, saat başı, haftada bir, "
"'7 gün her gün baksın' gibi) AITURK IDE'deki bota zamanlanmış iş olarak ver. PowerShell'de tek satır:\n{zamanla}\n"
"Belirli süreli işte (ör. 7 gün her gün) --repeat ile tekrar sayısını ver. HERMES_HOME kısmını atlama. "
"Aynı satırla 'cron list' çalıştırıp işin kaydını doğrula; kullanıcıya işin adını, zamanlamasını ve ilk çalışma "
"zamanını söyle. İşler AITURK IDE açıkken çalışır.\n"
"2. Kodlama işlerini AITURK'e verme: kolaysa kendin ya da kendi alt ajanınla yap, zorsa kendi alt ajan ekibine dağıt.\n"
"3. Basit soru, bilgi isteği ve küçük düzeltmeleri kendin yap.\n"
"4. Kullanıcı 'Codex' ya da 'sen yap' derse hiçbir işi devretme. Komut hata verirse işi kendin yap."
)
# Ayarlar ekranindaki hazir secenekler (bridge.json "aiturk_hat": {"secim": ...}).
SECIMLER = {"onerilen": VARSAYILAN, "buyuk": ESKI}
def zamanla_satiri(ortam=None):
"""AITURK IDE botuna zamanlanmis is veren hazir satir; IDE ajan klasoru yoksa ''."""
ortam = os.environ if ortam is None else ortam
ev = str(ortam.get("HERMES_HOME") or "").strip()
if not ev:
return ""
exe = os.path.join(ev, "bin", "hermes.exe")
return (f"$env:HERMES_HOME='{ev}'; & '{exe}' cron create --name '<kısa ad>' --workdir '<işin klasörü>' "
"'<zamanlama: 30m, every 2h ya da 0 9 * * *>' '<işin tam ve kendi başına anlaşılır talimatı>'")
def masaustu_mu(originator):
@@ -76,16 +103,25 @@ def komut_satiri(yol, sohbet=""):
return f'"{yol}" is' + (f" --sohbet {sohbet}" if sohbet else "") + ' --dosya "<iş metnini yazdığın dosya>"'
def talimat(ayar, bulunan=None, sohbet=""):
"""Eklenecek metin; hat kapaliysa ya da gonderme komutu yoksa None.
bulunan: bu bilgisayarda bulunan AITURK komutunun yolu (komut_bul); ayardaki "komut" onun onune gecer."""
def talimat(ayar, bulunan=None, sohbet="", zamanla=None):
"""Eklenecek metin; hat kapaliysa ya da metnin istedigi gonderme yolu yoksa None.
bulunan: bu bilgisayarda bulunan AITURK komutunun yolu (komut_bul); ayardaki "komut" onun onune gecer.
zamanla: AITURK IDE botuna zamanlanmis is veren satir (zamanla_satiri); None ise ortamdan bulunur.
Secim (ayarlar ekrani): "onerilen" (varsayilan), "buyuk" (eski davranis), "kapali", "ozel" ("talimat" metni).
Secimsiz eski ayarda "talimat" doluysa ozel sayilir."""
ayar = ayar if isinstance(ayar, dict) else {}
if ayar.get("enabled") is False:
secim = str(ayar.get("secim") or "").strip()
if ayar.get("enabled") is False or secim == "kapali":
return None
if secim in SECIMLER:
metin = SECIMLER[secim]
else:
metin = str(ayar.get("talimat") or "").strip() or VARSAYILAN
komut = str(ayar.get("komut") or "").strip() or (komut_satiri(bulunan, sohbet) if bulunan else "")
if not komut:
zamanla = zamanla_satiri() if zamanla is None else zamanla
if ("{komut}" in metin and not komut) or ("{zamanla}" in metin and not zamanla):
return None
return str(ayar.get("talimat") or VARSAYILAN).replace("{komut}", komut)
return metin.replace("{komut}", komut).replace("{zamanla}", zamanla)
def ekle(req, metin):
+18 -3
View File
@@ -42,7 +42,7 @@ from bekci_watch import MetadataFilter
# Installed only by owned native launcher, never an HTTP control endpoint.
WATCH_OWNER = None
SURUM = "19"
SURUM = "20"
HERE = os.path.dirname(os.path.abspath(__file__))
try:
with open(os.path.abspath(__file__), "rb") as _f:
@@ -416,7 +416,7 @@ HAT_KOMUT = [None]
def hat_komut_loop():
while True:
try:
if (CFG.get("aiturk_hat") or {}).get("enabled") is not False:
if True: # komut her zaman aranir; secim ayarlar ekranindan sonradan acilabilir
bulunan = is_hatti.komut_bul()
if bulunan != HAT_KOMUT[0]:
HAT_KOMUT[0] = bulunan
@@ -427,6 +427,21 @@ def hat_komut_loop():
threading.Thread(target=hat_komut_loop, daemon=True).start()
_HAT_AYAR = {"mtime": None, "deger": None}
def hat_ayari():
"""bridge.json "aiturk_hat" alani; ayarlar ekranindan yapilan degisiklik yeniden baslatmadan uygulanir."""
yol = os.path.join(HOME, "bridge.json")
try:
mt = os.path.getmtime(yol)
if mt != _HAT_AYAR["mtime"]:
with open(yol, encoding="utf-8") as f:
_HAT_AYAR["deger"] = (json.load(f) or {}).get("aiturk_hat")
_HAT_AYAR["mtime"] = mt
return _HAT_AYAR["deger"]
except Exception:
return CFG.get("aiturk_hat")
LAST_MODELS = {} # hesap kimligi -> son basarili model listesi (chatgpt.com hata verirse bu verilir)
CACHE_DIR = os.path.join(LOGDIR, "son-liste")
@@ -1755,7 +1770,7 @@ class H(http.server.BaseHTTPRequestHandler):
if CFG.get("sesle_model", True) and CFG.get("model_bilgi", True) and sohbet and model_gecis.bilgi_ekle(req):
govde_degisti = True
if is_hatti.masaustu_mu(kaynak) and is_hatti.ekle(
req, is_hatti.talimat(CFG.get("aiturk_hat"), HAT_KOMUT[0], sohbet)):
req, is_hatti.talimat(hat_ayari(), HAT_KOMUT[0], sohbet)):
govde_degisti = True
# Ek modelde yerlesik web aramasi yok (flatten_request atar): aramayi kabukla yapan komut soylenir.
if internet.ekle(req, internet.talimat(CFG.get("internet"), os.path.join(HERE, "ara.py"))):
@@ -11,24 +11,39 @@ class Hat(unittest.TestCase):
self.assertFalse(h.masaustu_mu(other), other)
def test_no_instruction_when_switched_off_or_without_a_send_command(self):
self.assertIsNone(h.talimat(None))
self.assertIsNone(h.talimat({}))
self.assertIsNone(h.talimat({"enabled": True, "komut": " "}))
self.assertIsNone(h.talimat({"secim": "buyuk"}, zamanla=""))
self.assertIsNone(h.talimat({"secim": "buyuk", "komut": " "}, zamanla="z"))
self.assertIsNone(h.talimat({"secim": "kapali", "komut": "x"}, "C:/a.exe", zamanla="z"))
self.assertIsNone(h.talimat({"enabled": False, "komut": "aiturk gonder"}))
self.assertIsNone(h.talimat({"enabled": False}, "C:/a/aiturk.exe", "s1"))
text = h.talimat({"enabled": True, "komut": "aiturk gonder --dosya is.md"})
text = h.talimat({"secim": "buyuk", "komut": "aiturk gonder --dosya is.md"})
self.assertIn("aiturk gonder --dosya is.md", text)
self.assertIn("sen yap", text)
self.assertEqual(h.talimat({"enabled": True, "komut": "x", "talimat": "ozel {komut}"}), "ozel x")
def test_on_by_default_once_a_send_command_exists_on_this_computer(self):
text = h.talimat(None, "C:/Program/aiturk.exe", "019a-bc_d")
text = h.talimat({"secim": "buyuk"}, "C:/Program/aiturk.exe", "019a-bc_d")
self.assertIn('"C:/Program/aiturk.exe" is --sohbet 019a-bc_d --dosya', text)
self.assertIn('"C:/Program/aiturk.exe" is --dosya', h.talimat({}, "C:/Program/aiturk.exe", ""))
self.assertIn('"C:/Program/aiturk.exe" is --dosya', h.talimat({"talimat": h.ESKI}, "C:/Program/aiturk.exe", ""))
# Sohbet kimligi komut satirina yazilir: kabuk karakteri tasiyamaz.
self.assertIn("--sohbet abrm-rf ", h.komut_satiri("x", 'a"b; rm -rf /'))
# Ayardaki komut bulunanin onune gecer.
self.assertIn("elle", h.talimat({"komut": "elle"}, "C:/Program/aiturk.exe"))
self.assertIn("elle", h.talimat({"secim": "buyuk", "komut": "elle"}, "C:/Program/aiturk.exe"))
def test_default_sends_scheduled_work_to_the_ide_bot_and_keeps_coding(self):
z = h.zamanla_satiri({"HERMES_HOME": "C:/ide/agent"})
self.assertIn("cron create", z)
self.assertIn("$env:HERMES_HOME='C:/ide/agent'", z)
self.assertEqual(h.zamanla_satiri({}), "")
for ayar in (None, {}, {"secim": "onerilen"}, {"secim": "bilinmeyen"}):
text = h.talimat(ayar, None, zamanla=z)
self.assertIn("cron create", text)
self.assertIn("Kodlama işlerini AITURK'e verme", text)
self.assertIn("'Codex' ya da 'sen yap'", text)
self.assertNotIn("{", text)
self.assertIsNone(h.talimat(None, "C:/a.exe", zamanla=""))
self.assertEqual(h.talimat({"secim": "ozel", "talimat": "kendi kuralim"}, zamanla=""), "kendi kuralim")
self.assertIn("cron create", h.talimat({"secim": "ozel", "talimat": " "}, zamanla=z))
def test_the_first_candidate_that_knows_the_command_is_used(self):
import os
@@ -58,7 +73,7 @@ class Hat(unittest.TestCase):
def test_instruction_is_put_first_once_and_never_repeated(self):
req = {"input": [{"type": "message", "role": "user", "content": "selam"}]}
text = h.talimat({"enabled": True, "komut": "aiturk gonder"})
text = h.talimat({"secim": "buyuk", "komut": "aiturk gonder"})
self.assertTrue(h.ekle(req, text))
self.assertEqual((req["input"][0]["role"], len(req["input"])), ("developer", 2))
self.assertFalse(h.ekle(req, text))
+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'),
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.39",
"version": "1.0.0-beta.40",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
@@ -12,6 +12,7 @@ import type { CodexLinkPatch, CodexLinkStatus } from '../../../electron/codex-li
import { EmptyState, ListRow, ListRowSkeleton, SettingsContent, SettingsSection } from './primitives'
import { CompressionSettingsView } from './compression-settings'
import { IsHattiSettingsView } from './is-hatti-settings'
type Tone = 'muted' | 'ok' | 'warn'
@@ -251,6 +252,7 @@ export function CodexLinkSettings() {
{copy.restartNote}
</p>
</SettingsSection>
<IsHattiSettingsView />
<CompressionSettingsView />
</SettingsContent>
)
@@ -0,0 +1,30 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/i18n/context'
import { IsHattiSettingsView } from './is-hatti-settings'
afterEach(() => {
cleanup()
delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop
})
it('shows the custom text box only for custom text and saves the choice', async () => {
const save = vi.fn(async (s: unknown) => s)
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
codexLink: { getIsHatti: async () => ({ secim: 'onerilen', talimat: '' }), setIsHatti: save }
}
render(<I18nProvider configClient={null} initialLocale="tr"><IsHattiSettingsView /></I18nProvider>)
const select = await screen.findByRole('combobox', { name: 'İş dağıtımı' })
expect(screen.queryByRole('textbox')).toBeNull()
fireEvent.change(select, { target: { value: 'ozel' } })
const box = screen.getByRole('textbox', { name: 'Codex’e eklenecek kural' })
expect((screen.getByRole('button', { name: 'Kaydet' }) as HTMLButtonElement).disabled).toBe(true)
fireEvent.change(box, { target: { value: 'kendi kuralım' } })
fireEvent.click(screen.getByRole('button', { name: 'Kaydet' }))
await waitFor(() => expect(save).toHaveBeenCalledWith({ secim: 'ozel', talimat: 'kendi kuralım' }))
expect(await screen.findByText(/yeniden başlatma gerekmez/)).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Varsayılana dön' }))
await waitFor(() => expect(save).toHaveBeenLastCalledWith({ secim: 'onerilen', talimat: 'kendi kuralım' }))
})
@@ -0,0 +1,119 @@
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { useI18n } from '@/i18n/context'
import { Link } from '@/lib/icons'
import type { IsHattiSecim, IsHattiSettings } from '../../../electron/is-hatti-settings'
import { ListRow, SettingsSection } from './primitives'
const SECENEKLER: { value: IsHattiSecim; tr: string; en: string; aciklama: string; desc: string }[] = [
{ value: 'onerilen', tr: 'Önerilen', en: 'Recommended', aciklama: 'Her gün, saat başı gibi tekrarlı işler AITURK IDE botuna zamanlanmış iş olarak gider. Kodlama işlerini Codex kendisi ya da kendi alt ajanlarıyla yapar.', desc: 'Repeating or scheduled work goes to the AITURK IDE bot. Codex keeps coding work and does it itself or with its own subagents.' },
{ value: 'buyuk', tr: 'Büyük işleri AITURK’e gönder', en: 'Send large work to AITURK', aciklama: 'Eski davranış: birkaç adımlı ya da çok dosyalı işler AITURK’e gönderilir, küçük işleri Codex yapar.', desc: 'Previous behaviour: multi-step or multi-file work goes to AITURK; Codex does small tasks.' },
{ value: 'kapali', tr: 'Hiçbir işi AITURK’e gönderme', en: 'Never send work to AITURK', aciklama: 'Codex’e iş dağıtımı hakkında hiçbir not eklenmez; her işi Codex yapar.', desc: 'No routing note is added; Codex does every task.' },
{ value: 'ozel', tr: 'Kendi metnim', en: 'Custom text', aciklama: 'Codex’e eklenecek kuralı siz yazarsınız.', desc: 'You write the rule given to Codex.' }
]
export function IsHattiSettingsView() {
const api = window.hermesDesktop?.codexLink
const { locale } = useI18n()
const tr = locale === 'tr'
const t = (a: string, b: string) => (tr ? a : b)
const [draft, setDraft] = useState<IsHattiSettings | null>(null)
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const [failed, setFailed] = useState(false)
useEffect(() => {
let active = true
api?.getIsHatti?.().then(
next => active && setDraft(next),
() => {
if (active) {
setFailed(true)
setMessage(tr ? 'İş dağıtımı ayarı okunamadı.' : 'Could not load the work routing setting.')
}
}
)
return () => {
active = false
}
}, [api, tr])
if (!api?.getIsHatti || !api.setIsHatti) return null
const save = async (next: IsHattiSettings) => {
setBusy(true)
setMessage('')
setFailed(false)
try {
setDraft(await api.setIsHatti!(next))
setMessage(t('Kaydedildi. Codex’in bir sonraki mesajından itibaren geçerli; yeniden başlatma gerekmez.', 'Saved. Applies from Codex’s next message; no restart needed.'))
} catch {
setFailed(true)
setMessage(t('Kaydedilemedi. Kendi metninizi seçtiyseniz kutunun boş olmadığını kontrol edin.', 'Could not save. If you chose custom text, make sure it is not empty.'))
} finally {
setBusy(false)
}
}
const secili = SECENEKLER.find(s => s.value === draft?.secim)
return (
<SettingsSection icon={Link} title={t('AITURK iş hattı', 'AITURK work routing')}>
<p className="text-sm text-(--ui-text-secondary)">
{t('Codex masaüstü uygulamasının hangi işleri AITURK’e devredeceğini seçin. Kullanıcı “Codex” ya da “sen yap” derse hiçbir iş devredilmez.', 'Choose which work the Codex desktop app hands to AITURK. Saying “Codex” or “do it yourself” keeps every task in Codex.')}
</p>
{draft && (
<>
<ListRow
action={
<select
aria-label={t('İş dağıtımı', 'Work routing')}
className="min-h-11 max-w-full rounded-md border bg-(--ui-bg) p-2"
disabled={busy}
onChange={e => setDraft({ ...draft, secim: e.target.value as IsHattiSecim })}
value={draft.secim}
>
{SECENEKLER.map(s => (
<option key={s.value} value={s.value}>
{tr ? s.tr : s.en}
</option>
))}
</select>
}
description={secili ? (tr ? secili.aciklama : secili.desc) : undefined}
title={t('İş dağıtımı', 'Work routing')}
/>
{draft.secim === 'ozel' && (
<>
<textarea
aria-label={t('Codex’e eklenecek kural', 'Rule given to Codex')}
className="min-h-40 w-full rounded-md border bg-(--ui-bg) p-2 text-sm"
disabled={busy}
onChange={e => setDraft({ ...draft, talimat: e.target.value })}
placeholder={t('Örnek: Kodlama işlerini kendin yap. Her gün tekrarlanacak işleri {zamanla} satırıyla AITURK IDE botuna ver.', 'Example: Do coding yourself. Give daily work to the AITURK IDE bot with {zamanla}.')}
value={draft.talimat}
/>
<p className="text-sm text-(--ui-text-secondary)">
{t('Metinde {zamanla} yazarsanız AITURK IDE botuna zamanlanmış iş veren komut, {komut} yazarsanız işi AITURK’e gönderen komut yerine konur.', 'Write {zamanla} to insert the command that schedules work on the AITURK IDE bot, or {komut} for the command that sends work to AITURK.')}
</p>
</>
)}
<div className="flex gap-2">
<Button disabled={busy || (draft.secim === 'ozel' && !draft.talimat.trim())} onClick={() => void save(draft)}>
{busy ? t('Kaydediliyor…', 'Saving…') : t('Kaydet', 'Save')}
</Button>
<Button disabled={busy} onClick={() => void save({ secim: 'onerilen', talimat: draft.talimat })} variant="secondary">
{t('Varsayılana dön', 'Restore default')}
</Button>
</div>
</>
)}
{message && <p role={failed ? 'alert' : 'status'}>{message}</p>}
</SettingsSection>
)
}
+3
View File
@@ -3,6 +3,7 @@ import type { TranslucencyState } from '@hermes/shared/translucency'
import type { CodexLinkPatch, CodexLinkStatus } from '../electron/codex-link-types'
import type { CompressionSettings, CompressionState } from '../electron/compression-settings-types'
import type { IsHattiSettings } from '../electron/is-hatti-settings'
import type { PoolLimits } from '../electron/pool-limits'
import type { StagedTransitionPatch, StagedTransitionState } from '../electron/staged-transition-types'
@@ -344,6 +345,8 @@ declare global {
codexLink?: {
getCompression: () => Promise<CompressionState>
setCompression: (settings: CompressionSettings) => Promise<CompressionState>
getIsHatti?: () => Promise<IsHattiSettings>
setIsHatti?: (settings: IsHattiSettings) => Promise<IsHattiSettings>
getStagedTransition: () => Promise<StagedTransitionState>
setStagedTransition: (patch: StagedTransitionPatch) => Promise<StagedTransitionState>
get: () => Promise<CodexLinkStatus>
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.39",
"version": "1.0.0-beta.40",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",