claude-sdk-tutor 0.1.6__tar.gz → 0.1.7__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.7
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,21 @@ 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_PATH = "phosphor.tcss"
39
+
28
40
  def __init__(self):
29
41
  super().__init__()
30
42
  self.tutor_mode = True
@@ -44,66 +56,71 @@ class MyApp(App):
44
56
  mcp_servers=self.mcp_config.get_enabled_servers_for_sdk(),
45
57
  )
46
58
 
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
59
  def compose(self) -> ComposeResult:
79
60
  with Vertical(id="main"):
80
- yield Static("Welcome to claude SDK tutor!", id="header")
61
+ yield Static(HEADER_TEXT, id="header")
62
+ yield StatusBar(id="status-bar")
81
63
  yield RichLog(markup=True, highlight=True)
82
- yield LoadingIndicator(id="spinner")
83
- yield HistoryInput(history=self.history)
64
+ yield ASCIISpinner(id="spinner")
65
+ yield HistoryInput(
66
+ history=self.history,
67
+ placeholder="Type a message or /help for commands...",
68
+ )
84
69
  yield Footer()
85
70
 
86
71
  async def on_mount(self) -> None:
87
- self.query_one("#spinner", LoadingIndicator).display = False
72
+ self.query_one("#spinner", ASCIISpinner).display = False
73
+ self._update_status_bar()
88
74
  await connect_client(self.client)
89
75
 
76
+ def _update_status_bar(self) -> None:
77
+ """Update the status bar with current mode states."""
78
+ status_bar = self.query_one("#status-bar", StatusBar)
79
+ status_bar.tutor_on = self.tutor_mode
80
+ status_bar.web_on = self.web_search_enabled
81
+ status_bar.mcp_count = len(self.mcp_config.get_enabled_servers_for_sdk())
82
+
90
83
  def write_user_message(self, message: str) -> None:
91
84
  log = self.query_one(RichLog)
92
- log.write(Panel(RichMarkdown(message), title="You", border_style="dodger_blue1"))
85
+ log.write(Panel(
86
+ RichMarkdown(message),
87
+ title="You",
88
+ border_style=USER_COLOR,
89
+ box=ROUNDED,
90
+ padding=(1, 2),
91
+ ))
93
92
 
94
93
  def write_system_message(self, message: str) -> None:
95
94
  log = self.query_one(RichLog)
96
- log.write(Panel(RichMarkdown(message), title="Claude", border_style="red"))
95
+ log.write(Panel(
96
+ RichMarkdown(message),
97
+ title="Claude",
98
+ border_style=CLAUDE_COLOR,
99
+ box=ROUNDED,
100
+ padding=(1, 2),
101
+ ))
97
102
 
98
103
  def write_tool_message(self, name: str, input: dict) -> None:
99
104
  log = self.query_one(RichLog)
100
105
  input_str = json.dumps(input, indent=2)
101
106
  content = f"**{name}**\n```json\n{input_str}\n```"
102
- log.write(Panel(RichMarkdown(content), title="Tool", border_style="grey50"))
107
+ log.write(Panel(
108
+ RichMarkdown(content),
109
+ title="Tool",
110
+ border_style=TOOL_COLOR,
111
+ box=ROUNDED,
112
+ padding=(1, 2),
113
+ ))
103
114
 
104
115
  def write_slash_message(self, message: str) -> None:
105
116
  log = self.query_one(RichLog)
106
- log.write(Panel(RichMarkdown(message), title="Slash", border_style="green"))
117
+ log.write(Panel(
118
+ RichMarkdown(message),
119
+ title="System",
120
+ border_style=SYSTEM_COLOR,
121
+ box=ROUNDED,
122
+ padding=(1, 2),
123
+ ))
107
124
 
108
125
  def on_input_submitted(self, event: Input.Submitted) -> None:
109
126
  command = event.value.strip()
@@ -136,7 +153,7 @@ class MyApp(App):
136
153
  self._handle_mcp_command(command)
137
154
  return
138
155
  self.write_user_message(event.value)
139
- self.query_one("#spinner", LoadingIndicator).display = True
156
+ self.query_one("#spinner", ASCIISpinner).start("Processing query...")
140
157
  self._query_running = True
141
158
  self.run_worker(self.get_response(event.value))
142
159
 
@@ -144,6 +161,7 @@ class MyApp(App):
144
161
  self.query_one(RichLog).clear()
145
162
  self.client = self._create_client()
146
163
  await connect_client(self.client)
164
+ self._update_status_bar()
147
165
  self.write_slash_message("Context cleared")
148
166
 
149
167
  async def toggle_tutor_mode(self) -> None:
@@ -151,7 +169,8 @@ class MyApp(App):
151
169
  self.query_one(RichLog).clear()
152
170
  self.client = self._create_client()
153
171
  await connect_client(self.client)
154
- status = "on" if self.tutor_mode else "off"
172
+ self._update_status_bar()
173
+ status = "enabled" if self.tutor_mode else "disabled"
155
174
  self.write_slash_message(f"Tutor mode {status}")
156
175
 
157
176
  async def toggle_web_search(self) -> None:
@@ -159,7 +178,8 @@ class MyApp(App):
159
178
  self.query_one(RichLog).clear()
160
179
  self.client = self._create_client()
161
180
  await connect_client(self.client)
162
- status = "on" if self.web_search_enabled else "off"
181
+ self._update_status_bar()
182
+ status = "enabled" if self.web_search_enabled else "disabled"
163
183
  self.write_slash_message(f"Web search {status}")
