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
@@ -1,9 +1,27 @@
1
- """Textual widgets for Claude Code UI."""
1
+ """Textual widgets for Claude Code UI.
2
2
 
3
- from claudechic.cursor import ClickableMixin
4
- from claudechic.widgets.button import Button
5
- from claudechic.widgets.indicators import CPUBar, ContextBar
6
- from claudechic.widgets.chat import (
3
+ Re-exports all widgets from submodules for backward compatibility.
4
+ """
5
+
6
+ # Base classes and mixins
7
+ from claudechic.widgets.base import (
8
+ ClickableMixin,
9
+ PointerMixin,
10
+ CopyButton,
11
+ CopyableMixin,
12
+ ToolWidget,
13
+ )
14
+
15
+ # Primitives
16
+ from claudechic.widgets.primitives import (
17
+ Button,
18
+ QuietCollapsible,
19
+ AutoHideScroll,
20
+ Spinner,
21
+ )
22
+
23
+ # Content widgets
24
+ from claudechic.widgets.content import (
7
25
  ChatMessage,
8
26
  ChatInput,
9
27
  ThinkingIndicator,
@@ -11,53 +29,85 @@ from claudechic.widgets.chat import (
11
29
  ErrorMessage,
12
30
  SystemInfo,
13
31
  ChatAttachment,
14
- Spinner,
15
- )
16
- from claudechic.widgets.tools import (
17
32
  ToolUseWidget,
18
33
  TaskWidget,
19
34
  AgentToolWidget,
20
35
  AgentListWidget,
21
36
  ShellOutputWidget,
22
37
  EditPlanRequested,
38
+ DiffWidget,
39
+ TodoWidget,
40
+ TodoPanel,
41
+ )
42
+
43
+ # Input widgets
44
+ from claudechic.widgets.input import TextAreaAutoComplete, HistorySearch
45
+
46
+ # Layout widgets
47
+ from claudechic.widgets.layout import (
48
+ ChatView,
49
+ AgentItem,
50
+ AgentSection,
51
+ WorktreeItem,
52
+ PlanItem,
53
+ PlanSection,
54
+ SidebarSection,
55
+ SidebarItem,
56
+ HamburgerButton,
57
+ SessionItem,
58
+ AutoEditLabel,
59
+ ModelLabel,
60
+ StatusFooter,
61
+ IndicatorWidget,
62
+ CPUBar,
63
+ ContextBar,
64
+ ProcessIndicator,
65
+ ProcessPanel,
66
+ ProcessItem,
23
67
  )
24
- from claudechic.widgets.diff import DiffWidget
25
- from claudechic.widgets.todo import TodoWidget, TodoPanel
68
+
69
+ # Base re-exports (ClickableLabel used by layout widgets)
70
+ from claudechic.widgets.base import ClickableLabel
71
+
72
+ # Data classes (re-exported for convenience)
73
+ from claudechic.processes import BackgroundProcess
74
+
75
+ # Report widgets
76
+ from claudechic.widgets.reports import UsageReport, ContextReport
77
+
78
+ # Modal screens
79
+ from claudechic.widgets.modals import ProfileModal, ProcessModal
80
+
81
+ # Prompts
26
82
  from claudechic.widgets.prompts import (
27
83
  BasePrompt,
28
84
  SelectionPrompt,
29
85
  QuestionPrompt,
30
- SessionItem,
31
- )
32
- from claudechic.widgets.model_prompt import ModelPrompt
33
- from claudechic.widgets.autocomplete import TextAreaAutoComplete
34
- from claudechic.widgets.agents import (
35
- AgentItem,
36
- AgentSidebar,
37
- WorktreeItem,
38
- PlanButton,
39
- HamburgerButton,
86
+ ModelPrompt,
87
+ WorktreePrompt,
88
+ UncommittedChangesPrompt,
40
89
  )
41
- from claudechic.widgets.scroll import AutoHideScroll
42
- from claudechic.widgets.chat_view import ChatView
43
- from claudechic.widgets.collapsible import QuietCollapsible
44
- from claudechic.widgets.history_search import HistorySearch
45
- from claudechic.widgets.usage import UsageReport
46
- from claudechic.widgets.profile_modal import ProfileModal
47
90
 
48
91
  __all__ = [
49
- "Button",
92
+ # Base
50
93
  "ClickableMixin",
51
- "CPUBar",
52
- "ContextBar",
94
+ "PointerMixin",
95
+ "CopyButton",
96
+ "CopyableMixin",
97
+ "ToolWidget",
98
+ # Primitives
99
+ "Button",
100
+ "QuietCollapsible",
101
+ "AutoHideScroll",
102
+ "Spinner",
103
+ # Content
53
104
  "ChatMessage",
54
105
  "ChatInput",
55
- "ChatAttachment",
56
- "Spinner",
57
106
  "ThinkingIndicator",
58
107
  "ImageAttachments",
59
108
  "ErrorMessage",
60
109
  "SystemInfo",
110
+ "ChatAttachment",
61
111
  "ToolUseWidget",
62
112
  "TaskWidget",
63
113
  "AgentToolWidget",
@@ -67,21 +117,42 @@ __all__ = [
67
117
  "DiffWidget",
68
118
  "TodoWidget",
69
119
  "TodoPanel",
70
- "BasePrompt",
71
- "SelectionPrompt",
72
- "QuestionPrompt",
73
- "SessionItem",
120
+ # Input
74
121
  "TextAreaAutoComplete",
122
+ "HistorySearch",
123
+ # Layout
124
+ "ChatView",
75
125
  "AgentItem",
76
- "AgentSidebar",
126
+ "AgentSection",
77
127
  "WorktreeItem",
78
- "PlanButton",
128
+ "SessionItem",
129
+ "PlanItem",
130
+ "PlanSection",
131
+ "SidebarSection",
132
+ "SidebarItem",
79
133
  "HamburgerButton",
80
- "AutoHideScroll",
81
- "ChatView",
82
- "QuietCollapsible",
83
- "HistorySearch",
134
+ "ClickableLabel",
135
+ "AutoEditLabel",
136
+ "ModelLabel",
137
+ "StatusFooter",
138
+ "IndicatorWidget",
139
+ "CPUBar",
140
+ "ContextBar",
141
+ "ProcessIndicator",
142
+ "ProcessPanel",
143
+ "ProcessItem",
144
+ "BackgroundProcess",
145
+ # Reports
84
146
  "UsageReport",
147
+ "ContextReport",
148
+ # Modals
85
149
  "ProfileModal",
150
+ "ProcessModal",
151
+ # Prompts
152
+ "BasePrompt",
153
+ "SelectionPrompt",
154
+ "QuestionPrompt",
86
155
  "ModelPrompt",
156
+ "WorktreePrompt",
157
+ "UncommittedChangesPrompt",
87
158
  ]
@@ -0,0 +1,20 @@
1
+ """Base classes and mixins for widgets."""
2
+
3
+ from claudechic.widgets.base.cursor import (
4
+ ClickableMixin,
5
+ PointerMixin,
6
+ set_pointer,
7
+ )
8
+ from claudechic.widgets.base.copyable import CopyButton, CopyableMixin
9
+ from claudechic.widgets.base.clickable import ClickableLabel
10
+ from claudechic.widgets.base.tool_protocol import ToolWidget
11
+
12
+ __all__ = [
13
+ "ClickableMixin",
14
+ "PointerMixin",
15
+ "set_pointer",
16
+ "CopyButton",
17
+ "CopyableMixin",
18
+ "ClickableLabel",
19
+ "ToolWidget",
20
+ ]
@@ -0,0 +1,23 @@
1
+ """Clickable label widget - static text with pointer cursor."""
2
+
3
+ from textual.widgets import Static
4
+
5
+ from claudechic.widgets.base.cursor import ClickableMixin
6
+
7
+
8
+ class ClickableLabel(Static, ClickableMixin):
9
+ """A static label that is clickable with pointer cursor.
10
+
11
+ Base class for labels that respond to clicks. Override on_click()
12
+ to handle clicks, or post custom messages.
13
+
14
+ Example:
15
+ class MyLabel(ClickableLabel):
16
+ class Clicked(Message):
17
+ pass
18
+
19
+ def on_click(self, event) -> None:
20
+ self.post_message(self.Clicked())
21
+ """
22
+
23
+ pass
@@ -0,0 +1,55 @@
1
+ """Copyable mixin for widgets with copy-to-clipboard functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from claudechic.widgets.primitives.button import Button
8
+
9
+ if TYPE_CHECKING:
10
+ from textual.app import App
11
+
12
+
13
+ class CopyButton(Button):
14
+ """Copy button with hand cursor on hover."""
15
+
16
+ pass
17
+
18
+
19
+ class CopyableMixin:
20
+ """Mixin for widgets that support copying content to clipboard.
21
+
22
+ Usage:
23
+ class MyWidget(Static, CopyableMixin):
24
+ def compose(self):
25
+ yield CopyButton("⧉", classes="copy-btn")
26
+ ...
27
+
28
+ def get_copyable_content(self) -> str:
29
+ return "content to copy"
30
+
31
+ def on_button_pressed(self, event):
32
+ if self.handle_copy_button(event):
33
+ return
34
+ """
35
+
36
+ def get_copyable_content(self) -> str:
37
+ """Return content to copy. Override in subclass."""
38
+ raise NotImplementedError("Subclass must implement get_copyable_content()")
39
+
40
+ # Type hint for app - mixin expects to be used with Widget
41
+ app: App
42
+
43
+ def handle_copy_button(self, event: Button.Pressed) -> bool:
44
+ """Handle copy button press. Returns True if handled."""
45
+ if "copy-btn" in event.button.classes:
46
+ event.stop()
47
+ try:
48
+ import pyperclip
49
+
50
+ pyperclip.copy(self.get_copyable_content())
51
+ self.app.notify("Copied to clipboard")
52
+ except Exception as e:
53
+ self.app.notify(f"Copy failed: {e}", severity="error")
54
+ return True
55
+ return False
@@ -1,12 +1,14 @@
1
- """Mouse cursor and hover mixins for Textual widgets.
1
+ """Mouse cursor mixins for Textual widgets.
2
2
 
3
- Provides three mixins for different use cases:
4
- - ClickableMixin: Hand cursor + .hovered class (for buttons)
3
+ Provides two mixins:
4
+ - ClickableMixin: Hand cursor on hover (for buttons)
5
5
  - PointerMixin: Configurable cursor style (for text areas)
6
- - HoverableMixin: Just .hovered class (for hover effects without cursor)
7
6
 
8
7
  Uses OSC 22 escape sequences supported by modern terminals
9
8
  (Ghostty, Kitty, WezTerm, foot). Unsupported terminals ignore the sequence.
9
+
10
+ Note: For hover visual effects, use CSS :hover pseudo-class instead of
11
+ adding/removing classes - much more efficient (no DOM style recalc).
10
12
  """
11
13
 
12
14
  import os
@@ -72,28 +74,11 @@ class PointerMixin:
72
74
  super().on_leave() # type: ignore[misc]
73
75
 
74
76
 
75
- class HoverableMixin:
76
- """Mixin for widgets that need a .hovered class for CSS styling.
77
-
78
- Adds/removes 'hovered' class on mouse enter/leave. Useful when
79
- CSS :hover doesn't propagate properly through child widgets.
80
- """
81
-
82
- def on_enter(self) -> None:
83
- if not self.has_class("hovered"): # type: ignore[attr-defined]
84
- self.add_class("hovered") # type: ignore[attr-defined]
85
-
86
- def on_leave(self) -> None:
87
- if self.has_class("hovered"): # type: ignore[attr-defined]
88
- self.remove_class("hovered") # type: ignore[attr-defined]
89
- set_pointer("default")
90
-
91
-
92
77
  class ClickableMixin:
93
- """Mixin for clickable widgets with hand cursor and hover state.
78
+ """Mixin for clickable widgets with hand cursor.
94
79
 
95
- Combines pointer cursor with .hovered class. Use for buttons and
96
- clickable containers.
80
+ Shows pointer cursor on hover. Use CSS :hover for visual styling
81
+ instead of .hovered class (much more efficient).
97
82
 
98
83
  Example:
99
84
  class MyButton(Static, ClickableMixin):
@@ -106,10 +91,6 @@ class ClickableMixin:
106
91
 
107
92
  def on_enter(self) -> None:
108
93
  set_pointer("pointer")
109
- if not self.has_class("hovered"): # type: ignore[attr-defined]
110
- self.add_class("hovered") # type: ignore[attr-defined]
111
94
 
112
95
  def on_leave(self) -> None:
113
96
  set_pointer("default")
114
- if self.has_class("hovered"): # type: ignore[attr-defined]
115
- self.remove_class("hovered") # type: ignore[attr-defined]
@@ -0,0 +1,30 @@
1
+ """Protocol for tool display widgets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
6
+
7
+ if TYPE_CHECKING:
8
+ from claude_agent_sdk import ToolResultBlock
9
+
10
+
11
+ @runtime_checkable
12
+ class ToolWidget(Protocol):
13
+ """Protocol for widgets that display tool use/results.
14
+
15
+ Implemented by:
16
+ - ToolUseWidget: Standard tool display with collapsible details
17
+ - TaskWidget: Nested subagent display
18
+ - AgentToolWidget: MCP chic agent tools (spawn_agent, ask_agent, etc.)
19
+
20
+ This protocol enables ChatView to treat all tool widgets uniformly
21
+ for operations like collapse() and set_result().
22
+ """
23
+
24
+ def collapse(self) -> None:
25
+ """Collapse this widget to save visual space."""
26
+ ...
27
+
28
+ def set_result(self, result: "ToolResultBlock") -> None:
29
+ """Update the widget with the tool's result."""
30
+ ...
@@ -0,0 +1,41 @@
1
+ """Content display widgets - messages, tools, diffs."""
2
+
3
+ from claudechic.widgets.content.message import (
4
+ ChatMessage,
5
+ ChatInput,
6
+ ThinkingIndicator,
7
+ ImageAttachments,
8
+ ErrorMessage,
9
+ SystemInfo,
10
+ ChatAttachment,
11
+ )
12
+ from claudechic.widgets.content.tools import (
13
+ ToolUseWidget,
14
+ TaskWidget,
15
+ AgentToolWidget,
16
+ AgentListWidget,
17
+ ShellOutputWidget,
18
+ EditPlanRequested,
19
+ )
20
+ from claudechic.widgets.content.diff import DiffWidget
21
+ from claudechic.widgets.content.todo import TodoWidget, TodoPanel, TodoItem
22
+
23
+ __all__ = [
24
+ "ChatMessage",
25
+ "ChatInput",
26
+ "ThinkingIndicator",
27
+ "ImageAttachments",
28
+ "ErrorMessage",
29
+ "SystemInfo",
30
+ "ChatAttachment",
31
+ "ToolUseWidget",
32
+ "TaskWidget",
33
+ "AgentToolWidget",
34
+ "AgentListWidget",
35
+ "ShellOutputWidget",
36
+ "EditPlanRequested",
37
+ "DiffWidget",
38
+ "TodoWidget",
39
+ "TodoPanel",
40
+ "TodoItem",
41
+ ]
@@ -5,7 +5,6 @@ import re
5
5
  from functools import lru_cache
6
6
 
7
7
  from pygments.lexers import get_lexer_by_name
8
- from pygments.token import Token
9
8
  from pygments.util import ClassNotFound
10
9
  from textual.content import Content, Span
11
10
  from textual.containers import HorizontalScroll
@@ -15,74 +14,25 @@ from textual.widgets import Static
15
14
  from claudechic.formatting import get_lang_from_path
16
15
 
17
16
 
17
+ # Colors - line backgrounds
18
+ REMOVED_BG = "#301010"
19
+ ADDED_BG = "#103010"
20
+ # Word-level change highlights
21
+ REMOVED_WORD_STYLE = "underline on #501818"
22
+ ADDED_WORD_STYLE = "underline on #185018"
23
+
24
+
18
25
  @lru_cache(maxsize=64)
19
26
  def _get_cached_lexer(language: str):
20
- """Get a cached Pygments lexer by language name.
21
-
22
- Caching lexers avoids the expensive lexer loading/guessing on every highlight call.
23
- The profiler showed ~15% of CPU time was spent in _load_lexers and guess_lexer.
24
- """
27
+ """Cache Pygments lexers to avoid repeated loading (~15% CPU savings)."""
25
28
  try:
26
29
  return get_lexer_by_name(language, stripnl=False, ensurenl=True, tabsize=8)
27
30
  except ClassNotFound:
28
31
  return None
29
32
 
30
33
 
31
- # Colors - line backgrounds (subtle tint)
32
- REMOVED_BG = "#200000"
33
- ADDED_BG = "#002000"
34
- # Word-level change highlights - subtle background + underline
35
- REMOVED_WORD_STYLE = "underline on #330808"
36
- ADDED_WORD_STYLE = "underline on #083308"
37
-
38
-
39
- class DiffHighlightTheme(HighlightTheme):
40
- """Syntax highlighting theme for diffs, aligned with chic theme.
41
-
42
- Uses orange as primary accent, saturated blues for structure,
43
- and avoids red/green that clash with diff backgrounds.
44
- """
45
-
46
- STYLES = {
47
- Token.Comment: "#888888", # Brighter gray for visibility
48
- Token.Error: "#ff6b6b", # Soft red for errors
49
- Token.Generic.Strong: "bold",
50
- Token.Generic.Emph: "italic",
51
- Token.Generic.Error: "#ff6b6b",
52
- Token.Generic.Heading: "#ff9922 underline", # Bright orange
53
- Token.Generic.Subheading: "#ff9922",
54
- Token.Keyword: "#ff9922", # Bright orange for keywords
55
- Token.Keyword.Constant: "#66bbff bold", # Vivid blue
56
- Token.Keyword.Namespace: "#ff9922", # Bright orange
57
- Token.Keyword.Type: "#66bbff bold", # Vivid blue
58
- Token.Literal.Number: "#ffcc66", # Bright gold
59
- Token.Literal.String.Backtick: "#888888", # Gray
60
- Token.Literal.String: "#77ccff", # Bright cyan-blue
61
- Token.Literal.String.Doc: "#77ccff italic",
62
- Token.Literal.String.Double: "#77ccff",
63
- Token.Name: "#dddddd", # Bright base text
64
- Token.Name.Attribute: "#ffcc66", # Bright gold
65
- Token.Name.Builtin: "#66bbff", # Vivid blue
66
- Token.Name.Builtin.Pseudo: "#66bbff italic",
67
- Token.Name.Class: "#ff9922 bold", # Bright orange
68
- Token.Name.Constant: "#66bbff", # Vivid blue
69
- Token.Name.Decorator: "#ff9922 bold", # Bright orange
70
- Token.Name.Function: "#ffcc66", # Bright gold
71
- Token.Name.Function.Magic: "#ffcc66",
72
- Token.Name.Tag: "#ff9922 bold", # Bright orange
73
- Token.Name.Variable: "#88ddff", # Light cyan
74
- Token.Operator: "#dddddd bold", # Bright base
75
- Token.Operator.Word: "#ff9922 bold", # Bright orange
76
- Token.Whitespace: "",
77
- }
78
-
79
-
80
34
  def _highlight_text(text: str, language: str) -> Content:
81
- """Syntax highlight text using cached lexer and DiffHighlightTheme.
82
-
83
- Performance-optimized version of textual.highlight.highlight() that
84
- reuses cached lexer instances instead of creating new ones each time.
85
- """
35
+ """Syntax highlight text using cached lexer and default HighlightTheme."""
86
36
  if not language:
87
37
  return Content(text)
88
38
 
@@ -90,19 +40,15 @@ def _highlight_text(text: str, language: str) -> Content:
90
40
  if lexer is None:
91
41
  return Content(text)
92
42
 
93
- # Normalize line endings
94
43
  text = "\n".join(text.splitlines())
95
-
96
- # Build spans from Pygments tokens
97
44
  token_start = 0
98
45
  spans: list[Span] = []
99
46
 
100
47
  for token_type, token in lexer.get_tokens(text):
101
48
  token_end = token_start + len(token)
102
- # Walk up parent chain to find matching style
103
49
  current_type = token_type
104
50
  while True:
105
- if style := DiffHighlightTheme.STYLES.get(current_type):
51
+ if style := HighlightTheme.STYLES.get(current_type):
106
52
  spans.append(Span(token_start, token_end, style))
107
53
  break
108
54
  if (current_type := current_type.parent) is None: