webbee 0.3.40__tar.gz → 0.3.41__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.
- {webbee-0.3.40 → webbee-0.3.41}/CHANGELOG.md +25 -0
- {webbee-0.3.40 → webbee-0.3.41}/PKG-INFO +1 -1
- {webbee-0.3.40 → webbee-0.3.41}/pyproject.toml +1 -1
- webbee-0.3.41/src/webbee/__init__.py +1 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/output_pane.py +8 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/render.py +27 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/selection.py +10 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/tui.py +55 -4
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_render.py +52 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_tui.py +110 -0
- webbee-0.3.40/src/webbee/__init__.py +0 -1
- {webbee-0.3.40 → webbee-0.3.41}/.github/workflows/publish.yml +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/.gitignore +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/LICENSE +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/README.md +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/install.sh +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/account.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/active_sessions.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/banner_art.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/boot.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/checkpoints.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/cli.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/clipboard.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/coding_context.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/commands.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/config.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/consent.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/details.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/events.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/frames.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/home.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/home_view.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/http.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/instance_lock.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/embed.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/graph.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/models.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/query.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/service.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/store.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/intel/watch.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/mode_store.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/queue_panel.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/reflow.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/remote.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/repl.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/repo.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/session.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/sessions.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/sizing.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/slots.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/steer.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/stream.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/tab_store.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/tabs.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/thread.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/tier_store.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/todo_panel.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/tokens.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/tools.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/update.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/urlopen.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/wallet.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/src/webbee/worktrees.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/__init__.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/conftest.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_account.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_active_sessions.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_checkpoints.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_cli.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_clipboard.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_clipboard_read.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_coding_context.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_commands.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_config.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_details.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_events.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_freeze_fix.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_home.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_home_view.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_instance_lock.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_chunker.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_embed.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_graph.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_indexer.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_query.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_service.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_store.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_vectors.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_intel_watch.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_marathon.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_mode_store.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_newtab_mode.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_packaging.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_reflow.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_repl.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_repo.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_session.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_sessions.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_sizing.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_slots.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_steer.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_stream.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_tabs.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_thread.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_tier_store.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_todo_panel.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_tokens.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_tools.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_tui_hardening.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_update.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_urlopen.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_version.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_version_badge_corner.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_wallet.py +0 -0
- {webbee-0.3.40 → webbee-0.3.41}/tests/test_worktrees.py +0 -0
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.41
|
|
4
|
+
|
|
5
|
+
The model tier is always visible now (not just via `/model`), and clicking a
|
|
6
|
+
tool line in the transcript expands it right there -- no keyboard needed.
|
|
7
|
+
|
|
8
|
+
- **The active model tier is now shown right in the toolbar**, next to
|
|
9
|
+
`mode:`, both idle and mid-turn -- it used to be visible only by running
|
|
10
|
+
`/model` with no argument or catching the flash right after a Ctrl+B
|
|
11
|
+
cycle. Silent (nothing shown) when no tier has been chosen, so it never
|
|
12
|
+
claims a value you didn't pick.
|
|
13
|
+
- **Click any finished tool line (bash, write_file, read_file, ...) in the
|
|
14
|
+
transcript to expand it** -- the exact same compact, bordered detail card
|
|
15
|
+
`/steps N` (or Up/Down + Enter) already gave you, one gesture shorter.
|
|
16
|
+
Clicking a banner, a progress line, or blank space is a harmless no-op --
|
|
17
|
+
nothing to expand there. Busy mid-turn is also a no-op (there's no
|
|
18
|
+
finished-turn detail to show yet). This reuses the existing `/steps`
|
|
19
|
+
machinery end to end -- it is not a new UI surface, never covers the
|
|
20
|
+
screen, and nothing needed to be kept in sync twice.
|
|
21
|
+
- Right-click paste (from 0.3.39) is confirmed working end-to-end on the
|
|
22
|
+
underlying mouse-report protocol; if it doesn't react in your terminal,
|
|
23
|
+
check that terminal's own mouse-reporting setting -- some terminal
|
|
24
|
+
emulators (notably macOS Terminal.app) never forward a right-click to the
|
|
25
|
+
program running inside them at all, by design, regardless of what an app
|
|
26
|
+
requests.
|
|
27
|
+
|
|
3
28
|
## 0.3.40
|
|
4
29
|
|
|
5
30
|
The bottom-right version badge is now real everywhere, and Home stopped
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.41"
|
|
@@ -49,6 +49,14 @@ class OutputPane:
|
|
|
49
49
|
pane_records.clear()
|
|
50
50
|
pane._reset_buffer() # StringIO + caches reset (Task 3 builds on it)
|
|
51
51
|
|
|
52
|
+
def record_count(self) -> int:
|
|
53
|
+
"""webbee-code-click-to-expand-v1: how many print records exist so
|
|
54
|
+
far -- RichSink.tool_result reads this RIGHT AFTER printing a step's
|
|
55
|
+
line to tag that exact record as \"step N\", so a later mouse click
|
|
56
|
+
on that line can look the step back up (see render.py's
|
|
57
|
+
`_STEP_RECORDS` / tui.py's click handler)."""
|
|
58
|
+
return len(pane_records)
|
|
59
|
+
|
|
52
60
|
self._records = pane_records
|
|
53
61
|
self._record_lines: list = [] # per-record NEW-line count, prefix-summable (reflow.py)
|
|
54
62
|
self._ring_base_lines = 0 # buffer content lines BEFORE ring record 0 (deque
|
|
@@ -167,6 +167,13 @@ class RichSink:
|
|
|
167
167
|
self._pending = ("", "")
|
|
168
168
|
self._consent = None # asyncio.Future while awaiting a reply
|
|
169
169
|
self._consent_summary = ""
|
|
170
|
+
# webbee-code-click-to-expand-v1: maps a printed record index (pane's
|
|
171
|
+
# RecordingConsole ring position, 0-based) -> 1-based step number for
|
|
172
|
+
# THIS turn, so a mouse click on a tool_result line can look its own
|
|
173
|
+
# step back up without any new wire protocol -- begin_turn resets both
|
|
174
|
+
# every turn, exactly like self.steps itself resets in session.py.
|
|
175
|
+
self._step_index = 0
|
|
176
|
+
self._step_records: dict = {}
|
|
170
177
|
# Cross-surface items already queued in the RUNNING kernel session
|
|
171
178
|
# (full-queue-layer K1: task_queued/task_dequeued frames). The dock's
|
|
172
179
|
# queue panel reads THIS list object every redraw — mutate in place
|
|
@@ -271,6 +278,11 @@ class RichSink:
|
|
|
271
278
|
self._reconnect_since = None
|
|
272
279
|
self._turn_failed = False
|
|
273
280
|
self._parked = False
|
|
281
|
+
# webbee-code-click-to-expand-v1: fresh per-turn -- step numbering
|
|
282
|
+
# (and thus which printed record maps to which step) restarts with
|
|
283
|
+
# every turn, exactly like self.steps itself does in session.py.
|
|
284
|
+
self._step_index = 0
|
|
285
|
+
self._step_records = {}
|
|
274
286
|
self.console.print() # breathing room between the user's message and the response
|
|
275
287
|
self._nudge()
|
|
276
288
|
|
|
@@ -580,8 +592,23 @@ class RichSink:
|
|
|
580
592
|
(_clean(summary)[:trunc(w, 0.35, 50)], "dim"),
|
|
581
593
|
))
|
|
582
594
|
self._pending = ("", "")
|
|
595
|
+
# webbee-code-click-to-expand-v1: tag the record JUST printed as
|
|
596
|
+
# "step N" (1-based, matching /steps' own numbering) -- record_count()
|
|
597
|
+
# already reflects this print (Rich prints synchronously), so the
|
|
598
|
+
# index this line landed at is exactly len(pane_records) - 1.
|
|
599
|
+
self._step_index += 1
|
|
600
|
+
rc = getattr(self.console, "record_count", None)
|
|
601
|
+
if callable(rc):
|
|
602
|
+
self._step_records[rc() - 1] = self._step_index
|
|
583
603
|
self._nudge()
|
|
584
604
|
|
|
605
|
+
def step_for_record(self, record_idx: int) -> "int | None":
|
|
606
|
+
"""webbee-code-click-to-expand-v1: 1-based step number that produced
|
|
607
|
+
`record_idx` (a pane RecordingConsole ring position), or None if that
|
|
608
|
+
record isn't a tool_result line (e.g. a progress/thinking/banner
|
|
609
|
+
line -- nothing to expand there, same as clicking blank space)."""
|
|
610
|
+
return self._step_records.get(record_idx)
|
|
611
|
+
|
|
585
612
|
async def ask_consent(self, app_id: str, tool: str, args: dict) -> str:
|
|
586
613
|
"""Ask for consent and return the user's RAW reply (trimmed only) —
|
|
587
614
|
NEVER interpret (the kernel decides, ICNLI). When the dock is running
|
|
@@ -111,6 +111,16 @@ def make_select_control(pane, FormattedTextControl, MouseEventType, MouseButton)
|
|
|
111
111
|
up_abs = (ev.position.y + pane._offset, ev.position.x)
|
|
112
112
|
if down_abs is not None and down_abs != up_abs:
|
|
113
113
|
pane._copy_selection(down_abs, up_abs)
|
|
114
|
+
elif down_abs is not None:
|
|
115
|
+
# webbee-code-click-to-expand-v1: a PLAIN click (press and
|
|
116
|
+
# release at the SAME absolute cell -- no drag, nothing to
|
|
117
|
+
# copy) on a transcript line asks "what happened here?" --
|
|
118
|
+
# same one-hook/getattr-guarded shape as on_middle_paste/
|
|
119
|
+
# on_right_paste above, so a pane with no hook wired
|
|
120
|
+
# (tests, no dock) stays a harmless no-op.
|
|
121
|
+
hook = getattr(pane, "on_line_click", None)
|
|
122
|
+
if hook is not None:
|
|
123
|
+
hook(down_abs[0])
|
|
114
124
|
pane._sel = None
|
|
115
125
|
pane._invalidate() # clear the highlight (colours restored)
|
|
116
126
|
return None
|
|
@@ -223,7 +223,8 @@ def next_tier(tier: str) -> str:
|
|
|
223
223
|
def build_toolbar(mode: str, tokens: int, credits: int, *, busy: bool = False,
|
|
224
224
|
current: str = "", elapsed: float = 0.0, tools: int = 0,
|
|
225
225
|
consent: bool = False, queued: int = 0,
|
|
226
|
-
reconnecting: int = 0, width: int = 0, live: str = ""
|
|
226
|
+
reconnecting: int = 0, width: int = 0, live: str = "",
|
|
227
|
+
tier: str = "") -> list:
|
|
227
228
|
"""The status line under the pinned input box, as prompt_toolkit formatted
|
|
228
229
|
text (per-segment styled). Four states: consent (awaiting a reply),
|
|
229
230
|
reconnecting (the stream transport is down mid-turn — honest, not a fake
|
|
@@ -249,14 +250,27 @@ def build_toolbar(mode: str, tokens: int, credits: int, *, busy: bool = False,
|
|
|
249
250
|
frags = [("class:tb.spin", f" {spin} "), ("class:tb.working", "working")]
|
|
250
251
|
if current:
|
|
251
252
|
frags += [("class:tb.dim", " · "), ("class:tb.action", current)]
|
|
253
|
+
# webbee-code-model-tier-toolbar-v1: keep the tier visible mid-turn too
|
|
254
|
+
# (a short "· smart" tag, NOT the full "model: " label used at idle --
|
|
255
|
+
# busy is already the densest row, this is the cheapest legible form).
|
|
256
|
+
if tier:
|
|
257
|
+
frags += [("class:tb.dim", " · "), ("class:tb.mode.default", tier)]
|
|
252
258
|
frags.append(("class:tb.dim",
|
|
253
259
|
f" · {elapsed:.0f}s · {tools} · {_fmt_tokens(tokens)} tok"))
|
|
254
260
|
frags += q
|
|
255
261
|
frags.append(("class:tb.dim", " · Esc/Ctrl-C to stop"))
|
|
256
262
|
return frags
|
|
257
263
|
frags = [("class:tb.dim", " mode: "),
|
|
258
|
-
(f"class:tb.mode.{mode}", mode)
|
|
259
|
-
|
|
264
|
+
(f"class:tb.mode.{mode}", mode)]
|
|
265
|
+
# webbee-code-model-tier-toolbar-v1: the tier was only ever visible via
|
|
266
|
+
# `/model` (no arg) or right after a Ctrl+B cycle flash -- ask once, then
|
|
267
|
+
# forget, exactly the visibility gap the mode segment right next to it
|
|
268
|
+
# never had. "" (unset -> server admin default) stays silent rather than
|
|
269
|
+
# claim a tier name nobody chose (I-DIAGNOSIS-GROUNDED spirit: never
|
|
270
|
+
# invent a value the user didn't pick and the server didn't confirm).
|
|
271
|
+
if tier:
|
|
272
|
+
frags += [("class:tb.dim", " · model: "), ("class:tb.mode.default", tier)]
|
|
273
|
+
frags += [("class:tb.dim", f" · {_fmt_tokens(tokens)} tok · {_fmt_tokens(credits)} credits this session"),
|
|
260
274
|
*q]
|
|
261
275
|
# 0.3.37: the PERSISTENT live-session indicator (active_sessions.
|
|
262
276
|
# session_indicator). Empty string = nothing running = say nothing, so an
|
|
@@ -846,6 +860,8 @@ async def run_session(*, slots, on_line, on_cycle, on_tier_cycle=None, steps_nav
|
|
|
846
860
|
p.on_middle_paste = _middle_paste
|
|
847
861
|
if getattr(p, "on_right_paste", None) is None:
|
|
848
862
|
p.on_right_paste = _right_paste
|
|
863
|
+
if getattr(p, "on_line_click", None) is None:
|
|
864
|
+
p.on_line_click = _line_click
|
|
849
865
|
return p
|
|
850
866
|
|
|
851
867
|
def _sink_attr(name, default=None):
|
|
@@ -1174,6 +1190,40 @@ async def run_session(*, slots, on_line, on_cycle, on_tier_cycle=None, steps_nav
|
|
|
1174
1190
|
|
|
1175
1191
|
get_app().create_background_task(_do_right_paste())
|
|
1176
1192
|
|
|
1193
|
+
def _line_click(abs_line: int) -> None:
|
|
1194
|
+
"""webbee-code-click-to-expand-v1: a PLAIN click on a transcript line
|
|
1195
|
+
(see selection.py's `on_line_click` hook) asks \"what happened here?\"
|
|
1196
|
+
without touching the keyboard at all -- the exact same reveal /steps
|
|
1197
|
+
N (and Up/Down + Enter) already show, just one gesture shorter. A
|
|
1198
|
+
click resolves to a RECORD via the pane's own reflow-anchoring math
|
|
1199
|
+
(`_record_at_line`, already used for reflow + scroll-anchoring), then
|
|
1200
|
+
to a step number via the sink's `step_for_record` (set right after
|
|
1201
|
+
each tool_result print, see render.py) -- so a click on a banner,
|
|
1202
|
+
a progress line, or blank space (no step there) is a harmless no-op,
|
|
1203
|
+
never an error. Busy mid-turn is ALSO a no-op (steps_nav reflects the
|
|
1204
|
+
LAST finished turn only -- clicking during a live one would expand
|
|
1205
|
+
stale data), same gate Enter's own steps_nav path already uses.
|
|
1206
|
+
Compact by construction: dispatches through steps_nav[\"expand\"],
|
|
1207
|
+
the SAME `/steps N` -> step_detail bordered panel Enter already
|
|
1208
|
+
shows -- never a new UI surface, never full-screen, nothing to
|
|
1209
|
+
keep in sync twice."""
|
|
1210
|
+
if not steps_nav or _busy_live():
|
|
1211
|
+
return
|
|
1212
|
+
slot = _a()
|
|
1213
|
+
sink = slot.sink
|
|
1214
|
+
step_fn = getattr(sink, "step_for_record", None) if sink is not None else None
|
|
1215
|
+
if step_fn is None:
|
|
1216
|
+
return
|
|
1217
|
+
pane = _pane()
|
|
1218
|
+
record_idx = pane._record_at_line(abs_line)
|
|
1219
|
+
step_no = step_fn(record_idx)
|
|
1220
|
+
if not step_no:
|
|
1221
|
+
return
|
|
1222
|
+
event = get_app_or_none()
|
|
1223
|
+
if event is None:
|
|
1224
|
+
return
|
|
1225
|
+
event.create_background_task(steps_nav["expand"](step_no - 1, slot))
|
|
1226
|
+
|
|
1177
1227
|
@kb.add("c-v")
|
|
1178
1228
|
def _paste_key(event):
|
|
1179
1229
|
# 0.3.34 (W3 Wave A): Ctrl-V pastes the OS clipboard. An IMAGE is read
|
|
@@ -1367,7 +1417,8 @@ async def run_session(*, slots, on_line, on_cycle, on_tier_cycle=None, steps_nav
|
|
|
1367
1417
|
queued=len(slot.pending) + len(_sink_attr("remote_pending", [])),
|
|
1368
1418
|
reconnecting=st.get("reconnecting", 0),
|
|
1369
1419
|
width=_toolbar_fit_width(),
|
|
1370
|
-
live=str((live or {}).get("text") or "")
|
|
1420
|
+
live=str((live or {}).get("text") or ""),
|
|
1421
|
+
tier=getattr(slot, "model_tier", "") or "")
|
|
1371
1422
|
else:
|
|
1372
1423
|
# Home has no sink of its OWN, but it is the dock's summary
|
|
1373
1424
|
# page: show the SESSION TOTAL across every open tab (0.3.36)
|
|
@@ -960,3 +960,55 @@ def test_tool_diff_caps_very_long_diffs_without_crashing():
|
|
|
960
960
|
out = s.console.export_text()
|
|
961
961
|
assert "line 0" in out
|
|
962
962
|
assert "truncated" in out.lower()
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
# --------------------------------------------------------------------------
|
|
966
|
+
# webbee-code-click-to-expand-v1: `/steps N` (and Up/Down + Enter) already
|
|
967
|
+
# let you expand any step of the last turn -- a mouse click on the printed
|
|
968
|
+
# tool_result line itself is the natural next step. RichSink tags the exact
|
|
969
|
+
# printed RECORD as "step N" the instant it prints the line, so a later
|
|
970
|
+
# click can resolve record -> step with no new wire protocol at all.
|
|
971
|
+
# --------------------------------------------------------------------------
|
|
972
|
+
def test_step_for_record_maps_the_printed_line_to_its_step_number():
|
|
973
|
+
from webbee.output_pane import OutputPane
|
|
974
|
+
pane = OutputPane(width=80)
|
|
975
|
+
s = RichSink(console=pane.console, live_enabled=False,
|
|
976
|
+
input_fn=lambda p: "", clock=lambda: 0.0, on_output=pane.notify)
|
|
977
|
+
s.begin_turn()
|
|
978
|
+
s.tool_start("bash", {"command": "ls"})
|
|
979
|
+
s.tool_result("bash", True, "done") # step 1 -> some record index
|
|
980
|
+
s.tool_start("read_file", {"path": "a.py"})
|
|
981
|
+
s.tool_result("read_file", True, "42 lines") # step 2 -> a later record index
|
|
982
|
+
|
|
983
|
+
# Whatever record each line landed at, step_for_record must resolve it
|
|
984
|
+
# back to the RIGHT step -- never the other one, never None.
|
|
985
|
+
recs = s._step_records
|
|
986
|
+
assert len(recs) == 2
|
|
987
|
+
ordered = sorted(recs.items()) # [(rec_idx, step_no), ...]
|
|
988
|
+
assert ordered[0][1] == 1
|
|
989
|
+
assert ordered[1][1] == 2
|
|
990
|
+
for rec_idx, step_no in recs.items():
|
|
991
|
+
assert s.step_for_record(rec_idx) == step_no
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def test_step_for_record_is_none_for_a_non_step_line():
|
|
995
|
+
s = _sink()
|
|
996
|
+
s.begin_turn()
|
|
997
|
+
s.tool_start("bash", {"command": "ls"})
|
|
998
|
+
s.tool_result("bash", True, "done")
|
|
999
|
+
assert s.step_for_record(999) is None # nothing ever printed there
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def test_step_records_reset_on_a_new_turn():
|
|
1003
|
+
from webbee.output_pane import OutputPane
|
|
1004
|
+
pane = OutputPane(width=80)
|
|
1005
|
+
s = RichSink(console=pane.console, live_enabled=False,
|
|
1006
|
+
input_fn=lambda p: "", clock=lambda: 0.0, on_output=pane.notify)
|
|
1007
|
+
s.begin_turn()
|
|
1008
|
+
s.tool_start("bash", {"command": "ls"})
|
|
1009
|
+
s.tool_result("bash", True, "done")
|
|
1010
|
+
assert len(s._step_records) == 1
|
|
1011
|
+
s.end_turn("ok")
|
|
1012
|
+
s.begin_turn() # a fresh turn, fresh numbering
|
|
1013
|
+
assert s._step_records == {}
|
|
1014
|
+
assert s._step_index == 0
|
|
@@ -5273,3 +5273,113 @@ def test_ticker_busy_includes_turn_flash_and_edge_drag():
|
|
|
5273
5273
|
def flash(self):
|
|
5274
5274
|
return ""
|
|
5275
5275
|
assert _ticker_busy(_Slots(_Bare()), idle) is False
|
|
5276
|
+
|
|
5277
|
+
|
|
5278
|
+
# --------------------------------------------------------------------------
|
|
5279
|
+
# webbee-code-model-tier-toolbar-v1: the chosen model tier was only ever
|
|
5280
|
+
# visible via `/model` (no arg) or a Ctrl+B cycle flash -- now it sits right
|
|
5281
|
+
# next to `mode:` in the idle toolbar (always visible, not on-demand), and as
|
|
5282
|
+
# a short tag mid-turn too.
|
|
5283
|
+
# --------------------------------------------------------------------------
|
|
5284
|
+
def test_idle_toolbar_shows_the_model_tier_when_set():
|
|
5285
|
+
t = _txt(build_toolbar("default", 0, 0.0, tier="supersmart"))
|
|
5286
|
+
assert "model: supersmart" in t
|
|
5287
|
+
|
|
5288
|
+
|
|
5289
|
+
def test_idle_toolbar_says_nothing_about_tier_when_unset():
|
|
5290
|
+
t = _txt(build_toolbar("default", 0, 0.0, tier=""))
|
|
5291
|
+
assert "model:" not in t
|
|
5292
|
+
|
|
5293
|
+
|
|
5294
|
+
def test_busy_toolbar_shows_a_short_tier_tag():
|
|
5295
|
+
t = _txt(build_toolbar("default", 0, 0.0, busy=True, elapsed=1.0, tier="ultrasmart"))
|
|
5296
|
+
assert "ultrasmart" in t
|
|
5297
|
+
|
|
5298
|
+
|
|
5299
|
+
def test_busy_toolbar_says_nothing_about_tier_when_unset():
|
|
5300
|
+
t = _txt(build_toolbar("default", 0, 0.0, busy=True, elapsed=1.0, tier=""))
|
|
5301
|
+
assert "ultrasmart" not in t and "supersmart" not in t and "smart" not in t
|
|
5302
|
+
|
|
5303
|
+
|
|
5304
|
+
# --------------------------------------------------------------------------
|
|
5305
|
+
# webbee-code-click-to-expand-v1: a plain mouse click on a printed tool_result
|
|
5306
|
+
# line expands that step's detail card -- the SAME reveal /steps N (and
|
|
5307
|
+
# Up/Down + Enter) already give, one gesture shorter, never a new surface.
|
|
5308
|
+
# --------------------------------------------------------------------------
|
|
5309
|
+
def test_clicking_a_tool_result_line_expands_its_step():
|
|
5310
|
+
from prompt_toolkit.application import create_app_session, get_app
|
|
5311
|
+
from prompt_toolkit.data_structures import Point
|
|
5312
|
+
from prompt_toolkit.input import create_pipe_input
|
|
5313
|
+
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
|
|
5314
|
+
from prompt_toolkit.output import DummyOutput
|
|
5315
|
+
|
|
5316
|
+
from webbee import tui
|
|
5317
|
+
from webbee.render import RichSink
|
|
5318
|
+
|
|
5319
|
+
async def scenario():
|
|
5320
|
+
pane = tui.OutputPane(width=80)
|
|
5321
|
+
sink = RichSink(console=pane.console, live_enabled=False,
|
|
5322
|
+
input_fn=lambda p: "", clock=lambda: 0.0, on_output=pane.notify)
|
|
5323
|
+
sink.begin_turn()
|
|
5324
|
+
sink.tool_start("bash", {"command": "ls"})
|
|
5325
|
+
sink.tool_result("bash", True, "done") # this is step 1
|
|
5326
|
+
sink.end_turn("ok")
|
|
5327
|
+
|
|
5328
|
+
slots = mk_slots(pane=pane, sink=sink)
|
|
5329
|
+
expanded = []
|
|
5330
|
+
|
|
5331
|
+
async def _expand(idx, slot):
|
|
5332
|
+
expanded.append(idx)
|
|
5333
|
+
|
|
5334
|
+
async def on_line(text, slot=None): ...
|
|
5335
|
+
|
|
5336
|
+
with create_pipe_input() as pipe:
|
|
5337
|
+
with create_app_session(input=pipe, output=DummyOutput()):
|
|
5338
|
+
task = asyncio.create_task(tui.run_session(
|
|
5339
|
+
slots=slots, on_line=on_line, on_cycle=lambda: None,
|
|
5340
|
+
steps_nav={"expand": _expand, "count": lambda: 1}))
|
|
5341
|
+
await asyncio.sleep(0.05)
|
|
5342
|
+
# begin_turn prints a leading blank line (breathing room)
|
|
5343
|
+
# before the tool_result line itself -- so the step's line is
|
|
5344
|
+
# absolute line 1, not 0, in a freshly-booted pane at offset 0.
|
|
5345
|
+
pane.on_line_click(1)
|
|
5346
|
+
await asyncio.sleep(0.02)
|
|
5347
|
+
assert expanded == [0] # step 1 -> steps_nav index 0
|
|
5348
|
+
pipe.send_text("\x04")
|
|
5349
|
+
ok = await asyncio.wait_for(task, 5)
|
|
5350
|
+
assert ok is True
|
|
5351
|
+
|
|
5352
|
+
asyncio.run(scenario())
|
|
5353
|
+
|
|
5354
|
+
|
|
5355
|
+
def test_clicking_a_non_step_line_is_a_harmless_noop():
|
|
5356
|
+
from prompt_toolkit.application import create_app_session, get_app
|
|
5357
|
+
from prompt_toolkit.input import create_pipe_input
|
|
5358
|
+
from prompt_toolkit.output import DummyOutput
|
|
5359
|
+
|
|
5360
|
+
from webbee import tui
|
|
5361
|
+
|
|
5362
|
+
async def scenario():
|
|
5363
|
+
pane = tui.OutputPane(width=80)
|
|
5364
|
+
slots = mk_slots(pane=pane, sink=_idle_sink())
|
|
5365
|
+
expanded = []
|
|
5366
|
+
|
|
5367
|
+
async def _expand(idx, slot):
|
|
5368
|
+
expanded.append(idx)
|
|
5369
|
+
|
|
5370
|
+
async def on_line(text, slot=None): ...
|
|
5371
|
+
|
|
5372
|
+
with create_pipe_input() as pipe:
|
|
5373
|
+
with create_app_session(input=pipe, output=DummyOutput()):
|
|
5374
|
+
task = asyncio.create_task(tui.run_session(
|
|
5375
|
+
slots=slots, on_line=on_line, on_cycle=lambda: None,
|
|
5376
|
+
steps_nav={"expand": _expand, "count": lambda: 0}))
|
|
5377
|
+
await asyncio.sleep(0.05)
|
|
5378
|
+
pane.on_line_click(0) # nothing was ever printed/tagged here
|
|
5379
|
+
await asyncio.sleep(0.02)
|
|
5380
|
+
assert expanded == []
|
|
5381
|
+
pipe.send_text("\x04")
|
|
5382
|
+
ok = await asyncio.wait_for(task, 5)
|
|
5383
|
+
assert ok is True
|
|
5384
|
+
|
|
5385
|
+
asyncio.run(scenario())
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.40"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webbee-0.3.40 → webbee-0.3.41}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md
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
|
|
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
|
|
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
|
|
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
|
|
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
|