mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Add experimental app-server project APIs (#38940)
## What changed - Add SQLite-backed `project/list`, `project/read`, `project/create`, `project/import`, `project/update`, `project/move`, and `project/delete` endpoints with ordered roots, metadata, manual positioning, pagination, and idempotent creation. - Add project assignment to thread start, metadata updates, list filtering, and fork inheritance. Project deletion clears assignments without deleting threads. - Emit `project/changed` and `thread/project/updated` notifications after committed changes, and export the new protocol schemas and TypeScript types. ## Testing - Cover project lifecycle and ordering, idempotency, atomic imports, cursor and filter validation, thread assignment, deletion, and fork inheritance. GitOrigin-RevId: 47ae621be01ab8ef70a74a629fb29a5c5709ea33
This commit is contained in:
1 parent
1f41cc5d92
commit
3b4569a920
107 files changed
+4210
-52
No files matched your search
@@ -283,6 +283,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: true,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
|
||||
@@ -172,6 +172,7 @@ pub(super) fn stored_thread_from_rollout_item(
|
||||
section: item.section,
|
||||
section_position: None,
|
||||
section_entered_at: None,
|
||||
project_id: item.project_id,
|
||||
cwd: item.cwd.unwrap_or_default(),
|
||||
cli_version: item.cli_version.unwrap_or_default(),
|
||||
source,
|
||||
|
||||
@@ -111,6 +111,10 @@ pub(super) async fn list_threads(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(project_id) = params.project_id.as_ref() {
|
||||
items.retain(|thread| &thread.project_id == project_id);
|
||||
}
|
||||
|
||||
Ok(ThreadPage { items, next_cursor })
|
||||
}
|
||||
|
||||
@@ -181,6 +185,10 @@ async fn list_section_threads(
|
||||
model_providers: params.model_providers.as_deref(),
|
||||
cwd_filters: normalized_cwd_filters.as_deref(),
|
||||
section: Some(Some(section)),
|
||||
project_id: params
|
||||
.project_id
|
||||
.as_ref()
|
||||
.map(|project_id| project_id.as_deref()),
|
||||
anchor: anchor.as_ref(),
|
||||
sort_key: codex_state::SortKey::SectionPosition,
|
||||
sort_direction: match params.sort_direction {
|
||||
@@ -245,7 +253,7 @@ pub(super) async fn list_rollout_threads(
|
||||
sort_key: codex_rollout::ThreadSortKey,
|
||||
sort_direction: codex_rollout::SortDirection,
|
||||
) -> ThreadStoreResult<codex_rollout::ThreadsPage> {
|
||||
if params.relation_filter.is_some() || params.section.is_some() {
|
||||
if params.relation_filter.is_some() || params.section.is_some() || params.project_id.is_some() {
|
||||
let relation_filter = params
|
||||
.relation_filter
|
||||
.map(|relation_filter| match relation_filter {
|
||||
@@ -269,6 +277,7 @@ pub(super) async fn list_rollout_threads(
|
||||
relation_filter,
|
||||
params.archived,
|
||||
params.section.as_ref().map(Option::as_deref),
|
||||
params.project_id.as_ref().map(Option::as_deref),
|
||||
params.search_term.as_deref(),
|
||||
)
|
||||
.await
|
||||
@@ -391,6 +400,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
@@ -452,6 +462,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: Some("needle".to_string()),
|
||||
relation_filter: None,
|
||||
@@ -525,6 +536,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: Some("canonical".to_string()),
|
||||
relation_filter: None,
|
||||
@@ -562,6 +574,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
@@ -579,6 +592,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: true,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
@@ -632,6 +646,7 @@ mod tests {
|
||||
model_providers: Some(vec!["test-provider".to_string()]),
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
@@ -710,6 +725,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: Some(Some(PINNED_THREAD_SECTION_ID.to_owned())),
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
@@ -763,6 +779,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
|
||||
@@ -8,6 +8,7 @@ mod model_context;
|
||||
mod move_thread_to_section;
|
||||
mod paginated_fork;
|
||||
mod pending_thread_metadata;
|
||||
mod projects;
|
||||
mod read_thread;
|
||||
mod revert_thread;
|
||||
mod rollout_migration;
|
||||
@@ -48,21 +49,27 @@ use tokio::sync::RwLock;
|
||||
use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::ArchiveThreadsParams;
|
||||
use crate::CreateProjectParams;
|
||||
use crate::CreateThreadParams;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::CreatedProject;
|
||||
use crate::DeleteThreadParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::DeleteThreadsParams;
|
||||
use crate::DeletedProject;
|
||||
use crate::ItemPage;
|
||||
use crate::ListItemsParams;
|
||||
use crate::ListProjectsParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::ListThreadsParams;
|
||||
use crate::ListTurnsParams;
|
||||
use crate::LoadThreadHistoryParams;
|
||||
use crate::MoveProjectParams;
|
||||
use crate::MoveThreadToSectionParams;
|
||||
use crate::PersistContext;
|
||||
use crate::PrepareForkParams;
|
||||
use crate::PreparedFork;
|
||||
use crate::ProjectMoveOutcome;
|
||||
use crate::ReadThreadByRolloutPathParams;
|
||||
use crate::ReadThreadParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
@@ -71,6 +78,8 @@ use crate::RevertThreadParams;
|
||||
use crate::SearchThreadOccurrencesParams;
|
||||
use crate::SearchThreadsParams;
|
||||
use crate::StoredModelContext;
|
||||
use crate::StoredProject;
|
||||
use crate::StoredProjectsPage;
|
||||
use crate::StoredThread;
|
||||
use crate::StoredThreadHistory;
|
||||
use crate::StoredThreadSection;
|
||||
@@ -84,7 +93,9 @@ use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreFuture;
|
||||
use crate::ThreadStoreResult;
|
||||
use crate::TurnPage;
|
||||
use crate::UpdateProjectParams;
|
||||
use crate::UpdateThreadMetadataParams;
|
||||
use crate::UpdatedProject;
|
||||
use crate::local::writer_lock::WriterLockCoordinator;
|
||||
use crate::local::writer_lock::WriterLockGuard;
|
||||
|
||||
@@ -535,6 +546,43 @@ impl ThreadStore for LocalThreadStore {
|
||||
Box::pin(async move { thread_sections::delete_thread_section(self, params).await })
|
||||
}
|
||||
|
||||
fn supports_projects(&self) -> bool {
|
||||
self.state_db.is_some()
|
||||
}
|
||||
|
||||
fn list_projects(
|
||||
&self,
|
||||
params: ListProjectsParams,
|
||||
) -> ThreadStoreFuture<'_, StoredProjectsPage> {
|
||||
Box::pin(async move { projects::list_projects(self, params).await })
|
||||
}
|
||||
|
||||
fn read_project(&self, project_id: String) -> ThreadStoreFuture<'_, Option<StoredProject>> {
|
||||
Box::pin(async move { projects::read_project(self, project_id).await })
|
||||
}
|
||||
|
||||
fn create_project(&self, params: CreateProjectParams) -> ThreadStoreFuture<'_, CreatedProject> {
|
||||
Box::pin(async move { projects::create_project(self, params).await })
|
||||
}
|
||||
|
||||
fn update_project(
|
||||
&self,
|
||||
params: UpdateProjectParams,
|
||||
) -> ThreadStoreFuture<'_, Option<UpdatedProject>> {
|
||||
Box::pin(async move { projects::update_project(self, params).await })
|
||||
}
|
||||
|
||||
fn move_project(
|
||||
&self,
|
||||
params: MoveProjectParams,
|
||||
) -> ThreadStoreFuture<'_, Option<ProjectMoveOutcome>> {
|
||||
Box::pin(async move { projects::move_project(self, params).await })
|
||||
}
|
||||
|
||||
fn delete_project(&self, project_id: String) -> ThreadStoreFuture<'_, Option<DeletedProject>> {
|
||||
Box::pin(async move { projects::delete_project(self, project_id).await })
|
||||
}
|
||||
|
||||
fn supports_paginated_history_lists(&self) -> bool {
|
||||
self.state_db.is_some()
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ async fn pending_thread_metadata_is_consumed_by_first_metadata_update() {
|
||||
.stage_pending_thread_metadata(thread_id, pending_patch.clone())
|
||||
.await
|
||||
.expect("stage pending metadata");
|
||||
|
||||
let live_thread = LiveThread::create(store.clone(), create_thread_params(thread_id))
|
||||
.await
|
||||
.expect("create live thread");
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
use super::LocalThreadStore;
|
||||
use crate::CreateProjectParams;
|
||||
use crate::CreatedProject;
|
||||
use crate::DeletedProject;
|
||||
use crate::ListProjectsParams;
|
||||
use crate::MoveProjectParams;
|
||||
use crate::ProjectMoveOutcome;
|
||||
use crate::StoredProject;
|
||||
use crate::StoredProjectRoot;
|
||||
use crate::StoredProjectsPage;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreResult;
|
||||
use crate::UpdateProjectParams;
|
||||
use crate::UpdatedProject;
|
||||
|
||||
pub(super) async fn list_projects(
|
||||
store: &LocalThreadStore,
|
||||
params: ListProjectsParams,
|
||||
) -> ThreadStoreResult<StoredProjectsPage> {
|
||||
let state = state(store)?;
|
||||
let has_cursor = params.cursor.is_some();
|
||||
let page = state
|
||||
.list_projects(params.cursor.as_deref(), params.limit)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
if has_cursor && error.to_string().starts_with("invalid project cursor:") {
|
||||
ThreadStoreError::InvalidRequest {
|
||||
message: error.to_string(),
|
||||
}
|
||||
} else {
|
||||
internal(error)
|
||||
}
|
||||
})?;
|
||||
Ok(StoredProjectsPage {
|
||||
projects: page.projects.into_iter().map(stored_project).collect(),
|
||||
next_cursor: page.next_cursor,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn read_project(
|
||||
store: &LocalThreadStore,
|
||||
project_id: String,
|
||||
) -> ThreadStoreResult<Option<StoredProject>> {
|
||||
state(store)?
|
||||
.get_project(&project_id)
|
||||
.await
|
||||
.map(|project| project.map(stored_project))
|
||||
.map_err(internal)
|
||||
}
|
||||
|
||||
pub(super) async fn create_project(
|
||||
store: &LocalThreadStore,
|
||||
params: CreateProjectParams,
|
||||
) -> ThreadStoreResult<CreatedProject> {
|
||||
state(store)?
|
||||
.create_project(
|
||||
params.name,
|
||||
params.roots.into_iter().map(state_root).collect(),
|
||||
params.metadata,
|
||||
¶ms.thread_ids,
|
||||
¶ms.idempotency_key,
|
||||
)
|
||||
.await
|
||||
.map(|created| CreatedProject {
|
||||
project: stored_project(created.project),
|
||||
created: created.created,
|
||||
})
|
||||
.map_err(project_idempotency_error)
|
||||
}
|
||||
|
||||
pub(super) async fn update_project(
|
||||
store: &LocalThreadStore,
|
||||
params: UpdateProjectParams,
|
||||
) -> ThreadStoreResult<Option<UpdatedProject>> {
|
||||
state(store)?
|
||||
.update_project(
|
||||
¶ms.project_id,
|
||||
params.name,
|
||||
params
|
||||
.roots
|
||||
.map(|roots| roots.into_iter().map(state_root).collect()),
|
||||
params.metadata,
|
||||
)
|
||||
.await
|
||||
.map(|result| {
|
||||
result.map(|(project, changed)| UpdatedProject {
|
||||
project: stored_project(project),
|
||||
changed,
|
||||
})
|
||||
})
|
||||
.map_err(internal)
|
||||
}
|
||||
|
||||
pub(super) async fn move_project(
|
||||
store: &LocalThreadStore,
|
||||
params: MoveProjectParams,
|
||||
) -> ThreadStoreResult<Option<ProjectMoveOutcome>> {
|
||||
state(store)?
|
||||
.move_project(¶ms.project_id, params.before_project_id.as_deref())
|
||||
.await
|
||||
.map(|result| {
|
||||
result.map(|changed| {
|
||||
if changed {
|
||||
ProjectMoveOutcome::Moved
|
||||
} else {
|
||||
ProjectMoveOutcome::Unchanged
|
||||
}
|
||||
})
|
||||
})
|
||||
.map_err(|error| {
|
||||
let message = error.to_string();
|
||||
if message.starts_with("before project ")
|
||||
|| message.starts_with("project ") && message.contains("cannot be moved")
|
||||
{
|
||||
ThreadStoreError::InvalidRequest { message }
|
||||
} else {
|
||||
internal(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn delete_project(
|
||||
store: &LocalThreadStore,
|
||||
project_id: String,
|
||||
) -> ThreadStoreResult<Option<DeletedProject>> {
|
||||
state(store)?
|
||||
.delete_project(&project_id)
|
||||
.await
|
||||
.map(|result| {
|
||||
result.map(
|
||||
|(affected_active_thread_ids, affected_archived_thread_ids)| DeletedProject {
|
||||
affected_active_thread_ids,
|
||||
affected_archived_thread_ids,
|
||||
},
|
||||
)
|
||||
})
|
||||
.map_err(internal)
|
||||
}
|
||||
|
||||
fn state(store: &LocalThreadStore) -> ThreadStoreResult<&codex_rollout::StateDbHandle> {
|
||||
store
|
||||
.state_db
|
||||
.as_ref()
|
||||
.ok_or(ThreadStoreError::Unsupported {
|
||||
operation: "projects",
|
||||
})
|
||||
}
|
||||
|
||||
fn internal(error: impl std::fmt::Display) -> ThreadStoreError {
|
||||
ThreadStoreError::Internal {
|
||||
message: error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn project_idempotency_error(error: impl std::fmt::Display) -> ThreadStoreError {
|
||||
let message = error.to_string();
|
||||
if message.starts_with("idempotency key refers to deleted project:") {
|
||||
ThreadStoreError::InvalidRequest { message }
|
||||
} else {
|
||||
internal(message)
|
||||
}
|
||||
}
|
||||
|
||||
fn state_root(root: StoredProjectRoot) -> codex_state::ProjectRoot {
|
||||
codex_state::ProjectRoot { path: root.path }
|
||||
}
|
||||
|
||||
fn stored_project(project: codex_state::Project) -> StoredProject {
|
||||
StoredProject {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
roots: project
|
||||
.roots
|
||||
.into_iter()
|
||||
.map(|root| StoredProjectRoot { path: root.path })
|
||||
.collect(),
|
||||
metadata: project.metadata,
|
||||
position: project.position,
|
||||
created_at_ms: project.created_at_ms,
|
||||
updated_at_ms: project.updated_at_ms,
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ pub(super) async fn read_thread(
|
||||
if thread.name.is_some() {
|
||||
rollout_thread.name = thread.name;
|
||||
}
|
||||
rollout_thread.project_id = thread.project_id;
|
||||
rollout_thread.git_info = thread.git_info;
|
||||
rollout_thread.permission_profile = permission_profile_from_metadata_value(
|
||||
&metadata_sandbox_policy,
|
||||
@@ -139,6 +140,7 @@ pub(super) async fn read_thread_by_rollout_path(
|
||||
thread.section = metadata.section;
|
||||
thread.section_position = metadata.section_position;
|
||||
thread.section_entered_at = metadata.section_entered_at;
|
||||
thread.project_id = metadata.project_id;
|
||||
if !metadata.cwd.as_os_str().is_empty()
|
||||
&& resolve_requested_rollout_path(store, metadata.rollout_path.clone())
|
||||
.await
|
||||
@@ -380,6 +382,7 @@ pub(super) fn stored_thread_from_state_metadata(
|
||||
section: metadata.section,
|
||||
section_position: metadata.section_position,
|
||||
section_entered_at: metadata.section_entered_at,
|
||||
project_id: metadata.project_id,
|
||||
cwd: metadata.cwd,
|
||||
cli_version: metadata.cli_version,
|
||||
source: parse_session_source(&metadata.source),
|
||||
@@ -478,6 +481,7 @@ fn stored_thread_from_meta_line(
|
||||
section: None,
|
||||
section_position: None,
|
||||
section_entered_at: None,
|
||||
project_id: None,
|
||||
cwd: meta_line.meta.cwd,
|
||||
cli_version: meta_line.meta.cli_version,
|
||||
source: meta_line.meta.source,
|
||||
|
||||
@@ -100,6 +100,7 @@ pub(super) async fn search_threads(
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: params.archived,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
|
||||
@@ -169,6 +169,7 @@ async fn split_homes_support_backfill_listing_and_paginated_history() {
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
section: None,
|
||||
project_id: None,
|
||||
use_state_db_only: true,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -46,6 +46,11 @@ pub(super) async fn update_thread_metadata(
|
||||
merged_patch.merge(patch);
|
||||
patch = merged_patch;
|
||||
}
|
||||
if patch.project_id.is_some() && store.state_db().await.is_none() {
|
||||
return Err(ThreadStoreError::Unsupported {
|
||||
operation: "projects",
|
||||
});
|
||||
}
|
||||
if patch.is_empty() {
|
||||
return read_thread::read_thread(
|
||||
store,
|
||||
@@ -312,6 +317,22 @@ async fn apply_metadata_update(
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
message: format!("failed to read thread metadata for {thread_id}: {err}"),
|
||||
})?;
|
||||
let project_id = if existing.is_none()
|
||||
&& let Some(Some(project_id)) = patch.project_id.as_ref()
|
||||
&& state_db
|
||||
.get_project(project_id)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to read initial thread project for {thread_id}: {err}"
|
||||
),
|
||||
})?
|
||||
.is_none()
|
||||
{
|
||||
Some(None)
|
||||
} else {
|
||||
patch.project_id.clone()
|
||||
};
|
||||
let advance_recency_at = patch.advance_recency_at;
|
||||
if existing.is_none() && rollout_path.is_none() {
|
||||
let resolved = if include_archived {
|
||||
@@ -425,12 +446,49 @@ async fn apply_metadata_update(
|
||||
metadata.git_branch = branch;
|
||||
metadata.git_origin_url = origin_url;
|
||||
}
|
||||
state_db
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
if let Some(project_id) = project_id.as_ref() {
|
||||
metadata.project_id = project_id.clone();
|
||||
}
|
||||
let upsert_result = state_db.upsert_thread(&metadata).await;
|
||||
if existing.is_none()
|
||||
&& metadata.project_id.is_some()
|
||||
&& matches!(&upsert_result, Err(err) if err.to_string().contains("FOREIGN KEY constraint failed"))
|
||||
{
|
||||
metadata.project_id = None;
|
||||
state_db.upsert_thread(&metadata).await.map_err(|err| {
|
||||
ThreadStoreError::Internal {
|
||||
message: format!("failed to update thread metadata for {thread_id}: {err}"),
|
||||
}
|
||||
})?;
|
||||
} else {
|
||||
upsert_result.map_err(|err| ThreadStoreError::Internal {
|
||||
message: format!("failed to update thread metadata for {thread_id}: {err}"),
|
||||
})?;
|
||||
}
|
||||
if existing.is_some()
|
||||
&& let Some(project_id) = project_id.as_ref()
|
||||
{
|
||||
state_db
|
||||
.set_thread_project(&thread_id.to_string(), project_id.as_deref())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let message = err.to_string();
|
||||
if message.contains("project not found") {
|
||||
ThreadStoreError::InvalidRequest { message }
|
||||
} else {
|
||||
ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to update thread project for {thread_id}: {err}"
|
||||
),
|
||||
}
|
||||
}
|
||||
})?
|
||||
.ok_or_else(|| ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"thread metadata unavailable before project update: {thread_id}"
|
||||
),
|
||||
})?;
|
||||
}
|
||||
if let Some(name) = patch.name.as_ref() {
|
||||
let history_mode = history_mode.ok_or_else(|| ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
@@ -595,8 +653,9 @@ fn sqlite_write_failure_should_block(patch: &ThreadMetadataPatch) -> bool {
|
||||
// transcript-derived metadata, thread names, and memory-mode indexing were log-only. Keep that
|
||||
// failure isolation so a corrupted optional state DB does not make JSONL transcript durability
|
||||
// look broken. Explicit git-only updates still require SQLite because partial git patches need
|
||||
// the existing SQLite value to preserve unspecified fields.
|
||||
patch.git_info.is_some() && !has_observed_metadata_facts(patch)
|
||||
// the existing SQLite value to preserve unspecified fields. Project updates always require
|
||||
// SQLite because assignment only exists in the state database.
|
||||
patch.project_id.is_some() || (patch.git_info.is_some() && !has_observed_metadata_facts(patch))
|
||||
}
|
||||
|
||||
fn sqlite_write_error_is_best_effort(err: &ThreadStoreError) -> bool {
|
||||
@@ -926,6 +985,7 @@ mod tests {
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
section: Some(Some(codex_state::PINNED_THREAD_SECTION_ID.to_string())),
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
@@ -2090,6 +2150,7 @@ mod tests {
|
||||
model_providers: Some(Vec::new()),
|
||||
cwd_filters: Some(vec![workspace]),
|
||||
section: None,
|
||||
project_id: None,
|
||||
archived: false,
|
||||
search_term: None,
|
||||
relation_filter: None,
|
||||
|
||||
Reference in new issue
Block a user