postboy-tui 1.3.9 → 1.4.1

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 (2) hide show
  1. package/dist/cli.js +692 -155
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -67534,7 +67534,7 @@ var useFocusManager = () => {
67534
67534
  };
67535
67535
  var use_focus_manager_default = useFocusManager;
67536
67536
  // src/ui/app/ui.tsx
67537
- var import_react34 = __toESM(require_react(), 1);
67537
+ var import_react35 = __toESM(require_react(), 1);
67538
67538
 
67539
67539
  // src/utils/history.ts
67540
67540
  import { promises as fs2 } from "fs";
@@ -67626,7 +67626,7 @@ var historyManager = new HistoryManager;
67626
67626
  import http from "http";
67627
67627
  import https from "https";
67628
67628
  import { URL as URL2 } from "url";
67629
- function sendRequest({ method, url, headers = {}, body }) {
67629
+ function sendRequest({ method, url, headers = {}, body, onProgress }) {
67630
67630
  return new Promise((resolve, reject) => {
67631
67631
  const urlObj = new URL2(url);
67632
67632
  const isHttps = urlObj.protocol === "https:";
@@ -67649,12 +67649,26 @@ function sendRequest({ method, url, headers = {}, body }) {
67649
67649
  const req = reqModule.request(options, (res) => {
67650
67650
  timings.ttfb = performance.now();
67651
67651
  let data = "";
67652
+ let bytesReceived = 0;
67653
+ const totalBytes = parseInt(res.headers["content-length"] || "0", 10);
67654
+ const downloadStartTime = performance.now();
67652
67655
  res.on("data", (chunk) => {
67653
67656
  data += chunk;
67657
+ bytesReceived += Buffer.byteLength(chunk);
67658
+ if (onProgress) {
67659
+ const elapsed = (performance.now() - downloadStartTime) / 1000;
67660
+ const speed = elapsed > 0 ? bytesReceived / elapsed : 0;
67661
+ onProgress({
67662
+ bytesReceived,
67663
+ totalBytes,
67664
+ speed,
67665
+ elapsed
67666
+ });
67667
+ }
67654
67668
  });
67655
67669
  res.on("end", () => {
67656
67670
  timings.end = performance.now();
67657
- const contentLength = parseInt(res.headers["content-length"] || "0", 10) || Buffer.byteLength(data, "utf8");
67671
+ const contentLength = totalBytes || Buffer.byteLength(data, "utf8");
67658
67672
  const metrics = {
67659
67673
  dnsLookup: timings.dnsLookup - timings.start,
67660
67674
  tcpConnection: timings.tcpConnection - timings.dnsLookup,
@@ -67859,7 +67873,7 @@ var Spinner = ({ theme }) => {
67859
67873
  // src/ui/app/components/Formfield.tsx
67860
67874
  var import_react24 = __toESM(require_react(), 1);
67861
67875
  var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
67862
- var InputDialog = ({ label, value, onChange, onClose, theme, suggestions = [] }) => {
67876
+ var InputDialog = ({ label, value, onChange, onClose, theme, suggestions = [], isActive = true }) => {
67863
67877
  const [localValue, setLocalValue] = import_react24.useState(value);
67864
67878
  const [showSuggestions, setShowSuggestions] = import_react24.useState(false);
67865
67879
  const [filteredSuggestions, setFilteredSuggestions] = import_react24.useState([]);
@@ -67908,7 +67922,7 @@ var InputDialog = ({ label, value, onChange, onClose, theme, suggestions = [] })
67908
67922
  if (!key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow && !key.return && !key.tab) {
67909
67923
  setLocalValue((v) => v + input);
67910
67924
  }
67911
- });
67925
+ }, { isActive });
67912
67926
  return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
67913
67927
  flexDirection: "column",
67914
67928
  borderStyle: "double",
@@ -67970,8 +67984,8 @@ var FormField = ({ label, value, onChange, placeholder, theme, suggestions = [],
67970
67984
  const { isFocused } = use_focus_default();
67971
67985
  const [showDialog, setShowDialog] = import_react24.useState(false);
67972
67986
  import_react24.useEffect(() => {
67973
- onFocusChange?.(isFocused || showDialog);
67974
- }, [isFocused, showDialog, onFocusChange]);
67987
+ onFocusChange?.(showDialog);
67988
+ }, [showDialog, onFocusChange]);
67975
67989
  use_input_default((_, key) => {
67976
67990
  if (isFocused && key.return && !showDialog) {
67977
67991
  setShowDialog(true);
@@ -68034,7 +68048,7 @@ var FormField = ({ label, value, onChange, placeholder, theme, suggestions = [],
68034
68048
  // src/ui/app/components/keyvaluefield.tsx
68035
68049
  var import_react25 = __toESM(require_react(), 1);
68036
68050
  var jsx_dev_runtime3 = __toESM(require_jsx_dev_runtime(), 1);
68037
- var KeyValueDialog = ({ label, pairs, onSave, onClose, theme }) => {
68051
+ var KeyValueDialog = ({ label, pairs, onSave, onClose, theme, isActive = true }) => {
68038
68052
  const [localPairs, setLocalPairs] = import_react25.useState(pairs.length > 0 ? pairs : [{ key: "", value: "" }]);
68039
68053
  const [activeField, setActiveField] = import_react25.useState("key");
68040
68054
  const [activeRow, setActiveRow] = import_react25.useState(0);
@@ -68109,7 +68123,7 @@ var KeyValueDialog = ({ label, pairs, onSave, onClose, theme }) => {
68109
68123
  return newPairs;
68110
68124
  });
68111
68125
  }
68112
- });
68126
+ }, { isActive });
68113
68127
  return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
68114
68128
  flexDirection: "column",
68115
68129
  borderStyle: "double",
@@ -68200,8 +68214,8 @@ var KeyValueField = ({ label, value, onChange, placeholder, theme, onFocusChange
68200
68214
  const { isFocused } = use_focus_default();
68201
68215
  const [showDialog, setShowDialog] = import_react25.useState(false);
68202
68216
  import_react25.useEffect(() => {
68203
- onFocusChange?.(isFocused || showDialog);
68204
- }, [isFocused, showDialog, onFocusChange]);
68217
+ onFocusChange?.(showDialog);
68218
+ }, [showDialog, onFocusChange]);
68205
68219
  const parseJsonToPairs = (json) => {
68206
68220
  try {
68207
68221
  const obj = JSON.parse(json || "{}");
@@ -68285,7 +68299,7 @@ var TabItem = ({ name, label, isActive, onChange, theme }) => {
68285
68299
  use_input_default((_, key) => {
68286
68300
  if (isFocused && key.return)
68287
68301
  onChange(name);
68288
- });
68302
+ }, { isActive: isFocused });
68289
68303
  return /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
68290
68304
  borderStyle: "round",
68291
68305
  borderTopColor: "grey",
@@ -68983,7 +68997,7 @@ var HistoryListItem = ({ item, isSelected, theme }) => {
68983
68997
  }, undefined, true, undefined, this)
68984
68998
  }, undefined, false, undefined, this);
68985
68999
  };
68986
- var HistoryList = ({ history, onItemClick, theme }) => {
69000
+ var HistoryList = ({ history, onItemClick, theme, onSearchingChange, startSearching }) => {
68987
69001
  const { stdout } = use_stdout_default();
68988
69002
  const { isFocused } = use_focus_default();
68989
69003
  const { focusNext } = use_focus_manager_default();
@@ -68992,29 +69006,45 @@ var HistoryList = ({ history, onItemClick, theme }) => {
68992
69006
  const [searchQuery, setSearchQuery] = import_react27.useState("");
68993
69007
  const [isSearching, setIsSearching] = import_react27.useState(false);
68994
69008
  const listRef = import_react27.useRef(null);
69009
+ import_react27.useEffect(() => {
69010
+ if (startSearching && !isSearching) {
69011
+ setIsSearching(true);
69012
+ onSearchingChange?.(true);
69013
+ }
69014
+ }, [startSearching, isSearching, onSearchingChange]);
68995
69015
  const filteredHistory = fuzzyFilter(history, searchQuery, (item) => `${item.method} ${item.url}`);
68996
69016
  const maxHeight = stdout.rows - 10;
68997
69017
  use_input_default((input, key) => {
68998
69018
  if (input === "/" && !isSearching) {
68999
69019
  setIsSearching(true);
69020
+ onSearchingChange?.(true);
69000
69021
  return;
69001
69022
  }
69002
69023
  if (key.escape && isSearching) {
69003
69024
  setIsSearching(false);
69004
69025
  setSearchQuery("");
69026
+ onSearchingChange?.(false);
69005
69027
  return;
69006
69028
  }
69007
- if (isSearching)
69008
- return;
69009
69029
  if (key.upArrow) {
69010
69030
  setSelectedIndex((prev) => Math.max(0, prev - 1));
69031
+ return;
69011
69032
  }
69012
69033
  if (key.downArrow) {
69013
69034
  setSelectedIndex((prev) => Math.min(filteredHistory.length - 1, prev + 1));
69035
+ return;
69014
69036
  }
69015
69037
  if (key.return && filteredHistory[selectedIndex]) {
69016
69038
  onItemClick(filteredHistory[selectedIndex]);
69039
+ if (isSearching) {
69040
+ setIsSearching(false);
69041
+ setSearchQuery("");
69042
+ onSearchingChange?.(false);
69043
+ }
69044
+ return;
69017
69045
  }
69046
+ if (isSearching)
69047
+ return;
69018
69048
  if (key.tab) {
69019
69049
  focusNext();
69020
69050
  }
@@ -69024,7 +69054,7 @@ var HistoryList = ({ history, onItemClick, theme }) => {
69024
69054
  if (key.pageDown) {
69025
69055
  setScrollPosition((prev) => Math.min(filteredHistory.length - maxHeight, prev + maxHeight));
69026
69056
  }
69027
- }, { isActive: isFocused });
69057
+ }, { isActive: isFocused || isSearching });
69028
69058
  import_react27.useEffect(() => {
69029
69059
  setSelectedIndex(0);
69030
69060
  setScrollPosition(0);
@@ -69076,7 +69106,7 @@ var HistoryList = ({ history, onItemClick, theme }) => {
69076
69106
  }, undefined, false, undefined, this)
69077
69107
  }, undefined, false, undefined, this) : filteredHistory.slice(scrollPosition, scrollPosition + maxHeight).map((item, index) => /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(HistoryListItem, {
69078
69108
  item,
69079
- isSelected: isFocused && selectedIndex === scrollPosition + index,
69109
+ isSelected: (isFocused || isSearching) && selectedIndex === scrollPosition + index,
69080
69110
  theme
69081
69111
  }, item.timestamp, false, undefined, this))
69082
69112
  }, undefined, false, undefined, this)
@@ -69101,7 +69131,7 @@ var Footer = import_react28.default.memo(({ theme }) => {
69101
69131
  color: theme.primary,
69102
69132
  children: "PostBoy"
69103
69133
  }, undefined, false, undefined, this),
69104
- " — [Q] Quit | [Ctrl+Enter] Send | [Ctrl+L/H] Switch Tabs | [T] Theme | [E] Export | [Tab] Navigate"
69134
+ " — [Q] Quit | [Ctrl+Enter] Send | [Ctrl+L/H] Switch Tabs | [T] Theme | [E] Export | [F] Find | [Tab] Navigate"
69105
69135
  ]
69106
69136
  }, undefined, true, undefined, this)
69107
69137
  }, undefined, false, undefined, this);
@@ -69148,7 +69178,7 @@ var getIndex = (theme) => {
69148
69178
  }
69149
69179
  return index;
69150
69180
  };
