claudechic 0.2.2__py3-none-any.whl → 0.3.1__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 (59) hide show
  1. claudechic/__init__.py +3 -1
  2. claudechic/__main__.py +12 -1
  3. claudechic/agent.py +60 -19
  4. claudechic/agent_manager.py +8 -2
  5. claudechic/analytics.py +62 -0
  6. claudechic/app.py +267 -158
  7. claudechic/commands.py +120 -6
  8. claudechic/config.py +80 -0
  9. claudechic/features/worktree/commands.py +70 -1
  10. claudechic/help_data.py +200 -0
  11. claudechic/messages.py +0 -17
  12. claudechic/processes.py +120 -0
  13. claudechic/profiling.py +18 -1
  14. claudechic/protocols.py +1 -1
  15. claudechic/remote.py +249 -0
  16. claudechic/sessions.py +60 -50
  17. claudechic/styles.tcss +19 -18
  18. claudechic/widgets/__init__.py +112 -41
  19. claudechic/widgets/base/__init__.py +20 -0
  20. claudechic/widgets/base/clickable.py +23 -0
  21. claudechic/widgets/base/copyable.py +55 -0
  22. claudechic/{cursor.py → widgets/base/cursor.py} +9 -28
  23. claudechic/widgets/base/tool_protocol.py +30 -0
  24. claudechic/widgets/content/__init__.py +41 -0
  25. claudechic/widgets/{diff.py → content/diff.py} +11 -65
  26. claudechic/widgets/{chat.py → content/message.py} +25 -76
  27. claudechic/widgets/{tools.py → content/tools.py} +12 -24
  28. claudechic/widgets/input/__init__.py +9 -0
  29. claudechic/widgets/layout/__init__.py +51 -0
  30. claudechic/widgets/{chat_view.py → layout/chat_view.py} +92 -43
  31. claudechic/widgets/{footer.py → layout/footer.py} +17 -7
  32. claudechic/widgets/{indicators.py → layout/indicators.py} +55 -7
  33. claudechic/widgets/layout/processes.py +68 -0
  34. claudechic/widgets/{agents.py → layout/sidebar.py} +163 -82
  35. claudechic/widgets/modals/__init__.py +9 -0
  36. claudechic/widgets/modals/process_modal.py +121 -0
  37. claudechic/widgets/{profile_modal.py → modals/profile.py} +2 -1
  38. claudechic/widgets/primitives/__init__.py +13 -0
  39. claudechic/widgets/{button.py → primitives/button.py} +1 -1
  40. claudechic/widgets/{collapsible.py → primitives/collapsible.py} +5 -1
  41. claudechic/widgets/{scroll.py → primitives/scroll.py} +2 -0
  42. claudechic/widgets/primitives/spinner.py +57 -0
  43. claudechic/widgets/prompts.py +146 -17
  44. claudechic/widgets/reports/__init__.py +10 -0
  45. claudechic-0.3.1.dist-info/METADATA +88 -0
  46. claudechic-0.3.1.dist-info/RECORD +71 -0
  47. {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/WHEEL +1 -1
  48. claudechic-0.3.1.dist-info/licenses/LICENSE +21 -0
  49. claudechic/features/worktree/prompts.py +0 -101
  50. claudechic/widgets/model_prompt.py +0 -56
  51. claudechic-0.2.2.dist-info/METADATA +0 -58
  52. claudechic-0.2.2.dist-info/RECORD +0 -54
  53. /claudechic/widgets/{todo.py → content/todo.py} +0 -0
  54. /claudechic/widgets/{autocomplete.py → input/autocomplete.py} +0 -0
  55. /claudechic/widgets/{history_search.py → input/history_search.py} +0 -0
  56. /claudechic/widgets/{context_report.py → reports/context.py} +0 -0
  57. /claudechic/widgets/{usage.py → reports/usage.py} +0 -0
  58. {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/entry_points.txt +0 -0
  59. {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,57 @@
1
+ """Animated spinner widget."""
2
+
3
+ from textual.widgets import Static
4
+
5
+ from claudechic.profiling import profile
6
+
7
+
8
+ class Spinner(Static):
9
+ """Animated spinner - all instances share a single timer for efficiency."""
10
+
11
+ FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
12
+ DEFAULT_CSS = """
13
+ Spinner {
14
+ width: 1;
15
+ height: 1;
16
+ color: $text-muted;
17
+ }
18
+ """
19
+
20
+ # Class-level shared state
21
+ _instances: set["Spinner"] = set()
22
+ _frame: int = 0
23
+ _timer = None
24
+
25
+ def __init__(self, text: str = "") -> None:
26
+ self._text = f" {text}" if text else ""
27
+ super().__init__()
28
+
29
+ def render(self) -> str:
30
+ """Return current frame from shared counter."""
31
+ return f"{self.FRAMES[Spinner._frame]}{self._text}"
32
+
33
+ def on_mount(self) -> None:
34
+ Spinner._instances.add(self)
35
+ # Start shared timer if this is the first spinner
36
+ # Use app.set_interval so timer survives widget unmount
37
+ if Spinner._timer is None:
38
+ Spinner._timer = self.app.set_interval(1 / 10, Spinner._tick_all) # 10 FPS
39
+
40
+ def on_unmount(self) -> None:
41
+ Spinner._instances.discard(self)
42
+ # Stop timer if no spinners left
43
+ if not Spinner._instances and Spinner._timer is not None:
44
+ Spinner._timer.stop()
45
+ Spinner._timer = None
46
+
47
+ @staticmethod
48
+ @profile
49
+ def _tick_all() -> None:
50
+ """Advance frame and refresh all spinners.
51
+
52
+ Note: We don't check visibility - refresh() on hidden widgets is cheap,
53
+ and the DOM-walking visibility check was more expensive than the savings.
54
+ """
55
+ Spinner._frame = (Spinner._frame + 1) % len(Spinner.FRAMES)
56
+ for spinner in list(Spinner._instances):
57
+ spinner.refresh(layout=False)
@@ -5,23 +5,7 @@ from collections.abc import Sequence
5
5
  from typing import Any
6
6
 
7
7
  from textual.app import ComposeResult
8
- from textual.widgets import Static, Label, ListItem
9
-
10
- from claudechic.cursor import PointerMixin
11
-
12
-
13
- class SessionItem(ListItem, PointerMixin):
14
- """A session in the sidebar."""
15
-
16
- def __init__(self, session_id: str, preview: str, msg_count: int = 0) -> None:
17
- super().__init__()
18
- self.session_id = session_id
19
- self.preview = preview
20
- self.msg_count = msg_count
21
-
22
- def compose(self) -> ComposeResult:
23
- yield Label(self.preview, classes="session-preview")
24
- yield Label(f"({self.msg_count} msgs)", classes="session-meta")
8
+ from textual.widgets import Static
25
9
 
26
10
 
27
11
  class BasePrompt(Static):
@@ -378,3 +362,148 @@ class QuestionPrompt(BasePrompt):
378
362
  """Wait for all answers. Returns answers dict or empty if cancelled."""
379
363
  await super().wait()
380
364
  return self._result_value if self._result_value else {}
365
+
366
+
367
+ class ModelPrompt(BasePrompt):
368
+ """Prompt for selecting a model from SDK-provided list."""
369
+
370
+ def __init__(self, models: list[dict], current_value: str | None = None) -> None:
371
+ """Create model prompt.
372
+
373
+ Args:
374
+ models: List of model dicts from SDK with 'value', 'displayName', 'description'
375
+ current_value: Currently selected model value (e.g., 'opus', 'sonnet')
376
+ """
377
+ super().__init__()
378
+ self.models = models
379
+ self.current_value = current_value
380
+ # Find current model index for initial selection
381
+ self.selected_idx = 0
382
+ for i, m in enumerate(models):
383
+ if m.get("value") == current_value:
384
+ self.selected_idx = i
385
+ break
386
+
387
+ def compose(self) -> ComposeResult:
388
+ yield Static("Select Model", classes="prompt-title")
389
+ for i, m in enumerate(self.models):
390
+ value = m.get("value", "")
391
+ # Extract short name from description like "Opus 4.5 · ..."
392
+ desc = m.get("description", "")
393
+ name = (
394
+ desc.split("·")[0].strip()
395
+ if "·" in desc
396
+ else m.get("displayName", value)
397
+ )
398
+ current = " *" if value == self.current_value else ""
399
+ classes = "prompt-option"
400
+ if i == self.selected_idx:
401
+ classes += " selected"
402
+ yield Static(f"{i + 1}. {name}{current}", classes=classes, id=f"opt-{i}")
403
+
404
+ def _total_options(self) -> int:
405
+ return len(self.models)
406
+
407
+ def _select_option(self, idx: int) -> None:
408
+ value = self.models[idx].get("value", "")
409
+ self._resolve(value)
410
+
411
+ async def wait(self) -> str | None:
412
+ """Wait for selection. Returns model value or None if cancelled."""
413
+ await super().wait()
414
+ return self._result_value
415
+
416
+
417
+ class WorktreePrompt(BasePrompt):
418
+ """Prompt for selecting or creating worktrees."""
419
+
420
+ def __init__(self, worktrees: list[tuple[str, str]]) -> None:
421
+ """Create worktree prompt.
422
+
423
+ Args:
424
+ worktrees: List of (path, branch) tuples for existing worktrees
425
+ """
426
+ super().__init__()
427
+ self.worktrees = worktrees
428
+
429
+ def compose(self) -> ComposeResult:
430
+ yield Static("Worktrees", classes="prompt-title")
431
+ for i, (path, branch) in enumerate(self.worktrees):
432
+ classes = "prompt-option selected" if i == 0 else "prompt-option"
433
+ yield Static(f"{i + 1}. {branch}", classes=classes, id=f"opt-{i}")
434
+ # "New" option at the end
435
+ new_idx = len(self.worktrees)
436
+ classes = "prompt-option prompt-placeholder"
437
+ if new_idx == 0:
438
+ classes += " selected"
439
+ yield Static(
440
+ f"{new_idx + 1}. {self._text_option_placeholder()}",
441
+ classes=classes,
442
+ id=f"opt-{new_idx}",
443
+ )
444
+
445
+ def _total_options(self) -> int:
446
+ return len(self.worktrees) + 1 # +1 for "New"
447
+
448
+ def _text_option_idx(self) -> int:
449
+ return len(self.worktrees)
450
+
451
+ def _text_option_placeholder(self) -> str:
452
+ return "Enter name..."
453
+
454
+ def _select_option(self, idx: int) -> None:
455
+ if idx < len(self.worktrees):
456
+ path, branch = self.worktrees[idx]
457
+ self._resolve(("switch", path))
458
+ else:
459
+ self._text_buffer = ""
460
+ self._enter_text_mode()
461
+ self._update_text_display()
462
+
463
+ def _submit_text(self, text: str) -> None:
464
+ self._resolve(("new", text))
465
+
466
+ async def wait(self) -> tuple[str, str] | None:
467
+ """Wait for selection. Returns (action, value) or None if cancelled."""
468
+ await super().wait()
469
+ return self._result_value
470
+
471
+
472
+ class UncommittedChangesPrompt(BasePrompt):
473
+ """Prompt for handling uncommitted changes during worktree finish."""
474
+
475
+ def __init__(
476
+ self,
477
+ uncommitted: list[str],
478
+ untracked: list[str],
479
+ ) -> None:
480
+ super().__init__()
481
+ self.uncommitted = uncommitted
482
+ self.untracked = untracked
483
+
484
+ def compose(self) -> ComposeResult:
485
+ yield Static("Uncommitted Changes", classes="prompt-title")
486
+
487
+ # Show summary
488
+ details = []
489
+ if self.uncommitted:
490
+ details.append(f"{len(self.uncommitted)} modified")
491
+ if self.untracked:
492
+ details.append(f"{len(self.untracked)} untracked")
493
+ yield Static(" | ".join(details), classes="prompt-subtitle")
494
+
495
+ yield Static("1. Commit changes", classes="prompt-option selected", id="opt-0")
496
+ yield Static("2. Discard all changes", classes="prompt-option", id="opt-1")
497
+ yield Static("3. Abort finish", classes="prompt-option", id="opt-2")
498
+
499
+ def _total_options(self) -> int:
500
+ return 3
501
+
502
+ def _select_option(self, idx: int) -> None:
503
+ choices = ["commit", "discard", "abort"]
504
+ self._resolve(choices[idx])
505
+
506
+ async def wait(self) -> str | None:
507
+ """Returns 'commit', 'discard', or 'abort'. None if cancelled."""
508
+ await super().wait()
509
+ return self._result_value
@@ -0,0 +1,10 @@
1
+ """Report widgets - in-page display components."""
2
+
3
+ from claudechic.widgets.reports.context import ContextReport
4
+ from claudechic.widgets.reports.usage import UsageReport, UsageBar
5
+
6
+ __all__ = [
7
+ "ContextReport",
8
+ "UsageReport",
9
+ "UsageBar",
10
+ ]
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: claudechic
3
+ Version: 0.3.1
4
+ Summary: Claude Chic - A stylish terminal UI for Claude Code
5
+ Author-email: Matthew Rocklin <mrocklin@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mrocklin/claudechic
8
+ Project-URL: Repository, https://github.com/mrocklin/claudechic
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: aiofiles>=25.1.0
21
+ Requires-Dist: aiohttp>=3.9.0
22
+ Requires-Dist: anthropic>=0.75.0
23
+ Requires-Dist: claude-agent-sdk>=0.1.19
24
+ Requires-Dist: psutil>=5.9.0
25
+ Requires-Dist: pyperclip>=1.11.0
26
+ Requires-Dist: pyyaml>=6.0
27
+ Requires-Dist: textual>=7.1.0
28
+ Requires-Dist: textual-autocomplete>=4.0.6
29
+ Dynamic: license-file
30
+
31
+ # Claude Chic
32
+
33
+ A stylish terminal UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), built with [Textual](https://textual.textualize.io/).
34
+
35
+ ## Start
36
+
37
+ ```bash
38
+ uvx claudechic /welcome
39
+ ```
40
+
41
+ <table>
42
+ <tbody>
43
+ <tr>
44
+ <td><img alt="Claude Chic Image" src="https://github.com/user-attachments/assets/24e083af-a500-43eb-80fb-bd5e0a2d9f4c" /></td>
45
+ <td><img alt="Claude Chic Image" src="https://github.com/user-attachments/assets/323e54dc-f7e4-4c5a-8b83-24df423c3eb8" /></td>
46
+ </tr>
47
+ <tr>
48
+ <td><img alt="Claude Chic Image" src="https://github.com/user-attachments/assets/6b6f52c0-e7ae-491a-b3e6-ff43c38678a8" /></td>
49
+ <td><img alt="Claude Chic Image" src="https://github.com/user-attachments/assets/6f999ada-c18e-413a-b1d2-a22c14fbcedd" /></td>
50
+ </tr>
51
+ </tbody>
52
+ </table>
53
+
54
+ ## Install
55
+
56
+ With `uv`
57
+ ```bash
58
+ uv tool install claudechic
59
+ ```
60
+
61
+ With `pip`
62
+
63
+ ```bash
64
+ pip install claudechic
65
+ ```
66
+
67
+ Requires Claude Code to be logged in (`claude /login`).
68
+
69
+ ## Introduction Video
70
+
71
+ [![Claude Chic Introduction](https://img.youtube.com/vi/2HcORToX5sU/maxresdefault.jpg)](https://www.youtube.com/watch?v=2HcORToX5sU)
72
+
73
+ ## Read More
74
+
75
+ Read more in the **[documentation](https://matthewrocklin.com/claudechic/)** about ...
76
+
77
+ - **[Style](https://matthewrocklin.com/claudechic/style/)** - Colors and layout to focus attention
78
+ - **[Multi-Agent Support](https://matthewrocklin.com/claudechic/agents/)** - Running multiple agents concurrently
79
+ - **[Worktrees](https://matthewrocklin.com/claudechic/agents/#worktrees)** - Isolated branches for parallel development
80
+ - **[Architecture](https://matthewrocklin.com/claudechic/architecture/)** - How Textual + Claude SDK makes experimentation easy
81
+ - [Related Work](https://matthewrocklin.com/claudechic/related/) - For similar and more mature projects
82
+
83
+ Built on the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview)
84
+
85
+ ## Alpha Status
86
+
87
+ This project is young and fresh. Expect bugs.
88
+ [Report issues](https://github.com/mrocklin/claudechic/issues/new).
@@ -0,0 +1,71 @@
1
+ claudechic/__init__.py,sha256=0L0Wms7ULpVbbZDpxgRU-7VOdENJ31i2JMIauzYNdfQ,421
2
+ claudechic/__main__.py,sha256=3gZ2oZhJ5q3KlV3tSqxP_7KAg5cyQx7rpjVcMyTpLNM,1951
3
+ claudechic/agent.py,sha256=qZ8dcoxiOkh4lvu5-a_-C5EyspnB80ZxE9oNf7CjBcc,27383
4
+ claudechic/agent_manager.py,sha256=WPqfACA6dnfqsk3JleQzTXuYWR0ifBHNvqGGeNkxsmY,7101
5
+ claudechic/analytics.py,sha256=a0AaOs2Rm4y3DLd7e-O9rHPcILxd04qEiPtd_P6y7As,2041
6
+ claudechic/app.py,sha256=AqdppkYhMzzRrNlnoRMxUHDVYiL1PbpOK4gxicp9wgQ,78753
7
+ claudechic/commands.py,sha256=kBX2Q1IocRWho5KB0OUNqoJHln_qXmbhZdnf46q6MZo,12843
8
+ claudechic/compact.py,sha256=0Zh-b0e7VpwxQKs7nRgmKLLnMOwx9VwiVQ6x1y5OwQQ,14683
9
+ claudechic/config.py,sha256=rl4BOxClr0TUQb4F_5qFgNDlYTq5hFjmdSasc0xtyPk,2151
10
+ claudechic/enums.py,sha256=CiyD9cfTYDH_IPg1B29E6xP3Xn62uIT6rIc2qhMdbZU,1324
11
+ claudechic/errors.py,sha256=HtJR_UTDLruiUeLGd_XUGAQuKSZ_Qtfo6lFpuOwfPxc,1585
12
+ claudechic/file_index.py,sha256=mXO0ztdHI_4IktHCLucMR8nPhsjzpumKeoQLFrYEeiw,6190
13
+ claudechic/formatting.py,sha256=XK8Y_myWoypjrRslZtWc0duG23jdPsFi0kfrXEB1siw,11293
14
+ claudechic/help_data.py,sha256=zhxmjX1PP8--KJcOPaYqr_i9TwIv6OXGtmtntFEcEQk,6325
15
+ claudechic/history.py,sha256=zcQYHYQve5T0hzs5guvUhK9sDTs21u9F0jVU4kPaVHk,1967
16
+ claudechic/mcp.py,sha256=Puvfp8caT_ToJFra43Te40ofkAlk30QvNkwTA5C3wcU,9288
17
+ claudechic/messages.py,sha256=AoS2bVj7GGJPyteJT2brPbykset8y7o1gBmA2l-BPGY,1990
18
+ claudechic/permissions.py,sha256=-qDJhwDiR_hPnmrqHdWBPN9XopH7ceP2VM4-DwaV2Hc,1486
19
+ claudechic/processes.py,sha256=ynfzJ0WTicTpO8J1doiipxHBs5pNKdVeLAcyQgjJQ6o,3401
20
+ claudechic/profiling.py,sha256=zMnz1hiXnfwLCAmSp97-PR0VnlyMbQDggs0ouUS6nes,3478
21
+ claudechic/protocols.py,sha256=vZKw0cZpCPOfiGkURZU5NGYjMHgIR3DWIT_0jMD0s2s,3312
22
+ claudechic/remote.py,sha256=ZQDLm9VWTLBtsyIcA23LyDjKMTWMc5FsPj0drmWWSjU,8180
23
+ claudechic/sampling.py,sha256=pjW1ovJpVpFFxY2z0vOZ0b0gdg-ebDNOUPknIYHnQHY,7032
24
+ claudechic/sessions.py,sha256=bkyF9-XJI_b2eCJPPAsVFk7xLQbHBQ3rOT-YpLa-Lvg,10236
25
+ claudechic/shell_complete.py,sha256=pKoPTTwycz-_URSBgdOQsxmNwqn6tpT6qV8iTO5iF7U,4147
26
+ claudechic/shell_runner.py,sha256=XEExwdsXLEqFjAmt3GlFJ1mqe2ZVWlnbWMbI3nwnaDY,1779
27
+ claudechic/styles.tcss,sha256=5-W8YxVxRlMWV7sBrYW0JHqqK5RsVsfjGEyV-TOc4SM,11699
28
+ claudechic/theme.py,sha256=5K8lV0jCUIa59Lf6oV5NAOlMoo2R4H5St1bDTqmGRJM,544
29
+ claudechic/usage.py,sha256=Z5sYeb7xJwIG3kptMxQQQmggD1qsGRH9n3rn2mnga38,3576
30
+ claudechic/features/__init__.py,sha256=wC6NXFQ3YJX0wy-H5H_1xzDzL5Sv1cCP0rL62uGHdoI,45
31
+ claudechic/features/worktree/__init__.py,sha256=-e9GnepGU6GfijNMnBPQY1Frw0yCyqkUTwA6vglIvXI,339
32
+ claudechic/features/worktree/commands.py,sha256=_wIk3czel7pJQjGmX3XTHC5GXxldCVNYwGdE6l8ZrII,16517
33
+ claudechic/features/worktree/git.py,sha256=lzAc8lHorB96ioiMfZy2Ce3_c-wHXT7wOSa2bp5V-6E,19357
34
+ claudechic/widgets/__init__.py,sha256=vR1WN06ngMzzDYi6m5UO9gZGeS7RklI7jGgEqt9MRGU,3098
35
+ claudechic/widgets/prompts.py,sha256=z6QfsByzHeAlyzjnjlHHaJScJasHKUvR1H5KN_4lvjo,18231
36
+ claudechic/widgets/base/__init__.py,sha256=nMSAx25UdgnvhIwj767GPJhLEke6i4BAQp_174Rmz7w,494
37
+ claudechic/widgets/base/clickable.py,sha256=d__CLXi0Bn0aj68jZf-dpyC3NteSqhhAzlO0vrYfnuw,612
38
+ claudechic/widgets/base/copyable.py,sha256=KQnT8pFkLQJrd2tAaJgtjRkFe2arJFMZ3rKXnqU7Ww0,1610
39
+ claudechic/widgets/base/cursor.py,sha256=uy2K0Jb-UqIyPbz4xsTMoU5fFmXZxMnDfmDg00BK7-U,2560
40
+ claudechic/widgets/base/tool_protocol.py,sha256=-0tysGg09qWNUSwhmDqgLU33n8FtHBW5OAXAiOX4cZ0,887
41
+ claudechic/widgets/content/__init__.py,sha256=56Yq6jPyC_YMwNBIbuMXVfAE4mVqHQq3z7RbFXNsZlc,906
42
+ claudechic/widgets/content/diff.py,sha256=gHnAbvpxDM2xlMS_4wx_W46Eu48wHU7XhBWQObEijFY,13044
43
+ claudechic/widgets/content/message.py,sha256=yJnNDRYwf4kXTw9E013QG0UiWrSUGhhYlslt5CV-BnE,17099
44
+ claudechic/widgets/content/todo.py,sha256=ngqZNwfo3wTG9ar2oGfBLs6Ks0JlYuytJBPZAcuq46I,2687
45
+ claudechic/widgets/content/tools.py,sha256=6HCtiWgccyVRlcSd5KskSHXw04yXM0ykRdoVQjW6IiY,22166
46
+ claudechic/widgets/input/__init__.py,sha256=gVqDFNV0QxKtdRwJq8N1JdR38lXLWPoUdBGuxPKxLDU,258
47
+ claudechic/widgets/input/autocomplete.py,sha256=x-ek_EB9ShNhtlK4sZLUBOHIztI-CHRrCoDNjJO68Ec,22363
48
+ claudechic/widgets/input/history_search.py,sha256=WyNERLIMaqwEyJzA9-Jp7BbXL5qEtwby_d9S4OnpsK4,5799
49
+ claudechic/widgets/layout/__init__.py,sha256=9Dko5_ji1vVuW-ov4JAp4_bDKm73oMLSvAFoS5EtBPM,1012
50
+ claudechic/widgets/layout/chat_view.py,sha256=Q1m1ZZHtmbLOSV89t25GoxJrGL8-vYq235QtLg0bwaM,11718
51
+ claudechic/widgets/layout/footer.py,sha256=C_CgtbW01vd5weFksO4cuCorHG0NYcLU0Z3BAW7qk_Y,3782
52
+ claudechic/widgets/layout/indicators.py,sha256=rR6gUgEpR6ue1qgxa5w7bOzEyIQE_zehCRkG_d0nSA0,4481
53
+ claudechic/widgets/layout/processes.py,sha256=uaRtFLtlC84w4XdYJ1ntSLkENUot5wRVKCTfJz3IwIE,1745
54
+ claudechic/widgets/layout/sidebar.py,sha256=CydtnG_tdZ2yWRsg9s2uz3SHNtakAP-Iqwc694R21ao,12056
55
+ claudechic/widgets/modals/__init__.py,sha256=rZmjiYuNjoG_VLxsTpCm_9CiRXD6IbgiUn1cXXPtFbs,208
56
+ claudechic/widgets/modals/process_modal.py,sha256=Y46bHLEl03_6TnnChjS46dtpr9VfRmJREaVyGjNeIYQ,3190
57
+ claudechic/widgets/modals/profile.py,sha256=I2-6WsTWvxlipg09o2nCWqRNo-lZLKHhav0T4lBu2Dw,5519
58
+ claudechic/widgets/primitives/__init__.py,sha256=2WyGPCLYQoKY117upc0Sb8JeqBAz6uvzipcHLk6Is2Q,380
59
+ claudechic/widgets/primitives/button.py,sha256=-1r1G_8eSlBxWUC21qAZ1NL12uTw52IRsOx_InpA6Yg,763
60
+ claudechic/widgets/primitives/collapsible.py,sha256=xYBtkJ8q8vGOkkxPsnZ6eHRK8gDCl_YPyK5prEhtMGI,1047
61
+ claudechic/widgets/primitives/scroll.py,sha256=7PZ9rmQpeg922OwGTcC1ai4k9sX0kj00ZfwOaOY2GWk,2805
62
+ claudechic/widgets/primitives/spinner.py,sha256=pC0X2C0zaIFku_OxHl0gKsEDdHqjZ94qe6dcTRYVmOM,1767
63
+ claudechic/widgets/reports/__init__.py,sha256=I7gW0NdavZ2yoY3eOz07L5Ig0SrweuW-WYSZOCJWyig,251
64
+ claudechic/widgets/reports/context.py,sha256=W-jPhCgMAis6CBYZNS_YbSPklAFM5wY1WRH7yfD1prM,10378
65
+ claudechic/widgets/reports/usage.py,sha256=l4-DHu-IYa5U35Va-0XS67vw0RMT2aN0jqtwNtpM6Gk,3206
66
+ claudechic-0.3.1.dist-info/licenses/LICENSE,sha256=G0LUJ5X9dFgxW3PoKs0YnMLSG9ho31Fq5nLbRDr6iEA,1072
67
+ claudechic-0.3.1.dist-info/METADATA,sha256=W_e1bZgE1BJ0RIVkX_m_8G79tmckwPHSxFfsrhy5IB4,3113
68
+ claudechic-0.3.1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
69
+ claudechic-0.3.1.dist-info/entry_points.txt,sha256=Sc5fC-THixL9JITMX0l6vZL3_w8ca2GlDz0vfwaxKO4,56
70
+ claudechic-0.3.1.dist-info/top_level.txt,sha256=lZKKh50h8hvU4mD1GHz-5QS3rfycrxBTu1iEpC3WUqM,11
71
+ claudechic-0.3.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Rocklin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,101 +0,0 @@
1
- """Worktree selection prompt."""
2
-
3
- from textual.app import ComposeResult
4
- from textual.widgets import Static
5
-
6
- from claudechic.widgets.prompts import BasePrompt
7
-
8
-
9
- class WorktreePrompt(BasePrompt):
10
- """Prompt for selecting or creating worktrees."""
11
-
12
- def __init__(self, worktrees: list[tuple[str, str]]) -> None:
13
- """Create worktree prompt.
14
-
15
- Args:
16
- worktrees: List of (path, branch) tuples for existing worktrees
17
- """
18
- super().__init__()
19
- self.worktrees = worktrees
20
-
21
- def compose(self) -> ComposeResult:
22
- yield Static("Worktrees", classes="prompt-title")
23
- for i, (path, branch) in enumerate(self.worktrees):
24
- classes = "prompt-option selected" if i == 0 else "prompt-option"
25
- yield Static(f"{i + 1}. {branch}", classes=classes, id=f"opt-{i}")
26
- # "New" option at the end
27
- new_idx = len(self.worktrees)
28
- classes = "prompt-option prompt-placeholder"
29
- if new_idx == 0:
30
- classes += " selected"
31
- yield Static(
32
- f"{new_idx + 1}. {self._text_option_placeholder()}",
33
- classes=classes,
34
- id=f"opt-{new_idx}",
35
- )
36
-
37
- def _total_options(self) -> int:
38
- return len(self.worktrees) + 1 # +1 for "New"
39
-
40
- def _text_option_idx(self) -> int:
41
- return len(self.worktrees)
42
-
43
- def _text_option_placeholder(self) -> str:
44
- return "Enter name..."
45
-
46
- def _select_option(self, idx: int) -> None:
47
- if idx < len(self.worktrees):
48
- path, branch = self.worktrees[idx]
49
- self._resolve(("switch", path))
50
- else:
51
- self._text_buffer = ""
52
- self._enter_text_mode()
53
- self._update_text_display()
54
-
55
- def _submit_text(self, text: str) -> None:
56
- self._resolve(("new", text))
57
-
58
- async def wait(self) -> tuple[str, str] | None:
59
- """Wait for selection. Returns (action, value) or None if cancelled."""
60
- await super().wait()
61
- return self._result_value
62
-
63
-
64
- class UncommittedChangesPrompt(BasePrompt):
65
- """Prompt for handling uncommitted changes during worktree finish."""
66
-
67
- def __init__(
68
- self,
69
- uncommitted: list[str],
70
- untracked: list[str],
71
- ) -> None:
72
- super().__init__()
73
- self.uncommitted = uncommitted
74
- self.untracked = untracked
75
-
76
- def compose(self) -> ComposeResult:
77
- yield Static("Uncommitted Changes", classes="prompt-title")
78
-
79
- # Show summary
80
- details = []
81
- if self.uncommitted:
82
- details.append(f"{len(self.uncommitted)} modified")
83
- if self.untracked:
84
- details.append(f"{len(self.untracked)} untracked")
85
- yield Static(" | ".join(details), classes="prompt-subtitle")
86
-
87
- yield Static("1. Commit changes", classes="prompt-option selected", id="opt-0")
88
- yield Static("2. Discard all changes", classes="prompt-option", id="opt-1")
89
- yield Static("3. Abort finish", classes="prompt-option", id="opt-2")
90
-
91
- def _total_options(self) -> int:
92
- return 3
93
-
94
- def _select_option(self, idx: int) -> None:
95
- choices = ["commit", "discard", "abort"]
96
- self._resolve(choices[idx])
97
-
98
- async def wait(self) -> str | None:
99
- """Returns 'commit', 'discard', or 'abort'. None if cancelled."""
100
- await super().wait()
101
- return self._result_value
@@ -1,56 +0,0 @@
1
- """Model selection prompt."""
2
-
3
- from textual.app import ComposeResult
4
- from textual.widgets import Static
5
-
6
- from claudechic.widgets.prompts import BasePrompt
7
-
8
-
9
- class ModelPrompt(BasePrompt):
10
- """Prompt for selecting a model from SDK-provided list."""
11
-
12
- def __init__(self, models: list[dict], current_value: str | None = None) -> None:
13
- """Create model prompt.
14
-
15
- Args:
16
- models: List of model dicts from SDK with 'value', 'displayName', 'description'
17
- current_value: Currently selected model value (e.g., 'opus', 'sonnet')
18
- """
19
- super().__init__()
20
- self.models = models
21
- self.current_value = current_value
22
- # Find current model index for initial selection
23
- self.selected_idx = 0
24
- for i, m in enumerate(models):
25
- if m.get("value") == current_value:
26
- self.selected_idx = i
27
- break
28
-
29
- def compose(self) -> ComposeResult:
30
- yield Static("Select Model", classes="prompt-title")
31
- for i, m in enumerate(self.models):
32
- value = m.get("value", "")
33
- # Extract short name from description like "Opus 4.5 · ..."
34
- desc = m.get("description", "")
35
- name = (
36
- desc.split("·")[0].strip()
37
- if "·" in desc
38
- else m.get("displayName", value)
39
- )
40
- current = " *" if value == self.current_value else ""
41
- classes = "prompt-option"
42
- if i == self.selected_idx:
43
- classes += " selected"
44
- yield Static(f"{i + 1}. {name}{current}", classes=classes, id=f"opt-{i}")
45
-
46
- def _total_options(self) -> int:
47
- return len(self.models)
48
-
49
- def _select_option(self, idx: int) -> None:
50
- value = self.models[idx].get("value", "")
51
- self._resolve(value)
52
-
53
- async def wait(self) -> str | None:
54
- """Wait for selection. Returns model value or None if cancelled."""
55
- await super().wait()
56
- return self._result_value
@@ -1,58 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: claudechic
3
- Version: 0.2.2
4
- Summary: Claude Chic - A stylish terminal UI for Claude Code
5
- Author-email: Matthew Rocklin <mrocklin@gmail.com>
6
- Project-URL: Homepage, https://github.com/mrocklin/claudechic
7
- Project-URL: Repository, https://github.com/mrocklin/claudechic
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Environment :: Console
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Requires-Python: >=3.10
17
- Description-Content-Type: text/markdown
18
- Requires-Dist: aiofiles>=25.1.0
19
- Requires-Dist: anthropic>=0.75.0
20
- Requires-Dist: claude-agent-sdk>=0.1.19
21
- Requires-Dist: psutil>=5.9.0
22
- Requires-Dist: pyperclip>=1.11.0
23
- Requires-Dist: textual>=7.1.0
24
- Requires-Dist: textual-autocomplete>=4.0.6
25
-
26
- # Claude Chic
27
-
28
- A stylish terminal UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), built with [Textual](https://textual.textualize.io/).
29
-
30
- ## Start
31
-
32
- ```bash
33
- uvx claudechic /welcome
34
- ```
35
-
36
- ## Install
37
-
38
- With `uv`
39
- ```bash
40
- uv tool install claudechic
41
- ```
42
-
43
- With `pip`
44
-
45
- ```bash
46
- pip install claudechic
47
- ```
48
-
49
- Requires Claude Code to be logged in (`claude /login`).
50
-
51
- ## Features
52
-
53
- - Styled version of the `claude` CLI
54
- - Run multiple agents concurrently
55
- - Manage Git Worktrees
56
- - Hackable in Python with Textual
57
-
58
- Built on the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview)