@design.estate/dees-catalog 3.92.0 → 3.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist_bundle/bundle.js +2125 -1392
  2. package/dist_ts_web/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.d.ts +15 -3
  4. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.demo.js +159 -18
  5. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +126 -49
  6. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.d.ts +13 -0
  7. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.js +5 -2
  8. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.js +118 -3
  9. package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.d.ts +25 -3
  10. package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.js +156 -48
  11. package/dist_ts_web/elements/00group-harness/dees-harness-permission-card/dees-harness-permission-card.d.ts +2 -0
  12. package/dist_ts_web/elements/00group-harness/dees-harness-permission-card/dees-harness-permission-card.js +9 -5
  13. package/dist_ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.d.ts +31 -0
  14. package/dist_ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.demo.d.ts +1 -0
  15. package/dist_ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.demo.js +69 -0
  16. package/dist_ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.js +382 -0
  17. package/dist_ts_web/elements/00group-harness/dees-harness-question-card/index.d.ts +1 -0
  18. package/dist_ts_web/elements/00group-harness/dees-harness-question-card/index.js +2 -0
  19. package/dist_ts_web/elements/00group-harness/index.d.ts +1 -0
  20. package/dist_ts_web/elements/00group-harness/index.js +2 -1
  21. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +33 -0
  22. package/dist_ts_web/elements/00group-harness/toolregistry.js +9 -1
  23. package/dist_ts_web/elements/00theme.js +6 -1
  24. package/dist_watch/bundle.js +2126 -1393
  25. package/dist_watch/bundle.js.map +4 -4
  26. package/package.json +1 -1
  27. package/readme.md +14 -4
  28. package/readme.playbook.md +1 -1
  29. package/ts_web/00_commitinfo_data.ts +1 -1
  30. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.demo.ts +168 -21
  31. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +98 -49
  32. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.ts +4 -1
  33. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.ts +99 -0
  34. package/ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.ts +148 -46
  35. package/ts_web/elements/00group-harness/dees-harness-permission-card/dees-harness-permission-card.ts +9 -4
  36. package/ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.demo.ts +70 -0
  37. package/ts_web/elements/00group-harness/dees-harness-question-card/dees-harness-question-card.ts +319 -0
  38. package/ts_web/elements/00group-harness/dees-harness-question-card/index.ts +1 -0
  39. package/ts_web/elements/00group-harness/index.ts +1 -0
  40. package/ts_web/elements/00group-harness/interfaces.ts +36 -0
  41. package/ts_web/elements/00group-harness/toolregistry.ts +8 -0
  42. package/ts_web/elements/00theme.ts +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "3.92.0",
3
+ "version": "3.94.0",
4
4
  "private": false,
5
5
  "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
6
6
  "main": "dist_ts_web/index.js",
package/readme.md CHANGED
@@ -64,7 +64,7 @@ For developers working on this library, please refer to the [UI Components Playb
64
64
  | **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) |
65
65
  | **Navigation** | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) |