69151
- var ThemeSelector = ({ onThemeChange, theme }) => {
69181
+ var ThemeSelector = ({ onThemeChange, theme, isActive = true }) => {
69152
69182
  const [selectedIndex, setSelectedIndex] = import_react29.useState(getIndex(theme.name));
69153
69183
  const themeNames = Object.keys(themes);
69154
69184
  import_react29.useEffect(() => {
@@ -69175,7 +69205,7 @@ var ThemeSelector = ({ onThemeChange, theme }) => {
69175
69205
  onThemeChange(themeNames[newIndex]);
69176
69206
  }
69177
69207
  }
69178
- }, { isActive: true });
69208
+ }, { isActive });
69179
69209
  return /* @__PURE__ */ jsx_dev_runtime7.jsxDEV(Box_default, {
69180
69210
  flexDirection: "column",
69181
69211
  padding: 1,
@@ -69209,7 +69239,7 @@ var ThemeSelector = ({ onThemeChange, theme }) => {
69209
69239
  // src/ui/app/components/scrollablebox.tsx
69210
69240
  var import_react30 = __toESM(require_react(), 1);
69211
69241
  var jsx_dev_runtime8 = __toESM(require_jsx_dev_runtime(), 1);
69212
- var ScrollableBox = ({ children }) => {
69242
+ var ScrollableBox = ({ children, isActive = true }) => {
69213
69243
  const { stdout } = use_stdout_default();
69214
69244
  const [scrollPosition, setScrollPosition] = import_react30.useState(0);
69215
69245
  const maxHeight = stdout.rows - 10;
@@ -69219,7 +69249,7 @@ var ScrollableBox = ({ children }) => {
69219
69249
  setScrollPosition((prev) => Math.max(0, prev - maxHeight));
69220
69250
  if (key.pageDown)
69221
69251
  setScrollPosition((prev) => Math.min(contentHeight - maxHeight, prev + maxHeight));
69222
- });
69252
+ }, { isActive });
69223
69253
  import_react30.useEffect(() => {
69224
69254
  const estimateHeight = (node) => {
69225
69255
  if (!node)
@@ -69368,6 +69398,140 @@ var JsonSyntaxHighlight = import_react31.default.memo(({ jsonString, theme }) =>
69368
69398
  // src/ui/app/components/responsepanel.tsx
69369
69399
  var import_react32 = __toESM(require_react(), 1);
69370
69400
  var jsx_dev_runtime10 = __toESM(require_jsx_dev_runtime(), 1);
69401
+ var StatusBadge = ({ status, statusText, theme }) => {
69402
+ const [glowFrame, setGlowFrame] = import_react32.useState(0);
69403
+ const statusNum = parseInt(status, 10);
69404
+ import_react32.useEffect(() => {
69405
+ const interval = setInterval(() => {
69406
+ setGlowFrame((prev) => (prev + 1) % 4);
69407
+ }, 300);
69408
+ return () => clearInterval(interval);
69409
+ }, []);
69410
+ const getStatusIcon = () => {
69411
+ if (statusNum >= 200 && statusNum < 300)
69412
+ return ["✓", "✔", "☑", "✓"][glowFrame];
69413
+ if (statusNum >= 300 && statusNum < 400)
69414
+ return ["↪", "→", "⇒", "↪"][glowFrame];
69415
+ if (statusNum >= 400 && statusNum < 500)
69416
+ return ["⚠", "⚡", "⚠", "⚡"][glowFrame];
69417
+ if (statusNum >= 500)
69418
+ return ["✗", "✘", "☒", "✗"][glowFrame];
69419
+ return "●";
69420
+ };
69421
+ const getBadgeStyle = () => {
69422
+ if (statusNum >= 200 && statusNum < 300)
69423
+ return { bg: theme.colors.success, icon: getStatusIcon(), label: "SUCCESS" };
69424
+ if (statusNum >= 300 && statusNum < 400)
69425
+ return { bg: theme.colors.accent, icon: getStatusIcon(), label: "REDIRECT" };
69426
+ if (statusNum >= 400 && statusNum < 500)
69427
+ return { bg: theme.colors.error, icon: getStatusIcon(), label: "CLIENT ERR" };
69428
+ if (statusNum >= 500)
69429
+ return { bg: theme.colors.error, icon: getStatusIcon(), label: "SERVER ERR" };
69430
+ return { bg: theme.colors.muted, icon: "●", label: "UNKNOWN" };
69431
+ };
69432
+ const style = getBadgeStyle();
69433
+ const glowChars = ["░", "▒", "▓", "▒"];
69434
+ if (!status || status === "Error") {
69435
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
69436
+ children: [
69437
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69438
+ color: theme.colors.error,
69439
+ bold: true,
69440
+ children: "✗ "
69441
+ }, undefined, false, undefined, this),
69442
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69443
+ color: theme.colors.error,
69444
+ children: statusText || "Error"
69445
+ }, undefined, false, undefined, this)
69446
+ ]
69447
+ }, undefined, true, undefined, this);
69448
+ }
69449
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
69450
+ children: [
69451
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69452
+ color: style.bg,
69453
+ dimColor: true,
69454
+ children: glowChars[glowFrame]
69455
+ }, undefined, false, undefined, this),
69456
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69457
+ backgroundColor: style.bg,
69458
+ color: theme.colors.white,
69459
+ bold: true,
69460
+ children: [
69461
+ " ",
69462
+ style.icon,
69463
+ " ",
69464
+ status,
69465
+ " "
69466
+ ]
69467
+ }, undefined, true, undefined, this),
69468
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69469
+ color: style.bg,
69470
+ dimColor: true,
69471
+ children: glowChars[glowFrame]
69472
+ }, undefined, false, undefined, this),
69473
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69474
+ color: theme.colors.muted,
69475
+ children: " │ "
69476
+ }, undefined, false, undefined, this),
69477
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69478
+ color: style.bg,
69479
+ bold: true,
69480
+ children: style.label
69481
+ }, undefined, false, undefined, this),
69482
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69483
+ color: theme.colors.muted,
69484
+ children: " │ "
69485
+ }, undefined, false, undefined, this),
69486
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69487
+ color: theme.colors.white,
69488
+ children: statusText
69489
+ }, undefined, false, undefined, this)
69490
+ ]
69491
+ }, undefined, true, undefined, this);
69492
+ };
69493
+ var TypewriterText = ({ text, theme, speed = 2 }) => {
69494
+ const [displayedLength, setDisplayedLength] = import_react32.useState(0);
69495
+ const [cursorVisible, setCursorVisible] = import_react32.useState(true);
69496
+ const previousTextRef = import_react32.useRef("");
69497
+ import_react32.useEffect(() => {
69498
+ if (text !== previousTextRef.current) {
69499
+ setDisplayedLength(0);
69500
+ previousTextRef.current = text;
69501
+ }
69502
+ }, [text]);
69503
+ import_react32.useEffect(() => {
69504
+ if (displayedLength < text.length) {
69505
+ const charsToAdd = Math.min(speed, text.length - displayedLength);
69506
+ const timeout = setTimeout(() => {
69507
+ setDisplayedLength((prev) => Math.min(prev + charsToAdd, text.length));
69508
+ }, 5);
69509
+ return () => clearTimeout(timeout);
69510
+ }
69511
+ }, [displayedLength, text, speed]);
69512
+ import_react32.useEffect(() => {
69513
+ const interval = setInterval(() => {
69514
+ setCursorVisible((prev) => !prev);
69515
+ }, 400);
69516
+ return () => clearInterval(interval);
69517
+ }, []);
69518
+ const displayedText = text.slice(0, displayedLength);
69519
+ const isComplete = displayedLength >= text.length;
69520
+ const cursor = !isComplete && cursorVisible ? "█" : !isComplete ? " " : "";
69521
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
69522
+ flexDirection: "column",
69523
+ children: [
69524
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(JsonSyntaxHighlight, {
69525
+ jsonString: displayedText,
69526
+ theme
69527
+ }, undefined, false, undefined, this),
69528
+ !isComplete && /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69529
+ color: theme.colors.accent,
69530
+ children: cursor
69531
+ }, undefined, false, undefined, this)
69532
+ ]
69533
+ }, undefined, true, undefined, this);
69534
+ };
69371
69535
  var ResponsePanel = import_react32.default.memo(({ response, theme }) => {
69372
69536
  const [activeTab, setActiveTab] = import_react32.useState("body");
69373
69537
  const tabs = [{ name: "body", label: "Body" }, { name: "headers", label: "Headers" }];
@@ -69377,25 +69541,12 @@ var ResponsePanel = import_react32.default.memo(({ response, theme }) => {
69377
69541
  children: [
69378
69542
  /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
69379
69543
  marginBottom: 1,
69380
- children: [
69381
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
69382
- width: 8,
69383
- children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69384
- color: theme.colors.primary,
69385
- children: "STATUS:"
69386
- }, undefined, false, undefined, this)
69387
- }, undefined, false, undefined, this),
69388
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
69389
- color: getStatusColor(response.status, theme),
69390
- bold: true,
69391
- children: [
69392
- response.status,
69393
- " ",
69394
- response.statustext
69395
- ]
69396
- }, undefined, true, undefined, this)
69397
- ]
69398
- }, undefined, true, undefined, this),
69544
+ children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(StatusBadge, {
69545
+ status: response.status,
69546
+ statusText: response.statustext,
69547
+ theme
69548
+ }, undefined, false, undefined, this)
69549
+ }, undefined, false, undefined, this),
69399
69550
  /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Tabs, {
69400
69551
  tabs,
69401
69552
  activeTab,
@@ -69407,6 +69558,7 @@ var ResponsePanel = import_react32.default.memo(({ response, theme }) => {
69407
69558
  flexGrow: 1,
69408
69559
  children: [
69409
69560
  activeTab === "headers" && /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ScrollableBox, {
69561
+ isActive: activeTab === "headers",
69410
69562
  children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
69411
69563
  flexDirection: "column",
69412
69564
  children: Object.entries(JSON.parse(response.headers || "{}")).map(([key, value]) => /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
@@ -69428,12 +69580,14 @@ var ResponsePanel = import_react32.default.memo(({ response, theme }) => {
69428
69580
  }, undefined, false, undefined, this)
69429
69581
  }, undefined, false, undefined, this),
69430
69582
  activeTab === "body" && /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ScrollableBox, {
69583
+ isActive: activeTab === "body",
69431
69584
  children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
69432
69585
  flexDirection: "column",
69433
69586
  flexGrow: 1,
69434
- children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(JsonSyntaxHighlight, {
69435
- jsonString: response.body,
69436
- theme
69587
+ children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TypewriterText, {
69588
+ text: response.body,
69589
+ theme,
69590
+ speed: 50
69437
69591
  }, undefined, false, undefined, this)
69438
69592
  }, undefined, false, undefined, this)
69439
69593
  }, undefined, false, undefined, this)
@@ -69872,42 +70026,100 @@ class ThemeManager {
69872
70026
  var themeManager = new ThemeManager;
69873
70027
 
69874
70028
  // src/ui/app/components/metricspanel.tsx
70029
+ var import_react34 = __toESM(require_react(), 1);
69875
70030
  var jsx_dev_runtime12 = __toESM(require_jsx_dev_runtime(), 1);
69876
- var MetricBar = ({ label, value, maxValue, color, theme }) => {
69877
- const barWidth = 40;
70031
+ var BAR_CHARS = ["▱", "▰"];
70032
+ var GLOW_FRAMES = ["◐", "◓", "◑", "◒"];
70033
+ var SPARK_CHARS = ["⚡", "✦", "✧", "⚡", "★", "☆"];
70034
+ var MetricBar = ({ label, value, maxValue, color, theme, delay }) => {
70035
+ const barWidth = 30;
69878
70036
  const safeValue = Math.max(0, value);
69879
- const filledWidth = Math.max(0, Math.min(barWidth, maxValue > 0 ? Math.round(safeValue / maxValue * barWidth) : 0));
69880
- const emptyWidth = barWidth - filledWidth;
69881
- const bar = "█".repeat(filledWidth) + "░".repeat(emptyWidth);
70037
+ const targetFilled = Math.max(0, Math.min(barWidth, maxValue > 0 ? Math.round(safeValue / maxValue * barWidth) : 0));
70038
+ const [animatedFilled, setAnimatedFilled] = import_react34.useState(0);
70039
+ const [glowFrame, setGlowFrame] = import_react34.useState(0);
70040
+ const [sparkle, setSparkle] = import_react34.useState(false);
70041
+ import_react34.useEffect(() => {
70042
+ const timeout = setTimeout(() => {
70043
+ if (animatedFilled < targetFilled) {
70044
+ const interval = setInterval(() => {
70045
+ setAnimatedFilled((prev) => {
70046
+ if (prev >= targetFilled) {
70047
+ clearInterval(interval);
70048
+ setSparkle(true);
70049
+ setTimeout(() => setSparkle(false), 300);
70050
+ return targetFilled;
70051
+ }
70052
+ return prev + 1;
70053
+ });
70054
+ }, 20);
70055
+ return () => clearInterval(interval);
70056
+ }
70057
+ }, delay);
70058
+ return () => clearTimeout(timeout);
70059
+ }, [targetFilled, delay]);
70060
+ import_react34.useEffect(() => {
70061
+ const interval = setInterval(() => {
70062
+ setGlowFrame((prev) => (prev + 1) % GLOW_FRAMES.length);
70063
+ }, 150);
70064
+ return () => clearInterval(interval);
70065
+ }, []);
70066
+ const filledBar = BAR_CHARS[1].repeat(animatedFilled);
70067
+ const emptyBar = BAR_CHARS[0].repeat(barWidth - animatedFilled);
70068
+ const indicator = animatedFilled > 0 && animatedFilled < targetFilled ? GLOW_FRAMES[glowFrame] : sparkle ? SPARK_CHARS[Math.floor(Math.random() * SPARK_CHARS.length)] : "";
69882
70069
  return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
69883
70070
  marginY: 0,
69884
70071
  children: [
69885
70072
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
69886
- width: 18,
70073
+ width: 16,
69887
70074
  children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
69888
- color: theme.colors.muted,
70075
+ color,
70076
+ bold: true,
69889
70077
  children: label
69890
70078
  }, undefined, false, undefined, this)
69891
70079
  }, undefined, false, undefined, this),
70080
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70081
+ color: theme.colors.muted,
70082
+ children: "│"
70083
+ }, undefined, false, undefined, this),
69892
70084
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
69893
- width: barWidth + 2,
69894
- children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
69895
- color,
69896
- children: bar
69897
- }, undefined, false, undefined, this)
70085
+ width: barWidth + 3,
70086
+ children: [
70087
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70088
+ color,
70089
+ children: filledBar
70090
+ }, undefined, false, undefined, this),
70091
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70092
+ color,
70093
+ bold: true,
70094
+ children: indicator
70095
+ }, undefined, false, undefined, this),
70096
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70097
+ color: theme.colors.muted,
70098
+ dimColor: true,
70099
+ children: emptyBar
70100
+ }, undefined, false, undefined, this)
70101
+ ]
70102
+ }, undefined, true, undefined, this),
70103
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70104
+ color: theme.colors.muted,
70105
+ children: "│"
69898
70106
  }, undefined, false, undefined, this),
