diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index acff8fd41a..d364d4a175 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -4620,9 +4620,38 @@ ], "type": "string" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSectionCreateParams": { "description": "Parameters for creating an independently persisted thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null + }, "name": { "description": "The user-visible name of the section.", "type": "string" @@ -4699,6 +4728,17 @@ "ThreadSectionUpdateParams": { "description": "Parameters for updating an independently persisted thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "description": "Omit to preserve appearance, use `null` to clear it, or provide a replacement." + }, "name": { "description": "The updated user-visible name of the section.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 813952c4c2..e8198c8d6e 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -5027,6 +5027,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -5042,6 +5054,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSettings": { "properties": { "activePermissionProfile": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index cc54dd03a6..6fe008b322 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -20922,6 +20922,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -20937,10 +20949,39 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSectionCreateParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Parameters for creating an independently persisted thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null + }, "name": { "description": "The user-visible name of the section.", "type": "string" @@ -21074,6 +21115,17 @@ "$schema": "http://json-schema.org/draft-07/schema#", "description": "Parameters for updating an independently persisted thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "description": "Omit to preserve appearance, use `null` to clear it, or provide a replacement." + }, "name": { "description": "The updated user-visible name of the section.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index ce794a584b..feee48c70c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -18681,6 +18681,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -18696,10 +18708,39 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSectionCreateParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Parameters for creating an independently persisted thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null + }, "name": { "description": "The user-visible name of the section.", "type": "string" @@ -18833,6 +18874,17 @@ "$schema": "http://json-schema.org/draft-07/schema#", "description": "Parameters for updating an independently persisted thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "description": "Omit to preserve appearance, use `null` to clear it, or provide a replacement." + }, "name": { "description": "The updated user-visible name of the section.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 41fb757965..300985b7f1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -2032,6 +2032,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -2047,6 +2059,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index c275d29636..0d53fc4b0b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1823,6 +1823,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -1838,6 +1850,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index b059503e2c..33ba54078f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1823,6 +1823,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -1838,6 +1850,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index a070a6175b..2ec7369931 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1823,6 +1823,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -1838,6 +1850,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 36cdea77f6..46884f4f1e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -2032,6 +2032,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -2047,6 +2059,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 1ae865b912..a390f5d370 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -1823,6 +1823,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -1838,6 +1850,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionCreateParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionCreateParams.json index 02af3c2422..eb3626b4a3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionCreateParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionCreateParams.json @@ -1,7 +1,38 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + }, "description": "Parameters for creating an independently persisted thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null + }, "name": { "description": "The user-visible name of the section.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionCreateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionCreateResponse.json index b5f52ab7a1..4d2a1df492 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionCreateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionCreateResponse.json @@ -4,6 +4,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -18,6 +30,24 @@ "name" ], "type": "object" + }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" } }, "description": "The independently persisted section created by the server.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionListResponse.json index 8e684aa854..6b203241fe 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionListResponse.json @@ -4,6 +4,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -18,6 +30,24 @@ "name" ], "type": "object" + }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" } }, "description": "One page of independently persisted thread sections.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionUpdateParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionUpdateParams.json index 5a9011937b..ced77f317a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionUpdateParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionUpdateParams.json @@ -1,7 +1,38 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + }, "description": "Parameters for updating an independently persisted thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "description": "Omit to preserve appearance, use `null` to clear it, or provide a replacement." + }, "name": { "description": "The updated user-visible name of the section.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionUpdateResponse.json index 09bda7bce0..9e843e6124 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadSectionUpdateResponse.json @@ -4,6 +4,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -18,6 +30,24 @@ "name" ], "type": "object" + }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" } }, "description": "The independently persisted section after its name is updated.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index f9c02bb22e..a99941bdd3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -2032,6 +2032,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -2047,6 +2059,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index ab18d0dfe6..a545a28025 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1823,6 +1823,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -1838,6 +1850,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 5f4b7488bc..e51e8d3ef5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1823,6 +1823,18 @@ "ThreadSection": { "description": "An independently persisted, user-visible thread section.", "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, "id": { "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", "type": "string" @@ -1838,6 +1850,24 @@ ], "type": "object" }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ThreadSource": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 1a280dbd2f..f18db7e8e6 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 2f0968f6c7..96613cfe3d 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSection.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSection.ts index ee5bc7e266..17cde81af1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSection.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSection.ts @@ -1,6 +1,7 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; /** * An independently persisted, user-visible thread section. @@ -13,4 +14,8 @@ id: string, /** * The current user-visible section name. */ -name: string, }; +name: string, +/** + * Optional appearance synchronized across clients. + */ +appearance: ThreadSectionAppearance | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionAppearance.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionAppearance.ts new file mode 100644 index 0000000000..c6eab7343f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionAppearance.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Extensible visual presentation for a custom thread section. + */ +export type ThreadSectionAppearance = { icon: string | null, color: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionCreateParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionCreateParams.ts index f9b1cffc95..e6272c3381 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionCreateParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionCreateParams.ts @@ -1,6 +1,7 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; /** * Parameters for creating an independently persisted thread section. @@ -9,4 +10,4 @@ export type ThreadSectionCreateParams = { /** * The user-visible name of the section. */ -name: string, }; +name: string, appearance?: ThreadSectionAppearance | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionUpdateParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionUpdateParams.ts index 4134f238f9..1010200c35 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionUpdateParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadSectionUpdateParams.ts @@ -1,6 +1,7 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; /** * Parameters for updating an independently persisted thread section. @@ -13,4 +14,8 @@ sectionId: string, /** * The updated user-visible name of the section. */ -name: string, }; +name: string, +/** + * Omit to preserve appearance, use `null` to clear it, or provide a replacement. + */ +appearance?: ThreadSectionAppearance | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index c3de4bf162..5ba323b7d9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -481,6 +481,7 @@ export type { ThreadRollbackResponse } from "./ThreadRollbackResponse"; export type { ThreadSearchResult } from "./ThreadSearchResult"; export type { ThreadSearchSortKey } from "./ThreadSearchSortKey"; export type { ThreadSection } from "./ThreadSection"; +export type { ThreadSectionAppearance } from "./ThreadSectionAppearance"; export type { ThreadSectionCreateParams } from "./ThreadSectionCreateParams"; export type { ThreadSectionCreateResponse } from "./ThreadSectionCreateResponse"; export type { ThreadSectionDeleteParams } from "./ThreadSectionDeleteParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index dbbe186f68..6743ed25bd 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -255,6 +255,7 @@ fn thread_resume_response_round_trips_initial_turns_page() { section: Some(ThreadSection { id: "01984de2-8f74-7c91-a3b2-5c5e937cf318".to_string(), name: "Pinned".to_string(), + appearance: None, }), section_entered_at: Some(1), history_mode: Default::default(), @@ -302,6 +303,7 @@ fn thread_resume_response_round_trips_initial_turns_page() { json!({ "id": "01984de2-8f74-7c91-a3b2-5c5e937cf318", "name": "Pinned", + "appearance": null, }) ); assert_eq!(value["thread"]["sectionEnteredAt"], json!(1)); @@ -496,6 +498,13 @@ fn thread_list_params_accepts_section_id_filter() { #[test] fn thread_section_list_params_and_response_round_trip() { + let legacy = serde_json::from_value::(json!({ + "id": "legacy", + "name": "Legacy", + })) + .expect("legacy sections without appearance should deserialize"); + assert_eq!(legacy.appearance, None); + let params = serde_json::from_value::(json!({ "cursor": "section-cursor", "limit": 25, @@ -513,6 +522,7 @@ fn thread_section_list_params_and_response_round_trip() { data: vec![ThreadSection { id: "01984de2-8f74-7c91-a3b2-5c5e937cf318".to_string(), name: "Pinned".to_string(), + appearance: None, }], next_cursor: None, }; @@ -523,6 +533,7 @@ fn thread_section_list_params_and_response_round_trip() { "data": [{ "id": "01984de2-8f74-7c91-a3b2-5c5e937cf318", "name": "Pinned", + "appearance": null, }], "nextCursor": null, }) @@ -534,6 +545,21 @@ fn thread_section_list_params_and_response_round_trip() { ); } +#[test] +fn thread_section_updates_distinguish_omitted_and_cleared_appearance() { + for (value, appearance) in [ + (json!({ "sectionId": "section", "name": "Work" }), None), + ( + json!({ "sectionId": "section", "name": "Work", "appearance": null }), + Some(None), + ), + ] { + let params = serde_json::from_value::(value) + .expect("section update should deserialize"); + assert_eq!(params.appearance, appearance); + } +} + #[test] fn collab_agent_state_maps_interrupted_status() { assert_eq!( diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 0208742789..fbf79341ca 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -7,6 +7,7 @@ use super::Thread; use super::ThreadHistoryMode; use super::ThreadItem; use super::ThreadSection; +use super::ThreadSectionAppearance; use super::ThreadSource; use super::Turn; use super::TurnEnvironmentParams; @@ -1146,6 +1147,9 @@ pub struct ThreadSectionListResponse { pub struct ThreadSectionCreateParams { /// The user-visible name of the section. pub name: String, + #[serde(default)] + #[ts(optional = nullable)] + pub appearance: Option, } /// The independently persisted section created by the server. @@ -1165,6 +1169,16 @@ pub struct ThreadSectionUpdateParams { pub section_id: String, /// The updated user-visible name of the section. pub name: String, + /// Omit to preserve appearance, use `null` to clear it, or provide a replacement. + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option" + )] + #[schemars(with = "Option")] + #[ts(optional = nullable, as = "Option")] + pub appearance: Option>, } /// The independently persisted section after its name is updated. diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs index 4a55a10839..304d5613f3 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -176,6 +176,18 @@ pub struct ThreadSection { pub id: String, /// The current user-visible section name. pub name: String, + /// Optional appearance synchronized across clients. + #[serde(default)] + pub appearance: Option, +} + +/// Extensible visual presentation for a custom thread section. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadSectionAppearance { + pub icon: Option, + pub color: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 8eb2a8d4d4..a55f887c5d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -170,9 +170,9 @@ Example with notification opt-out: - `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` boundary is rejected. Experimental `beforeTurnId` instead copies history strictly before the referenced turn, including when that turn is in progress, and cannot be combined with `lastTurnId`. If both boundaries are null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately, or `deferGoalContinuation: true` to carry the source thread's current goal into the fork and run an explicit turn before automatic continuation resumes. Deferred goal continuation is persisted until that turn starts and cannot be combined with `ephemeral: true`. Accepts the same permission override rules as `thread/start`. - `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior. - `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `sectionId`, `cwd`, and `searchTerm` filters. Set `sortKey` to `"section_position"` when listing a section in its persisted manual order. Experimental clients can use `parentThreadId` for direct spawned children or `ancestorThreadId` for spawned descendants at any depth; the two filters are mutually exclusive. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known. -- `threadSection/list` — page through independently persisted thread sections and their display names, including sections that do not currently contain any threads. -- `threadSection/create` — create a durable custom section with a server-generated UUID and a nonempty display name; returns its `section`. -- `threadSection/update` — rename an existing custom section while retaining its stable UUID; returns the updated `section`. The built-in pinned section cannot be renamed. +- `threadSection/list` — page through independently persisted thread sections, including their display names and optional `appearance` (`icon` and `color`). +- `threadSection/create` — create a durable custom section with a server-generated UUID, nonempty display name, and optional `appearance`; returns its `section`. +- `threadSection/update` — rename an existing custom section and optionally replace its `appearance`; omit appearance to preserve it or pass `null` to clear it. The built-in pinned section cannot be updated. - `threadSection/delete` — delete an existing custom section and atomically return its member threads to the unsectioned list; returns `{}`. The built-in pinned section cannot be deleted. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. For loaded threads, experimental clients can use `canAcceptDirectInput` to determine whether `turn/start` and `turn/steer` are accepted; unloaded stored threads report `null` when that capability is unavailable. diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index a12874fea3..7163358b2f 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -5,6 +5,7 @@ use super::*; use crate::error_code::method_not_found; use codex_app_server_protocol::SelectedCapabilityRoot; use codex_app_server_protocol::ThreadSection; +use codex_app_server_protocol::ThreadSectionAppearance; use codex_app_server_protocol::ThreadSectionMoveParams; use codex_app_server_protocol::ThreadSectionMoveResponse; use codex_extension_api::ExtensionDataInit; @@ -5242,6 +5243,12 @@ pub(crate) fn thread_from_stored_thread( section: thread.section.map(|section| ThreadSection { id: section.id, name: section.name, + appearance: section + .appearance + .map(|appearance| ThreadSectionAppearance { + icon: appearance.icon, + color: appearance.color, + }), }), section_entered_at: thread .section_entered_at diff --git a/codex-rs/app-server/src/request_processors/thread_sections.rs b/codex-rs/app-server/src/request_processors/thread_sections.rs index 9dbed3a0ed..6491b3bc78 100644 --- a/codex-rs/app-server/src/request_processors/thread_sections.rs +++ b/codex-rs/app-server/src/request_processors/thread_sections.rs @@ -7,6 +7,7 @@ use crate::error_code::method_not_found; use codex_app_server_protocol::ClientResponsePayload; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ThreadSection; +use codex_app_server_protocol::ThreadSectionAppearance; use codex_app_server_protocol::ThreadSectionCreateParams; use codex_app_server_protocol::ThreadSectionCreateResponse; use codex_app_server_protocol::ThreadSectionDeleteParams; @@ -20,8 +21,11 @@ use codex_thread_store::CreateThreadSectionParams as StoreCreateThreadSectionPar use codex_thread_store::DeleteThreadSectionParams as StoreDeleteThreadSectionParams; use codex_thread_store::ListThreadSectionsParams as StoreListThreadSectionsParams; use codex_thread_store::RenameThreadSectionParams as StoreRenameThreadSectionParams; +use codex_thread_store::StoredThreadSection; use codex_thread_store::ThreadStoreError; +const MAX_THREAD_SECTION_APPEARANCE_FIELD_BYTES: usize = 64; + impl ThreadRequestProcessor { pub(crate) async fn thread_section_list( &self, @@ -45,14 +49,7 @@ impl ThreadRequestProcessor { Ok(Some( ThreadSectionListResponse { - data: page - .sections - .into_iter() - .map(|section| ThreadSection { - id: section.id, - name: section.name, - }) - .collect(), + data: page.sections.into_iter().map(api_thread_section).collect(), next_cursor: page.next_cursor, } .into(), @@ -69,20 +66,21 @@ impl ThreadRequestProcessor { if name.is_empty() { return Err(invalid_params("section name must not be empty")); } + if let Some(appearance) = params.appearance.as_ref() { + validate_thread_section_appearance(appearance)?; + } let section = self .thread_store .create_thread_section(StoreCreateThreadSectionParams { name: name.to_string(), + appearance: params.appearance.map(state_thread_section_appearance), }) .await .map_err(|err| thread_section_store_error(OPERATION, err))?; Ok(Some( ThreadSectionCreateResponse { - section: ThreadSection { - id: section.id, - name: section.name, - }, + section: api_thread_section(section), } .into(), )) @@ -106,11 +104,17 @@ impl ThreadRequestProcessor { "the built-in pinned section cannot be renamed", )); } + if let Some(Some(appearance)) = params.appearance.as_ref() { + validate_thread_section_appearance(appearance)?; + } let section = self .thread_store .rename_thread_section(StoreRenameThreadSectionParams { section_id: params.section_id.clone(), name: name.to_string(), + appearance: params + .appearance + .map(|appearance| appearance.map(state_thread_section_appearance)), }) .await .map_err(|err| thread_section_store_error(OPERATION, err))? @@ -120,10 +124,7 @@ impl ThreadRequestProcessor { Ok(Some( ThreadSectionUpdateResponse { - section: ThreadSection { - id: section.id, - name: section.name, - }, + section: api_thread_section(section), } .into(), )) @@ -172,6 +173,44 @@ impl ThreadRequestProcessor { } } +fn validate_thread_section_appearance( + appearance: &ThreadSectionAppearance, +) -> Result<(), JSONRPCErrorError> { + for (field, value) in [ + ("icon", appearance.icon.as_ref()), + ("color", appearance.color.as_ref()), + ] { + if value.is_some_and(|value| value.len() > MAX_THREAD_SECTION_APPEARANCE_FIELD_BYTES) { + return Err(invalid_params(format!( + "section appearance {field} must not exceed {MAX_THREAD_SECTION_APPEARANCE_FIELD_BYTES} bytes" + ))); + } + } + Ok(()) +} + +fn api_thread_section(section: StoredThreadSection) -> ThreadSection { + ThreadSection { + id: section.id, + name: section.name, + appearance: section + .appearance + .map(|appearance| ThreadSectionAppearance { + icon: appearance.icon, + color: appearance.color, + }), + } +} + +fn state_thread_section_appearance( + appearance: ThreadSectionAppearance, +) -> codex_state::ThreadSectionAppearance { + codex_state::ThreadSectionAppearance { + icon: appearance.icon, + color: appearance.color, + } +} + fn unsupported_thread_section_operation(operation: &'static str) -> JSONRPCErrorError { method_not_found(format!("{operation} is unavailable without sqlite state")) } diff --git a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs index 91d4820f25..99a7a9636b 100644 --- a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs +++ b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs @@ -90,6 +90,7 @@ async fn thread_section_operations_without_sqlite_return_method_not_found() -> R request_id: RequestId::Integer(2), params: ThreadSectionCreateParams { name: "Work".to_string(), + appearance: None, }, }, ClientRequest::ThreadSectionUpdate { @@ -97,6 +98,7 @@ async fn thread_section_operations_without_sqlite_return_method_not_found() -> R params: ThreadSectionUpdateParams { section_id: section_id.clone(), name: "Projects".to_string(), + appearance: None, }, }, ClientRequest::ThreadSectionDelete { @@ -107,6 +109,7 @@ async fn thread_section_operations_without_sqlite_return_method_not_found() -> R request_id: RequestId::Integer(5), params: ThreadSectionCreateParams { name: " ".to_string(), + appearance: None, }, }, ClientRequest::ThreadSectionUpdate { @@ -114,6 +117,7 @@ async fn thread_section_operations_without_sqlite_return_method_not_found() -> R params: ThreadSectionUpdateParams { section_id: " ".to_string(), name: "Work".to_string(), + appearance: None, }, }, ClientRequest::ThreadSectionUpdate { @@ -121,6 +125,7 @@ async fn thread_section_operations_without_sqlite_return_method_not_found() -> R params: ThreadSectionUpdateParams { section_id: PINNED_THREAD_SECTION_ID.to_string(), name: "Pinned again".to_string(), + appearance: None, }, }, ClientRequest::ThreadSectionDelete { @@ -140,6 +145,7 @@ async fn thread_section_operations_without_sqlite_return_method_not_found() -> R params: ThreadSectionUpdateParams { section_id: PINNED_THREAD_SECTION_ID.to_string(), name: " ".to_string(), + appearance: None, }, }, ] { diff --git a/codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs b/codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs index a88d38209f..edf47fb6a0 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs @@ -102,6 +102,7 @@ async fn thread_section_move_pins_and_unpins_with_filtered_recency_pagination() let pinned_section = ThreadSection { id: PINNED_THREAD_SECTION_ID.to_string(), name: PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }; let section_list_id = mcp .send_raw_request( @@ -162,6 +163,7 @@ async fn thread_section_move_pins_and_unpins_with_filtered_recency_pagination() Some(codex_state::ThreadSection { id: pinned_section.id.clone(), name: pinned_section.name.clone(), + appearance: None, }) ); } diff --git a/codex-rs/app-server/tests/suite/v2/thread_sections.rs b/codex-rs/app-server/tests/suite/v2/thread_sections.rs index 5798238bed..e374d6e839 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_sections.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_sections.rs @@ -13,6 +13,7 @@ use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; use codex_app_server_protocol::ThreadSection; +use codex_app_server_protocol::ThreadSectionAppearance; use codex_app_server_protocol::ThreadSectionCreateParams; use codex_app_server_protocol::ThreadSectionCreateResponse; use codex_app_server_protocol::ThreadSectionDeleteParams; @@ -67,6 +68,10 @@ async fn custom_sections_remain_discoverable_across_ordered_updates_and_restart( request_id, params: ThreadSectionCreateParams { name: " Work ".to_string(), + appearance: Some(ThreadSectionAppearance { + icon: Some("folder".to_string()), + color: Some("purple".to_string()), + }), }, }) .await?; @@ -78,6 +83,7 @@ async fn custom_sections_remain_discoverable_across_ordered_updates_and_restart( request_id, params: ThreadSectionCreateParams { name: "Personal".to_string(), + appearance: None, }, }) .await?; @@ -103,7 +109,11 @@ async fn custom_sections_remain_discoverable_across_ordered_updates_and_restart( let second_rename = server .send_raw_request( "threadSection/update", - Some(json!({ "sectionId": created.section.id, "name": " Projects " })), + Some(json!({ + "sectionId": created.section.id, + "name": " Projects ", + "appearance": { "icon": "star", "color": "blue" }, + })), ) .await?; let listed_after_renames = server @@ -123,6 +133,10 @@ async fn custom_sections_remain_discoverable_across_ordered_updates_and_restart( ThreadSection { id: created.section.id, name: "Projects".to_string(), + appearance: Some(ThreadSectionAppearance { + icon: Some("star".to_string()), + color: Some("blue".to_string()), + }), } ); assert!(observed.data.contains(&renamed.section)); @@ -147,12 +161,25 @@ async fn custom_sections_remain_discoverable_across_ordered_updates_and_restart( ThreadSection { id: PINNED_THREAD_SECTION_ID.to_string(), name: PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }, renamed.section, retained.section, ] ); + let cleared: ThreadSectionUpdateResponse = restarted + .request(|request_id| ClientRequest::ThreadSectionUpdate { + request_id, + params: ThreadSectionUpdateParams { + section_id: persisted.data[1].id.clone(), + name: "Projects".to_string(), + appearance: Some(None), + }, + }) + .await?; + assert_eq!(cleared.section.appearance, None); + Ok(()) } @@ -194,6 +221,10 @@ async fn deleting_custom_sections_unassigns_active_and_archived_members() -> Res request_id, params: ThreadSectionCreateParams { name: "Work".to_string(), + appearance: Some(ThreadSectionAppearance { + icon: Some("folder".to_string()), + color: Some("purple".to_string()), + }), }, }) .await?; @@ -224,6 +255,7 @@ async fn deleting_custom_sections_unassigns_active_and_archived_members() -> Res params: ThreadSectionUpdateParams { section_id: created.section.id.clone(), name: "Projects".to_string(), + appearance: None, }, }) .await?; @@ -341,6 +373,25 @@ async fn custom_section_management_rejects_empty_names_missing_ids_and_pinned_mu assert_eq!(error.error.message, message); } + for (method, field) in [ + ("threadSection/create", "icon"), + ("threadSection/create", "color"), + ("threadSection/update", "icon"), + ("threadSection/update", "color"), + ] { + let mut params = json!({ "name": "Work", "appearance": {} }); + params["appearance"][field] = json!("x".repeat(65)); + if method == "threadSection/update" { + params["sectionId"] = json!(&missing_id); + } + let error = section_request_error(&mut server, method, params).await?; + assert_eq!(error.error.code, INVALID_PARAMS_ERROR_CODE); + assert_eq!( + error.error.message, + format!("section appearance {field} must not exceed 64 bytes") + ); + } + for (method, params) in [ ( "threadSection/update", diff --git a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs index bca6f11002..9bbc6cad1b 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs @@ -105,6 +105,7 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result let pinned_section = ThreadSection { id: PINNED_THREAD_SECTION_ID.to_string(), name: PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }; let pin_id = mcp .send_thread_section_move_request(ThreadSectionMoveParams { diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index 7279a73e34..62d652c030 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -1461,6 +1461,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi section: Some(codex_state::ThreadSection { id: codex_state::PINNED_THREAD_SECTION_ID.to_string(), name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }), cwd: Some(PathBuf::from("/tmp/state-cwd")), git_branch: Some("state-branch".to_string()), @@ -1487,6 +1488,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi Some(codex_state::ThreadSection { id: codex_state::PINNED_THREAD_SECTION_ID.to_string(), name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }) ); assert_eq!( diff --git a/codex-rs/state/migrations/0048_thread_section_appearance.sql b/codex-rs/state/migrations/0048_thread_section_appearance.sql new file mode 100644 index 0000000000..691cf48016 --- /dev/null +++ b/codex-rs/state/migrations/0048_thread_section_appearance.sql @@ -0,0 +1 @@ +ALTER TABLE thread_sections ADD COLUMN appearance TEXT; diff --git a/codex-rs/state/src/lib.rs b/codex-rs/state/src/lib.rs index 43731d39e4..40add08908 100644 --- a/codex-rs/state/src/lib.rs +++ b/codex-rs/state/src/lib.rs @@ -56,6 +56,7 @@ pub use model::ThreadMetadata; pub use model::ThreadMetadataBuilder; pub use model::ThreadRelationFilter; pub use model::ThreadSection; +pub use model::ThreadSectionAppearance; pub use model::ThreadSectionsPage; pub use model::ThreadsPage; pub use runtime::ExternalAgentConfigImportDetailsRecord; diff --git a/codex-rs/state/src/migrations_tests.rs b/codex-rs/state/src/migrations_tests.rs index 1167731ef2..fea8878d36 100644 --- a/codex-rs/state/src/migrations_tests.rs +++ b/codex-rs/state/src/migrations_tests.rs @@ -102,16 +102,18 @@ INSERT INTO threads ( .expect("legacy thread insert should succeed"); } - let registered_sections = - sqlx::query_as::<_, (String, String)>("SELECT id, name FROM thread_sections ORDER BY id") - .fetch_all(&pool) - .await - .expect("independent thread sections should load"); + let registered_sections = sqlx::query_as::<_, (String, String, Option)>( + "SELECT id, name, appearance FROM thread_sections ORDER BY id", + ) + .fetch_all(&pool) + .await + .expect("independent thread sections should load"); assert_eq!( registered_sections, vec![( PINNED_THREAD_SECTION_ID.to_string(), PINNED_THREAD_SECTION_NAME.to_string(), + None, )] ); diff --git a/codex-rs/state/src/model/mod.rs b/codex-rs/state/src/model/mod.rs index 0f4c4cf6d7..e11de8da58 100644 --- a/codex-rs/state/src/model/mod.rs +++ b/codex-rs/state/src/model/mod.rs @@ -33,6 +33,7 @@ pub use thread_metadata::ThreadMetadata; pub use thread_metadata::ThreadMetadataBuilder; pub use thread_metadata::ThreadRelationFilter; pub use thread_metadata::ThreadSection; +pub use thread_metadata::ThreadSectionAppearance; pub use thread_metadata::ThreadSectionsPage; pub use thread_metadata::ThreadsPage; diff --git a/codex-rs/state/src/model/thread_metadata.rs b/codex-rs/state/src/model/thread_metadata.rs index 2502558f9c..318eb7d931 100644 --- a/codex-rs/state/src/model/thread_metadata.rs +++ b/codex-rs/state/src/model/thread_metadata.rs @@ -53,13 +53,37 @@ pub struct Anchor { pub id: Option, } -/// An independently persisted thread section and its user-facing name. +/// Visual presentation metadata owned by a thread section. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreadSectionAppearance { + pub icon: Option, + pub color: Option, +} + +/// An independently persisted thread section and its user-facing presentation. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ThreadSection { /// Opaque UUIDv7 identifying the section independently of its name. pub id: String, /// User-facing section name. pub name: String, + #[serde(default)] + pub appearance: Option, +} + +impl ThreadSection { + pub(crate) fn from_row( + (id, name, appearance): (String, String, Option), + ) -> Result { + Ok(Self { + id, + name, + appearance: appearance + .map(|appearance| serde_json::from_str(&appearance)) + .transpose()?, + }) + } } /// A cursor-paginated page of independently persisted thread sections. @@ -453,6 +477,7 @@ pub(crate) struct ThreadRow { archived_at: Option, section: Option, section_name: Option, + section_appearance: Option, section_position: Option, section_entered_at_ms: Option, git_sha: Option, @@ -489,6 +514,7 @@ impl ThreadRow { archived_at: row.try_get("archived_at")?, section: row.try_get("section")?, section_name: row.try_get("section_name")?, + section_appearance: row.try_get("section_appearance")?, section_position: row.try_get("section_position")?, section_entered_at_ms: row.try_get("section_entered_at_ms")?, git_sha: row.try_get("git_sha")?, @@ -529,6 +555,7 @@ impl TryFrom for ThreadMetadata { archived_at, section, section_name, + section_appearance, section_position, section_entered_at_ms, git_sha, @@ -541,7 +568,9 @@ impl TryFrom for ThreadMetadata { .map_err(anyhow::Error::msg)?; let history_mode = history_mode.parse().map_err(anyhow::Error::msg)?; let section = match (section, section_name) { - (Some(id), Some(name)) => Some(ThreadSection { id, name }), + (Some(id), Some(name)) => { + Some(ThreadSection::from_row((id, name, section_appearance))?) + } (None, None) => None, (Some(id), None) => { return Err(anyhow::anyhow!( @@ -688,6 +717,7 @@ mod tests { archived_at: None, section: None, section_name: None, + section_appearance: None, section_position: None, section_entered_at_ms: None, git_sha: None, diff --git a/codex-rs/state/src/runtime/memories.rs b/codex-rs/state/src/runtime/memories.rs index f80bb6fdc8..6a778ee66e 100644 --- a/codex-rs/state/src/runtime/memories.rs +++ b/codex-rs/state/src/runtime/memories.rs @@ -201,6 +201,11 @@ SELECT FROM thread_sections WHERE thread_sections.id = threads.thread_section_id ) AS section_name, + ( + SELECT thread_sections.appearance + FROM thread_sections + WHERE thread_sections.id = threads.thread_section_id + ) AS section_appearance, threads.section_position, threads.section_entered_at_ms, threads.git_sha, @@ -584,6 +589,11 @@ SELECT FROM thread_sections WHERE thread_sections.id = threads.thread_section_id ) AS section_name, + ( + SELECT thread_sections.appearance + FROM thread_sections + WHERE thread_sections.id = threads.thread_section_id + ) AS section_appearance, threads.section_position, threads.section_entered_at_ms, threads.git_sha, diff --git a/codex-rs/state/src/runtime/thread_section_order.rs b/codex-rs/state/src/runtime/thread_section_order.rs index 15ce881ea1..f5b919847f 100644 --- a/codex-rs/state/src/runtime/thread_section_order.rs +++ b/codex-rs/state/src/runtime/thread_section_order.rs @@ -51,13 +51,13 @@ impl StateRuntime { &self, id: &str, ) -> anyhow::Result> { - let row = sqlx::query_as::<_, (String, String)>( - "SELECT id, name FROM thread_sections WHERE id = ?", + let row = sqlx::query_as::<_, (String, String, Option)>( + "SELECT id, name, appearance FROM thread_sections WHERE id = ?", ) .bind(id) .fetch_optional(self.pool.as_ref()) .await?; - Ok(row.map(|(id, name)| crate::ThreadSection { id, name })) + row.map(crate::ThreadSection::from_row).transpose() } /// List independently persisted sections in stable, cursor-paginated identifier order. @@ -68,9 +68,9 @@ impl StateRuntime { ) -> anyhow::Result { let page_size = limit.max(1); let fetch_limit = i64::try_from(page_size.saturating_add(1))?; - let rows = sqlx::query_as::<_, (String, String)>( + let rows = sqlx::query_as::<_, (String, String, Option)>( r#" -SELECT id, name +SELECT id, name, appearance FROM thread_sections WHERE (? IS NULL OR id > ?) ORDER BY id @@ -84,8 +84,8 @@ LIMIT ? .await?; let mut sections = rows .into_iter() - .map(|(id, name)| crate::ThreadSection { id, name }) - .collect::>(); + .map(crate::ThreadSection::from_row) + .collect::>>()?; let next_cursor = if sections.len() > page_size { sections.pop(); sections.last().map(|section| section.id.clone()) diff --git a/codex-rs/state/src/runtime/thread_section_order_tests.rs b/codex-rs/state/src/runtime/thread_section_order_tests.rs index 80618bb222..6a78fcb9a5 100644 --- a/codex-rs/state/src/runtime/thread_section_order_tests.rs +++ b/codex-rs/state/src/runtime/thread_section_order_tests.rs @@ -91,14 +91,17 @@ async fn thread_sections_paginate_and_require_registered_identities() { let before_pinned = crate::ThreadSection { id: "01984de2-8f74-7c91-a3b2-5c5e937cf317".to_string(), name: "Before pinned".to_string(), + appearance: None, }; let pinned = crate::ThreadSection { id: crate::PINNED_THREAD_SECTION_ID.to_string(), name: crate::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }; let after_pinned = crate::ThreadSection { id: "01984de2-8f74-7c91-a3b2-5c5e937cf319".to_string(), name: "After pinned".to_string(), + appearance: None, }; for section in [&before_pinned, &after_pinned] { @@ -236,6 +239,7 @@ async fn thread_section_moves_round_trip_and_survive_rollout_reconciliation() { Some(&crate::ThreadSection { id: crate::PINNED_THREAD_SECTION_ID.to_string(), name: crate::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }), Some(1_000_000), ) @@ -590,6 +594,7 @@ async fn section_moves_preserve_entry_order_and_renumber_exhausted_ranks() { Some(crate::ThreadSection { id: OTHER_THREAD_SECTION_ID.to_string(), name: "Other section".to_string(), + appearance: None, }) ); assert_eq!(moved_across_sections.section_position, Some(500_000)); diff --git a/codex-rs/state/src/runtime/thread_sections.rs b/codex-rs/state/src/runtime/thread_sections.rs index 0b620cc295..f8c6f02378 100644 --- a/codex-rs/state/src/runtime/thread_sections.rs +++ b/codex-rs/state/src/runtime/thread_sections.rs @@ -1,19 +1,32 @@ use super::StateRuntime; use crate::PINNED_THREAD_SECTION_ID; use crate::ThreadSection; +use crate::ThreadSectionAppearance; use uuid::Uuid; impl StateRuntime { /// Create a custom thread section with a stable, server-assigned UUIDv7. - pub async fn create_thread_section(&self, name: &str) -> anyhow::Result { + pub async fn create_thread_section( + &self, + name: &str, + appearance: Option, + ) -> anyhow::Result { let section = ThreadSection { id: Uuid::now_v7().to_string(), name: name.to_string(), + appearance, }; - sqlx::query("INSERT INTO thread_sections (id, name) VALUES (?, ?)") + sqlx::query("INSERT INTO thread_sections (id, name, appearance) VALUES (?, ?, ?)") .bind(§ion.id) .bind(§ion.name) + .bind( + section + .appearance + .as_ref() + .map(serde_json::to_string) + .transpose()?, + ) .execute(self.pool.as_ref()) .await?; @@ -25,20 +38,28 @@ impl StateRuntime { &self, id: &str, name: &str, + appearance: Option>, ) -> anyhow::Result> { if id == PINNED_THREAD_SECTION_ID { anyhow::bail!("built-in pinned thread section cannot be renamed"); } - let section = sqlx::query_as::<_, (String, String)>( - "UPDATE thread_sections SET name = ? WHERE id = ? RETURNING id, name", + let replace_appearance = appearance.is_some(); + let appearance = appearance + .flatten() + .map(|appearance| serde_json::to_string(&appearance)) + .transpose()?; + let section = sqlx::query_as::<_, (String, String, Option)>( + "UPDATE thread_sections SET name = ?, appearance = CASE WHEN ? THEN ? ELSE appearance END WHERE id = ? RETURNING id, name, appearance", ) .bind(name) + .bind(replace_appearance) + .bind(appearance) .bind(id) .fetch_optional(self.pool.as_ref()) .await?; - Ok(section.map(|(id, name)| ThreadSection { id, name })) + section.map(ThreadSection::from_row).transpose() } /// Delete a custom section and return its threads to the unsectioned list. diff --git a/codex-rs/state/src/runtime/thread_sections_tests.rs b/codex-rs/state/src/runtime/thread_sections_tests.rs index d556742a7f..e800c38d83 100644 --- a/codex-rs/state/src/runtime/thread_sections_tests.rs +++ b/codex-rs/state/src/runtime/thread_sections_tests.rs @@ -14,8 +14,18 @@ async fn deleting_custom_section_preserves_threads_and_clears_section_ordering() "test-provider".to_string(), ) .await?; - let deleted_section = runtime.create_thread_section("Research").await?; - let retained_section = runtime.create_thread_section("Planning").await?; + let deleted_section = runtime + .create_thread_section( + "Research", + Some(crate::ThreadSectionAppearance { + icon: Some("folder".to_string()), + color: Some("purple".to_string()), + }), + ) + .await?; + let retained_section = runtime + .create_thread_section("Planning", /*appearance*/ None) + .await?; let first = ThreadId::new(); let second = ThreadId::new(); let retained = ThreadId::new(); @@ -75,7 +85,9 @@ async fn concurrent_section_deletion_and_membership_moves_preserve_thread_invari "test-provider".to_string(), ) .await?; - let section = runtime.create_thread_section("Research").await?; + let section = runtime + .create_thread_section("Research", /*appearance*/ None) + .await?; let thread_id = ThreadId::new(); runtime .upsert_thread(&test_thread_metadata( diff --git a/codex-rs/state/src/runtime/threads.rs b/codex-rs/state/src/runtime/threads.rs index 1537a5d991..20710bc2d4 100644 --- a/codex-rs/state/src/runtime/threads.rs +++ b/codex-rs/state/src/runtime/threads.rs @@ -39,6 +39,11 @@ SELECT FROM thread_sections WHERE thread_sections.id = threads.thread_section_id ) AS section_name, + ( + SELECT thread_sections.appearance + FROM thread_sections + WHERE thread_sections.id = threads.thread_section_id + ) AS section_appearance, threads.section_position, threads.section_entered_at_ms, threads.git_sha, @@ -1245,6 +1250,11 @@ SELECT FROM thread_sections WHERE thread_sections.id = threads.thread_section_id ) AS section_name, + ( + SELECT thread_sections.appearance + FROM thread_sections + WHERE thread_sections.id = threads.thread_section_id + ) AS section_appearance, threads.section_position, threads.section_entered_at_ms, threads.git_sha, @@ -1607,6 +1617,7 @@ mod tests { metadata.section = section.map(|id| crate::ThreadSection { id: id.to_string(), name: crate::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }); runtime.upsert_thread(&metadata).await.unwrap(); } @@ -1636,6 +1647,7 @@ mod tests { Some(crate::ThreadSection { id: crate::PINNED_THREAD_SECTION_ID.to_string(), name: crate::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }) ); let second_page = runtime @@ -1736,6 +1748,7 @@ mod tests { metadata.section = Some(crate::ThreadSection { id: CUSTOM_THREAD_SECTION_ID.to_string(), name: "Custom section".to_string(), + appearance: None, }); metadata.section_position = Some(position); metadata.section_entered_at = Some(metadata.updated_at); diff --git a/codex-rs/thread-store/src/in_memory.rs b/codex-rs/thread-store/src/in_memory.rs index c8235f0f7f..f2e18c7d51 100644 --- a/codex-rs/thread-store/src/in_memory.rs +++ b/codex-rs/thread-store/src/in_memory.rs @@ -1019,6 +1019,7 @@ fn stored_thread_from_state( id.clone() }, id, + appearance: None, }), section_position: state.section_positions.get(&thread_id).copied(), section_entered_at: state.section_entered_at.get(&thread_id).copied(), diff --git a/codex-rs/thread-store/src/local/read_thread.rs b/codex-rs/thread-store/src/local/read_thread.rs index fa2a6621c7..548ce9d31a 100644 --- a/codex-rs/thread-store/src/local/read_thread.rs +++ b/codex-rs/thread-store/src/local/read_thread.rs @@ -686,6 +686,7 @@ mod tests { metadata.section = Some(codex_state::ThreadSection { id: codex_state::PINNED_THREAD_SECTION_ID.to_string(), name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }); metadata.section_position = Some(2_000_000); metadata.section_entered_at = Some(recency_at); diff --git a/codex-rs/thread-store/src/local/thread_sections.rs b/codex-rs/thread-store/src/local/thread_sections.rs index 8a68d0f4f8..acd6eae954 100644 --- a/codex-rs/thread-store/src/local/thread_sections.rs +++ b/codex-rs/thread-store/src/local/thread_sections.rs @@ -26,6 +26,7 @@ pub(super) async fn list_thread_sections( .map(|section| StoredThreadSection { id: section.id, name: section.name, + appearance: section.appearance, }) .collect(), next_cursor: page.next_cursor, @@ -38,13 +39,14 @@ pub(super) async fn create_thread_section( ) -> ThreadStoreResult { let state_db = state_db(store, "threadSection/create")?; let section = state_db - .create_thread_section(¶ms.name) + .create_thread_section(¶ms.name, params.appearance) .await .map_err(|err| section_error("create", err))?; Ok(StoredThreadSection { id: section.id, name: section.name, + appearance: section.appearance, }) } @@ -54,13 +56,14 @@ pub(super) async fn rename_thread_section( ) -> ThreadStoreResult> { let state_db = state_db(store, "threadSection/update")?; let section = state_db - .rename_thread_section(¶ms.section_id, ¶ms.name) + .rename_thread_section(¶ms.section_id, ¶ms.name, params.appearance) .await .map_err(|err| section_error("update", err))?; Ok(section.map(|section| StoredThreadSection { id: section.id, name: section.name, + appearance: section.appearance, })) } diff --git a/codex-rs/thread-store/src/local/thread_sections_tests.rs b/codex-rs/thread-store/src/local/thread_sections_tests.rs index 9585942a86..4bf4e5f12a 100644 --- a/codex-rs/thread-store/src/local/thread_sections_tests.rs +++ b/codex-rs/thread-store/src/local/thread_sections_tests.rs @@ -19,6 +19,7 @@ async fn local_thread_sections_require_sqlite_and_preserve_section_identity() { unsupported_store .create_thread_section(CreateThreadSectionParams { name: "Research".to_string(), + appearance: None, }) .await, Err(ThreadStoreError::Unsupported { @@ -35,9 +36,14 @@ async fn local_thread_sections_require_sqlite_and_preserve_section_identity() { let store = LocalThreadStore::new(config, Some(runtime)); assert!(store.supports_thread_sections()); + let appearance = codex_state::ThreadSectionAppearance { + icon: Some("folder".to_string()), + color: Some("purple".to_string()), + }; let section = store .create_thread_section(CreateThreadSectionParams { name: "Research".to_string(), + appearance: Some(appearance.clone()), }) .await .expect("create section"); @@ -54,6 +60,7 @@ async fn local_thread_sections_require_sqlite_and_preserve_section_identity() { .rename_thread_section(RenameThreadSectionParams { section_id: section.id.clone(), name: "Planning".to_string(), + appearance: None, }) .await .expect("rename section"); @@ -62,6 +69,7 @@ async fn local_thread_sections_require_sqlite_and_preserve_section_identity() { Some(StoredThreadSection { id: section.id.clone(), name: "Planning".to_string(), + appearance: Some(appearance), }) ); diff --git a/codex-rs/thread-store/src/local/unarchive_thread.rs b/codex-rs/thread-store/src/local/unarchive_thread.rs index 9a55959fad..c743c00d3d 100644 --- a/codex-rs/thread-store/src/local/unarchive_thread.rs +++ b/codex-rs/thread-store/src/local/unarchive_thread.rs @@ -168,6 +168,7 @@ mod tests { metadata.section = Some(codex_state::ThreadSection { id: codex_state::PINNED_THREAD_SECTION_ID.to_string(), name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }); runtime .upsert_thread(&metadata) diff --git a/codex-rs/thread-store/src/local/update_thread_metadata.rs b/codex-rs/thread-store/src/local/update_thread_metadata.rs index b4491f3cd4..2d9f4b45a9 100644 --- a/codex-rs/thread-store/src/local/update_thread_metadata.rs +++ b/codex-rs/thread-store/src/local/update_thread_metadata.rs @@ -897,6 +897,7 @@ mod tests { Some(codex_state::ThreadSection { id: codex_state::PINNED_THREAD_SECTION_ID.to_string(), name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }) ); let pinned_metadata = runtime @@ -909,6 +910,7 @@ mod tests { Some(codex_state::ThreadSection { id: codex_state::PINNED_THREAD_SECTION_ID.to_string(), name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }) ); assert_eq!(pinned_metadata.preview.as_deref(), Some("Hello from user")); @@ -951,6 +953,7 @@ mod tests { Some(codex_state::ThreadSection { id: codex_state::PINNED_THREAD_SECTION_ID.to_string(), name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(), + appearance: None, }) ); assert_eq!( diff --git a/codex-rs/thread-store/src/thread_sections.rs b/codex-rs/thread-store/src/thread_sections.rs index 53a9641372..0fda4a655e 100644 --- a/codex-rs/thread-store/src/thread_sections.rs +++ b/codex-rs/thread-store/src/thread_sections.rs @@ -12,6 +12,7 @@ pub struct ListThreadSectionsParams { pub struct CreateThreadSectionParams { /// User-facing section name. pub name: String, + pub appearance: Option, } /// Parameters for renaming a thread section. @@ -21,6 +22,7 @@ pub struct RenameThreadSectionParams { pub section_id: String, /// Replacement user-facing section name. pub name: String, + pub appearance: Option>, } /// Parameters for deleting a thread section. @@ -37,6 +39,7 @@ pub struct StoredThreadSection { pub id: String, /// User-facing section name. pub name: String, + pub appearance: Option, } /// A cursor-paginated page of independently persisted thread sections.