feat(settings): configure Jev evaluation and model transitions

This commit is contained in:
yilsem committed 2026-10-13 01:07:58 +03:00
1 parent 369f6c2ca8
commit 883b1dc824
20 files changed
+1195 -37

No files matched your search

+1 -1
View File
@@ -34,7 +34,7 @@ import urllib.request
import gecis_context
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "11" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
SURUM = "12" # 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]
@@ -0,0 +1,22 @@
{
"enabled": true,
"endpoint": "https://api.typesafe.ai/v1/systemone",
"model": "jev-1.13.0",
"fallbackModel": "Katman-1",
"rejectionRules": [
{ "from": "Katman-1", "to": "Katman-2" },
{ "from": "Katman-2", "to": "Katman-3" },
{ "from": "Katman-3", "to": "Katman-4" }
],
"exemptModels": ["Sesli-Hizli-Zeki", "Katman-4"],
"thresholds": { "ret": 0.5, "sart": 0.7 },
"questions": {
"ret": "The assistant did NOT do what the user asked or did NOT answer the user's question: it refused, declined, said it cannot or will not, avoided giving the requested judgment, or redirected to a different topic.",
"sart": "The assistant changed the request on its own: it added its own conditions, softened or relabeled what was asked, or left part of it out, instead of doing exactly what the user asked."
},
"timeoutSeconds": 3600,
"criteria": { "ret": { "true": "", "false": "" }, "sart": { "true": "", "false": "" } },
"maxConcurrent": 0,
"queueWaitSeconds": 3600,
"failureAction": "pass"
}
+64 -16
View File
@@ -18,6 +18,7 @@ import json
import os
import re
import urllib.request
import jev_settings
ADRES = "https://api.typesafe.ai/v1/systemone"
MODEL = "jev-1.13.0"
@@ -32,26 +33,44 @@ SORU = {
_ANAHTAR = {"okundu": False, "deger": ""}
def muaf_mi(hedef):
"""12 Ekim 2026: yalnız çözülmüş exact sesli combo hedefi Jev'den muaftır.
"""Settings' always-pass list matches exact resolved model IDs.
Taşıma hatalarının katman sırasını değiştirmez; yedek model kendi adına denetlenir.
"""
return hedef == "Sesli-Hizli-Zeki"
return hedef in jev_settings.read()[0]['exemptModels']
def sonrakiler(hedef):
"""Jev gecirmezse sirayla denenecek katmanlar. Secili model bir katmansa ondan sonrakiler; katman degilse
butun katmanlar bastan (once Katman-1). Son katman (Katman-4) kendisi gelince bos doner: sorgulanmadan gecer."""
"""Model-specific rejection edges take precedence over the general fallback.
Stop at an always-pass model or before revisiting any target.
"""
settings = jev_settings.read()[0]
ad = str(hedef or "")
if not ad or ad == KATMAN[-1]:
if not settings['enabled'] or not ad:
return []
if ad in KATMAN:
return list(KATMAN[KATMAN.index(ad) + 1:])
return list(KATMAN)
routes = {r['from']: r['to'] for r in settings['rejectionRules']}
result, seen = [], {ad}
# An exempt model can still use the general fallback on transport failure.
# Repeated targets stop the walk even if a file was edited outside Settings.
while len(result) < 65:
target = routes.get(ad, settings['fallbackModel'])
if target in seen:
break
seen.add(target)
result.append(target)
if target in settings['exemptModels']:
break
ad = target
return result
def anahtar(ortam=None, dosyalar=None):
"""TypeSafe anahtari: ortam degiskeni, sonra ~/.typesafe/typesafe.env. Yoksa ""."""
if ortam is None and dosyalar is None:
saved = jev_settings.key()
if saved:
return saved
ortam = os.environ if ortam is None else ortam
deger = str(ortam.get("TYPESAFE_API_KEY") or "").strip()
if deger:
@@ -147,24 +166,53 @@ def komutlar(govde):
return "\n".join(satirlar)
def _gonder(govde, key):
istek = urllib.request.Request(ADRES, data=json.dumps(govde).encode("utf-8"), method="POST",
def _gonder(govde, key, settings=None):
settings = settings or jev_settings.read()[0]
istek = urllib.request.Request(settings['endpoint'], data=json.dumps(govde).encode("utf-8"), method="POST",
headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"})
with urllib.request.urlopen(istek) as yanit:
with urllib.request.urlopen(istek, timeout=settings['timeoutSeconds']) as yanit:
return json.load(yanit)
class Puan(dict):
"""Carry the thresholds used for this evaluation through a settings change."""
pass
def sor(istek, cevap, key, gonder=_gonder):
"""Jev'e sorar. Donus: {"ret", "sart"} olasiliklari | None (sorulamadi)."""
govde = {"model": MODEL, "state": {"user_request": istek, "assistant_reply": cevap},
"questions": {k: {"type": "noul", "instructions": v} for k, v in SORU.items()}}
settings = jev_settings.read()[0]
govde = {"model": settings['model'], "state": {"user_request": istek, "assistant_reply": cevap},
"questions": {k: {"type": "noul", "instructions": v} for k, v in settings['questions'].items()}}
for name, criteria in settings.get('criteria', {}).items():
values = {k: v for k, v in criteria.items() if v.strip()}
if values:
govde['questions'][name]['criteria'] = values
if not jev_settings.acquire(settings):
return None
try:
yanitlar = (gonder(govde, key) or {}).get("answers") or {}
return {k: float(yanitlar[k]["noul"]) for k in SORU}
response = gonder(govde, key, settings) if gonder is _gonder else gonder(govde, key)
yanitlar = (response or {}).get("answers") or {}
result = Puan({k: float(yanitlar[k]["noul"]) for k in SORU})
if any(not 0 <= v <= 1 for v in result.values()):
return None
result.thresholds = settings['thresholds']
return result
except Exception:
return None
finally:
jev_settings.release()
def karar(puan):
"""True: adim gecmez, sonraki katman yapar. Istisna yok."""
return puan["ret"] >= ESIK["ret"] or puan["sart"] >= ESIK["sart"]
thresholds = getattr(puan, 'thresholds', None) or jev_settings.read()[0]['thresholds']
return puan["ret"] >= thresholds["ret"] or puan["sart"] >= thresholds["sart"]
def hata_gecis():
return jev_settings.read()[0]['failureAction'] == 'next'
def etkin_mi():
return bool(jev_settings.read()[0]['enabled'])
@@ -0,0 +1,89 @@
"""Per-user settings, re-read for new evaluations without restarting active chats."""
import base64
import copy
import ctypes
import json
import os
import sys
import threading
import time
from pathlib import Path
DEFAULTS = json.loads(Path(__file__).with_name('jev-settings-defaults.json').read_text(encoding='utf-8'))
_condition = threading.Condition()
_active = 0
def home():
if '--home' in sys.argv:
return Path(sys.argv[sys.argv.index('--home') + 1])
return Path(os.environ.get('AITURK_CODEX_BRIDGE_HOME') or Path(__file__).parent)
def read():
try:
data = json.loads((home() / 'jev-settings.json').read_text(encoding='utf-8'))
except FileNotFoundError:
return copy.deepcopy(DEFAULTS), None
settings = data['settings']
# A corrupted file must not silently change routing or contact another host.
from urllib.parse import urlsplit
url = urlsplit(settings['endpoint'])
if url.scheme != 'https' or not url.netloc or url.username or url.password:
raise ValueError('Invalid Jev endpoint')
for name in ('ret', 'sart'):
n = settings['thresholds'][name]
if not isinstance(n, (int, float)) or not 0 <= n <= 1 or not settings['questions'][name]:
raise ValueError('Invalid Jev rule')
for name in ('timeoutSeconds', 'queueWaitSeconds'):
if not isinstance(settings[name], int) or not 1 <= settings[name] <= 3600:
raise ValueError('Invalid Jev timeout')
if not isinstance(settings['maxConcurrent'], int) or not 0 <= settings['maxConcurrent'] <= 512:
raise ValueError('Invalid Jev concurrency')
if not settings['fallbackModel'] or not isinstance(settings['rejectionRules'], list) or settings['failureAction'] not in ('pass', 'next'):
raise ValueError('Invalid Jev routing')
return settings, data.get('protectedKey')
def key():
_, encrypted = read()
if not encrypted:
return ''
if os.name != 'nt':
raise RuntimeError('Windows credential protection required')
from ctypes import wintypes
class Blob(ctypes.Structure):
_fields_ = [('cbData', wintypes.DWORD), ('pbData', ctypes.POINTER(ctypes.c_ubyte))]
raw = base64.b64decode(encrypted, validate=True)
buf = (ctypes.c_ubyte * len(raw)).from_buffer_copy(raw)
source, result = Blob(len(raw), buf), Blob()
decrypt = ctypes.windll.crypt32.CryptUnprotectData
decrypt.argtypes = [ctypes.POINTER(Blob), ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, wintypes.DWORD, ctypes.POINTER(Blob)]
decrypt.restype = wintypes.BOOL
if not decrypt(ctypes.byref(source), None, None, None, None, 0, ctypes.byref(result)):
raise ctypes.WinError()
try:
return ctypes.string_at(result.pbData, result.cbData).decode('utf-8')
finally:
ctypes.windll.kernel32.LocalFree.argtypes = [ctypes.c_void_p]
ctypes.windll.kernel32.LocalFree(result.pbData)
def acquire(settings):
global _active
deadline = time.monotonic() + settings['queueWaitSeconds']
with _condition:
while settings['maxConcurrent'] and _active >= settings['maxConcurrent']:
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
_condition.wait(remaining)
_active += 1
return True
def release():
global _active
with _condition:
_active -= 1
_condition.notify_all()
+32 -13
View File
@@ -38,7 +38,7 @@ from bekci_watch import MetadataFilter
# Installed only by owned native launcher, never an HTTP control endpoint.
WATCH_OWNER = None
SURUM = "11"
SURUM = "12"
HERE = os.path.dirname(os.path.abspath(__file__))
try:
with open(os.path.abspath(__file__), "rb") as _f:
@@ -933,12 +933,24 @@ class H(http.server.BaseHTTPRequestHandler):
if hata:
gecmedi = True
elif metin and jev_denetim.muaf_mi(hedef):
log(f"{tag} Jev atlandi: Sesli-Hizli-Zeki")
log(f"{tag} Jev atlandi: her zaman gec ({hedef})")
elif metin:
t0 = time.time()
puan = jev_denetim.sor(istek, metin, jev_denetim.anahtar())
# Jev may wait for a slot or a remote response. Keep this chat alive.
import threading as jev_threading
jev_done, jev_result = jev_threading.Event(), [None]
def denetle():
try:
jev_result[0] = jev_denetim.sor(istek, metin, jev_denetim.anahtar())
finally:
jev_done.set()
jev_threading.Thread(target=denetle, daemon=True).start()
while not jev_done.wait(10):
w(b": jev-denetim\n\n")
puan = jev_result[0]
if puan is None:
log(f"{tag} katman denetimi: Jev'e sorulamadi, cevap oldugu gibi geciyor")
gecmedi = jev_denetim.hata_gecis()
log(f"{tag} katman denetimi: Jev'e sorulamadi, {'siradaki katman' if gecmedi else 'cevap oldugu gibi geciyor'}")
else:
gecmedi = jev_denetim.karar(puan)
log(f"{tag} katman denetimi: ret={puan['ret']:.2f} sart={puan['sart']:.2f} -> "
@@ -962,11 +974,16 @@ class H(http.server.BaseHTTPRequestHandler):
resp, rewriter, tag = yeni
hedef = ad # Muafiyet yalnız ilk exact hedefe ait; yedek katmana taşınmaz.
log(f"{tag} katman denetimi: adim bu katmana verildi")
if not kalan:
if jev_denetim.muaf_mi(hedef):
return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True)
if gecmedi:
# Son katman ne dondurduyse o gider, hicbir sey eklenmez; dondurmediyse hicbir sey gitmez.
w(son_govde)
if son_govde:
w(son_govde)
else:
event = {"type": "response.failed", "response": {"status": "failed", "error": {
"code": "jev_no_fallback", "message": "Jev denetiminden gecen cevap alinamadi; gecis kurallarini kontrol edin."}}}
w(b"event: response.failed\ndata: " + json.dumps(event).encode() + b"\n\n")
else:
for chunk in parcalar:
if izle is not None:
@@ -1064,7 +1081,9 @@ class H(http.server.BaseHTTPRequestHandler):
"ide_model": int((sayi.split() or ["0"])[0]),
"ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False)),
"is_hatti": bool(HAT_KOMUT[0]),
"adim_denetimi": bool(jev_denetim.anahtar())})
"adim_denetimi": jev_denetim.etkin_mi() and bool(jev_denetim.anahtar()),
"jev_models": [{"id": m.get("target"), "name": m.get("display_name") or m.get("target")}
for m in EXTRA_MODELS if m.get("route") == IDE_ROUTE and m.get("target")]})
if self.command == "POST" and path.rstrip("/").endswith("/stop"):
self._send_json(200, {"ok": True})
log("AITURK IDE istegiyle kapatiliyor")
@@ -1720,9 +1739,9 @@ class H(http.server.BaseHTTPRequestHandler):
# secili model baska bir rotada olsa bile. Gecit yoksa denetim yapilmaz. Ozet isteginde ve aracsiz
# (rewriter yok) yolda kapalidir.
jev_rota = ROUTES.get(ide_route)
jev_sira = ([] if ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar()
else jev_denetim.sonrakiler(target))
jev_istek = jev_denetim.son_istek(req) if jev_sira else ""
jev_etkin = jev_denetim.etkin_mi() and not (ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar())
jev_sira = jev_denetim.sonrakiler(target) if jev_etkin else []
jev_istek = jev_denetim.son_istek(req) if jev_etkin else ""
def jev_ac(ad):
katman_req = json.loads(data)
@@ -1758,7 +1777,7 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar")
time.sleep(min(2 * attempt, 8)); continue
log(f"{tag} HTTP {e.code} {errbody[:200]!r}")
if jev_sira: # secili model hata verdi: adimi siradaki katman yapar
if jev_etkin: # secili model hata verdi: adimi siradaki katman yapar
return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target)
if pencere.tasma_mi(etxt):
# Hedef "baglam sigmiyor" dedi: Codex'in tanidigi tasma hatasi olarak iletilir ve hedefin
@@ -1805,10 +1824,10 @@ class H(http.server.BaseHTTPRequestHandler):
sonuc = self._stream(resp, resp.status, tag, akis)
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
return sonuc
if jev_sira and resp.status == 200:
if jev_etkin and resp.status == 200:
return self._denetimli_akis(resp, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target)
return self._stream(resp, resp.status, tag, rewriter, izle)
if jev_sira: # secili modele ulasilamadi: adimi siradaki katman yapar
if jev_etkin: # secili modele ulasilamadi: adimi siradaki katman yapar
return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target)
self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}})
@@ -71,7 +71,7 @@ class Katman(unittest.TestCase):
self.assertEqual(jev_denetim.sonrakiler("Katman-3"), ["Katman-4"])
def test_last_layer_is_never_asked(self):
self.assertEqual(jev_denetim.sonrakiler("Katman-4"), [])
self.assertTrue(jev_denetim.muaf_mi("Katman-4"))
self.assertEqual(jev_denetim.sonrakiler(None), [])
@@ -129,6 +129,7 @@ class Akis(unittest.TestCase):
sahte.komutlar = jev_denetim.komutlar
sahte.hazirlik_mi = jev_denetim.hazirlik_mi
sahte.muaf_mi = jev_denetim.muaf_mi
sahte.hata_gecis = jev_denetim.hata_gecis
env = {"jev_denetim": sahte, "yarim": yarim, "time": time, "http": __import__("http.client"),
"log": self.logs.append, "RESP_HOP": set(), "json": json}
adlar = ("_akis_basligi", "_denetimli_akis", "_stream")
@@ -332,7 +333,7 @@ class Akis(unittest.TestCase):
self.calistir(sse("Bunu yapamam."))
self.assertEqual(self.opened, ["Katman-2", "Katman-3", "Katman-4"])
self.assertNotIn(b"yapamam", self.h.wfile.getvalue())
self.assertEqual(self.sent(), b"") # the last layer said nothing: nothing is added
self.assertIn(b'response.failed', self.sent())
def test_the_last_layers_own_error_body_is_delivered_untouched(self):
class Hata(Exception):
@@ -0,0 +1,111 @@
import copy
import json
import os
import subprocess
import tempfile
import threading
import time
import unittest
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from unittest.mock import patch
import jev_denetim
import jev_settings
class Settings(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.home = Path(self.tmp.name)
self.home_patch = patch.object(jev_settings, 'home', return_value=self.home)
self.home_patch.start()
self.addCleanup(self.home_patch.stop)
self.settings = copy.deepcopy(jev_settings.DEFAULTS)
def save(self, **changes):
self.settings.update(changes)
(self.home / 'jev-settings.json').write_text(json.dumps({'settings': self.settings}), encoding='utf-8')
def test_live_rules_general_override_and_always_pass(self):
self.save(fallbackModel='B', rejectionRules=[{'from': 'A', 'to': 'C'}, {'from': 'C', 'to': 'D'}], exemptModels=['D'])
self.assertEqual(jev_denetim.sonrakiler('A'), ['C', 'D'])
self.assertEqual(jev_denetim.sonrakiler('Other'), ['B'])
self.assertTrue(jev_denetim.muaf_mi('D'))
self.assertFalse(jev_denetim.muaf_mi('A'))
self.save(enabled=False)
self.assertFalse(jev_denetim.etkin_mi())
self.assertEqual(jev_denetim.sonrakiler('A'), [])
def test_cycle_cannot_spin_even_if_file_manually_edited(self):
self.save(rejectionRules=[{'from': 'A', 'to': 'B'}, {'from': 'B', 'to': 'A'}])
self.assertEqual(jev_denetim.sonrakiler('A'), ['B'])
def test_prompts_model_criteria_and_thresholds_are_applied(self):
self.save(model='jev-custom', questions={'ret': 'question A', 'sart': 'question B'}, thresholds={'ret': .9, 'sart': .8}, criteria={'ret': {'true': 'bad', 'false': 'good'}, 'sart': {'true': '', 'false': ''}})
captured = []
def send(body, key):
captured.append(body)
return {'answers': {'ret': {'noul': .6}, 'sart': {'noul': .2}}}
result = jev_denetim.sor('request', 'answer', 'test', gonder=send)
self.assertFalse(jev_denetim.karar(result))
self.assertEqual(captured[0]['model'], 'jev-custom')
self.assertEqual(captured[0]['questions']['ret']['instructions'], 'question A')
self.assertEqual(captured[0]['questions']['ret']['criteria'], {'true': 'bad', 'false': 'good'})
self.assertNotIn('criteria', captured[0]['questions']['sart'])
self.save(thresholds={'ret': .1, 'sart': .1})
self.assertFalse(jev_denetim.karar(result)) # in-flight result retains its own thresholds
def test_concurrency_is_limited_without_serializing_all_requests(self):
self.save(maxConcurrent=2)
count, peak = 0, 0
lock = threading.Lock()
def send(body, key):
nonlocal count, peak
with lock:
count += 1
peak = max(peak, count)
time.sleep(.04)
with lock:
count -= 1
return {'answers': {'ret': {'noul': 0}, 'sart': {'noul': 0}}}
with ThreadPoolExecutor(max_workers=8) as pool:
answers = list(pool.map(lambda _: jev_denetim.sor('r', 'a', 'test', gonder=send), range(8)))
self.assertEqual(peak, 2)
self.assertTrue(all(a is not None for a in answers))
def test_timeout_endpoint_and_failure_behavior(self):
self.save(endpoint='https://example.test/jev', timeoutSeconds=42, failureAction='next')
with patch('urllib.request.urlopen', side_effect=TimeoutError) as opening:
self.assertIsNone(jev_denetim.sor('r', 'a', 'test'))
self.assertEqual(opening.call_args.kwargs['timeout'], 42)
self.assertEqual(opening.call_args.args[0].full_url, 'https://example.test/jev')
self.assertTrue(jev_denetim.hata_gecis())
self.assertEqual(jev_settings._active, 0)
def test_queue_wait_expires_and_slot_count_is_preserved(self):
self.save(maxConcurrent=1, queueWaitSeconds=1)
settings = jev_settings.read()[0]
self.assertTrue(jev_settings.acquire(settings))
try:
with patch.object(jev_denetim, '_gonder') as send:
self.assertIsNone(jev_denetim.sor('r', 'a', 'test', gonder=send))
send.assert_not_called()
self.assertEqual(jev_settings._active, 1)
finally:
jev_settings.release()
@unittest.skipUnless(os.name == 'nt', 'Windows DPAPI')
def test_encrypted_key_roundtrip_without_plaintext_on_disk(self):
secret = 'dummy-key-for-local-test'
code = 'Add-Type -AssemblyName System.Security; $b=[Text.Encoding]::UTF8.GetBytes([Console]::In.ReadToEnd()); [Console]::Out.Write([Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser)))'
encrypted = subprocess.run(['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', code], input=secret, capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW).stdout.strip()
text = json.dumps({'settings': self.settings, 'protectedKey': encrypted})
(self.home / 'jev-settings.json').write_text(text, encoding='utf-8')
self.assertNotIn(secret, text)
self.assertEqual(jev_settings.key(), secret)
if __name__ == '__main__':
unittest.main()
+1 -1
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 = '11'
export const CODEX_BRIDGE_VERSION = '12'
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'
+44
View File
@@ -378,6 +378,7 @@ import { ensureLoginShellPath } from './shell-path'
import { createBootstrapCoordinator, sshConfigFingerprint } from './ssh-bootstrap-coordinator'
import { collectSshConfigHosts, parseSshGOutput } from './ssh-config'
import { createSshProbeConnection, pickLocalPort, redactSecrets, SshConnection } from './ssh-connection'
import { createStagedTransitionStore } from './staged-transition'
import { createStreamThrottle } from './stream-throttle'
import { registerTerminalIpc } from './terminal-ipc'
import { nativeOverlayWidth as computeNativeOverlayWidth, macTitleBarOverlayHeight } from './titlebar-overlay-width'
@@ -17192,6 +17193,21 @@ ipcMain.handle('hermes:quick-entry:settings:set', async (_event, patch) => {
// resulting state and asks for changes over IPC. Windows only (the login item is
// a Run-key value). All decisions live in electron/codex-link.ts.
const AITURK_IDE_DATA_ROOT = path.dirname(HERMES_HOME)
const stagedTransition = createStagedTransitionStore(
path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'),
path.join(app.getPath('home'), '.typesafe', 'typesafe.env'),
value =>
execFileSync(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-Command',
'Add-Type -AssemblyName System.Security; $bytes=[Text.Encoding]::UTF8.GetBytes([Console]::In.ReadToEnd()); [Console]::Out.Write([Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($bytes,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser)))'
],
{ input: value, encoding: 'utf8', windowsHide: true, timeout: 15000 }
).trim()
)
const codexLink = createCodexLink(
createNodeCodexLinkDeps(
@@ -17243,6 +17259,34 @@ ipcMain.handle('aiturk:codex-link:get', async event => {
return codexLink.getStatus()
})
async function stagedTransitionState() {
const state = stagedTransition.get()
try {
const response = await fetch('http://127.0.0.1:8795/aiturk/codex-baglama/status', {
signal: AbortSignal.timeout(5000)
})
if (!response.ok) throw new Error('Bridge unavailable')
const body = await response.json()
const models = Array.isArray(body.jev_models)
? body.jev_models.filter(m => typeof m?.id === 'string' && typeof m?.name === 'string')
: []
return { ...state, models, modelsAvailable: Array.isArray(body.jev_models) }
} catch {
return { ...state, models: [], modelsAvailable: false }
}
}
ipcMain.handle('aiturk:staged-transition:get', async event => {
assertCodexLinkSender(event)
return stagedTransitionState()
})
ipcMain.handle('aiturk:staged-transition:set', async (event, patch) => {
assertCodexLinkSender(event)
stagedTransition.save(patch)
return stagedTransitionState()
})
ipcMain.handle('aiturk:codex-link:set', async (event, patch) => {
assertCodexLinkSender(event)
+2
View File
@@ -295,6 +295,8 @@ contextBridge.exposeInMainWorld('hermesDesktop', {
...(process.platform === 'win32'
? {
codexLink: {
getStagedTransition: () => ipcRenderer.invoke('aiturk:staged-transition:get'),
setStagedTransition: patch => ipcRenderer.invoke('aiturk:staged-transition:set', patch),
get: () => ipcRenderer.invoke('aiturk:codex-link:get'),
set: patch => ipcRenderer.invoke('aiturk:codex-link:set', patch)
}
@@ -0,0 +1,27 @@
export interface StagedTransitionSettings {
enabled: boolean
endpoint: string
model: string
fallbackModel: string
rejectionRules: { from: string; to: string }[]
exemptModels: string[]
thresholds: { ret: number; sart: number }
questions: { ret: string; sart: string }
criteria: { ret: { true: string; false: string }; sart: { true: string; false: string } }
timeoutSeconds: number
maxConcurrent: number
queueWaitSeconds: number
failureAction: 'pass' | 'next'
}
export interface StagedTransitionState {
settings: StagedTransitionSettings
keyConfigured: boolean
models?: { id: string; name: string }[]
modelsAvailable?: boolean
}
export interface StagedTransitionPatch {
settings: StagedTransitionSettings
apiKey?: string
}
@@ -0,0 +1,53 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { createStagedTransitionStore, stagedTransitionDefaults, validateStagedTransition } from './staged-transition'
const dirs: string[] = []
afterEach(() => {
for (const dir of dirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true })
}
})
describe('staged transition settings', () => {
it('persists rules and protects keys without returning a secret', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jev-settings-test-'))
dirs.push(dir)
const store = createStagedTransitionStore(dir, path.join(dir, 'missing.env'), () => 'encrypted-test-value')
const settings = structuredClone(stagedTransitionDefaults)
settings.fallbackModel = 'fallback'
settings.rejectionRules = [{ from: 'model-a', to: 'model-b' }]
const saved = store.save({ settings, apiKey: 'dummy-secret' })
expect(saved.settings.rejectionRules).toEqual(settings.rejectionRules)
expect(saved.keyConfigured).toBe(true)
expect(JSON.stringify(saved)).not.toContain('dummy-secret')
expect(fs.readFileSync(path.join(dir, 'jev-settings.json'), 'utf8')).not.toContain('dummy-secret')
expect(store.get()).toEqual(saved)
store.save({ settings: { ...settings, timeoutSeconds: 123 } })
expect(store.get().keyConfigured).toBe(true)
})
it('rejects invalid rules without replacing the saved settings', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jev-settings-test-'))
dirs.push(dir)
const store = createStagedTransitionStore(dir, path.join(dir, 'missing.env'), x => x)
const before = store.save({ settings: structuredClone(stagedTransitionDefaults) })
expect(() =>
store.save({
settings: {
...before.settings,
rejectionRules: [
{ from: 'a', to: 'b' },
{ from: 'b', to: 'a' }
]
}
})
).toThrow()
expect(store.get().settings).toEqual(before.settings)
expect(() => validateStagedTransition({ ...before.settings, maxConcurrent: -1 })).toThrow()
expect(() => validateStagedTransition({ ...before.settings, thresholds: { ret: NaN, sart: 0.7 } })).toThrow()
expect(() => validateStagedTransition({ ...before.settings, endpoint: 'http://example.com' })).toThrow()
})
})
+174
View File
@@ -0,0 +1,174 @@
import fs from 'node:fs'
import path from 'node:path'
import defaults from '../assets/codex-bridge/jev-settings-defaults.json'
import type { StagedTransitionPatch, StagedTransitionSettings, StagedTransitionState } from './staged-transition-types'
export const stagedTransitionDefaults = defaults as StagedTransitionSettings
export function validateStagedTransition(value: unknown): StagedTransitionSettings {
if (!value || typeof value !== 'object') {
throw new Error('Invalid settings')
}
const v = value as StagedTransitionSettings
const text = (s: unknown, max = 10000): s is string => typeof s === 'string' && s.trim().length > 0 && s.length <= max
const list = (s: unknown, allowEmpty: boolean) =>
Array.isArray(s) &&
(allowEmpty || s.length > 0) &&
s.length <= 64 &&
s.every(x => text(x, 256)) &&
new Set(s.map(x => x.trim())).size === s.length
const seconds = (n: unknown) => typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 3600
if (typeof v.enabled !== 'boolean' || !text(v.endpoint, 2048) || !text(v.model, 256)) {
throw new Error('Invalid connection settings')
}
const url = new URL(v.endpoint)
if (url.protocol !== 'https:' || url.username || url.password || url.hash) {
throw new Error('HTTPS endpoint required')
}
if (
!text(v.fallbackModel, 256) ||
!list(v.exemptModels, true) ||
!Array.isArray(v.rejectionRules) ||
v.rejectionRules.length > 64 ||
v.rejectionRules.some(r => !text(r.from, 256) || !text(r.to, 256) || r.from.trim() === r.to.trim()) ||
new Set(v.rejectionRules.map(r => r.from.trim())).size !== v.rejectionRules.length
) {
throw new Error('Invalid model rules')
}
const routes = new Map(v.rejectionRules.map(r => [r.from.trim(), r.to.trim()]))
for (const start of routes.keys()) {
const seen = new Set<string>()
let current: string | undefined = start
while (current && !v.exemptModels.includes(current)) {
if (seen.has(current)) {
throw new Error('Model rules contain a cycle')
}
seen.add(current)
current = routes.get(current) ?? (current === v.fallbackModel.trim() ? undefined : v.fallbackModel.trim())
}
}
for (const id of ['ret', 'sart'] as const) {
if (
!text(v.questions?.[id]) ||
typeof v.thresholds?.[id] !== 'number' ||
!Number.isFinite(v.thresholds[id]) ||
v.thresholds[id] < 0 ||
v.thresholds[id] > 1
) {
throw new Error('Invalid evaluation rule')
}
for (const outcome of ['true', 'false'] as const) {
if (typeof v.criteria?.[id]?.[outcome] !== 'string' || v.criteria[id][outcome].length > 10000) {
throw new Error('Invalid decision criteria')
}
}
}
if (
!seconds(v.timeoutSeconds) ||
!seconds(v.queueWaitSeconds) ||
!Number.isInteger(v.maxConcurrent) ||
v.maxConcurrent < 0 ||
v.maxConcurrent > 512 ||
!['pass', 'next'].includes(v.failureAction)
) {
throw new Error('Invalid request limits')
}
return {
enabled: v.enabled,
endpoint: v.endpoint.trim(),
model: v.model.trim(),
fallbackModel: v.fallbackModel.trim(),
rejectionRules: v.rejectionRules.map(r => ({ from: r.from.trim(), to: r.to.trim() })),
exemptModels: v.exemptModels.map(x => x.trim()),
questions: { ...v.questions },
thresholds: { ...v.thresholds },
criteria: structuredClone(v.criteria),
timeoutSeconds: v.timeoutSeconds,
maxConcurrent: v.maxConcurrent,
queueWaitSeconds: v.queueWaitSeconds,
failureAction: v.failureAction
}
}
export function createStagedTransitionStore(
directory: string,
legacyKeyFile: string,
protect: (value: string) => string
) {
const file = path.join(directory, 'jev-settings.json')
const read = (): { settings: StagedTransitionSettings; protectedKey?: string } => {
if (!fs.existsSync(file)) {
return { settings: structuredClone(stagedTransitionDefaults) }
}
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
return { settings: validateStagedTransition(data.settings), protectedKey: data.protectedKey }
}
const state = (): StagedTransitionState => {
const current = read()
let legacy = Boolean(process.env.TYPESAFE_API_KEY?.trim())
if (!legacy && fs.existsSync(legacyKeyFile)) {
const match = /^\s*TYPESAFE_API_KEY\s*=\s*(.*)$/m.exec(fs.readFileSync(legacyKeyFile, 'utf8'))
legacy = Boolean(
match?.[1]
?.trim()
.replace(/^(['"])(.*)\1$/, '$2')
.trim()
)
}
return { settings: current.settings, keyConfigured: Boolean(current.protectedKey) || legacy }
}
return {
get: state,
save(patch: StagedTransitionPatch): StagedTransitionState {
const settings = validateStagedTransition(patch?.settings)
if (patch.apiKey !== undefined && (typeof patch.apiKey !== 'string' || patch.apiKey.length > 8192)) {
throw new Error('Invalid API key')
}
const current = read()
const protectedKey =
patch.apiKey === undefined
? current.protectedKey
: patch.apiKey.trim()
? protect(patch.apiKey.trim())
: undefined
fs.mkdirSync(directory, { recursive: true })
const tmp = `${file}.${process.pid}.tmp`
try {
fs.writeFileSync(tmp, JSON.stringify({ settings, protectedKey }, null, 2), { mode: 0o600 })
fs.renameSync(tmp, file)
} finally {
if (fs.existsSync(tmp)) {
fs.unlinkSync(tmp)
}
}
return state()
}
}
}
+1
View File
@@ -304,6 +304,7 @@
"from": "assets/codex-bridge",
"to": "codex-bridge",
"filter": [
"jev-settings-defaults.json",
"**/*.py",
"**/*.pyd",
"native-watch.exe",
+27 -3
View File
@@ -58,6 +58,7 @@ import { NotificationsSettings } from './notifications-settings'
import { PluginsSettings } from './plugins-settings'
import { PROVIDER_VIEWS, ProvidersSettings, type ProviderView } from './providers-settings'
import { SessionsSettings } from './sessions-settings'
import { StagedTransitionSettingsView } from './staged-transition-settings'
import type { SettingsPageProps, SettingsView as SettingsViewId } from './types'
const SETTINGS_VIEWS: readonly SettingsViewId[] = [
@@ -72,13 +73,14 @@ const SETTINGS_VIEWS: readonly SettingsViewId[] = [
'notifications',
'billing',
'codex-link',
'staged-transition',
'plugins',
'sessions',
'about'
]
export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: SettingsPageProps) {
const { t } = useI18n()
const { t, locale } = useI18n()
const aiturkCopy = useAiturkCopy()
const codexLinkCopy = useCodexLinkCopy()
const navigate = useNavigate()
@@ -267,7 +269,9 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
id: 'aiturk-phone',
label: aiturkCopy.mobile.nav,
onSelect: () => {
void window.hermesDesktop?.aiturkMobile?.open().catch(error => notifyError(error, aiturkCopy.mobile.openFailed))
void window.hermesDesktop?.aiturkMobile
?.open()
.catch(error => notifyError(error, aiturkCopy.mobile.openFailed))
}
}
]
@@ -282,6 +286,13 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
id: 'codex-link',
label: codexLinkCopy.nav,
onSelect: () => setActiveView('codex-link')
},
{
active: activeView === 'staged-transition',
icon: Link,
id: 'staged-transition',
label: locale === 'tr' ? 'Kademeli Geçiş' : 'Staged Transition',
onSelect: () => setActiveView('staged-transition')
}
]
: []),
@@ -338,7 +349,18 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
onSelect: () => setActiveView('about')
}
],
[activeView, keysView, providerView, t, aiturkCopy, codexLinkCopy, setActiveView, openProviderView, openKeysView]
[
activeView,
keysView,
providerView,
t,
locale,
aiturkCopy,
codexLinkCopy,
setActiveView,
openProviderView,
openKeysView
]
)
// Type-to-search: printable keystrokes on the Settings surface (outside any
@@ -459,6 +481,8 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
<PluginsSettings />
) : activeView === 'codex-link' ? (
<CodexLinkSettings />
) : activeView === 'staged-transition' ? (
<StagedTransitionSettingsView />
) : (
<SessionsSettings />
)
@@ -0,0 +1,71 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/i18n/context'
import defaults from '../../../assets/codex-bridge/jev-settings-defaults.json'
import { StagedTransitionSettingsView } from './staged-transition-settings'
const get = vi.fn()
const set = vi.fn()
const state = () => ({
settings: structuredClone(defaults),
keyConfigured: true,
modelsAvailable: true,
models: [
{ id: 'A', name: 'Model A' },
{ id: 'B', name: 'Model B' }
]
})
beforeEach(() => {
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
codexLink: { getStagedTransition: get, setStagedTransition: set }
}
get.mockResolvedValue(state())
set.mockImplementation(async patch => ({ ...state(), settings: patch.settings }))
})
afterEach(() => {
cleanup()
vi.resetAllMocks()
})
const show = () =>
render(
<I18nProvider configClient={null} initialLocale="tr">
<StagedTransitionSettingsView />
</I18nProvider>
)
describe('staged transition settings UI', () => {
it('adds and removes model routing and always-pass rows', async () => {
show()
await screen.findByText('Kademeli Geçiş')
fireEvent.click(screen.getByText('+ Geçiş kuralı ekle'))
expect(screen.getByLabelText('İtiraz edilen model 4')).toBeTruthy()
expect(screen.getByLabelText('Kullanılacak model 4')).toBeTruthy()
fireEvent.click(screen.getByLabelText('Geçiş kuralı 4 sil'))
expect(screen.queryByLabelText('İtiraz edilen model 4')).toBeNull()
fireEvent.click(screen.getByText('+ Her zaman geç kuralı ekle'))
expect(screen.getByLabelText('Her zaman geçen model 3')).toBeTruthy()
fireEvent.click(screen.getByLabelText('Her zaman geç kuralı 3 sil'))
fireEvent.change(screen.getByLabelText('İsteği yerine getirmeme denetim metni'), {
target: { value: 'custom prompt' }
})
fireEvent.click(screen.getByText('Kaydet'))
await screen.findByText('Kaydedildi. Yeni değerlendirmelerde uygulanır.')
expect(set.mock.calls[0][0].settings.questions.ret).toBe('custom prompt')
expect(set.mock.calls[0][0]).not.toHaveProperty('apiKey')
})
it('retains user edits if persistence fails', async () => {
set.mockRejectedValue(new Error('disk error'))
show()
await screen.findByText('Kademeli Geçiş')
fireEvent.change(screen.getByLabelText('Jev modeli'), { target: { value: 'new-model' } })
fireEvent.click(screen.getByText('Kaydet'))
await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('Kaydedilemedi'))
expect((screen.getByLabelText('Jev modeli') as HTMLInputElement).value).toBe('new-model')
})
})
@@ -0,0 +1,463 @@
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { useI18n } from '@/i18n/context'
import { Link } from '@/lib/icons'
import defaultSettings from '../../../assets/codex-bridge/jev-settings-defaults.json'
import type { StagedTransitionSettings, StagedTransitionState } from '../../../electron/staged-transition-types'
import { ListRow, SettingsContent, SettingsSection } from './primitives'
export function StagedTransitionSettingsView() {
const { locale } = useI18n()
const tr = locale === 'tr'
const text = (turkish: string, english: string) => (tr ? turkish : english)
const api = window.hermesDesktop?.codexLink
const [state, setState] = useState<StagedTransitionState | null>(null)
const [draft, setDraft] = useState<StagedTransitionSettings | null>(null)
const [key, setKey] = useState('')
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const [error, setError] = useState(false)
const adopt = (next: StagedTransitionState) => {
setState(next)
setDraft(next.settings)
}
useEffect(() => {
let active = true
if (api?.getStagedTransition) {
api.getStagedTransition().then(
next => {
if (active) {
adopt(next)
}
},
() => {
if (active) {
setError(true)
setMessage(tr ? 'Ayarlar okunamadı.' : 'Could not read settings.')
}
}
)
}
return () => {
active = false
}
}, [api, tr])
if (!api?.getStagedTransition) {
return (
<SettingsContent>
<p>
{text('Bu bölüm Windows masaüstü uygulamasında kullanılabilir.', 'Available in the Windows desktop app.')}
</p>
</SettingsContent>
)
}
if (!draft) {
return (
<SettingsContent>
<p role={error ? 'alert' : 'status'}>{message || text('Yükleniyor…', 'Loading…')}</p>
</SettingsContent>
)
}
const change = <K extends keyof StagedTransitionSettings>(name: K, value: StagedTransitionSettings[K]) =>
setDraft({ ...draft, [name]: value })
const models = new Map((state?.models ?? []).map(model => [model.id, model.name]))
const storedModels = [
draft.fallbackModel,
...draft.exemptModels,
...draft.rejectionRules.flatMap(rule => [rule.from, rule.to])
]
for (const id of storedModels) {
if (id && !models.has(id)) {
models.set(id, `${id} (${text('kayıtlı', 'saved')})`)
}
}
const modelSelect = (label: string, value: string, onChange: (value: string) => void) => (
<div className="w-full min-w-0 sm:w-auto sm:min-w-48 sm:flex-1">
<Select onValueChange={onChange} value={value}>
<SelectTrigger aria-label={label}>
<SelectValue placeholder={text('Model seçin', 'Select a model')} />
</SelectTrigger>
<SelectContent>
{Array.from(models, ([id, name]) => (
<SelectItem key={id} value={id}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
const number = (
name: 'timeoutSeconds' | 'maxConcurrent' | 'queueWaitSeconds',
label: string,
min: number,
max: number
) => (
<ListRow
action={
<Input
aria-label={label}
max={max}
min={min}
onChange={event => change(name, Number(event.target.value))}
type="number"
value={draft[name]}
/>
}
title={label}
/>
)
const save = async () => {
setBusy(true)
setMessage('')
setError(false)
try {
adopt(await api.setStagedTransition({ settings: draft, ...(key.trim() ? { apiKey: key.trim() } : {}) }))
setKey('')
setMessage(text('Kaydedildi. Yeni değerlendirmelerde uygulanır.', 'Saved. Applies to new evaluations.'))
} catch (failure) {
setError(true)
const cycle = String(failure).includes('cycle')
setMessage(
cycle
? text(
'Geçiş kuralları döngü oluşturuyor. Aynı modele geri dönen geçişi değiştirin.',
'Transition rules form a cycle. Change the rule that returns to an earlier model.'
)
: text(
'Kaydedilemedi. Alanları ve sayı aralıklarını kontrol edin; önceki ayarlar korunuyor.',
'Could not save. Check the fields and number ranges; previous settings are preserved.'
)
)
} finally {
setBusy(false)
}
}
return (
<SettingsContent>
<fieldset className="min-w-0" disabled={busy}>
<SettingsSection icon={Link} title={text('Kademeli Geçiş', 'Staged Transition')}>
<ListRow
action={
<Switch
aria-label={text('Jev denetimi', 'Jev evaluation')}
checked={draft.enabled}
onCheckedChange={value => change('enabled', value)}
/>
}
description={text(
'Köprüdeki ek modellerin nihai cevaplarını denetler. Uygun bulunmayan cevap sıradaki katmanda yeniden hazırlanır.',
'Evaluates final answers from bridge models. Rejected answers are retried with the next layer.'
)}
title={text('Jev denetimi ve kademeli geçiş', 'Jev evaluation and staged transition')}
/>
<ListRow
action={
<Input
aria-label={text('Jev modeli', 'Jev model')}
onChange={e => change('model', e.target.value)}
value={draft.model}
/>
}
title={text('Jev modeli', 'Jev model')}
/>
<ListRow
action={
<Input
aria-label={text('API adresi', 'API endpoint')}
onChange={e => change('endpoint', e.target.value)}
type="url"
value={draft.endpoint}
/>
}
title={text('API adresi', 'API endpoint')}
/>
<ListRow
action={
<Input
aria-label={text('TypeSafe API anahtarı', 'TypeSafe API key')}
autoComplete="new-password"
onChange={e => setKey(e.target.value)}
type="password"
value={key}
/>
}
description={
state?.keyConfigured
? text(
'Anahtar tanımlı. Değiştirmek için yeni anahtarı girin.',
'Key configured. Enter a new key to replace it.'
)
: text('Henüz anahtar tanımlı değil.', 'No key configured.')
}
title={text('TypeSafe API anahtarı', 'TypeSafe API key')}
/>
</SettingsSection>
<SettingsSection icon={Link} title={text('Denetim kuralları', 'Evaluation rules')}>
{(['ret', 'sart'] as const).map(id => {
const label =
id === 'ret'
? text('İsteği yerine getirmeme', 'Request not fulfilled')
: text('İsteği kendiliğinden değiştirme', 'Unrequested changes')
return (
<div key={id}>
<ListRow
action={
<Input
aria-label={`${label} ${text('eşiği', 'threshold')}`}
max={1}
min={0}
onChange={e => change('thresholds', { ...draft.thresholds, [id]: Number(e.target.value) })}
step={0.01}
type="number"
value={draft.thresholds[id]}
/>
}
description={text(
'0–1 arasında geçiş eşiği. Olasılık bu değere ulaşırsa sıradaki katman denenir.',
'Transition threshold from 0 to 1. A probability at or above this value tries the next layer.'
)}
title={label}
/>
<Textarea
aria-label={`${label} ${text('denetim metni', 'instructions')}`}
onChange={e => change('questions', { ...draft.questions, [id]: e.target.value })}
rows={4}
value={draft.questions[id]}
/>
{(['true', 'false'] as const).map(outcome => (
<label className="block py-2" key={outcome}>
<span>
{outcome === 'true'
? text(
'“Evet, sorun var” ne demek? (isteğe bağlı)',
'What does “yes, there is a problem” mean? (optional)'
)
: text(
'“Hayır, sorun yok” ne demek? (isteğe bağlı)',
'What does “no problem” mean? (optional)'
)}
</span>
<Textarea
aria-label={`${label} ${outcome}`}
onChange={e =>
change('criteria', {
...draft.criteria,
[id]: { ...draft.criteria[id], [outcome]: e.target.value }
})
}
rows={2}
value={draft.criteria[id][outcome]}
/>
</label>
))}
</div>
)
})}
</SettingsSection>
<SettingsSection
icon={Link}
title={text('Olasılık, onay ve sıcaklık', 'Probability, approval and temperature')}
>
<p>
{text(
'Jev her denetim sorusu için 0–1 arasında sorun olasılığı verir. Her iki olasılık da kendi itiraz eşiğinin altındaysa cevap geçer. Örneğin 0,70 eşiği, sorun olasılığı %70 veya üstündeyse geçiş demektir; %70 kalite garantisi değildir.',
'Jev returns a problem probability from 0 to 1 for each question. An answer passes when both probabilities are below their rejection thresholds. A threshold of 0.70 triggers transition at 70% problem probability; it is not a quality guarantee.'
)}
</p>
<ListRow
description={text('TypeSafe API bu parametreyi sunmuyor.', 'Not exposed by the TypeSafe API.')}
title={text('Sıcaklık', 'Temperature')}
/>
<ListRow
description={text(
'Kullanılan Noul (evet/hayır) türünde ayrı güven skoru yoktur. Karar yukarıdaki olasılık eşikleriyle verilir.',
'The Noul (yes/no) type has no separate confidence score. Decisions use the probability thresholds above.'
)}
title={text('Ayrı emin olma / güven değeri', 'Separate confidence value')}
/>
</SettingsSection>
<SettingsSection icon={Link} title={text('Geçiş kuralları', 'Transition rules')}>
{!state?.modelsAvailable && (
<p role="status">
{text(
'Canlı model listesi alınamadı. Kayıtlı seçimler korunuyor; Codex Bağlama açıkken listeyi yenileyin.',
'Live model list unavailable. Saved selections are preserved; refresh with Codex Link enabled.'
)}
</p>
)}
<Button
onClick={async () => {
try {
const next = await api.getStagedTransition()
setState(next)
} catch {
setError(true)
setMessage(text('Model listesi alınamadı.', 'Could not load models.'))
}
}}
variant="outline"
>
{text('Model listesini yenile', 'Refresh models')}
</Button>
<ListRow
action={modelSelect(text('Genel geçiş modeli', 'Default fallback model'), draft.fallbackModel, value =>
change('fallbackModel', value)
)}
description={text(
'Modele özel kural yoksa seçilen model kullanılır.',
'Uses this model when there is no model-specific rule.'
)}
title={text('Jev herhangi bir modele itiraz ederse', 'When Jev rejects any model')}
/>
{draft.rejectionRules.map((rule, index) => (
<div className="flex flex-wrap items-center gap-2 py-2" key={index}>
{modelSelect(text(`İtiraz edilen model ${index + 1}`, `Rejected model ${index + 1}`), rule.from, value =>
change(
'rejectionRules',
draft.rejectionRules.map((r, i) => (i === index ? { ...r, from: value } : r))
)
)}
<span>{text('modeline Jev itiraz ederse', 'rejected by Jev →')}</span>
{modelSelect(text(`Kullanılacak model ${index + 1}`, `Fallback model ${index + 1}`), rule.to, value =>
change(
'rejectionRules',
draft.rejectionRules.map((r, i) => (i === index ? { ...r, to: value } : r))
)
)}
<span>{text('modeli kullanılsın', 'use this model')}</span>
<Button
aria-label={text(`Geçiş kuralı ${index + 1} sil`, `Remove rule ${index + 1}`)}
onClick={() =>
change(
'rejectionRules',
draft.rejectionRules.filter((_, i) => i !== index)
)
}
variant="ghost"
>
{text('Sil', 'Remove')}
</Button>
</div>
))}
<Button
onClick={() => change('rejectionRules', [...draft.rejectionRules, { from: '', to: '' }])}
variant="outline"
>
{text('+ Geçiş kuralı ekle', '+ Add transition rule')}
</Button>
</SettingsSection>
<SettingsSection icon={Link} title={text('Jev’den her zaman geçsin', 'Always pass Jev')}>
<p>
{text(
'Bu modeller Jev denetimine gönderilmez. Bu seçim, modele özel ve genel itiraz kurallarından önce uygulanır.',
'These models bypass Jev evaluation. This takes priority over model-specific and default rejection rules.'
)}
</p>
{draft.exemptModels.map((model, index) => (
<div className="flex flex-wrap items-center gap-2 py-2" key={index}>
{modelSelect(text(`Her zaman geçen model ${index + 1}`, `Always-pass model ${index + 1}`), model, value =>
change(
'exemptModels',
draft.exemptModels.map((m, i) => (i === index ? value : m))
)
)}
<span>{text('modeli Jev’den her zaman geçsin', 'always passes Jev')}</span>
<Button
aria-label={text(`Her zaman geç kuralı ${index + 1} sil`, `Remove always-pass rule ${index + 1}`)}
onClick={() =>
change(
'exemptModels',
draft.exemptModels.filter((_, i) => i !== index)
)
}
variant="ghost"
>
{text('Sil', 'Remove')}
</Button>
</div>
))}
<Button onClick={() => change('exemptModels', [...draft.exemptModels, ''])} variant="outline">
{text('+ Her zaman geç kuralı ekle', '+ Add always-pass rule')}
</Button>
</SettingsSection>
<SettingsSection icon={Link} title={text('Paralellik ve bekleme', 'Concurrency and waiting')}>
{number(
'maxConcurrent',
text('Eşzamanlı Jev isteği (0: yerel sınır yok)', 'Concurrent Jev requests (0: no local limit)'),
0,
512
)}
{number(
'queueWaitSeconds',
text('Sırada en fazla bekleme (saniye)', 'Maximum queue wait (seconds)'),
1,
3600
)}
{number('timeoutSeconds', text('Jev yanıt zaman aşımı (saniye)', 'Jev response timeout (seconds)'), 1, 3600)}
<ListRow
action={
<Select
onValueChange={value => change('failureAction', value as 'pass' | 'next')}
value={draft.failureAction}
>
<SelectTrigger aria-label={text('Jev’e ulaşılamadığında', 'When Jev is unavailable')}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pass">{text('Mevcut cevabı ilet', 'Pass the current answer')}</SelectItem>
<SelectItem value="next">{text('Sıradaki katmanı dene', 'Try the next layer')}</SelectItem>
</SelectContent>
</Select>
}
title={text('Jev’e ulaşılamadığında', 'When Jev is unavailable')}
/>
</SettingsSection>
<Button onClick={() => void save()}>{busy ? text('Kaydediliyor…', 'Saving…') : text('Kaydet', 'Save')}</Button>
<Button
onClick={() => {
setDraft(structuredClone(defaultSettings) as StagedTransitionSettings)
setMessage(
text(
'Varsayılanlar forma yüklendi. Uygulamak için Kaydet’e basın.',
'Defaults loaded into the form. Save to apply.'
)
)
setError(false)
}}
variant="ghost"
>
{text('Varsayılanları yükle', 'Load defaults')}
</Button>
{message && (
<p className="py-3" role={error ? 'alert' : 'status'}>
{message}
</p>
)}
</fieldset>
</SettingsContent>
)
}
+1
View File
@@ -16,6 +16,7 @@ export type SettingsView =
| 'plugins'
| 'providers'
| 'sessions'
| 'staged-transition'
| `config:${string}`
export type EnvPatch = Partial<Pick<EnvVarInfo, 'is_set' | 'redacted_value'>>
+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 { PoolLimits } from '../electron/pool-limits'
import type { StagedTransitionPatch, StagedTransitionState } from '../electron/staged-transition-types'
import type { WakeIndicatorState } from './lib/wake-indicator'
import type {
@@ -340,6 +341,8 @@ declare global {
* use the IDE's models. `get` only reads; `set` applies and returns the
* state that actually resulted. Windows only. */
codexLink?: {
getStagedTransition: () => Promise<StagedTransitionState>
setStagedTransition: (patch: StagedTransitionPatch) => Promise<StagedTransitionState>
get: () => Promise<CodexLinkStatus>
set: (patch: CodexLinkPatch) => Promise<CodexLinkStatus>
}
+6 -1
View File
@@ -16,6 +16,11 @@
"rootDir": "..",
"outDir": "build/electron-types"
},
"include": ["electron", "../shared/src/data-url-read-max.ts", "../shared/src/translucency.ts"],
"include": [
"electron",
"assets/codex-bridge/jev-settings-defaults.json",
"../shared/src/data-url-read-max.ts",
"../shared/src/translucency.ts"
],
"exclude": ["src", "electron/**/*.e2e.mts"]
}