69899
70107
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
69900
- width: 12,
70108
+ width: 10,
69901
70109
  justifyContent: "flex-end",
69902
- children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
69903
- color: theme.colors.white,
69904
- bold: true,
69905
- children: [
69906
- safeValue.toFixed(1),
69907
- "ms"
69908
- ]
69909
- }, undefined, true, undefined, this)
69910
- }, undefined, false, undefined, this)
70110
+ children: [
70111
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70112
+ color: theme.colors.white,
70113
+ bold: true,
70114
+ children: safeValue.toFixed(1)
70115
+ }, undefined, false, undefined, this),
70116
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70117
+ color: theme.colors.muted,
70118
+ dimColor: true,
70119
+ children: "ms"
70120
+ }, undefined, false, undefined, this)
70121
+ ]
70122
+ }, undefined, true, undefined, this)
69911
70123
  ]
69912
70124
  }, undefined, true, undefined, this);
69913
70125
  };
@@ -69919,75 +70131,290 @@ var formatBytes = (bytes) => {
69919
70131
  const i = Math.floor(Math.log(bytes) / Math.log(k));
69920
70132
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
69921
70133
  };
69922
- var MetricsPanel = ({ metrics, theme }) => {
69923
- if (!metrics) {
69924
- return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
69925
- flexDirection: "column",
69926
- padding: 1,
69927
- children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
69928
- color: theme.colors.muted,
69929
- children: "No metrics available. Send a request first."
69930
- }, undefined, false, undefined, this)
69931
- }, undefined, false, undefined, this);
69932
- }
69933
- const maxTime = Math.max(Math.max(0, metrics.dnsLookup), Math.max(0, metrics.tcpConnection), Math.max(0, metrics.tlsHandshake), Math.max(0, metrics.ttfb), Math.max(0, metrics.contentDownload), 1);
70134
+ var AnimatedValue = ({ value, suffix, color }) => {
70135
+ const [displayValue, setDisplayValue] = import_react34.useState(0);
70136
+ import_react34.useEffect(() => {
70137
+ const duration = 500;
70138
+ const steps = 20;
70139
+ const increment = value / steps;
70140
+ let current = 0;
70141
+ const interval = setInterval(() => {
70142
+ current += increment;
70143
+ if (current >= value) {
70144
+ setDisplayValue(value);
70145
+ clearInterval(interval);
70146
+ } else {
70147
+ setDisplayValue(current);
70148
+ }
70149
+ }, duration / steps);
70150
+ return () => clearInterval(interval);
70151
+ }, [value]);
70152
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70153
+ color,
70154
+ bold: true,
70155
+ children: [
70156
+ displayValue.toFixed(2),
70157
+ " ",
70158
+ suffix
70159
+ ]
70160
+ }, undefined, true, undefined, this);
70161
+ };
70162
+ var PulsingDot = ({ color }) => {
70163
+ const [frame, setFrame] = import_react34.useState(0);
70164
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
70165
+ import_react34.useEffect(() => {
70166
+ const interval = setInterval(() => {
70167
+ setFrame((prev) => (prev + 1) % frames.length);
70168
+ }, 80);
70169
+ return () => clearInterval(interval);
70170
+ }, []);
70171
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70172
+ color,
70173
+ children: frames[frame]
70174
+ }, undefined, false, undefined, this);
70175
+ };
70176
+ var TimelineWaterfall = ({ metrics, theme }) => {
70177
+ const [animationProgress, setAnimationProgress] = import_react34.useState(0);
70178
+ const [pulseFrame, setPulseFrame] = import_react34.useState(0);
70179
+ const totalWidth = 50;
70180
+ const segments = [
70181
+ { label: "DNS", value: Math.max(0, metrics.dnsLookup), color: theme.colors.cool, icon: "⚡" },
70182
+ { label: "TCP", value: Math.max(0, metrics.tcpConnection), color: theme.colors.success, icon: "\uD83D\uDD0C" },
70183
+ ...metrics.tlsHandshake > 0 ? [{ label: "TLS", value: Math.max(0, metrics.tlsHandshake), color: theme.colors.secondary, icon: "\uD83D\uDD10" }] : [],
70184
+ { label: "TTFB", value: Math.max(0, metrics.ttfb), color: theme.colors.accent, icon: "⏱️" },
70185
+ { label: "DL", value: Math.max(0, metrics.contentDownload), color: theme.colors.primary, icon: "\uD83D\uDCE5" }
70186
+ ];
70187
+ const totalTime = segments.reduce((sum, s) => sum + s.value, 0);
70188
+ import_react34.useEffect(() => {
70189
+ setAnimationProgress(0);
70190
+ const interval = setInterval(() => {
70191
+ setAnimationProgress((prev) => {
70192
+ if (prev >= 100) {
70193
+ clearInterval(interval);
70194
+ return 100;
70195
+ }
70196
+ return prev + 2;
70197
+ });
70198
+ }, 15);
70199
+ return () => clearInterval(interval);
70200
+ }, [metrics]);
70201
+ import_react34.useEffect(() => {
70202
+ const interval = setInterval(() => {
70203
+ setPulseFrame((prev) => (prev + 1) % 4);
70204
+ }, 200);
70205
+ return () => clearInterval(interval);
70206
+ }, []);
70207
+ const pulseChars = ["▁", "▂", "▃", "▄"];
70208
+ const endCaps = ["◀", "▶"];
69934
70209
  return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
69935
70210
  flexDirection: "column",
69936
- paddingX: 1,
70211
+ marginY: 1,
69937
70212
  children: [
69938
70213
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
69939
70214
  marginBottom: 1,
69940
- flexDirection: "column",
69941
70215
  children: [
69942
70216
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
69943
70217
  color: theme.colors.accent,
69944
70218
  bold: true,
69945
- children: " Performance Breakdown"
70219
+ children: " Request Timeline"
69946
70220
  }, undefined, false, undefined, this),
69947
70221
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
69948
70222
  color: theme.colors.muted,
69949
- children: "────────────────────────────────────────────────────────────"
70223
+ children: [
70224
+ " (",
70225
+ totalTime.toFixed(0),
70226
+ "ms total)"
70227
+ ]
70228
+ }, undefined, true, undefined, this)
70229
+ ]
70230
+ }, undefined, true, undefined, this),
70231
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70232
+ children: [
70233
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70234
+ color: theme.colors.muted,
70235
+ children: endCaps[0]
70236
+ }, undefined, false, undefined, this),
70237
+ segments.map((segment, idx) => {
70238
+ const segmentWidth = Math.max(1, Math.round(segment.value / totalTime * totalWidth));
70239
+ const animatedWidth = Math.round(animationProgress / 100 * segmentWidth);
70240
+ const filledPart = "█".repeat(Math.max(0, animatedWidth));
70241
+ const emptyPart = "░".repeat(Math.max(0, segmentWidth - animatedWidth));
70242
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70243
+ children: [
70244
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70245
+ color: segment.color,
70246
+ children: filledPart
70247
+ }, undefined, false, undefined, this),
70248
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70249
+ color: segment.color,
70250
+ dimColor: true,
70251
+ children: emptyPart
70252
+ }, undefined, false, undefined, this)
70253
+ ]
70254
+ }, idx, true, undefined, this);
70255
+ }),
70256
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70257
+ color: theme.colors.muted,
70258
+ children: endCaps[1]
70259
+ }, undefined, false, undefined, this),
70260
+ animationProgress < 100 && /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70261
+ color: theme.colors.accent,
70262
+ children: [
70263
+ " ",
70264
+ pulseChars[pulseFrame]
70265
+ ]
70266
+ }, undefined, true, undefined, this),
70267
+ animationProgress >= 100 && /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70268
+ color: theme.colors.success,
70269
+ children: " ✓"
69950
70270
  }, undefined, false, undefined, this)
