claude-sdk-tutor 0.1.6__tar.gz → 0.1.8__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-sdk-tutor
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Add your description here
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.13
@@ -11,7 +11,8 @@ from rich.markdown import Markdown as RichMarkdown
11
11
  from rich.panel import Panel
12
12
  from textual.app import App, ComposeResult
13
13
  from textual.containers import Vertical
14
- from textual.widgets import Static, Footer, Input, RichLog, LoadingIndicator
14
+ from rich.box import ROUNDED
15
+ from textual.widgets import Static, Footer, Input, RichLog
15
16
 
16
17
  from claude.claude_agent import (
17
18
  connect_client,
@@ -21,10 +22,131 @@ from claude.claude_agent import (
21
22
  from claude.history import CommandHistory
22
23
  from claude.mcp_commands import McpAsyncCommand, McpCommandHandler
23
24
  from claude.mcp_config import McpConfigManager
24
- from claude.widgets import HistoryInput
25
+ from claude.widgets import ASCIISpinner, HistoryInput, StatusBar
26
+
27
+
28
+ # Message border colors (vivid)
29
+ USER_COLOR = "#00aaff" # Vivid cyan-blue
30
+ CLAUDE_COLOR = "#ff3333" # Vivid red
31
+ TOOL_COLOR = "#cccccc" # Bright grey
32
+ SYSTEM_COLOR = "#33ff66" # Vivid green
33
+
34
+ HEADER_TEXT = "Claude SDK Tutor"
25
35
 
26
36
 
27
37
  class MyApp(App):
38
+ CSS = """
39
+ /* Color Variables */
40
+ $bg-primary: #1a1a1a;
41
+ $bg-secondary: #242424;
42
+ $bg-elevated: #2a2a2a;
43
+ $text-primary: #e0e0e0;
44
+ $text-muted: #888888;
45
+ $accent: #5c9fd4;
46
+ $accent-dim: #4a7fa8;
47
+ $border: #333333;
48
+ $border-focus: #555555;
49
+
50
+ Screen {
51
+ background: $bg-primary;
52
+ }
53
+
54
+ #main {
55
+ height: 100%;
56
+ background: $bg-primary;
57
+ }
58
+
59
+ #header {
60
+ content-align: center middle;
61
+ width: 100%;
62
+ padding: 1 0;
63
+ height: auto;
64
+ color: $text-primary;
65
+ text-style: bold;
66
+ background: $bg-primary;
67
+ }
68
+
69
+ #status-bar {
70
+ content-align: center middle;
71
+ width: 100%;
72
+ height: auto;
73
+ padding-bottom: 1;
74
+ color: $text-muted;
75
+ background: $bg-primary;
76
+ }
77
+
78
+ RichLog {
79
+ background: $bg-secondary;
80
+ margin-left: 2;
81
+ margin-right: 2;
82
+ height: 1fr;
83
+ border: round $border;
84
+ scrollbar-color: $border;
85
+ scrollbar-color-hover: $border-focus;
86
+ scrollbar-color-active: $accent;
87
+ }
88
+
89
+ #spinner {
90
+ height: auto;
91
+ margin-left: 2;
92
+ margin-right: 2;
93
+ color: $text-muted;
94
+ background: $bg-primary;
95
+ }
96
+
97
+ HistoryInput {
98
+ height: auto;
99
+ margin-top: 1;
100
+ margin-left: 2;
101
+ margin-right: 2;
102
+ margin-bottom: 1;
103
+ background: $bg-elevated;
104
+ color: $text-primary;
105
+ border: round $border;
106
+ }
107
+
108
+ HistoryInput:focus {
109
+ border: round $accent-dim;
110
+ }
111
+
112
+ HistoryInput > .input--cursor {
113
+ color: $bg-primary;
114
+ background: $accent;
115
+ }
116
+
117
+ HistoryInput > .input--placeholder {
118
+ color: $text-muted;
119
+ }
120
+
121
+ Footer {
122
+ background: $bg-secondary;
123
+ color: $text-muted;
124
+ }
125
+
126
+ Footer > .footer--key {
127
+ background: $bg-elevated;
128
+ color: $text-primary;
129
+ }
130
+
131
+ Footer > .footer--description {
132
+ color: $text-muted;
133
+ }
134
+
135
+ FooterKey {
136
+ background: $bg-elevated;
137
+ color: $text-primary;
138
+ }
139
+
140
+ FooterKey:hover {
141
+ background: $accent-dim;
142
+ color: $bg-primary;
143
+ }
144
+
145
+ LoadingIndicator {
146
+ display: none;
147
+ }
148
+ """
149
+
28
150
  def __init__(self):
29
151
  super().__init__()
30
152
  self.tutor_mode = True
@@ -44,66 +166,71 @@ class MyApp(App):
44
166
  mcp_servers=self.mcp_config.get_enabled_servers_for_sdk(),
45
167
  )
46
168
 
47
- CSS = """
48
- #main {
49
- height: 100%;
50
- }
51
- Input {
52
- height: auto;
53
- margin-top: 1;
54
- margin-left: 3;
55
- margin-right: 3;
56
- margin-bottom: 1;
57
- }
58
- #header {
59
- content-align: center middle;
60
- width: 100%;
61
- margin-top: 1;
62
- margin-bottom: 1;
63
- height: auto;
64
- }
65
- RichLog {
66
- background: $boost;
67
- margin-left: 3;
68
- margin-right: 3;
69
- height: 1fr;
70
- }
71
- LoadingIndicator {
72
- height: auto;
73
- margin-left: 3;
74
- margin-right: 3;
75
- }
76
- """
77
-
78
169
  def compose(self) -> ComposeResult:
79
170
  with Vertical(id="main"):
80
- yield Static("Welcome to claude SDK tutor!", id="header")
171
+ yield Static(HEADER_TEXT, id="header")
172
+ yield StatusBar(id="status-bar")
81
173
  yield RichLog(markup=True, highlight=True)
82
- yield LoadingIndicator(id="spinner")
83
- yield HistoryInput(history=self.history)
174
+ yield ASCIISpinner(id="spinner")
175
+ yield HistoryInput(
176
+ history=self.history,
177
+ placeholder="Type a message or /help for commands...",
178
+ )
84
179
  yield Footer()
85
180
 
86
181
  async def on_mount(self) -> None:
87
- self.query_one("#spinner", LoadingIndicator).display = False
182
+ self.query_one("#spinner", ASCIISpinner).display = False
183
+ self._update_status_bar()
88
184
  await connect_client(self.client)
89
185
 
186
+ def _update_status_bar(self) -> None:
187
+ """Update the status bar with current mode states."""
188
+ status_bar = self.query_one("#status-bar", StatusBar)
189
+ status_bar.tutor_on = self.tutor_mode
190
+ status_bar.web_on = self.web_search_enabled
191
+ status_bar.mcp_count = len(self.mcp_config.get_enabled_servers_for_sdk())
192
+
90
193
  def write_user_message(self, message: str) -> None:
91
194
  log = self.query_one(RichLog)
92
- log.write(Panel(RichMarkdown(message), title="You", border_style="dodger_blue1"))
195
+ log.write(Panel(
196
+ RichMarkdown(message),
197
+ title="You",
198
+ border_style=USER_COLOR,
199
+ box=ROUNDED,
200
+ padding=(1, 2),
201
+ ))
93
202
 
94
203
  def write_system_message(self, message: str) -> None:
95
204
  log = self.query_one(RichLog)
96
- log.write(Panel(RichMarkdown(message), title="Claude", border_style="red"))
205
+ log.write(Panel(
206
+ RichMarkdown(message),
207
+ title="Claude",
208
+ border_style=CLAUDE_COLOR,
209
+ box=ROUNDED,
210
+ padding=(1, 2),
211
+ ))
97
212
 
98
213
  def write_tool_message(self, name: str, input: dict) -> None:
99
214
  log = self.query_one(RichLog)
100
215
  input_str = json.dumps(input, indent=2)
101
216
  content = f"**{name}**\n```json\n{input_str}\n```"
102
- log.write(Panel(RichMarkdown(content), title="Tool", border_style="grey50"))
217
+ log.write(Panel(
218
+ RichMarkdown(content),
219
+ title="Tool",
220
+ border_style=TOOL_COLOR,
221
+ box=ROUNDED,
222
+ padding=(1, 2),
223
+ ))
103
224
 
104
225
  def write_slash_message(self, message: str) -> None:
105
226
  log = self.query_one(RichLog)
106
- log.write(Panel(RichMarkdown(message), title="Slash", border_style="green"))
227
+ log.write(Panel(
228
+ RichMarkdown(message),
229
+ title="System",
230
+ border_style=SYSTEM_COLOR,
231
+ box=ROUNDED,
232
+ padding=(1, 2),
233
+ ))
107
234
 
108
235
  def on_input_submitted(self, event: Input.Submitted) -> None:
109
236
  command = event.value.strip()
@@ -136,7 +263,7 @@ class MyApp(App):
136
263
  self._handle_mcp_command(command)
137
264
  return
138
265
  self.write_user_message(event.value)
139
- self.query_one("#spinner", LoadingIndicator).display = True
266
+ self.query_one("#spinner", ASCIISpinner).start("Processing query...")
140
267
  self._query_running = True
141
268
  self.run_worker(self.get_response(event.value))
142
269
 
@@ -144,6 +271,7 @@ class MyApp(App):
144
271
  self.query_one(RichLog).clear()
145
272
  self.client = self._create_client()
146
273
  await connect_client(self.client)
274
+ self._update_status_bar()
147
275
  self.write_slash_message("Context cleared")
148
276
 
149
277
  async def toggle_tutor_mode(self) -> None:
@@ -151,7 +279,8 @@ class MyApp(App):
151
279
  self.query_one(RichLog).clear()
152
280
  self.client = self._create_client()
153
281
  await connect_client(self.client)
154
- status = "on" if self.tutor_mode else "off"
282
+ self._update_status_bar()
283
+ status = "enabled" if self.tutor_mode else "disabled"
155
284
  self.write_slash_message(f"Tutor mode {status}")
156
285
 
157
286
  async def toggle_web_search(self) -> None:
@@ -159,7 +288,8 @@ class MyApp(App):
159
288
  self.query_one(RichLog).clear()
160
289
  self.client = self._create_client()
161
290
  await connect_client(self.client)
162
- status = "on" if self.web_search_enabled else "off"
291
+ self._update_status_bar()
292
+ status = "enabled" if self.web_search_enabled else "disabled"
163
293
  self.write_slash_message(f"Web search {status}")
164
294
 
165
295
  def show_help(self) -> None:
@@ -183,7 +313,7 @@ class MyApp(App):
183
313
  )
