@design.estate/dees-catalog 5.0.0 → 6.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 (22) hide show
  1. package/dist_bundle/bundle.js +526 -134
  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 +7 -2
  4. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.demo.js +1 -2
  5. package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +110 -26
  6. package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.d.ts +5 -0
  7. package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.js +287 -66
  8. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.d.ts +5 -0
  9. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.js +91 -3
  10. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/renderers.js +6 -5
  11. package/dist_ts_web/elements/00group-harness/harness.styles.js +23 -4
  12. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +2 -0
  13. package/package.json +1 -1
  14. package/readme.md +6 -4
  15. package/ts_web/00_commitinfo_data.ts +1 -1
  16. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.demo.ts +0 -1
  17. package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +101 -25
  18. package/ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.ts +318 -73
  19. package/ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts +83 -1
  20. package/ts_web/elements/00group-harness/dees-harness-tool-card/renderers.ts +5 -4
  21. package/ts_web/elements/00group-harness/harness.styles.ts +22 -3
  22. package/ts_web/elements/00group-harness/interfaces.ts +2 -0
@@ -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;
@@ -306,6 +309,10 @@ export class DeesHarnessToolCard extends DeesElement {
306
309
  font-size: var(--dees-font-caption1-size, 11px);
307
310
  }
308
311
 
312
+ .harness-terminal-exit.tone-success {
313
+ color: var(--dees-color-accent-success);
314
+ }
315
+
309
316
  .harness-subtask-open {
310
317
  justify-self: start;
311
318
  display: inline-flex;
@@ -332,6 +339,30 @@ export class DeesHarnessToolCard extends DeesElement {
332
339
  transform-origin: center;
333
340
  }
334
341
 
342
+ .harness-card.outcomeScan {
343
+ --harness-outcome-scan-color: var(--dees-color-accent-success);
344
+ }
345
+
346
+ .harness-card.outcomeScan-error {
347
+ --harness-outcome-scan-color: var(--dees-color-accent-error);
348
+ }
349
+
350
+ .harness-card.outcomeScan::after {
351
+ content: '';
352
+ position: absolute;
353
+ inset: 0;
354
+ pointer-events: none;
355
+ background: linear-gradient(
356
+ 105deg,
357
+ transparent 40%,
358
+ color-mix(in srgb, var(--harness-outcome-scan-color) 3%, transparent) 45%,
359
+ color-mix(in srgb, var(--harness-outcome-scan-color) 14%, transparent) 50%,
360
+ color-mix(in srgb, var(--harness-outcome-scan-color) 3%, transparent) 55%,
361
+ transparent 60%
362
+ ) 100% 0 / 250% 100% no-repeat;
363
+ animation: harness-tool-outcome-scan 0.48s linear 0.08s both;
364
+ }
365
+
335
366
  @keyframes harness-card-return-flash {
336
367
  0%, 100% {
337
368
  transform: scale(1);
@@ -341,10 +372,20 @@ export class DeesHarnessToolCard extends DeesElement {
341
372
  }
342
373
  }
343
374
 
375
+ @keyframes harness-tool-outcome-scan {
376
+ from { background-position: 100% 0; }
377
+ to { background-position: 0 0; }
378
+ }
379
+
344
380
  @media (prefers-reduced-motion: reduce) {
345
381
  .harness-card.returnFlash {
346
382
  animation: none;
347
383
  }
384
+
385
+ .harness-card.outcomeScan::after {
386
+ animation: none;
387
+ opacity: 0;
388
+ }
348
389
  }
349
390
  `,
350
391
  ];
@@ -376,10 +417,12 @@ export class DeesHarnessToolCard extends DeesElement {
376
417
  const statusTone = call.isError ? 'error' : call.status === 'completed' ? 'success' : call.status;
377
418
  const duration = harnessFormatDuration(call.startedAt, call.finishedAt);
378
419
  return html`
379
- <div class="harness-card status-${call.isError ? 'error' : call.status} ${descriptor.destructive ? 'destructive' : ''} ${this.returnFlash ? 'returnFlash' : ''}"
420
+ <div class="harness-card status-${call.isError ? 'error' : call.status} ${descriptor.destructive ? 'destructive' : ''} ${this.returnFlash ? 'returnFlash' : ''} ${this.outcomeScanTone ? `outcomeScan outcomeScan-${this.outcomeScanTone}` : ''}"
380
421
  @animationend=${(event: AnimationEvent) => {
381
422
  if (event.animationName === 'harness-card-return-flash') {
382
423
  this.endReturnFlash();
424
+ } else if (event.animationName === 'harness-tool-outcome-scan') {
425
+ this.endOutcomeScan();
383
426
  }
384
427
  }}
385
428
  >
@@ -444,6 +487,7 @@ export class DeesHarnessToolCard extends DeesElement {
444
487
  };
445
488
 
446
489
  private flashFallbackTimeout: ReturnType<typeof setTimeout> | null = null;
490
+ private outcomeScanFallbackTimeout: ReturnType<typeof setTimeout> | null = null;
447
491
 
448
492
  private readonly closeFullscreen = (): void => {
449
493
  const portal = this.fullscreenPortal;
@@ -480,8 +524,42 @@ export class DeesHarnessToolCard extends DeesElement {
480
524
  }));
481
525
  }
