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.
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/PKG-INFO +1 -1
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/app.py +178 -48
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/pyproject.toml +1 -1
- claude_sdk_tutor-0.1.8/src/claude/widgets.py +87 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/uv.lock +1 -1
- claude_sdk_tutor-0.1.6/src/claude/widgets.py +0 -29
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/.gitignore +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/.python-version +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/CLAUDE.md +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/LICENSE +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/Makefile +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/README.md +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/__init__.py +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/src/__init__.py +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/src/claude/__init__.py +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/src/claude/claude_agent.py +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/src/claude/history.py +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/src/claude/mcp_commands.py +0 -0
- {claude_sdk_tutor-0.1.6 → claude_sdk_tutor-0.1.8}/src/claude/mcp_config.py +0 -0
|
@@ -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
|
|
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(
|
|
171
|
+
yield Static(HEADER_TEXT, id="header")
|
|
172
|
+
yield StatusBar(id="status-bar")
|
|
81
173
|
yield RichLog(markup=True, highlight=True)
|
|
82
|
-
yield
|
|
83
|
-
yield HistoryInput(
|
|
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",
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
480
|
+
self.query_one("#spinner", ASCIISpinner).stop()
|
|
351
481
|
|
|
352
482
|
|
|
353
483
|
def main():
|
|
@@ -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)
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|