184
314
  elif isinstance(result, McpAsyncCommand):
185
315
  # Async command needs connection testing
186
- self.query_one("#spinner", LoadingIndicator).display = True
316
+ self.query_one("#spinner", ASCIISpinner).start("Testing MCP connections...")
187
317
  self.run_worker(self._test_mcp_connections(result))
188
318
  else:
189
319
  self.write_slash_message(result)
@@ -236,7 +366,7 @@ class MyApp(App):
236
366
  except Exception as e:
237
367
  self.write_slash_message(f"**Error** testing MCP connections: {e}")
238
368
  finally:
239
- self.query_one("#spinner", LoadingIndicator).display = False
369
+ self.query_one("#spinner", ASCIISpinner).stop()
240
370
 
241
371
  def _handle_mcp_add_step(self, user_input: str) -> None:
242
372
  """Handle a step in the interactive MCP add wizard."""
@@ -330,7 +460,7 @@ class MyApp(App):
330
460
  elif isinstance(message, ResultMessage):
331
461
  pass # Might want to add logging later
332
462
  finally:
333
- self.query_one("#spinner", LoadingIndicator).display = False
463
+ self.query_one("#spinner", ASCIISpinner).stop()
334
464
  self._query_running = False
335
465
 
336
466
  def action_cancel_query(self) -> None:
