@skyhook-io/radar-app 1.8.6 → 1.8.7

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 (75) hide show
  1. package/README.md +7 -1
  2. package/package.json +11 -9
  3. package/src/App.tsx +221 -181
  4. package/src/RadarApp.tsx +91 -20
  5. package/src/api/client.delta.test.ts +89 -0
  6. package/src/api/client.deltaSync.test.ts +216 -0
  7. package/src/api/client.ts +262 -34
  8. package/src/api/diagnose.ts +288 -0
  9. package/src/api/timelineSource.test.ts +217 -0
  10. package/src/api/timelineSource.ts +580 -0
  11. package/src/components/ConnectionErrorView.tsx +174 -70
  12. package/src/components/ContextSwitcher.tsx +13 -5
  13. package/src/components/applications/ApplicationsView.tsx +327 -26
  14. package/src/components/audit/AuditSettingsDialog.tsx +2 -2
  15. package/src/components/curl/ServiceCurlButton.tsx +2 -2
  16. package/src/components/diagnose/AISettings.tsx +121 -0
  17. package/src/components/diagnose/DiagnoseContext.tsx +491 -0
  18. package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
  19. package/src/components/diagnose/Home.tsx +163 -0
  20. package/src/components/diagnose/InvestigationView.tsx +604 -0
  21. package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
  22. package/src/components/diagnose/launch.ts +65 -0
  23. package/src/components/diagnose/parts.tsx +1689 -0
  24. package/src/components/dock/BottomDock.tsx +2 -3
  25. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  26. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  27. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  28. package/src/components/execution/batch-run-actions.test.ts +48 -0
  29. package/src/components/execution/batch-run-actions.ts +24 -0
  30. package/src/components/execution/batch-timeline.test.ts +57 -0
  31. package/src/components/execution/batch-timeline.ts +46 -0
  32. package/src/components/execution/execution-definition.test.ts +208 -0
  33. package/src/components/execution/execution-definition.ts +245 -0
  34. package/src/components/helm/ChartBrowser.tsx +2 -3
  35. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  36. package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
  37. package/src/components/helm/HelmView.tsx +2 -3
  38. package/src/components/helm/InstallWizard.tsx +3 -5
  39. package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
  40. package/src/components/helm/ValuesDiffPreview.tsx +15 -4
  41. package/src/components/home/HomeView.tsx +17 -15
  42. package/src/components/home/MCPSetupDialog.tsx +1 -1
  43. package/src/components/home/mcpToolCatalog.ts +2 -2
  44. package/src/components/issues/IssuesPane.tsx +9 -1
  45. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  46. package/src/components/portforward/PortForwardButton.tsx +2 -2
  47. package/src/components/portforward/PortForwardManager.tsx +19 -14
  48. package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
  49. package/src/components/resource/RightsizingStrip.tsx +0 -3
  50. package/src/components/resources/ImageFilesystemModal.tsx +2 -2
  51. package/src/components/resources/PodFilesystemModal.tsx +2 -3
  52. package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
  53. package/src/components/resources/ResourcesView.tsx +13 -4
  54. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  55. package/src/components/resources/renderers/index.ts +1 -0
  56. package/src/components/settings/SettingsDialog.tsx +80 -23
  57. package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
  58. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  59. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  60. package/src/components/timeline/TimelineList.tsx +51 -10
  61. package/src/components/timeline/TimelineView.tsx +701 -15
  62. package/src/components/timeline/TimelineView.urlparams.test.ts +294 -0
  63. package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
  64. package/src/components/traffic/TrafficView.tsx +8 -4
  65. package/src/components/ui/CommandPalette.tsx +2 -2
  66. package/src/components/workload/WorkloadView.tsx +319 -35
  67. package/src/context/ConnectionContext.tsx +109 -11
  68. package/src/context/DiagnoseCustomization.tsx +42 -0
  69. package/src/context/TimelineSource.tsx +50 -0
  70. package/src/hooks/useClusterLoadState.ts +73 -0
  71. package/src/hooks/useEventSource.ts +6 -0
  72. package/src/index.css +157 -0
  73. package/src/index.ts +12 -0
  74. package/src/types/clusterLoadState.ts +33 -0
  75. package/src/utils/navigation.ts +10 -0
