mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: add Grok Build support (#1030)
* test: define Grok Build integration behavior * feat: add Grok Build agent integration * test: cover Grok permissions and resume paths * docs: add Grok Build setup guide * fix: scope Grok ACP discovery to session cwd * fix: align Grok permission UI semantics * docs: clarify Grok runner setup * test: require Grok create model and effort options * feat: add Grok create model and effort pickers * test: define Grok runtime parity behavior * feat: add Grok runtime ACP controls and discovery * fix: tighten Grok runtime controls * fix: suppress nonfatal Grok title quota errors * feat: support Grok Auto permission mode * feat: forward ACP native session titles for Grok * fix: guard Grok Windows shell arguments
This commit is contained in:
@@ -187,6 +187,79 @@ describe('AcpSdkBackend', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('captures Grok reasoning efforts from x.ai session metadata and switches with set_mode', async () => {
|
||||
const backend = new AcpSdkBackend({ command: 'grok' });
|
||||
const calls: Array<{ method: string; params: unknown }> = [];
|
||||
const backendInternal = backend as unknown as {
|
||||
transport: { sendRequest: (method: string, params: unknown) => Promise<unknown>; close: () => Promise<void> } | null;
|
||||
};
|
||||
backendInternal.transport = {
|
||||
sendRequest: async (method, params) => {
|
||||
calls.push({ method, params });
|
||||
if (method === 'session/new') {
|
||||
return {
|
||||
sessionId: 'grok-session-1',
|
||||
models: {
|
||||
currentModelId: 'grok-4.5',
|
||||
availableModels: [{
|
||||
modelId: 'grok-4.5',
|
||||
name: 'Grok 4.5',
|
||||
_meta: {
|
||||
reasoningEfforts: [
|
||||
{ value: 'high', label: 'High Effort', default: true },
|
||||
{ value: 'low', label: 'Low Effort', default: false }
|
||||
]
|
||||
}
|
||||
}]
|
||||
},
|
||||
_meta: {
|
||||
availableCommands: [{ name: 'auto' }],
|
||||
'x.ai/sessionConfig': {
|
||||
options: [
|
||||
{ id: 'high', category: 'mode', label: 'High Effort', selected: false },
|
||||
{ id: 'low', category: 'mode', label: 'Low Effort', selected: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (method === 'session/set_mode') return { meta: null };
|
||||
return null;
|
||||
},
|
||||
close: async () => {}
|
||||
};
|
||||
|
||||
const sessionId = await backend.newSession({ cwd: '/tmp/x', mcpServers: [] });
|
||||
|
||||
expect(backend.getSessionModelsMetadata(sessionId)).toEqual({
|
||||
availableModels: [{
|
||||
modelId: 'grok-4.5',
|
||||
name: 'Grok 4.5',
|
||||
reasoningEfforts: [
|
||||
{ value: 'high', name: 'High Effort', isDefault: true },
|
||||
{ value: 'low', name: 'Low Effort', isDefault: false }
|
||||
]
|
||||
}],
|
||||
currentModelId: 'grok-4.5'
|
||||
});
|
||||
expect(backend.getThoughtLevelConfigOption(sessionId)).toMatchObject({
|
||||
currentValue: 'low',
|
||||
options: [
|
||||
{ value: 'high', name: 'High Effort' },
|
||||
{ value: 'low', name: 'Low Effort' }
|
||||
]
|
||||
});
|
||||
expect(backend.hasAvailableCommand(sessionId, 'auto')).toBe(true);
|
||||
|
||||
await backend.setMode(sessionId, 'high');
|
||||
|
||||
expect(calls).toContainEqual({
|
||||
method: 'session/set_mode',
|
||||
params: { sessionId, modeId: 'high' }
|
||||
});
|
||||
expect(backend.getThoughtLevelConfigOption(sessionId)?.currentValue).toBe('high');
|
||||
});
|
||||
|
||||
it('merges configOptions model variants into availableModels when both are present', async () => {
|
||||
const backend = new AcpSdkBackend({ command: 'agent' });
|
||||
const backendInternal = backend as unknown as {
|
||||
@@ -755,6 +828,52 @@ describe('AcpSdkBackend', () => {
|
||||
expect(realtimeUsage.map((m) => m.contextTokens)).toEqual([1_000, 2_500]);
|
||||
});
|
||||
|
||||
it('forwards title changes from session_info_update', () => {
|
||||
const backend = new AcpSdkBackend({ command: 'agent' });
|
||||
const updates: Array<{ title?: string | null }> = [];
|
||||
backend.setSessionInfoUpdateListener((update) => updates.push(update));
|
||||
|
||||
const backendInternal = backend as unknown as {
|
||||
activeSessionId: string | null;
|
||||
handleSessionUpdate: (params: unknown) => void;
|
||||
};
|
||||
backendInternal.activeSessionId = 'session-1';
|
||||
|
||||
backendInternal.handleSessionUpdate({
|
||||
sessionId: 'session-1',
|
||||
update: {
|
||||
sessionUpdate: ACP_SESSION_UPDATE_TYPES.sessionInfoUpdate,
|
||||
title: 'Native ACP title'
|
||||
}
|
||||
});
|
||||
backendInternal.handleSessionUpdate({
|
||||
sessionId: 'session-1',
|
||||
update: {
|
||||
sessionUpdate: ACP_SESSION_UPDATE_TYPES.sessionInfoUpdate,
|
||||
title: null
|
||||
}
|
||||
});
|
||||
backendInternal.handleSessionUpdate({
|
||||
sessionId: 'session-1',
|
||||
update: {
|
||||
sessionUpdate: ACP_SESSION_UPDATE_TYPES.sessionInfoUpdate,
|
||||
title: 123
|
||||
}
|
||||
});
|
||||
backendInternal.handleSessionUpdate({
|
||||
sessionId: 'other-session',
|
||||
update: {
|
||||
sessionUpdate: ACP_SESSION_UPDATE_TYPES.sessionInfoUpdate,
|
||||
title: 'Wrong session'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updates).toEqual([
|
||||
{ title: 'Native ACP title' },
|
||||
{ title: null }
|
||||
]);
|
||||
});
|
||||
|
||||
it('emits a context-only usage on finalize when the prompt response carries no usage', async () => {
|
||||
backendStatics.UPDATE_QUIET_PERIOD_MS = 25;
|
||||
backendStatics.UPDATE_DRAIN_TIMEOUT_MS = 200;
|
||||
|
||||
@@ -25,9 +25,14 @@ type AcpUsageUpdate = {
|
||||
contextWindow: number | undefined;
|
||||
};
|
||||
|
||||
export type AcpSessionInfoUpdate = {
|
||||
title?: string | null;
|
||||
};
|
||||
|
||||
export type AcpModelDescriptor = {
|
||||
modelId: string;
|
||||
name?: string;
|
||||
reasoningEfforts?: Array<{ value: string; name?: string; isDefault?: boolean }>;
|
||||
};
|
||||
|
||||
export type AcpSessionModelsMetadata = {
|
||||
@@ -59,6 +64,9 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
private readonly pendingPermissions = new Map<string, PendingPermission>();
|
||||
private readonly sessionModelsMetadata = new Map<string, AcpSessionModelsMetadata>();
|
||||
private readonly sessionConfigOptions = new Map<string, AcpConfigOptionDescriptor[]>();
|
||||
private readonly initialAvailableCommands = new Set<string>();
|
||||
private readonly sessionAvailableCommands = new Map<string, Set<string>>();
|
||||
private autoPermissionModeEnabled: boolean | null = null;
|
||||
private messageHandler: AcpMessageHandler | null = null;
|
||||
private activeSessionId: string | null = null;
|
||||
private initializeResult: AcpInitializeResult | null = null;
|
||||
@@ -69,6 +77,7 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
private latestUsageUpdate: AcpUsageUpdate | null = null;
|
||||
private promptUsageCallback: ((msg: AgentMessage) => void) | null = null;
|
||||
private usageUpdateListener: ((msg: AgentMessage) => void) | null = null;
|
||||
private sessionInfoUpdateListener: ((update: AcpSessionInfoUpdate) => void) | null = null;
|
||||
private lastForwardedUsageUpdate: AcpUsageUpdate | null = null;
|
||||
|
||||
/** Retry configuration for ACP initialization */
|
||||
@@ -120,6 +129,12 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
this.transport.onNotification((method, params) => {
|
||||
if (method === 'session/update') {
|
||||
this.handleSessionUpdate(params);
|
||||
} else if (
|
||||
method === '_x.ai/settings/update'
|
||||
&& isObject(params)
|
||||
&& 'auto_permission_mode_enabled' in params
|
||||
) {
|
||||
this.autoPermissionModeEnabled = params.auto_permission_mode_enabled === true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -161,6 +176,8 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
throw new Error('Invalid initialize response from ACP agent');
|
||||
}
|
||||
|
||||
this.captureAvailableCommands(null, response);
|
||||
|
||||
this.initializeResult = {
|
||||
protocolVersion: response.protocolVersion,
|
||||
authMethods: Array.isArray(response.authMethods)
|
||||
@@ -238,6 +255,7 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
try {
|
||||
await this.transport.sendRequest('session/set_mode', { sessionId, modeId });
|
||||
this.setModeSupported = true;
|
||||
this.updateThoughtLevelCurrentValue(sessionId, modeId);
|
||||
return;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -335,7 +353,7 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
modelId
|
||||
});
|
||||
|
||||
if (opts?.flavor === 'opencode') {
|
||||
if (opts?.flavor === 'opencode' || opts?.flavor === 'grok') {
|
||||
// OpenCode's set_model response only carries an opaque `_meta` block,
|
||||
// not `availableModels`/`currentModelId`. Optimistically update the
|
||||
// cached currentModelId (the call succeeded, so the agent has switched)
|
||||
@@ -380,11 +398,24 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
return this.sessionConfigOptions.get(sessionId)?.find((option) => option.category === 'thought_level');
|
||||
}
|
||||
|
||||
hasAvailableCommand(sessionId: string, command: string): boolean {
|
||||
if (command === 'auto' && this.autoPermissionModeEnabled === true) {
|
||||
return true;
|
||||
}
|
||||
return this.sessionAvailableCommands.get(sessionId)?.has(command)
|
||||
?? this.initialAvailableCommands.has(command);
|
||||
}
|
||||
|
||||
/** Forwards ACP `usage_update` to the web status bar when no prompt is active (e.g. session resume). */
|
||||
setUsageUpdateListener(listener: ((msg: AgentMessage) => void) | null): void {
|
||||
this.usageUpdateListener = listener;
|
||||
}
|
||||
|
||||
/** Forwards ACP `session_info_update` metadata independently of prompt turns. */
|
||||
setSessionInfoUpdateListener(listener: ((update: AcpSessionInfoUpdate) => void) | null): void {
|
||||
this.sessionInfoUpdateListener = listener;
|
||||
}
|
||||
|
||||
async prompt(
|
||||
sessionId: string,
|
||||
content: PromptContent[],
|
||||
@@ -553,6 +584,9 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
this.activeSessionId = null;
|
||||
this.isProcessingMessage = false;
|
||||
this.sessionModelsMetadata.clear();
|
||||
this.initialAvailableCommands.clear();
|
||||
this.sessionAvailableCommands.clear();
|
||||
this.autoPermissionModeEnabled = null;
|
||||
this.notifyResponseComplete();
|
||||
await this.transport.close();
|
||||
this.transport = null;
|
||||
@@ -566,10 +600,24 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
}
|
||||
this.lastSessionUpdateAt = Date.now();
|
||||
const update = params.update;
|
||||
if (sessionId) {
|
||||
this.captureAvailableCommands(sessionId, update);
|
||||
}
|
||||
this.captureSessionInfoUpdate(update);
|
||||
this.captureUsageUpdate(update);
|
||||
this.messageHandler?.handleUpdate(update);
|
||||
}
|
||||
|
||||
private captureSessionInfoUpdate(update: unknown): void {
|
||||
if (!isObject(update)) return;
|
||||
if (asString(update.sessionUpdate) !== ACP_SESSION_UPDATE_TYPES.sessionInfoUpdate) return;
|
||||
if (!Object.prototype.hasOwnProperty.call(update, 'title')) return;
|
||||
|
||||
const title = update.title;
|
||||
if (typeof title !== 'string' && title !== null) return;
|
||||
this.sessionInfoUpdateListener?.({ title });
|
||||
}
|
||||
|
||||
private captureUsageUpdate(update: unknown): void {
|
||||
if (!isObject(update)) return;
|
||||
|
||||
@@ -771,6 +819,16 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
});
|
||||
}
|
||||
|
||||
private updateThoughtLevelCurrentValue(sessionId: string, value: string): void {
|
||||
const options = this.sessionConfigOptions.get(sessionId);
|
||||
if (!options) return;
|
||||
this.sessionConfigOptions.set(sessionId, options.map((option) => (
|
||||
option.category === 'thought_level'
|
||||
? { ...option, currentValue: value }
|
||||
: option
|
||||
)));
|
||||
}
|
||||
|
||||
/** After a successful model config apply, avoid stale base-only ACP currentValue overwriting cache. */
|
||||
pinSessionModelWireId(sessionId: string, modelId: string): void {
|
||||
this.updateCurrentModelOptimistic(sessionId, modelId);
|
||||
@@ -805,12 +863,41 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
private captureSessionMetadata(sessionId: string, response: unknown): void {
|
||||
this.captureSessionModelsMetadata(sessionId, response);
|
||||
this.captureSessionConfigOptions(sessionId, response);
|
||||
this.captureAvailableCommands(sessionId, response);
|
||||
}
|
||||
|
||||
private captureAvailableCommands(sessionId: string | null, source: unknown): void {
|
||||
if (!isObject(source)) return;
|
||||
|
||||
const meta = isObject(source._meta) ? source._meta : null;
|
||||
const rawCommands = Array.isArray(source.availableCommands)
|
||||
? source.availableCommands
|
||||
: meta && Array.isArray(meta.availableCommands)
|
||||
? meta.availableCommands
|
||||
: null;
|
||||
if (!rawCommands) return;
|
||||
|
||||
const commands = new Set(
|
||||
rawCommands
|
||||
.filter((entry): entry is Record<string, unknown> => isObject(entry))
|
||||
.map((entry) => asString(entry.name) ?? '')
|
||||
.filter((name) => name.length > 0)
|
||||
);
|
||||
if (sessionId) {
|
||||
this.sessionAvailableCommands.set(sessionId, commands);
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialAvailableCommands.clear();
|
||||
for (const command of commands) {
|
||||
this.initialAvailableCommands.add(command);
|
||||
}
|
||||
}
|
||||
|
||||
private captureSessionConfigOptions(sessionId: string, response: unknown): void {
|
||||
if (!isObject(response) || !Array.isArray(response.configOptions)) return;
|
||||
if (!isObject(response)) return;
|
||||
|
||||
const options = response.configOptions
|
||||
const options = (Array.isArray(response.configOptions) ? response.configOptions : [])
|
||||
.filter((entry): entry is Record<string, unknown> => isObject(entry))
|
||||
.map((entry): AcpConfigOptionDescriptor | null => {
|
||||
const id = asString(entry.id);
|
||||
@@ -831,7 +918,33 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
})
|
||||
.filter((entry): entry is AcpConfigOptionDescriptor => entry !== null);
|
||||
|
||||
this.sessionConfigOptions.set(sessionId, options);
|
||||
const meta = isObject(response._meta) ? response._meta : null;
|
||||
const xaiConfig = meta && isObject(meta['x.ai/sessionConfig'])
|
||||
? meta['x.ai/sessionConfig']
|
||||
: null;
|
||||
const xaiOptions = xaiConfig && Array.isArray(xaiConfig.options)
|
||||
? xaiConfig.options.filter((entry): entry is Record<string, unknown> => isObject(entry))
|
||||
: [];
|
||||
const effortOptions = xaiOptions
|
||||
.filter((entry) => asString(entry.category) === 'mode')
|
||||
.map((entry) => ({
|
||||
value: asString(entry.id) ?? '',
|
||||
name: asString(entry.label) ?? undefined,
|
||||
selected: entry.selected === true
|
||||
}))
|
||||
.filter((entry) => entry.value.length > 0);
|
||||
if (effortOptions.length > 0) {
|
||||
options.push({
|
||||
id: 'x.ai/reasoning-effort',
|
||||
category: 'thought_level',
|
||||
currentValue: effortOptions.find((entry) => entry.selected)?.value,
|
||||
options: effortOptions.map(({ value, name }) => ({ value, name }))
|
||||
});
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
this.sessionConfigOptions.set(sessionId, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -886,7 +999,11 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
}
|
||||
|
||||
const byModelId = new Map<string, AcpModelDescriptor>();
|
||||
const addModel = (modelId: string, name?: string) => {
|
||||
const addModel = (
|
||||
modelId: string,
|
||||
name?: string,
|
||||
reasoningEfforts?: AcpModelDescriptor['reasoningEfforts']
|
||||
) => {
|
||||
const trimmedId = modelId.trim();
|
||||
if (!trimmedId) return;
|
||||
const trimmedName = name?.trim();
|
||||
@@ -895,13 +1012,13 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
byModelId.set(
|
||||
trimmedId,
|
||||
trimmedName && trimmedName !== trimmedId
|
||||
? { modelId: trimmedId, name: trimmedName }
|
||||
: { modelId: trimmedId }
|
||||
? { modelId: trimmedId, name: trimmedName, ...(reasoningEfforts ? { reasoningEfforts } : {}) }
|
||||
: { modelId: trimmedId, ...(reasoningEfforts ? { reasoningEfforts } : {}) }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!existing.name && trimmedName && trimmedName !== trimmedId) {
|
||||
byModelId.set(trimmedId, { modelId: trimmedId, name: trimmedName });
|
||||
byModelId.set(trimmedId, { ...existing, name: trimmedName });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -910,14 +1027,25 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
if (!isObject(entry)) continue;
|
||||
const modelId = asString(entry.modelId) ?? asString(entry.value);
|
||||
if (!modelId) continue;
|
||||
addModel(modelId, asString(entry.name) ?? undefined);
|
||||
const meta = isObject(entry._meta) ? entry._meta : null;
|
||||
const reasoningEfforts = meta && Array.isArray(meta.reasoningEfforts)
|
||||
? meta.reasoningEfforts
|
||||
.filter((effort): effort is Record<string, unknown> => isObject(effort))
|
||||
.map((effort) => ({
|
||||
value: asString(effort.value) ?? asString(effort.id) ?? '',
|
||||
name: asString(effort.label) ?? undefined,
|
||||
isDefault: effort.default === true
|
||||
}))
|
||||
.filter((effort) => effort.value.length > 0)
|
||||
: undefined;
|
||||
addModel(modelId, asString(entry.name) ?? undefined, reasoningEfforts);
|
||||
}
|
||||
} else {
|
||||
// Preserve previously-captured availableModels when the response only
|
||||
// updates currentModelId (e.g. a setModel response from some agents).
|
||||
const existing = this.sessionModelsMetadata.get(sessionId);
|
||||
for (const entry of existing?.availableModels ?? []) {
|
||||
addModel(entry.modelId, entry.name);
|
||||
addModel(entry.modelId, entry.name, entry.reasoningEfforts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,23 @@ describe('sessionConfigRpc', () => {
|
||||
expect(onApply).toHaveBeenCalledWith({ modelReasoningEffort: 'high' })
|
||||
})
|
||||
|
||||
it('applies nullable launch effort for Grok runtime switching', async () => {
|
||||
const harness = createRpcHarness()
|
||||
const onApply = vi.fn()
|
||||
|
||||
registerSessionConfigRpc({
|
||||
rpcHandlerManager: harness.rpcHandlerManager,
|
||||
flavor: 'grok',
|
||||
effortMode: 'nullable',
|
||||
onApply
|
||||
})
|
||||
|
||||
const result = await harness.getHandler()({ effort: 'low' }) as { applied: Record<string, unknown> }
|
||||
|
||||
expect(result.applied.effort).toBe('low')
|
||||
expect(onApply).toHaveBeenCalledWith({ effort: 'low' })
|
||||
})
|
||||
|
||||
it('rejects model config for agents configured to reject model changes', async () => {
|
||||
const harness = createRpcHarness()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ type SessionConfigState<TPermissionMode extends PermissionMode = PermissionMode>
|
||||
permissionMode?: TPermissionMode
|
||||
model?: string | null
|
||||
modelReasoningEffort?: string | null
|
||||
effort?: string | null
|
||||
}
|
||||
|
||||
type RegisterSessionConfigRpcOptions<TPermissionMode extends PermissionMode = PermissionMode> = {
|
||||
@@ -15,6 +16,7 @@ type RegisterSessionConfigRpcOptions<TPermissionMode extends PermissionMode = Pe
|
||||
flavor: AgentFlavor
|
||||
modelMode?: 'nullable' | 'ignore' | 'reject'
|
||||
modelReasoningEffortMode?: 'nullable' | 'ignore' | 'reject'
|
||||
effortMode?: 'nullable' | 'ignore' | 'reject'
|
||||
appliedFallback?: () => Record<string, unknown>
|
||||
onApply: (config: SessionConfigState<TPermissionMode>) => void
|
||||
onAfterApply?: () => void
|
||||
@@ -58,6 +60,7 @@ export function registerSessionConfigRpc<TPermissionMode extends PermissionMode>
|
||||
flavor,
|
||||
modelMode = 'reject',
|
||||
modelReasoningEffortMode = 'reject',
|
||||
effortMode = 'reject',
|
||||
appliedFallback,
|
||||
onApply,
|
||||
onAfterApply
|
||||
@@ -67,7 +70,7 @@ export function registerSessionConfigRpc<TPermissionMode extends PermissionMode>
|
||||
throw new Error('Invalid session config payload')
|
||||
}
|
||||
|
||||
const config = payload as { permissionMode?: unknown; model?: unknown; modelReasoningEffort?: unknown }
|
||||
const config = payload as { permissionMode?: unknown; model?: unknown; modelReasoningEffort?: unknown; effort?: unknown }
|
||||
const applied: Record<string, unknown> = {}
|
||||
const next: SessionConfigState<TPermissionMode> = {}
|
||||
|
||||
@@ -97,6 +100,16 @@ export function registerSessionConfigRpc<TPermissionMode extends PermissionMode>
|
||||
}
|
||||
}
|
||||
|
||||
if (config.effort !== undefined) {
|
||||
if (effortMode === 'reject') {
|
||||
throw new Error('Invalid effort')
|
||||
}
|
||||
if (effortMode === 'nullable') {
|
||||
next.effort = resolveNullableSessionModel(config.effort)
|
||||
applied.effort = next.effort
|
||||
}
|
||||
}
|
||||
|
||||
onApply(next)
|
||||
onAfterApply?.()
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ describe('bootstrapExistingSession', () => {
|
||||
codexSessionId: 'codex-thread-1',
|
||||
geminiSessionId: 'gemini-thread-1',
|
||||
opencodeSessionId: 'opencode-thread-1',
|
||||
grokSessionId: 'grok-thread-1',
|
||||
cursorSessionId: 'cursor-thread-1',
|
||||
cursorSessionProtocol: 'acp',
|
||||
summary: {
|
||||
@@ -164,6 +165,7 @@ describe('bootstrapExistingSession', () => {
|
||||
codexSessionId: 'codex-thread-1',
|
||||
geminiSessionId: 'gemini-thread-1',
|
||||
opencodeSessionId: 'opencode-thread-1',
|
||||
grokSessionId: 'grok-thread-1',
|
||||
cursorSessionId: 'cursor-thread-1',
|
||||
cursorSessionProtocol: 'acp',
|
||||
summary: {
|
||||
@@ -176,12 +178,14 @@ describe('bootstrapExistingSession', () => {
|
||||
expect(sessionClient.updateMetadata).toHaveBeenCalledOnce()
|
||||
const updateHandler = sessionClient.updateMetadata.mock.calls[0][0]
|
||||
expect(updateHandler(session.metadata)).toEqual(expect.objectContaining({
|
||||
codexSessionId: 'codex-thread-1'
|
||||
codexSessionId: 'codex-thread-1',
|
||||
grokSessionId: 'grok-thread-1'
|
||||
}))
|
||||
expect(notifyRunnerSessionStartedMock).toHaveBeenCalledWith(
|
||||
'hapi-session-1',
|
||||
expect.objectContaining({
|
||||
codexSessionId: 'codex-thread-1'
|
||||
codexSessionId: 'codex-thread-1',
|
||||
grokSessionId: 'grok-thread-1'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -97,6 +97,7 @@ function pickExistingSessionMetadata(metadata: Metadata | null | undefined): Par
|
||||
if (metadata.codexSessionId !== undefined) preserved.codexSessionId = metadata.codexSessionId
|
||||
if (metadata.geminiSessionId !== undefined) preserved.geminiSessionId = metadata.geminiSessionId
|
||||
if (metadata.opencodeSessionId !== undefined) preserved.opencodeSessionId = metadata.opencodeSessionId
|
||||
if (metadata.grokSessionId !== undefined) preserved.grokSessionId = metadata.grokSessionId
|
||||
if (metadata.cursorSessionId !== undefined) preserved.cursorSessionId = metadata.cursorSessionId
|
||||
if (metadata.cursorSessionProtocol !== undefined) preserved.cursorSessionProtocol = metadata.cursorSessionProtocol
|
||||
if (metadata.kimiSessionId !== undefined) preserved.kimiSessionId = metadata.kimiSessionId
|
||||
|
||||
@@ -71,6 +71,7 @@ export type PermissionResponse =
|
||||
export type AgentSessionModelDescriptor = {
|
||||
modelId: string;
|
||||
name?: string;
|
||||
reasoningEfforts?: Array<{ value: string; name?: string; isDefault?: boolean }>;
|
||||
};
|
||||
|
||||
export type AgentSessionModelsMetadata = {
|
||||
|
||||
Reference in New Issue
Block a user