@tonyclaw/agent-inspector 2.1.15 → 2.1.17

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 (73) hide show
  1. package/.output/cli.js +57 -13
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
  7. package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
  9. package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
  10. package/.output/public/assets/index-Blqvndcn.js +1 -0
  11. package/.output/public/assets/index-Cs79WRQj.css +1 -0
  12. package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
  13. package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
  14. package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +255 -201
  16. package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
  19. package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
  20. package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
  21. package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
  23. package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
  26. package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
  27. package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
  28. package/.output/server/index.mjs +63 -63
  29. package/README.md +57 -2
  30. package/package.json +1 -1
  31. package/src/cli/startupOutput.ts +16 -0
  32. package/src/cli.ts +61 -15
  33. package/src/components/ProxyViewer.tsx +535 -163
  34. package/src/components/ProxyViewerContainer.tsx +77 -67
  35. package/src/components/alerts/AlertsDialog.tsx +2 -1
  36. package/src/components/clients/ClientLogo.tsx +1 -1
  37. package/src/components/groups/GroupsDialog.tsx +19 -1
  38. package/src/components/providers/ProviderCard.tsx +133 -32
  39. package/src/components/providers/ProviderForm.tsx +61 -29
  40. package/src/components/providers/ProviderLogo.tsx +6 -1
  41. package/src/components/providers/ProvidersPanel.tsx +7 -7
  42. package/src/components/providers/SettingsDialog.tsx +13 -6
  43. package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
  44. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  45. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  46. package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
  47. package/src/components/proxy-viewer/LogEntry.tsx +130 -43
  48. package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
  49. package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
  50. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  51. package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
  52. package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
  53. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  54. package/src/components/proxy-viewer/bodyHydration.ts +56 -0
  55. package/src/components/proxy-viewer/viewerState.ts +0 -4
  56. package/src/components/ui/icon-trigger.ts +2 -0
  57. package/src/components/ui/transient-toast.tsx +104 -0
  58. package/src/lib/providerTestContract.ts +56 -10
  59. package/src/lib/providerTestPrompt.ts +23 -1
  60. package/src/lib/sessionInfoContract.ts +4 -0
  61. package/src/lib/upstreamUrl.ts +105 -1
  62. package/src/mcp/server.ts +71 -27
  63. package/src/mcp/toolHandlers.ts +24 -5
  64. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  65. package/src/proxy/sessionInfo.ts +31 -2
  66. package/src/proxy/sessionSupervisor.ts +20 -1
  67. package/src/proxy/store.ts +1 -1
  68. package/src/routes/api/providers.$providerId.test.log.ts +93 -14
  69. package/styles/globals.css +180 -180
  70. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
  71. package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
  72. package/.output/public/assets/index-CI1-G8ua.js +0 -1
  73. package/.output/public/assets/index-DdhFqPsI.css +0 -1
@@ -6,7 +6,13 @@ import { Eye, EyeOff, Copy, Check, ChevronDown } from "lucide-react";
6
6
  import type { ProviderConfig, ProviderModelMetadata } from "../../lib/providerContract";
7
7
  import { copyTextToClipboard } from "../../lib/clipboard";
8
8
  import { maskApiKey } from "../../lib/mask";
9
- import { buildUpstreamUrl } from "../../lib/upstreamUrl";
9
+ import {
10
+ PATH_V1_CHAT_COMPLETIONS,
11
+ PATH_V1_MESSAGES,
12
+ PATH_V1_RESPONSES,
13
+ previewUpstreamUrl,
14
+ type UpstreamUrlPreview,
15
+ } from "../../lib/upstreamUrl";
10
16
  import { formatContextWindowInput, parseContextWindowTokensInput } from "../../lib/utils";
11
17
 
12
18
  const ZHIPU_PROVIDER_KEYWORDS = ["zhipu", "zhipuai", "bigmodel", "z.ai", "zai", "glm"];
@@ -17,9 +23,9 @@ const ZHIPU_CODING_PROVIDER_KEYWORDS = [
17
23
  "z.ai coding",
18
24
  "zai coding",
19
25
  ];
