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
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Vendored
+3
@@ -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>
|
||||
|
||||
Generated
+1
-1
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user