Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/lint_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,24 @@ jobs:

- name: Run the tests
run: npm t

run-e2e:
name: Running e2e tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Run e2e tests
run: npm run e2e
31 changes: 31 additions & 0 deletions WORKLOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Worklog

<<<<<<< feat/plan-tasks
## 2026-06-22

### nx2 chat — plan card UX polish + task-status persistence fixes

**Task status persistence across tool boundaries** (`chat.js`):
- `TEXT_END` fires after each text segment between tool calls, splitting output into multiple string messages per inter-tool boundary. Task-item directives for step N live in a different message than step N+1, so the previous code (passing only the last message) missed earlier `done` directives. Fixed by concatenating all completed assistant text messages before passing to `mergeTaskItemsFromText`.

**Plan card renderer fixes** (`renderers.js`):
- Switched `renderSubmitPlanCard` and `renderApprovalCard` from `document.createElement` to Lit `html\`\`` templates; `document.createElement` recreated the element each render, losing `_expanded` state.
- `renderMessageContent`: returns `nothing` for `TASK_ITEM` directives (previously rendered as visible text); filters `nothing` items; returns `nothing` if all items suppressed.
- `renderAssistantMessage`: skips message wrapper when content is `nothing` (eliminates empty `<div class="message message-assistant">` gaps in the DOM).
- Removed unused `renderTaskItemDirective` function.

**Plan card template restructure** (`campaign-plan-card.js`):
- `_expanded` defaults to `true`.
- `showCollapsed = !this._expanded` (was: only collapsed when running AND not expanded).
- Moved title/description out of `.plan-header` into a new `.plan-body` div below the header strip.
- Header strip is now 48px fixed-height with border-bottom, matching Figma spec.

**CSS fixes** (`campaign-plan-card.css`, `task-item.css`):
- All `--s2-spacing-150` (undefined) replaced with `--s2-spacing-200` (12px).
- All `--s2-spacing-115` replaced with `--s2-spacing-100` (8px).
- `.plan-card` gap: 12px; `.plan-header`: height 48px, border-bottom, padding 12px.
- `.plan-body`: padding 12px 16px, gap 8px.
- `.plan-tasks`: background `--s2-gray-50`, border `1px solid --s2-gray-200`, border-radius 8px, margin with side 16px offset.
- `.plan-btn`: height 24px, padding `0 16px`, weight 400.
- `.plan-btn-primary`: color `--s2-static-white` (was `--s2-gray-25` which flips to near-black in dark mode).
- `task-item.css`: `.task-label` margin-left `--s2-spacing-100`; `:host` gap `--s2-spacing-200`.
=======
## 2026-06-23

### nx2/blocks/shared/dialog — configurable panel sizing (dialog-css-vars branch)
Expand All @@ -14,6 +44,7 @@ Exposes four CSS custom properties on `.panel` so consumers can resize the dialo
Values stay clamped to the viewport via the existing `min(<custom>, calc(100vw - 2 * --s2-spacing-500))` envelope, so a too-large custom value won't overflow. Purely additive — existing usage of `<nx-dialog>` is unchanged (each fallback in the `var()` call matches the previous literal).

Driving use case is da-live's new EW block library modal, which needs a ~960px wide 2-column tree+preview layout that the previous fixed 480px cap couldn't accommodate.
>>>>>>> main

## 2026-05-28

Expand Down
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,12 @@ export default defineConfig([
'no-unused-expressions': 0,
},
},
{
// Allow devDependencies in Playwright config and e2e specs
files: ['playwright.config.js', 'nx2/test/e2e/**/*.js'],
rules: {
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
},
},
]);

2 changes: 1 addition & 1 deletion nx2/blocks/chat/chat-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function affectedFolders(toolName, input) {
}

const AGENT_URL = new URLSearchParams(window.location.search).get('ref') === 'local'
? 'http://localhost:4200/chat'
? 'http://localhost:4002/chat'
: 'https://agent.da.live/chat';

/**
Expand Down
24 changes: 23 additions & 1 deletion nx2/blocks/chat/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { renderMessage, renderApprovalCard } from './renderers.js';
import './welcome/welcome.js';
import './prompts/prompts.js';
import './pills/pills.js';
import './messages/campaign-plan-card.js';
import './messages/preflight-card.js';
import './messages/task-list.js';
import './messages/task-item.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';
Expand Down Expand Up @@ -481,6 +485,24 @@ class NxChat extends LitElement {
await this._onFilesSelected(accepted);
}

get _taskText() {
const msgs = this.messages ?? [];
const last = msgs.at(-1);
const streamingText = last?.streaming ? last.content : null;
if (streamingText) return streamingText;
// TEXT_END splits output into one string message per inter-tool segment, so
// task-item directives for step N may live in a different message than step N+1.
// Concatenate all assistant text to let mergeTaskItemsFromText find them all.
return msgs
.filter((m) => m.role === ROLE.ASSISTANT && typeof m.content === 'string' && !m.streaming)
.map((m) => m.content)
.join('\n') || null;
}

_renderMessages() {
return (this.messages ?? []).map((msg) => renderMessage(msg, this.toolCards, this._taskText));
}

render() {
const { view } = this._context ?? {};
const prompts = (this._prompts ?? [])
Expand Down Expand Up @@ -517,7 +539,7 @@ class NxChat extends LitElement {
@nx-show-prompts=${this._openPrompts}
></nx-chat-welcome>`
: nothing}
${this.messages?.map((msg) => renderMessage(msg, this.toolCards))}
${this._renderMessages()}
${this.thinking && !this.messages?.at(-1)?.streaming ? html`<div class="chat-thinking">Thinking...</div>` : nothing}
</div>
</div>
Expand Down
29 changes: 29 additions & 0 deletions nx2/blocks/chat/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ const TOOL_NAME = {
CONTENT_MOVE: 'content_move',
CONTENT_UPDATE: 'content_update',
CONTENT_UPLOAD: 'content_upload',
ENTER_PLAN_MODE: 'enter_plan_mode',
EXIT_PLAN_MODE: 'exit_plan_mode',
RUN_PREFLIGHT: 'run_preflight',
};

/**
Expand Down Expand Up @@ -92,13 +95,39 @@ const ROLE = {
TOOL: 'tool',
};

/**
* Directive fence types that map to rich interactive renderer components.
* These are emitted by da-agent inside :::type ... ::: fences in text-delta events.
*/
const DIRECTIVE_TYPE = {
PLAN: 'plan',
TASK_LIST: 'task-list',
TASK_ITEM: 'task-item',
PREFLIGHT: 'preflight',
};

