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
claudechic/__init__.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Claude Chic - A stylish terminal UI for Claude Code."""
|
|
2
2
|
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
3
5
|
from claudechic.app import ChatApp
|
|
4
6
|
from claudechic.theme import CHIC_THEME
|
|
5
7
|
from claudechic.protocols import AgentManagerObserver, AgentObserver, PermissionHandler
|
|
@@ -11,4 +13,4 @@ __all__ = [
|
|
|
11
13
|
"AgentObserver",
|
|
12
14
|
"PermissionHandler",
|
|
13
15
|
]
|
|
14
|
-
__version__ = "
|
|
16
|
+
__version__ = version("claudechic")
|
claudechic/__main__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Entry point for claudechic CLI."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import os
|
|
4
5
|
from importlib.metadata import version
|
|
5
6
|
|
|
6
7
|
from claudechic.app import ChatApp
|
|
@@ -24,6 +25,12 @@ def main():
|
|
|
24
25
|
parser.add_argument(
|
|
25
26
|
"--session", "-s", type=str, help="Resume a specific session ID"
|
|
26
27
|
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--remote-port",
|
|
30
|
+
type=int,
|
|
31
|
+
default=int(os.environ.get("CLAUDECHIC_REMOTE_PORT", "0")),
|
|
32
|
+
help="Start HTTP server for remote control on this port",
|
|
33
|
+
)
|
|
27
34
|
parser.add_argument("prompt", nargs="*", help="Initial prompt to send")
|
|
28
35
|
args = parser.parse_args()
|
|
29
36
|
|
|
@@ -42,7 +49,11 @@ def main():
|
|
|
42
49
|
Console().control(Control.title(f"Claude Chic · {Path.cwd().name}"))
|
|
43
50
|
|
|
44
51
|
try:
|
|
45
|
-
app = ChatApp(
|
|
52
|
+
app = ChatApp(
|
|
53
|
+
resume_session_id=resume_id,
|
|
54
|
+
initial_prompt=initial_prompt,
|
|
55
|
+
remote_port=args.remote_port,
|
|
56
|
+
)
|
|
46
57
|
app.run()
|
|
47
58
|
except (KeyboardInterrupt, SystemExit):
|
|
48
59
|
pass
|
claudechic/agent.py
CHANGED
|
@@ -75,12 +75,22 @@ class ToolUse:
|
|
|
75
75
|
is_error: bool = False
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
@dataclass
|
|
79
|
+
class TextBlock:
|
|
80
|
+
"""A text block within an assistant turn."""
|
|
81
|
+
|
|
82
|
+
text: str
|
|
83
|
+
|
|
84
|
+
|
|
78
85
|
@dataclass
|
|
79
86
|
class AssistantContent:
|
|
80
|
-
"""An assistant message in chat history.
|
|
87
|
+
"""An assistant message in chat history.
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
Contains an ordered list of blocks (TextBlock or ToolUse) to preserve
|
|
90
|
+
the original interleaving of text and tool uses.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
blocks: list[TextBlock | ToolUse] = field(default_factory=list)
|
|
84
94
|
|
|
85
95
|
|
|
86
96
|
@dataclass
|
|
@@ -159,6 +169,7 @@ class Agent:
|
|
|
159
169
|
self.auto_approve_edits: bool = False
|
|
160
170
|
self.session_allowed_tools: set[str] = set() # Tools allowed for this session
|
|
161
171
|
self._pending_followup: str | None = None # Auto-send after current response
|
|
172
|
+
self.model: str | None = None # Model override (None = SDK default)
|
|
162
173
|
|
|
163
174
|
# Worktree finish state (for /worktree finish flow)
|
|
164
175
|
self.finish_state: Any = None
|
|
@@ -173,6 +184,9 @@ class Agent:
|
|
|
173
184
|
self.observer: AgentObserver | None = None
|
|
174
185
|
self.permission_handler: PermissionHandler | None = None
|
|
175
186
|
|
|
187
|
+
# Background process tracking (PID of claude binary)
|
|
188
|
+
self._claude_pid: int | None = None
|
|
189
|
+
|
|
176
190
|
# -----------------------------------------------------------------------
|
|
177
191
|
# Lifecycle
|
|
178
192
|
# -----------------------------------------------------------------------
|
|
@@ -194,6 +208,11 @@ class Agent:
|
|
|
194
208
|
self.client = ClaudeSDKClient(options)
|
|
195
209
|
await self.client.connect()
|
|
196
210
|
|
|
211
|
+
# Capture the claude process PID for background process tracking
|
|
212
|
+
from claudechic.processes import get_claude_pid_from_client
|
|
213
|
+
|
|
214
|
+
self._claude_pid = get_claude_pid_from_client(self.client)
|
|
215
|
+
|
|
197
216
|
if resume:
|
|
198
217
|
self.session_id = resume
|
|
199
218
|
|
|
@@ -217,7 +236,7 @@ class Agent:
|
|
|
217
236
|
pass
|
|
218
237
|
self.client = None
|
|
219
238
|
|
|
220
|
-
async def load_history(self,
|
|
239
|
+
async def load_history(self, cwd: Path | None = None) -> None:
|
|
221
240
|
"""Load message history from session file into self.messages.
|
|
222
241
|
|
|
223
242
|
This populates Agent.messages from the persisted session,
|
|
@@ -225,7 +244,6 @@ class Agent:
|
|
|
225
244
|
Call ChatView._render_full() after this to update UI.
|
|
226
245
|
|
|
227
246
|
Args:
|
|
228
|
-
limit: Maximum number of messages to load
|
|
229
247
|
cwd: Working directory for session lookup (defaults to self.cwd)
|
|
230
248
|
"""
|
|
231
249
|
from claudechic.sessions import load_session_messages
|
|
@@ -234,9 +252,7 @@ class Agent:
|
|
|
234
252
|
return
|
|
235
253
|
|
|
236
254
|
self.messages.clear()
|
|
237
|
-
raw_messages = await load_session_messages(
|
|
238
|
-
self.session_id, limit=limit, cwd=cwd or self.cwd
|
|
239
|
-
)
|
|
255
|
+
raw_messages = await load_session_messages(self.session_id, cwd=cwd or self.cwd)
|
|
240
256
|
|
|
241
257
|
current_assistant: AssistantContent | None = None
|
|
242
258
|
|
|
@@ -253,17 +269,15 @@ class Agent:
|
|
|
253
269
|
ChatItem(role="user", content=UserContent(text=m["content"]))
|
|
254
270
|
)
|
|
255
271
|
elif m["type"] == "assistant":
|
|
256
|
-
#
|
|
272
|
+
# Add text block to current assistant content (preserving order)
|
|
257
273
|
if current_assistant is None:
|
|
258
|
-
current_assistant = AssistantContent(
|
|
259
|
-
|
|
260
|
-
# Append to existing (shouldn't happen often with current parser)
|
|
261
|
-
current_assistant.text += "\n" + m["content"]
|
|
274
|
+
current_assistant = AssistantContent()
|
|
275
|
+
current_assistant.blocks.append(TextBlock(text=m["content"]))
|
|
262
276
|
elif m["type"] == "tool_use":
|
|
263
|
-
# Add tool use to current assistant content
|
|
277
|
+
# Add tool use to current assistant content (preserving order)
|
|
264
278
|
if current_assistant is None:
|
|
265
279
|
current_assistant = AssistantContent()
|
|
266
|
-
current_assistant.
|
|
280
|
+
current_assistant.blocks.append(
|
|
267
281
|
ToolUse(
|
|
268
282
|
id=m.get("id", ""),
|
|
269
283
|
name=m["name"],
|
|
@@ -475,7 +489,8 @@ class Agent:
|
|
|
475
489
|
)
|
|
476
490
|
|
|
477
491
|
self._current_text_buffer += text
|
|
478
|
-
|
|
492
|
+
# Update the current TextBlock in-place for live streaming display
|
|
493
|
+
self._update_current_text_block()
|
|
479
494
|
if self.observer:
|
|
480
495
|
self.observer.on_message_updated(self)
|
|
481
496
|
self.observer.on_text_chunk(self, text, new_message, parent_tool_use_id)
|
|
@@ -496,10 +511,24 @@ class Agent:
|
|
|
496
511
|
self._needs_new_message = False
|
|
497
512
|
self._handle_text_chunk(text, new_msg, parent_id)
|
|
498
513
|
|
|
514
|
+
def _update_current_text_block(self) -> None:
|
|
515
|
+
"""Update the current TextBlock with accumulated text (for streaming)."""
|
|
516
|
+
if not self._current_assistant or not self._current_text_buffer:
|
|
517
|
+
return
|
|
518
|
+
# Find or create the trailing TextBlock
|
|
519
|
+
if self._current_assistant.blocks and isinstance(
|
|
520
|
+
self._current_assistant.blocks[-1], TextBlock
|
|
521
|
+
):
|
|
522
|
+
self._current_assistant.blocks[-1].text = self._current_text_buffer
|
|
523
|
+
else:
|
|
524
|
+
self._current_assistant.blocks.append(
|
|
525
|
+
TextBlock(text=self._current_text_buffer)
|
|
526
|
+
)
|
|
527
|
+
|
|
499
528
|
def _flush_current_text(self) -> None:
|
|
500
|
-
"""Flush accumulated text to current assistant message."""
|
|
529
|
+
"""Flush accumulated text to current assistant message and reset buffer."""
|
|
501
530
|
if self._current_assistant and self._current_text_buffer:
|
|
502
|
-
self.
|
|
531
|
+
self._update_current_text_block()
|
|
503
532
|
self._current_text_buffer = ""
|
|
504
533
|
if self.observer:
|
|
505
534
|
self.observer.on_message_updated(self)
|
|
@@ -544,7 +573,7 @@ class Agent:
|
|
|
544
573
|
self.messages.append(
|
|
545
574
|
ChatItem(role="assistant", content=self._current_assistant)
|
|
546
575
|
)
|
|
547
|
-
self._current_assistant.
|
|
576
|
+
self._current_assistant.blocks.append(tool)
|
|
548
577
|
if self.observer:
|
|
549
578
|
self.observer.on_message_updated(self)
|
|
550
579
|
self.observer.on_tool_use(self, tool)
|
|
@@ -710,3 +739,15 @@ class Agent:
|
|
|
710
739
|
"message": {"role": "user", "content": content},
|
|
711
740
|
"parent_tool_use_id": None,
|
|
712
741
|
}
|
|
742
|
+
|
|
743
|
+
def get_background_processes(self) -> list:
|
|
744
|
+
"""Get list of background processes for this agent.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
List of BackgroundProcess objects
|
|
748
|
+
"""
|
|
749
|
+
if not self._claude_pid:
|
|
750
|
+
return []
|
|
751
|
+
from claudechic.processes import get_child_processes
|
|
752
|
+
|
|
753
|
+
return get_child_processes(self._claude_pid)
|
claudechic/agent_manager.py
CHANGED
|
@@ -115,6 +115,7 @@ class AgentManager:
|
|
|
115
115
|
worktree: str | None = None,
|
|
116
116
|
resume: str | None = None,
|
|
117
117
|
switch_to: bool = True,
|
|
118
|
+
model: str | None = None,
|
|
118
119
|
) -> Agent:
|
|
119
120
|
"""Create and connect a new agent.
|
|
120
121
|
|
|
@@ -124,17 +125,21 @@ class AgentManager:
|
|
|
124
125
|
worktree: Git worktree branch name if applicable
|
|
125
126
|
resume: Session ID to resume
|
|
126
127
|
switch_to: Whether to make this the active agent
|
|
128
|
+
model: Model override (None = SDK default)
|
|
127
129
|
|
|
128
130
|
Returns:
|
|
129
131
|
The created agent (connected and ready)
|
|
130
132
|
"""
|
|
131
133
|
agent = Agent(name=name, cwd=cwd, worktree=worktree)
|
|
134
|
+
agent.model = model
|
|
132
135
|
|
|
133
136
|
# Wire callbacks
|
|
134
137
|
self._wire_agent_callbacks(agent)
|
|
135
138
|
|
|
136
139
|
# Create options and connect
|
|
137
|
-
options = self._options_factory(
|
|
140
|
+
options = self._options_factory(
|
|
141
|
+
cwd=cwd, resume=resume, agent_name=agent.name, model=model
|
|
142
|
+
)
|
|
138
143
|
await agent.connect(options, resume=resume)
|
|
139
144
|
|
|
140
145
|
# Register agent
|
|
@@ -192,13 +197,14 @@ class AgentManager:
|
|
|
192
197
|
|
|
193
198
|
name = agent.name
|
|
194
199
|
was_active = agent_id == self.active_id
|
|
200
|
+
message_count = len(agent.messages)
|
|
195
201
|
|
|
196
202
|
# Disconnect
|
|
197
203
|
await agent.disconnect()
|
|
198
204
|
log.info(f"Closed agent '{name}' (id={agent_id})")
|
|
199
205
|
|
|
200
206
|
if self.manager_observer:
|
|
201
|
-
self.manager_observer.on_agent_closed(agent_id)
|
|
207
|
+
self.manager_observer.on_agent_closed(agent_id, message_count)
|
|
202
208
|
|
|
203
209
|
# Switch to another agent if we closed the active one
|
|
204
210
|
if was_active and self.agents:
|
claudechic/analytics.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""PostHog analytics for claudechic - fire-and-forget event tracking."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import uuid as uuid_mod
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from claudechic.config import get_analytics_enabled, get_analytics_id
|
|
12
|
+
|
|
13
|
+
VERSION = "0.1.0" # Keep in sync with __init__.py
|
|
14
|
+
SESSION_ID = str(uuid_mod.uuid4()) # Unique per process
|
|
15
|
+
|
|
16
|
+
POSTHOG_HOST = "https://us.i.posthog.com"
|
|
17
|
+
POSTHOG_API_KEY = "phc_M0LMkbSaDsaXi5LeYE5A95Kz8hTHgsJ4POlqucehsse"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def capture(event: str, **properties: str | int | float | bool) -> None:
|
|
21
|
+
"""Capture an analytics event to PostHog.
|
|
22
|
+
|
|
23
|
+
Fire-and-forget: failures are silently ignored.
|
|
24
|
+
Respects analytics opt-out setting.
|
|
25
|
+
"""
|
|
26
|
+
if not get_analytics_enabled():
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
# Build properties - session_id on all events, context only on app_started
|
|
30
|
+
props: dict = {"$session_id": SESSION_ID, **properties}
|
|
31
|
+
|
|
32
|
+
if event in ("app_started", "app_installed"):
|
|
33
|
+
# Include version and environment context on session start and install
|
|
34
|
+
props["claudechic_version"] = VERSION
|
|
35
|
+
try:
|
|
36
|
+
term_size = os.get_terminal_size()
|
|
37
|
+
props["term_width"] = term_size.columns
|
|
38
|
+
props["term_height"] = term_size.lines
|
|
39
|
+
except OSError:
|
|
40
|
+
pass
|
|
41
|
+
props["term_program"] = os.environ.get("TERM_PROGRAM", "unknown")
|
|
42
|
+
props["os"] = platform.system()
|
|
43
|
+
props["has_uv"] = shutil.which("uv") is not None
|
|
44
|
+
props["has_conda"] = shutil.which("conda") is not None
|
|
45
|
+
|
|
46
|
+
payload = {
|
|
47
|
+
"api_key": POSTHOG_API_KEY,
|
|
48
|
+
"event": event,
|
|
49
|
+
"distinct_id": get_analytics_id(),
|
|
50
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
51
|
+
"properties": props,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
async with httpx.AsyncClient() as client:
|
|
56
|
+
await client.post(
|
|
57
|
+
f"{POSTHOG_HOST}/capture/",
|
|
58
|
+
json=payload,
|
|
59
|
+
timeout=5.0,
|
|
60
|
+
)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass # Silent failure - analytics should never impact user experience
|