69951
70271
  ]
69952
70272
  }, undefined, true, undefined, this),
70273
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70274
+ marginTop: 1,
70275
+ flexWrap: "wrap",
70276
+ children: segments.map((segment, idx) => /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70277
+ marginRight: 2,
70278
+ children: [
70279
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70280
+ color: segment.color,
70281
+ children: "■"
70282
+ }, undefined, false, undefined, this),
70283
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70284
+ color: theme.colors.muted,
70285
+ children: [
70286
+ " ",
70287
+ segment.label,
70288
+ " "
70289
+ ]
70290
+ }, undefined, true, undefined, this),
70291
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70292
+ color: theme.colors.white,
70293
+ bold: true,
70294
+ children: segment.value.toFixed(0)
70295
+ }, undefined, false, undefined, this),
70296
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70297
+ color: theme.colors.muted,
70298
+ dimColor: true,
70299
+ children: "ms"
70300
+ }, undefined, false, undefined, this)
70301
+ ]
70302
+ }, idx, true, undefined, this))
70303
+ }, undefined, false, undefined, this)
70304
+ ]
70305
+ }, undefined, true, undefined, this);
70306
+ };
70307
+ var MetricsPanel = ({ metrics, theme }) => {
70308
+ const [showContent, setShowContent] = import_react34.useState(false);
70309
+ import_react34.useEffect(() => {
70310
+ if (metrics) {
70311
+ setShowContent(false);
70312
+ const timeout = setTimeout(() => setShowContent(true), 100);
70313
+ return () => clearTimeout(timeout);
70314
+ }
70315
+ }, [metrics]);
70316
+ if (!metrics) {
70317
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70318
+ flexDirection: "column",
70319
+ padding: 1,
70320
+ alignItems: "center",
70321
+ children: [
70322
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70323
+ marginBottom: 1,
70324
+ children: [
70325
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(PulsingDot, {
70326
+ color: theme.colors.muted
70327
+ }, undefined, false, undefined, this),
70328
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70329
+ color: theme.colors.muted,
70330
+ children: " Waiting for request..."
70331
+ }, undefined, false, undefined, this)
70332
+ ]
70333
+ }, undefined, true, undefined, this),
70334
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70335
+ color: theme.colors.muted,
70336
+ dimColor: true,
70337
+ children: "Send a request to see metrics"
70338
+ }, undefined, false, undefined, this)
70339
+ ]
70340
+ }, undefined, true, undefined, this);
70341
+ }
70342
+ const maxTime = Math.max(Math.max(0, metrics.dnsLookup), Math.max(0, metrics.tcpConnection), Math.max(0, metrics.tlsHandshake), Math.max(0, metrics.ttfb), Math.max(0, metrics.contentDownload), 1);
70343
+ if (!showContent) {
70344
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70345
+ flexDirection: "column",
70346
+ padding: 1,
70347
+ alignItems: "center",
70348
+ children: [
70349
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(PulsingDot, {
70350
+ color: theme.colors.accent
70351
+ }, undefined, false, undefined, this),
70352
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70353
+ color: theme.colors.accent,
70354
+ children: " Analyzing..."
70355
+ }, undefined, false, undefined, this)
70356
+ ]
70357
+ }, undefined, true, undefined, this);
70358
+ }
70359
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70360
+ flexDirection: "column",
70361
+ paddingX: 1,
70362
+ children: [
70363
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(TimelineWaterfall, {
70364
+ metrics,
70365
+ theme
70366
+ }, undefined, false, undefined, this),
70367
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70368
+ marginTop: 1,
70369
+ flexDirection: "column",
70370
+ children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70371
+ color: theme.colors.muted,
70372
+ children: "┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈"
70373
+ }, undefined, false, undefined, this)
70374
+ }, undefined, false, undefined, this),
69953
70375
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
69954
70376
  flexDirection: "column",
