Skip to content

Commit 71d8fb7

Browse files
committed
fix: normalize wizard tab names
1 parent 5e0773f commit 71d8fb7

5 files changed

Lines changed: 70 additions & 14 deletions

File tree

‎src/codex_tabs/cli.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
load_ignored_session_ids,
4444
load_registry,
4545
load_registry_data,
46+
normalize_name,
4647
normalize_tags,
4748
validate_name,
4849
validate_session_id,
@@ -301,6 +302,7 @@ def main(argv: list[str] | None = None) -> int:
301302
"load_registry",
302303
"load_registry_data",
303304
"main",
305+
"normalize_name",
304306
"normalize_tags",
305307
"open_named_sessions",
306308
"parse_saved_tab_selection",

‎src/codex_tabs/registry.py‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
import re
45
from pathlib import Path
56

67
import tomllib
@@ -108,10 +109,21 @@ def escape_toml(value: str) -> str:
108109
return value.replace("\\", "\\\\").replace('"', '\\"')
109110

110111

112+
def normalize_name(name: str) -> str:
113+
normalized = name.strip().lower()
114+
normalized = re.sub(r"\s+", "-", normalized)
115+
normalized = re.sub(r"[^a-z0-9._-]+", "-", normalized)
116+
normalized = re.sub(r"^[^a-z0-9]+", "", normalized)
117+
normalized = re.sub(r"[^a-z0-9]+$", "", normalized)
118+
normalized = re.sub(r"-{2,}", "-", normalized)
119+
return normalized
120+
121+
111122
def validate_name(name: str) -> str:
112-
if not NAME_RE.fullmatch(name):
113-
raise ValueError("session names must match ^[a-z0-9][a-z0-9._-]*$")
114-
return name
123+
normalized = normalize_name(name)
124+
if not NAME_RE.fullmatch(normalized):
125+
raise ValueError("Choose a name that includes at least one letter or number.")
126+
return normalized
115127

116128

117129
def validate_session_id(session_id: str) -> str:

‎src/codex_tabs/wizard.py‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -305,15 +305,17 @@ def process_selected_thread(
305305
print(header_text("Selected session:", stream=output), file=output)
306306
print_thread_details(thread, output=output)
307307
print("", file=output)
308-
name = prompt_input(input_fn, "Name this tab (leave blank to cancel): ", output=output).strip()
309-
if not name:
310-
return
308+
while True:
309+
name = prompt_input(input_fn, "Name this tab (leave blank to cancel): ", output=output).strip()
310+
if not name:
311+
return
311312

312-
try:
313-
validated_name = validate_name(name)
314-
except ValueError as exc:
315-
print(error_text(str(exc), stream=output), file=output)
316-
return
313+
try:
314+
validated_name = validate_name(name)
315+
except ValueError as exc:
316+
print(error_text(str(exc), stream=output), file=output)
317+
continue
318+
break
317319

318320
entries[validated_name] = SessionEntry(
319321
name=validated_name,

‎tests/test_validation.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22

33
import unittest
44

5-
from codex_tabs.cli import normalize_tags, validate_name, validate_session_id
5+
from codex_tabs.cli import normalize_name, normalize_tags, validate_name, validate_session_id
66

77

88
class ValidationTests(unittest.TestCase):
99
def test_validate_name_accepts_expected_format(self) -> None:
1010
self.assertEqual(validate_name("work.main"), "work.main")
1111

12-
def test_validate_name_rejects_invalid_format(self) -> None:
12+
def test_validate_name_normalizes_common_input(self) -> None:
13+
self.assertEqual(validate_name("Work Main"), "work-main")
14+
15+
def test_validate_name_rejects_non_names(self) -> None:
1316
with self.assertRaises(ValueError):
14-
validate_name("Work Main")
17+
validate_name("!!!")
18+
19+
def test_normalize_name_slugifies_friendly_input(self) -> None:
20+
self.assertEqual(normalize_name(" Obsidian Notes "), "obsidian-notes")
1521

1622
def test_validate_session_id_accepts_uuid_like_value(self) -> None:
1723
value = "01234567-89ab-cdef-0123-456789abcdef"

‎tests/test_wizard.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
resolve_single_saved_tab_selection,
1919
run_wizard,
2020
)
21+
from codex_tabs.wizard import process_selected_thread
2122

2223

2324
class WizardTests(unittest.TestCase):
@@ -224,3 +225,36 @@ def test_browse_recent_threads_can_show_more(self) -> None:
224225

225226
self.assertIsNotNone(selected)
226227
self.assertEqual(selected.session_id, threads_twenty[11].session_id)
228+
229+
def test_process_selected_thread_retries_invalid_name(self) -> None:
230+
with tempfile.TemporaryDirectory() as tmp:
231+
config = Path(tmp) / "sessions.toml"
232+
output = io.StringIO()
233+
responses = iter(["!!!", "Obsidian"])
234+
thread = CodexThread(
235+
session_id="01234567-89ab-cdef-0123-456789abcdef",
236+
title="Thread",
237+
cwd="/tmp/work",
238+
created_at=1,
239+
updated_at=2,
240+
first_user_message="first",
241+
last_user_message="last",
242+
last_codex_message="assistant",
243+
)
244+
245+
with patch("codex_tabs.wizard.prompt_yes_no", return_value=False):
246+
entries: dict[str, SessionEntry] = {}
247+
process_selected_thread(
248+
thread,
249+
entries,
250+
config,
251+
input_fn=lambda _prompt: next(responses),
252+
output=output,
253+
)
254+
255+
self.assertIn("obsidian", entries)
256+
self.assertEqual(entries["obsidian"].session_id, thread.session_id)
257+
self.assertIn(
258+
"Choose a name that includes at least one letter or number.",
259+
output.getvalue(),
260+
)

0 commit comments

Comments
 (0)