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.
- pycodex/__init__.py +18 -14
- pycodex/agent.py +468 -462
- pycodex/bootstrap.py +417 -0
- pycodex/cli.py +236 -436
- pycodex/compat.py +19 -5
- pycodex/context.py +222 -212
- pycodex/doctor.py +52 -48
- pycodex/events.py +857 -0
- pycodex/feishu_card.py +217 -163
- pycodex/feishu_link.py +43 -83
- pycodex/model.py +329 -252
- pycodex/model_metadata.py +19 -7
- pycodex/portable.py +90 -52
- pycodex/portable_server.py +32 -24
- pycodex/prompts/models.json +235 -803
- pycodex/protocol.py +177 -137
- pycodex/runtime.py +579 -174
- pycodex/runtime_services.py +204 -157
- pycodex/tools/__init__.py +4 -1
- pycodex/tools/apply_patch_tool.py +69 -48
- pycodex/tools/base_tool.py +89 -42
- pycodex/tools/clock_tool.py +201 -0
- pycodex/tools/close_agent_tool.py +2 -2
- pycodex/tools/code_mode_manager.py +77 -64
- pycodex/tools/exec_command_tool.py +26 -11
- pycodex/tools/exec_tool.py +4 -4
- pycodex/tools/grep_files_tool.py +12 -10
- pycodex/tools/ipython_tool.py +10 -13
- pycodex/tools/list_dir_tool.py +13 -9
- pycodex/tools/read_file_tool.py +29 -17
- pycodex/tools/request_permissions_tool.py +15 -5
- pycodex/tools/request_user_input_tool.py +13 -104
- pycodex/tools/resume_agent_tool.py +2 -2
- pycodex/tools/send_input_tool.py +11 -8
- pycodex/tools/shell_command_tool.py +7 -5
- pycodex/tools/shell_tool.py +7 -5
- pycodex/tools/spawn_agent_tool.py +7 -4
- pycodex/tools/unified_exec_manager.py +102 -69
- pycodex/tools/update_plan_tool.py +8 -5
- pycodex/tools/view_image_tool.py +13 -13
- pycodex/tools/wait_agent_tool.py +27 -4
- pycodex/tools/wait_tool.py +5 -4
- pycodex/tools/web_search_tool.py +4 -2
- pycodex/tools/write_stdin_tool.py +12 -11
- pycodex/utils/__init__.py +2 -17
- pycodex/utils/compactor.py +50 -66
- pycodex/utils/debug.py +2 -2
- pycodex/utils/dotenv.py +6 -7
- pycodex/utils/event_helpers.py +190 -0
- pycodex/utils/get_env.py +27 -70
- pycodex/utils/image_utils.py +76 -0
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +263 -161
- pycodex/utils/truncation.py +21 -45
- python_codex-0.3.0.dist-info/METADATA +704 -0
- python_codex-0.3.0.dist-info/RECORD +90 -0
- responses_server/__init__.py +1 -5
- responses_server/__main__.py +0 -1
- responses_server/app.py +36 -31
- responses_server/config.py +25 -22
- responses_server/messages_api.py +96 -49
- responses_server/payload_processors.py +25 -19
- responses_server/server.py +11 -11
- responses_server/session_store.py +14 -11
- responses_server/stream_router.py +196 -107
- responses_server/tools/custom_adapter.py +17 -16
- responses_server/tools/web_search.py +39 -36
- responses_server/trajectory_dump.py +51 -13
- workspace_server/__main__.py +0 -1
- workspace_server/app.py +470 -384
- workspace_server/workspace.html +859 -232
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +168 -100
- pycodex/collaboration.py +0 -20
- pycodex/interactive_session.py +0 -415
- pycodex/prompts/collaboration_default.md +0 -11
- pycodex/prompts/collaboration_plan.md +0 -128
- pycodex/utils/toolcall_visualize.py +0 -713
- pycodex/utils/visualize.py +0 -553
- python_codex-0.2.6.dist-info/METADATA +0 -441
- python_codex-0.2.6.dist-info/RECORD +0 -91
- {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
- {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
workspace_server/workspaces.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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:
|
|
74
|
-
board_path:
|
|
75
|
-
work_dir:
|
|
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:
|
|
80
|
-
) ->
|
|
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(
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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:
|
|
152
|
-
definitions:
|
|
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:
|
|
171
|
-
base_dir:
|
|
172
|
-
) ->
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
240
|
-
self.
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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.
|
|
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
|
-
|
|
286
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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(
|
|
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:
|
|
387
|
-
manager:
|
|
448
|
+
definition: "WorkspaceDefinition"
|
|
449
|
+
manager: "WorkspaceSessionManager"
|
|
388
450
|
|
|
389
|
-
def to_dict(self) ->
|
|
451
|
+
def to_dict(self) -> "typing.Dict[str, object]":
|
|
390
452
|
return {
|
|
391
453
|
"id": self.definition.workspace_id,
|
|
392
454
|
"board_path": (
|
|
393
|
-
""
|
|
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:
|
|
411
|
-
config_path:
|
|
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:
|
|
415
|
-
self._name_to_key:
|
|
416
|
-
self._order:
|
|
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:
|
|
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:
|
|
504
|
+
def _key_for_definition(self, definition: "WorkspaceDefinition") -> str:
|
|
442
505
|
if definition.board_path is None:
|
|
443
|
-
|
|
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) ->
|
|
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
|
-
) ->
|
|
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 =
|
|
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
|
-
|
|
480
|
-
if
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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) ->
|
|
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) ->
|
|
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())
|