@workbench-kit/react 0.0.2-prototype.0.2.2 → 0.0.2-prototype.0.2.20

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 (169) hide show
  1. package/README.md +125 -0
  2. package/package.json +27 -20
  3. package/src/index.ts +60 -29
  4. package/src/jdw/JsonCodeEditorPane.tsx +7 -6
  5. package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +1 -3
  6. package/src/jdw/createBuiltinJdwRegistry.ts +1 -4
  7. package/src/jdw/cssRenderBackend.tsx +1 -4
  8. package/src/json-config/JsonConfigWorkbench.tsx +1 -1
  9. package/src/layout/index.ts +11 -0
  10. package/src/layout/sidebar/SideBarTree.tsx +301 -0
  11. package/src/layout/sidebar/SidebarActionIconBar.tsx +3 -3
  12. package/src/layout/sidebar/WorkbenchSidebarActions.tsx +1 -1
  13. package/src/layout/sidebar/index.ts +13 -0
  14. package/src/modal/Modal.tsx +55 -25
  15. package/src/modal/ModalTitlebar.tsx +8 -8
  16. package/src/modal/modalPosition.ts +3 -6
  17. package/src/modal/useModalFocusTrap.ts +142 -0
  18. package/src/modal/useModalWindowFrame.ts +2 -2
  19. package/src/overlay/ContextMenu.tsx +112 -5
  20. package/src/overlay/context-menu.css +4 -2
  21. package/src/overlay/measureAnchoredOverlayPanel.ts +4 -6
  22. package/src/overlay/usePointerPassthroughRegion.ts +2 -0
  23. package/src/primitives/absolute-box/AbsoluteBox.tsx +4 -4
  24. package/src/primitives/button/Button.tsx +4 -4
  25. package/src/primitives/catalog-browse-card/CatalogBrowseCard.tsx +6 -6
  26. package/src/primitives/catalog-browse-pane/CatalogBrowsePane.tsx +15 -15
  27. package/src/primitives/catalog-filter-overlay/CatalogFilterOverlay.tsx +2 -2
  28. package/src/primitives/checkbox/Checkbox.tsx +1 -1
  29. package/src/primitives/chip/Chip.tsx +2 -2
  30. package/src/primitives/codicon/Codicon.tsx +1 -1
  31. package/src/primitives/file-icon/FileIcon.tsx +1 -1
  32. package/src/primitives/icon-button/IconButton.tsx +1 -1
  33. package/src/primitives/index.ts +5 -0
  34. package/src/primitives/library-detail-layout/LibraryDetailLayout.tsx +123 -9
  35. package/src/primitives/library-detail-layout/index.ts +5 -0
  36. package/src/primitives/library-detail-layout/library-detail-layout.css +57 -0
  37. package/src/primitives/library-detail-layout/resolve-hero-cover-media.ts +80 -0
  38. package/src/primitives/library-facet-filter-panel/LibraryFacetFilterPanel.tsx +12 -12
  39. package/src/primitives/library-facet-filter-strip/LibraryFacetFilterStrip.tsx +4 -4
  40. package/src/primitives/number-input/NumberInput.tsx +5 -5
  41. package/src/primitives/record-media-hero/RecordMediaHero.tsx +10 -6
  42. package/src/primitives/scroll-area/ScrollArea.tsx +5 -5
  43. package/src/primitives/scroll-area-infinite-load/useScrollAreaInfiniteLoad.ts +5 -5
  44. package/src/primitives/searchable-multi-select/SearchableMultiSelect.tsx +7 -7
  45. package/src/primitives/select/Select.tsx +1 -13
  46. package/src/primitives/text-area/TextArea.tsx +1 -1
  47. package/src/primitives/text-input/TextInput.tsx +1 -1
  48. package/src/primitives/workbench-media-slot/WorkbenchMediaPlaceholder.tsx +2 -2
  49. package/src/primitives/workbench-media-slot/WorkbenchMediaSlot.tsx +16 -10
  50. package/src/primitives/workbench-thumbnail/WorkbenchThumbnail.tsx +4 -4
  51. package/src/styles.css +1 -2
  52. package/src/test-utils/workbenchMonacoMock.tsx +20 -0
  53. package/src/utils/clamp.ts +4 -0
  54. package/src/utils/normalizeKeyToken.ts +13 -0
  55. package/src/utils/readNumber.ts +4 -0
  56. package/src/widget-tree/WidgetTreeCanvasPreview.tsx +1 -4
  57. package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +2 -2
  58. package/src/workbench/chat/ChatCommandProposalCard.tsx +4 -4
  59. package/src/workbench/chat/ChatConversationBar.tsx +3 -3
  60. package/src/workbench/chat/ChatHistoryMenu.tsx +1 -1
  61. package/src/workbench/chat/ChatMessageCollapsible.tsx +4 -0
  62. package/src/workbench/chat/ChatMessageItem.tsx +68 -10
  63. package/src/workbench/chat/ChatMessageList.tsx +1 -1
  64. package/src/workbench/chat/ChatPanel.tsx +32 -23
  65. package/src/workbench/chat/ChatPhasedRunProgress.tsx +86 -13
  66. package/src/workbench/chat/ChatRenameDialog.tsx +1 -1
  67. package/src/workbench/chat/chat-conversation.css +2 -2
  68. package/src/workbench/chat/chat-ui.css +8 -0
  69. package/src/workbench/chat/chatConversation.ts +2 -2
  70. package/src/workbench/chat/chatMessageMeta.tsx +4 -4
  71. package/src/workbench/chat/index.ts +1 -0
  72. package/src/workbench/chat/types.ts +7 -7
  73. package/src/workbench/chat/useCancelRuntimeOnUnmount.ts +4 -4
  74. package/src/workbench/chat/useChatPrependPagination.ts +10 -10
  75. package/src/workbench/chat/useSlashCommandSuggest.ts +1 -1
  76. package/src/workbench/{WorkbenchModalPortal.tsx → chrome/WorkbenchModalPortal.tsx} +1 -1
  77. package/src/workbench/chrome/workbench-chrome.css +4 -0
  78. package/src/workbench/{CommandGroupShell.tsx → commands/CommandGroupShell.tsx} +2 -2
  79. package/src/workbench/{CommandList.tsx → commands/CommandList.tsx} +5 -5
  80. package/src/workbench/{CommandPalette.tsx → commands/CommandPalette.tsx} +16 -42
  81. package/src/workbench/{ConfirmationFlow.tsx → commands/ConfirmationFlow.tsx} +5 -5
  82. package/src/workbench/{ShortcutCommandBridge.tsx → commands/ShortcutCommandBridge.tsx} +1 -13
  83. package/src/workbench/commands/WorkbenchQuickOpen.tsx +462 -0
  84. package/src/workbench/{command-model.ts → commands/command-model.ts} +5 -1
  85. package/src/workbench/commands/command-ui.ts +3 -0
  86. package/src/workbench/{commands.ts → commands/commands.ts} +1 -1
  87. package/src/workbench/commands/createWorkspaceFilesQuickOpenProvider.ts +86 -0
  88. package/src/workbench/commands/quick-open-model.ts +66 -0
  89. package/src/workbench/{WorkbenchEditorTabs.tsx → editor/WorkbenchEditorTabs.tsx} +1 -1
  90. package/src/workbench/{editor-tabs-dnd.ts → editor/editor-tabs-dnd.ts} +1 -1
  91. package/src/workbench/{editorTabContextMenu.ts → editor/editorTabContextMenu.ts} +2 -2
  92. package/src/workbench/{useEditorTabsStripDnd.ts → editor/useEditorTabsStripDnd.ts} +1 -1
  93. package/src/workbench/{useWorkbenchEditorTabContextMenu.tsx → editor/useWorkbenchEditorTabContextMenu.tsx} +1 -1
  94. package/src/workbench/editor-tabs/index.ts +9 -9
  95. package/src/workbench/index.ts +110 -74
  96. package/src/workbench/management/ExtensionManagementPanel.tsx +32 -36
  97. package/src/workbench/management/ExtensionManagementSidebar.tsx +19 -35
  98. package/src/workbench/management/IntegrationSettingsSurface.tsx +2 -5
  99. package/src/workbench/management/IntegrationsShell.tsx +1 -1
  100. package/src/workbench/management/KeybindingCaptureField.tsx +1 -9
  101. package/src/workbench/management/WorkbenchDialogFrame.tsx +2 -2
  102. package/src/workbench/management/WorkbenchNotice.tsx +2 -5
  103. package/src/workbench/management/createWorkbenchNotify.tsx +88 -0
  104. package/src/workbench/management/extension-install-approval.ts +72 -0
  105. package/src/workbench/management/extension-management-filters.ts +38 -0
  106. package/src/workbench/management/index.ts +14 -0
  107. package/src/workbench/management/management.css +28 -0
  108. package/src/workbench/management/types.ts +14 -1
  109. package/src/workbench/management/useWorkbenchNotify.ts +10 -0
  110. package/src/workbench/{MarkdownPreview.tsx → markdown/MarkdownPreview.tsx} +22 -7
  111. package/src/workbench/markdown/markdownRemarkPlugins.ts +12 -0
  112. package/src/workbench/markdown/sanitizeMarkdownHref.ts +29 -0
  113. package/src/workbench/settings/StructuredDataForm.tsx +1 -4
  114. package/src/workbench/settings/structuredDataFormModel.ts +2 -1
  115. package/src/workbench/{ActivityBar.tsx → shell/ActivityBar.tsx} +8 -8
  116. package/src/workbench/{ArtifactShell.tsx → shell/ArtifactShell.tsx} +25 -25
  117. package/src/workbench/{SplitView.tsx → shell/SplitView.tsx} +3 -3
  118. package/src/workbench/{StatusBar.tsx → shell/StatusBar.tsx} +8 -8
  119. package/src/workbench/{StructuredArtifactEditor.tsx → shell/StructuredArtifactEditor.tsx} +9 -9
  120. package/src/workbench/{Timeline.tsx → shell/Timeline.tsx} +77 -20
  121. package/src/workbench/{WorkbenchCanvasShell.defaults.ts → shell/WorkbenchCanvasShell.defaults.ts} +1 -1
  122. package/src/workbench/{WorkbenchCanvasShell.model.ts → shell/WorkbenchCanvasShell.model.ts} +5 -5
  123. package/src/workbench/{WorkbenchCanvasShell.tsx → shell/WorkbenchCanvasShell.tsx} +6 -6
  124. package/src/workbench/{WorkbenchDesktopTitleBar.tsx → shell/WorkbenchDesktopTitleBar.tsx} +15 -15
  125. package/src/workbench/{WorkbenchShell.tsx → shell/WorkbenchShell.tsx} +45 -6
  126. package/src/workbench/{WorkbenchShellTitleBarLayoutControls.tsx → shell/WorkbenchShellTitleBarLayoutControls.tsx} +16 -19
  127. package/src/workbench/{WorkbenchStandaloneShell.tsx → shell/WorkbenchStandaloneShell.tsx} +4 -4
  128. package/src/workbench/{WorkbenchViewEditor.tsx → shell/WorkbenchViewEditor.tsx} +7 -7
  129. package/src/workbench/{WorkbenchViewSidebar.tsx → shell/WorkbenchViewSidebar.tsx} +7 -7
  130. package/src/workbench/{WorkbenchWindowChromeControls.tsx → shell/WorkbenchWindowChromeControls.tsx} +18 -15
  131. package/src/workbench/shell/activity-bar.css +47 -0
  132. package/src/workbench/{shell.ts → shell/shell.ts} +18 -12
  133. package/src/workbench/{shellViewModel.ts → shell/shellViewModel.ts} +3 -3
  134. package/src/workbench/{standalone.ts → shell/standalone.ts} +14 -2
  135. package/src/workbench/{useDeferredWorkbenchMount.ts → shell/useDeferredWorkbenchMount.ts} +3 -3
  136. package/src/workbench/{useWorkbenchAppearanceDocumentSync.tsx → shell/useWorkbenchAppearanceDocumentSync.tsx} +1 -1
  137. package/src/workbench/{useWorkbenchSidebarActionBarDnd.ts → shell/useWorkbenchSidebarActionBarDnd.ts} +1 -1
  138. package/src/workbench/{useWorkbenchSidebarViewPlacementDropZone.ts → shell/useWorkbenchSidebarViewPlacementDropZone.ts} +2 -2
  139. package/src/workbench/{workbenchViewRouteState.ts → shell/workbenchViewRouteState.ts} +4 -4
  140. package/src/workbench/{WorkbenchThemeProvider.tsx → theme/WorkbenchThemeProvider.tsx} +4 -4
  141. package/src/workbench/{theme.ts → theme/theme.ts} +1 -1
  142. package/src/workbench/{themePresets.ts → theme/themePresets.ts} +3 -2
  143. package/src/workbench/views/view-hosts.css +3 -3
  144. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +1 -1
  145. package/src/workbench/workspace/WorkspaceExplorer.tsx +104 -3
  146. package/src/workbench/workspace/workspaceExplorerKeyboard.ts +109 -0
  147. package/src/workbench/workspace/workspaceJsonDiagnostics.ts +6 -5
  148. package/src/workbench/jdw/jdw-editors.css +0 -4
  149. package/src/workbench/jdw/jdw.css +0 -2
  150. package/src/workbench/markdownRemarkPlugins.ts +0 -4
  151. package/src/workbench/workbench-chrome.css +0 -4
  152. /package/src/workbench/{WorkbenchPlatformContext.tsx → chrome/WorkbenchPlatformContext.tsx} +0 -0
  153. /package/src/workbench/{workbenchModalViewState.ts → chrome/workbenchModalViewState.ts} +0 -0
  154. /package/src/workbench/{workbenchOverlaysContext.tsx → chrome/workbenchOverlaysContext.tsx} +0 -0
  155. /package/src/workbench/{workbenchPlatformChrome.ts → chrome/workbenchPlatformChrome.ts} +0 -0
  156. /package/src/workbench/{command-execution-policy.ts → commands/command-execution-policy.ts} +0 -0
  157. /package/src/workbench/{keyboard.ts → commands/keyboard.ts} +0 -0
  158. /package/src/workbench/{workbenchContextMenu.ts → commands/workbenchContextMenu.ts} +0 -0
  159. /package/src/workbench/{activityBarOrder.ts → shell/activityBarOrder.ts} +0 -0
  160. /package/src/workbench/{appearanceLabels.ts → shell/appearanceLabels.ts} +0 -0
  161. /package/src/workbench/{hardReset.ts → shell/hardReset.ts} +0 -0
  162. /package/src/workbench/{shellPresets.ts → shell/shellPresets.ts} +0 -0
  163. /package/src/workbench/{shellState.ts → shell/shellState.ts} +0 -0
  164. /package/src/workbench/{sidebarViewPlacementDnd.ts → shell/sidebarViewPlacementDnd.ts} +0 -0
  165. /package/src/workbench/{status.ts → shell/status.ts} +0 -0
  166. /package/src/workbench/{useWorkbenchSidebarLayout.tsx → shell/useWorkbenchSidebarLayout.tsx} +0 -0
  167. /package/src/workbench/{useWorkbenchStandaloneShellStateSync.tsx → shell/useWorkbenchStandaloneShellStateSync.tsx} +0 -0
  168. /package/src/workbench/{workbenchSidebarLayoutContext.ts → shell/workbenchSidebarLayoutContext.ts} +0 -0
  169. /package/src/workbench/{workbenchStandaloneShellReactContext.tsx → shell/workbenchStandaloneShellReactContext.tsx} +0 -0