69955
70377
  gap: 0,
69956
70378
  children: [
69957
70379
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(MetricBar, {
69958
- label: "\uD83D\uDD0D DNS Lookup",
70380
+ label: " DNS",
69959
70381
  value: metrics.dnsLookup,
69960
70382
  maxValue: maxTime,
69961
70383
  color: theme.colors.cool,
69962
- theme
70384
+ theme,
70385
+ delay: 0
69963
70386
  }, undefined, false, undefined, this),
69964
70387
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(MetricBar, {
69965
- label: "\uD83D\uDD0C TCP Connect",
70388
+ label: "\uD83D\uDD0C TCP",
69966
70389
  value: metrics.tcpConnection,
69967
70390
  maxValue: maxTime,
69968
70391
  color: theme.colors.success,
69969
- theme
70392
+ theme,
70393
+ delay: 100
69970
70394
  }, undefined, false, undefined, this),
69971
70395
  metrics.tlsHandshake > 0 && /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(MetricBar, {
69972
- label: "\uD83D\uDD10 TLS Handshake",
70396
+ label: "\uD83D\uDD10 TLS",
69973
70397
  value: metrics.tlsHandshake,
69974
70398
  maxValue: maxTime,
69975
70399
  color: theme.colors.secondary,
69976
- theme
70400
+ theme,
70401
+ delay: 200
69977
70402
  }, undefined, false, undefined, this),
69978
70403
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(MetricBar, {
69979
70404
  label: "⏱️ TTFB",
69980
70405
  value: metrics.ttfb,
69981
70406
  maxValue: maxTime,
69982
70407
  color: theme.colors.accent,
69983
- theme
70408
+ theme,
70409
+ delay: 300
69984
70410
  }, undefined, false, undefined, this),
69985
70411
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(MetricBar, {
69986
70412
  label: "\uD83D\uDCE5 Download",
69987
70413
  value: metrics.contentDownload,
69988
70414
  maxValue: maxTime,
69989
70415
  color: theme.colors.primary,
69990
- theme
70416
+ theme,
70417
+ delay: 400
69991
70418
  }, undefined, false, undefined, this)
69992
70419
  ]
