Compare commits

...
Author SHA1 Message Date
yilsemandClaude Opus 5.5 5c76c24c9c feat(codex-link): choose which models fill the Codex model menu and beta.41
Server CI and Windows package / verify (push) Successful in 2m54s
Server CI and Windows package / windows-package (push) Successful in 4m50s
The Codex desktop app's model picker asks the app server for one page of
at most 100 models and never asks for the next page (a fixed limit in the
app). AITURK sent 262, so everything after the first 100 in alphabetical
provider order was out of reach from the menu (Gemini, most NanoGPT
uncensored models, Grok, ModelArk...).

- Bridge: bridge.json codex_liste {medya_gizle, efor_gizle, one_al, gizli},
  re-read when the file changes. Image/audio/video models and "-low/-high"
  copies whose base model is listed are left out by default; pinned models
  follow Codex's own models in the given order; hidden ones are removed.
  The candidate list with each model's place is written to
  log/son-liste/codex-adaylar.json for the settings screen.
- Settings > Codex Bağlama: "Models shown in Codex" with a live count of the
  100 slots, search, filter, pin/move up/hide per model and the two switches.
- The IDE keeps codex_liste when it rewrites bridge.json.
- Bridge version 21, IDE 1.0.0-beta.41.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 05:16:40 +03:00
17 changed files with 696 additions and 8 deletions

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ import gecis_context
import terminal_stream
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "20" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
SURUM = "21" # 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]
@@ -192,7 +192,41 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"], "yol": s["yol"]} for s in secilen[:EN_COK_MODEL]]
def sirala_codex(modeller, ekler, ozgun_kimlikler=()):
# ---- Codex secicisinde gorunen liste (15 Ekim 2026) ----
# Codex masaustu uygulamasinin model secicisi listeyi tek sayfada, en cok 100 modelle ister (uygulamanin kendi kodunda
# sabit; sonraki sayfayi istemez). Kullanici o 100'e hangi modellerin girecegini secer (bridge.json "codex_liste"):
# "one_al" ozgun modellerin hemen arkasina verdigi sirayla alinir, "gizli" cikarilir; resim/ses/video modelleri ve
# ana modeli listede olan dusuk-orta-yuksek kopyalari istege bagli gizlenir (varsayilan: gizli).
CODEX_SECICI_SINIRI = 100
_MEDYA = re.compile(r"(?:^|[-_/. ])(?:tts|image|imagen|veo|video|lyria|embedding|transcribe|whisper|flux|kokoro|orpheus|"
r"chatterbox|seedance|seedream|nano-banana|aqa|robotics|computer-use|realtime|music|audio|speech)"
r"(?=$|[-_/. ])", re.I)
_EFOR = re.compile(r"-(?:none|minimal|low|medium|high|xhigh|max)$", re.I)
def liste_ayari(ham):
"""bridge.json "codex_liste" -> {medya_gizle, efor_gizle, one_al, gizli}; eksik ya da bozuk alan varsayilandir."""
a = ham if isinstance(ham, dict) else {}
def liste(anahtar):
deger = a.get(anahtar)
return [str(s) for s in deger if isinstance(s, str) and s.strip()][:2000] if isinstance(deger, list) else []
return {"medya_gizle": a.get("medya_gizle") is not False, "efor_gizle": a.get("efor_gizle") is not False,
"one_al": list(dict.fromkeys(liste("one_al"))), "gizli": liste("gizli")}
def medya_mi(slug, ad=""):
"""Codex sohbetinde calismayan resim, ses, video, gomme modeli mi (kimlikten ya da adindan)."""
return bool(_MEDYA.search(slug or "") or _MEDYA.search(ad or ""))
def efor_kopyasi_mi(slug, kimlikler):
""""...-high" gibi bir kopya ve ana modeli de listede mi; Codex'in kendi dusunme seviyesi secimi ana modelde var."""
taban = _EFOR.sub("", slug or "")
return taban != slug and taban in kimlikler
def sirala_codex(modeller, ekler, ozgun_kimlikler=(), ayar=None):
"""Ozgun abonelik modelleri oldugu gibi kalir: sirasi, priority degeri ve adi degismez,
listenin basinda durur. Ek modeller onlarin ARKASINA eklenir: combo A-Z, sonra
saglayici A-Z / model A-Z. Ozgun bir modelle ayni adi tasiyan ek model " (AITURK)"
@@ -227,6 +261,26 @@ def sirala_codex(modeller, ekler, ozgun_kimlikler=()):
return (1, saglayici.casefold(), model.casefold(), slug)
ek_modeller.sort(key=anahtar)
adaylar = None
if ayar is not None:
# Kullanicinin secimi: once kimin gizlenecegi, sonra one alinanlar (verdigi sirayla); gerisi yukaridaki sirada.
ayar = liste_ayari(ayar)
kimlikler = {m.get("slug", "") for m in ek_modeller}
one_sira = {s: i for i, s in enumerate(s for s in ayar["one_al"] if s in kimlikler)}
gizli = set(ayar["gizli"])
adaylar, kalan = [], []
for m in ek_modeller:
slug = m.get("slug", "")
medya, efor = medya_mi(slug, m.get("display_name")), efor_kopyasi_mi(slug, kimlikler)
one = slug in one_sira
neden = ("gizli" if slug in gizli else "medya" if medya and ayar["medya_gizle"] and not one
else "efor" if efor and ayar["efor_gizle"] and not one else "")
adaylar.append({"slug": slug, "ad": m.get("display_name") or slug, "kind": bilgiler.get(slug, {}).get("kind") or "model",
"medya": medya, "efor": efor, "neden": neden})
if not neden:
kalan.append(m)
kalan.sort(key=lambda m: (0, one_sira[m.get("slug", "")]) if m.get("slug", "") in one_sira else (1, 0))
ek_modeller = kalan
taban = max([m.get("priority") or 0 for m in ozgunler], default=-1) + 1
for n, model in enumerate(ek_modeller):
model["priority"] = taban + n
@@ -234,6 +288,14 @@ def sirala_codex(modeller, ekler, ozgun_kimlikler=()):
if yalin(ad) in ozgun_adlar:
model["display_name"] = f"{ad}{AITURK_EKI}"
modeller[:] = (ozgunler + ek_modeller)[:EN_COK_MODEL]
if adaylar is None:
return None
# Secicide gorunecek yer: ozgun modeller once sayilir; Codex ilk 100 girdiyi gosterir.
yer = {m.get("slug", ""): i for i, m in enumerate(modeller)}
for a in adaylar:
a["sira"] = yer.get(a["slug"]) if not a["neden"] else None
a["menude"] = a["sira"] is not None and a["sira"] < CODEX_SECICI_SINIRI
return {"sinir": CODEX_SECICI_SINIRI, "ozgun_sayisi": len(ozgunler), "adaylar": adaylar}
# ---- Secicide model adinin altindaki satir ----
+37 -1
View File
@@ -442,6 +442,42 @@ def hat_ayari():
except Exception:
return CFG.get("aiturk_hat")
_LISTE_AYAR = {"mtime": None, "deger": None}
def liste_ayari():
"""bridge.json "codex_liste": Codex secicisinde gorunecek modeller (ayarlar ekrani yazar, yeniden baslatmadan uygulanir)."""
yol = os.path.join(HOME, "bridge.json")
try:
mt = os.path.getmtime(yol)
if mt != _LISTE_AYAR["mtime"]:
with open(yol, encoding="utf-8") as f:
_LISTE_AYAR["deger"] = (json.load(f) or {}).get("codex_liste")
_LISTE_AYAR["mtime"] = mt
return _LISTE_AYAR["deger"] or {}
except Exception:
return CFG.get("codex_liste") or {}
_ADAY_IZ = {"iz": None}
def aday_kaydet(sonuc):
"""Ayarlar ekrani icin aday listesi (log/son-liste/codex-adaylar.json); icerik degismediyse yazilmaz."""
if not sonuc:
return
try:
govde = json.dumps(sonuc, ensure_ascii=False, sort_keys=True)
iz = hashlib.sha256(govde.encode("utf-8")).hexdigest()
if iz == _ADAY_IZ["iz"]:
return
os.makedirs(CACHE_DIR, exist_ok=True)
yol = os.path.join(CACHE_DIR, "codex-adaylar.json")
gecici = "%s.%d.tmp" % (yol, threading.get_ident())
with open(gecici, "w", encoding="utf-8") as f:
json.dump({**sonuc, "at": time.strftime("%Y-%m-%d %H:%M:%S")}, f, ensure_ascii=False)
os.replace(gecici, yol)
_ADAY_IZ["iz"] = iz
except Exception as e:
log(f"Codex aday listesi yazilamadi: {e}")
LAST_MODELS = {} # hesap kimligi -> son basarili model listesi (chatgpt.com hata verirse bu verilir)
CACHE_DIR = os.path.join(LOGDIR, "son-liste")
@@ -1569,7 +1605,7 @@ class H(http.server.BaseHTTPRequestHandler):
m.update(saved)
models.append(m); changed += 1
if ekler:
ide_catalog.sirala_codex(models, ekler, ozgun_kimlikler)
aday_kaydet(ide_catalog.sirala_codex(models, ekler, ozgun_kimlikler, liste_ayari()))
out = json.dumps(data, ensure_ascii=False).encode("utf-8")
except Exception as e:
log(f"{tag} model listesi degistirilemedi: {e}")
@@ -0,0 +1,83 @@
"""Codex secicisinde gorunen liste: kullanicinin one aldigi, gizledigi modeller ve 100'luk secici siniri."""
import unittest
import ide_catalog as c
def liste(*slugs, ozgun=("gpt-6-sol", "gpt-6-astra")):
models = [{"slug": s, "display_name": s, "priority": i} for i, s in enumerate(ozgun)]
models += [{"slug": "ide/" + s, "display_name": s, "priority": 0} for s in slugs]
extras = [{"slug": "ide/" + s, "kind": "combo" if s.startswith("Takim") else "model"} for s in slugs]
return models, extras
class CodexListe(unittest.TestCase):
def test_no_setting_keeps_the_old_behaviour(self):
models, extras = liste("agnes-agnes-2.0-flash", "agnes-agnes-2.0-flash-high", "gemini-gemini-2.5-flash-preview-tts")
self.assertIsNone(c.sirala_codex(models, extras))
self.assertEqual(len(models), 5)
def test_default_hides_media_and_effort_copies_whose_base_is_listed(self):
models, extras = liste("agnes-agnes-2.0-flash", "agnes-agnes-2.0-flash-high", "agy-gemini-3.8-flash-high",
"gemini-gemini-2.5-flash-preview-tts", "gemini-gemini-3.1-flash-image", "gemini-aqa",
"claude-claude-sonnet-5-5")
sonuc = c.sirala_codex(models, extras, (), {})
slugs = [m["slug"] for m in models]
self.assertNotIn("ide/agnes-agnes-2.0-flash-high", slugs) # ana model listede: kopya gizlenir
self.assertIn("ide/agy-gemini-3.8-flash-high", slugs) # ana modeli yok: kalir
self.assertFalse([s for s in slugs if "tts" in s or "image" in s or "aqa" in s])
nedenler = {a["slug"]: a["neden"] for a in sonuc["adaylar"]}
self.assertEqual(nedenler["ide/agnes-agnes-2.0-flash-high"], "efor")
self.assertEqual(nedenler["ide/gemini-aqa"], "medya")
self.assertEqual(nedenler["ide/claude-claude-sonnet-5-5"], "")
self.assertEqual(sonuc["ozgun_sayisi"], 2)
def test_switches_off_show_everything(self):
models, extras = liste("agnes-agnes-2.0-flash", "agnes-agnes-2.0-flash-high", "gemini-gemini-2.5-flash-preview-tts")
c.sirala_codex(models, extras, (), {"medya_gizle": False, "efor_gizle": False})
self.assertEqual(len(models), 5)
def test_pinned_models_follow_the_subscription_models_in_the_given_order(self):
models, extras = liste("agnes-a", "claude-claude-opus-5-5", "Takim-Hizli", "nanogpt-glm-uncensored", "zeta-x")
originals = [dict(m) for m in models if not m["slug"].startswith("ide/")]
sonuc = c.sirala_codex(models, extras, (), {"one_al": ["ide/nanogpt-glm-uncensored", "ide/claude-claude-opus-5-5", "ide/yok"]})
self.assertEqual([m["slug"] for m in models[:4]],
["gpt-6-sol", "gpt-6-astra", "ide/nanogpt-glm-uncensored", "ide/claude-claude-opus-5-5"])
self.assertEqual([m for m in models if not m["slug"].startswith("ide/")], originals)
self.assertEqual([m["priority"] for m in models[2:]], list(range(2, 2 + len(models) - 2)))
sira = {a["slug"]: a["sira"] for a in sonuc["adaylar"]}
self.assertEqual(sira["ide/nanogpt-glm-uncensored"], 2)
def test_hidden_models_leave_and_a_pin_overrides_the_automatic_filters(self):
models, extras = liste("agnes-a", "agnes-a-high", "gemini-veo-3", "claude-x")
c.sirala_codex(models, extras, (), {"gizli": ["ide/claude-x"], "one_al": ["ide/agnes-a-high", "ide/gemini-veo-3"]})
slugs = [m["slug"] for m in models]
self.assertNotIn("ide/claude-x", slugs)
self.assertEqual(slugs[2:4], ["ide/agnes-a-high", "ide/gemini-veo-3"])
def test_menu_limit_counts_the_subscription_models_first(self):
slugs = ["model-%03d" % i for i in range(120)]
models, extras = liste(*slugs)
sonuc = c.sirala_codex(models, extras, (), {"one_al": ["ide/model-119"]})
aday = {a["slug"]: a for a in sonuc["adaylar"]}
self.assertTrue(aday["ide/model-119"]["menude"]) # one alindi: 3. sirada
self.assertEqual(aday["ide/model-119"]["sira"], 2)
menude = [a for a in sonuc["adaylar"] if a["menude"]]
self.assertEqual(len(menude), 100 - 2) # 2 ozgun + 98 ek model = 100
self.assertFalse(aday["ide/model-118"]["menude"])
def test_broken_setting_falls_back_to_defaults(self):
self.assertEqual(c.liste_ayari("bozuk"), {"medya_gizle": True, "efor_gizle": True, "one_al": [], "gizli": []})
self.assertEqual(c.liste_ayari({"one_al": ["a", "a", 3, " "], "gizli": "x"})["one_al"], ["a"])
def test_media_detection_does_not_catch_chat_models(self):
for sohbet in ("ide/claude-claude-opus-5-5", "ide/agnes-agnes-2.5-flash", "ide/gemini-gemini-3.8-flash",
"ide/nanogpt-z-ai-glm-5.3-flash-uncensored", "ide/bekci-sesli-bekci", "ide/Chat-Model"):
self.assertFalse(c.medya_mi(sohbet), sohbet)
for medya in ("ide/agnes-agnes-video-2.5", "ide/gemini-lyria-3-clip-preview", "ide/gemini-gemini-embedding-001",
"ide/gemini-gemini-3.5-transcribe", "ide/gemini-nano-banana-pro-preview", "ide/agnes-agnes-image-2.5-flash"):
self.assertTrue(c.medya_mi(medya), medya)
if __name__ == "__main__":
unittest.main()
@@ -457,6 +457,7 @@ class Liste(unittest.TestCase):
env = {"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "copy": copy, "hashlib": hashlib, "time": time, "log": lambda m: None, "CFG": {},
"decompress": lambda raw, enc: raw, "EXTRA_MODELS": [], "IDE_PREFIX": "ide/", "SLIM_CLONES": False,
"SLIM_FIELDS": [], "ide_catalog": ide_catalog, "LAST_MODELS": {}, "save_last_models": Mock(),
"liste_ayari": lambda: {}, "aday_kaydet": Mock(),
"RESP_HOP": {"content-length", "content-encoding"}, "REQ_HOP": {"host"}, "CHATGPT": "https://example.invalid",
"UPSTREAM_IDLE_TIMEOUT": 1,
"urllib": types.SimpleNamespace(request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=Mock()),
+6 -3
View File
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '20'
export const CODEX_BRIDGE_VERSION = '21'
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'
@@ -359,8 +359,11 @@ export function serializeCodexBridgeConfig(previous?: null | string): string {
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
// The user's own choices from Settings → Codex Bağlama survive the IDE rewriting the file.
for (const key of ['aiturk_hat', 'codex_liste']) {
if (old && typeof old === 'object' && old[key] && typeof old[key] === 'object') {
config[key] = old[key]
}
}
} catch {
// An unreadable file has nothing worth keeping.
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest'
import { serializeCodexBridgeConfig } from './codex-link-config'
import { onizle, readAdaylar, readCodexListe, writeCodexListe } from './codex-liste-settings'
const aday = (slug: string, extra: Record<string, unknown> = {}) => ({ slug, ad: slug, kind: 'model', medya: false, efor: false, neden: '', sira: null, menude: false, ...extra })
describe('Codex model picker list setting', () => {
it('defaults to hiding media models and effort copies, with nothing pinned or hidden', () => {
expect(readCodexListe(null)).toEqual({ medya_gizle: true, efor_gizle: true, one_al: [], gizli: [] })
expect(readCodexListe('bozuk').medya_gizle).toBe(true)
expect(readCodexListe('{"codex_liste":{"efor_gizle":false,"one_al":["a","a",3]}}')).toEqual({ medya_gizle: true, efor_gizle: false, one_al: ['a'], gizli: [] })
})
it('writes only codex_liste, keeps the rest of bridge.json, and a pin wins over hiding', () => {
const raw = '{"port":8795,"aiturk_hat":{"secim":"onerilen"}}'
const out = JSON.parse(writeCodexListe(raw, { medya_gizle: false, efor_gizle: true, one_al: ['x'], gizli: ['x', 'y'] }))
expect(out).toEqual({ port: 8795, aiturk_hat: { secim: 'onerilen' }, codex_liste: { medya_gizle: false, efor_gizle: true, one_al: ['x'], gizli: ['y'] } })
expect(() => writeCodexListe('{bozuk', { one_al: [], gizli: [] })).toThrow()
expect(() => writeCodexListe(null, { one_al: 'x', gizli: [] })).toThrow()
})
it('reads the candidate list the bridge wrote and survives a missing or broken file', () => {
expect(readAdaylar(null)).toEqual({ adaylar: [], ozgunSayisi: 0, sinir: 100, at: null })
const st = readAdaylar(JSON.stringify({ sinir: 100, ozgun_sayisi: 9, at: 't', adaylar: [aday('ide/a', { neden: 'medya', medya: true }), { yanlis: 1 }] }))
expect(st.ozgunSayisi).toBe(9)
expect(st.adaylar).toHaveLength(1)
expect(st.adaylar[0].neden).toBe('medya')
})
it('previews the menu like the bridge: pins first, filters, and the 100 limit counts subscription models', () => {
const adaylar = [aday('ide/a'), aday('ide/b', { efor: true }), aday('ide/c', { medya: true }), ...Array.from({ length: 100 }, (_, i) => aday(`ide/m${i}`))]
const sonuc = onizle({ adaylar, ozgunSayisi: 2, sinir: 100 }, { medya_gizle: true, efor_gizle: true, one_al: ['ide/m99', 'ide/c'], gizli: ['ide/a'] })
const bul = (s: string) => sonuc.find(a => a.slug === s)!
expect(bul('ide/m99')).toMatchObject({ sira: 2, menude: true, neden: '' })
expect(bul('ide/c')).toMatchObject({ sira: 3, neden: '' }) // pinned media model is shown
expect(bul('ide/a').neden).toBe('gizli')
expect(bul('ide/b').neden).toBe('efor')
expect(sonuc.filter(a => a.menude)).toHaveLength(98)
expect(bul('ide/m98').menude).toBe(false)
})
it('survives the IDE rewriting bridge.json together with the work routing choice', () => {
const previous = JSON.stringify({ port: 1, aiturk_hat: { secim: 'kapali' }, codex_liste: { medya_gizle: false, efor_gizle: true, one_al: ['ide/x'], gizli: [] } })
const out = JSON.parse(serializeCodexBridgeConfig(previous))
expect(out.aiturk_hat).toEqual({ secim: 'kapali' })
expect(out.codex_liste).toEqual({ medya_gizle: false, efor_gizle: true, one_al: ['ide/x'], gizli: [] })
expect(JSON.parse(serializeCodexBridgeConfig('bozuk')).codex_liste).toBeUndefined()
})
})
@@ -0,0 +1,115 @@
import fs from 'node:fs'
import path from 'node:path'
import { type CodexAday, CODEX_LISTE_VARSAYILAN, CODEX_SECICI_SINIRI, type CodexListeSettings, type CodexListeState } from './codex-liste-types'
export type { CodexAday, CodexListeSettings, CodexListeState } from './codex-liste-types'
export { onizle } from './codex-liste-types'
const SINIR = CODEX_SECICI_SINIRI
const MAX_SLUGS = 2000
const VARSAYILAN: CodexListeSettings = CODEX_LISTE_VARSAYILAN
function slugs(value: unknown): string[] {
if (!Array.isArray(value)) return []
return [...new Set(value.filter((s): s is string => typeof s === 'string' && s.trim() !== ''))].slice(0, MAX_SLUGS)
}
export function readCodexListe(raw: null | string): CodexListeSettings {
try {
const liste = raw ? JSON.parse(raw)?.codex_liste : null
if (!liste || typeof liste !== 'object') return { ...VARSAYILAN, one_al: [], gizli: [] }
return {
medya_gizle: liste.medya_gizle !== false,
efor_gizle: liste.efor_gizle !== false,
one_al: slugs(liste.one_al),
gizli: slugs(liste.gizli)
}
} catch {
return { ...VARSAYILAN, one_al: [], gizli: [] }
}
}
export function writeCodexListe(raw: null | string, value: unknown): string {
const v = value as Partial<CodexListeSettings> | null
if (!v || typeof v !== 'object') throw new Error('Invalid setting')
if (!Array.isArray(v.one_al) || !Array.isArray(v.gizli)) throw new Error('Invalid model list')
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 liste: CodexListeSettings = {
medya_gizle: v.medya_gizle !== false,
efor_gizle: v.efor_gizle !== false,
one_al: slugs(v.one_al),
gizli: slugs(v.gizli).filter(s => !slugs(v.one_al).includes(s))
}
return `${JSON.stringify({ ...config, codex_liste: liste }, null, 2)}\n`
}
export function readAdaylar(raw: null | string): Omit<CodexListeState, 'ayar'> {
const bos = { adaylar: [], ozgunSayisi: 0, sinir: SINIR, at: null }
try {
const veri = raw ? JSON.parse(raw) : null
if (!veri || !Array.isArray(veri.adaylar)) return bos
const adaylar: CodexAday[] = veri.adaylar
.filter((a: unknown): a is Record<string, unknown> => Boolean(a) && typeof (a as { slug?: unknown }).slug === 'string')
.map((a: Record<string, unknown>) => ({
slug: String(a.slug),
ad: typeof a.ad === 'string' ? a.ad : String(a.slug),
kind: typeof a.kind === 'string' ? a.kind : 'model',
medya: a.medya === true,
efor: a.efor === true,
neden: (['efor', 'gizli', 'medya'].includes(String(a.neden)) ? a.neden : '') as CodexAday['neden'],
sira: typeof a.sira === 'number' ? a.sira : null,
menude: a.menude === true
}))
return {
adaylar,
ozgunSayisi: typeof veri.ozgun_sayisi === 'number' ? veri.ozgun_sayisi : 0,
sinir: typeof veri.sinir === 'number' ? veri.sinir : SINIR,
at: typeof veri.at === 'string' ? veri.at : null
}
} catch {
return bos
}
}
export function createCodexListeStore(directory: string) {
const file = path.join(directory, 'bridge.json')
const adayFile = path.join(directory, 'log', 'son-liste', 'codex-adaylar.json')
const read = (f: string) => (fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : null)
const state = (raw: null | string): CodexListeState => ({ ayar: readCodexListe(raw), ...readAdaylar(read(adayFile)) })
return {
get: (): CodexListeState => state(read(file)),
save(value: unknown): CodexListeState {
const text = writeCodexListe(read(file), 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 state(text)
}
}
}
@@ -0,0 +1,62 @@
/** Pure part of the Codex model-list setting (no Node imports), shared by the main process and the
* settings screen. */
/** Which added models Codex's model picker shows. The Codex desktop app asks for one page of at most
* 100 models and never asks for the next page, so the user chooses which models fill it. Stored in the
* Codex bridge's `bridge.json` under `codex_liste`; the bridge re-reads it when the file changes and writes
* the candidate list it built to `log/son-liste/codex-adaylar.json`. */
export interface CodexListeSettings {
medya_gizle: boolean
efor_gizle: boolean
one_al: string[]
gizli: string[]
}
export interface CodexAday {
slug: string
ad: string
kind: string
medya: boolean
efor: boolean
neden: '' | 'efor' | 'gizli' | 'medya'
sira: null | number
menude: boolean
}
export interface CodexListeState {
ayar: CodexListeSettings
adaylar: CodexAday[]
ozgunSayisi: number
sinir: number
at: null | string
}
export const CODEX_SECICI_SINIRI = 100
export const CODEX_LISTE_VARSAYILAN: CodexListeSettings = { medya_gizle: true, efor_gizle: true, one_al: [], gizli: [] }
/** The menu the setting produces, computed the way the bridge does it, so the screen can show the effect
* before the bridge rebuilds the list. Candidates arrive in the bridge's base order. */
export function onizle(state: Pick<CodexListeState, 'adaylar' | 'ozgunSayisi' | 'sinir'>, ayar: CodexListeSettings) {
const one = ayar.one_al.filter(s => state.adaylar.some(a => a.slug === s))
const gizli = new Set(ayar.gizli)
const neden = (a: CodexAday): CodexAday['neden'] => {
if (gizli.has(a.slug)) return 'gizli'
if (one.includes(a.slug)) return ''
if (a.medya && ayar.medya_gizle) return 'medya'
if (a.efor && ayar.efor_gizle) return 'efor'
return ''
}
const kalan = state.adaylar.filter(a => !neden(a))
const sirali = [...one.map(s => kalan.find(a => a.slug === s)).filter((a): a is CodexAday => Boolean(a)), ...kalan.filter(a => !one.includes(a.slug))]
const yer = new Map(sirali.map((a, i) => [a.slug, state.ozgunSayisi + i]))
return state.adaylar.map(a => {
const n = neden(a)
const sira = n ? null : (yer.get(a.slug) ?? null)
return { ...a, neden: n, sira, menude: sira !== null && sira < state.sinir }
})
}
+12
View File
@@ -380,6 +380,7 @@ import { collectSshConfigHosts, parseSshGOutput } from './ssh-config'
import { createSshProbeConnection, pickLocalPort, redactSecrets, SshConnection } from './ssh-connection'
import { createStagedTransitionStore } from './staged-transition'
import { createCompressionStore } from './compression-settings'
import { createCodexListeStore } from './codex-liste-settings'
import { createIsHattiStore } from './is-hatti-settings'
import { createStreamThrottle } from './stream-throttle'
import { registerTerminalIpc } from './terminal-ipc'
@@ -17197,6 +17198,7 @@ ipcMain.handle('hermes:quick-entry:settings:set', async (_event, patch) => {
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 codexListeStore = createCodexListeStore(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'),
@@ -17319,6 +17321,16 @@ ipcMain.handle('aiturk:is-hatti:set', async (event, settings) => {
return isHattiStore.save(settings)
})
ipcMain.handle('aiturk:codex-liste:get', async event => {
assertCodexLinkSender(event)
return codexListeStore.get()
})
ipcMain.handle('aiturk:codex-liste:set', async (event, settings) => {
assertCodexLinkSender(event)
return codexListeStore.save(settings)
})
ipcMain.handle('aiturk:staged-transition:set', async (event, patch) => {
assertCodexLinkSender(event)
stagedTransition.save(patch)
+2
View File
@@ -299,6 +299,8 @@ contextBridge.exposeInMainWorld('hermesDesktop', {
setCompression: settings => ipcRenderer.invoke('aiturk:compression:set', settings),
getIsHatti: () => ipcRenderer.invoke('aiturk:is-hatti:get'),
setIsHatti: settings => ipcRenderer.invoke('aiturk:is-hatti:set', settings),
getCodexListe: () => ipcRenderer.invoke('aiturk:codex-liste:get'),
setCodexListe: settings => ipcRenderer.invoke('aiturk:codex-liste:set', settings),
getStagedTransition: () => ipcRenderer.invoke('aiturk:staged-transition:get'),
setStagedTransition: patch => ipcRenderer.invoke('aiturk:staged-transition:set', patch),
get: () => ipcRenderer.invoke('aiturk:codex-link:get'),
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.40",
"version": "1.0.0-beta.41",
"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 { CodexListeSettingsView } from './codex-liste-settings'
import { IsHattiSettingsView } from './is-hatti-settings'
type Tone = 'muted' | 'ok' | 'warn'
@@ -253,6 +254,7 @@ export function CodexLinkSettings() {
</p>
</SettingsSection>
<IsHattiSettingsView />
<CodexListeSettingsView />
<CompressionSettingsView />
</SettingsContent>
)
@@ -0,0 +1,48 @@
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/i18n/context'
import { CodexListeSettingsView } from './codex-liste-settings'
afterEach(() => {
cleanup()
delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop
})
const aday = (slug: string, extra: Record<string, unknown> = {}) => ({ slug, ad: slug.replace('ide/', ''), kind: 'model', medya: false, efor: false, neden: '', sira: null, menude: false, ...extra })
it('counts the menu with the subscription models, pins and hides, and saves the choice', async () => {
const adaylar = [aday('ide/agnes'), aday('ide/agnes-high', { efor: true }), aday('ide/veo', { medya: true }), ...Array.from({ length: 99 }, (_, i) => aday(`ide/m${String(i).padStart(2, '0')}`))]
const save = vi.fn(async (ayar: unknown) => ({ ayar, adaylar, ozgunSayisi: 2, sinir: 100, at: 't' }))
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
codexLink: {
getCodexListe: async () => ({ ayar: { medya_gizle: true, efor_gizle: true, one_al: [], gizli: [] }, adaylar, ozgunSayisi: 2, sinir: 100, at: 't' }),
setCodexListe: save
}
}
render(<I18nProvider configClient={null} initialLocale="tr"><CodexListeSettingsView /></I18nProvider>)
// 2 subscription + 98 of the 100 shown candidates fit; agnes-high and veo are hidden.
expect(await screen.findByText('Menüde 100 / 100 · menüye sığmayan 2 · gizli 2')).toBeTruthy()
fireEvent.change(screen.getByRole('textbox', { name: 'Model ara' }), { target: { value: 'm98' } })
const satir = screen.getByText('m98').closest('li')!
expect(within(satir).getByText('Menüye sığmıyor')).toBeTruthy()
fireEvent.click(within(satir).getByRole('button', { name: 'Öne al' }))
expect(within(satir).getByText('Menüde, 3. sıra')).toBeTruthy()
fireEvent.change(screen.getByRole('textbox', { name: 'Model ara' }), { target: { value: 'agnes' } })
fireEvent.click(within(screen.getByText('agnes').closest('li')!).getByRole('button', { name: 'Gizle' }))
fireEvent.click(screen.getByRole('switch', { name: 'Düşük/orta/yüksek kopyalarını gizle' }))
fireEvent.click(screen.getByRole('button', { name: 'Kaydet' }))
await waitFor(() => expect(save).toHaveBeenCalledWith({ medya_gizle: true, efor_gizle: false, one_al: ['ide/m98'], gizli: ['ide/agnes'] }))
expect(await screen.findByText(/Codex model listesini birkaç dakika içinde yeniler/)).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Varsayılana dön' }))
await waitFor(() => expect(save).toHaveBeenLastCalledWith({ medya_gizle: true, efor_gizle: true, one_al: [], gizli: [] }))
})
it('explains when the bridge has not built the list yet', async () => {
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
codexLink: { getCodexListe: async () => ({ ayar: { medya_gizle: true, efor_gizle: true, one_al: [], gizli: [] }, adaylar: [], ozgunSayisi: 0, sinir: 100, at: null }), setCodexListe: vi.fn() }
}
render(<I18nProvider configClient={null} initialLocale="tr"><CodexListeSettingsView /></I18nProvider>)
expect(await screen.findByText(/Model listesi henüz oluşmadı/)).toBeTruthy()
})
@@ -0,0 +1,209 @@
import { useEffect, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { useI18n } from '@/i18n/context'
import { Link } from '@/lib/icons'
import {
CODEX_LISTE_VARSAYILAN,
type CodexAday,
type CodexListeSettings,
type CodexListeState,
onizle
} from '../../../electron/codex-liste-types'
import { ListRow, SettingsSection } from './primitives'
type Filtre = 'hepsi' | 'menude' | 'disarida' | 'gizli'
/** Which added models fill the Codex model picker. The Codex desktop app shows at most 100 models (its own
* fixed limit); the user pins, hides and filters to choose which ones make it. */
export function CodexListeSettingsView() {
const api = window.hermesDesktop?.codexLink
const { locale } = useI18n()
const tr = locale === 'tr'
const t = (a: string, b: string) => (tr ? a : b)
const [state, setState] = useState<CodexListeState | null>(null)
const [draft, setDraft] = useState<CodexListeSettings | null>(null)
const [arama, setArama] = useState('')
const [filtre, setFiltre] = useState<Filtre>('hepsi')
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const [failed, setFailed] = useState(false)
useEffect(() => {
let active = true
api?.getCodexListe?.().then(
next => {
if (active) {
setState(next)
setDraft(next.ayar)
}
},
() => {
if (active) {
setFailed(true)
setMessage(tr ? 'Codex model listesi ayarı okunamadı.' : 'Could not load the Codex model list setting.')
}
}
)
return () => {
active = false
}
}, [api, tr])
const onizleme = useMemo(() => (state && draft ? onizle(state, draft) : []), [state, draft])
if (!api?.getCodexListe || !api.setCodexListe) return null
const menude = onizleme.filter(a => a.menude).length
const disarida = onizleme.filter(a => !a.neden && !a.menude).length
const gizlenen = onizleme.filter(a => a.neden).length
const sinir = state?.sinir ?? 100
const ozgun = state?.ozgunSayisi ?? 0
const sirali = [...onizleme].sort((a, b) => (a.sira ?? 1e9) - (b.sira ?? 1e9))
const aranan = arama.trim().toLocaleLowerCase(locale)
const gorunen = sirali.filter(a => {
if (aranan && !`${a.ad} ${a.slug}`.toLocaleLowerCase(locale).includes(aranan)) return false
if (filtre === 'menude') return a.menude
if (filtre === 'disarida') return !a.neden && !a.menude
if (filtre === 'gizli') return Boolean(a.neden)
return true
})
const degistir = (patch: Partial<CodexListeSettings>) => draft && setDraft({ ...draft, ...patch })
const oneAl = (slug: string) => {
if (!draft) return
const pinned = draft.one_al.includes(slug)
degistir({ one_al: pinned ? draft.one_al.filter(s => s !== slug) : [...draft.one_al, slug], gizli: draft.gizli.filter(s => s !== slug) })
}
const yukari = (slug: string) => {
if (!draft) return
const i = draft.one_al.indexOf(slug)
if (i <= 0) return
const next = [...draft.one_al]
;[next[i - 1], next[i]] = [next[i], next[i - 1]]
degistir({ one_al: next })
}
const gizle = (slug: string) => {
if (!draft) return
const hidden = draft.gizli.includes(slug)
degistir({ gizli: hidden ? draft.gizli.filter(s => s !== slug) : [...draft.gizli, slug], one_al: draft.one_al.filter(s => s !== slug) })
}
const save = async (next: CodexListeSettings) => {
setBusy(true)
setMessage('')
setFailed(false)
try {
const saved = await api.setCodexListe!(next)
setState({ ...saved, adaylar: state?.adaylar ?? saved.adaylar })
setDraft(saved.ayar)
setMessage(t('Kaydedildi. Codex model listesini birkaç dakika içinde yeniler (en geç 5 dakika sonra Codex penceresine dönünce).', 'Saved. Codex refreshes its model list within a few minutes (at the latest when you return to its window after 5 minutes).'))
} catch {
setFailed(true)
setMessage(t('Kaydedilemedi.', 'Could not save.'))
} finally {
setBusy(false)
}
}
const durum = (a: CodexAday) => {
if (a.neden === 'gizli') return t('Gizli', 'Hidden')
if (a.neden === 'medya') return t('Resim/ses modeli, gizli', 'Image/audio model, hidden')
if (a.neden === 'efor') return t('Düşük/orta/yüksek kopyası, gizli', 'Effort copy, hidden')
if (a.menude) return t(`Menüde, ${(a.sira ?? 0) + 1}. sıra`, `In the menu, position ${(a.sira ?? 0) + 1}`)
return t('Menüye sığmıyor', 'Does not fit in the menu')
}
return (
<SettingsSection icon={Link} title={t('Codex’te görünecek modeller', 'Models shown in Codex')}>
<p className="text-sm text-(--ui-text-secondary)">
{t(
`Codex’in model menüsü en çok ${sinir} model gösterir; bu sınır Codex uygulamasının kendi sınırıdır. Codex’in kendi ${ozgun} modeli her zaman başta durur. Kalan yerlere hangi modellerin gireceğini burada seçin: öne aldıklarınız başa gelir, gizledikleriniz çıkar. Menüde olmayan bir modeli Codex’e “modeli … yap” diyerek yine seçebilirsiniz.`,
`The Codex model menu shows at most ${sinir} models; this is the Codex app’s own limit. Codex’s own ${ozgun} models always come first. Choose which models fill the rest: pinned ones come first, hidden ones are removed. You can still pick a model that is not in the menu by asking Codex to switch to it.`
)}
</p>
{state && draft && (
<>
<p className="text-sm font-medium" role="status">
{t(`Menüde ${ozgun + menude} / ${sinir} · menüye sığmayan ${disarida} · gizli ${gizlenen}`, `In the menu ${ozgun + menude} / ${sinir} · not fitting ${disarida} · hidden ${gizlenen}`)}
</p>
<ListRow
action={<Switch aria-label={t('Resim, ses ve video modellerini gizle', 'Hide image, audio and video models')} checked={draft.medya_gizle} disabled={busy} onCheckedChange={v => degistir({ medya_gizle: v })} />}
description={t('Bunlar Codex sohbetinde çalışmaz.', 'These do not work in a Codex chat.')}
title={t('Resim, ses ve video modellerini gizle', 'Hide image, audio and video models')}
/>
<ListRow
action={<Switch aria-label={t('Düşük/orta/yüksek kopyalarını gizle', 'Hide low/medium/high copies')} checked={draft.efor_gizle} disabled={busy} onCheckedChange={v => degistir({ efor_gizle: v })} />}
description={t('Ana modeli listede olan “-low”, “-high” gibi kopyalar gizlenir; düşünme seviyesini Codex’in kendi seçicisinden seçersiniz.', 'Copies such as “-low” or “-high” whose base model is listed are hidden; pick the reasoning level in Codex’s own selector.')}
title={t('Düşük/orta/yüksek kopyalarını gizle', 'Hide low/medium/high copies')}
/>
{state.adaylar.length === 0 ? (
<p className="text-sm text-(--ui-text-secondary)">
{t('Model listesi henüz oluşmadı. Codex’i açıp model menüsüne bir kez bakın, sonra bu sayfayı yeniden açın.', 'The model list has not been built yet. Open the Codex model menu once, then reopen this page.')}
</p>
) : (
<>
<div className="flex flex-wrap gap-2">
<Input aria-label={t('Model ara', 'Search models')} className="min-h-11 min-w-0 flex-1" onChange={e => setArama(e.target.value)} placeholder={t('Model ara', 'Search models')} value={arama} />
<select aria-label={t('Göster', 'Show')} className="min-h-11 rounded-md border bg-(--ui-bg) p-2" onChange={e => setFiltre(e.target.value as Filtre)} value={filtre}>
<option value="hepsi">{t('Hepsi', 'All')}</option>
<option value="menude">{t('Menüde olanlar', 'In the menu')}</option>
<option value="disarida">{t('Menüye sığmayanlar', 'Not fitting')}</option>
<option value="gizli">{t('Gizlenenler', 'Hidden')}</option>
</select>
</div>
<ul aria-label={t('Modeller', 'Models')} className="max-h-[32rem] divide-y overflow-y-auto rounded-md border">
{gorunen.map(a => {
const pinned = draft.one_al.includes(a.slug)
const hidden = draft.gizli.includes(a.slug)
return (
<li className="flex flex-wrap items-center gap-2 p-2" key={a.slug}>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{a.ad}</div>
<div className="truncate text-xs text-(--ui-text-tertiary)">{durum(a)}</div>
</div>
{pinned && draft.one_al.indexOf(a.slug) > 0 && (
<Button aria-label={t(`${a.ad} bir yukarı`, `Move ${a.ad} up`)} className="min-h-11" disabled={busy} onClick={() => yukari(a.slug)} size="sm" variant="ghost">
↑
</Button>
)}
<Button aria-pressed={pinned} className="min-h-11" disabled={busy} onClick={() => oneAl(a.slug)} size="sm" variant={pinned ? 'default' : 'secondary'}>
{pinned ? t('Öne alındı', 'Pinned') : t('Öne al', 'Pin')}
</Button>
<Button aria-pressed={hidden} className="min-h-11" disabled={busy} onClick={() => gizle(a.slug)} size="sm" variant="ghost">
{hidden ? t('Göster', 'Show') : t('Gizle', 'Hide')}
</Button>
</li>
)
})}
</ul>
</>
)}
<div className="flex gap-2">
<Button disabled={busy} onClick={() => void save(draft)}>
{busy ? t('Kaydediliyor…', 'Saving…') : t('Kaydet', 'Save')}
</Button>
<Button disabled={busy} onClick={() => void save({ ...CODEX_LISTE_VARSAYILAN, one_al: [], gizli: [] })} variant="secondary">
{t('Varsayılana dön', 'Restore default')}
</Button>
</div>
</>
)}
{message && <p role={failed ? 'alert' : 'status'}>{message}</p>}
</SettingsSection>
)
}
+3
View File
@@ -3,6 +3,7 @@ import type { TranslucencyState } from '@hermes/shared/translucency'
import type { CodexLinkPatch, CodexLinkStatus } from '../electron/codex-link-types'
import type { CompressionSettings, CompressionState } from '../electron/compression-settings-types'
import type { CodexListeSettings, CodexListeState } from '../electron/codex-liste-settings'
import type { IsHattiSettings } from '../electron/is-hatti-settings'
import type { PoolLimits } from '../electron/pool-limits'
import type { StagedTransitionPatch, StagedTransitionState } from '../electron/staged-transition-types'
@@ -347,6 +348,8 @@ declare global {
setCompression: (settings: CompressionSettings) => Promise<CompressionState>
getIsHatti?: () => Promise<IsHattiSettings>
setIsHatti?: (settings: IsHattiSettings) => Promise<IsHattiSettings>
getCodexListe?: () => Promise<CodexListeState>
setCodexListe?: (settings: CodexListeSettings) => Promise<CodexListeState>
getStagedTransition: () => Promise<StagedTransitionState>
setStagedTransition: (patch: StagedTransitionPatch) => Promise<StagedTransitionState>
get: () => Promise<CodexLinkStatus>
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.40",
"version": "1.0.0-beta.41",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",