shotgun-sh 0.3.3.dev1__py3-none-any.whl → 0.6.2__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.
- shotgun/agents/agent_manager.py +497 -30
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +90 -77
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +52 -8
- shotgun/agents/config/models.py +21 -27
- shotgun/agents/config/provider.py +44 -27
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/export.py +12 -13
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +90 -2
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +384 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +557 -0
- shotgun/agents/router/tools/plan_tools.py +403 -0
- shotgun/agents/runner.py +17 -2
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +81 -3
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +46 -6
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/build_constants.py +4 -7
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/error_handler.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +9 -9
- shotgun/main.py +3 -16
- shotgun/posthog_telemetry.py +165 -24
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -52
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
- shotgun/prompts/agents/plan.j2 +38 -12
- shotgun/prompts/agents/research.j2 +70 -31
- shotgun/prompts/agents/router.j2 +713 -0
- shotgun/prompts/agents/specify.j2 +53 -16
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +24 -13
- shotgun/prompts/agents/tasks.j2 +72 -34
- shotgun/settings.py +49 -10
- shotgun/tui/app.py +154 -24
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/prompt_input.py +25 -28
- shotgun/tui/components/status_bar.py +14 -7
- shotgun/tui/dependencies.py +58 -8
- shotgun/tui/protocols.py +55 -0
- shotgun/tui/screens/chat/chat.tcss +24 -1
- shotgun/tui/screens/chat/chat_screen.py +1376 -213
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -97
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +58 -6
- shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +1 -3
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/state/processing_state.py +19 -0
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +88 -35
- shotgun_sh-0.6.2.dist-info/RECORD +291 -0
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/sentry_telemetry.py +0 -232
- shotgun/tui/screens/onboarding.py +0 -580
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +0 -229
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/licenses/LICENSE +0 -0
shotgun/tui/app.py
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
1
6
|
from collections.abc import Iterable
|
|
2
|
-
from typing import Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from shotgun.codebase.core.errors import DatabaseIssue
|
|
3
11
|
|
|
4
12
|
from textual.app import App, SystemCommand
|
|
5
13
|
from textual.binding import Binding
|
|
@@ -13,6 +21,7 @@ from shotgun.agents.config import (
|
|
|
13
21
|
from shotgun.agents.models import AgentType
|
|
14
22
|
from shotgun.logging_config import get_logger
|
|
15
23
|
from shotgun.tui.containers import TUIContainer
|
|
24
|
+
from shotgun.tui.dependencies import create_default_router_deps
|
|
16
25
|
from shotgun.tui.screens.splash import SplashScreen
|
|
17
26
|
from shotgun.utils.file_system_utils import (
|
|
18
27
|
ensure_shotgun_directory_exists,
|
|
@@ -44,7 +53,10 @@ class ShotgunApp(App[None]):
|
|
|
44
53
|
"github_issue": GitHubIssueScreen,
|
|
45
54
|
}
|
|
46
55
|
BINDINGS = [
|
|
47
|
-
|
|
56
|
+
# Use smart_quit to support ctrl+c for copying selected text
|
|
57
|
+
Binding("ctrl+c", "smart_quit", "Quit/Copy", show=False),
|
|
58
|
+
# Cancel quit confirmation with ESC
|
|
59
|
+
Binding("escape", "cancel_quit", "Cancel Quit", show=False),
|
|
48
60
|
]
|
|
49
61
|
|
|
50
62
|
CSS_PATH = "styles.tcss"
|
|
@@ -56,6 +68,7 @@ class ShotgunApp(App[None]):
|
|
|
56
68
|
force_reindex: bool = False,
|
|
57
69
|
show_pull_hint: bool = False,
|
|
58
70
|
pull_version_id: str | None = None,
|
|
71
|
+
pending_db_issues: list[DatabaseIssue] | None = None,
|
|
59
72
|
) -> None:
|
|
60
73
|
super().__init__()
|
|
61
74
|
self.config_manager: ConfigManager = get_config_manager()
|
|
@@ -64,6 +77,12 @@ class ShotgunApp(App[None]):
|
|
|
64
77
|
self.force_reindex = force_reindex
|
|
65
78
|
self.show_pull_hint = show_pull_hint
|
|
66
79
|
self.pull_version_id = pull_version_id
|
|
80
|
+
# Database issues detected at startup (locked, corrupted, timeout)
|
|
81
|
+
# These will be shown to the user via dialogs when ChatScreen mounts
|
|
82
|
+
self.pending_db_issues = pending_db_issues or []
|
|
83
|
+
|
|
84
|
+
# Quit confirmation state for double Ctrl+C to quit
|
|
85
|
+
self._quit_pending = False
|
|
67
86
|
|
|
68
87
|
# Initialize dependency injection container
|
|
69
88
|
self.container = TUIContainer()
|
|
@@ -166,13 +185,11 @@ class ShotgunApp(App[None]):
|
|
|
166
185
|
return
|
|
167
186
|
|
|
168
187
|
# Create ChatScreen with all dependencies injected from container
|
|
169
|
-
# Get the default agent mode (
|
|
170
|
-
agent_mode = AgentType.
|
|
171
|
-
|
|
172
|
-
# Create AgentDeps asynchronously (get_provider_model is now async)
|
|
173
|
-
from shotgun.tui.dependencies import create_default_tui_deps
|
|
188
|
+
# Get the default agent mode (ROUTER)
|
|
189
|
+
agent_mode = AgentType.ROUTER
|
|
174
190
|
|
|
175
|
-
|
|
191
|
+
# Create RouterDeps asynchronously (get_provider_model is now async)
|
|
192
|
+
agent_deps = await create_default_router_deps()
|
|
176
193
|
|
|
177
194
|
# Create AgentManager with async initialization
|
|
178
195
|
agent_manager = AgentManager(deps=agent_deps, initial_type=agent_mode)
|
|
@@ -228,6 +245,63 @@ class ShotgunApp(App[None]):
|
|
|
228
245
|
# Continue to ChatScreen
|
|
229
246
|
self.refresh_startup_screen()
|
|
230
247
|
|
|
248
|
+
@property
|
|
249
|
+
def quit_pending(self) -> bool:
|
|
250
|
+
"""Whether a quit confirmation is pending.
|
|
251
|
+
|
|
252
|
+
Returns True if user pressed Ctrl+C and needs to press again or ESC to cancel.
|
|
253
|
+
"""
|
|
254
|
+
return self._quit_pending
|
|
255
|
+
|
|
256
|
+
def _reset_quit_pending(self) -> None:
|
|
257
|
+
"""Reset the quit confirmation state and refresh the status bar."""
|
|
258
|
+
self._quit_pending = False
|
|
259
|
+
self._refresh_status_bar()
|
|
260
|
+
|
|
261
|
+
def _refresh_status_bar(self) -> None:
|
|
262
|
+
"""Refresh the StatusBar widget to reflect current state."""
|
|
263
|
+
from textual.css.query import NoMatches
|
|
264
|
+
|
|
265
|
+
from shotgun.tui.components.status_bar import StatusBar
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
status_bar = self.screen.query_one(StatusBar)
|
|
269
|
+
status_bar.refresh()
|
|
270
|
+
except NoMatches:
|
|
271
|
+
# StatusBar might not exist on all screens
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
def action_cancel_quit(self) -> None:
|
|
275
|
+
"""Cancel the quit confirmation when ESC is pressed."""
|
|
276
|
+
if self._quit_pending:
|
|
277
|
+
self._reset_quit_pending()
|
|
278
|
+
|
|
279
|
+
async def action_smart_quit(self) -> None:
|
|
280
|
+
"""Handle ctrl+c: copy selected text if any, otherwise quit.
|
|
281
|
+
|
|
282
|
+
This allows users to select text in the TUI and copy it with ctrl+c,
|
|
283
|
+
while still supporting ctrl+c to quit when no text is selected.
|
|
284
|
+
Requires pressing Ctrl+C twice to quit, or ESC to cancel.
|
|
285
|
+
"""
|
|
286
|
+
# Check if there's selected text on the current screen
|
|
287
|
+
selected_text = self.screen.get_selected_text()
|
|
288
|
+
if selected_text:
|
|
289
|
+
# Copy selected text to clipboard
|
|
290
|
+
self.copy_to_clipboard(selected_text)
|
|
291
|
+
# Clear the selection after copying
|
|
292
|
+
self.screen.clear_selection()
|
|
293
|
+
self.notify("Copied to clipboard", timeout=2)
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# No selection - check if quit is already pending
|
|
297
|
+
if self._quit_pending:
|
|
298
|
+
await self.action_quit()
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Start quit confirmation
|
|
302
|
+
self._quit_pending = True
|
|
303
|
+
self._refresh_status_bar()
|
|
304
|
+
|
|
231
305
|
async def action_quit(self) -> None:
|
|
232
306
|
"""Quit the application."""
|
|
233
307
|
# Shut down PostHog client to prevent threading errors
|
|
@@ -250,6 +324,22 @@ class ShotgunApp(App[None]):
|
|
|
250
324
|
self.push_screen(GitHubIssueScreen())
|
|
251
325
|
|
|
252
326
|
|
|
327
|
+
def _log_startup_info() -> None:
|
|
328
|
+
"""Log startup information for debugging purposes."""
|
|
329
|
+
# Import here to avoid circular import (shotgun.__init__ imports from submodules)
|
|
330
|
+
from shotgun import __version__
|
|
331
|
+
|
|
332
|
+
logger.info("=" * 60)
|
|
333
|
+
logger.info("Shotgun TUI Starting")
|
|
334
|
+
logger.info("=" * 60)
|
|
335
|
+
logger.info(f" Version: {__version__}")
|
|
336
|
+
logger.info(f" Python: {sys.version.split()[0]}")
|
|
337
|
+
logger.info(f" Platform: {platform.system()} {platform.release()}")
|
|
338
|
+
logger.info(f" Architecture: {platform.machine()}")
|
|
339
|
+
logger.info(f" Working Directory: {os.getcwd()}")
|
|
340
|
+
logger.info("=" * 60)
|
|
341
|
+
|
|
342
|
+
|
|
253
343
|
def run(
|
|
254
344
|
no_update_check: bool = False,
|
|
255
345
|
continue_session: bool = False,
|
|
@@ -266,24 +356,54 @@ def run(
|
|
|
266
356
|
show_pull_hint: If True, show hint about recently pulled spec.
|
|
267
357
|
pull_version_id: If provided, pull this spec version before showing ChatScreen.
|
|
268
358
|
"""
|
|
269
|
-
#
|
|
270
|
-
|
|
359
|
+
# Log startup information
|
|
360
|
+
_log_startup_info()
|
|
361
|
+
|
|
362
|
+
# Detect database issues BEFORE starting the TUI (but don't auto-delete)
|
|
363
|
+
# Issues will be presented to the user via dialogs once the TUI is running
|
|
271
364
|
import asyncio
|
|
272
365
|
|
|
366
|
+
from shotgun.codebase.core.errors import KuzuErrorType
|
|
273
367
|
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
274
368
|
from shotgun.utils import get_shotgun_home
|
|
275
369
|
|
|
276
370
|
storage_dir = get_shotgun_home() / "codebases"
|
|
277
371
|
manager = CodebaseGraphManager(storage_dir)
|
|
278
372
|
|
|
373
|
+
pending_db_issues: list[DatabaseIssue] = []
|
|
279
374
|
try:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
375
|
+
# First pass: 10-second timeout
|
|
376
|
+
issues = asyncio.run(manager.detect_database_issues(timeout_seconds=10.0))
|
|
377
|
+
if issues:
|
|
378
|
+
# Categorize issues for logging
|
|
379
|
+
for issue in issues:
|
|
380
|
+
logger.info(
|
|
381
|
+
f"Detected database issue: {issue.graph_id} - "
|
|
382
|
+
f"{issue.error_type.value}: {issue.message}"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Only pass issues that require user interaction to the TUI
|
|
386
|
+
# Schema issues (incomplete builds) can be auto-cleaned silently
|
|
387
|
+
user_facing_issues = [
|
|
388
|
+
i
|
|
389
|
+
for i in issues
|
|
390
|
+
if i.error_type
|
|
391
|
+
in (
|
|
392
|
+
KuzuErrorType.LOCKED,
|
|
393
|
+
KuzuErrorType.CORRUPTION,
|
|
394
|
+
KuzuErrorType.TIMEOUT,
|
|
395
|
+
)
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
# Auto-delete schema issues (incomplete builds) - safe to remove
|
|
399
|
+
schema_issues = [i for i in issues if i.error_type == KuzuErrorType.SCHEMA]
|
|
400
|
+
for issue in schema_issues:
|
|
401
|
+
asyncio.run(manager.delete_database(issue.graph_id))
|
|
402
|
+
logger.info(f"Auto-removed incomplete database: {issue.graph_id}")
|
|
403
|
+
|
|
404
|
+
pending_db_issues = user_facing_issues
|
|
285
405
|
except Exception as e:
|
|
286
|
-
logger.error(f"Failed to
|
|
406
|
+
logger.error(f"Failed to detect database issues: {e}")
|
|
287
407
|
# Continue anyway - the TUI can still function
|
|
288
408
|
|
|
289
409
|
app = ShotgunApp(
|
|
@@ -292,6 +412,7 @@ def run(
|
|
|
292
412
|
force_reindex=force_reindex,
|
|
293
413
|
show_pull_hint=show_pull_hint,
|
|
294
414
|
pull_version_id=pull_version_id,
|
|
415
|
+
pending_db_issues=pending_db_issues,
|
|
295
416
|
)
|
|
296
417
|
app.run(inline_no_clear=True)
|
|
297
418
|
|
|
@@ -314,12 +435,14 @@ def serve(
|
|
|
314
435
|
continue_session: If True, continue from previous conversation.
|
|
315
436
|
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
316
437
|
"""
|
|
317
|
-
#
|
|
318
|
-
#
|
|
438
|
+
# Detect database issues BEFORE starting the TUI
|
|
439
|
+
# Note: In serve mode, issues are logged but user interaction happens in
|
|
440
|
+
# the spawned process via run()
|
|
319
441
|
import asyncio
|
|
320
442
|
|
|
321
443
|
from textual_serve.server import Server
|
|
322
444
|
|
|
445
|
+
from shotgun.codebase.core.errors import KuzuErrorType
|
|
323
446
|
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
324
447
|
from shotgun.utils import get_shotgun_home
|
|
325
448
|
|
|
@@ -327,13 +450,20 @@ def serve(
|
|
|
327
450
|
manager = CodebaseGraphManager(storage_dir)
|
|
328
451
|
|
|
329
452
|
try:
|
|
330
|
-
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
453
|
+
issues = asyncio.run(manager.detect_database_issues(timeout_seconds=10.0))
|
|
454
|
+
if issues:
|
|
455
|
+
for issue in issues:
|
|
456
|
+
logger.info(
|
|
457
|
+
f"Detected database issue: {issue.graph_id} - "
|
|
458
|
+
f"{issue.error_type.value}: {issue.message}"
|
|
459
|
+
)
|
|
460
|
+
# Auto-delete only schema issues (incomplete builds)
|
|
461
|
+
schema_issues = [i for i in issues if i.error_type == KuzuErrorType.SCHEMA]
|
|
462
|
+
for issue in schema_issues:
|
|
463
|
+
asyncio.run(manager.delete_database(issue.graph_id))
|
|
464
|
+
logger.info(f"Auto-removed incomplete database: {issue.graph_id}")
|
|
335
465
|
except Exception as e:
|
|
336
|
-
logger.error(f"Failed to
|
|
466
|
+
logger.error(f"Failed to detect database issues: {e}")
|
|
337
467
|
# Continue anyway - the TUI can still function
|
|
338
468
|
|
|
339
469
|
# Create a new event loop after asyncio.run() closes the previous one
|
shotgun/tui/commands/__init__.py
CHANGED
|
@@ -54,10 +54,18 @@ class CommandHandler:
|
|
|
54
54
|
**Commands:**
|
|
55
55
|
• `/help` - Show this help message
|
|
56
56
|
|
|
57
|
+
**Shell Commands:**
|
|
58
|
+
• `!<command>` - Execute shell commands directly (e.g., `!ls`, `!git status`)
|
|
59
|
+
- Commands run in your current working directory
|
|
60
|
+
- Output is displayed in the chat (not sent to AI)
|
|
61
|
+
- Commands are NOT added to conversation history
|
|
62
|
+
- Leading whitespace is allowed: ` !echo hi` works
|
|
63
|
+
- Note: `!!` is treated as `!` (no history expansion in this version)
|
|
64
|
+
|
|
57
65
|
**Keyboard Shortcuts:**
|
|
58
66
|
|
|
59
67
|
* `Enter` - Send message
|
|
60
|
-
*
|
|
68
|
+
* `/` - Open command palette (for usage, context, and other commands)
|
|
61
69
|
* `Shift+Tab` - Cycle agent modes
|
|
62
70
|
* `Ctrl+C` - Quit application
|
|
63
71
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Attachment bar widget for showing pending file attachment."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.css.query import NoMatches
|
|
5
|
+
from textual.reactive import reactive
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
from textual.widgets import Static
|
|
8
|
+
|
|
9
|
+
from shotgun.attachments import (
|
|
10
|
+
AttachmentBarState,
|
|
11
|
+
FileAttachment,
|
|
12
|
+
format_file_size,
|
|
13
|
+
get_attachment_icon,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AttachmentBar(Widget):
|
|
18
|
+
"""Widget showing pending attachment above input.
|
|
19
|
+
|
|
20
|
+
Displays format: [icon filename.ext (size)]
|
|
21
|
+
Hidden when no attachment is pending.
|
|
22
|
+
|
|
23
|
+
Styles defined in chat.tcss.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
state: reactive[AttachmentBarState] = reactive(AttachmentBarState, init=False)
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
name: str | None = None,
|
|
32
|
+
id: str | None = None,
|
|
33
|
+
classes: str | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Initialize the attachment bar.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name: Optional widget name.
|
|
39
|
+
id: Optional widget ID.
|
|
40
|
+
classes: Optional CSS classes.
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(name=name, id=id, classes=classes)
|
|
43
|
+
self.state = AttachmentBarState(attachment=None)
|
|
44
|
+
self.add_class("hidden")
|
|
45
|
+
|
|
46
|
+
def compose(self) -> ComposeResult:
|
|
47
|
+
"""Compose the attachment bar widget."""
|
|
48
|
+
yield Static("", id="attachment-display")
|
|
49
|
+
|
|
50
|
+
def update_attachment(self, attachment: FileAttachment | None) -> None:
|
|
51
|
+
"""Update the displayed attachment.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
attachment: FileAttachment to display, or None to hide bar.
|
|
55
|
+
"""
|
|
56
|
+
self.state = AttachmentBarState(attachment=attachment)
|
|
57
|
+
|
|
58
|
+
if attachment is None:
|
|
59
|
+
self.add_class("hidden")
|
|
60
|
+
else:
|
|
61
|
+
self.remove_class("hidden")
|
|
62
|
+
self._refresh_display()
|
|
63
|
+
|
|
64
|
+
def _refresh_display(self) -> None:
|
|
65
|
+
"""Refresh the attachment display text."""
|
|
66
|
+
attachment = self.state.attachment
|
|
67
|
+
if attachment is None:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
icon = get_attachment_icon(attachment.file_type)
|
|
71
|
+
size_str = format_file_size(attachment.file_size_bytes)
|
|
72
|
+
display_text = f"[{icon} {attachment.file_name} ({size_str})]"
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
display_widget = self.query_one("#attachment-display", Static)
|
|
76
|
+
display_widget.update(display_text)
|
|
77
|
+
except NoMatches:
|
|
78
|
+
pass # Widget not mounted yet
|
|
79
|
+
|
|
80
|
+
def watch_state(self, new_state: AttachmentBarState) -> None:
|
|
81
|
+
"""React to state changes.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
new_state: The new attachment bar state.
|
|
85
|
+
"""
|
|
86
|
+
if new_state.attachment is not None:
|
|
87
|
+
self._refresh_display()
|
|
@@ -1,20 +1,68 @@
|
|
|
1
1
|
"""Widget to display the current agent mode."""
|
|
2
2
|
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
3
5
|
from textual.widget import Widget
|
|
4
6
|
|
|
5
7
|
from shotgun.agents.models import AgentType
|
|
6
|
-
from shotgun.
|
|
8
|
+
from shotgun.agents.router.models import RouterMode
|
|
9
|
+
from shotgun.tui.protocols import (
|
|
10
|
+
ActiveSubAgentProvider,
|
|
11
|
+
QAStateProvider,
|
|
12
|
+
RouterModeProvider,
|
|
13
|
+
)
|
|
7
14
|
from shotgun.tui.utils.mode_progress import PlaceholderHints
|
|
8
15
|
|
|
9
16
|
|
|
17
|
+
class RouterModeCssClass(StrEnum):
|
|
18
|
+
"""CSS class names for router mode styling."""
|
|
19
|
+
|
|
20
|
+
PLANNING = "mode-planning"
|
|
21
|
+
DRAFTING = "mode-drafting"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Shared display name mapping for agent types
|
|
25
|
+
AGENT_DISPLAY_NAMES: dict[AgentType, str] = {
|
|
26
|
+
AgentType.RESEARCH: "Research",
|
|
27
|
+
AgentType.SPECIFY: "Specify",
|
|
28
|
+
AgentType.PLAN: "Planning",
|
|
29
|
+
AgentType.TASKS: "Tasks",
|
|
30
|
+
AgentType.EXPORT: "Export",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Mode descriptions for legacy agent display
|
|
34
|
+
AGENT_DESCRIPTIONS: dict[AgentType, str] = {
|
|
35
|
+
AgentType.RESEARCH: "Research topics with web search and synthesize findings",
|
|
36
|
+
AgentType.PLAN: "Create comprehensive, actionable plans with milestones",
|
|
37
|
+
AgentType.TASKS: "Generate specific, actionable tasks from research and plans",
|
|
38
|
+
AgentType.SPECIFY: "Create detailed specifications and requirements documents",
|
|
39
|
+
AgentType.EXPORT: "Export artifacts and findings to various formats",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
10
43
|
class ModeIndicator(Widget):
|
|
11
|
-
"""Widget to display the current agent mode.
|
|
44
|
+
"""Widget to display the current agent mode.
|
|
45
|
+
|
|
46
|
+
For router mode, displays:
|
|
47
|
+
- Idle: "📋 Planning mode" or "✍️ Drafting mode"
|
|
48
|
+
- During execution: "📋 Planning → Research" format
|
|
49
|
+
|
|
50
|
+
For legacy agents, displays the agent name and description.
|
|
51
|
+
"""
|
|
12
52
|
|
|
13
53
|
DEFAULT_CSS = """
|
|
14
54
|
ModeIndicator {
|
|
15
55
|
text-wrap: wrap;
|
|
16
56
|
padding-left: 1;
|
|
17
57
|
}
|
|
58
|
+
|
|
59
|
+
ModeIndicator.mode-planning {
|
|
60
|
+
/* Planning mode styling - blue/cyan accent */
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ModeIndicator.mode-drafting {
|
|
64
|
+
/* Drafting mode styling - green accent */
|
|
65
|
+
}
|
|
18
66
|
"""
|
|
19
67
|
|
|
20
68
|
def __init__(self, mode: AgentType) -> None:
|
|
@@ -29,41 +77,88 @@ class ModeIndicator(Widget):
|
|
|
29
77
|
|
|
30
78
|
def render(self) -> str:
|
|
31
79
|
"""Render the mode indicator."""
|
|
32
|
-
# Check if in Q&A mode first
|
|
80
|
+
# Check if in Q&A mode first - takes priority
|
|
33
81
|
if isinstance(self.screen, QAStateProvider) and self.screen.qa_mode:
|
|
34
82
|
return (
|
|
35
83
|
"[bold $text-accent]Q&A mode[/]"
|
|
36
84
|
"[$foreground-muted] (Answer the clarifying questions or ESC to cancel)[/]"
|
|
37
85
|
)
|
|
38
86
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
AgentType.TASKS: "Tasks",
|
|
43
|
-
AgentType.SPECIFY: "Specify",
|
|
44
|
-
AgentType.EXPORT: "Export",
|
|
45
|
-
}
|
|
46
|
-
mode_description = {
|
|
47
|
-
AgentType.RESEARCH: (
|
|
48
|
-
"Research topics with web search and synthesize findings"
|
|
49
|
-
),
|
|
50
|
-
AgentType.PLAN: "Create comprehensive, actionable plans with milestones",
|
|
51
|
-
AgentType.TASKS: (
|
|
52
|
-
"Generate specific, actionable tasks from research and plans"
|
|
53
|
-
),
|
|
54
|
-
AgentType.SPECIFY: (
|
|
55
|
-
"Create detailed specifications and requirements documents"
|
|
56
|
-
),
|
|
57
|
-
AgentType.EXPORT: "Export artifacts and findings to various formats",
|
|
58
|
-
}
|
|
87
|
+
# Router mode display
|
|
88
|
+
if self.mode == AgentType.ROUTER:
|
|
89
|
+
return self._render_router_mode()
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
# Legacy agent mode display
|
|
92
|
+
return self._render_legacy_mode()
|
|
93
|
+
|
|
94
|
+
def _render_router_mode(self) -> str:
|
|
95
|
+
"""Render the router mode indicator.
|
|
96
|
+
|
|
97
|
+
Shows:
|
|
98
|
+
- "📋 Planning mode" or "✍️ Drafting mode" when idle
|
|
99
|
+
- "📋 Planning → Research" format when sub-agent is executing
|
|
100
|
+
"""
|
|
101
|
+
# Get router mode from screen
|
|
102
|
+
router_mode: str | None = None
|
|
103
|
+
if isinstance(self.screen, RouterModeProvider):
|
|
104
|
+
router_mode = self.screen.router_mode
|
|
105
|
+
|
|
106
|
+
# Get active sub-agent from screen
|
|
107
|
+
active_sub_agent: AgentType | None = None
|
|
108
|
+
if isinstance(self.screen, ActiveSubAgentProvider):
|
|
109
|
+
sub_agent_str = self.screen.active_sub_agent
|
|
110
|
+
if sub_agent_str:
|
|
111
|
+
# Convert string back to AgentType enum
|
|
112
|
+
try:
|
|
113
|
+
active_sub_agent = AgentType(sub_agent_str)
|
|
114
|
+
except ValueError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# Determine mode display using RouterMode enum
|
|
118
|
+
if router_mode == RouterMode.DRAFTING.value:
|
|
119
|
+
icon = "✍️"
|
|
120
|
+
mode_name = "Drafting"
|
|
121
|
+
description = "Auto-execute without confirmation"
|
|
122
|
+
css_class = RouterModeCssClass.DRAFTING
|
|
123
|
+
else:
|
|
124
|
+
# Default to planning mode
|
|
125
|
+
icon = "📋"
|
|
126
|
+
mode_name = "Planning"
|
|
127
|
+
description = "Review plans before execution"
|
|
128
|
+
css_class = RouterModeCssClass.PLANNING
|
|
129
|
+
|
|
130
|
+
# Update CSS class for styling
|
|
131
|
+
self.set_classes(css_class)
|
|
132
|
+
|
|
133
|
+
# Add sub-agent suffix if executing
|
|
134
|
+
if active_sub_agent:
|
|
135
|
+
# Use shared display name mapping
|
|
136
|
+
sub_agent_name = AGENT_DISPLAY_NAMES.get(
|
|
137
|
+
active_sub_agent, active_sub_agent.value.title()
|
|
138
|
+
)
|
|
139
|
+
return f"[bold $text-accent]{icon} {mode_name} → {sub_agent_name}[/]"
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
f"[bold $text-accent]{icon} {mode_name} mode[/]"
|
|
143
|
+
f"[$foreground-muted] ({description})[/]"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _render_legacy_mode(self) -> str:
|
|
147
|
+
"""Render the legacy agent mode indicator.
|
|
148
|
+
|
|
149
|
+
Shows the agent name with description and content status.
|
|
150
|
+
"""
|
|
151
|
+
mode_title = AGENT_DISPLAY_NAMES.get(self.mode, self.mode.value.title())
|
|
152
|
+
description = AGENT_DESCRIPTIONS.get(self.mode, "")
|
|
62
153
|
|
|
63
154
|
# Check if mode has content
|
|
64
155
|
has_content = self.progress_checker.has_mode_content(self.mode)
|
|
65
156
|
status_icon = " ✓" if has_content else ""
|
|
66
157
|
|
|
158
|
+
# Clear any router mode CSS classes
|
|
159
|
+
self.remove_class(RouterModeCssClass.PLANNING)
|
|
160
|
+
self.remove_class(RouterModeCssClass.DRAFTING)
|
|
161
|
+
|
|
67
162
|
return (
|
|
68
163
|
f"[bold $text-accent]{mode_title}{status_icon} mode[/]"
|
|
69
164
|
f"[$foreground-muted] ({description})[/]"
|
|
@@ -27,43 +27,40 @@ class PromptInput(TextArea):
|
|
|
27
27
|
super().__init__()
|
|
28
28
|
self.text = text
|
|
29
29
|
|
|
30
|
+
class OpenCommandPalette(Message):
|
|
31
|
+
"""Request to open the command palette."""
|
|
32
|
+
|
|
30
33
|
def action_submit(self) -> None:
|
|
31
34
|
"""An action to submit the text."""
|
|
32
35
|
self.post_message(self.Submitted(self.text))
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
"""Handle key presses
|
|
36
|
-
|
|
37
|
-
# Don't handle Enter key here - let the binding handle it
|
|
37
|
+
def on_key(self, event: events.Key) -> None:
|
|
38
|
+
"""Handle key presses for special actions."""
|
|
39
|
+
# Submit on Enter
|
|
38
40
|
if event.key == "enter":
|
|
41
|
+
event.stop()
|
|
42
|
+
event.prevent_default()
|
|
39
43
|
self.action_submit()
|
|
40
|
-
|
|
41
|
-
self._restart_blink()
|
|
42
|
-
|
|
43
|
-
if self.read_only:
|
|
44
44
|
return
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
event.stop()
|
|
53
|
-
event.prevent_default()
|
|
54
|
-
self.screen.focus_next()
|
|
55
|
-
return
|
|
56
|
-
if self.indent_type == "tabs":
|
|
57
|
-
insert_values["tab"] = "\t"
|
|
58
|
-
else:
|
|
59
|
-
insert_values["tab"] = " " * self._find_columns_to_next_tab_stop()
|
|
46
|
+
# Detect "/" as first character to trigger command palette
|
|
47
|
+
if event.character == "/" and not self.text.strip():
|
|
48
|
+
event.stop()
|
|
49
|
+
event.prevent_default()
|
|
50
|
+
self.post_message(self.OpenCommandPalette())
|
|
51
|
+
return
|
|
60
52
|
|
|
61
|
-
|
|
53
|
+
# Handle ctrl+j or shift+enter for newline (since enter is for submit)
|
|
54
|
+
# Note: shift+enter only works if terminal is configured to send escape sequence
|
|
55
|
+
# Common terminals: iTerm2, VS Code, WezTerm can be configured for this
|
|
56
|
+
if event.key in ("ctrl+j", "shift+enter"):
|
|
62
57
|
event.stop()
|
|
63
58
|
event.prevent_default()
|
|
64
|
-
insert = insert_values.get(key, event.character)
|
|
65
|
-
# `insert` is not None because event.character cannot be
|
|
66
|
-
# None because we've checked that it's printable.
|
|
67
|
-
assert insert is not None # noqa: S101
|
|
68
59
|
start, end = self.selection
|
|
69
|
-
self.
|
|
60
|
+
self.replace(
|
|
61
|
+
"\n",
|
|
62
|
+
start,
|
|
63
|
+
end,
|
|
64
|
+
maintain_selection_offset=False,
|
|
65
|
+
)
|
|
66
|
+
return
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from textual.widget import Widget
|
|
4
4
|
|
|
5
|
-
from shotgun.tui.protocols import QAStateProvider
|
|
5
|
+
from shotgun.tui.protocols import QAStateProvider, QuitConfirmationProvider
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class StatusBar(Widget):
|
|
@@ -26,7 +26,11 @@ class StatusBar(Widget):
|
|
|
26
26
|
|
|
27
27
|
def render(self) -> str:
|
|
28
28
|
"""Render the status bar with contextual help text."""
|
|
29
|
-
# Check if
|
|
29
|
+
# Check if quit confirmation is pending (highest priority)
|
|
30
|
+
if isinstance(self.app, QuitConfirmationProvider) and self.app.quit_pending:
|
|
31
|
+
return "[$foreground-muted][bold $warning]Press Ctrl+C again to quit[/] • [bold $text]esc[/] to cancel[/]"
|
|
32
|
+
|
|
33
|
+
# Check if in Q&A mode
|
|
30
34
|
if isinstance(self.screen, QAStateProvider) and self.screen.qa_mode:
|
|
31
35
|
return (
|
|
32
36
|
"[$foreground-muted][bold $text]esc[/] to exit Q&A mode • "
|
|
@@ -36,13 +40,16 @@ class StatusBar(Widget):
|
|
|
36
40
|
if self.working:
|
|
37
41
|
return (
|
|
38
42
|
"[$foreground-muted][bold $text]esc[/] to stop • "
|
|
39
|
-
"[bold $text]enter[/] to send • [bold $text]ctrl+j[/]
|
|
40
|
-
"[bold $text]
|
|
41
|
-
"
|
|
43
|
+
"[bold $text]enter[/] to send • [bold $text]ctrl+j[/] newline • "
|
|
44
|
+
"[bold $text]/[/] command palette • "
|
|
45
|
+
"[bold $text]shift+tab[/] toggle mode • "
|
|
46
|
+
"[bold $text]ctrl+c[/] copy • [bold $text]ctrl+v[/] paste[/]"
|
|
42
47
|
)
|
|
43
48
|
else:
|
|
44
49
|
return (
|
|
45
50
|
"[$foreground-muted][bold $text]enter[/] to send • "
|
|
46
|
-
"[bold $text]ctrl+j[/]
|
|
47
|
-
"[bold $text]
|
|
51
|
+
"[bold $text]ctrl+j[/] newline • "
|
|
52
|
+
"[bold $text]/[/] command palette • "
|
|
53
|
+
"[bold $text]shift+tab[/] toggle mode • "
|
|
54
|
+
"[bold $text]ctrl+c[/] copy • [bold $text]ctrl+v[/] paste[/]"
|
|
48
55
|
)
|