wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<!-- ABOUTME: Expandable inline view of a subagent's conversation.
     ABOUTME: Lazily loads and renders subagent messages within a parent ToolBlock. -->
<script lang="ts">
  import type { Message, Session } from "../../api/types.js";
  import { getMessages, getSession } from "../../api/client.js";
  import { formatTokenCount } from "../../utils/format.js";
  import { computeMainModel } from "../../utils/model.js";
  import { sessions } from "../../stores/sessions.svelte.js";
  import { router } from "../../stores/router.svelte.js";
  import { context } from "../../stores/context.svelte.js";
  import MessageContent from "./MessageContent.svelte";

  interface Props {
    sessionId: string;
  }

  let { sessionId }: Props = $props();
  let expanded = $state(false);
  let messages = $state<Message[] | null>(null);
  let sessionMeta = $state<Session | null>(null);
  let loading = $state(false);
  let error = $state<string | null>(null);

  let subagentSession = $derived(sessions.childSessions.get(sessionId) ?? null);

  async function toggleExpand() {
    expanded = !expanded;
    if (expanded && !messages) {
      loading = true;
      error = null;
      try {
        const [resp, meta] = await Promise.all([
          getMessages(sessionId, { limit: 1000 }),
          getSession(sessionId).catch(() => null),
        ]);
        messages = resp.messages;
        sessionMeta = meta;
      } catch (e) {
        error = e instanceof Error ? e.message : "Failed to load";
      } finally {
        loading = false;
      }
    }
  }

  async function openAsSession(e: MouseEvent) {
    e.preventDefault();
    e.stopPropagation();
    void context.openSession(sessionId, false);
  }

  let agentLabel = $derived(sessionMeta?.agent ?? null);
  let messageCountLabel = $derived(
    sessionMeta ? `${sessionMeta.message_count} messages` : null,
  );
  let subagentModel = $derived(
    messages && sessionMeta &&
    messages.length >= sessionMeta.message_count
      ? computeMainModel(messages)
      : "",
  );
</script>

<div class="subagent-inline">
  <div class="subagent-header">
    <button class="subagent-toggle" onclick={toggleExpand}>
      <span class="toggle-chevron" class:open={expanded}>&#9656;</span>
      <span class="toggle-label">Subagent session</span>
      {#if agentLabel}
        <span class="toggle-meta">{agentLabel}</span>
      {/if}
      {#if messageCountLabel}
        <span class="toggle-meta">{messageCountLabel}</span>
      {/if}
      <span class="toggle-session-id">{sessionId.slice(0, 12)}</span>
      {#if subagentSession}
        {@const ctxTokens = subagentSession.peak_context_tokens}
        {#if ctxTokens + subagentSession.total_output_tokens > 0}
          <span class="toggle-tokens">({formatTokenCount(ctxTokens)} ctx / {formatTokenCount(subagentSession.total_output_tokens)} out)</span>
        {/if}
        {#if subagentModel}
          <span class="toggle-model" title={subagentModel}>{subagentModel}</span>
        {/if}
      {/if}
    </button>
    <a
      href={router.href({ level: "session", session: sessionId })}
      class="open-session-link"
      onclick={openAsSession}
      title="Open as full session"
    >
      Open session &#8599;
    </a>
  </div>

  {#if expanded}
    <div class="subagent-messages">
      {#if loading}
        <div class="subagent-status">Loading...</div>
      {:else if error}
        <div class="subagent-status subagent-error">{error}</div>
      {:else if messages && messages.length > 0}
        {#each messages as message}
          <MessageContent {message} isSubagentContext={true} />
        {/each}
      {:else if messages}
        <div class="subagent-status">No messages</div>
      {/if}
    </div>
  {/if}
</div>

<style>
  .subagent-inline {
    border-top: 1px solid var(--vscode-widget-border);
    margin-top: var(--wb-spacing-2);
  }

  .subagent-header {
    display: flex;
    align-items: center;
  }

  .subagent-toggle {
    display: flex;
    align-items: center;
    gap: var(--wb-spacing-6);
    padding: var(--wb-spacing-6) var(--wb-spacing-10);
    flex: 1;
    min-width: 0;
    text-align: left;
    font-size: var(--wb-font-size-11);
    color: var(--vscode-testing-iconPassed);
    border-radius: 0 0 0 var(--wsb-radius-sm);
    transition: background 0.1s;
  }

  .subagent-toggle:hover {
    background: var(--vscode-list-hoverBackground);
  }

  .toggle-chevron {
    display: inline-block;
    font-size: var(--wb-font-size-10);
    transition: transform 0.15s;
    flex-shrink: 0;
  }

  .toggle-chevron.open {
    transform: rotate(90deg);
  }

  .toggle-label {
    font-weight: 600;
    white-space: nowrap;
  }

  .toggle-meta {
    font-family: var(--wsb-font-mono);
    font-size: var(--wb-font-size-10);
    color: var(--vscode-disabledForeground);
    background: var(--vscode-input-background);
    padding: var(--wb-spacing-1) var(--wb-spacing-5);
    border-radius: var(--wsb-radius-sm);
    white-space: nowrap;
  }

  .toggle-session-id {
    font-family: var(--wsb-font-mono);
    font-size: var(--wb-font-size-10);
    color: var(--vscode-disabledForeground);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
  }

  .open-session-link {
    font-size: var(--wb-font-size-10);
    color: var(--vscode-foreground);
    padding: var(--wb-spacing-6) var(--wb-spacing-10);
    white-space: nowrap;
    flex-shrink: 0;
    text-decoration: none;
    transition: color 0.1s, background 0.1s;
  }

  .open-session-link:hover {
    color: var(--vscode-testing-iconPassed);
    background: var(--vscode-list-hoverBackground);
  }

  .toggle-tokens {
    font-size: var(--wb-font-size-10);
    font-variant-numeric: tabular-nums;
    color: color-mix(in srgb, var(--vscode-testing-iconPassed) 60%, var(--vscode-disabledForeground));
    white-space: nowrap;
    flex-shrink: 0;
  }

  .toggle-model {
    font-size: var(--wb-font-size-10);
    color: var(--vscode-disabledForeground);
    white-space: nowrap;
    flex-shrink: 0;
  }

  .subagent-messages {
    border-left: 3px solid var(--vscode-testing-iconPassed);
    margin: 0 0 var(--wb-spacing-4) var(--wb-spacing-10);
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-4);
    padding: var(--wb-spacing-4) 0;
  }

  .subagent-status {
    padding: var(--wb-spacing-8) var(--wb-spacing-14);
    font-size: var(--wb-font-size-12);
    color: var(--vscode-disabledForeground);
  }

  .subagent-error {
    color: var(--vscode-errorForeground);
  }
</style>