@@ -7,6 +7,7 @@ import {
7
7
  WorkbenchPropertySection,
8
8
  WorkbenchPropertyStack,
9
9
  } from '../../primitives/index';
10
+ import { cx } from '../../utils/cx';
10
11
 
11
12
  type IntegrationSettingsDensity = 'compact' | 'default';
12
13
  type IntegrationSettingsInset = 'default' | 'flush';
@@ -206,7 +207,7 @@ export function IntegrationAccountRowEditor({
206
207
  export function IntegrationBodyText({ children, size = 'default' }: IntegrationBodyTextProps) {
207
208
  return (
208
209
  <WorkbenchPropertyHint
209
- className={joinClasses('block', size === 'compact' ? 'text-xs' : undefined)}
210
+ className={cx('block', size === 'compact' ? 'text-xs' : undefined)}
210
211
  data-size={size}
211
212
  >
212
213
  {children}
@@ -284,7 +285,3 @@ function IntegrationEditorDescription({ children }: { children?: ReactNode }) {
284
285
  function hasRenderableNode(node: ReactNode | undefined): boolean {
285
286
  return node !== undefined && node !== null && node !== false;
286
287
  }
287
-
288
- function joinClasses(...classes: Array<string | undefined>): string {
289
- return classes.filter(Boolean).join(' ');
290
- }
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import { PanelHeader } from '../../layout/panel';
3
3
  import { WorkbenchEditorFrame, WorkbenchPanelScroll } from '../../layout/WorkbenchLayoutBase';
4
4
  import { WorkbenchPropertyHint } from '../../layout/WorkbenchPropertyPanel';
5
- import { SplitView } from '../SplitView';
5
+ import { SplitView } from '../shell/SplitView';
6
6
 
7
7
  export interface IntegrationsShellProps {
8
8
  children: ReactNode;
@@ -2,6 +2,7 @@ import { useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';
2
2
  import { formatKeybindingLabel } from '@workbench-kit/platform';
3
3
  import { Button } from '../../primitives/button';
4
4
  import { cx } from '../../utils/cx';
5
+ import { normalizeKeyToken } from '../../utils/normalizeKeyToken';
5
6
 
6
7
  export interface KeybindingCaptureFieldProps {
7
8
  ariaLabel?: string | undefined;
@@ -131,12 +132,3 @@ function normalizeKeybindingKeyFromEvent(
131
132
  parts.push(normalizeKeyToken(event.key));
132
133
  return parts.join('+');
133
134
  }
134
-
135
- function normalizeKeyToken(token: string): string {
136
- const key = token.trim().toLowerCase();
137
- if (key === 'del') return 'delete';
138
- if (key === 'esc') return 'escape';
139
- if (key === 'return') return 'enter';
140
- if (key === 'spacebar' || key === 'space') return 'space';
141
- return key.length === 1 ? key : key;
142
- }
@@ -1,7 +1,7 @@
1
1
  import type { FormEventHandler, ReactNode } from 'react';
2
2
  import { Modal, type ModalBodyLayout, type ModalBodyPadding } from '../../modal/Modal';
3
- import type { WorkbenchWindowChromeMode } from '../workbenchPlatformChrome';
4
- import { WorkbenchModalPortal } from '../WorkbenchModalPortal';
3
+ import type { WorkbenchWindowChromeMode } from '../chrome/workbenchPlatformChrome';
4
+ import { WorkbenchModalPortal } from '../chrome/WorkbenchModalPortal';
5
5
 
6
6
  export type WorkbenchDialogBodyLayout = 'column-fill' | 'default' | 'padded-fill';
7
7
  export type WorkbenchDialogFrameSize =
@@ -9,6 +9,7 @@ import {
9
9
  type ReactNode,
10
10
  } from 'react';
11
11
  import { IconButton, WorkbenchBanner, WorkbenchBannerMessage } from '../../primitives/index';
12
+ import { cx } from '../../utils/cx';
12
13
 
13
14
  export type WorkbenchNoticeTone = 'error' | 'info' | 'success' | 'warning';
14
15
  export type WorkbenchNoticePosition = 'bottom-center' | 'bottom-right';
@@ -187,7 +188,7 @@ export function useWorkbenchNotice(): WorkbenchNoticeController {
187
188
  }
188
189
 
189
190
  function resolveNoticeViewportClassName(position: WorkbenchNoticePosition): string {
190
- return joinClasses(
191
+ return cx(
191
192
  'pointer-events-none fixed inset-x-0 bottom-4 z-50 flex flex-col gap-2 px-4',
192
193
  position === 'bottom-center' ? 'items-center' : 'items-end',
193
194
  );
@@ -205,7 +206,3 @@ function resolveNoticeBannerStyle(tone: WorkbenchNoticeTone): CSSProperties {
205
206
  minWidth: 'min(420px, calc(100vw - 32px))',
206
207
  };
207
208
  }
208
-
209
- function joinClasses(...classes: Array<string | undefined>): string {
210
- return classes.filter(Boolean).join(' ');
211
- }
@@ -0,0 +1,88 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import type {
4
+ ShowWorkbenchNoticeInput,
5
+ WorkbenchNoticeController,
6
+ WorkbenchNoticeTone,
7
+ } from './WorkbenchNotice.js';
8
+
9
+ export interface WorkbenchNotificationAction {
10
+ readonly label: string;
11
+ readonly onAction: () => void;
12
+ }
13
+
14
+ export interface WorkbenchNotifyOptions {
15
+ readonly actions?: readonly WorkbenchNotificationAction[];
16
+ readonly dataAttributes?: ShowWorkbenchNoticeInput['dataAttributes'];
17
+ readonly durationMs?: number;
18
+ readonly id?: string;
19
+ }
20
+
21
+ /**
22
+ * NotificationService-shaped facade over {@link WorkbenchNoticeController}.
23
+ * Prefer this for command/install/save feedback; keep modal confirms separate.
24
+ */
25
+ export interface WorkbenchNotify {
26
+ info(message: ReactNode, options?: WorkbenchNotifyOptions): string;
27
+ error(message: ReactNode, options?: WorkbenchNotifyOptions): string;
28
+ success(message: ReactNode, options?: WorkbenchNotifyOptions): string;
29
+ warning(message: ReactNode, options?: WorkbenchNotifyOptions): string;
30
+ dismiss(id: string): void;
31
+ clear(): void;
32
+ }
33
+
34
+ function showTone(
35
+ controller: WorkbenchNoticeController,
36
+ tone: WorkbenchNoticeTone,
37
+ message: ReactNode,
38
+ options: WorkbenchNotifyOptions = {},
39
+ ): string {
40
+ const { actions, dataAttributes, durationMs, id } = options;
41
+ const noticeId = id ?? `workbench-notify-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
42
+
43
+ const noticeMessage =
44
+ actions && actions.length > 0 ? (
45
+ <span className="ui-workbench-notify-message">
46
+ <span className="ui-workbench-notify-message__text">{message}</span>
47
+ <span className="ui-workbench-notify-message__actions">
48
+ {actions.map((action) => (
49
+ <button
50
+ className="ui-workbench-notify-message__action"
51
+ key={action.label}
52
+ type="button"
53
+ onClick={() => {
54
+ action.onAction();
55
+ controller.dismissNotice(noticeId);
56
+ }}
57
+ >
58
+ {action.label}
59
+ </button>
60
+ ))}
61
+ </span>
62
+ </span>
63
+ ) : (
64
+ message
65
+ );
66
+
67
+ return controller.showNotice({
68
+ ...(dataAttributes === undefined ? {} : { dataAttributes }),
69
+ ...(durationMs === undefined ? {} : { durationMs }),
70
+ id: noticeId,
71
+ message: noticeMessage,
72
+ tone,
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Create a `notify.info` / `notify.error` facade over an existing notice controller.
78
+ */
79
+ export function createWorkbenchNotify(controller: WorkbenchNoticeController): WorkbenchNotify {
80
+ return {
81
+ info: (message, options) => showTone(controller, 'info', message, options),
82
+ error: (message, options) => showTone(controller, 'error', message, options),
83
+ success: (message, options) => showTone(controller, 'success', message, options),
84
+ warning: (message, options) => showTone(controller, 'warning', message, options),
85
+ dismiss: (noticeId) => controller.dismissNotice(noticeId),
86
+ clear: () => controller.clearNotices(),
87
+ };
88
+ }
@@ -0,0 +1,72 @@
1
+ import type { ExtensionCatalogBrowseEntry, ExtensionInstallOptions } from './types.js';
2
+
3
+ export function extensionInstallRequiresApproval(entry: ExtensionCatalogBrowseEntry): boolean {
4
+ return Boolean(
5
+ entry.installPlan?.requiresApproval && !entry.installed && !entry.installPlan.blocked,
6
+ );
7
+ }
8
+
9
+ export function formatExtensionInstallApprovalMessage(entry: ExtensionCatalogBrowseEntry): string {
10
+ const permissions = entry.installPlan?.permissions ?? [];
11
+ const permissionLine =
12
+ permissions.length > 0
13
+ ? `Requested permissions: ${permissions.join(', ')}.`
14
+ : 'This package requests privileges that require approval.';
15
+
16
+ return [
17
+ `Install "${entry.displayName}"?`,
18
+ '',
19
+ permissionLine,
20
+ 'Continue only if you trust this package.',
21
+ ].join('\n');
22
+ }
23
+
24
+ export interface ResolveExtensionInstallOptionsInput {
25
+ readonly confirm?: ((message: string) => boolean) | undefined;
26
+ /**
27
+ * When true (or when the predicate returns true), skip the confirm prompt and
28
+ * return `{ approved: true }`. Hosts typically back this with a durable trust
29
+ * store keyed by extension id + permission fingerprint.
30
+ */
31
+ readonly isTrusted?: boolean | ((entry: ExtensionCatalogBrowseEntry) => boolean) | undefined;
32
+ /** Called after the user explicitly confirms trust for this install. */
33
+ readonly rememberTrust?: ((entry: ExtensionCatalogBrowseEntry) => void) | undefined;
34
+ }
35
+
36
+ /**
37
+ * Resolve install options for UI actions.
38
+ *
39
+ * Approval is required when the install plan requests permissions. A trusted
40
+ * predicate may skip the prompt; otherwise the host confirm dialog runs and
41
+ * successful consent may be persisted via `rememberTrust`.
42
+ */
43
+ export function resolveExtensionInstallOptions(
44
+ entry: ExtensionCatalogBrowseEntry,
45
+ confirmOrOptions: ((message: string) => boolean) | ResolveExtensionInstallOptionsInput = (
46
+ message,
47
+ ) => (typeof globalThis.confirm === 'function' ? globalThis.confirm(message) : false),
48
+ ): ExtensionInstallOptions | undefined {
49
+ if (!extensionInstallRequiresApproval(entry)) {
50
+ return {};
51
+ }
52
+
53
+ const options =
54
+ typeof confirmOrOptions === 'function' ? { confirm: confirmOrOptions } : confirmOrOptions;
55
+ const confirm =
56
+ options.confirm ??
57
+ ((message: string) =>
58
+ typeof globalThis.confirm === 'function' ? globalThis.confirm(message) : false);
59
+
60
+ const trusted =
61
+ typeof options.isTrusted === 'function' ? options.isTrusted(entry) : Boolean(options.isTrusted);
62
+ if (trusted) {
63
+ return { approved: true };
64
+ }
65
+
66
+ if (!confirm(formatExtensionInstallApprovalMessage(entry))) {
67
+ return undefined;
68
+ }
69
+
70
+ options.rememberTrust?.(entry);
71
+ return { approved: true };
72
+ }
@@ -0,0 +1,38 @@
1
+ import type { ExtensionCatalogBrowseEntry, ExtensionManagementEntry } from './types.js';
2
+
3
+ export function filterInstalledEntries(
4
+ entries: readonly ExtensionManagementEntry[],
5
+ query: string,
6
+ ): readonly ExtensionManagementEntry[] {
7
+ const normalized = query.trim().toLowerCase();
8
+ if (!normalized) {
9
+ return entries;
10
+ }
11
+
12
+ return entries.filter((entry) =>
13
+ [entry.displayName, entry.id, entry.category, entry.description ?? '']
14
+ .join(' ')
15
+ .toLowerCase()
16
+ .includes(normalized),
17
+ );
18
+ }
19
+
20
+ export function filterBrowseEntries(
21
+ entries: readonly ExtensionCatalogBrowseEntry[],
22
+ query: string,
23
+ category?: string,
24
+ ): readonly ExtensionCatalogBrowseEntry[] {
25
+ const normalized = query.trim().toLowerCase();
26
+ return entries.filter((entry) => {
27
+ if (category && entry.category !== category) {
28
+ return false;
29
+ }
30
+ if (!normalized) {
31
+ return true;
32
+ }
33
+ return [entry.displayName, entry.id, entry.category, entry.description]
34
+ .join(' ')
35
+ .toLowerCase()
36
+ .includes(normalized);
37
+ });
38
+ }
@@ -75,6 +75,13 @@ export type {
75
75
  WorkbenchNoticePosition,
76
76
  WorkbenchNoticeTone,
77
77
  } from './WorkbenchNotice.js';
78
+ export { createWorkbenchNotify } from './createWorkbenchNotify.js';
79
+ export type {
80
+ WorkbenchNotificationAction,
81
+ WorkbenchNotify,
82
+ WorkbenchNotifyOptions,
83
+ } from './createWorkbenchNotify.js';
84
+ export { useWorkbenchNotify } from './useWorkbenchNotify.js';
78
85
  export { KeybindingCaptureField } from './KeybindingCaptureField.js';
79
86
  export type { KeybindingCaptureFieldProps } from './KeybindingCaptureField.js';
80
87
  export { KeybindingManagementPanel } from './KeybindingManagementPanel.js';
@@ -112,6 +119,12 @@ export {
112
119
  extensionCategoryIconTone,
113
120
  formatExtensionCategoryLabel,
114
121
  } from './extension-category-display.js';
122
+ export {
123
+ extensionInstallRequiresApproval,
124
+ formatExtensionInstallApprovalMessage,
125
+ resolveExtensionInstallOptions,
126
+ type ResolveExtensionInstallOptionsInput,
127
+ } from './extension-install-approval.js';
115
128
  export type {
116
129
  AccountManagementEntry,
117
130
  AccountManagementEntryStatus,
@@ -122,6 +135,7 @@ export type {
122
135
  CommandManagementPanelProps,
123
136
  CommandManagementRunState,
124
137
  ExtensionCatalogBrowseEntry,
138
+ ExtensionInstallOptions,
125
139
  ExtensionManagementDiagnosticSummary,
126
140
  ExtensionManagementEntry,
127
141
  ExtensionManagementFeatureItem,
@@ -2,3 +2,31 @@
2
2
  @import './library-catalog-picker-dialog.css';
3
3
  @import './library-facet-filter-dialog.css';
4
4
  @import './management-panel.css';
5
+
6
+ .ui-workbench-notify-message {
7
+ display: inline-flex;
8
+ flex-wrap: wrap;
9
+ gap: 8px;
10
+ align-items: center;
11
+ }
12
+
13
+ .ui-workbench-notify-message__actions {
14
+ display: inline-flex;
15
+ flex-wrap: wrap;
16
+ gap: 6px;
17
+ }
18
+
19
+ .ui-workbench-notify-message__action {
20
+ margin: 0;
21
+ padding: 2px 8px;
22
+ border: 1px solid var(--color-border, currentColor);
23
+ border-radius: var(--radius-sm, 4px);
24
+ background: transparent;
25
+ color: inherit;
26
+ font: inherit;
27
+ cursor: pointer;
28
+ }
29
+
30
+ .ui-workbench-notify-message__action:hover {
31
+ background: var(--color-surface-hover, rgba(255, 255, 255, 0.08));
32
+ }
@@ -115,12 +115,25 @@ export interface ExtensionCatalogBrowseEntry {
115
115
  readonly manifestUrl: string;
116
116
  }
117
117
 
118
+ export interface ExtensionInstallOptions {
119
+ /** Required when the install plan sets `requiresApproval`. */
120
+ readonly approved?: boolean | undefined;
121
+ }
122
+
118
123
  export interface ExtensionManagementPanelProps {
119
124
  browseEntries: readonly ExtensionCatalogBrowseEntry[];
120
125
  catalogError?: string | undefined;
121
126
  catalogLoading?: boolean | undefined;
122
127
  className?: string | undefined;
123
128
  installedEntries: readonly ExtensionManagementEntry[];
124
- onInstall?: ((entry: ExtensionCatalogBrowseEntry) => void) | undefined;
129
+ onInstall?:
130
+ ((entry: ExtensionCatalogBrowseEntry, options?: ExtensionInstallOptions) => void) | undefined;
125
131
  onToggleEnabled?: ((entry: ExtensionManagementEntry, enabled: boolean) => void) | undefined;
132
+ /**
133
+ * Host-owned durable trust check. When true for an entry, the install confirm
134
+ * prompt is skipped and `{ approved: true }` is used.
135
+ */
136
+ isInstallTrusted?: ((entry: ExtensionCatalogBrowseEntry) => boolean) | undefined;
137
+ /** Persist trust after the user confirms a permissioned install. */
138
+ onRememberInstallTrust?: ((entry: ExtensionCatalogBrowseEntry) => void) | undefined;
126
139
  }
@@ -0,0 +1,10 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { createWorkbenchNotify, type WorkbenchNotify } from './createWorkbenchNotify.js';
4
+ import { useWorkbenchNotice } from './WorkbenchNotice.js';
5
+
6
+ /** Convenience hook: NotificationService-shaped API inside a notice provider. */
7
+ export function useWorkbenchNotify(): WorkbenchNotify {
8
+ const controller = useWorkbenchNotice();
9
+ return useMemo(() => createWorkbenchNotify(controller), [controller]);
10
+ }
@@ -1,7 +1,11 @@
1
1
  import Markdown from 'react-markdown';
2
2
  import { Children, isValidElement, type ComponentPropsWithoutRef, type ReactNode } from 'react';
3
- import { cx } from '../utils/cx';
4
- import { workbenchMarkdownRemarkPlugins } from './markdownRemarkPlugins';
3
+ import { cx } from '../../utils/cx';
4
+ import {
5
+ workbenchMarkdownRemarkPlugins,
6
+ workbenchMarkdownRehypePlugins,
7
+ } from './markdownRemarkPlugins';
8
+ import { sanitizeMarkdownHref } from './sanitizeMarkdownHref';
5
9
 
6
10
  export interface WorkbenchMarkdownPreviewProps extends ComponentPropsWithoutRef<'article'> {
7
11
  source: string;
@@ -16,12 +20,23 @@ export function WorkbenchMarkdownPreview({
16
20
  <article className={cx('ui-workbench-markdown-preview', className)} {...props}>
17
21
  <Markdown
18
22
  remarkPlugins={workbenchMarkdownRemarkPlugins}
23
+ rehypePlugins={workbenchMarkdownRehypePlugins}
19
24
  components={{
20
- a: ({ children, href }) => (
21
- <a href={href} rel="noreferrer" target={href?.startsWith('#') ? undefined : '_blank'}>
22
- {children}
23
- </a>
24
- ),
25
+ a: ({ children, href }) => {
26
+ const safeHref = sanitizeMarkdownHref(href);
27
+ if (!safeHref) {
28
+ return <span>{children}</span>;
29
+ }
30
+ return (
31
+ <a
32
+ href={safeHref}
33
+ rel="noreferrer"
34
+ target={safeHref.startsWith('#') ? undefined : '_blank'}
35
+ >
36
+ {children}
37
+ </a>
38
+ );
39
+ },
25
40
  code: ({ children, className: codeClassName }) => (
26
41
  <code className={cx('ui-workbench-markdown-preview__code', codeClassName)}>
27
42
  {children}
@@ -0,0 +1,12 @@
1
+ import remarkGfm from 'remark-gfm';
2
+ import rehypeSanitize from 'rehype-sanitize';
3
+
4
+ /** Shared remark plugins for workbench markdown surfaces (preview, chat, etc.). */
5
+ export const workbenchMarkdownRemarkPlugins = [remarkGfm];
6
+
7
+ /**
8
+ * Default rehype pipeline for untrusted markdown (chat / preview).
9
+ * Strips script/event handlers and dangerous URLs via rehype-sanitize.
10
+ * Hosts that need raw HTML must render outside these kit surfaces.
11
+ */
12
+ export const workbenchMarkdownRehypePlugins = [rehypeSanitize];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Allowlist markdown link targets for safe preview rendering.
3
+ * Blocks javascript:/data:/vbscript: and other non-navigation schemes.
4
+ */
5
+ export function sanitizeMarkdownHref(href: string | undefined): string | undefined {
6
+ if (href == null || href.trim() === '') {
7
+ return undefined;
8
+ }
9
+
10
+ const trimmed = href.trim();
11
+ if (trimmed.startsWith('#')) {
12
+ return trimmed;
13
+ }
14
+
15
+ try {
16
+ const base =
17
+ typeof globalThis.location?.href === 'string' && globalThis.location.href.length > 0
18
+ ? globalThis.location.href
19
+ : 'https://workbench.local/';
20
+ const url = new URL(trimmed, base);
21
+ const protocol = url.protocol.toLowerCase();
22
+ if (protocol === 'http:' || protocol === 'https:' || protocol === 'mailto:') {
23
+ return trimmed;
24
+ }
25
+ return undefined;
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
@@ -37,6 +37,7 @@ import {
37
37
  type WorkbenchStructuredDataSchemaFieldInputProps,
38
38
  type WorkbenchStructuredDataTextArrayInputProps,
39
39
  } from './structuredDataSchema';
40
+ import { isRenderableWorkbenchStructuredDataFormError } from './structuredDataFormModel';
40
41
 
41
42
  export function WorkbenchStructuredDataForm({
42
43
  ariaLabel,
@@ -156,10 +157,6 @@ export function WorkbenchStructuredDataForm({
156
157
  );
157
158
  }
158
159
 
159
- function isRenderableWorkbenchStructuredDataFormError(error: ReactNode) {
160
- return error !== undefined && error !== null && error !== false && error !== '';
161
- }
162
-
163
160
  function StructuredDataSection({
164
161
  data,
165
162
  disabled,
@@ -135,6 +135,7 @@ function isWorkbenchStructuredDataFormEmptyValue(value: WorkbenchStructuredDataF
135
135
  return value === '' || value === false;
136
136
  }
137
137
 
138
- function isRenderableWorkbenchStructuredDataFormError(error: ReactNode) {
138
+ /** True when a form field error should render (non-empty ReactNode). */
139
+ export function isRenderableWorkbenchStructuredDataFormError(error: ReactNode) {
139
140
  return error !== undefined && error !== null && error !== false && error !== '';
140
141
  }
@@ -1,7 +1,7 @@
1
- import './shell/activity-bar.css';
1
+ import './activity-bar.css';
2
2
  import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
3
- import { Button } from '../primitives/button';
4
- import { cx } from '../utils/cx';
3
+ import { Button } from '../../primitives/button';
4
+ import { cx } from '../../utils/cx';
5
5
  import { type ActivityBarOrientation } from './activityBarOrder';
6
6
  import {
7
7
  mergeWorkbenchSidebarViewPlacementDropZoneProps,
@@ -20,18 +20,18 @@ export interface ActivityBarItem {
20
20
  }
21
21
 
22
22
  export interface ActivityBarProps extends Omit<ComponentPropsWithoutRef<'nav'>, 'children'> {
23
- acceptSidebarViewPlacementDrop?: ((viewId: string) => boolean) | undefined;
23
+ acceptSidebarViewPlacementDrop?: (viewId: string) => boolean;
24
24
  itemDataAttributeName?: string;
25
25
  items: ActivityBarItem[];
26
26
  onItemActivate?: (item: ActivityBarItem) => void;
27
27
  onItemsReorder?: (itemIds: string[]) => void;
28
- onSidebarViewPlacementDrop?: ((viewId: string) => void) | undefined;
29
- orientation?: ActivityBarOrientation | undefined;
28
+ onSidebarViewPlacementDrop?: (viewId: string) => void;
29
+ orientation?: ActivityBarOrientation;
30
30
  reorderable?: boolean;
31
31
  secondaryItems?: ActivityBarItem[];
32
- sidebarViewPlacementDropZoneId?: string | undefined;
32
+ sidebarViewPlacementDropZoneId?: string;
33
33
  /** Enables cross-slot placement drag without reordering within the bar. */
34
- placementDraggable?: boolean | undefined;
34
+ placementDraggable?: boolean;
35
35
  }
36
36
 
37
37
  /** @deprecated Use WORKBENCH_SIDEBAR_VIEW_PLACEMENT_DRAG_DATA_TYPE */
@@ -1,8 +1,8 @@
1
1
  import { useState, type ComponentPropsWithRef, type ReactNode } from 'react';
2
- import { EmptyState } from '../primitives/empty-state';
3
- import { IconButton } from '../primitives/icon-button';
4
- import { cx } from '../utils/cx';
5
- import { WorkspacePathLabel } from './workspace/WorkspacePathLabel';
2
+ import { EmptyState } from '../../primitives/empty-state';
3
+ import { IconButton } from '../../primitives/icon-button';
4
+ import { cx } from '../../utils/cx';
5
+ import { WorkspacePathLabel } from '../workspace/WorkspacePathLabel';
6
6
  import { SplitView } from './SplitView';
7
7
 
8
8
  export type WorkbenchArtifactMode = 'code' | 'preview' | 'split';
@@ -31,8 +31,8 @@ export interface WorkbenchPreviewRendererMatch {
31
31
 
32
32
  export interface WorkbenchPreviewRendererContext {
33
33
  artifact: WorkbenchArtifactDescriptor;
34
- match?: WorkbenchPreviewRendererMatch | undefined;
35
- renderer?: WorkbenchPreviewRenderer | undefined;
34
+ match?: WorkbenchPreviewRendererMatch;
35
+ renderer?: WorkbenchPreviewRenderer;
36
36
  }
37
37
 
38
38
  export interface WorkbenchPreviewRenderer {
@@ -58,11 +58,11 @@ export type WorkbenchArtifactShellRenderPreview = (
58
58
  ) => ReactNode;
59
59
 
60
60
  export interface WorkbenchArtifactModeControlsProps extends ComponentPropsWithRef<'div'> {
61
- codeLabel?: string | undefined;
61
+ codeLabel?: string;
62
62
  mode: WorkbenchArtifactMode;
63
63
  onModeChange: (mode: WorkbenchArtifactMode) => void;
64
- previewLabel?: string | undefined;
65
- splitLabel?: string | undefined;
64
+ previewLabel?: string;
65
+ splitLabel?: string;
66
66
  }
67
67
 
68
68
  function normalizeToken(value: string | undefined) {
@@ -229,9 +229,9 @@ export function WorkbenchArtifactModeControls({
229
229
 
230
230
  export interface WorkbenchArtifactPreviewProps extends ComponentPropsWithRef<'div'> {
231
231
  artifact: WorkbenchArtifactDescriptor;
232
- renderPreview?: WorkbenchArtifactShellRenderPreview | undefined;
233
- renderers?: readonly WorkbenchPreviewRenderer[] | undefined;
234
- unsupportedLabel?: ReactNode | undefined;
232
+ renderPreview?: WorkbenchArtifactShellRenderPreview;
233
+ renderers?: readonly WorkbenchPreviewRenderer[];
234
+ unsupportedLabel?: ReactNode;
235
235
  }
236
236
 
237
237
  export function WorkbenchArtifactPreview({
@@ -270,19 +270,19 @@ export interface WorkbenchArtifactShellProps extends Omit<
270
270
  ComponentPropsWithRef<'div'>,
271
271
  'children'
272
272
  > {
273
- artifact?: WorkbenchArtifactDescriptor | undefined;
274
- codeLabel?: string | undefined;
275
- defaultMode?: WorkbenchArtifactMode | undefined;
276
- emptyLabel?: ReactNode | undefined;
277
- mode?: WorkbenchArtifactMode | undefined;
278
- onModeChange?: ((mode: WorkbenchArtifactMode) => void) | undefined;
279
- previewLabel?: string | undefined;
280
- previewRenderers?: readonly WorkbenchPreviewRenderer[] | undefined;
281
- renderCode?: WorkbenchArtifactShellRenderCode | undefined;
282
- renderPreview?: WorkbenchArtifactShellRenderPreview | undefined;
283
- showHeader?: boolean | undefined;
284
- splitLabel?: string | undefined;
285
- unsupportedPreviewLabel?: ReactNode | undefined;
273
+ artifact?: WorkbenchArtifactDescriptor;
274
+ codeLabel?: string;
275
+ defaultMode?: WorkbenchArtifactMode;
276
+ emptyLabel?: ReactNode;
277
+ mode?: WorkbenchArtifactMode;
278
+ onModeChange?: (mode: WorkbenchArtifactMode) => void;
279
+ previewLabel?: string;
280
+ previewRenderers?: readonly WorkbenchPreviewRenderer[];
281
+ renderCode?: WorkbenchArtifactShellRenderCode;
282
+ renderPreview?: WorkbenchArtifactShellRenderPreview;
283
+ showHeader?: boolean;
284
+ splitLabel?: string;
285
+ unsupportedPreviewLabel?: ReactNode;
286
286
  }
287
287
 
288
288
  export function WorkbenchArtifactShell({
@@ -1,4 +1,4 @@
1
- import './shell/split-view.css';
1
+ import './split-view.css';
2
2
  import {
3
3
  useEffect,
4
4
  useLayoutEffect,
@@ -9,7 +9,7 @@ import {
9
9
  type PointerEvent,
10
10
  type ReactNode,
11
11
  } from 'react';
12
- import { cx } from '../utils/cx';
12
+ import { cx } from '../../utils/cx';
13
13
 
14
14
  export type SplitViewOrientation = 'horizontal' | 'vertical';
15
15
  export type SplitViewPrimarySizeUnit = 'percent' | 'pixels';
@@ -42,7 +42,7 @@ export interface SplitViewProps {
42
42
  onPrimarySizePxPreviewChange?: (primarySizePx: number) => void;
43
43
  onSecondarySizePxChange?: (secondarySizePx: number) => void;
44
44
  onSecondarySizePxPreviewChange?: (secondarySizePx: number) => void;
45
- orientation?: SplitViewOrientation | undefined;
45
+ orientation?: SplitViewOrientation;
46
46
  primary: ReactNode;
47
47
  primarySizePercent?: number;
48
48
  primarySizePx?: number;