@lincyaw/dsh 0.0.0-stage → 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 (68) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +26 -2
  3. package/build/client.mjs +72 -0
  4. package/build/dist/client.js +1390 -0
  5. package/build/dist/client.json +274 -0
  6. package/build/ensure.mjs +39 -0
  7. package/cordis.patch.yml +26 -0
  8. package/entries/context.mjs +7 -0
  9. package/entries/events.mjs +7 -0
  10. package/entries/load.mjs +11 -0
  11. package/entries/policy.mjs +7 -0
  12. package/entries/routes.mjs +7 -0
  13. package/entries/service.mjs +7 -0
  14. package/entries/skills.mjs +7 -0
  15. package/entries/telemetry.mjs +7 -0
  16. package/entries/tools.mjs +7 -0
  17. package/entries/turns.mjs +7 -0
  18. package/entries/workbench.mjs +9 -0
  19. package/entries/workspaces.mjs +7 -0
  20. package/package.json +108 -3
  21. package/profile.patch.yml +25 -0
  22. package/src/client/RootLayout.tsx +332 -0
  23. package/src/client/agent-requests.ts +64 -0
  24. package/src/client/assistant-setup.tsx +185 -0
  25. package/src/client/author-settings.ts +57 -0
  26. package/src/client/brand.tsx +97 -0
  27. package/src/client/contract.ts +82 -0
  28. package/src/client/document-context.ts +39 -0
  29. package/src/client/dsh.d.ts +155 -0
  30. package/src/client/fallback.tsx +53 -0
  31. package/src/client/frame.css +505 -0
  32. package/src/client/index.tsx +157 -0
  33. package/src/client/launch-target.ts +89 -0
  34. package/src/client/layout.ts +230 -0
  35. package/src/client/mascot.tsx +37 -0
  36. package/src/client/messages.ts +171 -0
  37. package/src/client/navigation-shell.tsx +74 -0
  38. package/src/client/observable.ts +16 -0
  39. package/src/client/paths.ts +17 -0
  40. package/src/client/root-reload.ts +163 -0
  41. package/src/client/runtime-diagnostics.tsx +228 -0
  42. package/src/client/settings-opener.ts +26 -0
  43. package/src/client/source-links.tsx +71 -0
  44. package/src/client/speech-settings.tsx +7 -0
  45. package/src/client/styles.css +45 -0
  46. package/src/client/theme.ts +151 -0
  47. package/src/client/workbench-keys.ts +36 -0
  48. package/src/client/workspace-events.ts +114 -0
  49. package/src/node/bindings.ts +345 -0
  50. package/src/node/context.ts +166 -0
  51. package/src/node/dsh-types.ts +124 -0
  52. package/src/node/fake-collector.ts +46 -0
  53. package/src/node/fake-model.ts +80 -0
  54. package/src/node/launch.ts +176 -0
  55. package/src/node/policy.ts +56 -0
  56. package/src/node/routes.ts +56 -0
  57. package/src/node/server-routes.ts +3 -0
  58. package/src/node/service.ts +131 -0
  59. package/src/node/skills.ts +24 -0
  60. package/src/node/telemetry.ts +126 -0
  61. package/src/node/tools.ts +66 -0
  62. package/src/node/turns.ts +176 -0
  63. package/src/node/watchdog.mjs +36 -0
  64. package/src/node/workbench.ts +88 -0
  65. package/src/node/workspace-events.ts +167 -0
  66. package/src/node/workspaces.ts +10 -0
  67. package/src/workspace-events.ts +31 -0
  68. package/web.patch.yml +161 -0