20
- const ANTHROPIC_MESSAGES_ENDPOINT = "/v1/messages";
21
- const OPENAI_CHAT_COMPLETIONS_ENDPOINT = "/v1/chat/completions";
22
- const OPENAI_RESPONSES_ENDPOINT = "/v1/responses";
26
+ const ANTHROPIC_MESSAGES_ENDPOINT = PATH_V1_MESSAGES;
27
+ const OPENAI_CHAT_COMPLETIONS_ENDPOINT = PATH_V1_CHAT_COMPLETIONS;
28
+ const OPENAI_RESPONSES_ENDPOINT = PATH_V1_RESPONSES;
23
29
 
24
30
  // Known provider presets - maps provider name keywords to their API URLs
25
31
  const KNOWN_PROVIDER_PRESETS: Record<
@@ -107,7 +113,7 @@ const MINIMAX_MODELS = [
107
113
  ];
108
114
 
109
115
  // Alibaba model options
110
- const ALIBABA_MODELS = ["glm-5", "glm-5.1", "glm-5.2", "qwen3.6-plus", "qwen3.7-max"];
116
+ const ALIBABA_MODELS = ["qwen3.6-plus", "qwen3.7-max"];
111
117
 
112
118
  // ZhipuAI / Z.AI model options
113
119
  const ZHIPU_MODELS = [
@@ -275,25 +281,45 @@ function stripEndpointSuffix(value: string | undefined, endpoint: string): strin
275
281
  : trimmed;
276
282
  }
277
283
 
278
- function previewEndpointUrl(baseUrl: string, endpoint: string): string | null {
284
+ function previewEndpointUrl(
285
+ baseUrl: string,
286
+ endpoint: string,
287
+ providerName: string,
288
+ ): UpstreamUrlPreview | null {
279
289
  const trimmed = baseUrl.trim();
280
- return trimmed === "" ? null : buildUpstreamUrl(trimmed, endpoint);
290
+ return trimmed === "" ? null : previewUpstreamUrl(trimmed, endpoint, { providerName });
281
291
  }
282
292
 
283
293
  function EndpointUrlPreview({
284
294
  baseUrl,
285
295
  endpoint,
296
+ providerName,
286
297
  }: {
287
298
  baseUrl: string;
288
299
  endpoint: string;
300
+ providerName: string;
289
301
  }): JSX.Element | null {
290
- const finalUrl = previewEndpointUrl(baseUrl, endpoint);
291
- if (finalUrl === null) return null;
302
+ const preview = previewEndpointUrl(baseUrl, endpoint, providerName);
303
+ if (preview === null) return null;
292
304
 
293
305
  return (
294
- <div className="rounded-md border border-border/70 bg-muted/35 px-3 py-2 text-xs">
306
+ <div className="border border-border bg-muted/40 rounded-md px-3 py-2 text-xs">
295
307
  <div className="text-muted-foreground">Final URL</div>
296
- <div className="break-all font-mono text-foreground">{finalUrl}</div>
308
+ <div className="break-all font-mono text-foreground">{preview.finalUrl}</div>
309
+ {preview.diagnostics.length > 0 && (
310
+ <div className="mt-2 space-y-1">
311
+ {preview.diagnostics.map((diagnostic) => (
312
+ <div
313
+ key={`${diagnostic.severity}:${diagnostic.message}`}
314
+ className={
315
+ diagnostic.severity === "warning" ? "text-amber-200" : "text-muted-foreground"
316
+ }
317
+ >
318
+ {diagnostic.message}
319
+ </div>
320
+ ))}
321
+ </div>
322
+ )}
297
323
  </div>
298
324
  );
299
325
  }
@@ -656,7 +682,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
656
682
  }
657
683
  className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
658
684
  >
659
- <option value="">—</option>
685
+ <option value="">Unspecified</option>
660
686
  <option value="personal">个人 (Personal)</option>
661
687
  <option value="company">公司 (Company)</option>
662
688
  </select>
@@ -681,7 +707,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
681
707
  <p className="text-xs text-destructive">{errors.modelMetadataUrl}</p>
682
708
  )}
