Compare commits

..
Author SHA1 Message Date
yilsem 8f53b090f7 feat(codex-link): persist direct subscription and AITURK connection modes
Server CI and Windows package / verify (push) Successful in 2m54s
Server CI and Windows package / windows-package (push) Successful in 7m17s
2026-10-13 19:56:40 +03:00
11 changed files with 188 additions and 17 deletions

No files matched your search

@@ -225,12 +225,17 @@ describe('preference file', () => {
})
it('writes the contract shape and keeps unknown fields', () => {
expect(JSON.parse(serializeCodexLinkSettings({}, false))).toEqual({ enabled: false, kapsam: 'ide' })
expect(JSON.parse(serializeCodexLinkSettings({}, false))).toEqual({
enabled: false,
mod: 'dogrudan',
kapsam: 'ide'
})
const { stored } = parseCodexLinkSettings('{"enabled": false, "kapsam": "hepsi", "gelecek": {"a": 1}}')
expect(JSON.parse(serializeCodexLinkSettings(stored, true))).toEqual({
enabled: true,
mod: 'dogrudan',
gelecek: { a: 1 },
kapsam: 'hepsi'
})
@@ -308,3 +313,17 @@ describe('bridge configuration and command', () => {
expect(codexAutostartPlan(`${deep}\\pythonw.exe`, launch, `${deep}\\baslat.pyw`)).toBeNull()
})
})
describe('mode preference', () => {
it('uses direct for missing, invalid and malformed modes', () => {
for (const raw of [null, '{', '{}', '{"mod":"bad"}', '{"mod":false}']) {
expect(parseCodexLinkSettings(raw).mod).toBe('dogrudan')
}
expect(parseCodexLinkSettings('{"mod":"aiturk"}').mod).toBe('aiturk')
})
it('preserves other fields when changing mode', () => {
const stored = { enabled: false, mod: 'aiturk', kapsam: 'hepsi', future: { a: 1 } }
expect(JSON.parse(serializeCodexLinkSettings(stored, false, 'dogrudan'))).toEqual({ ...stored, mod: 'dogrudan' })
expect(JSON.parse(serializeCodexLinkSettings(stored, true)).mod).toBe('aiturk')
})
})
+15 -6
View File
@@ -300,6 +300,7 @@ export function removeCodexLinkLine(source: string): CodexConfigEdit {
export interface CodexLinkSettings {
enabled: boolean
mod: 'dogrudan' | 'aiturk'
/** Every field of the stored object, so a rewrite keeps what it did not set. */
stored: Record<string, unknown>
}
@@ -307,26 +308,34 @@ export interface CodexLinkSettings {
/** Missing, unreadable or malformed means ON: the feature is on by default. */
export function parseCodexLinkSettings(raw: null | string | undefined): CodexLinkSettings {
if (typeof raw !== 'string') {
return { enabled: true, stored: {} }
return { enabled: true, mod: 'dogrudan', stored: {} }
}
try {
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return { enabled: true, stored: {} }
return { enabled: true, mod: 'dogrudan', stored: {} }
}
return { enabled: parsed.enabled !== false, stored: parsed as Record<string, unknown> }
return {
enabled: parsed.enabled !== false,
mod: parsed.mod === 'aiturk' ? 'aiturk' : 'dogrudan',
stored: parsed as Record<string, unknown>
}
} catch {
return { enabled: true, stored: {} }
return { enabled: true, mod: 'dogrudan', stored: {} }
}
}
export function serializeCodexLinkSettings(stored: Record<string, unknown>, enabled: boolean): string {
export function serializeCodexLinkSettings(
stored: Record<string, unknown>,
enabled: boolean,
mod: 'dogrudan' | 'aiturk' = stored.mod === 'aiturk' ? 'aiturk' : 'dogrudan'
): string {
const kapsam = typeof stored.kapsam === 'string' && stored.kapsam ? stored.kapsam : 'ide'
return `${JSON.stringify({ ...stored, enabled, kapsam }, null, 2)}\n`
return `${JSON.stringify({ ...stored, enabled, mod, kapsam }, null, 2)}\n`
}
// ── Bridge configuration ────────────────────────────────────────────────────
@@ -5,6 +5,8 @@
* code into its program. See codex-link.ts for what the fields mean in action.
*/
export type CodexLinkMode = 'dogrudan' | 'aiturk'
export type CodexLinkIdeState = 'anahtar-yok' | 'kapali' | 'tamam'
/** Stable codes; the renderer owns the wording. Detail goes to the desktop log. */
@@ -18,6 +20,7 @@ export type CodexLinkError =
export interface CodexLinkStatus {
enabled: boolean
mod: CodexLinkMode
/** Codex's `config.toml` exists on this machine. */
codexFound: boolean
/** The IDE's own Python environment exists (first-run setup finished). */
@@ -34,6 +37,7 @@ export interface CodexLinkStatus {
export interface CodexLinkPatch {
enabled?: boolean
mod?: CodexLinkMode
/** "Sync now": re-run the apply without changing the preference. */
sync?: boolean
}
+52 -3
View File
@@ -52,6 +52,7 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
events: [],
files: new Map([
[PATHS.codexConfig, USER_CONFIG],
[PATHS.settingsFile, JSON.stringify({ enabled: true, mod: 'aiturk' })],
[PATHS.bridgeScript, '# bridge'],
[PATHS.pythonw, ''],
[PATHS.python, '']
@@ -192,6 +193,7 @@ describe('applyCodexLink', () => {
expect(status).toEqual({
enabled: true,
mod: 'aiturk',
codexFound: true,
venvReady: true,
foreign: false,
@@ -328,7 +330,11 @@ describe('applyCodexLink', () => {
'autostart clear',
'stop'
])
expect(JSON.parse(world.files.get(PATHS.settingsFile) ?? '')).toEqual({ enabled: false, kapsam: 'ide' })
expect(JSON.parse(world.files.get(PATHS.settingsFile) ?? '')).toEqual({
enabled: false,
mod: 'aiturk',
kapsam: 'ide'
})
// Byte for byte what the user had before the feature touched it.
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(world.autostart).toBeNull()
@@ -344,7 +350,8 @@ describe('applyCodexLink', () => {
world.configWriteFails = true
expect(await link.apply({ enabled: false })).toMatchObject({
bridgeRunning: true, lastError: 'config-write-failed'
bridgeRunning: true,
lastError: 'config-write-failed'
})
expect(world.files.get(PATHS.codexConfig)).toBe(linkedConfig)
expect(world.autostart).toBe(autostart)
@@ -358,7 +365,7 @@ describe('applyCodexLink', () => {
it('stays off across applies and comes back when turned on', async () => {
const { link, world } = createWorld()
world.files.set(PATHS.settingsFile, '{"enabled": false, "kapsam": "ide", "not": "ours"}')
world.files.set(PATHS.settingsFile, '{"enabled": false, "mod": "aiturk", "kapsam": "ide", "not": "ours"}')
expect(await link.apply()).toMatchObject({ enabled: false, bridgeRunning: false })
expect(spawns(world)).toEqual([])
@@ -367,6 +374,7 @@ describe('applyCodexLink', () => {
expect(await link.apply({ enabled: true })).toMatchObject({ enabled: true, bridgeRunning: true })
expect(JSON.parse(world.files.get(PATHS.settingsFile) ?? '')).toEqual({
enabled: true,
mod: 'aiturk',
kapsam: 'ide',
not: 'ours'
})
@@ -424,3 +432,44 @@ describe('getStatus', () => {
expect(effects(world)).toEqual([])
})
})
describe('connection mode', () => {
it('defaults to direct, starts bridge and autostart without writing a line', async () => {
const { link, world } = createWorld()
world.files.delete(PATHS.settingsFile)
expect(await link.apply()).toMatchObject({ mod: 'dogrudan', bridgeRunning: true })
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(spawns(world)).toHaveLength(1)
expect(world.autostart).not.toBeNull()
})
it('removes the old line and preserves the current bridge in direct mode', async () => {
const { link, world } = createWorld({ listening: true })
world.files.set(PATHS.settingsFile, '{"mod":"dogrudan"}')
world.files.set(PATHS.codexConfig, `${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
await link.apply()
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(spawns(world)).toHaveLength(0)
expect(world.events).not.toContain('stop')
expect(world.autostart).not.toBeNull()
})
it('switches both ways, persists mode and preserves unrelated preferences', async () => {
const { link, world } = createWorld({ listening: true })
world.files.set(PATHS.settingsFile, '{"mod":"dogrudan","custom":{"keep":1}}')
await link.apply({ mod: 'aiturk' })
expect(world.files.get(PATHS.codexConfig)).toContain(CODEX_LINK_LINE)
await link.apply({ mod: 'dogrudan' })
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(JSON.parse(world.files.get(PATHS.settingsFile)!)).toMatchObject({ mod: 'dogrudan', custom: { keep: 1 } })
await link.apply()
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(world.events).not.toContain('stop')
})
it.each(['dogrudan', 'aiturk'] as const)('preserves foreign URL in %s', async mod => {
const { link, world } = createWorld()
const foreign = 'openai_base_url = "https://other.example"\n'
world.files.set(PATHS.codexConfig, foreign)
await link.apply({ mod })
expect(world.files.get(PATHS.codexConfig)).toBe(foreign)
expect(spawns(world)).toHaveLength(0)
})
})
+12 -3
View File
@@ -186,6 +186,7 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
bridge: BridgeProbe
): CodexLinkStatus => ({
enabled,
mod: readSettings().mod,
codexFound: config.found,
venvReady: interpreter() !== null,
foreign: config.owner === 'foreign',
@@ -329,7 +330,8 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
return bridge
}
const edit = addCodexLinkLine(config.text ?? '')
const edit =
readSettings().mod === 'aiturk' ? addCodexLinkLine(config.text ?? '') : removeCodexLinkLine(config.text ?? '')
if (edit.changed) {
try {
@@ -364,9 +366,16 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
const run = async (patch?: CodexLinkPatch): Promise<CodexLinkStatus> => {
lastError = null
if (typeof patch?.enabled === 'boolean') {
if (typeof patch?.enabled === 'boolean' || patch?.mod === 'aiturk' || patch?.mod === 'dogrudan') {
try {
deps.writeTextAtomic(paths.settingsFile, serializeCodexLinkSettings(readSettings().stored, patch.enabled))
deps.writeTextAtomic(
paths.settingsFile,
serializeCodexLinkSettings(
readSettings().stored,
patch?.enabled ?? readSettings().enabled,
patch?.mod ?? readSettings().mod
)
)
} catch (error) {
// The preference did not stick, so do not act on it either: what the
// machine does must match what the file says after a restart.
+1
View File
@@ -17318,6 +17318,7 @@ ipcMain.handle('aiturk:codex-link:set', async (event, patch) => {
return codexLink.apply({
enabled: typeof patch?.enabled === 'boolean' ? patch.enabled : undefined,
mod: patch?.mod === 'aiturk' || patch?.mod === 'dogrudan' ? patch.mod : undefined,
sync: patch?.sync === true
})
})
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.37",
"version": "1.0.0-beta.38",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
@@ -12,6 +12,7 @@ vi.mock('@/store/notifications', () => ({ notifyError }))
const RUNNING: CodexLinkStatus = {
enabled: true,
mod: 'aiturk',
codexFound: true,
venvReady: true,
foreign: false,
@@ -198,3 +199,18 @@ describe('Codex Link settings', () => {
expect(get).not.toHaveBeenCalled()
})
})
describe('connection mode controls', () => {
it('sends the mode and renders the saved direct preference', async () => {
get.mockResolvedValue({ ...RUNNING, mod: 'dogrudan' })
set.mockResolvedValue({ ...RUNNING, mod: 'aiturk' })
renderPage()
const direct = await screen.findByRole('radio', { name: 'Doğrudan (önerilen): abonelik modelleri köprüden bağımsız' })
await waitFor(() => expect((direct as HTMLInputElement).checked).toBe(true))
fireEvent.click(screen.getByRole('radio', { name: 'AITURK: combolar Codex listesinde, tüm modeller köprüden' }))
await waitFor(() => expect(set).toHaveBeenCalledWith({ mod: 'aiturk' }))
await waitFor(() => expect((direct as HTMLInputElement).checked).toBe(false))
expect(screen.getByText("Codex'i yeniden başlatın")).toBeTruthy()
})
})
@@ -57,7 +57,7 @@ export function codexLinkStatusLines(status: CodexLinkStatus, copy: CodexLinkCop
if (status.ideDurum === 'anahtar-yok') {
lines.push({ id: 'ide', text: copy.signIn, tone: 'warn' })
} else if (status.ideDurum === 'tamam' && status.ideModel !== null) {
} else if (status.mod === 'aiturk' && status.ideDurum === 'tamam' && status.ideModel !== null) {
lines.push({ id: 'ide', text: copy.models(status.ideModel), tone: 'ok' })
}
@@ -170,6 +170,27 @@ export function CodexLinkSettings() {
title={copy.title}
/>
<ListRow
title={copy.modeTitle}
below={
<fieldset disabled={!status || busy !== null} className="grid gap-2 py-2">
{(['dogrudan', 'aiturk'] as const).map(mod => (
<label key={mod} className="flex min-h-11 cursor-pointer items-center gap-2">
<input
type="radio"
name="codex-link-mode"
value={mod}
checked={(status?.mod ?? 'dogrudan') === mod}
onChange={() => void change({ mod }, 'toggle')}
/>
<span>{mod === 'dogrudan' ? copy.directMode : copy.aiturkMode}</span>
</label>
))}
<p className="text-(--ui-text-tertiary)">{copy.modeRestartNote}</p>
</fieldset>
}
/>
{!status && !loadFailed && <ListRowSkeleton wide />}
{loadFailed && !status && (
+44 -1
View File
@@ -6,6 +6,10 @@ import { useI18n } from './context'
// AITURK-distribution strings (see ./aiturk): Turkish for `tr`, English for
// every other locale.
const en = {
modeTitle: 'Connection mode',
directMode: 'Direct (recommended): subscription models independent of the bridge',
aiturkMode: 'AITURK: combos in the Codex list, all models through the bridge',
modeRestartNote: 'Restart Codex.',
nav: 'Codex Link',
title: 'Codex Link',
description:
@@ -39,6 +43,10 @@ const en = {
}
const tr: typeof en = {
modeTitle: 'Bağlantı modu',
directMode: 'Doğrudan (önerilen): abonelik modelleri köprüden bağımsız',
aiturkMode: 'AITURK: combolar Codex listesinde, tüm modeller köprüden',
modeRestartNote: "Codex'i yeniden başlatın",
nav: 'Codex Bağlama',
title: 'Codex Bağlama',
description:
@@ -71,10 +79,45 @@ const tr: typeof en = {
restartNote: "Codex'te yeni modelleri görmek için Codex'i yeniden açın."
}
const modeTranslations = {
zh: {
modeTitle: '连接模式',
directMode: '直接(推荐):订阅模型独立于桥接',
aiturkMode: 'AITURK:组合显示在 Codex 列表中,所有模型通过桥接',
modeRestartNote: '请重启 Codex。'
},
'zh-hant': {
modeTitle: '連線模式',
directMode: '直接(建議):訂閱模型獨立於橋接',
aiturkMode: 'AITURK:組合顯示在 Codex 清單中,所有模型透過橋接',
modeRestartNote: '請重新啟動 Codex。'
},
ja: {
modeTitle: '接続モード',
directMode: '直接(推奨):サブスクリプションモデルはブリッジから独立',
aiturkMode: 'AITURK:Codex の一覧にコンボを表示、すべてのモデルはブリッジ経由',
modeRestartNote: 'Codex を再起動してください。'
},
ar: {
modeTitle: 'وضع الاتصال',
directMode: 'مباشر (موصى به): نماذج الاشتراك مستقلة عن الجسر',
aiturkMode: 'AITURK: مجموعات النماذج في قائمة Codex، وجميع النماذج عبر الجسر',
modeRestartNote: 'أعد تشغيل Codex.'
},
ru: {
modeTitle: 'Режим подключения',
directMode: 'Напрямую (рекомендуется): модели подписки независимы от моста',
aiturkMode: 'AITURK: комбинации в списке Codex, все модели через мост',
modeRestartNote: 'Перезапустите Codex.'
}
}
export type CodexLinkCopy = typeof en
export function useCodexLinkCopy(): CodexLinkCopy {
const { locale } = useI18n()
return locale === 'tr' ? tr : en
if (locale === 'tr') return tr
if (locale === 'en') return en
return { ...en, ...modeTranslations[locale] }
}
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.37",
"version": "1.0.0-beta.38",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",