@@ -347,7 +477,7 @@ class MyApp(App):
347
477
  pass # Ignore errors if not connected or no active query
348
478
  finally:
349
479
  self._query_running = False
350
- self.query_one("#spinner", LoadingIndicator).display = False
480
+ self.query_one("#spinner", ASCIISpinner).stop()
351
481
 
352
482
 
353
483
  def main():
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-sdk-tutor"
3
- version = "0.1.6"
3
+ version = "0.1.8"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -0,0 +1,87 @@
1
+ from textual.binding import Binding
2
+ from textual.reactive import reactive
3
+ from textual.widgets import Input, Static
4
+
5
+ from claude.history import CommandHistory
6
+
7
+
8
+ class StatusBar(Static):
9
+ """Reactive status bar showing tutor/web/mcp states."""
10
+
11
+ tutor_on: reactive[bool] = reactive(True)
12
+ web_on: reactive[bool] = reactive(False)
13
+ mcp_count: reactive[int] = reactive(0)
14
+
15
+ def render(self) -> str:
16
+ tutor = "on" if self.tutor_on else "off"
17
+ web = "on" if self.web_on else "off"
18
+ mcp = f"{self.mcp_count} server{'s' if self.mcp_count != 1 else ''}"
19
+ return f"tutor: {tutor} · web: {web} · mcp: {mcp}"
20
+
21
+
22
+ class ASCIISpinner(Static):
23
+ """Minimal spinner that cycles through frames with a label."""
24
+
25
+ SPINNER_FRAMES = ["· ", "·· ", "···", " ··", " ·", " "]
26
+
27
+ _frame: reactive[int] = reactive(0)
28
+ _label: reactive[str] = reactive("")
29
+ _running: reactive[bool] = reactive(False)
30
+
31
+ def __init__(self, label: str = "Processing...", **kwargs):
32
+ super().__init__(**kwargs)
33
+ self._label = label
34
+ self._timer = None
35
+
36
+ def render(self) -> str:
37
+ if not self._running:
38
+ return ""
39
+ frame = self.SPINNER_FRAMES[self._frame % len(self.SPINNER_FRAMES)]
40
+ return f"{frame} {self._label}"
41
+
42
+ def start(self, label: str = "Processing query...") -> None:
43
+ """Start the spinner animation."""
44
+ self._label = label
45
+ self._running = True
46
+ self._frame = 0
47
+ self.display = True
48
+ if self._timer is None:
49
+ self._timer = self.set_interval(0.1, self._advance_frame)
50
+
51
+ def stop(self) -> None:
52
+ """Stop the spinner animation."""
53
+ self._running = False
54
+ self.display = False
55
+ if self._timer is not None:
56
+ self._timer.stop()
57
+ self._timer = None
58
+
59
+ def _advance_frame(self) -> None:
60
+ """Advance to the next spinner frame."""
61
+ if self._running:
62
+ self._frame = (self._frame + 1) % len(self.SPINNER_FRAMES)
63
+
64
+
65
+ class HistoryInput(Input):
66
+ """Input widget with command history navigation."""
67
+
68
+ BINDINGS = [
69
+ Binding("up", "history_previous", "Previous command", show=False),
70
+ Binding("down", "history_next", "Next command", show=False),
71
+ Binding("escape", "app.cancel_query", "Cancel", show=False),
72
+ Binding("ctrl+c", "app.cancel_query", "Cancel", show=False),
73
+ ]
74
+
75
+ def __init__(self, history: CommandHistory, **kwargs):
76
+ super().__init__(**kwargs)
77
+ self.history = history
78
+
79
+ def action_history_previous(self) -> None:
80
+ """Navigate to previous command in history."""
81
+ self.value = self.history.navigate_up(self.value)
82
+ self.cursor_position = len(self.value)
83
+
84
+ def action_history_next(self) -> None:
85
+ """Navigate to next command in history."""
86
+ self.value = self.history.navigate_down(self.value)
87
+ self.cursor_position = len(self.value)
@@ -206,7 +206,7 @@ wheels = [
206
206
 
207
207
  [[package]]
208
208
  name = "claude-sdk-tutor"
209
- version = "0.1.5"
209
+ version = "0.1.7"
210
210
  source = { editable = "." }
211
211
  dependencies = [
212
212
  { name = "claude-agent-sdk" },
@@ -1,29 +0,0 @@
1
- from textual.binding import Binding
2
- from textual.widgets import Input
3
-
4
- from claude.history import CommandHistory
5
-
6
-
7
- class HistoryInput(Input):
8
- """Input widget with command history navigation."""
9
-
10
- BINDINGS = [
11
- Binding("up", "history_previous", "Previous command", show=False),
12
- Binding("down", "history_next", "Next command", show=False),
13
- Binding("escape", "app.cancel_query", "Cancel", show=False),
14
- Binding("ctrl+c", "app.cancel_query", "Cancel", show=False),
15
- ]
16
-
17
- def __init__(self, history: CommandHistory, **kwargs):
18
- super().__init__(**kwargs)
19
- self.history = history
20
-
21
- def action_history_previous(self) -> None:
22
- """Navigate to previous command in history."""
23
- self.value = self.history.navigate_up(self.value)
24
- self.cursor_position = len(self.value)
25
-
26
- def action_history_next(self) -> None:
27
- """Navigate to next command in history."""
28
- self.value = self.history.navigate_down(self.value)
29
- self.cursor_position = len(self.value)