Skip to content

Refactor Chat to ACP-Native Subpackage with Deep Agents Provider#10

Open
deeleeramone wants to merge 69 commits intomainfrom
claude/goofy-diffie
Open

Refactor Chat to ACP-Native Subpackage with Deep Agents Provider#10
deeleeramone wants to merge 69 commits intomainfrom
claude/goofy-diffie

Conversation

@deeleeramone
Copy link
Copy Markdown
Owner

This PR restructures the chat system from 3 monolithic files into a pywry/chat/ subpackage implementing the Agent Client Protocol (ACP). All provider adapters now conform to ACP's session lifecycle (initialize → new_session → prompt → cancel) and yield typed SessionUpdate notifications.

Chat subpackage:

  • models.py: ACP content blocks, ACPToolCall, ChatMessage, ChatThread
  • session.py: SessionMode, SessionConfigOption, PlanEntry, PermissionRequest, Capabilities
  • updates.py: SessionUpdate discriminated union (11 types)
  • artifacts.py: 8 artifact types including TradingViewArtifact
  • manager.py: ChatManager accepting providers or legacy handlers
  • providers/: OpenAI, Anthropic, Magentic, Callback, Stdio, DeepAgent
  • permissions.py: RBAC permission mappings for ACP operations
  • html.py: build_chat_html()

DeepAgent provider:

  • Wraps LangChain Deep Agents CompiledGraph
  • Maps LangGraph astream_events to ACP SessionUpdate types
  • Auto-configures checkpointer and memory store from PyWry state backend
  • Built-in system prompt onboarding the agent to the chat UI
  • Tool kind mapping for Deep Agents built-in tools
  • write_todos → PlanUpdate translation

SQLite state backend:

  • Complete implementation of all 5 state ABCs (Widget, Session, Chat, EventBus, ConnectionRouter)
  • Encrypted at rest via SQLCipher with keyring-based key management
  • Full audit trail: tool calls, artifacts, token usage, resources, skills
  • Auto-created admin session with seeded role permissions
  • Same schema as Redis — fully interchangeable

JS extraction:

  • 8 JS files extracted from scripts.py to frontend/src/
  • WS bridge extracted from inline.py to frontend/src/ws-bridge.js
  • scripts.py rewritten to load from files

Frontend:

  • TradingView artifact renderer in chat-handlers.js
  • ACP event handlers: permission-request, plan-update, mode-update, config-update, commands-update
  • PyWryChatWidget expanded with chat ESM and trait-based asset loading

Documentation:

  • Rewrote chat guide and providers page with full ACP coverage
  • Created PyTauri, Anywidget, and IFrame+WebSocket transport protocol pages
  • Rewrote AG Grid and Plotly integration pages
  • Rewrote Why PyWry page reflecting current capabilities
  • Moved multi-widget to concepts, tauri-plugins under pytauri
  • Purged all print() statements from example code across all docs
  • Removed all section-banner comments and forbidden comment words

deeleeramone and others added 2 commits April 14, 2026 16:57
…SQLite state backend

Restructures the chat system from 3 monolithic files into a `pywry/chat/`
subpackage implementing the Agent Client Protocol (ACP). All provider
adapters now conform to ACP's session lifecycle (initialize → new_session →
prompt → cancel) and yield typed SessionUpdate notifications.

Chat subpackage:
- models.py: ACP content blocks, ACPToolCall, ChatMessage, ChatThread
- session.py: SessionMode, SessionConfigOption, PlanEntry, PermissionRequest, Capabilities
- updates.py: SessionUpdate discriminated union (11 types)
- artifacts.py: 8 artifact types including TradingViewArtifact
- manager.py: ChatManager accepting providers or legacy handlers
- providers/: OpenAI, Anthropic, Magentic, Callback, Stdio, DeepAgent
- permissions.py: RBAC permission mappings for ACP operations
- html.py: build_chat_html()

DeepAgent provider:
- Wraps LangChain Deep Agents CompiledGraph
- Maps LangGraph astream_events to ACP SessionUpdate types
- Auto-configures checkpointer and memory store from PyWry state backend
- Built-in system prompt onboarding the agent to the chat UI
- Tool kind mapping for Deep Agents built-in tools
- write_todos → PlanUpdate translation

SQLite state backend:
- Complete implementation of all 5 state ABCs (Widget, Session, Chat, EventBus, ConnectionRouter)
- Encrypted at rest via SQLCipher with keyring-based key management
- Full audit trail: tool calls, artifacts, token usage, resources, skills
- Auto-created admin session with seeded role permissions
- Same schema as Redis — fully interchangeable

