3434 CloudTenantIndexStatusResponse ,
3535 ProjectVisibility ,
3636 WorkspaceInfo ,
37+ format_workspace_choices ,
38+ format_workspace_selection_choices ,
39+ workspace_matches_identifier ,
3740)
3841from basic_memory .schemas .project_info import ProjectItem , ProjectList
3942from basic_memory .utils import generate_permalink , normalize_project_path
@@ -284,27 +287,24 @@ def _normalize_project_visibility(visibility: str | None) -> ProjectVisibility:
284287def _resolve_workspace_id (config , workspace : str | None ) -> str | None :
285288 """Resolve a workspace name, slug, type, or tenant_id to a tenant_id."""
286289 from basic_memory .mcp .project_context import (
287- _workspace_choices ,
288- _workspace_matches_identifier ,
289- _workspace_selection_choices ,
290290 get_available_workspaces ,
291291 )
292292
293293 if workspace is not None :
294294 workspaces = run_with_cleanup (get_available_workspaces ())
295- matches = [ws for ws in workspaces if _workspace_matches_identifier (ws , workspace )]
295+ matches = [ws for ws in workspaces if workspace_matches_identifier (ws , workspace )]
296296 if not matches :
297297 console .print (f"[red]Error: Workspace '{ workspace } ' not found[/red]" )
298298 if workspaces :
299- console .print (f"[dim]Available:\n { _workspace_choices (workspaces )} [/dim]" )
299+ console .print (f"[dim]Available:\n { format_workspace_choices (workspaces )} [/dim]" )
300300 raise typer .Exit (1 )
301301 if len (matches ) > 1 :
302302 console .print (
303303 f"[red]Error: Workspace '{ workspace } ' matches multiple workspaces.[/red]"
304304 )
305305 console .print (
306306 "[dim]Choose one of these matching workspaces by slug:\n "
307- f"{ _workspace_selection_choices (matches )} [/dim]"
307+ f"{ format_workspace_selection_choices (matches )} [/dim]"
308308 )
309309 raise typer .Exit (1 )
310310 return matches [0 ].tenant_id
@@ -347,6 +347,8 @@ async def _list_projects(ws: str | None = None):
347347
348348 try :
349349 config = ConfigManager ().config
350+ workspace_filter = workspace
351+ workspace_filter_requested = workspace_filter is not None
350352
351353 local_result : ProjectList | None = None
352354 cloud_results : list [tuple [WorkspaceInfo | None , ProjectList ]] = []
@@ -355,47 +357,55 @@ async def _list_projects(ws: str | None = None):
355357 cloud_workspace_error : Exception | None = None
356358 failed_cloud_workspaces : list [tuple [WorkspaceInfo , Exception ]] = []
357359
358- def _fetch_cloud_workspace_results () -> list [tuple [WorkspaceInfo | None , ProjectList ]]:
359- nonlocal available_cloud_workspaces , cloud_workspace_error
360-
360+ def _fetch_cloud_workspace_results () -> tuple [
361+ list [tuple [WorkspaceInfo | None , ProjectList ]],
362+ list [WorkspaceInfo ],
363+ Exception | None ,
364+ list [tuple [WorkspaceInfo , Exception ]],
365+ ]:
361366 from basic_memory .mcp .project_context import (
362- _workspace_choices ,
363- _workspace_matches_identifier ,
364- _workspace_selection_choices ,
365367 get_available_workspaces ,
366368 )
367369
368370 try :
369371 workspaces = run_with_cleanup (get_available_workspaces ())
370- available_cloud_workspaces = workspaces
371372 except Exception as exc :
372- cloud_workspace_error = exc
373- fallback_workspace = workspace or config .default_workspace
374- return [(None , run_with_cleanup (_list_projects (fallback_workspace )))]
373+ fallback_workspace = workspace_filter or config .default_workspace
374+ return (
375+ [(None , run_with_cleanup (_list_projects (fallback_workspace )))],
376+ [],
377+ exc ,
378+ [],
379+ )
375380
376381 selected_workspaces = workspaces
377- if workspace is not None :
378- matches = [ws for ws in workspaces if _workspace_matches_identifier (ws , workspace )]
382+ if workspace_filter is not None :
383+ matches = [
384+ ws for ws in workspaces if workspace_matches_identifier (ws , workspace_filter )
385+ ]
379386 if not matches :
380- console .print (f"[red]Error: Workspace '{ workspace } ' not found[/red]" )
387+ console .print (f"[red]Error: Workspace '{ workspace_filter } ' not found[/red]" )
381388 if workspaces :
382- console .print (f"[dim]Available:\n { _workspace_choices (workspaces )} [/dim]" )
389+ console .print (
390+ f"[dim]Available:\n { format_workspace_choices (workspaces )} [/dim]"
391+ )
383392 raise typer .Exit (1 )
384393 if len (matches ) > 1 :
385394 console .print (
386- f"[red]Error: Workspace '{ workspace } ' matches multiple workspaces.[/red]"
395+ f"[red]Error: Workspace '{ workspace_filter } ' matches multiple workspaces.[/red]"
387396 )
388397 console .print (
389398 "[dim]Choose one of these matching workspaces by slug:\n "
390- f"{ _workspace_selection_choices (matches )} [/dim]"
399+ f"{ format_workspace_selection_choices (matches )} [/dim]"
391400 )
392401 raise typer .Exit (1 )
393402 selected_workspaces = matches
394403
395404 if not selected_workspaces :
396- return []
405+ return [], workspaces , None , []
397406
398407 results : list [tuple [WorkspaceInfo | None , ProjectList ]] = []
408+ failed_workspaces : list [tuple [WorkspaceInfo , Exception ]] = []
399409 for cloud_workspace in selected_workspaces :
400410 try :
401411 results .append (
@@ -405,17 +415,22 @@ def _fetch_cloud_workspace_results() -> list[tuple[WorkspaceInfo | None, Project
405415 )
406416 )
407417 except Exception as exc :
408- failed_cloud_workspaces .append ((cloud_workspace , exc ))
418+ failed_workspaces .append ((cloud_workspace , exc ))
409419
410- if not results and failed_cloud_workspaces :
411- raise failed_cloud_workspaces [0 ][1 ]
420+ if not results and failed_workspaces :
421+ raise failed_workspaces [0 ][1 ]
412422
413- return results
423+ return results , workspaces , None , failed_workspaces
414424
415425 if cloud :
416426 with console .status ("[bold blue]Fetching cloud projects..." , spinner = "dots" ):
417427 with force_routing (cloud = True ):
418- cloud_results = _fetch_cloud_workspace_results ()
428+ (
429+ cloud_results ,
430+ available_cloud_workspaces ,
431+ cloud_workspace_error ,
432+ failed_cloud_workspaces ,
433+ ) = _fetch_cloud_workspace_results ()
419434 elif local :
420435 with force_routing (local = True ):
421436 local_result = run_with_cleanup (_list_projects ())
@@ -428,7 +443,12 @@ def _fetch_cloud_workspace_results() -> list[tuple[WorkspaceInfo | None, Project
428443 try :
429444 with console .status ("[bold blue]Fetching cloud projects..." , spinner = "dots" ):
430445 with force_routing (cloud = True ):
431- cloud_results = _fetch_cloud_workspace_results ()
446+ (
447+ cloud_results ,
448+ available_cloud_workspaces ,
449+ cloud_workspace_error ,
450+ failed_cloud_workspaces ,
451+ ) = _fetch_cloud_workspace_results ()
432452 except typer .Exit :
433453 raise
434454 except Exception as exc : # pragma: no cover
@@ -491,7 +511,7 @@ def _workspace_priority(row_key: tuple[str | None, str]) -> tuple[bool, int, str
491511
492512 def _select_attached_row_key (
493513 permalink : str , entry : ProjectEntry | None
494- ) -> tuple [str | None , str ]:
514+ ) -> tuple [str | None , str ] | None :
495515 """Choose the single row that owns local config/default/sync state."""
496516 cloud_keys = cloud_keys_by_permalink .get (permalink , [])
497517 if not cloud_keys :
@@ -517,8 +537,10 @@ def _select_attached_row_key(
517537 if row_key [0 ] == workspace_id :
518538 return row_key
519539
520- if workspace is not None and preferred_workspace_ids :
521- return (preferred_workspace_ids [0 ], permalink )
540+ if workspace_filter_requested and preferred_workspace_ids :
541+ # A filtered list can exclude the workspace that owns local config state.
542+ # In that case, do not attach local/default/sync state to another workspace row.
543+ return None
522544
523545 default_workspace_keys = [
524546 row_key
@@ -534,7 +556,7 @@ def _select_attached_row_key(
534556
535557 return sorted (cloud_keys , key = _workspace_priority )[0 ]
536558
537- attached_row_by_permalink : dict [str , tuple [str | None , str ]] = {}
559+ attached_row_by_permalink : dict [str , tuple [str | None , str ] | None ] = {}
538560 for permalink in set (local_projects_by_permalink ) | set (configured_names_by_permalink ):
539561 configured_name = configured_names_by_permalink .get (permalink )
540562 local_project = local_projects_by_permalink .get (permalink )
@@ -633,7 +655,7 @@ def _select_attached_row_key(
633655 if display_name :
634656 row_data ["display_name" ] = display_name
635657 if cloud_project is not None and cloud_ws_name :
636- row_data ["workspace" ] = cloud_ws_name or ""
658+ row_data ["workspace" ] = cloud_ws_name
637659 if cloud_ws_type :
638660 row_data ["workspace_type" ] = cloud_ws_type
639661
0 commit comments