69993
70420
  }, undefined, true, undefined, this),
@@ -69996,7 +70423,7 @@ var MetricsPanel = ({ metrics, theme }) => {
69996
70423
  flexDirection: "column",
69997
70424
  children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
69998
70425
  color: theme.colors.muted,
69999
- children: "────────────────────────────────────────────────────────────"
70426
+ children: "┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈"
70000
70427
  }, undefined, false, undefined, this)
70001
70428
  }, undefined, false, undefined, this),
70002
70429
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
@@ -70007,30 +70434,25 @@ var MetricsPanel = ({ metrics, theme }) => {
70007
70434
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70008
70435
  children: [
70009
70436
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70010
- width: 18,
70437
+ width: 12,
70011
70438
  children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70012
70439
  color: theme.colors.accent,
70013
- bold: true,
70014
- children: "\uD83D\uDCCA Total Time"
70440
+ children: "⚡ Total"
70015
70441
  }, undefined, false, undefined, this)
70016
70442
  }, undefined, false, undefined, this),
70017
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70018
- color: theme.colors.white,
70019
- bold: true,
70020
- children: [
70021
- metrics.total.toFixed(2),
70022
- " ms"
70023
- ]
70024
- }, undefined, true, undefined, this)
70443
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(AnimatedValue, {
70444
+ value: metrics.total,
70445
+ suffix: "ms",
70446
+ color: theme.colors.white
70447
+ }, undefined, false, undefined, this)
70025
70448
  ]
70026
70449
  }, undefined, true, undefined, this),
70027
70450
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70028
70451
  children: [
70029
70452
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70030
- width: 18,
70453
+ width: 12,
70031
70454
  children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70032
70455
  color: theme.colors.accent,
70033
- bold: true,
70034
70456
  children: "\uD83D\uDCE6 Size"
70035
70457
  }, undefined, false, undefined, this)
70036
70458
  }, undefined, false, undefined, this),
@@ -70044,10 +70466,9 @@ var MetricsPanel = ({ metrics, theme }) => {
70044
70466
  metrics.contentLength > 0 && metrics.contentDownload > 0 && /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70045
70467
  children: [
70046
70468
  /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70047
- width: 18,
70469
+ width: 12,
70048
70470
  children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70049
70471
  color: theme.colors.accent,
70050
- bold: true,
70051
70472
  children: "\uD83D\uDE80 Speed"
70052
70473
  }, undefined, false, undefined, this)
70053
70474
  }, undefined, false, undefined, this),
@@ -70062,31 +70483,135 @@ var MetricsPanel = ({ metrics, theme }) => {
70062
70483
  ]
70063
70484
  }, undefined, true, undefined, this)
70064
70485
  ]
70065
- }, undefined, true, undefined, this),
70066
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70067
- marginTop: 1,
70068
- flexDirection: "column",
70486
+ }, undefined, true, undefined, this)
70487
+ ]
70488
+ }, undefined, true, undefined, this);
70489
+ };
70490
+
70491
+ // src/ui/app/ui.tsx
70492
+ var jsx_dev_runtime13 = __toESM(require_jsx_dev_runtime(), 1);
70493
+ var formatBytes2 = (bytes) => {
70494
+ if (bytes === 0)
70495
+ return "0 B";
70496
+ const k = 1024;
70497
+ const sizes = ["B", "KB", "MB", "GB"];
70498
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
70499
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
70500
+ };
70501
+ var LiveByteCounter = ({ progress, theme }) => {
70502
+ const [pulseFrame, setPulseFrame] = import_react35.useState(0);
70503
+ const pulseChars = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
70504
+ const arrowFrames = ["↓", "⬇", "↓", "⇣"];
70505
+ import_react35.useEffect(() => {
70506
+ const interval = setInterval(() => {
70507
+ setPulseFrame((prev) => (prev + 1) % pulseChars.length);
70508
+ }, 100);
70509
+ return () => clearInterval(interval);
70510
+ }, []);
70511
+ if (!progress)
70512
+ return null;
70513
+ const { bytesReceived, totalBytes, speed } = progress;
70514
+ const percentage = totalBytes > 0 ? Math.round(bytesReceived / totalBytes * 100) : 0;
70515
+ const progressBarWidth = 20;
70516
+ const filledWidth = totalBytes > 0 ? Math.round(bytesReceived / totalBytes * progressBarWidth) : 0;
70517
+ const filledBar = "█".repeat(filledWidth);
70518
+ const emptyBar = "░".repeat(progressBarWidth - filledWidth);
70519
+ return /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70520
+ flexDirection: "column",
70521
+ paddingX: 1,
70522
+ paddingY: 1,
70523
+ borderStyle: "round",
70524
+ borderColor: theme.accent,
70525
+ children: [
70526
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70527
+ marginBottom: 1,
70069
70528
  children: [
70070
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70071
- color: theme.colors.muted,
70072
- children: "────────────────────────────────────────────────────────────"
70529
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70530
+ color: theme.accent,
70531
+ bold: true,
70532
+ children: [
70533
+ pulseChars[pulseFrame],
70534
+ " "
70535
+ ]
70536
+ }, undefined, true, undefined, this),
70537
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70538
+ color: theme.primary,
70539
+ bold: true,
70540
+ children: "DOWNLOADING "
70073
70541
  }, undefined, false, undefined, this),
