@design.estate/dees-catalog 9.7.0 → 9.8.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 (26) hide show
  1. package/dist_bundle/bundle.js +194 -45
  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-appui/dees-appui/dees-appui.js +6 -1
  5. package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.js +14 -4
  6. package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.d.ts +4 -0
  7. package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.js +52 -1
  8. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.d.ts +4 -1
  9. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.js +93 -20
  10. package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/renderers.js +7 -21
  11. package/dist_ts_web/elements/00group-harness/harness.helpers.d.ts +20 -0
  12. package/dist_ts_web/elements/00group-harness/harness.helpers.js +58 -1
  13. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +6 -0
  14. package/dist_ts_web/elements/00group-harness/toolregistry.d.ts +8 -0
  15. package/dist_ts_web/elements/00group-harness/toolregistry.js +10 -2
  16. package/package.json +2 -2
  17. package/readme.md +2 -2
  18. package/ts_web/00_commitinfo_data.ts +1 -1
  19. package/ts_web/elements/00group-appui/dees-appui/dees-appui.ts +4 -0
  20. package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.ts +12 -3
  21. package/ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.ts +50 -0
  22. package/ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts +95 -23
  23. package/ts_web/elements/00group-harness/dees-harness-tool-card/renderers.ts +7 -20
  24. package/ts_web/elements/00group-harness/harness.helpers.ts +63 -0
  25. package/ts_web/elements/00group-harness/interfaces.ts +6 -0
  26. package/ts_web/elements/00group-harness/toolregistry.ts +22 -1
