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.
Files changed (143) hide show
  1. windcode/__init__.py +6 -0
  2. windcode/__main__.py +4 -0
  3. windcode/auth/__init__.py +11 -0
  4. windcode/auth/store.py +90 -0
  5. windcode/cli.py +181 -0
  6. windcode/config/__init__.py +41 -0
  7. windcode/config/loader.py +117 -0
  8. windcode/config/models.py +203 -0
  9. windcode/config/writer.py +74 -0
  10. windcode/context/__init__.py +18 -0
  11. windcode/context/compactor.py +72 -0
  12. windcode/context/estimator.py +89 -0
  13. windcode/context/truncation.py +87 -0
  14. windcode/domain/__init__.py +1 -0
  15. windcode/domain/errors.py +33 -0
  16. windcode/domain/events.py +597 -0
  17. windcode/domain/messages.py +226 -0
  18. windcode/domain/models.py +73 -0
  19. windcode/domain/subagents.py +263 -0
  20. windcode/domain/tools.py +55 -0
  21. windcode/extensions/__init__.py +27 -0
  22. windcode/extensions/commands.py +25 -0
  23. windcode/extensions/discovery.py +139 -0
  24. windcode/extensions/events.py +58 -0
  25. windcode/extensions/hooks/__init__.py +4 -0
  26. windcode/extensions/hooks/dispatcher.py +107 -0
  27. windcode/extensions/hooks/executor.py +49 -0
  28. windcode/extensions/hooks/loader.py +101 -0
  29. windcode/extensions/hooks/models.py +109 -0
  30. windcode/extensions/mcp/__init__.py +10 -0
  31. windcode/extensions/mcp/adapter.py +101 -0
  32. windcode/extensions/mcp/catalog.py +153 -0
  33. windcode/extensions/mcp/client.py +273 -0
  34. windcode/extensions/mcp/runtime.py +144 -0
  35. windcode/extensions/mcp/tools.py +570 -0
  36. windcode/extensions/models.py +168 -0
  37. windcode/extensions/paths.py +71 -0
  38. windcode/extensions/plugins/__init__.py +3 -0
  39. windcode/extensions/plugins/installer.py +93 -0
  40. windcode/extensions/plugins/manifest.py +166 -0
  41. windcode/extensions/runtime.py +366 -0
  42. windcode/extensions/service.py +437 -0
  43. windcode/extensions/skills/__init__.py +3 -0
  44. windcode/extensions/skills/loader.py +67 -0
  45. windcode/extensions/skills/parser.py +57 -0
  46. windcode/extensions/skills/tools.py +213 -0
  47. windcode/extensions/snapshot.py +57 -0
  48. windcode/extensions/state.py +158 -0
  49. windcode/instructions/__init__.py +8 -0
  50. windcode/instructions/loader.py +50 -0
  51. windcode/memory/__init__.py +57 -0
  52. windcode/memory/extraction.py +83 -0
  53. windcode/memory/models.py +218 -0
  54. windcode/memory/refiner.py +189 -0
  55. windcode/memory/security.py +23 -0
  56. windcode/memory/service.py +179 -0
  57. windcode/memory/store.py +362 -0
  58. windcode/observability/__init__.py +4 -0
  59. windcode/observability/redaction.py +95 -0
  60. windcode/observability/trace.py +115 -0
  61. windcode/policy/__init__.py +22 -0
  62. windcode/policy/engine.py +137 -0
  63. windcode/policy/models.py +69 -0
  64. windcode/providers/__init__.py +29 -0
  65. windcode/providers/_utils.py +19 -0
  66. windcode/providers/anthropic.py +215 -0
  67. windcode/providers/base.py +46 -0
  68. windcode/providers/catalog.py +119 -0
  69. windcode/providers/errors.py +96 -0
  70. windcode/providers/openai_compat.py +231 -0
  71. windcode/providers/openai_responses.py +189 -0
  72. windcode/providers/registry.py +105 -0
  73. windcode/runtime/__init__.py +15 -0
  74. windcode/runtime/control.py +89 -0
  75. windcode/runtime/event_bus.py +67 -0
  76. windcode/runtime/loop.py +510 -0
  77. windcode/runtime/prompts.py +132 -0
  78. windcode/runtime/report.py +64 -0
  79. windcode/runtime/retry.py +73 -0
  80. windcode/runtime/scheduler.py +194 -0
  81. windcode/runtime/subagents/__init__.py +28 -0
  82. windcode/runtime/subagents/approvals.py +88 -0
  83. windcode/runtime/subagents/budgets.py +79 -0
  84. windcode/runtime/subagents/coordinator.py +714 -0
  85. windcode/runtime/subagents/factory.py +311 -0
  86. windcode/runtime/subagents/roles.py +74 -0
  87. windcode/runtime/subagents/verification.py +53 -0
  88. windcode/sandbox/__init__.py +3 -0
  89. windcode/sandbox/bwrap.py +79 -0
  90. windcode/sdk.py +1259 -0
  91. windcode/sessions/__init__.py +23 -0
  92. windcode/sessions/artifacts.py +69 -0
  93. windcode/sessions/models.py +104 -0
  94. windcode/sessions/store.py +167 -0
  95. windcode/sessions/tree.py +35 -0
  96. windcode/tools/__init__.py +10 -0
  97. windcode/tools/apply_patch.py +191 -0
  98. windcode/tools/ask_user.py +58 -0
  99. windcode/tools/builtins.py +44 -0
  100. windcode/tools/edit_file.py +54 -0
  101. windcode/tools/filesystem.py +77 -0
  102. windcode/tools/glob.py +35 -0
  103. windcode/tools/grep.py +66 -0
  104. windcode/tools/memory.py +400 -0
  105. windcode/tools/read_file.py +54 -0
  106. windcode/tools/registry.py +120 -0
  107. windcode/tools/shell.py +159 -0
  108. windcode/tools/subagents/__init__.py +31 -0
  109. windcode/tools/subagents/cancel.py +35 -0
  110. windcode/tools/subagents/integrate.py +54 -0
  111. windcode/tools/subagents/list.py +46 -0
  112. windcode/tools/subagents/spawn.py +86 -0
  113. windcode/tools/subagents/wait.py +74 -0
  114. windcode/tools/write_file.py +61 -0
  115. windcode/tui/__init__.py +3 -0
  116. windcode/tui/app.py +1015 -0
  117. windcode/tui/commands.py +90 -0
  118. windcode/tui/permission_display.py +24 -0
  119. windcode/tui/styles.tcss +591 -0
  120. windcode/tui/widgets/__init__.py +31 -0
  121. windcode/tui/widgets/approval.py +98 -0
  122. windcode/tui/widgets/command_menu.py +90 -0
  123. windcode/tui/widgets/extensions.py +48 -0
  124. windcode/tui/widgets/input.py +110 -0
  125. windcode/tui/widgets/memory.py +143 -0
  126. windcode/tui/widgets/messages.py +299 -0
  127. windcode/tui/widgets/models.py +565 -0
  128. windcode/tui/widgets/question.py +46 -0
  129. windcode/tui/widgets/sessions.py +68 -0
  130. windcode/tui/widgets/status.py +46 -0
  131. windcode/tui/widgets/subagents.py +195 -0
  132. windcode/tui/widgets/tools.py +66 -0
  133. windcode/tui/widgets/welcome.py +149 -0
  134. windcode/types.py +80 -0
  135. windcode/worktrees/__init__.py +24 -0
  136. windcode/worktrees/git.py +92 -0
  137. windcode/worktrees/manager.py +265 -0
  138. windcode/worktrees/models.py +62 -0
  139. windcode-0.1.0.dist-info/METADATA +207 -0
  140. windcode-0.1.0.dist-info/RECORD +143 -0
  141. windcode-0.1.0.dist-info/WHEEL +4 -0
  142. windcode-0.1.0.dist-info/entry_points.txt +2 -0
  143. 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())