Auto-select workspace from Telegram prompt

This commit is contained in:
2026-06-10 02:31:46 +09:00
parent 4f0ae2cc88
commit 3ca8095be4
2 changed files with 50 additions and 1 deletions

View File

@@ -43,6 +43,11 @@ On Synology/NAS Docker deployments, keep `CODEX_WRITE_SANDBOX=danger-full-access
for `/work`. The stricter `workspace-write` sandbox may fail with bubblewrap for `/work`. The stricter `workspace-write` sandbox may fail with bubblewrap
namespace errors inside the container. namespace errors inside the container.
If no `/repo` is set, the bridge tries to auto-select a workspace folder when a
Telegram prompt mentions an exact folder name such as `card_game` or
`nas_connection`. Use `/repo <name>` when you want to pin the workspace
explicitly.
`/settings`, `/models`, `/schedules`, `/records`, and `/sessions` return inline `/settings`, `/models`, `/schedules`, `/records`, and `/sessions` return inline
buttons, so you can change speed/model/reasoning settings, delete schedules, buttons, so you can change speed/model/reasoning settings, delete schedules,
manage personal records, and switch or archive sessions without copying ids. manage personal records, and switch or archive sessions without copying ids.

View File

@@ -945,6 +945,9 @@ class TelegramCodexBot:
return return
selected_sandbox = sandbox or self.config.readonly_sandbox selected_sandbox = sandbox or self.config.readonly_sandbox
auto_repo_path = self._auto_select_repo_for_prompt(chat_id, prompt)
if auto_repo_path is not None:
await self._close_client_for_chat(chat_id)
preview = _preview_prompt(prompt) preview = _preview_prompt(prompt)
task = asyncio.create_task( task = asyncio.create_task(
self._run_codex_job( self._run_codex_job(
@@ -961,11 +964,13 @@ class TelegramCodexBot:
sandbox=selected_sandbox, sandbox=selected_sandbox,
) )
mode = "쓰기 작업" if selected_sandbox != self.config.readonly_sandbox else "조회 작업" mode = "쓰기 작업" if selected_sandbox != self.config.readonly_sandbox else "조회 작업"
repo_note = f"\n자동 작업폴더: {auto_repo_path}" if auto_repo_path else ""
await self._reply( await self._reply(
update, update,
f"{mode}을 시작했습니다.\n" f"{mode}을 시작했습니다.\n"
f"요청: {preview}\n" f"요청: {preview}\n"
"완료되면 결과를 보내드릴게요. 진행 중에는 /status로 확인할 수 있습니다.", "완료되면 결과를 보내드릴게요. 진행 중에는 /status로 확인할 수 있습니다."
f"{repo_note}",
) )
async def _run_codex_job( async def _run_codex_job(
@@ -1348,6 +1353,40 @@ class TelegramCodexBot:
return False return False
return sandbox == self.config.readonly_sandbox return sandbox == self.config.readonly_sandbox
def _auto_select_repo_for_prompt(self, chat_id: int, prompt: str) -> Path | None:
chat_state = self.state.get_chat(chat_id)
if chat_state.repo_path:
return None
repo_path = self._detect_workspace_repo(prompt)
if repo_path is None:
return None
self.state.set_repo(chat_id, repo_path)
return repo_path
def _detect_workspace_repo(self, prompt: str) -> Path | None:
root = self.config.workspace_root.resolve()
if not root.exists():
return None
tokens = _repo_name_tokens(prompt)
if not tokens:
return None
children: dict[str, Path] = {}
try:
for child in root.iterdir():
if child.is_dir():
children[child.name.casefold()] = child.resolve()
except OSError:
return None
for token in tokens:
candidate = children.get(token.casefold())
if candidate and (candidate / ".git").exists():
return candidate
for token in tokens:
candidate = children.get(token.casefold())
if candidate:
return candidate
return None
def _resolve_workspace_path(self, requested: str) -> Path: def _resolve_workspace_path(self, requested: str) -> Path:
root = self.config.workspace_root.resolve() root = self.config.workspace_root.resolve()
path = Path(requested) path = Path(requested)
@@ -1388,6 +1427,11 @@ def _preview_prompt(prompt: str, limit: int = 120) -> str:
return preview[: limit - 3] + "..." return preview[: limit - 3] + "..."
def _repo_name_tokens(prompt: str) -> list[str]:
tokens = re.findall(r"[A-Za-z0-9][A-Za-z0-9_.-]{1,79}", prompt)
return _dedupe(tokens)
def _checked_label(label: str, checked: bool) -> str: def _checked_label(label: str, checked: bool) -> str:
return f"{label}" if checked else label return f"{label}" if checked else label