Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/main/presenter/configPresenter/acpConfHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import { McpConfHelper } from './mcpConfHelper'
const ACP_STORE_VERSION = '2'
const DEFAULT_PROFILE_NAME = 'Default'

const BUILTIN_ORDER: AcpBuiltinAgentId[] = ['kimi-cli', 'claude-code-acp', 'codex-acp']
const BUILTIN_ORDER: AcpBuiltinAgentId[] = [
'kimi-cli',
'claude-code-acp',
'codex-acp',
'dimcode-acp'
]

interface BuiltinTemplate {
name: string
Expand Down Expand Up @@ -47,6 +52,15 @@ const BUILTIN_TEMPLATES: Record<AcpBuiltinAgentId, BuiltinTemplate> = {
args: ['-y', '@zed-industries/codex-acp'],
env: {}
})
},
'dimcode-acp': {
name: 'DimCode',
defaultProfile: () => ({
name: DEFAULT_PROFILE_NAME,
command: 'dim',
args: ['acp'],
env: {}
})
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/main/presenter/configPresenter/acpInitHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ const BUILTIN_INIT_COMMANDS: Record<AcpBuiltinAgentId, InitCommandConfig> = {
'codex-acp': {
commands: ['npm i -g @zed-industries/codex-acp', 'npm install -g @openai/codex', 'codex'],
description: 'Initialize Codex CLI ACP'
},
'dimcode-acp': {
commands: ['npm i -g dimcode', 'dim'],
description: 'Initialize DimCode ACP'
}
}

Expand Down
28 changes: 27 additions & 1 deletion src/main/presenter/deepchatAgentPresenter/accumulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ function getCurrentBlock(
return block
}

function updateReasoningMetadata(state: StreamState, start: number, end: number): void {
const relativeStart = Math.max(0, start - state.startTime)
const relativeEnd = Math.max(0, end - state.startTime)

if (state.metadata.reasoningStartTime === undefined) {
state.metadata.reasoningStartTime = relativeStart
}
state.metadata.reasoningEndTime = relativeEnd
}

/**
* Apply a single stream event to the accumulator state.
* Pure block mutations only — no I/O, no finalization, no emit.
Expand All @@ -34,9 +44,25 @@ export function accumulate(state: StreamState, event: LLMCoreStreamEvent): void
break
}
case 'reasoning': {
if (state.firstTokenTime === null) state.firstTokenTime = Date.now()
const currentTime = Date.now()
if (state.firstTokenTime === null) state.firstTokenTime = currentTime
const block = getCurrentBlock(state.blocks, 'reasoning_content')
block.content += event.reasoning_content
if (
typeof block.reasoning_time !== 'object' ||
block.reasoning_time === null ||
typeof block.reasoning_time.start !== 'number' ||
typeof block.reasoning_time.end !== 'number'
) {
block.reasoning_time = {
start: currentTime,
end: currentTime
}
} else {
block.reasoning_time.end = currentTime
}
const reasoningTime = block.reasoning_time as { start: number; end: number }
updateReasoningMetadata(state, reasoningTime.start, reasoningTime.end)
state.dirty = true
break
}
Expand Down
9 changes: 3 additions & 6 deletions src/main/presenter/deepchatAgentPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,12 +618,9 @@ export class DeepChatAgentPresenter implements IAgentImplementation {
}

const currentGeneration = await this.getEffectiveSessionGenerationSettings(sessionId)
const sanitized = await this.sanitizeGenerationSettings(
nextProviderId,
nextModelId,
{},
currentGeneration
)
const sanitized = await this.sanitizeGenerationSettings(nextProviderId, nextModelId, {
systemPrompt: currentGeneration.systemPrompt
})

if (state) {
state.providerId = nextProviderId
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/src/assets/llm-icons/dimcode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 19 additions & 10 deletions src/renderer/src/components/chat/ChatStatusBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class="h-6 px-2 gap-1 text-xs text-muted-foreground hover:text-foreground backdrop-blur-lg"
>
<ModelIcon
:model-id="displayProviderId"
:model-id="displayIconId"
custom-class="w-3.5 h-3.5"
:is-dark="themeStore.isDark"
/>
Expand All @@ -25,7 +25,7 @@
@click="selectModel(group.providerId, group.model.id)"
>
<ModelIcon
:model-id="group.providerId"
:model-id="resolveModelIconId(group.providerId, group.model.id)"
custom-class="w-3.5 h-3.5"
:is-dark="themeStore.isDark"
/>
Expand All @@ -42,7 +42,7 @@
:disabled="true"
>
<ModelIcon
:model-id="displayProviderId"
:model-id="displayIconId"
custom-class="w-3.5 h-3.5"
:is-dark="themeStore.isDark"
/>
Expand Down Expand Up @@ -635,18 +635,27 @@ watch(
{ immediate: true }
)

const displayProviderId = computed(() => {
const resolveModelIconId = (providerId?: string | null, modelId?: string | null): string => {
if (providerId === 'acp' && modelId) {
return modelId
}
return providerId || 'anthropic'
}

const displayIconId = computed(() => {
if (hasActiveSession.value) {
return (
activeSessionSelection.value?.providerId ||
draftModelSelection.value?.providerId ||
'anthropic'
return resolveModelIconId(
activeSessionSelection.value?.providerId || draftModelSelection.value?.providerId,
activeSessionSelection.value?.modelId || draftModelSelection.value?.modelId
)
}
if (isAcpAgent.value) {
return agentStore.selectedAgentId ?? 'acp'
return resolveModelIconId('acp', agentStore.selectedAgentId)
}
return draftModelSelection.value?.providerId || 'anthropic'
return resolveModelIconId(
draftModelSelection.value?.providerId,
draftModelSelection.value?.modelId
)
})

const displayModelName = computed(() => {
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/src/components/icons/ModelIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import deepseekColorIcon from '@/assets/llm-icons/deepseek-color.svg?url'
import openaiColorIcon from '@/assets/llm-icons/openai.svg?url'
import ollamaColorIcon from '@/assets/llm-icons/ollama.svg?url'
import doubaoColorIcon from '@/assets/llm-icons/doubao-color.svg?url'
import dimcodeColorIcon from '@/assets/llm-icons/dimcode.svg?url'
import minimaxColorIcon from '@/assets/llm-icons/minimax-color.svg?url'
import fireworksColorIcon from '@/assets/llm-icons/fireworks-color.svg?url'
import zerooneColorIcon from '@/assets/llm-icons/zeroone.svg?url'
Expand Down Expand Up @@ -75,6 +76,7 @@ const icons = {
'kimi-cli': moonshotColorIcon,
'claude-code-acp': claudeColorIcon,
'codex-acp': openaiColorIcon,
'dimcode-acp': dimcodeColorIcon,
o3fan: o3fanColorIcon,
cherryin: cherryinColorIcon,
modelscope: modelscopeColorIcon,
Expand Down Expand Up @@ -197,6 +199,7 @@ const iconKey = computed(() => {
// Monochrome icon URLs that need inversion in dark mode
const monoIconUrls = new Set([
openaiColorIcon,
dimcodeColorIcon,
ollamaColorIcon,
zerooneColorIcon,
xaiColorIcon,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/zh-CN/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"settings-prompt": "Prompt管理",
"settings-mcp-market": "MCP市场",
"settings-acp": "ACP Agent",
"settings-skills": "skills设置",
"settings-skills": "Skills设置",
"settings-notifications-hooks": "通知与Hooks"
}
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@
"resetToDefaultFailed": "重置失败,请重试"
},
"skills": {
"title": "skills管理",
"title": "Skills设置",
"description": "管理和配置 AI 助手的 skill 模块",
"openFolder": "打开文件夹",
"addSkill": "添加 skill",
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/zh-HK/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"settings-mcp-market": "MCP市場",
"playground": "Playground 實驗室",
"settings-acp": "ACP Agent",
"settings-skills": "skills設置",
"settings-skills": "Skills設置",
"settings-notifications-hooks": "通知與 Hooks"
}
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/zh-HK/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,6 @@
"syncing": "正在匯入...",
"title": "外部工具"
},
"title": "skills管理"
"title": "Skills設置"
}
}
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/zh-TW/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"settings-mcp-market": "MCP市場",
"playground": "Playground 實驗室",
"settings-acp": "ACP Agent",
"settings-skills": "skills管理",
"settings-skills": "Skills設定",
"settings-notifications-hooks": "通知與 Hooks"
}
2 changes: 1 addition & 1 deletion src/renderer/src/i18n/zh-TW/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,6 @@
"syncing": "正在匯入...",
"title": "外部工具"
},
"title": "skills管理"
"title": "Skills設定"
}
}
4 changes: 2 additions & 2 deletions src/renderer/src/pages/ChatPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ function buildUsage(metadata: MessageMetadata): Message['usage'] {
total_tokens: metadata.totalTokens ?? 0,
generation_time: metadata.generationTime ?? 0,
first_token_time: metadata.firstTokenTime ?? 0,
reasoning_start_time: 0,
reasoning_end_time: 0,
reasoning_start_time: metadata.reasoningStartTime ?? 0,
reasoning_end_time: metadata.reasoningEndTime ?? 0,
input_tokens: metadata.inputTokens ?? 0,
output_tokens: metadata.outputTokens ?? 0
}
Expand Down
2 changes: 2 additions & 0 deletions src/shared/types/agent-interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ export interface MessageMetadata {
outputTokens?: number
generationTime?: number
firstTokenTime?: number
reasoningStartTime?: number
reasoningEndTime?: number
tokensPerSecond?: number
model?: string
provider?: string
Expand Down
2 changes: 1 addition & 1 deletion src/shared/types/presenters/legacy.presenters.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,7 @@ export interface AcpDebugRunResult {
events: AcpDebugEventEntry[]
}

export type AcpBuiltinAgentId = 'kimi-cli' | 'claude-code-acp' | 'codex-acp'
export type AcpBuiltinAgentId = 'kimi-cli' | 'claude-code-acp' | 'codex-acp' | 'dimcode-acp'

export interface AcpAgentProfile {
id: string
Expand Down
86 changes: 86 additions & 0 deletions test/main/presenter/configPresenter/acpConfHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const mockStores = vi.hoisted(() => new Map<string, Record<string, any>>())

const clone = <T>(value: T): T => {
const cloneFn = (globalThis as typeof globalThis & { structuredClone?: (input: T) => T })
.structuredClone

if (typeof cloneFn === 'function') {
return cloneFn(value)
}

return JSON.parse(JSON.stringify(value)) as T
}

vi.mock('electron-store', () => ({
default: class MockElectronStore {
private readonly data: Record<string, any>

constructor(options: { name: string; defaults?: Record<string, any> }) {
if (!mockStores.has(options.name)) {
mockStores.set(options.name, clone(options.defaults ?? {}))
}
this.data = mockStores.get(options.name)!
}

get(key: string) {
return this.data[key]
}

set(key: string, value: any) {
this.data[key] = value
}

delete(key: string) {
delete this.data[key]
}
}
}))

vi.mock('../../../../src/main/presenter/configPresenter/mcpConfHelper', () => ({
McpConfHelper: class MockMcpConfHelper {
async getMcpServers() {
return {}
}
}
}))

describe('AcpConfHelper DimCode builtin', () => {
beforeEach(() => {
mockStores.clear()
vi.resetModules()
})

it('includes DimCode in default builtins with dim acp profile', async () => {
const { AcpConfHelper } =
await import('../../../../src/main/presenter/configPresenter/acpConfHelper')
const helper = new AcpConfHelper()

const dimcode = helper.getBuiltins().find((agent) => agent.id === 'dimcode-acp')

expect(dimcode).toBeDefined()
expect(dimcode?.name).toBe('DimCode')
expect(dimcode?.profiles).toHaveLength(1)
expect(dimcode?.profiles[0]?.command).toBe('dim')
expect(dimcode?.profiles[0]?.args).toEqual(['acp'])
})

it('returns enabled DimCode from getEnabledAgents', async () => {
const { AcpConfHelper } =
await import('../../../../src/main/presenter/configPresenter/acpConfHelper')
const helper = new AcpConfHelper()

helper.setGlobalEnabled(true)
helper.setBuiltinEnabled('dimcode-acp', true)

expect(helper.getEnabledAgents()).toContainEqual(
expect.objectContaining({
id: 'dimcode-acp',
name: 'DimCode - Default',
command: 'dim',
args: ['acp']
})
)
})
})
Loading