Handle Codex CLI 0.133.0 app-server notifications

This commit is contained in:
murashit 2026-05-22 09:54:05 +09:00
parent 31715f2889
commit a8bff12df2
9 changed files with 129 additions and 0 deletions

View file

@ -257,6 +257,10 @@ export class PanelController {
const name = typeof params.threadName === "string" && params.threadName.trim() ? params.threadName.trim() : null;
this.state.listedThreads = this.state.listedThreads.map((thread) => (thread.id === params.threadId ? { ...thread, name } : thread));
this.actions.refreshThreads();
} else if (method === "thread/settings/updated") {
this.applyThreadSettings(params.threadSettings);
} else if (method === "thread/goal/updated" || method === "thread/goal/cleared") {
return;
}
}
@ -414,6 +418,15 @@ export class PanelController {
if (!params?.name) return;
this.actions.recordMcpStartupStatus(params.name, params.status, params.error ?? null);
}
private applyThreadSettings(
settings: Extract<ServerNotification, { method: "thread/settings/updated" }>["params"]["threadSettings"],
): void {
this.state.activeThreadCwd = settings.cwd ?? this.state.activeThreadCwd;
this.state.activeModel = settings.model ?? null;
this.state.activeServiceTier = settings.serviceTier ?? null;
this.state.activeApprovalsReviewer = settings.approvalsReviewer ?? null;
}
}
function removeUnstructuredAutoReviewWarnings(items: DisplayItem[]): DisplayItem[] {

View file

@ -111,6 +111,9 @@ function isThreadLifecycleNotification(notification: ServerNotification): boolea
case "thread/archived":
case "thread/unarchived":
case "thread/name/updated":
case "thread/goal/updated":
case "thread/goal/cleared":
case "thread/settings/updated":
return true;
default:
return false;

View file

@ -54,6 +54,7 @@ function model(name: string, efforts: ReasoningEffort[], overrides: Partial<Mode
supportsPersonality: false,
additionalSpeedTiers: [],
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
...overrides,
};

View file

@ -571,6 +571,7 @@ describe("thread item conversion preserves app-server semantics", () => {
tool: "pull_request_read",
status: "inProgress",
arguments: { id: 123 },
pluginId: null,
result: null,
error: null,
durationMs: null,
@ -599,6 +600,7 @@ describe("thread item conversion preserves app-server semantics", () => {
tool: "pull_request_read",
status: "failed",
arguments: { owner: "org", repo: "project", id: 123 },
pluginId: null,
result: null,
error: { message: "Not found" },
durationMs: 10,

View file

@ -190,6 +190,7 @@ function model(name: string, efforts: Model["supportedReasoningEfforts"][number]
supportsPersonality: false,
additionalSpeedTiers: [],
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
};
}

View file

@ -992,6 +992,68 @@ describe("PanelController", () => {
expect(state.listedThreads[0]?.name).toBe("Codex Panel自動命名");
expect(refreshThreads).toHaveBeenCalled();
});
it("syncs active runtime state from thread settings notifications", () => {
const state = createPanelState();
state.activeThreadId = "thread-active";
const controller = controllerForState(state);
controller.handleNotification({
method: "thread/settings/updated",
params: {
threadId: "thread-active",
threadSettings: {
cwd: "/workspace/active",
approvalPolicy: "on-request",
approvalsReviewer: "auto_review",
sandboxPolicy: { type: "readOnly", networkAccess: false },
activePermissionProfile: null,
model: "gpt-5.5",
modelProvider: "openai",
serviceTier: "fast",
effort: "high",
summary: null,
collaborationMode: {
mode: "default",
settings: { model: "gpt-5.5", reasoning_effort: "high", developer_instructions: null },
},
personality: null,
},
},
} satisfies Extract<ServerNotification, { method: "thread/settings/updated" }>);
expect(state.activeThreadCwd).toBe("/workspace/active");
expect(state.activeModel).toBe("gpt-5.5");
expect(state.activeServiceTier).toBe("fast");
expect(state.activeApprovalsReviewer).toBe("auto_review");
expect(state.displayItems).toEqual([]);
});
it("keeps goal notifications out of the message stream", () => {
const state = createPanelState();
state.activeThreadId = "thread-active";
const controller = controllerForState(state);
controller.handleNotification({
method: "thread/goal/updated",
params: {
threadId: "thread-active",
turnId: null,
goal: {
threadId: "thread-active",
objective: "Finish",
status: "active",
tokenBudget: null,
tokensUsed: 0,
timeUsedSeconds: 0,
createdAt: 1,
updatedAt: 1,
},
},
} satisfies Extract<ServerNotification, { method: "thread/goal/updated" }>);
expect(state.displayItems).toEqual([]);
});
});
describe("auto-review display", () => {

View file

@ -66,6 +66,8 @@ describe("panel inbound routing", () => {
expect(routeServerNotification(agentDeltaNotification(), activeScope).kind).toBe("streamUpdate");
expect(routeServerNotification(turnStartedNotification(), activeScope).kind).toBe("turnLifecycle");
expect(routeServerNotification(threadArchivedNotification(), activeScope).kind).toBe("threadLifecycle");
expect(routeServerNotification(threadSettingsUpdatedNotification(), activeScope).kind).toBe("threadLifecycle");
expect(routeServerNotification(threadGoalUpdatedNotification(), activeScope).kind).toBe("threadLifecycle");
expect(routeServerNotification(serverRequestResolvedNotification(), activeScope).kind).toBe("requestResolved");
expect(routeServerNotification(mcpStartupStatusNotification(), activeScope).kind).toBe("diagnosticStatus");
expect(routeServerNotification(warningNotification(), activeScope).kind).toBe("userVisibleNotice");
@ -165,6 +167,49 @@ function threadArchivedNotification(): ServerNotification {
};
}
function threadSettingsUpdatedNotification(): ServerNotification {
return {
method: "thread/settings/updated",
params: {
threadId: "thread-active",
threadSettings: {
cwd: "/vault",
approvalPolicy: "on-request",
approvalsReviewer: "user",
sandboxPolicy: { type: "readOnly", networkAccess: false },
activePermissionProfile: null,
model: "gpt-5.5",
modelProvider: "openai",
serviceTier: null,
effort: "medium",
summary: null,
collaborationMode: { mode: "default", settings: { model: "gpt-5.5", reasoning_effort: "medium", developer_instructions: null } },
personality: null,
},
},
};
}
function threadGoalUpdatedNotification(): ServerNotification {
return {
method: "thread/goal/updated",
params: {
threadId: "thread-active",
turnId: null,
goal: {
threadId: "thread-active",
objective: "Finish",
status: "active",
tokenBudget: null,
tokensUsed: 0,
timeUsedSeconds: 0,
createdAt: 1,
updatedAt: 1,
},
},
};
}
function serverRequestResolvedNotification(): ServerNotification {
return {
method: "serverRequest/resolved",

View file

@ -222,6 +222,7 @@ function model(name: string, efforts: Model["supportedReasoningEfforts"][number]
supportsPersonality: false,
additionalSpeedTiers: [],
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
};
}

View file

@ -262,6 +262,7 @@ function model(modelId: string, isDefault = false, hidden = false, efforts: Reas
supportsPersonality: false,
additionalSpeedTiers: [],
serviceTiers: [],
defaultServiceTier: null,
} satisfies Model;
}