70074
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
70075
- marginTop: 1,
70076
- children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
70077
- color: theme.colors.muted,
70078
- dimColor: true,
70079
- children: "TTFB = Time To First Byte (DNS + TCP + TLS + Server Processing)"
70080
- }, undefined, false, undefined, this)
70542
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70543
+ color: theme.accent,
70544
+ children: arrowFrames[pulseFrame % arrowFrames.length]
70081
70545
  }, undefined, false, undefined, this)
70082
70546
  ]
70547
+ }, undefined, true, undefined, this),
70548
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70549
+ children: [
70550
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70551
+ color: theme.success,
70552
+ children: filledBar
70553
+ }, undefined, false, undefined, this),
70554
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70555
+ color: theme.muted,
70556
+ dimColor: true,
70557
+ children: emptyBar
70558
+ }, undefined, false, undefined, this),
70559
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70560
+ color: theme.white,
70561
+ bold: true,
70562
+ children: [
70563
+ " ",
70564
+ percentage,
70565
+ "%"
70566
+ ]
70567
+ }, undefined, true, undefined, this)
70568
+ ]
70569
+ }, undefined, true, undefined, this),
70570
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70571
+ marginTop: 1,
70572
+ children: [
70573
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70574
+ marginRight: 2,
70575
+ children: [
70576
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70577
+ color: theme.accent,
70578
+ children: "\uD83D\uDCE6 "
70579
+ }, undefined, false, undefined, this),
70580
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70581
+ color: theme.white,
70582
+ bold: true,
70583
+ children: formatBytes2(bytesReceived)
70584
+ }, undefined, false, undefined, this),
70585
+ totalBytes > 0 && /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70586
+ color: theme.muted,
70587
+ children: [
70588
+ " / ",
70589
+ formatBytes2(totalBytes)
70590
+ ]
70591
+ }, undefined, true, undefined, this)
70592
+ ]
70593
+ }, undefined, true, undefined, this),
70594
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70595
+ children: [
70596
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70597
+ color: theme.success,
70598
+ children: "\uD83D\uDE80 "
70599
+ }, undefined, false, undefined, this),
70600
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
70601
+ color: theme.success,
70602
+ bold: true,
70603
+ children: [
70604
+ formatBytes2(speed),
70605
+ "/s"
70606
+ ]
70607
+ }, undefined, true, undefined, this)
70608
+ ]
70609
+ }, undefined, true, undefined, this)
70610
+ ]
70083
70611
  }, undefined, true, undefined, this)
70084
70612
  ]
70085
70613
  }, undefined, true, undefined, this);
70086
70614
  };