@@ -111,7 +111,7 @@ export class DeesHarnessToolCard extends DeesElement {
111
111
  private fullscreenPortal: HTMLElementTagNameMap['dees-harness-tool-fullscreen'] | null = null;
112
112
  private disclosureFrame: number | null = null;
113
113
  private returnScrollFrame: number | null = null;
114
- private subtaskResultElement: HTMLElement | null = null;
114
+ private subtaskDetailElements: HTMLElement[] = [];
115
115
  private subtaskResultResizeObserver: ResizeObserver | null = null;
116
116
  private terminalTailFollowing = true;
117
117
  private terminalScrollHydrated = false;
@@ -141,7 +141,11 @@ export class DeesHarnessToolCard extends DeesElement {
141
141
  .harness-card.subtaskPreviewOpen {
142
142
  display: flex;
143
143
  flex-direction: column;
144
- height: var(--dees-harness-active-subtask-height, 320px);
144
+ /*
145
+ * A cap, not a fixed height: a short brief and result let the card size to their own
146
+ * text, and only content taller than the cap hands the overflow to the inner scrollers.
147
+ */
148
+ max-height: var(--dees-harness-active-subtask-height, 320px);
145
149
  box-sizing: border-box;
146
150
  overflow: hidden;
147
151
  }
@@ -169,7 +173,27 @@ export class DeesHarnessToolCard extends DeesElement {
169
173
  overflow: hidden;
170
174
  }
171
175
 
172
- .harness-card.subtaskPreviewOpen .body,
176
+ /*
177
+ * The body takes its height from flex layout rather than a percentage: a capped card has no
178
+ * definite height for a percentage to resolve against, so a 100% height would collapse to
179
+ * auto and the inner scrollers would never engage. As a flex item it shrinks to the cap when
180
+ * the content overflows and sizes to its text when it does not.
181
+ */
182
+ .harness-card.subtaskPreviewOpen .bodyClipFrame {
183
+ display: flex;
184
+ flex-direction: column;
185
+ }
186
+
187
+ .harness-card.subtaskPreviewOpen .body {
188
+ flex: 1;
189
+ min-height: 0;
190
+ }
191
+
192
+ /* A streaming child fills the cap so its transcript has a stable window to scroll in. */
193
+ .harness-card.subtaskPreviewOpen:has(.liveSubtaskBody) {
194
+ height: var(--dees-harness-active-subtask-height, 320px);
195
+ }
196
+
173
197
  .harness-card.subtaskPreviewOpen .subtaskPreview {
174
198
  min-height: 0;
175
199
  height: 100%;
@@ -259,6 +283,15 @@ export class DeesHarnessToolCard extends DeesElement {
259
283
  font-size: 12px;
260
284
  }
261
285
 
286
+ /* Stands in for the body while a finished card is collapsed. */
287
+ .resultSummary {
288
+ display: block;
289
+ margin-top: 2px;
290
+ color: var(--dees-color-text-secondary);
291
+ font-family: var(--dees-font-family-mono, monospace);
292
+ font-size: 11px;
293
+ }
294
+
262
295
  .metaColumn {
263
296
  display: flex;
264
297
  align-items: center;
@@ -680,10 +713,27 @@ export class DeesHarnessToolCard extends DeesElement {
680
713
  && (this.call.subtask.status.type === 'busy' || this.call.subtask.status.type === 'error');
681
714
  }
682
715
  if (this.call.status === 'error' || this.call.isError) return true;
716
+ // A card that opened while it was live closes again once the work has settled, so a finished
717
+ // run stops holding transcript height. 'stopped' is deliberately excluded: a killed command
718
+ // keeps its partial output on screen, like an error does.
719
+ if (this.call.status === 'completed' && this.resolvedCollapseWhenFinished) return false;
683
720
  if (typeof descriptor.defaultExpanded === 'function') return descriptor.defaultExpanded(this.enrichedCall);
684
721
  return descriptor.defaultExpanded ?? false;
685
722
  }
686
723
 
724
+ private get resolvedCollapseWhenFinished(): boolean {
725
+ const collapseWhenFinished = this.descriptor.collapseWhenFinished;
726
+ if (typeof collapseWhenFinished === 'function') return collapseWhenFinished(this.enrichedCall);
727
+ return collapseWhenFinished ?? false;
728
+ }
729
+
730
+ /** Outcome line shown only while collapsed; the subtitle already carries the request. */
731
+ private get collapsedSummary(): string {
732
+ if (this.isOpen || !this.descriptor.summary) return '';
733
+ if (this.call.status !== 'completed' && this.call.status !== 'stopped') return '';
734
+ return this.descriptor.summary(this.enrichedCall).trim();
735
+ }
736
+
687
737
  private get hasSubtaskPreview(): boolean {
688
738
  return this.allowSubtaskStreams
689
739
  && this.descriptor.kind === 'subtask'
@@ -704,11 +754,19 @@ export class DeesHarnessToolCard extends DeesElement {
704
754
  const open = this.isOpen;
705
755
  const renderBody = this.bodyMounted || open;
706
756
  const label = typeof descriptor.label === 'function' ? descriptor.label(call) : descriptor.label;
707
- const subtitle = descriptor.subtitle?.(call) ?? '';
757
+ const derivedSubtitle = descriptor.subtitle?.(call) ?? '';
758
+ // A producer-supplied title describes the call in its own words — "Read SKILL.md" rather than
759
+ // the sed invocation behind it — so it leads, and the derived line stays as the tooltip.
760
+ const producerTitle = typeof call.title === 'string' ? call.title.trim() : '';
761
+ const subtitle = producerTitle || derivedSubtitle;
762
+ const subtitleTooltip = producerTitle && derivedSubtitle && derivedSubtitle !== producerTitle
763
+ ? `${producerTitle}\n${derivedSubtitle}`
764
+ : subtitle;
708
765
  const effectiveStatus = harnessEffectiveToolStatus(call);
709
766
  const statusTone = effectiveStatus === 'completed' ? 'success'
710
767
  : effectiveStatus === 'stopped' ? 'warning' : effectiveStatus;
711
768
  const duration = harnessFormatDuration(call.startedAt, call.finishedAt);
769
+ const collapsedSummary = this.collapsedSummary;
712
770
  return html`
713
771
  <div class="harness-card status-${effectiveStatus} ${descriptor.destructive ? 'destructive' : ''} ${this.hasSubtaskPreview ? 'subtaskPreviewCard' : ''} ${this.hasSubtaskPreview && open ? 'subtaskPreviewOpen' : ''} ${this.returnFlash ? 'returnFlash' : ''} ${this.outcomeScanTone ? `outcomeScan outcomeScan-${this.outcomeScanTone}` : ''}"
714
772
  @mouseenter=${this.cancelTerminalTailReturn}
@@ -734,7 +792,10 @@ export class DeesHarnessToolCard extends DeesElement {
734
792
  ${this.renderMcpBadges(call)}
735
793
  </span>
736
794
  ${subtitle
737
- ? html`<dees-harness-overflow-text class="subtitle" title=${subtitle}>${subtitle}</dees-harness-overflow-text>`
795
+ ? html`<dees-harness-overflow-text class="subtitle" title=${subtitleTooltip}>${subtitle}</dees-harness-overflow-text>`
796
+ : ''}
797
+ ${collapsedSummary
798
+ ? html`<dees-harness-overflow-text class="resultSummary" title=${collapsedSummary}>${collapsedSummary}</dees-harness-overflow-text>`
738
799
  : ''}
739
800
  </span>
740
801
  <span class="metaColumn">
@@ -1047,30 +1108,39 @@ export class DeesHarnessToolCard extends DeesElement {
1047
1108
  };
1048
1109
 
1049
1110
  private readonly syncSubtaskResultScroll = (): void => {
1050
- const result = this.subtaskResultElement;
1051
- if (!result) return;
1052
- result.classList.toggle('fadeTop', result.scrollTop > 1);
1053
- result.classList.toggle(
1054
- 'fadeBottom',
1055
- result.scrollHeight - result.scrollTop - result.clientHeight > 1,
1056
- );
1111
+ // Both panels hide their scrollbar, so the mask is the only cue that there is more text.
1112
+ // Leaving the brief unwired made a long brief look truncated rather than scrollable.
1113
+ for (const panel of this.subtaskDetailElements) {
1114
+ panel.classList.toggle('fadeTop', panel.scrollTop > 1);
1115
+ panel.classList.toggle(
1116
+ 'fadeBottom',
1117
+ panel.scrollHeight - panel.scrollTop - panel.clientHeight > 1,
1118
+ );
1119
+ }
1057
1120
  };
1058
1121
 
1059
1122
  private connectSubtaskResultScroll(): void {
1060
- const nextResult = this.shadowRoot?.querySelector<HTMLElement>(
1061
- '.harness-subtask-detail-result .harness-subtask-detail-text',
1062
- ) ?? null;
1063
- if (nextResult === this.subtaskResultElement) {
1123
+ const nextPanels = [...(this.shadowRoot?.querySelectorAll<HTMLElement>(
1124
+ '.harness-subtask-detail-brief .harness-subtask-detail-text,'
1125
+ + ' .harness-subtask-detail-result .harness-subtask-detail-text',
1126
+ ) ?? [])];
1127
+ const unchanged = nextPanels.length === this.subtaskDetailElements.length
1128
+ && nextPanels.every((panel, index) => panel === this.subtaskDetailElements[index]);
1129
+ if (unchanged) {
1064
1130
  this.syncSubtaskResultScroll();
1065
1131
  return;
1066
1132
  }
1067
- this.subtaskResultElement?.removeEventListener('scroll', this.syncSubtaskResultScroll);
1133
+ for (const panel of this.subtaskDetailElements) {
1134
+ panel.removeEventListener('scroll', this.syncSubtaskResultScroll);
1135
+ }
1068
1136
  this.subtaskResultResizeObserver?.disconnect();
1069
- this.subtaskResultElement = nextResult;
1070
- if (!nextResult) return;
1071
- nextResult.addEventListener('scroll', this.syncSubtaskResultScroll, { passive: true });
1137
+ this.subtaskDetailElements = nextPanels;
1138
+ if (!nextPanels.length) return;
1072
1139
  this.subtaskResultResizeObserver ??= new ResizeObserver(this.syncSubtaskResultScroll);
1073
- this.subtaskResultResizeObserver.observe(nextResult);
1140
+ for (const panel of nextPanels) {
1141
+ panel.addEventListener('scroll', this.syncSubtaskResultScroll, { passive: true });
1142
+ this.subtaskResultResizeObserver.observe(panel);
1143
+ }
1074
1144
  this.syncSubtaskResultScroll();
1075
1145
  }
1076
1146
 
@@ -1116,8 +1186,10 @@ export class DeesHarnessToolCard extends DeesElement {
1116
1186
  this.bodyResizeObserver?.disconnect();
1117
1187
  this.bodyResizeObserver = null;
1118
1188
  this.observedBodyInner = null;
1119
- this.subtaskResultElement?.removeEventListener('scroll', this.syncSubtaskResultScroll);
1120
- this.subtaskResultElement = null;
1189
+ for (const panel of this.subtaskDetailElements) {
1190
+ panel.removeEventListener('scroll', this.syncSubtaskResultScroll);
1191
+ }
1192
+ this.subtaskDetailElements = [];
1121
1193
  this.subtaskResultResizeObserver?.disconnect();
1122
1194
  this.subtaskResultResizeObserver = null;
1123
1195
  await super.disconnectedCallback();
@@ -6,7 +6,9 @@ import {
6
6
  harnessLanguageForPath,
7
7
  harnessParseTodos,
8
8
  harnessStringifyValue,
9
+ harnessStripAnsi,
9
10
  harnessSummarizeValue,
11
+ harnessTerminalOutcome,
10
12
  } from '../harness.helpers.js';
11
13
  import '../dees-harness-todos/dees-harness-todos.js';
12
14
 
@@ -125,35 +127,20 @@ export const applyPatchFirstPath = (call: IHarnessToolCall): string => {
125
127
  return match ? match[1].trim() : '';
126
128
  };
127
129
 
128
- // eslint-disable-next-line no-control-regex
129
- const ansiEscapePattern = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b[=>]/g;
130
-
131
- const stripAnsi = (text: string): string => text.replace(ansiEscapePattern, '');
132
-
133
130
  const renderTerminal = (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpers): TemplateResult => {
134
131
  const input = inputRecord(call);
135
132
  const command = String(input.command ?? '');
136
- const output = call.output;
137
- let stdout = '';
138
- let stderr = '';
139
- let nestedExitCode: number | undefined;
140
- if (harnessIsRecord(output)) {
141
- stdout = typeof output.stdout === 'string' ? output.stdout : harnessStringifyValue(output.stdout ?? '');
142
- stderr = typeof output.stderr === 'string' ? output.stderr : '';
143
- nestedExitCode = typeof output.exitCode === 'number' ? output.exitCode : undefined;
144
- } else {
145
- stdout = harnessStringifyValue(output);
146
- }
147
- const exitCode = typeof call.exitCode === 'number' ? call.exitCode : nestedExitCode;
133
+ // Shared with the collapsed-card summary so both read the same outcome.
134
+ const { stdout, stderr, exitCode } = harnessTerminalOutcome(call);
148
135
  const running = call.status === 'pending' || call.status === 'running';
149
136
  return html`
150
137
  <div class="harness-terminal">
151
138
  ${command
152
139
  ? html`<div class="harness-terminal-command"><span class="prompt">$</span> ${command}</div>`
153
140
  : ''}
154
- ${stdout ? html`<pre class="harness-terminal-output">${stripAnsi(stdout)}</pre>` : ''}
155
- ${stderr ? html`<pre class="harness-terminal-output tone-error">${stripAnsi(stderr)}</pre>` : ''}
156
- ${call.errorText ? html`<pre class="harness-terminal-output tone-error">${stripAnsi(call.errorText)}</pre>` : ''}
141
+ ${stdout ? html`<pre class="harness-terminal-output">${harnessStripAnsi(stdout)}</pre>` : ''}
142
+ ${stderr ? html`<pre class="harness-terminal-output tone-error">${harnessStripAnsi(stderr)}</pre>` : ''}
143
+ ${call.errorText ? html`<pre class="harness-terminal-output tone-error">${harnessStripAnsi(call.errorText)}</pre>` : ''}
157
144
  ${running && !stdout && !stderr
158
145
  ? html`<div class="harness-terminal-running"><span class="cursor"></span> running…</div>`
159
146
  : ''}
@@ -313,3 +313,66 @@ export const harnessSummarizeValue = (value: unknown): string => {
313
313
  export const harnessSummaryLine = (value: unknown): string => {
314
314
  return harnessSummarizeValue(value).split('\n')[0] || '';
315
315
  };
316
+
317
+ // eslint-disable-next-line no-control-regex
318
+ const ansiEscapePattern = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b[=>]/g;
319
+
320
+ /** Terminal output carries raw escape sequences; they must never reach a header or a summary. */
321
+ export const harnessStripAnsi = (text: string): string => text.replace(ansiEscapePattern, '');
322
+
323
+ export interface IHarnessTerminalOutcome {
324
+ stdout: string;
325
+ stderr: string;
326
+ /** Top-level exitCode wins over a nested one, matching how producers report it. */
327
+ exitCode?: number;
328
+ }
329
+
330
+ /**
331
+ * Normalizes the three shapes producers use for shell output: a `{stdout, stderr, exitCode}`
332
+ * record, a combined string with a separate top-level `exitCode`, or an arbitrary value.
333
+ */
334
+ export const harnessTerminalOutcome = (callArg: IHarnessToolCall): IHarnessTerminalOutcome => {
335
+ const output = callArg.output;
336
+ let stdout = '';
337
+ let stderr = '';
338
+ let nestedExitCode: number | undefined;
339
+ if (harnessIsRecord(output)) {
340
+ stdout = typeof output.stdout === 'string' ? output.stdout : harnessStringifyValue(output.stdout ?? '');
341
+ stderr = typeof output.stderr === 'string' ? output.stderr : '';
342
+ nestedExitCode = typeof output.exitCode === 'number' ? output.exitCode : undefined;
343
+ } else {
344
+ stdout = harnessStringifyValue(output);
345
+ }
346
+ const exitCode = typeof callArg.exitCode === 'number' ? callArg.exitCode : nestedExitCode;
347
+ return { stdout, stderr, ...(exitCode === undefined ? {} : { exitCode }) };
348
+ };
349
+
350
+ const harnessLastMeaningfulLine = (text: string): string => {
351
+ const lines = harnessStripAnsi(text).split('\n');
352
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
353
+ const line = lines[index].trim();
354
+ if (line) return line;
355
+ }
356
+ return '';
357
+ };
358
+
359
+ /**
360
+ * One-line outcome for a finished shell call, for the collapsed card header. The command itself
361
+ * is already the card subtitle, so this describes the RESULT: exit code, how much came back, and
362
+ * the last meaningful line. Harness support for a richer summary is uneven — only OpenCode
363
+ * supplies a model-authored description, so this is derived from output that every harness has.
364
+ */
365
+ export const harnessTerminalResultSummary = (callArg: IHarnessToolCall): string => {
366
+ const { stdout, stderr, exitCode } = harnessTerminalOutcome(callArg);
367
+ const parts: string[] = [];
368
+ if (exitCode !== undefined) parts.push(exitCode === 0 ? 'exit 0' : `exit ${exitCode}`);
369
+ const combined = `${stdout}${stdout && stderr ? '\n' : ''}${stderr}`;
370
+ const lineCount = combined ? harnessStripAnsi(combined).split('\n').filter((line) => line.trim()).length : 0;
371
+ if (lineCount > 0) parts.push(`${lineCount} ${lineCount === 1 ? 'line' : 'lines'}`);
372
+ else if (exitCode !== undefined) parts.push('no output');
373
+ const lastLine = harnessLastMeaningfulLine(stderr) || harnessLastMeaningfulLine(stdout);
374
+ const prefix = parts.join(' · ');
375
+ if (!lastLine) return prefix;
376
+ const trimmed = lastLine.length > 160 ? `${lastLine.slice(0, 160)}…` : lastLine;
377
+ return prefix ? `${prefix} · ${trimmed}` : trimmed;
378
+ };
@@ -230,6 +230,12 @@ export interface IHarnessToolCall {
230
230
  subtask?: IHarnessSubtaskStream;
231
231
  /** model the spawned session runs on, e.g. "anthropic/claude-opus-5" */
232
232
  model?: string;
233
+ /**
234
+ * Producer-supplied one-line description of the call, e.g. OpenCode's tool title or a parsed
235
+ * shell action. Single line, already bounded by the producer. Preferred over any line the card
236
+ * derives for itself; absent whenever the producer offers nothing.
237
+ */
238
+ title?: string;
233
239
  /** epoch ms */
234
240
  startedAt?: number;
235
241
  finishedAt?: number;
@@ -1,7 +1,12 @@
1
1
  import type { TemplateResult } from '@design.estate/dees-element';
2
2
  import type { TDiffViewRequest } from '../00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js';
3
3
  import type { IHarnessToolCall, THarnessContentBlock } from './interfaces.js';
4
- import { harnessIsRecord, harnessSummaryLine } from './harness.helpers.js';
4
+ import {
5
+ harnessIsRecord,
6
+ harnessSummaryLine,
7
+ harnessTerminalOutcome,
8
+ harnessTerminalResultSummary,
9
+ } from './harness.helpers.js';
5
10
 
6
11
  /**
7
12
  * Render helpers handed to tool body renderers. Implemented by
@@ -49,6 +54,14 @@ export interface IHarnessToolDescriptor {
49
54
  */
50
55
  renderBody?: (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpers) => TemplateResult;
51
56
  defaultExpanded?: boolean | ((call: IHarnessToolCall) => boolean);
57
+ /**
58
+ * Collapses a card that opened while it was live once its work is done, so a finished run
59
+ * stops occupying the transcript. Evaluated only for a settled call, and always subordinate to
60
+ * an explicit user toggle.
61
+ */
62
+ collapseWhenFinished?: boolean | ((call: IHarnessToolCall) => boolean);
63
+ /** One-line outcome shown in the header while the card is collapsed. */
64
+ summary?: (call: IHarnessToolCall) => string;
52
65
  /** Transcript grid packing; omitted descriptors use the compact layout. */
53
66
  layout?: THarnessToolLayout;
54
67
  /** styles the card with a destructive accent regardless of status */
@@ -101,6 +114,14 @@ const defaultDescriptors: IHarnessToolDescriptor[] = [
101
114
  },
102
115
  defaultExpanded: true,
103
116
  layout: 'full-row',
117
+ /*
118
+ * Only a command that can be PROVEN to have succeeded collapses. An absent exit code is not
119
+ * evidence of success: a producer that reports a failed command as completed and encodes the
120
+ * failure in its output would otherwise have that failure hidden behind a one-line summary.
121
+ * Such a card stays open, and the producer reporting an exit code is the real fix.
122
+ */
123
+ collapseWhenFinished: (call) => harnessTerminalOutcome(call).exitCode === 0,
124
+ summary: harnessTerminalResultSummary,
104
125
  },
105
126
  {
106
127
  kind: 'file-read',