feat(codex-link): AITURK work routing setting and beta.40
Server CI and Windows package / verify (push) Successful in 2m42s
Server CI and Windows package / windows-package (push) Successful in 6m21s

- Default routing rule (user decision, 15 Oct 2026): repeating/scheduled work goes to the AITURK IDE bot as a cron job; coding stays in Codex (itself or its subagents); 'Codex' or 'sen yap' keeps everything in Codex.
- Settings > Codex Bağlama: choice (recommended / send large work / never / custom text) with a text box and restore-default; written to bridge.json aiturk_hat.
- Bridge re-reads aiturk_hat when bridge.json changes: no restart. IDE apply keeps aiturk_hat instead of wiping it.
- Bridge version 20, IDE 1.0.0-beta.40. Jev untouched.
This commit is contained in:
yilsem committed 2026-10-15 02:18:18 +03:00
1 parent a7ba300448
commit 557e619499
16 files changed
+388 -26

No files matched your search

@@ -12,6 +12,7 @@ import type { CodexLinkPatch, CodexLinkStatus } from '../../../electron/codex-li
import { EmptyState, ListRow, ListRowSkeleton, SettingsContent, SettingsSection } from './primitives'
import { CompressionSettingsView } from './compression-settings'
import { IsHattiSettingsView } from './is-hatti-settings'
type Tone = 'muted' | 'ok' | 'warn'
@@ -251,6 +252,7 @@ export function CodexLinkSettings() {
{copy.restartNote}
</p>
</SettingsSection>
<IsHattiSettingsView />
<CompressionSettingsView />
</SettingsContent>
)
@@ -0,0 +1,30 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/i18n/context'
import { IsHattiSettingsView } from './is-hatti-settings'
afterEach(() => {
cleanup()
delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop
})
it('shows the custom text box only for custom text and saves the choice', async () => {
const save = vi.fn(async (s: unknown) => s)
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
codexLink: { getIsHatti: async () => ({ secim: 'onerilen', talimat: '' }), setIsHatti: save }
}
render(<I18nProvider configClient={null} initialLocale="tr"><IsHattiSettingsView /></I18nProvider>)
const select = await screen.findByRole('combobox', { name: 'İş dağıtımı' })
expect(screen.queryByRole('textbox')).toBeNull()
fireEvent.change(select, { target: { value: 'ozel' } })
const box = screen.getByRole('textbox', { name: 'Codex’e eklenecek kural' })
expect((screen.getByRole('button', { name: 'Kaydet' }) as HTMLButtonElement).disabled).toBe(true)
fireEvent.change(box, { target: { value: 'kendi kuralım' } })
fireEvent.click(screen.getByRole('button', { name: 'Kaydet' }))
await waitFor(() => expect(save).toHaveBeenCalledWith({ secim: 'ozel', talimat: 'kendi kuralım' }))
expect(await screen.findByText(/yeniden başlatma gerekmez/)).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Varsayılana dön' }))
await waitFor(() => expect(save).toHaveBeenLastCalledWith({ secim: 'onerilen', talimat: 'kendi kuralım' }))
})
@@ -0,0 +1,119 @@
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { useI18n } from '@/i18n/context'
import { Link } from '@/lib/icons'
import type { IsHattiSecim, IsHattiSettings } from '../../../electron/is-hatti-settings'
import { ListRow, SettingsSection } from './primitives'
const SECENEKLER: { value: IsHattiSecim; tr: string; en: string; aciklama: string; desc: string }[] = [
{ value: 'onerilen', tr: 'Önerilen', en: 'Recommended', aciklama: 'Her gün, saat başı gibi tekrarlı işler AITURK IDE botuna zamanlanmış iş olarak gider. Kodlama işlerini Codex kendisi ya da kendi alt ajanlarıyla yapar.', desc: 'Repeating or scheduled work goes to the AITURK IDE bot. Codex keeps coding work and does it itself or with its own subagents.' },
{ value: 'buyuk', tr: 'Büyük işleri AITURK’e gönder', en: 'Send large work to AITURK', aciklama: 'Eski davranış: birkaç adımlı ya da çok dosyalı işler AITURK’e gönderilir, küçük işleri Codex yapar.', desc: 'Previous behaviour: multi-step or multi-file work goes to AITURK; Codex does small tasks.' },
{ value: 'kapali', tr: 'Hiçbir işi AITURK’e gönderme', en: 'Never send work to AITURK', aciklama: 'Codex’e iş dağıtımı hakkında hiçbir not eklenmez; her işi Codex yapar.', desc: 'No routing note is added; Codex does every task.' },
{ value: 'ozel', tr: 'Kendi metnim', en: 'Custom text', aciklama: 'Codex’e eklenecek kuralı siz yazarsınız.', desc: 'You write the rule given to Codex.' }
]
export function IsHattiSettingsView() {
const api = window.hermesDesktop?.codexLink
const { locale } = useI18n()
const tr = locale === 'tr'
const t = (a: string, b: string) => (tr ? a : b)
const [draft, setDraft] = useState<IsHattiSettings | null>(null)
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const [failed, setFailed] = useState(false)
useEffect(() => {
let active = true
api?.getIsHatti?.().then(
next => active && setDraft(next),
() => {
if (active) {
setFailed(true)
setMessage(tr ? 'İş dağıtımı ayarı okunamadı.' : 'Could not load the work routing setting.')
}
}
)
return () => {
active = false
}
}, [api, tr])
if (!api?.getIsHatti || !api.setIsHatti) return null
const save = async (next: IsHattiSettings) => {
setBusy(true)
setMessage('')
setFailed(false)
try {
setDraft(await api.setIsHatti!(next))
setMessage(t('Kaydedildi. Codex’in bir sonraki mesajından itibaren geçerli; yeniden başlatma gerekmez.', 'Saved. Applies from Codex’s next message; no restart needed.'))
} catch {
setFailed(true)
setMessage(t('Kaydedilemedi. Kendi metninizi seçtiyseniz kutunun boş olmadığını kontrol edin.', 'Could not save. If you chose custom text, make sure it is not empty.'))
} finally {
setBusy(false)
}
}
const secili = SECENEKLER.find(s => s.value === draft?.secim)
return (
<SettingsSection icon={Link} title={t('AITURK iş hattı', 'AITURK work routing')}>
<p className="text-sm text-(--ui-text-secondary)">
{t('Codex masaüstü uygulamasının hangi işleri AITURK’e devredeceğini seçin. Kullanıcı “Codex” ya da “sen yap” derse hiçbir iş devredilmez.', 'Choose which work the Codex desktop app hands to AITURK. Saying “Codex” or “do it yourself” keeps every task in Codex.')}
</p>
{draft && (
<>
<ListRow
action={
<select
aria-label={t('İş dağıtımı', 'Work routing')}
className="min-h-11 max-w-full rounded-md border bg-(--ui-bg) p-2"
disabled={busy}
onChange={e => setDraft({ ...draft, secim: e.target.value as IsHattiSecim })}
value={draft.secim}
>
{SECENEKLER.map(s => (
<option key={s.value} value={s.value}>
{tr ? s.tr : s.en}
</option>
))}
</select>
}
description={secili ? (tr ? secili.aciklama : secili.desc) : undefined}
title={t('İş dağıtımı', 'Work routing')}
/>
{draft.secim === 'ozel' && (
<>
<textarea
aria-label={t('Codex’e eklenecek kural', 'Rule given to Codex')}
className="min-h-40 w-full rounded-md border bg-(--ui-bg) p-2 text-sm"
disabled={busy}
onChange={e => setDraft({ ...draft, talimat: e.target.value })}
placeholder={t('Örnek: Kodlama işlerini kendin yap. Her gün tekrarlanacak işleri {zamanla} satırıyla AITURK IDE botuna ver.', 'Example: Do coding yourself. Give daily work to the AITURK IDE bot with {zamanla}.')}
value={draft.talimat}
/>
<p className="text-sm text-(--ui-text-secondary)">
{t('Metinde {zamanla} yazarsanız AITURK IDE botuna zamanlanmış iş veren komut, {komut} yazarsanız işi AITURK’e gönderen komut yerine konur.', 'Write {zamanla} to insert the command that schedules work on the AITURK IDE bot, or {komut} for the command that sends work to AITURK.')}
</p>
</>
)}
<div className="flex gap-2">
<Button disabled={busy || (draft.secim === 'ozel' && !draft.talimat.trim())} onClick={() => void save(draft)}>
{busy ? t('Kaydediliyor…', 'Saving…') : t('Kaydet', 'Save')}
</Button>
<Button disabled={busy} onClick={() => void save({ secim: 'onerilen', talimat: draft.talimat })} variant="secondary">
{t('Varsayılana dön', 'Restore default')}
</Button>
</div>
</>
)}
{message && <p role={failed ? 'alert' : 'status'}>{message}</p>}
</SettingsSection>
)
}
+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 { IsHattiSettings } from '../electron/is-hatti-settings'
import type { PoolLimits } from '../electron/pool-limits'
import type { StagedTransitionPatch, StagedTransitionState } from '../electron/staged-transition-types'
@@ -344,6 +345,8 @@ declare global {
codexLink?: {
getCompression: () => Promise<CompressionState>
setCompression: (settings: CompressionSettings) => Promise<CompressionState>
getIsHatti?: () => Promise<IsHattiSettings>
setIsHatti?: (settings: IsHattiSettings) => Promise<IsHattiSettings>
getStagedTransition: () => Promise<StagedTransitionState>
setStagedTransition: (patch: StagedTransitionPatch) => Promise<StagedTransitionState>
get: () => Promise<CodexLinkStatus>