mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
f8f7152d6397bdbc0c21969045a094e64f97fb3e
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f8f7152d63 |
feat(ui): split view returns to the L-shell — PaneManager layout tree
Revives the split-pane feature retired with ui/static (step 6), rebuilt on PaneManager: an optional binary layout tree (null = the one-pane-per- tab behaviour, unchanged) renders visible panes as %-inset cells — no reparenting, so live stream DOM, scroll state and media survive layout changes. Tabs stay global: the active tab is the focused cell, a backgrounded tab swaps into it, clicking inside a visible pane focuses its cell, .shown marks visible-unfocused tabs. Separators resize by pointer-capture drag and arrow keys (role=separator + aria-value*); the tree persists in the working-set blob and rehydrate prunes leaves whose pane did not restore. Limits: 6 cells, 200x150 cell minimums, denials toast the manager's reason. Affordance: Split right / Split down / Unsplit buttons in the tab-bar tail replace the redundant [+] (the permanent Dashboard tab is the launcher) — deliberately no contextmenu override this time. The dead TS_APP.focusLauncher seam goes with it. Measured chrome: the focused cell wears a 2px accent top bar (no thin tinted ring clears 3:1 in both themes) plus a 55%-mix inset ring; separators rest at --ink-4 with solid-accent hover/drag/focus; .shown tabs carry an accent underline; the tail cluster is fenced and lifted to --ink-3. scripts/livepass.py grows a third surface: shell/livepass.html boots the real shell.js + pane.js and drives ?split=right|down|three|none (+ &theme=light), stamping SPLIT-READY-<cells> / SPLIT-FAILED-<reason>. |
||
|
|
b3c3acc5d1 |
fix(scripts): livepass dialog-tier riders + loud open-failure + dock-displacement probe
Designer-review round on the scroll fix found the harness's dialog-tier gate silently green: confirm-dialog (and install/coord-delete) markup lives OUTSIDE #admin-layout, so the fragment extraction never embedded it — ?open=confirm threw at showConfirmModal and screenshot a normal, dialog-less page. build() now injects every hatch dialog the fragment does not already contain, and a driven ?open= that ends with no open dialog stamps OPEN-FAILED-<state> into the title instead of passing. Also upstreams the review's probe states: &focuslast=1 focuses the last shelf-body control (the displaced-dock regression class — only .sh-body may scroll; head/foot must stay pinned) and &scrolled=bottom shows the 24px scroll tail. |
||
|
|
6bb47cad2d |
fix(console): manage-pane scroll regressions — interior scroller, clip the hatch-host, anchor hidden inputs
The L-shell height-pins the admin chain and .hatch-host clipped it, so no box below the pane could scroll: tabs taller than the pane were cut dead, and the overflow:hidden host doubled as a hidden scroll container that focus-into-view silently scrolled — visually-hidden toggle/cap/radio inputs escape the .sh-body scroller (abspos under an unpositioned label), overhang the shelf, and a Tab keypress shoved the docked hatch off its head with no scrollbar to recover by. - .admin-content becomes the manage pane's interior scroller (the #main precedent); switchAdminTab resets it on real tab changes only - .hatch-host: overflow hidden -> clip — paint clipping without a scroll container, so focus can never displace the dock - position:relative anchors on the three hidden-input labels (toggle-switch, .sh-body .cap, segmented-option); .settings-toggle already carried one - livepass: the console harness wraps the fragment in the REAL L-shell chain (its bespoke height pin is exactly how this bug class stayed invisible to the screenshot gates) and gains a ?tall=1/&scrolled=1 scroll state |
||
|
|
b991dc2e83 |
fix: CI lint pin + copilot-thread hardening
The wiring-lint test used percent-formatted regex patterns — UP031 under the ruff 0.15.6 the CI pre-commit pins (the older venv binary let it through; checked repo-wide against the exact pin now). f-strings with doubled quantifier braces, plus one over-long fixture line in the livepass generator split. Copilot threads, both validated rather than blindly applied: - closeShelf's scrim-ownership scan now skips detached entries. The thread's throw scenario doesn't occur on the real removal path (a pane close detaches an ANCESTOR, so _hostOf still resolves inside the detached subtree) — but a detached shelf is genuinely not a scrim owner, so the guard is correct beyond being defensive. - toast.js drops the popover attribute via removeAttribute instead of the null assignment. The claim that null leaves popover="null" is refuted — the IDL is nullable and null removes the attribute (verified empirically in headless Chrome) — but removeAttribute reads correct without requiring that spec knowledge. |
||
|
|
9102f858a4 |
chore(scripts): commit the livepass harness generator
The livepass harness — the headless-render rig that verified every converted modal surface and click-drives submits (the dead-Save bug class) — lived as ad hoc files in /tmp and got wiped once already. The durable piece is the GENERATOR: the markup is extracted fresh from the index files at build time (a committed snapshot would drift) and the stylesheets/scripts are symlinked so edits are live on refresh. scripts/livepass.py builds both harnesses into /tmp/livepass/ (ui: all six dialog-tier surfaces incl. the real cards.js batch controller drive; console: the admin-pane fragment hosting the shelves, with schedule/model/policy/confirm/token fixtures and the model-save click drive that flips document.title to PUT-OK-<n>). --serve included; the chrome screenshot incantation and the ?open= registry are in the module docstring. Governance fixtures (roles/HR/OGP/memory/skill) are documented seams for when those surfaces need driving. |
||
|
|
80d201a67b |
chore(ui): L-shell step 6 (5e.2f) — retire dead split-pane CSS from ui/static
Removes the structurally-dead CSS the L-shell superseded — 794 lines: the tab bar (.ws-tab*, #tab-bar, #split-btn, .ws-tab-dropdown-*), the binary split-pane machinery (.split-*, #split-root, .pane-ctx-*), the old approval/verdict card (.ts-approval-*, .verdict-*, the judge spinner), the fixed .dashboard-overlay, and the retired appbar/settings-overlay bits — plus their [data-theme=light] overrides. The standalone now styles its conversation from the shared sheets (chat/conversation/interactive.css); the dashboard table + saved list were always shared (base/cards.css). Method: a conservative token-diff — a rule is dropped only when EVERY selector's class/id token is absent (word-boundary, comments stripped) from the standalone runtime (index.html + every JS it loads, incl. the vendored hljs/katex/mermaid so their runtime-built classes aren't mistaken for dead). Mixed/any-live rules are kept verbatim (no reformatting), so ~50 dead-but-harmless rules that share a generic token like `.active` survive — safe over-keep. The markdown / syntax / math / diagram theme lives ONLY in this style.css (the shared sheets don't carry it), so the hljs/katex/mermaid families are protected from removal. Also fixes four dead tab-DOM pokes in app.js (editWorkstreamTitle / confirmDeleteWorkstream read the title from the workstreams roster now, not the retired .ws-tab .tab-name; the cancel handlers drop the gone .tab-chevron focus restore). Verified: braces balanced (370/370), the headless harness still builds clean (errs:[]), git diff confirms zero live dashboard/render rules removed, and the css_specificity_audit (manifest synced to the standalone's new sheet set) shows the SAME 10 pre-existing findings before/after — zero new cascade flips (removing a rule for a non-existent selector can't change any live element's cascade). 121 JS guards green, ruff/mypy clean. |
||
|
|
215f7506ba |
feat(rerank): wire endpoint-backed reranking into BM25 retrieval surfaces
Reuse the shipped Cohere/Jina rerank client as an optional post-process on the BM25 surfaces (tool search, skill search, memory composition) via one seam: BM25Index gains an injected reranker + a two-stage search (BM25 recall top-50 -> rerank -> top-k). No new storage. Gated on a configured endpoint plus tools.rerank_bm25 (default on, matching rerank_web_search). tools.rerank_bm25_threshold (default 0.0 = off) is a relevance FLOOR for proactive memory surfacing: BM25 always returns something, so without a floor every-turn memory injection spends tokens on the top-k of whatever lexically matched; the reranker score is what makes a meaningful "inject nothing" gate possible. Two reranker modes (BM25Index rerank_filters): - REORDER (reactive tool/skill search): the reranker must never drop results -> fall back to BM25 order on empty, backfill omitted pool items, so a misbehaving endpoint can't silently lose tools. - FILTER (memory, rerank_filters = threshold > 0): a clean empty/short result is honoured (inject nothing) -- a deliberate divergence from web_search._rerank_results. Parse/endpoint failure is a discrete branch from the floor: an empty result for non-empty input means an unparseable response (a conforming reranker scores every doc), so the closure raises RerankError and BM25Index falls back to BM25 order in BOTH modes -- the floor only acts on valid scores. Also: cap the rerank client timeout at 15s (the per-turn memory path can't afford tools.timeout's 120s default); move the Reranker alias to rerank.py (shared, no import cycle); document the endpoint egress in the rerank_bm25 help, the admin Reranker-role description, and docs/tools.md; add scripts/bench_bm25_rerank.py (manual, needs a live endpoint) to measure precision@k/MRR lift and recommend a threshold default. Negative-tested: reorder fallback-on-empty and omitted-item backfill, filter-mode honor-empty, singleton-still-floored, the parse-fail RerankError raise, the >= floor boundary, and pool-position-to-doc-index mapping -- each guard reverted to confirm its test fails, then restored. |
||
|
|
a8eec0d740 |
fix(vendor): widen update-vendored-js sweep to catch shared_static/ + .py
The shared_static exclude in scripts/update-vendored-js.sh was meant to skip self-references inside vendored libraries, but it also hid shared_static/renderer.js — which loads the vendored libs and pinned mermaid-11.14.0 across every renovate bump since #426. Tests under tests/test_web_helpers.py were similarly invisible because the include list omitted *.py. Replace the broad shared_static exclude with the specific old-versioned vendor directory (about to be rm -rf'd next anyway), and add *.py to the include list. Bump renderer.js to mermaid-11.15.0 to repair the live 404, and refresh the test fixtures to current vendor versions so they stop drifting. |
||
|
|
4b5edce8c5 |
fix(css): two cascade-flip bugs found by specificity audit (#433)
* fix(css): two cascade-flip bugs found by specificity audit PR #431 stripped [data-design="v1"] from ~400 rules, dropping each by a specificity tier; two cascade flips (#header outranking .appbar, #header h1 outranking .appbar-title) were caught visually during that PR's review and fixed by renaming id="header" → id="ui-header" on the per-node UI page. This is the audit follow-up; it found two more: - textarea.skill-content-area (was .skill-content-area) — bumped to (0,1,1) so the rule ties with `.admin-modal textarea` (0,1,1) and wins on source order. Without the bump, min-height: 220px was clobbered to 40px by the modal default and the spec-content textarea rendered short. The three !important markers (font-family/size/line-height) are now redundant against the modal's font: inherit shorthand and are dropped. - h3.skill-spec-heading — removed `font-size: inherit;`. The author wrote it to "reset UA defaults" but it locked font-size to the parent's (~14-16px) at (0,1,1), silently overriding `.skill-spec-heading`'s 10px at (0,1,0). The bare class already beats UA `h3` on specificity (class > tag), so no font-size reset was needed; the `margin-block: 0` line stays because the bare class's `margin: 14px 0 6px` shorthand may not reset the UA's logical margin-block-start/end on every engine. Adds scripts/css_specificity_audit.py — the audit tool. It parses every CSS file referenced from the project's three HTML entry points, computes selector specificity (incl. :not/:is/:has math, attribute selectors, and !important), and flags every place an unscoped legacy rule could outrank a bare-class designed primitive. Honours per-page stylesheet manifests, state-pseudo subset gating (a `:hover` rule overriding a resting-state base rule is intentional, not a flip), and shorthand→longhand expansion for font/padding/margin/border/background. Triage of remaining findings (26 id-tier in default mode, 74 total at --all-tiers) confirmed all are intentional designer overrides — id-scoped buttons, BEM modifier classes, contextual ancestor selectors, last-child margin reset, [hidden] toggle. * fix(css-audit): correct two cascade-resolution bugs flagged by Copilot 1. _parse_declarations dict insertion order didn't update on overwrite, so a sequence like `font-size: 13px; font: inherit; font-size: 12px;` would iterate as (font-size=12px, font=inherit) and the shorthand expansion then clobbered font-size back to `inherit` — wrong. Delete-then-insert on overwrite so the last occurrence lands at the dict's tail and the shorthand expansion sees the real source order. 2. The cascade-winner tie-break used `rule.line_no` only, ignoring the stylesheet load order. A rule at line 1000 of `base.css` looked "later" than a rule at line 50 of `style.css`, even though the page loads `base.css` BEFORE `style.css`. Sort by `(file_index, line_no)` keyed off the element's per-page stylesheet manifest instead. |
||
|
|
7968f1b361 |
feat: auto-invalidate JWT and static assets on version upgrade (#307)
* feat: auto-invalidate JWT and static assets on version upgrade
Add a `ver` claim (major.minor) to user-facing JWTs so tokens from
previous versions are rejected after upgrade, triggering re-login.
Service tokens are excluded for rolling-deployment safety. Tokens
without a `ver` claim (pre-upgrade) are accepted for backward compat.
Inject `?v={__version__}` query strings into static asset URLs at
startup so browsers fetch fresh JS/CSS after any release. Vendored
libraries (KaTeX, Highlight.js, etc.) are skipped since they already
carry version numbers in directory paths. HTML responses now include
`Cache-Control: no-cache` to ensure browsers always revalidate.
Frontend detects upgrade-specific 401s and shows a contextual subtitle
("The server was updated — please sign in again"), then performs a full
page reload after re-auth to load the new versioned assets.
* refactor: address PR review — public API name, single decode, idempotent regex
Rename _version_slot() → jwt_version_slot() to make the cross-module
import explicit rather than relying on a private name.
Move version gating from validate_jwt() into check_request() via a new
AuthResult.token_version field. This eliminates the double JWT decode
that occurred on version-mismatch detection — the token is now decoded
once and the version compared afterward.
Guard version_html() regex against double-apply by excluding URLs that
already contain a query string ([^"?]+ instead of [^"]+).
* feat: structured version_mismatch code, ETag, cross-tab auth sync
Add structured "code": "version_mismatch" field to the 401 response
so the frontend detects upgrade-triggered re-auth without string
matching on the error message.
Add ETag headers to HTML index responses (server, console, and proxied
node UI). Combined with Cache-Control: no-cache, browsers send
conditional GETs and receive 304 between upgrades, saving bandwidth.
Add BroadcastChannel-based cross-tab auth sync so logging in on one
tab dismisses the login modal on all other tabs (and vice-versa for
logout).
Add a reminder to the vendored JS update script about the
version_html() regex lookahead.
* fix: remove unused import in test_web_helpers
|
||
|
|
db0baefeb2 |
feat: render rich media embeds for MCP tool results (#292)
* feat: render rich media embeds for MCP tool results Detect structured media JSON (stream_url, results, sessions) in MCP tool output and render interactive cards instead of plain text. Web UI: media cards with thumbnail, title, metadata, and click-to-play video/audio. HLS via lazy-loaded hls.js with direct-stream preference. Collapsed raw JSON (API keys redacted) for inspection. Discord: rich embeds with proxied thumbnail images (fetched by the bot since Discord CDN cannot reach private media servers). Search results as numbered lists, session state as "Now Playing" cards. Stream URLs never exposed in embeds — web_url used for safe clickable links. CI: vendor hls.js 1.6.15 with renovate tracking and update script. * fix: address PR #292 review — SSRF guards, streaming fetch, tests - URL validation: reject non-http(s) schemes and userinfo in thumbnail URLs. Private IPs intentionally allowed (media servers are on LAN). - Streaming fetch: use http.stream() with aiter_bytes() and a running byte count to enforce the 2MB cap without buffering the full response. Validate content-type is image/* before downloading. - Resilience: wrap try_build_media_embed in try/except in bot.py so a media embed failure falls through to the code-block path. - LICENSE: download hls.js LICENSE from npm on update instead of only copying from old dir. - Tests: add 19 new tests — try_parse_media (8 cases), _is_safe_image_url (7 cases), embed builders (4 cases including stream_url exclusion and string season/episode safety). * chore: add LICENSE file for vendored hls.js * fix: remove ANSI escape codes from tool preview fields Preview text (tool args, URLs, queries) was wrapped in DIM/RESET ANSI codes at the source in session.py, which leaked into SSE events and rendered as raw escape sequences in Discord and the web UI. Move ANSI styling to the CLI consumer (cli.py) where it belongs. Also escape markdown in Discord tool name titles to prevent __ from being interpreted as underline formatting. * fix: drop [MCP: server] prefix from tool descriptions The prefix made MCP tools look second-class compared to builtins, causing models to hesitate using them. The server name is already encoded in the tool name (mcp__server__tool). * feat: pretty-print JSON tool output, player error state, broader key redaction - JSON tool results are detected and pretty-printed with 2-space indent instead of rendering as a wall of text - API key redaction extended to cover api_key, apiKey, api-key, and token query params across all tool output (not just media embeds) - Video/audio player shows styled error message when stream fails to load instead of leaving a broken player element - Both appendToolOutput and replayHistory use shared renderToolOutput() * fix: designer review — player error retry, contrast, tool-cmd cap - Player error: role="alert" for screen readers, retry button that reuses existing play handler, includes media title in error message - Light theme: darken --red from #dc2626 to #b91c1c (5.7:1 contrast on --code-bg, was 4.3:1 failing WCAG AA at 12px) - Pretty-print collapsed raw JSON in media embeds (was missed earlier) - Cap .tool-cmd at 120px to prevent tools with many args from making approval blocks disproportionately tall in history replay - Dedicated .media-player-error class instead of reusing .tool-output * fix: Discord tool info name matching regression, suppress deprecation warning The escape_markdown call on tool names was stored for matching against ToolResultEvent.name, but event.name is raw/unescaped. The escaped name never matched, so the "Running → Done" transition silently failed and previews disappeared from the status embed. Fix: store raw name for matching, use escaped name only for display. Also suppress discord.py's re.sub count deprecation warning (Python 3.13+ issue, fixed upstream). * fix: update MCP tool description tests to match prefix removal * fix: address PR #292 review round 2 - Retry button: handle missing span children in click handler so retry buttons from player error state don't throw - Footer count: use len(lines) instead of min(len(results), 10) to reflect actual rendered count after char budget truncation - Null display: use "null" instead of "None" in JS tool arg preview - Broader redaction: also redact JSON "api_key": "..." patterns - SSRF hardening: block loopback and link-local IPs plus cloud metadata hostnames in thumbnail fetch (private LAN IPs still allowed) |
||
|
|
57080f4615 |
chore: release infrastructure for dual-track stable/experimental (#282)
* chore: release infrastructure for dual-track stable/experimental CI/CD changes for the 1.0 release: - Gate PyPI publish and Docker publish on CI success via workflow_run - Add docker-publish.yml: builds and pushes to GHCR with smart tagging (stable gets :X.Y.Z/:X.Y/:stable/:latest, pre-release gets :experimental) - Add stable/* and v* tags to CI and docker-scan triggers - Remove stale [mq] extra and types-redis from CI (Redis MQ deleted) - Remove stale redis from Renovate package rules Release tooling: - scripts/release.sh: bump version, uv lock, commit, tag (with --push) - docs/releasing.md: documents stable/experimental workflow Docker: - Add /workspace mount point (WORKSPACE_MOUNT env var, defaults to empty volume) - Update .env.example: remove stale Redis/auth-token refs, add workspace/model/discord README: - Remove beta warning, add hero image and release tracks table * fix: derive release tag from git instead of workflow_run.head_branch Use git tag --points-at HEAD after checkout to resolve the release tag instead of relying on workflow_run.head_branch, which may not reliably be the tag name for tag-triggered CI runs. Both publish and docker-publish workflows now skip cleanly when no v* tag exists at the checked-out commit. |
||
|
|
c5d5d0b7cd |
fix: update-vendored-js.sh detects old version from filesystem
The script detected the old version from pyproject.toml, which Renovate had already updated. This caused OLD_DIR == NEW_DIR, so the script downloaded files then immediately deleted them. Fix: detect old version from the actual directory on disk. Add a guard that errors if old == new version to prevent silent data loss. Also: run the fixed script to vendor katex 0.16.40 (fonts + css + js). |
||
|
|
22402e89de |
feat: add dependency management with Renovate, uv.lock, and security … (#83)
* feat: add dependency management with Renovate, uv.lock, and security scanning Adds automated dependency update detection and vulnerability scanning across all dependency layers (Python, vendored JS, TypeScript SDK, Docker, GitHub Actions). - Renovate config with 10 package groups and custom regex managers for vendored JS (KaTeX, Highlight.js, Mermaid) tracking via npm registry - uv.lock for reproducible builds (80 packages) - Dockerfile switched to uv sync --frozen with layer caching - CI: pip-audit (via lock file), npm audit, lock-check jobs - CI: lint job uses pre-commit for ruff version consistency - Docker security scan workflow (weekly Trivy, HIGH/CRITICAL) - Helper script for vendored JS library updates * fix: resolve CI failures and address review feedback - Update pre-commit hooks: ruff v0.9.10 -> v0.15.6 (fixes deprecated UP038 rule), mypy v1.14.1 -> v1.19.1 - Add per-file-ignore for N802 on sandbox.py (ast visitor convention) - Fix pip-audit: install into uv venv so uv run can find it - Pin uv-version in CI to match lock file generator (0.9.18) - Upgrade vitest ^2.0 -> ^4.1 to fix esbuild GHSA-67mh-4wv8-2f99 - Vendored JS script: use grep -rl for auto-discovery of version refs (catches docs/architecture.md), fix LICENSE comment, portable grep |