context-forge-cli 0.4.4__tar.gz → 0.4.6__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.
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/PKG-INFO +1 -1
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/app.py +8 -7
- context_forge_cli-0.4.6/contextforge/tui/styles.tcss +85 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/widgets/session_detail.py +26 -20
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/widgets/session_table.py +31 -23
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/widgets/stats_panel.py +178 -100
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/widgets/status_bar.py +10 -9
- context_forge_cli-0.4.6/contextforge/tui/widgets/tokens_panel.py +372 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/pyproject.toml +1 -1
- context_forge_cli-0.4.4/contextforge/tui/styles.tcss +0 -50
- context_forge_cli-0.4.4/contextforge/tui/widgets/tokens_panel.py +0 -293
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/.contextforge.toml +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/.github/workflows/publish.yml +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/.gitignore +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/.python-version +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/AGENTS.md +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/PLAN.md +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/README.md +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/adapters/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/adapters/altimate_code.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/adapters/base.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/adapters/claude_code.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/adapters/claude_desktop.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/adapters/codex.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/adapters/gemini.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/adapters/registry.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/cli.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/core/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/core/analytics.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/core/compactor.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/core/db.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/core/injector.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/core/scanner.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/core/summarizer.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/core/token_analyzer.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/mcp_server.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/models/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/models/config.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/models/session.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/widgets/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/widgets/transfer_panel.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/utils/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/utils/display.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/utils/tokens.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/adapters/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/adapters/test_claude_code.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/conftest.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/core/__init__.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/core/test_compactor.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/core/test_db.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/core/test_injector.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/core/test_summarizer.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/core/test_token_analyzer.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/fixtures/claude_session_sample.jsonl +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/tests/test_mcp_server.py +0 -0
- {context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: context-forge-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
4
4
|
Summary: Session manager and context bridge for agentic CLI tools
|
|
5
5
|
Project-URL: Homepage, https://github.com/emmver/contextforge
|
|
6
6
|
Project-URL: Repository, https://github.com/emmver/contextforge
|
|
@@ -19,17 +19,18 @@ class ContextForgeApp(App):
|
|
|
19
19
|
"""ContextForge dashboard — manage agentic CLI sessions."""
|
|
20
20
|
|
|
21
21
|
TITLE = "ContextForge"
|
|
22
|
+
SUB_TITLE = "agentic session manager"
|
|
22
23
|
CSS_PATH = "styles.tcss"
|
|
23
24
|
|
|
24
25
|
BINDINGS = [
|
|
25
26
|
Binding("q", "quit", "Quit"),
|
|
26
|
-
Binding("r", "rescan", "Rescan"),
|
|
27
|
-
Binding("s", "summarize", "Summarize"),
|
|
28
|
-
Binding("t", "transfer", "Transfer"),
|
|
29
|
-
Binding("c", "compact", "Compact"),
|
|
30
|
-
Binding("x", "tokens", "Tokens"),
|
|
31
|
-
Binding("a", "analytics", "Analytics"),
|
|
32
|
-
Binding("/", "filter", "Filter"),
|
|
27
|
+
Binding("r", "rescan", "⟳ Rescan"),
|
|
28
|
+
Binding("s", "summarize", "✎ Summarize"),
|
|
29
|
+
Binding("t", "transfer", "⇄ Transfer"),
|
|
30
|
+
Binding("c", "compact", "⊛ Compact"),
|
|
31
|
+
Binding("x", "tokens", "# Tokens"),
|
|
32
|
+
Binding("a", "analytics", "▤ Analytics"),
|
|
33
|
+
Binding("/", "filter", "⌕ Filter"),
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
def __init__(self, db_path: Path | None = None) -> None:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* ContextForge TUI Styles */
|
|
2
|
+
|
|
3
|
+
/* ── Layout ─────────────────────────────────────────────── */
|
|
4
|
+
|
|
5
|
+
Screen {
|
|
6
|
+
layout: vertical;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#main-horizontal {
|
|
10
|
+
height: 1fr;
|
|
11
|
+
layout: horizontal;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* ── Header ─────────────────────────────────────────────── */
|
|
15
|
+
|
|
16
|
+
Header {
|
|
17
|
+
background: $primary-background-darken-1;
|
|
18
|
+
color: $accent;
|
|
19
|
+
text-style: bold;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ── Footer ─────────────────────────────────────────────── */
|
|
23
|
+
|
|
24
|
+
Footer {
|
|
25
|
+
background: $primary-background-darken-2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ── DataTable global tweaks ─────────────────────────────── */
|
|
29
|
+
|
|
30
|
+
DataTable > .datatable--header {
|
|
31
|
+
text-style: bold;
|
|
32
|
+
color: $accent;
|
|
33
|
+
background: $primary-background-darken-1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
DataTable > .datatable--cursor {
|
|
37
|
+
background: $primary;
|
|
38
|
+
color: $text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ── Transfer modal ─────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
TransferPanel {
|
|
44
|
+
align: center middle;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
TransferPanel > Vertical {
|
|
48
|
+
width: 64;
|
|
49
|
+
height: auto;
|
|
50
|
+
max-height: 38;
|
|
51
|
+
border: thick $accent;
|
|
52
|
+
background: $surface;
|
|
53
|
+
padding: 1 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#panel-title {
|
|
57
|
+
text-style: bold;
|
|
58
|
+
color: $accent;
|
|
59
|
+
margin-bottom: 1;
|
|
60
|
+
padding-bottom: 1;
|
|
61
|
+
border-bottom: solid $primary-background-lighten-1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.section-label {
|
|
65
|
+
text-style: bold;
|
|
66
|
+
margin-top: 1;
|
|
67
|
+
color: $text-muted;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#btn-row {
|
|
71
|
+
margin-top: 2;
|
|
72
|
+
height: auto;
|
|
73
|
+
layout: horizontal;
|
|
74
|
+
align: right middle;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#btn-row Button {
|
|
78
|
+
margin-left: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── Notifications ──────────────────────────────────────── */
|
|
82
|
+
|
|
83
|
+
Toast {
|
|
84
|
+
border: tall $accent;
|
|
85
|
+
}
|
{context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/widgets/session_detail.py
RENAMED
|
@@ -11,21 +11,23 @@ from textual.widget import Widget
|
|
|
11
11
|
from textual.widgets import Markdown, Static
|
|
12
12
|
|
|
13
13
|
TOOL_COLORS = {
|
|
14
|
-
"claude_code":
|
|
15
|
-
"codex":
|
|
16
|
-
"altimate_code":
|
|
14
|
+
"claude_code": "cyan",
|
|
15
|
+
"codex": "green",
|
|
16
|
+
"altimate_code": "magenta",
|
|
17
17
|
"claude_desktop": "yellow",
|
|
18
|
+
"gemini": "blue",
|
|
18
19
|
}
|
|
19
20
|
TOOL_LABELS = {
|
|
20
|
-
"claude_code":
|
|
21
|
-
"codex":
|
|
22
|
-
"altimate_code": "
|
|
23
|
-
"claude_desktop": "Desktop",
|
|
21
|
+
"claude_code": "◆ Claude Code",
|
|
22
|
+
"codex": "⬡ Codex",
|
|
23
|
+
"altimate_code": "⚡ Altimate",
|
|
24
|
+
"claude_desktop": "◇ Claude Desktop",
|
|
25
|
+
"gemini": "✦ Gemini",
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def _meta_line(label: str, value: str, color: str = "cyan") -> str:
|
|
28
|
-
return f"[
|
|
30
|
+
return f"[dim]{label:<10}[/dim] {value}"
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
def _fmt_tokens(n: int | None) -> str:
|
|
@@ -75,11 +77,14 @@ class SessionDetail(Widget):
|
|
|
75
77
|
color: $text-disabled;
|
|
76
78
|
margin: 4 0;
|
|
77
79
|
text-align: center;
|
|
80
|
+
text-style: italic;
|
|
78
81
|
}
|
|
79
82
|
SessionDetail #detail-title {
|
|
80
83
|
text-style: bold;
|
|
81
84
|
color: $accent;
|
|
82
85
|
margin-bottom: 1;
|
|
86
|
+
padding-bottom: 1;
|
|
87
|
+
border-bottom: solid $primary-background-lighten-1;
|
|
83
88
|
}
|
|
84
89
|
SessionDetail #detail-meta-block {
|
|
85
90
|
margin-bottom: 1;
|
|
@@ -88,8 +93,9 @@ class SessionDetail(Widget):
|
|
|
88
93
|
}
|
|
89
94
|
SessionDetail #detail-summary-label {
|
|
90
95
|
text-style: bold;
|
|
91
|
-
color: $
|
|
96
|
+
color: $accent;
|
|
92
97
|
margin-bottom: 1;
|
|
98
|
+
margin-top: 1;
|
|
93
99
|
}
|
|
94
100
|
SessionDetail Markdown {
|
|
95
101
|
background: transparent;
|
|
@@ -111,11 +117,11 @@ class SessionDetail(Widget):
|
|
|
111
117
|
yield Static("", id="detail-status")
|
|
112
118
|
yield Static("", id="detail-tags")
|
|
113
119
|
yield Static("", id="detail-id")
|
|
114
|
-
yield Static("
|
|
120
|
+
yield Static("✎ Summary", id="detail-summary-label")
|
|
115
121
|
yield Markdown("", id="detail-summary-md")
|
|
116
122
|
|
|
117
123
|
def on_mount(self) -> None:
|
|
118
|
-
self.border_title = "Detail"
|
|
124
|
+
self.border_title = "◈ Detail"
|
|
119
125
|
self.query_one("#detail-summary-label").display = False
|
|
120
126
|
self.query_one("#detail-meta-block").display = False
|
|
121
127
|
self.query_one("#detail-title").display = False
|
|
@@ -168,15 +174,15 @@ class SessionDetail(Widget):
|
|
|
168
174
|
self.query_one("#detail-meta-block").display = True
|
|
169
175
|
self.query_one("#detail-summary-label").display = True
|
|
170
176
|
|
|
171
|
-
self.query_one("#detail-title", Static).update(f"[bold
|
|
172
|
-
self.query_one("#detail-tool", Static).update(_meta_line("Tool", tool_markup
|
|
173
|
-
self.query_one("#detail-cwd", Static).update(_meta_line("
|
|
174
|
-
self.query_one("#detail-tokens", Static).update(_meta_line("Tokens", tok_markup
|
|
175
|
-
self.query_one("#detail-created", Static).update(_meta_line("Created", f"[dim]{created_str}[/dim]"
|
|
176
|
-
self.query_one("#detail-updated", Static).update(_meta_line("Updated", f"[dim]{updated_str}[/dim]"
|
|
177
|
-
self.query_one("#detail-status", Static).update(_meta_line("Status",
|
|
178
|
-
self.query_one("#detail-tags", Static).update(_meta_line("Tags",
|
|
179
|
-
self.query_one("#detail-id", Static).update(_meta_line("ID", f"[dim]{session_id}[/dim]"
|
|
177
|
+
self.query_one("#detail-title", Static).update(f"[bold]{title}[/bold]")
|
|
178
|
+
self.query_one("#detail-tool", Static).update(_meta_line("Tool", tool_markup))
|
|
179
|
+
self.query_one("#detail-cwd", Static).update(_meta_line("Project", f"[dim]{cwd}[/dim]"))
|
|
180
|
+
self.query_one("#detail-tokens", Static).update(_meta_line("Tokens", tok_markup))
|
|
181
|
+
self.query_one("#detail-created", Static).update(_meta_line("Created", f"[dim]{created_str}[/dim]"))
|
|
182
|
+
self.query_one("#detail-updated", Static).update(_meta_line("Updated", f"[dim]{updated_str}[/dim]"))
|
|
183
|
+
self.query_one("#detail-status", Static).update(_meta_line("Status", f"[dim]{status}[/dim]"))
|
|
184
|
+
self.query_one("#detail-tags", Static).update(_meta_line("Tags", f"[dim]{tags}[/dim]"))
|
|
185
|
+
self.query_one("#detail-id", Static).update(_meta_line("ID", f"[dim]{session_id}[/dim]"))
|
|
180
186
|
|
|
181
187
|
md = self.query_one("#detail-summary-md", Markdown)
|
|
182
188
|
self.app.call_later(md.update, summary)
|
{context_forge_cli-0.4.4 → context_forge_cli-0.4.6}/contextforge/tui/widgets/session_table.py
RENAMED
|
@@ -12,31 +12,36 @@ from textual.reactive import reactive
|
|
|
12
12
|
from textual.widget import Widget
|
|
13
13
|
from textual.widgets import Button, DataTable, Input, Label
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
15
|
+
# Unicode symbols that evoke each tool brand (terminals can't render images)
|
|
16
|
+
TOOL_ICON = {
|
|
17
|
+
"claude_code": "◆", # Anthropic — solid diamond
|
|
18
|
+
"codex": "⬡", # OpenAI — hexagon
|
|
19
|
+
"altimate_code": "⚡", # Altimate — lightning
|
|
20
|
+
"claude_desktop": "◇", # Claude Desktop — open diamond
|
|
21
|
+
"gemini": "✦", # Google Gemini — four-pointed star
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
TOOL_MARKUP = {
|
|
23
|
-
"claude_code":
|
|
24
|
-
"codex":
|
|
25
|
-
"altimate_code":
|
|
26
|
-
"claude_desktop": "[yellow]
|
|
25
|
+
"claude_code": "[cyan]◆ Claude[/cyan]",
|
|
26
|
+
"codex": "[green]⬡ Codex[/green]",
|
|
27
|
+
"altimate_code": "[magenta]⚡ Alt[/magenta]",
|
|
28
|
+
"claude_desktop": "[yellow]◇ Desktop[/yellow]",
|
|
29
|
+
"gemini": "[blue]✦ Gemini[/blue]",
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
TOOL_DISPLAY = {
|
|
30
|
-
"claude_code": "
|
|
31
|
-
"codex":
|
|
32
|
-
"altimate_code":
|
|
33
|
-
"claude_desktop": "Desktop",
|
|
33
|
+
"claude_code": "◆ Claude",
|
|
34
|
+
"codex": "⬡ Codex",
|
|
35
|
+
"altimate_code": "⚡ Alt",
|
|
36
|
+
"claude_desktop": "◇ Desktop",
|
|
37
|
+
"gemini": "✦ Gemini",
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
_FTOOL_MAP = {
|
|
37
41
|
"ftool-all": None,
|
|
38
42
|
"ftool-cc": "claude_code",
|
|
39
43
|
"ftool-codex": "codex",
|
|
44
|
+
"ftool-gem": "gemini",
|
|
40
45
|
"ftool-alt": "altimate_code",
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -89,14 +94,16 @@ class SessionTable(Widget):
|
|
|
89
94
|
SessionTable #filter-label {
|
|
90
95
|
width: auto;
|
|
91
96
|
margin-right: 1;
|
|
92
|
-
color: $
|
|
97
|
+
color: $accent;
|
|
98
|
+
text-style: bold;
|
|
93
99
|
}
|
|
94
100
|
SessionTable #filter-input {
|
|
95
|
-
width:
|
|
101
|
+
width: 26;
|
|
96
102
|
margin-right: 1;
|
|
103
|
+
border: tall $primary;
|
|
97
104
|
}
|
|
98
105
|
SessionTable .filter-tool-btn {
|
|
99
|
-
min-width:
|
|
106
|
+
min-width: 9;
|
|
100
107
|
height: 1;
|
|
101
108
|
margin-right: 1;
|
|
102
109
|
background: $surface;
|
|
@@ -147,14 +154,15 @@ class SessionTable(Widget):
|
|
|
147
154
|
with Horizontal(id="filter-bar", classes="hidden"):
|
|
148
155
|
yield Label("Filter:", id="filter-label")
|
|
149
156
|
yield Input(placeholder="title or project…", id="filter-input")
|
|
150
|
-
yield Button("All",
|
|
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("
|
|
154
|
-
|
|
157
|
+
yield Button("All", id="ftool-all", classes="filter-tool-btn active")
|
|
158
|
+
yield Button("◆ CC", id="ftool-cc", classes="filter-tool-btn")
|
|
159
|
+
yield Button("⬡ Codex", id="ftool-codex", classes="filter-tool-btn")
|
|
160
|
+
yield Button("✦ Gem", id="ftool-gem", classes="filter-tool-btn")
|
|
161
|
+
yield Button("⚡ Alt", id="ftool-alt", classes="filter-tool-btn")
|
|
162
|
+
yield DataTable(cursor_type="row", zebra_stripes=True, id="sessions-datatable")
|
|
155
163
|
|
|
156
164
|
def on_mount(self) -> None:
|
|
157
|
-
self.border_title = "Sessions"
|
|
165
|
+
self.border_title = "◈ Sessions"
|
|
158
166
|
table = self.query_one(DataTable)
|
|
159
167
|
table.add_columns("Tool", "Title", "Updated", "Tokens", "ID")
|
|
160
168
|
self.reload()
|
|
@@ -210,7 +218,7 @@ class SessionTable(Widget):
|
|
|
210
218
|
tool_label = TOOL_MARKUP.get(tool, f"⚪ {tool}")
|
|
211
219
|
|
|
212
220
|
from contextforge.utils.display import _clean_title
|
|
213
|
-
title = _clean_title(row.get("title") or "", max_len=
|
|
221
|
+
title = _clean_title(row.get("title") or "", max_len=40) or "(no title)"
|
|
214
222
|
|
|
215
223
|
updated_ms = row.get("updated_at") or 0
|
|
216
224
|
try:
|