From 510fd8cbe53abb39cba2c7cbaaefcf2783dc0066 Mon Sep 17 00:00:00 2001
From: "netop://ウィビ" <paul@webb.page>
Date: Fri, 24 Apr 2026 16:37:33 -0700
Subject: Implement v0.6-1.0: shortcuts, format, export/import, splitter,
 timing, APQ

- v0.6: matchShortcut + format(); Cmd+Shift+Enter/W/F + Cmd+Alt+arrows
- v0.7: SessionStore.exportAll/importTabs with version-1 validator
- v0.8: Splitter component + four resize handles persisted under layout.*
- v0.10: createApqFetcher (HTTP-only) wrapping shared http-body helpers
- Drop .svelte re-exports from index.ts for multi-entry JSR/npm publishing
---
 source/library/components/ResultViewer.svelte | 51 +++++++++++++++++++++++++--
 1 file changed, 49 insertions(+), 2 deletions(-)

(limited to 'source/library/components/ResultViewer.svelte')

diff --git a/source/library/components/ResultViewer.svelte b/source/library/components/ResultViewer.svelte
index 083828d..d277ec8 100644
--- a/source/library/components/ResultViewer.svelte
+++ b/source/library/components/ResultViewer.svelte
@@ -2,14 +2,50 @@
   import Editor from "./Editor.svelte";
   import type { Extension } from "@codemirror/state";
   import type { Snippet } from "svelte";
+  import type { TabTiming } from "../state/session.svelte.ts";
 
   type Props = {
     footer?: Snippet<[{ result: string }]>;
+    streamIntervals?: number[];
     theme?: Extension;
+    timing?: TabTiming | null;
     value: string;
   };
 
-  let { footer, theme, value }: Props = $props();
+  let {
+    footer,
+    streamIntervals = [],
+    theme,
+    timing = null,
+    value
+  }: Props = $props();
+
+  function median(values: number[]): number {
+    if (values.length === 0)
+      return 0;
+
+    const sorted = [...values].sort((a, b) => a - b);
+    const mid = Math.floor(sorted.length / 2);
+
+    return sorted.length % 2 === 0 ?
+      (sorted[mid - 1] + sorted[mid]) / 2 :
+      sorted[mid];
+  }
+
+  let metadata = $derived.by(() => {
+    if (!timing)
+      return null;
+
+    if (streamIntervals.length > 0) {
+      const medianMs = Math.round(median(streamIntervals));
+      const messages = streamIntervals.length + 1;
+      return `→ ${messages} messages · median ${medianMs}ms`;
+    }
+
+    const totalMs = Math.round(timing.endMs - timing.startMs);
+    const firstByteMs = Math.round(timing.firstByteMs - timing.startMs);
+    return `→ ${totalMs}ms · first byte ${firstByteMs}ms`;
+  });
 
   function noop(_v: string) {}
 </script>
@@ -17,7 +53,7 @@
 <style lang="scss">
   .result {
     display: grid;
-    grid-template-rows: auto 1fr auto;
+    grid-template-rows: auto 1fr auto auto;
     height: 100%;
     min-height: 0;
   }
@@ -30,6 +66,14 @@
     text-transform: uppercase;
   }
 
+  .metadata {
+    background: var(--graphiql-panel, #252526);
+    border-top: 1px solid var(--graphiql-border, #333);
+    color: var(--graphiql-muted, #858585);
+    font-size: 0.75rem;
+    padding: 0.25rem 0.75rem;
+  }
+
   .footer {
     background: var(--graphiql-panel, #252526);
     border-top: 1px solid var(--graphiql-border, #333);
@@ -41,6 +85,9 @@
 <div class="result">
   <div class="label">Response</div>
   <Editor language="json" onChange={noop} readOnly {theme} {value}/>
+  {#if metadata}
+    <div class="metadata">{metadata}</div>
+  {/if}
   {#if footer}
     <div class="footer">{@render footer({ result: value })}</div>
   {/if}
-- 
cgit v1.2.3

