Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
375e88a
feat(grok-build): host Grok Build on the Cindy loop like Claude Code
yuchenlin Sep 5, 2026
25b47f9
fix(desktop): rewind and fork grok-build on the hosted Pi path
yuchenlin Sep 5, 2026
f3a7b47
fix(grok-build): address review comments and CI contract tests
yuchenlin Sep 5, 2026
a8370d4
fix(design-tokens): classify engine-badge-grok-build in the DS-3 snap…
yuchenlin Sep 5, 2026
3cc247f
fix(i18n): capitalize Agent in Grok Build permission copy
yuchenlin Sep 5, 2026
8c88c7c
fix(desktop): close grok-build type holes that fail tsc
yuchenlin Sep 5, 2026
c569748
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 5, 2026
3a21a08
test(mobile): expect workspaceKind when persisting grok-build prefs
yuchenlin Sep 5, 2026
5882a8a
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 5, 2026
102dba2
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
7814bb7
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
69b9804
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
8519b50
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
6c91953
fix(mobile): include grok-build in session usage source label
yuchenlin Sep 6, 2026
2def1a8
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
0f9f680
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
890831a
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
eb6e949
chore: retrigger CI after Windows PI runner flake
yuchenlin Sep 6, 2026
6fc0be6
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
9aaab4e
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
823dfc7
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
bd83f37
fix(desktop): include grok-build in settings harness maps
yuchenlin Sep 6, 2026
943d05b
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
a165f66
merge(upstream): resolve conflicts with main for PR 3967
yuchenlin Sep 6, 2026
e5b17b8
fix(desktop): map bot model-chain select away from grok-build
yuchenlin Sep 6, 2026
497ae59
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 6, 2026
54ed15b
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
8b534e6
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
15039db
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
c204922
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
364d068
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
695819d
chore: retrigger CI after Windows packaged-instance flake
yuchenlin Sep 7, 2026
f432ae1
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
d0d5dab
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
fbb4b6d
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
39a129a
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
8be4afa
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
f5d5299
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
ac2abcc
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
8f6ee72
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 7, 2026
5965b42
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
aab9f36
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
8af9c66
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
a920841
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
a5913bd
test(maker-core): allow Windows Bot Home memory copy latency
yuchenlin Sep 8, 2026
da4d4d4
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
0778e78
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
746e70e
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
05f9b94
fix(desktop): wait for bot read mark after unread boundary
yuchenlin Sep 8, 2026
f47ec18
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
a6de6f5
chore: retrigger CI after Windows iOS simulator flake
yuchenlin Sep 8, 2026
b5898f1
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
60a5547
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
8b5029b
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
ec82735
test(desktop): allow Windows stale-lock takeover latency
yuchenlin Sep 8, 2026
56cf1bf
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
008a4f1
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
97be810
test(device-link): ACK replay copies before asserting unique seq
yuchenlin Sep 8, 2026
f584d13
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
44ed021
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
42e56ca
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
3ef0149
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
6762178
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
43f0860
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
3d1b0d0
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
c7d96c7
Merge remote-tracking branch 'origin/main' into feat/grok-build-hoste…
yuchenlin Sep 8, 2026
9df4690
test(mobile): expect workingDirByDevice when persisting grok-build prefs
yuchenlin Sep 8, 2026
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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ harnesses, models and tools into one agent that finishes real work in your
projects and apps. Ready from day one, yours to shape over time.

Cindy runs locally on your own machine, using your real files and logged-in
apps. The first supported harnesses are **Claude Code** and **Codex** — more are
being added, and a native harness is in the works. Models and harnesses mix
apps. The first supported harnesses are **Claude Code**, **Codex**, and **Pi**. **Grok Build** is a fourth Cindy-hosted harness (same loop as Pi) when SuperGrok (xAI) is connected. Models and harnesses mix
freely and can switch mid-task while your workspace, memory, skills and tools
stay continuous; one task can even be planned, executed in parallel, and
reviewed by agents on different harness × model combos. She can drive your
Expand Down
3 changes: 1 addition & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ Cindy 是一个开源、开箱即用的 AI Agent。她把多套 Harness、模型
持续成长的伙伴,在真实工程和软件里把任务做完。一开始就好用,任你打扮,任你培养。

