@aparte/plugin-subagents 1.6.0 → 1.7.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.
@@ -1,4 +1,4 @@
1
- import { createSegmentList, createStreamAdapter, runWithConfig, getSegmentRenderer, installDefaultRenderersOnce, deferHydrationOf, subscribeConfigChange, resolveConfig, contextConfig, pluralize, formatDuration, segmentTiming, aparteGlobalConfig, registerSegmentRenderer, unregisterSegmentRenderer } from "@aparte/core";
1
+ import { createSegmentList, createStreamAdapter, runWithConfig, getSegmentRenderer, installDefaultRenderersOnce, pluralize, deferHydrationOf, onRenderersChange, subscribeConfigChange, resolveConfig, contextConfig, formatDuration, segmentTiming, aparteGlobalConfig, registerSegmentRenderer, unregisterSegmentRenderer } from "@aparte/core";
2
2
  const SUBAGENT_SEGMENT_TYPE = "subagent";
3
3
  const STATUSES = /* @__PURE__ */ new Set(["running", "waiting", "done", "failed", "stopped"]);
4
4
  function subagentOf(segment) {
@@ -55,11 +55,41 @@ function callStatusOf(status) {
55
55
  return "aborted";
56
56
  }
57
57
  }
58
+ const open = /* @__PURE__ */ new Map();
59
+ const keyOf$1 = (messageId, agentId) => `${messageId}\0${agentId}`;
60
+ function stepStarted(messageId, agentId, name) {
61
+ const key = keyOf$1(messageId, agentId);
62
+ open.set(key, [...open.get(key) ?? [], name]);
63
+ }
64
+ function stepEnded(messageId, agentId, name) {
65
+ const key = keyOf$1(messageId, agentId);
66
+ const steps = open.get(key);
67
+ if (!steps) return;
68
+ const at = steps.lastIndexOf(name);
69
+ if (at < 0) return;
70
+ const rest = steps.filter((_, i) => i !== at);
71
+ if (rest.length > 0) open.set(key, rest);
72
+ else open.delete(key);
73
+ }
74
+ function stepsEnded(messageId, agentId) {
75
+ open.delete(keyOf$1(messageId, agentId));
76
+ }
77
+ function currentStep(messageId, agentId) {
78
+ if (messageId !== void 0) return open.get(keyOf$1(messageId, agentId))?.at(-1);
79
+ const suffix = `\0${agentId}`;
80
+ for (const [key, steps] of open) if (key.endsWith(suffix)) return steps.at(-1);
81
+ return void 0;
82
+ }
58
83
  const MAX_RUNS = 64;
59
84
  const MAX_LANES = 512;
60
85
  const MAX_EARLY = 2e3;
61
86
  const COALESCED = /* @__PURE__ */ new Set(["text-delta", "thinking-delta", "sources", "text-flush", "turn-start"]);
62
87
  const isOpen = (status) => (status ?? "pending") === "pending" || status === "awaiting-approval";
88
+ const lastWrites = /* @__PURE__ */ new Map();
89
+ const writeKey = (messageId, agentId) => `${messageId}\0${agentId}`;
90
+ function lastLaneWrite(messageId, agentId) {
91
+ return lastWrites.get(writeKey(messageId, agentId));
92
+ }
63
93
  const laneWatchers = /* @__PURE__ */ new Set();
64
94
  function watchLaneWrites(fn) {
65
95
  laneWatchers.add(fn);
@@ -160,7 +190,7 @@ function createLanes() {
160
190
  }
161
191
  }
162
192
  write(run, lane.host, lane.anchorId, updates);