66
66
  | **Workspace / IDE** | [`DeesWorkspace`](#deesworkspace), [`DeesWorkspaceMonaco`](#deesworkspacemonaco), [`DeesWorkspaceDiffEditor`](#deesworkspacediffeditor), [`DeesWorkspaceFiletree`](#deesworkspacefiletree), [`DeesWorkspaceTerminal`](#deesworkspaceterminal), [`DeesWorkspaceTerminalPreview`](#deesworkspaceterminalpreview), [`DeesWorkspaceMarkdown`](#deesworkspacemarkdown), [`DeesWorkspaceMarkdownoutlet`](#deesworkspacemarkdownoutlet), [`DeesWorkspaceBottombar`](#deesworkspacebottombar) |
67
- | **Agentic Chat** | [`DeesHarnessChat`](#deesharnesschat), [`DeesHarnessMessageList`](#deesharnessmessagelist), [`DeesHarnessMessage`](#deesharnessmessage), [`DeesHarnessReasoning`](#deesharnessreasoning), [`DeesHarnessToolCard`](#deesharnesstoolcard), [`DeesHarnessContentBlocks`](#deesharnesscontentblocks), [`DeesHarnessPermissionCard`](#deesharnesspermissioncard), [`DeesHarnessComposer`](#deesharnesscomposer), [`DeesHarnessStatus`](#deesharnessstatus), [`DeesHarnessTodos`](#deesharnesstodos), [`DeesHarnessUsage`](#deesharnessusage), [`DeesHarnessSessionList`](#deesharnesssessionlist) |
67
+ | **Agentic Chat** | [`DeesHarnessChat`](#deesharnesschat), [`DeesHarnessMessageList`](#deesharnessmessagelist), [`DeesHarnessMessage`](#deesharnessmessage), [`DeesHarnessReasoning`](#deesharnessreasoning), [`DeesHarnessToolCard`](#deesharnesstoolcard), [`DeesHarnessContentBlocks`](#deesharnesscontentblocks), [`DeesHarnessPermissionCard`](#deesharnesspermissioncard), [`DeesHarnessQuestionCard`](#deesharnessquestioncard), [`DeesHarnessComposer`](#deesharnesscomposer), [`DeesHarnessStatus`](#deesharnessstatus), [`DeesHarnessTodos`](#deesharnesstodos), [`DeesHarnessUsage`](#deesharnessusage), [`DeesHarnessSessionList`](#deesharnesssessionlist) |
68
68
  | **Theming** | [`DeesTheme`](#deestheme), [`DeesUpdater`](#deesupdater) |
69
69
  | **Pre-built Templates** | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) |
70
70
  | **Shopping** | [`DeesShoppingProductcard`](#deesshoppingproductcard) |
@@ -1699,7 +1699,7 @@ The `00group-harness` family renders what an agent harness produces — streamin
1699
1699
 
1700
1700
  #### `DeesHarnessChat`
1701
1701
 
1702
- The assembled chat: optional toolbar (heading, usage chip, history/new-session buttons), streaming message list, pinned permission queue, status line, and composer.
1702
+ The assembled chat: optional toolbar (heading, usage chip, history/new-session buttons), streaming message list with inline permission cards, transcript status line, and docked composer.
1703
1703
 
1704
1704
  ```typescript
1705
1705
  const chat = document.querySelector('dees-harness-chat');
@@ -1721,11 +1721,13 @@ chat.applyDelta({ type: 'text', messageId: 'm1', delta: 'chunk' });
1721
1721
  chat.applyDelta({ type: 'message-end', messageId: 'm1', usage: { totalTokens: 1200 } });
1722
1722
  ```
1723
1723
 
1724
- Key props: `messages`, `permissions`, `status`, `usage`, `heading`, `subheading`, `showToolbar`, `busy`, `queuedCount`, `disabled`, `model`/`modelOptions`, `reasoningEffort`/`effortOptions`, `toolRegistry`. Methods: `applyDelta()`, `focusComposer()`, `clearComposer()`, `scrollToBottom()`. Events (also from children, all composed): `harness-send`, `harness-steer`, `harness-abort`, `harness-input`, `harness-attachments-change`, `harness-attachment-error`, `harness-model-change`, `harness-permission-response`, `harness-new-session`, `harness-open-sessions`.
1724
+ Key props: `messages`, `permissions`, `questions`, `status`, `usage`, `todos`/`showTodosPanel`, `heading`, `subheading`, `showToolbar`, `busy`, `queuedCount`, `disabled`, `model`/`modelOptions`, `reasoningEffort`/`effortOptions`, `mode`/`modeOptions`, `toolRegistry`. The status line renders inside the transcript column (it never pushes between the Tasks panel and the composer), and both the toolbar and the docked composer cast a subtle shadow over the scrolling messages.
1725
+
1726
+ A **Tasks side panel** (OpenCode-style) shows the current todo list next to the transcript: set `.todos` explicitly, or leave it empty and the chat derives it from the newest todo tool call in `messages`. The panel hides itself in narrow containers (< 520px) where the transcript's todo cards carry the same information; `showTodosPanel={false}` disables it entirely. Methods: `applyDelta()`, `focusComposer()`, `clearComposer()`, `scrollToBottom()`. Events (also from children, all composed): `harness-send`, `harness-steer`, `harness-abort`, `harness-input`, `harness-attachments-change`, `harness-attachment-error`, `harness-model-change`, `harness-mode-change`, `harness-permission-response`, `harness-question-response`, `harness-new-session`, `harness-open-sessions`.
1725
1727
 
1726
1728
  #### `DeesHarnessMessageList`
1727
1729
 
1728
- Scrollable streaming-safe log. Keyed repeat by `message.id` keeps DOM identity stable; `applyDelta()` routes updates to the affected message element only. Auto-follows the bottom while pinned; otherwise raises a "Jump to latest" chip instead of yanking the scroll. Props: `messages`, `status`, `autoFollow`, `showRoles`, `showTimestamps`, `emptyTitle`/`emptyText` (+ `slot="empty"`).
1730
+ Scrollable streaming-safe log. Keyed repeat keeps DOM identity stable; `applyDelta()` routes updates to the affected message element only. Auto-follows the bottom while pinned; otherwise raises a "Jump to latest" chip instead of yanking the scroll. Permission requests and agent questions render inline in the flow, and while pinned the list follows content growth frame-by-frame (ResizeObserver), so a new box growing into place and the scroll move as one motion. Props: `messages`, `permissions`/`permissionsBusy`, `questions`, `status`, `autoFollow`, `showRoles`, `showTimestamps`, `emptyTitle`/`emptyText` (+ `slot="empty"`).
1729
1731
 
1730
1732
  #### `DeesHarnessMessage`
1731
1733
 
@@ -1762,12 +1764,20 @@ Renders MCP `CallToolResult` content structurally: text, base64 images, audio wi
1762
1764
 
1763
1765
  Permission prompt: title, type badge (shell/delete → error tone, write/browser → warning), metadata key/values, Allow Once / Always Allow / Reject, and a "remember" checkbox (`always` implies remember). Emits `harness-permission-response`; disables itself once answered or while `busy`.
1764
1766
 
1767
+ Inside the chat, permission cards render **inline with the messages**: pending requests pin to the end of the transcript (action required), and answered ones freeze at their response time in the flow. Set `response`/`respondedAt` on the request after handling it and keep it in the array — the card stays in the transcript showing what was granted or rejected.
1768
+
1769
+ #### `DeesHarnessQuestionCard`
1770
+
1771
+ A question the agent asks mid-run: option buttons with descriptions (single-select answers on click; `multiSelect` collects then confirms) plus an optional free-text answer (`allowCustom`, default on — Enter or "Send Answer" submits, combining selections with the text). Emits `harness-question-response` with `{ requestId, answers, request }`. Questions flow through `dees-harness-chat`'s `questions` property and render inline like permissions: pending ones pin to the end of the transcript, and answered ones persist showing the chosen answers once the host sets `response`/`respondedAt`.
1772
+
1765
1773
  #### `DeesHarnessComposer`
1766
1774
 
1767
1775
  Prompt input: autogrow textarea, attachment picker/paste/drag-drop with per-file (10 MB) and total (30 MB) limits and MIME allowlist, optional model + reasoning-effort dropdowns, Cmd/Ctrl+Enter to send. Controlled component — the host owns `value` and `attachments`.
1768
1776
 
1769
1777
  While `busy`, Stop renders as an additional button and Send stays available so hosts can queue messages. With an empty prompt and `queuedCount > 0`, the primary action becomes **Steer now** (`harness-steer` event) to force the agent to continue with the queued messages immediately.
1770
1778
 
1779
+ Set `modeOptions` (e.g. `['Planning', 'Building']`) to render a segmented **mode switcher**; the active `mode` changes on click — or **Shift+Tab** inside the prompt cycles it — and emits `harness-mode-change`. Inside `dees-harness-chat` the composer renders `docked` — flush edge-to-edge with a top border and an upward shadow (`--dees-shadow-up-sm`) so it meets the transcript and the Tasks panel like the toolbar does; standalone composers keep the floating-card chrome.
1780
+
1771
1781
  #### `DeesHarnessTodos`
1772
1782
 
1773
1783
  Agent task list for `IHarnessTodoItem[]` — completed items dim with a check and strikethrough, the in-progress item leads with a pulsing accent marker, and an optional heading shows `completed/total` progress. Used automatically as the todo tool-card body (`harnessParseTodos` normalizes tool input shapes) and standalone for a "current tasks" sidebar panel fed from the latest `todowrite` call.
@@ -439,7 +439,7 @@ Components that render continuously arriving data (agent chat, logs) follow the
439
439
  - **Props are the source of truth; deltas are the fast path.** The host owns the array (e.g. `.messages`). Incremental updates go through `applyDelta()`, which mutates the same object instance held in the array and calls `requestUpdate()` on the one affected child element. A later wholesale re-assignment of the array is therefore idempotent — no divergence between streamed and re-set state.
440
440
  - **Keyed `repeat` for identity.** List children render via `directives.repeat` keyed by a stable id so streaming never recreates sibling DOM (which would drop scroll position, expansion state, and selection).
441
441
  - **Cheap while streaming, rich when done.** While `streaming` is set, text renders as pre-wrap with a cursor; expensive work (markdown parse) is throttled (≥150 ms, token-guarded like the codebox highlight pattern) and syntax highlighting waits for the final parse.
442
- - **Scroll follow with user override.** Auto-scroll only when the user is already pinned to the bottom (`scrollHeight - scrollTop - clientHeight < threshold`); otherwise show a "jump to latest" chip. Schedule follow-scrolls through a single pending `requestAnimationFrame`, cancelled in `disconnectedCallback`.
442
+ - **Scroll follow with user override.** Auto-scroll only when the user is already pinned to the bottom (`scrollHeight - scrollTop - clientHeight < threshold`); otherwise show a "jump to latest" chip. While pinned, a content change opens a short follow window and a `ResizeObserver` on the scroll content re-pins the bottom on every layout change (post-layout, pre-paint), so a growing box and the scroll move as one motion; an external upward scroll ends the window before the next pin so following never fights the user. Disconnect the observer in `disconnectedCallback`.
443
443
  - **Interaction state lives on the child.** Expand/collapse (reasoning, tool cards) is element-local state seeded from data (`expanded ?? isStreaming`), so parent re-renders can't reset what the user opened.
444
444
 
445
445
  ## Performance Optimization
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '3.92.0',
6
+ version: '3.94.0',
7
7
  description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
8
8
  }
@@ -1,7 +1,7 @@
1
1
  import { html } from '@design.estate/dees-element';
2
2
  import type { DeesHarnessChat } from './dees-harness-chat.js';
3
3
  import type { IHarnessMessage, IHarnessPermissionRequest } from '../interfaces.js';
4
- import { harnessDemoConversation, makeStreamingSimulator } from '../harness.demodata.js';
4
+ import { harnessDemoConversation } from '../harness.demodata.js';
5
5
 
6
6
  const toolShowcase = (): IHarnessMessage[] => {
7
7
  const now = Date.now();
@@ -84,30 +84,160 @@ let stopStream: (() => void) | undefined;
84
84
  const chatOf = (event: MouseEvent): DeesHarnessChat | null =>
85
85
  (event.target as HTMLElement).closest('.chatDemo')?.querySelector('dees-harness-chat') ?? null;
86
86
 
87
+ /**
88
+ * Streams a realistic turn: reasoning, a todo write, a test run, a file
89
+ * edit rendered as a diff, then the streamed summary text.
90
+ */
87
91
  const simulate = (event: MouseEvent) => {
88
92
  const chat = chatOf(event);
89
93
  if (!chat) return;
90
94
  stopStream?.();
91
- const message: IHarnessMessage = {
92
- id: `stream-${Date.now()}`,
93
- role: 'assistant',
94
- text: '',
95
- createdAt: Date.now(),
96
- streaming: true,
95
+ const runId = Date.now();
96
+ const cancels: Array<() => void> = [];
97
+ const at = (ms: number, action: () => void) => {
98
+ const timeout = setTimeout(action, ms);
99
+ cancels.push(() => clearTimeout(timeout));
97
100
  };
98
- chat.messages = [...chat.messages, message];
99
- chat.status = { type: 'busy', message: 'Responding…' };
100
- stopStream = makeStreamingSimulator(
101
- {
102
- applyDelta: (delta) => {
103
- chat.applyDelta(delta);
104
- if (delta.type === 'message-end') {
105
- chat.status = { type: 'idle' };
106
- }
101
+ stopStream = () => {
102
+ for (const cancel of cancels) cancel();
103
+ chat.status = { type: 'idle' };
104
+ };
105
+ const pushMessage = (message: IHarnessMessage) => {
106
+ chat.messages = [...chat.messages, message];
107
+ };
108
+ const updateTool = (messageId: string, tool: Partial<IHarnessMessage['toolCall']> & { id: string }) => {
109
+ chat.applyDelta({ type: 'tool-update', messageId, tool: tool as never });
110
+ };
111
+
112
+ chat.status = { type: 'busy', message: 'Planning…' };
113
+
114
+ // 1) reasoning streams first
115
+ const reasoningMessage: IHarnessMessage = {
116
+ id: `sim-${runId}-reasoning`, role: 'assistant', text: '', createdAt: Date.now(), streaming: true,
117
+ };
118
+ pushMessage(reasoningMessage);
119
+ chat.applyDelta({ type: 'reasoning-start', messageId: reasoningMessage.id, partId: 'r1', startedAt: Date.now() });
120
+ const reasoningText = 'The cache key includes the locale list, so every rollout re-crawls. Plan: track the fix as todos, run the tests, then pin the key to the route manifest.';
121
+ reasoningText.split(' ').forEach((word, index) => {
122
+ at(80 + index * 45, () => chat.applyDelta({ type: 'reasoning', messageId: reasoningMessage.id, partId: 'r1', delta: word + ' ' }));
123
+ });
124
+ const reasoningDoneAt = 80 + reasoningText.split(' ').length * 45 + 150;
125
+ at(reasoningDoneAt, () => {
126
+ chat.applyDelta({ type: 'reasoning-end', messageId: reasoningMessage.id, partId: 'r1', endedAt: Date.now() });
127
+ chat.applyDelta({ type: 'message-end', messageId: reasoningMessage.id });
128
+ });
129
+
130
+ // 2) todo write
131
+ const todoId = `sim-${runId}-todos`;
132
+ at(reasoningDoneAt + 300, () => {
133
+ chat.status = { type: 'busy', message: 'Updating task list…' };
134
+ pushMessage({
135
+ id: todoId, role: 'tool', text: '', createdAt: Date.now(),
136
+ toolCall: {
137
+ id: `${todoId}-call`, name: 'todowrite', status: 'running',
138
+ input: { todos: [
139
+ { content: 'Pin sitemap cache key to route manifest hash', status: 'in_progress' },
140
+ { content: 'Run the sitemap test suite', status: 'pending' },
141
+ { content: 'Apply the cache-key patch', status: 'pending' },
142
+ { content: 'Verify staging timings', status: 'pending' },
143
+ ] },
144
+ startedAt: Date.now(),
107
145
  },
146
+ });
147
+ });
148
+ at(reasoningDoneAt + 1000, () => updateTool(todoId, { id: `${todoId}-call`, status: 'completed', finishedAt: Date.now() }));
149
+
150
+ // 3) test run
151
+ const testId = `sim-${runId}-test`;
152
+ at(reasoningDoneAt + 1400, () => {
153
+ chat.status = { type: 'busy', message: 'Running tests…' };
154
+ pushMessage({
155
+ id: testId, role: 'tool', text: '', createdAt: Date.now(),
156
+ toolCall: {
157
+ id: `${testId}-call`, name: 'run_command', status: 'running',
158
+ input: { command: 'pnpm test --filter sitemap', cwd: '/srv/app' },
159
+ startedAt: Date.now(),
160
+ },
161
+ });
162
+ });
163
+ at(reasoningDoneAt + 3200, () => updateTool(testId, {
164
+ id: `${testId}-call`, status: 'completed',
165
+ output: { exitCode: 0, stdout: '14 tests passed\n0 failed' },
166
+ finishedAt: Date.now(),
167
+ }));
168
+
169
+ // 4) file edit rendered as a diff
170
+ const editId = `sim-${runId}-edit`;
171
+ at(reasoningDoneAt + 3600, () => {
172
+ chat.status = { type: 'busy', message: 'Applying the patch…' };
173
+ pushMessage({
174
+ id: editId, role: 'tool', text: '', createdAt: Date.now(),
175
+ toolCall: {
176
+ id: `${editId}-call`, name: 'write_file', status: 'running',
177
+ input: {
178
+ path: '/srv/app/src/jobs/sitemap.job.ts',
179
+ oldContent: 'const cacheKey = hash(locales);\nconst ttl = 3600;',
180
+ content: 'const cacheKey = hash(routeManifest);\nconst ttl = 3600;',
181
+ },
182
+ startedAt: Date.now(),
183
+ },
184
+ });
185
+ });
186
+ at(reasoningDoneAt + 4600, () => updateTool(editId, { id: `${editId}-call`, status: 'completed', finishedAt: Date.now() }));
187
+
188
+ // 5) updated todos, then the streamed summary
189
+ const todoDoneId = `sim-${runId}-todos-2`;
190
+ at(reasoningDoneAt + 5000, () => {
191
+ pushMessage({
192
+ id: todoDoneId, role: 'tool', text: '', createdAt: Date.now(),
193
+ toolCall: {
194
+ id: `${todoDoneId}-call`, name: 'todowrite', status: 'completed',
195
+ input: { todos: [
196
+ { content: 'Pin sitemap cache key to route manifest hash', status: 'completed' },
197
+ { content: 'Run the sitemap test suite', status: 'completed' },
198
+ { content: 'Apply the cache-key patch', status: 'completed' },
199
+ { content: 'Verify staging timings', status: 'in_progress' },
200
+ ] },
201
+ startedAt: Date.now(), finishedAt: Date.now(),
202
+ },
203
+ });
204
+ });
205
+ const summaryId = `sim-${runId}-summary`;
206
+ at(reasoningDoneAt + 5400, () => {
207
+ chat.status = { type: 'busy', message: 'Responding…' };
208
+ pushMessage({ id: summaryId, role: 'assistant', text: '', createdAt: Date.now(), streaming: true });
209
+ const summary = 'Patched `sitemap.job.ts` to key the cache on the **route manifest hash** — tests pass (14/14). Staging verification is the last open task.';
210
+ let offset = 0;
211
+ const interval = setInterval(() => {
212
+ const chunk = summary.slice(offset, offset + 6);
213
+ offset += 6;
214
+ if (!chunk) {
215
+ clearInterval(interval);
216
+ chat.applyDelta({ type: 'message-end', messageId: summaryId, usage: { inputTokens: 2100, outputTokens: 260, totalTokens: 2360 } });
217
+ chat.status = { type: 'idle' };
218
+ return;
219
+ }
220
+ chat.applyDelta({ type: 'text', messageId: summaryId, delta: chunk });
221
+ }, 40);
222
+ cancels.push(() => clearInterval(interval));
223
+ });
224
+ };
225
+
226
+ const askQuestion = (event: MouseEvent) => {
227
+ const chat = chatOf(event);
228
+ if (!chat) return;
229
+ chat.questions = [
230
+ ...chat.questions,
231
+ {
232
+ id: `question-${Date.now()}`,
233
+ question: 'Which cache-key strategy should I roll out?',
234
+ createdAt: Date.now(),
235
+ options: [
236
+ { label: 'Route manifest hash', description: 'Deterministic, invalidates on route changes' },
237
+ { label: 'Content hash', description: 'Fine-grained, more churn' },
238
+ ],
108
239
  },
109
- message.id,
110
- );
240
+ ];
111
241
  };
112
242
 
113
243
  const askPermission = (event: MouseEvent) => {
@@ -161,9 +291,23 @@ export const demoFunc = () => html`
161
291
  console.log('harness-permission-response', event.detail);
162
292
  const chat = (event.target as HTMLElement).closest('.chatDemo')?.querySelector('dees-harness-chat');
163
293
  if (chat) {
164
- setTimeout(() => {
165
- chat.permissions = chat.permissions.filter((request) => request.id !== event.detail.requestId);
166
- }, 600);
294
+ // answered cards stay in the transcript showing the outcome
295
+ chat.permissions = chat.permissions.map((request) =>
296
+ request.id === event.detail.requestId
297
+ ? { ...request, response: event.detail.response, respondedAt: Date.now() }
298
+ : request,
299
+ );
300
+ }
301
+ }}
302
+ @harness-question-response=${(event: CustomEvent) => {
303
+ console.log('harness-question-response', event.detail);
304
+ const chat = (event.target as HTMLElement).closest('.chatDemo')?.querySelector('dees-harness-chat');
305
+ if (chat) {
306
+ chat.questions = chat.questions.map((request) =>
307
+ request.id === event.detail.requestId
308
+ ? { ...request, response: event.detail.answers, respondedAt: Date.now() }
309
+ : request,
310
+ );
167
311
  }
168
312
  }}
169
313
  @harness-new-session=${() => console.log('harness-new-session')}
@@ -176,6 +320,7 @@ export const demoFunc = () => html`
176
320
  <div class="controls">
177
321
  <dees-button type="accent" @clicked=${simulate}>Simulate streaming</dees-button>
178
322
  <dees-button @clicked=${askPermission}>Ask permission</dees-button>
323
+ <dees-button @clicked=${askQuestion}>Ask a question</dees-button>
179
324
  <dees-button @clicked=${toggleError}>Toggle error status</dees-button>
180
325
  <dees-button type="ghost" @clicked=${resetChat}>Reset</dees-button>
181
326
  </div>
@@ -184,6 +329,8 @@ export const demoFunc = () => html`
184
329
  subheading="acme-app · main · connected"
185
330
  model="gpt-5.5"
186
331
  reasoningEffort="high"
332
+ mode="Building"
333
+ .modeOptions=${['Planning', 'Building']}
187
334
  .modelOptions=${['gpt-5.5', 'gpt-5', 'o4-mini']}
188
335
  .effortOptions=${['xhigh', 'high', 'medium', 'low']}
189
336
  .usage=${{ inputTokens: 18_400, outputTokens: 3_100, totalTokens: 21_500 }}
@@ -13,15 +13,17 @@ import type {
13
13
  IHarnessAttachment,
14
14
  IHarnessMessage,
15
15
  IHarnessPermissionRequest,
16
+ IHarnessQuestionRequest,
16
17
  IHarnessStatus,
18
+ IHarnessTodoItem,
17
19
  IHarnessUsage,
18
20
  THarnessStreamDelta,
19
21
  } from '../interfaces.js';
22
+ import { harnessParseTodos } from '../harness.helpers.js';
20
23
  import { DeesHarnessToolRegistry } from '../toolregistry.js';
21
24
  import { DeesHarnessMessageList } from '../dees-harness-message-list/dees-harness-message-list.js';
22
25
  import { DeesHarnessComposer } from '../dees-harness-composer/dees-harness-composer.js';
23
- import '../dees-harness-permission-card/dees-harness-permission-card.js';
24
- import '../dees-harness-status/dees-harness-status.js';
26
+ import '../dees-harness-todos/dees-harness-todos.js';
25
27
  import '../dees-harness-usage/dees-harness-usage.js';
26
28
  import '../../00group-button/dees-button/dees-button.js';
27
29
  import '../../00group-utility/dees-icon/dees-icon.js';
@@ -32,9 +34,6 @@ declare global {
32
34
  }
33
35
  }
34
36
 
35
- // stable idle binding for the embedded list — a fresh object literal per
36
- // render would re-trigger its follow logic on every chat render
37
- const idleStatus: IHarnessStatus = { type: 'idle' };
38
37
 
39
38
  /**
40
39
  * The assembled agent chat: optional toolbar, streaming message list,
@@ -55,12 +54,27 @@ export class DeesHarnessChat extends DeesElement {
55
54
  @property({ attribute: false })
56
55
  accessor permissions: IHarnessPermissionRequest[] = [];
57
56
 
57
+ /** agent questions render inline like permissions; answered ones persist */
58
+ @property({ attribute: false })
59
+ accessor questions: IHarnessQuestionRequest[] = [];
60
+
58
61
  @property({ attribute: false })
59
62
  accessor status: IHarnessStatus = { type: 'idle' };
60
63
 
61
64
  @property({ attribute: false })
62
65
  accessor usage: IHarnessUsage = {};
63
66
 
67
+ /**
68
+ * Current task list for the side panel. When empty, the panel derives it
69
+ * from the newest todo tool call in `messages` (registry kind 'todo'), so
70
+ * hosts that stream todowrite calls get the sidebar for free.
71
+ */
72
+ @property({ attribute: false })
73
+ accessor todos: IHarnessTodoItem[] = [];
74
+
75
+ @property({ type: Boolean })
76
+ accessor showTodosPanel: boolean = true;
77
+
64
78
  @property()
65
79
  accessor heading: string = '';
66
80
 
@@ -101,6 +115,13 @@ export class DeesHarnessChat extends DeesElement {
101
115
  @property({ attribute: false })
102
116
  accessor effortOptions: string[] = [];
103
117
 
118
+ /** agent mode switcher (e.g. Planning / Building); empty hides it */
119
+ @property()
120
+ accessor mode: string = '';
121
+
122
+ @property({ attribute: false })
123
+ accessor modeOptions: string[] = [];
124
+
104
125
  @property({ attribute: false })
105
126
  accessor toolRegistry: DeesHarnessToolRegistry = DeesHarnessToolRegistry.default;
106
127
 
@@ -116,6 +137,32 @@ export class DeesHarnessChat extends DeesElement {
116
137
  font-family: var(--dees-font-family);
117
138
  color: var(--dees-color-text-primary);
118
139
  background: var(--dees-color-bg-primary);
140
+ container-type: inline-size;
141
+ }
142
+
143
+ .chatBody {
144
+ flex: 1;
145
+ min-height: 0;
146
+ display: flex;
147
+ }
148
+
149
+ .todosPanel {
150
+ flex: 0 0 232px;
151
+ min-width: 0;
152
+ overflow-y: auto;
153
+ scrollbar-width: thin;
154
+ scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
155
+ padding: var(--dees-spacing-sm) var(--dees-spacing-md);
156
+ border-left: 1px solid var(--dees-color-border-subtle);
157
+ background: var(--dees-color-bg-secondary);
158
+ }
159
+
160
+ /* narrow hosts (e.g. a sidebar chat) drop the panel; todo tool cards
161
+ in the transcript still carry the same information */
162
+ @container (max-width: 520px) {
163
+ .todosPanel {
164
+ display: none;
165
+ }
119
166
  }
120
167
 
121
168
  .toolbar {
@@ -126,6 +173,10 @@ export class DeesHarnessChat extends DeesElement {
126
173
  padding: var(--dees-spacing-sm) var(--dees-spacing-md);
127
174
  border-bottom: 1px solid var(--dees-color-border-subtle);
128
175
  background: var(--dees-color-bg-secondary);
176
+ /* chrome floats above the scrolling transcript */
177
+ position: relative;
178
+ z-index: 1;
179
+ box-shadow: var(--dees-shadow-sm);
129
180
  }
130
181
 
131
182
  .headingBlock {
@@ -158,37 +209,18 @@ export class DeesHarnessChat extends DeesElement {
158
209
  flex: 0 0 auto;
159
210
  }
160
211
 
161
- dees-harness-message-list {
212
+ .chatBody dees-harness-message-list {
162
213
  flex: 1;
163
214
  min-height: 0;
164
215
  }
165
216
 
166
- .permissionQueue {
167
- flex: 0 0 auto;
168
- display: block;
169
- padding: 0 var(--dees-spacing-md);
170
- max-height: 40%;
171
- overflow-y: auto;
172
- }
173
-
174
- .permissionQueue > * + * {
175
- margin-top: var(--dees-spacing-xs);
176
- }
177
-
178
- /* always mounted so the status leave transition can play; the status
179
- element renders empty when idle, collapsing the row to zero height */
180
- .statusRow {
181
- flex: 0 0 auto;
182
- padding: 0 var(--dees-spacing-md);
183
- }
184
-
185
- .statusRow dees-harness-status {
186
- margin: 0;
187
- }
188
-
217
+ /* the composer docks edge-to-edge like the toolbar, so the tasks
218
+ panel's border meets its top border cleanly; z-order keeps its
219
+ upward shadow above the transcript */
189
220
  .composerRow {
190
221
  flex: 0 0 auto;
191
- padding: var(--dees-spacing-sm) var(--dees-spacing-md) var(--dees-spacing-md);
222
+ position: relative;
223
+ z-index: 1;
192
224
  }
193
225
  `,
194
226
  ];
@@ -214,6 +246,19 @@ export class DeesHarnessChat extends DeesElement {
214
246
  this.listElement?.scrollToBottom(force);
215
247
  }
216
248
 
249
+ public get effectiveTodos(): IHarnessTodoItem[] {
250
+ if (this.todos.length) return this.todos;
251
+ // the newest todo call is the current state — an empty write clears
252
+ // the panel instead of falling back to an older list
253
+ for (let index = this.messages.length - 1; index >= 0; index--) {
254
+ const call = this.messages[index]?.toolCall;
255
+ if (!call) continue;
256
+ if (this.toolRegistry.resolve(call).kind !== 'todo') continue;
257
+ return harnessParseTodos(call.input);
258
+ }
259
+ return [];
260
+ }
261
+
217
262
  private get listElement(): DeesHarnessMessageList | undefined {
218
263
  const element = this.shadowRoot?.querySelector('dees-harness-message-list');
219
264
  return element instanceof DeesHarnessMessageList ? element : undefined;
@@ -229,28 +274,27 @@ export class DeesHarnessChat extends DeesElement {
229
274
  public render(): TemplateResult {
230
275
  return html`
231
276
  ${this.showToolbar ? this.renderToolbar() : ''}
232
- <dees-harness-message-list
233
- .messages=${this.messages}
234
- .status=${idleStatus}
235
- .showRoles=${this.showRoles}
236
- .toolRegistry=${this.toolRegistry}
237
- ></dees-harness-message-list>
238
- ${this.permissions.length
239
- ? html`
240
- <div class="permissionQueue">
241
- ${this.permissions.map(
242
- (request) => html`
243
- <dees-harness-permission-card .request=${request} .busy=${this.disabled}></dees-harness-permission-card>
244
- `,
245
- )}
246
- </div>
247
- `
248
- : ''}
249
- <div class="statusRow">
250
- <dees-harness-status .status=${this.status}></dees-harness-status>
277
+ <div class="chatBody">
278
+ <dees-harness-message-list
279
+ .messages=${this.messages}
280
+ .permissions=${this.permissions}
281
+ .permissionsBusy=${this.disabled}
282
+ .questions=${this.questions}
283
+ .status=${this.status}
284
+ .showRoles=${this.showRoles}
285
+ .toolRegistry=${this.toolRegistry}
286
+ ></dees-harness-message-list>
287
+ ${this.showTodosPanel && this.effectiveTodos.length
288
+ ? html`
289
+ <aside class="todosPanel">
290
+ <dees-harness-todos heading="Tasks" .todos=${this.effectiveTodos}></dees-harness-todos>
291
+ </aside>
292
+ `
293
+ : ''}
251
294
  </div>
252
295
  <div class="composerRow">
253
296
  <dees-harness-composer
297
+ docked
254
298
  .value=${this.composerValue}
255
299
  .attachments=${this.attachments}
256
300
  .disabled=${this.disabled}
@@ -260,6 +304,8 @@ export class DeesHarnessChat extends DeesElement {
260
304
  .modelOptions=${this.modelOptions}
261
305
  .reasoningEffort=${this.reasoningEffort}
262
306
  .effortOptions=${this.effortOptions}
307
+ .mode=${this.mode}
308
+ .modeOptions=${this.modeOptions}
263
309
  @harness-input=${(event: CustomEvent<{ value: string }>) => {
264
310
  this.composerValue = event.detail?.value ?? '';
265
311
  }}
@@ -270,6 +316,9 @@ export class DeesHarnessChat extends DeesElement {
270
316
  if (event.detail?.model) this.model = event.detail.model;
271
317
  if (event.detail?.reasoningEffort) this.reasoningEffort = event.detail.reasoningEffort;
272
318
  }}
319
+ @harness-mode-change=${(event: CustomEvent<{ mode?: string }>) => {
320
+ if (event.detail?.mode) this.mode = event.detail.mode;
321
+ }}
273
322
  ></dees-harness-composer>
274
323
  </div>
275
324
  `;
@@ -30,11 +30,14 @@ export const demoFunc = () => html`
30
30
  @harness-attachments-change=${logEvent}
31
31
  @harness-attachment-error=${logEvent}
32
32
  @harness-model-change=${logEvent}
33
+ @harness-mode-change=${logEvent}
33
34
  >
34
- <div class="label">default with model controls (events log to console)</div>
35
+ <div class="label">default with mode switcher + model controls (events log to console)</div>
35
36
  <dees-harness-composer
36
37
  model="gpt-5.5"
37
38
  reasoningEffort="high"
39
+ mode="Planning"
40
+ .modeOptions=${['Planning', 'Building']}
38
41
  .modelOptions=${['gpt-5.5', 'gpt-5', 'o4-mini']}
39
42
  .effortOptions=${['xhigh', 'high', 'medium', 'low']}
40
43
  ></dees-harness-composer>