@design.estate/dees-catalog 9.0.0 → 9.1.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 (38) hide show
  1. package/dist_bundle/bundle.js +2636 -2150
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +1 -1
  4. package/dist_ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.d.ts +7 -2
  5. package/dist_ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js +56 -5
  6. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.d.ts +1 -0
  7. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +62 -18
  8. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.d.ts +4 -2
  9. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.js +7 -2
  10. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.js +69 -29
  11. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +6 -1
  12. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.d.ts +2 -0
  13. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +419 -83
  14. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +247 -118
  15. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.js +2 -2
  16. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/renderers.js +2 -4
  17. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +15 -4
  18. package/dist_ts_web/elements/00group-harness/toolregistry.d.ts +2 -1
  19. package/dist_ts_web/elements/00group-harness/toolregistry.js +1 -1
  20. package/dist_ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.d.ts +1 -1
  21. package/dist_ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.js +30 -12
  22. package/dist_ts_web/elements/00theme.js +3 -1
  23. package/package.json +1 -1
  24. package/readme.md +19 -3
  25. package/readme.plan.md +97 -0
  26. package/ts_web/00_commitinfo_data.ts +1 -1
  27. package/ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.ts +48 -4
  28. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +62 -17
  29. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.ts +6 -1
  30. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.ts +60 -27
  31. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +341 -88
  32. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +257 -125
  33. package/ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts +1 -1
  34. package/ts_web/elements/00group-harness/dees-harness-tool-card/renderers.ts +1 -3
  35. package/ts_web/elements/00group-harness/interfaces.ts +16 -4
  36. package/ts_web/elements/00group-harness/toolregistry.ts +2 -1
  37. package/ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.ts +29 -15
  38. package/ts_web/elements/00theme.ts +2 -0
@@ -1,37 +1,63 @@
1
- import { html } from '@design.estate/dees-element';
2
- import { demoPage } from '../../../demos/templates.js';
1
+ import { DeesElement, css, customElement, directives, html, state } from '@design.estate/dees-element';
2
+ import { demoApp, demoOutput, demoStyles, showDemoResult } from '../../../demos/templates.js';
3
+ import { themeDefaultStyles } from '../../00theme.js';
4
+ import { harnessPillStyles } from '../harness.styles.js';
5
+ import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
6
+ import '../dees-harness-question-card/dees-harness-question-card.js';
7
+ import '../dees-harness-permission-card/dees-harness-permission-card.js';
8
+ import type { DeesHarnessSessionList } from './dees-harness-session-list.js';
3
9
  import type {
4
10
  IHarnessResourceAssociation,
5
11
  IHarnessResourceContextDetail,
6
12
  IHarnessResourceEventDetail,
7
13
  IHarnessResourceMeta,
8
14
  IHarnessSessionGroup,
15
+ IHarnessSessionContextDetail,
9
16
  IHarnessSessionMeta,
10
17
  IHarnessSessionMoveDetail,
18
+ IHarnessQuestionResponseDetail,
19
+ IHarnessPermissionResponseDetail,
11
20
  THarnessResourceAssociationRequestDetail,
12
21
  } from '../interfaces.js';
13
22
 