163
- if (laneWatchers.size === 0 || run.closed) return;
193
+ if (run.closed) return;
164
194
  const anchor = anchorOf(run, lane) ?? {
165
195
  id: lane.anchorId,
166
196
  type: SUBAGENT_SEGMENT_TYPE,
@@ -174,6 +204,7 @@ function createLanes() {
174
204
  agentId: lane.id,
175
205
  anchor
176
206
  };
207
+ lastWrites.set(writeKey(run.messageId, lane.id), note);
177
208
  for (const fn of [...laneWatchers]) {
178
209
  try {
179
210
  fn(note);
@@ -224,6 +255,7 @@ function createLanes() {
224
255
  if (run.closed) return;
225
256
  for (const lane of run.lanes.values()) {
226
257
  const working = lane.status === "running" || lane.status === "waiting";
258
+ stepsEnded(run.messageId, lane.id);
227
259
  settleLane(lane, true);
228
260
  lane.streaming = false;
229
261
  run.dirty.add(lane);
@@ -233,6 +265,10 @@ function createLanes() {
233
265
  forget(run);
234
266
  };
235
267
  const forget = (run) => {
268
+ for (const lane of run.lanes.values()) {
269
+ stepsEnded(run.messageId, lane.id);
270
+ lastWrites.delete(writeKey(run.messageId, lane.id));
271
+ }
236
272
  run.closed = true;
237
273
  stopFrame(run);
238
274
  run.unlisten();
@@ -325,6 +361,7 @@ function createLanes() {
325
361
  };
326
362
  const retire = (run, lane) => {
327
363
  run.lanes.delete(lane.id);
364
+ stepsEnded(run.messageId, lane.id);
328
365
  if (lane.status === "running" || lane.status === "waiting") {
329
366
  lane.status = "stopped";
330
367
  settleLane(lane, true);
@@ -423,6 +460,7 @@ function createLanes() {
423
460
  if (e.result !== void 0) lane.result = e.result;
424
461
  if (e.error !== void 0) lane.error = e.error;
425
462
  if (e.status !== "waiting") {
463
+ stepsEnded(run.messageId, lane.id);
426
464
  settleLane(lane, false);
427
465
  lane.streaming = false;
428
466
  }
@@ -435,6 +473,8 @@ function createLanes() {
435
473
  if (event.type === "tool-awaiting-approval") lane.asking.add(event.toolCallId);
436
474
  else if (event.type !== "tool-start") lane.asking.delete(event.toolCallId);
437
475
  }
476
+ if (event.type === "step-start") stepStarted(run.messageId, lane.id, event.name);
477
+ else if (event.type === "step-end") stepEnded(run.messageId, lane.id, event.name);
438
478
  const before = lane.list.segments.length;
439
479
  lane.emit(event);
440
480
  if (event.type === "tool-start") callStarted(run, lane, event.toolCallId);
@@ -493,6 +533,58 @@ function createLanes() {
493
533
  }
494
534
  };
495
535
  }
536
+ const shown = /* @__PURE__ */ new Map();
537
+ const keyOf = (messageId, agentId) => `${messageId}\0${agentId}`;
538
+ function isShown(messageId, agentId) {
539
+ if (!messageId || shown.size === 0) return false;
540
+ const key = keyOf(messageId, agentId);
541
+ for (const value of shown.values()) if (value === key) return true;
542
+ return false;
543
+ }
544
+ function setShown(owner, at) {
545
+ if (at) shown.set(owner, keyOf(at.messageId, at.agentId));
546
+ else shown.delete(owner);
547
+ }
548
+ const openButtonOf = (row) => row.querySelector(":scope > :is(.aparte-tool-line, .aparte-tool-open-slot) > .aparte-tool-open");
549
+ function markRow(row, on) {
550
+ const button = openButtonOf(row);
551
+ if (!button) return;
552
+ row.toggleAttribute("data-current", on);
553
+ if (on) button.setAttribute("aria-current", "true");
554
+ else button.removeAttribute("aria-current");
555
+ }
556
+ function unmarkAll(root) {
557
+ for (const row of root.querySelectorAll(".aparte-segment-tool-call[data-current]")) markRow(row, false);
558
+ }
559
+ function targetIdOf(row) {
560
+ const chat = row.closest("aparte-chat");
561
+ if (chat) return chat.id || void 0;
562
+ const view = row.closest("aparte-subagent-view:not([data-inline])");
563
+ if (!view) return void 0;
564
+ const id = view.getAttribute("for") || (typeof view.chat?.id === "string" ? view.chat.id : "");
565
+ return id || void 0;
566
+ }
567
+ function dispatchOpen(row, segment, sub, standIn) {
568
+ const messageId = segment.messageId || row.closest("[message-id]")?.getAttribute("message-id") || "";
569
+ if (!messageId) return;
570
+ const targetId = targetIdOf(row);
571
+ const name = sub.name?.trim();
572
+ const toolCallId = standIn ? void 0 : segment.toolCall?.id;
573
+ const detail = {
574
+ ...targetId ? { targetId } : {},
575
+ messageId,
576
+ agentId: sub.id,
577
+ ...name ? { name } : {},
578
+ ...toolCallId ? { toolCallId } : {}
579
+ };
580
+ row.dispatchEvent(new CustomEvent("aparte-subagent-open", { bubbles: true, composed: true, detail }));
581
+ }
582
+ function innerRendererFor(segment, config) {
583
+ if (segment.type === "activity") return config.getActivityRenderer(segment.activityType) ?? null;
584
+ if (segment.type !== "tool_call") return null;
585
+ const name = segment.toolCall?.name;
586
+ return name && !config.isToolHidden(name) ? config.getToolRenderer(name) ?? null : null;
587
+ }
496
588
  function rendererFor(type, config) {
497
589
  const renderer = getSegmentRenderer(type, config);
498
590
  if (renderer) return renderer;
@@ -546,7 +638,7 @@ class LaneDrawing {
546
638
  seen.add(segment.id);
547
639
  const renderer = rendererFor(segment.type, config);
548
640
  let row = this.rows.get(segment.id);
549
- if (row && row.renderer !== renderer) {
641
+ if (row && (row.renderer !== renderer || row.inner !== innerRendererFor(segment, config))) {
550
642
  this.release(segment.id, config);
551
643
  row.el.remove();
552
644
  row = void 0;
@@ -582,7 +674,7 @@ class LaneDrawing {
582
674
  draw(segment, renderer, config, live) {
583
675
  const el = renderer ? toElement(runWithConfig(config, () => renderer.render(segment)), segment) : fallbackOf(segment);
584
676
  if (!el) return void 0;
585
- const row = { el, renderer, segment };
677
+ const row = { el, renderer, inner: innerRendererFor(segment, config), segment };
586
678
  this.rows.set(segment.id, row);
587
679
  if (renderer?.setup) {
588
680
  const as = drawnAs(segment, live);
@@ -603,9 +695,10 @@ class LaneDrawing {
603
695
  }
604
696
  if (renderer.update) {
605
697
  const before = row.el;
698
+ const next2 = before.nextSibling;
606
699
  runWithConfig(config, () => renderer.update(before, segment));
607
700
  if (before.parentNode !== this.container) {
608
- const now = this.childOf(segment.id);
701
+ const now = this.childOf(segment.id) ?? this.inPlaceOf(next2, segment.id);
609
702
  if (now) row.el = now;
610
703
  }
611
704
  return;
@@ -624,6 +717,18 @@ class LaneDrawing {
624
717
  }
625
718
  return null;
626
719
  }
720
+ /**
721
+ * The element standing where a row stood (before `next`) when it is no row's: what a
722
+ * renderer put in place of its own without the id (`outerHTML`, `replaceWith`), stamped so
723
+ * the loop finds it. A renderer that took its element away left another row there, or
724
+ * nothing: the row keeps its element, and the loop puts it back.
725
+ */
726
+ inPlaceOf(next, id) {
727
+ const there = next ? next.previousSibling : this.container.lastChild;
728
+ if (there?.nodeType !== Node.ELEMENT_NODE || there.hasAttribute("data-segment-id")) return null;
729
+ there.setAttribute("data-segment-id", id);
730
+ return there;
731
+ }
627
732
  release(id, config) {
628
733
  const row = this.rows.get(id);
629
734
  if (!row) return;
@@ -640,7 +745,7 @@ class LaneDrawing {
640
745
  }
641
746
  }
642
747
  }
643
- const styles = "/*\n * @aparte/plugin-subagents — a sub-agent's conversation.\n *\n * Only what is the plugin's own. The rows inside are core's (text, reasoning, tool rows,\n * a nested sub-agent's line) and wear core's sheets; under a line, the transcript sits in\n * core's tool detail, which already draws the rule and the indent a disclosure has. So\n * this sheet draws the task line, the spacing of the rows, and — for the standalone view\n * only — the same quiet rule down the leading edge, since nothing of core's is around it.\n *\n * Logical properties throughout, so a right-to-left locale puts the rule on its own\n * leading edge. No colour of its own: core's tokens. Nothing moves, so there is no motion\n * to reduce.\n */\naparte-subagent-view {\n display: block;\n min-inline-size: 0;\n}\n\n/* Standalone (a canvas, a pane, a dialog): outside any message, so the conversation's\n type is set here, as core's segment container sets it in a reply. */\naparte-subagent-view:not([data-inline]) {\n font-size: var(--aparte-content-font-size);\n line-height: var(--aparte-content-line-height);\n color: var(--aparte-content-color);\n font-family: var(--aparte-font-family);\n}\n\n/* Nothing to show (an unknown sub-agent, or one not started yet): nothing drawn. */\naparte-subagent-view[data-empty] {\n display: none;\n}\n\n.aparte-subagent-task {\n margin: 0 0 var(--aparte-space-3);\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-sm);\n}\n\n.aparte-subagent-transcript {\n display: flex;\n flex-direction: column;\n gap: var(--aparte-space-4);\n min-inline-size: 0;\n}\n\n.aparte-subagent-transcript:empty {\n display: none;\n}\n\naparte-subagent-view:not([data-inline]) > .aparte-subagent-transcript {\n padding-inline-start: var(--aparte-subagent-indent, var(--aparte-space-6));\n border-inline-start: var(--aparte-border-width) solid var(--aparte-border);\n}\n";
748
+ const styles = "/*\n * @aparte/plugin-subagents — a sub-agent's conversation.\n *\n * Only what is the plugin's own. The rows inside are core's (text, reasoning, tool rows,\n * a nested sub-agent's line) and wear core's sheets; under a line, the transcript sits in\n * core's tool detail, which already draws the rule and the indent a disclosure has. So\n * this sheet draws the task line, the spacing of the rows, and — for the standalone view\n * only — the same quiet rule down the leading edge, since nothing of core's is around it.\n *\n * Logical properties throughout, so a right-to-left locale puts the rule on its own\n * leading edge. No colour of its own: core's tokens. Nothing moves, so there is no motion\n * to reduce.\n */\naparte-subagent-view {\n display: block;\n min-inline-size: 0;\n}\n\n/* Standalone (a canvas, a pane, a dialog): outside any message, so the conversation's\n type is set here, as core's segment container sets it in a reply. */\naparte-subagent-view:not([data-inline]) {\n font-size: var(--aparte-content-font-size);\n line-height: var(--aparte-content-line-height);\n color: var(--aparte-content-color);\n font-family: var(--aparte-font-family);\n}\n\n/* Nothing to show (an unknown sub-agent, or one not started yet): nothing drawn. */\naparte-subagent-view[data-empty] {\n display: none;\n}\n\n/* A closable view (a pane of its own): the sub-agent's name, and Close at the far end. The\n name takes the focus when the pane opens, so it shows a ring only to the keyboard. */\n.aparte-subagent-head {\n display: flex;\n align-items: center;\n gap: var(--aparte-space-3);\n margin-block-end: var(--aparte-space-3);\n}\n\n.aparte-subagent-head[hidden] {\n display: none;\n}\n\n.aparte-subagent-name {\n flex: 1 1 auto;\n min-inline-size: 0;\n margin: 0;\n overflow: hidden;\n font-size: var(--aparte-font-size-md);\n font-weight: var(--aparte-font-weight-semibold);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.aparte-subagent-name:focus {\n outline: none;\n}\n\n.aparte-subagent-name:focus-visible {\n outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);\n}\n\n.aparte-subagent-close {\n flex: none;\n}\n\n.aparte-subagent-task {\n margin: 0 0 var(--aparte-space-3);\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-sm);\n}\n\n.aparte-subagent-transcript {\n display: flex;\n flex-direction: column;\n gap: var(--aparte-space-4);\n min-inline-size: 0;\n}\n\n.aparte-subagent-transcript:empty {\n display: none;\n}\n\naparte-subagent-view:not([data-inline]) > .aparte-subagent-transcript {\n padding-inline-start: var(--aparte-subagent-indent, var(--aparte-space-6));\n border-inline-start: var(--aparte-border-width) solid var(--aparte-border);\n}\n";
644
749
  const STYLE_ID = "aparte-subagent-styles";
645
750
  const MARK = ".aparte-subagent-transcript";
646
751
  let holder = null;
@@ -660,6 +765,86 @@ function ensureStyles() {
660
765
  document.head.appendChild(style);
661
766
  holder = style;
662
767
  }
768
+ const WORDS = {
769
+ /** How many tool calls a sub-agent made, on its line: "3 tools". */
770
+ subagentsTools: {
771
+ en: "one:{n} tool|other:{n} tools",
772
+ fr: "one:{n} outil|other:{n} outils",
773
+ de: "one:{n} Tool|other:{n} Tools",
774
+ es: "one:{n} herramienta|other:{n} herramientas",
775
+ it: "one:{n} strumento|other:{n} strumenti",
776
+ ja: "other:{n} 個のツール",
777
+ ko: "other:도구 {n}개",
778
+ pt: "one:{n} ferramenta|other:{n} ferramentas",
779
+ ru: "one:{n} инструмент|few:{n} инструмента|many:{n} инструментов|other:{n} инструмента",
780
+ zh: "other:{n} 个工具"
781
+ },
782
+ /** What a sub-agent's second line says before its first call. */
783
+ subagentsStarting: {
784
+ en: "Starting…",
785
+ fr: "Démarrage…",
786
+ de: "Wird gestartet…",
787
+ es: "Iniciando…",
788
+ it: "Avvio…",
789
+ ja: "開始しています…",
790
+ ko: "시작하는 중…",
791
+ pt: "Iniciando…",
792
+ ru: "Запуск…",
793
+ zh: "正在启动…"
794
+ },
795
+ /** The name of a sub-agent that was given none, when no call names its line either. */
796
+ subagentsUnnamed: {
797
+ en: "Sub-agent",
798
+ fr: "Sous-agent",
799
+ de: "Subagent",
800
+ es: "Subagente",
801
+ it: "Sottoagente",
802
+ ja: "サブエージェント",
803
+ ko: "하위 에이전트",
804
+ pt: "Subagente",
805
+ ru: "Субагент",
806
+ zh: "子智能体"
807
+ },
808
+ /** The Open button on a sub-agent's line (`setupSubagents({ open: 'event' })`): its name and tooltip. */
809
+ subagentsOpen: {
810
+ en: "Open {name}",
811
+ fr: "Ouvrir {name}",
812
+ de: "{name} öffnen",
813
+ es: "Abrir {name}",
814
+ it: "Apri {name}",
815
+ ja: "{name} を開く",
816
+ ko: "{name} 열기",
817
+ pt: "Abrir {name}",
818
+ ru: "Открыть: {name}",
819
+ zh: "打开 {name}"
820
+ },
821
+ /** The Close button of a `<aparte-subagent-view closable>`. */
822
+ subagentsClose: {
823
+ en: "Close",
824
+ fr: "Fermer",
825
+ de: "Schließen",
826
+ es: "Cerrar",
827
+ it: "Chiudi",
828
+ ja: "閉じる",
829
+ ko: "닫기",
830
+ pt: "Fechar",
831
+ ru: "Закрыть",
832
+ zh: "关闭"
833
+ }
834
+ };
835
+ function languageOf(config) {
836
+ const tag = config.getLocale().tag;
837
+ return typeof tag === "string" && tag ? tag.split("-")[0].toLowerCase() : "en";
838
+ }
839
+ function word(config, key) {
840
+ const fromApp = config.getLocale()[key];
841
+ const table = WORDS[key];
842
+ return typeof fromApp === "string" && fromApp || table[languageOf(config)] || table.en;
843
+ }
844
+ function toolCount(config, n) {
845
+ const tag = config.getLocale().tag;
846
+ return pluralize(word(config, "subagentsTools"), n, typeof tag === "string" && tag ? tag : void 0);
847
+ }
663
848
  const CONVERSATION_EVENTS = [
664
849
  "aparte-subagent-start",
665
850
  "aparte-subagent-end",
@@ -702,10 +887,14 @@ const ElementBase = typeof HTMLElement === "undefined" ? class {
702
887
  } : HTMLElement;
703
888
  class AparteSubagentView extends ElementBase {
704
889
  static get observedAttributes() {
705
- return ["for", "message-id", "agent-id"];
890
+ return ["for", "message-id", "agent-id", "closable"];
706
891
  }
707
892
  _chat = null;
708
893
  _task = null;
894
+ /** A closable view's head: the sub-agent's name and Close. */
895
+ _head = null;
896
+ _name = null;
897
+ _close = null;
709
898
  _drawing = null;
710
899
  /** The config the rows were drawn with, and the segment they were drawn from. */
711
900
  _config = null;
@@ -713,10 +902,20 @@ class AparteSubagentView extends ElementBase {
713
902
  _listening = null;
714
903
  _stopConfig = null;
715
904
  _stopWatching = null;
905
+ _stopRenderers = null;
716
906
  _queued = false;
717
907
  _releaseQueued = false;
718
908
  _warned = "";
719
909
  _onConversation = () => this._schedule();
910
+ _onKeydown = (event) => {
911
+ if (event.key !== "Escape" || event.defaultPrevented || !this._closable()) return;
912
+ event.preventDefault();
913
+ this._emitClose();
914
+ };
915
+ /** A renderer registered or taken off on the config the rows were drawn with: the rows it changes are drawn again. */
916
+ _onRenderers = (config) => {
917
+ if (config === this._config) this.refresh();
918
+ };
720
919
  _onWrite = (write) => {
721
920
  if (fed.has(this) || !this.isConnected) return;
722
921
  if (write.messageId !== this.getAttribute("message-id") || write.agentId !== this.getAttribute("agent-id")) return;
@@ -734,6 +933,10 @@ class AparteSubagentView extends ElementBase {
734
933
  this._listen();
735
934
  this.refresh();
736
935
  }
936
+ /** Put the focus on the sub-agent's name, in a `closable` view: where a pane that just opened starts. */
937
+ focusTitle() {
938
+ if (this._name && !this._head?.hidden) this._name.focus();
939
+ }
737
940
  /** Read the sub-agent again and draw it: what the element does on every change it hears. */
738
941
  refresh() {
739
942
  const feed = fed.get(this);
@@ -745,6 +948,8 @@ class AparteSubagentView extends ElementBase {
745
948
  connectedCallback() {
746
949
  ensureStyles();
747
950
  this._build();
951
+ this.addEventListener("keydown", this._onKeydown);
952
+ this._stopRenderers ??= onRenderersChange(this._onRenderers);
748
953
  if (!fed.has(this)) {
749
954
  this._listen();
750
955
  this._stopWatching ??= watchLaneWrites(this._onWrite);
@@ -755,11 +960,14 @@ class AparteSubagentView extends ElementBase {
755
960
  this.refresh();
756
961
  }
757
962
  disconnectedCallback() {
963
+ this.removeEventListener("keydown", this._onKeydown);
758
964
  this._unlisten();
759
965
  this._stopWatching?.();
760
966
  this._stopWatching = null;
761
967
  this._stopConfig?.();
762
968
  this._stopConfig = null;
969
+ this._stopRenderers?.();
970
+ this._stopRenderers = null;
763
971
  if (this._releaseQueued) return;
764
972
  this._releaseQueued = true;
765
973
  queueMicrotask(() => {
@@ -772,6 +980,7 @@ class AparteSubagentView extends ElementBase {
772
980
  if (previous === value || !this.isConnected || fed.has(this)) return;
773
981
  if (_name === "for") this._listen();
774
982
  this.refresh();
983
+ if (_name === "closable" && this._config) this._relabelHead(this._config);
775
984
  }
776
985
  // ── Reading the conversation ───────────────────────────────────────────
777
986
  _source() {
@@ -809,7 +1018,8 @@ class AparteSubagentView extends ElementBase {
809
1018
  } catch {
810
1019
  return null;
811
1020
  }
812
- const anchor = message ? findSubagent(message.segments ?? [], agentId) : void 0;
1021
+ const live = lastLaneWrite(messageId, agentId);
1022
+ const anchor = (message ? findSubagent(message.segments ?? [], agentId) : void 0) ?? (live && this._matches(live) ? live.anchor : void 0);
813
1023
  const key = `${messageId}\0${agentId}`;
814
1024
  if (message && !anchor && !message.isStreaming && message.status !== "pending" && message.status !== "streaming" && this._warned !== key) {
815
1025
  this._warned = key;
@@ -844,21 +1054,64 @@ class AparteSubagentView extends ElementBase {
844
1054
  }
845
1055
  _onConfigChange() {
846
1056
  this.refresh();
847
- if (this._config) this._drawing?.relabel(this._config);
1057
+ if (this._config) {
1058
+ this._drawing?.relabel(this._config);
1059
+ this._relabelHead(this._config);
1060
+ }
1061
+ }
1062
+ /** A pane of its own, never a line's detail. */
1063
+ _closable() {
1064
+ return this.hasAttribute("closable") && !fed.has(this);
1065
+ }
1066
+ _emitClose() {
1067
+ this.dispatchEvent(new CustomEvent("aparte-subagent-view-close", { bubbles: true, composed: true }));
1068
+ }
1069
+ /** The head's words, in the chat's language: the name, and Close. */
1070
+ _relabelHead(config) {
1071
+ if (!this._head) return;
1072
+ const closable = this._closable();
1073
+ this._head.hidden = !closable;
1074
+ if (!closable) {
1075
+ this._name.textContent = "";
1076
+ return;
1077
+ }
1078
+ const name = (this._anchor ? subagentOf(this._anchor)?.name?.trim() : "") || word(config, "subagentsUnnamed");
1079
+ if (this._name.textContent !== name) this._name.textContent = name;
1080
+ const close = word(config, "subagentsClose");
1081
+ if (this._close.getAttribute("aria-label") !== close) {
1082
+ this._close.setAttribute("aria-label", close);
1083
+ this._close.title = close;
1084
+ this._close.innerHTML = config.getIcon("close");
1085
+ }
848
1086
  }
849
1087
  // ── Drawing ────────────────────────────────────────────────────────────
850
1088
  _build() {
851
1089
  if (this._drawing) {
852
1090
  if (this._task.parentNode !== this) this.prepend(this._task);
1091
+ if (this._head.parentNode !== this) this.prepend(this._head);
853
1092
  if (this._drawing.container.parentNode !== this) this.append(this._drawing.container);
854
1093
  return;
855
1094
  }
1095
+ const head = this.ownerDocument.createElement("div");
1096
+ head.className = "aparte-subagent-head";
1097
+ head.hidden = true;
1098
+ const name = this.ownerDocument.createElement("h2");
1099
+ name.className = "aparte-subagent-name";
1100
+ name.tabIndex = -1;
1101
+ const close = this.ownerDocument.createElement("button");
1102
+ close.type = "button";
1103
+ close.className = "aparte-btn aparte-btn--icon aparte-btn--sm aparte-subagent-close";
1104
+ close.addEventListener("click", () => this._emitClose());
1105
+ head.append(name, close);
1106
+ this._head = head;
1107
+ this._name = name;
1108
+ this._close = close;
856
1109
  const task = this.ownerDocument.createElement("p");
857
1110
  task.className = "aparte-subagent-task";
858
1111
  task.hidden = true;
859
1112
  const list = this.ownerDocument.createElement("div");
860
1113
  list.className = "aparte-subagent-transcript";
861
- this.append(task, list);
1114
+ this.append(head, task, list);
862
1115
  this._task = task;
863
1116
  this._drawing = new LaneDrawing(list);
864
1117
  }
@@ -874,6 +1127,7 @@ class AparteSubagentView extends ElementBase {
874
1127
  const task = (anchor ? subagentOf(anchor)?.description?.trim() : "") ?? "";
875
1128
  if (this._task.textContent !== task) this._task.textContent = task;
876
1129
  this._task.hidden = !task;
1130
+ this._relabelHead(config);
877
1131
  const lane = anchor ? laneOf(anchor) : { segments: [], live: false };
878
1132
  runWithConfig(config, () => drawing.sync(lane.segments, config, lane.live));
879
1133
  }
@@ -927,60 +1181,6 @@ const subagentTranscript = (segment) => {
927
1181
  feedView(view, segment, config);
928
1182
  return view;
929
1183
  };
930
- const WORDS = {
931
- /** How many tool calls a sub-agent made, on its line: "3 tools". */
932
- subagentsTools: {
933
- en: "one:{n} tool|other:{n} tools",
934
- fr: "one:{n} outil|other:{n} outils",
935
- de: "one:{n} Tool|other:{n} Tools",
936
- es: "one:{n} herramienta|other:{n} herramientas",
937
- it: "one:{n} strumento|other:{n} strumenti",
938
- ja: "other:{n} 個のツール",
939
- ko: "other:도구 {n}개",
940
- pt: "one:{n} ferramenta|other:{n} ferramentas",
941
- ru: "one:{n} инструмент|few:{n} инструмента|many:{n} инструментов|other:{n} инструмента",
942
- zh: "other:{n} 个工具"
943
- },
944
- /** What a sub-agent's second line says before its first call. */
945
- subagentsStarting: {
946
- en: "Starting…",
947
- fr: "Démarrage…",
948
- de: "Wird gestartet…",
949
- es: "Iniciando…",
950
- it: "Avvio…",
951
- ja: "開始しています…",
952
- ko: "시작하는 중…",
953
- pt: "Iniciando…",
954
- ru: "Запуск…",
955
- zh: "正在启动…"
956
- },
957
- /** The name of a sub-agent that was given none, when no call names its line either. */
958
- subagentsUnnamed: {
959
- en: "Sub-agent",
960
- fr: "Sous-agent",
961
- de: "Subagent",
962
- es: "Subagente",
963
- it: "Sottoagente",
964
- ja: "サブエージェント",
965
- ko: "하위 에이전트",
966
- pt: "Subagente",
967
- ru: "Субагент",
968
- zh: "子智能体"
969
- }
970
- };
971
- function languageOf(config) {
972
- const tag = config.getLocale().tag;
973
- return typeof tag === "string" && tag ? tag.split("-")[0].toLowerCase() : "en";
974
- }
975
- function word(config, key) {
976
- const fromApp = config.getLocale()[key];
977
- const table = WORDS[key];
978
- return typeof fromApp === "string" && fromApp || table[languageOf(config)] || table.en;
979
- }
980
- function toolCount(config, n) {
981
- const tag = config.getLocale().tag;
982
- return pluralize(word(config, "subagentsTools"), n, typeof tag === "string" && tag ? tag : void 0);
983
- }
984
1184
  const standIns = /* @__PURE__ */ new WeakSet();
985
1185
  function markStandIn(segment) {
986
1186
  standIns.add(segment);
@@ -999,12 +1199,12 @@ function callLine(call, config) {
999
1199
  const name = call.toolCall?.name ?? "";
1000
1200
  const nested = subagentOf(call);
1001
1201
  const label = config.getToolDisplay(name)?.label;
1002
- const shown = (label !== void 0 ? textOf(() => typeof label === "function" ? label(call) : label) : "") || nested?.name?.trim() || name;
1202
+ const shown2 = (label !== void 0 ? textOf(() => typeof label === "function" ? label(call) : label) : "") || nested?.name?.trim() || name;
1003
1203
  const summary = config.getToolSummary(name);
1004
1204
  const target = (summary ? textOf(() => summary(call)) : "") || nested?.description?.trim() || "";
1005
- return [shown, target].filter(Boolean).join(" ");
1205
+ return [shown2, target].filter(Boolean).join(" ");
1006
1206
  }
1007
- function activityOf(sub, config) {
1207
+ function activityOf(sub, config, step) {
1008
1208
  let latest;
1009
1209
  for (let i = sub.segments.length - 1; i >= 0; i -= 1) {
1010
1210
  const segment = sub.segments[i];
@@ -1012,6 +1212,7 @@ function activityOf(sub, config) {
1012
1212
  if (isOpenCall(segment)) return callLine(segment, config);
1013
1213
  latest ??= segment;
1014
1214
  }
1215
+ if (step) return step;
1015
1216
  return latest ? callLine(latest, config) : "";
1016
1217
  }
1017
1218
  function durationOf(anchor, sub) {
@@ -1069,7 +1270,7 @@ function createLineProvider(options = {}) {
1069
1270
  const note = [];
1070
1271
  if (!standIn && statusOfCall(segment.status) !== status) note.push(stateWord(config, status));
1071
1272
  if (options.activity !== false) {
1072
- line.activity = live ? activityOf(sub, config) || word(config, "subagentsStarting") : summary() || stateWord(config, status);
1273
+ line.activity = live ? activityOf(sub, config, currentStep(segment.messageId, sub.id)) || word(config, "subagentsStarting") : summary() || stateWord(config, status);
1073
1274
  } else if (!live) {
1074
1275
  const done = summary();
1075
1276
  if (done) note.push(done);
@@ -1077,6 +1278,14 @@ function createLineProvider(options = {}) {
1077
1278
  if (note.length > 0) line.note = note.join(" · ");
1078
1279
  const details = options.details;
1079
1280
  line.details = typeof details === "function" ? details : details === "transcript" ? subagentTranscript : false;
1281
+ if (options.open === "event") {
1282
+ const shownName = name || word(config, "subagentsUnnamed");
1283
+ line.open = {
1284
+ label: word(config, "subagentsOpen").replace("{name}", () => shownName),
1285
+ current: isShown(segment.messageId, sub.id),
1286
+ onOpen: (row) => dispatchOpen(row, segment, sub, standIn)
1287
+ };
1288
+ }
1080
1289
  return line;
1081
1290
  };
1082
1291
  }
@@ -1153,12 +1362,108 @@ function setupSubagents(options = {}, config = aparteGlobalConfig) {
1153
1362
  installed.set(config, dispose);
1154
1363
  return dispose;
1155
1364
  }
1365
+ function connectSubagentView(options) {
1366
+ const { chat, view, split } = options;
1367
+ const viewPane = options.pane ?? "end";
1368
+ const chatPane = viewPane === "end" ? "start" : "end";
1369
+ const owner = {};
1370
+ if (!view.chat && !view.getAttribute("for")) view.chat = chat;
1371
+ const wasClosable = view.hasAttribute("closable");
1372
+ view.setAttribute("closable", "");
1373
+ let opener = null;
1374
+ let opened = null;
1375
+ let isOpen2 = false;
1376
+ const roots = () => chat.contains(view) ? [chat] : [chat, view];
1377
+ const mark = (row) => {
1378
+ for (const root of roots()) unmarkAll(root);
1379
+ if (row) markRow(row, true);
1380
+ };
1381
+ const fold = () => {
1382
+ if (split) {
1383
+ split.pane = chatPane;
1384
+ split.single = true;
1385
+ }
1386
+ isOpen2 = false;
1387
+ options.onClose?.();
1388
+ };
1389
+ const onOpen = (event) => {
1390
+ const detail = event.detail;
1391
+ if (!detail?.messageId || !detail.agentId) return;
1392
+ const row = event.target instanceof HTMLElement ? event.target : null;
1393
+ opener = row;
1394
+ opened = detail;
1395
+ setShown(owner, { messageId: detail.messageId, agentId: detail.agentId });
1396
+ mark(row);
1397
+ view.setAttribute("message-id", detail.messageId);
1398
+ view.setAttribute("agent-id", detail.agentId);
1399
+ if (split) {
1400
+ split.single = false;
1401
+ split.pane = viewPane;
1402
+ }
1403
+ isOpen2 = true;
1404
+ options.onOpen?.(detail);
1405
+ view.focusTitle();
1406
+ };
1407
+ const close = (giveFocusBack) => {
1408
+ const active = view.ownerDocument.activeElement;
1409
+ const adrift = !active || active === view.ownerDocument.body || view.contains(active);
1410
+ setShown(owner, null);
1411
+ mark(null);
1412
+ fold();
1413
+ if (giveFocusBack) {
1414
+ const line = opener?.isConnected ? opener : lineOf(opened);
1415
+ const button = line ? openButtonOf(line) : null;
1416
+ if (button) button.focus();
1417
+ else if (adrift) focusComposer(chat);
1418
+ }
1419
+ opener = null;
1420
+ opened = null;
1421
+ };
1422
+ const lineOf = (detail) => {
1423
+ if (!detail) return null;
1424
+ const id = detail.toolCallId ?? detail.agentId;
1425
+ for (const root of roots()) {
1426
+ for (const row of root.querySelectorAll(".aparte-segment-tool-call[data-tool-call-id]")) {
1427
+ if (row.getAttribute("data-tool-call-id") === id && openButtonOf(row)) return row;
1428
+ }
1429
+ }
1430
+ return null;
1431
+ };
1432
+ const onClose = () => close(true);
1433
+ const onLoaded = () => {
1434
+ if (isOpen2 || opened) close(false);
1435
+ view.removeAttribute("message-id");
1436
+ view.removeAttribute("agent-id");
1437
+ };
1438
+ chat.addEventListener("aparte-subagent-open", onOpen);
1439
+ const viewListens = !chat.contains(view);
1440
+ if (viewListens) view.addEventListener("aparte-subagent-open", onOpen);
1441
+ view.addEventListener("aparte-subagent-view-close", onClose);
1442
+ for (const name of ["aparte-conversation-loaded", "aparte-reset-done"]) chat.addEventListener(name, onLoaded);
1443
+ if (split && !view.getAttribute("agent-id")) {
1444
+ split.pane = chatPane;
1445
+ split.single = true;
1446
+ }
1447
+ return () => {
1448
+ chat.removeEventListener("aparte-subagent-open", onOpen);
1449
+ if (viewListens) view.removeEventListener("aparte-subagent-open", onOpen);
1450
+ view.removeEventListener("aparte-subagent-view-close", onClose);
1451
+ for (const name of ["aparte-conversation-loaded", "aparte-reset-done"]) chat.removeEventListener(name, onLoaded);
1452
+ setShown(owner, null);
1453
+ mark(null);
1454
+ if (!wasClosable) view.removeAttribute("closable");
1455
+ };
1456
+ }
1457
+ function focusComposer(chat) {
1458
+ chat.querySelector("aparte-composer-input")?.focusEnd?.();
1459
+ }
1156
1460
  export {
1157
1461
  AparteSubagentView as A,
1158
1462
  SUBAGENT_SEGMENT_TYPE as S,
1159
1463
  subagentOf as a,
1160
1464
  subagentStatus as b,
1161
- subagentTranscript as c,
1465
+ connectSubagentView as c,
1466
+ subagentTranscript as d,
1162
1467
  setupSubagents as s
1163
1468
  };
1164
- //# sourceMappingURL=setup-CaTzFtu8.js.map
1469
+ //# sourceMappingURL=connect-65jiVmf_.js.map