JS extraction:
- 8 JS files extracted from scripts.py to frontend/src/
- WS bridge extracted from inline.py to frontend/src/ws-bridge.js
- scripts.py rewritten to load from files

Frontend:
- TradingView artifact renderer in chat-handlers.js
- ACP event handlers: permission-request, plan-update, mode-update, config-update, commands-update
- PyWryChatWidget expanded with chat ESM and trait-based asset loading

Documentation:
- Rewrote chat guide and providers page with full ACP coverage
- Created PyTauri, Anywidget, and IFrame+WebSocket transport protocol pages
- Rewrote AG Grid and Plotly integration pages
- Rewrote Why PyWry page reflecting current capabilities
- Moved multi-widget to concepts, tauri-plugins under pytauri
- Purged all print() statements from example code across all docs
- Removed all section-banner comments and forbidden comment words

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Local settings file should not be in the repository.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@deeleeramone deeleeramone added enhancement New feature or request integrations Issues related to library integrations. labels Apr 15, 2026
deeleeramone and others added 15 commits April 14, 2026 17:17
- callback.py: remove redundant GenerationCancelledError import (used in _iter_result)
- deepagent.py: add logger, log exceptions instead of bare pass, add
  on_chat_model_start and on_chain_start event handlers (StatusUpdate was unused)
- base.py: add return statements to audit trail no-op methods (B027)
- sqlite.py: restructure keyring try/except with else block (TRY300),
  log keyring fallback instead of bare pass (S110), add D102 per-file-ignore
- test files: remove unused imports (F401), fix async sleep (ASYNC251),
  inline lambda (PLW0108), remove unused variable assignments (F841)
- ruff.toml: add sqlite.py D102 ignore for ABC-documented methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- deepagent.py: fix E402 import order, extract _handle_tool_end to
  reduce prompt() complexity (C901/PLR0912)
- ruff.toml: add S108 and PERF401 to test ignores
- test_deepagent_provider.py: add test for on_chat_model_start →
  StatusUpdate (was imported but unused)
- test_state_sqlite.py: remove unused time import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SqliteWidgetStore: implement all 10 abstract methods from WidgetStore
  ABC (register, get, get_html, get_token, exists, delete, list_active,
  update_html, update_token, count)
- SqliteEventBus/SqliteConnectionRouter: alias to Memory implementations
  since SQLite is single-process (no custom pub/sub or routing needed)
- DeepagentProvider: guard all langgraph imports with try/except so tests
  pass without langgraph installed, return None from _create_store and
  _create_checkpointer when unavailable
- Tests: use correct ABC method names, skip langgraph-dependent test when
  not installed, remove cache_clear call on non-cached function, pass
  auto_store=False alongside auto_checkpointer=False in all tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- models.py: remove unused type:ignore comments
- providers/__init__.py: annotate return from getattr to satisfy no-any-return
- providers/stdio.py: annotate session_id as str
- providers/deepagent.py: add type:ignore for optional dep imports,
  use alias names in ToolCallUpdate constructors for mypy pydantic plugin,
  type _map_todo_status return as Literal
- state/sqlite.py: type:ignore pysqlcipher3 import, annotate conn and
  cost variables