683
709
  {modelMetadataUrl.trim() === "" && (
684
- <div className="rounded-md border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
710
+ <div className="border border-border bg-muted/40 rounded-md px-3 py-2 text-xs text-muted-foreground">
685
711
  Built-in limits cover DeepSeek v4 pro/flash, MiniMax M3/M2.7/M2.5/M2.1, and GLM coding
686
712
  models. Use a registry URL for private or custom model limits.
687
713
  </div>
@@ -729,7 +755,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
729
755
  className="text-muted-foreground hover:text-foreground transition-colors p-1 shrink-0"
730
756
  aria-label="Copy API key"
731
757
  >
732
- {copied ? <Check className="size-4 text-green-500" /> : <Copy className="size-4" />}
758
+ {copied ? <Check className="size-4 text-cyan-300" /> : <Copy className="size-4" />}
733
759
  </button>
734
760
  </div>
735
761
  {errors.apiKey !== undefined && <p className="text-xs text-destructive">{errors.apiKey}</p>}
@@ -751,7 +777,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
751
777
  ref={(el) => {
752
778
  modelRowRefs.current[i] = el;
753
779
  }}
754
- className="space-y-2 rounded-md border border-border bg-muted/10 p-2"
780
+ className="border border-border bg-card shadow-sm space-y-2 rounded-[8px] p-2"
755
781
  >
756
782
  <div className="flex items-center gap-2">
757
783
  <div className="relative flex-1">
@@ -775,7 +801,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
775
801
  <ChevronDown className="size-4" />
776
802
  </button>
777
803
  {openModelDropdown === i && (
778
- <div className="absolute left-0 right-0 top-full mt-1 z-50 bg-popover border border-border rounded-md shadow-md max-h-48 overflow-y-auto">
804
+ <div className="bg-popover text-popover-foreground absolute left-0 right-0 top-full z-50 mt-1 max-h-48 overflow-y-auto rounded-md border shadow-md">
779
805
  {suggestedModels.map((opt) => (
780
806
  <button
781
807
  key={opt}
@@ -784,7 +810,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
784
810
  updateModel(i, opt);
785
811
  setOpenModelDropdown(null);
786
812
  }}
787
- className="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors"
813
+ className="w-full px-3 py-2 text-left text-sm transition-colors hover:bg-white/[0.06]"
788
814
  >
789
815
  {opt}
790
816
  </button>
@@ -899,7 +925,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
899
925
  </div>
900
926
 
901
927
  <div className="space-y-2">
902
- <div className="flex gap-1 border-b border-border">
928
+ <div className="border border-border bg-muted/40 flex gap-1 rounded-[8px] p-1">
903
929
  <TooltipProvider>
904
930
  <Tooltip>
905
931
  <TooltipTrigger asChild>
@@ -945,7 +971,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
945
971
  <label htmlFor="provider-anthropic-base-url" className="text-sm font-medium">
946
972
  Anthropic Base URL
947
973
  </label>
948
- <div className="flex min-w-0 overflow-hidden rounded-md border border-input bg-background ring-offset-background focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
974
+ <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
949
975
  <input
950
976
  id="provider-anthropic-base-url"
951
977
  type="text"
@@ -955,16 +981,20 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
955
981
  setAnthropicBaseUrl(e.target.value);
956
982
  }}
957
983
  placeholder="https://api.anthropic.com"
958
- className="min-w-0 flex-1 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
984
+ className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
959
985
  />
960
- <span className="shrink-0 select-none border-l bg-muted px-3 py-2 font-mono text-xs text-muted-foreground">
986
+ <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
961
987
  {ANTHROPIC_MESSAGES_ENDPOINT}
962
988
  </span>
963
989
  </div>
964
990
  {errors.anthropicBaseUrl !== undefined && (
965
991
  <p className="text-xs text-destructive">{errors.anthropicBaseUrl}</p>
966
992
  )}
967
- <EndpointUrlPreview baseUrl={anthropicBaseUrl} endpoint={ANTHROPIC_MESSAGES_ENDPOINT} />
993
+ <EndpointUrlPreview
994
+ baseUrl={anthropicBaseUrl}
995
+ endpoint={ANTHROPIC_MESSAGES_ENDPOINT}
996
+ providerName={name}
997
+ />
968
998
  <p className="text-xs text-muted-foreground">
969
999
  Anthropic-compatible base URL. Leave empty if this provider does not support Anthropic
970
1000
  format.
@@ -978,7 +1008,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
978
1008
  <label htmlFor="provider-openai-base-url" className="text-sm font-medium">
979
1009
  OpenAI Chat Base URL
980
1010
  </label>
981
- <div className="flex min-w-0 overflow-hidden rounded-md border border-input bg-background ring-offset-background focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
1011
+ <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
982
1012
  <input
983
1013
  id="provider-openai-base-url"
984
1014
  type="text"
@@ -988,9 +1018,9 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
988
1018
  setOpenaiBaseUrl(e.target.value);
989
1019
  }}
990
1020
  placeholder="https://api.openai.com"
991
- className="min-w-0 flex-1 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1021
+ className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
992
1022
  />
993
- <span className="shrink-0 select-none border-l bg-muted px-3 py-2 font-mono text-xs text-muted-foreground">
1023
+ <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
994
1024
  {OPENAI_CHAT_COMPLETIONS_ENDPOINT}
995
1025
  </span>
996
1026
  </div>
@@ -1000,6 +1030,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
1000
1030
  <EndpointUrlPreview
1001
1031
  baseUrl={openaiBaseUrl}
1002
1032
  endpoint={OPENAI_CHAT_COMPLETIONS_ENDPOINT}
1033
+ providerName={name}
1003
1034
  />
1004
1035
  <p className="text-xs text-muted-foreground">
1005
1036
  OpenAI Chat Completions-compatible base URL. Leave empty if this provider only
@@ -1010,7 +1041,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
1010
1041
  <label htmlFor="provider-openai-responses-base-url" className="text-sm font-medium">
1011
1042
  OpenAI Responses Base URL
1012
1043
  </label>
1013
- <div className="flex min-w-0 overflow-hidden rounded-md border border-input bg-background ring-offset-background focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
1044
+ <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
1014
1045
  <input
1015
1046
  id="provider-openai-responses-base-url"
1016
1047
  type="text"
@@ -1020,9 +1051,9 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
1020
1051
  setOpenaiResponsesBaseUrl(e.target.value);
1021
1052
  }}
