mycode-coding-agent 0.1.0__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 (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,305 @@
1
+ """Textual screens for the MyCode welcome and startup flow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from textual.css.query import NoMatches
8
+ from textual.app import ComposeResult
9
+ from textual.containers import Vertical
10
+ from textual.screen import ModalScreen, Screen
11
+ from textual.widgets import Button, Input, LoadingIndicator, OptionList, Static
12
+ from textual.widgets.option_list import Option
13
+
14
+ from mycode.application.sessions import SessionStartRequest
15
+ from mycode.mcp.trust import MCPTrustRequest
16
+ from mycode.permissions import ConfirmationRequest, ConfirmationResult
17
+ from mycode.persistence.session_store import SessionRecord
18
+ from mycode.presentation.tui.widgets import (
19
+ SPLASH_LOGO,
20
+ ConversationView,
21
+ HeaderBar,
22
+ StatusBar,
23
+ )
24
+
25
+
26
+ def _session_options(sessions: Sequence[SessionRecord]) -> list[Option]:
27
+ options = [
28
+ Option("Continue latest", id="continue"),
29
+ Option("New session", id="new"),
30
+ ]
31
+ options.extend(
32
+ Option(
33
+ f"Resume: {session.title} {session.id[:8]}",
34
+ id=f"resume:{session.id}",
35
+ )
36
+ for session in sessions
37
+ )
38
+ return options
39
+
40
+
41
+ class WelcomeScreen(Screen[None]):
42
+ def __init__(
43
+ self,
44
+ *,
45
+ workspace: str,
46
+ model: str,
47
+ sessions: Sequence[SessionRecord] = (),
48
+ enabled: bool = False,
49
+ notice: str = "",
50
+ error: str = "",
51
+ ) -> None:
52
+ super().__init__()
53
+ self.workspace = workspace
54
+ self.model = model
55
+ self.sessions = tuple(sessions)
56
+ self.enabled = enabled
57
+ self.notice = notice
58
+ self.error = error
59
+
60
+ def compose(self) -> ComposeResult:
61
+ with Vertical(id="welcome-content"):
62
+ yield Static(SPLASH_LOGO, id="welcome-logo", markup=False)
63
+ yield Static(
64
+ f"Workspace {self.workspace}\nModel {self.model}",
65
+ id="welcome-meta",
66
+ markup=False,
67
+ )
68
+ if self.error:
69
+ yield Static(f"⚠ {self.error}", id="welcome-error")
70
+ elif self.notice:
71
+ yield Static(self.notice, id="welcome-notice")
72
+ yield OptionList(
73
+ *_session_options(self.sessions),
74
+ id="session-options",
75
+ disabled=not self.enabled,
76
+ )
77
+ yield Static(
78
+ "↑ ↓ select Enter confirm",
79
+ id="welcome-help",
80
+ )
81
+
82
+ def on_mount(self) -> None:
83
+ if self.enabled:
84
+ self.call_after_refresh(self._focus_options)
85
+
86
+ def _focus_options(self) -> None:
87
+ if not self.is_mounted or self.app.screen is not self:
88
+ return
89
+ try:
90
+ self.query_one(OptionList).focus()
91
+ except NoMatches:
92
+ return
93
+
94
+ @staticmethod
95
+ def request_from_option(option_id: str | None) -> SessionStartRequest | None:
96
+ if option_id == "new":
97
+ return SessionStartRequest(mode="new")
98
+ if option_id == "continue":
99
+ return SessionStartRequest(mode="continue")
100
+ if option_id is not None and option_id.startswith("resume:"):
101
+ return SessionStartRequest(
102
+ mode="resume",
103
+ session_id=option_id.removeprefix("resume:"),
104
+ )
105
+ return None
106
+
107
+
108
+ class LoadingScreen(Screen[None]):
109
+ def __init__(self, *, workspace: str, model: str, request_label: str) -> None:
110
+ super().__init__()
111
+ self.workspace = workspace
112
+ self.model = model
113
+ self.request_label = request_label
114
+ self.progress = "Opening session..."
115
+
116
+ def compose(self) -> ComposeResult:
117
+ with Vertical(id="loading-content"):
118
+ yield Static(SPLASH_LOGO, id="loading-logo", markup=False)
119
+ yield Static(
120
+ f"Workspace {self.workspace}\nModel {self.model}",
121
+ id="loading-meta",
122
+ markup=False,
123
+ )
124
+ yield Static(f"Session {self.request_label}", id="loading-session")
125
+ yield LoadingIndicator(id="loading-indicator")
126
+ yield Static(self.progress, id="loading-status")
127
+
128
+ def set_progress(self, value: str) -> None:
129
+ self.progress = value
130
+ if self.is_mounted:
131
+ self.query_one("#loading-status", Static).update(value)
132
+
133
+
134
+ class MainScreen(Screen[None]):
135
+ def __init__(
136
+ self,
137
+ *,
138
+ workspace: str,
139
+ model: str,
140
+ history: tuple[tuple[str, str], ...] = (),
141
+ ) -> None:
142
+ super().__init__()
143
+ self.workspace = workspace
144
+ self.model = model
145
+ self.history = history
146
+
147
+ def compose(self) -> ComposeResult:
148
+ yield HeaderBar(self.workspace, self.model, id="header")
149
+ yield ConversationView(id="conversation")
150
+ yield StatusBar("Runtime Ready", id="status")
151
+ yield Input(placeholder="Ask MyCode...", id="prompt")
152
+
153
+ def on_mount(self) -> None:
154
+ self.app._activate_main_screen(self)
155
+ self.query_one(Input).focus()
156
+
157
+ def on_input_submitted(self, event: Input.Submitted) -> None:
158
+ content = event.value
159
+ event.input.value = ""
160
+ if content.strip():
161
+ self.app.submit_user_message(content)
162
+
163
+
164
+ class MCPTrustScreen(ModalScreen[bool]):
165
+ BINDINGS = [("escape", "reject", "Reject")]
166
+
167
+ def __init__(self, request: MCPTrustRequest) -> None:
168
+ super().__init__()
169
+ self.request = request
170
+
171
+ def compose(self) -> ComposeResult:
172
+ lines = [
173
+ "Project MCP trust request",
174
+ "Review the safe configuration summary before enabling MCP.",
175
+ "",
176
+ ]
177
+ for server in self.request.servers:
178
+ lines.extend((f"server: {server.alias}", f"transport: {server.transport}"))
179
+ if server.transport == "stdio":
180
+ lines.append(f"command: {server.command!r}")
181
+ lines.append(f"args: {list(server.args)!r}")
182
+ if server.env_keys:
183
+ lines.append(f"env keys: {list(server.env_keys)!r}")
184
+ else:
185
+ lines.append(f"url template: {server.url_template!r}")
186
+ if server.destination is not None:
187
+ lines.append(f"destination: {server.destination!r}")
188
+ if server.header_keys:
189
+ lines.append(f"header keys: {list(server.header_keys)!r}")
190
+ lines.append("")
191
+
192
+ with Vertical(id="mcp-trust-dialog"):
193
+ yield Static("\n".join(lines), id="mcp-trust-details")
194
+ with Vertical(id="mcp-trust-actions"):
195
+ yield Button("Approve", id="mcp-trust-approve", variant="success")
196
+ yield Button("Reject", id="mcp-trust-reject", variant="error")
197
+
198
+ def on_mount(self) -> None:
199
+ self.query_one("#mcp-trust-approve", Button).focus()
200
+
201
+ def on_button_pressed(self, event: Button.Pressed) -> None:
202
+ if event.button.id == "mcp-trust-approve":
203
+ self.dismiss(True)
204
+ elif event.button.id == "mcp-trust-reject":
205
+ self.dismiss(False)
206
+
207
+ def action_reject(self) -> None:
208
+ self.dismiss(False)
209
+
210
+
211
+ _PERMISSION_METADATA_KEYS = (
212
+ "resolved_path",
213
+ "workspace_root",
214
+ "path_scope",
215
+ "pattern_scope",
216
+ "command_display",
217
+ "resolved_cwd",
218
+ "cwd_scope",
219
+ "command_risk_category",
220
+ "command_risk",
221
+ "command_risk_reason",
222
+ "memory_scope",
223
+ "memory_kind",
224
+ "memory_key",
225
+ "memory_path",
226
+ "skill_name",
227
+ "skill_source",
228
+ "script",
229
+ )
230
+
231
+
232
+ def _permission_details(request: ConfirmationRequest) -> str:
233
+ permission_request = request.permission_request
234
+ lines = [
235
+ f"tool: {permission_request.tool_name}",
236
+ f"capability: {permission_request.capability}",
237
+ f"action: {permission_request.action}",
238
+ ]
239
+ if permission_request.target is not None:
240
+ lines.append(f"target: {permission_request.target}")
241
+ lines.append(f"reason: {request.permission_decision.reason}")
242
+ if request.prompt:
243
+ lines.append(f"prompt: {request.prompt}")
244
+
245
+ metadata = {**request.permission_decision.metadata, **request.metadata}
246
+ for key in _PERMISSION_METADATA_KEYS:
247
+ value = metadata.get(key)
248
+ if value is None:
249
+ continue
250
+ rendered = str(value)
251
+ if len(rendered) > 240:
252
+ rendered = rendered[:237] + "..."
253
+ lines.append(f"{key}: {rendered}")
254
+ return "\n".join(lines)
255
+
256
+
257
+ class PermissionScreen(ModalScreen[ConfirmationResult]):
258
+ BINDINGS = [("escape", "deny", "Deny")]
259
+
260
+ def __init__(self, request: ConfirmationRequest) -> None:
261
+ super().__init__()
262
+ self.request = request
263
+
264
+ def compose(self) -> ComposeResult:
265
+ with Vertical(id="permission-dialog"):
266
+ yield Static(
267
+ "Permission required\n\n" + _permission_details(self.request),
268
+ id="permission-details",
269
+ markup=False,
270
+ )
271
+ with Vertical(id="permission-actions"):
272
+ yield Button("Allow once", id="permission-allow-once", variant="success")
273
+ yield Button("Allow task", id="permission-allow-task", variant="success")
274
+ yield Button("Allow session", id="permission-allow-session", variant="success")
275
+ yield Button("Deny", id="permission-deny", variant="error")
276
+
277
+ def on_mount(self) -> None:
278
+ self.query_one("#permission-allow-once", Button).focus()
279
+
280
+ def on_button_pressed(self, event: Button.Pressed) -> None:
281
+ scopes = {
282
+ "permission-allow-once": "once",
283
+ "permission-allow-task": "task",
284
+ "permission-allow-session": "session",
285
+ }
286
+ if event.button.id in scopes:
287
+ self.dismiss(ConfirmationResult.approved(scope=scopes[event.button.id]))
288
+ elif event.button.id == "permission-deny":
289
+ self.dismiss(
290
+ ConfirmationResult.rejected(message="Permission denied by user.")
291
+ )
292
+
293
+ def action_deny(self) -> None:
294
+ self.dismiss(
295
+ ConfirmationResult.rejected(message="Permission denied by user.")
296
+ )
297
+
298
+
299
+ __all__ = [
300
+ "LoadingScreen",
301
+ "MCPTrustScreen",
302
+ "MainScreen",
303
+ "PermissionScreen",
304
+ "WelcomeScreen",
305
+ ]
@@ -0,0 +1,214 @@
1
+ """Reusable widgets for the MyCode Textual presentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from rich.text import Text
8
+ from textual.containers import VerticalScroll
9
+ from textual.widgets import Static
10
+
11
+
12
+ SPLASH_LOGO = r""" ╭──────────╮
13
+ │ >_ │
14
+ │ MYCODE │
15
+ ╰──────────╯
16
+
17
+ __ __ ____ _
18
+ | \/ |_ _/ ___|___ __| | ___
19
+ | |\/| | | | | | / _ \ / _` |/ _ \
20
+ | | | | |_| | |__| (_) | (_| | __/
21
+ |_| |_|\__, |\____\___/ \__,_|\___|
22
+ |___/
23
+
24
+ Extensible Terminal Coding Agent"""
25
+
26
+
27
+ @dataclass
28
+ class _ConversationEntry:
29
+ kind: str
30
+ content: str
31
+ name: str = ""
32
+ arguments: str = ""
33
+ completed: bool = False
34
+ ok: bool = True
35
+ summary: str = ""
36
+
37
+
38
+ class _ConversationEntryWidget(Static):
39
+ """Render one conversation entry that can be updated in place."""
40
+
41
+ def __init__(self, entry: _ConversationEntry) -> None:
42
+ self.entry = entry
43
+ super().__init__(self._render_entry(entry), markup=False)
44
+
45
+ def update_entry(self, entry: _ConversationEntry) -> None:
46
+ self.entry = entry
47
+ self.update(self._render_entry(entry))
48
+
49
+ @staticmethod
50
+ def _render_entry(entry: _ConversationEntry) -> Text:
51
+ if entry.kind == "user":
52
+ return Text.assemble(("You", "bold cyan"), "\n", entry.content)
53
+ if entry.kind == "assistant":
54
+ return Text.assemble(("MyCode", "bold green"), "\n", entry.content)
55
+ if entry.kind == "tool":
56
+ if not entry.completed:
57
+ line = f"› {entry.name}"
58
+ if entry.arguments:
59
+ line += f" {entry.arguments}"
60
+ return Text(line, style="yellow")
61
+ marker = "✓" if entry.ok else "✗"
62
+ line = f"{marker} {entry.name}"
63
+ if not entry.ok and entry.summary:
64
+ line += f" {entry.summary}"
65
+ return Text(line, style="green" if entry.ok else "red")
66
+
67
+ style = {
68
+ "warning": "yellow",
69
+ "error": "red",
70
+ "info": "dim",
71
+ }.get(entry.kind, "dim")
72
+ return Text(entry.content, style=style)
73
+
74
+
75
+ class ConversationView(VerticalScroll):
76
+ """Incremental conversation projection with a model-backed transcript."""
77
+
78
+ def __init__(self, *args, **kwargs) -> None:
79
+ super().__init__(*args, **kwargs)
80
+ self._entries: list[_ConversationEntry] = []
81
+ self._entry_widgets: dict[int, _ConversationEntryWidget] = {}
82
+
83
+ def add_user_message(self, content: str) -> None:
84
+ self._entries.append(_ConversationEntry("user", content))
85
+ self._mount_entry(len(self._entries) - 1)
86
+
87
+ def add_assistant_message(self, content: str) -> None:
88
+ self._entries.append(_ConversationEntry("assistant", content))
89
+ self._mount_entry(len(self._entries) - 1)
90
+
91
+ def add_history_message(self, role: str, content: str) -> None:
92
+ if role == "user":
93
+ self.add_user_message(content)
94
+ elif role == "assistant":
95
+ self.add_assistant_message(content)
96
+
97
+ def append_assistant_delta(self, content: str) -> None:
98
+ if self._entries and self._entries[-1].kind == "assistant":
99
+ entry_index = len(self._entries) - 1
100
+ self._entries[entry_index].content += content
101
+ self._refresh_entry(entry_index)
102
+ else:
103
+ self.add_assistant_message(content)
104
+
105
+ def add_notice(self, content: str, *, level: str = "info") -> None:
106
+ self._entries.append(_ConversationEntry(level, content))
107
+ self._mount_entry(len(self._entries) - 1)
108
+
109
+ def add_tool_activity(self, name: str, arguments: str) -> int:
110
+ token = len(self._entries)
111
+ self._entries.append(
112
+ _ConversationEntry("tool", "", name=name, arguments=arguments)
113
+ )
114
+ self._mount_entry(token)
115
+ return token
116
+
117
+ def complete_tool_activity(
118
+ self,
119
+ token: int,
120
+ name: str,
121
+ *,
122
+ ok: bool,
123
+ summary: str,
124
+ ) -> None:
125
+ if token >= len(self._entries) or self._entries[token].kind != "tool":
126
+ self.add_notice(
127
+ f"⚠ tool result without pending call: {name}",
128
+ level="warning",
129
+ )
130
+ return
131
+ entry = self._entries[token]
132
+ entry.completed = True
133
+ entry.ok = ok
134
+ entry.summary = summary
135
+ self._refresh_entry(token)
136
+
137
+ @property
138
+ def transcript_text(self) -> str:
139
+ lines: list[str] = []
140
+ for entry in self._entries:
141
+ if entry.kind in {"user", "assistant", "info", "warning", "error"}:
142
+ lines.append(entry.content)
143
+ elif entry.kind == "tool":
144
+ if entry.completed:
145
+ marker = "✓" if entry.ok else "✗"
146
+ line = f"{marker} {entry.name}"
147
+ if not entry.ok and entry.summary:
148
+ line += f" {entry.summary}"
149
+ lines.append(line)
150
+ else:
151
+ line = f"› {entry.name}"
152
+ if entry.arguments:
153
+ line += f" {entry.arguments}"
154
+ lines.append(line)
155
+ return "\n".join(lines)
156
+
157
+ def _mount_entry(self, index: int) -> None:
158
+ widget = _ConversationEntryWidget(self._entries[index])
159
+ self._entry_widgets[index] = widget
160
+ self.mount(widget)
161
+ self.call_after_refresh(self._scroll_to_end)
162
+
163
+ def _refresh_entry(self, index: int) -> None:
164
+ widget = self._entry_widgets.get(index)
165
+ if widget is None:
166
+ self._mount_entry(index)
167
+ return
168
+ widget.update_entry(self._entries[index])
169
+ self.call_after_refresh(self._scroll_to_end)
170
+
171
+ def _scroll_to_end(self) -> None:
172
+ if self.is_mounted:
173
+ self.scroll_end(animate=False, immediate=True, force=True)
174
+
175
+
176
+ class HeaderBar(Static):
177
+ def __init__(
178
+ self,
179
+ workspace: str = "—",
180
+ model: str = "—",
181
+ *args,
182
+ **kwargs,
183
+ ) -> None:
184
+ super().__init__(*args, **kwargs)
185
+ self._workspace = workspace or "—"
186
+ self._model = model or "—"
187
+ self._session = "—"
188
+
189
+ def on_mount(self) -> None:
190
+ self._refresh()
191
+
192
+ def set_session(self, value: str) -> None:
193
+ self._session = value or "—"
194
+ self._refresh()
195
+
196
+ def _refresh(self) -> None:
197
+ self.update(
198
+ Text(
199
+ f">_ MyCode {self._workspace} {self._model} "
200
+ f"session: {self._session}",
201
+ style="bold",
202
+ )
203
+ )
204
+
205
+
206
+ class StatusBar(Static):
207
+ def __init__(self, value: str = "UI Ready", *args, **kwargs) -> None:
208
+ super().__init__(value, *args, **kwargs)
209
+
210
+ def set_status(self, value: str) -> None:
211
+ self.update(Text(value))
212
+
213
+
214
+ __all__ = ["ConversationView", "HeaderBar", "SPLASH_LOGO", "StatusBar"]
mycode/project.py ADDED
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+ import hashlib
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ProjectIdentity:
9
+ key: str
10
+ workspace_root: Path
11
+
12
+ @classmethod
13
+ def from_workspace(cls, workspace_root: str | Path) -> "ProjectIdentity":
14
+ resolved = Path(workspace_root).resolve(strict=False)
15
+ if not resolved.exists():
16
+ raise ValueError(f"Workspace root does not exist: {resolved}")
17
+ if not resolved.is_dir():
18
+ raise ValueError(f"Workspace root is not a directory: {resolved}")
19
+
20
+ normalized = os.path.normcase(str(resolved)).replace("\\", "/")
21
+ key = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
22
+ return cls(key=key, workspace_root=resolved)