context-forge-cli 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.
- context_forge_cli-0.1.0.dist-info/METADATA +340 -0
- context_forge_cli-0.1.0.dist-info/RECORD +37 -0
- context_forge_cli-0.1.0.dist-info/WHEEL +4 -0
- context_forge_cli-0.1.0.dist-info/entry_points.txt +2 -0
- contextforge/__init__.py +3 -0
- contextforge/adapters/__init__.py +3 -0
- contextforge/adapters/altimate_code.py +179 -0
- contextforge/adapters/base.py +44 -0
- contextforge/adapters/claude_code.py +393 -0
- contextforge/adapters/claude_desktop.py +306 -0
- contextforge/adapters/codex.py +220 -0
- contextforge/adapters/registry.py +31 -0
- contextforge/cli.py +523 -0
- contextforge/core/__init__.py +0 -0
- contextforge/core/analytics.py +226 -0
- contextforge/core/compactor.py +182 -0
- contextforge/core/db.py +181 -0
- contextforge/core/injector.py +94 -0
- contextforge/core/scanner.py +72 -0
- contextforge/core/summarizer.py +155 -0
- contextforge/core/token_analyzer.py +91 -0
- contextforge/models/__init__.py +4 -0
- contextforge/models/config.py +34 -0
- contextforge/models/session.py +37 -0
- contextforge/tui/__init__.py +0 -0
- contextforge/tui/app.py +204 -0
- contextforge/tui/styles.tcss +50 -0
- contextforge/tui/widgets/__init__.py +0 -0
- contextforge/tui/widgets/session_detail.py +191 -0
- contextforge/tui/widgets/session_table.py +277 -0
- contextforge/tui/widgets/stats_panel.py +340 -0
- contextforge/tui/widgets/status_bar.py +102 -0
- contextforge/tui/widgets/tokens_panel.py +134 -0
- contextforge/tui/widgets/transfer_panel.py +119 -0
- contextforge/utils/__init__.py +0 -0
- contextforge/utils/display.py +105 -0
- contextforge/utils/tokens.py +17 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""SessionDetail widget — right panel showing summary and metadata for selected session."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Vertical
|
|
10
|
+
from textual.widget import Widget
|
|
11
|
+
from textual.widgets import Markdown, Static
|
|
12
|
+
|
|
13
|
+
TOOL_COLORS = {
|
|
14
|
+
"claude_code": "cyan",
|
|
15
|
+
"codex": "green",
|
|
16
|
+
"altimate_code": "magenta",
|
|
17
|
+
"claude_desktop": "yellow",
|
|
18
|
+
}
|
|
19
|
+
TOOL_LABELS = {
|
|
20
|
+
"claude_code": "Claude Code",
|
|
21
|
+
"codex": "Codex",
|
|
22
|
+
"altimate_code": "altimate",
|
|
23
|
+
"claude_desktop": "Desktop",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _meta_line(label: str, value: str, color: str = "cyan") -> str:
|
|
28
|
+
return f"[bold {color}]{label:<10}[/bold {color}] {value}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _fmt_tokens(n: int | None) -> str:
|
|
32
|
+
if not n:
|
|
33
|
+
return "—"
|
|
34
|
+
if n >= 1_000_000:
|
|
35
|
+
return f"{n / 1_000_000:.1f}M"
|
|
36
|
+
if n >= 1_000:
|
|
37
|
+
return f"{n // 1_000}k"
|
|
38
|
+
return str(n)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _token_color(n: int | None) -> str:
|
|
42
|
+
if not n:
|
|
43
|
+
return "dim"
|
|
44
|
+
if n >= 100_000:
|
|
45
|
+
return "bold red"
|
|
46
|
+
if n >= 20_000:
|
|
47
|
+
return "yellow"
|
|
48
|
+
return "dim"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _fmt_ts(ms: int | None, fmt: str = "%Y-%m-%d %H:%M UTC") -> str:
|
|
52
|
+
if not ms:
|
|
53
|
+
return "?"
|
|
54
|
+
try:
|
|
55
|
+
dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
|
56
|
+
return dt.strftime(fmt)
|
|
57
|
+
except Exception:
|
|
58
|
+
return "?"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SessionDetail(Widget):
|
|
62
|
+
"""Shows title, metadata, and summary for the currently selected session."""
|
|
63
|
+
|
|
64
|
+
DEFAULT_CSS = """
|
|
65
|
+
SessionDetail {
|
|
66
|
+
width: 2fr;
|
|
67
|
+
height: 1fr;
|
|
68
|
+
padding: 1 2;
|
|
69
|
+
border: tall $primary;
|
|
70
|
+
border-title-color: $accent;
|
|
71
|
+
border-title-style: bold;
|
|
72
|
+
overflow-y: auto;
|
|
73
|
+
}
|
|
74
|
+
SessionDetail #detail-placeholder {
|
|
75
|
+
color: $text-disabled;
|
|
76
|
+
margin: 4 0;
|
|
77
|
+
text-align: center;
|
|
78
|
+
}
|
|
79
|
+
SessionDetail #detail-title {
|
|
80
|
+
text-style: bold;
|
|
81
|
+
color: $accent;
|
|
82
|
+
margin-bottom: 1;
|
|
83
|
+
}
|
|
84
|
+
SessionDetail #detail-meta-block {
|
|
85
|
+
margin-bottom: 1;
|
|
86
|
+
border-bottom: solid $primary-background-lighten-1;
|
|
87
|
+
padding-bottom: 1;
|
|
88
|
+
}
|
|
89
|
+
SessionDetail #detail-summary-label {
|
|
90
|
+
text-style: bold;
|
|
91
|
+
color: $text-muted;
|
|
92
|
+
margin-bottom: 1;
|
|
93
|
+
}
|
|
94
|
+
SessionDetail Markdown {
|
|
95
|
+
background: transparent;
|
|
96
|
+
color: $text;
|
|
97
|
+
}
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
_current_id: str | None = None
|
|
101
|
+
|
|
102
|
+
def compose(self) -> ComposeResult:
|
|
103
|
+
yield Static("← Select a session", id="detail-placeholder")
|
|
104
|
+
yield Static("", id="detail-title")
|
|
105
|
+
with Vertical(id="detail-meta-block"):
|
|
106
|
+
yield Static("", id="detail-tool")
|
|
107
|
+
yield Static("", id="detail-cwd")
|
|
108
|
+
yield Static("", id="detail-tokens")
|
|
109
|
+
yield Static("", id="detail-created")
|
|
110
|
+
yield Static("", id="detail-updated")
|
|
111
|
+
yield Static("", id="detail-status")
|
|
112
|
+
yield Static("", id="detail-tags")
|
|
113
|
+
yield Static("", id="detail-id")
|
|
114
|
+
yield Static("── Summary ──", id="detail-summary-label")
|
|
115
|
+
yield Markdown("", id="detail-summary-md")
|
|
116
|
+
|
|
117
|
+
def on_mount(self) -> None:
|
|
118
|
+
self.border_title = "Detail"
|
|
119
|
+
self.query_one("#detail-summary-label").display = False
|
|
120
|
+
self.query_one("#detail-meta-block").display = False
|
|
121
|
+
self.query_one("#detail-title").display = False
|
|
122
|
+
|
|
123
|
+
def load(self, session_id: str) -> None:
|
|
124
|
+
"""Load and display the given session."""
|
|
125
|
+
if session_id == self._current_id:
|
|
126
|
+
return
|
|
127
|
+
self._current_id = session_id
|
|
128
|
+
|
|
129
|
+
db_path: Path | None = getattr(self.app, "db_path", None)
|
|
130
|
+
if db_path is None:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
from contextforge.core.db import get_db, get_session
|
|
134
|
+
db = get_db(db_path)
|
|
135
|
+
row = get_session(db, session_id)
|
|
136
|
+
if row is None:
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
tool = row.get("tool", "?")
|
|
140
|
+
title = row.get("title") or "(no title)"
|
|
141
|
+
cwd = row.get("cwd") or "?"
|
|
142
|
+
tokens_int = row.get("token_count")
|
|
143
|
+
status = row.get("status") or "?"
|
|
144
|
+
tags_raw = row.get("tags") or "[]"
|
|
145
|
+
try:
|
|
146
|
+
tags = ", ".join(json.loads(tags_raw)) or "—"
|
|
147
|
+
except Exception:
|
|
148
|
+
tags = "—"
|
|
149
|
+
|
|
150
|
+
created_str = _fmt_ts(row.get("created_at"))
|
|
151
|
+
updated_str = _fmt_ts(row.get("updated_at"))
|
|
152
|
+
|
|
153
|
+
summary = row.get("summary") or "*No summary yet* — run: `cf summarize <id>`"
|
|
154
|
+
|
|
155
|
+
# Tool label with color
|
|
156
|
+
tool_color = TOOL_COLORS.get(tool, "white")
|
|
157
|
+
tool_display = TOOL_LABELS.get(tool, tool)
|
|
158
|
+
tool_markup = f"[{tool_color}]{tool_display}[/{tool_color}]"
|
|
159
|
+
|
|
160
|
+
# Token display with color
|
|
161
|
+
tok_color = _token_color(tokens_int)
|
|
162
|
+
tok_str = _fmt_tokens(tokens_int)
|
|
163
|
+
tok_markup = f"[{tok_color}]{tok_str}[/{tok_color}]"
|
|
164
|
+
|
|
165
|
+
# Show everything
|
|
166
|
+
self.query_one("#detail-placeholder").display = False
|
|
167
|
+
self.query_one("#detail-title").display = True
|
|
168
|
+
self.query_one("#detail-meta-block").display = True
|
|
169
|
+
self.query_one("#detail-summary-label").display = True
|
|
170
|
+
|
|
171
|
+
self.query_one("#detail-title", Static).update(f"[bold accent]{title}[/bold accent]")
|
|
172
|
+
self.query_one("#detail-tool", Static).update(_meta_line("Tool", tool_markup, tool_color))
|
|
173
|
+
self.query_one("#detail-cwd", Static).update(_meta_line("CWD", f"[dim]{cwd}[/dim]", "dim"))
|
|
174
|
+
self.query_one("#detail-tokens", Static).update(_meta_line("Tokens", tok_markup, "dim"))
|
|
175
|
+
self.query_one("#detail-created", Static).update(_meta_line("Created", f"[dim]{created_str}[/dim]", "dim"))
|
|
176
|
+
self.query_one("#detail-updated", Static).update(_meta_line("Updated", f"[dim]{updated_str}[/dim]", "dim"))
|
|
177
|
+
self.query_one("#detail-status", Static).update(_meta_line("Status", status, "dim"))
|
|
178
|
+
self.query_one("#detail-tags", Static).update(_meta_line("Tags", tags, "dim"))
|
|
179
|
+
self.query_one("#detail-id", Static).update(_meta_line("ID", f"[dim]{session_id}[/dim]", "dim"))
|
|
180
|
+
|
|
181
|
+
md = self.query_one("#detail-summary-md", Markdown)
|
|
182
|
+
self.app.call_later(md.update, summary)
|
|
183
|
+
|
|
184
|
+
def clear(self) -> None:
|
|
185
|
+
self._current_id = None
|
|
186
|
+
self.query_one("#detail-placeholder").display = True
|
|
187
|
+
self.query_one("#detail-title").display = False
|
|
188
|
+
self.query_one("#detail-meta-block").display = False
|
|
189
|
+
self.query_one("#detail-summary-label").display = False
|
|
190
|
+
self.query_one("#detail-title", Static).update("")
|
|
191
|
+
self.app.call_later(self.query_one("#detail-summary-md", Markdown).update, "")
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""SessionTable widget — scrollable DataTable of all indexed sessions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import Horizontal, Vertical
|
|
9
|
+
from textual.events import Key
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
from textual.reactive import reactive
|
|
12
|
+
from textual.widget import Widget
|
|
13
|
+
from textual.widgets import Button, DataTable, Input, Label
|
|
14
|
+
|
|
15
|
+
TOOL_EMOJI = {
|
|
16
|
+
"claude_code": "🔵",
|
|
17
|
+
"codex": "🟢",
|
|
18
|
+
"altimate_code": "🟣",
|
|
19
|
+
"claude_desktop": "🟡",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
TOOL_MARKUP = {
|
|
23
|
+
"claude_code": "[cyan]🔵 CC[/cyan]",
|
|
24
|
+
"codex": "[green]🟢 Codex[/green]",
|
|
25
|
+
"altimate_code": "[magenta]🟣 Alt[/magenta]",
|
|
26
|
+
"claude_desktop": "[yellow]🟡 Desktop[/yellow]",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
TOOL_DISPLAY = {
|
|
30
|
+
"claude_code": "CC",
|
|
31
|
+
"codex": "Codex",
|
|
32
|
+
"altimate_code": "Alt",
|
|
33
|
+
"claude_desktop": "Desktop",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_FTOOL_MAP = {
|
|
37
|
+
"ftool-all": None,
|
|
38
|
+
"ftool-cc": "claude_code",
|
|
39
|
+
"ftool-codex": "codex",
|
|
40
|
+
"ftool-alt": "altimate_code",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _fmt_tokens(n: int | None) -> str:
|
|
45
|
+
if not n:
|
|
46
|
+
return ""
|
|
47
|
+
if n >= 1_000_000:
|
|
48
|
+
return f"{n / 1_000_000:.1f}M"
|
|
49
|
+
if n >= 1_000:
|
|
50
|
+
return f"{n // 1_000}k"
|
|
51
|
+
return str(n)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _token_markup(n: int | None) -> str:
|
|
55
|
+
if not n:
|
|
56
|
+
return "[dim]—[/dim]"
|
|
57
|
+
raw = _fmt_tokens(n)
|
|
58
|
+
if n >= 100_000:
|
|
59
|
+
return f"[bold red]{raw}[/bold red]"
|
|
60
|
+
if n >= 20_000:
|
|
61
|
+
return f"[yellow]{raw}[/yellow]"
|
|
62
|
+
return f"[dim]{raw}[/dim]"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SessionTable(Widget):
|
|
66
|
+
"""Displays all sessions in a navigable DataTable with live filtering."""
|
|
67
|
+
|
|
68
|
+
DEFAULT_CSS = """
|
|
69
|
+
SessionTable {
|
|
70
|
+
width: 1fr;
|
|
71
|
+
height: 1fr;
|
|
72
|
+
border: tall $primary;
|
|
73
|
+
border-title-color: $accent;
|
|
74
|
+
border-title-style: bold;
|
|
75
|
+
}
|
|
76
|
+
SessionTable > Vertical {
|
|
77
|
+
height: 1fr;
|
|
78
|
+
}
|
|
79
|
+
SessionTable DataTable {
|
|
80
|
+
height: 1fr;
|
|
81
|
+
}
|
|
82
|
+
SessionTable #filter-bar {
|
|
83
|
+
height: 3;
|
|
84
|
+
background: $primary-background-darken-1;
|
|
85
|
+
padding: 0 1;
|
|
86
|
+
layout: horizontal;
|
|
87
|
+
align: left middle;
|
|
88
|
+
}
|
|
89
|
+
SessionTable #filter-label {
|
|
90
|
+
width: auto;
|
|
91
|
+
margin-right: 1;
|
|
92
|
+
color: $text-muted;
|
|
93
|
+
}
|
|
94
|
+
SessionTable #filter-input {
|
|
95
|
+
width: 28;
|
|
96
|
+
margin-right: 1;
|
|
97
|
+
}
|
|
98
|
+
SessionTable .filter-tool-btn {
|
|
99
|
+
min-width: 7;
|
|
100
|
+
height: 1;
|
|
101
|
+
margin-right: 1;
|
|
102
|
+
background: $surface;
|
|
103
|
+
border: none;
|
|
104
|
+
color: $text-muted;
|
|
105
|
+
}
|
|
106
|
+
SessionTable .filter-tool-btn.active {
|
|
107
|
+
background: $primary;
|
|
108
|
+
color: $text;
|
|
109
|
+
text-style: bold;
|
|
110
|
+
}
|
|
111
|
+
SessionTable .hidden {
|
|
112
|
+
display: none;
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
# ── Messages ──────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
class RowSelected(Message):
|
|
119
|
+
"""Emitted when the user highlights a session row."""
|
|
120
|
+
def __init__(self, session_id: str, tool: str) -> None:
|
|
121
|
+
super().__init__()
|
|
122
|
+
self.session_id = session_id
|
|
123
|
+
self.tool = tool
|
|
124
|
+
|
|
125
|
+
class FilterChanged(Message):
|
|
126
|
+
"""Emitted when filter state changes."""
|
|
127
|
+
def __init__(self, text: str, tool: str | None, match_count: int) -> None:
|
|
128
|
+
super().__init__()
|
|
129
|
+
self.text = text
|
|
130
|
+
self.tool = tool
|
|
131
|
+
self.match_count = match_count
|
|
132
|
+
|
|
133
|
+
# ── State ─────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
def __init__(self, **kwargs) -> None:
|
|
136
|
+
super().__init__(**kwargs)
|
|
137
|
+
self._rows: list[dict] = []
|
|
138
|
+
self._display_rows: list[dict] = []
|
|
139
|
+
self._filter_text: str = ""
|
|
140
|
+
self._filter_tool: str | None = None
|
|
141
|
+
self._filter_visible: bool = False
|
|
142
|
+
|
|
143
|
+
# ── Compose ───────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
def compose(self) -> ComposeResult:
|
|
146
|
+
with Vertical():
|
|
147
|
+
with Horizontal(id="filter-bar", classes="hidden"):
|
|
148
|
+
yield Label("Filter:", id="filter-label")
|
|
149
|
+
yield Input(placeholder="title or project…", id="filter-input")
|
|
150
|
+
yield Button("All", id="ftool-all", classes="filter-tool-btn active")
|
|
151
|
+
yield Button("CC", id="ftool-cc", classes="filter-tool-btn")
|
|
152
|
+
yield Button("Codex", id="ftool-codex", classes="filter-tool-btn")
|
|
153
|
+
yield Button("Alt", id="ftool-alt", classes="filter-tool-btn")
|
|
154
|
+
yield DataTable(cursor_type="row", zebra_stripes=True)
|
|
155
|
+
|
|
156
|
+
def on_mount(self) -> None:
|
|
157
|
+
self.border_title = "Sessions"
|
|
158
|
+
table = self.query_one(DataTable)
|
|
159
|
+
table.add_columns("Tool", "Title", "Updated", "Tokens", "ID")
|
|
160
|
+
self.reload()
|
|
161
|
+
|
|
162
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
def toggle_filter(self) -> None:
|
|
165
|
+
"""Show or hide the filter bar."""
|
|
166
|
+
bar = self.query_one("#filter-bar")
|
|
167
|
+
if self._filter_visible:
|
|
168
|
+
self._filter_visible = False
|
|
169
|
+
bar.add_class("hidden")
|
|
170
|
+
self._filter_text = ""
|
|
171
|
+
self._filter_tool = None
|
|
172
|
+
self.query_one("#filter-input", Input).value = ""
|
|
173
|
+
# Reset active button
|
|
174
|
+
for btn_id in _FTOOL_MAP:
|
|
175
|
+
btn = self.query_one(f"#{btn_id}", Button)
|
|
176
|
+
if btn_id == "ftool-all":
|
|
177
|
+
btn.add_class("active")
|
|
178
|
+
else:
|
|
179
|
+
btn.remove_class("active")
|
|
180
|
+
self.reload()
|
|
181
|
+
self.post_message(self.FilterChanged("", None, len(self._rows)))
|
|
182
|
+
else:
|
|
183
|
+
self._filter_visible = True
|
|
184
|
+
bar.remove_class("hidden")
|
|
185
|
+
self.query_one("#filter-input", Input).focus()
|
|
186
|
+
|
|
187
|
+
def reload(self, tool_filter: str | None = None) -> None:
|
|
188
|
+
"""Re-read sessions from DB and refresh the table."""
|
|
189
|
+
db_path: Path | None = getattr(self.app, "db_path", None)
|
|
190
|
+
if db_path is None:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
from contextforge.core.db import get_db, get_sessions
|
|
194
|
+
db = get_db(db_path)
|
|
195
|
+
rows = get_sessions(db, tool=tool_filter, limit=500)
|
|
196
|
+
self._rows = rows
|
|
197
|
+
self._display_rows = rows
|
|
198
|
+
self._render_rows(rows)
|
|
199
|
+
|
|
200
|
+
# ── Internal rendering ────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
def _render_rows(self, rows: list[dict]) -> None:
|
|
203
|
+
"""Populate the DataTable from a list of session dicts."""
|
|
204
|
+
self._display_rows = rows
|
|
205
|
+
table = self.query_one(DataTable)
|
|
206
|
+
table.clear()
|
|
207
|
+
|
|
208
|
+
for row in rows:
|
|
209
|
+
tool = row.get("tool", "")
|
|
210
|
+
tool_label = TOOL_MARKUP.get(tool, f"⚪ {tool}")
|
|
211
|
+
|
|
212
|
+
from contextforge.utils.display import _clean_title
|
|
213
|
+
title = _clean_title(row.get("title") or "", max_len=38) or "(no title)"
|
|
214
|
+
|
|
215
|
+
updated_ms = row.get("updated_at") or 0
|
|
216
|
+
try:
|
|
217
|
+
dt = datetime.fromtimestamp(updated_ms / 1000, tz=timezone.utc)
|
|
218
|
+
updated = dt.strftime("%m-%d %H:%M")
|
|
219
|
+
except Exception:
|
|
220
|
+
updated = "?"
|
|
221
|
+
|
|
222
|
+
tokens = _token_markup(row.get("token_count"))
|
|
223
|
+
session_id = str(row.get("id", ""))[:12]
|
|
224
|
+
|
|
225
|
+
table.add_row(tool_label, title, updated, tokens, session_id)
|
|
226
|
+
|
|
227
|
+
def _apply_filter(self) -> None:
|
|
228
|
+
"""Filter self._rows in memory and re-render."""
|
|
229
|
+
text = self._filter_text.lower()
|
|
230
|
+
tool = self._filter_tool
|
|
231
|
+
|
|
232
|
+
filtered = [
|
|
233
|
+
row for row in self._rows
|
|
234
|
+
if (not tool or row.get("tool") == tool)
|
|
235
|
+
and (
|
|
236
|
+
not text
|
|
237
|
+
or text in (row.get("title") or "").lower()
|
|
238
|
+
or text in (row.get("cwd") or "").lower()
|
|
239
|
+
)
|
|
240
|
+
]
|
|
241
|
+
self._render_rows(filtered)
|
|
242
|
+
self.post_message(self.FilterChanged(self._filter_text, tool, len(filtered)))
|
|
243
|
+
|
|
244
|
+
# ── Event handlers ────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
247
|
+
if event.input.id == "filter-input":
|
|
248
|
+
self._filter_text = event.value
|
|
249
|
+
self._apply_filter()
|
|
250
|
+
|
|
251
|
+
def on_key(self, event: Key) -> None:
|
|
252
|
+
if event.key == "escape" and self._filter_visible:
|
|
253
|
+
self.toggle_filter()
|
|
254
|
+
event.stop()
|
|
255
|
+
|
|
256
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
257
|
+
btn_id = event.button.id
|
|
258
|
+
if btn_id not in _FTOOL_MAP:
|
|
259
|
+
return
|
|
260
|
+
self._filter_tool = _FTOOL_MAP[btn_id]
|
|
261
|
+
for bid in _FTOOL_MAP:
|
|
262
|
+
btn = self.query_one(f"#{bid}", Button)
|
|
263
|
+
if bid == btn_id:
|
|
264
|
+
btn.add_class("active")
|
|
265
|
+
else:
|
|
266
|
+
btn.remove_class("active")
|
|
267
|
+
self._apply_filter()
|
|
268
|
+
|
|
269
|
+
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
270
|
+
idx = event.cursor_row
|
|
271
|
+
display = self._display_rows
|
|
272
|
+
if 0 <= idx < len(display):
|
|
273
|
+
row = display[idx]
|
|
274
|
+
self.post_message(self.RowSelected(
|
|
275
|
+
session_id=str(row["id"]),
|
|
276
|
+
tool=str(row.get("tool", "")),
|
|
277
|
+
))
|