Cindy 运行在你自己的电脑上,使用你本地的文件和已登录的应用。首批兼容
**Claude Code** 与 **Codex** 两套 Agent Harness——更多 Harness 正在接入,自研
Harness 也在酝酿。模型与 Harness 自由组合、同一任务中随时切换,工作现场、记忆、
**Claude Code**、**Codex** 与 **Pi** 三套 Agent Harness;已连接 SuperGrok(xAI)时还可选 **Grok Build**(与 Pi 同一套 Cindy 托管循环)。模型与 Harness 自由组合、同一任务中随时切换,工作现场、记忆、
Skill 和工具始终连续;一个任务还可以由不同 Harness × 模型组合的多个 agent
规划、并行执行、独立 review。她能操作浏览器、电脑和手机,并支持从 IM 和
定时任务派活。
Expand Down
65 changes: 65 additions & 0 deletions apps/desktop/src/main/__tests__/fork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,71 @@ describe('forkSessionAtMessage', () => {
expect(result.parentSessionId).toBe('src-session');
});

it('grok-build hosted loop: fork uses the Pi tail-turn path, not UNSUPPORTED_HISTORY', async () => {
const target = makeMessageRow({ id: 'target-user', role: 'user', createdAt: 3000 });
const priorUser = makeMessageRow({
id: 'user-1',
role: 'user',
content: '"hi"',
createdAt: 2000,
});
const priorAssistant = makeMessageRow({
id: 'asst-1',
role: 'assistant',
content: '"hi back"',
createdAt: 2500,
});

selectQueue.push([makeSourceRow({
agentKind: 'grok-build',
model: 'grok-4.6',
providerId: 'xai',
sdkSessionId: 'grok-build-session-source',
})]);
selectQueue.push([target]);
selectQueue.push([priorUser, priorAssistant]);
selectQueue.push([makeSourceRow({
agentKind: 'grok-build',
model: 'grok-4.6',
providerId: 'xai',
sdkSessionId: 'grok-build-session-forked',
parentSessionId: 'src-session',
forkedAtMessageId: 'target-user',
})]);
queryMock.mockResolvedValue([
{ role: 'user', content: '"later"' },
{ role: 'user', content: '"target"' },
]);
forkSdkSessionMock.mockResolvedValue({
newSdkSessionId: 'grok-build-session-forked',
uuidMap: new Map<string, string>(),
});

const result = await forkSessionAtMessage('src-session', 'target-user');

expect(forkSdkSessionMock).toHaveBeenCalledOnce();
expect(forkSdkSessionMock).toHaveBeenCalledWith('grok-build', {
sourceSdkSessionId: 'grok-build-session-source',
upToMessageId: undefined,
tailTurnsToDrop: 2,
title: '[Fork] Project A',
workingDir: '/work',
remoteHostId: null,
});
const txArgs = txCalls.find((call) => call.name === 'fork.session')?.args as {
newSession: Record<string, unknown>;
};
expect(txArgs.newSession).toMatchObject({
agentKind: 'grok-build',
model: 'grok-4.6',
providerId: 'xai',
sdkSessionId: 'grok-build-session-forked',
parentSessionId: 'src-session',
forkedAtMessageId: 'target-user',
});
expect(result.parentSessionId).toBe('src-session');
});

