Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
346 KiB
JSON
1 line
346 KiB
JSON
{"repo": "azzab/opencodex-desktop", "files": {"kun/config.example.json": {"ab1332c56336d43f0780fc6a9fa1fe66582bb7e9": {"t": 1785746283.922343, "n": 4295, "h": "a6159f2783cefef1", "c": "{\n \"serve\": {\n \"host\": \"127.0.0.1\",\n \"port\": 18999,\n \"dataDir\": \"~/.opencodex/kun\",\n \"runtimeToken\": \"\",\n \"apiKey\": \"\",\n \"baseUrl\": \"https://api.deepseek.com/beta\",\n \"model\": \"deepseek-v4-pro\",\n \"approvalPolicy\": \"auto\",\n \"sandboxMode\": \"workspace-write\",\n \"tokenEconomyMode\": false,\n \"tokenEconomy\": {\n \"enabled\": false,\n \"compressToolDescriptions\": true,\n \"compressToolResults\": true,\n \"conciseResponses\": true,\n \"historyHygiene\": {\n \"maxToolResultLines\": 320,\n \"maxToolResultBytes\": 32768,\n \"maxToolResultTokens\": 8000,\n \"maxToolArgumentStringBytes\": 8192,\n \"maxToolArgumentStringTokens\": 2000,\n \"maxArrayItems\": 80\n }\n },\n \"insecure\": false,\n \"storage\": {\n \"backend\": \"hybrid\"\n }\n },\n \"contextCompaction\": {\n \"defaultSoftThreshold\": 16000,\n \"defaultHardThreshold\": 24000,\n \"summaryMode\": \"heuristic\",\n \"summaryTimeoutMs\": 15000,\n \"summaryMaxTokens\": 1200,\n \"summaryInputMaxBytes\": 98304\n },\n \"models\": {\n \"profiles\": {\n \"deepseek-v4-pro\": {\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n },\n \"deepseek-v4-flash\": {\n \"aliases\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n }\n }\n },\n \"capabilities\": {\n \"mcp\": {\n \"enabled\": false,\n \"search\": {\n \"enabled\": false,\n \"mode\": \"auto\",\n \"autoThresholdToolCount\": 24,\n \"topKDefault\": 5,\n \"topKMax\": 10,\n \"minScore\": 0.15,\n \"bm25\": {\n \"k1\": 1.2,\n \"b\": 0.75\n }\n },\n \"servers\": {}\n },\n \"web\": {\n \"enabled\": false,\n \"fetchEnabled\": false,\n \"searchEnabled\": false,\n \"allowDomains\": [],\n \"denyDomains\": []\n },\n \"skills\": {\n \"enabled\": false,\n \"roots\": [],\n \"legacySkillMd\": true\n },\n \"subagents\": {\n \"enabled\": false,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"defaultPreset\": \"research_split\",\n \"maxParallel\": 2,\n \"maxChildRuns\": 4,\n \"maxTotalChildTokens\": 50000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 120000,\n \"workflowPresets\": {\n \"review_swarm\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 4,\n \"maxChildRuns\": 8,\n \"maxTotalChildTokens\": 80000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 90000\n },\n \"implementation_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 2,\n \"maxChildRuns\": 4,\n \"maxTotalChildTokens\": 70000,\n \"maxChildCostUsd\": 1.5,\n \"perAgentTimeoutMs\": 180000\n },\n \"research_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 3,\n \"maxChildRuns\": 6,\n \"maxTotalChildTokens\": 50000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 120000\n },\n \"audit_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 3,\n \"maxChildRuns\": 6,\n \"maxTotalChildTokens\": 80000,\n \"maxChildCostUsd\": 1.5,\n \"perAgentTimeoutMs\": 150000\n }\n }\n },\n \"attachments\": {\n \"enabled\": false,\n \"maxImageBytes\": 5242880,\n \"maxImageDimension\": 4096,\n \"allowedMimeTypes\": [\"image/png\", \"image/jpeg\", \"image/webp\"],\n \"textFallbackMaxBase64Bytes\": 524288,\n \"textFallbackMaxImageDimension\": 1280,\n \"textFallbackPreferredMimeType\": \"image/webp\"\n },\n \"memory\": {\n \"enabled\": false,\n \"scopes\": [\"user\", \"workspace\", \"project\"],\n \"maxInjectedRecords\": 8\n }\n }\n}\n"}, "7120350c4879848998ad65c19d63e75a7514b4a8": {"t": 1785761429.1754425, "n": 4295, "h": "a6159f2783cefef1", "c": "{\n \"serve\": {\n \"host\": \"127.0.0.1\",\n \"port\": 18999,\n \"dataDir\": \"~/.opencodex/kun\",\n \"runtimeToken\": \"\",\n \"apiKey\": \"\",\n \"baseUrl\": \"https://api.deepseek.com/beta\",\n \"model\": \"deepseek-v4-pro\",\n \"approvalPolicy\": \"auto\",\n \"sandboxMode\": \"workspace-write\",\n \"tokenEconomyMode\": false,\n \"tokenEconomy\": {\n \"enabled\": false,\n \"compressToolDescriptions\": true,\n \"compressToolResults\": true,\n \"conciseResponses\": true,\n \"historyHygiene\": {\n \"maxToolResultLines\": 320,\n \"maxToolResultBytes\": 32768,\n \"maxToolResultTokens\": 8000,\n \"maxToolArgumentStringBytes\": 8192,\n \"maxToolArgumentStringTokens\": 2000,\n \"maxArrayItems\": 80\n }\n },\n \"insecure\": false,\n \"storage\": {\n \"backend\": \"hybrid\"\n }\n },\n \"contextCompaction\": {\n \"defaultSoftThreshold\": 16000,\n \"defaultHardThreshold\": 24000,\n \"summaryMode\": \"heuristic\",\n \"summaryTimeoutMs\": 15000,\n \"summaryMaxTokens\": 1200,\n \"summaryInputMaxBytes\": 98304\n },\n \"models\": {\n \"profiles\": {\n \"deepseek-v4-pro\": {\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n },\n \"deepseek-v4-flash\": {\n \"aliases\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n }\n }\n },\n \"capabilities\": {\n \"mcp\": {\n \"enabled\": false,\n \"search\": {\n \"enabled\": false,\n \"mode\": \"auto\",\n \"autoThresholdToolCount\": 24,\n \"topKDefault\": 5,\n \"topKMax\": 10,\n \"minScore\": 0.15,\n \"bm25\": {\n \"k1\": 1.2,\n \"b\": 0.75\n }\n },\n \"servers\": {}\n },\n \"web\": {\n \"enabled\": false,\n \"fetchEnabled\": false,\n \"searchEnabled\": false,\n \"allowDomains\": [],\n \"denyDomains\": []\n },\n \"skills\": {\n \"enabled\": false,\n \"roots\": [],\n \"legacySkillMd\": true\n },\n \"subagents\": {\n \"enabled\": false,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"defaultPreset\": \"research_split\",\n \"maxParallel\": 2,\n \"maxChildRuns\": 4,\n \"maxTotalChildTokens\": 50000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 120000,\n \"workflowPresets\": {\n \"review_swarm\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 4,\n \"maxChildRuns\": 8,\n \"maxTotalChildTokens\": 80000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 90000\n },\n \"implementation_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 2,\n \"maxChildRuns\": 4,\n \"maxTotalChildTokens\": 70000,\n \"maxChildCostUsd\": 1.5,\n \"perAgentTimeoutMs\": 180000\n },\n \"research_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 3,\n \"maxChildRuns\": 6,\n \"maxTotalChildTokens\": 50000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 120000\n },\n \"audit_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 3,\n \"maxChildRuns\": 6,\n \"maxTotalChildTokens\": 80000,\n \"maxChildCostUsd\": 1.5,\n \"perAgentTimeoutMs\": 150000\n }\n }\n },\n \"attachments\": {\n \"enabled\": false,\n \"maxImageBytes\": 5242880,\n \"maxImageDimension\": 4096,\n \"allowedMimeTypes\": [\"image/png\", \"image/jpeg\", \"image/webp\"],\n \"textFallbackMaxBase64Bytes\": 524288,\n \"textFallbackMaxImageDimension\": 1280,\n \"textFallbackPreferredMimeType\": \"image/webp\"\n },\n \"memory\": {\n \"enabled\": false,\n \"scopes\": [\"user\", \"workspace\", \"project\"],\n \"maxInjectedRecords\": 8\n }\n }\n}\n"}, "55c6ec8a92f1febd6fb698ad191957901e0aaaaf": {"t": 1785761430.7477114, "n": 4296, "h": "441a73e120684610", "c": "{\n \"serve\": {\n \"host\": \"127.0.0.1\",\n \"port\": 8899,\n \"dataDir\": \"~/.deepseekgui/kun\",\n \"runtimeToken\": \"\",\n \"apiKey\": \"\",\n \"baseUrl\": \"https://api.deepseek.com/beta\",\n \"model\": \"deepseek-v4-pro\",\n \"approvalPolicy\": \"auto\",\n \"sandboxMode\": \"workspace-write\",\n \"tokenEconomyMode\": false,\n \"tokenEconomy\": {\n \"enabled\": false,\n \"compressToolDescriptions\": true,\n \"compressToolResults\": true,\n \"conciseResponses\": true,\n \"historyHygiene\": {\n \"maxToolResultLines\": 320,\n \"maxToolResultBytes\": 32768,\n \"maxToolResultTokens\": 8000,\n \"maxToolArgumentStringBytes\": 8192,\n \"maxToolArgumentStringTokens\": 2000,\n \"maxArrayItems\": 80\n }\n },\n \"insecure\": false,\n \"storage\": {\n \"backend\": \"hybrid\"\n }\n },\n \"contextCompaction\": {\n \"defaultSoftThreshold\": 16000,\n \"defaultHardThreshold\": 24000,\n \"summaryMode\": \"heuristic\",\n \"summaryTimeoutMs\": 15000,\n \"summaryMaxTokens\": 1200,\n \"summaryInputMaxBytes\": 98304\n },\n \"models\": {\n \"profiles\": {\n \"deepseek-v4-pro\": {\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n },\n \"deepseek-v4-flash\": {\n \"aliases\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n }\n }\n },\n \"capabilities\": {\n \"mcp\": {\n \"enabled\": false,\n \"search\": {\n \"enabled\": false,\n \"mode\": \"auto\",\n \"autoThresholdToolCount\": 24,\n \"topKDefault\": 5,\n \"topKMax\": 10,\n \"minScore\": 0.15,\n \"bm25\": {\n \"k1\": 1.2,\n \"b\": 0.75\n }\n },\n \"servers\": {}\n },\n \"web\": {\n \"enabled\": false,\n \"fetchEnabled\": false,\n \"searchEnabled\": false,\n \"allowDomains\": [],\n \"denyDomains\": []\n },\n \"skills\": {\n \"enabled\": false,\n \"roots\": [],\n \"legacySkillMd\": true\n },\n \"subagents\": {\n \"enabled\": false,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"defaultPreset\": \"research_split\",\n \"maxParallel\": 2,\n \"maxChildRuns\": 4,\n \"maxTotalChildTokens\": 50000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 120000,\n \"workflowPresets\": {\n \"review_swarm\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 4,\n \"maxChildRuns\": 8,\n \"maxTotalChildTokens\": 80000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 90000\n },\n \"implementation_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 2,\n \"maxChildRuns\": 4,\n \"maxTotalChildTokens\": 70000,\n \"maxChildCostUsd\": 1.5,\n \"perAgentTimeoutMs\": 180000\n },\n \"research_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 3,\n \"maxChildRuns\": 6,\n \"maxTotalChildTokens\": 50000,\n \"maxChildCostUsd\": 1,\n \"perAgentTimeoutMs\": 120000\n },\n \"audit_split\": {\n \"enabled\": true,\n \"defaultModel\": \"deepseek-v4-flash\",\n \"maxParallel\": 3,\n \"maxChildRuns\": 6,\n \"maxTotalChildTokens\": 80000,\n \"maxChildCostUsd\": 1.5,\n \"perAgentTimeoutMs\": 150000\n }\n }\n },\n \"attachments\": {\n \"enabled\": false,\n \"maxImageBytes\": 5242880,\n \"maxImageDimension\": 4096,\n \"allowedMimeTypes\": [\"image/png\", \"image/jpeg\", \"image/webp\"],\n \"textFallbackMaxBase64Bytes\": 524288,\n \"textFallbackMaxImageDimension\": 1280,\n \"textFallbackPreferredMimeType\": \"image/webp\"\n },\n \"memory\": {\n \"enabled\": false,\n \"scopes\": [\"user\", \"workspace\", \"project\"],\n \"maxInjectedRecords\": 8\n }\n }\n}\n"}, "faa4db70da8261d37f47e6c62ec23c342c6ed69a": {"t": 1785761432.1371956, "n": 2954, "h": "3f10bdc1a98f4e3f", "c": "{\n \"serve\": {\n \"host\": \"127.0.0.1\",\n \"port\": 8899,\n \"dataDir\": \"~/.deepseekgui/kun\",\n \"runtimeToken\": \"\",\n \"apiKey\": \"\",\n \"baseUrl\": \"https://api.deepseek.com/beta\",\n \"model\": \"deepseek-v4-pro\",\n \"approvalPolicy\": \"auto\",\n \"sandboxMode\": \"workspace-write\",\n \"tokenEconomyMode\": false,\n \"tokenEconomy\": {\n \"enabled\": false,\n \"compressToolDescriptions\": true,\n \"compressToolResults\": true,\n \"conciseResponses\": true,\n \"historyHygiene\": {\n \"maxToolResultLines\": 320,\n \"maxToolResultBytes\": 32768,\n \"maxToolResultTokens\": 8000,\n \"maxToolArgumentStringBytes\": 8192,\n \"maxToolArgumentStringTokens\": 2000,\n \"maxArrayItems\": 80\n }\n },\n \"insecure\": false,\n \"storage\": {\n \"backend\": \"hybrid\"\n }\n },\n \"contextCompaction\": {\n \"defaultSoftThreshold\": 16000,\n \"defaultHardThreshold\": 24000,\n \"summaryMode\": \"heuristic\",\n \"summaryTimeoutMs\": 15000,\n \"summaryMaxTokens\": 1200,\n \"summaryInputMaxBytes\": 98304\n },\n \"models\": {\n \"profiles\": {\n \"deepseek-v4-pro\": {\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n },\n \"deepseek-v4-flash\": {\n \"aliases\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n }\n }\n },\n \"capabilities\": {\n \"mcp\": {\n \"enabled\": false,\n \"search\": {\n \"enabled\": false,\n \"mode\": \"auto\",\n \"autoThresholdToolCount\": 24,\n \"topKDefault\": 5,\n \"topKMax\": 10,\n \"minScore\": 0.15,\n \"bm25\": {\n \"k1\": 1.2,\n \"b\": 0.75\n }\n },\n \"servers\": {}\n },\n \"web\": {\n \"enabled\": false,\n \"fetchEnabled\": false,\n \"searchEnabled\": false,\n \"allowDomains\": [],\n \"denyDomains\": []\n },\n \"skills\": {\n \"enabled\": false,\n \"roots\": [],\n \"legacySkillMd\": true\n },\n \"subagents\": {\n \"enabled\": false,\n \"maxParallel\": 0,\n \"maxChildRuns\": 0\n },\n \"attachments\": {\n \"enabled\": false,\n \"maxImageBytes\": 5242880,\n \"maxImageDimension\": 4096,\n \"allowedMimeTypes\": [\"image/png\", \"image/jpeg\", \"image/webp\"],\n \"textFallbackMaxBase64Bytes\": 524288,\n \"textFallbackMaxImageDimension\": 1280,\n \"textFallbackPreferredMimeType\": \"image/webp\"\n },\n \"memory\": {\n \"enabled\": false,\n \"scopes\": [\"user\", \"workspace\", \"project\"],\n \"maxInjectedRecords\": 8\n }\n }\n}\n"}, "0c60469336ccc97eae89bc09c3500603df88bd16": {"t": 1785761433.3680112, "n": 2960, "h": "da624ded6814ffd2", "c": "{\n \"serve\": {\n \"host\": \"127.0.0.1\",\n \"port\": 8899,\n \"dataDir\": \"~/.deepseekgui/kun\",\n \"runtimeToken\": \"\",\n \"apiKey\": \"\",\n \"baseUrl\": \"https://api.deepseek.com/beta\",\n \"model\": \"deepseek-v4-pro\",\n \"approvalPolicy\": \"on-request\",\n \"sandboxMode\": \"workspace-write\",\n \"tokenEconomyMode\": false,\n \"tokenEconomy\": {\n \"enabled\": false,\n \"compressToolDescriptions\": true,\n \"compressToolResults\": true,\n \"conciseResponses\": true,\n \"historyHygiene\": {\n \"maxToolResultLines\": 320,\n \"maxToolResultBytes\": 32768,\n \"maxToolResultTokens\": 8000,\n \"maxToolArgumentStringBytes\": 8192,\n \"maxToolArgumentStringTokens\": 2000,\n \"maxArrayItems\": 80\n }\n },\n \"insecure\": false,\n \"storage\": {\n \"backend\": \"hybrid\"\n }\n },\n \"contextCompaction\": {\n \"defaultSoftThreshold\": 16000,\n \"defaultHardThreshold\": 24000,\n \"summaryMode\": \"heuristic\",\n \"summaryTimeoutMs\": 15000,\n \"summaryMaxTokens\": 1200,\n \"summaryInputMaxBytes\": 98304\n },\n \"models\": {\n \"profiles\": {\n \"deepseek-v4-pro\": {\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n },\n \"deepseek-v4-flash\": {\n \"aliases\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n \"contextWindowTokens\": 1000000,\n \"contextCompaction\": {\n \"softThreshold\": 980000,\n \"hardThreshold\": 990000\n },\n \"inputModalities\": [\"text\"],\n \"outputModalities\": [\"text\"],\n \"supportsToolCalling\": true,\n \"messageParts\": [\"text\"]\n }\n }\n },\n \"capabilities\": {\n \"mcp\": {\n \"enabled\": false,\n \"search\": {\n \"enabled\": false,\n \"mode\": \"auto\",\n \"autoThresholdToolCount\": 24,\n \"topKDefault\": 5,\n \"topKMax\": 10,\n \"minScore\": 0.15,\n \"bm25\": {\n \"k1\": 1.2,\n \"b\": 0.75\n }\n },\n \"servers\": {}\n },\n \"web\": {\n \"enabled\": false,\n \"fetchEnabled\": false,\n \"searchEnabled\": false,\n \"allowDomains\": [],\n \"denyDomains\": []\n },\n \"skills\": {\n \"enabled\": false,\n \"roots\": [],\n \"legacySkillMd\": true\n },\n \"subagents\": {\n \"enabled\": false,\n \"maxParallel\": 0,\n \"maxChildRuns\": 0\n },\n \"attachments\": {\n \"enabled\": false,\n \"maxImageBytes\": 5242880,\n \"maxImageDimension\": 4096,\n \"allowedMimeTypes\": [\"image/png\", \"image/jpeg\", \"image/webp\"],\n \"textFallbackMaxBase64Bytes\": 524288,\n \"textFallbackMaxImageDimension\": 1280,\n \"textFallbackPreferredMimeType\": \"image/webp\"\n },\n \"memory\": {\n \"enabled\": false,\n \"scopes\": [\"user\", \"workspace\", \"project\"],\n \"maxInjectedRecords\": 8\n }\n }\n}\n"}}, "src/main/services/write-inline-completion-service.test.ts": {"ab1332c56336d43f0780fc6a9fa1fe66582bb7e9": {"t": 1785746520.0576293, "n": 22964, "h": "3b9c7b122a569c25", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: defaultModelProviderSettings(),\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n apiKey: 'sk-test'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n schedule: defaultScheduleSettings(),\n guiUpdate: {\n channel: 'stable'\n },\n codePromptPrefix: '',\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('OpenCodex Desktop inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.agents.kun.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('OpenCodex Desktop inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('falls back to the General baseUrl and Kun model when write keeps defaults', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' fallback text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings()\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n settings.write.inlineCompletion.baseUrl = 'https://api.deepseek.com/beta'\n settings.write.inlineCompletion.model = 'deepseek-v4-flash'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-chat'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(url).toContain('/completions')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-chat'\n })\n })\n\n it('uses an explicit flash override when write disables model inheritance', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' explicit flash' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings({\n inheritModel: false,\n model: 'deepseek-v4-flash'\n })\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-flash'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'OpenCodex Desktop keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'OpenCodex Desktop',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'OpenCodex Desktop keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'OpenCodex Desktop keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'OpenCodex Desktop keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('OpenCodex Desktop inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'OpenCodex Desktop',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'OpenCodex Desktop',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n\n it('parses JSON action payloads', () => {\n expect(parseWriteInlineAction(JSON.stringify({ kind: 'long', text: 'Continue the paragraph.' }))).toEqual({\n kind: 'long',\n text: 'Continue the paragraph.'\n })\n expect(parseWriteInlineAction(JSON.stringify({ action: 'edit', replacement: 'Rewrite locally.' }), {\n editTarget: {\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite locally.',\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n })\n })\n\n it('parses XML-style action wrappers', () => {\n expect(parseWriteInlineAction('<short>next words</short>')).toEqual({\n kind: 'short',\n text: 'next words'\n })\n expect(parseWriteInlineAction('<long>Two sentences.\\nMaybe three.</long>')).toEqual({\n kind: 'long',\n text: 'Two sentences.\\nMaybe three.'\n })\n expect(parseWriteInlineAction('<edit>Replace this scope</edit>', {\n editTarget: {\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Replace this scope',\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n })\n })\n\n it('parses labeled plain-text fallbacks', () => {\n expect(parseWriteInlineAction('completion: next sentence')).toEqual({\n kind: 'short',\n text: 'next sentence'\n })\n expect(parseWriteInlineAction('long: A fuller continuation.')).toEqual({\n kind: 'long',\n text: 'A fuller continuation.'\n })\n expect(parseWriteInlineAction('edit: Rewrite this block', {\n editTarget: {\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite this block',\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n })\n })\n\n it('falls back to the requested mode for unstructured plain text', () => {\n expect(parseWriteInlineAction('Raw continuation text')).toEqual({\n kind: 'short',\n text: 'Raw continuation text'\n })\n expect(parseWriteInlineAction('Raw long continuation', { fallbackKind: 'long' })).toEqual({\n kind: 'long',\n text: 'Raw long continuation'\n })\n expect(parseWriteInlineAction('Raw edit replacement', {\n fallbackKind: 'edit',\n editTarget: {\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Raw edit replacement',\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n })\n })\n})\n"}, "3d7f114890551b5e97f9c5f3be45937862985ef9": {"t": 1785761431.871813, "n": 22964, "h": "3b9c7b122a569c25", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: defaultModelProviderSettings(),\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n apiKey: 'sk-test'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n schedule: defaultScheduleSettings(),\n guiUpdate: {\n channel: 'stable'\n },\n codePromptPrefix: '',\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('OpenCodex Desktop inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.agents.kun.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('OpenCodex Desktop inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('falls back to the General baseUrl and Kun model when write keeps defaults', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' fallback text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings()\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n settings.write.inlineCompletion.baseUrl = 'https://api.deepseek.com/beta'\n settings.write.inlineCompletion.model = 'deepseek-v4-flash'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-chat'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(url).toContain('/completions')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-chat'\n })\n })\n\n it('uses an explicit flash override when write disables model inheritance', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' explicit flash' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings({\n inheritModel: false,\n model: 'deepseek-v4-flash'\n })\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-flash'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'OpenCodex Desktop keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'OpenCodex Desktop',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'OpenCodex Desktop keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'OpenCodex Desktop keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'OpenCodex Desktop keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('OpenCodex Desktop inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'OpenCodex Desktop',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'OpenCodex Desktop',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n\n it('parses JSON action payloads', () => {\n expect(parseWriteInlineAction(JSON.stringify({ kind: 'long', text: 'Continue the paragraph.' }))).toEqual({\n kind: 'long',\n text: 'Continue the paragraph.'\n })\n expect(parseWriteInlineAction(JSON.stringify({ action: 'edit', replacement: 'Rewrite locally.' }), {\n editTarget: {\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite locally.',\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n })\n })\n\n it('parses XML-style action wrappers', () => {\n expect(parseWriteInlineAction('<short>next words</short>')).toEqual({\n kind: 'short',\n text: 'next words'\n })\n expect(parseWriteInlineAction('<long>Two sentences.\\nMaybe three.</long>')).toEqual({\n kind: 'long',\n text: 'Two sentences.\\nMaybe three.'\n })\n expect(parseWriteInlineAction('<edit>Replace this scope</edit>', {\n editTarget: {\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Replace this scope',\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n })\n })\n\n it('parses labeled plain-text fallbacks', () => {\n expect(parseWriteInlineAction('completion: next sentence')).toEqual({\n kind: 'short',\n text: 'next sentence'\n })\n expect(parseWriteInlineAction('long: A fuller continuation.')).toEqual({\n kind: 'long',\n text: 'A fuller continuation.'\n })\n expect(parseWriteInlineAction('edit: Rewrite this block', {\n editTarget: {\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite this block',\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n })\n })\n\n it('falls back to the requested mode for unstructured plain text', () => {\n expect(parseWriteInlineAction('Raw continuation text')).toEqual({\n kind: 'short',\n text: 'Raw continuation text'\n })\n expect(parseWriteInlineAction('Raw long continuation', { fallbackKind: 'long' })).toEqual({\n kind: 'long',\n text: 'Raw long continuation'\n })\n expect(parseWriteInlineAction('Raw edit replacement', {\n fallbackKind: 'edit',\n editTarget: {\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Raw edit replacement',\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n })\n })\n})\n"}, "c6a1dc3cad44079127354294f2d0d07db36d4c30": {"t": 1785761433.7116637, "n": 22914, "h": "e2ecd974ee304665", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: defaultModelProviderSettings(),\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n apiKey: 'sk-test'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n schedule: defaultScheduleSettings(),\n guiUpdate: {\n channel: 'stable'\n },\n codePromptPrefix: '',\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('DeepSeek GUI inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.agents.kun.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('DeepSeek GUI inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('falls back to the General baseUrl and Kun model when write keeps defaults', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' fallback text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings()\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n settings.write.inlineCompletion.baseUrl = 'https://api.deepseek.com/beta'\n settings.write.inlineCompletion.model = 'deepseek-v4-flash'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-chat'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(url).toContain('/completions')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-chat'\n })\n })\n\n it('uses an explicit flash override when write disables model inheritance', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' explicit flash' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings({\n inheritModel: false,\n model: 'deepseek-v4-flash'\n })\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-flash'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'DeepSeek GUI',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'DeepSeek GUI keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('DeepSeek GUI inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n\n it('parses JSON action payloads', () => {\n expect(parseWriteInlineAction(JSON.stringify({ kind: 'long', text: 'Continue the paragraph.' }))).toEqual({\n kind: 'long',\n text: 'Continue the paragraph.'\n })\n expect(parseWriteInlineAction(JSON.stringify({ action: 'edit', replacement: 'Rewrite locally.' }), {\n editTarget: {\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite locally.',\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n })\n })\n\n it('parses XML-style action wrappers', () => {\n expect(parseWriteInlineAction('<short>next words</short>')).toEqual({\n kind: 'short',\n text: 'next words'\n })\n expect(parseWriteInlineAction('<long>Two sentences.\\nMaybe three.</long>')).toEqual({\n kind: 'long',\n text: 'Two sentences.\\nMaybe three.'\n })\n expect(parseWriteInlineAction('<edit>Replace this scope</edit>', {\n editTarget: {\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Replace this scope',\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n })\n })\n\n it('parses labeled plain-text fallbacks', () => {\n expect(parseWriteInlineAction('completion: next sentence')).toEqual({\n kind: 'short',\n text: 'next sentence'\n })\n expect(parseWriteInlineAction('long: A fuller continuation.')).toEqual({\n kind: 'long',\n text: 'A fuller continuation.'\n })\n expect(parseWriteInlineAction('edit: Rewrite this block', {\n editTarget: {\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite this block',\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n })\n })\n\n it('falls back to the requested mode for unstructured plain text', () => {\n expect(parseWriteInlineAction('Raw continuation text')).toEqual({\n kind: 'short',\n text: 'Raw continuation text'\n })\n expect(parseWriteInlineAction('Raw long continuation', { fallbackKind: 'long' })).toEqual({\n kind: 'long',\n text: 'Raw long continuation'\n })\n expect(parseWriteInlineAction('Raw edit replacement', {\n fallbackKind: 'edit',\n editTarget: {\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Raw edit replacement',\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n })\n })\n})\n"}, "dacbc955a9770141838fcc1193bad40610738cb3": {"t": 1785761435.9755218, "n": 22835, "h": "0fca6b9d6ea15a14", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: defaultModelProviderSettings(),\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n apiKey: 'sk-test'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n schedule: defaultScheduleSettings(),\n guiUpdate: {\n channel: 'stable'\n },\n codePromptPrefix: '',\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('DeepSeek GUI inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.agents.kun.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('DeepSeek GUI inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('falls back to the General baseUrl and Kun model when write keeps defaults', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' fallback text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings()\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n settings.write.inlineCompletion.baseUrl = 'https://api.deepseek.com/beta'\n settings.write.inlineCompletion.model = 'deepseek-v4-flash'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-chat'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(url).toContain('/completions')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-chat'\n })\n })\n\n it('uses an explicit flash override when write disables model inheritance', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' explicit flash' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings({\n inheritModel: false,\n model: 'deepseek-v4-flash'\n })\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-flash'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'DeepSeek GUI',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'DeepSeek GUI keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('DeepSeek GUI inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n\n it('parses JSON action payloads', () => {\n expect(parseWriteInlineAction(JSON.stringify({ kind: 'long', text: 'Continue the paragraph.' }))).toEqual({\n kind: 'long',\n text: 'Continue the paragraph.'\n })\n expect(parseWriteInlineAction(JSON.stringify({ action: 'edit', replacement: 'Rewrite locally.' }), {\n editTarget: {\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite locally.',\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n })\n })\n\n it('parses XML-style action wrappers', () => {\n expect(parseWriteInlineAction('<short>next words</short>')).toEqual({\n kind: 'short',\n text: 'next words'\n })\n expect(parseWriteInlineAction('<long>Two sentences.\\nMaybe three.</long>')).toEqual({\n kind: 'long',\n text: 'Two sentences.\\nMaybe three.'\n })\n expect(parseWriteInlineAction('<edit>Replace this scope</edit>', {\n editTarget: {\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Replace this scope',\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n })\n })\n\n it('parses labeled plain-text fallbacks', () => {\n expect(parseWriteInlineAction('completion: next sentence')).toEqual({\n kind: 'short',\n text: 'next sentence'\n })\n expect(parseWriteInlineAction('long: A fuller continuation.')).toEqual({\n kind: 'long',\n text: 'A fuller continuation.'\n })\n expect(parseWriteInlineAction('edit: Rewrite this block', {\n editTarget: {\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite this block',\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n })\n })\n\n it('falls back to the requested mode for unstructured plain text', () => {\n expect(parseWriteInlineAction('Raw continuation text')).toEqual({\n kind: 'short',\n text: 'Raw continuation text'\n })\n expect(parseWriteInlineAction('Raw long continuation', { fallbackKind: 'long' })).toEqual({\n kind: 'long',\n text: 'Raw long continuation'\n })\n expect(parseWriteInlineAction('Raw edit replacement', {\n fallbackKind: 'edit',\n editTarget: {\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Raw edit replacement',\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n })\n })\n})\n"}, "056cd8d420f1af2b92aa731a9b3b09b303845be1": {"t": 1785761437.5795524, "n": 22888, "h": "02f5dd4e092202ae", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: defaultModelProviderSettings(),\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n apiKey: 'sk-test'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n schedule: defaultScheduleSettings(),\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('DeepSeek GUI inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.agents.kun.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('DeepSeek GUI inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('falls back to the General baseUrl and Kun model when write keeps defaults', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' fallback text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings()\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n settings.write.inlineCompletion.baseUrl = 'https://api.deepseek.com/beta'\n settings.write.inlineCompletion.model = 'deepseek-v4-flash'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-chat'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(url).toContain('/completions')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-chat'\n })\n })\n\n it('uses an explicit flash override when write disables model inheritance', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' explicit flash' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings({\n inheritModel: false,\n model: 'deepseek-v4-flash'\n })\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-flash'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'DeepSeek GUI',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'DeepSeek GUI keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('DeepSeek GUI inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n\n it('parses JSON action payloads', () => {\n expect(parseWriteInlineAction(JSON.stringify({ kind: 'long', text: 'Continue the paragraph.' }))).toEqual({\n kind: 'long',\n text: 'Continue the paragraph.'\n })\n expect(parseWriteInlineAction(JSON.stringify({ action: 'edit', replacement: 'Rewrite locally.' }), {\n editTarget: {\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite locally.',\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n })\n })\n\n it('parses XML-style action wrappers', () => {\n expect(parseWriteInlineAction('<short>next words</short>')).toEqual({\n kind: 'short',\n text: 'next words'\n })\n expect(parseWriteInlineAction('<long>Two sentences.\\nMaybe three.</long>')).toEqual({\n kind: 'long',\n text: 'Two sentences.\\nMaybe three.'\n })\n expect(parseWriteInlineAction('<edit>Replace this scope</edit>', {\n editTarget: {\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Replace this scope',\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n })\n })\n\n it('parses labeled plain-text fallbacks', () => {\n expect(parseWriteInlineAction('completion: next sentence')).toEqual({\n kind: 'short',\n text: 'next sentence'\n })\n expect(parseWriteInlineAction('long: A fuller continuation.')).toEqual({\n kind: 'long',\n text: 'A fuller continuation.'\n })\n expect(parseWriteInlineAction('edit: Rewrite this block', {\n editTarget: {\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite this block',\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n })\n })\n\n it('falls back to the requested mode for unstructured plain text', () => {\n expect(parseWriteInlineAction('Raw continuation text')).toEqual({\n kind: 'short',\n text: 'Raw continuation text'\n })\n expect(parseWriteInlineAction('Raw long continuation', { fallbackKind: 'long' })).toEqual({\n kind: 'long',\n text: 'Raw long continuation'\n })\n expect(parseWriteInlineAction('Raw edit replacement', {\n fallbackKind: 'edit',\n editTarget: {\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Raw edit replacement',\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n })\n })\n})\n"}, "b88432536f576b0ad8891e6d60d55503a2423537": {"t": 1785761439.463329, "n": 22809, "h": "0ae7a8e3481c90f9", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: defaultModelProviderSettings(),\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n apiKey: 'sk-test'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n schedule: defaultScheduleSettings(),\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('DeepSeek GUI inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.agents.kun.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('DeepSeek GUI inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('falls back to the General baseUrl and Kun model when write keeps defaults', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' fallback text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings()\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n settings.write.inlineCompletion.baseUrl = 'https://api.deepseek.com/beta'\n settings.write.inlineCompletion.model = 'deepseek-v4-flash'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-chat'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(url).toContain('/completions')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-chat'\n })\n })\n\n it('uses an explicit flash override when write disables model inheritance', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' explicit flash' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings({\n inheritModel: false,\n model: 'deepseek-v4-flash'\n })\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-flash'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'DeepSeek GUI',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'DeepSeek GUI keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('DeepSeek GUI inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n\n it('parses JSON action payloads', () => {\n expect(parseWriteInlineAction(JSON.stringify({ kind: 'long', text: 'Continue the paragraph.' }))).toEqual({\n kind: 'long',\n text: 'Continue the paragraph.'\n })\n expect(parseWriteInlineAction(JSON.stringify({ action: 'edit', replacement: 'Rewrite locally.' }), {\n editTarget: {\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite locally.',\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n })\n })\n\n it('parses XML-style action wrappers', () => {\n expect(parseWriteInlineAction('<short>next words</short>')).toEqual({\n kind: 'short',\n text: 'next words'\n })\n expect(parseWriteInlineAction('<long>Two sentences.\\nMaybe three.</long>')).toEqual({\n kind: 'long',\n text: 'Two sentences.\\nMaybe three.'\n })\n expect(parseWriteInlineAction('<edit>Replace this scope</edit>', {\n editTarget: {\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Replace this scope',\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n })\n })\n\n it('parses labeled plain-text fallbacks', () => {\n expect(parseWriteInlineAction('completion: next sentence')).toEqual({\n kind: 'short',\n text: 'next sentence'\n })\n expect(parseWriteInlineAction('long: A fuller continuation.')).toEqual({\n kind: 'long',\n text: 'A fuller continuation.'\n })\n expect(parseWriteInlineAction('edit: Rewrite this block', {\n editTarget: {\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite this block',\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n })\n })\n\n it('falls back to the requested mode for unstructured plain text', () => {\n expect(parseWriteInlineAction('Raw continuation text')).toEqual({\n kind: 'short',\n text: 'Raw continuation text'\n })\n expect(parseWriteInlineAction('Raw long continuation', { fallbackKind: 'long' })).toEqual({\n kind: 'long',\n text: 'Raw long continuation'\n })\n expect(parseWriteInlineAction('Raw edit replacement', {\n fallbackKind: 'edit',\n editTarget: {\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Raw edit replacement',\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n })\n })\n})\n"}, "0c60469336ccc97eae89bc09c3500603df88bd16": {"t": 1785761441.0080116, "n": 22725, "h": "36f33c1ee7f4561b", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: defaultModelProviderSettings(),\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n apiKey: 'sk-test'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n schedule: defaultScheduleSettings(),\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('DeepSeek GUI inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.agents.kun.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('DeepSeek GUI inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('falls back to the General baseUrl and Kun model when write keeps defaults', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' fallback text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings()\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n settings.write.inlineCompletion.baseUrl = 'https://api.deepseek.com/beta'\n settings.write.inlineCompletion.model = 'deepseek-v4-flash'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-chat'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(url).toContain('/completions')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-chat'\n })\n })\n\n it('uses an explicit flash override when write disables model inheritance', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' explicit flash' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const settings = createSettings({\n inheritModel: false,\n model: 'deepseek-v4-flash'\n })\n settings.provider.baseUrl = 'https://general.example/v1'\n settings.agents.kun.model = 'deepseek-chat'\n\n const result = await requestWriteInlineCompletion(settings, {\n ...createRequest(),\n model: ''\n })\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-flash'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toContain('https://general.example')\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'DeepSeek GUI',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'DeepSeek GUI keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('DeepSeek GUI inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n\n it('parses JSON action payloads', () => {\n expect(parseWriteInlineAction(JSON.stringify({ kind: 'long', text: 'Continue the paragraph.' }))).toEqual({\n kind: 'long',\n text: 'Continue the paragraph.'\n })\n expect(parseWriteInlineAction(JSON.stringify({ action: 'edit', replacement: 'Rewrite locally.' }), {\n editTarget: {\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite locally.',\n from: 3,\n to: 11,\n original: 'Old text',\n scopeKind: 'paragraph'\n })\n })\n\n it('parses XML-style action wrappers', () => {\n expect(parseWriteInlineAction('<short>next words</short>')).toEqual({\n kind: 'short',\n text: 'next words'\n })\n expect(parseWriteInlineAction('<long>Two sentences.\\nMaybe three.</long>')).toEqual({\n kind: 'long',\n text: 'Two sentences.\\nMaybe three.'\n })\n expect(parseWriteInlineAction('<edit>Replace this scope</edit>', {\n editTarget: {\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Replace this scope',\n from: 12,\n to: 20,\n original: 'old value',\n scopeKind: 'selection'\n })\n })\n\n it('parses labeled plain-text fallbacks', () => {\n expect(parseWriteInlineAction('completion: next sentence')).toEqual({\n kind: 'short',\n text: 'next sentence'\n })\n expect(parseWriteInlineAction('long: A fuller continuation.')).toEqual({\n kind: 'long',\n text: 'A fuller continuation.'\n })\n expect(parseWriteInlineAction('edit: Rewrite this block', {\n editTarget: {\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Rewrite this block',\n from: 1,\n to: 4,\n original: 'old',\n scopeKind: 'paragraph'\n })\n })\n\n it('falls back to the requested mode for unstructured plain text', () => {\n expect(parseWriteInlineAction('Raw continuation text')).toEqual({\n kind: 'short',\n text: 'Raw continuation text'\n })\n expect(parseWriteInlineAction('Raw long continuation', { fallbackKind: 'long' })).toEqual({\n kind: 'long',\n text: 'Raw long continuation'\n })\n expect(parseWriteInlineAction('Raw edit replacement', {\n fallbackKind: 'edit',\n editTarget: {\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Raw edit replacement',\n from: 8,\n to: 15,\n original: 'old text',\n scopeKind: 'selection'\n })\n })\n})\n"}, "cdb2ae09b274f75beee48d6b5a2266514a679689": {"t": 1785761444.0050333, "n": 17905, "h": "765f9ff8e896c80c", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n agentProvider: 'codewhale',\n agents: {\n codewhale: {\n binaryPath: '',\n port: 7878,\n autoStart: true,\n apiKey: 'sk-test',\n baseUrl: 'https://api.deepseek.com/beta',\n runtimeToken: '',\n extraCorsOrigins: [],\n approvalPolicy: 'auto',\n sandboxMode: 'workspace-write'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('DeepSeek GUI inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.agents.codewhale.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('DeepSeek GUI inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'DeepSeek GUI',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'DeepSeek GUI keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('DeepSeek GUI inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n})\n"}, "af6c07a10fef0f428371bd2e2fa2fe74a5ab7182": {"t": 1785761445.3365643, "n": 17861, "h": "a2ccc1bb27472842", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n parseWriteInlineAction,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n agentProvider: 'deepseek-runtime',\n deepseek: {\n binaryPath: '',\n port: 7878,\n autoStart: true,\n apiKey: 'sk-test',\n baseUrl: 'https://api.deepseek.com/beta',\n runtimeToken: '',\n extraCorsOrigins: [],\n approvalPolicy: 'auto',\n sandboxMode: 'workspace-write'\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n action: {\n kind: 'short',\n text: ' only a test'\n },\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n const body = JSON.parse(String(init.body)) as { prompt: string; suffix: string; max_tokens: number }\n expect(body).toMatchObject({\n model: 'deepseek-v4-flash',\n suffix: ' a test.',\n max_tokens: 64\n })\n expect(body.prompt).toContain('DeepSeek GUI inline completion')\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<SHORT')\n expect(body.prompt).toContain('<<<PREFIX')\n expect(body.prompt).toContain('<<<SUFFIX')\n expect(body.prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.deepseek.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n suffix: ' a test.',\n responseChars: 0\n })\n expect(debugEntries[0].prompt).toContain('DeepSeek GUI inline completion')\n expect(debugEntries[0].prompt.endsWith('# Draft\\n\\nThis is')).toBe(true)\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('Trigger hint: long')\n expect(body.prompt).toContain('paused for inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('records plain long completions from the FIM request', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA fuller continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings(), {\n ...createRequest(),\n mode: 'long'\n })\n\n expect(result).toMatchObject({\n ok: true,\n completion: '\\n\\nA fuller continuation.',\n action: {\n kind: 'long',\n text: '\\n\\nA fuller continuation.'\n },\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Return only the text to insert at the cursor')\n expect(body.prompt).not.toContain('<<<LONG')\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('Reference snippets from the same writing workspace')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('uses chat completions when the unified request may return an edit action', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n },\n recentEdits: [{\n source: 'user' as const,\n ageMs: 1_200,\n filePath: '/tmp/workspace/draft.md',\n from: 9,\n to: 21,\n deletedText: 'DeepSeek GUI',\n insertedText: 'Write mode',\n beforeContext: '',\n afterContext: ' keeps text editing local.'\n }]\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n completion: 'Write mode keeps text editing local.',\n action: {\n kind: 'edit',\n replacement: 'Write mode keeps text editing local.',\n from: 9,\n to: 47,\n original: 'DeepSeek GUI keeps text editing local.',\n scopeKind: 'paragraph'\n }\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n prompt?: string\n suffix?: string\n max_tokens: number\n thinking?: { type: string }\n }\n expect(body.max_tokens).toBe(320)\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.prompt).toBeUndefined()\n expect(body.suffix).toBeUndefined()\n expect(body.messages[1].content).toContain('Recent local edits in this file')\n expect(body.messages[1].content).toContain('Editable local scope if EDIT is the best action')\n expect(body.messages[1].content).toContain('<<<EDIT_SCOPE')\n })\n\n it('uses chat completions for explicit edit mode even without recent edit signals', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'edit',\n completion: 'Write mode keeps text editing local.'\n })\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/v1/chat/completions')\n const body = JSON.parse(String(init.body)) as {\n messages: Array<{ role: string; content: string }>\n suffix?: string\n thinking?: { type: string }\n }\n expect(body.suffix).toBeUndefined()\n expect(body.thinking).toEqual({ type: 'disabled' })\n expect(body.messages[1].content).toContain('Trigger hint: edit')\n expect(body.messages[1].content).toContain('<<<PREFIX')\n expect(body.messages[1].content).toContain('<<<SUFFIX')\n })\n\n it('does not send DeepSeek thinking controls for custom chat completion models', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({\n choices: [{\n message: {\n content: '<<<EDIT\\nWrite mode keeps text editing local.\\n>>>'\n }\n }]\n }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'gpt-4.1-mini',\n mode: 'edit' as const,\n editCandidate: {\n kind: 'paragraph' as const,\n from: 9,\n to: 47,\n startLine: 3,\n startColumn: 1,\n endLine: 3,\n endColumn: 38,\n original: 'DeepSeek GUI keeps text editing local.'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings({ longMaxTokens: 320 }), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'gpt-4.1-mini',\n mode: 'edit'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { thinking?: { type: string } }\n expect(body.thinking).toBeUndefined()\n })\n\n it('builds the unified action prompt without retrieval snippets when none are supplied', () => {\n const request = createRequest()\n\n const prompt = buildWriteInlineCompletionPrompt(request, null)\n expect(prompt).toContain('DeepSeek GUI inline completion')\n expect(prompt).toContain('<<<PREFIX')\n expect(prompt).toContain('<<<SUFFIX')\n expect(prompt).not.toContain('<<<SHORT')\n expect(prompt).not.toContain('Reference snippets from the same writing workspace')\n expect(prompt.endsWith(request.prefix)).toBe(true)\n })\n})\n\ndescribe('parseWriteInlineAction', () => {\n it('parses TextIDE-style marked short, long, and edit blocks', () => {\n expect(parseWriteInlineAction('<<<SHORT\\n next words\\n>>>')).toEqual({\n kind: 'short',\n text: ' next words'\n })\n expect(parseWriteInlineAction('<<<LONG\\n\\nA fuller continuation.\\n>>>')).toEqual({\n kind: 'long',\n text: '\\nA fuller continuation.'\n })\n expect(parseWriteInlineAction('<<<EDIT\\nWrite mode\\n>>>', {\n editTarget: {\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n }\n })).toEqual({\n kind: 'edit',\n replacement: 'Write mode',\n from: 9,\n to: 21,\n original: 'DeepSeek GUI',\n scopeKind: 'selection'\n })\n })\n\n it('suppresses echoed boundary-marker prompts', () => {\n expect(parseWriteInlineAction('<<<PREFIX\\nThis is\\n>>>\\n<<<SUFFIX\\n a test.\\n>>>')).toEqual({\n kind: 'short',\n text: ''\n })\n })\n})\n"}, "df545ec8b148fda7d563d894d5bf09f79928b307": {"t": 1785761446.6210775, "n": 9711, "h": "d9b55d646b304bfc", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n agentProvider: 'deepseek-runtime',\n deepseek: {\n binaryPath: '',\n port: 7878,\n autoStart: true,\n apiKey: 'sk-test',\n baseUrl: 'https://api.deepseek.com/beta',\n runtimeToken: '',\n extraCorsOrigins: [],\n approvalPolicy: 'auto',\n sandboxMode: 'workspace-write'\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash',\n prompt: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n max_tokens: 64\n })\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Inline completion is disabled.',\n completion: '',\n responseChars: 0\n })\n })\n\n it('records missing API key failures in the debug log', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n const settings = createSettings()\n settings.deepseek.apiKey = ''\n\n const result = await requestWriteInlineCompletion(settings, createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Missing API key for inline completion.' })\n expect(fetchMock).not.toHaveBeenCalled()\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: false,\n errorMessage: 'Missing API key for inline completion.',\n mode: 'short',\n prompt: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n responseChars: 0\n })\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('inline completion mode: long inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('DeepSeek GUI inline completion references')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('leaves the prompt untouched when retrieval has no snippets', () => {\n const request = createRequest()\n\n expect(buildWriteInlineCompletionPrompt(request, null)).toBe(request.prefix)\n })\n})\n"}, "04eb10f65703c8e4f96d7a0fe1921bb0a7bb59b4": {"t": 1785761448.1789684, "n": 8657, "h": "f1825897d1c27729", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n clearWriteInlineCompletionDebugEntries,\n listWriteInlineCompletionDebugEntries,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n agentProvider: 'deepseek-runtime',\n deepseek: {\n binaryPath: '',\n port: 7878,\n autoStart: true,\n apiKey: 'sk-test',\n baseUrl: 'https://api.deepseek.com/beta',\n runtimeToken: '',\n extraCorsOrigins: [],\n approvalPolicy: 'auto',\n sandboxMode: 'workspace-write'\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n clearWriteInlineCompletionDebugEntries()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash',\n prompt: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n max_tokens: 64\n })\n const debugEntries = listWriteInlineCompletionDebugEntries()\n expect(debugEntries).toHaveLength(1)\n expect(debugEntries[0]).toMatchObject({\n ok: true,\n completion: ' only a test',\n mode: 'short',\n model: 'deepseek-v4-flash'\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('inline completion mode: long inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('DeepSeek GUI inline completion references')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('leaves the prompt untouched when retrieval has no snippets', () => {\n const request = createRequest()\n\n expect(buildWriteInlineCompletionPrompt(request, null)).toBe(request.prefix)\n })\n})\n"}, "2cc5f5088b0f091c4c30ad04b192857185aae3a6": {"t": 1785761449.4459243, "n": 8270, "h": "db6cf1973c6bd0c7", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { mkdir, mkdtemp, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport {\n defaultClawSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport {\n buildWriteInlineCompletionPrompt,\n requestWriteInlineCompletion\n} from './write-inline-completion-service'\nimport { clearWriteRetrievalCache } from './write-retrieval-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n agentProvider: 'deepseek-runtime',\n deepseek: {\n binaryPath: '',\n port: 7878,\n autoStart: true,\n apiKey: 'sk-test',\n baseUrl: 'https://api.deepseek.com/beta',\n runtimeToken: '',\n extraCorsOrigins: [],\n approvalPolicy: 'auto',\n sandboxMode: 'workspace-write'\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n clearWriteRetrievalCache()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n model: 'deepseek-v4-flash',\n mode: 'short'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash',\n prompt: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n max_tokens: 64\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n })\n\n it('preserves an explicit pro completion model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-pro'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-pro'\n })\n })\n\n it('uses the long-completion prompt and token budget for inspiration mode', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: '\\n\\nA longer continuation.' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n mode: 'long' as const,\n suffix: '',\n context: {\n ...createRequest().context,\n currentLineSuffix: ''\n }\n }\n const result = await requestWriteInlineCompletion(\n createSettings({ longMaxTokens: 320 }),\n request\n )\n\n expect(result).toMatchObject({\n ok: true,\n mode: 'long'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string; max_tokens: number }\n expect(body.max_tokens).toBe(320)\n expect(body.prompt).toContain('inline completion mode: long inspiration')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('adds BM25 retrieval snippets to the FIM prompt when workspace context is available', async () => {\n const workspaceRoot = await mkdtemp(join(tmpdir(), 'ds-gui-write-rag-'))\n await mkdir(join(workspaceRoot, 'notes'), { recursive: true })\n await writeFile(\n join(workspaceRoot, 'notes', 'retrieval.md'),\n [\n '# RAG notes',\n '',\n 'BM25 keyword retrieval keeps inline completion grounded in project terminology.',\n 'Use retrieved snippets as reference-only context for local text completion.'\n ].join('\\n'),\n 'utf8'\n )\n await writeFile(\n join(workspaceRoot, 'draft.md'),\n '# Draft\\n\\nBM25 keyword',\n 'utf8'\n )\n\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' retrieval can improve continuity' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n workspaceRoot,\n currentFilePath: join(workspaceRoot, 'draft.md'),\n prefix: '# Draft\\n\\nBM25 keyword',\n suffix: '',\n context: {\n ...createRequest().context,\n currentLinePrefix: 'BM25 keyword',\n currentLineSuffix: '',\n previousNonEmptyLine: '# Draft'\n },\n preview: {\n local: 'BM25 keyword',\n documentTail: '# Draft BM25 keyword'\n }\n }\n\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({ ok: true })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n const body = JSON.parse(String(init.body)) as { prompt: string }\n expect(body.prompt).toContain('DeepSeek GUI inline completion references')\n expect(body.prompt).toContain('notes/retrieval.md')\n expect(body.prompt).toContain('BM25 keyword retrieval keeps inline completion grounded')\n expect(body.prompt.endsWith(request.prefix)).toBe(true)\n })\n\n it('leaves the prompt untouched when retrieval has no snippets', () => {\n const request = createRequest()\n\n expect(buildWriteInlineCompletionPrompt(request, null)).toBe(request.prefix)\n })\n})\n"}, "a932ba88998872a5a0dd913776ee4c69e6a7ac87": {"t": 1785761450.9207723, "n": 4705, "h": "030cc287206e43ff", "c": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport {\n defaultClawSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../../shared/app-settings'\nimport type { WriteInlineCompletionRequest } from '../../shared/write-inline-completion'\nimport { requestWriteInlineCompletion } from './write-inline-completion-service'\n\nfunction createSettings(patch: Partial<AppSettingsV1['write']['inlineCompletion']> = {}): AppSettingsV1 {\n const write = defaultWriteSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n agentProvider: 'deepseek-runtime',\n deepseek: {\n binaryPath: '',\n port: 7878,\n autoStart: true,\n apiKey: 'sk-test',\n baseUrl: 'https://api.deepseek.com/beta',\n runtimeToken: '',\n extraCorsOrigins: [],\n approvalPolicy: 'auto',\n sandboxMode: 'workspace-write'\n },\n workspaceRoot: '/tmp/workspace',\n log: {\n enabled: true,\n retentionDays: 2\n },\n notifications: {\n turnComplete: true\n },\n write: {\n ...write,\n inlineCompletion: {\n ...write.inlineCompletion,\n ...patch\n }\n },\n guiUpdate: {\n channel: 'stable'\n },\n claw: defaultClawSettings()\n }\n}\n\nfunction createRequest(): WriteInlineCompletionRequest {\n return {\n prefix: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n currentFilePath: '/tmp/workspace/draft.md',\n cursor: {\n line: 3,\n column: 7\n },\n context: {\n language: 'markdown',\n currentLinePrefix: 'This is',\n currentLineSuffix: ' a test.',\n previousLine: '',\n previousNonEmptyLine: '# Draft',\n nextLine: '',\n indentation: '',\n signals: {\n list: false,\n quote: false,\n heading: false,\n table: false,\n atLineEnd: false,\n endsWithSentencePunctuation: false,\n previousLineEndsWithSentencePunctuation: false,\n prefersNewLineCompletion: false,\n paragraphBreakOpportunity: false\n }\n },\n policy: {\n name: 'precision-inline-v2',\n instruction: 'Return only inserted text.',\n acceptanceCriteria: ['Keep it short.'],\n rejectionCriteria: ['Do not ramble.']\n },\n preview: {\n local: 'This is',\n documentTail: '# Draft This is'\n },\n model: 'deepseek-v4-flash'\n }\n}\n\nafterEach(() => {\n vi.unstubAllGlobals()\n})\n\ndescribe('requestWriteInlineCompletion', () => {\n it('calls DeepSeek FIM completions directly instead of chat completions', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' only a test' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ maxTokens: 64 }), createRequest())\n\n expect(result).toEqual({\n ok: true,\n completion: ' only a test',\n model: 'deepseek-v4-flash'\n })\n expect(fetchMock).toHaveBeenCalledTimes(1)\n const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(url).toBe('https://api.deepseek.com/beta/completions')\n expect(url).not.toContain('/chat/completions')\n expect(init.headers).toMatchObject({\n Authorization: 'Bearer sk-test'\n })\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash',\n prompt: '# Draft\\n\\nThis is',\n suffix: ' a test.',\n max_tokens: 64\n })\n })\n\n it('does not request the API when inline completion is disabled', async () => {\n const fetchMock = vi.fn()\n vi.stubGlobal('fetch', fetchMock)\n\n const result = await requestWriteInlineCompletion(createSettings({ enabled: false }), createRequest())\n\n expect(result).toEqual({ ok: false, message: 'Inline completion is disabled.' })\n expect(fetchMock).not.toHaveBeenCalled()\n })\n\n it('normalizes the legacy completion default to the flash model', async () => {\n const fetchMock = vi.fn(async () =>\n new Response(JSON.stringify({ choices: [{ text: ' flash text' }] }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n })\n )\n vi.stubGlobal('fetch', fetchMock)\n\n const request = {\n ...createRequest(),\n model: 'deepseek-v4-pro'\n }\n const result = await requestWriteInlineCompletion(createSettings(), request)\n\n expect(result).toMatchObject({\n ok: true,\n model: 'deepseek-v4-flash'\n })\n const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]\n expect(JSON.parse(String(init.body))).toMatchObject({\n model: 'deepseek-v4-flash'\n })\n })\n})\n"}}, "src/main/upstream-models.test.ts": {"ab1332c56336d43f0780fc6a9fa1fe66582bb7e9": {"t": 1785746527.385549, "n": 8116, "h": "befa675e5790a79a", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n DEFAULT_MODEL_ENDPOINT_FORMAT,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport {\n fetchModelProviderCatalog,\n fetchUpstreamModelIds,\n parseOpenRouterModelCatalog,\n readConfiguredKunModelIds\n} from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n endpointFormat: DEFAULT_MODEL_ENDPOINT_FORMAT,\n models: ['custom-provider-model'],\n catalogModels: []\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' },\n codePromptPrefix: ''\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n models: {\n profiles: {\n 'deepseek-v4-flash': {\n aliases: ['deepseek-chat', 'deepseek-reasoner']\n }\n }\n }\n }),\n 'utf8'\n )\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n }),\n expect.objectContaining({\n providerId: 'deepseek',\n label: 'DeepSeek',\n modelIds: expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner'])\n })\n ]))\n }\n })\n\n it('maps OpenRouter catalog rows into durable model metadata', () => {\n const catalog = parseOpenRouterModelCatalog({\n data: [{\n id: 'openai/gpt-4.1-mini',\n name: 'OpenAI: GPT-4.1 Mini',\n context_length: 1047576,\n architecture: {\n input_modalities: ['text'],\n output_modalities: ['text'],\n tokenizer: 'GPT'\n },\n pricing: {\n prompt: '0.0000004',\n completion: '0.0000016',\n input_cache_read: '0.0000001',\n input_cache_write: '0.0000004'\n },\n supported_parameters: [\n 'tools',\n 'tool_choice',\n 'reasoning',\n 'include_reasoning',\n 'response_format'\n ]\n }]\n })\n\n expect(catalog).toEqual([{\n id: 'openai/gpt-4.1-mini',\n name: 'OpenAI: GPT-4.1 Mini',\n providerId: 'openrouter',\n contextLength: 1047576,\n tokenizer: 'GPT',\n pricingUsdPerMillion: {\n input: 0.4,\n output: 1.6,\n cacheRead: 0.1,\n cacheWrite: 0.4\n },\n capabilities: {\n inputModalities: ['text'],\n outputModalities: ['text'],\n reasoning: true,\n tools: true,\n recommendedUse: ['coding', 'review', 'research']\n }\n }])\n })\n\n it('fetches OpenRouter models from the first-class catalog endpoint', async () => {\n const calls: Array<{ url: string; headers: Record<string, string> }> = []\n const fetchImpl = async (url: URL | RequestInfo, init?: RequestInit): Promise<Response> => {\n calls.push({\n url: String(url),\n headers: Object.fromEntries(new Headers(init?.headers).entries())\n })\n return new Response(JSON.stringify({\n data: [{\n id: 'anthropic/claude-sonnet-4.5',\n name: 'Anthropic: Claude Sonnet 4.5',\n context_length: 200000,\n architecture: {\n input_modalities: ['text'],\n output_modalities: ['text'],\n tokenizer: 'Claude'\n },\n pricing: {\n prompt: '0.000003',\n completion: '0.000015'\n },\n supported_parameters: ['tools', 'reasoning']\n }]\n }), {\n status: 200,\n headers: { 'content-type': 'application/json' }\n })\n }\n\n const result = await fetchModelProviderCatalog({\n provider: {\n id: 'openrouter',\n name: 'OpenRouter',\n apiKey: 'sk-openrouter-secret',\n baseUrl: 'https://openrouter.ai/api/v1',\n endpointFormat: DEFAULT_MODEL_ENDPOINT_FORMAT,\n models: []\n },\n fetchImpl,\n nowIso: () => '2026-06-09T00:00:00.000Z'\n })\n\n expect(result).toMatchObject({\n ok: true,\n providerId: 'openrouter',\n catalogUpdatedAt: '2026-06-09T00:00:00.000Z'\n })\n if (result.ok) {\n expect(result.catalogModels[0]).toMatchObject({\n id: 'anthropic/claude-sonnet-4.5',\n providerId: 'openrouter',\n pricingUsdPerMillion: {\n input: 3,\n output: 15\n }\n })\n }\n expect(calls).toEqual([{\n url: 'https://openrouter.ai/api/v1/models',\n headers: expect.objectContaining({\n accept: 'application/json',\n authorization: 'Bearer sk-openrouter-secret'\n })\n }])\n })\n\n it('redacts provider secrets from catalog fetch errors', async () => {\n const fetchImpl = async (): Promise<Response> =>\n new Response('Authorization: Bearer sk-openrouter-secret token=sk-openrouter-secret', {\n status: 401,\n headers: { 'content-type': 'text/plain' }\n })\n\n const result = await fetchModelProviderCatalog({\n provider: {\n id: 'openrouter',\n name: 'OpenRouter',\n apiKey: 'sk-openrouter-secret',\n baseUrl: 'https://openrouter.ai/api/v1',\n endpointFormat: DEFAULT_MODEL_ENDPOINT_FORMAT,\n models: []\n },\n fetchImpl\n })\n\n expect(result).toMatchObject({ ok: false, providerId: 'openrouter' })\n if (!result.ok) {\n expect(result.message).toContain('<redacted>')\n expect(result.message).not.toContain('sk-openrouter-secret')\n }\n })\n})\n"}, "7120350c4879848998ad65c19d63e75a7514b4a8": {"t": 1785761430.1876943, "n": 8116, "h": "befa675e5790a79a", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n DEFAULT_MODEL_ENDPOINT_FORMAT,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport {\n fetchModelProviderCatalog,\n fetchUpstreamModelIds,\n parseOpenRouterModelCatalog,\n readConfiguredKunModelIds\n} from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n endpointFormat: DEFAULT_MODEL_ENDPOINT_FORMAT,\n models: ['custom-provider-model'],\n catalogModels: []\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' },\n codePromptPrefix: ''\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n models: {\n profiles: {\n 'deepseek-v4-flash': {\n aliases: ['deepseek-chat', 'deepseek-reasoner']\n }\n }\n }\n }),\n 'utf8'\n )\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n }),\n expect.objectContaining({\n providerId: 'deepseek',\n label: 'DeepSeek',\n modelIds: expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner'])\n })\n ]))\n }\n })\n\n it('maps OpenRouter catalog rows into durable model metadata', () => {\n const catalog = parseOpenRouterModelCatalog({\n data: [{\n id: 'openai/gpt-4.1-mini',\n name: 'OpenAI: GPT-4.1 Mini',\n context_length: 1047576,\n architecture: {\n input_modalities: ['text'],\n output_modalities: ['text'],\n tokenizer: 'GPT'\n },\n pricing: {\n prompt: '0.0000004',\n completion: '0.0000016',\n input_cache_read: '0.0000001',\n input_cache_write: '0.0000004'\n },\n supported_parameters: [\n 'tools',\n 'tool_choice',\n 'reasoning',\n 'include_reasoning',\n 'response_format'\n ]\n }]\n })\n\n expect(catalog).toEqual([{\n id: 'openai/gpt-4.1-mini',\n name: 'OpenAI: GPT-4.1 Mini',\n providerId: 'openrouter',\n contextLength: 1047576,\n tokenizer: 'GPT',\n pricingUsdPerMillion: {\n input: 0.4,\n output: 1.6,\n cacheRead: 0.1,\n cacheWrite: 0.4\n },\n capabilities: {\n inputModalities: ['text'],\n outputModalities: ['text'],\n reasoning: true,\n tools: true,\n recommendedUse: ['coding', 'review', 'research']\n }\n }])\n })\n\n it('fetches OpenRouter models from the first-class catalog endpoint', async () => {\n const calls: Array<{ url: string; headers: Record<string, string> }> = []\n const fetchImpl = async (url: URL | RequestInfo, init?: RequestInit): Promise<Response> => {\n calls.push({\n url: String(url),\n headers: Object.fromEntries(new Headers(init?.headers).entries())\n })\n return new Response(JSON.stringify({\n data: [{\n id: 'anthropic/claude-sonnet-4.5',\n name: 'Anthropic: Claude Sonnet 4.5',\n context_length: 200000,\n architecture: {\n input_modalities: ['text'],\n output_modalities: ['text'],\n tokenizer: 'Claude'\n },\n pricing: {\n prompt: '0.000003',\n completion: '0.000015'\n },\n supported_parameters: ['tools', 'reasoning']\n }]\n }), {\n status: 200,\n headers: { 'content-type': 'application/json' }\n })\n }\n\n const result = await fetchModelProviderCatalog({\n provider: {\n id: 'openrouter',\n name: 'OpenRouter',\n apiKey: 'sk-openrouter-secret',\n baseUrl: 'https://openrouter.ai/api/v1',\n endpointFormat: DEFAULT_MODEL_ENDPOINT_FORMAT,\n models: []\n },\n fetchImpl,\n nowIso: () => '2026-06-09T00:00:00.000Z'\n })\n\n expect(result).toMatchObject({\n ok: true,\n providerId: 'openrouter',\n catalogUpdatedAt: '2026-06-09T00:00:00.000Z'\n })\n if (result.ok) {\n expect(result.catalogModels[0]).toMatchObject({\n id: 'anthropic/claude-sonnet-4.5',\n providerId: 'openrouter',\n pricingUsdPerMillion: {\n input: 3,\n output: 15\n }\n })\n }\n expect(calls).toEqual([{\n url: 'https://openrouter.ai/api/v1/models',\n headers: expect.objectContaining({\n accept: 'application/json',\n authorization: 'Bearer sk-openrouter-secret'\n })\n }])\n })\n\n it('redacts provider secrets from catalog fetch errors', async () => {\n const fetchImpl = async (): Promise<Response> =>\n new Response('Authorization: Bearer sk-openrouter-secret token=sk-openrouter-secret', {\n status: 401,\n headers: { 'content-type': 'text/plain' }\n })\n\n const result = await fetchModelProviderCatalog({\n provider: {\n id: 'openrouter',\n name: 'OpenRouter',\n apiKey: 'sk-openrouter-secret',\n baseUrl: 'https://openrouter.ai/api/v1',\n endpointFormat: DEFAULT_MODEL_ENDPOINT_FORMAT,\n models: []\n },\n fetchImpl\n })\n\n expect(result).toMatchObject({ ok: false, providerId: 'openrouter' })\n if (!result.ok) {\n expect(result.message).toContain('<redacted>')\n expect(result.message).not.toContain('sk-openrouter-secret')\n }\n })\n})\n"}, "55c6ec8a92f1febd6fb698ad191957901e0aaaaf": {"t": 1785761431.440356, "n": 7916, "h": "3bda8d8da40a4e58", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport {\n fetchModelProviderCatalog,\n fetchUpstreamModelIds,\n parseOpenRouterModelCatalog,\n readConfiguredKunModelIds\n} from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n models: ['custom-provider-model'],\n catalogModels: []\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' },\n codePromptPrefix: ''\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n models: {\n profiles: {\n 'deepseek-v4-flash': {\n aliases: ['deepseek-chat', 'deepseek-reasoner']\n }\n }\n }\n }),\n 'utf8'\n )\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n }),\n expect.objectContaining({\n providerId: 'deepseek',\n label: 'DeepSeek',\n modelIds: expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner'])\n })\n ]))\n }\n })\n\n it('maps OpenRouter catalog rows into durable model metadata', () => {\n const catalog = parseOpenRouterModelCatalog({\n data: [{\n id: 'openai/gpt-4.1-mini',\n name: 'OpenAI: GPT-4.1 Mini',\n context_length: 1047576,\n architecture: {\n input_modalities: ['text'],\n output_modalities: ['text'],\n tokenizer: 'GPT'\n },\n pricing: {\n prompt: '0.0000004',\n completion: '0.0000016',\n input_cache_read: '0.0000001',\n input_cache_write: '0.0000004'\n },\n supported_parameters: [\n 'tools',\n 'tool_choice',\n 'reasoning',\n 'include_reasoning',\n 'response_format'\n ]\n }]\n })\n\n expect(catalog).toEqual([{\n id: 'openai/gpt-4.1-mini',\n name: 'OpenAI: GPT-4.1 Mini',\n providerId: 'openrouter',\n contextLength: 1047576,\n tokenizer: 'GPT',\n pricingUsdPerMillion: {\n input: 0.4,\n output: 1.6,\n cacheRead: 0.1,\n cacheWrite: 0.4\n },\n capabilities: {\n inputModalities: ['text'],\n outputModalities: ['text'],\n reasoning: true,\n tools: true,\n recommendedUse: ['coding', 'review', 'research']\n }\n }])\n })\n\n it('fetches OpenRouter models from the first-class catalog endpoint', async () => {\n const calls: Array<{ url: string; headers: Record<string, string> }> = []\n const fetchImpl = async (url: URL | RequestInfo, init?: RequestInit): Promise<Response> => {\n calls.push({\n url: String(url),\n headers: Object.fromEntries(new Headers(init?.headers).entries())\n })\n return new Response(JSON.stringify({\n data: [{\n id: 'anthropic/claude-sonnet-4.5',\n name: 'Anthropic: Claude Sonnet 4.5',\n context_length: 200000,\n architecture: {\n input_modalities: ['text'],\n output_modalities: ['text'],\n tokenizer: 'Claude'\n },\n pricing: {\n prompt: '0.000003',\n completion: '0.000015'\n },\n supported_parameters: ['tools', 'reasoning']\n }]\n }), {\n status: 200,\n headers: { 'content-type': 'application/json' }\n })\n }\n\n const result = await fetchModelProviderCatalog({\n provider: {\n id: 'openrouter',\n name: 'OpenRouter',\n apiKey: 'sk-openrouter-secret',\n baseUrl: 'https://openrouter.ai/api/v1',\n models: []\n },\n fetchImpl,\n nowIso: () => '2026-06-09T00:00:00.000Z'\n })\n\n expect(result).toMatchObject({\n ok: true,\n providerId: 'openrouter',\n catalogUpdatedAt: '2026-06-09T00:00:00.000Z'\n })\n if (result.ok) {\n expect(result.catalogModels[0]).toMatchObject({\n id: 'anthropic/claude-sonnet-4.5',\n providerId: 'openrouter',\n pricingUsdPerMillion: {\n input: 3,\n output: 15\n }\n })\n }\n expect(calls).toEqual([{\n url: 'https://openrouter.ai/api/v1/models',\n headers: expect.objectContaining({\n accept: 'application/json',\n authorization: 'Bearer sk-openrouter-secret'\n })\n }])\n })\n\n it('redacts provider secrets from catalog fetch errors', async () => {\n const fetchImpl = async (): Promise<Response> =>\n new Response('Authorization: Bearer sk-openrouter-secret token=sk-openrouter-secret', {\n status: 401,\n headers: { 'content-type': 'text/plain' }\n })\n\n const result = await fetchModelProviderCatalog({\n provider: {\n id: 'openrouter',\n name: 'OpenRouter',\n apiKey: 'sk-openrouter-secret',\n baseUrl: 'https://openrouter.ai/api/v1',\n models: []\n },\n fetchImpl\n })\n\n expect(result).toMatchObject({ ok: false, providerId: 'openrouter' })\n if (!result.ok) {\n expect(result.message).toContain('<redacted>')\n expect(result.message).not.toContain('sk-openrouter-secret')\n }\n })\n})\n"}, "c6a1dc3cad44079127354294f2d0d07db36d4c30": {"t": 1785761433.1410575, "n": 3813, "h": "75212f9337bff556", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport { fetchUpstreamModelIds, readConfiguredKunModelIds } from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n models: ['custom-provider-model']\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' },\n codePromptPrefix: ''\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n models: {\n profiles: {\n 'deepseek-v4-flash': {\n aliases: ['deepseek-chat', 'deepseek-reasoner']\n }\n }\n }\n }),\n 'utf8'\n )\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n }),\n expect.objectContaining({\n providerId: 'deepseek',\n label: 'DeepSeek',\n modelIds: expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner'])\n })\n ]))\n }\n })\n})\n"}, "dacbc955a9770141838fcc1193bad40610738cb3": {"t": 1785761434.4932613, "n": 3734, "h": "b8583e8530f128da", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport { fetchUpstreamModelIds, readConfiguredKunModelIds } from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n models: ['custom-provider-model']\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' },\n codePromptPrefix: ''\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n models: {\n profiles: {\n 'deepseek-v4-flash': {\n aliases: ['deepseek-chat', 'deepseek-reasoner']\n }\n }\n }\n }),\n 'utf8'\n )\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n }),\n expect.objectContaining({\n providerId: 'deepseek',\n label: 'DeepSeek',\n modelIds: expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner'])\n })\n ]))\n }\n })\n})\n"}, "056cd8d420f1af2b92aa731a9b3b09b303845be1": {"t": 1785761436.007246, "n": 3787, "h": "0b52cd1c99ce77a9", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKeyboardShortcuts,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport { fetchUpstreamModelIds, readConfiguredKunModelIds } from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n models: ['custom-provider-model']\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n keyboardShortcuts: defaultKeyboardShortcuts(),\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' }\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n models: {\n profiles: {\n 'deepseek-v4-flash': {\n aliases: ['deepseek-chat', 'deepseek-reasoner']\n }\n }\n }\n }),\n 'utf8'\n )\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n }),\n expect.objectContaining({\n providerId: 'deepseek',\n label: 'DeepSeek',\n modelIds: expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner'])\n })\n ]))\n }\n })\n})\n"}, "b88432536f576b0ad8891e6d60d55503a2423537": {"t": 1785761437.384989, "n": 3708, "h": "9ca8b60ab68d071a", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport { fetchUpstreamModelIds, readConfiguredKunModelIds } from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n models: ['custom-provider-model']\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n appBehavior: { openAtLogin: false, startMinimized: false, closeToTray: false },\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' }\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n models: {\n profiles: {\n 'deepseek-v4-flash': {\n aliases: ['deepseek-chat', 'deepseek-reasoner']\n }\n }\n }\n }),\n 'utf8'\n )\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n }),\n expect.objectContaining({\n providerId: 'deepseek',\n label: 'DeepSeek',\n modelIds: expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner'])\n })\n ]))\n }\n })\n})\n"}, "a2707a2fd992af158c60b5a0f017e973a4aafb4a": {"t": 1785761440.1095278, "n": 3624, "h": "79abe58fea18c02f", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport { fetchUpstreamModelIds, readConfiguredKunModelIds } from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n models: ['custom-provider-model']\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' }\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n models: {\n profiles: {\n 'deepseek-v4-flash': {\n aliases: ['deepseek-chat', 'deepseek-reasoner']\n }\n }\n }\n }),\n 'utf8'\n )\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n }),\n expect.objectContaining({\n providerId: 'deepseek',\n label: 'DeepSeek',\n modelIds: expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner'])\n })\n ]))\n }\n })\n})\n"}, "0c60469336ccc97eae89bc09c3500603df88bd16": {"t": 1785761441.3559895, "n": 3104, "h": "deea5ba33b8e48c4", "c": "import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdtempSync } from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n defaultClawSettings,\n defaultKunRuntimeSettings,\n defaultModelProviderSettings,\n defaultScheduleSettings,\n defaultWriteSettings,\n type AppSettingsV1\n} from '../shared/app-settings'\nimport { fetchUpstreamModelIds, readConfiguredKunModelIds } from './upstream-models'\n\nfunction settings(dataDir: string, model = 'settings-model'): AppSettingsV1 {\n const provider = defaultModelProviderSettings()\n return {\n version: 1,\n locale: 'en',\n theme: 'system',\n uiFontScale: 'small',\n provider: {\n ...provider,\n providers: [\n ...provider.providers,\n {\n id: 'custom-provider',\n name: 'Custom Provider',\n apiKey: 'sk-custom',\n baseUrl: 'https://custom.example/v1',\n models: ['custom-provider-model']\n }\n ]\n },\n agents: {\n kun: {\n ...defaultKunRuntimeSettings(),\n dataDir,\n model,\n providerId: 'custom-provider'\n }\n },\n workspaceRoot: '/tmp/workspace',\n log: { enabled: false, retentionDays: 7 },\n notifications: { turnComplete: true },\n write: defaultWriteSettings(),\n claw: defaultClawSettings(),\n schedule: defaultScheduleSettings(),\n guiUpdate: { channel: 'stable' }\n }\n}\n\ndescribe('upstream model picker list', () => {\n it('includes Kun config model profiles, aliases, and the configured agent model', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n await mkdir(dataDir, { recursive: true })\n await writeFile(\n join(dataDir, 'config.json'),\n JSON.stringify({\n contextCompaction: {\n modelProfiles: {\n 'legacy-model': {}\n }\n },\n models: {\n profiles: {\n 'custom-model': {\n aliases: ['vendor/custom-model']\n }\n }\n }\n }),\n 'utf8'\n )\n\n const ids = await readConfiguredKunModelIds(settings(dataDir))\n\n expect(ids).toEqual(expect.arrayContaining([\n 'auto',\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'settings-model',\n 'legacy-model',\n 'custom-model',\n 'vendor/custom-model'\n ]))\n })\n\n it('falls back to configured model ids when upstream cannot be queried', async () => {\n const dataDir = mkdtempSync(join(tmpdir(), 'deepseek-gui-models-'))\n const result = await fetchUpstreamModelIds(settings(dataDir, 'local-only-model'), '')\n\n expect(result).toMatchObject({ ok: true })\n if (result.ok) {\n expect(result.modelIds).toContain('local-only-model')\n expect(result.modelIds).toContain('custom-provider-model')\n expect(result.modelGroups).toEqual(expect.arrayContaining([\n expect.objectContaining({\n providerId: 'custom-provider',\n label: 'Custom Provider',\n modelIds: expect.arrayContaining(['custom-provider-model'])\n })\n ]))\n }\n })\n})\n"}}, "src/shared/app-settings-kun.ts": {"ab1332c56336d43f0780fc6a9fa1fe66582bb7e9": {"t": 1786068460.5919387, "n": 46594, "h": "0a51095a509943aa", "c": "import {\n DEFAULT_APPROVAL_POLICY,\n DEFAULT_DEEPSEEK_BASE_URL,\n DEFAULT_KUN_DATA_DIR,\n DEFAULT_KUN_MODEL,\n DEFAULT_KUN_PORT,\n DEFAULT_MODEL_ENDPOINT_FORMAT,\n DEFAULT_SANDBOX_MODE,\n normalizeModelEndpointFormat,\n defaultKunHookSettings,\n type AppSettingsV1,\n type KunCheckpointSettingsV1,\n type KunContextCompactionSettingsV1,\n type KunHistoryHygieneSettingsV1,\n type KunHookSettingsV1,\n type KunHookTrustEntryV1,\n type KunMcpSearchSettingsV1,\n type KunRemoteRunnersSettingsV1,\n type KunRuntimeTuningSettingsV1,\n type KunRuntimeSettingsPatchV1,\n type KunRuntimeSettingsV1,\n type KunSettingsEnvelopePatchV1,\n type KunSettingsEnvelopeV1,\n type KunAutomationAuditLogSettingsV1,\n type KunAutomationPermissionModeV1,\n type KunAutomationPermissionsV1,\n type KunAutomationSettingsV1,\n type KunAutomationsSettingsV1,\n type KunGoalAutomationSettingsV1,\n type KunGoalEvalBudgetV1,\n type KunLoopAutomationSettingsV1,\n type KunTerminalSettingsV1,\n type KunStorageSettingsV1,\n type KunSubagentSettingsV1,\n type KunSubagentWorkflowPresetIdV1,\n type KunSubagentWorkflowPresetSettingsV1,\n type KunTokenEconomySettingsV1,\n type RemoteRunnerAuditEntryV1,\n type RemoteRunnerHostConfigV1,\n type RemoteRunnerSettingsV1,\n type RemoteRunnerTrustedPathV1,\n type UserAgentStackCliStatusV1,\n type UserAgentStackMcpServerV1,\n type UserAgentStackProfileV1,\n type UserAgentStackSkillRootV1,\n type UserAgentStackValidationErrorV1,\n type ModelProviderSettingsV1,\n type ApprovalPolicy,\n type SandboxMode\n} from './app-settings-types'\nimport {\n normalizeModelProviderSettings,\n resolveKunRuntimeSettings\n} from './app-settings-provider'\nimport { compactStrings } from './app-settings-normalizers'\n\nconst LEGACY_COREAGENT_DATA_DIR = '~/.deepseekgui/coreagent'\nconst LEGACY_KUN_DATA_DIR = '~/.deepseekgui/kun'\nconst LEGACY_KUN_DEFAULT_MODEL = 'deepseek-chat'\nconst LEGACY_LOCAL_HTTP_DEFAULT_PORT = 7878\nconst LEGACY_DEEPSEEK_GUI_KUN_DEFAULT_PORT = 8899\nconst DEFAULT_KUN_CHILD_MODEL = 'deepseek-v4-flash'\nconst DEFAULT_KUN_AUTOMATION_ALLOWED_HOSTS = ['localhost', '127.0.0.1', '::1']\nconst SUBAGENT_WORKFLOW_PRESET_IDS = [\n 'review_swarm',\n 'implementation_split',\n 'research_split',\n 'audit_split'\n] as const\n\ntype LegacyLocalHttpRuntimeSettingsV1 = {\n binaryPath: string\n port: number\n autoStart: boolean\n apiKey: string\n baseUrl: string\n runtimeToken: string\n extraCorsOrigins: string[]\n approvalPolicy: ApprovalPolicy\n sandboxMode: SandboxMode\n}\n\ntype LegacyReasoningEffort = 'low' | 'medium' | 'high' | 'max'\ntype LegacyReasoningEditMode = 'review' | 'auto' | 'yolo' | 'plan'\n\ntype LegacyReasoningRuntimeSettingsV1 = {\n binaryPath: string\n autoStart: boolean\n apiKey: string\n baseUrl: string\n model: string\n reasoningEffort: LegacyReasoningEffort\n editMode: LegacyReasoningEditMode\n}\n\n/**\n * Kun runtime settings. Mirrors the `kun serve` CLI\n * options. It is the only active agent settings object the GUI\n * stores after legacy settings have been migrated.\n */\nfunction legacyLocalHttpRuntimeDefaults(port = 7878): LegacyLocalHttpRuntimeSettingsV1 {\n return {\n binaryPath: '',\n port,\n autoStart: true,\n apiKey: '',\n baseUrl: DEFAULT_DEEPSEEK_BASE_URL,\n runtimeToken: '',\n extraCorsOrigins: ['http://localhost:5173', 'http://127.0.0.1:5173'],\n approvalPolicy: DEFAULT_APPROVAL_POLICY,\n sandboxMode: 'workspace-write'\n }\n}\n\nfunction legacyReasoningRuntimeDefaults(): LegacyReasoningRuntimeSettingsV1 {\n return {\n binaryPath: '',\n autoStart: true,\n apiKey: '',\n baseUrl: DEFAULT_DEEPSEEK_BASE_URL,\n model: LEGACY_KUN_DEFAULT_MODEL,\n reasoningEffort: 'medium',\n editMode: 'auto'\n }\n}\n\nexport function defaultKunRemoteRunnersSettings(): KunRemoteRunnersSettingsV1 {\n return {\n enabled: false,\n hosts: [],\n dataPolicy: {\n defaultAllowed: ['thread_metadata', 'redacted_progress', 'approval_metadata', 'audit_metadata', 'workspace_label'],\n consentRequired: ['selected_file_excerpt', 'diff_excerpt', 'terminal_excerpt', 'screenshot', 'browser_evidence', 'full_prompt', 'full_assistant_output'],\n never: ['source_file', 'raw_terminal_stream', 'browser_cookies', 'api_keys', 'oauth_tokens', 'mcp_credentials', 'env_values', 'keychain_material']\n },\n auditLog: [],\n maxAuditEntries: 500\n }\n}\n\nexport function defaultKunRuntimeSettings(\n port = DEFAULT_KUN_PORT\n): KunRuntimeSettingsV1 {\n return {\n binaryPath: '',\n port,\n autoStart: true,\n apiKey: '',\n baseUrl: '',\n providerId: '',\n endpointFormat: DEFAULT_MODEL_ENDPOINT_FORMAT,\n runtimeToken: '',\n dataDir: DEFAULT_KUN_DATA_DIR,\n model: DEFAULT_KUN_MODEL,\n approvalPolicy: DEFAULT_APPROVAL_POLICY,\n sandboxMode: DEFAULT_SANDBOX_MODE,\n tokenEconomyMode: false,\n tokenEconomy: defaultKunTokenEconomySettings(),\n insecure: false,\n mcpSearch: defaultKunMcpSearchSettings(),\n storage: defaultKunStorageSettings(),\n contextCompaction: defaultKunContextCompactionSettings(),\n runtimeTuning: defaultKunRuntimeTuningSettings(),\n userAgentStack: defaultUserAgentStackProfile(),\n subagents: defaultKunSubagentSettings(),\n automation: defaultKunAutomationSettings(),\n automations: defaultKunAutomationsSettings(),\n terminal: defaultKunTerminalSettings(),\n checkpoints: defaultKunCheckpointSettings(),\n hooks: defaultKunHookSettings(),\n remoteRunners: defaultKunRemoteRunnersSettings()\n }\n}\n\nexport function defaultKunTerminalSettings(): KunTerminalSettingsV1 {\n return {\n enabled: true\n }\n}\n\nexport function defaultKunCheckpointSettings(): KunCheckpointSettingsV1 {\n return {\n maxPerThread: 20,\n maxTotal: 200,\n autoBeforeMutation: true\n }\n}\n\nexport function defaultKunAutomationSettings(): KunAutomationSettingsV1 {\n return {\n enabled: false,\n browserWorkbenchEnabled: true,\n localDevOnly: true,\n allowedHosts: [...DEFAULT_KUN_AUTOMATION_ALLOWED_HOSTS],\n permissions: {\n browserNavigation: 'ask',\n browserInteraction: 'ask',\n screenshots: 'ask',\n localFileAccess: 'deny',\n appControl: 'deny'\n },\n auditLog: {\n enabled: true,\n maxEntries: 500\n }\n }\n}\n\nexport function defaultKunGoalEvalBudget(): KunGoalEvalBudgetV1 {\n return {\n maxIterations: 20,\n maxTokensPerEval: 512,\n maxCostUsdPerEval: 0.01,\n totalMaxIterations: 200,\n totalMaxTokens: 25_000,\n totalMaxCostUsd: 0.5\n }\n}\n\nexport function defaultKunGoalAutomationSettings(): KunGoalAutomationSettingsV1 {\n return {\n enabled: true,\n model: 'deepseek-v4-flash',\n maxContinuationTurns: 50,\n blockedRetryAfterTurns: 3,\n budget: defaultKunGoalEvalBudget()\n }\n}\n\nexport function defaultKunLoopAutomationSettings(): KunLoopAutomationSettingsV1 {\n return {\n enabled: true,\n defaultModel: 'deepseek-v4-pro',\n maxConcurrentLoops: 5,\n minIntervalMinutes: 1,\n requireProjectId: true\n }\n}\n\nexport function defaultKunAutomationsSettings(): KunAutomationsSettingsV1 {\n return {\n goal: defaultKunGoalAutomationSettings(),\n loop: defaultKunLoopAutomationSettings()\n }\n}\n\nexport function defaultKunSubagentSettings(): KunSubagentSettingsV1 {\n return {\n enabled: false,\n defaultModel: DEFAULT_KUN_CHILD_MODEL,\n defaultPreset: 'research_split',\n maxParallel: 2,\n maxChildRuns: 4,\n maxTotalChildTokens: 50_000,\n maxChildCostUsd: 1,\n perAgentTimeoutMs: 120_000,\n workflowPresets: {\n review_swarm: {\n id: 'review_swarm',\n enabled: true,\n label: 'Review swarm',\n defaultModel: DEFAULT_KUN_CHILD_MODEL,\n maxParallel: 4,\n maxChildRuns: 8,\n maxTotalChildTokens: 80_000,\n maxChildCostUsd: 1,\n perAgentTimeoutMs: 90_000\n },\n implementation_split: {\n id: 'implementation_split',\n enabled: true,\n label: 'Implementation split',\n defaultModel: DEFAULT_KUN_CHILD_MODEL,\n maxParallel: 2,\n maxChildRuns: 4,\n maxTotalChildTokens: 70_000,\n maxChildCostUsd: 1.5,\n perAgentTimeoutMs: 180_000\n },\n research_split: {\n id: 'research_split',\n enabled: true,\n label: 'Research split',\n defaultModel: DEFAULT_KUN_CHILD_MODEL,\n maxParallel: 3,\n maxChildRuns: 6,\n maxTotalChildTokens: 50_000,\n maxChildCostUsd: 1,\n perAgentTimeoutMs: 120_000\n },\n audit_split: {\n id: 'audit_split',\n enabled: true,\n label: 'Audit split',\n defaultModel: DEFAULT_KUN_CHILD_MODEL,\n maxParallel: 3,\n maxChildRuns: 6,\n maxTotalChildTokens: 80_000,\n maxChildCostUsd: 1.5,\n perAgentTimeoutMs: 150_000\n }\n }\n }\n}\n\nexport function defaultUserAgentStackProfile(): UserAgentStackProfileV1 {\n const profile = {\n enabled: true,\n importedAt: '',\n refreshedAt: '',\n sourcePaths: [],\n skillRoots: [],\n mcpServers: [],\n cli: [],\n redactedPreviewJson: '',\n validationErrors: []\n }\n return {\n ...profile,\n redactedPreviewJson: JSON.stringify(profile, null, 2)\n }\n}\n\nexport function defaultKunMcpSearchSettings(): KunMcpSearchSettingsV1 {\n return {\n enabled: false,\n mode: 'auto',\n autoThresholdToolCount: 24,\n topKDefault: 5,\n topKMax: 10,\n minScore: 0.15\n }\n}\n\nexport function defaultKunTokenEconomySettings(): KunTokenEconomySettingsV1 {\n return {\n enabled: false,\n compressToolDescriptions: true,\n compressToolResults: true,\n conciseResponses: true,\n historyHygiene: defaultKunHistoryHygieneSettings()\n }\n}\n\nexport function defaultKunHistoryHygieneSettings(): KunHistoryHygieneSettingsV1 {\n return {\n maxToolResultLines: 320,\n maxToolResultBytes: 32 * 1024,\n maxToolResultTokens: 8_000,\n maxToolArgumentStringBytes: 8 * 1024,\n maxToolArgumentStringTokens: 2_000,\n maxArrayItems: 80\n }\n}\n\nexport function defaultKunStorageSettings(): KunStorageSettingsV1 {\n return {\n backend: 'hybrid',\n sqlitePath: ''\n }\n}\n\nexport function defaultKunContextCompactionSettings(): KunContextCompactionSettingsV1 {\n return {\n defaultSoftThreshold: 16_000,\n defaultHardThreshold: 24_000,\n summaryMode: 'heuristic',\n summaryTimeoutMs: 15_000,\n summaryMaxTokens: 1_200,\n summaryInputMaxBytes: 96 * 1024\n }\n}\n\nexport function defaultKunRuntimeTuningSettings(): KunRuntimeTuningSettingsV1 {\n return {\n toolStorm: {\n enabled: true,\n windowSize: 8,\n threshold: 3\n },\n toolArgumentRepair: {\n maxStringBytes: 512 * 1024\n }\n }\n}\n\nexport function getKunRuntimeSettings(\n settings: AppSettingsV1\n): KunRuntimeSettingsV1 {\n const raw = (settings as { agents?: { kun?: Partial<KunRuntimeSettingsV1> } }).agents?.kun\n return mergeKunRuntimeSettings(defaultKunRuntimeSettings(), raw)\n}\n\nexport function kunSettingsEnvelope(\n kun: KunRuntimeSettingsV1\n): KunSettingsEnvelopeV1 {\n return { kun }\n}\n\nexport function kunSettingsPatch(\n kun: KunRuntimeSettingsPatchV1 | undefined\n): KunSettingsEnvelopePatchV1 {\n return kun ? { kun } : {}\n}\n\nexport function mergeKunRuntimeSettings(\n current: KunRuntimeSettingsV1,\n patch: KunRuntimeSettingsPatchV1 | undefined\n): KunRuntimeSettingsV1 {\n const currentMcpSearch = normalizeKunMcpSearchSettings(current.mcpSearch)\n const nextMcpSearch = normalizeKunMcpSearchSettings({\n ...currentMcpSearch,\n ...(patch?.mcpSearch ?? {})\n })\n const currentTokenEconomy = normalizeKunTokenEconomySettings(\n current.tokenEconomy,\n current.tokenEconomyMode\n )\n const patchedTokenEconomy = normalizeKunTokenEconomySettings({\n ...currentTokenEconomy,\n ...(patch?.tokenEconomy ?? {}),\n historyHygiene: {\n ...currentTokenEconomy.historyHygiene,\n ...(patch?.tokenEconomy?.historyHygiene ?? {})\n }\n }, currentTokenEconomy.enabled)\n const tokenEconomyEnabled = typeof patch?.tokenEconomy?.enabled === 'boolean'\n ? patch.tokenEconomy.enabled\n : typeof patch?.tokenEconomyMode === 'boolean'\n ? patch.tokenEconomyMode\n : patchedTokenEconomy.enabled\n const nextTokenEconomy = {\n ...patchedTokenEconomy,\n enabled: tokenEconomyEnabled\n }\n const currentStorage = normalizeKunStorageSettings(current.storage)\n const nextStorage = normalizeKunStorageSettings({\n ...currentStorage,\n ...(patch?.storage ?? {})\n })\n const currentContextCompaction = normalizeKunContextCompactionSettings(current.contextCompaction)\n const nextContextCompaction = normalizeKunContextCompactionSettings({\n ...currentContextCompaction,\n ...(patch?.contextCompaction ?? {})\n })\n const currentRuntimeTuning = normalizeKunRuntimeTuningSettings(current.runtimeTuning)\n const nextRuntimeTuning = normalizeKunRuntimeTuningSettings({\n ...currentRuntimeTuning,\n ...(patch?.runtimeTuning\n ? {\n toolStorm: {\n ...currentRuntimeTuning.toolStorm,\n ...(patch.runtimeTuning.toolStorm ?? {})\n },\n toolArgumentRepair: {\n ...currentRuntimeTuning.toolArgumentRepair,\n ...(patch.runtimeTuning.toolArgumentRepair ?? {})\n }\n }\n : {})\n })\n const currentUserAgentStack = normalizeUserAgentStackProfile(current.userAgentStack)\n const nextUserAgentStack = normalizeUserAgentStackProfile({\n ...currentUserAgentStack,\n ...(patch?.userAgentStack ?? {})\n })\n const currentSubagents = normalizeKunSubagentSettings(current.subagents)\n const mergedSubagentWorkflowPresets = {} as Record<KunSubagentWorkflowPresetIdV1, KunSubagentWorkflowPresetSettingsV1>\n for (const id of SUBAGENT_WORKFLOW_PRESET_IDS) {\n mergedSubagentWorkflowPresets[id] = {\n ...currentSubagents.workflowPresets[id],\n ...(patch?.subagents?.workflowPresets?.[id] ?? {})\n }\n }\n const nextSubagents = normalizeKunSubagentSettings({\n ...currentSubagents,\n ...(patch?.subagents ?? {}),\n workflowPresets: mergedSubagentWorkflowPresets\n })\n const currentAutomation = normalizeKunAutomationSettings(current.automation)\n const nextAutomation = normalizeKunAutomationSettings({\n ...currentAutomation,\n ...(patch?.automation ?? {}),\n permissions: {\n ...currentAutomation.permissions,\n ...(patch?.automation?.permissions ?? {})\n },\n auditLog: {\n ...currentAutomation.auditLog,\n ...(patch?.automation?.auditLog ?? {})\n }\n })\n const currentAutomations = normalizeKunAutomationsSettings(current.automations)\n const nextAutomations = normalizeKunAutomationsSettings({\n goal: {\n ...currentAutomations.goal,\n ...(patch?.automations?.goal ?? {})\n },\n loop: {\n ...currentAutomations.loop,\n ...(patch?.automations?.loop ?? {})\n }\n })\n const nextTerminal = normalizeKunTerminalSettings({\n ...current.terminal,\n ...(patch?.terminal ?? {})\n })\n const currentCheckpoints = normalizeKunCheckpointSettings(current.checkpoints)\n const nextCheckpoints = normalizeKunCheckpointSettings({\n ...currentCheckpoints,\n ...(patch?.checkpoints ?? {})\n })\n const currentHooks = normalizeKunHookSettings(current.hooks)\n const nextHooks = normalizeKunHookSettings({\n ...currentHooks,\n ...(patch?.hooks ?? {}),\n trustedHooks: {\n ...currentHooks.trustedHooks,\n ...(patch?.hooks?.trustedHooks\n ? Object.fromEntries(\n Object.entries(patch.hooks.trustedHooks).map(([id, entry]) => [\n id,\n { ...currentHooks.trustedHooks[id], ...entry } as KunHookTrustEntryV1\n ])\n )\n : {})\n },\n auditLog: patch?.hooks?.auditLog ?? currentHooks.auditLog\n })\n const currentRemoteRunners = normalizeRemoteRunnersSettings(current.remoteRunners)\n const nextRemoteRunners = normalizeRemoteRunnersSettings({\n ...currentRemoteRunners,\n ...(patch?.remoteRunners ?? {}),\n hosts: mergeRemoteRunnerHosts(currentRemoteRunners.hosts, patch?.remoteRunners?.hosts ?? []),\n auditLog: mergeRemoteRunnerAuditLog(\n currentRemoteRunners.auditLog,\n patch?.remoteRunners?.auditLog ?? [],\n patch?.remoteRunners?.maxAuditEntries ?? currentRemoteRunners.maxAuditEntries\n )\n })\n return {\n ...current,\n ...(patch ?? {}),\n tokenEconomyMode: nextTokenEconomy.enabled,\n endpointFormat: normalizeModelEndpointFormat(patch?.endpointFormat ?? current.endpointFormat),\n tokenEconomy: nextTokenEconomy,\n mcpSearch: nextMcpSearch,\n storage: nextStorage,\n contextCompaction: nextContextCompaction,\n runtimeTuning: nextRuntimeTuning,\n userAgentStack: nextUserAgentStack,\n subagents: nextSubagents,\n automation: nextAutomation,\n automations: nextAutomations,\n terminal: nextTerminal,\n checkpoints: nextCheckpoints,\n hooks: nextHooks,\n remoteRunners: nextRemoteRunners\n }\n}\n\nfunction normalizeKunHookSettings(\n input: Partial<KunHookSettingsV1> | undefined\n): KunHookSettingsV1 {\n const defaults = defaultKunHookSettings()\n return {\n enabled: input?.enabled === true,\n trustedHooks: input?.trustedHooks ?? defaults.trustedHooks,\n defaultTimeoutMs: boundedPositiveInt(input?.defaultTimeoutMs, defaults.defaultTimeoutMs, 120_000),\n maxOutputBytes: boundedPositiveInt(input?.maxOutputBytes, defaults.maxOutputBytes, 8 * 1024 * 1024),\n maxAuditEvents: boundedPositiveInt(input?.maxAuditEvents, defaults.maxAuditEvents, 10_000),\n auditLog: Array.isArray(input?.auditLog) ? input.auditLog.slice(0, defaults.maxAuditEvents) : defaults.auditLog\n }\n}\n\nfunction normalizeKunTerminalSettings(\n input: Partial<KunTerminalSettingsV1> | undefined\n): KunTerminalSettingsV1 {\n const defaults = defaultKunTerminalSettings()\n return {\n enabled: input?.enabled !== false\n }\n}\n\nfunction normalizeKunCheckpointSettings(\n input: Partial<KunCheckpointSettingsV1> | undefined\n): KunCheckpointSettingsV1 {\n const defaults = defaultKunCheckpointSettings()\n return {\n maxPerThread: boundedPositiveInt(input?.maxPerThread, defaults.maxPerThread, 1000),\n maxTotal: boundedPositiveInt(input?.maxTotal, defaults.maxTotal, 10000),\n autoBeforeMutation: input?.autoBeforeMutation !== false\n }\n}\n\nfunction normalizeKunAutomationSettings(\n input: Partial<KunAutomationSettingsV1> | undefined\n): KunAutomationSettingsV1 {\n const defaults = defaultKunAutomationSettings()\n return {\n enabled: input?.enabled === true,\n browserWorkbenchEnabled: input?.browserWorkbenchEnabled !== false,\n localDevOnly: input?.localDevOnly !== false,\n allowedHosts: normalizeAutomationAllowedHosts(input?.allowedHosts),\n permissions: normalizeAutomationPermissions(input?.permissions),\n auditLog: normalizeAutomationAuditLogSettings(input?.auditLog)\n }\n}\n\nfunction normalizeKunAutomationsSettings(\n input: Partial<KunAutomationsSettingsV1> | undefined\n): KunAutomationsSettingsV1 {\n const defaults = defaultKunAutomationsSettings()\n return {\n goal: normalizeKunGoalAutomationSettings(input?.goal),\n loop: normalizeKunLoopAutomationSettings(input?.loop)\n }\n}\n\nfunction normalizeKunGoalAutomationSettings(\n input: Partial<KunGoalAutomationSettingsV1> | undefined\n): KunGoalAutomationSettingsV1 {\n const defaults = defaultKunGoalAutomationSettings()\n return {\n enabled: input?.enabled !== false,\n model: (input?.model ?? defaults.model).trim() || defaults.model,\n maxContinuationTurns: boundedPositiveInt(input?.maxContinuationTurns, defaults.maxContinuationTurns, 500),\n blockedRetryAfterTurns: boundedNonNegativeInt(input?.blockedRetryAfterTurns, defaults.blockedRetryAfterTurns, 100),\n budget: normalizeKunGoalEvalBudget(input?.budget)\n }\n}\n\nfunction normalizeKunGoalEvalBudget(\n input: Partial<KunGoalEvalBudgetV1> | undefined\n): KunGoalEvalBudgetV1 {\n const defaults = defaultKunGoalEvalBudget()\n return {\n maxIterations: boundedPositiveInt(input?.maxIterations, defaults.maxIterations, 100),\n maxTokensPerEval: boundedPositiveInt(input?.maxTokensPerEval, defaults.maxTokensPerEval, 65536),\n maxCostUsdPerEval: boundedPositiveFloat(input?.maxCostUsdPerEval, defaults.maxCostUsdPerEval, 10),\n totalMaxIterations: boundedPositiveInt(input?.totalMaxIterations, defaults.totalMaxIterations, 10000),\n totalMaxTokens: boundedPositiveInt(input?.totalMaxTokens, defaults.totalMaxTokens, 10_000_000),\n totalMaxCostUsd: boundedPositiveFloat(input?.totalMaxCostUsd, defaults.totalMaxCostUsd, 100)\n }\n}\n\nfunction normalizeKunLoopAutomationSettings(\n input: Partial<KunLoopAutomationSettingsV1> | undefined\n): KunLoopAutomationSettingsV1 {\n const defaults = defaultKunLoopAutomationSettings()\n return {\n enabled: input?.enabled !== false,\n defaultModel: (input?.defaultModel ?? defaults.defaultModel).trim() || defaults.defaultModel,\n maxConcurrentLoops: boundedPositiveInt(input?.maxConcurrentLoops, defaults.maxConcurrentLoops, 50),\n minIntervalMinutes: boundedPositiveInt(input?.minIntervalMinutes, defaults.minIntervalMinutes, 1440),\n requireProjectId: input?.requireProjectId !== false\n }\n}\n\nfunction normalizeAutomationPermissions(\n input: Partial<KunAutomationPermissionsV1> | undefined\n): KunAutomationPermissionsV1 {\n const defaults = defaultKunAutomationSettings().permissions\n return {\n browserNavigation: normalizeAutomationPermissionMode(input?.browserNavigation, defaults.browserNavigation),\n browserInteraction: normalizeAutomationPermissionMode(input?.browserInteraction, defaults.browserInteraction),\n screenshots: normalizeAutomationPermissionMode(input?.screenshots, defaults.screenshots),\n localFileAccess: normalizeAutomationPermissionMode(input?.localFileAccess, defaults.localFileAccess),\n appControl: normalizeAutomationPermissionMode(input?.appControl, defaults.appControl)\n }\n}\n\nfunction normalizeAutomationAuditLogSettings(\n input: Partial<KunAutomationAuditLogSettingsV1> | undefined\n): KunAutomationAuditLogSettingsV1 {\n const defaults = defaultKunAutomationSettings().auditLog\n return {\n enabled: input?.enabled !== false,\n maxEntries: boundedPositiveInt(input?.maxEntries, defaults.maxEntries, 10_000)\n }\n}\n\nfunction normalizeAutomationPermissionMode(\n value: unknown,\n fallback: KunAutomationPermissionModeV1\n): KunAutomationPermissionModeV1 {\n return value === 'deny' || value === 'ask' || value === 'allow' ? value : fallback\n}\n\nfunction normalizeAutomationAllowedHosts(values: unknown): string[] {\n const seen = new Set<string>()\n const out: string[] = []\n const rawValues = Array.isArray(values) && values.length > 0\n ? values\n : DEFAULT_KUN_AUTOMATION_ALLOWED_HOSTS\n for (const value of rawValues) {\n if (typeof value !== 'string') continue\n const host = value.trim().toLowerCase().replace(/^\\[/, '').replace(/\\]$/, '')\n if (!host || seen.has(host)) continue\n seen.add(host)\n out.push(host)\n if (out.length >= 128) break\n }\n return out.length > 0 ? out : [...DEFAULT_KUN_AUTOMATION_ALLOWED_HOSTS]\n}\n\nfunction normalizeKunSubagentSettings(\n input: Partial<KunSubagentSettingsV1> | undefined\n): KunSubagentSettingsV1 {\n const defaults = defaultKunSubagentSettings()\n const defaultPreset = isSubagentPresetId(input?.defaultPreset)\n ? input.defaultPreset\n : defaults.defaultPreset\n const workflowPresets = {} as Record<KunSubagentWorkflowPresetIdV1, KunSubagentWorkflowPresetSettingsV1>\n for (const id of SUBAGENT_WORKFLOW_PRESET_IDS) {\n workflowPresets[id] = normalizeKunSubagentPreset(id, input?.workflowPresets?.[id], defaults.workflowPresets[id])\n }\n return {\n enabled: input?.enabled === true,\n defaultModel: nonEmptyTrimmedString(input?.defaultModel, defaults.defaultModel),\n defaultPreset,\n maxParallel: boundedPositiveIntClampZero(input?.maxParallel, defaults.maxParallel, 64),\n maxChildRuns: boundedPositiveInt(input?.maxChildRuns, defaults.maxChildRuns, 1_000),\n maxTotalChildTokens: boundedPositiveInt(input?.maxTotalChildTokens, defaults.maxTotalChildTokens, 10_000_000),\n maxChildCostUsd: boundedNonNegativeNumber(input?.maxChildCostUsd, defaults.maxChildCostUsd, 10_000),\n perAgentTimeoutMs: boundedPositiveInt(input?.perAgentTimeoutMs, defaults.perAgentTimeoutMs, 600_000),\n workflowPresets\n }\n}\n\nfunction normalizeKunSubagentPreset(\n id: KunSubagentWorkflowPresetIdV1,\n input: Partial<KunSubagentWorkflowPresetSettingsV1> | undefined,\n defaults: KunSubagentWorkflowPresetSettingsV1\n): KunSubagentWorkflowPresetSettingsV1 {\n return {\n id,\n enabled: input?.enabled !== false,\n label: nonEmptyTrimmedString(input?.label, defaults.label),\n defaultModel: nonEmptyTrimmedString(input?.defaultModel, defaults.defaultModel),\n maxParallel: boundedPositiveIntClampZero(input?.maxParallel, defaults.maxParallel, 64),\n maxChildRuns: boundedPositiveInt(input?.maxChildRuns, defaults.maxChildRuns, 1_000),\n maxTotalChildTokens: boundedPositiveInt(input?.maxTotalChildTokens, defaults.maxTotalChildTokens, 10_000_000),\n maxChildCostUsd: boundedNonNegativeNumber(input?.maxChildCostUsd, defaults.maxChildCostUsd, 10_000),\n perAgentTimeoutMs: boundedPositiveInt(input?.perAgentTimeoutMs, defaults.perAgentTimeoutMs, 600_000)\n }\n}\n\nfunction isSubagentPresetId(value: unknown): value is KunSubagentWorkflowPresetIdV1 {\n return typeof value === 'string' && (SUBAGENT_WORKFLOW_PRESET_IDS as readonly string[]).includes(value)\n}\n\nfunction nonEmptyTrimmedString(value: unknown, fallback: string): string {\n return typeof value === 'string' && value.trim() ? value.trim() : fallback\n}\n\nfunction normalizeUserAgentStackProfile(\n input: Partial<UserAgentStackProfileV1> | undefined\n): UserAgentStackProfileV1 {\n const defaults = defaultUserAgentStackProfile()\n const skillRoots = Array.isArray(input?.skillRoots)\n ? input.skillRoots.map(normalizeUserAgentStackSkillRoot).filter((item): item is UserAgentStackSkillRootV1 => item !== null)\n : defaults.skillRoots\n const mcpServers = Array.isArray(input?.mcpServers)\n ? input.mcpServers.map(normalizeUserAgentStackMcpServer).filter((item): item is UserAgentStackMcpServerV1 => item !== null)\n : defaults.mcpServers\n const cli = Array.isArray(input?.cli)\n ? input.cli.map(normalizeUserAgentStackCliStatus).filter((item): item is UserAgentStackCliStatusV1 => item !== null)\n : defaults.cli\n const validationErrors = Array.isArray(input?.validationErrors)\n ? input.validationErrors\n .map(normalizeUserAgentStackValidationError)\n .filter((item): item is UserAgentStackValidationErrorV1 => item !== null)\n : defaults.validationErrors\n const sourcePaths = compactStrings(input?.sourcePaths)\n const profile: UserAgentStackProfileV1 = {\n enabled: input?.enabled !== false,\n importedAt: typeof input?.importedAt === 'string' ? input.importedAt : defaults.importedAt,\n refreshedAt: typeof input?.refreshedAt === 'string' ? input.refreshedAt : defaults.refreshedAt,\n sourcePaths,\n skillRoots,\n mcpServers,\n cli,\n redactedPreviewJson: typeof input?.redactedPreviewJson === 'string'\n ? input.redactedPreviewJson\n : defaults.redactedPreviewJson,\n validationErrors\n }\n if (!profile.redactedPreviewJson.trim()) {\n profile.redactedPreviewJson = JSON.stringify({\n sourcePaths: profile.sourcePaths,\n skillRoots: profile.skillRoots,\n mcpServers: profile.mcpServers,\n cli: profile.cli,\n validationErrors: profile.validationErrors\n }, null, 2)\n }\n return profile\n}\n\nfunction normalizeUserAgentStackSkillRoot(value: unknown): UserAgentStackSkillRootV1 | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n const raw = value as Partial<UserAgentStackSkillRootV1>\n const path = typeof raw.path === 'string' ? raw.path.trim() : ''\n if (!path) return null\n return {\n path,\n scope: raw.scope === 'project' || raw.scope === 'plugin' || raw.scope === 'user'\n ? raw.scope\n : 'user',\n source: typeof raw.source === 'string' && raw.source.trim() ? raw.source.trim() : 'imported',\n available: raw.available !== false\n }\n}\n\nfunction normalizeUserAgentStackMcpServer(value: unknown): UserAgentStackMcpServerV1 | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n const raw = value as Partial<UserAgentStackMcpServerV1>\n const id = typeof raw.id === 'string' ? raw.id.trim() : ''\n const transport = raw.transport === 'stdio' || raw.transport === 'streamable-http' || raw.transport === 'sse'\n ? raw.transport\n : undefined\n if (!id || !transport) return null\n return {\n id,\n enabled: raw.enabled !== false,\n transport,\n ...(typeof raw.command === 'string' && raw.command.trim() ? { command: raw.command.trim() } : {}),\n args: compactStrings(raw.args),\n ...(typeof raw.url === 'string' && raw.url.trim() ? { url: raw.url.trim() } : {}),\n headers: stringRecord(raw.headers),\n env: stringRecord(raw.env),\n trustScope: raw.trustScope === 'workspace' ? 'workspace' : 'user',\n trustedWorkspaceRoots: compactStrings(raw.trustedWorkspaceRoots),\n ...(typeof raw.timeoutMs === 'number' && Number.isInteger(raw.timeoutMs) && raw.timeoutMs > 0\n ? { timeoutMs: raw.timeoutMs }\n : {}),\n ...(typeof raw.sourcePath === 'string' && raw.sourcePath.trim() ? { sourcePath: raw.sourcePath.trim() } : {})\n }\n}\n\nfunction normalizeUserAgentStackCliStatus(value: unknown): UserAgentStackCliStatusV1 | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n const raw = value as Partial<UserAgentStackCliStatusV1>\n const name = typeof raw.name === 'string' ? raw.name.trim() : ''\n if (!name) return null\n return {\n name,\n available: raw.available === true,\n ...(typeof raw.path === 'string' && raw.path.trim() ? { path: raw.path.trim() } : {}),\n ...(typeof raw.version === 'string' && raw.version.trim() ? { version: raw.version.trim() } : {}),\n ...(typeof raw.message === 'string' && raw.message.trim() ? { message: raw.message.trim() } : {})\n }\n}\n\nfunction normalizeUserAgentStackValidationError(value: unknown): UserAgentStackValidationErrorV1 | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n const raw = value as Partial<UserAgentStackValidationErrorV1>\n const source = typeof raw.source === 'string' ? raw.source.trim() : ''\n const message = typeof raw.message === 'string' ? raw.message.trim() : ''\n return source && message ? { source, message } : null\n}\n\nfunction stringRecord(value: unknown): Record<string, string> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return {}\n const out: Record<string, string> = {}\n for (const [key, item] of Object.entries(value)) {\n if (typeof item === 'string') out[key] = item\n }\n return out\n}\n\nfunction normalizeKunTokenEconomySettings(\n input: Partial<KunTokenEconomySettingsV1> | undefined,\n enabledFallback = false\n): KunTokenEconomySettingsV1 {\n return {\n enabled: typeof input?.enabled === 'boolean' ? input.enabled : enabledFallback,\n compressToolDescriptions: input?.compressToolDescriptions !== false,\n compressToolResults: input?.compressToolResults !== false,\n conciseResponses: input?.conciseResponses !== false,\n historyHygiene: normalizeKunHistoryHygieneSettings(input?.historyHygiene)\n }\n}\n\nfunction normalizeKunHistoryHygieneSettings(\n input: Partial<KunHistoryHygieneSettingsV1> | undefined\n): KunHistoryHygieneSettingsV1 {\n const defaults = defaultKunHistoryHygieneSettings()\n return {\n maxToolResultLines: boundedPositiveInt(input?.maxToolResultLines, defaults.maxToolResultLines, 100_000),\n maxToolResultBytes: boundedPositiveInt(input?.maxToolResultBytes, defaults.maxToolResultBytes, 8 * 1024 * 1024),\n maxToolResultTokens: boundedPositiveInt(input?.maxToolResultTokens, defaults.maxToolResultTokens, 256_000),\n maxToolArgumentStringBytes: boundedPositiveInt(\n input?.maxToolArgumentStringBytes,\n defaults.maxToolArgumentStringBytes,\n 8 * 1024 * 1024\n ),\n maxToolArgumentStringTokens: boundedPositiveInt(\n input?.maxToolArgumentStringTokens,\n defaults.maxToolArgumentStringTokens,\n 64_000\n ),\n maxArrayItems: boundedPositiveInt(input?.maxArrayItems, defaults.maxArrayItems, 10_000)\n }\n}\n\nfunction normalizeKunMcpSearchSettings(\n input: Partial<KunMcpSearchSettingsV1> | undefined\n): KunMcpSearchSettingsV1 {\n const defaults = defaultKunMcpSearchSettings()\n const topKMax = positiveInt(input?.topKMax, defaults.topKMax)\n const topKDefault = Math.min(positiveInt(input?.topKDefault, defaults.topKDefault), topKMax)\n return {\n enabled: input?.enabled === true,\n mode: input?.mode === 'direct' || input?.mode === 'search' || input?.mode === 'auto'\n ? input.mode\n : defaults.mode,\n autoThresholdToolCount: positiveInt(input?.autoThresholdToolCount, defaults.autoThresholdToolCount),\n topKDefault,\n topKMax,\n minScore: nonNegativeNumber(input?.minScore, defaults.minScore)\n }\n}\n\nfunction positiveInt(value: unknown, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n ? Math.floor(value)\n : fallback\n}\n\nfunction nonNegativeNumber(value: unknown, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0\n ? value\n : fallback\n}\n\nfunction boundedNonNegativeNumber(value: unknown, fallback: number, max = Number.MAX_SAFE_INTEGER): number {\n if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return fallback\n return Math.min(value, max)\n}\n\nfunction boundedPositiveInt(value: unknown, fallback: number, max = Number.MAX_SAFE_INTEGER): number {\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return fallback\n return Math.min(Math.floor(value), max)\n}\n\nfunction boundedPositiveIntClampZero(value: unknown, fallback: number, max = Number.MAX_SAFE_INTEGER): number {\n if (value === 0) return 1\n return boundedPositiveInt(value, fallback, max)\n}\n\nfunction boundedPositiveFloat(value: unknown, fallback: number, max = Number.MAX_SAFE_INTEGER): number {\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return fallback\n return Math.min(value, max)\n}\n\nfunction boundedNonNegativeInt(value: unknown, fallback: number, max = Number.MAX_SAFE_INTEGER): number {\n if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value !== Math.floor(value)) return fallback\n return Math.min(value, max)\n}\n\nfunction normalizeKunStorageSettings(\n input: Partial<KunStorageSettingsV1> | undefined\n): KunStorageSettingsV1 {\n const defaults = defaultKunStorageSettings()\n return {\n backend: input?.backend === 'file' || input?.backend === 'hybrid'\n ? input.backend\n : defaults.backend,\n sqlitePath: typeof input?.sqlitePath === 'string' ? input.sqlitePath.trim() : defaults.sqlitePath\n }\n}\n\nfunction normalizeKunContextCompactionSettings(\n input: Partial<KunContextCompactionSettingsV1> | undefined\n): KunContextCompactionSettingsV1 {\n const defaults = defaultKunContextCompactionSettings()\n const defaultSoftThreshold = boundedPositiveInt(input?.defaultSoftThreshold, defaults.defaultSoftThreshold)\n const requestedHardThreshold = boundedPositiveInt(input?.defaultHardThreshold, defaults.defaultHardThreshold)\n return {\n defaultSoftThreshold,\n defaultHardThreshold: Math.max(defaultSoftThreshold, requestedHardThreshold),\n summaryMode: input?.summaryMode === 'model' || input?.summaryMode === 'heuristic'\n ? input.summaryMode\n : defaults.summaryMode,\n summaryTimeoutMs: boundedPositiveInt(input?.summaryTimeoutMs, defaults.summaryTimeoutMs, 120_000),\n summaryMaxTokens: boundedPositiveInt(input?.summaryMaxTokens, defaults.summaryMaxTokens, 16_000),\n summaryInputMaxBytes: boundedPositiveInt(input?.summaryInputMaxBytes, defaults.summaryInputMaxBytes, 8 * 1024 * 1024)\n }\n}\n\nfunction normalizeKunRuntimeTuningSettings(\n input: Partial<KunRuntimeTuningSettingsV1> | undefined\n): KunRuntimeTuningSettingsV1 {\n const defaults = defaultKunRuntimeTuningSettings()\n return {\n toolStorm: {\n enabled: input?.toolStorm?.enabled !== false,\n windowSize: boundedPositiveInt(input?.toolStorm?.windowSize, defaults.toolStorm.windowSize, 128),\n threshold: Math.max(2, boundedPositiveInt(input?.toolStorm?.threshold, defaults.toolStorm.threshold, 128))\n },\n toolArgumentRepair: {\n maxStringBytes: boundedPositiveInt(\n input?.toolArgumentRepair?.maxStringBytes,\n defaults.toolArgumentRepair.maxStringBytes,\n 16 * 1024 * 1024\n )\n }\n }\n}\n\nfunction normalizeRemoteRunnersSettings(\n input: Partial<RemoteRunnerSettingsV1> | undefined\n): RemoteRunnerSettingsV1 {\n const defaults = defaultKunRemoteRunnersSettings()\n return {\n enabled: input?.enabled === true,\n hosts: Array.isArray(input?.hosts) ? input.hosts.map(normalizeRemoteRunnerHost) : defaults.hosts,\n dataPolicy: {\n defaultAllowed: Array.isArray(input?.dataPolicy?.defaultAllowed) ? input.dataPolicy.defaultAllowed : defaults.dataPolicy.defaultAllowed,\n consentRequired: Array.isArray(input?.dataPolicy?.consentRequired) ? input.dataPolicy.consentRequired : defaults.dataPolicy.consentRequired,\n never: Array.isArray(input?.dataPolicy?.never) ? input.dataPolicy.never : defaults.dataPolicy.never\n },\n auditLog: Array.isArray(input?.auditLog) ? input.auditLog.slice(0, input?.maxAuditEntries ?? defaults.maxAuditEntries) : defaults.auditLog,\n maxAuditEntries: boundedPositiveInt(input?.maxAuditEntries, defaults.maxAuditEntries, 10_000)\n }\n}\n\nfunction normalizeRemoteRunnerHost(host: Partial<RemoteRunnerHostConfigV1>): RemoteRunnerHostConfigV1 {\n return {\n id: nonEmptyTrimmedString(host.id, `host_${Date.now()}`),\n label: nonEmptyTrimmedString(host.label, 'SSH Host'),\n enabled: host.enabled !== false,\n endpointRef: nonEmptyTrimmedString(host.endpointRef, ''),\n usernameRef: typeof host.usernameRef === 'string' ? host.usernameRef : undefined,\n credentialStorage: {\n kind: (host.credentialStorage?.kind === 'os-keychain' || host.credentialStorage?.kind === 'ssh-agent' || host.credentialStorage?.kind === 'secret-manager' || host.credentialStorage?.kind === 'none') ? host.credentialStorage.kind : 'ssh-agent',\n credentialRef: typeof host.credentialStorage?.credentialRef === 'string' ? host.credentialStorage.credentialRef : undefined,\n exportsRawSecret: false\n },\n hostKeyPolicy: (host.hostKeyPolicy === 'known-hosts' || host.hostKeyPolicy === 'pinned-fingerprint-ref' || host.hostKeyPolicy === 'manual-confirm') ? host.hostKeyPolicy : 'known-hosts',\n connectionStatus: (host.connectionStatus === 'connected' || host.connectionStatus === 'connecting' || host.connectionStatus === 'handshaking' || host.connectionStatus === 'error') ? host.connectionStatus : 'disconnected',\n lastHandshake: host.lastHandshake ?? null,\n lastHandshakeError: typeof host.lastHandshakeError === 'string' ? host.lastHandshakeError : null,\n trustedPaths: Array.isArray(host.trustedPaths) ? host.trustedPaths.map(normalizeRemoteRunnerTrustedPath) : []\n }\n}\n\nfunction normalizeRemoteRunnerTrustedPath(path: Partial<RemoteRunnerTrustedPathV1>): RemoteRunnerTrustedPathV1 {\n return {\n path: nonEmptyTrimmedString(path.path, ''),\n label: nonEmptyTrimmedString(path.label, ''),\n trustedAt: nonEmptyTrimmedString(path.trustedAt, new Date().toISOString()),\n auditId: nonEmptyTrimmedString(path.auditId, '')\n }\n}\n\nfunction mergeRemoteRunnerHosts(\n current: RemoteRunnerHostConfigV1[],\n patches: Array<Partial<RemoteRunnerHostConfigV1>>\n): RemoteRunnerHostConfigV1[] {\n if (patches.length === 0) return current\n const hostMap = new Map<string, RemoteRunnerHostConfigV1>()\n for (const host of current) {\n hostMap.set(host.id, host)\n }\n for (const patch of patches) {\n const id = typeof patch.id === 'string' && patch.id.trim() ? patch.id.trim() : ''\n if (!id) continue\n const existing = hostMap.get(id)\n const merged = normalizeRemoteRunnerHost({ ...existing, ...patch })\n hostMap.set(id, merged)\n }\n return [...hostMap.values()]\n}\n\nfunction mergeRemoteRunnerAuditLog(\n current: RemoteRunnerAuditEntryV1[],\n patches: RemoteRunnerAuditEntryV1[],\n maxEntries: number\n): RemoteRunnerAuditEntryV1[] {\n const combined = [...current, ...patches]\n return combined.slice(-maxEntries)\n}\n\nexport function withKunRuntimeSettings(\n settings: AppSettingsV1,\n kun: KunRuntimeSettingsV1\n): AppSettingsV1 {\n return {\n ...settings,\n agents: kunSettingsEnvelope(kun)\n }\n}\n\nexport function applyKunRuntimePatch(\n settings: AppSettingsV1,\n patch: KunRuntimeSettingsPatchV1 | undefined\n): AppSettingsV1 {\n return withKunRuntimeSettings(\n settings,\n mergeKunRuntimeSettings(getKunRuntimeSettings(settings), patch)\n )\n}\n\nexport function isKunRuntimeInsecure(runtime: Pick<KunRuntimeSettingsV1, 'insecure' | 'runtimeToken'>): boolean {\n return runtime.insecure || !runtime.runtimeToken.trim()\n}\n\nexport function getActiveAgentApiKey(settings: AppSettingsV1): string {\n return resolveKunRuntimeSettings(settings).apiKey?.trim() ?? ''\n}\n\nexport function mergeAgentRuntimeSettings(\n defaults: KunSettingsEnvelopeV1,\n patch: KunSettingsEnvelopePatchV1 | undefined\n): KunSettingsEnvelopeV1 {\n return kunSettingsEnvelope(\n mergeKunRuntimeSettings(defaults.kun, patch?.kun)\n )\n}\n\ntype LegacyAgentsSettingsShape = {\n kun?: Partial<KunRuntimeSettingsV1>\n codewhale?: Partial<LegacyLocalHttpRuntimeSettingsV1>\n reasonix?: Partial<LegacyReasoningRuntimeSettingsV1>\n}\n\ntype LegacyAppSettingsShape = Partial<Omit<AppSettingsV1, 'agents' | 'provider'>> & {\n agents?: LegacyAgentsSettingsShape\n provider?: Partial<ModelProviderSettingsV1>\n deepseek?: Partial<LegacyLocalHttpRuntimeSettingsV1>\n /** Legacy single-provider discriminator. Read only inside migration. */\n agentProvider?: unknown\n}\n\nfunction nonEmptyStringOrFallback(value: unknown, fallback: string): string {\n return typeof value === 'string' && value.trim() ? value : fallback\n}\n\nfunction upgradeLegacyKunDefaultDataDir(value: unknown): string {\n if (typeof value !== 'string') return DEFAULT_KUN_DATA_DIR\n const trimmed = value.trim()\n const normalized = trimmed.replace(/\\\\/g, '/').toLowerCase()\n if (\n !trimmed ||\n normalized === LEGACY_COREAGENT_DATA_DIR ||\n normalized === LEGACY_KUN_DATA_DIR ||\n normalized.endsWith('/.deepseekgui/coreagent') ||\n normalized.endsWith('/.deepseekgui/kun')\n ) {\n return DEFAULT_KUN_DATA_DIR\n }\n return trimmed\n}\n\nfunction upgradeLegacyKunDefaultModel(value: unknown, fallback: string): string {\n const model = nonEmptyStringOrFallback(value, fallback).trim()\n return model === LEGACY_KUN_DEFAULT_MODEL ? DEFAULT_KUN_MODEL : model\n}\n\nfunction upgradeLegacyKunDefaultPort(value: unknown, fallback: number): number {\n return value === LEGACY_LOCAL_HTTP_DEFAULT_PORT || value === LEGACY_DEEPSEEK_GUI_KUN_DEFAULT_PORT\n ? DEFAULT_KUN_PORT\n : fallback\n}\n\nexport function migrateLegacyAppSettings(parsed: LegacyAppSettingsShape): Partial<AppSettingsV1> {\n const rawAgentProvider = parsed.agentProvider\n const isReasoningLegacy = rawAgentProvider === 'reasonix'\n const hasProviderSettings = typeof parsed.provider === 'object' && parsed.provider !== null\n const defaults = legacyLocalHttpRuntimeDefaults()\n const kunDefaults = defaultKunRuntimeSettings()\n const legacyDeepseek = parsed.deepseek ?? {}\n const legacyLocalHttp = {\n ...defaults,\n ...(parsed.agents?.codewhale ?? {}),\n ...legacyDeepseek\n }\n const legacyReasoning = {\n ...legacyReasoningRuntimeDefaults(),\n ...(parsed.agents?.reasonix ?? {})\n }\n const explicitKun: Partial<KunRuntimeSettingsV1> = parsed.agents?.kun ?? {}\n const legacySource = isReasoningLegacy ? legacyReasoning : legacyLocalHttp\n const legacySeed = {\n binaryPath: kunDefaults.binaryPath,\n port: isReasoningLegacy\n ? kunDefaults.port\n : upgradeLegacyKunDefaultPort(legacyLocalHttp.port, legacyLocalHttp.port),\n autoStart: isReasoningLegacy ? legacyReasoning.autoStart : legacyLocalHttp.autoStart,\n apiKey: legacySource.apiKey,\n baseUrl: legacySource.baseUrl,\n providerId: '',\n endpointFormat: DEFAULT_MODEL_ENDPOINT_FORMAT,\n runtimeToken: isReasoningLegacy ? kunDefaults.runtimeToken : legacyLocalHttp.runtimeToken,\n model: isReasoningLegacy ? legacyReasoning.model : kunDefaults.model,\n approvalPolicy: isReasoningLegacy ? kunDefaults.approvalPolicy : legacyLocalHttp.approvalPolicy,\n sandboxMode: isReasoningLegacy ? kunDefaults.sandboxMode : legacyLocalHttp.sandboxMode\n }\n const provider = normalizeModelProviderSettings({\n apiKey: hasProviderSettings\n ? parsed.provider?.apiKey\n : nonEmptyStringOrFallback(explicitKun.apiKey, legacySeed.apiKey),\n baseUrl: hasProviderSettings\n ? parsed.provider?.baseUrl\n : nonEmptyStringOrFallback(explicitKun.baseUrl, legacySeed.baseUrl),\n providers: parsed.provider?.providers\n })\n const kun = {\n ...kunDefaults,\n ...legacySeed,\n ...explicitKun,\n port: upgradeLegacyKunDefaultPort(explicitKun.port, legacySeed.port),\n apiKey: hasProviderSettings ? explicitKun.apiKey ?? '' : '',\n baseUrl: hasProviderSettings ? explicitKun.baseUrl ?? '' : '',\n endpointFormat: normalizeModelEndpointFormat(explicitKun.endpointFormat ?? legacySeed.endpointFormat),\n runtimeToken: nonEmptyStringOrFallback(explicitKun.runtimeToken, legacySeed.runtimeToken),\n dataDir: upgradeLegacyKunDefaultDataDir(explicitKun.dataDir),\n model: upgradeLegacyKunDefaultModel(explicitKun.model, legacySeed.model),\n tokenEconomyMode: typeof explicitKun.tokenEconomy?.enabled === 'boolean'\n ? explicitKun.tokenEconomy.enabled\n : explicitKun.tokenEconomyMode ?? kunDefaults.tokenEconomyMode,\n tokenEconomy: normalizeKunTokenEconomySettings(\n explicitKun.tokenEconomy,\n explicitKun.tokenEconomyMode ?? kunDefaults.tokenEconomyMode\n ),\n mcpSearch: normalizeKunMcpSearchSettings(explicitKun.mcpSearch),\n storage: normalizeKunStorageSettings(explicitKun.storage),\n contextCompaction: normalizeKunContextCompactionSettings(explicitKun.contextCompaction),\n runtimeTuning: normalizeKunRuntimeTuningSettings(explicitKun.runtimeTuning)\n }\n // Strip the legacy `agentProvider` discriminator and the legacy\n // per-provider settings from the surfaced migration result. The\n // runtime now has a single agent (Kun) and we no longer\n // round-trip the legacy value into the new settings shape.\n const { deepseek: _legacyDeepseek, agents: _agents, agentProvider: _agentProvider, ...rest } = parsed\n void _legacyDeepseek\n void _agents\n void _agentProvider\n return {\n ...rest,\n provider,\n agents: {\n kun\n }\n }\n}\n"}}}} |