@lincyaw/dsh 0.1.0 → 0.2.0-rc.0

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 (78) hide show
  1. package/README.md +8 -4
  2. package/build/client.mjs +7 -1
  3. package/build/dist/client.js +1364 -64
  4. package/build/dist/client.json +225 -21
  5. package/build/ensure.mjs +11 -2
  6. package/cordis.patch.yml +9 -0
  7. package/entries/events.mjs +7 -0
  8. package/entries/load.mjs +11 -0
  9. package/entries/telemetry.mjs +7 -0
  10. package/entries/turns.mjs +7 -0
  11. package/package.json +32 -19
  12. package/src/client/RootLayout.tsx +332 -0
  13. package/src/client/agent-requests.ts +64 -0
  14. package/src/client/assistant-setup.tsx +185 -0
  15. package/src/client/author-settings.ts +57 -0
  16. package/src/client/brand.tsx +21 -14
  17. package/src/client/contract.ts +48 -1
  18. package/src/client/document-context.ts +39 -0
  19. package/src/client/dsh.d.ts +27 -1
  20. package/src/client/fallback.tsx +23 -19
  21. package/src/client/frame.css +280 -120
  22. package/src/client/index.tsx +72 -69
  23. package/src/client/launch-target.ts +89 -0
  24. package/src/client/layout.ts +90 -77
  25. package/src/client/mascot.tsx +37 -0
  26. package/src/client/messages.ts +142 -49
  27. package/src/client/{chrome.tsx → navigation-shell.tsx} +20 -25
  28. package/src/client/observable.ts +16 -0
  29. package/src/client/paths.ts +17 -0
  30. package/src/client/root-reload.ts +163 -0
  31. package/src/client/runtime-diagnostics.tsx +228 -0
  32. package/src/client/settings-opener.ts +26 -0
  33. package/src/client/{source.tsx → source-links.tsx} +16 -26
  34. package/src/client/speech-settings.tsx +7 -0
  35. package/src/client/styles.css +42 -0
  36. package/src/client/theme.ts +4 -2
  37. package/src/client/workbench-keys.ts +36 -0
  38. package/src/client/workspace-events.ts +114 -0
  39. package/src/node/bindings.ts +345 -0
  40. package/src/node/context.ts +131 -20
  41. package/src/node/{dsh.ts → dsh-types.ts} +33 -2
  42. package/src/node/fake-collector.ts +46 -0
  43. package/src/node/fake-model.ts +80 -0
  44. package/src/node/launch.ts +127 -10
  45. package/src/node/policy.ts +20 -23
  46. package/src/node/routes.ts +21 -11
  47. package/src/node/server-routes.ts +3 -0
  48. package/src/node/service.ts +58 -207
  49. package/src/node/skills.ts +9 -31
  50. package/src/node/telemetry.ts +126 -0
  51. package/src/node/tools.ts +34 -11
  52. package/src/node/{turns/index.ts → turns.ts} +48 -37
  53. package/src/node/workbench.ts +51 -6
  54. package/src/node/workspace-events.ts +167 -0
  55. package/src/node/workspaces.ts +2 -2
  56. package/src/workspace-events.ts +31 -0
  57. package/web.patch.yml +27 -56
  58. package/rows/load.mjs +0 -8
  59. package/rows/turns.mjs +0 -7
  60. package/skills/presmium-author/SKILL.md +0 -95
  61. package/skills/presmium-author/references/assistance.md +0 -27
  62. package/skills/presmium-author/references/code.md +0 -52
  63. package/skills/presmium-author/references/directives.md +0 -592
  64. package/skills/presmium-author/references/publishing.md +0 -60
  65. package/skills/presmium-author/references/writing.md +0 -72
  66. package/skills/presmium-present/SKILL.md +0 -69
  67. package/skills/presmium-present/references/classroom.md +0 -46
  68. package/skills/presmium-present/references/delivery.md +0 -63
  69. package/skills/presmium-present/references/lecture.md +0 -58
  70. package/src/client/Frame.tsx +0 -314
  71. /package/{rows → entries}/context.mjs +0 -0
  72. /package/{rows → entries}/policy.mjs +0 -0
  73. /package/{rows → entries}/routes.mjs +0 -0
  74. /package/{rows → entries}/service.mjs +0 -0
  75. /package/{rows → entries}/skills.mjs +0 -0
  76. /package/{rows → entries}/tools.mjs +0 -0
  77. /package/{rows → entries}/workbench.mjs +0 -0
  78. /package/{rows → entries}/workspaces.mjs +0 -0