1022
1053
  placeholder="https://api.openai.com"
1023
- className="min-w-0 flex-1 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1054
+ className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1024
1055
  />
1025
- <span className="shrink-0 select-none border-l bg-muted px-3 py-2 font-mono text-xs text-muted-foreground">
1056
+ <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
1026
1057
  {OPENAI_RESPONSES_ENDPOINT}
1027
1058
  </span>
1028
1059
  </div>
@@ -1032,6 +1063,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
1032
1063
  <EndpointUrlPreview
1033
1064
  baseUrl={openaiResponsesBaseUrl}
1034
1065
  endpoint={OPENAI_RESPONSES_ENDPOINT}
1066
+ providerName={name}
1035
1067
  />
1036
1068
  <p className="text-xs text-muted-foreground">
1037
1069
  Optional Responses-compatible base URL. Set only when the provider supports the
@@ -1058,7 +1090,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
1058
1090
  </p>
1059
1091
  </div>
1060
1092
 
1061
- <div className="sticky bottom-0 bg-card border-t flex gap-2 justify-end pt-2 pb-2">
1093
+ <div className="border border-border bg-background/95 shadow-sm sticky bottom-0 flex justify-end gap-2 rounded-[8px] px-3 py-2">
1062
1094
  <Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
1063
1095
  Cancel
1064
1096
  </Button>
@@ -26,12 +26,17 @@ const PROVIDER_MAP: Record<string, Provider> = {
26
26
  "deepseek-": "deepseek",
27
27
  MiniMax: "minimax",
28
28
  qwen: "qwen",
29
- "glm-": "zhipuai",
30
29
  };
31
30
 
