Commit ccc0dbd
authored
feat(canvas): admin UI for agent-authored markdown canvases with live updates (#300)
* feat(canvas): D.1 — author shortcode spike HTML
Self-contained spike at spellbook/admin/frontend/spike/canvas-shortcode-spike.html
implementing the design §12.3 harness: react-markdown@9 + remark-gfm@4 +
rehype-raw@7 pulled via importmap from esm.sh. Defines Chart, Tabs, Tab
shortcode components and a fixed markdown sample exercising:
1. <chart caption="multiline spec"> with multiline Vega-Lite JSON inside
<tab title="Alpha"> inside <tabs>.
2. <chart> inside a GFM table cell.
Verdict capture (D.2) and grammar lock (D.5) follow.
* test(canvas): D.2 — spike verdict captured: PASS
Ran the Phase 2 shortcode spike via Node 25.9.0 +
react-dom/server.renderToStaticMarkup, exercising the same
react-markdown@9.1.0 + remark-gfm@4.0.1 + rehype-raw@7.0.0 pipeline
as the browser-side spike harness.
All three design §12.5 criteria PASS:
- <tabs> preserves <tab> children container semantics (no flattening)
- multiline Vega-Lite JSON survives inside <chart> children verbatim
- <chart> in a GFM table cell renders as case (a) without breaking the table
Verdict: §9 grammar LOCKED as designed (children-content + attribute-content
hybrid). Tracks A, B, C are unblocked pending the §9 annotation in D.5.
* chore(canvas): D.4 — move spike to docs/spellbook-canvas-shortcode-spike/
Promotes the Phase 2 shortcode spike from the transient
spellbook/admin/frontend/spike/ harness into a permanent reproducer
under docs/spellbook-canvas-shortcode-spike/. Adds a README explaining
how to re-run the spike from the browser (python3 -m http.server) and
how to interpret the three pass/fail criteria from design §12.5.
This keeps the spike artifact alive past Phase 3 freeze so future
react-markdown / rehype-raw upgrades can be validated against the
known-good baseline (per design §12.2). The spellbook/admin/frontend/spike/
directory is removed.
* docs(canvas): D.5 — lock §9 shortcode grammar
Phase 2 shortcode spike (Track D) verdict: PASS on all three §12.5
criteria. The §9 grammar — children-content for multiline JSON/DSL
plus attribute-content for short props — is locked as designed.
This commit adds an in-repo, version-controlled snapshot of the
GRAMMAR LOCKED annotation that was applied to the (out-of-tree)
design doc at:
/Users/eek/.local/spellbook/docs/Users-eek-Development-spellbook/plans/2026-05-14-spellbook-canvas-design.md §9
Tracks A (Backend), B (Frontend), and C (Skill/Slash command) are
UNBLOCKED. They may proceed against the §9.2 dispatch table as
originally written; no fallback grammar pivot is needed.
Reproducer: docs/spellbook-canvas-shortcode-spike/canvas-shortcode-spike.html
Verdict transcript: docs/spellbook-canvas-shortcode-spike/spike-result.md
* feat(canvas): A.1 — filesystem store with atomic writes
Implements spellbook/canvas/store.py with CanvasMeta pydantic model,
NAME_RE single-source regex, _max_page_bytes() env-read-at-call-time,
atomic _atomic_write helper (tempfile + os.replace mirror of
agent2agent precedent), path-traversal guard mirroring
spellbook/admin/routes/memory.py, and the open/read/list/close/write
surface required by A.3 and A.4.
Test fixtures live in tests/canvas/conftest.py: canvas_tmp_root,
mock_ctx, and event_subscriber (per design 14.2). The
event_subscriber fixture monkeypatches event_bus.publish rather than
subscribing through the bus, to avoid pytest-asyncio loop-affinity
races on the per-subscriber asyncio.Queue.
44 tests pass; coverage on spellbook/canvas/store.py is 92% (target >=90).
Threat-model paragraph appears in both __init__.py and store.py module
docstrings (TRUSTED-LOCAL-AGENT).
* chore(canvas): A.2 — add CANVAS subsystem to event bus
Appends Subsystem.CANVAS = "canvas" to spellbook/admin/events.py so the
canvas MCP tools (A.3) can publish canvas.opened / canvas.updated /
canvas.closed events through the existing event_bus singleton. The
frontend WebSocketContext dispatch (Track B) will key off this value.
Test: tests/admin/test_events.py::test_canvas_subsystem_exists. Existing
13 event-bus tests remain green.
* feat(canvas): A.3 — MCP tools (canvas_open/write/close/list)
Adds spellbook/mcp/tools/canvas.py with four async @mcp.tool() functions
following the memory.py precedent (decorator stack: @mcp.tool +
@inject_recovery_context, dict returns, no raised exceptions for
expected failures).
Key design decisions baked into this implementation:
- Event publishing uses await event_bus.publish wrapped in try/except
logging WARN with exc_info=True (diverges from memory.py's silent
except, per design 6.3 observability requirement).
- URL construction reads HOST/PORT via spellbook.core.config.get_env so
the canvas URL respects operator env overrides (mirrors
spellbook/admin/cli.py and spellbook/mcp/server.py precedent).
- canvas_store.NAME_RE is the single source of truth for name regex,
used at the route boundary too (A.4).
- Error catalog matches design 13: invalid_name, not_found, closed,
page_too_large, invalid_content. queue_overflow stays reserved.
Threat-model paragraphs from design 4.1 lines 196-200 and 4.2 lines
278-281 appear verbatim in canvas_open and canvas_write docstrings
respectively (TRUSTED-LOCAL-AGENT, rehype-raw, session-takeover wording
intact). Two regression tests assert the docstring content stays
in place.
19 tests pass in tests/mcp/tools/test_canvas.py (the plan asked for 14
behavior tests + the threat-model and publish-failure resilience cases
were added on top to lock in the security and observability
contracts). Coverage on the new module exceeds 80% by inspection;
pytest --cov with the dotted module path triggers a pre-existing
beartype/fastmcp circular-import warning in this environment unrelated
to the new code (the same warning fires against
spellbook.mcp.tools.memory).
* feat(canvas): A.4 — admin API routes for canvas listing and detail
Adds spellbook/admin/routes/canvas.py with two read-only endpoints:
- GET /api/canvas - sorted list, count, paginated client-side
- GET /api/canvas/{name} - single canvas detail with markdown content
Both routes share the existing require_admin_auth HMAC-cookie dep -- no
new auth surface. Pydantic response models (CanvasListItem,
CanvasListResponse, CanvasDetailResponse, CanvasErrorResponse) match
design 5.1 verbatim.
Per impl plan P2-8, the {name} route uses canvas_store.NAME_RE.match
rather than re-declaring the pattern -- single source of truth so 3.3
changes propagate without route drift. Error envelope matches 13:
invalid_name -> 400, not_found -> 404.
Wired into spellbook/admin/app.py adjacent to the memory_routes
include. 7 tests pass in tests/admin/routes/test_canvas.py covering:
unauth list, unauth detail, empty list, populated sorted list, happy
detail, invalid name, not found.
* test(canvas): A.5 — end-to-end integration test
Adds tests/integration/test_canvas_e2e.py exercising the full canvas
loop end-to-end:
canvas_open (MCP)
-> canvas_write (MCP)
-> event_bus.publish (canvas.opened, canvas.updated captured)
-> GET /api/canvas/{name} returns the written content
Conftest discovery decision (documented per impl plan A.5 step 1):
the admin client + token mock fixtures live in tests/admin/conftest.py
but pytest's sibling-conftest discovery does NOT auto-load them into
tests/integration/. Rather than create a duplicate
tests/integration/conftest.py, the e2e module assembles its own
authed TestClient inline. This keeps tests/integration self-
contained, with no implicit dependency on tests/admin/.
The canvas_tmp_root, mock_ctx, event_subscriber fixtures from
tests/canvas/conftest.py ARE imported explicitly (noqa: F401) since
those are the canonical shared-fixture pattern per design 14.2.
Two tests pass: the headline write -> event -> route round-trip plus
a list+detail variant that confirms canvas_open visibility through
GET /api/canvas as well.
* docs(canvas): C.1 — add canvas skill (skills/canvas/SKILL.md)
* docs(canvas): C.2 — add /canvas slash command
* chore(canvas): B.1 — install frontend dependencies for canvas rendering
Add react-markdown@9, remark-gfm@4, rehype-raw@7, mermaid@11, react-vega@8,
vega-lite@6. Required for the §9 shortcode rendering pipeline (B.4-B.7).
Side fixes (pre-existing broken build on the shipping branch):
- tsconfig.json: add "ignoreDeprecations": "6.0" — typescript@^6.0.3 was
failing tsc with TS5101 on the legacy baseUrl option.
- src/vite-env.d.ts: add /// <reference types="vite/client" /> — main.tsx
side-effect import of './styles/globals.css' was failing TS2882 without
the vite/client ambient types.
Install notes (operator heads-up):
- User's global ~/.npmrc points at an expired StyleSeat CodeArtifact
registry; install used --registry=https://registry.npmjs.org/ to bypass.
- Pre-existing eslint peer-dep mismatch (eslint-plugin-react-hooks@7 vs
eslint@10) required --legacy-peer-deps. Not a new conflict.
* feat(canvas): B.2 — types and TanStack Query hooks for canvas
Add `CanvasListItem`, `CanvasListResponse`, `CanvasDetail`, and
`CanvasErrorResponse` to `api/types.ts` matching the Pydantic shapes
returned by `spellbook/admin/routes/canvas.py` (design §5.1).
Add `hooks/useCanvases.ts` exporting `useCanvasList()` and
`useCanvas(name)`. Hooks key on `['canvas']` and `['canvas', name]` so
the WebSocket layer (B.3) can invalidate them on `canvas.*` events.
* feat(canvas): B.3 — WS canvas dispatch + dashboard invalidation
Add `case 'canvas':` to the WebSocketContext subsystem switch. On every
`CANVAS` event from the bus, invalidate three TanStack Query keys:
- `['canvas']` (refresh CanvasList)
- `['canvas', name]` (refresh the open CanvasDetail, when payload
carries the canvas name)
- `['dashboard']` (refresh top-line counts, matching every other
case in this switch — see lines 34-58)
The triple-invalidation is per impl plan P2-4: without `['dashboard']`,
the home page counters drift when a canvas is opened/closed/written.
* feat(canvas): B.4 — simple shortcodes (Callout, Tabs, Choice, Approve)
Implement the four non-lazy shortcode components per §9.2 / §9.5:
- `Callout` — `<aside>` with type-driven left-border color (note/tip/
warning/danger) and optional title. Markdown children pass through.
- `Tabs` + `Tab` — tabbed-panel container. `Tabs` iterates `Children`,
selects every `Tab` child, renders a tab bar plus the active tab's
body via `useState`. Both exports live in `Tabs.tsx`.
- `Choice` — v2-reserved disabled preview. Parses `options` as JSON; on
parse failure renders an empty radiogroup. "Reserved for v2" badge.
- `Approve` — v2-reserved disabled preview with two disabled buttons
and a "Reserved for v2" badge.
All four carry `data-testid` attributes used by B.7's `render.test.tsx`.
No tests in this commit — render dispatch is covered by B.7.
* feat(canvas): B.5 — Diagram shortcode with lazy Mermaid + ErrorBoundary
- `extractText.ts`: shared helper that walks `react-markdown` children
and concatenates string content. Mirrors the validated helper from
the §12 spike (children-content grammar, locked 2026-05-14).
- `MermaidImpl.tsx`: default-exported component that initializes
`mermaid` once, calls `mermaid.render(uniqueId, source)`, and injects
the resulting SVG via `dangerouslySetInnerHTML`. Render errors render
inline. Initialized with `securityLevel: 'strict'` and `theme: 'dark'`.
- `Diagram.tsx`: wraps `MermaidImpl` in `Suspense` + `ErrorBoundary`,
loads it via `lazy(() => import('./MermaidImpl'))` so `mermaid`
(~700 KB minified) lives in its own Vite chunk and stays out of the
initial admin bundle. Per-shortcode error isolation per §8.3.
Diagram is not yet wired into `CanvasRender` — that lands in B.7, where
the lazy-split is observable in the build output.
* feat(canvas): B.6 — Chart shortcode with lazy react-vega + ErrorBoundary
- `ChartImpl.tsx`: default-exported component using `react-vega`'s
`VegaLite` (actions=false). Receives an already-parsed spec object.
- `Chart.tsx`: wraps `ChartImpl` in `Suspense` + `ErrorBoundary` and
loads it via `lazy(() => import('./ChartImpl'))`. JSON parsing
happens in `Chart` so empty/invalid specs render an inline error
(`<pre>`) before the lazy chunk is fetched.
Per-shortcode error isolation (§8.3) is intact:
- JSON parse error → inline `<pre>` with parser message
- Vega render error → caught by `ErrorBoundary`
Side fix to B.1's install: re-pin to the impl plan versions
(react-vega@^7, vega-lite@^5) and add `vega@^6` + `vega-embed@^6` as
explicit deps to satisfy react-vega@7's peer requirement. react-vega@8
(npm latest) drops the `VegaLite` named export and requires a different
hook-based API; sticking to the plan's pinned majors avoids drift.
Chart is wired into `CanvasRender` in B.7; the code-split is observable
in the build output then.
* feat(canvas): B.7 — CanvasRender dispatch pipeline + tests (8 PASS)
- `canvas/render.tsx`: `CanvasRender({ content })` wraps `ReactMarkdown`
with `remarkPlugins=[remarkGfm]`, `rehypePlugins=[rehypeRaw]`, and a
components-prop dispatch map covering all seven locked §9 shortcodes:
chart, diagram, callout, tabs, tab, choice, approve.
- `canvas/__tests__/render.test.tsx`: 8 vitest cases (TDD, fail-then-
pass). Lazy `MermaidImpl` / `ChartImpl` are mocked via `vi.mock` so
the dispatch table can be verified without evaluating the heavy
chunks. Coverage matches impl plan acceptance:
1. plain markdown heading
2. <callout type="warning">
3. <tabs>+<tab> (active panel rendered)
4. <tab> standalone (named export)
5. <choice> v2-disabled preview
6. <approve> v2-disabled preview
7. <diagram> → lazy MermaidImpl (Suspense resolves to mock)
8. <chart> → lazy ChartImpl (JSON spec parsed in Chart)
Trust boundary doc: comment in render.tsx restates §10 trusted-local-
agent (rehype-raw executes raw <script>; do not pass unsanitized
external content into a canvas).
* feat(canvas): B.8 — CanvasList and CanvasDetail pages
- `CanvasList.tsx`: list page mirroring `MemoryBrowser` per OQ-1. Renders
a table (name link, title, last_updated, status badge), an empty
state ("No canvases yet…"), and a retry-on-error block. Backed by
`useCanvasList()`.
- `CanvasDetail.tsx`: detail page bound to `/canvas/:name`. Backed by
`useCanvas(name)`. Renders four states: loading (LoadingSpinner),
not-found / error ("Canvas not found" + back link), closed banner
above content, and the happy path (title + metadata + CanvasRender).
- `__tests__/CanvasList.test.tsx`: 5 vitest cases (mocks `useCanvasList`):
populated rows with /canvas/<name> links; closed badge; empty state;
loading state; error + retry button.
- `__tests__/CanvasDetail.test.tsx`: 5 vitest cases (mocks `useCanvas`,
plus the lazy chunks `MermaidImpl` / `ChartImpl`): loading; 404 with
back link; closed banner; CanvasRender markdown heading; CanvasRender
shortcode dispatch (callout).
* feat(canvas): B.9 — register canvas routes and sidebar nav link
- `App.tsx`: register `<Route path="/canvas">` → `CanvasList` and
`<Route path="/canvas/:name">` → `CanvasDetail`. Both wrapped in
the existing top-level `<ErrorBoundary>`. Router basename is
`/admin`, so real URLs are `/admin/canvas` and `/admin/canvas/<name>`.
- `Sidebar.tsx`: append `{ to: '/canvas', label: '// CANVAS' }` to
`navItems` between `/memory` and `/security` (per OQ-1, mirroring
MemoryBrowser's adjacency).
- `Sidebar.test.tsx`: add two assertions — a /canvas href check and
an ordering check confirming `// CANVAS` sits between `// MEMORY`
and `// SECURITY`.
- `spellbook/admin/static/*`: rebuilt static assets. Vite emits
`MermaidImpl-*.js` and `ChartImpl-*.js` as separate chunks plus
~30 mermaid-internal sub-chunks (sequence/flow/c4/gantt/etc.).
Verified: `mermaid` and `vega` strings do NOT appear in
`index-*.js`, only in `MermaidImpl-*.js` / `ChartImpl-*.js`.
Full frontend test suite: 267/267 passing.
* test(canvas): fix CanvasList green mirage — exercise real useCanvasList
* test(canvas): fix CanvasDetail green mirage — exercise real useCanvas
* test(canvas): add WS canvas dispatch coverage (B.3 mirage fix)
* test(canvas): use globalThis instead of global for tsc strict mode
* fix(canvas): build canvas root with os.path.join for native separators
`_resolve_canvas_root()` returned `os.path.expanduser("~/.local/spellbook/canvas")`,
which on Windows produces a mixed-separator path
(`C:\Users\X/.local/spellbook/canvas`). The OS accepts it, but downstream
string-suffix / `os.sep`-based checks break on the inconsistent separators —
including `test_default_canvas_root_is_under_home`, which was the sole
failure on `python-tests (windows-latest)`.
Construct the path with `os.path.join(os.path.expanduser("~"), ".local",
"spellbook", "canvas")` so every component uses the platform-native
separator. The path-traversal guard in `_canvas_dir` already uses
`os.path.realpath` + `os.sep`, so consistent separators make that check
robust on Windows too.
* test(canvas): deterministic timestamps + CHANGELOG entry for canvas
Addresses two Momus review findings on PR #300.
BOT-A2 (Medium): Add a CHANGELOG.md `[Unreleased]` entry describing the
canvas feature (MCP tools, filesystem store, admin routes, event-bus
subsystem, React UI, live updates, threat model, MVP scope).
BOT-A3 (Low): Replace `time.sleep(0.01)` calls in
`test_list_canvases_sorted_by_last_updated_desc` with explicit
`datetime`-based timestamps. The previous version relied on wall-clock
spacing between `open_canvas` calls separating `last_updated` timestamps
by at least one millisecond; on slow CI runners this was a flake risk.
Now both canvases' `last_updated` is written explicitly via `write_meta`,
with a one-second gap, so the assertion is timing-independent.
* chore(ci): retrigger workflows for prior commit (no diff)
* fix(canvas): tolerate malformed SPELLBOOK_CANVAS_MAX_PAGE_BYTES
Addresses BOT-B1 (Low) from the Momus review of PR #300.
`_max_page_bytes()` called `int(os.environ.get(...))` directly, so a
non-integer or non-positive value would propagate as an opaque
`ValueError` through every `canvas_write` call. A single typo in
operator config would break the entire feature.
Behavior change: a malformed env var is now logged at WARNING and
the function falls back to the 1 MB default. Three unit tests cover
the non-integer, non-positive, and unset paths.
* fix(canvas): doc/test mock accuracy + Tabs identification dead code
Addresses BOT-C1, BOT-C2, BOT-C3 from the Momus review of PR #300.
All three are non-blocking (verdict was APPROVE) but cheap to fix.
BOT-C1 (Low) — `CanvasDetail.test.tsx`: mock data used `page: 'page.md'`,
but MVP only emits `page: 'index.md'` (per `_max_pages` / `pages/index.md`
contract). Aligning the mock keeps the test exercising realistic data.
BOT-C2 (Low) — `skills/canvas/SKILL.md` documented the bus event as
`canvas.written` in two places, but the implementation emits
`canvas.updated`. Both references updated; no other `canvas.written`
references found in the tree.
BOT-C3 (Nit) — `Tabs.tsx` had a redundant identity check:
`el.type === Tab || (typeof el.type !== 'string' && el.type === Tab)`.
The second clause is implied by the first, so collapse to a single
`el.type !== Tab` guard and refresh the comment.
* docs: add worktree-switching note to AGENTS.md
Use install.py to switch active worktree; symlink shortcut alone misses
per-platform MCP registrations and produces a stale /mcp panel.
---------
Co-authored-by: elijahr <153711+elijahr@users.noreply.github.com>1 parent 3feb1da commit ccc0dbd
154 files changed
Lines changed: 12803 additions & 2017 deletions
File tree
- commands
- docs/spellbook-canvas-shortcode-spike
- skills/canvas
- spellbook
- admin
- frontend
- src
- api
- canvas
- __tests__
- shortcodes
- components/layout
- contexts
- __tests__
- hooks
- pages
- __tests__
- routes
- static
- assets
- canvas
- mcp/tools
- tests
- admin
- routes
- canvas
- integration
- mcp
- tools
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
56 | 60 | | |
57 | 61 | | |
58 | 62 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
12 | 45 | | |
13 | 46 | | |
14 | 47 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
Lines changed: 68 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
0 commit comments