it('codex path: maps preparation or thread/fork failures to a diagnosable error', async () => {
const target = makeMessageRow({ id: 'target-user', role: 'user', createdAt: 3000 });
selectQueue.push([
Expand Down
30 changes: 27 additions & 3 deletions apps/desktop/src/main/__tests__/rewind.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const writeWorktreeTreeForPathsMock = vi.fn();
const gitExecMock = vi.fn();

type FakeSession = {
agentKind: 'claude-code' | 'codex' | 'pi';
agentKind: 'claude-code' | 'codex' | 'pi' | 'grok-build';
sdkSessionId: string;
workDir: string;
remoteHostId: string | null;
Expand All @@ -66,13 +66,13 @@ const getSessionMock = vi.fn(() => fakeSession);
const getSessionMetaMock = vi.fn(async () =>
fakeSession ? { sdkSessionId: fakeSession.sdkSessionId } : null,
);
function useFakeSession(agentKind: 'claude-code' | 'codex' | 'pi') {
function useFakeSession(agentKind: 'claude-code' | 'codex' | 'pi' | 'grok-build') {
fakeSession = {
agentKind,
sdkSessionId:
agentKind === 'codex'
? 'codex-thread-old'
: agentKind === 'pi'
: agentKind === 'pi' || agentKind === 'grok-build'
? 'pi-session-old'
: 'sdk-uuid-old',
workDir: '/repo',
Expand Down Expand Up @@ -957,6 +957,30 @@ describe('commitRewindAtMessage', () => {
expect(result.id).toBe('sess-1');
});

it('Grok Build hosted loop: rewind uses the Pi tail-turn path, not ACP fail-closed', async () => {
useFakeSession('grok-build');
commitRewindFilesMock.mockResolvedValueOnce({ sdkSessionId: 'grok-build-session-rewound' });
selectQueue.push([makeUserMessageRow({ agentMeta: null })]);
selectQueue.push([]);
selectQueue.push([
makeUserMessageRow({ clientId: 'client-id', createdAt: 3000 }),
makeUserMessageRow({ clientId: 'later-user', createdAt: 5000 }),
]);
selectQueue.push([
makeSessionRow({ agentKind: 'grok-build', sdkSessionId: 'grok-build-session-rewound' }),
]);

const result = await commitRewindAtMessage('sess-1', 'client-id');

expect(result.id).toBe('sess-1');
expect(commitRewindFilesMock).toHaveBeenCalledOnce();
expect(commitRewindFilesMock).toHaveBeenCalledWith('', '', { tailTurnsToDrop: 2 });
expect(txCalls.find((call) => call.name === 'rewind.commit')?.args).toMatchObject({
sessionId: 'sess-1',
sdkSessionId: 'grok-build-session-rewound',
});
});

it('Codex: previews file rewind savepoints before commit (legacy numstat 方向对调)', async () => {
useFakeSession('codex');
detectCwdMock.mockResolvedValueOnce({ gitInstalled: true, isGitRepo: true, repoRoot: '/repo', isInsideWorktree: false });
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/cindy-brain/errandPrefsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ownerScopedUserDataPath } from '../appSessionState.js';
const log = desktopMakerLogger.child('errand-prefs-store');

/** errand 会话可选的 agent 种类(与 sessions.agent_kind 同词汇表)。 */
export const GHOST_ERRAND_AGENT_KINDS = ['cc', 'codex', 'pi'] as const;
export const GHOST_ERRAND_AGENT_KINDS = ['cc', 'codex', 'pi', 'grok-build'] as const;
export type GhostErrandAgentKind = (typeof GHOST_ERRAND_AGENT_KINDS)[number];

/** errand 会话可选的思考强度(与 worker 同集合;minimal 刻意不收)。 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,11 @@ describe('接管陈旧锁', () => {
await expect(
withCrossProcessLock(lock, { label: 'churn', waitMs: 2_000 }, async (s) => s),
).resolves.toEqual({ held: false, reason: 'busy' });
expect(performance.now() - started).toBeLessThan(1_000);
// Windows rename+utimes on three reclaim hops is slower than the
// POSIX 1s bound (CI measured 1372ms). Still well under waitMs.
expect(performance.now() - started).toBeLessThan(
process.platform === 'win32' ? 2_000 : 1_000,
);
expect(takeovers).toBe(3);
} finally {
spy.mockRestore();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function deps(over?: Partial<HookDefaultsDeps>): HookDefaultsDeps {
'claude-code': { providerId: null, model: 'claude-opus-4-8', effort: 'xhigh' },
codex: { providerId: 'xd', model: 'gpt-5.5', effort: 'high' },
pi: { providerId: null, model: 'claude-sonnet-5', effort: 'high' },
'grok-build': { providerId: null, model: 'grok-4.6', effort: 'high' },
},
}),
getModels: (agentKind) =>
Expand Down Expand Up @@ -116,6 +117,7 @@ describe('resolveHookSessionConfig', () => {
'claude-code': { providerId: null, model: 'claude-opus-4-8', effort: 'ultra-draft' },
codex: { providerId: null, model: 'gpt-5.5', effort: 'high' },
pi: { providerId: null, model: 'claude-sonnet-5', effort: 'high' },
'grok-build': { providerId: null, model: 'grok-4.6', effort: 'high' },
},
}),
}),
Expand All @@ -138,6 +140,7 @@ describe('resolveHookSessionConfig', () => {
'claude-code': { providerId: null, model: 'gone-model', effort: 'high' },
codex: { providerId: null, model: 'gpt-5.5', effort: 'high' },
pi: { providerId: null, model: 'claude-sonnet-5', effort: 'high' },
'grok-build': { providerId: null, model: 'grok-4.6', effort: 'high' },
},
}),
}),
Expand Down Expand Up @@ -203,6 +206,7 @@ describe('resolveHookSessionConfig', () => {
'claude-code': { providerId: null, model: 'claude-opus-4-8', effort: 'xhigh' },
codex: { providerId: ' ', model: 'gpt-5.5', effort: 'high' },
pi: { providerId: null, model: 'claude-sonnet-5', effort: 'high' },
'grok-build': { providerId: null, model: 'grok-4.6', effort: 'high' },
},
}),
}),
Expand Down
16 changes: 8 additions & 8 deletions apps/desktop/src/main/hook-control/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,24 @@ import type { Effort } from '@cindy/maker-core';
/** 依赖注入面: IM 默认值 + 各 agent 当前可用模型清单。 */
export interface HookDefaultsDeps {
readDefaults: () => {
agentKind: 'claude-code' | 'codex' | 'pi';
agentKind: 'claude-code' | 'codex' | 'pi' | 'grok-build';
agents: Record<
'claude-code' | 'codex' | 'pi',
'claude-code' | 'codex' | 'pi' | 'grok-build',
{ providerId: string | null; model: string; effort: string }
>;
};
getModels: (agentKind: 'claude-code' | 'codex' | 'pi') => Array<{
getModels: (agentKind: 'claude-code' | 'codex' | 'pi' | 'grok-build') => Array<{
id: string;
efforts: readonly string[];
defaultEffort: string | null;
}>;
/** 该 agent 支持的权限档 id 清单(capabilities.permissionModes)。 */
getPermissionModes: (agentKind: 'claude-code' | 'codex' | 'pi') => readonly string[];
getPermissionModes: (agentKind: 'claude-code' | 'codex' | 'pi' | 'grok-build') => readonly string[];
log: { warn(msg: string): void };
}

export interface ResolvedHookSessionConfig {
agentKind: 'claude-code' | 'codex' | 'pi';
agentKind: 'claude-code' | 'codex' | 'pi' | 'grok-build';
model: string;
effort: Effort | undefined;
permissionMode: string;
Expand All @@ -66,7 +66,7 @@ export interface ResolvedHookSessionConfig {
providerId: string | null;
}

const AGENT_KINDS = new Set(['claude-code', 'codex', 'pi']);
const AGENT_KINDS = new Set(['claude-code', 'codex', 'pi', 'grok-build']);

/**
* 合成新 hook 会话的 agent/model/effort。
Expand All @@ -84,9 +84,9 @@ export function resolveHookSessionConfig(
const defaults = deps.readDefaults();

// 1. agent: 显式合法值 > 草稿默认
const agentKind: 'claude-code' | 'codex' | 'pi' =
const agentKind: 'claude-code' | 'codex' | 'pi' | 'grok-build' =
overrides.agentKind !== null && AGENT_KINDS.has(overrides.agentKind)
? (overrides.agentKind as 'claude-code' | 'codex' | 'pi')
? (overrides.agentKind as 'claude-code' | 'codex' | 'pi' | 'grok-build')
: defaults.agentKind;

const models = deps.getModels(agentKind);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,17 @@ describe('im default settings store', () => {
model: 'gpt-5.5',
effort: 'high',
},
// legacy root mirror 是 resolved 满射快照(global 才做 diff),pi 槽为系统默认
// legacy root mirror 是 resolved 满射快照(global 才做 diff),未改过的槽为系统默认
pi: {
providerId: null,
model: 'claude-sonnet-5',
effort: 'high',
},
'grok-build': {
providerId: null,
model: 'grok-4.6',
effort: 'high',
},
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe('parseImDefaultSettingsPatch', () => {
'claude-code': { providerId: 'p-cc', model: 'm-cc', effort: 'low' },
codex: { providerId: 'p-codex', model: 'm-codex', effort: 'low' },
pi: { providerId: 'p-pi', model: 'm-pi', effort: 'low' },
'grok-build': { providerId: 'p-gb', model: 'm-gb', effort: 'low' },
},
};
for (const key of Object.keys(IM_DEFAULT_SETTINGS) as Array<keyof ImDefaultSettings>) {
Expand Down
10 changes: 8 additions & 2 deletions apps/desktop/src/main/im/defaultSessionSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
import {
IM_DEFAULT_EFFORT_OVERRIDES,
IM_DEFAULT_SETTINGS,
isImDefaultAgentKind,
type ImDefaultAgentKind,
type ImDefaultAgentSettings,
type ImDefaultSettingsChannel,
} from '../../shared/imDefaultSettings.js';
Expand Down Expand Up @@ -72,7 +74,10 @@ export async function resolveImSessionDefaults(
const requestedSettings = raw.agents[requestedAgent];
const model = pickModel(requestedAgent, requestedSettings, config, providers);
const agentKind = model.agentKind;
const agentSettings = raw.agents[agentKind] ?? requestedSettings;
// IM 默认设置按可选 harness 存 per-agent 拷贝(含 grok-build);
// 落到未知 agent 时沿用请求 agent 的那份,来源/档位随后仍按落地模型 reconcile。
const agentSettings =
(isImDefaultAgentKind(agentKind) ? raw.agents[agentKind] : undefined) ?? requestedSettings;
// 先定来源再定 effort:effort 支持是 per-(来源, 模型) 的,保存的来源被停用改道后,
// 必须按**最终落地来源**的拷贝 reconcile —— 按第一份 connected 拷贝(可能正是那份
// 停用拷贝)算出的档位,启用替代来源未必支持,直建会话会被上游拒
Expand Down Expand Up @@ -122,7 +127,8 @@ export async function resolveDefaultProviderIdForModel(
}

function pickModel(
requestedAgent: AgentKind,
// 请求 agent 恒来自 IM 默认设置(三选一);兜底才可能落到渠道配置的其它 agent。
requestedAgent: ImDefaultAgentKind,
settings: ImDefaultAgentSettings,
config: ImOrchestratorConfig,
providers: ProviderView[] | null,
Expand Down
18 changes: 13 additions & 5 deletions apps/desktop/src/main/im/defaultSettingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import path from 'node:path';
import { app } from 'electron';

import {
IM_DEFAULT_AGENT_KINDS,
IM_DEFAULT_SETTINGS,
IM_DEFAULT_SETTINGS_CHANNELS,
type ImDefaultAgentKind,
Expand Down Expand Up @@ -81,6 +82,10 @@ function normalizeSettings(raw: unknown): ImDefaultSettings {
'pi',
rawAgentOrLegacy(rawAgents, 'pi', agentKind, legacySettings),
),
'grok-build': normalizeAgentSettings(
'grok-build',
rawAgentOrLegacy(rawAgents, 'grok-build', agentKind, legacySettings),
),
},
};
}
Expand Down Expand Up @@ -348,9 +353,10 @@ function settingsOverrides(
overrides.groupPermissionMode = value.groupPermissionMode;
}
const agents: Partial<Record<ImDefaultAgentKind, ImDefaultAgentSettings>> = {};
for (const agentKind of ['claude-code', 'codex', 'pi'] as const) {
if (!agentSettingsEqual(value.agents[agentKind], defaults.agents[agentKind])) {
agents[agentKind] = value.agents[agentKind];
for (const agentKind of IM_DEFAULT_AGENT_KINDS) {
const current = value.agents[agentKind] ?? defaults.agents[agentKind];
if (!agentSettingsEqual(current, defaults.agents[agentKind])) {
agents[agentKind] = current;
}
}
if (Object.keys(agents).length > 0) overrides.agents = agents;
Expand All @@ -364,8 +370,9 @@ function settingsCustomizedKeys(value: ImDefaultSettings, defaults: ImDefaultSet
if (value.groupPermissionMode !== defaults.groupPermissionMode) {
keys.push('groupPermissionMode');
}
for (const agentKind of ['claude-code', 'codex', 'pi'] as const) {
if (!agentSettingsEqual(value.agents[agentKind], defaults.agents[agentKind])) {
for (const agentKind of IM_DEFAULT_AGENT_KINDS) {
const current = value.agents[agentKind] ?? defaults.agents[agentKind];
if (!agentSettingsEqual(current, defaults.agents[agentKind])) {
keys.push(`agents.${agentKind}`);
}
}
Expand All @@ -385,6 +392,7 @@ function cloneSettings(settings: ImDefaultSettings): ImDefaultSettings {
'claude-code': { ...settings.agents['claude-code'] },
codex: { ...settings.agents.codex },
pi: { ...settings.agents.pi },
'grok-build': { ...settings.agents['grok-build'] },
},
};
}
3 changes: 2 additions & 1 deletion apps/desktop/src/main/im/parseDefaultSettingsPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

import {
IM_DEFAULT_AGENT_KINDS,
IM_DEFAULT_SETTINGS,
isImDefaultAgentKind,
isImDefaultEffort,
Expand Down Expand Up @@ -57,7 +58,7 @@ export function parseImDefaultSettingsPatch(raw: unknown): ImDefaultSettingsPatc
const agentsPatch: NonNullable<ImDefaultSettingsPatch['agents']> = {};
// 三个 harness 必须对称解析;漏掉 pi 会让 IM 设置页切 Pi 后改模型静默丢弃
// (store 本身支持 pi,见 defaultSettingsStore / IM_DEFAULT_SETTINGS.agents.pi)。
for (const kind of ['claude-code', 'codex', 'pi'] as const) {
for (const kind of IM_DEFAULT_AGENT_KINDS) {
if (kind in agentInput) {
agentsPatch[kind] = parseImDefaultAgentSettings(kind, agentInput[kind]);
}
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/main/localDb/chatHistoryReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const messageRowid = sql<number>`"messages"."rowid"`;
// ── Types ───────────────────────────────────────────────────────────────────

export type HistoryOrder = 'asc' | 'desc';
export type HistoryAgentKind = 'cc' | 'codex' | 'pi';
/** sessions.agent_kind 的历史筛选取值;'cc' 是 Claude Code 的历史存储形态。 */
export type HistoryAgentKind = 'cc' | 'codex' | 'pi' | 'grok-build';

export interface HistoryCursor {
createdAt: number; // unix ms
Expand Down
Loading