From 9b9c5538804db669f6da60f8e2761e4879e42074 Mon Sep 17 00:00:00 2001 From: svinod Date: Fri, 29 May 2026 15:29:57 +0200 Subject: [PATCH 1/5] fix: markdown rendering for custom types --- docs/chat-ui-component.md | 2 +- nx2/blocks/chat/chat.css | 99 ++++++++++++++++++++++++ nx2/blocks/chat/renderers.js | 94 +++++++++++++++++++++- nx2/img/icons/S2_Icon_Checkmark_20_N.svg | 5 ++ test/nx2/blocks/chat/renderers.test.js | 79 +++++++++++++++++++ 5 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 nx2/img/icons/S2_Icon_Checkmark_20_N.svg create mode 100644 test/nx2/blocks/chat/renderers.test.js diff --git a/docs/chat-ui-component.md b/docs/chat-ui-component.md index 92cf3a2e6..7f93ac82c 100644 --- a/docs/chat-ui-component.md +++ b/docs/chat-ui-component.md @@ -183,7 +183,7 @@ Each conversation has a `sessionId` (UUID) stored in IndexedDB alongside its mes | Event | Bubbles | Detail | Description | |---|---|---|---| -| `nx-agent-change` | Yes | — | The agent completed a tool action. Host views can listen to react (e.g. reload the document). Fired once per successful tool-result. | +| `nx-agent-change` | Yes | `{ scope: 'file' \| 'document', paths: string[] }` | The agent completed a tool action that changed content. `scope: 'file'` means the file tree changed (files created, deleted, moved, or copied); `scope: 'document'` means a document's content was modified. `paths` contains the affected parent folder paths. | ## Skills slash menu diff --git a/nx2/blocks/chat/chat.css b/nx2/blocks/chat/chat.css index 8926513fa..dc721a52c 100644 --- a/nx2/blocks/chat/chat.css +++ b/nx2/blocks/chat/chat.css @@ -142,6 +142,7 @@ height: 100%; } +.directive-toggle-list summary::before, .tool-card summary::before, .message .selection-context > summary::before { content: ""; @@ -192,6 +193,104 @@ } } +.directive-checklist ul, +.directive-toggle-list { + list-style: none; + padding: 0; + margin: 0; +} + +.directive-toggle-list { + details { + summary { + cursor: pointer; + display: flex; + align-items: flex-start; + gap: var(--s2-spacing-75); + + &::-webkit-details-marker { + display: none; + } + + &::before { + margin-top: 6px; + } + } + + &[open] > summary::before { + transform: rotate(180deg); + } + + &:not(:has(> :not(summary))) > summary { + pointer-events: none; + + &::before { + display: none; + } + } + } +} + +.directive-checklist { + li { + display: flex; + align-items: flex-start; + gap: var(--s2-spacing-75); + } + + input[type="checkbox"] { + position: relative; + width: 14px; + height: 14px; + flex-shrink: 0; + margin-top: 4px; + appearance: none; + border: 2px solid var(--s2-gray-800); + border-radius: var(--s2-corner-radius-75); + background: var(--s2-gray-75); + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + + &:checked { + background: var(--s2-gray-800); + + &::after { + content: ""; + position: absolute; + inset: 0; + background-color: var(--s2-gray-25); + mask-image: url("/nx2/img/icons/S2_Icon_Checkmark_20_N.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + } + } + } +} + +.directive-alert-info, +.directive-alert-warning, +.directive-alert-error { + padding: 6px var(--s2-spacing-200); + border-radius: 12px; + margin: 12px 0; +} + +.directive-alert-info { + background-color: var(--s2-blue-200); +} + +.directive-alert-warning { + background-color: var(--s2-orange-200); +} + +.directive-alert-error { + background-color: var(--s2-red-200); +} + .message { padding: 6px var(--s2-spacing-200); border-radius: 12px; diff --git a/nx2/blocks/chat/renderers.js b/nx2/blocks/chat/renderers.js index 6517e9358..6526b821e 100644 --- a/nx2/blocks/chat/renderers.js +++ b/nx2/blocks/chat/renderers.js @@ -48,10 +48,98 @@ function renderNode(node) { const parser = unified().use(remarkParse); +function parseDirectives(text) { + const segments = []; + const buf = []; + let type = null; + let openLine = null; + + for (const line of text.split('\n')) { + if (!type && line.startsWith(':::')) { + const content = buf.splice(0).join('\n'); + if (content) segments.push({ kind: 'text', content }); + [type] = line.slice(3).split(' '); + openLine = line; + } else if (type && line.trimEnd() === ':::') { + segments.push({ kind: 'directive', type, content: buf.splice(0).join('\n') }); + type = null; + openLine = null; + } else { + buf.push(line); + } + } + + if (openLine) buf.unshift(openLine); + const tail = buf.join('\n'); + if (tail) segments.push({ kind: 'text', content: tail }); + return segments; +} + +function renderChecklistItem(node) { + const para = node.children[0]; + if (para?.type !== 'paragraph') return html`
  • ${node.children.map(renderNode)}
  • `; + const first = para.children[0]; + if (first?.type !== 'text') return html`
  • ${renderNode(para)}
  • `; + + const checked = first.value.startsWith('[x] ') || first.value.startsWith('[X] '); + const unchecked = first.value.startsWith('[ ] '); + if (!checked && !unchecked) return html`
  • ${renderNode(para)}
  • `; + + const inline = [ + { ...first, value: first.value.slice(4) }, + ...para.children.slice(1), + ].map(renderNode); + return html`
  • + ${inline} +
  • `; +} + +function renderToggleList(tree) { + const items = []; + let i = 0; + while (i < tree.children.length) { + const node = tree.children[i]; + if (node.type === 'blockquote') { + const summary = node.children.flatMap((c) => (c.children ?? []).map(renderNode)); + const body = []; + while (i + 1 < tree.children.length && tree.children[i + 1].type !== 'blockquote') { + i += 1; + body.push(renderNode(tree.children[i])); + } + items.push(html`
  • ${summary}${body}
  • `); + } else { + items.push(html`
  • ${renderNode(node)}
  • `); + } + i += 1; + } + return html``; +} + +function renderChecklist(tree) { + const inner = tree.children.map((n) => { + if (n.type !== 'list') return renderNode(n); + return n.ordered + ? html`
      ${n.children.map(renderChecklistItem)}
    ` + : html``; + }); + return html`
    ${inner}
    `; +} + +function renderDirective(type, content) { + const tree = parser.parse(content); + if (type === 'toggle-list') return renderToggleList(tree); + if (type === 'checklist') return renderChecklist(tree); + return html`
    ${renderNode(tree)}
    `; +} + function renderMessageContent(text) { if (!text) return nothing; - const tree = parser.parse(text); - return renderNode(tree); + const segments = parseDirectives(text); + return segments.map(({ kind, type, content }) => { + if (kind === 'directive') return renderDirective(type, content); + const tree = parser.parse(content); + return renderNode(tree); + }); } function approvalSummary(input) { @@ -160,4 +248,4 @@ function renderMessage(msg, toolCards) { `; } -export { renderMessage, renderApprovalCard }; +export { renderMessage, renderApprovalCard, parseDirectives }; diff --git a/nx2/img/icons/S2_Icon_Checkmark_20_N.svg b/nx2/img/icons/S2_Icon_Checkmark_20_N.svg new file mode 100644 index 000000000..187b80794 --- /dev/null +++ b/nx2/img/icons/S2_Icon_Checkmark_20_N.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/test/nx2/blocks/chat/renderers.test.js b/test/nx2/blocks/chat/renderers.test.js new file mode 100644 index 000000000..31c9ff963 --- /dev/null +++ b/test/nx2/blocks/chat/renderers.test.js @@ -0,0 +1,79 @@ +import { expect } from '@esm-bundle/chai'; +import { parseDirectives } from '../../../../nx2/blocks/chat/renderers.js'; + +describe('parseDirectives', () => { + describe('plain text', () => { + it('returns a single text segment for plain text', () => { + const result = parseDirectives('hello world'); + expect(result).to.deep.equal([{ kind: 'text', content: 'hello world' }]); + }); + + it('returns empty array for empty string', () => { + expect(parseDirectives('')).to.deep.equal([]); + }); + }); + + describe('single directive', () => { + it('parses a basic directive', () => { + const text = ':::checklist\n- [x] Done\n:::'; + expect(parseDirectives(text)).to.deep.equal([ + { kind: 'directive', type: 'checklist', content: '- [x] Done' }, + ]); + }); + + it('parses a hyphenated type', () => { + const text = ':::alert-error\nSomething failed.\n:::'; + expect(parseDirectives(text)).to.deep.equal([ + { kind: 'directive', type: 'alert-error', content: 'Something failed.' }, + ]); + }); + }); + + describe('mixed content', () => { + it('handles text before a directive', () => { + const text = 'Intro text.\n:::list\n- item\n:::'; + const result = parseDirectives(text); + expect(result).to.deep.equal([ + { kind: 'text', content: 'Intro text.' }, + { kind: 'directive', type: 'list', content: '- item' }, + ]); + }); + + it('handles text after a directive', () => { + const text = ':::list\n- item\n:::\nTrailing text.'; + const result = parseDirectives(text); + expect(result).to.deep.equal([ + { kind: 'directive', type: 'list', content: '- item' }, + { kind: 'text', content: 'Trailing text.' }, + ]); + }); + + it('handles multiple directives', () => { + const text = ':::list\n- a\n:::\n:::checklist\n- [x] b\n:::'; + const result = parseDirectives(text); + expect(result).to.deep.equal([ + { kind: 'directive', type: 'list', content: '- a' }, + { kind: 'directive', type: 'checklist', content: '- [x] b' }, + ]); + }); + }); + + describe('unclosed directive (streaming)', () => { + it('emits the opening line as text when directive is never closed', () => { + const text = ':::checklist\n- [x] partial'; + const result = parseDirectives(text); + expect(result).to.deep.equal([ + { kind: 'text', content: ':::checklist\n- [x] partial' }, + ]); + }); + + it('emits completed directives before an unclosed one', () => { + const text = ':::list\n- done\n:::\n:::checklist\n- partial'; + const result = parseDirectives(text); + expect(result).to.deep.equal([ + { kind: 'directive', type: 'list', content: '- done' }, + { kind: 'text', content: ':::checklist\n- partial' }, + ]); + }); + }); +}); From 90b6db880b824cb97136162a12314eaf8db7444e Mon Sep 17 00:00:00 2001 From: svinod Date: Fri, 29 May 2026 17:26:06 +0200 Subject: [PATCH 2/5] fix: review comments --- nx2/blocks/chat/chat-controller.js | 4 +-- nx2/blocks/chat/chat.css | 11 ++++--- nx2/blocks/chat/chat.js | 4 +-- nx2/blocks/chat/renderers.js | 30 ++---------------- nx2/blocks/chat/{ => utils}/api.js | 4 +-- nx2/blocks/chat/utils/parse.js | 31 +++++++++++++++++++ nx2/blocks/chat/{ => utils}/persistence.js | 0 nx2/blocks/chat/{utils.js => utils/stream.js} | 2 +- nx2/img/icons/S2_Icon_Checkmark_20_N.svg | 5 --- test/nx2/blocks/chat/renderers.test.js | 17 +++++++++- 10 files changed, 62 insertions(+), 46 deletions(-) rename nx2/blocks/chat/{ => utils}/api.js (85%) create mode 100644 nx2/blocks/chat/utils/parse.js rename nx2/blocks/chat/{ => utils}/persistence.js (100%) rename nx2/blocks/chat/{utils.js => utils/stream.js} (97%) delete mode 100644 nx2/img/icons/S2_Icon_Checkmark_20_N.svg diff --git a/nx2/blocks/chat/chat-controller.js b/nx2/blocks/chat/chat-controller.js index d5e200370..0daf62148 100644 --- a/nx2/blocks/chat/chat-controller.js +++ b/nx2/blocks/chat/chat-controller.js @@ -1,7 +1,7 @@ import { loadIms } from '../../utils/ims.js'; import { AGENT_EVENT, ROLE, TOOL_NAME, TOOL_SCOPE, TOOL_STATE } from './constants.js'; -import { readStream } from './utils.js'; -import { loadMessages, saveMessages, resetSession } from './persistence.js'; +import { readStream } from './utils/stream.js'; +import { loadMessages, saveMessages, resetSession } from './utils/persistence.js'; function affectedFolders(toolName, input) { const { org, repo } = input ?? {}; diff --git a/nx2/blocks/chat/chat.css b/nx2/blocks/chat/chat.css index dc721a52c..292ba14bc 100644 --- a/nx2/blocks/chat/chat.css +++ b/nx2/blocks/chat/chat.css @@ -213,7 +213,7 @@ } &::before { - margin-top: 6px; + margin-top: var(--s2-spacing-75); } } @@ -243,26 +243,27 @@ width: 14px; height: 14px; flex-shrink: 0; - margin-top: 4px; + margin-top: var(--s2-spacing-75); appearance: none; border: 2px solid var(--s2-gray-800); border-radius: var(--s2-corner-radius-75); background: var(--s2-gray-75); &:disabled { - opacity: 0.3; + opacity: 0.8; cursor: not-allowed; } &:checked { background: var(--s2-gray-800); + color: var(--s2-gray-25); &::after { content: ""; position: absolute; inset: 0; - background-color: var(--s2-gray-25); - mask-image: url("/nx2/img/icons/S2_Icon_Checkmark_20_N.svg"); + background-color: currentcolor; + mask-image: url("https://da.live/img/icons/s2-icon-checkmark-20-n.svg"); mask-size: contain; mask-repeat: no-repeat; mask-position: center; diff --git a/nx2/blocks/chat/chat.js b/nx2/blocks/chat/chat.js index be4c716b5..acef82b2d 100644 --- a/nx2/blocks/chat/chat.js +++ b/nx2/blocks/chat/chat.js @@ -1,13 +1,13 @@ import { LitElement, html, nothing } from 'da-lit'; import { loadStyle, hashChange } from '../../utils/utils.js'; -import { readFileAsBase64 } from './utils.js'; +import { readFileAsBase64 } from './utils/stream.js'; import '../shared/menu/menu.js'; import ChatController from './chat-controller.js'; import { renderMessage, renderApprovalCard } from './renderers.js'; import './welcome/welcome.js'; import './prompts/prompts.js'; import './pills/pills.js'; -import { loadSiteConfig } from './api.js'; +import { loadSiteConfig } from './utils/api.js'; import { ADOBE_AI_GUIDELINES_URL, ADD_MENU_ITEMS, MENU_OPTIONS, ROLE, TOOL_STATE } from './constants.js'; import { getConfig } from '../../scripts/nx.js'; diff --git a/nx2/blocks/chat/renderers.js b/nx2/blocks/chat/renderers.js index 6526b821e..82f2c513b 100644 --- a/nx2/blocks/chat/renderers.js +++ b/nx2/blocks/chat/renderers.js @@ -1,6 +1,7 @@ import { html, nothing } from 'da-lit'; import { AGENT_EVENT, ROLE, TOOL_INPUT, TOOL_STATE } from './constants.js'; import { getConfig } from '../../scripts/nx.js'; +import { parseDirectives } from './utils/parse.js'; const { codeBase } = getConfig(); @@ -48,33 +49,6 @@ function renderNode(node) { const parser = unified().use(remarkParse); -function parseDirectives(text) { - const segments = []; - const buf = []; - let type = null; - let openLine = null; - - for (const line of text.split('\n')) { - if (!type && line.startsWith(':::')) { - const content = buf.splice(0).join('\n'); - if (content) segments.push({ kind: 'text', content }); - [type] = line.slice(3).split(' '); - openLine = line; - } else if (type && line.trimEnd() === ':::') { - segments.push({ kind: 'directive', type, content: buf.splice(0).join('\n') }); - type = null; - openLine = null; - } else { - buf.push(line); - } - } - - if (openLine) buf.unshift(openLine); - const tail = buf.join('\n'); - if (tail) segments.push({ kind: 'text', content: tail }); - return segments; -} - function renderChecklistItem(node) { const para = node.children[0]; if (para?.type !== 'paragraph') return html`
  • ${node.children.map(renderNode)}
  • `; @@ -248,4 +222,4 @@ function renderMessage(msg, toolCards) { `; } -export { renderMessage, renderApprovalCard, parseDirectives }; +export { renderMessage, renderApprovalCard }; diff --git a/nx2/blocks/chat/api.js b/nx2/blocks/chat/utils/api.js similarity index 85% rename from nx2/blocks/chat/api.js rename to nx2/blocks/chat/utils/api.js index 4ce8f9c77..6c91dff78 100644 --- a/nx2/blocks/chat/api.js +++ b/nx2/blocks/chat/utils/api.js @@ -1,5 +1,5 @@ -import { daFetch } from '../../utils/api.js'; -import { DA_ADMIN } from '../../utils/utils.js'; +import { daFetch } from '../../../utils/api.js'; +import { DA_ADMIN } from '../../../utils/utils.js'; export async function loadSiteConfig(org, site) { try { diff --git a/nx2/blocks/chat/utils/parse.js b/nx2/blocks/chat/utils/parse.js new file mode 100644 index 000000000..0915a3640 --- /dev/null +++ b/nx2/blocks/chat/utils/parse.js @@ -0,0 +1,31 @@ +export function parseDirectives(text) { + const segments = []; + const buf = []; + let type = null; + let openLine = null; + + for (const line of text.split('\n')) { + if (!type && line.startsWith(':::')) { + const extracted = line.slice(3).split(' ')[0]; + if (!extracted) { + buf.push(line); + } else { + const content = buf.splice(0).join('\n'); + if (content) segments.push({ kind: 'text', content }); + type = extracted; + openLine = line; + } + } else if (type && line.trimEnd() === ':::') { + segments.push({ kind: 'directive', type, content: buf.splice(0).join('\n') }); + type = null; + openLine = null; + } else { + buf.push(line); + } + } + + if (openLine) buf.unshift(openLine); + const tail = buf.join('\n'); + if (tail) segments.push({ kind: 'text', content: tail }); + return segments; +} diff --git a/nx2/blocks/chat/persistence.js b/nx2/blocks/chat/utils/persistence.js similarity index 100% rename from nx2/blocks/chat/persistence.js rename to nx2/blocks/chat/utils/persistence.js diff --git a/nx2/blocks/chat/utils.js b/nx2/blocks/chat/utils/stream.js similarity index 97% rename from nx2/blocks/chat/utils.js rename to nx2/blocks/chat/utils/stream.js index ab3833cdf..eb107feea 100644 --- a/nx2/blocks/chat/utils.js +++ b/nx2/blocks/chat/utils/stream.js @@ -1,4 +1,4 @@ -import { AGENT_EVENT as EVENT, TOOL_SCOPE } from './constants.js'; +import { AGENT_EVENT as EVENT, TOOL_SCOPE } from '../constants.js'; function processEvent(event, streaming, callbacks) { const { onDelta, onText, onTool } = callbacks; diff --git a/nx2/img/icons/S2_Icon_Checkmark_20_N.svg b/nx2/img/icons/S2_Icon_Checkmark_20_N.svg deleted file mode 100644 index 187b80794..000000000 --- a/nx2/img/icons/S2_Icon_Checkmark_20_N.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/test/nx2/blocks/chat/renderers.test.js b/test/nx2/blocks/chat/renderers.test.js index 31c9ff963..69e18056b 100644 --- a/test/nx2/blocks/chat/renderers.test.js +++ b/test/nx2/blocks/chat/renderers.test.js @@ -1,5 +1,5 @@ import { expect } from '@esm-bundle/chai'; -import { parseDirectives } from '../../../../nx2/blocks/chat/renderers.js'; +import { parseDirectives } from '../../../../nx2/blocks/chat/utils/parse.js'; describe('parseDirectives', () => { describe('plain text', () => { @@ -58,6 +58,21 @@ describe('parseDirectives', () => { }); }); + describe('bare ::: with no type', () => { + it('treats ::: with no type as plain text', () => { + expect(parseDirectives(':::')).to.deep.equal([{ kind: 'text', content: ':::' }]); + }); + + it('does not open a directive scope for bare :::', () => { + const text = ':::\n:::checklist\n- item\n:::'; + const result = parseDirectives(text); + expect(result).to.deep.equal([ + { kind: 'text', content: ':::' }, + { kind: 'directive', type: 'checklist', content: '- item' }, + ]); + }); + }); + describe('unclosed directive (streaming)', () => { it('emits the opening line as text when directive is never closed', () => { const text = ':::checklist\n- [x] partial'; From 74bb4c227815f973243861a2cd68401cae4067ad Mon Sep 17 00:00:00 2001 From: svinod Date: Fri, 29 May 2026 17:32:28 +0200 Subject: [PATCH 3/5] chore: code cleanup --- nx2/blocks/chat/chat.css | 4 ++-- test/nx2/blocks/chat/persistence.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nx2/blocks/chat/chat.css b/nx2/blocks/chat/chat.css index cc59172e5..13ea11ba1 100644 --- a/nx2/blocks/chat/chat.css +++ b/nx2/blocks/chat/chat.css @@ -275,9 +275,9 @@ .directive-alert-info, .directive-alert-warning, .directive-alert-error { - padding: 6px var(--s2-spacing-200); + padding: var(--s2-spacing-75) var(--s2-spacing-200); border-radius: 12px; - margin: 12px 0; + margin: var(--s2-spacing-200) 0; } .directive-alert-info { diff --git a/test/nx2/blocks/chat/persistence.test.js b/test/nx2/blocks/chat/persistence.test.js index 1fa304030..9c592d806 100644 --- a/test/nx2/blocks/chat/persistence.test.js +++ b/test/nx2/blocks/chat/persistence.test.js @@ -3,7 +3,7 @@ import { loadMessages, saveMessages, resetSession, -} from '../../../../nx2/blocks/chat/persistence.js'; +} from '../../../../nx2/blocks/chat/utils/persistence.js'; let counter = 0; const room = () => { From c433b01e3825966e7ccb91314449e52fe2366772 Mon Sep 17 00:00:00 2001 From: svinod Date: Fri, 29 May 2026 17:36:49 +0200 Subject: [PATCH 4/5] fix: render progressively --- nx2/blocks/chat/utils/parse.js | 9 ++++++--- test/nx2/blocks/chat/renderers.test.js | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/nx2/blocks/chat/utils/parse.js b/nx2/blocks/chat/utils/parse.js index 0915a3640..df68d2cb8 100644 --- a/nx2/blocks/chat/utils/parse.js +++ b/nx2/blocks/chat/utils/parse.js @@ -24,8 +24,11 @@ export function parseDirectives(text) { } } - if (openLine) buf.unshift(openLine); - const tail = buf.join('\n'); - if (tail) segments.push({ kind: 'text', content: tail }); + if (openLine) { + segments.push({ kind: 'directive', type, content: buf.join('\n') }); + } else { + const tail = buf.join('\n'); + if (tail) segments.push({ kind: 'text', content: tail }); + } return segments; } diff --git a/test/nx2/blocks/chat/renderers.test.js b/test/nx2/blocks/chat/renderers.test.js index 69e18056b..9184e1120 100644 --- a/test/nx2/blocks/chat/renderers.test.js +++ b/test/nx2/blocks/chat/renderers.test.js @@ -74,20 +74,20 @@ describe('parseDirectives', () => { }); describe('unclosed directive (streaming)', () => { - it('emits the opening line as text when directive is never closed', () => { + it('renders an unclosed directive as a directive with partial content', () => { const text = ':::checklist\n- [x] partial'; const result = parseDirectives(text); expect(result).to.deep.equal([ - { kind: 'text', content: ':::checklist\n- [x] partial' }, + { kind: 'directive', type: 'checklist', content: '- [x] partial' }, ]); }); - it('emits completed directives before an unclosed one', () => { + it('renders completed and unclosed directives in order', () => { const text = ':::list\n- done\n:::\n:::checklist\n- partial'; const result = parseDirectives(text); expect(result).to.deep.equal([ { kind: 'directive', type: 'list', content: '- done' }, - { kind: 'text', content: ':::checklist\n- partial' }, + { kind: 'directive', type: 'checklist', content: '- partial' }, ]); }); }); From fdfb36614ec79f8fc30c9ec6ab35e88399e4c467 Mon Sep 17 00:00:00 2001 From: svinod Date: Mon, 1 Jun 2026 11:13:35 +0200 Subject: [PATCH 5/5] chore: code cleanup --- nx2/blocks/chat/pills/pills.js | 5 ++--- nx2/blocks/chat/renderers.js | 16 ++-------------- nx2/blocks/chat/utils/icons.js | 10 ++++++++++ .../{renderers.test.js => utils/parse.test.js} | 2 +- .../blocks/chat/{ => utils}/persistence.test.js | 2 +- 5 files changed, 16 insertions(+), 19 deletions(-) create mode 100644 nx2/blocks/chat/utils/icons.js rename test/nx2/blocks/chat/{renderers.test.js => utils/parse.test.js} (97%) rename test/nx2/blocks/chat/{ => utils}/persistence.test.js (98%) diff --git a/nx2/blocks/chat/pills/pills.js b/nx2/blocks/chat/pills/pills.js index 50f89f60e..fe7b8d182 100644 --- a/nx2/blocks/chat/pills/pills.js +++ b/nx2/blocks/chat/pills/pills.js @@ -1,6 +1,7 @@ import { LitElement, html, nothing } from 'da-lit'; import { loadStyle } from '../../../utils/utils.js'; import { getConfig } from '../../../scripts/nx.js'; +import { fileIconName } from '../utils/icons.js'; const styles = await loadStyle(import.meta.url); const { codeBase } = getConfig(); @@ -19,9 +20,7 @@ class NxChatPills extends LitElement { _pillTypeIcon(label, thumbnail) { if (thumbnail) return html``; - const isImage = label && /\.(png|jpg|jpeg|gif|webp|svg|mp4|webm|mov)$/i.test(label); - const icon = isImage ? 's2-icon-image-20-n' : 's2-icon-filetext-20-n'; - return html``; + return html``; } _renderPill({ id, label, thumbnail, type }) { diff --git a/nx2/blocks/chat/renderers.js b/nx2/blocks/chat/renderers.js index 82f2c513b..ab362e7e6 100644 --- a/nx2/blocks/chat/renderers.js +++ b/nx2/blocks/chat/renderers.js @@ -2,16 +2,10 @@ import { html, nothing } from 'da-lit'; import { AGENT_EVENT, ROLE, TOOL_INPUT, TOOL_STATE } from './constants.js'; import { getConfig } from '../../scripts/nx.js'; import { parseDirectives } from './utils/parse.js'; +import { fileIconName } from './utils/icons.js'; const { codeBase } = getConfig(); -const SELECTION_ICON_NAMES = { - block: 's2-icon-3d-20-n', - file: 's2-icon-filetext-20-n', - image: 's2-icon-image-20-n', - table: 's2-icon-table-20-n', -}; - const { unified, remarkParse } = await import('../../deps/mdast/dist/index.js'); function renderNode(node) { @@ -163,14 +157,8 @@ function renderApprovalCard(pending, onApprove) { `; } -// Mirrors entryTypeFromExtension in browse/utils.js — switch to common utils once migrated. function selectionIcon(blockName) { - const ext = (blockName ?? '').includes('.') ? blockName.split('.').pop().toLowerCase() : ''; - let name = SELECTION_ICON_NAMES.block; - if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'mp4', 'webm', 'mov'].includes(ext)) name = SELECTION_ICON_NAMES.image; - else if (['json', 'xlsx', 'xls', 'csv'].includes(ext)) name = SELECTION_ICON_NAMES.table; - else if (ext) name = SELECTION_ICON_NAMES.file; - return html``; + return html``; } function renderMessage(msg, toolCards) { diff --git a/nx2/blocks/chat/utils/icons.js b/nx2/blocks/chat/utils/icons.js new file mode 100644 index 000000000..ec8116ff3 --- /dev/null +++ b/nx2/blocks/chat/utils/icons.js @@ -0,0 +1,10 @@ +const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'mp4', 'webm', 'mov']); +const TABLE_EXTS = new Set(['json', 'xlsx', 'xls', 'csv']); + +export function fileIconName(filename) { + const ext = (filename ?? '').includes('.') ? filename.split('.').pop().toLowerCase() : ''; + if (IMAGE_EXTS.has(ext)) return 's2-icon-image-20-n'; + if (TABLE_EXTS.has(ext)) return 's2-icon-table-20-n'; + if (ext) return 's2-icon-filetext-20-n'; + return 's2-icon-3d-20-n'; +} diff --git a/test/nx2/blocks/chat/renderers.test.js b/test/nx2/blocks/chat/utils/parse.test.js similarity index 97% rename from test/nx2/blocks/chat/renderers.test.js rename to test/nx2/blocks/chat/utils/parse.test.js index 9184e1120..79fd42be1 100644 --- a/test/nx2/blocks/chat/renderers.test.js +++ b/test/nx2/blocks/chat/utils/parse.test.js @@ -1,5 +1,5 @@ import { expect } from '@esm-bundle/chai'; -import { parseDirectives } from '../../../../nx2/blocks/chat/utils/parse.js'; +import { parseDirectives } from '../../../../../nx2/blocks/chat/utils/parse.js'; describe('parseDirectives', () => { describe('plain text', () => { diff --git a/test/nx2/blocks/chat/persistence.test.js b/test/nx2/blocks/chat/utils/persistence.test.js similarity index 98% rename from test/nx2/blocks/chat/persistence.test.js rename to test/nx2/blocks/chat/utils/persistence.test.js index 9c592d806..c107378f0 100644 --- a/test/nx2/blocks/chat/persistence.test.js +++ b/test/nx2/blocks/chat/utils/persistence.test.js @@ -3,7 +3,7 @@ import { loadMessages, saveMessages, resetSession, -} from '../../../../nx2/blocks/chat/utils/persistence.js'; +} from '../../../../../nx2/blocks/chat/utils/persistence.js'; let counter = 0; const room = () => {