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.
- claudechic/__init__.py +3 -1
- claudechic/__main__.py +12 -1
- claudechic/agent.py +60 -19
- claudechic/agent_manager.py +8 -2
- claudechic/analytics.py +62 -0
- claudechic/app.py +267 -158
- claudechic/commands.py +120 -6
- claudechic/config.py +80 -0
- claudechic/features/worktree/commands.py +70 -1
- claudechic/help_data.py +200 -0
- claudechic/messages.py +0 -17
- claudechic/processes.py +120 -0
- claudechic/profiling.py +18 -1
- claudechic/protocols.py +1 -1
- claudechic/remote.py +249 -0
- claudechic/sessions.py +60 -50
- claudechic/styles.tcss +19 -18
- claudechic/widgets/__init__.py +112 -41
- claudechic/widgets/base/__init__.py +20 -0
- claudechic/widgets/base/clickable.py +23 -0
- claudechic/widgets/base/copyable.py +55 -0
- claudechic/{cursor.py → widgets/base/cursor.py} +9 -28
- claudechic/widgets/base/tool_protocol.py +30 -0
- claudechic/widgets/content/__init__.py +41 -0
- claudechic/widgets/{diff.py → content/diff.py} +11 -65
- claudechic/widgets/{chat.py → content/message.py} +25 -76
- claudechic/widgets/{tools.py → content/tools.py} +12 -24
- claudechic/widgets/input/__init__.py +9 -0
- claudechic/widgets/layout/__init__.py +51 -0
- claudechic/widgets/{chat_view.py → layout/chat_view.py} +92 -43
- claudechic/widgets/{footer.py → layout/footer.py} +17 -7
- claudechic/widgets/{indicators.py → layout/indicators.py} +55 -7
- claudechic/widgets/layout/processes.py +68 -0
- claudechic/widgets/{agents.py → layout/sidebar.py} +163 -82
- claudechic/widgets/modals/__init__.py +9 -0
- claudechic/widgets/modals/process_modal.py +121 -0
- claudechic/widgets/{profile_modal.py → modals/profile.py} +2 -1
- claudechic/widgets/primitives/__init__.py +13 -0
- claudechic/widgets/{button.py → primitives/button.py} +1 -1
- claudechic/widgets/{collapsible.py → primitives/collapsible.py} +5 -1
- claudechic/widgets/{scroll.py → primitives/scroll.py} +2 -0
- claudechic/widgets/primitives/spinner.py +57 -0
- claudechic/widgets/prompts.py +146 -17
- claudechic/widgets/reports/__init__.py +10 -0
- claudechic-0.3.1.dist-info/METADATA +88 -0
- claudechic-0.3.1.dist-info/RECORD +71 -0
- {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/WHEEL +1 -1
- claudechic-0.3.1.dist-info/licenses/LICENSE +21 -0
- claudechic/features/worktree/prompts.py +0 -101
- claudechic/widgets/model_prompt.py +0 -56
- claudechic-0.2.2.dist-info/METADATA +0 -58
- claudechic-0.2.2.dist-info/RECORD +0 -54
- /claudechic/widgets/{todo.py → content/todo.py} +0 -0
- /claudechic/widgets/{autocomplete.py → input/autocomplete.py} +0 -0
- /claudechic/widgets/{history_search.py → input/history_search.py} +0 -0
- /claudechic/widgets/{context_report.py → reports/context.py} +0 -0
- /claudechic/widgets/{usage.py → reports/usage.py} +0 -0
- {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/entry_points.txt +0 -0
- {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)
|
claudechic/widgets/prompts.py
CHANGED
|
@@ -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
|
|
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,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
|
+
[](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,,
|
|
@@ -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)
|