feat(codex-link): persist direct subscription and AITURK connection modes
This commit is contained in:
1 parent
049c4d1338
commit
8f53b090f7
11 files changed
+188
-17
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')
|
||||
})
|
||||
})
|
||||
@@ -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,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)
|
||||
})
|
||||
})
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user