mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Add appearance metadata to thread sections (#37898)
## What changed - Add optional `icon` and `color` appearance fields to custom thread sections and expose them through the app-server protocol. - Persist appearance metadata in SQLite and include it when listing sections or returning threads. - Let `threadSection/update` preserve an omitted appearance, clear it with `null`, or replace it with a new value. - Reject appearance fields larger than 64 bytes. ## Testing - Cover protocol compatibility, persistence across restart, update and clear behavior, and field-length validation. GitOrigin-RevId: 8f55ce4f42f8bd17aa5416848a85845f681953df
This commit is contained in:
committed by
copyberry
parent
92912d6d84
commit
1549756b78
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+52
@@ -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"
|
||||
|
||||
+52
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+30
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
+30
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"
|
||||
|
||||
+30
@@ -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.",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
+2
-1
@@ -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, };
|
||||
|
||||
+6
-1
@@ -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, };
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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::<ThreadSection>(json!({
|
||||
"id": "legacy",
|
||||
"name": "Legacy",
|
||||
}))
|
||||
.expect("legacy sections without appearance should deserialize");
|
||||
assert_eq!(legacy.appearance, None);
|
||||
|
||||
let params = serde_json::from_value::<ThreadSectionListParams>(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::<ThreadSectionUpdateParams>(value)
|
||||
.expect("section update should deserialize");
|
||||
assert_eq!(params.appearance, appearance);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collab_agent_state_maps_interrupted_status() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -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<ThreadSectionAppearance>,
|
||||
}
|
||||
|
||||
/// 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<ThreadSectionAppearance>")]
|
||||
#[ts(optional = nullable, as = "Option<ThreadSectionAppearance>")]
|
||||
pub appearance: Option<Option<ThreadSectionAppearance>>,
|
||||
}
|
||||
|
||||
/// The independently persisted section after its name is updated.
|
||||
|
||||
@@ -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<ThreadSectionAppearance>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
] {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE thread_sections ADD COLUMN appearance TEXT;
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String>)>(
|
||||
"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,
|
||||
)]
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -53,13 +53,37 @@ pub struct Anchor {
|
||||
pub id: Option<ThreadId>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<ThreadSectionAppearance>,
|
||||
}
|
||||
|
||||
impl ThreadSection {
|
||||
pub(crate) fn from_row(
|
||||
(id, name, appearance): (String, String, Option<String>),
|
||||
) -> Result<Self> {
|
||||
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<i64>,
|
||||
section: Option<String>,
|
||||
section_name: Option<String>,
|
||||
section_appearance: Option<String>,
|
||||
section_position: Option<i64>,
|
||||
section_entered_at_ms: Option<i64>,
|
||||
git_sha: Option<String>,
|
||||
@@ -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<ThreadRow> for ThreadMetadata {
|
||||
archived_at,
|
||||
section,
|
||||
section_name,
|
||||
section_appearance,
|
||||
section_position,
|
||||
section_entered_at_ms,
|
||||
git_sha,
|
||||
@@ -541,7 +568,9 @@ impl TryFrom<ThreadRow> 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -51,13 +51,13 @@ impl StateRuntime {
|
||||
&self,
|
||||
id: &str,
|
||||
) -> anyhow::Result<Option<crate::ThreadSection>> {
|
||||
let row = sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT id, name FROM thread_sections WHERE id = ?",
|
||||
let row = sqlx::query_as::<_, (String, String, Option<String>)>(
|
||||
"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<crate::ThreadSectionsPage> {
|
||||
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<String>)>(
|
||||
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::<Vec<_>>();
|
||||
.map(crate::ThreadSection::from_row)
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
let next_cursor = if sections.len() > page_size {
|
||||
sections.pop();
|
||||
sections.last().map(|section| section.id.clone())
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<ThreadSection> {
|
||||
pub async fn create_thread_section(
|
||||
&self,
|
||||
name: &str,
|
||||
appearance: Option<ThreadSectionAppearance>,
|
||||
) -> anyhow::Result<ThreadSection> {
|
||||
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<Option<ThreadSectionAppearance>>,
|
||||
) -> anyhow::Result<Option<ThreadSection>> {
|
||||
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<String>)>(
|
||||
"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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<StoredThreadSection> {
|
||||
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<Option<StoredThreadSection>> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct ListThreadSectionsParams {
|
||||
pub struct CreateThreadSectionParams {
|
||||
/// User-facing section name.
|
||||
pub name: String,
|
||||
pub appearance: Option<codex_state::ThreadSectionAppearance>,
|
||||
}
|
||||
|
||||
/// 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<Option<codex_state::ThreadSectionAppearance>>,
|
||||
}
|
||||
|
||||
/// 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<codex_state::ThreadSectionAppearance>,
|
||||
}
|
||||
|
||||
/// A cursor-paginated page of independently persisted thread sections.
|
||||
|
||||
Reference in New Issue
Block a user