/**
* Task status values shared across plan/task-list/task-item components.
* Matches values sent in :::plan / :::task-list / :::task-item directive payloads.
*/
const TASK_STATUS = {
PENDING: 'pending',
RUNNING: 'running',
DONE: 'done',
};

const PLAN_RUN_EVENT = 'nx-plan-run';

export {
ADOBE_AI_GUIDELINES_URL,
ADD_MENU_ITEMS,
AGENT_EVENT,
CHAT_ICONS,
DIRECTIVE_TYPE,
MENU_OPTIONS,
PLAN_RUN_EVENT,
ROLE,
TASK_STATUS,
TOOL_INPUT,
TOOL_NAME,
TOOL_SCOPE,
Expand Down
181 changes: 181 additions & 0 deletions nx2/blocks/chat/messages/campaign-plan-card.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
:host {
display: block;
font-family: var(--s2-font-family);
}

.plan-card {
background: var(--s2-gray-25);
border-radius: var(--s2-corner-radius-400);
box-shadow: 0 1px 4px 0 color-mix(in srgb, var(--s2-gray-800) 12%, transparent);
overflow: hidden;
display: flex;
flex-direction: column;
gap: var(--s2-spacing-200);
margin-top: var(--s2-spacing-200);
margin-bottom: var(--s2-spacing-75);
}

/* ── Header strip (48px, border-bottom) ── */

.plan-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
padding: var(--s2-spacing-200);
border-bottom: 1px solid var(--s2-gray-200, #e1e1e1);
box-sizing: border-box;
}

.plan-type-label {
font-size: var(--s2-body-size-xs);
color: var(--s2-gray-600);
display: flex;
align-items: center;
gap: var(--s2-spacing-75);
}

/* ── Title / description area ── */

.plan-body {
display: flex;
flex-direction: column;
gap: var(--s2-spacing-100);
padding: var(--s2-spacing-200) var(--s2-spacing-300);
}

.plan-type-icon {
width: 14px;
height: 14px;
display: inline-block;
flex-shrink: 0;
background-color: currentcolor;
mask-image: url("/img/icons/s2-icon-aichat-20-n.svg");
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
}

.plan-title {
font-size: var(--s2-font-size-200, 18px);
font-weight: 700;
line-height: var(--s2-line-height-200, 1.3);
letter-spacing: var(--s2-letter-spacing, normal);
color: var(--s2-gray-900);
margin: 0;
}

.plan-description {
font-size: var(--s2-body-size-s);
color: var(--s2-gray-700);
margin: 0;
line-height: 1.4;
}

.plan-header-actions {
display: flex;
align-items: center;
gap: var(--s2-spacing-75);
flex-shrink: 0;
}

.plan-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: var(--s2-corner-radius-300);
border: none;
background: transparent;
color: var(--s2-gray-700);
cursor: pointer;
padding: 0;

&:hover {
background-color: var(--s2-gray-100);
}
}

.plan-chevron-icon {
width: 16px;
height: 16px;
display: block;
transition: transform 0.2s ease;
}

.plan-icon-btn-expanded .plan-chevron-icon {
transform: rotate(180deg);
}

/* ── Task area ── */

.plan-tasks {
display: flex;
flex-direction: column;
gap: var(--s2-spacing-200);
padding: var(--s2-spacing-200);
margin: 0 var(--s2-spacing-300) var(--s2-spacing-200);
background-color: var(--s2-gray-50, #f8f8f8);
border-radius: var(--s2-corner-radius-500);
border: 1px solid var(--s2-gray-200, #e1e1e1);
}

.plan-tasks-header {
font-size: var(--s2-body-size-xs);
color: var(--s2-gray-600);
margin-bottom: var(--s2-spacing-50);
}

.plan-tasks-progress {
font-size: var(--s2-body-size-xs);
color: var(--s2-gray-600);
font-variant-numeric: tabular-nums;
}

.plan-task-row {
display: flex;
align-items: center;
gap: var(--s2-spacing-200);
}

/* ── Buttons ── */

.plan-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 24px;
padding: 0 var(--s2-spacing-300);
border-radius: var(--s2-corner-radius-300);
font-family: var(--s2-font-family);
font-size: var(--s2-body-size-s);
font-weight: 400;
cursor: pointer;
border: none;
white-space: nowrap;

&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
}

.plan-btn-ghost {
background: transparent;
color: var(--s2-gray-1000);
border: 1px solid var(--s2-gray-300);

&:hover:not(:disabled) {
background: var(--s2-gray-75);
}
}

.plan-btn-primary {
background: var(--s2-blue-900);
color: var(--s2-static-white, #fff);

&:hover:not(:disabled) {
background: var(--s2-blue-1000);
}
}
Loading