@design.estate/dees-catalog 5.0.0 → 6.0.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.
@@ -26,6 +26,7 @@ import '../../00group-utility/dees-icon/dees-icon.js';
26
26
  const { repeat } = directives;
27
27
 
28
28
  const formatTokenCount = (valueArg: number): string => valueArg.toLocaleString('en-US');
29
+ type TSessionSidebarSection = 'tasks' | 'metrics' | 'scratchpad' | 'intelligence';
29
30
 
30
31
  declare global {
31
32
  interface HTMLElementTagNameMap {
@@ -90,13 +91,20 @@ export class DeesHarnessSessionSidebar extends DeesElement {
90
91
  @state()
91
92
  private accessor intelligenceQuestion: string = '';
92
93
 
94
+ @state()
95
+ private accessor expandedSection: TSessionSidebarSection | undefined = undefined;
96
+
97
+ private sectionStateInitialized = false;
98
+
93
99
  public static styles = [
94
100
  themeDefaultStyles,
95
101
  cssManager.defaultStyles,
96
102
  css`
97
103
  :host {
98
- display: block;
104
+ display: flex;
105
+ flex-direction: column;
99
106
  min-width: 0;
107
+ min-height: 0;
100
108
  height: 100%;
101
109
  font-family: var(--dees-font-family);
102
110
  color: var(--dees-color-text-primary);
@@ -105,32 +113,206 @@ export class DeesHarnessSessionSidebar extends DeesElement {
105
113
  .sidebar {
106
114
  display: flex;
107
115
  flex-direction: column;
108
- min-height: 100%;
116
+ min-height: 0;
117
+ height: 100%;
118
+ overflow: hidden;
109
119
  }
110
120
 
111
121
  .section {
112
- padding: var(--dees-spacing-sm) var(--dees-spacing-md);
122
+ display: flex;
123
+ flex: 0 0 auto;
124
+ flex-direction: column;
125
+ min-height: 0;
126
+ overflow: hidden;
113
127
  border-bottom: 1px solid var(--dees-color-border-subtle);
114
128
  }
115
129
 
130
+ .section.expanded {
131
+ flex: 1 1 0;
132
+ }
133
+
116
134
  .section:last-child {
117
135
  border-bottom: 0;
118
136
  }
119
137
 
120
138
  .sectionHeading {
121
139
  display: flex;
140
+ flex: 0 0 auto;
122
141
  align-items: center;
123
142
  gap: var(--dees-spacing-xs);
124
- margin-bottom: var(--dees-spacing-sm);
143
+ width: 100%;
144
+ min-height: 30px;
145
+ margin: 0;
146
+ padding: 6px var(--dees-spacing-md) 6px var(--dees-spacing-xs);
147
+ border: 0;
148
+ background: var(--dees-color-bg-secondary);
125
149
  color: var(--dees-color-text-primary);
150
+ font-family: inherit;
126
151
  font-size: var(--dees-font-control-size-sm, 12px);
127
152
  font-weight: 650;
153
+ text-align: left;
154
+ cursor: pointer;
155
+ }
156
+
157
+ .sectionHeading:hover {
158
+ background: var(--dees-color-badge-default-bg);
159
+ }
160
+
161
+ .sectionHeading:focus-visible {
162
+ position: relative;
163
+ z-index: 1;
164
+ outline: 2px solid var(--dees-color-accent-primary);
165
+ outline-offset: -2px;
128
166
  }
129
167
 
130
168
  .sectionHeading dees-icon {
169
+ flex: 0 0 auto;
131
170
  color: var(--dees-color-text-secondary);
132
171
  }
133
172
 
173
+ .sectionHeadingText {
174
+ min-width: 0;
175
+ overflow: hidden;
176
+ text-overflow: ellipsis;
177
+ white-space: nowrap;
178
+ }
179
+
180
+ .sectionSummary {
181
+ flex: 0 0 auto;
182
+ margin-left: auto;
183
+ color: var(--dees-color-text-muted);
184
+ font-size: var(--dees-font-caption2-size, 10px);
185
+ font-variant-numeric: tabular-nums;
186
+ font-weight: 500;
187
+ }
188
+
189
+ .sectionContent {
190
+ flex: 1;
191
+ min-height: 0;
192
+ overflow-x: hidden;
193
+ overflow-y: auto;
194
+ padding: var(--dees-spacing-sm) var(--dees-spacing-md);
195
+ scrollbar-width: thin;
196
+ scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
197
+ }
198
+
199
+ .sectionContent[hidden] {
200
+ display: none;
201
+ }
202
+
203
+ .sectionContent dees-harness-todos {
204
+ min-width: 0;
205
+ }
206
+
207
+ .scratchpadSection .sectionContent:not([hidden]) {
208
+ display: flex;
209
+ flex-direction: column;
210
+ overflow: hidden;
211
+ padding: 0;
212
+ }
213
+
214
+ .scratchpadSection textarea {
215
+ flex: 1;
216
+ min-height: 0;
217
+ resize: none;
218
+ overflow-y: auto;
219
+ padding: var(--dees-spacing-sm) var(--dees-spacing-md);
220
+ border: 0;
221
+ border-radius: 0;
222
+ background: color-mix(in srgb, var(--dees-color-bg-secondary) 92%, black);
223
+ color: var(--dees-color-text-secondary);
224
+ }
225
+
226
+ .scratchpadSection textarea:focus-visible {
227
+ border-color: transparent;
228
+ background: color-mix(in srgb, var(--dees-color-bg-secondary) 92%, black);
229
+ box-shadow: none;
230
+ }
231
+
232
+ .scratchpadSection .conflict,
233
+ .scratchpadSection .error {
234
+ margin-right: var(--dees-spacing-md);
235
+ margin-left: var(--dees-spacing-md);
236
+ }
237
+
238
+ .scratchpadFooter {
239
+ display: flex;
240
+ flex: 0 0 auto;
241
+ align-items: center;
242
+ justify-content: space-between;
243
+ gap: var(--dees-spacing-sm);
244
+ padding: var(--dees-spacing-xs) var(--dees-spacing-md) var(--dees-spacing-sm);
245
+ border-top: 0;
246
+ background: linear-gradient(
247
+ to bottom,
248
+ color-mix(in srgb, var(--dees-color-bg-secondary) 92%, black),
249
+ var(--dees-color-bg-secondary)
250
+ );
251
+ }
252
+
253
+ .scratchpadFooter .editorMeta {
254
+ margin-top: 0;
255
+ }
256
+
257
+ .intelligenceSection .sectionContent:not([hidden]) {
258
+ display: flex;
259
+ flex-direction: column;
260
+ overflow: hidden;
261
+ padding: 0;
262
+ }
263
+
264
+ .intelligenceHistory {
265
+ flex: 1;
266
+ min-height: 0;
267
+ overflow-x: hidden;
268
+ overflow-y: auto;
269
+ padding: var(--dees-spacing-sm) var(--dees-spacing-md);
270
+ scrollbar-width: thin;
271
+ scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
272
+ }
273
+
274
+ .intelligenceComposer {
275
+ flex: 0 0 auto;
276
+ padding: var(--dees-spacing-sm) var(--dees-spacing-md) var(--dees-spacing-md);
277
+ background: linear-gradient(
278
+ to bottom,
279
+ var(--dees-color-bg-primary),
280
+ var(--dees-color-bg-secondary)
281
+ );
282
+ box-shadow: var(--dees-shadow-up-sm);
283
+ }
284
+
285
+ .intelligenceComposer textarea {
286
+ min-height: 64px;
287
+ max-height: 160px;
288
+ resize: none;
289
+ padding: var(--dees-spacing-sm) 0;
290
+ border: 0;
291
+ border-radius: 0;
292
+ background: transparent;
293
+ color: var(--dees-color-text-primary);
294
+ font-family: inherit;
295
+ font-size: var(--dees-font-control-size, 13px);
296
+ }
297
+
298
+ .intelligenceComposer textarea:focus-visible {
299
+ border-color: transparent;
300
+ box-shadow: none;
301
+ outline: 2px solid color-mix(in srgb, var(--dees-color-accent-primary) 55%, transparent);
302
+ outline-offset: 2px;
303
+ }
304
+
305
+ .intelligenceControls {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: var(--dees-spacing-sm);
309
+ }
310
+
311
+ .intelligenceControls .hint {
312
+ flex: 1;
313
+ min-width: 0;
314
+ }
315
+
134
316
  .metricList {
135
317
  display: grid;
136
318
  gap: 6px;
@@ -281,6 +463,16 @@ export class DeesHarnessSessionSidebar extends DeesElement {
281
463
  ];
282
464
 
283
465
  protected willUpdate(changedPropertiesArg: Map<PropertyKey, unknown>): void {
466
+ const availableSections = this.availableSections;
467
+ if (!this.sectionStateInitialized && availableSections.length > 0) {
468
+ this.expandedSection = this.defaultExpandedSection;
469
+ this.sectionStateInitialized = true;
470
+ } else if (
471
+ this.expandedSection !== undefined
472
+ && !availableSections.includes(this.expandedSection)
473
+ ) {
474
+ this.expandedSection = this.defaultExpandedSection;
475
+ }
284
476
  if (!changedPropertiesArg.has('scratchpad')) return;
285
477
  const next = this.scratchpad;
286
478
  if (!next) {
@@ -324,14 +516,70 @@ export class DeesHarnessSessionSidebar extends DeesElement {
324
516
  return Object.values(this.metrics).some((value) => value !== undefined);
325
517
  }
326
518
 
327
- private renderTasks(): TemplateResult {
519
+ private get availableSections(): TSessionSidebarSection[] {
520
+ return [
521
+ ...(this.showTasks ? ['tasks' as const] : []),
522
+ ...(this.hasMetrics ? ['metrics' as const] : []),
523
+ ...(this.scratchpad ? ['scratchpad' as const] : []),
524
+ ...(this.intelligenceEnabled ? ['intelligence' as const] : []),
525
+ ];
526
+ }
527
+
528
+ private get defaultExpandedSection(): TSessionSidebarSection | undefined {
529
+ if (this.showTasks && this.todos.length > 0) return 'tasks';
530
+ if (this.hasMetrics) return 'metrics';
531
+ if (this.scratchpad) return 'scratchpad';
532
+ if (this.intelligenceEnabled) return 'intelligence';
533
+ return this.showTasks ? 'tasks' : undefined;
534
+ }
535
+
536
+ private renderSection(
537
+ idArg: TSessionSidebarSection,
538
+ labelArg: string,
539
+ iconArg: string,
540
+ contentArg: TemplateResult,
541
+ summaryArg = '',
542
+ ): TemplateResult {
543
+ const expanded = this.expandedSection === idArg;
544
+ const contentId = `harness-session-section-${idArg}`;
328
545
  return html`
329
- <section class="section tasksSection">
330
- <dees-harness-todos heading="Tasks" .todos=${this.todos}></dees-harness-todos>
546
+ <section class=${`section ${idArg}Section ${expanded ? 'expanded' : ''}`}>
547
+ <button
548
+ class="sectionHeading"
549
+ type="button"
550
+ aria-expanded=${String(expanded)}
551
+ aria-controls=${contentId}
552
+ @click=${() => {
553
+ this.sectionStateInitialized = true;
554
+ this.expandedSection = expanded ? undefined : idArg;
555
+ }}
556
+ >
557
+ <dees-icon
558
+ icon=${expanded ? 'lucide:ChevronDown' : 'lucide:ChevronRight'}
559
+ iconSize="13"
560
+ ></dees-icon>
561
+ <dees-icon icon=${iconArg} iconSize="13"></dees-icon>
562
+ <span class="sectionHeadingText">${labelArg}</span>
563
+ ${summaryArg ? html`<span class="sectionSummary">${summaryArg}</span>` : ''}
564
+ </button>
565
+ <div id=${contentId} class="sectionContent" ?hidden=${!expanded}>
566
+ ${contentArg}
567
+ </div>
331
568
  </section>
332
569
  `;
333
570
  }
334
571
 
572
+ private renderTasks(): TemplateResult {
573
+ const completed = this.todos.filter((todo) => todo.status === 'completed').length;
574
+ return this.renderSection(
575
+ 'tasks',
576
+ 'Tasks',
577
+ 'lucide:ListChecks',
578
+ html`<dees-harness-todos .todos=${this.todos} .showProgress=${false}></dees-harness-todos>`,
579
+ this.todos.length > 0 ? `${completed}/${this.todos.length}` : '',
580
+ );
581
+ }
582
+
335
583
  private renderMetrics(): TemplateResult {
336
584
  const metrics = this.metrics;
337
585
  const percentage = this.contextPercentage;
@@ -342,12 +590,11 @@ export class DeesHarnessSessionSidebar extends DeesElement {
342
590
  ['Context window', metrics.maxContextTokens],
343
591
  ['Current context', metrics.currentContextTokens],
344
592
  ];
345
- return html`
346
- <section class="section metricsSection">
347
- <div class="sectionHeading">
348
- <dees-icon icon="lucide:Gauge" iconSize="13"></dees-icon>
349
- <span>Tokens</span>
350
- </div>
593
+ return this.renderSection(
594
+ 'metrics',
595
+ 'Tokens',
596
+ 'lucide:Gauge',
597
+ html`
351
598
  <div class="metricList">
352
599
  ${rows.map(([label, value]) => value === undefined ? '' : html`
353
600
  <div class="metricRow">
@@ -372,19 +619,17 @@ export class DeesHarnessSessionSidebar extends DeesElement {
372
619
  </div>
373
620
  `}
374
621
  </div>
375
- </section>
376
- `;
622
+ `,
623
+ );
377
624
  }
378
625
 
379
626
  private renderScratchpad(): TemplateResult {
380
627
  const scratchpad = this.scratchpad!;
381
- const updatedBy = scratchpad.updatedBy === 'intelligence' ? 'Session Intelligence' : 'you';
382
- return html`
383
- <section class="section scratchpadSection">
384
- <div class="sectionHeading">
385
- <dees-icon icon="lucide:NotebookPen" iconSize="13"></dees-icon>
386
- <span>Session Scratchpad</span>
387
- </div>
628
+ return this.renderSection(
629
+ 'scratchpad',
630
+ 'Session Scratchpad',
631
+ 'lucide:NotebookPen',
632
+ html`
388
633
  <textarea
389
634
  aria-label="Session scratchpad"
390
635
  maxlength="32768"
@@ -401,11 +646,6 @@ export class DeesHarnessSessionSidebar extends DeesElement {
401
646
  }
402
647
  }}
403
648
  ></textarea>
404
- <div class="editorMeta">
405
- Revision ${scratchpad.revision}${scratchpad.updatedAt
406
- ? ` · updated by ${updatedBy} ${new Date(scratchpad.updatedAt).toLocaleString()}`
407
- : ''}
408
- </div>
409
649
  ${this.scratchpadStale ? html`
410
650
  <div class="conflict" role="status">
411
651
  A newer revision arrived. Keep this draft on the latest revision or discard it.
@@ -426,7 +666,8 @@ export class DeesHarnessSessionSidebar extends DeesElement {
426
666
  </div>
427
667
  ` : ''}
428
668
  ${this.scratchpadError ? html`<div class="error" role="alert">${this.scratchpadError}</div>` : ''}
429
- <div class="actions">
669
+ <div class="scratchpadFooter">
670
+ <span class="editorMeta">rev. ${scratchpad.revision}</span>
430
671
  <dees-button
431
672
  size="sm"
432
673
  type="accent"
@@ -436,57 +677,61 @@ export class DeesHarnessSessionSidebar extends DeesElement {
436
677
  @clicked=${this.saveScratchpad}
437
678
  ></dees-button>
438
679
  </div>
439
- </section>
440
- `;
680
+ `,
681
+ );
441
682
  }
442
683
 
443
684
  private renderIntelligence(): TemplateResult {
444
- return html`
445
- <section class="section intelligenceSection">
446
- <div class="sectionHeading">
447
- <dees-icon icon="lucide:Sparkles" iconSize="13"></dees-icon>
448
- <span>Session Intelligence</span>
685
+ return this.renderSection(
686
+ 'intelligence',
687
+ 'Session Intelligence',
688
+ 'lucide:Sparkles',
689
+ html`
690
+ <div class="intelligenceHistory" role="log" aria-live="polite">
691
+ ${this.intelligenceExchanges.length ? html`
692
+ <div class="exchangeList">
693
+ ${repeat(
694
+ this.intelligenceExchanges,
695
+ (exchange) => exchange.id,
696
+ (exchange) => this.renderExchange(exchange),
697
+ )}
698
+ </div>
699
+ ` : html`<div class="empty">Ask about this session without interrupting its main work.</div>`}
449
700
  </div>
450
- ${this.intelligenceExchanges.length ? html`
451
- <div class="exchangeList" role="log" aria-live="polite">
452
- ${repeat(
453
- this.intelligenceExchanges,
454
- (exchange) => exchange.id,
455
- (exchange) => this.renderExchange(exchange),
456
- )}
701
+ <div class="intelligenceComposer">
702
+ <textarea
703
+ class="intelligenceInput"
704
+ aria-label="Ask Session Intelligence"
705
+ placeholder="Ask a parallel question…"
706
+ maxlength="8192"
707
+ rows="2"
708
+ .value=${this.intelligenceQuestion}
709
+ ?disabled=${this.disabled || this.intelligenceBusy}
710
+ @input=${(event: InputEvent) => {
711
+ this.intelligenceQuestion = (event.currentTarget as HTMLTextAreaElement).value;
712
+ }}
713
+ @keydown=${(event: KeyboardEvent) => {
714
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
715
+ event.preventDefault();
716
+ this.askIntelligence();
717
+ }
718
+ }}
719
+ ></textarea>
720
+ <div class="intelligenceControls">
721
+ <span class="hint">Parallel session · Ctrl/⌘ + Enter</span>
722
+ <dees-button
723
+ size="sm"
724
+ type="accent"
725
+ text="Ask"
726
+ .status=${this.intelligenceBusy ? 'pending' : 'normal'}
727
+ ?disabled=${this.disabled || this.intelligenceBusy || !this.intelligenceQuestion.trim()}
728
+ @clicked=${this.askIntelligence}
729
+ ></dees-button>
457
730
  </div>
458
- ` : html`<div class="empty">Ask about this session without interrupting its main work.</div>`}
459
- <textarea
460
- class="intelligenceInput"
461
- aria-label="Ask Session Intelligence"
462
- placeholder="Ask a parallel question…"
463
- maxlength="8192"
464
- .value=${this.intelligenceQuestion}
465
- ?disabled=${this.disabled || this.intelligenceBusy}
466
- @input=${(event: InputEvent) => {
467
- this.intelligenceQuestion = (event.currentTarget as HTMLTextAreaElement).value;
468
- }}
469
- @keydown=${(event: KeyboardEvent) => {
470
- if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
471
- event.preventDefault();
472
- this.askIntelligence();
473
- }
474
- }}
475
- ></textarea>
476
- <div class="hint">Runs separately from the main session. Ctrl/⌘ + Enter to ask.</div>
477
- ${this.intelligenceError ? html`<div class="error" role="alert">${this.intelligenceError}</div>` : ''}
478
- <div class="actions">
479
- <dees-button
480
- size="sm"
481
- type="accent"
482
- text="Ask"
483
- .status=${this.intelligenceBusy ? 'pending' : 'normal'}
484
- ?disabled=${this.disabled || this.intelligenceBusy || !this.intelligenceQuestion.trim()}
485
- @clicked=${this.askIntelligence}
486
- ></dees-button>
731
+ ${this.intelligenceError ? html`<div class="error" role="alert">${this.intelligenceError}</div>` : ''}
487
732
  </div>
488
- </section>
489
- `;
733
+ `,
734
+ );
490
735
  }
491
736
 
492
737
  private renderExchange(exchangeArg: IHarnessIntelligenceExchange): TemplateResult {
@@ -69,6 +69,9 @@ export class DeesHarnessToolCard extends DeesElement {
69
69
  @state()
70
70
  accessor returnFlash: boolean = false;
71
71
 
72
+ @state()
73
+ accessor outcomeScanTone: 'success' | 'error' | null = null;
74
+
72
75
  private bodyResizeObserver: ResizeObserver | null = null;
73
76
  private observedBodyInner: Element | null = null;
74
77
  private fullscreenPortal: HTMLElementTagNameMap['dees-harness-tool-fullscreen'] | null = null;
@@ -332,6 +335,30 @@ export class DeesHarnessToolCard extends DeesElement {
332
335
  transform-origin: center;
333
336
  }
334
337
 
338
+ .harness-card.outcomeScan {
339
+ --harness-outcome-scan-color: var(--dees-color-accent-success);
340
+ }
341
+
342
+ .harness-card.outcomeScan-error {
343
+ --harness-outcome-scan-color: var(--dees-color-accent-error);
344
+ }
345
+
346
+ .harness-card.outcomeScan::after {
347
+ content: '';
348
+ position: absolute;
349
+ inset: 0;
350
+ pointer-events: none;
351
+ background: linear-gradient(
352
+ 105deg,
353
+ transparent 40%,
354
+ color-mix(in srgb, var(--harness-outcome-scan-color) 3%, transparent) 45%,
355
+ color-mix(in srgb, var(--harness-outcome-scan-color) 14%, transparent) 50%,
356
+ color-mix(in srgb, var(--harness-outcome-scan-color) 3%, transparent) 55%,
357
+ transparent 60%
358
+ ) 100% 0 / 250% 100% no-repeat;
359
+ animation: harness-tool-outcome-scan 0.48s linear 0.08s both;
360
+ }
361
+
335
362
  @keyframes harness-card-return-flash {
336
363
  0%, 100% {
337
364
  transform: scale(1);
@@ -341,10 +368,20 @@ export class DeesHarnessToolCard extends DeesElement {
341
368
  }
342
369
  }
343
370
 
371
+ @keyframes harness-tool-outcome-scan {
372
+ from { background-position: 100% 0; }
373
+ to { background-position: 0 0; }
374
+ }
375
+
344
376
  @media (prefers-reduced-motion: reduce) {
345
377
  .harness-card.returnFlash {
346
378
  animation: none;
347
379
  }
380
+
381
+ .harness-card.outcomeScan::after {
382
+ animation: none;
383
+ opacity: 0;
384
+ }
348
385
  }
349
386
  `,
350
387
  ];
@@ -376,10 +413,12 @@ export class DeesHarnessToolCard extends DeesElement {
376
413
  const statusTone = call.isError ? 'error' : call.status === 'completed' ? 'success' : call.status;
377
414
  const duration = harnessFormatDuration(call.startedAt, call.finishedAt);
378
415
  return html`
379
- <div class="harness-card status-${call.isError ? 'error' : call.status} ${descriptor.destructive ? 'destructive' : ''} ${this.returnFlash ? 'returnFlash' : ''}"
416
+ <div class="harness-card status-${call.isError ? 'error' : call.status} ${descriptor.destructive ? 'destructive' : ''} ${this.returnFlash ? 'returnFlash' : ''} ${this.outcomeScanTone ? `outcomeScan outcomeScan-${this.outcomeScanTone}` : ''}"
380
417
  @animationend=${(event: AnimationEvent) => {
381
418
  if (event.animationName === 'harness-card-return-flash') {
382
419
  this.endReturnFlash();
420
+ } else if (event.animationName === 'harness-tool-outcome-scan') {
421
+ this.endOutcomeScan();
383
422
  }
384
423
  }}
385
424
  >
@@ -444,6 +483,7 @@ export class DeesHarnessToolCard extends DeesElement {
444
483
  };
445
484
 
446
485
  private flashFallbackTimeout: ReturnType<typeof setTimeout> | null = null;
486
+ private outcomeScanFallbackTimeout: ReturnType<typeof setTimeout> | null = null;
447
487
 
448
488
  private readonly closeFullscreen = (): void => {
449
489
  const portal = this.fullscreenPortal;
@@ -480,8 +520,42 @@ export class DeesHarnessToolCard extends DeesElement {
480
520
  }));
481
521
  }
482
522
 
523
+ private startOutcomeScan(toneArg: 'success' | 'error'): void {
524
+ if (this.outcomeScanFallbackTimeout) clearTimeout(this.outcomeScanFallbackTimeout);
525
+ this.outcomeScanTone = toneArg;
526
+ this.outcomeScanFallbackTimeout = setTimeout(() => this.endOutcomeScan(), 750);
527
+ }
528
+
529
+ private endOutcomeScan(): void {
530
+ if (this.outcomeScanFallbackTimeout) {
531
+ clearTimeout(this.outcomeScanFallbackTimeout);
532
+ this.outcomeScanFallbackTimeout = null;
533
+ }
534
+ this.outcomeScanTone = null;
535
+ }
536
+
537
+ public willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
538
+ super.willUpdate(changedProperties);
539
+ const previousCall = changedProperties.get('call') as IHarnessToolCall | undefined;
540
+ if (!previousCall || previousCall.id !== this.call.id) return;
541
+ const wasActive = previousCall.status === 'pending' || previousCall.status === 'running';
542
+ const outcomeTone = this.call.status === 'error' || this.call.isError
543
+ ? 'error'
544
+ : this.call.status === 'completed'
545
+ ? 'success'
546
+ : null;
547
+ if (wasActive && outcomeTone) {
548
+ this.startOutcomeScan(outcomeTone);
549
+ } else if (!outcomeTone && this.outcomeScanTone) {
550
+ this.endOutcomeScan();
551
+ }
552
+ }
553
+
483
554
  public updated(changedProperties: Map<PropertyKey, unknown>): void {
484
555
  super.updated(changedProperties);
556
+ if (changedProperties.has('call') && this.fullscreenPortal) {
557
+ this.fullscreenPortal.call = this.enrichedCall;
558
+ }
485
559
  if (this.uncapped) return;
486
560
  const bodyInner = this.shadowRoot?.querySelector('.bodyInner') ?? null;
487
561
  if (bodyInner === this.observedBodyInner) return;
@@ -505,6 +579,10 @@ export class DeesHarnessToolCard extends DeesElement {
505
579
  clearTimeout(this.flashFallbackTimeout);
506
580
  this.flashFallbackTimeout = null;
507
581
  }
582
+ if (this.outcomeScanFallbackTimeout) {
583
+ clearTimeout(this.outcomeScanFallbackTimeout);
584
+ this.outcomeScanFallbackTimeout = null;
585
+ }
508
586
  this.fullscreenPortal?.remove();
509
587
  this.fullscreenPortal = null;
510
588
  this.bodyResizeObserver?.disconnect();
@@ -7,6 +7,7 @@ import { css, type CSSResult } from '@design.estate/dees-element';
7
7
  */
8
8
  export const harnessCardStyles: CSSResult = css`
9
9
  .harness-card {
10
+ position: relative;
10
11
  border: 1px solid var(--dees-color-border-subtle);
11
12
  border-left: 2px solid var(--dees-color-border-default);
12
13
  border-radius: var(--dees-radius-lg);
@@ -17,6 +18,14 @@ export const harnessCardStyles: CSSResult = css`
17
18
  .harness-card.status-running,
18
19
  .harness-card.status-pending {
19
20
  border-left-color: var(--dees-color-accent-primary);
21
+ }
22
+ .harness-card.status-running::before,
23
+ .harness-card.status-pending::before,
24
+ .harness-card.outcomeScan::before {
25
+ content: '';
26
+ position: absolute;
27
+ inset: 0;
28
+ pointer-events: none;
20
29
  background:
21
30
  linear-gradient(
22
31
  105deg,
@@ -25,9 +34,13 @@ export const harnessCardStyles: CSSResult = css`
25
34
  color-mix(in srgb, var(--dees-color-accent-primary) 20%, transparent) 50%,
26
35
  color-mix(in srgb, var(--dees-color-accent-primary) 6%, transparent) 55%,
27
36
  transparent 60%
28
- ) 100% 0 / 250% 100% no-repeat,
29
- var(--dees-color-bg-secondary);
37
+ ) 100% 0 / 250% 100% no-repeat;
30
38
  animation: harness-card-running-scan 2s linear infinite;
39
+ opacity: 1;
40
+ transition: opacity 0.18s var(--dees-ease-out);
41
+ }
42
+ .harness-card.outcomeScan::before {
43
+ opacity: 0;
31
44
  }
32
45
  .harness-card.status-completed {
33
46
  border-left-color: var(--dees-color-accent-success);
@@ -47,13 +60,19 @@ export const harnessCardStyles: CSSResult = css`
47
60
  @media (prefers-reduced-motion: reduce) {
48
61
  .harness-card.status-running,
49
62
  .harness-card.status-pending {
50
- animation: none;
51
63
  background: color-mix(
52
64
  in srgb,
53
65
  var(--dees-color-accent-primary) 8%,
54
66
  var(--dees-color-bg-secondary)
55
67
  );
56
68
  }
69
+ .harness-card.status-running::before,
70
+ .harness-card.status-pending::before,
71
+ .harness-card.outcomeScan::before {
72
+ animation: none;
73
+ opacity: 0;
74
+ transition: none;
75
+ }
57
76
  }
58
77
  `;
59
78