14
- const demoSessions: IHarnessSessionMeta[] = Array.from({ length: 24 }, (_, index) => ({
23
+ type TDemoStage = 'idle' | 'starting' | 'working' | 'feedback' | 'approval' | 'finished' | 'error' | 'paused';
24
+ interface IDemoSession extends IHarnessSessionMeta {
25
+ stage: TDemoStage;
26
+ outcome?: string;
27
+ }
28
+
29
+ const stages: Record<TDemoStage, { state: NonNullable<IHarnessSessionMeta['state']>; label: string; description: string }> = {
30
+ idle: { state: 'normal', label: 'Ready to start', description: 'The brief is ready. Start the session to prepare the workspace.' },
31
+ starting: { state: 'working', label: 'Starting', description: 'Opening the workspace and reading the project instructions.' },
32
+ working: { state: 'working', label: 'Working', description: 'The agent is inspecting the changes and running focused checks.' },
33
+ feedback: { state: 'attention', label: 'Waiting for feedback', description: 'Work is paused at a decision. Your answer will let the agent continue.' },
34
+ approval: { state: 'attention', label: 'Approval needed', description: 'The agent has prepared the next action and is waiting for permission.' },
35
+ finished: { state: 'finished', label: 'Completed', description: 'The changes and focused checks are complete. The result is ready to review.' },
36
+ error: { state: 'error', label: 'Run failed', description: 'The model endpoint rejected the request. Retry when the connection is ready.' },
37
+ paused: { state: 'normal', label: 'Paused', description: 'Permission was declined. The requested action did not run.' },
38
+ };
39
+
40
+ const demoSessions = (): IDemoSession[] => [
41
+ { title: 'Fix sitemap cache key', harness: { id: 'codex', name: 'Codex' }, stage: 'approval' as const },
42
+ { title: 'Investigate model rate limits', harness: { id: 'opencode', name: 'OpenCode' }, stage: 'working' as const },
43
+ { title: 'Refactor workbench layout', harness: { id: 'flexharness', name: 'FlexHarness' }, stage: 'feedback' as const },
44
+ { title: 'Add adaptive code diffs', harness: { id: 'codex', name: 'Codex' }, stage: 'finished' as const },
45
+ { title: 'Prepare the catalog release', harness: { id: 'opencode', name: 'OpenCode' }, stage: 'error' as const },
46
+ { title: 'Improve agent onboarding', harness: { id: 'flexharness', name: 'FlexHarness' }, stage: 'idle' as const },
47
+ { title: 'Draft the migration guide', harness: { id: 'codex', name: 'Codex' }, stage: 'starting' as const },
48
+ ].map((session, index) => ({
49
+ ...session,
15
50
  id: `session-${index}`,
16
- title: [
17
- 'Fix sitemap cache key',
18
- 'Investigate 429s from model endpoint',
19
- 'Refactor workbench layout',
20
- 'Add diff support to codebox',
21
- 'Release 3.91.0',
22
- 'Migrate agent chat to harness group',
23
- ][index % 6],
24
- createdAt: Date.now() - (index + 2) * 3_600_000,
25
- updatedAt: Date.now() - (index + 1) * 3_500_000,
26
- messageCount: 4 + (index % 9),
27
- preview: 'Latest: the regression test asserts the cache key stays stable across…',
28
- state: (['normal', 'working', 'finished', 'attention', 'error'] as const)[index % 5],
29
- working: index % 5 === 1,
51
+ updatedAt: Date.now() - (index + 1) * 60_000,
52
+ messageCount: 4 + index * 3,
53
+ state: stages[session.stage].state,
54
+ statusLabel: stages[session.stage].label,
55
+ preview: stages[session.stage].description,
30
56
  }));
31
57
 
32
58
  const demoGroups: IHarnessSessionGroup[] = [
33
- { id: 'active', name: 'Active work', sessionIds: ['session-0', 'session-1', 'session-2'] },
34
- { id: 'review', name: 'Review', sessionIds: ['session-3', 'session-4'] },
59
+ { id: 'active', name: 'Workspace', sessionIds: ['session-0', 'session-1', 'session-2', 'session-6'] },
60
+ { id: 'review', name: 'Recent', sessionIds: ['session-3', 'session-5', 'session-4'] },
35
61
  ];
36
62
 
37
63
  const demoResources: IHarnessResourceMeta[] = [
@@ -49,7 +75,7 @@ const demoResources: IHarnessResourceMeta[] = [
49
75
  kind: 'file',
50
76
  title: 'Release notes',
51
77
  icon: 'lucide:FileText',
52
- preview: '6.7.0 controlled resource draft',
78
+ preview: 'Catalog migration notes',
53
79
  updatedAt: Date.now() - 180_000,
54
80
  },
55
81
  {
@@ -63,22 +89,15 @@ const demoResources: IHarnessResourceMeta[] = [
63
89
  ];
64
90
 
65
91
  const demoResourceAssociations: IHarnessResourceAssociation[] = [
66
- { resourceId: 'terminal:build', sessionId: 'session-0' },
67
- { resourceId: 'file:release-notes', sessionId: 'session-0' },
92
+ { resourceId: 'terminal:build', sessionId: 'session-4' },
93
+ { resourceId: 'file:release-notes', sessionId: 'session-4' },
68
94
  ];
69
95
 
70
96
  const demoResourceEligibleSessionIds = Object.fromEntries(
71
- demoResources.map((resource) => [resource.id, ['session-0', 'session-1', 'session-2']]),
97
+ demoResources.map((resource) => [resource.id, ['session-0', 'session-1', 'session-2', 'session-4']]),
72
98
  );
73
99
 
74
- type TDemoSessionList = HTMLElement & {
75
- groups: IHarnessSessionGroup[];
76
- resources: IHarnessResourceMeta[];
77
- resourceAssociations: IHarnessResourceAssociation[];
78
- selectedResourceId: string;
79
- selectedSessionId: string;
80
- ungroupedSessionIds: string[];
81
- };
100
+ type TDemoSessionList = DeesHarnessSessionList;
82
101
 
83
102
  const handleSessionSelect = (event: CustomEvent<{ session: IHarnessSessionMeta }>): void => {
84
103
  const list = event.currentTarget as TDemoSessionList;
@@ -87,7 +106,11 @@ const handleSessionSelect = (event: CustomEvent<{ session: IHarnessSessionMeta }
87
106
 
88
107
  const handleSessionMove = (event: CustomEvent<IHarnessSessionMoveDetail>): void => {
89
108
  const list = event.currentTarget as TDemoSessionList;
90
- const { sessionId, groupId, beforeSessionId } = event.detail;
109
+ applySessionMove(list, event.detail);
110
+ };
111
+
112
+ const applySessionMove = (list: TDemoSessionList, detail: IHarnessSessionMoveDetail): void => {
113
+ const { sessionId, groupId, beforeSessionId } = detail;
91
114
  const groups = list.groups.map((group) => ({
92
115
  ...group,
93
116
  sessionIds: group.sessionIds.filter((id) => id !== sessionId),
@@ -103,6 +126,64 @@ const handleSessionMove = (event: CustomEvent<IHarnessSessionMoveDetail>): void
103
126
  list.ungroupedSessionIds = ungroupedSessionIds;
104
127
  };
105
128
 
129
+ const handleSessionOpen = (event: CustomEvent<{ session: IHarnessSessionMeta }>): void => {
130
+ const list = event.currentTarget as TDemoSessionList;
131
+ list.selectedSessionId = event.detail.session.id;
132
+ showDemoResult(list, `Opened “${event.detail.session.title}”.`);
133
+ };
134
+
135
+ const handleSessionContext = async (
136
+ event: CustomEvent<IHarnessSessionContextDetail>,
137
+ onChange: (opened?: boolean) => void = () => {},
138
+ ): Promise<void> => {
139
+ const list = event.currentTarget as TDemoSessionList;
140
+ const { session, originalEvent } = event.detail;
141
+ const invokingControl = originalEvent.composedPath().find(
142
+ (target): target is HTMLButtonElement => target instanceof HTMLButtonElement,
143
+ );
144
+ const currentGroup = list.groups.find((group) => group.sessionIds.includes(session.id));
145
+ // Capture dialog ownership before event dispatch ends: no await before opening.
146
+ const menu = await DeesContextmenu.openContextMenuWithOptions(originalEvent, [
147
+ {
148
+ name: 'Open conversation', iconName: 'lucide:MessageSquare',
149
+ action: async () => {
150
+ list.selectedSessionId = session.id;
151
+ onChange(true);
152
+ showDemoResult(list, `Opened “${session.title}”.`);
153
+ },
154
+ },
155
+ {
156
+ name: 'Show details', iconName: 'lucide:Info',
157
+ action: async () => showDemoResult(list, `${session.title} · ${session.messageCount ?? 0} messages · ${session.state ?? 'normal'}`),
158
+ },
159
+ ...list.groups.filter((group) => group.id !== currentGroup?.id).map((group) => ({
160
+ name: `Move to ${group.name}`, iconName: 'lucide:FolderInput',
161
+ action: async () => {
162
+ applySessionMove(list, { sessionId: session.id, groupId: group.id, index: group.sessionIds.length });
163
+ onChange();
164
+ showDemoResult(list, `Moved “${session.title}” to ${group.name}.`);
165
+ },
166
+ })),
167
+ ...(currentGroup ? [{
168
+ name: 'Move to Ungrouped', iconName: 'lucide:FolderInput',
169
+ action: async () => {
170
+ applySessionMove(list, { sessionId: session.id, groupId: null, index: list.ungroupedSessionIds.length });
171
+ onChange();
172
+ showDemoResult(list, `Moved “${session.title}” to Ungrouped.`);
173
+ },
174
+ }] : []),
175
+ ]);
176
+ menu?.registerGarbageFunction(async () => {
177
+ await list.updateComplete;
178
+ if (!list.isConnected) return;
179
+ // Moving between groups recreates the row; restore its equivalent control.
180
+ const control = invokingControl?.isConnected ? invokingControl : list.shadowRoot?.querySelector<HTMLButtonElement>(
181
+ `[data-session-id="${CSS.escape(session.id)}"] .${invokingControl?.classList.contains('itemPrimary') ? 'itemPrimary' : 'sessionActions'}`,
182
+ );
183
+ control?.focus({ preventScroll: true });
184
+ });
185
+ };
186
+
106
187
  const handleResourceAssociation = (
107
188
  event: CustomEvent<THarnessResourceAssociationRequestDetail>,
108
189
  ): void => {
@@ -123,9 +204,8 @@ const handleResourceContext = async (
123
204
  event: CustomEvent<IHarnessResourceContextDetail>,
124
205
  ): Promise<void> => {
125
206
  const list = event.currentTarget as TDemoSessionList;
126
- const { DeesContextmenu } = await import('../../00group-overlay/dees-contextmenu/dees-contextmenu.js');
127
207
  const currentSessionId = event.detail.sessionId;
128
- const eligibleSessionIds = ['session-0', 'session-1', 'session-2'];
208
+ const eligibleSessionIds = list.eligibleSessionIdsByResourceId[event.detail.resource.id] ?? [];
129
209
  const menu = await DeesContextmenu.openContextMenuWithOptions(
130
210
  new MouseEvent('contextmenu', {
131
211
  clientX: event.detail.clientX,
@@ -136,7 +216,7 @@ const handleResourceContext = async (
136
216
  .filter((sessionId) => sessionId !== currentSessionId)
137
217
  .map((sessionId) => ({
138
218
  name: `${currentSessionId ? 'Reassign' : 'Attach'} to ${
139
- demoSessions.find((session) => session.id === sessionId)?.title
219
+ list.sessions.find((session) => session.id === sessionId)?.title
140
220
  }`,
141
221
  iconName: 'lucide:Link',
142
222
  action: async () => {
@@ -159,73 +239,246 @@ const handleResourceContext = async (
159
239
  if (!menu) {
160
240
  event.detail.close();
161
241
  } else {
162
- const observer = new MutationObserver(() => {
163
- if (menu.isConnected) return;
164
- observer.disconnect();
165
- event.detail.close();
166
- });
167
- observer.observe(document.body, { childList: true });
242
+ menu.registerGarbageFunction(() => event.detail.close());
168
243
  }
169
244
  list.selectedResourceId = event.detail.resource.id;
170
245
  };
171
246
 
172
- const openModal = async () => {
247
+ const openModal = async (sessions: IHarnessSessionMeta[], groups: IHarnessSessionGroup[]) => {
173
248
  const { DeesModal } = await import('../../00group-overlay/dees-modal/dees-modal.js');
174
249
  const content = document.createElement('dees-harness-session-list');
175
- content.sessions = demoSessions;
250
+ content.sessions = sessions;
251
+ content.groups = groups;
252
+ content.ungroupedSessionIds = [];
253
+ content.enableGrouping = true;
176
254
  content.style.height = '420px';
177
255
  content.addEventListener('harness-session-select', handleSessionSelect as EventListener);
178
- content.addEventListener('harness-session-open', (event) => {
179
- console.log('open session', (event as CustomEvent).detail.session);
256
+ content.addEventListener('harness-session-open', handleSessionOpen as EventListener);
257
+ content.addEventListener('harness-session-context', (event) => {
258
+ void handleSessionContext(event as CustomEvent<IHarnessSessionContextDetail>);
180
259
  });
260
+ content.addEventListener('harness-session-move', handleSessionMove as EventListener);
181
261
  DeesModal.createAndShow({
182
262
  heading: 'Conversations',
183
- content: html`${content}`,
263
+ content: html`<style>${demoStyles}</style><div class="dees-demo-stack">${content}${demoOutput('Choose a conversation action.')}</div>`,
184
264
  menuOptions: [],
185
265
  });
186
266
  };
187
267
 
188
- export const demoFunc = () => demoPage('Sessions', 'Select, organize and attach resources to conversations. Selection keeps the layout steady.', html`
189
- <style>
190
- .sessionDemo {
191
- display: grid;
192
- grid-template-rows: auto auto minmax(0, 1fr);
193
- gap: 12px;
194
- height: 640px;
195
- width: min(100%, 360px);
196
- box-sizing: border-box;
197
- padding: 12px;
198
- border-radius: var(--dees-radius-xl);
199
- border: 1px solid var(--dees-color-border-subtle);
200
- background: var(--dees-color-bg-canvas);
268
+ @customElement('dees-demo-session-workspace')
269
+ class DeesDemoSessionWorkspace extends DeesElement {
270
+ @state() private accessor sessions = demoSessions();
271
+ @state() private accessor selectedSessionId = 'session-0';
272
+ @state() private accessor groups = demoGroups.map((group) => ({ ...group, sessionIds: [...group.sessionIds] }));
273
+ @state() private accessor ungroupedSessionIds: string[] = [];
274
+ @state() private accessor associations = [...demoResourceAssociations];
275
+ @state() private accessor notice = 'Interactive demo · Changes stay in this example.';
276
+ @state() private accessor showDetails = false;
277
+
278
+ public static styles = [themeDefaultStyles, demoStyles, harnessPillStyles, css`
279
+ :host { display: flex; flex-direction: column; height: 100%; min-height: 0; gap: 16px; container-type: inline-size; }
280
+ * { box-sizing: border-box; }
281
+ .topbar, .toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
282
+ .topbar { justify-content: space-between; }
283
+ h1 { margin: 0; font-size: 22px; font-weight: 620; letter-spacing: -.025em; }
284
+ .subtitle { margin: 4px 0 0; font-size: 12px; color: var(--dees-color-text-secondary); }
285
+ .workspace { display: grid; grid-template-columns: minmax(330px, 39%) minmax(0, 1fr); flex: 1; min-height: 0; border: 1px solid var(--dees-color-border-subtle); border-radius: var(--dees-radius-2xl); overflow: hidden; background: var(--dees-color-bg-primary); }
286
+ .sidebar { display: flex; flex-direction: column; min-height: 0; min-width: 0; padding-top: 8px; border-right: 1px solid var(--dees-color-border-subtle); background: var(--dees-color-bg-canvas); }
287
+ .sidebarSummary { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; font-size: 12px; font-weight: 600; }
288
+ .attentionSummary { display: inline-flex; align-items: center; gap: 5px; color: var(--dees-color-text-warning); font-size: 11px; }
289
+ dees-harness-session-list { flex: 1; min-height: 0; }
290
+ .sidebar output { margin: 0; padding: 10px 16px; min-height: 44px; font-size: 11px; line-height: 1.5; color: var(--dees-color-text-secondary); border-top: 1px solid var(--dees-color-border-subtle); }
291
+ .detail { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: auto; }
292
+ .detailHeader { padding: 24px 28px 20px; border-bottom: 1px solid var(--dees-color-border-subtle); }
293
+ .identity { font-size: 12px; color: var(--dees-color-text-secondary); margin-bottom: 12px; }
294
+ .detailHeader .harness-pill { text-transform: none; }
295
+ h2 { margin: 0 0 12px; font-size: 23px; line-height: 1.25; font-weight: 600; letter-spacing: -.025em; overflow-wrap: anywhere; }
296
+ h2:focus-visible { outline: 2px solid var(--dees-color-focus-ring); outline-offset: 4px; border-radius: 4px; }
297
+ .detailBody { display: grid; align-content: start; gap: 20px; padding: 24px 28px; flex: 1; }
298
+ .description { font-size: 13px; line-height: 1.65; color: var(--dees-color-text-secondary); margin: 0; }
299
+ .activity { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
300
+ .activity li { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--dees-color-border-subtle); font-size: 12px; color: var(--dees-color-text-secondary); }
301
+ .activity dees-icon { color: var(--dees-color-text-success); }
302
+ .activity .pending dees-icon { color: var(--dees-color-text-muted); }
303
+ .result { border-left: 3px solid var(--dees-color-text-success); padding: 10px 14px; background: var(--dees-color-bg-secondary); font-size: 12px; line-height: 1.6; border-radius: 0 var(--dees-radius-md) var(--dees-radius-md) 0; }
304
+ .error { border-left-color: var(--dees-color-text-error); }
305
+ .scenarioControls { display: grid; gap: 10px; padding: 20px 28px; background: var(--dees-color-bg-canvas); border-top: 1px solid var(--dees-color-border-subtle); }
306
+ .scenarioLabel { font-size: 11px; color: var(--dees-color-text-muted); }
307
+ .back { display: none; }
308
+ @container (max-width: 700px) {
309
+ .workspace { grid-template-columns: minmax(0, 1fr); }
310
+ .detail { display: none; }
311
+ .workspace.showDetails .sidebar { display: none; }
312
+ .workspace.showDetails .detail { display: flex; }
313
+ .sidebar { border-right: 0; }
314
+ .back { display: inline-block; margin-bottom: 14px; }
315
+ .detailHeader, .detailBody, .scenarioControls { padding: 20px; }
316
+ .topbar { gap: 12px; }
317
+ h2 { font-size: 21px; }
201
318
  }
202
- .demoHint {
203
- color: var(--dees-color-text-secondary);
204
- font-size: var(--dees-font-caption1-size, 11px);
319
+ `];
320
+
321
+ private get list(): DeesHarnessSessionList | null {
322
+ return this.shadowRoot?.querySelector('dees-harness-session-list') ?? null;
323
+ }
324
+
325
+ private syncList(): void {
326
+ const list = this.list;
327
+ if (!list) return;
328
+ this.selectedSessionId = list.selectedSessionId;
329
+ this.groups = list.groups;
330
+ this.ungroupedSessionIds = list.ungroupedSessionIds;
331
+ this.associations = list.resourceAssociations;
332
+ }
333
+
334
+ private async select(event: CustomEvent<{ session: IHarnessSessionMeta }>): Promise<void> {
335
+ handleSessionSelect(event);
336
+ this.selectedSessionId = event.detail.session.id;
337
+ await this.revealSession();
338
+ }
339
+
340
+ private async revealSession(): Promise<void> {
341
+ this.showDetails = true;
342
+ await this.updateComplete;
343
+ if (this.list && getComputedStyle(this.list.parentElement!).display === 'none') {
344
+ this.shadowRoot?.querySelector<HTMLElement>('h2')?.focus();
205
345
  }
206
- .sessionDemo dees-harness-session-list { min-height: 0; }
207
- </style>
208
- <div class="sessionDemo">
209
- <dees-button @clicked=${openModal}>Open session browser</dees-button>
210
- <div class="demoHint">
211
- Drag grips to organize sessions and their resources. Use Actions or Shift+F10 to attach or move a resource.
212
- </div>
213
- <dees-harness-session-list
214
- .sessions=${demoSessions}
215
- .groups=${demoGroups}
216
- .ungroupedSessionIds=${demoSessions.slice(5).map((session) => session.id)}
217
- .resources=${demoResources}
218
- .resourceAssociations=${demoResourceAssociations}
219
- .eligibleSessionIdsByResourceId=${demoResourceEligibleSessionIds}
220
- .enableGrouping=${true}
221
- selectedSessionId="session-2"
222
- @harness-session-select=${handleSessionSelect}
223
- @harness-session-move=${handleSessionMove}
224
- @harness-resource-select=${(event: CustomEvent<IHarnessResourceEventDetail>) => {
225
- (event.currentTarget as TDemoSessionList).selectedResourceId = event.detail.resource.id;
226
- }}
227
- @harness-resource-context=${handleResourceContext}
228
- @harness-resource-association-request=${handleResourceAssociation}
229
- ></dees-harness-session-list>
230
- </div>
231
- `);
346
+ }
347
+
348
+ private async advance(stage: TDemoStage, outcome?: string): Promise<void> {
349
+ const selected = this.sessions.find((session) => session.id === this.selectedSessionId)!;
350
+ this.sessions = this.sessions.map((session) => session.id !== selected.id ? session : {
351
+ ...session, stage, state: stages[stage].state, statusLabel: stages[stage].label,
352
+ preview: stages[stage].description, updatedAt: Date.now(), outcome,
353
+ });
354
+ this.notice = `${selected.title} · ${stages[stage].label}${outcome ? ` · ${outcome}` : ''}`;
355
+ await this.updateComplete;
356
+ this.shadowRoot?.querySelector<HTMLElement>('h2')?.focus({ preventScroll: true });
357
+ }
358
+
359
+ private reset(): void {
360
+ this.sessions = demoSessions();
361
+ this.selectedSessionId = 'session-0';
362
+ this.groups = demoGroups.map((group) => ({ ...group, sessionIds: [...group.sessionIds] }));
363
+ this.ungroupedSessionIds = [];
364
+ this.associations = [...demoResourceAssociations];
365
+ this.notice = 'Demo reset. Choose a session or respond to the approval request.';
366
+ if (this.list) {
367
+ this.list.search = '';
368
+ this.list.selectedResourceId = '';
369
+ this.list.expandedSessionIds = new Set();
370
+ this.list.collapsedGroupIds = new Set();
371
+ }
372
+ }
373
+
374
+ public render() {
375
+ const selected = this.sessions.find((session) => session.id === this.selectedSessionId) ?? this.sessions[0];
376
+ const current = stages[selected.stage];
377
+ const attentionCount = this.sessions.filter((session) => session.state === 'attention' || session.state === 'error').length;
378
+ const tone = { normal: 'default', working: 'running', attention: 'warning', error: 'error', finished: 'success' }[current.state];
379
+ return html`
380
+ <header class="topbar">
381
+ <div><h1>Sessions</h1><p class="subtitle">Follow the work. Step in when you’re needed.</p></div>
382
+ <div class="toolbar">
383
+ <dees-button size="sm" @clicked=${() => openModal(this.sessions, this.groups)}>Open session browser</dees-button>
384
+ <dees-button size="sm" type="ghost" @clicked=${this.reset}>Reset demo</dees-button>
385
+ </div>
386
+ </header>
387
+ <div class="workspace ${this.showDetails ? 'showDetails' : ''}">
388
+ <aside class="sidebar" aria-label="Sessions">
389
+ <div class="sidebarSummary"><span>All sessions <span class="scenarioLabel">${this.sessions.length}</span></span>
390
+ <span class="attentionSummary"><dees-icon icon="lucide:CircleAlert" iconSize="12"></dees-icon>${attentionCount} need attention</span>
391
+ </div>
392
+ <dees-harness-session-list
393
+ .sessions=${this.sessions} .groups=${this.groups} .ungroupedSessionIds=${this.ungroupedSessionIds}
394
+ .resources=${demoResources} .resourceAssociations=${this.associations}
395
+ .eligibleSessionIdsByResourceId=${demoResourceEligibleSessionIds} .enableGrouping=${true}
396
+ .selectedSessionId=${this.selectedSessionId}
397
+ @harness-session-select=${this.select} @harness-session-open=${this.select}
398
+ @harness-session-context=${(event: CustomEvent<IHarnessSessionContextDetail>) => handleSessionContext(event, (opened) => {
399
+ this.syncList();
400
+ if (opened) void this.revealSession();
401
+ })}
402
+ @harness-session-move=${(event: CustomEvent<IHarnessSessionMoveDetail>) => { handleSessionMove(event); this.syncList(); }}
403
+ @harness-group-create-request=${() => {
404
+ this.groups = [...this.groups, { id: `group-${this.groups.length}`, name: `Group ${this.groups.length + 1}`, sessionIds: [] }];
405
+ if (this.list) this.list.search = '';
406
+ this.notice = 'Group created. Drag a session here or use its Actions menu.';
407
+ }}
408
+ @harness-resource-select=${(event: CustomEvent<IHarnessResourceEventDetail>) => {
409
+ (event.currentTarget as TDemoSessionList).selectedResourceId = event.detail.resource.id;
410
+ this.notice = `Selected ${event.detail.resource.title}. Use Actions to attach or move it.`;
411
+ }}
412
+ @harness-resource-context=${handleResourceContext}
413
+ @harness-resource-association-request=${(event: CustomEvent<THarnessResourceAssociationRequestDetail>) => { handleResourceAssociation(event); this.syncList(); }}
414
+ ></dees-harness-session-list>
415
+ <output role="status">${this.notice}</output>
416
+ </aside>
417
+ <section class="detail" aria-label="Selected session">
418
+ <header class="detailHeader">
419
+ <dees-button class="back" size="sm" type="ghost" icon="lucide:ChevronLeft" @clicked=${async () => {
420
+ this.showDetails = false;
421
+ await this.updateComplete;
422
+ this.list?.shadowRoot?.querySelector<HTMLElement>(`[data-session-id="${this.selectedSessionId}"] .itemPrimary`)?.focus();
423
+ }}>All sessions</dees-button>
424
+ <div class="identity">${selected.harness?.name} <span aria-hidden="true">·</span> design.estate</div>
425
+ <h2 tabindex="-1">${selected.title}</h2>
426
+ <span class="harness-pill tone-${tone}">${current.label}</span>
427
+ </header>
428
+ <div class="detailBody">
429
+ <p class="description">${current.description}</p>
430
+ ${directives.keyed(`${selected.id}:${selected.stage}:${selected.updatedAt}`, html`
431
+ ${selected.stage === 'feedback' ? html`
432
+ <dees-harness-question-card .suppressEntry=${true} .request=${{
433
+ id: `${selected.id}:feedback`, sessionId: selected.id,
434
+ question: 'How should we handle the compact layout?',
435
+ options: [
436
+ { label: 'Keep a compact sidebar', description: 'Preserve quick access to sessions on wider screens.' },
437
+ { label: 'Use a focused single pane', description: 'Give the active task all available space.' },
438
+ ],
439
+ }} @harness-question-response=${(event: CustomEvent<IHarnessQuestionResponseDetail>) => {
440
+ void this.advance('working', `Feedback received: ${event.detail.answers.join(', ')}`);
441
+ }}></dees-harness-question-card>
442
+ ` : selected.stage === 'approval' ? html`
443
+ <dees-harness-permission-card .suppressEntry=${true} .request=${{
444
+ id: `${selected.id}:approval`, sessionId: selected.id, title: 'Run the test suite',
445
+ type: 'shell', metadata: { command: 'pnpm test', cwd: '/workspace/design.estate' },
446
+ }} @harness-permission-response=${(event: CustomEvent<IHarnessPermissionResponseDetail>) => {
447
+ void this.advance(event.detail.response === 'reject' ? 'paused' : 'working',
448
+ event.detail.response === 'reject' ? 'Permission declined.' : 'Permission granted. Continuing the checks.');
449
+ }}></dees-harness-permission-card>
450
+ ` : ''}
451
+ `)}
452
+ ${selected.outcome ? html`<div class="result">${selected.outcome}</div>` : ''}
453
+ ${selected.stage === 'error' ? html`<div class="result error"><strong>Model request failed</strong><br />HTTP 429 · Request limit reached.<br />No changes were lost.</div>` : ''}
454
+ ${['idle', 'starting', 'working', 'finished'].includes(selected.stage) ? html`
455
+ <ol class="activity" aria-label="Work progress">
456
+ ${['Prepare workspace', 'Inspect and update the implementation', 'Run checks and summarize'].map((label, index) => {
457
+ const complete = selected.stage === 'finished' || (selected.stage === 'working' && index === 0);
458
+ return html`<li class=${complete ? '' : 'pending'}><dees-icon aria-hidden="true" icon=${complete ? 'lucide:CircleCheck' : 'lucide:CircleDashed'} iconSize="16"></dees-icon>${label}${complete ? html`<span class="scenarioLabel">Done</span>` : ''}</li>`;
459
+ })}
460
+ </ol>
461
+ ` : ''}
462
+ </div>
463
+ <footer class="scenarioControls">
464
+ <div class="scenarioLabel">Demo controls · Advance this session</div>
465
+ <div class="toolbar">
466
+ ${selected.stage === 'idle' || selected.stage === 'paused' || selected.stage === 'finished' || selected.stage === 'error' ? html`
467
+ <dees-button size="sm" type="accent" @clicked=${() => this.advance('starting')}>${selected.stage === 'error' ? 'Retry session' : selected.stage === 'finished' ? 'Start another run' : 'Start work'}</dees-button>
468
+ ` : selected.stage === 'starting' ? html`
469
+ <dees-button size="sm" type="accent" @clicked=${() => this.advance('working')}>Begin task</dees-button>
470
+ ` : selected.stage === 'working' ? html`
471
+ <dees-button size="sm" type="accent" @clicked=${() => this.advance('feedback')}>Ask for feedback</dees-button>
472
+ <dees-button size="sm" @clicked=${() => this.advance('approval')}>Request approval</dees-button>
473
+ <dees-button size="sm" @clicked=${() => this.advance('finished')}>Complete work</dees-button>
474
+ <dees-button size="sm" type="ghost" @clicked=${() => this.advance('error')}>Simulate failure</dees-button>
475
+ ` : html`<span class="description">Respond above to continue the session.</span>`}
476
+ </div>
477
+ </footer>
478
+ </section>
479
+ </div>
480
+ `;
481
+ }
482
+ }
483
+
484
+ export const demoFunc = () => demoApp(html`<dees-demo-session-workspace></dees-demo-session-workspace>`);