482
526
 
527
+ private startOutcomeScan(toneArg: 'success' | 'error'): void {
528
+ if (this.outcomeScanFallbackTimeout) clearTimeout(this.outcomeScanFallbackTimeout);
529
+ this.outcomeScanTone = toneArg;
530
+ this.outcomeScanFallbackTimeout = setTimeout(() => this.endOutcomeScan(), 750);
531
+ }
532
+
533
+ private endOutcomeScan(): void {
534
+ if (this.outcomeScanFallbackTimeout) {
535
+ clearTimeout(this.outcomeScanFallbackTimeout);
536
+ this.outcomeScanFallbackTimeout = null;
537
+ }
538
+ this.outcomeScanTone = null;
539
+ }
540
+
541
+ public willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
542
+ super.willUpdate(changedProperties);
543
+ const previousCall = changedProperties.get('call') as IHarnessToolCall | undefined;
544
+ if (!previousCall || previousCall.id !== this.call.id) return;
545
+ const wasActive = previousCall.status === 'pending' || previousCall.status === 'running';
546
+ const outcomeTone = this.call.status === 'error' || this.call.isError
547
+ ? 'error'
548
+ : this.call.status === 'completed'
549
+ ? 'success'
550
+ : null;
551
+ if (wasActive && outcomeTone) {
552
+ this.startOutcomeScan(outcomeTone);
553
+ } else if (!outcomeTone && this.outcomeScanTone) {
554
+ this.endOutcomeScan();
555
+ }
556
+ }
557
+
483
558
  public updated(changedProperties: Map<PropertyKey, unknown>): void {
484
559
  super.updated(changedProperties);
560
+ if (changedProperties.has('call') && this.fullscreenPortal) {
561
+ this.fullscreenPortal.call = this.enrichedCall;
562
+ }
485
563
  if (this.uncapped) return;
486
564
  const bodyInner = this.shadowRoot?.querySelector('.bodyInner') ?? null;
487
565
  if (bodyInner === this.observedBodyInner) return;
@@ -505,6 +583,10 @@ export class DeesHarnessToolCard extends DeesElement {
505
583
  clearTimeout(this.flashFallbackTimeout);
506
584
  this.flashFallbackTimeout = null;
507
585
  }
586
+ if (this.outcomeScanFallbackTimeout) {
587
+ clearTimeout(this.outcomeScanFallbackTimeout);
588
+ this.outcomeScanFallbackTimeout = null;
589
+ }
508
590
  this.fullscreenPortal?.remove();
509
591
  this.fullscreenPortal = null;
510
592
  this.bodyResizeObserver?.disconnect();
@@ -105,14 +105,15 @@ const renderTerminal = (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpe
105
105
  const output = call.output;
106
106
  let stdout = '';
107
107
  let stderr = '';
108
- let exitCode: number | undefined;
108
+ let nestedExitCode: number | undefined;
109
109
  if (harnessIsRecord(output)) {
110
110
  stdout = typeof output.stdout === 'string' ? output.stdout : harnessStringifyValue(output.stdout ?? '');
111
111
  stderr = typeof output.stderr === 'string' ? output.stderr : '';
112
- exitCode = typeof output.exitCode === 'number' ? output.exitCode : undefined;
112
+ nestedExitCode = typeof output.exitCode === 'number' ? output.exitCode : undefined;
113
113
  } else {
114
114
  stdout = harnessStringifyValue(output);
115
115
  }
116
+ const exitCode = typeof call.exitCode === 'number' ? call.exitCode : nestedExitCode;
116
117
  const running = call.status === 'pending' || call.status === 'running';
117
118
  return html`
118
119
  <div class="harness-terminal">
@@ -125,8 +126,8 @@ const renderTerminal = (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpe
125
126
  ${running && !stdout && !stderr
126
127
  ? html`<div class="harness-terminal-running"><span class="cursor"></span> running…</div>`
127
128
  : ''}
128
- ${exitCode !== undefined && exitCode !== 0
129
- ? html`<div class="harness-terminal-exit">exit ${exitCode}</div>`
129
+ ${exitCode !== undefined
130
+ ? html`<div class="harness-terminal-exit ${exitCode === 0 ? 'tone-success' : ''}">exit code ${exitCode}</div>`
130
131
  : ''}
131
132
  </div>
132
133
  `;