diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py index 600d793..fc7025d 100644 --- a/apps/desktop/assets/codex-bridge/bridge.py +++ b/apps/desktop/assets/codex-bridge/bridge.py @@ -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] diff --git a/apps/desktop/assets/codex-bridge/is_hatti.py b/apps/desktop/assets/codex-bridge/is_hatti.py index 50ec847..f8b75cb 100644 --- a/apps/desktop/assets/codex-bridge/is_hatti.py +++ b/apps/desktop/assets/codex-bridge/is_hatti.py @@ -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 '' --workdir '' " + "'' ''") def masaustu_mu(originator): @@ -76,16 +103,25 @@ def komut_satiri(yol, sohbet=""): return f'"{yol}" is' + (f" --sohbet {sohbet}" if sohbet else "") + ' --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): diff --git a/apps/desktop/assets/codex-bridge/kopru.py b/apps/desktop/assets/codex-bridge/kopru.py index b6bd4b6..c1de2c1 100644 --- a/apps/desktop/assets/codex-bridge/kopru.py +++ b/apps/desktop/assets/codex-bridge/kopru.py @@ -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"))): diff --git a/apps/desktop/assets/codex-bridge/test_is_hatti.py b/apps/desktop/assets/codex-bridge/test_is_hatti.py index 1dabdcc..25f31f8 100644 --- a/apps/desktop/assets/codex-bridge/test_is_hatti.py +++ b/apps/desktop/assets/codex-bridge/test_is_hatti.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)) diff --git a/apps/desktop/electron/codex-link-config.ts b/apps/desktop/electron/codex-link-config.ts index e5d0b2f..4279905 100644 --- a/apps/desktop/electron/codex-link-config.ts +++ b/apps/desktop/electron/codex-link-config.ts @@ -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 = 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 ───────────────────────────────────────────────────── diff --git a/apps/desktop/electron/codex-link.ts b/apps/desktop/electron/codex-link.ts index a8c3b14..5b1314c 100644 --- a/apps/desktop/electron/codex-link.ts +++ b/apps/desktop/electron/codex-link.ts @@ -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) diff --git a/apps/desktop/electron/is-hatti-settings.test.ts b/apps/desktop/electron/is-hatti-settings.test.ts new file mode 100644 index 0000000..90f34a6 --- /dev/null +++ b/apps/desktop/electron/is-hatti-settings.test.ts @@ -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() + }) +}) diff --git a/apps/desktop/electron/is-hatti-settings.ts b/apps/desktop/electron/is-hatti-settings.ts new file mode 100644 index 0000000..bba4204 --- /dev/null +++ b/apps/desktop/electron/is-hatti-settings.ts @@ -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 | 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 = {} + + 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) : {} + const hat: Record = { ...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) + } + } +} diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 2f671c3..71bcafa 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -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) diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts index 4b49357..9970db9 100644 --- a/apps/desktop/electron/preload.ts +++ b/apps/desktop/electron/preload.ts @@ -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'), diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ff8fc67..e840a71 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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": { diff --git a/apps/desktop/src/app/settings/codex-link-settings.tsx b/apps/desktop/src/app/settings/codex-link-settings.tsx index cd7c374..8668985 100644 --- a/apps/desktop/src/app/settings/codex-link-settings.tsx +++ b/apps/desktop/src/app/settings/codex-link-settings.tsx @@ -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}

+ ) diff --git a/apps/desktop/src/app/settings/is-hatti-settings.test.tsx b/apps/desktop/src/app/settings/is-hatti-settings.test.tsx new file mode 100644 index 0000000..d66b2cc --- /dev/null +++ b/apps/desktop/src/app/settings/is-hatti-settings.test.tsx @@ -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() + 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' })) +}) diff --git a/apps/desktop/src/app/settings/is-hatti-settings.tsx b/apps/desktop/src/app/settings/is-hatti-settings.tsx new file mode 100644 index 0000000..d3c4b68 --- /dev/null +++ b/apps/desktop/src/app/settings/is-hatti-settings.tsx @@ -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(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 ( + +

+ {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.')} +

+ {draft && ( + <> + setDraft({ ...draft, secim: e.target.value as IsHattiSecim })} + value={draft.secim} + > + {SECENEKLER.map(s => ( + + ))} + + } + description={secili ? (tr ? secili.aciklama : secili.desc) : undefined} + title={t('İş dağıtımı', 'Work routing')} + /> + {draft.secim === 'ozel' && ( + <> +