feat(settings): add Codex link controls and authenticated phone bridge

This commit is contained in:
yilsem committed 2026-10-09 11:55:16 +03:00
1 parent 1acb5dd90f
commit 55a1beaf9d
22 files changed
+2857 -3

No files matched your search

@@ -0,0 +1,196 @@
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 type {
CodexLinkContextView,
CodexLinkControlRequest,
CodexLinkControlResponse,
CodexLinkSettings as SavedSettings
} from '../../../codex-link-control.types'
import { CodexLinkSettings } from './codex-link-settings'
let settings: SavedSettings
let views: CodexLinkContextView[]
let writes: CodexLinkControlRequest[]
let failWrite: boolean
const control = vi.fn(async (request: CodexLinkControlRequest): Promise<CodexLinkControlResponse> => {
const base = {
contract_version: 'codex-link/1' as const,
request_id: request.request_id,
operation_id: request.operation_id
}
if (request.kind === 'status_get') {
return {
...base,
kind: 'status_result',
status: {
settings_revision: settings.settings_revision,
runtime_mode: 'managed',
native_available: true,
external_admission: 'open',
needs_restart: false,
catalog_state: 'fresh',
active_turns: 0,
uncertain_operations: 0,
last_operation_id: null,
error: null
}
}
}
if (request.kind === 'context_views_get') {
return { ...base, kind: 'context_views_result', settings_revision: settings.settings_revision, views }
}
if (request.kind === 'settings_update') {
writes.push(request)
if (failWrite) {
throw new Error('outcome_unknown')
}
settings = { ...settings, ...request.patch, settings_revision: settings.settings_revision + 1 }
} else if (request.kind !== 'settings_get') {
writes.push(request)
return {
...base,
kind: 'control_accepted',
settings_revision: settings.settings_revision,
operation_state: 'accepted',
error: null
}
}
return { ...base, kind: 'settings_result', settings, available_sources: [] }
})
const mount = () =>
render(
<I18nProvider configClient={null} initialLocale="tr">
<CodexLinkSettings />
</I18nProvider>
)
beforeEach(() => {
sessionStorage.clear()
control.mockClear()
writes = []
views = []
failWrite = false
settings = {
settings_revision: 7,
enabled: false,
compression_enabled: true,
threshold_tokens: 250000,
source_binding_handle: null,
compact_route_id: 'Compact',
legacy_intent_imported: true,
ownership_manifest_revision: 2,
activation_state: 'disabled'
}
vi.stubGlobal('hermesDesktop', { codexLink: { control } } satisfies Pick<NonNullable<typeof window.hermesDesktop>, 'codexLink'>)
})
afterEach(() => {
cleanup()
vi.unstubAllGlobals()
})
describe('Codex connection settings', () => {
it('shows the persisted disabled preference, without turning it on during mount', async () => {
mount()
const toggle = await screen.findByRole('switch', { name: 'Modelleri Codex’e bağla' })
expect(toggle.getAttribute('aria-checked')).toBe('false')
expect(writes).toEqual([])
expect((screen.getByLabelText('Sıkıştırma eşiği (token)') as HTMLInputElement).value).toBe('250000')
})
it('uses a checkpoint-safe settings patch and rejects decimal/unsafe thresholds', async () => {
mount()
const input = await screen.findByLabelText('Sıkıştırma eşiği (token)')
fireEvent.change(input, { target: { value: '1.5' } })
expect((screen.getByRole('button', { name: 'Kaydet' }) as HTMLButtonElement).disabled).toBe(true)
fireEvent.change(input, { target: { value: '9007199254740992' } })
expect((screen.getByRole('button', { name: 'Kaydet' }) as HTMLButtonElement).disabled).toBe(true)
fireEvent.change(input, { target: { value: '125000' } })
fireEvent.click(screen.getByRole('button', { name: 'Kaydet' }))
await waitFor(() => expect(writes).toHaveLength(1))
expect(writes[0]).toMatchObject({
kind: 'settings_update',
expected_settings_revision: 7,
patch: { threshold_tokens: 125000 }
})
})
it('keeps an uncertain operation across remount and reconciles the exact ID/body', async () => {
failWrite = true
const first = mount()
fireEvent.click(await screen.findByRole('switch', { name: 'Modelleri Codex’e bağla' }))
await screen.findByRole('button', { name: 'İşlemi kontrol et' })
expect(writes).toHaveLength(1)
const original = writes[0]
first.unmount()
mount()
failWrite = false
fireEvent.click(await screen.findByRole('button', { name: 'İşlemi kontrol et' }))
await waitFor(() => expect(writes).toHaveLength(2))
expect(writes[1]).toEqual(original)
await waitFor(() => expect(sessionStorage.getItem('aiturk.codex-link.pending-control.v1')).toBeNull())
})
it('does not present unknown token measurement as zero or enable unsafe compaction', async () => {
views = [
{
view_handle: 'view-a',
can_compact: false,
label: 'Ayrı sohbet',
checkpoint_revision: 3,
current_model_label: 'Model A',
limiting_target_id: null,
target_budgets: null,
active_summary_count: 2,
summary_tokens: null,
summary_token_measurement: null,
last_operation_id: null,
last_operation_state: null,
last_operation_error: null,
compact_attempts: []
}
]
mount()
await screen.findByText('Ayrı sohbet')
expect(screen.getAllByText(/Henüz ölçülmedi/).length).toBeGreaterThanOrEqual(3)
expect((screen.getByRole('button', { name: 'Şimdi sıkıştır' }) as HTMLButtonElement).disabled).toBe(true)
})
it('binds manual compaction to a manager view and its exact checkpoint revision', async () => {
views = [
{
view_handle: 'opaque-view-a',
can_compact: true,
label: 'Sohbet A',
checkpoint_revision: 9,
current_model_label: 'Model A',
limiting_target_id: null,
target_budgets: null,
active_summary_count: 0,
summary_tokens: null,
summary_token_measurement: null,
last_operation_id: null,
last_operation_state: null,
last_operation_error: null,
compact_attempts: []
}
]
mount()
fireEvent.click(await screen.findByRole('button', { name: 'Şimdi sıkıştır' }))
await waitFor(() => expect(writes).toHaveLength(1))
expect(writes[0]).toMatchObject({
kind: 'context_compact_view',
view_handle: 'opaque-view-a',
expected_checkpoint_revision: 9
})
expect(writes[0]).not.toHaveProperty('thread_id')
})
})
@@ -0,0 +1,465 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { useCodexLinkCopy } from '@/i18n/codex-link'
import { Cpu } from '@/lib/icons'
import { validateControlRequest } from '../../../codex-link-control'
import type {
CodexLinkContextView,
CodexLinkControlRequest,
CodexLinkControlResponse,
CodexLinkSettingsResult,
CodexLinkSettingsUpdate,
CodexLinkStatusSnapshot
} from '../../../codex-link-control.types'
import { SettingsContent, SettingsSection } from './primitives'
const PENDING_KEY = 'aiturk.codex-link.pending-control.v1'
function envelope() {
return {
contract_version: 'codex-link/1' as const,
request_id: crypto.randomUUID(),
operation_id: crypto.randomUUID()
}
}
function readPending(): CodexLinkControlRequest | null {
try {
const raw = sessionStorage.getItem(PENDING_KEY)
if (!raw) {
return null
}
const value: unknown = JSON.parse(raw)
validateControlRequest(value)
return ['settings_update', 'catalog_sync', 'context_compact_view'].includes(value.kind) ? value : null
} catch {
return null
}
}
function savedPending(value: CodexLinkControlRequest | null) {
if (value) {
sessionStorage.setItem(PENDING_KEY, JSON.stringify(value))
} else {
sessionStorage.removeItem(PENDING_KEY)
}
}
function Preference({
id,
label,
description,
checked,
disabled,
onChange
}: {
id: string
label: string
description: string
checked: boolean
disabled: boolean
onChange: (value: boolean) => void
}) {
return (
<label className="flex min-h-11 cursor-pointer items-center gap-4 py-3" htmlFor={id}>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium">{label}</span>
<span className="mt-1 block text-sm text-muted-foreground">{description}</span>
</span>
<Switch aria-label={label} checked={checked} disabled={disabled} id={id} onCheckedChange={onChange} />
</label>
)
}
export function CodexLinkSettings() {
const copy = useCodexLinkCopy()
const [settings, setSettings] = useState<CodexLinkSettingsResult | null>(null)
const [status, setStatus] = useState<CodexLinkStatusSnapshot | null>(null)
const [views, setViews] = useState<CodexLinkContextView[]>([])
const [threshold, setThreshold] = useState('')
const [busy, setBusy] = useState(false)
const [pending, setPending] = useState(readPending)
const [error, setError] = useState('')
const [notice, setNotice] = useState('')
const mounted = useRef(true)
const writeLock = useRef(false)
const readLock = useRef(false)
const generation = useRef(0)
const savedRevision = useRef(-1)
const bridge = window.hermesDesktop?.codexLink
const refresh = useCallback(async () => {
if (!bridge || readLock.current || writeLock.current) {
return
}
readLock.current = true
const current = generation.current
try {
const results = await Promise.all(
['settings_get', 'status_get', 'context_views_get'].map(kind =>
bridge.control({ ...envelope(), kind } as CodexLinkControlRequest)
)
)
if (!mounted.current || current !== generation.current) {
return
}
const [saved, live, chats] = results
if (saved.kind !== 'settings_result' || live.kind !== 'status_result' || chats.kind !== 'context_views_result') {
throw new Error('read_failed')
}
// A write in another window may fall between these three reads. Never paint a mixed revision.
if (
saved.settings.settings_revision !== live.status.settings_revision ||
saved.settings.settings_revision !== chats.settings_revision
) {
return
}
if (saved.settings.settings_revision < savedRevision.current) {
return
}
if (savedRevision.current !== saved.settings.settings_revision) {
setThreshold(String(saved.settings.threshold_tokens))
}
savedRevision.current = saved.settings.settings_revision
setSettings(saved)
setStatus(live.status)
setViews(chats.views)
setError('')
} catch {
if (mounted.current && current === generation.current) {
setError(copy.failed)
}
} finally {
readLock.current = false
}
}, [bridge, copy.failed])
// This is a component lifetime flag, not a mirror of a reactive atom/value.
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
mounted.current = true
void refresh()
const timer = setInterval(() => {
if (document.visibilityState === 'visible') {
void refresh()
}
}, 5000)
return () => {
mounted.current = false
// The current numeric generation is invalidated; it is not a captured DOM ref.
// eslint-disable-next-line react-hooks/exhaustive-deps
generation.current++
clearInterval(timer)
}
}, [refresh])
const send = async (request: CodexLinkControlRequest) => {
if (!bridge || writeLock.current) {
return
}
writeLock.current = true
generation.current++
setBusy(true)
setError('')
setNotice('')
// Persist the exact operation before dispatch: an uncertain reply must reuse this ID and body.
try {
savedPending(request)
setPending(request)
} catch {
writeLock.current = false
setBusy(false)
setError(copy.failed)
return
}
try {
const result: CodexLinkControlResponse = await bridge.control(request)
if (!mounted.current) {
return
}
if (
(result.kind === 'control_error' && result.error.recovery_action === 'reconcile_only') ||
(result.kind === 'control_accepted' && result.operation_state === 'indeterminate')
) {
setError(copy.unknown)
return
}
savedPending(null)
setPending(null)
if (result.kind === 'control_error') {
setNotice(
result.error.code.includes('conflict') || result.error.code.includes('cas')
? copy.conflict
: result.error.message
)
} else if (result.kind === 'settings_result') {
savedRevision.current = result.settings.settings_revision
setSettings(result)
setThreshold(String(result.settings.threshold_tokens))
setNotice(copy.saved)
} else if (result.kind === 'control_accepted') {
setNotice(result.error?.message ?? copy.accepted)
}
} catch {
if (mounted.current) {
setError(copy.unknown)
}
} finally {
writeLock.current = false
if (mounted.current) {
setBusy(false)
void refresh()
}
}
}
const update = (patch: CodexLinkSettingsUpdate['patch']) => {
if (settings && !pending) {
void send({
...envelope(),
kind: 'settings_update',
expected_settings_revision: settings.settings.settings_revision,
patch
})
}
}
const thresholdValue = /^[0-9]+$/.test(threshold) ? Number(threshold) : NaN
const validThreshold = Number.isSafeInteger(thresholdValue) && thresholdValue > 0
const locked = busy || pending !== null
const number = (n: number | null) => (n === null ? copy.unmeasured : n.toLocaleString())
return (
<SettingsContent>
<div className="min-w-0 py-4" data-testid="codex-link-settings">
<SettingsSection icon={Cpu} title={copy.title}>
<p className="mb-4 text-sm text-muted-foreground">{copy.intro}</p>
{!bridge ? (
<p role="alert">{copy.unavailable}</p>
) : !settings ? (
<p role="status">{error || copy.loading}</p>
) : (
<>
<Preference
checked={settings.settings.enabled}
description={copy.enabledHelp}
disabled={locked}
id="codex-link-enabled"
label={copy.enabled}
onChange={enabled => update({ enabled })}
/>
<Preference
checked={settings.settings.compression_enabled}
description={copy.compressionHelp}
disabled={locked}
id="codex-link-compression"
label={copy.compression}
onChange={compression_enabled => update({ compression_enabled })}
/>
<div className="my-4 space-y-2">
<label className="block text-sm font-medium" htmlFor="codex-link-threshold">
{copy.threshold}
</label>
<div className="flex flex-wrap gap-2">
<Input
aria-describedby="codex-link-threshold-help"
aria-invalid={!validThreshold}
className="min-h-11 w-full min-w-0 sm:w-48"
disabled={locked}
id="codex-link-threshold"
inputMode="numeric"
onChange={event => setThreshold(event.target.value)}
value={threshold}
/>
<Button
className="min-h-11"
disabled={locked || !validThreshold || thresholdValue === settings.settings.threshold_tokens}
onClick={() => update({ threshold_tokens: thresholdValue })}
>
{copy.save}
</Button>
</div>
<p className="text-sm text-muted-foreground" id="codex-link-threshold-help">
{validThreshold ? copy.thresholdHelp : copy.invalidThreshold}
</p>
</div>
<div className="my-4 space-y-2">
<label className="block text-sm font-medium" htmlFor="codex-link-source">
{copy.source}
</label>
{settings.available_sources.length ? (
<select
className="min-h-11 w-full min-w-0 rounded-md border bg-background px-3 text-sm"
disabled={locked}
id="codex-link-source"
onChange={event => update({ source_binding_handle: event.target.value })}
value={settings.settings.source_binding_handle ?? ''}
>
{!settings.settings.source_binding_handle && (
<option disabled value="">
{copy.noSource}
</option>
)}
{settings.available_sources.map(source => (
<option
disabled={!source.authorized}
key={source.source_binding_handle}
value={source.source_binding_handle}
>
{source.label}
</option>
))}
</select>
) : (
<p className="text-sm text-muted-foreground">{copy.noSource}</p>
)}
</div>
<p className="text-sm" role="status">
{copy.activation[settings.settings.activation_state]}
{status ? ` · ${copy.admission[status.external_admission]}` : ''}
</p>
{status?.needs_restart && <p className="mt-2 text-sm text-muted-foreground">{copy.restart}</p>}
{status?.error && (
<p className="mt-2 text-sm" role="alert">
{status.error.message}
</p>
)}
<div className="mt-4 flex flex-wrap gap-2">
<Button
className="min-h-11"
disabled={locked}
onClick={() => void send({ ...envelope(), kind: 'catalog_sync' })}
variant="outline"
>
{copy.sync}
</Button>
<Button className="min-h-11" disabled={busy} onClick={() => void refresh()} variant="outline">
{copy.refresh}
</Button>
</div>
</>
)}
{pending && (
<div className="mt-4 space-y-2">
<p className="text-sm" role="alert">
{copy.unknown}
</p>
<Button className="min-h-11" disabled={busy} onClick={() => void send(pending)}>
{copy.reconcile}
</Button>
</div>
)}
{error && settings && (
<p className="mt-3 text-sm" role="alert">
{error}
</p>
)}
{notice && (
<p className="mt-3 text-sm" role="status">
{notice}
</p>
)}
</SettingsSection>
{settings && (
<SettingsSection icon={Cpu} title={copy.chats}>
{!views.length && <p className="text-sm text-muted-foreground">{copy.empty}</p>}
{views.map(view => {
const target = view.target_budgets?.find(item => item.target_id === view.limiting_target_id)
const hardLimit = target
? Math.min(
target.max_input_tokens ?? Number.MAX_SAFE_INTEGER,
target.context_window_tokens - target.output_cap_tokens - target.safety_margin_tokens
)
: null
const limit =
hardLimit === null
? null
: settings.settings.compression_enabled
? Math.min(settings.settings.threshold_tokens, hardLimit)
: hardLimit
return (
<article className="min-w-0 border-b py-4 last:border-b-0" key={view.view_handle}>
<h3 className="break-words text-sm font-medium">{view.label}</h3>
<p className="break-words text-sm text-muted-foreground">{view.current_model_label}</p>
<dl className="my-3 grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-2 text-sm">
<dt>{copy.current}</dt>
<dd className="break-words">{number(target?.input_tokens ?? null)}</dd>
<dt>{copy.limit}</dt>
<dd className="break-words">{number(limit)}</dd>
<dt>{copy.summaries}</dt>
<dd>
{view.active_summary_count} · {number(view.summary_tokens)}{' '}
{view.summary_tokens !== null ? copy.tokens : ''}
</dd>
</dl>
{view.last_operation_state && (
<p className="my-2 text-sm" role="status">
{copy.operation[view.last_operation_state]}
</p>
)}
{view.last_operation_error && (
<p className="my-2 text-sm" role="alert">
{view.last_operation_error.message}
</p>
)}
{view.compact_attempts.map(attempt => (
<p className="break-words text-sm text-muted-foreground" key={attempt.attempt_id}>
{attempt.member_label} · {copy.operation[attempt.state]}
</p>
))}
<Button
className="mt-2 min-h-11"
disabled={locked || !view.can_compact}
onClick={() =>
void send({
...envelope(),
kind: 'context_compact_view',
view_handle: view.view_handle,
expected_checkpoint_revision: view.checkpoint_revision
})
}
variant="outline"
>
{copy.compact}
</Button>
</article>
)
})}
</SettingsSection>
)}
</div>
</SettingsContent>
)
}
+14 -1
View File
@@ -9,6 +9,7 @@ import { Tip } from '@/components/ui/tooltip'
import { getHermesConfigDefaults, getHermesConfigRecord, saveHermesConfig } from '@/hermes'
import { useI18n } from '@/i18n'
import { useAiturkCopy } from '@/i18n/aiturk'
import { useCodexLinkCopy } from '@/i18n/codex-link'
import { triggerHaptic } from '@/lib/haptics'
import {
Archive,
@@ -47,6 +48,7 @@ import { AboutSettings } from './about-settings'
import { AppearanceSettings } from './appearance-settings'
import { BillingSettings } from './billing'
import { ConfigSettings } from './config-settings'
import { CodexLinkSettings } from './codex-link-settings'
import { SECTIONS } from './constants'
import { GatewaySettings } from './gateway-settings'
import { KeybindSettings } from './keybind-settings'
@@ -61,6 +63,7 @@ const SETTINGS_VIEWS: readonly SettingsViewId[] = [
...SECTIONS.map(s => `config:${s.id}` as SettingsViewId),
'providers',
'gateway',
'codex-link',
// Legacy alias: the Connections page merged into Gateways. Kept in the enum
// so saved `?tab=connections` deep links still resolve (redirected below).
'connections',
@@ -76,6 +79,7 @@ const SETTINGS_VIEWS: readonly SettingsViewId[] = [
export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: SettingsPageProps) {
const { t } = useI18n()
const aiturkCopy = useAiturkCopy()
const codexLinkCopy = useCodexLinkCopy()
const navigate = useNavigate()
const { hash, pathname, search } = useLocation()
@@ -252,6 +256,13 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
label: t.settings.nav.gateway,
onSelect: () => setActiveView('gateway')
},
{
active: activeView === 'codex-link',
icon: Cpu,
id: 'codex-link',
label: codexLinkCopy.title,
onSelect: () => setActiveView('codex-link')
},
// "Telefon bağlantısı" lives in its own small window (QR + code); this
// entry only opens it. Present only where the desktop bridge offers it.
...(window.hermesDesktop?.aiturkMobile
@@ -320,7 +331,7 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
onSelect: () => setActiveView('about')
}
],
[activeView, keysView, providerView, t, aiturkCopy, setActiveView, openProviderView, openKeysView]
[activeView, keysView, providerView, t, aiturkCopy, codexLinkCopy, setActiveView, openProviderView, openKeysView]
)
// Type-to-search: printable keystrokes on the Settings surface (outside any
@@ -414,6 +425,8 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
// 'connections' renders the unified page too so the frame before
// the alias redirect lands doesn't flash the fallback view.
<GatewaySettings />
) : activeView === 'codex-link' ? (
<CodexLinkSettings />
) : activeView === 'keybinds' ? (
<KeybindSettings />
) : activeView.startsWith('config:') ? (
+1
View File
@@ -8,6 +8,7 @@ export type SettingsView =
| 'about'
| 'billing'
| 'connections'
| 'codex-link'
| 'gateway'
| 'keybinds'
| 'keys'
@@ -39,7 +39,7 @@ describe('ShareControls', () => {
const dialog = await screen.findByRole('dialog')
const input = within(dialog).getByRole('textbox')
fireEvent.change(input, { target: { value: 'not a valid code !!!' } })
fireEvent.click(within(dialog).getByRole('button', { name: 'Yükle', exact: true }))
fireEvent.click(within(dialog).getByRole('button', { name: 'Yükle' }))
expect(within(dialog).getByText('Bu metin bir harita koduna benzemiyor.')).toBeTruthy()
expect((input as HTMLTextAreaElement).value).toBe('not a valid code !!!')
})
+2
View File
@@ -2,6 +2,7 @@ import type { GatewayWsUrlResult } from '@hermes/shared'
import type { TranslucencyState } from '@hermes/shared/translucency'
import type { PoolLimits } from '../electron/pool-limits'
import type { CodexLinkControlRequest, CodexLinkControlResponse } from '../codex-link-control.types'
import type { WakeIndicatorState } from './lib/wake-indicator'
import type {
@@ -17,6 +18,7 @@ export {}
declare global {
interface Window {
hermesDesktop: {
codexLink?: { control: (request: CodexLinkControlRequest) => Promise<CodexLinkControlResponse> }
setUiLanguage?: (locale: string) => void
// Resolve a backend connection. Omit `profile` (or pass the primary) for
// the window's backend; pass a named profile to lazily spawn/reuse that
+121
View File
@@ -0,0 +1,121 @@
import { useI18n } from './context'
const en = {
title: 'Connect Codex',
intro: 'Use this IDE’s models and combos in Codex. Each chat keeps its own memory.',
enabled: 'Connect models to Codex',
enabledHelp: 'Enabled on first installation. Your choice is saved.',
compression: 'Automatic compaction',
compressionHelp:
'Checks the complete request before sending. Earlier summaries are retained as the available context allows.',
threshold: 'Compaction threshold (tokens)',
thresholdHelp:
'The effective threshold is lower when a model has less available input space. Output space is reserved too.',
invalidThreshold: 'Enter a positive whole number.',
save: 'Save',
source: 'Connected account',
noSource: 'No authorized model source is available.',
sync: 'Refresh models',
refresh: 'Refresh status',
loading: 'Reading saved settings…',
unavailable: 'The local Codex connection service is unavailable. Check the AITURK installation.',
failed: 'Could not read the latest state.',
unknown: 'The result is not confirmed. Check this same operation before making another change.',
reconcile: 'Check operation',
conflict: 'Settings changed elsewhere. The latest saved state has been loaded.',
saved: 'Settings saved.',
accepted: 'Operation accepted. Its progress appears below.',
chats: 'Chat memory',
empty: 'No managed Codex chat is currently visible.',
compact: 'Compact now',
summaries: 'Saved summaries',
tokens: 'tokens',
unmeasured: 'Not measured yet',
current: 'Current input',
limit: 'Effective threshold',
restart: 'The next Codex launch will use the new runtime. Existing chats keep their current process.',
activation: {
disabled: 'Disabled',
pending_runtime_acceptance: 'Runtime verification pending',
ready: 'Ready',
restart_required: 'Restart required',
conflict: 'Configuration conflict',
recovery_pending: 'Recovering the saved state'
},
admission: {
open: 'External models available',
closed: 'External models unavailable',
draining: 'Finishing active work',
recovery_required: 'Operation recovery required'
},
operation: {
accepted: 'Accepted',
prepared: 'Prepared',
running: 'Running',
completed: 'Completed',
failed: 'Failed',
indeterminate: 'Result unknown'
}
}
const tr: typeof en = {
title: 'Codex Bağlama',
intro: 'Bu IDE’deki modelleri ve comboları Codex’te kullanın. Her sohbet kendi hafızasını tutar.',
enabled: 'Modelleri Codex’e bağla',
enabledHelp: 'İlk kurulumda açıktır. Değiştirdiğiniz tercih kaydedilir.',
compression: 'Otomatik sıkıştırma',
compressionHelp:
'Gönderilmeden önce isteğin tamamı kontrol edilir. Önceki özetler kullanılabilir bağlamın izin verdiği ölçüde korunur.',
threshold: 'Sıkıştırma eşiği (token)',
thresholdHelp:
'Modelin kullanılabilir giriş alanı daha küçükse etkin eşik düşer. Yanıt için gereken alan da ayrılır.',
invalidThreshold: 'Pozitif bir tam sayı girin.',
save: 'Kaydet',
source: 'Bağlı hesap',
noSource: 'Yetkili model kaynağı bulunamadı.',
sync: 'Modelleri yenile',
refresh: 'Durumu yenile',
loading: 'Kayıtlı ayarlar okunuyor…',
unavailable: 'Yerel Codex bağlantı hizmetine ulaşılamıyor. AITURK kurulumunu kontrol edin.',
failed: 'Güncel durum okunamadı.',
unknown: 'İşlemin sonucu henüz doğrulanmadı. Yeni değişiklikten önce aynı işlemin durumunu kontrol edin.',
reconcile: 'İşlemi kontrol et',
conflict: 'Ayarlar başka bir yerde değişti. Güncel kayıtlı ayarlar yüklendi.',
saved: 'Ayarlar kaydedildi.',
accepted: 'İşlem kabul edildi. İlerlemesi aşağıda gösterilir.',
chats: 'Sohbet hafızası',
empty: 'Şu anda görünen yönetilen Codex sohbeti yok.',
compact: 'Şimdi sıkıştır',
summaries: 'Kayıtlı özetler',
tokens: 'token',
unmeasured: 'Henüz ölçülmedi',
current: 'Mevcut giriş',
limit: 'Etkin eşik',
restart: 'Yeni runtime sonraki Codex açılışında kullanılacak. Açık sohbetler mevcut süreçleriyle devam eder.',
activation: {
disabled: 'Kapalı',
pending_runtime_acceptance: 'Runtime doğrulaması bekleniyor',
ready: 'Hazır',
restart_required: 'Yeniden açılış gerekli',
conflict: 'Ayar çakışması',
recovery_pending: 'Kayıtlı durum kurtarılıyor'
},
admission: {
open: 'Harici modeller kullanılabilir',
closed: 'Harici modeller kullanılamıyor',
draining: 'Etkin işler bitiriliyor',
recovery_required: 'İşlem durumu uzlaştırılmalı'
},
operation: {
accepted: 'Kabul edildi',
prepared: 'Hazırlandı',
running: 'Çalışıyor',
completed: 'Tamamlandı',
failed: 'Başarısız',
indeterminate: 'Sonuç belirsiz'
}
}
export function useCodexLinkCopy() {
return useI18n().locale === 'tr' ? tr : en
}