python-codex 0.2.6__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. pycodex/__init__.py +18 -14
  2. pycodex/agent.py +468 -462
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -436
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +329 -252
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +90 -52
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +235 -803
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -174
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +4 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +201 -0
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +13 -13
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +50 -66
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/utils/image_utils.py +76 -0
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +263 -161
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +25 -22
  61. responses_server/messages_api.py +96 -49
  62. responses_server/payload_processors.py +25 -19
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +196 -107
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +51 -13
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +470 -384
  71. workspace_server/workspace.html +859 -232
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +168 -100
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -553
  80. python_codex-0.2.6.dist-info/METADATA +0 -441
  81. python_codex-0.2.6.dist-info/RECORD +0 -91
  82. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,6 @@ from uuid import uuid4
9
9
 
10
10
  from pycodex.utils import uuid7_string
11
11
 
12
-
13
12
  SessionFactory = typing.Callable[[], object]
14
13
 
15
14
 
@@ -23,7 +22,7 @@ class WorkspaceStateStore:
23
22
  None if board_path is None else board_path.with_suffix(".pycodex-ws.json")
24
23
  )
25
24
 
26
- def load_tabs(self) -> "typing.List[typing.Dict[str, str]]":
25
+ def load_tabs(self) -> "typing.List[typing.Dict[str, object]]":
27
26
  if self.path is None or not self.path.is_file():
28
27
  return []
29
28
 
@@ -45,10 +44,13 @@ class WorkspaceStateStore:
45
44
  title = str(tab.get("title") or "").strip()
46
45
  rollout_path = str(tab.get("rollout_path") or "").strip()
47
46
  if title or rollout_path:
48
- result.append({"title": title, "rollout_path": rollout_path})
47
+ restored = {"title": title, "rollout_path": rollout_path}
48
+ if tab.get("fork") is True:
49
+ restored["fork"] = True
50
+ result.append(restored)
49
51
  return result
50
52
 
51
- def save_tabs(self, tabs: "typing.Iterable[typing.Dict[str, str]]") -> None:
53
+ def save_tabs(self, tabs: "typing.Iterable[typing.Dict[str, object]]") -> None:
52
54
  if self.path is None:
53
55
  return
54
56
 
@@ -56,28 +58,32 @@ class WorkspaceStateStore:
56
58
  {
57
59
  "title": str(tab.get("title") or ""),
58
60
  "rollout_path": str(tab.get("rollout_path") or ""),
61
+ **({"fork": True} if tab.get("fork") is True else {}),
59
62
  }
60
63
  for tab in tabs
61
64
  ]
62
- payload = json.dumps(
63
- {"version": 1, "tabs": state_tabs},
64
- ensure_ascii=False,
65
- indent=2,
66
- ) + "\n"
65
+ payload = (
66
+ json.dumps(
67
+ {"version": 1, "tabs": state_tabs},
68
+ ensure_ascii=False,
69
+ indent=2,
70
+ )
71
+ + "\n"
72
+ )
67
73
  self.path.parent.mkdir(parents=True, exist_ok=True)
68
74
  self.path.write_text(payload, encoding="utf-8")
69
75
 
70
76
 
71
77
  @dataclass(frozen=True)
72
78
  class WorkspaceDefinition:
73
- workspace_id: 'str'
74
- board_path: 'typing.Union[Path, None]'
75
- work_dir: 'Path'
79
+ workspace_id: "str"
80
+ board_path: "typing.Union[Path, None]"
81
+ work_dir: "Path"
76
82
 
77
83
 
78
84
  def load_workspace_definitions(
79
- config_path: 'typing.Union[str, Path]',
80
- ) -> 'typing.List[WorkspaceDefinition]':
85
+ config_path: "typing.Union[str, Path]",
86
+ ) -> "typing.List[WorkspaceDefinition]":
81
87
  path = Path(config_path).expanduser().resolve()
82
88
  if not path.exists():
83
89
  return []
