python-codex 0.2.7__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 +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  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 +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  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 +7 -5
  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 +41 -72
  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/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  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 +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  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 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.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(
@@ -127,7 +135,9 @@ def load_workspace_definitions(
127
135
  if board_value is not False:
128
136
  if not isinstance(board_value, str) or not board_value.strip():
129
137
  raise ValueError(
130
- "workspace `{0}` requires a board path or false".format(workspace_id)
138
+ "workspace `{0}` requires a board path or false".format(
139
+ workspace_id
140
+ )
131
141
  )
132
142
  board_path = _resolve_workspace_path(board_value, path.parent)
133
143
  if board_path in seen_boards:
@@ -152,8 +162,8 @@ def load_workspace_definitions(
152
162
 
153
163
 
154
164
  def save_workspace_definitions(
155
- config_path: 'typing.Union[str, Path]',
156
- definitions: 'typing.Iterable[WorkspaceDefinition]',
165
+ config_path: "typing.Union[str, Path]",
166
+ definitions: "typing.Iterable[WorkspaceDefinition]",
157
167
  ) -> None:
158
168
  path = Path(config_path).expanduser().resolve()
159
169
  base_dir = path.parent
@@ -171,9 +181,9 @@ def save_workspace_definitions(
171
181
 
172
182
 
173
183
  def _workspace_definition_to_json(
174
- definition: 'WorkspaceDefinition',
175
- base_dir: 'Path',
176
- ) -> 'typing.Dict[str, object]':
184
+ definition: "WorkspaceDefinition",
185
+ base_dir: "Path",
186
+ ) -> "typing.Dict[str, object]":
177
187
  result = {
178
188
  "id": definition.workspace_id,
179
189
  "work_dir": _format_path_for_workspace_config(definition.work_dir, base_dir),
@@ -188,7 +198,7 @@ def _workspace_definition_to_json(
188
198
  return result
189
199
 
190
200
 
191
- 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":
192
202
  resolved = path.resolve()
193
203
  try:
194
204
  relative = os.path.relpath(str(resolved), str(base_dir.resolve()))
@@ -201,14 +211,14 @@ def _format_path_for_workspace_config(path: 'Path', base_dir: 'Path') -> 'str':
201
211
  return relative
202
212
 
203
213
 
204
- def _resolve_workspace_path(value: str, base_dir: 'Path') -> 'Path':
214
+ def _resolve_workspace_path(value: str, base_dir: "Path") -> "Path":
205
215
  path = Path(str(value)).expanduser()
206
216
  if not path.is_absolute():
207
217
  path = base_dir / path
208
218
  return path.resolve()
209
219
 
210
220
 
211
- def _next_workspace_id(existing: 'typing.Container[str]') -> str:
221
+ def _next_workspace_id(existing: "typing.Container[str]") -> str:
212
222
  index = 1
213
223
  while True:
214
224
  candidate = "workspace-{0}".format(index)
@@ -242,11 +252,17 @@ class WorkspaceSessionManager:
242
252
  self._sessions: "typing.Dict[str, object]" = {}
243
253
  self._session_order: "typing.List[str]" = []
244
254
  self._state_watchers: "typing.Dict[str, asyncio.Task]" = {}
245
- self._persisted_titles: "typing.Dict[str, str]" = {}
246
- 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
247
258
  self._state_store = WorkspaceStateStore(board_path)
248
259
  self._persist_callback = persist_callback
249
260
 
261
+ def _get_lock(self):
262
+ if self._lock is None:
263
+ self._lock = asyncio.Lock()
264
+ return self._lock
265
+
250
266
  def set_persist_callback(
251
267
  self,
252
268
  callback: "typing.Union[typing.Callable[[], None], None]",
@@ -262,54 +278,73 @@ class WorkspaceSessionManager:
262
278
  await self.create_session(
263
279
  title=str(tab.get("title") or ""),
264
280
  rollout_path=str(tab.get("rollout_path") or ""),
281
+ fork=tab.get("fork") is True,
265
282
  )
266
283
 
267
284
  async def close(self) -> None:
268
285
  sessions = list(self._sessions.values())
269
286
  watchers = list(self._state_watchers.values())
270
- self._sessions.clear()
271
- self._session_order = []
272
- self._state_watchers.clear()
273
- self._persisted_titles.clear()
274
- for watcher in watchers:
275
- watcher.cancel()
276
- if watchers:
277
- await asyncio.gather(*watchers, return_exceptions=True)
278
- for session in sessions:
279
- 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()
280
302
 
281
303
  async def create_session(
282
304
  self,
283
305
  title: str = "",
284
306
  rollout_path: str = "",
307
+ fork: bool = False,
285
308
  ) -> str:
286
- async with self._lock:
309
+ async with self._get_lock():
287
310
  session_id = uuid7_string()
288
311
  session = self._session_factory()
289
312
  await session.start()
290
313
 
291
- if rollout_path:
292
- 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
293
322
 
294
323
  self._sessions[session_id] = session
295
324
  self._session_order.append(session_id)
296
- self._persisted_titles[session_id] = str(
297
- session_summary(session).get("title") or ""
298
- )
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()
299
333
  self._state_watchers[session_id] = asyncio.create_task(
300
- self._watch_session_title(session_id, session)
334
+ self._watch_session_state(session_id, session, subscriber)
301
335
  )
302
336
  return session_id
303
337
 
304
338
  async def close_session(self, session_id: str) -> None:
305
- async with self._lock:
339
+ async with self._get_lock():
306
340
  if len(self._session_order) <= 1:
307
341
  raise ValueError("cannot close the last session")
308
342
  session = self._sessions.pop(session_id, None)
309
343
  if session is None:
310
344
  raise KeyError(session_id)
311
345
  watcher = self._state_watchers.pop(session_id, None)
312
- self._persisted_titles.pop(session_id, None)
346
+ self._tab_states.pop(session_id, None)
347
+ self._saved_session_ids.discard(session_id)
313
348
  self._session_order = [
314
349
  item for item in self._session_order if item != session_id
315
350
  ]
@@ -319,20 +354,38 @@ class WorkspaceSessionManager:
319
354
  await session.close()
320
355
  self.persist_workspace_state()
321
356
 
322
- async def _watch_session_title(self, session_id: str, session) -> None:
323
- subscriber = session.subscribe()
357
+ async def _watch_session_state(self, session_id: str, session, subscriber) -> None:
324
358
  try:
325
359
  while True:
326
360
  event = await subscriber.get()
327
361
  if event is None:
328
362
  return
329
- if not isinstance(event, dict) or event.get("type") != "title_changed":
330
- continue
331
- title = str(event.get("title") or "")
332
- if title == self._persisted_titles.get(session_id, ""):
363
+ if not isinstance(event, dict) or "snapshot" not in event:
333
364
  continue
334
- self._persisted_titles[session_id] = title
335
- 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()
336
389
  finally:
337
390
  session.unsubscribe(subscriber)
338
391
 
@@ -342,13 +395,16 @@ class WorkspaceSessionManager:
342
395
  session = self._sessions.get(session_id)
343
396
  if session is None:
344
397
  continue
345
- summary = session_summary(session)
346
- title = str(summary.get("title") or "").strip()
347
- rollout_path = str(session.rollout_path() or "")
348
- if not title and not rollout_path:
398
+ state = self._tab_states[session_id]
399
+ if not state["title"] and not state["rollout_path"]:
349
400
  continue
350
- tabs.append({"title": title, "rollout_path": rollout_path})
401
+ tabs.append(state)
351
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
+ )
352
408
  if self._persist_callback is not None:
353
409
  self._persist_callback()
354
410
 
@@ -389,14 +445,15 @@ class WorkspaceSessionManager:
389
445
 
390
446
  @dataclass(frozen=True)
391
447
  class WorkspaceEntry:
392
- definition: 'WorkspaceDefinition'
393
- manager: 'WorkspaceSessionManager'
448
+ definition: "WorkspaceDefinition"
449
+ manager: "WorkspaceSessionManager"
394
450
 
395
- def to_dict(self) -> 'typing.Dict[str, object]':
451
+ def to_dict(self) -> "typing.Dict[str, object]":
396
452
  return {
397
453
  "id": self.definition.workspace_id,
398
454
  "board_path": (
399
- "" if self.definition.board_path is None
455
+ ""
456
+ if self.definition.board_path is None
400
457
  else str(self.definition.board_path)
401
458
  ),
402
459
  "work_dir": str(self.definition.work_dir),
@@ -413,13 +470,13 @@ WorkspaceEntryFactory = typing.Callable[
413
470
  class WorkspaceRegistry:
414
471
  def __init__(
415
472
  self,
416
- entries: 'typing.Iterable[WorkspaceEntry]',
417
- config_path: 'typing.Union[str, Path, None]' = None,
473
+ entries: "typing.Iterable[WorkspaceEntry]",
474
+ config_path: "typing.Union[str, Path, None]" = None,
418
475
  entry_factory: "typing.Union[WorkspaceEntryFactory, None]" = None,
419
476
  ) -> None:
420
- self._entries: 'typing.Dict[str, WorkspaceEntry]' = {}
421
- self._name_to_key: 'typing.Dict[str, str]' = {}
422
- 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]" = []
423
480
  self._started = False
424
481
  self._config_path = (
425
482
  None if config_path is None else Path(config_path).expanduser().resolve()
@@ -430,7 +487,7 @@ class WorkspaceRegistry:
430
487
  if not self._entries and self._entry_factory is None:
431
488
  raise ValueError("at least one workspace is required")
432
489
 
433
- def _register(self, entry: 'WorkspaceEntry') -> None:
490
+ def _register(self, entry: "WorkspaceEntry") -> None:
434
491
  workspace_id = entry.definition.workspace_id
435
492
  key = self._key_for_definition(entry.definition)
436
493
  if key in self._entries:
@@ -444,7 +501,7 @@ class WorkspaceRegistry:
444
501
  self._order.append(key)
445
502
  entry.manager.set_persist_callback(self.persist_definitions)
446
503
 
447
- def _key_for_definition(self, definition: 'WorkspaceDefinition') -> str:
504
+ def _key_for_definition(self, definition: "WorkspaceDefinition") -> str:
448
505
  if definition.board_path is None:
449
506
  return "workspace:{0}".format(definition.workspace_id)
450
507
  return str(definition.board_path.resolve())
@@ -459,7 +516,7 @@ class WorkspaceRegistry:
459
516
  await self._entries[key].manager.close()
460
517
  self._started = False
461
518
 
462
- def get(self, workspace_id: str) -> 'WorkspaceEntry':
519
+ def get(self, workspace_id: str) -> "WorkspaceEntry":
463
520
  workspace_id = normalize_workspace_id(workspace_id)
464
521
  if not workspace_id:
465
522
  raise KeyError(workspace_id)
@@ -473,11 +530,13 @@ class WorkspaceRegistry:
473
530
  name: str,
474
531
  work_dir: str = "./",
475
532
  board: "typing.Union[str, bool, None]" = None,
476
- ) -> 'WorkspaceEntry':
533
+ ) -> "WorkspaceEntry":
477
534
  if self._entry_factory is None:
478
535
  raise ValueError("workspace creation is unavailable")
479
536
 
480
- 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
+ )
481
540
  resolved_work_dir = _resolve_workspace_path(str(work_dir or "./"), base_dir)
482
541
  resolved_work_dir.mkdir(parents=True, exist_ok=True)
483
542
 
@@ -493,7 +552,9 @@ class WorkspaceRegistry:
493
552
  board_path.parent.mkdir(parents=True, exist_ok=True)
494
553
  board_key = str(board_path.resolve())
495
554
  if board_key in self._entries:
496
- raise ValueError("workspace board already exists: {0}".format(board_path))
555
+ raise ValueError(
556
+ "workspace board already exists: {0}".format(board_path)
557
+ )
497
558
 
498
559
  workspace_id = normalize_workspace_id(name)
499
560
  if not workspace_id:
@@ -513,7 +574,7 @@ class WorkspaceRegistry:
513
574
  self.persist_definitions()
514
575
  return entry
515
576
 
516
- async def delete_workspace(self, name: str) -> 'WorkspaceDefinition':
577
+ async def delete_workspace(self, name: str) -> "WorkspaceDefinition":
517
578
  workspace_id = normalize_workspace_id(name)
518
579
  if not workspace_id:
519
580
  raise KeyError(workspace_id)
@@ -535,11 +596,8 @@ class WorkspaceRegistry:
535
596
  [self._entries[key].definition for key in self._order],
536
597
  )
537
598
 
538
- def list_workspaces(self) -> 'typing.List[typing.Dict[str, object]]':
539
- return [
540
- self._entries[key].to_dict()
541
- for key in self._order
542
- ]
599
+ def list_workspaces(self) -> "typing.List[typing.Dict[str, object]]":
600
+ return [self._entries[key].to_dict() for key in self._order]
543
601
 
544
602
 
545
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())