Removed the 3-line block in parseWindowPath that redirected #/org/site/path/index → #/org/site/path:
if (location.hash.endsWith('/index')) {
const clean = location.hash.slice(0, -5);
history.replaceState(null, '', clean);
}Reasoning: parseWindowPath is shared by both browse and canvas. In canvas (da-live), this silently redirected hash URLs before the editor could read the path, breaking direct links to index files (e.g. /canvas#/org/site/path/index). The stripping was introduced by Claude in commit 9626865e with no explanation — likely a browse UX convention (index ≡ directory) applied incorrectly to a shared parser. Removed from nx2 only; nx is left unchanged as it's a separate code path.
Resolved origin/main ↔ branch conflict in nx/public/plugins/quick-edit/quick-edit.js: kept a single handleReady, retained branch checkDomain + parent-controller flow, removed duplicate checkDomain() invocation left from the merge.
Moved nx2/blocks/chat/ and nx2/blocks/tool-panel/ from da-nx into da-live as blocks/ew-chat/ and blocks/ew-tool-panel/, following the same procedure as canvas/inventory.
What landed in da-live ew:
blocks/ew-chat/— full chat block with sub-components (pills,prompts,welcome), controller, persistence, renderers, utilsblocks/ew-tool-panel/— tool panel (picker, fullsize-dialog, header actions)deps/mdast/— copied from da-nx; used byrenderers.jsfor markdown rendering
Custom element renames:
nx-chat→ew-chatnx-tool-panel→ew-tool-panel- Internal sub-elements (
nx-chat-welcome,nx-chat-pills,nx-prompts) kept as-is
Import adaptations:
../../utils/utils.js→../shared/nxutils.js(loadStyle, hashChange, getNx, DA_ADMIN)../../utils/api.jsdaFetch →../shared/utils.jsdaFetch (positional signature); api.js call site updated../../utils/ims.jsloadIms →../shared/utils.jsinitIms (aliased as loadIms)../shared/menu/menu.js(static) →await import(\${getNx()}/blocks/shared/menu/menu.js`)` (top-level dynamic; menu stays in shell)../../shared/picker/picker.js(static) →await import(\${getNx()}/blocks/shared/picker/picker.js`)` in prompts.js and tool-panel.js
Icon migration applied (per feedback_icon_migration.md):
- Removed
loadHrefSvg/ICONS_BASE/loadChatIconsfrom all files - chat.js:
ICON_SRCSmap with/img/icons/s2-icon-*-20-n.svgURLs;icon()returns<img>TemplateResult - tool-panel.js: close icon now
<img src="/img/icons/s2-icon-splitright-20-n.svg"> - CSS:
svgselectors →img; removedpath { fill: ... }rules;/nx2/img/icons/→/img/icons/(lowercase kebab); addedfilter: invert(1)on.action-btn imgfor dark-background buttons
canvas.js + inventory.js updated:
- Dynamic imports now point to local
../ew-chat/chat.jsand../ew-tool-panel/tool-panel.js document.createElement('nx-chat/nx-tool-panel')→ew-chat/ew-tool-panelquerySelector('nx-tool-panel')selectors updated toew-tool-panel- Removed
getNxfrom canvas.js imports (no longer needed there)
nx-panel-library.js: OOTB block library / templates / icons / placeholders UI (fetch, insert, preview, sprites); sharesnx-panel-extensions.csswith the iframe host.nx-panel-extensions.js:nx-panel-extensiononly choosesnx-panel-libraryvs BYOiframe+iframe-protocol.
helpers.js:getCanvasToolPanelViews— Editor placeholder tab (editor-coming-soon), Library = OOTB plugins +aem-assets(sortedblocks→aem-assets→icons→templates→placeholders), Extensions = other configured plugins.tool-panel.js/.css: Picker items built withnx-pickersectionheadings; initial tab isviews[0]; prune_loaded/ clear content whenviewsempty or ids change. Placeholder host class.nx-tool-panel-editor-placeholder.canvas.js: loadsgetCanvasToolPanelViewsinstead ofgetExtensionViews.
nx2/utils/daConfig.js:getFirstSheet,fetchDaConfigs(moved fromnx-panel-extensions/config.js). Canvashelpers.js/aem-assets.jsimport from utils; branchrefstays local tohelpers.js.
nx-panel-extensions.js/.css: Add / Preview use the same/blocks/edit/img/SVGs and<use href="#S2_Icon_Experience_Add">/#S2_Icon_ExperiencePreviewpattern as da.liveda-library(via sharedinlinesvgpreload). Source SVGs live in.ext-svg-sprites(visually hidden) so they are not laid out in the panel body.
nx-panel-extensions.js/.css: variant rows no longer embedv.domin the Lit tree (avoids cloning / ownership issues). Insert still usesvariant.domvia_insertBlock.
aem-assets.js: passonClosethrough toPureJSSelectors.renderAssetSelector(same hook as da.liveda-assets.js).nx-panel-extensions.js:onClosedispatchesnx-panel-closesopanel.jshides the right aside.
helpers.js:extensionToPanelViewpasses throughexperienceandsourcesfrom the extension config (no separate URL / modal flags).aem-assets.js:getAssetsPluginusesexperience: 'fullsize-dialog'(wasaem-assets).picker.js/.css:experience === 'window'+sources[0]→ new tab;fullsize-dialog→nx-picker-experience-dialog(nochange); open-in icon for those rows.tool-panel.js/.css: same rules in_activate/showView;_fullsizeDialogViewIddrives.tool-panel-fullsize-dialog; body mountsawait view.load().@nx-panel-closeondialogstops propagation and closes the dialog (not the whole panel).nx-panel-extensions.js:fullsize-dialog+aem-assetsrenders the assets host div and runsrenderAssetsfromupdated; otherfullsize-dialogthird-party configs use the iframe path as today.nx-panel-extensions.js: no inline AEM Assets mount (modal-only).
chat-controller.js:_pageContextForAgent()shared bysendMessageandapproveToolCallso post-approval/chatresumes includepageContext(da-agent collab gate).
breadcrumb.js/breadcrumb.css: removedvariant(was onlylarge); typography and chevrons use the default M component tokens everywhere.nav.js: nav breadcrumb no longer setsvariant="large".
command-defs.js:nx-canvas-open-paneldetail includesviewId: 'blocks'so the after tool panel selects the Blocks extension when present.canvas.js:openCanvasPanelaccepts optionalpreferredViewIdfrom eventviewId; aftersyncToolPanelViews, waits forupdateCompletethen callsnx-tool-panelshowViewonly ifviewscontains that id.tool-panel.js: publicshowView(id)wraps_activatefor external callers.
nx2/blocks/shared/breadcrumb/:nx-breadcrumb— optional.baseUrl,.pathSegments; parent steps are plain<a href>(hash-only or resolved viaresolveBreadcrumbHref+ currentlocation.search).hashStateToPathSegments/pathSegmentsToCrumbsinutils.js. No custom events.nav.js/nav.css:decorateBreadcrumbs(fragment)— same idea asdecorateBrand: mutates the loaded fragment, returnsnullor{ baseUrl };loadNavsets_navBreadcrumbs(@state) and plain_breadcrumbBaseHref.HashController,brand-cluster,brand-areaon the brand<a>.browse.js: unchanged integration —nx-breadcrumbwith segments only (default / medium typography).
nx-canvas-header: third segmented control optionsplit(grid-compare icon,aria-label/title“Split view”);EDITOR_VIEWSincludessplit.canvas.js/canvas.css:normalizeCanvasEditorViewpersistssplit. Split layout, gutter DOM, drag/persist ratio, and split-only CSS live innx-editor-split/(nx-editor-split.js+nx-editor-split.css, adopted on import):nx-canvas-editor-mount--splitrow (WYSIWYG left, 2pxnx-canvas-split-gutter, doc right),--nx-canvas-split-ratio, pointer-drag 15–85% → sessionStorage (nx-canvas-split-ratio). Split-modenx-editor-wysiwyguses matchingflex-basis/width/min-widthso the preview column does not collapse before the iframe is ready.nx-editor-doc/nx-editor-wysiwyg: visibility treatssplitlike both single-pane modes (doc + preview visible when iframe port is ready).nx-editor-wysiwyg: hosthiddenonly when the canvas mode hides the preview entirely; while cookies / quick-edit port load,.nx-editor-wysiwyg-surfaceishiddenso the custom element still participates in split flex sizing without a layout jump.selection-toolbar.js: ProseMirror selection toolbar sync runs insplitas well ascontent.selection-toolbar.js/handlers.js: iframeselection-changemarks PM transactions with meta and plugin state (fromIframe); doc-basedsyncToolbar/ doc scroll positioning skip while the mirrored range came from WYSIWYG so split view does not draw the bar from doccoordsAtPos. Collapsed iframe selection dispatches a no-op tr to clear that origin.
canvas-actions.js:HashControllerand initial_busymoved to class fields so the custom constructor can be dropped;_sendIconis not a reactive property (set once infirstUpdated+requestUpdate()); dropped redundantrequestUpdate()after_busy/_errorchanges (Lit@stateassignments schedule updates).
prose.js: removed customhandleUndo/handleRedothat duplicatedyUndo/yRedofrom y-prosemirror (same pattern asnx-editor-wysiwyg/utils/handlers.jsand da.live’s underlying commands).
prose.js: movedkeymap(baseKeymap)to afterbuildKeymap+handleTableBackspace(andcodemarkafterbaseKeymap), matchingda-live/blocks/edit/prose/index.js, so full-table delete with Backspace and Enter in lists behave like da.live.
- Added
nx2/blocks/canvas/nx-editor-doc/prose-plugins/:codemark,columnResizing(fromda-y-wrapper),imageDrop,imageFocalPoint,tableSelectHandle,sectionPasteHandler,base64Uploader, plussourceUploadContext,tableUtils,inlinesvg,focalPointDialog(native<dialog>; no face-api). - Wired plugins in
prose.jsfor writable sessions; styles innx-editor-doc.css. Upload paths derive from the editorsourceURL. Focal-point block metadata still loads fromhttps://da.live/.../da-library/helpers/.
selection-toolbar.js: exportsEDITOR_TEXT_FORMAT_ITEMSand prose helpers (applyHeadingLevel,wrapInBlockquote,setCodeBlock,setParagraph, list wraps) for slash menu; block-type picker fromBLOCK_TYPE_PICKER_DEFS;STRUCTURE_COMMANDS(isActive+run);markIsActiveInSelection; structure buttons from a toolbar subset ofEDITOR_TEXT_FORMAT_ITEMS.slash-menu-items.js/slash-menu-handlers.js: import shared catalog/helpers fromselection-toolbar.js(slash-only rows stay in items).
Created AGENTS.md to capture conventions not derivable from the code. Key entries:
undefinedvs empty array for loading state detectionsomethingUrl(URL object) vshref(string) naming convention- Avoid attaching custom properties to
window(built-in browser APIs are fine) - Error return shape (
{ error }vs{ json }) - Lazy loading with
firstUpdated+ null check pattern - IIFE memoization pattern
- Functional style with companion utils
Decided to wrap nav and sidenav in semantic HTML elements:
<header>wraps<nx-nav><nav>wraps<nx-sidenav>— givesnavigationlandmark for free- header and nav are siblings in the DOM
- Skipping
aria-labelon<nav>unless multiple nav landmarks are needed
- Added Adobe Spectrum design language section — Nexter uses Spectrum design but not Spectrum libraries. Reference sites: express.adobe.com, experience.adobe.com.
- Added light/dark mode as a hard requirement with
light-dark()CSS tip. - Expanded lazy loading strategies: DOM-first hydrate-later, event-driven loading.
- Added iframe/customer code isolation convention (
setIntervalpolling oversetTimeout). - Renamed "sidecar" utils to "companion" utils.
- Added
CLAUDE.mdinstruction to read AGENTS.md for conventions. - Added worklog trimming rule: delete git-recoverable info, condense completed work, keep open questions and key decisions.
- Added "Context" section linking to AGENTS.md and WORKLOG.md with descriptions.
- Added
panel.js: Litnx-panel(shadow shell, default slot, resize handle in shadow),createPanel/showPanel({ width, beforeMain }),setPanelsGridfor app-frame column/area CSS vars. Shell isaside.panelwithdata-positionbefore/after main;createPanel/showPanelreturn thenx-panelelement. Emptyasideafter removingnx-panelis dropped indisconnectedCallback. decorate(block): if the block has an anchor →loadFragment(a.href)→createPanel, move fragment children ontonx-panelwith DOM APIs, remove the block.- Styling split:
styles.csskeeps app-frame grid (--app-frame-*,body.app-framerow);panel.cssholds panel surface and resize affordance. - Mobile-first: default
body.app-frameuses fixed panel insets +:has(aside.panel)::beforescrim;@media (width >= 600px)restores grid layout and clears modal positioning.setPanelsGridalways sets--app-frame-*(only applied at 600px+).
- Replaced stub
DA_ORIGIN/daFetchexports with real environment-aware origins for DA services (admin, collab, content, preview, etc.). getEnv(key, envs)resolves origin per service: checks query param → localStorage → default (stage for dev/stage, prod for prod).- Removed
HashControllerreactive controller; sidenav no longer uses it. parseWindowPathnow returnsnullfor missing/invalid hashes and strips trailing/indexfrom hash.
daFetchhandles auth token injection, checks URL againstALLOWED_TOKENorigins before attaching bearer.ping,source,list,signout— thin wrappers for DA/AEM endpoints.- Profile block now imports
signoutfrom api.js instead of inlining the fetch.
- Spectrum Edge and app-frame layouts no longer rely on JS adding classes (
spectrum-edge,app-frame). - Replaced with
html:has(meta[content="edge-delivery"])andhtml:has(meta[content="app-frame"])— pure CSS, no JS decoration needed. - Removed
spectrum-edgeclass addition fromdecorateDocin nx.js. - App-frame grid extracted to its own top-level rule block.
- Color scheme toggle simplified: remove both classes, add the toggled one. No intermediate object.
- Added to JS conventions section. Core idea: push validation to the boundary where data enters, return
nullor a well-formed result — no ambiguous middle ground. Downstream code trusts the shape without re-checking. - Codifies the distinct meaning of
null(absent),undefined(not yet loaded), and''(explicitly cleared). parseWindowPathis the canonical example: returns a clean{ view, org, site, path }ornull.
- Replaced flat exports (
getSource,putSource, etc.) with namespaced objects:source,versions,config,org,status,aem(combined preview + live),log,snapshot,jobs. Low-level primitives (daFetch,isHlx6,signout,hlx6ToDaList) stay top-level. - Two private URL builders:
getDaApiPathfor DA ↔ AEM endpoints (source/list/config/versions),getAemApiPathfor AEM-only endpoints. AEM-only legacy fallback hitsHLX_ADMINwith hardcodedref=main. - Bulk endpoints inlined:
status.get,aem.preview/unPreview/publish/unPublish,snapshot.addPath/removePathacceptdaPathas string or array. Array of length ≥ 2 dispatches to/*with JSON body{ paths, delete? }. - hlx6-only methods (
source.copy/move,versions.get,org.listSites,config.getAggregated,jobs.test) return{ error, status: 501 }on legacy. - IMS import refactored from doubly-dynamic IIFE to relative
import { loadIms, handleSignIn } from './ims.js';— same production behavior, no top-level await, lets the wtr importmap mock cleanly. - Snapshots: new API uses plural
/snapshots/{path}, legacy uses singular/snapshot/{org}/{site}/main{path}— handled ingetAemApiPath. Same singular/plural switch forjobs/job. - Migrated
nx/blocks/importer/index.jsfromputSourcetosource.put. - New tests at
test/nx2/utils/api.test.js(68 tests) covering daFetch, isHlx6, every namespace method, bulk dispatcher, hlx6-only short-circuits, hlx6ToDaList, signout. Added/nx2/utils/ims.js→/nx2/test/mocks/ims.jsto the top-levelweb-test-runner.config.mjsimportmap.
code,cache,index,sitemap,media,discovernamespaces — explicitly skipped.- Login/logout/profile, config sub-namespaces (users/secrets/apikeys/tokens), nested config, profile config, org profiles — DA uses IMS, none of these are needed in the DA flow.
versions.getlegacy — DA's versionsource get-by-id pattern isn't documented and existing repo usage only has POST-to-create. Marked hlx6-only with 501 on legacy.
- Canvas chat/tool panels get the same split-left / split-right control as
nx-canvas-header, placed top-right inside.panel-body; the header copy is hidden while that side's panel is visible.restorePanelsstill firesnx-panels-restoredso restored panels get the bar.
toggleCanvasPaneland fragment URLs live inblocks/canvas/canvas.js;nx-canvas-headerdispatchesnx-canvas-toggle-panel(detail.position:before|after, aligned withaside.panel[data-position]) and the decorate step listens on the host.
canvas.jsnow callsloadStyle(import.meta.url)and adopts the sheet ondocumentonce (deduped), matching nx's automatic block CSS for light-DOM rules (e.g..fragment-content).
nx2/utils/daFetch.js:DA_ORIGIN,COLLAB_ORIGIN,CON_ORIGIN,AEM_ORIGINwith?da-admin=/ localStorage overrides (aligned with da-live);daFetchattaches bearer for allowlisted admin/content/AEM URLs.utils.jsre-exportsDA_ORIGINanddaFetch; profile imports fromdaFetch.js.- Deps:
da-y-wrapper+da-parserdist copied from da-live intonx2/deps/…;head.htmlimportmap;npm run nx2:copy:editor-deps(nx2/scripts/copy-editor-deps.mjs, optionalDA_LIVE_ROOT). - Superseded 2026-04-09 — see nx-editor-doc / nx-editor-wysiwyg below (renamed from
nx-doc-editor/nx-wysiwyg-frame;prose.js+extraPlugins; quick-edit + preview utils under wysiwyg).
- Superseded 2026-04-09 — structure was
nx-doc-editor+nx-wysiwyg-frame; see next section.
selection-toolbar.js: “Change into” picker includes Code block (setBlockType(code_block)); new Inline code toggle uses the schemacodemark (toggleMarkOnSelection). Toolbar order: block-type picker, then mark buttons, then structure actions (separators between groups).canvas.css: monospace styling for the inline-code toolbar button.
canvas.js:nx-canvas-editor-activeon the mount root replaces directhiddentoggling onnx-editor-doc/nx-editor-wysiwyg; each editor listens onparentElementand updates its own visibility (wysiwyg still gates ondata-nx-wysiwyg-port-ready).nx-editor-wysiwyg: close unused parent-sideMessageChannelports before each init retry and on disconnect; keep the port handed tonx-editor-docopen.nx-editor-doc:port.close()when clearing the quick-edit controller port.
- Hash /
ctx.pathisorg/site/...with no.htmlsuffix;buildSourceUrlno longer appends.html**. Quick-edit pathname / iframe URL / controller pathname use the path segments as-is (removed.replace(/\.html$/i));image.jsgetPageNameno longer strips.html.
nx2/blocks/canvas/nx-editor-doc/:nx-editor-docLit element + CSS;prose.js— Yjs + ProseMirror init only,extraPluginsfor injected plugins;utils/source.js(source URL, HEAD permissions);utils/collab.js(awareness color + identity).nx2/blocks/canvas/nx-editor-wysiwyg/:nx-editor-wysiwygLit iframe + cookie + MessageChannel;quick-edit-controller.js(MessagePort → ProseMirror).nx2/blocks/canvas/editor-utils/(2026-04-14): shared editor plumbing —preview.js,document.js,state.js;prose-diff.js(createTrackingPlugin, doc diff helpers for ProseMirror → iframe sync; wired fromnx-editor-doc.jsintoinitProse).canvas.js/canvas.css: lazy-importnx-editor-doc+nx-editor-wysiwyg;nx-editor-doclistens onparentElementfornx-wysiwyg-port-readyand setsquickEditPort.
- When either side panel is visible (
aside.panel:not([hidden])),.default-contentinsidemainnow usesmax-width: 83.4%instead of the fixed--se-grid-container-widthvalue. - Uses sibling selectors:
main:has(~ aside.panel:not([hidden]))for panels after main,aside.panel:not([hidden]) ~ mainfor panels before main. - The fixed
1200pxmedia query (@media (width >= 1440px)) remains for the no-panel case.
replaceHtmlwas interpolating${value}directly into the<div class="da-metadata">rows.getElementMetadatareturns values as{ content, text }objects, so any caller that round-tripped existing metadata (rolloutCopy,mergeCopy) wrote[object Object]into the saved HTML.- Fix unwraps
value.textwhen present, falls back to the raw value, and emits''for nullish — so the function handles both shapes (object fromgetElementMetadata, plain string fromdaMetadata['diff-label-local'] = labelLocal). - Kept
getElementMetadata's{ content, text }shape sinceregional-diffcallers use.content(the DOM element) directly for diffing.
- Approval popover: persistent
nx-popover(addedpersistentflag to skip light-dismiss) positioned above the chat form viagetBoundingClientRect()on the host element. Auto-shows/closes inupdated()whentoolCardschanges. - Approval card (
renderApprovalCardinrenderers.js): tool name, summary line, three action buttons (Reject/Always approve/Approve) with<kbd>shortcut hints. - Approval summary priority:
humanReadableSummary→sourcePath→destinationPath→path→skillId→name.contentexcluded. Field names extracted toTOOL_INPUTinconstants.js(same TODO asAGENT_EVENT). - Auto-approve: if tool is in
_autoApprovedTools, card goes straight toapprovedstate — skipsapproval-requestedentirely to avoid flash. - "Always approve" is conversation-scoped — resets on
clear()only, not per message. - Conversation history keyed by
org--site--userId— site-scoped, not path-scoped. - Agent stream contract and persistence model documented in
docs/chat-ui-component.md.