@@ -112,7 +118,9 @@ def load_workspace_definitions(
112
118
  if work_dir_value is None:
113
119
  work_dir_value = item.get("cwd")
114
120
  if not str(work_dir_value or "").strip():
115
- raise ValueError("workspace `{0}` is missing `work_dir`".format(workspace_id))
121
+ raise ValueError(
122
+ "workspace `{0}` is missing `work_dir`".format(workspace_id)
123
+ )
116
124
  work_dir = _resolve_workspace_path(str(work_dir_value), path.parent)
117
125
  if not work_dir.is_dir():
118
126
  raise ValueError(
@@ -123,19 +131,25 @@ def load_workspace_definitions(
123
131
  )
124
132
 
125
133
  board_value = item.get("board")
126
- if not str(board_value or "").strip():
127
- raise ValueError("workspace `{0}` is missing `board`".format(workspace_id))
128
- board_path = _resolve_workspace_path(str(board_value), path.parent)
129
- if board_path in seen_boards:
130
- raise ValueError("duplicate workspace board: {0}".format(board_path))
131
- seen_boards.add(board_path)
132
- if not board_path.parent.is_dir():
133
- raise ValueError(
134
- "workspace `{0}` board parent directory does not exist: {1}".format(
135
- workspace_id,
136
- board_path.parent,
134
+ board_path = None
135
+ if board_value is not False:
136
+ if not isinstance(board_value, str) or not board_value.strip():
137
+ raise ValueError(
138
+ "workspace `{0}` requires a board path or false".format(
139
+ workspace_id
140
+ )
141
+ )
142
+ board_path = _resolve_workspace_path(board_value, path.parent)
143
+ if board_path in seen_boards:
144
+ raise ValueError("duplicate workspace board: {0}".format(board_path))
145
+ seen_boards.add(board_path)
146
+ if not board_path.parent.is_dir():
147
+ raise ValueError(
148
+ "workspace `{0}` board parent directory does not exist: {1}".format(
149
+ workspace_id,
150
+ board_path.parent,
151
+ )
137
152
  )
138
- )
139
153
 
140
154
  result.append(
141
155
  WorkspaceDefinition(
@@ -148,8 +162,8 @@ def load_workspace_definitions(
148
162
 
149
163
 
150
164
  def save_workspace_definitions(
151
- config_path: 'typing.Union[str, Path]',
152
- definitions: 'typing.Iterable[WorkspaceDefinition]',
165
+ config_path: "typing.Union[str, Path]",
166
+ definitions: "typing.Iterable[WorkspaceDefinition]",
153
167
  ) -> None:
154
168
  path = Path(config_path).expanduser().resolve()
155
169
  base_dir = path.parent
@@ -167,9 +181,9 @@ def save_workspace_definitions(
167
181
 
168
182
 
169
183
  def _workspace_definition_to_json(
170
- definition: 'WorkspaceDefinition',
171
- base_dir: 'Path',
172
- ) -> 'typing.Dict[str, str]':
184
+ definition: "WorkspaceDefinition",
185
+ base_dir: "Path",
186
+ ) -> "typing.Dict[str, object]":
173
187
  result = {
174
188
  "id": definition.workspace_id,
175
189
  "work_dir": _format_path_for_workspace_config(definition.work_dir, base_dir),
@@ -179,10 +193,12 @@ def _workspace_definition_to_json(
179
193
  definition.board_path,
180
194
  base_dir,
181
195
  )
196
+ else:
197
+ result["board"] = False
182
198
  return result
183
199
 
184
200
 
185
- def _format_path_for_workspace_config(path: 'Path', base_dir: 'Path') -> 'str':
201
+ def _format_path_for_workspace_config(path: "Path", base_dir: "Path") -> "str":
186
202
  resolved = path.resolve()
187
203
  try:
188
204
  relative = os.path.relpath(str(resolved), str(base_dir.resolve()))
@@ -195,14 +211,14 @@ def _format_path_for_workspace_config(path: 'Path', base_dir: 'Path') -> 'str':
195
211
  return relative
196
212
 
197
213
 
198
- def _resolve_workspace_path(value: str, base_dir: 'Path') -> 'Path':
214
+ def _resolve_workspace_path(value: str, base_dir: "Path") -> "Path":
199
215
  path = Path(str(value)).expanduser()
200
216
  if not path.is_absolute():
201
217
  path = base_dir / path
202
218
  return path.resolve()
203
219
 
204
220
 
205
- def _next_workspace_id(existing: 'typing.Container[str]') -> str:
221
+ def _next_workspace_id(existing: "typing.Container[str]") -> str:
206
222
  index = 1
207
223
  while True:
208
224
  candidate = "workspace-{0}".format(index)
@@ -236,11 +252,17 @@ class WorkspaceSessionManager:
236
252
  self._sessions: "typing.Dict[str, object]" = {}
237
253
  self._session_order: "typing.List[str]" = []
238
254
  self._state_watchers: "typing.Dict[str, asyncio.Task]" = {}
239
- self._persisted_titles: "typing.Dict[str, str]" = {}
240
- self._lock = asyncio.Lock()
255
+ self._tab_states: "typing.Dict[str, typing.Dict[str, object]]" = {}
256
+ self._saved_session_ids: "typing.Set[str]" = set()
257
+ self._lock = None
241
258
  self._state_store = WorkspaceStateStore(board_path)
242
259
  self._persist_callback = persist_callback
243
260
 
261
+ def _get_lock(self):
262
+ if self._lock is None:
263
+ self._lock = asyncio.Lock()
264
+ return self._lock
265
+
244
266
  def set_persist_callback(
245
267
  self,
246
268
  callback: "typing.Union[typing.Callable[[], None], None]",
@@ -256,54 +278,73 @@ class WorkspaceSessionManager:
256
278
  await self.create_session(
257
279
  title=str(tab.get("title") or ""),
258
280
  rollout_path=str(tab.get("rollout_path") or ""),
281
+ fork=tab.get("fork") is True,
259
282
  )
260
283
 
261
284
  async def close(self) -> None:
262
285
  sessions = list(self._sessions.values())
263
286
  watchers = list(self._state_watchers.values())
264
- self._sessions.clear()
265
- self._session_order = []
266
- self._state_watchers.clear()
267
- self._persisted_titles.clear()
268
- for watcher in watchers:
269
- watcher.cancel()
270
- if watchers:
271
- await asyncio.gather(*watchers, return_exceptions=True)
272
- for session in sessions:
273
- await session.close()
287
+ try:
288
+ for session in sessions:
289
+ await session.close()
290
+ if watchers:
291
+ await asyncio.gather(*watchers)
292
+ finally:
293
+ for watcher in watchers:
294
+ watcher.cancel()
295
+ if watchers:
296
+ await asyncio.gather(*watchers, return_exceptions=True)
297
+ self._sessions.clear()
298
+ self._session_order = []
299
+ self._state_watchers.clear()
300
+ self._tab_states.clear()
301
+ self._saved_session_ids.clear()
274
302
 
275
303
  async def create_session(
276
304
  self,
277
305
  title: str = "",
278
306
  rollout_path: str = "",
307
+ fork: bool = False,
279
308
  ) -> str:
280
- async with self._lock:
309
+ async with self._get_lock():
281
310
  session_id = uuid7_string()
282
311
  session = self._session_factory()
283
312
  await session.start()
284
313
 
285
- if rollout_path:
286
- await session.restore_from_rollout(rollout_path, title=title)
314
+ try:
315
+ if rollout_path or title:
316
+ await session.restore_from_rollout(
317
+ rollout_path, title=title, fork=fork
318
+ )
319
+ except BaseException:
320
+ await session.close()
321
+ raise
287
322
 
288
323
  self._sessions[session_id] = session
289
324
  self._session_order.append(session_id)
290
- self._persisted_titles[session_id] = str(
291
- session_summary(session).get("title") or ""
292
- )
325
+ self._tab_states[session_id] = {
326
+ "title": str(session_summary(session).get("title") or ""),
327
+ "rollout_path": rollout_path,
328
+ **({"fork": True} if fork else {}),
329
+ }
330
+ if title or rollout_path:
331
+ self._saved_session_ids.add(session_id)
332
+ subscriber = session.subscribe()
293
333
  self._state_watchers[session_id] = asyncio.create_task(
294
- self._watch_session_title(session_id, session)
334
+ self._watch_session_state(session_id, session, subscriber)
295
335
  )
296
336
  return session_id
297
337
 
298
338
  async def close_session(self, session_id: str) -> None:
299
- async with self._lock:
339
+ async with self._get_lock():
300
340
  if len(self._session_order) <= 1:
301
341
  raise ValueError("cannot close the last session")
302
342
  session = self._sessions.pop(session_id, None)
303
343
  if session is None:
304
344
  raise KeyError(session_id)
305
345
  watcher = self._state_watchers.pop(session_id, None)
306
- self._persisted_titles.pop(session_id, None)
346
+ self._tab_states.pop(session_id, None)
347
+ self._saved_session_ids.discard(session_id)
307
348
  self._session_order = [
308
349
  item for item in self._session_order if item != session_id
309
350
  ]
@@ -313,20 +354,38 @@ class WorkspaceSessionManager:
313
354
  await session.close()
314
355
  self.persist_workspace_state()
315
356
 
316
- async def _watch_session_title(self, session_id: str, session) -> None:
317
- subscriber = session.subscribe()
357
+ async def _watch_session_state(self, session_id: str, session, subscriber) -> None:
318
358
  try:
319
359
  while True:
320
360
  event = await subscriber.get()
321
361
  if event is None:
322
362
  return
323
- if not isinstance(event, dict) or event.get("type") != "title_changed":
324
- continue
325
- title = str(event.get("title") or "")
326
- if title == self._persisted_titles.get(session_id, ""):
363
+ if not isinstance(event, dict) or "snapshot" not in event:
327
364
  continue
328
- self._persisted_titles[session_id] = title
329
- self.persist_workspace_state()
365
+ reason = (
366
+ event["payload"]["reason"]
367
+ if event.get("kind") == "session_state"
368
+ else None
369
+ )
370
+ previous = self._tab_states[session_id]
371
+ state = dict(previous)
372
+ snapshot = event["snapshot"]
373
+ state["title"] = str(snapshot.get("title") or "")
374
+ if reason == "identity" and state["rollout_path"]:
375
+ # Recreate an unwritten fork from its recorded ancestor.
376
+ state["fork"] = True
377
+ path = str(snapshot["recorded_rollout_path"] or "")
378
+ if path or not snapshot["rollout_path"]:
379
+ state["rollout_path"] = path
380
+ state.pop("fork", None)
381
+ self._tab_states[session_id] = state
382
+ save_requested = event.get("type") == "title_changed" or reason in {
383
+ "identity",
384
+ "history",
385
+ }
386
+ saved = session_id in self._saved_session_ids
387
+ if (save_requested or saved) and (state != previous or not saved):
388
+ self.persist_workspace_state()
330
389
  finally:
331
390
  session.unsubscribe(subscriber)
332
391
 
@@ -336,13 +395,16 @@ class WorkspaceSessionManager:
336
395
  session = self._sessions.get(session_id)
337
396
  if session is None:
338
397
  continue
339
- summary = session_summary(session)
340
- title = str(summary.get("title") or "").strip()
341
- rollout_path = str(session.rollout_path() or "")
342
- if not title and not rollout_path:
398
+ state = self._tab_states[session_id]
399
+ if not state["title"] and not state["rollout_path"]:
343
400
  continue
344
- tabs.append({"title": title, "rollout_path": rollout_path})
401
+ tabs.append(state)
345
402
  self._state_store.save_tabs(tabs)
403
+ self._saved_session_ids.update(
404
+ session_id
405
+ for session_id, state in self._tab_states.items()
406
+ if state["title"] or state["rollout_path"]
407
+ )
346
408
  if self._persist_callback is not None:
347
409
  self._persist_callback()
348
410
 
@@ -383,14 +445,15 @@ class WorkspaceSessionManager:
383
445
 
384
446
  @dataclass(frozen=True)
385
447
  class WorkspaceEntry:
386
- definition: 'WorkspaceDefinition'
387
- manager: 'WorkspaceSessionManager'
448
+ definition: "WorkspaceDefinition"
449
+ manager: "WorkspaceSessionManager"
388
450
 
389
- def to_dict(self) -> 'typing.Dict[str, object]':
451
+ def to_dict(self) -> "typing.Dict[str, object]":
390
452
  return {
391
453
  "id": self.definition.workspace_id,
392
454
  "board_path": (
393
- "" if self.definition.board_path is None
455
+ ""
456
+ if self.definition.board_path is None
394
457
  else str(self.definition.board_path)
395
458
  ),
396
459
  "work_dir": str(self.definition.work_dir),
@@ -407,13 +470,13 @@ WorkspaceEntryFactory = typing.Callable[
407
470
  class WorkspaceRegistry:
408
471
  def __init__(
409
472
  self,
410
- entries: 'typing.Iterable[WorkspaceEntry]',
411
- config_path: 'typing.Union[str, Path, None]' = None,
473
+ entries: "typing.Iterable[WorkspaceEntry]",
474
+ config_path: "typing.Union[str, Path, None]" = None,
412
475
  entry_factory: "typing.Union[WorkspaceEntryFactory, None]" = None,
413
476
  ) -> None:
414
- self._entries: 'typing.Dict[str, WorkspaceEntry]' = {}
415
- self._name_to_key: 'typing.Dict[str, str]' = {}
416
- self._order: 'typing.List[str]' = []
477
+ self._entries: "typing.Dict[str, WorkspaceEntry]" = {}
478
+ self._name_to_key: "typing.Dict[str, str]" = {}
479
+ self._order: "typing.List[str]" = []
417
480
  self._started = False
418
481
  self._config_path = (
419
482
  None if config_path is None else Path(config_path).expanduser().resolve()
@@ -424,7 +487,7 @@ class WorkspaceRegistry:
424
487
  if not self._entries and self._entry_factory is None:
425
488
  raise ValueError("at least one workspace is required")
426
489
 
427
- def _register(self, entry: 'WorkspaceEntry') -> None:
490
+ def _register(self, entry: "WorkspaceEntry") -> None:
428
491
  workspace_id = entry.definition.workspace_id
429
492
  key = self._key_for_definition(entry.definition)
430
493
  if key in self._entries:
@@ -438,9 +501,9 @@ class WorkspaceRegistry:
438
501
  self._order.append(key)
439
502
  entry.manager.set_persist_callback(self.persist_definitions)
440
503
 
441
- def _key_for_definition(self, definition: 'WorkspaceDefinition') -> str:
504
+ def _key_for_definition(self, definition: "WorkspaceDefinition") -> str:
442
505
  if definition.board_path is None:
443
- raise ValueError("workspace `{0}` is missing board".format(definition.workspace_id))
506
+ return "workspace:{0}".format(definition.workspace_id)
444
507
  return str(definition.board_path.resolve())
445
508
 
446
509
  async def start(self) -> None:
@@ -453,7 +516,7 @@ class WorkspaceRegistry:
453
516
  await self._entries[key].manager.close()
454
517
  self._started = False
455
518
 
456
- def get(self, workspace_id: str) -> 'WorkspaceEntry':
519
+ def get(self, workspace_id: str) -> "WorkspaceEntry":
457
520
  workspace_id = normalize_workspace_id(workspace_id)
458
521
  if not workspace_id:
459
522
  raise KeyError(workspace_id)
@@ -466,24 +529,32 @@ class WorkspaceRegistry:
466
529
  self,
467
530
  name: str,
468
531
  work_dir: str = "./",
469
- board: "typing.Union[str, None]" = None,
470
- ) -> 'WorkspaceEntry':
532
+ board: "typing.Union[str, bool, None]" = None,
533
+ ) -> "WorkspaceEntry":
471
534
  if self._entry_factory is None:
472
535
  raise ValueError("workspace creation is unavailable")
473
536
 
474
- base_dir = self._config_path.parent if self._config_path is not None else Path.cwd()
537
+ base_dir = (
538
+ self._config_path.parent if self._config_path is not None else Path.cwd()
539
+ )
475
540
  resolved_work_dir = _resolve_workspace_path(str(work_dir or "./"), base_dir)
476
541
  resolved_work_dir.mkdir(parents=True, exist_ok=True)
477
542
 
478
- board_path = (
479
- _resolve_workspace_path(str(board), base_dir)
480
- if str(board or "").strip()
481
- else default_board_path()
482
- )
483
- board_path.parent.mkdir(parents=True, exist_ok=True)
484
- board_key = str(board_path.resolve())
485
- if board_key in self._entries:
486
- raise ValueError("workspace board already exists: {0}".format(board_path))
543
+ board_path = None
544
+ if board is not False:
545
+ if board is not None and not isinstance(board, str):
546
+ raise ValueError("board must be a path string or false")
547
+ board_path = (
548
+ _resolve_workspace_path(board, base_dir)
549
+ if board and board.strip()
550
+ else default_board_path()
551
+ )
552
+ board_path.parent.mkdir(parents=True, exist_ok=True)
553
+ board_key = str(board_path.resolve())
554
+ if board_key in self._entries:
555
+ raise ValueError(
556
+ "workspace board already exists: {0}".format(board_path)
557
+ )
487
558
 
488
559
  workspace_id = normalize_workspace_id(name)
489
560
  if not workspace_id:
@@ -503,7 +574,7 @@ class WorkspaceRegistry:
503
574
  self.persist_definitions()
504
575
  return entry
505
576
 
506
- async def delete_workspace(self, name: str) -> 'WorkspaceDefinition':
577
+ async def delete_workspace(self, name: str) -> "WorkspaceDefinition":
507
578
  workspace_id = normalize_workspace_id(name)
508
579
  if not workspace_id:
509
580
  raise KeyError(workspace_id)
@@ -525,11 +596,8 @@ class WorkspaceRegistry:
525
596
  [self._entries[key].definition for key in self._order],
526
597
  )
527
598
 
528
- def list_workspaces(self) -> 'typing.List[typing.Dict[str, object]]':
529
- return [
530
- self._entries[key].to_dict()
531
- for key in self._order
532
- ]
599
+ def list_workspaces(self) -> "typing.List[typing.Dict[str, object]]":
600
+ return [self._entries[key].to_dict() for key in self._order]
533
601
 
534
602
 
535
603
  def session_snapshot(session) -> "typing.Dict[str, object]":
pycodex/collaboration.py DELETED
@@ -1,20 +0,0 @@
1
-
2
- from .compat import Literal
3
- import typing
4
-
5
- CollaborationMode = Literal["default", "plan", "execute", "pair_programming"]
6
-
7
- DEFAULT_COLLABORATION_MODE: 'CollaborationMode' = "default"
8
- PLAN_COLLABORATION_MODE: 'CollaborationMode' = "plan"
9
-
10
- _MODE_DISPLAY_NAMES: 'typing.Dict[str, str]' = {
11
- "default": "Default",
12
- "plan": "Plan",
13
- "execute": "Execute",
14
- "pair_programming": "Pair Programming",
15
- }
16
-
17
-
18
- def collaboration_mode_display_name(mode: 'typing.Union[str, None]') -> 'str':
19
- normalized = (mode or DEFAULT_COLLABORATION_MODE).strip().lower()
20
- return _MODE_DISPLAY_NAMES.get(normalized, normalized.replace("_", " ").title())