@@ -1,5 +1,5 @@
1
- import { XCircle, RefreshCw, Loader2, Copy, Check, TerminalSquare } from 'lucide-react'
2
- import { useState } from 'react'
1
+ import { ServerOff, RefreshCw, Loader2, Copy, Check, TerminalSquare, ChevronRight } from 'lucide-react'
2
+ import { useEffect, useState } from 'react'
3
3
  import type { ConnectionState } from '../context/ConnectionContext'
4
4
  import { ContextSwitcher } from './ContextSwitcher'
5
5
  import { parseContextName } from '../utils/context-name'
@@ -29,8 +29,8 @@ function getAuthHints(context: string): AuthHints {
29
29
  case 'GKE': {
30
30
  const result: AuthHints = {
31
31
  title: 'GKE Authentication Failed',
32
- hints: ['Your Google Cloud credentials have expired.'],
33
- authCommand: { label: 'Re-authenticate with Google Cloud:', command: 'gcloud auth login' },
32
+ hints: ['Radar could not get Google Cloud credentials for this context.'],
33
+ authCommand: { label: 'Refresh Google Cloud credentials:', command: 'gcloud auth login' },
34
34
  }
35
35
  if (parsed.region && parsed.account) {
36
36
  const isZone = /^[a-z]+-[a-z]+\d+-[a-z]$/.test(parsed.region)
@@ -45,8 +45,11 @@ function getAuthHints(context: string): AuthHints {
45
45
  case 'EKS': {
46
46
  const result: AuthHints = {
47
47
  title: 'EKS Authentication Failed',
48
- hints: ['Your AWS credentials have expired.'],
49
- authCommand: { label: 'Re-authenticate with AWS:', command: 'aws sso login' },
48
+ hints: [
49
+ 'Radar could not get AWS credentials for this context.',
50
+ 'For AWS SSO contexts, the SSO session may need login.',
51
+ ],
52
+ authCommand: { label: 'If this context uses AWS SSO, refresh credentials:', command: 'aws sso login' },
50
53
  }
51
54
  if (parsed.region) {
52
55
  result.fallbackCommand = {
@@ -59,21 +62,71 @@ function getAuthHints(context: string): AuthHints {
59
62
  case 'AKS':
60
63
  return {
61
64
  title: 'AKS Authentication Failed',
62
- hints: ['Your Azure credentials have expired.'],
63
- authCommand: { label: 'Re-authenticate with Azure:', command: 'az login' },
65
+ hints: ['Radar could not get Azure credentials for this context.'],
66
+ authCommand: { label: 'Refresh Azure credentials:', command: 'az login' },
64
67
  fallbackCommand: { label: 'If that doesn\'t work, refresh cluster credentials:', command: 'az aks get-credentials --name <cluster> --resource-group <rg>' },
65
68
  }
66
69
  default:
67
70
  return {
68
71
  title: 'Authentication Failed',
69
72
  hints: [
70
- 'Your credentials may have expired',
73
+ 'Radar could not get Kubernetes credentials for this context',
71
74
  'Re-authenticate with your cloud provider and try again',
72
75
  ],
73
76
  }
74
77
  }
75
78
  }
76
79
 
80
+ function getTimeoutHints(context: string): AuthHints | null {
81
+ const parsed = parseContextName(context)
82
+ const baseHints = [
83
+ 'The Kubernetes API did not respond before the deadline.',
84
+ 'Check VPN, firewall rules, and whether the cluster endpoint is reachable.',
85
+ ]
86
+
87
+ switch (parsed.provider) {
88
+ case 'GKE': {
89
+ const result: AuthHints = {
90
+ title: 'Connection Timed Out',
91
+ hints: [...baseHints, 'If the endpoint is reachable, Google Cloud credentials may need refresh.'],
92
+ authCommand: { label: 'If network access looks healthy, refresh Google Cloud credentials:', command: 'gcloud auth login' },
93
+ }
94
+ if (parsed.region && parsed.account) {
95
+ const isZone = /^[a-z]+-[a-z]+\d+-[a-z]$/.test(parsed.region)
96
+ const flag = isZone ? '--zone' : '--region'
97
+ result.fallbackCommand = {
98
+ label: 'If that does not work, refresh cluster credentials:',
99
+ command: `gcloud container clusters get-credentials ${parsed.clusterName} ${flag} ${parsed.region} --project ${parsed.account}`,
100
+ }
101
+ }
102
+ return result
103
+ }
104
+ case 'EKS': {
105
+ const result: AuthHints = {
106
+ title: 'Connection Timed Out',
107
+ hints: [...baseHints, 'If the endpoint is reachable, AWS credentials or SSO may need refresh.'],
108
+ authCommand: { label: 'If this context uses AWS SSO and network access looks healthy, refresh credentials:', command: 'aws sso login' },
109
+ }
110
+ if (parsed.region) {
111
+ result.fallbackCommand = {
112
+ label: 'If that does not work, refresh cluster credentials:',
113
+ command: `aws eks update-kubeconfig --name ${parsed.clusterName} --region ${parsed.region}`,
114
+ }
115
+ }
116
+ return result
117
+ }
118
+ case 'AKS':
119
+ return {
120
+ title: 'Connection Timed Out',
121
+ hints: [...baseHints, 'If the endpoint is reachable, Azure credentials may need refresh.'],
122
+ authCommand: { label: 'If network access looks healthy, refresh Azure credentials:', command: 'az login' },
123
+ fallbackCommand: { label: 'If that does not work, refresh cluster credentials:', command: 'az aks get-credentials --name <cluster> --resource-group <rg>' },
124
+ }
125
+ default:
126
+ return null
127
+ }
128
+ }
129
+
77
130
  const errorHints: Record<string, { title: string; hints: string[] }> = {
78
131
  config: {
79
132
  title: 'No Kubeconfig Found',
@@ -101,6 +154,14 @@ const errorHints: Record<string, { title: string; hints: string[] }> = {
101
154
  'Confirm the cluster is running',
102
155
  ],
103
156
  },
157
+ tls: {
158
+ title: 'Certificate Error',
159
+ hints: [
160
+ 'Radar reached the Kubernetes API, but could not verify its TLS certificate',
161
+ 'Check the kubeconfig cluster server hostname and certificate-authority settings',
162
+ 'If this cluster intentionally uses a private CA, refresh the kubeconfig for this context',
163
+ ],
164
+ },
104
165
  timeout: {
105
166
  title: 'Connection Timed Out',
106
167
  hints: [
@@ -122,6 +183,7 @@ const errorHints: Record<string, { title: string; hints: string[] }> = {
122
183
 
123
184
  function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunInTerminal?: (command: string) => void }) {
124
185
  const [copied, setCopied] = useState(false)
186
+ const commandParts = command.split(/(\s+)/)
125
187
 
126
188
  const handleCopy = () => {
127
189
  navigator.clipboard.writeText(command).then(() => {
@@ -134,30 +196,36 @@ function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunI
134
196
 
135
197
  return (
136
198
  <div className="mt-2 flex items-center gap-2 bg-theme-elevated border border-theme-border rounded-md px-3 py-2 group">
137
- <code className="text-xs font-mono text-theme-text-primary flex-1 select-all break-all">
138
- {command}
199
+ <code className="text-xs font-mono text-theme-text-primary flex-1 min-w-0 select-all whitespace-pre-wrap break-normal">
200
+ {commandParts.map((part, index) => (
201
+ /\s+/.test(part) ? part : <span key={index} className="inline-block whitespace-nowrap">{part}</span>
202
+ ))}
139
203
  </code>
140
204
  {onRunInTerminal && (
141
205
  <Tooltip content="Run in terminal" wrapperClassName="shrink-0">
206
+ <button
207
+ type="button"
208
+ onClick={() => onRunInTerminal(command)}
209
+ aria-label="Run command in terminal"
210
+ className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
211
+ >
212
+ <TerminalSquare className="w-3.5 h-3.5" />
213
+ </button>
214
+ </Tooltip>
215
+ )}
216
+ <Tooltip content="Copy to clipboard" wrapperClassName="shrink-0">
142
217
  <button
143
- onClick={() => onRunInTerminal(command)}
218
+ type="button"
219
+ onClick={handleCopy}
220
+ aria-label="Copy command to clipboard"
144
221
  className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
145
222
  >
146
- <TerminalSquare className="w-3.5 h-3.5" />
223
+ {copied ? (
224
+ <Check className="w-3.5 h-3.5 text-green-400" />
225
+ ) : (
226
+ <Copy className="w-3.5 h-3.5" />
227
+ )}
147
228
  </button>
148
- </Tooltip>
149
- )}
150
- <Tooltip content="Copy to clipboard" wrapperClassName="shrink-0">
151
- <button
152
- onClick={handleCopy}
153
- className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
154
- >
155
- {copied ? (
156
- <Check className="w-3.5 h-3.5 text-green-400" />
157
- ) : (
158
- <Copy className="w-3.5 h-3.5" />
159
- )}
160
- </button>
161
229
  </Tooltip>
162
230
  </div>
163
231
  )
@@ -166,10 +234,21 @@ function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunI
166
234
  export function ConnectionErrorView({ connection, onRetry, isRetrying }: ConnectionErrorViewProps) {
167
235
  // For auth errors, generate context-aware hints with a specific re-auth command
168
236
  const isAuth = connection.errorType === 'auth'
169
- const authInfo = isAuth ? getAuthHints(connection.context || '') : null
170
- const errorInfo = authInfo || errorHints[connection.errorType || 'unknown'] || errorHints.unknown
237
+ const isTimeout = connection.errorType === 'timeout'
238
+ const commandInfo = isAuth
239
+ ? getAuthHints(connection.context || '')
240
+ : isTimeout
241
+ ? getTimeoutHints(connection.context || '')
242
+ : null
243
+ const errorInfo = commandInfo || errorHints[connection.errorType || 'unknown'] || errorHints.unknown
171
244
  const openLocalTerminal = useOpenLocalTerminal()
172
245
  const { data: authMe } = useAuthMe()
246
+ const rawErrorDefaultOpen = !connection.errorType || connection.errorType === 'unknown'
247
+ const [showRawError, setShowRawError] = useState(rawErrorDefaultOpen)
248
+
249
+ useEffect(() => {
250
+ setShowRawError(rawErrorDefaultOpen)
251
+ }, [connection.error, rawErrorDefaultOpen])
173
252
 
174
253
  // Auto-retry after successful auth. The terminal shell runs on the server
175
254
  // host, so the auth command itself fixes the server's credentials in every
@@ -179,10 +258,10 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
179
258
  const retryCmd = `curl -s -X POST http://${window.location.host}/api/connection/retry > /dev/null`
180
259
 
181
260
  const handleAuthInTerminal = () => {
182
- if (!authInfo?.authCommand) return
261
+ if (!commandInfo?.authCommand) return
183
262
  const cmd = authMe?.authEnabled === false
184
- ? `${authInfo.authCommand.command} && ${retryCmd}`
185
- : authInfo.authCommand.command
263
+ ? `${commandInfo.authCommand.command} && ${retryCmd}`
264
+ : commandInfo.authCommand.command
186
265
  openLocalTerminal({
187
266
  initialCommand: cmd,
188
267
  title: 'Auth',
@@ -194,32 +273,34 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
194
273
  }
195
274
 
196
275
  return (
197
- <div className="flex-1 flex items-start justify-center pt-16 px-8">
198
- <div className="max-w-lg w-full">
276
+ <div className="flex-1 flex items-start justify-center pt-12 px-8">
277
+ <div className="max-w-xl w-full">
199
278
  <div className="flex flex-col items-center text-center">
200
- <div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center mb-6">
201
- <XCircle className="w-10 h-10 text-red-400" />
279
+ <div className="w-14 h-14 rounded-full bg-red-500/10 flex items-center justify-center mb-5">
280
+ <ServerOff className="w-8 h-8 text-red-400" />
202
281
  </div>
203
282
 
204
283
  <h2 className="text-xl font-semibold text-theme-text-primary mb-2">
205
284
  {connection.errorType === 'config' ? 'No Cluster Configuration' : 'Cannot Connect to Cluster'}
206
285
  </h2>
207
286
 
208
- <p className="text-sm text-theme-text-secondary mb-1 inline-flex items-center gap-1.5">
209
- Context: {connection.context ? (
210
- <ClusterName name={connection.context} />
211
- ) : (
212
- <span className="inline-code">(none)</span>
213
- )}
214
- </p>
215
-
216
- {connection.clusterName && (
217
- <p className="text-sm text-theme-text-secondary mb-4">
218
- Cluster: <span className="inline-code">{connection.clusterName}</span>
287
+ <div className="mb-6 space-y-1">
288
+ <p className="text-sm text-theme-text-secondary inline-flex items-center gap-1.5">
289
+ Context: {connection.context ? (
290
+ <ClusterName name={connection.context} />
291
+ ) : (
292
+ <span className="inline-code">(none)</span>
293
+ )}
219
294
  </p>
220
- )}
221
295
 
222
- <div className="w-full bg-theme-surface border border-theme-border rounded-lg p-4 mb-6 text-left">
296
+ {connection.clusterName && (
297
+ <p className="text-sm text-theme-text-secondary">
298
+ Cluster: <span className="inline-code">{connection.clusterName}</span>
299
+ </p>
300
+ )}
301
+ </div>
302
+
303
+ <div className="w-full bg-theme-surface border border-theme-border rounded-lg p-4 mb-5 text-left">
223
304
  <h3 className="text-sm font-medium text-theme-text-primary mb-2">
224
305
  {errorInfo.title}
225
306
  </h3>
@@ -231,35 +312,52 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
231
312
  </li>
232
313
  ))}
233
314
  </ul>
234
- {authInfo?.authCommand && (
315
+ {commandInfo?.authCommand && (
235
316
  <div className="mt-3">
236
- <p className="text-xs text-theme-text-tertiary">{authInfo.authCommand.label}</p>
237
- <CopyableCommand command={authInfo.authCommand.command} onRunInTerminal={handleRunInTerminal} />
238
- <button
239
- onClick={handleAuthInTerminal}
240
- className="mt-3 w-full inline-flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium btn-brand rounded-md"
241
- >
242
- <TerminalSquare className="w-3.5 h-3.5" />
243
- Authenticate in terminal
244
- </button>
317
+ <p className="text-xs text-theme-text-tertiary">{commandInfo.authCommand.label}</p>
318
+ <CopyableCommand command={commandInfo.authCommand.command} onRunInTerminal={handleRunInTerminal} />
319
+ {isAuth && (
320
+ <button
321
+ onClick={handleAuthInTerminal}
322
+ className="mt-3 w-full inline-flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium btn-brand rounded-md"
323
+ >
324
+ <TerminalSquare className="w-3.5 h-3.5" />
325
+ Authenticate in terminal
326
+ </button>
327
+ )}
328
+ </div>
329
+ )}
330
+ {commandInfo?.fallbackCommand && (
331
+ <div className="mt-4 pt-3 border-t border-theme-border/50">
332
+ <p className="text-xs text-theme-text-tertiary">{commandInfo.fallbackCommand.label}</p>
333
+ <CopyableCommand command={commandInfo.fallbackCommand.command} onRunInTerminal={handleRunInTerminal} />
245
334
  </div>
246
335
  )}
247
- {authInfo?.fallbackCommand && (
336
+ {connection.error && (
248
337
  <div className="mt-4 pt-3 border-t border-theme-border/50">
249
- <p className="text-xs text-theme-text-tertiary">{authInfo.fallbackCommand.label}</p>
250
- <CopyableCommand command={authInfo.fallbackCommand.command} onRunInTerminal={handleRunInTerminal} />
338
+ <button
339
+ type="button"
340
+ aria-expanded={showRawError}
341
+ aria-controls="connection-raw-error"
342
+ onClick={() => setShowRawError((open) => !open)}
343
+ className="flex items-center gap-1 text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
344
+ >
345
+ <ChevronRight className={`h-3.5 w-3.5 transition-transform duration-200 ${showRawError ? 'rotate-90' : ''}`} />
346
+ Raw error
347
+ </button>
348
+ <div className={`issue-details-motion ${showRawError ? 'issue-details-motion-open' : ''}`}>
349
+ <div className="overflow-hidden">
350
+ <div id="connection-raw-error" className="mt-2 bg-theme-elevated border border-theme-border rounded-md p-3 overflow-auto max-h-32">
351
+ <code className="text-xs text-theme-text-tertiary font-mono whitespace-pre-wrap break-words">
352
+ {connection.error}
353
+ </code>
354
+ </div>
355
+ </div>
356
+ </div>
251
357
  </div>
252
358
  )}
253
359
  </div>
254
360
 
255
- {connection.error && (
256
- <div className="w-full bg-theme-elevated border border-theme-border rounded-lg p-3 mb-6 overflow-auto max-h-32">
257
- <code className="text-xs text-red-400 font-mono whitespace-pre-wrap break-words">
258
- {connection.error}
259
- </code>
260
- </div>
261
- )}
262
-
263
361
  <div className="flex items-center gap-5">
264
362
  <button
265
363
  onClick={onRetry}
@@ -279,8 +377,14 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
279
377
  )}
280
378
  </button>
281
379
 
282
- {connection.errorType !== 'config' && <ContextSwitcher />}
380
+ {connection.errorType !== 'config' && <ContextSwitcher triggerName="Switch context" />}
283
381
  </div>
382
+
383
+ {isAuth && (
384
+ <p className="mt-4 text-xs text-theme-text-tertiary">
385
+ Radar will keep retrying after credentials are refreshed.
386
+ </p>
387
+ )}
284
388
  </div>
285
389
  </div>
286
390
  </div>
@@ -16,6 +16,7 @@ interface ContextSwitcherProps {
16
16
  className?: string
17
17
  variant?: 'chip' | 'segment'
18
18
  label?: string
19
+ triggerName?: string
19
20
  }
20
21
 
21
22
  export interface ContextSwitcherHandle {
@@ -26,7 +27,12 @@ interface ParsedContext extends ParsedContextName {
26
27
  context: ContextInfo
27
28
  }
28
29
 
29
- export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcherProps>(({ className = '', variant, label }, ref) => {
30
+ function shouldSuppressSwitchErrorToast(error: unknown): boolean {
31
+ const message = error instanceof Error ? error.message : ''
32
+ return message.includes('cluster connection failed:')
33
+ }
34
+
35
+ export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcherProps>(({ className = '', variant, label, triggerName }, ref) => {
30
36
  const [showConfirm, setShowConfirm] = useState(false)
31
37
  const [pendingSwitch, setPendingSwitch] = useState<ParsedContext | null>(null)
32
38
  const [sessionCounts, setSessionCounts] = useState<SessionCounts | null>(null)
@@ -117,8 +123,10 @@ export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcher
117
123
  endSwitch()
118
124
  // Backend may not transition to StateDisconnected on client-side errors
119
125
  // (network, timeout) — without this toast the user gets no feedback.
120
- const message = error instanceof Error ? error.message : 'Unknown error'
121
- showError('Failed to switch context', message)
126
+ if (!shouldSuppressSwitchErrorToast(error)) {
127
+ const message = error instanceof Error ? error.message : 'Unknown error'
128
+ showError('Failed to switch context', message)
129
+ }
122
130
  }
123
131
  }
124
132
 
@@ -185,8 +193,8 @@ export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcher
185
193
  // Fall back to clusterInfo.context for the very-early window before
186
194
  // /api/contexts has resolved.
187
195
  const currentParsed = currentId ? parsedById.get(currentId) : undefined
188
- const currentRaw = currentParsed?.raw || clusterInfo?.context || currentCtx?.name || 'Unknown'
189
- const currentSourceLabel = hasMultipleSources ? currentCtx?.source || undefined : undefined
196
+ const currentRaw = triggerName || currentParsed?.raw || clusterInfo?.context || currentCtx?.name || 'Unknown'
197
+ const currentSourceLabel = triggerName ? undefined : hasMultipleSources ? currentCtx?.source || undefined : undefined
190
198
 
191
199
  return (
192
200
  <>