@design.estate/dees-catalog 6.9.0 → 6.10.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.
@@ -40,6 +40,8 @@ declare global {
40
40
  }
41
41
 
42
42
  const pinThresholdPx = 48;
43
+ const loadEarlierThresholdPx = 24;
44
+ const scrollIntentSettlementMs = 250;
43
45
  const virtualizationThreshold = 40;
44
46
  const virtualOverscanPx = 800;
45
47
  const virtualGapPx = 8;
@@ -112,6 +114,16 @@ export class DeesHarnessMessageList extends DeesElement {
112
114
  @property({ type: Boolean })
113
115
  accessor autoFollow: boolean = true;
114
116
 
117
+ /** Stable host-owned identity for one transcript and its scroll context. */
118
+ @property()
119
+ accessor transcriptKey: string = '';
120
+
121
+ @property({ type: Boolean })
122
+ accessor hasEarlierMessages: boolean = false;
123
+
124
+ @property({ type: Boolean })
125
+ accessor loadingEarlier: boolean = false;
126
+
115
127
  @property({ type: Boolean })
116
128
  accessor hideScrollbar: boolean = false;
117
129
 
@@ -200,6 +212,16 @@ export class DeesHarnessMessageList extends DeesElement {
200
212
  private virtualizationDisablePending = false;
201
213
  private virtualizationActivationFrame: number | null = null;
202
214
  private selectionChangeListening = false;
215
+ private loadEarlierLatched = false;
216
+ private loadingEarlierObserved = false;
217
+ private earlierScrollIntent = false;
218
+ private scrollbarGestureActive = false;
219
+ private touchScrollY: number | undefined;
220
+ private scrollIntentTimer: ReturnType<typeof setTimeout> | undefined;
221
+ private programmaticScrollTop: number | undefined;
222
+ private programmaticScrollGeneration = 0;
223
+ private programmaticScrollFrame: number | null = null;
224
+ private programmaticScrollSecondFrame: number | null = null;
203
225
 
204
226
  public static styles = [
205
227
  themeDefaultStyles,
@@ -492,6 +514,20 @@ export class DeesHarnessMessageList extends DeesElement {
492
514
 
493
515
  public willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
494
516
  super.willUpdate(changedProperties);
517
+ if (changedProperties.has('transcriptKey')) this.resetTranscriptContext();
518
+ if (changedProperties.has('hasEarlierMessages') && !this.hasEarlierMessages) {
519
+ this.loadEarlierLatched = false;
520
+ this.loadingEarlierObserved = false;
521
+ this.clearEarlierScrollIntent();
522
+ }
523
+ if (changedProperties.has('loadingEarlier')) {
524
+ const wasLoadingEarlier = changedProperties.get('loadingEarlier') === true;
525
+ if (this.loadingEarlier) this.loadingEarlierObserved = true;
526
+ else if (wasLoadingEarlier && this.loadingEarlierObserved) {
527
+ this.loadEarlierLatched = false;
528
+ this.loadingEarlierObserved = false;
529
+ }
530
+ }
495
531
  if (changedProperties.has('viewState') && this.viewState) {
496
532
  this.clearTranscriptViewState(this.localViewState);
497
533
  }
@@ -503,6 +539,7 @@ export class DeesHarnessMessageList extends DeesElement {
503
539
  || changedProperties.has('questions')
504
540
  || changedProperties.has('toolRegistry')
505
541
  || changedProperties.has('viewState')
542
+ || changedProperties.has('transcriptKey')
506
543
  || registryChanged
507
544
  ) {
508
545
  this.rebuildTimeline();
@@ -766,8 +803,12 @@ export class DeesHarnessMessageList extends DeesElement {
766
803
  <div
767
804
  class=${`scroll ${this.hideScrollbar ? 'hideScrollbar' : ''} ${this.scrollFadeTop ? 'fadeTop' : ''} ${this.scrollFadeBottom ? 'fadeBottom' : ''}`}
768
805
  @scroll=${this.handleScroll}
806
+ @scrollend=${this.handleScrollEnd}
769
807
  @wheel=${this.handleUserScrollIntent}
808
+ @touchstart=${this.handleTouchStart}
770
809
  @touchmove=${this.handleUserScrollIntent}
810
+ @touchend=${this.handleTouchEnd}
811
+ @touchcancel=${this.handleTouchCancel}
771
812
  @pointerdown=${this.handlePointerDown}
772
813
  @pointerup=${this.handlePointerUp}
773
814
  @pointercancel=${this.handlePointerCancel}
@@ -1042,8 +1083,7 @@ export class DeesHarnessMessageList extends DeesElement {
1042
1083
  if (!this.autoFollow || this.userUnpinned) return;
1043
1084
  const container = this.scrollContainer;
1044
1085
  if (!container) return;
1045
- container.scrollTop = container.scrollHeight;
1046
- this.lastObservedScrollTop = container.scrollTop;
1086
+ this.setProgrammaticScrollTop(container, container.scrollHeight);
1047
1087
  };
1048
1088
 
1049
1089
  private followAfterContentChange(): void {
@@ -1068,26 +1108,213 @@ export class DeesHarnessMessageList extends DeesElement {
1068
1108
  }
1069
1109
 
1070
1110
  private handleUserScrollIntent = (event: Event): void => {
1071
- if (event.composedPath().some(
1072
- (candidate) => candidate instanceof DeesHarnessMessageList && candidate !== this,
1073
- )) return;
1111
+ if (this.eventComesFromNestedMessageList(event)) return;
1074
1112
  if (event.type === 'wheel') {
1075
1113
  this.cancelFullAnchorRestore();
1076
- if ((event as WheelEvent).deltaY >= 0) return;
1114
+ const deltaY = (event as WheelEvent).deltaY;
1115
+ if (deltaY < 0) {
1116
+ this.armEarlierScrollIntent();
1117
+ this.requestEarlierIfAtTop();
1118
+ this.markUserUnpinned();
1119
+ } else if (deltaY > 0) {
1120
+ this.clearEarlierScrollIntent();
1121
+ }
1122
+ return;
1077
1123
  }
1078
- if (
1079
- event.type === 'keydown'
1080
- && !['ArrowUp', 'PageUp', 'Home'].includes((event as KeyboardEvent).key)
1081
- ) {
1082
- if (['ArrowDown', 'PageDown', 'End', ' '].includes((event as KeyboardEvent).key)) {
1124
+ if (event.type === 'keydown') {
1125
+ if (this.keyboardEventOriginIsInteractive(event as KeyboardEvent)) return;
1126
+ const key = (event as KeyboardEvent).key;
1127
+ if (['ArrowUp', 'PageUp', 'Home'].includes(key)) {
1128
+ this.cancelFullAnchorRestore();
1129
+ this.armEarlierScrollIntent();
1130
+ this.requestEarlierIfAtTop();
1131
+ this.markUserUnpinned();
1132
+ } else if (['ArrowDown', 'PageDown', 'End', ' '].includes(key)) {
1133
+ this.clearEarlierScrollIntent();
1083
1134
  this.cancelFullAnchorRestore();
1084
1135
  }
1085
1136
  return;
1086
1137
  }
1087
- if (event.type === 'pointerdown' && event.target !== this.scrollContainer) return;
1088
- this.cancelFullAnchorRestore();
1138
+ if (event.type === 'touchmove') {
1139
+ const currentY = (event as TouchEvent).touches[0]?.clientY;
1140
+ const previousY = this.touchScrollY;
1141
+ this.touchScrollY = currentY;
1142
+ if (currentY === undefined || previousY === undefined) return;
1143
+ if (currentY > previousY + 0.5) {
1144
+ this.armEarlierScrollIntent();
1145
+ this.requestEarlierIfAtTop();
1146
+ } else if (currentY < previousY - 0.5) {
1147
+ this.clearEarlierScrollIntent();
1148
+ } else {
1149
+ return;
1150
+ }
1151
+ this.cancelFullAnchorRestore();
1152
+ this.markUserUnpinned();
1153
+ return;
1154
+ }
1155
+ if (event.type === 'pointerdown') {
1156
+ if ((event as PointerEvent).button !== 0) return;
1157
+ if (event.target !== this.scrollContainer) return;
1158
+ this.scrollbarGestureActive = true;
1159
+ this.cancelFullAnchorRestore();
1160
+ this.markUserUnpinned();
1161
+ }
1162
+ };
1163
+
1164
+ private markUserUnpinned(): void {
1089
1165
  this.userUnpinned = true;
1090
1166
  this.measurementAnchor = undefined;
1167
+ }
1168
+
1169
+ private eventComesFromNestedMessageList(eventArg: Event): boolean {
1170
+ return eventArg.composedPath().some(
1171
+ (candidate) => candidate instanceof DeesHarnessMessageList && candidate !== this,
1172
+ );
1173
+ }
1174
+
1175
+ private keyboardEventOriginIsInteractive(eventArg: KeyboardEvent): boolean {
1176
+ return eventArg.composedPath().some((candidate) => (
1177
+ candidate instanceof HTMLElement
1178
+ && candidate !== this
1179
+ && (
1180
+ candidate.isContentEditable
1181
+ || candidate.matches('input, textarea, select, button, [contenteditable]')
1182
+ )
1183
+ ));
1184
+ }
1185
+
1186
+ private armEarlierScrollIntent(): void {
1187
+ this.earlierScrollIntent = true;
1188
+ this.scheduleScrollIntentSettlement();
1189
+ }
1190
+
1191
+ private scheduleScrollIntentSettlement(): void {
1192
+ if (this.scrollIntentTimer) clearTimeout(this.scrollIntentTimer);
1193
+ this.scrollIntentTimer = setTimeout(() => {
1194
+ this.scrollIntentTimer = undefined;
1195
+ this.earlierScrollIntent = false;
1196
+ }, scrollIntentSettlementMs);
1197
+ }
1198
+
1199
+ private clearEarlierScrollIntent(): void {
1200
+ if (this.scrollIntentTimer) clearTimeout(this.scrollIntentTimer);
1201
+ this.scrollIntentTimer = undefined;
1202
+ this.earlierScrollIntent = false;
1203
+ }
1204
+
1205
+ private requestEarlierIfAtTop(): void {
1206
+ const container = this.scrollContainer;
1207
+ if (
1208
+ !container
1209
+ || container.scrollTop > loadEarlierThresholdPx
1210
+ || !this.hasEarlierMessages
1211
+ || this.loadingEarlier
1212
+ || this.loadEarlierLatched
1213
+ ) return;
1214
+ this.loadEarlierLatched = true;
1215
+ this.clearEarlierScrollIntent();
1216
+ this.dispatchEvent(new CustomEvent('harness-load-earlier', {
1217
+ bubbles: true,
1218
+ composed: true,
1219
+ }));
1220
+ }
1221
+
1222
+ private resetLoadEarlierState(): void {
1223
+ this.loadEarlierLatched = false;
1224
+ this.loadingEarlierObserved = false;
1225
+ this.scrollbarGestureActive = false;
1226
+ this.touchScrollY = undefined;
1227
+ this.clearEarlierScrollIntent();
1228
+ }
1229
+
1230
+ private setProgrammaticScrollTop(containerArg: HTMLElement, topArg: number): void {
1231
+ this.clearProgrammaticScrollMarker();
1232
+ const generation = ++this.programmaticScrollGeneration;
1233
+ containerArg.scrollTop = topArg;
1234
+ this.programmaticScrollTop = containerArg.scrollTop;
1235
+ this.lastObservedScrollTop = containerArg.scrollTop;
1236
+ this.programmaticScrollFrame = requestAnimationFrame(() => {
1237
+ this.programmaticScrollFrame = null;
1238
+ if (generation !== this.programmaticScrollGeneration) return;
1239
+ this.programmaticScrollSecondFrame = requestAnimationFrame(() => {
1240
+ this.programmaticScrollSecondFrame = null;
1241
+ if (generation === this.programmaticScrollGeneration) {
1242
+ this.programmaticScrollTop = undefined;
1243
+ }
1244
+ });
1245
+ });
1246
+ }
1247
+
1248
+ private consumeProgrammaticScroll(topArg: number): boolean {
1249
+ if (this.programmaticScrollTop === undefined) return false;
1250
+ const matches = Math.abs(this.programmaticScrollTop - topArg) <= 1;
1251
+ this.clearProgrammaticScrollMarker();
1252
+ return matches;
1253
+ }
1254
+
1255
+ private clearProgrammaticScrollMarker(): void {
1256
+ this.programmaticScrollGeneration += 1;
1257
+ if (this.programmaticScrollFrame !== null) cancelAnimationFrame(this.programmaticScrollFrame);
1258
+ if (this.programmaticScrollSecondFrame !== null) cancelAnimationFrame(this.programmaticScrollSecondFrame);
1259
+ this.programmaticScrollFrame = null;
1260
+ this.programmaticScrollSecondFrame = null;
1261
+ this.programmaticScrollTop = undefined;
1262
+ }
1263
+
1264
+ private resetTranscriptContext(): void {
1265
+ this.cancelFullAnchorRestore();
1266
+ if (this.followFrame !== null) cancelAnimationFrame(this.followFrame);
1267
+ if (this.followSecondFrame !== null) cancelAnimationFrame(this.followSecondFrame);
1268
+ if (this.virtualMeasureFrame !== null) cancelAnimationFrame(this.virtualMeasureFrame);
1269
+ if (this.virtualRangeFrame !== null) cancelAnimationFrame(this.virtualRangeFrame);
1270
+ if (this.virtualizationActivationFrame !== null) cancelAnimationFrame(this.virtualizationActivationFrame);
1271
+ this.followFrame = null;
1272
+ this.followSecondFrame = null;
1273
+ this.virtualMeasureFrame = null;
1274
+ this.virtualRangeFrame = null;
1275
+ this.virtualizationActivationFrame = null;
1276
+ this.pendingVirtualAnchor = undefined;
1277
+ this.pendingFullAnchor = undefined;
1278
+ this.measurementAnchor = undefined;
1279
+ this.pendingVirtualRepin = false;
1280
+ this.virtualizationEnabled = false;
1281
+ this.virtualizationEnablePending = false;
1282
+ this.virtualizationDisablePending = false;
1283
+ this.virtualStartIndex = 0;
1284
+ this.virtualEndIndex = 0;
1285
+ this.retainedVirtualKeys.clear();
1286
+ this.explicitRetainedVirtualKey = undefined;
1287
+ this.measuredVirtualHeights.clear();
1288
+ this.virtualOffsets = [];
1289
+ this.virtualTotalHeight = 0;
1290
+ this.toolGroupIdByMessageId.clear();
1291
+ this.nextToolGroupId = 1;
1292
+ this.timelineRegistryRevision = -1;
1293
+ this.knownContentKeys.clear();
1294
+ this.currentNewContentKeys.clear();
1295
+ this.clearTranscriptViewState(this.localViewState);
1296
+ this.clearSelectionGesture();
1297
+ this.disconnectSelectionChangeListener();
1298
+ this.userUnpinned = false;
1299
+ this.lastObservedScrollTop = 0;
1300
+ this.showJumpChip = false;
1301
+ this.resetLoadEarlierState();
1302
+ this.clearProgrammaticScrollMarker();
1303
+ }
1304
+
1305
+ private readonly handleTouchStart = (eventArg: TouchEvent): void => {
1306
+ if (this.eventComesFromNestedMessageList(eventArg)) return;
1307
+ this.touchScrollY = eventArg.touches[0]?.clientY;
1308
+ };
1309
+
1310
+ private readonly handleTouchEnd = (): void => {
1311
+ this.touchScrollY = undefined;
1312
+ if (this.earlierScrollIntent) this.scheduleScrollIntentSettlement();
1313
+ };
1314
+
1315
+ private readonly handleTouchCancel = (): void => {
1316
+ this.touchScrollY = undefined;
1317
+ this.clearEarlierScrollIntent();
1091
1318
  };
1092
1319
 
1093
1320
  public scrollToBottom(force = false): void {
@@ -1099,8 +1326,7 @@ export class DeesHarnessMessageList extends DeesElement {
1099
1326
  this.measurementAnchor = undefined;
1100
1327
  }
1101
1328
  if (force || this.isPinnedToBottom) {
1102
- container.scrollTop = container.scrollHeight;
1103
- this.lastObservedScrollTop = container.scrollTop;
1329
+ this.setProgrammaticScrollTop(container, container.scrollHeight);
1104
1330
  this.showJumpChip = false;
1105
1331
  }
1106
1332
  this.syncScrollEdgeFades();
@@ -1110,6 +1336,20 @@ export class DeesHarnessMessageList extends DeesElement {
1110
1336
  const container = this.scrollContainer;
1111
1337
  if (!container) return;
1112
1338
  const top = container.scrollTop;
1339
+ const previousTop = this.lastObservedScrollTop;
1340
+ const programmatic = this.consumeProgrammaticScroll(top);
1341
+ if (programmatic) {
1342
+ this.clearEarlierScrollIntent();
1343
+ } else {
1344
+ if (this.scrollbarGestureActive && top < previousTop - 0.5) {
1345
+ this.armEarlierScrollIntent();
1346
+ } else if (top > previousTop + 0.5) {
1347
+ this.clearEarlierScrollIntent();
1348
+ } else if (this.earlierScrollIntent) {
1349
+ this.scheduleScrollIntentSettlement();
1350
+ }
1351
+ if (this.earlierScrollIntent) this.requestEarlierIfAtTop();
1352
+ }
1113
1353
  this.syncScrollEdgeFades();
1114
1354
  if (this.isPinnedToBottom) {
1115
1355
  // reaching the bottom — by any means — resumes following
@@ -1123,6 +1363,10 @@ export class DeesHarnessMessageList extends DeesElement {
1123
1363
  this.schedulePendingVirtualizationActivation();
1124
1364
  };
1125
1365
 
1366
+ private readonly handleScrollEnd = (): void => {
1367
+ this.clearEarlierScrollIntent();
1368
+ };
1369
+
1126
1370
  private schedulePendingVirtualizationActivation(): void {
1127
1371
  if (
1128
1372
  (!this.virtualizationEnablePending && !this.virtualizationDisablePending)
@@ -1187,6 +1431,7 @@ export class DeesHarnessMessageList extends DeesElement {
1187
1431
  };
1188
1432
 
1189
1433
  private readonly handlePointerUp = (eventArg: PointerEvent): void => {
1434
+ this.scrollbarGestureActive = false;
1190
1435
  if (eventArg.button !== 0) return;
1191
1436
  const key = this.timelineKeyForEvent(eventArg);
1192
1437
  if (key) this.selectionGestureKeys.add(key);
@@ -1205,6 +1450,8 @@ export class DeesHarnessMessageList extends DeesElement {
1205
1450
  };
1206
1451
 
1207
1452
  private readonly handlePointerCancel = (): void => {
1453
+ this.scrollbarGestureActive = false;
1454
+ this.clearEarlierScrollIntent();
1208
1455
  this.clearSelectionGesture();
1209
1456
  this.handleProtectedStateChange();
1210
1457
  };
@@ -1228,6 +1475,13 @@ export class DeesHarnessMessageList extends DeesElement {
1228
1475
 
1229
1476
  public updated(changedProperties: Map<string, any>): void {
1230
1477
  super.updated(changedProperties);
1478
+ if (changedProperties.has('transcriptKey')) {
1479
+ const container = this.scrollContainer;
1480
+ if (container) {
1481
+ if (this.autoFollow) this.scrollToBottom(true);
1482
+ else this.setProgrammaticScrollTop(container, 0);
1483
+ }
1484
+ }
1231
1485
  if (changedProperties.has('messages') || changedProperties.has('status') || changedProperties.has('permissions') || changedProperties.has('questions')) {
1232
1486
  this.followAfterContentChange();
1233
1487
  }
@@ -1424,7 +1678,10 @@ export class DeesHarnessMessageList extends DeesElement {
1424
1678
  const index = this.timelineIndexByKey.get(anchorArg.key);
1425
1679
  if (index === undefined) return;
1426
1680
  const stack = this.shadowRoot?.querySelector<HTMLElement>('.virtualStack');
1427
- containerArg.scrollTop = (stack?.offsetTop ?? 0) + (this.virtualOffsets[index] ?? 0) + anchorArg.offset;
1681
+ this.setProgrammaticScrollTop(
1682
+ containerArg,
1683
+ (stack?.offsetTop ?? 0) + (this.virtualOffsets[index] ?? 0) + anchorArg.offset,
1684
+ );
1428
1685
  }
1429
1686
 
1430
1687
  private restoreFullAnchor(
@@ -1435,9 +1692,13 @@ export class DeesHarnessMessageList extends DeesElement {
1435
1692
  `.stack > [data-timeline-key="${CSS.escape(anchorArg.key)}"]`,
1436
1693
  );
1437
1694
  if (!element) return;
1438
- containerArg.scrollTop += element.getBoundingClientRect().top
1439
- - containerArg.getBoundingClientRect().top
1440
- + anchorArg.offset;
1695
+ this.setProgrammaticScrollTop(
1696
+ containerArg,
1697
+ containerArg.scrollTop
1698
+ + element.getBoundingClientRect().top
1699
+ - containerArg.getBoundingClientRect().top
1700
+ + anchorArg.offset,
1701
+ );
1441
1702
  }
1442
1703
 
1443
1704
  private scheduleFullAnchorRestore(
@@ -1774,6 +2035,8 @@ export class DeesHarnessMessageList extends DeesElement {
1774
2035
  this.explicitRetainedVirtualKey = undefined;
1775
2036
  this.retainedVirtualKeys.clear();
1776
2037
  this.clearSelectionGesture();
2038
+ this.resetLoadEarlierState();
2039
+ this.clearProgrammaticScrollMarker();
1777
2040
  this.entryAnimationsEnabled = true;
1778
2041
  this.disconnectContentResizeObserver();
1779
2042
  this.disconnectVirtualObservers();
@@ -334,6 +334,33 @@ const renderTodo = (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpers):
334
334
  return html`<dees-harness-todos compact .todos=${todos}></dees-harness-todos>`;
335
335
  };
336
336
 
337
+ const projectTaskOutputItems = (output: unknown): unknown[] | undefined => {
338
+ if (!harnessIsRecord(output)) return undefined;
339
+ if (Array.isArray(output.tasks)) return output.tasks;
340
+ if (Object.prototype.hasOwnProperty.call(output, 'task')) return [output.task];
341
+ return harnessIsRecord(output.state) && Array.isArray(output.state.tasks)
342
+ ? output.state.tasks
343
+ : undefined;
344
+ };
345
+
346
+ const hasUnsupportedTodoStatus = (items: unknown[]): boolean => items.some((item) => {
347
+ if (!harnessIsRecord(item) || typeof item.status !== 'string') return false;
348
+ return item.status !== 'pending' && item.status !== 'in_progress' && item.status !== 'completed';
349
+ });
350
+
351
+ const renderProjectTask = (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpers): TemplateResult => {
352
+ const output = outputText(call);
353
+ const outputItems = projectTaskOutputItems(call.output);
354
+ if (outputItems !== undefined) {
355
+ const tasks = hasUnsupportedTodoStatus(outputItems) ? [] : harnessParseTodos(outputItems);
356
+ if (tasks.length) {
357
+ return html`<dees-harness-todos compact .todos=${tasks}></dees-harness-todos>`;
358
+ }
359
+ return helpers.monoBlock(output || harnessStringifyValue(outputItems));
360
+ }
361
+ return helpers.monoBlock(output || harnessSummarizeValue(call.input));
362
+ };
363
+
337
364
  const renderMcp = (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpers): TemplateResult => {
338
365
  return html`
339
366
  ${call.contentBlocks?.length ? helpers.contentBlocks(call.contentBlocks) : ''}
@@ -373,6 +400,7 @@ export const builtinBodyRenderers: Record<
373
400
  'json': renderJson,
374
401
  'search': renderSearch,
375
402
  'subtask': renderSubtask,
403
+ 'project-task': renderProjectTask,
376
404
  'todo': renderTodo,
377
405
  'mcp': renderMcp,
378
406
  'unknown': renderUnknown,
@@ -222,7 +222,7 @@ export interface IHarnessToolCall {
222
222
  isError?: boolean;
223
223
  errorText?: string;
224
224
  mcp?: IHarnessMcpMeta;
225
- /** Session spawned by this call (subagent/task tools); enables drill-in. */
225
+ /** Session spawned by this call (delegate/subagent tools); enables drill-in. */
226
226
  childSessionId?: string;
227
227
  /** Bounded live preview of the spawned session. Consumers own hydration. */
228
228
  subtask?: IHarnessSubtaskStream;
@@ -180,7 +180,7 @@ const defaultDescriptors: IHarnessToolDescriptor[] = [
180
180
  },
181
181
  {
182
182
  kind: 'subtask',
183
- names: ['task', 'subagent'],
183
+ names: ['delegate', 'subagent'],
184
184
  label: 'Subagent',
185
185
  icon: 'lucide:Bot',
186
186
  // The child's model belongs in the collapsed header: a subagent often
@@ -202,6 +202,16 @@ const defaultDescriptors: IHarnessToolDescriptor[] = [
202
202
  subtitle: (call) => inputField(call, 'question') || inputField(call, 'prompt') || harnessSummaryLine(call.input),
203
203
  defaultExpanded: true,
204
204
  },
205
+ {
206
+ kind: 'project-task',
207
+ names: ['task'],
208
+ label: 'Project task',
209
+ icon: 'lucide:ListChecks',
210
+ subtitle: (call) => {
211
+ const action = inputField(call, 'action');
212
+ return action ? `Action: ${action}` : harnessSummaryLine(call.input);
213
+ },
214
+ },
205
215
  {
206
216
  kind: 'todo',
207
217
  names: ['todowrite', 'todo_write', 'todoread'],