Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
272a10a
feat(chat): add plan and task renderer components (SITES-46674)
anfibiacreativa Jun 17, 2026
b85d392
feat(chat): redesign plan card UX and fix local agent port
anfibiacreativa Jun 19, 2026
c8b5f10
feat(chat): wire submit_plan tool to plan card renderer
anfibiacreativa Jun 19, 2026
7699b3e
refactor(chat): align plan tool names with AO enter_plan_mode / exit_…
anfibiacreativa Jun 19, 2026
e268fc4
Update worklog
claude Jun 22, 2026
fd505ec
fix(chat): fix plan card rendering, task-status persistence, and CSS
anfibiacreativa Jun 22, 2026
226d0cd
fix(chat): remove task-label margin-left and add plan-card vertical m…
anfibiacreativa Jun 22, 2026
7881f35
fix(chat): apply AITA review fixes to plan card and renderers
anfibiacreativa Jun 22, 2026
a19a55c
feat(chat): add preflight card and run_preflight tool wiring
anfibiacreativa Jun 25, 2026
8bf8bb1
Merge branch 'main' into feat/plan-tasks
anfibiacreativa Jun 25, 2026
4eb3d94
refactor(chat): apply AITA review follow-ups to plan/task components
anfibiacreativa Jul 1, 2026
dd40516
refactor(chat): second AITA pass on plan/task components
anfibiacreativa Jul 1, 2026
65a5c24
Merge remote-tracking branch 'origin/main' into feat/plan-tasks
anfibiacreativa Jul 14, 2026
2952ad7
Merge branch 'main' into feat/plan-tasks
andreituicu Jul 25, 2026
7a0278e
feat: Governance Page Evaluation card
andreituicu Jul 25, 2026
2925c8c
feat: Governance Page Evaluation card - Add reasoning and suggestions…
andreituicu Jul 25, 2026
be3941e
feat: Governance Page Evaluation card - Image evaluations section col…
andreituicu Jul 25, 2026
ad2cfac
feat: Governance Page Evaluation card - Loading state
andreituicu Jul 27, 2026
be58899
feat: Governance Page Evaluation card - failed state
andreituicu Jul 27, 2026
21c6522
fix: custom renderers for tools are not appearing after refresh
andreituicu Jul 29, 2026
9c6092a
fix(cleanup): Cleaup for review
andreituicu Jul 29, 2026
6daf3a9
Merge branch 'main' into feat/plan-tasks
andreituicu Jul 30, 2026
8885700
fix: Reinstate skipped tests
andreituicu Aug 1, 2026
3468132
fix: Use S2 icons, instead of inline svgs
andreituicu Aug 1, 2026
5d71144
fix: Use native summary/description HTML elements, instead of custom …
andreituicu Aug 1, 2026
d23cb45
fix: Use native progress bar HTML element, instead of custom one
andreituicu Aug 1, 2026
72f9da1
refactor: Extract directive parsing and merge logic from renderers.js…
andreituicu Aug 1, 2026
2328dc9
refactor: Move card component imports from chat.js to renderers scope
andreituicu Aug 1, 2026
18c2247
Merge branch 'main' into feat/plan-tasks
andreituicu Aug 1, 2026
fb1a01f
fix: Use native summary/description HTML elements, instead of custom …
andreituicu Aug 4, 2026
2b8d706
fix: Use S2 icons, instead of inline svgs
andreituicu Aug 4, 2026
89e2f48
fix: reuse style guides. unify CSS for message cards
andreituicu Aug 5, 2026
9f3cd1a
fix: cleanup remove unused code
andreituicu Aug 5, 2026
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
29 changes: 29 additions & 0 deletions WORKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,35 @@ New internal helpers `parseListItems` (ok-check + parse-or-`[]`) and `dedupeByNa

**Test-suite flake fixed in passing:** `test/nx2/utils/api.test.js`'s outer `beforeEach` did a blanket `localStorage.removeItem('hlx6-upgrade')`. Since `tree.test.js` seeds the same shared-origin key for its own hlx6 tests, and wtr runs test files concurrently (`--concurrent-browsers 4`) with a shared localStorage, this occasionally wiped `tree.test.js`'s seeded entry mid-run, causing intermittent unrelated failures. Removed the clear — every `org`/`site` pair in `api.test.js` already comes from a randomized `uniq()` helper, so the blanket clear was never actually load-bearing.

## 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-07-14

### nx2/styles/styles.css — pin to light mode
Expand Down
50 changes: 50 additions & 0 deletions nx2/blocks/chat/chat-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ export default class ChatController {
output: part.output,
errorText: part.errorText,
approvalRequired: part.approvalRequired,
// Ephemeral, transient continuation prompt (never persisted in message history).
continuationPending: this._continuationPendingIds?.has(part.toolCallId) ?? false,
});
});
});
Expand Down Expand Up @@ -278,6 +280,18 @@ export default class ChatController {
return;
}

// Post-execution continuation gate: the tool already finished (its result is shown).
// Flag it as awaiting a Continue/Stop decision. Ephemeral (UI-only) — nothing is pushed
// to _messages, so a reload simply drops the prompt while the result persists.
if (type === AGENT_EVENT.CONTINUATION) {
const existing = this._findToolPart(toolCallId);
if (!existing) return;
this._continuationPendingIds ??= new Set();
this._continuationPendingIds.add(toolCallId);
this._update();
return;
}

// The agent gates this call behind user approval. Auto-approved tools skip
// the queue and join the next batch directly.
if (type === AGENT_EVENT.TOOL_APPROVAL_REQUEST) {
Expand Down Expand Up @@ -375,6 +389,42 @@ export default class ChatController {
}
};

/** Clear the ephemeral continuation-pending flags (never persisted). */
_clearContinuationPending() {
this._continuationPendingIds?.clear();
}

// Continuation gate — user chose "Continue": resume the agentic loop. The gated tool's
// result is already persisted for the current turn, so re-streaming replays it to the
// agent (keeping the same turnId) and the model picks up where it left off.
continueExecution = async () => {
this._clearContinuationPending();
this._thinking = true;
this._update();
try {
await this._stream(this._pageContextForAgent());
} catch (err) {
if (err.name !== 'AbortError') {
this._messages = [...this._messages, { role: ROLE.ASSISTANT, content: `Error: ${err.message}` }];
}
} finally {
this._done();
}
};

// Continuation gate — user chose "Stop": record the decision as a user message and halt.
// No re-stream and no assistant reply — the turn simply ends (code-driven, not the LLM).
stopExecution = async () => {
this._clearContinuationPending();
this._messages = [
...this._messages,
{ role: ROLE.USER, content: 'User decided not to continue further.' },
];
this._update();
const room = await this._getRoom();
saveMessages(room, this._messages, this._sessionId);
};

// Prune prior-turn non-gated tool reads to bound payload size, mirroring the
// old virtual-message pruning. Approval-gated tool parts are kept across turns
// so the agent retains the record of destructive actions it took.
Expand Down
48 changes: 45 additions & 3 deletions nx2/blocks/chat/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { loadStyle, hashChange } from '../../utils/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 { renderMessage, renderApprovalCard, renderContinuationCard } from './renderers.js';
import './welcome/welcome.js';
import './prompts/prompts.js';
import './pills/pills.js';
Expand Down Expand Up @@ -243,7 +243,26 @@ class NxChat extends LitElement {
return null;
}

_pendingContinuation() {
if (!this.toolCards) return null;
for (const [toolCallId, card] of this.toolCards) {
if (card.continuationPending) return { toolCallId, ...card };
}
return null;
}

_onApprovalKeydown = (e) => {
const continuation = this._pendingContinuation();
if (continuation) {
if (e.key === 'Escape') {
e.preventDefault();
this._controller.stopExecution();
} else if (e.key === 'Enter') {
e.preventDefault();
this._controller.continueExecution();
}
return;
}
const pending = this._pendingApproval();
if (!pending) return;
if (e.key === 'Escape') {
Expand Down Expand Up @@ -277,7 +296,7 @@ class NxChat extends LitElement {
this.shadowRoot.querySelector('.chat-input')?.focus();
}
if (changed.has('toolCards')) {
if (this._pendingApproval()) {
if (this._pendingApproval() || this._pendingContinuation()) {
document.addEventListener('keydown', this._onApprovalKeydown);
} else {
document.removeEventListener('keydown', this._onApprovalKeydown);
Expand Down Expand Up @@ -497,6 +516,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 @@ -533,7 +570,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 All @@ -546,6 +583,11 @@ class NxChat extends LitElement {
@mousedown=${(e) => e.preventDefault()}
></nx-menu>
${renderApprovalCard(this._pendingApproval(), this._controller.approveToolCall)}
${renderContinuationCard(
this._pendingContinuation(),
this._controller.continueExecution,
this._controller.stopExecution,
)}
<form class="chat-form" autocomplete="off" @submit=${this._submit}
@dragenter=${this._onDragEnter}
@dragleave=${this._onDragLeave}
Expand Down
32 changes: 32 additions & 0 deletions nx2/blocks/chat/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const AGENT_EVENT = {
// Result of an executed tool.
TOOL_OUTPUT_AVAILABLE: 'tool-output-available',
TOOL_OUTPUT_ERROR: 'tool-output-error',
// Transient (UI-only, never persisted) part emitted after a continuation-gated tool
// finishes, so the user can review results and decide whether the agent continues.
CONTINUATION: 'data-continuation',
};

/**
Expand Down Expand Up @@ -69,6 +72,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',
EVALUATE_PAGE: 'evaluate_page',
};

/**
Expand Down Expand Up @@ -100,6 +106,29 @@ 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',
GOVERNANCE_EVALUATION: 'governance-evaluation',
};

/**
* 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';

/**
* DOM CustomEvent names for <nx-chat>'s boundary with the outside world — part of
* chat's public surface, see docs/chat-ui-component.md ("Events in"/"Events out").
Expand All @@ -118,9 +147,12 @@ export {
ADD_MENU_ITEMS,
AGENT_EVENT,
CHAT_EVENT,
DIRECTIVE_TYPE,
MENU_OPTIONS,
PART_TYPE,
PLAN_RUN_EVENT,
ROLE,
TASK_STATUS,
TOOL_INPUT,
TOOL_NAME,
TOOL_SCOPE,
Expand Down
83 changes: 83 additions & 0 deletions nx2/blocks/chat/messages/campaign-plan-card.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* Shared card shell, header, type-label/icon, chevron, and title live in messages.css. */

:host {
display: block;
}

.plan-card {
gap: var(--s2-spacing-200);
}

.plan-summary {
display: flex;
flex-direction: column;
gap: var(--s2-spacing-200);
cursor: pointer;
list-style: none;
}

/* ── Body / 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 {
mask-image: url("/img/icons/s2-icon-aichat-20-n.svg");
}

.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-chevron-icon {
color: var(--s2-gray-700);
}

.plan-card[open] .plan-tasks-collapsed {
display: none;
}

/* ── 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);
}
Loading
Loading