agent-bios 0.17.1 → 0.19.0
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.
- package/DEPENDENCIES.md +236 -80
- package/INSTALL.md +112 -0
- package/README.md +184 -486
- package/claude/CLAUDE.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +1 -1
- package/claude/guides/learning-flow.md +23 -12
- package/claude/guides/session-distill-workflow.md +22 -12
- package/codex/AGENTS.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +1 -1
- package/codex/guides/learning-flow.md +23 -12
- package/codex/guides/session-distill-workflow.md +22 -12
- package/compose/app_bridge/SKILL.md +75 -0
- package/compose/app_bridge/agents/openai.yaml +2 -0
- package/compose/app_bridge/scripts/bridge.py +76 -0
- package/compose/bootstrap/SKILL.md +23 -2
- package/compose/corpus.py +31 -9
- package/compose/corpus_app.py +456 -0
- package/compose/corpus_import.py +529 -0
- package/compose/corpus_install.py +196 -18
- package/compose/corpus_session.py +27 -0
- package/compose/corpus_setup.py +674 -0
- package/compose/corpus_setup_cli.py +582 -0
- package/compose/corpus_setup_i18n.py +318 -0
- package/compose/corpus_setup_ui.py +633 -0
- package/compose/corpus_store.py +236 -34
- package/compose/corpus_transaction.py +43 -10
- package/compose/corpus_ui.py +279 -8
- package/compose/corpus_ui_runtime.py +278 -0
- package/compose/corpus_understand.py +6 -1
- package/compose/setup/START.md +147 -0
- package/compose/ui_runtime/linkify_it_py-2.2.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/manifest.json +238 -0
- package/compose/ui_runtime/markdown_it_py-4.2.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/mdit_py_plugins-0.6.1-py3-none-any.whl +0 -0
- package/compose/ui_runtime/mdurl-0.1.2-py3-none-any.whl +0 -0
- package/compose/ui_runtime/platformdirs-4.11.8-py3-none-any.whl +0 -0
- package/compose/ui_runtime/pygments-2.21.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/rich-15.0.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/textual-8.2.8-py3-none-any.whl +0 -0
- package/compose/ui_runtime/typing_extensions-4.16.0-py3-none-any.whl +0 -0
- package/docs/advanced-launch.md +131 -0
- package/docs/assets/corpus-studio.svg +227 -0
- package/docs/corpus.md +117 -0
- package/docs/recovery.md +201 -0
- package/docs/session-model.md +120 -0
- package/docs/setup.md +190 -0
- package/docs/understand.md +40 -0
- package/install.sh +75 -46
- package/launch/agent-launch.py +91 -47
- package/launch/provision-venv.sh +44 -13
- package/learn/collect-learning.py +14 -5
- package/learn/learning.schema.json +2 -2
- package/package.json +14 -2
- package/provenance.json +1 -1
- package/wrappers/claude-run.sh +10 -13
package/compose/corpus_ui.py
CHANGED
|
@@ -18,6 +18,7 @@ from rich.text import Text
|
|
|
18
18
|
from textual.app import App, ComposeResult
|
|
19
19
|
from textual.binding import Binding
|
|
20
20
|
from textual.containers import Horizontal, Vertical
|
|
21
|
+
from textual.events import Key
|
|
21
22
|
from textual.screen import ModalScreen
|
|
22
23
|
from textual.widgets import (
|
|
23
24
|
Button,
|
|
@@ -43,6 +44,10 @@ SURFACES = ("always", "relevant", "requested", "event", "delegated")
|
|
|
43
44
|
VIEWS = ("effective", "installed", "change", "diff", "history")
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def availability_label(state: str) -> str:
|
|
48
|
+
return "available" if state == "active" else state
|
|
49
|
+
|
|
50
|
+
|
|
46
51
|
def guide_pointers(item: dict[str, Any], inventory: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
47
52
|
"""Display literal guide references, not inferred dependencies or new routing."""
|
|
48
53
|
if item.get("kind") != "rule":
|
|
@@ -63,6 +68,8 @@ def guide_pointers(item: dict[str, Any], inventory: list[dict[str, Any]]) -> lis
|
|
|
63
68
|
def library_label(item: dict[str, Any], inventory: list[dict[str, Any]]) -> Text:
|
|
64
69
|
pointers = guide_pointers(item, inventory)
|
|
65
70
|
label = Text()
|
|
71
|
+
mark = "-" if item.get("state") == "removed" else "x" if item.get("enabled", True) else " "
|
|
72
|
+
label.append(f"[{mark}]" + ("* " if item.get("enabled_pending") else " "))
|
|
66
73
|
if pointers:
|
|
67
74
|
label.append("→ GUIDE ", style="bold cyan")
|
|
68
75
|
label.append(", ".join(PurePosixPath(pointer["path"]).stem for pointer in pointers))
|
|
@@ -112,6 +119,31 @@ class Confirmation(ModalScreen[bool]):
|
|
|
112
119
|
class CorpusMarkdownViewer(MarkdownViewer):
|
|
113
120
|
"""Keep all links inside Studio; never hand a URL to the operating system."""
|
|
114
121
|
|
|
122
|
+
BINDINGS = [
|
|
123
|
+
Binding("left", "left_or_library", "Library", show=False),
|
|
124
|
+
Binding("up", "up_or_controls", "Controls", show=False),
|
|
125
|
+
Binding("down", "down_or_pending", "Pending choices", show=False),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def action_left_or_library(self) -> None:
|
|
129
|
+
if self.scroll_x <= 0:
|
|
130
|
+
self.app.action_focus_library()
|
|
131
|
+
else:
|
|
132
|
+
self.action_scroll_left()
|
|
133
|
+
|
|
134
|
+
def action_up_or_controls(self) -> None:
|
|
135
|
+
if self.scroll_y <= 0:
|
|
136
|
+
if not self.app._focus_id("member-select"):
|
|
137
|
+
self.app._focus_controls()
|
|
138
|
+
else:
|
|
139
|
+
self.action_scroll_up()
|
|
140
|
+
|
|
141
|
+
def action_down_or_pending(self) -> None:
|
|
142
|
+
if self.scroll_y >= self.max_scroll_y:
|
|
143
|
+
self.app._focus_id("enablement-preview")
|
|
144
|
+
else:
|
|
145
|
+
self.action_scroll_down()
|
|
146
|
+
|
|
115
147
|
async def _on_markdown_link_clicked(self, message: Markdown.LinkClicked) -> None:
|
|
116
148
|
# stop() prevents bubbling, not the inherited MarkdownViewer handler.
|
|
117
149
|
# Its default go() would try to load corpus:// as a filesystem path.
|
|
@@ -124,6 +156,57 @@ class CorpusMarkdownViewer(MarkdownViewer):
|
|
|
124
156
|
self.app.notify("External links are disabled in Corpus Studio.", severity="warning")
|
|
125
157
|
|
|
126
158
|
|
|
159
|
+
class CorpusSearch(Input):
|
|
160
|
+
"""Leave search with arrows without taking editing keys away from text."""
|
|
161
|
+
|
|
162
|
+
BINDINGS = [
|
|
163
|
+
Binding("down", "focus_controls", "Controls", show=False),
|
|
164
|
+
Binding("right", "right_or_view", "View", show=False),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
def action_focus_controls(self) -> None:
|
|
168
|
+
self.app._focus_controls()
|
|
169
|
+
|
|
170
|
+
def action_right_or_view(self) -> None:
|
|
171
|
+
if self.selection.start == self.selection.end == len(self.value):
|
|
172
|
+
self.app._focus_id("view-select")
|
|
173
|
+
else:
|
|
174
|
+
self.action_cursor_right()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CorpusTree(Tree):
|
|
178
|
+
"""Space is local to the library, never intercepted in text inputs."""
|
|
179
|
+
|
|
180
|
+
BINDINGS = [
|
|
181
|
+
Binding("space", "toggle_item", "On/off"),
|
|
182
|
+
Binding("up", "up_or_controls", "Controls", show=False),
|
|
183
|
+
Binding("down", "down_or_pending", "Pending choices", show=False),
|
|
184
|
+
Binding("right", "focus_document", "Document", show=False),
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
def action_up_or_controls(self) -> None:
|
|
188
|
+
if self.cursor_line <= 0:
|
|
189
|
+
self.app._focus_controls()
|
|
190
|
+
else:
|
|
191
|
+
self.action_cursor_up()
|
|
192
|
+
|
|
193
|
+
def action_down_or_pending(self) -> None:
|
|
194
|
+
before = self.cursor_node
|
|
195
|
+
self.action_cursor_down()
|
|
196
|
+
if self.cursor_node is before:
|
|
197
|
+
self.app._focus_id("enablement-preview")
|
|
198
|
+
|
|
199
|
+
def action_focus_document(self) -> None:
|
|
200
|
+
self.app._focus_document()
|
|
201
|
+
|
|
202
|
+
async def action_toggle_item(self) -> None:
|
|
203
|
+
node = self.cursor_node
|
|
204
|
+
if node is not None and isinstance(node.data, str):
|
|
205
|
+
await self.app.toggle_enabled(node.data)
|
|
206
|
+
else:
|
|
207
|
+
self.action_toggle_node()
|
|
208
|
+
|
|
209
|
+
|
|
127
210
|
class CorpusStudio(App[None]):
|
|
128
211
|
"""Searchable corpus library, editor, and revision-bound Preview → Apply flow."""
|
|
129
212
|
|
|
@@ -169,6 +252,9 @@ class CorpusStudio(App[None]):
|
|
|
169
252
|
.actions { height: 3; align-horizontal: right; }
|
|
170
253
|
.actions Button { margin-left: 1; }
|
|
171
254
|
#status-line { height: 1; padding: 0 1; color: $text-muted; }
|
|
255
|
+
#enablement-bar { height: 3; display: none; }
|
|
256
|
+
#enablement-summary { width: 1fr; padding: 1; }
|
|
257
|
+
#enablement-bar Button { margin-right: 1; }
|
|
172
258
|
"""
|
|
173
259
|
|
|
174
260
|
def __init__(self, store: Any) -> None:
|
|
@@ -183,13 +269,16 @@ class CorpusStudio(App[None]):
|
|
|
183
269
|
self.view_item: dict[str, Any] | None = None
|
|
184
270
|
self.member_drafts: dict[str, str] = {}
|
|
185
271
|
self.pending_plan: dict[str, Any] | None = None
|
|
272
|
+
self.toggle_drafts: dict[str, bool] = {}
|
|
273
|
+
self.toggle_original: dict[str, bool] = {}
|
|
274
|
+
self.toggle_revision: str | None = None
|
|
186
275
|
self.mode = "view"
|
|
187
276
|
|
|
188
277
|
def compose(self) -> ComposeResult:
|
|
189
278
|
yield Header()
|
|
190
279
|
with Vertical(id="toolbar"):
|
|
191
280
|
with Horizontal(id="search-row"):
|
|
192
|
-
yield
|
|
281
|
+
yield CorpusSearch(placeholder="Search title, content, ref, package, domain, or state (↓ controls)", id="search")
|
|
193
282
|
yield Select([(view.title(), view) for view in VIEWS], value="effective",
|
|
194
283
|
allow_blank=False, id="view-select")
|
|
195
284
|
with Horizontal(id="action-row"):
|
|
@@ -200,7 +289,7 @@ class CorpusStudio(App[None]):
|
|
|
200
289
|
yield Button("Recover", id="recover", compact=True)
|
|
201
290
|
yield Button("Reset", id="reset", variant="error", compact=True)
|
|
202
291
|
with Horizontal(id="workspace"):
|
|
203
|
-
yield
|
|
292
|
+
yield CorpusTree("Corpus", id="library")
|
|
204
293
|
with Vertical(id="detail"):
|
|
205
294
|
yield Select([], prompt="Bundle document", allow_blank=True, id="member-select")
|
|
206
295
|
yield CorpusMarkdownViewer(
|
|
@@ -240,6 +329,10 @@ class CorpusStudio(App[None]):
|
|
|
240
329
|
with Horizontal(classes="actions"):
|
|
241
330
|
yield Button("Cancel plan", id="plan-cancel")
|
|
242
331
|
yield Button("Apply", id="apply", variant="success")
|
|
332
|
+
with Horizontal(id="enablement-bar"):
|
|
333
|
+
yield Static("", id="enablement-summary", markup=False)
|
|
334
|
+
yield Button("Preview on/off", id="enablement-preview", variant="primary")
|
|
335
|
+
yield Button("Discard on/off", id="enablement-discard")
|
|
243
336
|
yield Static("Stored privately; no host session is activated by this screen.", id="status-line")
|
|
244
337
|
yield Footer()
|
|
245
338
|
|
|
@@ -247,6 +340,63 @@ class CorpusStudio(App[None]):
|
|
|
247
340
|
await self.refresh_library()
|
|
248
341
|
self.query_one("#search", Input).focus()
|
|
249
342
|
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _focusable(widget: Any) -> bool:
|
|
345
|
+
return widget.focusable and all(getattr(node, "display", True) for node in [widget, *widget.ancestors])
|
|
346
|
+
|
|
347
|
+
def _focus_id(self, *ids: str) -> bool:
|
|
348
|
+
# Query the current screen only: arrows must never escape a modal.
|
|
349
|
+
for ident in ids:
|
|
350
|
+
for widget in self.screen.query(f"#{ident}"):
|
|
351
|
+
if self._focusable(widget):
|
|
352
|
+
widget.focus()
|
|
353
|
+
return True
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
def _focus_controls(self) -> None:
|
|
357
|
+
for button in self.screen.query("#action-row Button"):
|
|
358
|
+
if self._focusable(button):
|
|
359
|
+
button.focus()
|
|
360
|
+
return
|
|
361
|
+
self._focus_id("library" if getattr(self.focused, "id", None) == "search" else "search")
|
|
362
|
+
|
|
363
|
+
def _focus_document(self) -> None:
|
|
364
|
+
if self.mode != "view" or self._focus_id("member-select"):
|
|
365
|
+
return
|
|
366
|
+
for viewer in self.screen.query("#wiki"):
|
|
367
|
+
if self._focusable(viewer.document):
|
|
368
|
+
viewer.document.focus()
|
|
369
|
+
|
|
370
|
+
def on_key(self, event: Key) -> None:
|
|
371
|
+
focused = self.focused
|
|
372
|
+
moved = False
|
|
373
|
+
if isinstance(focused, Button) and focused.parent is not None:
|
|
374
|
+
siblings = [button for button in focused.parent.query(Button)
|
|
375
|
+
if button.parent is focused.parent and self._focusable(button)]
|
|
376
|
+
if event.key in {"left", "right"} and focused in siblings:
|
|
377
|
+
index = siblings.index(focused) + (-1 if event.key == "left" else 1)
|
|
378
|
+
if 0 <= index < len(siblings):
|
|
379
|
+
siblings[index].focus()
|
|
380
|
+
moved = True
|
|
381
|
+
elif event.key in {"up", "down"} and len(self.screen_stack) == 1:
|
|
382
|
+
if focused.parent.id == "action-row":
|
|
383
|
+
moved = self._focus_id("search" if event.key == "up" else "library")
|
|
384
|
+
elif focused.parent.id == "enablement-bar":
|
|
385
|
+
moved = self._focus_id("library")
|
|
386
|
+
elif self.mode == "editor" and event.key == "up":
|
|
387
|
+
moved = self._focus_id("editor-body")
|
|
388
|
+
elif isinstance(focused, Select) and not focused.expanded and self.mode == "view":
|
|
389
|
+
if event.key == "left" and focused.id in {"view-select", "member-select"}:
|
|
390
|
+
moved = self._focus_id("search" if focused.id == "view-select" else "library")
|
|
391
|
+
elif event.key == "right" and focused.id == "member-select":
|
|
392
|
+
for viewer in self.screen.query("#wiki"):
|
|
393
|
+
if self._focusable(viewer.document):
|
|
394
|
+
viewer.document.focus()
|
|
395
|
+
moved = True
|
|
396
|
+
if moved:
|
|
397
|
+
event.prevent_default()
|
|
398
|
+
event.stop()
|
|
399
|
+
|
|
250
400
|
def _set_mode(self, mode: str) -> None:
|
|
251
401
|
self.mode = mode
|
|
252
402
|
self.query_one("#member-select", Select).disabled = mode == "preview"
|
|
@@ -257,6 +407,83 @@ class CorpusStudio(App[None]):
|
|
|
257
407
|
self.query_one("#wiki").display = mode == "view"
|
|
258
408
|
self.query_one("#editor-panel").display = mode == "editor"
|
|
259
409
|
self.query_one("#preview-panel").display = mode == "preview"
|
|
410
|
+
self._toggle_controls()
|
|
411
|
+
# Hiding a panel does not reliably release its focused child. Choose a
|
|
412
|
+
# visible, non-destructive entry point for the next screen state.
|
|
413
|
+
if mode == "preview":
|
|
414
|
+
self._focus_id("plan-cancel")
|
|
415
|
+
elif mode == "view":
|
|
416
|
+
self._focus_id("enablement-preview", "library")
|
|
417
|
+
|
|
418
|
+
def _display_items(self) -> list[dict[str, Any]]:
|
|
419
|
+
return [dict(row, enabled=self.toggle_drafts[row["ref"]], enabled_pending=True)
|
|
420
|
+
if row.get("ref") in self.toggle_drafts else row for row in self.items]
|
|
421
|
+
|
|
422
|
+
def _toggle_controls(self) -> None:
|
|
423
|
+
pending = bool(self.toggle_drafts)
|
|
424
|
+
self.query_one("#enablement-bar").display = pending and self.mode == "view"
|
|
425
|
+
self.query_one("#enablement-summary", Static).update(
|
|
426
|
+
f"{len(self.toggle_drafts)} unapplied on/off change(s) · * = pending")
|
|
427
|
+
if self.mode == "view":
|
|
428
|
+
for selector in ("#create", "#reset"):
|
|
429
|
+
self.query_one(selector).disabled = pending
|
|
430
|
+
if pending:
|
|
431
|
+
for selector in ("#edit", "#remove", "#restore", "#recover"):
|
|
432
|
+
self.query_one(selector).disabled = True
|
|
433
|
+
|
|
434
|
+
async def _repaint_enablement(self) -> None:
|
|
435
|
+
displayed = self._display_items()
|
|
436
|
+
by_ref = {row["ref"]: row for row in displayed}
|
|
437
|
+
nodes = [self.query_one("#library", Tree).root]
|
|
438
|
+
while nodes:
|
|
439
|
+
node = nodes.pop()
|
|
440
|
+
if isinstance(node.data, str) and node.data in by_ref:
|
|
441
|
+
node.set_label(library_label(by_ref[node.data], displayed))
|
|
442
|
+
nodes.extend(node.children)
|
|
443
|
+
self._toggle_controls()
|
|
444
|
+
if self.current_ref:
|
|
445
|
+
await self.select_ref(self.current_ref)
|
|
446
|
+
|
|
447
|
+
async def toggle_enabled(self, ref: str) -> None:
|
|
448
|
+
if self.mode != "view":
|
|
449
|
+
return
|
|
450
|
+
row = self._row(ref)
|
|
451
|
+
if row is None or row.get("state") == "removed":
|
|
452
|
+
self.notify("Recover or restore this item before changing its use.", severity="warning")
|
|
453
|
+
return
|
|
454
|
+
if not self.toggle_drafts:
|
|
455
|
+
self.toggle_revision = row["revision"]
|
|
456
|
+
original = self.toggle_original.setdefault(ref, row["enabled"])
|
|
457
|
+
value = not self.toggle_drafts.get(ref, row["enabled"])
|
|
458
|
+
if value == original:
|
|
459
|
+
self.toggle_drafts.pop(ref, None)
|
|
460
|
+
self.toggle_original.pop(ref, None)
|
|
461
|
+
else:
|
|
462
|
+
self.toggle_drafts[ref] = value
|
|
463
|
+
if not self.toggle_drafts:
|
|
464
|
+
self.toggle_revision = None
|
|
465
|
+
await self._repaint_enablement()
|
|
466
|
+
|
|
467
|
+
def _clear_toggle_drafts(self) -> None:
|
|
468
|
+
self.toggle_drafts.clear()
|
|
469
|
+
self.toggle_original.clear()
|
|
470
|
+
self.toggle_revision = None
|
|
471
|
+
|
|
472
|
+
async def _discard_toggles(self) -> None:
|
|
473
|
+
self._clear_toggle_drafts()
|
|
474
|
+
self._toggle_controls()
|
|
475
|
+
await self.refresh_library(self.query_one("#search", Input).value)
|
|
476
|
+
if self.current_ref:
|
|
477
|
+
await self.select_ref(self.current_ref)
|
|
478
|
+
self._focus_id("library")
|
|
479
|
+
|
|
480
|
+
def _preview_toggles(self) -> None:
|
|
481
|
+
if not self.toggle_drafts or self.mode != "view":
|
|
482
|
+
return
|
|
483
|
+
summary = "Use in future sessions (no content is deleted):\n" + "\n".join(
|
|
484
|
+
f"{'ON' if enabled else 'OFF'} {ref}" for ref, enabled in self.toggle_drafts.items())
|
|
485
|
+
self._stage_plan({"operation": "enable", "items": dict(self.toggle_drafts),
|
|
486
|
+
"expected_revision": self.toggle_revision}, summary)
|
|
260
487
|
|
|
261
488
|
async def refresh_library(self, query: str = "") -> None:
|
|
262
489
|
if self.mode != "view":
|
|
@@ -272,20 +499,22 @@ class CorpusStudio(App[None]):
|
|
|
272
499
|
self.query_one("#status-line", Static).update(str(exc))
|
|
273
500
|
return
|
|
274
501
|
visible = [item for item in self.items if self._matches(item, query)]
|
|
502
|
+
displayed = self._display_items()
|
|
503
|
+
display_by_ref = {item["ref"]: item for item in displayed}
|
|
275
504
|
groups: dict[tuple[str, str], Any] = {}
|
|
276
505
|
state_nodes: dict[str, Any] = {}
|
|
277
506
|
for item in visible:
|
|
278
507
|
state = str(item.get("state", "active"))
|
|
279
508
|
package = str(item.get("package_id", "unknown"))
|
|
280
509
|
if state not in state_nodes:
|
|
281
|
-
state_nodes[state] = tree.root.add(state.title())
|
|
510
|
+
state_nodes[state] = tree.root.add(availability_label(state).title())
|
|
282
511
|
state_nodes[state].expand()
|
|
283
512
|
key = (state, package)
|
|
284
513
|
if key not in groups:
|
|
285
514
|
groups[key] = state_nodes[state].add(package)
|
|
286
515
|
groups[key].expand()
|
|
287
516
|
groups[key].add_leaf(
|
|
288
|
-
library_label(item,
|
|
517
|
+
library_label(display_by_ref[item["ref"]], displayed),
|
|
289
518
|
data=item.get("ref"),
|
|
290
519
|
)
|
|
291
520
|
tree.root.expand()
|
|
@@ -296,6 +525,7 @@ class CorpusStudio(App[None]):
|
|
|
296
525
|
)
|
|
297
526
|
if visible and (self.current_ref is None or not any(i.get("ref") == self.current_ref for i in visible)):
|
|
298
527
|
await self.select_ref(str(visible[0]["ref"]))
|
|
528
|
+
self._toggle_controls()
|
|
299
529
|
|
|
300
530
|
@staticmethod
|
|
301
531
|
def _matches(item: dict[str, Any], query: str) -> bool:
|
|
@@ -342,6 +572,7 @@ class CorpusStudio(App[None]):
|
|
|
342
572
|
self.query_one("#remove", Button).disabled = row.get("state") == "removed"
|
|
343
573
|
self.query_one("#restore", Button).disabled = not bool(row.get("baseline_ref"))
|
|
344
574
|
self.query_one("#recover", Button).disabled = not self._recoverable(row)
|
|
575
|
+
self._toggle_controls()
|
|
345
576
|
|
|
346
577
|
def _select_members(self, item: dict[str, Any] | None, preferred: str | None = None) -> None:
|
|
347
578
|
selector = self.query_one("#member-select", Select)
|
|
@@ -376,7 +607,9 @@ class CorpusStudio(App[None]):
|
|
|
376
607
|
+ (f" Available members: {members}." if members else "") + "\n"
|
|
377
608
|
)
|
|
378
609
|
body = render_member_body(item.get("body", ""), item.get("primary_member"), item.get("kind"))
|
|
379
|
-
|
|
610
|
+
displayed = self._display_items()
|
|
611
|
+
display_row = next((entry for entry in displayed if entry.get("ref") == item.get("ref")), row)
|
|
612
|
+
pointers = guide_pointers(item, displayed)
|
|
380
613
|
guide_links = ""
|
|
381
614
|
if pointers:
|
|
382
615
|
lines = []
|
|
@@ -386,8 +619,9 @@ class CorpusStudio(App[None]):
|
|
|
386
619
|
lines.append(f"- `{pointer['path']}` — {pointer['problem']}")
|
|
387
620
|
else:
|
|
388
621
|
destination = quote(target["ref"], safe="@/:")
|
|
622
|
+
use = "ON" if target.get("enabled", True) else "OFF — linked guide disabled"
|
|
389
623
|
lines.append(f"- [{pointer['path']}](corpus://{destination}) — "
|
|
390
|
-
f"**{target['surface']}** · {target.get('state', 'active')}")
|
|
624
|
+
f"**{target['surface']}** · {availability_label(target.get('state', 'active'))} · {use}")
|
|
391
625
|
guide_links = (
|
|
392
626
|
"## Guide pointer\n\nThis rule explicitly references the following guide(s). "
|
|
393
627
|
"The rule keeps its own consumption surface; these links do not change "
|
|
@@ -395,8 +629,11 @@ class CorpusStudio(App[None]):
|
|
|
395
629
|
)
|
|
396
630
|
return (
|
|
397
631
|
f"# {item.get('title', item.get('ref'))}\n\n"
|
|
398
|
-
f"`{item.get('ref')}` · **{row.get('state', 'active')}** · "
|
|
632
|
+
f"`{item.get('ref')}` · **{availability_label(row.get('state', 'active'))}** · "
|
|
399
633
|
f"{item.get('surface')} · {item.get('kind')}\n\n"
|
|
634
|
+
f"Use in future sessions: **{'ON' if display_row.get('enabled', True) else 'OFF'}**"
|
|
635
|
+
+ (" (pending; not applied)" if display_row.get("enabled_pending") else " (saved selection)")
|
|
636
|
+
+ ". Inclusion is not proof of loading or permission to execute.\n\n"
|
|
400
637
|
f"{guide_links}{reconciliation}\n{body}\n\n## Dependencies\n\n{links}\n"
|
|
401
638
|
+ ("\n## Native consumption\n\nRequires explicit `agent-launch --corpus-native`.\n"
|
|
402
639
|
if item.get("kind") == "hook" else "")
|
|
@@ -408,6 +645,26 @@ class CorpusStudio(App[None]):
|
|
|
408
645
|
if isinstance(event.node.data, str):
|
|
409
646
|
await self.select_ref(event.node.data)
|
|
410
647
|
|
|
648
|
+
async def on_tree_node_highlighted(self, event: Tree.NodeHighlighted) -> None:
|
|
649
|
+
tree = self.query_one("#library", Tree)
|
|
650
|
+
# Ignore queued highlights superseded by another cursor move or tree rebuild.
|
|
651
|
+
if self.mode != "view" or event.node is not tree.cursor_node:
|
|
652
|
+
return
|
|
653
|
+
if isinstance(event.node.data, str):
|
|
654
|
+
await self.select_ref(event.node.data)
|
|
655
|
+
return
|
|
656
|
+
# A group is not the previously displayed item: never leave its edit/delete
|
|
657
|
+
# actions live while the cursor points at a different kind of node.
|
|
658
|
+
self.current_ref = None
|
|
659
|
+
self.view_item = None
|
|
660
|
+
self._select_members(None)
|
|
661
|
+
for selector in ("#edit", "#remove", "#restore", "#recover"):
|
|
662
|
+
self.query_one(selector, Button).disabled = True
|
|
663
|
+
label = str(event.node.label.plain)
|
|
664
|
+
await self.query_one("#wiki", CorpusMarkdownViewer).document.update(
|
|
665
|
+
f"# Corpus group\n\n{label}\n\nMove the cursor to a corpus item to read it."
|
|
666
|
+
)
|
|
667
|
+
|
|
411
668
|
async def on_markdown_link_clicked(self, message: Markdown.LinkClicked) -> None:
|
|
412
669
|
# CorpusMarkdownViewer normally handles this before it bubbles. Keeping
|
|
413
670
|
# the app handler makes direct Markdown messages safe in tests and future layouts.
|
|
@@ -459,6 +716,9 @@ class CorpusStudio(App[None]):
|
|
|
459
716
|
def _open_editor(self, item: dict[str, Any] | None) -> None:
|
|
460
717
|
if self.mode != "view":
|
|
461
718
|
return
|
|
719
|
+
if self.toggle_drafts:
|
|
720
|
+
self.notify("Apply or discard on/off changes first.", severity="warning")
|
|
721
|
+
return
|
|
462
722
|
preferred = self.current_member if item and item.get("ref") == self.current_ref else None
|
|
463
723
|
self.editor_item = item
|
|
464
724
|
self.editor_ref = str(item["ref"]) if item else None
|
|
@@ -577,6 +837,8 @@ class CorpusStudio(App[None]):
|
|
|
577
837
|
async def action_request_quit(self) -> None:
|
|
578
838
|
if self.editor_dirty():
|
|
579
839
|
self.push_screen(Confirmation("Discard the unsaved draft and exit Studio?"), self._discard_and_exit)
|
|
840
|
+
elif self.toggle_drafts:
|
|
841
|
+
self.push_screen(Confirmation("Discard unapplied on/off changes and exit Studio?"), self._discard_and_exit)
|
|
580
842
|
else:
|
|
581
843
|
self.exit()
|
|
582
844
|
|
|
@@ -637,6 +899,9 @@ class CorpusStudio(App[None]):
|
|
|
637
899
|
}, diff + hook_summary
|
|
638
900
|
|
|
639
901
|
def _stage_plan(self, payload: dict[str, Any], summary: str) -> None:
|
|
902
|
+
if self.toggle_drafts and payload.get("operation") != "enable":
|
|
903
|
+
self.notify("Apply or discard on/off changes first.", severity="warning")
|
|
904
|
+
return
|
|
640
905
|
try:
|
|
641
906
|
plan = self.store.plan(payload)
|
|
642
907
|
except Exception as exc:
|
|
@@ -665,6 +930,8 @@ class CorpusStudio(App[None]):
|
|
|
665
930
|
self.notify(str(exc), severity="error")
|
|
666
931
|
return
|
|
667
932
|
ref = result.get("details", {}).get("ref")
|
|
933
|
+
if result.get("details", {}).get("operation") == "enable":
|
|
934
|
+
self._clear_toggle_drafts()
|
|
668
935
|
self.pending_plan = None
|
|
669
936
|
self._set_mode("view")
|
|
670
937
|
await self.refresh_library(self.query_one("#search", Input).value)
|
|
@@ -674,7 +941,11 @@ class CorpusStudio(App[None]):
|
|
|
674
941
|
|
|
675
942
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
676
943
|
button = event.button.id
|
|
677
|
-
if button == "
|
|
944
|
+
if button == "enablement-preview":
|
|
945
|
+
self._preview_toggles()
|
|
946
|
+
elif button == "enablement-discard":
|
|
947
|
+
await self._discard_toggles()
|
|
948
|
+
elif button == "create":
|
|
678
949
|
self.action_create()
|
|
679
950
|
elif button == "edit":
|
|
680
951
|
self.action_edit()
|