windcode 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.
- windcode/__init__.py +6 -0
- windcode/__main__.py +4 -0
- windcode/auth/__init__.py +11 -0
- windcode/auth/store.py +90 -0
- windcode/cli.py +181 -0
- windcode/config/__init__.py +41 -0
- windcode/config/loader.py +117 -0
- windcode/config/models.py +203 -0
- windcode/config/writer.py +74 -0
- windcode/context/__init__.py +18 -0
- windcode/context/compactor.py +72 -0
- windcode/context/estimator.py +89 -0
- windcode/context/truncation.py +87 -0
- windcode/domain/__init__.py +1 -0
- windcode/domain/errors.py +33 -0
- windcode/domain/events.py +597 -0
- windcode/domain/messages.py +226 -0
- windcode/domain/models.py +73 -0
- windcode/domain/subagents.py +263 -0
- windcode/domain/tools.py +55 -0
- windcode/extensions/__init__.py +27 -0
- windcode/extensions/commands.py +25 -0
- windcode/extensions/discovery.py +139 -0
- windcode/extensions/events.py +58 -0
- windcode/extensions/hooks/__init__.py +4 -0
- windcode/extensions/hooks/dispatcher.py +107 -0
- windcode/extensions/hooks/executor.py +49 -0
- windcode/extensions/hooks/loader.py +101 -0
- windcode/extensions/hooks/models.py +109 -0
- windcode/extensions/mcp/__init__.py +10 -0
- windcode/extensions/mcp/adapter.py +101 -0
- windcode/extensions/mcp/catalog.py +153 -0
- windcode/extensions/mcp/client.py +273 -0
- windcode/extensions/mcp/runtime.py +144 -0
- windcode/extensions/mcp/tools.py +570 -0
- windcode/extensions/models.py +168 -0
- windcode/extensions/paths.py +71 -0
- windcode/extensions/plugins/__init__.py +3 -0
- windcode/extensions/plugins/installer.py +93 -0
- windcode/extensions/plugins/manifest.py +166 -0
- windcode/extensions/runtime.py +366 -0
- windcode/extensions/service.py +437 -0
- windcode/extensions/skills/__init__.py +3 -0
- windcode/extensions/skills/loader.py +67 -0
- windcode/extensions/skills/parser.py +57 -0
- windcode/extensions/skills/tools.py +213 -0
- windcode/extensions/snapshot.py +57 -0
- windcode/extensions/state.py +158 -0
- windcode/instructions/__init__.py +8 -0
- windcode/instructions/loader.py +50 -0
- windcode/memory/__init__.py +57 -0
- windcode/memory/extraction.py +83 -0
- windcode/memory/models.py +218 -0
- windcode/memory/refiner.py +189 -0
- windcode/memory/security.py +23 -0
- windcode/memory/service.py +179 -0
- windcode/memory/store.py +362 -0
- windcode/observability/__init__.py +4 -0
- windcode/observability/redaction.py +95 -0
- windcode/observability/trace.py +115 -0
- windcode/policy/__init__.py +22 -0
- windcode/policy/engine.py +137 -0
- windcode/policy/models.py +69 -0
- windcode/providers/__init__.py +29 -0
- windcode/providers/_utils.py +19 -0
- windcode/providers/anthropic.py +215 -0
- windcode/providers/base.py +46 -0
- windcode/providers/catalog.py +119 -0
- windcode/providers/errors.py +96 -0
- windcode/providers/openai_compat.py +231 -0
- windcode/providers/openai_responses.py +189 -0
- windcode/providers/registry.py +105 -0
- windcode/runtime/__init__.py +15 -0
- windcode/runtime/control.py +89 -0
- windcode/runtime/event_bus.py +67 -0
- windcode/runtime/loop.py +510 -0
- windcode/runtime/prompts.py +132 -0
- windcode/runtime/report.py +64 -0
- windcode/runtime/retry.py +73 -0
- windcode/runtime/scheduler.py +194 -0
- windcode/runtime/subagents/__init__.py +28 -0
- windcode/runtime/subagents/approvals.py +88 -0
- windcode/runtime/subagents/budgets.py +79 -0
- windcode/runtime/subagents/coordinator.py +714 -0
- windcode/runtime/subagents/factory.py +311 -0
- windcode/runtime/subagents/roles.py +74 -0
- windcode/runtime/subagents/verification.py +53 -0
- windcode/sandbox/__init__.py +3 -0
- windcode/sandbox/bwrap.py +79 -0
- windcode/sdk.py +1259 -0
- windcode/sessions/__init__.py +23 -0
- windcode/sessions/artifacts.py +69 -0
- windcode/sessions/models.py +104 -0
- windcode/sessions/store.py +167 -0
- windcode/sessions/tree.py +35 -0
- windcode/tools/__init__.py +10 -0
- windcode/tools/apply_patch.py +191 -0
- windcode/tools/ask_user.py +58 -0
- windcode/tools/builtins.py +44 -0
- windcode/tools/edit_file.py +54 -0
- windcode/tools/filesystem.py +77 -0
- windcode/tools/glob.py +35 -0
- windcode/tools/grep.py +66 -0
- windcode/tools/memory.py +400 -0
- windcode/tools/read_file.py +54 -0
- windcode/tools/registry.py +120 -0
- windcode/tools/shell.py +159 -0
- windcode/tools/subagents/__init__.py +31 -0
- windcode/tools/subagents/cancel.py +35 -0
- windcode/tools/subagents/integrate.py +54 -0
- windcode/tools/subagents/list.py +46 -0
- windcode/tools/subagents/spawn.py +86 -0
- windcode/tools/subagents/wait.py +74 -0
- windcode/tools/write_file.py +61 -0
- windcode/tui/__init__.py +3 -0
- windcode/tui/app.py +1015 -0
- windcode/tui/commands.py +90 -0
- windcode/tui/permission_display.py +24 -0
- windcode/tui/styles.tcss +591 -0
- windcode/tui/widgets/__init__.py +31 -0
- windcode/tui/widgets/approval.py +98 -0
- windcode/tui/widgets/command_menu.py +90 -0
- windcode/tui/widgets/extensions.py +48 -0
- windcode/tui/widgets/input.py +110 -0
- windcode/tui/widgets/memory.py +143 -0
- windcode/tui/widgets/messages.py +299 -0
- windcode/tui/widgets/models.py +565 -0
- windcode/tui/widgets/question.py +46 -0
- windcode/tui/widgets/sessions.py +68 -0
- windcode/tui/widgets/status.py +46 -0
- windcode/tui/widgets/subagents.py +195 -0
- windcode/tui/widgets/tools.py +66 -0
- windcode/tui/widgets/welcome.py +149 -0
- windcode/types.py +80 -0
- windcode/worktrees/__init__.py +24 -0
- windcode/worktrees/git.py +92 -0
- windcode/worktrees/manager.py +265 -0
- windcode/worktrees/models.py +62 -0
- windcode-0.1.0.dist-info/METADATA +207 -0
- windcode-0.1.0.dist-info/RECORD +143 -0
- windcode-0.1.0.dist-info/WHEEL +4 -0
- windcode-0.1.0.dist-info/entry_points.txt +2 -0
- windcode-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from rich.markup import escape
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.binding import Binding, BindingType
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
|
|
12
|
+
from windcode.domain.events import ApprovalRequested
|
|
13
|
+
|
|
14
|
+
CHOICE_LABELS = {
|
|
15
|
+
"allow_once": "仅本次允许",
|
|
16
|
+
"allow_session": "会话内允许",
|
|
17
|
+
"deny": "拒绝",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ApprovalWidget(Vertical, can_focus=True):
|
|
22
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
23
|
+
Binding("up", "cursor_up", "向上", priority=True),
|
|
24
|
+
Binding("down", "cursor_down", "向下", priority=True),
|
|
25
|
+
Binding("enter", "select", "选择", priority=True),
|
|
26
|
+
Binding("escape", "deny", "拒绝", priority=True),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
class Decision(Message):
|
|
30
|
+
def __init__(self, request_id: str, decision: str) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.request_id = request_id
|
|
33
|
+
self.decision = decision
|
|
34
|
+
|
|
35
|
+
def __init__(self, request: ApprovalRequested) -> None:
|
|
36
|
+
super().__init__(classes="interaction", id=f"approval-{request.request_id}")
|
|
37
|
+
self.request = request
|
|
38
|
+
self.cursor = 0
|
|
39
|
+
|
|
40
|
+
def compose(self) -> ComposeResult:
|
|
41
|
+
yield Static(self._build_content(), id="approval-content")
|
|
42
|
+
|
|
43
|
+
def on_mount(self) -> None:
|
|
44
|
+
self.focus()
|
|
45
|
+
|
|
46
|
+
def _build_content(self) -> str:
|
|
47
|
+
risk = {"low": "低风险", "medium": "中风险", "high": "高风险"}.get(
|
|
48
|
+
self.request.risk, self.request.risk
|
|
49
|
+
)
|
|
50
|
+
lines = [
|
|
51
|
+
f"\n [bold yellow]需要授权 · {risk}[/bold yellow]\n",
|
|
52
|
+
]
|
|
53
|
+
if self.request.subagent_id is not None:
|
|
54
|
+
role = escape(self.request.subagent_role or "unknown")
|
|
55
|
+
subagent_id = escape(self.request.subagent_id)
|
|
56
|
+
lines.append(f" [bold cyan]子智能体 {subagent_id} · {role}[/bold cyan]")
|
|
57
|
+
if self.request.tool_name:
|
|
58
|
+
tool_name = escape(self.request.tool_name)
|
|
59
|
+
arguments = (
|
|
60
|
+
None
|
|
61
|
+
if self.request.arguments_summary is None
|
|
62
|
+
else escape(self.request.arguments_summary)
|
|
63
|
+
)
|
|
64
|
+
if self.request.tool_name == "shell" and arguments:
|
|
65
|
+
lines.append(f" [bold cyan]bash:[/bold cyan] {arguments}")
|
|
66
|
+
elif arguments:
|
|
67
|
+
lines.append(f" [bold cyan]{tool_name}:[/bold cyan] {arguments}")
|
|
68
|
+
else:
|
|
69
|
+
lines.append(f" 工具: {tool_name}")
|
|
70
|
+
summary = escape(self.request.summary)
|
|
71
|
+
lines.extend((f" {summary}\n", " [dim]是否继续执行?[/dim]\n"))
|
|
72
|
+
for index, choice in enumerate(self.request.choices):
|
|
73
|
+
label = CHOICE_LABELS.get(choice, choice)
|
|
74
|
+
if index == self.cursor:
|
|
75
|
+
lines.append(f" [bold cyan]>[/bold cyan] {index + 1}. [bold]{label}[/bold]")
|
|
76
|
+
else:
|
|
77
|
+
lines.append(f" {index + 1}. [dim]{label}[/dim]")
|
|
78
|
+
return "\n".join(lines)
|
|
79
|
+
|
|
80
|
+
def _refresh(self) -> None:
|
|
81
|
+
self.query_one("#approval-content", Static).update(self._build_content())
|
|
82
|
+
|
|
83
|
+
def action_cursor_up(self) -> None:
|
|
84
|
+
if self.cursor > 0:
|
|
85
|
+
self.cursor -= 1
|
|
86
|
+
self._refresh()
|
|
87
|
+
|
|
88
|
+
def action_cursor_down(self) -> None:
|
|
89
|
+
if self.cursor < len(self.request.choices) - 1:
|
|
90
|
+
self.cursor += 1
|
|
91
|
+
self._refresh()
|
|
92
|
+
|
|
93
|
+
def action_select(self) -> None:
|
|
94
|
+
decision = self.request.choices[self.cursor]
|
|
95
|
+
self.post_message(self.Decision(self.request.request_id, decision))
|
|
96
|
+
|
|
97
|
+
def action_deny(self) -> None:
|
|
98
|
+
self.post_message(self.Decision(self.request.request_id, "deny"))
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rich.markup import escape
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
|
|
6
|
+
from windcode.tui.commands import CompletionDefinition
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CommandMenu(Static):
|
|
10
|
+
"""Keyboard-driven command and Skill candidates shown above the prompt."""
|
|
11
|
+
|
|
12
|
+
visible_items = 8
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
name: str | None = None,
|
|
18
|
+
id: str | None = None,
|
|
19
|
+
classes: str | None = None,
|
|
20
|
+
disabled: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__("", name=name, id=id, classes=classes, disabled=disabled)
|
|
23
|
+
self._items: tuple[CompletionDefinition, ...] = ()
|
|
24
|
+
self._cursor = 0
|
|
25
|
+
self.display = False
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def is_open(self) -> bool:
|
|
29
|
+
return bool(self.display and self._items)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def items(self) -> tuple[CompletionDefinition, ...]:
|
|
33
|
+
return self._items
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def cursor(self) -> int:
|
|
37
|
+
return self._cursor
|
|
38
|
+
|
|
39
|
+
def show_commands(self, items: tuple[CompletionDefinition, ...]) -> None:
|
|
40
|
+
if not items:
|
|
41
|
+
self.hide()
|
|
42
|
+
return
|
|
43
|
+
self._items = items
|
|
44
|
+
self._cursor = 0
|
|
45
|
+
self.display = True
|
|
46
|
+
self._render_items()
|
|
47
|
+
|
|
48
|
+
def hide(self) -> None:
|
|
49
|
+
self.display = False
|
|
50
|
+
self._items = ()
|
|
51
|
+
self._cursor = 0
|
|
52
|
+
self.update("")
|
|
53
|
+
|
|
54
|
+
def move_up(self) -> None:
|
|
55
|
+
if self._cursor > 0:
|
|
56
|
+
self._cursor -= 1
|
|
57
|
+
self._render_items()
|
|
58
|
+
|
|
59
|
+
def move_down(self) -> None:
|
|
60
|
+
if self._cursor < len(self._items) - 1:
|
|
61
|
+
self._cursor += 1
|
|
62
|
+
self._render_items()
|
|
63
|
+
|
|
64
|
+
def selected_value(self) -> str | None:
|
|
65
|
+
if not self._items:
|
|
66
|
+
return None
|
|
67
|
+
return self._items[self._cursor].value
|
|
68
|
+
|
|
69
|
+
def _render_items(self) -> None:
|
|
70
|
+
window_start = min(
|
|
71
|
+
max(0, self._cursor - self.visible_items + 1),
|
|
72
|
+
max(0, len(self._items) - self.visible_items),
|
|
73
|
+
)
|
|
74
|
+
visible = self._items[window_start : window_start + self.visible_items]
|
|
75
|
+
command_width = max(
|
|
76
|
+
len(item.value) + (len(item.argument_hint) + 1 if item.argument_hint else 0)
|
|
77
|
+
for item in visible
|
|
78
|
+
)
|
|
79
|
+
lines: list[str] = []
|
|
80
|
+
for offset, item in enumerate(visible):
|
|
81
|
+
index = window_start + offset
|
|
82
|
+
command = item.value
|
|
83
|
+
if item.argument_hint:
|
|
84
|
+
command = f"{command} {item.argument_hint}"
|
|
85
|
+
label = f"{command:<{command_width}} {item.description}"
|
|
86
|
+
if index == self._cursor:
|
|
87
|
+
lines.append(f"[bold reverse]> {escape(label)} [/]")
|
|
88
|
+
else:
|
|
89
|
+
lines.append(f" [dim]{escape(label)}[/]")
|
|
90
|
+
self.update("\n".join(lines))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual.widgets import Static
|
|
4
|
+
|
|
5
|
+
from windcode.extensions.models import CapabilityRecord
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExtensionList(Static):
|
|
9
|
+
"""Responsive textual representation of the shared extension catalog."""
|
|
10
|
+
|
|
11
|
+
DEFAULT_CSS = """
|
|
12
|
+
ExtensionList {
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: auto;
|
|
15
|
+
max-height: 16;
|
|
16
|
+
overflow-y: auto;
|
|
17
|
+
padding: 0 2;
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def render_text(records: tuple[CapabilityRecord, ...]) -> str:
|
|
23
|
+
lines = ["扩展能力:"]
|
|
24
|
+
for record in records:
|
|
25
|
+
state = record.activation.value
|
|
26
|
+
trust = "trusted" if record.trusted else "untrusted"
|
|
27
|
+
permissions = [
|
|
28
|
+
name
|
|
29
|
+
for name, required in (
|
|
30
|
+
("read", record.permissions.filesystem_read),
|
|
31
|
+
("write", record.permissions.filesystem_write),
|
|
32
|
+
("network", record.permissions.network),
|
|
33
|
+
("process", record.permissions.process),
|
|
34
|
+
)
|
|
35
|
+
if required
|
|
36
|
+
]
|
|
37
|
+
lines.append(
|
|
38
|
+
f"{record.capability_id}\n {record.kind.value} · {record.source.scope.value} · "
|
|
39
|
+
f"{state} · {trust} · permissions={','.join(permissions) or 'none'}"
|
|
40
|
+
)
|
|
41
|
+
lines.extend(
|
|
42
|
+
f" diagnostic={diagnostic.category}: {diagnostic.message}"
|
|
43
|
+
for diagnostic in record.diagnostics
|
|
44
|
+
)
|
|
45
|
+
return "\n".join(lines) if records else "扩展能力:\n未发现扩展能力"
|
|
46
|
+
|
|
47
|
+
def __init__(self, records: tuple[CapabilityRecord, ...]) -> None:
|
|
48
|
+
super().__init__(self.render_text(records))
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, ClassVar
|
|
4
|
+
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.message import Message
|
|
7
|
+
from textual.widgets import TextArea
|
|
8
|
+
|
|
9
|
+
from windcode.tui.widgets.command_menu import CommandMenu
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChatInput(TextArea):
|
|
13
|
+
BINDINGS: ClassVar[list[Binding]] = [
|
|
14
|
+
Binding("enter", "submit", "发送", priority=True),
|
|
15
|
+
Binding("shift+enter", "newline", "换行", priority=True),
|
|
16
|
+
Binding("ctrl+j", "newline", "换行", priority=True),
|
|
17
|
+
Binding("tab", "complete", "补全", priority=True),
|
|
18
|
+
Binding("escape", "dismiss_menu", "关闭菜单", priority=True),
|
|
19
|
+
Binding("up", "nav_up", "向上选择", priority=True),
|
|
20
|
+
Binding("down", "nav_down", "向下选择", priority=True),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
class Submitted(Message):
|
|
24
|
+
def __init__(self, text: str) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.text = text
|
|
27
|
+
|
|
28
|
+
class SlashMenuUpdate(Message):
|
|
29
|
+
def __init__(self, prefix: str | None) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.prefix = prefix
|
|
32
|
+
|
|
33
|
+
class SkillMenuUpdate(Message):
|
|
34
|
+
def __init__(self, prefix: str | None) -> None:
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.prefix = prefix
|
|
37
|
+
|
|
38
|
+
class EscapePressed(Message):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
42
|
+
super().__init__(**kwargs)
|
|
43
|
+
self.cursor_blink = False
|
|
44
|
+
|
|
45
|
+
def action_submit(self) -> None:
|
|
46
|
+
menu = self._command_menu()
|
|
47
|
+
if menu is not None and menu.is_open:
|
|
48
|
+
selected = menu.selected_value()
|
|
49
|
+
menu.hide()
|
|
50
|
+
if selected is not None:
|
|
51
|
+
self.post_message(self.Submitted(selected))
|
|
52
|
+
self.clear()
|
|
53
|
+
return
|
|
54
|
+
text = self.text.strip()
|
|
55
|
+
if text:
|
|
56
|
+
self.post_message(self.Submitted(text))
|
|
57
|
+
self.clear()
|
|
58
|
+
|
|
59
|
+
def action_newline(self) -> None:
|
|
60
|
+
self.insert("\n")
|
|
61
|
+
|
|
62
|
+
def action_complete(self) -> None:
|
|
63
|
+
menu = self._command_menu()
|
|
64
|
+
if menu is not None and menu.is_open:
|
|
65
|
+
selected = menu.selected_value()
|
|
66
|
+
if selected is not None:
|
|
67
|
+
menu.hide()
|
|
68
|
+
self.clear()
|
|
69
|
+
self.insert(f"{selected} ")
|
|
70
|
+
return
|
|
71
|
+
self.insert("\t")
|
|
72
|
+
|
|
73
|
+
def action_dismiss_menu(self) -> None:
|
|
74
|
+
menu = self._command_menu()
|
|
75
|
+
if menu is not None and menu.is_open:
|
|
76
|
+
menu.hide()
|
|
77
|
+
else:
|
|
78
|
+
self.post_message(self.EscapePressed())
|
|
79
|
+
self.focus()
|
|
80
|
+
|
|
81
|
+
def action_nav_up(self) -> None:
|
|
82
|
+
menu = self._command_menu()
|
|
83
|
+
if menu is not None and menu.is_open:
|
|
84
|
+
menu.move_up()
|
|
85
|
+
return
|
|
86
|
+
self.action_cursor_up()
|
|
87
|
+
|
|
88
|
+
def action_nav_down(self) -> None:
|
|
89
|
+
menu = self._command_menu()
|
|
90
|
+
if menu is not None and menu.is_open:
|
|
91
|
+
menu.move_down()
|
|
92
|
+
return
|
|
93
|
+
self.action_cursor_down()
|
|
94
|
+
|
|
95
|
+
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
|
96
|
+
event.stop()
|
|
97
|
+
value = self.text
|
|
98
|
+
single_token = " " not in value and "\n" not in value
|
|
99
|
+
if value.startswith("/") and single_token:
|
|
100
|
+
self.post_message(self.SlashMenuUpdate(value))
|
|
101
|
+
elif value.startswith("$") and single_token:
|
|
102
|
+
self.post_message(self.SkillMenuUpdate(value))
|
|
103
|
+
else:
|
|
104
|
+
self.post_message(self.SlashMenuUpdate(None))
|
|
105
|
+
|
|
106
|
+
def _command_menu(self) -> CommandMenu | None:
|
|
107
|
+
try:
|
|
108
|
+
return self.screen.query_one(CommandMenu)
|
|
109
|
+
except Exception:
|
|
110
|
+
return None
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, cast
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from textual import on
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
from textual.screen import ModalScreen
|
|
12
|
+
from textual.widgets import Button, OptionList, Select, Static, Switch
|
|
13
|
+
from textual.widgets.option_list import Option
|
|
14
|
+
|
|
15
|
+
from windcode.memory import MemoryActivation, MemoryRecord, MemoryStatus
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MemoryManager(ModalScreen[None]):
|
|
19
|
+
BINDINGS: ClassVar[list[Binding]] = [Binding("escape", "close", "关闭")]
|
|
20
|
+
|
|
21
|
+
class EnabledChanged(Message):
|
|
22
|
+
def __init__(self, enabled: bool) -> None:
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.enabled = enabled
|
|
25
|
+
|
|
26
|
+
class Forget(Message):
|
|
27
|
+
def __init__(self, memory_id: str) -> None:
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.memory_id = memory_id
|
|
30
|
+
|
|
31
|
+
class ActivationChanged(Message):
|
|
32
|
+
def __init__(self, memory_id: str, activation: MemoryActivation) -> None:
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.memory_id = memory_id
|
|
35
|
+
self.activation = activation
|
|
36
|
+
|
|
37
|
+
class Rebuild(Message):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
class Closed(Message):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def __init__(self, records: tuple[MemoryRecord, ...], *, enabled: bool) -> None:
|
|
44
|
+
super().__init__(id="memory-manager")
|
|
45
|
+
self.records = records
|
|
46
|
+
self.enabled = enabled
|
|
47
|
+
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
with Vertical(id="memory-dialog"):
|
|
50
|
+
yield Static("长期记忆", id="memory-manager-title")
|
|
51
|
+
with Horizontal(id="memory-enabled-row"):
|
|
52
|
+
yield Static("启用跨会话记忆", id="memory-enabled-label")
|
|
53
|
+
yield Switch(value=self.enabled, id="memory-enabled")
|
|
54
|
+
yield Static(
|
|
55
|
+
"稳定事实与用户偏好自动保存; 经验遵循 No Execution, No Memory。",
|
|
56
|
+
id="memory-policy",
|
|
57
|
+
)
|
|
58
|
+
yield OptionList(*self._options(), id="memory-list")
|
|
59
|
+
yield Static("选择一条记忆查看详情", id="memory-details")
|
|
60
|
+
yield Select(
|
|
61
|
+
((item.value, item.value) for item in MemoryActivation),
|
|
62
|
+
id="memory-activation",
|
|
63
|
+
prompt="激活策略",
|
|
64
|
+
allow_blank=False,
|
|
65
|
+
disabled=True,
|
|
66
|
+
)
|
|
67
|
+
with Horizontal(id="memory-actions", classes="dialog-actions"):
|
|
68
|
+
yield Button("忘记所选", id="memory-forget", variant="error")
|
|
69
|
+
yield Button("重建索引", id="memory-rebuild")
|
|
70
|
+
yield Button("关闭", id="memory-close", variant="primary")
|
|
71
|
+
|
|
72
|
+
def _options(self) -> tuple[Option, ...]:
|
|
73
|
+
options: list[Option] = []
|
|
74
|
+
for record in self.records:
|
|
75
|
+
text = Text()
|
|
76
|
+
text.append("● " if record.status.value == "active" else "○ ", style="cyan")
|
|
77
|
+
text.append(record.title, style="bold")
|
|
78
|
+
text.append(
|
|
79
|
+
f" {record.kind.value} · {record.scope.value} · {record.status.value} · "
|
|
80
|
+
f"{record.activation.value}",
|
|
81
|
+
style="dim",
|
|
82
|
+
)
|
|
83
|
+
options.append(Option(text, id=record.memory_id))
|
|
84
|
+
return tuple(options)
|
|
85
|
+
|
|
86
|
+
def _selected_id(self) -> str | None:
|
|
87
|
+
option = self.query_one("#memory-list", OptionList).highlighted_option
|
|
88
|
+
return option.id if option is not None else None
|
|
89
|
+
|
|
90
|
+
@on(OptionList.OptionHighlighted, "#memory-list")
|
|
91
|
+
def highlighted(self, event: OptionList.OptionHighlighted) -> None:
|
|
92
|
+
record = next(item for item in self.records if item.memory_id == event.option.id)
|
|
93
|
+
summary = " ".join(record.summary.split())
|
|
94
|
+
body = " ".join(record.body.split())
|
|
95
|
+
content = (
|
|
96
|
+
record.body if summary == body else f"摘要: {record.summary}\n\n内容: {record.body}"
|
|
97
|
+
)
|
|
98
|
+
self.query_one("#memory-details", Static).update(
|
|
99
|
+
f"类型: {record.kind.value} · 状态: {record.status.value} · "
|
|
100
|
+
f"激活: {record.activation.value} · 优先级: {record.priority}\n\n{content}\n\n"
|
|
101
|
+
f"更新时间: {record.updated_at.isoformat()} · "
|
|
102
|
+
f"置信度: {record.confidence:.0%}"
|
|
103
|
+
)
|
|
104
|
+
selector = cast(Select[str], self.query_one("#memory-activation", Select))
|
|
105
|
+
selector.value = record.activation.value
|
|
106
|
+
selector.disabled = record.status is not MemoryStatus.ACTIVE
|
|
107
|
+
|
|
108
|
+
@on(Select.Changed, "#memory-activation")
|
|
109
|
+
def activation_changed(self, event: Select.Changed) -> None:
|
|
110
|
+
memory_id = self._selected_id()
|
|
111
|
+
if memory_id is None or event.value is Select.BLANK:
|
|
112
|
+
return
|
|
113
|
+
record = next(item for item in self.records if item.memory_id == memory_id)
|
|
114
|
+
activation = MemoryActivation(str(event.value))
|
|
115
|
+
if record.status is MemoryStatus.ACTIVE and activation is not record.activation:
|
|
116
|
+
self.post_message(self.ActivationChanged(memory_id, activation))
|
|
117
|
+
|
|
118
|
+
@on(Switch.Changed, "#memory-enabled")
|
|
119
|
+
def enabled_changed(self, event: Switch.Changed) -> None:
|
|
120
|
+
if event.value != self.enabled:
|
|
121
|
+
self.enabled = event.value
|
|
122
|
+
self.post_message(self.EnabledChanged(event.value))
|
|
123
|
+
|
|
124
|
+
@on(Button.Pressed)
|
|
125
|
+
def button_pressed(self, event: Button.Pressed) -> None:
|
|
126
|
+
if event.button.id == "memory-forget":
|
|
127
|
+
if memory_id := self._selected_id():
|
|
128
|
+
self.post_message(self.Forget(memory_id))
|
|
129
|
+
elif event.button.id == "memory-rebuild":
|
|
130
|
+
self.post_message(self.Rebuild())
|
|
131
|
+
elif event.button.id == "memory-close":
|
|
132
|
+
self.action_close()
|
|
133
|
+
|
|
134
|
+
def refresh_records(self, records: tuple[MemoryRecord, ...]) -> None:
|
|
135
|
+
self.records = records
|
|
136
|
+
listing = self.query_one("#memory-list", OptionList)
|
|
137
|
+
listing.clear_options()
|
|
138
|
+
listing.add_options(self._options())
|
|
139
|
+
self.query_one("#memory-details", Static).update("选择一条记忆查看详情")
|
|
140
|
+
cast(Select[str], self.query_one("#memory-activation", Select)).disabled = True
|
|
141
|
+
|
|
142
|
+
def action_close(self) -> None:
|
|
143
|
+
self.post_message(self.Closed())
|