@@ -1,10 +1,10 @@
1
1
  import { PmButton, WorkbenchProvider } from '@lincyaw/workbench'
2
2
  import type { Workbench } from '@lincyaw/workbench'
3
- import { useEffect, useSyncExternalStore } from 'react'
3
+ import { useEffect } from 'react'
4
4
  import { Modal } from '@deepseek-ai/dsh-client-ui-primitives'
5
- import { BRAND_LANGUAGES, BRAND_SLOTS, ONBOARDING } from './contract.ts'
6
- import { BRAND_COPY, FRAME_MESSAGES } from './messages.ts'
7
- import type { FrameMessages } from './messages.ts'
5
+ import { BRAND_SLOTS, ONBOARDING } from './contract.ts'
6
+ import { BRAND_COPY, BRAND_LANGUAGES, useClientMessages } from './messages.ts'
7
+ import { rememberOpenSection } from './settings-opener.ts'
8
8
 
9
9
  export function Logo({ size = 24, className }: { size?: number, className?: string }) {
10
10
  return (
@@ -15,11 +15,6 @@ export function Logo({ size = 24, className }: { size?: number, className?: stri
15
15
  )
16
16
  }
17
17
 
18
- export function useFrameMessages(workbench: Workbench): FrameMessages {
19
- const locale = useSyncExternalStore(workbench.subscribe, () => workbench.getState().locale)
20
- return FRAME_MESSAGES[locale]
21
- }
22
-
23
18
  const WELCOMED = 'presmium.workbench.welcomed'
24
19
 
25
20
  function welcomed(): boolean {
@@ -31,8 +26,9 @@ function welcomed(): boolean {
31
26
  }
32
27
 
33
28
  function Welcome({ workbench, complete, openSection }: { workbench: Workbench, complete?: () => void, openSection?: (id: string) => void }) {
34
- const t = useFrameMessages(workbench)
29
+ const t = useClientMessages(workbench)
35
30
  const read = welcomed()
31
+ useEffect(() => rememberOpenSection(openSection), [openSection])
36
32
  useEffect(() => {
37
33
  if (read) complete?.()
38
34
  }, [read, complete])
@@ -45,7 +41,7 @@ function Welcome({ workbench, complete, openSection }: { workbench: Workbench, c
45
41
  complete?.()
46
42
  }
47
43
  return (
48
- <WorkbenchProvider workbench={workbench} protectChanges={false}>
44
+ <WorkbenchProvider workbench={workbench}>
49
45
  <Modal open title={t.welcomeTitle} onClose={done} headless>
50
46
  <div className="pmw-welcome__card pm-ui" data-pmw-welcome="">
51
47
  <Logo size={40} />
@@ -68,14 +64,16 @@ function Welcome({ workbench, complete, openSection }: { workbench: Workbench, c
68
64
  )
69
65
  }
70
66
 
71
- function PassOver({ complete }: { complete?: () => void }) {
67
+ function PassOver({ complete, openSection }: { complete?: () => void, openSection?: (id: string) => void }) {
72
68
  useEffect(() => {
69
+ rememberOpenSection(openSection)
73
70
  complete?.()
74
- }, [complete])
71
+ }, [complete, openSection])
75
72
  return null
76
73
  }
77
74
 
78
- export function brand(ctx: DshClientContext, workbench: Workbench): void {
75
+ /** Puts Presmium's mark, name, first-use notice and languages in DeepSeek Harness's slots, in every layout. */
76
+ export function registerBrand(ctx: DshClientContext, workbench: Workbench): void {
79
77
  const [mark, name, hero] = BRAND_SLOTS
80
78
  ctx.effect(() => ctx.slots.inject(mark, () => ctx.slots.register({ name: mark }, ({ size }: { size?: number }) => <Logo size={size ?? 24} />)), 'presmium: sidebar brand mark')
81
79
  ctx.effect(() => ctx.slots.inject(name, () => ctx.slots.register({ name }, () => <span className="pmw-brand-name">Presmium</span>)), 'presmium: sidebar brand name')
@@ -83,8 +81,17 @@ export function brand(ctx: DshClientContext, workbench: Workbench): void {
83
81
  const [welcome, signIn] = ONBOARDING.steps
84
82
  ctx.effect(() => ctx.slots.inject(ONBOARDING.slot, () => ctx.slots.register({ name: ONBOARDING.slot, id: welcome, priority: -1, order: -100 }, props => <Welcome workbench={workbench} {...props} />)), 'presmium: first-use notice')
85
83
  ctx.effect(() => ctx.slots.inject(ONBOARDING.slot, () => ctx.slots.register({ name: ONBOARDING.slot, id: signIn, priority: -1, order: 0 }, PassOver)), 'presmium: no DeepSeek sign-in step')
84
+ // Harness's own `English` and `中文` carry its developer copy in the AI panel ("Describe what you want to build…"),
85
+ // and a language's dictionary cannot be registered twice; a choice of either becomes the same language with Presmium's
86
+ // copy. Only changes are followed: the page's first, provisional language may still give way to the stored one.
86
87
  for (const language of BRAND_LANGUAGES) {
87
88
  ctx.effect(() => ctx.locale.addLanguage({ id: language.id, label: language.label, fallback: language.fallback }), `presmium: language ${language.id}`)
88
89
  for (const [ns, dict] of Object.entries(BRAND_COPY[language.id])) ctx.effect(() => ctx.locale.register(ns, language.id, dict), `presmium: ${ns} copy ${language.id}`)
89
90
  }
91
+ ctx.effect(() => ctx.locale.subscribe(() => {
92
+ const active = ctx.locale.getLocale().active
93
+ const language = BRAND_LANGUAGES.find(candidate => candidate.fallback === active)
94
+ // The language may still be unregistered while its definition loads or after it is unloaded.
95
+ if (language && ctx.locale.getLocale().locales.some(locale => locale.id === language.id)) void ctx.locale.setLocale(language.id)
96
+ }), 'presmium: Harness\'s languages with Presmium\'s copy')
90
97
  }
@@ -1,3 +1,6 @@
1
+ // What the client part assumes of DeepSeek Harness's client: the IDs it registers and looks for, the children of the
2
+ // root slot, the members of ctx.layout, the brand and first-use slots, the shell's module table and the plugin group
3
+ // of the plugins page. The build (build/client.mjs) and the upgrade gate (e2e/workbench.spec.ts) read these too.
1
4
  export const CLIENT_ID = '@lincyaw/dsh'
2
5
 
3
6
  export const ROOT_CHILDREN = {
@@ -14,6 +17,29 @@ export const LAYOUT_MEMBERS = ['panelInfo', 'selectPanel', 'beginNavigation', 't
14
17
  export const BRAND_SLOTS = ['sidebar.brand.mark', 'sidebar.brand.name', 'conversation.hero.brand.mark'] as const
15
18
  export const ONBOARDING = { slot: 'settings.onboarding', steps: ['welcome-notice', 'deepseek-official'] } as const
16
19
 
20
+ /**
21
+ * Where the AI assistant explains a missing model and a failed turn: the composer's takeover chain, and the chat's
22
+ * `turn-error` node (its shipped cell is replaced, so every failed turn reads in the author's terms).
23
+ */
24
+ export const ASSISTANT_SLOTS = ['conversation.composer', { name: 'conversation.chat.node', key: 'turn-error' }] as const
25
+
26
+ /** Where the kitten sits beside the conversation: an added entry (its own id) among the session header's actions. */
27
+ export const MASCOT_SLOT = { name: 'conversation.session.header.actions', id: 'presmium-mascot' } as const
28
+
29
+ /** Where the author turns the kitten on or off: an added row (its own id) in Settings → General. */
30
+ export const MASCOT_SETTING = { name: 'settings.general.item', id: 'presmium-mascot' } as const
31
+
32
+ /**
33
+ * Settings → Models, and when the model catalog is read again: on the Host's events that change what can be routed,
34
+ * whenever the window regains focus, and on a slow poll while no model is usable (Settings is a modal over this page,
35
+ * so saving a key there does not refocus the window).
36
+ */
37
+ export const MODEL_SETTINGS = {
38
+ section: 'models',
39
+ events: ['credentials/reference-updated', 'settings/document-updated', 'llm/adapters-updated'],
40
+ pollMs: 3000,
41
+ } as const
42
+
17
43
  export const SHELL_MODULES = [
18
44
  'react',
19
45
  'react/jsx-runtime',
@@ -28,8 +54,29 @@ export const SHELL_MODULES = [
28
54
  '@deepseek-ai/dsh-client-ui-dockkit',
29
55
  ] as const
30
56
 
57
+ /**
58
+ * Harness's technical rows in Settings → General, by cell id, that stay out of an author's Settings while the coding
59
+ * view is off: the default permission mode and the display of the AI's work (work details and when they collapse, send
60
+ * behaviour while busy, performance and usage).
61
+ */
62
+ export const TECHNICAL_SETTINGS = { slot: 'settings.general.item', ids: ['permission', 'transcript-view', 'composer-enter', 'performance-usage'] } as const
63
+
64
+ /**
65
+ * The author's defaults for Harness's display preferences, by settings namespace (docs/studio.md 7.2):
66
+ * the coding view (trajectory, code diffs, every agent preset and the technical rows above) off, and the AI's work as
67
+ * compact, collapsed summaries without token statistics.
68
+ */
69
+ export const AUTHOR_DEFAULTS: Record<string, Record<string, unknown>> = {
70
+ 'ui-settings': { enabled: false },
71
+ 'ui-chat': { transcriptView: 'compact', performanceUsage: 'compact' },
72
+ }
73
+
31
74
  export const EXPERIMENTAL_GROUP = '[data-plugin-group="extensions"]'
32
75
 
33
76
  export const UI_LAYOUT = '@deepseek-ai/dsh-client-ui-layout'
77
+ export const UI_SIDEBAR = '@deepseek-ai/dsh-client-ui-sidebar'
34
78
 
35
- export const BRAND_LANGUAGES = [{ id: 'zh-CN', label: '简体中文(Presmium)', fallback: 'zh' }, { id: 'en-US', label: 'English (Presmium)', fallback: 'en' }] as const
79
+ /** Whether DeepSeek Harness's boot graph has the plugin `id` (a running host keeps its roster until it restarts). */
80
+ export function isBootEntryLoaded(id: string): boolean {
81
+ return window.__DSH_BOOT__?.entries.some(entry => entry.id === id) ?? false
82
+ }
@@ -0,0 +1,39 @@
1
+ import { selectedDocument } from '@lincyaw/workbench'
2
+ import type { Workbench } from '@lincyaw/workbench'
3
+
4
+ /** The agent session the main view shows. */
5
+ export function mainViewAgentSession<T extends { retainedBy: { mainView?: number } }>(byId: Record<string, T>): T | undefined {
6
+ return Object.values(byId).find(session => (session.retainedBy.mainView ?? 0) > 0)
7
+ }
8
+
9
+ /** Tells the bundle which document the agent session works on (`null`: none, the workspace root). */
10
+ export function postDocumentContext(agentSessionId: string, document: string | null, signal?: AbortSignal): Promise<Response> {
11
+ // Relative to the page: Studio may be served below a path (`--public-url`).
12
+ return fetch('api/presmium/context', {
13
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
14
+ body: JSON.stringify({ session: agentSessionId, document }), ...signal ? { signal } : {},
15
+ })
16
+ }
17
+
18
+ /** Keeps the main view's agent session on the document selected in the workbench, in every layout. */
19
+ export function syncDocumentContext(ctx: DshClientContext, workbench: Workbench): void {
20
+ ctx.inject(['sessions'], inner => inner.effect(() => {
21
+ let previous: string | undefined
22
+ let pending: AbortController | undefined
23
+ const sync = () => {
24
+ const session = mainViewAgentSession(inner.sessions.list.getSnapshot().byId)
25
+ if (!session) return
26
+ const document = selectedDocument(workbench.state)
27
+ const key = JSON.stringify([session.id, document])
28
+ if (key === previous) return
29
+ previous = key
30
+ pending?.abort()
31
+ pending = new AbortController()
32
+ void postDocumentContext(session.id, document, pending.signal).catch(() => {})
33
+ }
34
+ const offSession = inner.sessions.list.subscribe(sync)
35
+ const offDocument = workbench.subscribe(sync)
36
+ sync()
37
+ return () => { offSession(); offDocument(); pending?.abort() }
38
+ }, 'presmium: selected document context (all layouts)'))
39
+ }
@@ -28,6 +28,12 @@ interface DshObservable<T> {
28
28
 
29
29
  type DshSelectorHook<T> = <U>(select: (value: T) => U) => U
30
30
 
31
+ interface DshConfigForm {
32
+ getSnapshot(): { status: 'loading' | 'ready' | 'unavailable', user: unknown, revision: number | undefined, writable: boolean }
33
+ subscribe(listener: () => void): () => void
34
+ set(field: string, value: unknown): Promise<boolean>
35
+ }
36
+
31
37
  interface DshSlotOptions {
32
38
  name: string
33
39
  key?: string
@@ -39,6 +45,8 @@ interface DshSlotOptions {
39
45
  children?: Record<string, { kind: 'single' | 'keyed' | 'list', scope: 'root' }>
40
46
  store?: unknown
41
47
  inject?: unknown
48
+ /** A `chain` slot's election: the first non-null result in priority order renders. */
49
+ select?: (owner: unknown) => unknown
42
50
  }
43
51
 
44
52
  interface DshSlotEntry {
@@ -71,9 +79,14 @@ interface DshLocale {
71
79
  addLanguage(language: { id: string, label: string, fallback?: string }): () => void
72
80
  register(ns: string, locale: string | Record<string, Record<string, string>>, dict?: Record<string, string>): () => void
73
81
  bind(ns: string): (key: string, values?: Record<string, string>) => string
82
+ setLocale(id: string): unknown
74
83
  }
75
84
 
85
+ interface DshShortcutBinding { code: string, modifiers: string[] }
86
+
76
87
  interface DshShortcuts {
88
+ /** A read-only row of the shortcut reference; its keys are handled by their owner and kept from editable commands. */
89
+ registerFixed(command: { id: string, label: () => string, keys: readonly string[], bindings: readonly [DshShortcutBinding, ...DshShortcutBinding[]], group: 'application' | 'input' | 'menus' | 'approval' }): () => void
77
90
  register(command: {
78
91
  id: string
79
92
  label: () => string
@@ -107,7 +120,15 @@ interface DshSourceTabProps {
107
120
  } }
108
121
  }
109
122
 
123
+ interface DshSessionBinding {
124
+ session: {
125
+ beginSubmission(input: { mode: 'queue', text: string, attachments: [] }): { requestId: string, abandon(): void }
126
+ prompt(content: { type: 'text', text: string }[], mode: 'queue', signal: AbortSignal | undefined, requestId: string): Promise<{ ok: true, value: { accepted: true } } | { ok: false, error: { code: string, message: string } }>
127
+ }
128
+ }
129
+
110
130
  interface DshClientContext {
131
+ remote: { session: { modelCatalog(): Promise<{ ok: boolean, value?: { routableProviders?: readonly string[] } }> } }
111
132
  effect(effect: () => unknown, label?: string): void
112
133
  on(event: string, listener: (...args: any[]) => void): () => void
113
134
  inject(names: string[], apply: (ctx: DshClientContext) => void): void
@@ -119,9 +140,14 @@ interface DshClientContext {
119
140
  sidebarRightTabs: DshSidebarRightTabs
120
141
  sidebarRight: { active(): { id: string, kind: string, contentId: string } | undefined, close(id: string): void, isExpanded(): boolean, toggleExpanded(): void }
121
142
  layout: { selectPanel(id: string | null): void }
122
- sessions: { list: DshObservable<{ byId: Record<string, { id: string, cwd?: string, retainedBy: { mainView?: number } }> }> }
143
+ sessions: { list: DshObservable<{ byId: Record<string, { id: string, cwd?: string, retainedBy: { mainView?: number } }> }>, binding(id: string): DshSessionBinding | undefined }
123
144
  uiWorkspace: { openWorkspace(id: string, beforeOpen?: (sessionId: string) => void): Promise<void>, startSession(id?: string): void }
124
145
  workspaces: { list: DshObservable<{ items: readonly { workspaceId: string, path: string }[] }> }
146
+ configForms: {
147
+ developerTools: { enabled: DshObservable<boolean> }
148
+ get(entryId: string): DshConfigForm
149
+ whileServed(namespaces: readonly string[], register: (served: ReadonlySet<string>) => () => void): () => void
150
+ }
125
151
  }
126
152
 
127
153
  interface Window {
@@ -1,43 +1,47 @@
1
- import { ChangesPanel, DocumentView, FilesPanel, HomePanel, PreviewPanel, useWorkbench, WorkbenchProvider } from '@lincyaw/workbench'
1
+ import { DocumentView, HistoryPanel, HomePanel, PreviewPanel, useWorkbench, useWorkbenchState, WorkbenchProvider } from '@lincyaw/workbench'
2
2
  import type { Workbench } from '@lincyaw/workbench'
3
+ import { useRef } from 'react'
3
4
  import type { ComponentType } from 'react'
4
5
  import { Logo } from './brand.tsx'
5
6
  import { CLIENT_ID } from './contract.ts'
6
- import { FRAME_MESSAGES } from './messages.ts'
7
+ import { CLIENT_MESSAGES } from './messages.ts'
8
+ import { RuntimeDiagnosticsNotice, WorkbenchRuntimeBoundary } from './runtime-diagnostics.tsx'
7
9
 
8
10
  export const FALLBACK_PANEL = 'presmium'
9
11
 
10
12
  function Centre() {
11
- const document_ = useWorkbench(state => state.document)
13
+ const workbench = useWorkbench()
14
+ const document_ = useWorkbenchState(state => state.document)
15
+ const retainedDocument = useRef(false)
16
+ if (document_ !== undefined) retainedDocument.current = true
12
17
  return (
13
18
  <div className="pmw-fallback pm-ui" data-pmw-fallback="">
14
- {document_ === undefined
15
- ? <HomePanel />
16
- : (
17
- <div className="pmw-fallback__doc">
18
- <FilesPanel />
19
- <DocumentView />
20
- </div>
21
- )}
19
+ <RuntimeDiagnosticsNotice workbench={workbench} />
20
+ {document_ === undefined && <HomePanel />}
21
+ {retainedDocument.current && <div className="pmw-fallback__doc" hidden={document_ === undefined}>
22
+ <DocumentView active={document_ !== undefined} />
23
+ </div>}
22
24
  </div>
23
25
  )
24
26
  }
25
27
 
26
- export function fallback(ctx: DshClientContext, workbench: Workbench): void {
27
- const t = () => FRAME_MESSAGES[workbench.state.locale]
28
- const provided = (Component: ComponentType, className?: string, protectChanges = false) => function PresmiumPanel() {
28
+ /** With DeepSeek Harness's own layout: the workbench is a global panel of `main`, the document and its history are rightbar tab types. */
29
+ export function registerFallbackLayout(ctx: DshClientContext, workbench: Workbench): void {
30
+ const t = () => CLIENT_MESSAGES[workbench.state.locale]
31
+ const provided = (Component: ComponentType, className?: string) => function PresmiumPanel() {
29
32
  return (
30
- <WorkbenchProvider workbench={workbench} protectChanges={protectChanges}>
31
- {className ? <div className={className}><Component /></div> : <Component />}
33
+ <WorkbenchProvider workbench={workbench}>
34
+ <WorkbenchRuntimeBoundary workbench={workbench}>{className ? <div className={className}><Component /></div> : <Component />}</WorkbenchRuntimeBoundary>
32
35
  </WorkbenchProvider>
33
36
  )
34
37
  }
35
38
  ctx.effect(() => ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', id: FALLBACK_PANEL, order: -10, label: () => t().product }, () => <Logo size={20} />)), 'presmium: the panel list entry')
36
- ctx.effect(() => ctx.slots.register({ name: 'main', key: FALLBACK_PANEL }, provided(Centre, undefined, true)), 'presmium: the workbench as a global panel')
39
+ ctx.effect(() => ctx.slots.register({ name: 'main', key: FALLBACK_PANEL }, provided(Centre)), 'presmium: the workbench as a global panel')
37
40
  ctx.inject(['sidebarRightTabs'], (inner) => {
41
+ const History = () => <HistoryPanel onClose={() => {}} />
38
42
  const tabs = [
39
- { kind: 'presmium-preview', Panel: PreviewPanel, title: () => `Presmium · ${t().preview}` },
40
- { kind: 'presmium-changes', Panel: ChangesPanel, title: () => `Presmium · ${t().changes}` },
43
+ { kind: 'presmium-preview', Panel: PreviewPanel as ComponentType, title: () => `Presmium · ${t().preview}` },
44
+ { kind: 'presmium-history', Panel: History, title: () => `Presmium · ${t().history}` },
41
45
  ]
42
46
  for (const { kind, Panel, title } of tabs) {
43
47
  const id = `${CLIENT_ID}/${kind}`