- state/_factory.py: use MemoryEventBus/MemoryConnectionRouter directly
  for SQLite backend (no db_path needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous type:ignore removal merged two statements onto
one line, causing a SyntaxError that broke every test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…it trail

- chat-providers.md: add DeepagentProvider mkdocstrings entry
- state.md: add ChatStore ABC, SqliteWidgetStore, SqliteSessionStore,
  SqliteChatStore mkdocstrings entries
- reference/index.md: update state description to include SQLite
- state-and-auth.md: add SQLite section with audit trail details,
  encryption, auto-admin setup, config table entry
- deploy-mode.md: update backend value to include "sqlite"
- configuration.md: document sqlite_path setting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- manager.py: renamed _process_legacy_item to _process_handler_item,
  removed "legacy" from docstrings
- test_chat_protocol.py: renamed TestLegacyHandlerWithNewUpdates
- auth/deploy_routes.py: removed "legacy alias" comment
- cli.py: removed "backwards compatibility" comment
- inline.py, templates.py: removed "legacy template" from JS comments
- tvchart/__init__.py: removed "backward compatibility" from docstring
- tvchart/udf.py: removed "legacy" from docstring
- mcp/skills/__init__.py: removed "backward compatibility" from docstring
- test_mcp_unit.py: removed "legacy" from comment
- test_plotly_theme_merge.py: renamed legacy -> single in test names,
  comments, and JS template strings
- test_plotly_theme_merge_e2e.py: renamed storedLegacy -> storedSingle
- test_tvchart.py: renamed test_symbol_info_legacy_alias, removed
  "backward compat" from docstrings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Check for iterators (__next__) after coroutines and strings, not
before. Using __iter__ matched strings; __next__ correctly identifies
generators and iterators without matching strings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Python 3.10's asyncio.iscoroutine() returns True for some generator
objects. Use inspect.isgenerator() and inspect.isasyncgen() to detect
generators first, then inspect.iscoroutine() for strict coroutine
checking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@deeleeramone deeleeramone requested a review from Copilot April 16, 2026 02:25

This comment was marked as outdated.

deeleeramone and others added 9 commits April 15, 2026 20:48
1. Provider factory: explicit class name mapping instead of
   capitalize() which produced wrong names (OpenaiProvider vs
   OpenAIProvider)
2. PermissionRequestUpdate: add alias on request_id field so
   model_dump(by_alias=True) produces requestId for frontend
3. StdioProvider: map _request_id to request_id instead of
   stripping it, preserving correlation ID for permission flow
4. system-events.js: initialize window.pywry if missing before
   registering handlers, prevents undefined errors if loaded
   before bridge.js
5. chat-handlers.js: only prepend / to command names if not
   already present, preventing //foo double-slash
6. events/chat.md: replace stale InputRequiredResponse example
   with correct PermissionRequestUpdate usage
7. test_chat.py: add OpenAI provider factory test to cover
   class name resolution
8. widget.py: move asset listener registration inside render()
   function via __TOOLBAR_HANDLERS__ replacement where model
   is in scope, instead of module-level IIFE where it isn't

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Key changes across the stack:

- NEW `examples/pywry_demo_deepagent_nvidia.py`: LangChain Deep Agent
  driving the TradingView chart through the in-process FastMCP server;
  model discovery via NVIDIA NIM, graceful uvicorn shutdown, proactor
  close-race filtering.

- `chat/providers/deepagent.py`:
  - `InlineToolCallMiddleware` rewrites leaked
    ``functions.<name>:<idx>{...}`` markup from the model's text stream
    into structured `tool_calls`, appending to any existing structured
    calls so mixed-format responses fire end-to-end.
  - `PlanContinuationMiddleware` re-enters the graph on exit when
    `state.todos` still has pending entries — no nudge counting, no
    auto-completion, pure state read.
  - `_ToolCallTextFilter` stateful stripper removes both inline markup
    and `<|...|>` chat-template special tokens from the streamed text,
    handling chunk-boundary splits.

- TVChart frontend:
  - Event-based data-settled signal (no state polling) tracks
    applyDefault, chart-type re-apply, and indicator re-add so Python
    tools see the chart fully stable before the next call.
  - `whenMainSeriesReady` callback replaces the poll that chained
    chart-type-change off series attachment.
  - Case-insensitive time-range values ("1D" == "1d") and preserved
    range cleared on new zoom so applyDefault can't clobber the user's
    explicit zoom.
  - Theme-aware CSS vars for hollow-body, hidden, price-line, and
    settings-dialog defaults — no hardcoded hex in JS.

- MCP handlers: `_emit_zoom_and_confirm` waits on tvchart:data-settled
  rather than polling state; `_minimal_confirm_state` strips rawData
  from tool returns so the agent can't fabricate prices.

- Three SKILL.md files (tvchart, chat_agent, events) added under
  `pywry/mcp/skills/` for agents operating inside the demo.

- Docs updated for the new MCP tools and skills surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ``PYWRY_TVCHART_CREATE`` now calls ``_tvEnsureDrawingLayer`` so every
  live chart has a drawing overlay canvas from the start, rather than
  lazy-creating it on first tool selection.  The visibility / lock /
  state-export paths depend on the overlay existing.

- ``tvchart:request-state``, ``tvchart:tool-visibility``, and
  ``tvchart:tool-lock`` handlers resolve chart id via a shared
  ``_resolveCid(data)`` that falls back through ``data.chartId`` →
  ``bridge._chartId`` → the first entry on ``__PYWRY_TVCHARTS__``.
  Single-widget native-window mode doesn't set ``bridge._chartId``, so
  the old ``_cid``-only code no-op'd against a live chart.

- ``tests/test_tvchart_e2e.py`` drawing tests (20-24) rewritten to
  drive the REAL drawing pipeline: select the tool with
  ``_tvSetDrawTool`` then dispatch a synthetic ``click`` MouseEvent on
  the overlay canvas.  The click handler does the work — computes
  price/time via ``_tvFromPixel``, pushes the drawing, calls
  ``_tvRenderDrawings``, creates the native ``priceLine``.  The prior
  ``ds.drawings.push`` shortcut bypassed rendering, so nothing ever
  actually landed on the canvas.  Test 20 also verifies the canvas
  has non-empty alpha pixels afterwards.

- ``tests/test_chat_protocol.py`` updated to the current chat-manager
  event split: ``chat:tool-call`` for pending / in_progress (creates
  the collapsible tool card); ``chat:tool-result`` for completed /
  failed (fills the result body).  Same ``toolCallId`` links the pair.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Kept the property-accessor pattern on ``bridge._chartId`` (from
  main) so native-mode ``PYWRY_TVCHART_CREATE`` propagates the
  chart id into the closure.
- Kept the ``_resolveCid(data)`` registry-fallback helper (from
  branch) so handlers that operate on the live chart still resolve
  when the accessor hasn't been set yet.
- Kept the backwards-compatible ``_handler`` test helper (accepts
  both ``window.pywry.on('<event>', ...)`` and
  ``bridge.on('<event>', ...)``).
- Kept the ``_tvApplyAbsoluteDateRange`` assertion in the
  time-range-selection test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``pysqlcipher3`` (used by the encrypted SQLite state backend, pulled
in via the ``features`` / ``dev`` / ``sqlite`` / ``all`` groups)
builds a C extension that ``#include``s ``sqlcipher/sqlite3.h``.  The
previous workflows didn't install the SQLCipher headers on any
runner, so every ``pip install .[dev]`` step failed with
``fatal error: sqlcipher/sqlite3.h: No such file or directory``.

- Linux (``test-pywry.yml`` + ``publish-pywry.yml``): ``libsqlcipher-
  dev`` added to the existing ``apt-get install`` lines in the Lint,
  Test, Wheel-build verification, and Publish-verification jobs.
- macOS: ``brew install sqlcipher`` + ``LDFLAGS`` / ``CPPFLAGS`` env
  vars so Homebrew's ``/opt/homebrew`` (or ``/usr/local``) include
  and lib directories are on the compile / link paths.
- Windows: ``vcpkg install sqlcipher:<triplet>`` covering both
  ``x64-windows`` and ``arm64-windows``, with ``SQLCIPHER_PATH`` /
  ``INCLUDE`` / ``LIB`` / ``PATH`` pointed at the vcpkg install
  root so the MSVC build picks up the headers and .lib files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``pysqlcipher3`` 1.2.0 hasn't seen a release since 2021 and its C
extension fails to compile against Python 3.13+: it calls
``PyObject_AsCharBuffer`` (removed in 3.10) and ``_PyLong_AsInt``
(removed in 3.13).  That breaks every CI runner on Python 3.13 /
3.14 and every ``pip install`` of the encrypted-SQLite opt-in.

Switching to ``sqlcipher3-binary>=0.5.4``:

- Actively maintained drop-in fork (same ``dbapi2`` API).
- Ships prebuilt manylinux / macOS / Windows wheels on PyPI, so no
  ``libsqlcipher-dev`` / ``brew install sqlcipher`` /
  ``vcpkg install sqlcipher`` setup is needed in CI.  The system-
  dep installs added in c0e0f4a are reverted.

``pywry/state/sqlite.py`` tries ``sqlcipher3`` first and falls back
to ``pysqlcipher3`` for users pinning the legacy package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
deeleeramone and others added 30 commits April 19, 2026 15:48
Asserts are stripped under `python -O`, so any runtime invariant that
was "enforced" with `assert` would silently evaporate in an optimised
install.  This commit takes the invariants seriously:

- ruff.toml: remove S101 from the global ignore list so asserts in
  non-test code fail lint.  S101 stays per-file-ignored under
  tests/ (with a leading block comment + an inline reason on every
  rule explaining why the check doesn't apply under tests/), and
  the remaining global ignores now carry short inline comments so
  future readers can evaluate whether each one still pays rent.
- pywry/chat/providers/stdio.py: the four `assert proc.stdin is
  not None` / `assert self._process{,.stdout} is not None` spots
  now raise RuntimeError with a specific message.  Creating the
  subprocess with PIPE means these are practically unreachable,
  but when they DO trip we want a diagnostic exception, not a bare
  AssertionError or (worse) a silent skip under -O.
- pywry/mcp/handlers.py: every `assert widget is not None` after
  `_get_widget_or_error()` is replaced with an explicit
  `if error is not None or widget is None: return error or {...}`
  guard.  This matches the existing pattern in
  `_get_widget_or_error` itself and makes the invariant visible at
  the call site.

ruff check pywry/ tests/ is clean and all touched unit tests still
pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend load-order robustness:
- event-bridge.js (#20): install a minimal `window.pywry` + `_trigger`
  shim so the Tauri `pywry:event` listener can fire before `bridge.js`
  has finished loading.
- hot-reload.js (#21): guard against missing `window.pywry` and WRAP
  any existing `pywry.refresh` rather than clobbering it (so a later
  bridge.js / custom app can still add pre-refresh hooks).  Also
  gates the "initialised" console.log behind `PYWRY_DEBUG`.
- theme-manager.js (#30): gate the remaining unconditional
  console.log calls behind `window.PYWRY_DEBUG`, matching bridge.js.

stdio provider reliability:
- #24–#26: `_read_loop` now wraps its body in try/except/finally that
  fails every pending JSON-RPC request when the subprocess exits,
  stdout closes, or the reader is cancelled — callers awaiting
  `_send_request` used to hang forever.  Extracted
  `_fail_pending_requests` and `_dispatch_rpc_message` helpers to
  keep complexity under the PLR0912 budget.
- #22–#23: `prompt()` now always settles `prompt_future` in a
  `finally` block (cancel if still running, then await and log any
  exception), so agent errors surface to the caller and
  `_pending` can't leak on cancellation.  Extracted
  `_serialize_content_blocks`, `_update_type_map`, and
  `_settle_prompt_future` helpers to keep `prompt()` itself simple.

Pydantic / docs clarity:
- chat/updates.py (#27): add a comment explaining why
  `Field(discriminator="session_update")` still routes raw
  `{"sessionUpdate": ...}` JSON correctly — every variant has
  `populate_by_name=True` + `alias="sessionUpdate"`, so the union
  dispatches on both key styles.  Verified locally via TypeAdapter.

CI:
- test-pywry.yml (#29): explicitly guard the `lint` job's
  "Build SQLCipher from source (Linux)" step with
  `if: runner.os == 'Linux'`.  The job is already pinned to
  ubuntu-24.04, but the guard makes intent explicit and stops the
  step running if anyone later adds matrix support to the lint job.

Verified false positives (no code change):
- #28: `pywry:download-csv` IS implemented, in plotly-widget.js.
- #29 "duplicate build step": the second SQLCipher build is in a
  different job (`test`), not a duplicate.

Full `ruff check pywry/ tests/` and `mypy pywry/` are clean; chat
unit + state unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loaded via importlib.import_module(), so mypy never sees them as
direct imports and the overrides are reported as unused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port of the upstream TradingView lightweight-charts volume-profile
plugin example.  Ships both variants: Fixed Range anchors the
histogram to a bar-index range, Visible Range follows the viewport
and recomputes on every pan/zoom (debounced via rAF).

- 09-indicators.js: _tvComputeVolumeProfile buckets OHLCV volume
  across price levels, _tvMakeVolumeProfilePrimitive renders the
  histogram via the ISeriesPrimitive API, _tvRefreshVisibleVolumeProfiles
  recomputes visible-range instances on viewport change.
- 04-series.js: _tvAddIndicator branch for both VP variants; creates
  the primitive and attaches it to the main price series.
- 09-indicators.js: _tvRemoveIndicator detaches the primitive
  alongside the usual series cleanup.
- 05-lifecycle.js: subscribeVisibleLogicalRangeChange now also
  calls the VP refresh hook (rAF-debounced).
- 05-lifecycle.js + 06-storage.js: layout export/restore round-trips
  mode / bucketCount / fromIndex / toIndex.
- 10-events.js: tvchart:add-indicator handler forwards fromIndex /
  toIndex to _tvAddIndicator.
- tvchart/mixin.py: new add_volume_profile() convenience method
  with mode="fixed"|"visible", bucket_count, from_index/to_index.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Volume Profile: replace upstream time-anchored port with proper
VPVR/VPFR — horizontal rows pinned to the pane edge (right by default),
bar length proportional to net volume at each price bucket, split into
up-volume / down-volume by bar direction, POC line, and 70%-volume
Value Area colouring.

New indicators:
- Moving Averages: HMA, VWMA, Ichimoku Cloud
- Volatility: Keltner Channels, Historical Volatility
- Trend: Parabolic SAR
- Momentum: MACD, Stochastic, Williams %R, CCI, ADX, Aroon
- Volume: Accumulation/Distribution

Multi-line indicators (MACD, Stochastic, Aroon, ADX) share a subplot
pane and a group id so they remove together.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 09-indicators.js: fix bitmap-pixel-ratio bug (bars now render);
  cap bar width at 15% of pane (down from 25); add Volume mode
  (Up/Down / Total / Delta); add Developing POC + Developing VA
  step-line plots; recompute on viewport change with the new opts
  shape (was passing bucketCount as opts, silently ignoring layout
  toggles); fix _tvApplyIndicatorSettings VP branch to use the new
  rowsLayout / rowSize / volumeMode schema; surface settings-dialog
  click errors via try/catch + console.error.
- tvchart.css: VP defaults now use a blue / violet palette with low
  opacity so the histogram doesn't camouflage the green/red candles
  and price action stays readable through it; new --pywry-tvchart-vp-*
  CSS variables override the old ones (and a small indicator
  multi-plot palette --pywry-tvchart-ind-* powers MACD / Stoch /
  Aroon / ADX / KC / Ichimoku / SAR defaults).
- 04-series.js: the VP add branch + the new indicator branches now
  pull every default colour from the CSS palette (themed) instead
  of hard-coded literals; VP carries rowsLayout / rowSize /
  volumeMode through to the slot + indicator state.
- 10-events.js: forward the new VP / indicator opts (rowsLayout,
  rowSize, volumeMode, developingPOC/VA, dev colours, fast/slow/
  signal/dPeriod/step/maxStep/tenkan/kijun/senkouB/annualization)
  from the Python ``tvchart:add-indicator`` event into the JS
  indicator def.
- mixin.py: extend add_volume_profile() with placement / width /
  value-area / show-poc / show-value-area / colour overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _tvSetIndicatorVisibility now toggles the VP primitive's hidden
  flag and triggers a redraw — the eye icon actually hides VP.
- VP renderer skips drawing when getHidden() is true.
- Legend row now shows "VPVR Number Of Rows 24 Up/Down 70"
  (matches TradingView), reads from rowsLayout / rowSize /
  volumeMode / valueAreaPct.
- Legend dot uses the up-volume swatch when the indicator has no
  line colour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ReferenceError when clicking the gear — _tvAppendOverlay(chartId, …)
read an undeclared identifier.  Other indicators happened to never
hit this path because the dialog throws AFTER the body is rendered
in some flows; VPVR exposes it consistently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Every Inputs / Style row in the VP settings dialog now funnels
  through _tvApplyVPDraftLive() so changes paint instantly: colours
  swap, width / placement reflow, rows-layout / row-size /
  value-area / developing-poc/va recompute the bucket profile and
  redraw without waiting for the OK button.
- Legend row now carries a value span showing up / down / total
  volume formatted as 1.23M / 4.56K, refreshed on every recompute
  (visible-range pan / zoom, settings change, layout restore).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
03-theme.js: set explicit textColor on rightPriceScale + leftPriceScale
when building chart options.  layout.textColor doesn't always cascade
to price-scale labels, so labels rendered with the dark-theme grey on
the white light-mode background.

05-lifecycle.js: on theme switch, clear the stored Text-Color /
Lines-Color overrides in _chartPrefs so the new palette wins; pass
explicit textColor to both price scales alongside the layout update.

07-drawing.js: _emitDrawingAdded now calls _tvRevertToCursor so the
left-toolbar drawing button drops its "active" state the moment the
drawing finishes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files grew to 3.9k–6.2k lines each; break them up into modular
subfolders loaded recursively via `rglob`, alphabetical order
preserved so dependencies still resolve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove-Volume only flipped a legend dataset flag — series and pane
stayed on the chart. Now it calls chart.removeSeries, deletes the
pane, and reindexes higher panes. Restore-Volume rebuilds the
histogram from the stored raw bars. Also fix the volume series to
use the pane's standard 'right' scale so its axis labels render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_tvRecomputeIndicatorSeries was missing branches for VWAP, MACD,
Stochastic, Aroon, ADX, CCI, Williams %R, A/D, Historical Volatility,
Keltner Channels, Ichimoku, Parabolic SAR, and Volume Profile — so
when the datafeed replaced initial bars with real data, these
indicators stayed frozen at their initial snapshot. Most visibly:
VWAP stuck at 9.99 (the placeholder bars' h=l=c value) on a $270
stock because it was computed once, never again.

Add recompute branches for every new indicator, matching each
group's actual series-key prefix (_macd_line_, _adx_plus_,
_ichi_tenkan_, etc.) and reapplying per-bar colors for the MACD
histogram. moving-average-ex now also recomputes HMA/VWMA, not
just SMA/EMA/WMA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a real contract suite under TestTVChartIndicatorCatalog /
TestTVChartVolumeProfile / TestTVChartLegendVolumeRemoval /
TestTVChartThemeVariables: every catalog entry has a compute fn,
add branch, and recompute branch; VP returns the expected fields;
volume Remove actually removes; every --pywry-tvchart-vp-* and
--pywry-tvchart-ind-* var is defined for both themes (86 new tests,
267 total).

Docs: new Indicators reference page listing TradingView-accurate
parameter names for every indicator, CSS reference updated with VP
and indicator palette sections, events reference updated with the
full name list for tvchart:add-indicator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ywhere

The SMA/EMA/WMA catalog entries were removed in the MA unification; the
recompute path still had a dead baseName branch for them, and the e2e /
unit tests + docs still called add_builtin_indicator("SMA", ...).
Update tests and docstrings to add indicators as Moving Average with
method="SMA" | "EMA" | "WMA" | "HMA" | "VWMA", matching how the
indicator panel UI actually lets users add them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dialog previously only pushed edits on Ok — the user couldn't see
their colour / period / line-width change until they dismissed the
modal. Wire every row helper through a shared _livePreview() that
calls _tvApplyIndicatorSettings on each edit, coalesced to rAF so a
number-spinner drag paints once per frame.

Also make the "Values in status line" and "Inputs in status line"
checkboxes actually drive the legend rebuild — when off, drop the
numeric-parameter suffix from the indicator label and skip appending
the per-series value spans.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symbol changes used to wipe indicators and compare overlays; an
SMA(20) on AAPL is still an SMA(20) on MSFT — the user shouldn't
need to re-add it after switching tickers. The restore loop also
only forwarded five config fields so MACD lengths, Ichimoku lines,
VP range + layout, status-line toggles, and every other tunable
got reset to defaults on interval change.

Carry every info.* field through as indicatorDef._*, clean up stale
_activeIndicators / _volumeProfilePrimitives entries before destroy
so the re-add doesn't duplicate rows in the legend, and skip extra
group members during re-add since _tvAddIndicator materialises them
itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ttach

Datafeed mode was firing the ready callback as soon as the series
was constructed — BEFORE the async getBars + setData step populated
_seriesRawData. Every consumer that needs the bars (indicator re-add
after interval/symbol change in particular) saw an empty bar set and
silently produced lines with stale or zeroed values. Static-bar mode
was already firing at the right spot; move the datafeed fire to match
so the ready semantics are consistent: "series has data", not "series
exists". Switch the post-destroy indicator re-add from setTimeout(100)
to whenMainSeriesReady and force a recompute pass after all indicators
are attached, so every saved indicator redraws against the current
bars across both interval and symbol changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs when flipping ETH → RTH:

1. Session filter iterated seriesMap and setData'd every series in it —
   including indicator series. Indicators have no _seriesRawData entry
   so they silently fell through, but the filter shouldn't be touching
   them at all. Skip any sid with an _activeIndicators entry up front.

2. After filtering the main bars to the RTH subset, indicators weren't
   recomputed. An SMA(9) stayed at the ETH-computed value even though
   the chart now shows only RTH bars — a 9-bar window on the RTH chart
   is very different from a 9-bar ETH window with an overnight gap.
   Teach _tvSeriesRawData to return _seriesDisplayData when the user
   flipped to RTH, then drive _tvRecomputeIndicatorsForChart and
   _tvRefreshVisibleVolumeProfiles from _tvApplySessionFilter so every
   indicator and VPVR lines up with the filtered bar set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The RTH toggle shrinks the main bar array (often by 2-3x for US
equities), and LWC's getVisibleLogicalRange keeps pointing at the
ETH indices until the time scale is refit. _tvRefreshVisibleVolumeProfiles
was pulling that stale range and the VP compute clamped its lo/hi
to the last single bar — explaining the "877K up, 0 down" VPVR
readout on an otherwise empty-looking chart.

Reorder _tvApplySessionFilter so fitContent runs first (re-seating
the visible range on the filtered set), then indicator recompute +
VP refresh run against the fresh range. Also harden the VP refresh
itself: when the visible range sits entirely outside the new bar
array, fall back to [0, N-1] instead of clamping to one bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three regressions surfaced when flipping session to RTH:

1. _tvFilterBarsBySession couldn't parse TradingView-style strings
   like "0930-1600:23456" (weekday suffix) or pipe-separated
   multi-sessions — split on both `,` and `|`, strip anything after
   `:` before the HHMM-HHMM match.

2. Scrollback's onVisibleLogicalRangeChange handler called
   series.setData(merged) directly, bypassing the active session
   filter — so every chunk of older bars loaded via the left-edge
   scrollback wiped the RTH filter for the merged view, leaving an
   empty-looking chart because _seriesDisplayData went stale.
   Hand off to _tvApplySessionFilter when RTH is active so the
   merged bars get re-filtered, indicators recompute, and VP
   refreshes atomically.

3. The security-info session schedule never rendered the overnight
   leg because nothing derived it from Yahoo metadata and the
   legend-side window builder only looked at pre/regular/post.
   Add a sixth `overnight_str` field to `_build_session_string`,
   ship it as `session_overnight` in the symbolInfo dict, wire
   it through the datafeed normalizer, and teach `_sessionWindows`
   to split the wrap-around "2000-0400" into an evening leg
   (Sun-Thu) and a next-morning early leg (Mon-Fri).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ounce range emit

Three fixes for the scroll-past-data overload + lingering RTH leaks:

1. Streaming bars during after-hours kept painting onto an RTH chart
   because subscribeBars only checked an optional bar.market_hours
   field most providers don't ship. Drive the gate off
   _tvIsBarInCurrentSession (uses the symbol's actual session string)
   and keep market_hours as a fallback.

2. Scrollback was infinite-looping after RTH was on: the post-merge
   re-filter via _tvApplySessionFilter always called fitContent at
   the end, which snapped range.from back to 0, retriggered the
   scrollback handler, fetched another batch, filtered again,
   fitContent again. The chain hogged the JS event loop hard enough
   that toolbar clicks (search/compare) never reached their handlers.
   Add an opts.skipFitContent flag and pass it from the scrollback
   path; the user-facing session toggle still fits.

3. Scroll-past-data overload: each visible-range tick fired straight
   to Python via bridge.emit (60+/sec when scrolling). Coalesce via
   rAF and skip emit when the range hasn't moved by ≥ 0.5 logical
   bars. Also harden the scrollback callback to mark exhausted on
   ANY non-OK response (error, status:no_data, noData:true, empty
   bars, or bars that don't actually advance past the current
   oldest), and add an extra guard against rapidly dragging the
   scroll wheel past where data could possibly exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dark theme dim was #787b86 — TradingView's default but too washed
out in PyWry's dark background. The labels in the legend (Volume,
VPVR header, OHLC O/C tags, time stamp) and pane legends became
hard to read against the deep-black background.

Lift each text tier one notch: text → #e6e9f0, muted → #c4c9d4,
dim → #a3a8b5 in dark; mirror with deeper-than-default values in
light. Bump main legend font from 12px → 13px and indicator legend
font from 11px → 13px so the per-bar OHLC, the symbol-time line,
and indicator names actually read at a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LWC's series.update throws "Cannot update oldest data" if the
incoming bar's time is before the last bar already in the series.
That happens easily after a session-filter swap (the series was
just setData'd with filtered bars whose newest is e.g. yesterday's
RTH close, then a late tick arrives for an earlier extended-hours
bar) and the unhandled throw bubbled out of pywry.dispatch into
the console as an "Error in event handler".

Skip the tick when its time is older than _seriesRawData[sid]'s
last entry, and wrap series.update / volumeMap.update in try/catch
so any remaining mismatch (format, business-day vs unix) drops the
tick instead of breaking the stream subscription.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Volume Profile indicator keeps info.color = null because its
colours live on opts.upColor / downColor / vaUpColor / vaDownColor /
pocColor. But the settings dialog seeded draft.color with the
fallback "#e6b32c" (a gold that matched the default POC shade), and
the "Apply per-plot styles" pass wrote draft.color back onto
info.color for every style-sid — including VP.  The next legend
rebuild saw info.color = gold, skipped the VP-specific blue-up fallback,
and rendered the whole "VPVR Number Of Rows 24 Up/Down 70 ..." row
in POC-looking gold.

Guard both sides:

- 11-apply-settings skips the per-plot colour write when type is VP.
- 09-legend-rebuild forces the VP name to the default legend text
  colour (and the dot to the up-volume swatch) regardless of any
  info.color that might have leaked through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cross destroy

_tvBuildChartOptions previously set only ~60% of what LWC's
ChartOptionsImpl exposes — gaps like kineticScroll, trackingMode,
addDefaultPane, full timeScale tuning (rightOffset, barSpacing,
fixLeftEdge, lockVisibleTimeRangeOnResize, etc.), and full
price-scale tuning (mode, invertScale, alignLabels, entireTextOnly,
minimumWidth, visible) meant any payload.chartOptions override for
those silently fell through to LWC's library default instead of
PyWry's.

Add defaults for every property on the interface so payloads can
override anything and the chart still boots with sensible PyWry
behaviour.

Also persist _chartPrefs / _sessionMode / _selectedTimezone through
the interval/symbol destroy-recreate path so user-tuned scale mode,
bar spacing, navigation toggles, and session filter don't reset on
every interval switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request integrations Issues related to library integrations.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants