context-forge-cli 0.4.14__tar.gz → 0.4.15__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.14 → context_forge_cli-0.4.15}/PKG-INFO +5 -2
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/README.md +4 -1
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/cli.py +12 -3
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/session_table.py +107 -20
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/utils/display.py +11 -1
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/pyproject.toml +1 -1
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/core/test_display.py +24 -1
- context_forge_cli-0.4.15/tests/test_tui_table.py +128 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/uv.lock +1 -1
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/.contextforge.toml +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/.github/workflows/publish.yml +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/.gitignore +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/.python-version +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/AGENTS.md +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/PLAN.md +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/altimate_code.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/antigravity.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/base.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/claude_code.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/claude_desktop.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/codex.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/cursor.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/datatzis.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/gemini.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/pi.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/registry.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/core/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/core/analytics.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/core/compactor.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/core/db.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/core/injector.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/core/scanner.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/core/summarizer.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/core/token_analyzer.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/mcp_server.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/models/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/models/config.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/models/session.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/app.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/clipboard.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/styles.tcss +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/session_detail.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/stats_panel.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/status_bar.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/tokens_panel.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/transfer_panel.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/utils/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/utils/tokens.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/adapters/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/adapters/test_antigravity.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/adapters/test_claude_code.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/adapters/test_cursor.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/adapters/test_datatzis.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/adapters/test_pi.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/conftest.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/core/__init__.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/core/test_compactor.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/core/test_db.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/core/test_injector.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/core/test_summarizer.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/core/test_token_analyzer.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/claude_session_sample.jsonl +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/datatzis/my-repo/.datatzis/tasks/22222222222222222222222222222222.json +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/datatzis/my-repo/.datatzis/tasks/33333333333333333333333333333333.json +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/datatzis/my-repo/.datatzis/workspaces/11111111111111111111111111111111.json +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/datatzis/my-repo/.datatzis/workspaces/99999999999999999999999999999999.json +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/pi/--home-dev-project-a-/2026-01-15T10-30-00-000Z_a1b2c3d4-0000-4000-8000-000000000001/e5f61122-3333-4444-5555-666677778888/run-0/session.jsonl +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/pi/--home-dev-project-a-/2026-01-15T10-30-00-000Z_a1b2c3d4-0000-4000-8000-000000000001/e5f61122-3333-4444-5555-666677778888/run-1/session.jsonl +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/pi/--home-dev-project-a-/2026-01-15T10-30-00-000Z_a1b2c3d4-0000-4000-8000-000000000001.jsonl +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/pi/--home-dev-project-a-/2026-01-15T11-20-00-000Z_c9d8e7f6-0000-4000-8000-000000000003.jsonl +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/test_mcp_server.py +0 -0
- {context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/test_tui_detail.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: context-forge-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.15
|
|
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
|
|
@@ -180,7 +180,10 @@ Notes:
|
|
|
180
180
|
- **Sub-agent tracking** — sessions spawned by sub-agents are tracked and cross-linked: Pi
|
|
181
181
|
(`pi-subagents`) child transcripts get `subagent` + `parent:<session-id>` tags naming the
|
|
182
182
|
parent session; the datatzis harness's orchestrator tasks (one per user prompt) get
|
|
183
|
-
`subagent` + `workspace:<session-id>` tags.
|
|
183
|
+
`subagent` + `workspace:<session-id>` tags. Surfaces are parent-first: `cf ls` shows only
|
|
184
|
+
main sessions unless `--all` is passed, and in the TUI dashboard sub-agents stay hidden
|
|
185
|
+
until you select their parent row, which nests them underneath as an indented `↳` list —
|
|
186
|
+
selecting the parent again collapses them.
|
|
184
187
|
- **datatzis discovery** — per-repo storage: ContextForge scans repository roots for
|
|
185
188
|
`*/.datatzis/workspaces/` (up to two levels deep). Configure roots via the
|
|
186
189
|
`DATATZIS_SEARCH_ROOTS` environment variable (colon-separated; default `~/Documents/Github`).
|
|
@@ -151,7 +151,10 @@ Notes:
|
|
|
151
151
|
- **Sub-agent tracking** — sessions spawned by sub-agents are tracked and cross-linked: Pi
|
|
152
152
|
(`pi-subagents`) child transcripts get `subagent` + `parent:<session-id>` tags naming the
|
|
153
153
|
parent session; the datatzis harness's orchestrator tasks (one per user prompt) get
|
|
154
|
-
`subagent` + `workspace:<session-id>` tags.
|
|
154
|
+
`subagent` + `workspace:<session-id>` tags. Surfaces are parent-first: `cf ls` shows only
|
|
155
|
+
main sessions unless `--all` is passed, and in the TUI dashboard sub-agents stay hidden
|
|
156
|
+
until you select their parent row, which nests them underneath as an indented `↳` list —
|
|
157
|
+
selecting the parent again collapses them.
|
|
155
158
|
- **datatzis discovery** — per-repo storage: ContextForge scans repository roots for
|
|
156
159
|
`*/.datatzis/workspaces/` (up to two levels deep). Configure roots via the
|
|
157
160
|
`DATATZIS_SEARCH_ROOTS` environment variable (colon-separated; default `~/Documents/Github`).
|
|
@@ -21,6 +21,7 @@ from contextforge.utils.display import (
|
|
|
21
21
|
console,
|
|
22
22
|
display_safe,
|
|
23
23
|
err_console,
|
|
24
|
+
is_subagent as display_is_subagent,
|
|
24
25
|
row_tags as display_row_tags,
|
|
25
26
|
sessions_table,
|
|
26
27
|
)
|
|
@@ -130,9 +131,10 @@ def scan_cmd(
|
|
|
130
131
|
def list_sessions(
|
|
131
132
|
tool: Annotated[Optional[str], typer.Option("--tool", "-t")] = None,
|
|
132
133
|
limit: Annotated[int, typer.Option("--limit", "-n")] = 50,
|
|
134
|
+
all_sessions: Annotated[bool, typer.Option("--all", "-a", help="Include sub-agent (child) sessions")] = False,
|
|
133
135
|
fmt: Annotated[str, typer.Option("--format", "-f")] = "table",
|
|
134
136
|
):
|
|
135
|
-
"""List sessions."""
|
|
137
|
+
"""List sessions (parent sessions only, unless --all)."""
|
|
136
138
|
cfg = _get_config()
|
|
137
139
|
database = _get_db(cfg)
|
|
138
140
|
rows = db_module.get_sessions(database, tool=tool, limit=limit)
|
|
@@ -145,9 +147,16 @@ def list_sessions(
|
|
|
145
147
|
console.print("[dim]No sessions found. Run [bold]cf scan[/bold] first.[/dim]")
|
|
146
148
|
return
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
hidden = sum(1 for r in rows if display_is_subagent(r))
|
|
151
|
+
table = sessions_table(rows, include_subagents=all_sessions)
|
|
149
152
|
console.print(table)
|
|
150
|
-
|
|
153
|
+
if all_sessions or not hidden:
|
|
154
|
+
console.print(f"[dim]{len(rows)} sessions[/dim]")
|
|
155
|
+
else:
|
|
156
|
+
console.print(
|
|
157
|
+
f"[dim]{len(rows) - hidden} sessions "
|
|
158
|
+
f"(+{hidden} sub-agent hidden — use --all to show)[/dim]"
|
|
159
|
+
)
|
|
151
160
|
|
|
152
161
|
|
|
153
162
|
# ---------------------------------------------------------------------------
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/session_table.py
RENAMED
|
@@ -163,6 +163,8 @@ class SessionTable(Widget):
|
|
|
163
163
|
self._filter_text: str = ""
|
|
164
164
|
self._filter_tool: str | None = None
|
|
165
165
|
self._filter_visible: bool = False
|
|
166
|
+
self._expanded_parents: set[str] = set()
|
|
167
|
+
self._preserve_cursor_id: str | None = None
|
|
166
168
|
|
|
167
169
|
# ── Compose ───────────────────────────────────────────────────────────────
|
|
168
170
|
|
|
@@ -183,7 +185,7 @@ class SessionTable(Widget):
|
|
|
183
185
|
yield DataTable(cursor_type="row", zebra_stripes=True, id="sessions-datatable")
|
|
184
186
|
|
|
185
187
|
def on_mount(self) -> None:
|
|
186
|
-
self.border_title = "◈ Sessions"
|
|
188
|
+
self.border_title = "◈ Sessions — select a parent to expand sub-agents"
|
|
187
189
|
table = self.query_one(DataTable)
|
|
188
190
|
table.add_columns("Tool", "Title", "Updated", "Tokens", "ID")
|
|
189
191
|
self.reload()
|
|
@@ -223,8 +225,82 @@ class SessionTable(Widget):
|
|
|
223
225
|
db = get_db(db_path)
|
|
224
226
|
rows = get_sessions(db, tool=tool_filter, limit=500)
|
|
225
227
|
self._rows = rows
|
|
226
|
-
self.
|
|
227
|
-
|
|
228
|
+
self._rerender()
|
|
229
|
+
|
|
230
|
+
# ── Hierarchy: parent sessions first, sub-agents under their parent ───────
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def _children_by_parent(rows: list[dict]) -> dict[str, list[dict]]:
|
|
234
|
+
from contextforge.utils.display import parent_id_of
|
|
235
|
+
|
|
236
|
+
children: dict[str, list[dict]] = {}
|
|
237
|
+
for row in rows:
|
|
238
|
+
parent = parent_id_of(row)
|
|
239
|
+
if parent:
|
|
240
|
+
children.setdefault(parent, []).append(row)
|
|
241
|
+
return children
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _mains(rows: list[dict]) -> list[dict]:
|
|
245
|
+
from contextforge.utils.display import is_subagent
|
|
246
|
+
return [row for row in rows if not is_subagent(row)]
|
|
247
|
+
|
|
248
|
+
def _build_display_rows(self, mains: list[dict]) -> list[dict]:
|
|
249
|
+
"""Main sessions in order, with children of expanded parents nested."""
|
|
250
|
+
children = self._children_by_parent(self._rows)
|
|
251
|
+
display: list[dict] = []
|
|
252
|
+
for main in mains:
|
|
253
|
+
display.append(main)
|
|
254
|
+
if main["id"] in self._expanded_parents:
|
|
255
|
+
for child in children.get(main["id"], []):
|
|
256
|
+
row = dict(child)
|
|
257
|
+
row["_is_child"] = True
|
|
258
|
+
display.append(row)
|
|
259
|
+
return display
|
|
260
|
+
|
|
261
|
+
def _rerender(self) -> None:
|
|
262
|
+
"""Recompute the display list (filter + expansion) and render."""
|
|
263
|
+
self._apply_filter_and_render()
|
|
264
|
+
|
|
265
|
+
def _apply_filter_and_render(self) -> None:
|
|
266
|
+
text = self._filter_text.lower()
|
|
267
|
+
tool = self._filter_tool
|
|
268
|
+
|
|
269
|
+
def _row_matches(row: dict) -> bool:
|
|
270
|
+
if tool and row.get("tool") != tool:
|
|
271
|
+
return False
|
|
272
|
+
if not text:
|
|
273
|
+
return True
|
|
274
|
+
return (
|
|
275
|
+
text in (row.get("title") or "").lower()
|
|
276
|
+
or text in (row.get("cwd") or "").lower()
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
children = self._children_by_parent(self._rows)
|
|
280
|
+
|
|
281
|
+
filtered: list[dict] = []
|
|
282
|
+
for main in self._mains(self._rows):
|
|
283
|
+
if _row_matches(main):
|
|
284
|
+
filtered.append(main)
|
|
285
|
+
continue
|
|
286
|
+
# A child's match keeps its parent (and its siblings) visible
|
|
287
|
+
for child in children.get(main["id"], []):
|
|
288
|
+
if _row_matches(child):
|
|
289
|
+
filtered.append(main)
|
|
290
|
+
break
|
|
291
|
+
self._render_rows(self._build_display_rows(filtered))
|
|
292
|
+
self.post_message(self.FilterChanged(self._filter_text, tool, len(self._display_rows)))
|
|
293
|
+
|
|
294
|
+
def toggle_expansion(self, parent_id: str | None) -> None:
|
|
295
|
+
"""Expand (or collapse) a parent session's sub-agent rows in place."""
|
|
296
|
+
if not parent_id:
|
|
297
|
+
return
|
|
298
|
+
if parent_id in self._expanded_parents:
|
|
299
|
+
self._expanded_parents.discard(parent_id)
|
|
300
|
+
else:
|
|
301
|
+
self._expanded_parents.add(parent_id)
|
|
302
|
+
self._preserve_cursor_id = parent_id
|
|
303
|
+
self._rerender()
|
|
228
304
|
|
|
229
305
|
# ── Internal rendering ────────────────────────────────────────────────────
|
|
230
306
|
|
|
@@ -232,16 +308,22 @@ class SessionTable(Widget):
|
|
|
232
308
|
"""Populate the DataTable from a list of session dicts."""
|
|
233
309
|
self._display_rows = rows
|
|
234
310
|
table = self.query_one(DataTable)
|
|
311
|
+
restore_id = self._preserve_cursor_id
|
|
312
|
+
restore_row_index: int | None = None
|
|
235
313
|
table.clear()
|
|
236
314
|
|
|
237
|
-
|
|
315
|
+
from contextforge.utils.display import _clean_title, display_safe, is_subagent
|
|
316
|
+
|
|
317
|
+
for idx, row in enumerate(rows):
|
|
238
318
|
tool = row.get("tool", "")
|
|
319
|
+
is_child = bool(row.get("_is_child"))
|
|
239
320
|
tool_label = TOOL_MARKUP.get(tool, f"⚪ {tool}")
|
|
240
321
|
|
|
241
|
-
from contextforge.utils.display import _clean_title, display_safe, is_subagent
|
|
242
322
|
title = _clean_title(row.get("title") or "", max_len=40) or "(no title)"
|
|
243
323
|
if is_subagent(row):
|
|
244
324
|
title = f"↳ {title}"
|
|
325
|
+
if is_child:
|
|
326
|
+
title = f" {title}"
|
|
245
327
|
title = display_safe(title)
|
|
246
328
|
|
|
247
329
|
updated_ms = row.get("updated_at") or 0
|
|
@@ -255,23 +337,17 @@ class SessionTable(Widget):
|
|
|
255
337
|
session_id = str(row.get("id", ""))[:12]
|
|
256
338
|
|
|
257
339
|
table.add_row(tool_label, title, updated, tokens, session_id)
|
|
340
|
+
if restore_id is not None and str(row.get("id", "")) == restore_id:
|
|
341
|
+
restore_row_index = idx
|
|
258
342
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
tool = self._filter_tool
|
|
343
|
+
if restore_row_index is not None:
|
|
344
|
+
table.move_cursor(row=restore_row_index)
|
|
345
|
+
self._preserve_cursor_id = None
|
|
263
346
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
not text
|
|
269
|
-
or text in (row.get("title") or "").lower()
|
|
270
|
-
or text in (row.get("cwd") or "").lower()
|
|
271
|
-
)
|
|
272
|
-
]
|
|
273
|
-
self._render_rows(filtered)
|
|
274
|
-
self.post_message(self.FilterChanged(self._filter_text, tool, len(filtered)))
|
|
347
|
+
def _apply_filter(self) -> None:
|
|
348
|
+
"""Filter in memory: parent rows match their own fields or any of
|
|
349
|
+
their children's; then re-render with the current expansion state."""
|
|
350
|
+
self._apply_filter_and_render()
|
|
275
351
|
|
|
276
352
|
# ── Event handlers ────────────────────────────────────────────────────────
|
|
277
353
|
|
|
@@ -298,6 +374,17 @@ class SessionTable(Widget):
|
|
|
298
374
|
btn.remove_class("active")
|
|
299
375
|
self._apply_filter()
|
|
300
376
|
|
|
377
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
378
|
+
"""Enter/click on a parent row expands its sub-agent rows beneath it."""
|
|
379
|
+
idx = event.cursor_row
|
|
380
|
+
display = self._display_rows
|
|
381
|
+
if 0 <= idx < len(display):
|
|
382
|
+
row = display[idx]
|
|
383
|
+
from contextforge.utils.display import is_subagent
|
|
384
|
+
if not is_subagent(row):
|
|
385
|
+
self.toggle_expansion(str(row["id"]))
|
|
386
|
+
event.stop()
|
|
387
|
+
|
|
301
388
|
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
302
389
|
idx = event.cursor_row
|
|
303
390
|
display = self._display_rows
|
|
@@ -94,7 +94,17 @@ def _project_name(cwd: str) -> str:
|
|
|
94
94
|
return parts[-1]
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def sessions_table(
|
|
97
|
+
def sessions_table(
|
|
98
|
+
rows: list[dict],
|
|
99
|
+
include_subagents: bool = False,
|
|
100
|
+
) -> Table:
|
|
101
|
+
"""Build a table of sessions.
|
|
102
|
+
|
|
103
|
+
Sub-agent (child) sessions are hidden by default — surfaces show the
|
|
104
|
+
parent-first view and may opt in with ``include_subagents=True``; rows
|
|
105
|
+
then carry a ``↳`` prefix."""
|
|
106
|
+
if not include_subagents:
|
|
107
|
+
rows = [r for r in rows if not is_subagent(r)]
|
|
98
108
|
table = Table(
|
|
99
109
|
show_header=True,
|
|
100
110
|
header_style="bold",
|
|
@@ -63,6 +63,29 @@ def test_sessions_table_escapes_hostile_titles() -> None:
|
|
|
63
63
|
assert "Says ❰bold❱hi❰/bold❱ with ❰/weird/brackets." in exported
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def test_sessions_table_hides_subagents_by_default() -> None:
|
|
67
|
+
"""Parent-first: `cf ls` shows main sessions unless --all is given."""
|
|
68
|
+
now = "2026-01-15T12:31:00+00:00"
|
|
69
|
+
rows = [
|
|
70
|
+
{"id": "b" * 32, "tool": "pi", "title": "Parent chat", "cwd": "/proj",
|
|
71
|
+
"token_count": 9, "updated_at": now, "tags": "[]", "summary": ""},
|
|
72
|
+
{"id": "a" * 32, "tool": "pi", "title": "Child run", "cwd": "/proj",
|
|
73
|
+
"token_count": 5, "updated_at": now,
|
|
74
|
+
"tags": json.dumps(["subagent", "parent:" + "b" * 32]), "summary": ""},
|
|
75
|
+
]
|
|
76
|
+
console = Console(record=True, width=200)
|
|
77
|
+
console.print(sessions_table(rows))
|
|
78
|
+
exported = console.export_text()
|
|
79
|
+
assert "Parent chat" in exported
|
|
80
|
+
assert "Child run" not in exported
|
|
81
|
+
|
|
82
|
+
console = Console(record=True, width=200)
|
|
83
|
+
console.print(sessions_table(rows, include_subagents=True))
|
|
84
|
+
exported = console.export_text()
|
|
85
|
+
assert "↳ Child run" in exported
|
|
86
|
+
assert "Parent chat" in exported
|
|
87
|
+
|
|
88
|
+
|
|
66
89
|
def test_sessions_table_marks_subagent_rows() -> None:
|
|
67
90
|
rows = [
|
|
68
91
|
{"id": "a" * 32, "tool": "pi", "title": "Child run", "cwd": "/proj",
|
|
@@ -72,7 +95,7 @@ def test_sessions_table_marks_subagent_rows() -> None:
|
|
|
72
95
|
"token_count": 9, "updated_at": "2026-01-15T12:30:00+00:00",
|
|
73
96
|
"tags": "[]", "summary": ""},
|
|
74
97
|
]
|
|
75
|
-
table = sessions_table(rows)
|
|
98
|
+
table = sessions_table(rows, include_subagents=True)
|
|
76
99
|
console = Console(record=True, width=200)
|
|
77
100
|
console.print(table)
|
|
78
101
|
exported = console.export_text()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Regression tests for the SessionTable hierarchy: parent sessions first,
|
|
2
|
+
sub-agent rows nested under their expanded parent."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import tempfile
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from textual.app import App, ComposeResult
|
|
12
|
+
from textual.widgets import DataTable
|
|
13
|
+
|
|
14
|
+
from contextforge.core.db import get_db, upsert_session
|
|
15
|
+
from contextforge.models.session import Message, Session
|
|
16
|
+
from contextforge.tui.widgets.session_table import SessionTable
|
|
17
|
+
|
|
18
|
+
PARENT_ID = "1" * 32
|
|
19
|
+
CHILD_ID = "2" * 32
|
|
20
|
+
OTHER_ID = "3" * 32
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _seed(db_path: Path) -> None:
|
|
24
|
+
now = datetime(2026, 1, 15, 12, 30, tzinfo=timezone.utc)
|
|
25
|
+
db = get_db(db_path)
|
|
26
|
+
upsert_session(db, Session(
|
|
27
|
+
id=PARENT_ID, tool="pi", title="Parent chat", cwd="/proj",
|
|
28
|
+
created_at=now, updated_at=now,
|
|
29
|
+
messages=[Message(role="user", content="parent")],
|
|
30
|
+
))
|
|
31
|
+
upsert_session(db, Session(
|
|
32
|
+
id=CHILD_ID, tool="pi", title="Child worker run", cwd="/proj",
|
|
33
|
+
created_at=now, updated_at=now,
|
|
34
|
+
messages=[Message(role="user", content="child")],
|
|
35
|
+
tags=["subagent", f"parent:{PARENT_ID}"],
|
|
36
|
+
))
|
|
37
|
+
upsert_session(db, Session(
|
|
38
|
+
id=OTHER_ID, tool="codex", title="Another parent chat", cwd="/proj2",
|
|
39
|
+
created_at=now, updated_at=now,
|
|
40
|
+
messages=[Message(role="user", content="other")],
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _HarnessApp(App):
|
|
45
|
+
def __init__(self, db_path: Path) -> None:
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.db_path = db_path
|
|
48
|
+
|
|
49
|
+
def compose(self) -> ComposeResult:
|
|
50
|
+
yield SessionTable()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_session_table_parents_first_and_expandable():
|
|
54
|
+
async def scenario():
|
|
55
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
56
|
+
db_path = Path(tmpdir) / "test.db"
|
|
57
|
+
_seed(db_path)
|
|
58
|
+
app = _HarnessApp(db_path)
|
|
59
|
+
async with app.run_test() as pilot:
|
|
60
|
+
table = app.query_one(SessionTable)
|
|
61
|
+
await pilot.pause()
|
|
62
|
+
dt = table.query_one(DataTable)
|
|
63
|
+
|
|
64
|
+
# At first glance: parent sessions only, hidden children
|
|
65
|
+
assert len(table._display_rows) == 2
|
|
66
|
+
assert [r["id"][:1] for r in table._display_rows] == ["1", "3"]
|
|
67
|
+
|
|
68
|
+
# Selecting (entering) the parent expands its children in place
|
|
69
|
+
table.toggle_expansion(PARENT_ID)
|
|
70
|
+
await pilot.pause()
|
|
71
|
+
assert len(table._display_rows) == 3
|
|
72
|
+
assert table._display_rows[0]["id"] == PARENT_ID
|
|
73
|
+
assert table._display_rows[1]["id"] == CHILD_ID
|
|
74
|
+
assert table._display_rows[1]["_is_child"] is True
|
|
75
|
+
assert table._display_rows[2]["id"] == OTHER_ID
|
|
76
|
+
|
|
77
|
+
# Toggling again collapses
|
|
78
|
+
table.toggle_expansion(PARENT_ID)
|
|
79
|
+
await pilot.pause()
|
|
80
|
+
assert len(table._display_rows) == 2
|
|
81
|
+
|
|
82
|
+
asyncio.run(scenario())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_session_table_child_match_surfaces_parent():
|
|
86
|
+
async def scenario():
|
|
87
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
88
|
+
db_path = Path(tmpdir) / "test.db"
|
|
89
|
+
_seed(db_path)
|
|
90
|
+
app = _HarnessApp(db_path)
|
|
91
|
+
async with app.run_test() as pilot:
|
|
92
|
+
table = app.query_one(SessionTable)
|
|
93
|
+
await pilot.pause()
|
|
94
|
+
|
|
95
|
+
# Text filter matching only the CHILD surfaces its parent
|
|
96
|
+
table._filter_text = "worker"
|
|
97
|
+
table._apply_filter()
|
|
98
|
+
await pilot.pause()
|
|
99
|
+
ids = [r["id"] for r in table._display_rows]
|
|
100
|
+
assert ids == [PARENT_ID]
|
|
101
|
+
|
|
102
|
+
asyncio.run(scenario())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_session_table_expansion_survives_filter_toggle():
|
|
106
|
+
async def scenario():
|
|
107
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
108
|
+
db_path = Path(tmpdir) / "test.db"
|
|
109
|
+
_seed(db_path)
|
|
110
|
+
app = _HarnessApp(db_path)
|
|
111
|
+
async with app.run_test() as pilot:
|
|
112
|
+
table = app.query_one(SessionTable)
|
|
113
|
+
await pilot.pause()
|
|
114
|
+
table.toggle_expansion(PARENT_ID)
|
|
115
|
+
await pilot.pause()
|
|
116
|
+
|
|
117
|
+
table._filter_tool = "codex"
|
|
118
|
+
table._apply_filter()
|
|
119
|
+
await pilot.pause()
|
|
120
|
+
assert len(table._display_rows) == 1 # only the codex parent
|
|
121
|
+
|
|
122
|
+
table._filter_tool = None
|
|
123
|
+
table._apply_filter()
|
|
124
|
+
await pilot.pause()
|
|
125
|
+
# Expansion state persists across filter changes
|
|
126
|
+
assert len(table._display_rows) == 3
|
|
127
|
+
|
|
128
|
+
asyncio.run(scenario())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/altimate_code.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/adapters/claude_desktop.py
RENAMED
|
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
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/session_detail.py
RENAMED
|
File without changes
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/stats_panel.py
RENAMED
|
File without changes
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/status_bar.py
RENAMED
|
File without changes
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/tokens_panel.py
RENAMED
|
File without changes
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/contextforge/tui/widgets/transfer_panel.py
RENAMED
|
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
|
{context_forge_cli-0.4.14 → context_forge_cli-0.4.15}/tests/fixtures/claude_session_sample.jsonl
RENAMED
|
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
|