@@ -0,0 +1,97 @@
1
+ import { PmButton, WorkbenchProvider } from '@lincyaw/workbench'
2
+ import type { Workbench } from '@lincyaw/workbench'
3
+ import { useEffect } from 'react'
4
+ import { Modal } from '@deepseek-ai/dsh-client-ui-primitives'
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
+
9
+ export function Logo({ size = 24, className }: { size?: number, className?: string }) {
10
+ return (
11
+ <svg className={className ? `pmw-logo ${className}` : 'pmw-logo'} width={size} height={size} viewBox="0 0 32 32" role="img" aria-label="Presmium">
12
+ <rect width="32" height="32" rx="7" fill="#0F5F5C" />
13
+ <path d="M11 8h7.5a6.5 6.5 0 0 1 0 13H15v4h-4zM15 12v5h3.5a2.5 2.5 0 0 0 0-5z" fill="#F7F5F0" fillRule="evenodd" />
14
+ </svg>
15
+ )
16
+ }
17
+
18
+ const WELCOMED = 'presmium.workbench.welcomed'
19
+
20
+ function welcomed(): boolean {
21
+ try {
22
+ return localStorage.getItem(WELCOMED) === '1'
23
+ } catch {
24
+ return false
25
+ }
26
+ }
27
+
28
+ function Welcome({ workbench, complete, openSection }: { workbench: Workbench, complete?: () => void, openSection?: (id: string) => void }) {
29
+ const t = useClientMessages(workbench)
30
+ const read = welcomed()
31
+ useEffect(() => rememberOpenSection(openSection), [openSection])
32
+ useEffect(() => {
33
+ if (read) complete?.()
34
+ }, [read, complete])
35
+ if (read) return null
36
+ const done = () => {
37
+ try {
38
+ localStorage.setItem(WELCOMED, '1')
39
+ } catch {
40
+ }
41
+ complete?.()
42
+ }
43
+ return (
44
+ <WorkbenchProvider workbench={workbench}>
45
+ <Modal open title={t.welcomeTitle} onClose={done} headless>
46
+ <div className="pmw-welcome__card pm-ui" data-pmw-welcome="">
47
+ <Logo size={40} />
48
+ <h2 id="pmw-welcome-title" className="pmw-welcome__title">{t.welcomeTitle}</h2>
49
+ <p>{t.welcome}</p>
50
+ <div className="pmw-welcome__actions">
51
+ <PmButton
52
+ onClick={() => {
53
+ done()
54
+ openSection?.('models')
55
+ }}
56
+ >
57
+ {t.models}
58
+ </PmButton>
59
+ <PmButton variant="primary" onClick={done}>{t.start}</PmButton>
60
+ </div>
61
+ </div>
62
+ </Modal>
63
+ </WorkbenchProvider>
64
+ )
65
+ }
66
+
67
+ function PassOver({ complete, openSection }: { complete?: () => void, openSection?: (id: string) => void }) {
68
+ useEffect(() => {
69
+ rememberOpenSection(openSection)
70
+ complete?.()
71
+ }, [complete, openSection])
72
+ return null
73
+ }
74
+
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 {
77
+ const [mark, name, hero] = BRAND_SLOTS
78
+ ctx.effect(() => ctx.slots.inject(mark, () => ctx.slots.register({ name: mark }, ({ size }: { size?: number }) => <Logo size={size ?? 24} />)), 'presmium: sidebar brand mark')
79
+ ctx.effect(() => ctx.slots.inject(name, () => ctx.slots.register({ name }, () => <span className="pmw-brand-name">Presmium</span>)), 'presmium: sidebar brand name')
80
+ ctx.effect(() => ctx.slots.inject(hero, () => ctx.slots.register({ name: hero }, ({ size, className }: { size?: number, className?: string }) => <Logo size={size ?? 34} className={className} />)), 'presmium: conversation brand mark')
81
+ const [welcome, signIn] = ONBOARDING.steps
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')
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.
87
+ for (const language of BRAND_LANGUAGES) {
88
+ ctx.effect(() => ctx.locale.addLanguage({ id: language.id, label: language.label, fallback: language.fallback }), `presmium: language ${language.id}`)
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}`)
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')
97
+ }
@@ -0,0 +1,82 @@
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.
4
+ export const CLIENT_ID = '@lincyaw/dsh'
5
+
6
+ export const ROOT_CHILDREN = {
7
+ 'sidebar': { kind: 'single', scope: 'root' },
8
+ 'main': { kind: 'keyed', scope: 'root' },
9
+ 'rightbar': { kind: 'single', scope: 'root' },
10
+ 'shell.bottom': { kind: 'single', scope: 'root' },
11
+ 'shell.overlay': { kind: 'list', scope: 'root' },
12
+ 'shell.leading': { kind: 'single', scope: 'root' },
13
+ } as const
14
+
15
+ export const LAYOUT_MEMBERS = ['panelInfo', 'selectPanel', 'beginNavigation', 'toggleSidebar', 'openRightbar', 'closeRightbar'] as const
16
+
17
+ export const BRAND_SLOTS = ['sidebar.brand.mark', 'sidebar.brand.name', 'conversation.hero.brand.mark'] as const
18
+ export const ONBOARDING = { slot: 'settings.onboarding', steps: ['welcome-notice', 'deepseek-official'] } as const
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
+
43
+ export const SHELL_MODULES = [
44
+ 'react',
45
+ 'react/jsx-runtime',
46
+ 'react-dom',
47
+ 'react-dom/client',
48
+ '@deepseek-ai/cordis',
49
+ 'zod',
50
+ '@tanstack/react-virtual',
51
+ '@deepseek-ai/dsh-client-store',
52
+ '@deepseek-ai/dsh-client-ui-slots',
53
+ '@deepseek-ai/dsh-client-ui-primitives',
54
+ '@deepseek-ai/dsh-client-ui-dockkit',
55
+ ] as const
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
+
74
+ export const EXPERIMENTAL_GROUP = '[data-plugin-group="extensions"]'
75
+
76
+ export const UI_LAYOUT = '@deepseek-ai/dsh-client-ui-layout'
77
+ export const UI_SIDEBAR = '@deepseek-ai/dsh-client-ui-sidebar'
78
+
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
+ }
@@ -0,0 +1,155 @@
1
+ declare module '@deepseek-ai/dsh-client-ui-primitives' {
2
+ export const Modal: import('react').ComponentType<{
3
+ open: boolean
4
+ title: string
5
+ onClose(): void
6
+ headless: true
7
+ children?: import('react').ReactNode
8
+ }>
9
+ }
10
+
11
+ declare module '@deepseek-ai/dsh-client-store' {
12
+ export interface StoreInstance<S, A> {
13
+ actions: A
14
+ getSnapshot(): S
15
+ subscribe(listener: () => void): () => void
16
+ }
17
+ export interface StoreHandle<S, A> {
18
+ create(): StoreInstance<S, A>
19
+ }
20
+ type Bound<A> = { [K in keyof A]: A[K] extends (draft: any, ...args: infer P) => void ? (...args: P) => void : never }
21
+ export function defineStore<S, A extends Record<string, (draft: S, ...args: any[]) => void>>(spec: { init: () => S, actions: A }): StoreHandle<S, Bound<A>>
22
+ }
23
+
24
+ interface DshObservable<T> {
25
+ getSnapshot(): T
26
+ subscribe(listener: () => void): () => void
27
+ }
28
+
29
+ type DshSelectorHook<T> = <U>(select: (value: T) => U) => U
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
+
37
+ interface DshSlotOptions {
38
+ name: string
39
+ key?: string
40
+ id?: string
41
+ priority?: number
42
+ order?: number
43
+ locale?: string
44
+ label?: string | (() => string)
45
+ children?: Record<string, { kind: 'single' | 'keyed' | 'list', scope: 'root' }>
46
+ store?: unknown
47
+ inject?: unknown
48
+ /** A `chain` slot's election: the first non-null result in priority order renders. */
49
+ select?: (owner: unknown) => unknown
50
+ }
51
+
52
+ interface DshSlotEntry {
53
+ options: { key?: string, id?: string, label?: string | (() => string) }
54
+ }
55
+
56
+ interface DshSlots {
57
+ register(options: DshSlotOptions, component: (props: any) => unknown): () => void
58
+ inject(name: string, effect: () => unknown): void
59
+ entries(name: string): DshSlotEntry[]
60
+ subscribe(name: string, listener: () => void): () => void
61
+ provideRoot(options: { hooks: Record<string, DshObservable<unknown>> }): () => void
62
+ }
63
+
64
+ interface DshThemeSnapshot {
65
+ preference: string
66
+ fontSizes: { text: number, code: number, terminal: number }
67
+ fontFamilies: { text: string, code: string, terminal: string }
68
+ active: { id: string, colorScheme: 'light' | 'dark', tokens: Record<string, string> }
69
+ }
70
+
71
+ interface DshTheme {
72
+ getTheme(): DshThemeSnapshot
73
+ overrideTokens(source: string, tokens: Record<string, { light: string, dark: string }>): () => void
74
+ }
75
+
76
+ interface DshLocale {
77
+ getLocale(): { active: string, locales: readonly { id: string }[] }
78
+ subscribe(listener: () => void): () => void
79
+ addLanguage(language: { id: string, label: string, fallback?: string }): () => void
80
+ register(ns: string, locale: string | Record<string, Record<string, string>>, dict?: Record<string, string>): () => void
81
+ bind(ns: string): (key: string, values?: Record<string, string>) => string
82
+ setLocale(id: string): unknown
83
+ }
84
+
85
+ interface DshShortcutBinding { code: string, modifiers: string[] }
86
+
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
90
+ register(command: {
91
+ id: string
92
+ label: () => string
93
+ aliases?: string[]
94
+ defaults: Record<string, { code: string, modifiers: string[] }>
95
+ regions?: string[]
96
+ modals?: string[]
97
+ resolve: () => { status: 'handled', run: () => void }
98
+ }): () => void
99
+ }
100
+
101
+ interface DshSidebarRightTabs {
102
+ register(definition: {
103
+ id: string
104
+ kind: string
105
+ priority?: 'extension' | 'builtin' | 'fallback'
106
+ keepMounted?: boolean
107
+ patterns?: readonly string[]
108
+ canOpen?(address: string): boolean
109
+ title: (address?: string) => string
110
+ guide?: { id: string, order?: number, title: () => string, description?: () => string }[]
111
+ }): () => void
112
+ }
113
+
114
+ interface DshSourceTabProps {
115
+ useTabInfo(): { tab: {
116
+ contentId: string
117
+ visible: boolean
118
+ navigation: { revision: number, params?: { line?: unknown } }
119
+ actions: { close(): void }
120
+ } }
121
+ }
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
+
130
+ interface DshClientContext {
131
+ remote: { session: { modelCatalog(): Promise<{ ok: boolean, value?: { routableProviders?: readonly string[] } }> } }
132
+ effect(effect: () => unknown, label?: string): void
133
+ on(event: string, listener: (...args: any[]) => void): () => void
134
+ inject(names: string[], apply: (ctx: DshClientContext) => void): void
135
+ reflect: { provide(name: string, value: unknown): () => void }
136
+ slots: DshSlots
137
+ theme: DshTheme
138
+ locale: DshLocale
139
+ shortcuts: DshShortcuts
140
+ sidebarRightTabs: DshSidebarRightTabs
141
+ sidebarRight: { active(): { id: string, kind: string, contentId: string } | undefined, close(id: string): void, isExpanded(): boolean, toggleExpanded(): void }
142
+ layout: { selectPanel(id: string | null): void }
143
+ sessions: { list: DshObservable<{ byId: Record<string, { id: string, cwd?: string, retainedBy: { mainView?: number } }> }>, binding(id: string): DshSessionBinding | undefined }
144
+ uiWorkspace: { openWorkspace(id: string, beforeOpen?: (sessionId: string) => void): Promise<void>, startSession(id?: string): void }
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
+ }
151
+ }
152
+
153
+ interface Window {
154
+ __DSH_BOOT__?: { entries: { id: string }[], release?: { version: string, commit?: string } }
155
+ }
@@ -0,0 +1,53 @@
1
+ import { DocumentView, HistoryPanel, HomePanel, PreviewPanel, useWorkbench, useWorkbenchState, WorkbenchProvider } from '@lincyaw/workbench'
2
+ import type { Workbench } from '@lincyaw/workbench'
3
+ import { useRef } from 'react'
4
+ import type { ComponentType } from 'react'
5
+ import { Logo } from './brand.tsx'
6
+ import { CLIENT_ID } from './contract.ts'
7
+ import { CLIENT_MESSAGES } from './messages.ts'
8
+ import { RuntimeDiagnosticsNotice, WorkbenchRuntimeBoundary } from './runtime-diagnostics.tsx'
9
+
10
+ export const FALLBACK_PANEL = 'presmium'
11
+
12
+ function Centre() {
13
+ const workbench = useWorkbench()
14
+ const document_ = useWorkbenchState(state => state.document)
15
+ const retainedDocument = useRef(false)
16
+ if (document_ !== undefined) retainedDocument.current = true
17
+ return (
18
+ <div className="pmw-fallback pm-ui" data-pmw-fallback="">
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>}
24
+ </div>
25
+ )
26
+ }
27
+
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() {
32
+ return (
33
+ <WorkbenchProvider workbench={workbench}>
34
+ <WorkbenchRuntimeBoundary workbench={workbench}>{className ? <div className={className}><Component /></div> : <Component />}</WorkbenchRuntimeBoundary>
35
+ </WorkbenchProvider>
36
+ )
37
+ }
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')
39
+ ctx.effect(() => ctx.slots.register({ name: 'main', key: FALLBACK_PANEL }, provided(Centre)), 'presmium: the workbench as a global panel')
40
+ ctx.inject(['sidebarRightTabs'], (inner) => {
41
+ const History = () => <HistoryPanel onClose={() => {}} />
42
+ const tabs = [
43
+ { kind: 'presmium-preview', Panel: PreviewPanel as ComponentType, title: () => `Presmium · ${t().preview}` },
44
+ { kind: 'presmium-history', Panel: History, title: () => `Presmium · ${t().history}` },
45
+ ]
46
+ for (const { kind, Panel, title } of tabs) {
47
+ const id = `${CLIENT_ID}/${kind}`
48
+ const Body = provided(Panel, 'pmw-fallback__tab pm-ui')
49
+ inner.effect(() => inner.sidebarRightTabs.register({ id, kind, priority: 'extension', keepMounted: true, title, guide: [{ id: kind, order: 50, title }] }), `presmium: ${kind} tab type`)
50
+ inner.effect(() => inner.slots.inject('sidebar.right.pane.tab', () => inner.slots.register({ name: 'sidebar.right.pane.tab', key: id }, Body)), `presmium: ${kind} tab body`)
51
+ }
52
+ })
53
+ }