164
184
 
165
185
  def show_help(self) -> None:
@@ -183,7 +203,7 @@ class MyApp(App):
183
203
  )
184
204
  elif isinstance(result, McpAsyncCommand):
185
205
  # Async command needs connection testing
186
- self.query_one("#spinner", LoadingIndicator).display = True
206
+ self.query_one("#spinner", ASCIISpinner).start("Testing MCP connections...")
187
207
  self.run_worker(self._test_mcp_connections(result))
188
208
  else:
189
209
  self.write_slash_message(result)
@@ -236,7 +256,7 @@ class MyApp(App):
236
256
  except Exception as e:
237
257
  self.write_slash_message(f"**Error** testing MCP connections: {e}")
238
258
  finally:
239
- self.query_one("#spinner", LoadingIndicator).display = False
259
+ self.query_one("#spinner", ASCIISpinner).stop()
240
260
 
241
261
  def _handle_mcp_add_step(self, user_input: str) -> None:
242
262
  """Handle a step in the interactive MCP add wizard."""
@@ -330,7 +350,7 @@ class MyApp(App):
330
350
  elif isinstance(message, ResultMessage):
331
351
  pass # Might want to add logging later
332
352
  finally:
333
- self.query_one("#spinner", LoadingIndicator).display = False
353
+ self.query_one("#spinner", ASCIISpinner).stop()
334
354
  self._query_running = False
335
355
 
336
356
  def action_cancel_query(self) -> None:
@@ -347,7 +367,7 @@ class MyApp(App):
347
367
  pass # Ignore errors if not connected or no active query
348
368
  finally:
349
369
  self._query_running = False
350
- self.query_one("#spinner", LoadingIndicator).display = False
370
+ self.query_one("#spinner", ASCIISpinner).stop()
351
371
 
352
372
 
353
373
  def main():
@@ -0,0 +1,122 @@
1
+ /* MINIMAL Theme - Clean, Modern, Professional */
2
+
3
+ /* Color Variables */
4
+ $bg-primary: #1a1a1a;
5
+ $bg-secondary: #242424;
6
+ $bg-elevated: #2a2a2a;
7
+ $text-primary: #e0e0e0;
8
+ $text-muted: #888888;
9
+ $accent: #5c9fd4;
10
+ $accent-dim: #4a7fa8;
11
+ $border: #333333;
12
+ $border-focus: #555555;
13
+ $success: #6fbf73;
14
+ $warning: #d4a05c;
15
+
16
+ /* Global App Styling */
17
+ Screen {
18
+ background: $bg-primary;
19
+ }
20
+
21
+ /* Main Container */
22
+ #main {
23
+ height: 100%;
24
+ background: $bg-primary;
25
+ }
26
+
27
+ /* Header */
28
+ #header {
29
+ content-align: center middle;
30
+ width: 100%;
31
+ padding: 1 0;
32
+ height: auto;
33
+ color: $text-primary;
34
+ text-style: bold;
35
+ background: $bg-primary;
36
+ }
37
+
38
+ /* Status Bar */
39
+ #status-bar {
40
+ content-align: center middle;
41
+ width: 100%;
42
+ height: auto;
43
+ padding-bottom: 1;
44
+ color: $text-muted;
45
+ background: $bg-primary;
46
+ }
47
+
48
+ /* Message Log */
49
+ RichLog {
50
+ background: $bg-secondary;
51
+ margin-left: 2;
52
+ margin-right: 2;
53
+ height: 1fr;
54
+ border: round $border;
55
+ scrollbar-color: $border;
56
+ scrollbar-color-hover: $border-focus;
57
+ scrollbar-color-active: $accent;
58
+ }
59
+
60
+ /* Spinner */
61
+ #spinner {
62
+ height: auto;
63
+ margin-left: 2;
64
+ margin-right: 2;
65
+ color: $text-muted;
66
+ background: $bg-primary;
67
+ }
68
+
69
+ /* Input Field */
70
+ HistoryInput {
71
+ height: auto;
72
+ margin-top: 1;
73
+ margin-left: 2;
74
+ margin-right: 2;
75
+ margin-bottom: 1;
76
+ background: $bg-elevated;
77
+ color: $text-primary;
78
+ border: round $border;
79
+ }
80
+
81
+ HistoryInput:focus {
82
+ border: round $accent-dim;
83
+ }
84
+
85
+ HistoryInput > .input--cursor {
86
+ color: $bg-primary;
87
+ background: $accent;
88
+ }
89
+
90
+ HistoryInput > .input--placeholder {
91
+ color: $text-muted;
92
+ }
93
+
94
+ /* Footer */
95
+ Footer {
96
+ background: $bg-secondary;
97
+ color: $text-muted;
98
+ }
99
+
100
+ Footer > .footer--key {
101
+ background: $bg-elevated;
102
+ color: $text-primary;
103
+ }
104
+
105
+ Footer > .footer--description {
106
+ color: $text-muted;
107
+ }
108
+
109
+ FooterKey {
110
+ background: $bg-elevated;
111
+ color: $text-primary;
112
+ }
113
+
114
+ FooterKey:hover {
115
+ background: $accent-dim;
116
+ color: $bg-primary;
117
+ }
118
+
119
+ /* Hide the default loading indicator */
120
+ LoadingIndicator {
121
+ display: none;
122
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-sdk-tutor"
3
- version = "0.1.6"
3
+ version = "0.1.7"
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.6"
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)