31
+ function isGlmModel(modelLower: string): boolean {
32
+ const normalized = modelLower.replace(/[_\s]+/g, "-");
33
+ return normalized.split(/[/:@]/).some((segment) => segment.startsWith("glm-"));
34
+ }
35
+
32
36
  export function detectProvider(model: string | null): Provider {
33
37
  if (model === null) return "unknown";
34
38
  const modelLower = model.toLowerCase();
39
+ if (isGlmModel(modelLower)) return "zhipuai";
35
40
  for (const [prefix, provider] of Object.entries(PROVIDER_MAP)) {
36
41
  if (modelLower.startsWith(prefix.toLowerCase())) {
37
42
  return provider;
@@ -521,7 +521,7 @@ export function ProvidersPanel({
521
521
  if (showForm || editingProvider) {
522
522
  return (
523
523
  <div className="space-y-4">
524
- <div className="flex items-center justify-between sticky top-0 bg-background z-10 pb-2">
524
+ <div className="border border-border bg-background/95 shadow-sm sticky top-0 z-10 flex items-center justify-between rounded-[8px] px-3 py-2">
525
525
  <h3 className="text-lg font-medium">
526
526
  {editingProvider ? "Edit Provider" : "Add New Provider"}
527
527
  </h3>
@@ -540,7 +540,7 @@ export function ProvidersPanel({
540
540
 
541
541
  return (
542
542
  <div className="space-y-4">
543
- <div className="flex items-center justify-between sticky top-0 z-10 bg-background pb-2">
543
+ <div className="border border-border bg-background/95 shadow-sm sticky top-0 z-10 flex items-center justify-between rounded-[8px] px-3 py-2">
544
544
  <h3 className="text-lg font-medium">Providers</h3>
545
545
  <div className="flex items-center gap-2">
546
546
  <TooltipProvider>
@@ -608,7 +608,7 @@ export function ProvidersPanel({
608
608
  </div>
609
609
 
610
610
  {configPath !== null && (
611
- <div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/30 rounded-md px-3 py-2">
611
+ <div className="border border-border bg-card shadow-sm flex items-center gap-2 rounded-[8px] px-3 py-2 text-xs text-muted-foreground">
612
612
  <span className="shrink-0">Config:</span>
613
613
  <span className="font-mono truncate" title={configPath}>
614
614
  {configPath}
@@ -631,7 +631,7 @@ export function ProvidersPanel({
631
631
  }
632
632
  >
633
633
  {configPathCopied ? (
634
- <Check className="size-3 text-green-500" />
634
+ <Check className="size-3 text-cyan-300" />
635
635
  ) : (
636
636
  <Copy className="size-3" />
637
637
  )}
@@ -644,14 +644,14 @@ export function ProvidersPanel({
644
644
  )}
645
645
 
646
646
  {error !== null && (
647
- <div className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
647
+ <div className="border border-border bg-card shadow-sm flex items-center gap-2 rounded-[8px] px-3 py-2 text-sm text-destructive">
648
648
  <AlertCircle className="size-4 shrink-0" />
649
649
  {error}
650
650
  </div>
651
651
  )}
652
652
 
653
653
  {providers.length === 0 ? (
654
- <div className="text-center py-12 space-y-3">
654
+ <div className="border border-border bg-card shadow-sm space-y-3 rounded-[8px] py-12 text-center">
655
655
  <p className="text-sm text-muted-foreground">No providers configured yet.</p>
656
656
  <Button onClick={() => setShowForm(true)} variant="outline" size="sm">
657
657
  <Plus className="size-4" />
@@ -660,7 +660,7 @@ export function ProvidersPanel({
660
660
  </div>
661
661
  ) : (
662
662
  <div className="space-y-3">
663
- <div className="flex gap-1 border-b border-border">
663
+ <div className="border border-border bg-muted/40 flex gap-1 rounded-[8px] p-1">
664
664
  {(["all", "personal", "company"] as const).map((tab) => (
665
665
  <TooltipProvider key={tab}>
666
666
  <Tooltip>
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
5
5
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
6
6
  import { Button } from "../ui/button";
7
+ import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
7
8
  import { ProvidersPanel } from "./ProvidersPanel";
8
9
  import { useProviders } from "../../lib/useProviders";
9
10
  import { useStripConfig } from "../../lib/useStripConfig";
@@ -61,7 +62,13 @@ export function SettingsDialog(): JSX.Element {
61
62
  return (
62
63
  <Dialog open={open} onOpenChange={setOpen}>
63
64
  <DialogTrigger asChild>
64
- <Button variant="ghost" size="icon" className="size-8">
65
+ <Button
66
+ variant="ghost"
67
+ size="icon"
68
+ className={INSPECTOR_ICON_TRIGGER_CLASS}
69
+ aria-label="Settings"
70
+ title="Settings"
71
+ >
65
72
  <Settings className="size-4" />
66
73
  <span className="sr-only">Settings</span>
67
74
  </Button>
@@ -226,25 +233,25 @@ function StorageSettingsTab(): JSX.Element {
226
233
  copiedId={copiedId}
227
234
  onCopy={handleCopy}
228
235
  />
229
- <div className="rounded-md border border-border bg-muted/20 px-3 py-2">
236
+ <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
230
237
  <div className="font-medium">In-memory logs</div>
231
238
  <div className="mt-1 font-mono text-muted-foreground">
232
239
  {stats.memoryCount.toLocaleString()} loaded
233
240
  </div>
234
241
  </div>
235
- <div className="rounded-md border border-border bg-muted/20 px-3 py-2">
242
+ <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
236
243
  <div className="font-medium">Log files</div>
237
244
  <div className="mt-1 font-mono text-muted-foreground">
238
245
  {stats.logFileCount.toLocaleString()} files / {formatBytes(stats.logBytes)}
239
246
  </div>
240
247
  </div>
241
- <div className="rounded-md border border-border bg-muted/20 px-3 py-2">
248
+ <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
242
249
  <div className="font-medium">Streaming chunks</div>
243
250
  <div className="mt-1 font-mono text-muted-foreground">
244
251
  {stats.chunkFileCount.toLocaleString()} files / {formatBytes(stats.chunkBytes)}
245
252
  </div>
246
253
  </div>
247
- <div className="rounded-md border border-border bg-muted/20 px-3 py-2">
254
+ <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
248
255
  <div className="font-medium">Session archives</div>
249
256
  <div className="mt-1 font-mono text-muted-foreground">
250
257
  {stats.sessionArchiveFileCount.toLocaleString()} files /{" "}
@@ -274,7 +281,7 @@ function CopyableSetupValue({
274
281
  }): JSX.Element {
275
282
  const copied = copiedId === id;
276
283
  return (
277
- <div className="min-w-0 rounded-md border border-border bg-muted/20 px-3 py-2">
284
+ <div className="border border-border bg-muted/40 min-w-0 rounded-md px-3 py-2">
278
285
  <div className="mb-1 text-xs font-medium text-muted-foreground">{label}</div>
279
286
  <div className="flex min-w-0 items-center gap-2">
280
287
  <code className="min-w-0 flex-1 truncate font-mono text-xs text-foreground">{value}</code>
@@ -392,7 +392,7 @@ function CandidateItem({
392
392
 
393
393
  if (editing) {
394
394
  return (
395
- <div className="rounded-md border border-border/80 bg-background/60 px-2.5 py-2">
395
+ <div className="border border-border bg-card shadow-sm rounded-[8px] px-2.5 py-2">
396
396
  <div className="grid gap-2">
397
397
  <div className="flex flex-wrap items-center gap-2">
398
398
  <select
@@ -404,7 +404,7 @@ function CandidateItem({
404
404
  );
405
405
  if (parsed.success) setDraftType(parsed.data);
406
406
  }}
407
- className="h-7 rounded-md border border-input bg-background px-2 text-xs"
407
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-7 rounded-md px-2 text-xs"
408
408
  aria-label="Candidate type"
409
409
  >
410
410
  <option value="episode">episode</option>
@@ -416,7 +416,7 @@ function CandidateItem({
416
416
  value={draftTitle}
417
417
  disabled={isUpdating}
418
418
  onChange={(event) => setDraftTitle(event.currentTarget.value)}
419
- className="h-7 min-w-[220px] flex-1 rounded-md border border-input bg-background px-2 text-xs"
419
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-7 min-w-[220px] flex-1 rounded-md px-2 text-xs"
420
420
  aria-label="Candidate title"
421
421
  />
422
422
  </div>
@@ -424,14 +424,14 @@ function CandidateItem({
424
424
  value={draftContent}
425
425
  disabled={isUpdating}
426
426
  onChange={(event) => setDraftContent(event.currentTarget.value)}
427
- className="min-h-28 rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed"
427
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground min-h-28 rounded-md px-2 py-1.5 font-mono text-[11px] leading-relaxed"
428
428
  aria-label="Candidate content"
429
429
  />
430
430
  <input
431
431
  value={draftTags}
432
432
  disabled={isUpdating}
433
433
  onChange={(event) => setDraftTags(event.currentTarget.value)}
434
- className="h-7 rounded-md border border-input bg-background px-2 text-xs"
434
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-7 rounded-md px-2 text-xs"
435
435
  aria-label="Candidate tags"
436
436
  />
437
437
  <div className="flex flex-wrap items-center gap-2">
@@ -794,7 +794,7 @@ export function AgentTraceSummary({
794
794
  if (logs.length === 0) return null;
795
795
 
796
796
  return (
797
- <section className="mb-2 rounded-lg border border-border bg-muted/10 px-3 py-2">
797
+ <section className="border border-border bg-card shadow-sm mb-2 rounded-[8px] px-3 py-2">
798
798
  <div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-xs">
799
799
  {showRollupMetrics && (
800
800
  <span className="inline-flex items-center gap-1.5 font-semibold text-foreground">
@@ -8,7 +8,7 @@ import { TurnGroupList } from "./TurnGroupList";
8
8
  import { AgentTraceSummary } from "./AgentTraceSummary";
9
9
  import type { CacheTrendEntry } from "./cacheTrend";
10
10
  import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
11
- import { buildTurnGroups, shouldRenderConversationContent } from "./viewerState";
11
+ import { buildTurnGroups } from "./viewerState";
12
12
 
13
13
  export type ConversationGroupProps = {
14
14
  group: ConversationGroupData;
@@ -25,14 +25,18 @@ export type ConversationGroupProps = {
25
25
  /** Callback to open CompareDrawer with a log and its immediate predecessor. */
26
26
  onCompareWithPrevious: (log: CapturedLog) => void;
27
27
  comparisonPredecessors: Map<number, CapturedLog>;
28
- /** When true, skip the group header and render content directly. */
29
- standalone?: boolean;
30
- /** When true, the route-level session context already shows rollup metrics. */
31
- hasPinnedSessionContext?: boolean;
28
+ /** Initial expansion state. Single-session pages use this to show logs under the session header. */
29
+ defaultExpanded?: boolean;
32
30
  /** Controls whether timestamps render as compact local time or full ISO strings. */
33
31
  timeDisplayFormat: TimeDisplayFormat;
34
32
  /** Clear all logs that belong to this group. */
35
33
  onClearGroup: (ids: number[]) => void;
34
+ /** Show IDE identity in the session header. */
35
+ showClientIdentity?: boolean;
36
+ /** Show process/project metadata in the session header. */
37
+ showProcessMetadata?: boolean;
38
+ /** Show client PID/project metadata on individual log rows. */
39
+ showLogClientMetadata?: boolean;
36
40
  /** Optional controlled expansion state used by virtualized lists. */
37
41
  expanded?: boolean;
38
42
  /** Optional controlled expansion setter used by virtualized lists. */
@@ -104,13 +108,15 @@ export const ConversationGroup = memo(function ({
104
108
  onCompareWithPrevious,
105
109
  comparisonPredecessors,
106
110
  onClearGroup,
107
- standalone = false,
108
- hasPinnedSessionContext = false,
111
+ showClientIdentity = true,
112
+ showProcessMetadata = true,
113
+ showLogClientMetadata = true,
114
+ defaultExpanded = false,
109
115
  timeDisplayFormat,
110
116
  expanded: controlledExpanded,
111
117
  onExpandedChange,
112
118
  }: ConversationGroupProps): JSX.Element {
113
- const [internalExpanded, setInternalExpanded] = useState(false);
119
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
114
120
  const expanded = controlledExpanded ?? internalExpanded;
115
121
  const setExpanded = useCallback(
116
122
  (nextExpanded: boolean) => {
@@ -122,12 +128,17 @@ export const ConversationGroup = memo(function ({
122
128
  },
123
129
  [onExpandedChange],
124
130
  );
131
+
132
+ useEffect(() => {
133
+ if (!defaultExpanded || controlledExpanded !== undefined) return;
134
+ setInternalExpanded(true);
135
+ }, [controlledExpanded, defaultExpanded]);
136
+
125
137
  const stats = useMemo(() => computeStats(group.logs), [group.logs]);
126
138
  const providers = useMemo(() => collectProviders(group.logs), [group.logs]);
127
139
  const startTime = group.logs[0]?.timestamp ?? new Date().toISOString();
128
140
  const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
129
141
  const isLoading = group.logs.some((log) => log.responseStatus === null);
130
- const showTraceRollupMetrics = standalone && !hasPinnedSessionContext;
131
142
  const clientPid = useMemo(() => getFirstClientPid(group.logs), [group.logs]);
132
143
  const clientProjectFolder = useMemo(() => getFirstClientProjectFolder(group.logs), [group.logs]);
133
144
 
@@ -136,7 +147,6 @@ export const ConversationGroup = memo(function ({
136
147
 
137
148
  useEffect(() => {
138
149
  const handleLogFocusRequest = (event: Event): void => {
139
- if (standalone) return;
140
150
  const request = readLogFocusRequest(event);
141
151
  if (request === null) return;
142
152
  if (!group.logs.some((log) => log.id === request.logId)) return;
@@ -147,37 +157,37 @@ export const ConversationGroup = memo(function ({
147
157
  return () => {
148
158
  window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
149
159
  };
150
- }, [group.logs, setExpanded, standalone]);
160
+ }, [group.logs, setExpanded]);
151
161
 
152
162
  return (
153
163
  <div className="mb-2">
154
- {!standalone && (
155
- <ConversationHeader
156
- conversationId={group.conversationId}
157
- startTime={startTime}
158
- endTime={endTime}
159
- totalCalls={group.logs.length}
160
- totalInputTokens={stats.totalInputTokens}
161
- totalOutputTokens={stats.totalOutputTokens}
162
- expanded={expanded}
163
- onToggle={() => setExpanded(!expanded)}
164
- providers={providers}
165
- isLoading={isLoading}
166
- userAgent={group.logs[0]?.userAgent ?? null}
167
- clientPid={clientPid}
168
- clientProjectFolder={clientProjectFolder}
169
- timeDisplayFormat={timeDisplayFormat}
170
- onClear={() => onClearGroup(group.logs.map((l) => l.id))}
171
- />
172
- )}
164
+ <ConversationHeader
165
+ conversationId={group.conversationId}
166
+ startTime={startTime}
167
+ endTime={endTime}
168
+ totalCalls={group.logs.length}
169
+ totalInputTokens={stats.totalInputTokens}
170
+ totalOutputTokens={stats.totalOutputTokens}
171
+ expanded={expanded}
172
+ onToggle={() => setExpanded(!expanded)}
173
+ providers={providers}
174
+ isLoading={isLoading}
175
+ userAgent={group.logs[0]?.userAgent ?? null}
176
+ clientPid={clientPid}
177
+ clientProjectFolder={clientProjectFolder}
178
+ showClientIdentity={showClientIdentity}
179
+ showProcessMetadata={showProcessMetadata}
180
+ timeDisplayFormat={timeDisplayFormat}
181
+ onClear={() => onClearGroup(group.logs.map((l) => l.id))}
182
+ />
173
183
 
174
- {shouldRenderConversationContent(standalone, expanded) && (
184
+ {expanded && (
175
185
  <div>
176
186
  <AgentTraceSummary
177
187
  logs={group.logs}
178
188
  scopeId={group.conversationId}
179
189
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
180
- showRollupMetrics={showTraceRollupMetrics}
190
+ showRollupMetrics={false}
181
191
  timeDisplayFormat={timeDisplayFormat}
182
192
  />
183
193
  <TurnGroupList
@@ -189,6 +199,7 @@ export const ConversationGroup = memo(function ({
189
199
  cacheTrends={cacheTrends}
190
200
  onCompareWithPrevious={onCompareWithPrevious}
191
201
  comparisonPredecessors={comparisonPredecessors}
202
+ showLogClientMetadata={showLogClientMetadata}
192
203
  />
193
204
  </div>
194
205
  )}