deepy-cli 0.2.24__tar.gz → 0.2.25__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.
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/PKG-INFO +1 -1
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/pyproject.toml +1 -1
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/__init__.py +1 -1
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/agents.py +29 -3
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/app.py +58 -1
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/screens.py +183 -1
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/widgets.py +42 -5
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/file_mentions.py +1 -2
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/terminal.py +5 -2
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/README.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/audit.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/background_tasks.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/cli.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/Read.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/Search.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/Update.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/Write.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/task_list.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/task_output.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/task_stop.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/test_shell.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/data/tools/todo_write.md +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/errors.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/input_suggestions.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/cache_context.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/mcp.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/prompts/init_agents.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/prompts/system.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/session_cost.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/sessions/index.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/sessions/session.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/sessions/store_helpers.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/skill_market.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/skills.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/status.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/subagents.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/todos.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/builtin.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/search.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/shell_output.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tools/test_shell.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/commands.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/compat.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/diff.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/runner.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/tui/state.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/types/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/types/sdk.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/types/tool_payloads.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/audit_approval_panel.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/audit_approval_picker.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/local_command.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/message_view.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/prompt_input.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/skill_picker.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/status_footer.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/usage.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.2.24 → deepy_cli-0.2.25}/src/deepy/utils/notify.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import re
|
|
4
5
|
from copy import deepcopy
|
|
5
6
|
from typing import TYPE_CHECKING, Any
|
|
6
7
|
|
|
@@ -283,7 +284,10 @@ def build_function_tools(
|
|
|
283
284
|
name="Read",
|
|
284
285
|
description=(
|
|
285
286
|
"Read one or more project files or directories. Use files=[...] to read "
|
|
286
|
-
|
|
287
|
+
'multiple targets in one call; use quoted range strings like {"path": '
|
|
288
|
+
'"src/app.py", "range": "80-120"} or {"files": [{"path": '
|
|
289
|
+
'"src/app.py", "range": "80-120"}]}. Use head/tail/offset/limit '
|
|
290
|
+
"for other slices."
|
|
287
291
|
),
|
|
288
292
|
params_json_schema=READ_SCHEMA,
|
|
289
293
|
on_invoke_tool=invoke_read,
|
|
@@ -406,7 +410,7 @@ def _tool_args(
|
|
|
406
410
|
try:
|
|
407
411
|
parsed = json_utils.loads(raw_input or "{}")
|
|
408
412
|
except json_utils.JSONDecodeError as exc:
|
|
409
|
-
repaired, repair_metadata = _repair_tool_arguments(raw_input or "")
|
|
413
|
+
repaired, repair_metadata = _repair_tool_arguments(raw_input or "", tool_name=tool_name)
|
|
410
414
|
if repaired is not None:
|
|
411
415
|
try:
|
|
412
416
|
parsed = json_utils.loads(repaired)
|
|
@@ -460,11 +464,15 @@ def _invalid_tool_arguments_result(
|
|
|
460
464
|
).to_json()
|
|
461
465
|
|
|
462
466
|
|
|
463
|
-
def _repair_tool_arguments(raw_input: str) -> tuple[str | None, dict[str, Any]]:
|
|
467
|
+
def _repair_tool_arguments(raw_input: str, *, tool_name: str) -> tuple[str | None, dict[str, Any]]:
|
|
464
468
|
repaired = raw_input.strip()
|
|
465
469
|
if not repaired:
|
|
466
470
|
return None, {}
|
|
467
471
|
operations: list[str] = []
|
|
472
|
+
if tool_name == "Read":
|
|
473
|
+
repaired, changed = _quote_unquoted_read_ranges(repaired)
|
|
474
|
+
if changed:
|
|
475
|
+
operations.append("read_range_string")
|
|
468
476
|
repaired, changed = _replace_unquoted_python_literals(repaired)
|
|
469
477
|
if changed:
|
|
470
478
|
operations.append("json_literals")
|
|
@@ -481,6 +489,24 @@ def _repair_tool_arguments(raw_input: str) -> tuple[str | None, dict[str, Any]]:
|
|
|
481
489
|
}
|
|
482
490
|
|
|
483
491
|
|
|
492
|
+
def _quote_unquoted_read_ranges(value: str) -> tuple[str, bool]:
|
|
493
|
+
pattern = re.compile(
|
|
494
|
+
r'(?P<lead>(?:^|[,{]\s*))(?P<key>"range"|range)\s*:\s*'
|
|
495
|
+
r'(?P<start>[1-9]\d*)\s*-\s*(?P<end>[1-9]\d*)'
|
|
496
|
+
r'(?P<trail>\s*(?=[,}\]]))'
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
def replace(match: re.Match[str]) -> str:
|
|
500
|
+
return (
|
|
501
|
+
f'{match.group("lead")}"range": '
|
|
502
|
+
f'"{match.group("start")}-{match.group("end")}"'
|
|
503
|
+
f'{match.group("trail")}'
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
repaired, count = pattern.subn(replace, value)
|
|
507
|
+
return repaired, count > 0
|
|
508
|
+
|
|
509
|
+
|
|
484
510
|
def _replace_unquoted_python_literals(value: str) -> tuple[str, bool]:
|
|
485
511
|
replacements = {"None": "null", "True": "true", "False": "false"}
|
|
486
512
|
output: list[str] = []
|
|
@@ -18,6 +18,7 @@ from textual.reactive import var
|
|
|
18
18
|
from textual.widget import MountError
|
|
19
19
|
from textual.widgets import Footer, Header, Label, Static
|
|
20
20
|
|
|
21
|
+
from deepy.audit import ApprovalDecision, AuditModeState, PendingApproval
|
|
21
22
|
from deepy.background_tasks import BackgroundTaskManager, BackgroundTaskSnapshot
|
|
22
23
|
from deepy.config import (
|
|
23
24
|
PROVIDER_CATALOG,
|
|
@@ -79,6 +80,8 @@ from deepy.tui.commands import (
|
|
|
79
80
|
)
|
|
80
81
|
from deepy.tui.diff import diff_view_from_tool_output
|
|
81
82
|
from deepy.tui.screens import (
|
|
83
|
+
AUDIT_APPROVAL_APPROVE,
|
|
84
|
+
AuditApprovalScreen,
|
|
82
85
|
Choice,
|
|
83
86
|
ChoiceScreen,
|
|
84
87
|
InfoScreen,
|
|
@@ -175,6 +178,7 @@ class DeepyTuiApp(App[None]):
|
|
|
175
178
|
Binding("ctrl+d", "confirm_quit", "Quit", priority=True),
|
|
176
179
|
Binding("escape", "interrupt_or_focus_prompt", "Interrupt"),
|
|
177
180
|
Binding("ctrl+o", "toggle_help_panel", "Panel"),
|
|
181
|
+
Binding("shift+tab", "cycle_audit_mode", "Audit", priority=True),
|
|
178
182
|
Binding("alt+up", "focus_previous_block", "Previous block"),
|
|
179
183
|
Binding("alt+down", "focus_next_block", "Next block"),
|
|
180
184
|
]
|
|
@@ -398,6 +402,7 @@ class DeepyTuiApp(App[None]):
|
|
|
398
402
|
self.run_once = run_once
|
|
399
403
|
self.guide_missing_config = guide_missing_config
|
|
400
404
|
self.controller = TuiController(settings=settings)
|
|
405
|
+
self.audit_state = AuditModeState(settings.audit.mode)
|
|
401
406
|
self.input_suggestions = InputSuggestionController(
|
|
402
407
|
enabled=settings.ui.input_suggestions_enabled
|
|
403
408
|
)
|
|
@@ -622,6 +627,7 @@ class DeepyTuiApp(App[None]):
|
|
|
622
627
|
"- **Ctrl+J** - insert newline\n"
|
|
623
628
|
"- **Ctrl+P** - command palette\n"
|
|
624
629
|
"- **Ctrl+O** - toggle side panel\n"
|
|
630
|
+
"- **Shift+Tab** - cycle audit mode\n"
|
|
625
631
|
"- **Alt+Up / Alt+Down** - move between transcript blocks\n"
|
|
626
632
|
"- **Ctrl+Up / Ctrl+Down** - prompt history\n"
|
|
627
633
|
"- **Ctrl+D twice** - exit",
|
|
@@ -651,6 +657,7 @@ class DeepyTuiApp(App[None]):
|
|
|
651
657
|
f"- Session: `{self.state.session_id or 'new'}`",
|
|
652
658
|
f"- Theme: `{self.settings.ui.theme}`",
|
|
653
659
|
f"- View: `{self.settings.ui.view_mode}`",
|
|
660
|
+
f"- Audit: `{_format_tui_audit_mode(self.audit_state, self.settings)}`",
|
|
654
661
|
f"- Input suggestions: `{'enabled' if self.settings.ui.input_suggestions_enabled else 'disabled'}`",
|
|
655
662
|
f"- Loaded skills: `{', '.join(self.controller.loaded_skill_names) or 'none'}`",
|
|
656
663
|
f"- Sessions: `{report.session_count}`",
|
|
@@ -1357,6 +1364,8 @@ class DeepyTuiApp(App[None]):
|
|
|
1357
1364
|
session_id=self.state.session_id,
|
|
1358
1365
|
skill_names=skill_names,
|
|
1359
1366
|
background_tasks=self.background_tasks,
|
|
1367
|
+
audit_mode=self.audit_state,
|
|
1368
|
+
approval_resolver=self._tui_approval_resolver,
|
|
1360
1369
|
)
|
|
1361
1370
|
except Exception as exc:
|
|
1362
1371
|
self.post_message(TurnFailedMessage(exc))
|
|
@@ -1393,6 +1402,27 @@ class DeepyTuiApp(App[None]):
|
|
|
1393
1402
|
await self._append_block(ErrorBlock(str(message.error)))
|
|
1394
1403
|
self._update_status("Error")
|
|
1395
1404
|
|
|
1405
|
+
async def _tui_approval_resolver(self, pending: list[PendingApproval]) -> list[ApprovalDecision]:
|
|
1406
|
+
decisions: list[ApprovalDecision] = []
|
|
1407
|
+
for item in pending:
|
|
1408
|
+
choice = await self.push_screen_wait(
|
|
1409
|
+
AuditApprovalScreen(
|
|
1410
|
+
item,
|
|
1411
|
+
project_root=self.project_root,
|
|
1412
|
+
width=max(40, self.size.width - 6),
|
|
1413
|
+
)
|
|
1414
|
+
)
|
|
1415
|
+
approved = choice == AUDIT_APPROVAL_APPROVE
|
|
1416
|
+
decisions.append(
|
|
1417
|
+
ApprovalDecision(
|
|
1418
|
+
outcome="approve" if approved else "reject",
|
|
1419
|
+
rejection_message=None
|
|
1420
|
+
if approved
|
|
1421
|
+
else "Tool execution was rejected by the user audit approval decision.",
|
|
1422
|
+
)
|
|
1423
|
+
)
|
|
1424
|
+
return decisions
|
|
1425
|
+
|
|
1396
1426
|
@on(QuestionBlock.Answered)
|
|
1397
1427
|
async def on_question_answered(self, message: QuestionBlock.Answered) -> None:
|
|
1398
1428
|
message.stop()
|
|
@@ -1686,6 +1716,7 @@ class DeepyTuiApp(App[None]):
|
|
|
1686
1716
|
self.state.session_id,
|
|
1687
1717
|
self.controller.loaded_skill_names,
|
|
1688
1718
|
self._todo_text,
|
|
1719
|
+
audit_state=self.audit_state,
|
|
1689
1720
|
)
|
|
1690
1721
|
)
|
|
1691
1722
|
|
|
@@ -1707,8 +1738,13 @@ class DeepyTuiApp(App[None]):
|
|
|
1707
1738
|
project_root=self.project_root,
|
|
1708
1739
|
settings=self.settings,
|
|
1709
1740
|
background_tasks=self.background_tasks,
|
|
1741
|
+
audit_state=self.audit_state,
|
|
1710
1742
|
)
|
|
1711
1743
|
|
|
1744
|
+
def action_cycle_audit_mode(self) -> None:
|
|
1745
|
+
mode = self.audit_state.cycle()
|
|
1746
|
+
self._update_status(f"Audit {mode.value}")
|
|
1747
|
+
|
|
1712
1748
|
def action_confirm_quit(self) -> None:
|
|
1713
1749
|
if self.state.quit_confirm_pending:
|
|
1714
1750
|
self._exit_with_summary()
|
|
@@ -1810,7 +1846,9 @@ class DeepyTuiApp(App[None]):
|
|
|
1810
1846
|
self.state = request_interrupt(self.state)
|
|
1811
1847
|
self._update_status("Interrupt requested")
|
|
1812
1848
|
return
|
|
1813
|
-
self.query_one("#prompt-input", PromptTextArea)
|
|
1849
|
+
prompt = self.query_one("#prompt-input", PromptTextArea)
|
|
1850
|
+
prompt.prepare_clear_on_next_delete()
|
|
1851
|
+
prompt.focus()
|
|
1814
1852
|
|
|
1815
1853
|
def action_focus_next_block(self) -> None:
|
|
1816
1854
|
blocks = list(self.query(".transcript-block"))
|
|
@@ -2103,11 +2141,13 @@ def _build_tui_status_context(
|
|
|
2103
2141
|
project_root: Path,
|
|
2104
2142
|
settings: Settings,
|
|
2105
2143
|
background_tasks: BackgroundTaskManager | None = None,
|
|
2144
|
+
audit_state: AuditModeState | None = None,
|
|
2106
2145
|
) -> str:
|
|
2107
2146
|
segments = [
|
|
2108
2147
|
f"provider {settings.model.provider}",
|
|
2109
2148
|
f"model {settings.model.name}[{settings.model.reasoning_mode}]",
|
|
2110
2149
|
f"cwd {format_home_relative_path(project_root)}",
|
|
2150
|
+
f"audit {_active_tui_audit_mode(audit_state, settings)}",
|
|
2111
2151
|
]
|
|
2112
2152
|
if has_agents_instructions(project_root):
|
|
2113
2153
|
segments.append("[AGENTS.md]")
|
|
@@ -2145,6 +2185,8 @@ def _format_tui_side_status(
|
|
|
2145
2185
|
session_id: str | None,
|
|
2146
2186
|
loaded_skill_names: list[str],
|
|
2147
2187
|
todo_text: str,
|
|
2188
|
+
*,
|
|
2189
|
+
audit_state: AuditModeState | None = None,
|
|
2148
2190
|
) -> str:
|
|
2149
2191
|
session_entry = _tui_session_entry(project_root, session_id)
|
|
2150
2192
|
lines = [
|
|
@@ -2152,6 +2194,7 @@ def _format_tui_side_status(
|
|
|
2152
2194
|
f"Provider: {settings.model.provider}",
|
|
2153
2195
|
f"Model: {settings.model.name}",
|
|
2154
2196
|
f"Thinking: {settings.model.reasoning_mode}",
|
|
2197
|
+
f"Audit: {_format_tui_audit_mode(audit_state, settings)}",
|
|
2155
2198
|
f"Session: {session_id or 'new'}",
|
|
2156
2199
|
f"Cache: {_format_tui_cache_status(session_entry)}",
|
|
2157
2200
|
f"Skills: {', '.join(loaded_skill_names) or 'none'}",
|
|
@@ -2161,6 +2204,20 @@ def _format_tui_side_status(
|
|
|
2161
2204
|
return "\n".join(lines)
|
|
2162
2205
|
|
|
2163
2206
|
|
|
2207
|
+
def _active_tui_audit_mode(audit_state: AuditModeState | None, settings: Settings) -> str:
|
|
2208
|
+
if audit_state is not None:
|
|
2209
|
+
return audit_state.mode.value
|
|
2210
|
+
return settings.audit.mode.value
|
|
2211
|
+
|
|
2212
|
+
|
|
2213
|
+
def _format_tui_audit_mode(audit_state: AuditModeState | None, settings: Settings) -> str:
|
|
2214
|
+
active = _active_tui_audit_mode(audit_state, settings)
|
|
2215
|
+
configured = settings.audit.mode.value
|
|
2216
|
+
if active == configured:
|
|
2217
|
+
return active
|
|
2218
|
+
return f"{active} (runtime, config {configured})"
|
|
2219
|
+
|
|
2220
|
+
|
|
2164
2221
|
def _format_tui_cache_status(session_entry: Any | None) -> str:
|
|
2165
2222
|
if session_entry is None:
|
|
2166
2223
|
return "unknown"
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
from typing import Literal
|
|
5
6
|
|
|
6
7
|
from textual import on
|
|
7
8
|
from textual.app import ComposeResult
|
|
8
9
|
from textual.binding import Binding
|
|
9
|
-
from textual.containers import Vertical
|
|
10
|
+
from textual.containers import Vertical, VerticalScroll
|
|
10
11
|
from textual.screen import ModalScreen
|
|
11
12
|
from textual.widgets import Footer, Input, Label, Markdown, OptionList, Static
|
|
12
13
|
from textual.widgets.option_list import Option
|
|
13
14
|
|
|
15
|
+
from deepy.audit import PendingApproval
|
|
16
|
+
from deepy.ui.audit_approval_panel import build_approval_view
|
|
17
|
+
from deepy.ui.styles import DARK_PALETTE, UiPalette
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
AUDIT_APPROVAL_APPROVE = "approve"
|
|
21
|
+
AUDIT_APPROVAL_REJECT = "reject"
|
|
22
|
+
|
|
14
23
|
|
|
15
24
|
class InfoScreen(ModalScreen[None]):
|
|
16
25
|
BINDINGS = [
|
|
@@ -55,6 +64,179 @@ class InfoScreen(ModalScreen[None]):
|
|
|
55
64
|
self.dismiss(None)
|
|
56
65
|
|
|
57
66
|
|
|
67
|
+
class AuditApprovalScreen(ModalScreen[str]):
|
|
68
|
+
BINDINGS = [
|
|
69
|
+
Binding("escape", "dismiss", "Reject"),
|
|
70
|
+
Binding("y", "ignore_letter_shortcut", show=False),
|
|
71
|
+
Binding("a", "ignore_letter_shortcut", show=False),
|
|
72
|
+
Binding("n", "ignore_letter_shortcut", show=False),
|
|
73
|
+
Binding("r", "ignore_letter_shortcut", show=False),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
CSS = """
|
|
77
|
+
AuditApprovalScreen {
|
|
78
|
+
align: center middle;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
AuditApprovalScreen > Vertical {
|
|
82
|
+
width: 112;
|
|
83
|
+
max-width: 98%;
|
|
84
|
+
height: auto;
|
|
85
|
+
max-height: 92%;
|
|
86
|
+
border: round $warning;
|
|
87
|
+
background: $surface;
|
|
88
|
+
padding: 1 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
AuditApprovalScreen > Vertical.-has-preview {
|
|
92
|
+
height: 92%;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
AuditApprovalScreen .approval-summary {
|
|
96
|
+
margin-top: 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
AuditApprovalScreen .approval-preview {
|
|
100
|
+
height: 1fr;
|
|
101
|
+
max-height: 1fr;
|
|
102
|
+
margin-top: 1;
|
|
103
|
+
border: tall $warning;
|
|
104
|
+
padding: 0 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
AuditApprovalScreen OptionList {
|
|
108
|
+
height: 4;
|
|
109
|
+
max-height: 4;
|
|
110
|
+
margin-top: 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
AuditApprovalScreen .screen-help {
|
|
114
|
+
color: $text-muted;
|
|
115
|
+
margin: 1 0 0 0;
|
|
116
|
+
}
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
item: PendingApproval,
|
|
122
|
+
*,
|
|
123
|
+
project_root: str | Path | None = None,
|
|
124
|
+
palette: UiPalette | None = None,
|
|
125
|
+
width: int | None = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
super().__init__()
|
|
128
|
+
self.item = item
|
|
129
|
+
self.project_root = project_root
|
|
130
|
+
self.palette = palette or DARK_PALETTE
|
|
131
|
+
self.width = width
|
|
132
|
+
self._title_label: Label | None = None
|
|
133
|
+
self._summary: Static | None = None
|
|
134
|
+
self._container: Vertical | None = None
|
|
135
|
+
self._preview_container: VerticalScroll | None = None
|
|
136
|
+
self._preview: Static | None = None
|
|
137
|
+
self._options: OptionList | None = None
|
|
138
|
+
|
|
139
|
+
def compose(self) -> ComposeResult:
|
|
140
|
+
self._container = Vertical()
|
|
141
|
+
with self._container:
|
|
142
|
+
self._title_label = Label("", id="approval-title", classes="block-title")
|
|
143
|
+
self._summary = Static("", id="approval-summary", classes="approval-summary")
|
|
144
|
+
self._options = OptionList(id="approval-options")
|
|
145
|
+
yield self._title_label
|
|
146
|
+
yield self._summary
|
|
147
|
+
self._preview_container = VerticalScroll(id="approval-preview", classes="approval-preview")
|
|
148
|
+
with self._preview_container:
|
|
149
|
+
self._preview = Static("", id="approval-preview-content")
|
|
150
|
+
yield self._preview
|
|
151
|
+
yield self._options
|
|
152
|
+
yield Static("Use Up/Down to select, Enter to activate, Esc to reject.", classes="screen-help")
|
|
153
|
+
|
|
154
|
+
def on_mount(self) -> None:
|
|
155
|
+
self._refresh_view()
|
|
156
|
+
self._approval_options().focus()
|
|
157
|
+
|
|
158
|
+
@on(OptionList.OptionSelected, "#approval-options")
|
|
159
|
+
def on_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
160
|
+
event.stop()
|
|
161
|
+
option_id = str(event.option_id or "")
|
|
162
|
+
if option_id == AUDIT_APPROVAL_APPROVE:
|
|
163
|
+
self.dismiss(AUDIT_APPROVAL_APPROVE)
|
|
164
|
+
return
|
|
165
|
+
if option_id == AUDIT_APPROVAL_REJECT:
|
|
166
|
+
self.dismiss(AUDIT_APPROVAL_REJECT)
|
|
167
|
+
|
|
168
|
+
def action_ignore_letter_shortcut(self) -> None:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
async def action_dismiss(self, result: str | None = None) -> None:
|
|
172
|
+
self.dismiss(AUDIT_APPROVAL_REJECT)
|
|
173
|
+
|
|
174
|
+
def _refresh_view(self) -> None:
|
|
175
|
+
view = build_approval_view(
|
|
176
|
+
self.item,
|
|
177
|
+
palette=self.palette,
|
|
178
|
+
project_root=self.project_root,
|
|
179
|
+
expanded=True,
|
|
180
|
+
width=self.width,
|
|
181
|
+
)
|
|
182
|
+
self._approval_title().update(view.title)
|
|
183
|
+
summary = f"{view.target_label}: {view.target or '-'}"
|
|
184
|
+
if view.metadata:
|
|
185
|
+
summary += "\n" + "\n".join(f"{label}: {value}" for label, value in view.metadata)
|
|
186
|
+
self._approval_summary().update(summary)
|
|
187
|
+
preview = self._approval_preview()
|
|
188
|
+
preview_container = self._approval_preview_container()
|
|
189
|
+
container = self._approval_container()
|
|
190
|
+
if view.preview is None:
|
|
191
|
+
preview.update("")
|
|
192
|
+
preview_container.display = False
|
|
193
|
+
container.set_class(False, "-has-preview")
|
|
194
|
+
else:
|
|
195
|
+
preview.update(view.preview)
|
|
196
|
+
preview_container.display = True
|
|
197
|
+
container.set_class(True, "-has-preview")
|
|
198
|
+
options = self._approval_options()
|
|
199
|
+
options.clear_options()
|
|
200
|
+
options.add_options(
|
|
201
|
+
[
|
|
202
|
+
Option("Approve", id=AUDIT_APPROVAL_APPROVE),
|
|
203
|
+
Option("Reject", id=AUDIT_APPROVAL_REJECT),
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
options.highlighted = 0
|
|
207
|
+
self.call_after_refresh(options.refresh)
|
|
208
|
+
|
|
209
|
+
def _approval_title(self) -> Label:
|
|
210
|
+
if self._title_label is None:
|
|
211
|
+
raise RuntimeError("Approval title is not mounted.")
|
|
212
|
+
return self._title_label
|
|
213
|
+
|
|
214
|
+
def _approval_summary(self) -> Static:
|
|
215
|
+
if self._summary is None:
|
|
216
|
+
raise RuntimeError("Approval summary is not mounted.")
|
|
217
|
+
return self._summary
|
|
218
|
+
|
|
219
|
+
def _approval_container(self) -> Vertical:
|
|
220
|
+
if self._container is None:
|
|
221
|
+
raise RuntimeError("Approval container is not mounted.")
|
|
222
|
+
return self._container
|
|
223
|
+
|
|
224
|
+
def _approval_preview(self) -> Static:
|
|
225
|
+
if self._preview is None:
|
|
226
|
+
raise RuntimeError("Approval preview is not mounted.")
|
|
227
|
+
return self._preview
|
|
228
|
+
|
|
229
|
+
def _approval_preview_container(self) -> VerticalScroll:
|
|
230
|
+
if self._preview_container is None:
|
|
231
|
+
raise RuntimeError("Approval preview container is not mounted.")
|
|
232
|
+
return self._preview_container
|
|
233
|
+
|
|
234
|
+
def _approval_options(self) -> OptionList:
|
|
235
|
+
if self._options is None:
|
|
236
|
+
raise RuntimeError("Approval option list is not mounted.")
|
|
237
|
+
return self._options
|
|
238
|
+
|
|
239
|
+
|
|
58
240
|
@dataclass(frozen=True)
|
|
59
241
|
class Choice:
|
|
60
242
|
label: str
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
import time
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import cast
|
|
6
7
|
|
|
@@ -95,6 +96,10 @@ class _KeyboardProtocolTextMixin:
|
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
class PromptTextArea(_KeyboardProtocolTextMixin, TextArea):
|
|
99
|
+
_clear_draft_delete_deadline: float | None = None
|
|
100
|
+
_CLEAR_DRAFT_DELETE_WINDOW_SECONDS = 2.0
|
|
101
|
+
_clock = staticmethod(time.monotonic)
|
|
102
|
+
|
|
98
103
|
BINDINGS = [
|
|
99
104
|
Binding("enter", "submit", "Send", priority=True),
|
|
100
105
|
Binding("ctrl+j", "newline", "Newline", priority=True),
|
|
@@ -119,9 +124,26 @@ class PromptTextArea(_KeyboardProtocolTextMixin, TextArea):
|
|
|
119
124
|
|
|
120
125
|
@on(TextArea.Changed)
|
|
121
126
|
def on_keyboard_protocol_text_changed(self, event: TextArea.Changed) -> None:
|
|
127
|
+
if self._clear_draft_delete_deadline is not None and not self._normalizing_keyboard_protocol_text:
|
|
128
|
+
self._clear_draft_delete_deadline = None
|
|
122
129
|
if self._normalize_keyboard_protocol_text():
|
|
123
130
|
event.stop()
|
|
124
131
|
|
|
132
|
+
def prepare_clear_on_next_delete(self) -> None:
|
|
133
|
+
self._clear_draft_delete_deadline = (
|
|
134
|
+
self._clock() + self._CLEAR_DRAFT_DELETE_WINDOW_SECONDS
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def action_delete_left(self) -> None:
|
|
138
|
+
if self._clear_draft_if_pending():
|
|
139
|
+
return
|
|
140
|
+
super().action_delete_left()
|
|
141
|
+
|
|
142
|
+
def action_delete_right(self) -> None:
|
|
143
|
+
if self._clear_draft_if_pending():
|
|
144
|
+
return
|
|
145
|
+
super().action_delete_right()
|
|
146
|
+
|
|
125
147
|
def action_submit(self) -> None:
|
|
126
148
|
panel = self.parent
|
|
127
149
|
if isinstance(panel, PromptPanel) and panel.accept_selected_suggestion():
|
|
@@ -173,6 +195,18 @@ class PromptTextArea(_KeyboardProtocolTextMixin, TextArea):
|
|
|
173
195
|
return
|
|
174
196
|
super().action_cursor_down(select)
|
|
175
197
|
|
|
198
|
+
def _clear_draft_if_pending(self) -> bool:
|
|
199
|
+
deadline = self._clear_draft_delete_deadline
|
|
200
|
+
if deadline is None:
|
|
201
|
+
return False
|
|
202
|
+
self._clear_draft_delete_deadline = None
|
|
203
|
+
if self._clock() > deadline:
|
|
204
|
+
return False
|
|
205
|
+
if not self.text:
|
|
206
|
+
return False
|
|
207
|
+
self.clear()
|
|
208
|
+
return True
|
|
209
|
+
|
|
176
210
|
|
|
177
211
|
class QuestionTextArea(_KeyboardProtocolTextMixin, TextArea):
|
|
178
212
|
BINDINGS = [
|
|
@@ -322,11 +356,14 @@ class PromptPanel(Vertical):
|
|
|
322
356
|
mention = extract_file_mention_fragment(text)
|
|
323
357
|
if mention is None:
|
|
324
358
|
return []
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
359
|
+
if "/" not in mention.fragment:
|
|
360
|
+
paths = (
|
|
361
|
+
self.discovery.top_level_paths()
|
|
362
|
+
if not mention.fragment
|
|
363
|
+
else self.discovery.deep_paths()
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
paths = self.discovery.deep_paths(mention.fragment.rsplit("/", 1)[0])
|
|
330
367
|
return [f"@{path}" for path in rank_file_mention_candidates(paths, mention.fragment)]
|
|
331
368
|
|
|
332
369
|
def _apply_suggestion(self, text: str, suggestion: str) -> str:
|
|
@@ -14,7 +14,6 @@ from prompt_toolkit.document import Document
|
|
|
14
14
|
|
|
15
15
|
DEFAULT_FILE_MENTION_LIMIT = 1000
|
|
16
16
|
DEFAULT_FILE_MENTION_REFRESH_INTERVAL = 2.0
|
|
17
|
-
DEEP_SEARCH_MIN_FRAGMENT_LENGTH = 3
|
|
18
17
|
|
|
19
18
|
_IGNORED_NAMES = frozenset(
|
|
20
19
|
{
|
|
@@ -292,7 +291,7 @@ class FileMentionCompleter(Completer):
|
|
|
292
291
|
)
|
|
293
292
|
|
|
294
293
|
def _paths_for_fragment(self, fragment: str) -> list[str]:
|
|
295
|
-
if "/" not in fragment and
|
|
294
|
+
if "/" not in fragment and not fragment:
|
|
296
295
|
return self._discovery.top_level_paths()
|
|
297
296
|
scope = fragment.rsplit("/", 1)[0] if "/" in fragment else None
|
|
298
297
|
return self._discovery.deep_paths(scope)
|
|
@@ -104,6 +104,7 @@ from deepy.ui.local_command import (
|
|
|
104
104
|
shell_tool_result_json,
|
|
105
105
|
)
|
|
106
106
|
from deepy.ui.message_view import (
|
|
107
|
+
ToolOutputView,
|
|
107
108
|
format_tool_display_name,
|
|
108
109
|
format_tool_display_label,
|
|
109
110
|
format_tool_call_summary,
|
|
@@ -3981,7 +3982,7 @@ def _print_stream_event(
|
|
|
3981
3982
|
call_summary = call.summary if call is not None else ""
|
|
3982
3983
|
summary = (
|
|
3983
3984
|
_audit_rejection_tool_summary(call.name if call is not None else view.name)
|
|
3984
|
-
if _is_audit_rejection_tool_output(event.text)
|
|
3985
|
+
if _is_audit_rejection_tool_output(event.text, view)
|
|
3985
3986
|
else format_tool_progress_summary(call_summary, event.text)
|
|
3986
3987
|
)
|
|
3987
3988
|
diff = render_tool_diff_preview(
|
|
@@ -4021,7 +4022,9 @@ def _stream_event_writes_terminal(event: DeepyStreamEvent) -> bool:
|
|
|
4021
4022
|
return event.kind in {"tool_output", "status"}
|
|
4022
4023
|
|
|
4023
4024
|
|
|
4024
|
-
def _is_audit_rejection_tool_output(output: str) -> bool:
|
|
4025
|
+
def _is_audit_rejection_tool_output(output: str, view: ToolOutputView) -> bool:
|
|
4026
|
+
if view.ok is True:
|
|
4027
|
+
return False
|
|
4025
4028
|
normalized = output.strip().lower()
|
|
4026
4029
|
return "audit approval" in normalized and "reject" in normalized
|
|
4027
4030
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|