70087
-
70088
- // src/ui/app/ui.tsx
70089
- var jsx_dev_runtime13 = __toESM(require_jsx_dev_runtime(), 1);
70090
70615
  var SendButton = ({ onPress, loading, theme }) => {
70091
70616
  const { isFocused } = use_focus_default();
70092
70617
  use_input_default((_, key) => {
@@ -70108,7 +70633,7 @@ var SendButton = ({ onPress, loading, theme }) => {
70108
70633
  }, undefined, false, undefined, this);
70109
70634
  };
70110
70635
  var HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
70111
- var RequestPanel = import_react34.default.memo(({ request, onMethodChange, onUrlChange, onHeadersChange, onBodyChange, onSend, loading, theme, historyUrls = [], onInputFocus }) => /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70636
+ var RequestPanel = import_react35.default.memo(({ request, onMethodChange, onUrlChange, onHeadersChange, onBodyChange, onSend, loading, theme, historyUrls = [], onInputFocus }) => /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70112
70637
  flexDirection: "column",
70113
70638
  gap: 1,
70114
70639
  flexGrow: 1,
@@ -70159,29 +70684,31 @@ var RequestPanel = import_react34.default.memo(({ request, onMethodChange, onUrl
70159
70684
  ]
70160
70685
  }, undefined, true, undefined, this));
70161
70686
  var UI = () => {
70162
- const [theme, setTheme] = import_react34.useState(themes.catppuccin);
70687
+ const [theme, setTheme] = import_react35.useState(themes.catppuccin);
70163
70688
  const { exit } = use_app_default();
70164
- const [activeTab, setActiveTab] = import_react34.useState("request");
70165
- const [request, setRequest] = import_react34.useState({ method: "GET", url: "", headers: "", body: "" });
70166
- const [response, setResponse] = import_react34.useState({ statustext: "", status: "", headers: "", body: "", error: "" });
70167
- const [metrics, setMetrics] = import_react34.useState(null);
70168
- const [history, setHistory] = import_react34.useState([]);
70169
- const [loading, setLoading] = import_react34.useState(false);
70170
- const requestRef = import_react34.useRef(request);
70689
+ const [activeTab, setActiveTab] = import_react35.useState("request");
70690
+ const [request, setRequest] = import_react35.useState({ method: "GET", url: "", headers: "", body: "" });
70691
+ const [response, setResponse] = import_react35.useState({ statustext: "", status: "", headers: "", body: "", error: "" });
70692
+ const [metrics, setMetrics] = import_react35.useState(null);
70693
+ const [history, setHistory] = import_react35.useState([]);
70694
+ const [loading, setLoading] = import_react35.useState(false);
70695
+ const [downloadProgress, setDownloadProgress] = import_react35.useState(null);
70696
+ const requestRef = import_react35.useRef(request);
70171
70697
  requestRef.current = request;
70172
- import_react34.useEffect(() => {
70698
+ import_react35.useEffect(() => {
70173
70699
  const loadHistory = async () => setHistory((await historyManager.loadHistory()).entries);
70174
70700
  loadHistory();
70175
70701
  }, []);
70176
- import_react34.useEffect(() => {
70702
+ import_react35.useEffect(() => {
70177
70703
  const loadTheme = async () => {
70178
70704
  const loadedTheme = await themeManager.loadCurrTheme();
70179
70705
  setTheme(loadedTheme);
70180
70706
  };
70181
70707
  loadTheme();
70182
70708
  }, []);
70183
- const handleSend = import_react34.useCallback(async () => {
70709
+ const handleSend = import_react35.useCallback(async () => {
70184
70710
  setLoading(true);
70711
+ setDownloadProgress(null);
70185
70712
  const startTime = Date.now();
70186
70713
  const currentRequest = requestRef.current;
70187
70714
  try {
@@ -70203,7 +70730,8 @@ var UI = () => {
70203
70730
  method: currentRequest.method,
70204
70731
  url: currentRequest.url,
70205
70732
  headers: parsedHeaders,
70206
- body: reqBody
70733
+ body: reqBody,
70734
+ onProgress: (progress) => setDownloadProgress(progress)
70207
70735
  });
70208
70736
  const responseTime = Date.now() - startTime;
70209
70737
  await historyManager.addEntry({ ...currentRequest }, res.status, responseTime);
@@ -70216,13 +70744,14 @@ var UI = () => {
70216
70744
  setActiveTab("response");
70217
70745
  } finally {
70218
70746
  setLoading(false);
70747
+ setDownloadProgress(null);
70219
70748
  }
70220
70749
  }, []);
70221
70750
  const handleThemeChange = (theme2) => {
70222
70751
  themeManager.ChangeTheme(theme2);
70223
70752
  setTheme(theme2);
70224
70753
  };
70225
- const handleHistoryClick = import_react34.useCallback((item) => {
70754
+ const handleHistoryClick = import_react35.useCallback((item) => {
70226
70755
  setRequest({
70227
70756
  method: item.method,
70228
70757
  url: item.url,
@@ -70233,9 +70762,10 @@ var UI = () => {
70233
70762
  }, []);
70234
70763
  const tabs = [{ name: "request", label: "Request" }, { name: "response", label: "Response" }];
70235
70764
  const activeIndex = tabs.findIndex((t) => t.name === activeTab);
70236
- const [showThemeSelector, setShowThemeSelector] = import_react34.useState(false);
70237
- const [showExportDialog, setShowExportDialog] = import_react34.useState(false);
70238
- const [inputFocused, setInputFocused] = import_react34.useState(false);
70765
+ const [showThemeSelector, setShowThemeSelector] = import_react35.useState(false);
70766
+ const [showExportDialog, setShowExportDialog] = import_react35.useState(false);
70767
+ const [inputFocused, setInputFocused] = import_react35.useState(false);
70768
+ const [historySearching, setHistorySearching] = import_react35.useState(false);
70239
70769
  use_input_default((input, key) => {
70240
70770
  if (input === "q" && !showExportDialog)
70241
70771
  exit();
@@ -70249,15 +70779,17 @@ var UI = () => {
70249
70779
  setShowThemeSelector(false);
70250
70780
  if (key.escape && showExportDialog)
70251
70781
  setShowExportDialog(false);
70252
- if ((input === "t" || input === "T") && !key.ctrl && !key.meta && !inputFocused && !showExportDialog)
70782
+ if ((input === "t" || input === "T") && !key.ctrl && !key.meta && !inputFocused && !historySearching && !showExportDialog)
70253
70783
  setShowThemeSelector((prev) => !prev);
70254
- if ((input === "e" || input === "E") && !key.ctrl && !key.meta && !inputFocused && !showThemeSelector)
70784
+ if ((input === "e" || input === "E") && !key.ctrl && !key.meta && !inputFocused && !historySearching && !showThemeSelector)
70255
70785
  setShowExportDialog((prev) => !prev);
70786
+ if ((input === "f" || input === "F") && !key.ctrl && !key.meta && !inputFocused && !historySearching && !showThemeSelector && !showExportDialog)
70787
+ setHistorySearching(true);
70256
70788
  }, { isActive: !showExportDialog });
70257
- const onMethodChange = import_react34.useCallback((method) => setRequest((r) => ({ ...r, method })), []);
70258
- const onUrlChange = import_react34.useCallback((url) => setRequest((r) => ({ ...r, url })), []);
70259
- const onHeadersChange = import_react34.useCallback((headers) => setRequest((r) => ({ ...r, headers })), []);
70260
- const onBodyChange = import_react34.useCallback((body) => setRequest((r) => ({ ...r, body })), []);
70789
+ const onMethodChange = import_react35.useCallback((method) => setRequest((r) => ({ ...r, method })), []);
70790
+ const onUrlChange = import_react35.useCallback((url) => setRequest((r) => ({ ...r, url })), []);
70791
+ const onHeadersChange = import_react35.useCallback((headers) => setRequest((r) => ({ ...r, headers })), []);
70792
+ const onBodyChange = import_react35.useCallback((body) => setRequest((r) => ({ ...r, body })), []);
70261
70793
  const historyUrls = Array.from(new Set(history.map((h) => h.url))).filter(Boolean);
70262
70794
  return /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70263
70795
  padding: 1,
@@ -70272,7 +70804,8 @@ var UI = () => {
70272
70804
  theme,
70273
70805
  onThemeChange: (themeName) => {
70274
70806
  handleThemeChange(themes[themeName]);
70275
- }
70807
+ },
70808
+ isActive: showThemeSelector
70276
70809
  }, undefined, false, undefined, this)
70277
70810
  }, undefined, false, undefined, this),
70278
70811
  showExportDialog && /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
@@ -70359,7 +70892,9 @@ var UI = () => {
70359
70892
  }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(HistoryList, {
70360
70893
  history,
70361
70894
  onItemClick: handleHistoryClick,
70362
- theme
70895
+ theme,
70896
+ onSearchingChange: setHistorySearching,
70897
+ startSearching: historySearching
70363
70898
  }, undefined, false, undefined, this)
70364
70899
  }, undefined, false, undefined, this),
70365
70900
  /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
@@ -70415,6 +70950,14 @@ var UI = () => {
70415
70950
  onChange: setActiveTab,
70416
70951
  theme
70417
70952
  }, undefined, false, undefined, this),
70953
+ downloadProgress && /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70954
+ marginTop: 1,
70955
+ justifyContent: "center",
70956
+ children: /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(LiveByteCounter, {
70957
+ progress: downloadProgress,
70958
+ theme: theme.colors
70959
+ }, undefined, false, undefined, this)
70960
+ }, undefined, false, undefined, this),
70418
70961
  /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
70419
70962
  marginTop: 1,
70420
70963
  flexDirection: "column",
@@ -70477,12 +71020,6 @@ function App2() {
70477
71020
  var import_chalk6 = __toESM(require_source(), 1);
70478
71021
  var jsx_dev_runtime15 = __toESM(require_jsx_dev_runtime(), 1);
70479
71022
  var UIWrapper = () => {
70480
- const { exit } = use_app_default();
70481
- use_input_default((input) => {
70482
- if (input === "q") {
70483
- exit();
70484
- }
70485
- });
70486
71023
  return /* @__PURE__ */ jsx_dev_runtime15.jsxDEV(Box_default, {
70487
71024
  flexDirection: "column",
70488
71025
  children: [
@@ -70663,7 +71200,7 @@ async function mockApis() {
70663
71200
 
70664
71201
  // src/index.ts
70665
71202
  var program2 = new Command;
70666
- program2.version("1.3.9").description(import_chalk9.default.yellow("PostBoy CLI - Test your APIs with ease"));
71203
+ program2.version("1.4.1").description(import_chalk9.default.yellow("PostBoy CLI - Test your APIs with ease"));
70667
71204
  program2.command("run").description("Run a test API request").action(testCommand);
70668
71205
  program2.command("mock-list").description("List the mock API servers").action(mockApis);
70669
71206
  program2.command("ui").description("UI for PostBoy").action(uiCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postboy-tui",
3
- "version": "1.3.9",
3
+ "version": "1.4.1",
4
4
  "main": "dist/cli.js",
5
5
  "bin": {
6
6
  "postboy-tui": "dist/cli.js"