@lincyaw/dsh 0.1.0 → 0.2.0-rc.1
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.
- package/README.md +8 -4
- package/build/client.mjs +7 -1
- package/build/dist/client.js +1364 -64
- package/build/dist/client.json +225 -21
- package/build/ensure.mjs +11 -2
- package/cordis.patch.yml +9 -0
- package/entries/events.mjs +7 -0
- package/entries/load.mjs +11 -0
- package/entries/telemetry.mjs +7 -0
- package/entries/turns.mjs +7 -0
- package/package.json +32 -19
- package/src/client/RootLayout.tsx +332 -0
- package/src/client/agent-requests.ts +64 -0
- package/src/client/assistant-setup.tsx +185 -0
- package/src/client/author-settings.ts +57 -0
- package/src/client/brand.tsx +21 -14
- package/src/client/contract.ts +48 -1
- package/src/client/document-context.ts +39 -0
- package/src/client/dsh.d.ts +27 -1
- package/src/client/fallback.tsx +23 -19
- package/src/client/frame.css +280 -120
- package/src/client/index.tsx +72 -69
- package/src/client/launch-target.ts +89 -0
- package/src/client/layout.ts +90 -77
- package/src/client/mascot.tsx +37 -0
- package/src/client/messages.ts +142 -49
- package/src/client/{chrome.tsx → navigation-shell.tsx} +20 -25
- package/src/client/observable.ts +16 -0
- package/src/client/paths.ts +17 -0
- package/src/client/root-reload.ts +163 -0
- package/src/client/runtime-diagnostics.tsx +228 -0
- package/src/client/settings-opener.ts +26 -0
- package/src/client/{source.tsx → source-links.tsx} +16 -26
- package/src/client/speech-settings.tsx +7 -0
- package/src/client/styles.css +42 -0
- package/src/client/theme.ts +4 -2
- package/src/client/workbench-keys.ts +36 -0
- package/src/client/workspace-events.ts +114 -0
- package/src/node/bindings.ts +345 -0
- package/src/node/context.ts +131 -20
- package/src/node/{dsh.ts → dsh-types.ts} +33 -2
- package/src/node/fake-collector.ts +46 -0
- package/src/node/fake-model.ts +80 -0
- package/src/node/launch.ts +127 -10
- package/src/node/policy.ts +20 -23
- package/src/node/routes.ts +21 -11
- package/src/node/server-routes.ts +3 -0
- package/src/node/service.ts +58 -207
- package/src/node/skills.ts +9 -31
- package/src/node/telemetry.ts +126 -0
- package/src/node/tools.ts +34 -11
- package/src/node/{turns/index.ts → turns.ts} +48 -37
- package/src/node/workbench.ts +51 -6
- package/src/node/workspace-events.ts +167 -0
- package/src/node/workspaces.ts +2 -2
- package/src/workspace-events.ts +31 -0
- package/web.patch.yml +27 -56
- package/rows/load.mjs +0 -8
- package/rows/turns.mjs +0 -7
- package/skills/presmium-author/SKILL.md +0 -95
- package/skills/presmium-author/references/assistance.md +0 -27
- package/skills/presmium-author/references/code.md +0 -52
- package/skills/presmium-author/references/directives.md +0 -592
- package/skills/presmium-author/references/publishing.md +0 -60
- package/skills/presmium-author/references/writing.md +0 -72
- package/skills/presmium-present/SKILL.md +0 -69
- package/skills/presmium-present/references/classroom.md +0 -46
- package/skills/presmium-present/references/delivery.md +0 -63
- package/skills/presmium-present/references/lecture.md +0 -58
- package/src/client/Frame.tsx +0 -314
- /package/{rows → entries}/context.mjs +0 -0
- /package/{rows → entries}/policy.mjs +0 -0
- /package/{rows → entries}/routes.mjs +0 -0
- /package/{rows → entries}/service.mjs +0 -0
- /package/{rows → entries}/skills.mjs +0 -0
- /package/{rows → entries}/tools.mjs +0 -0
- /package/{rows → entries}/workbench.mjs +0 -0
- /package/{rows → entries}/workspaces.mjs +0 -0
package/src/client/index.tsx
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import { localeOf, Workbench } from '@lincyaw/workbench'
|
|
1
|
+
import { connectAuthoringClient, localeOf, Workbench, workspaceClient } from '@lincyaw/workbench'
|
|
2
2
|
import type { Locale } from '@lincyaw/workbench'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { workspaceSubscriptions } from './workspace-events.ts'
|
|
4
|
+
import { registerAuthorSettings } from './author-settings.ts'
|
|
5
|
+
import { registerSpeechSettings } from './speech-settings.tsx'
|
|
6
|
+
import { openSpeechSettings } from './settings-opener.ts'
|
|
7
|
+
import { registerBrand } from './brand.tsx'
|
|
8
|
+
import { registerMascot } from './mascot.tsx'
|
|
9
|
+
import { CLIENT_ID, isBootEntryLoaded, ROOT_CHILDREN, UI_LAYOUT, UI_SIDEBAR } from './contract.ts'
|
|
10
|
+
import { syncDocumentContext } from './document-context.ts'
|
|
11
|
+
import { FALLBACK_PANEL, registerFallbackLayout } from './fallback.tsx'
|
|
8
12
|
import { createLayoutStore, LayoutController } from './layout.ts'
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
13
|
+
import { connectLaunchTarget } from './launch-target.ts'
|
|
14
|
+
import { CLIENT_MESSAGES } from './messages.ts'
|
|
15
|
+
import { registerNavigationShell } from './navigation-shell.tsx'
|
|
16
|
+
import { createObservable } from './observable.ts'
|
|
17
|
+
import { isSamePath } from './paths.ts'
|
|
18
|
+
import { registerAgentRequests } from './agent-requests.ts'
|
|
19
|
+
import { registerAssistantSetup } from './assistant-setup.tsx'
|
|
20
|
+
import { rootLayoutFor } from './RootLayout.tsx'
|
|
21
|
+
import { beginRootReload, restoreRootReturnPoint, saveRootReturnPoint } from './root-reload.ts'
|
|
22
|
+
import { registerSourceLinks } from './source-links.tsx'
|
|
23
|
+
import { registerWorkbenchKeys } from './workbench-keys.ts'
|
|
24
|
+
import { runtimeDiagnostics } from './runtime-diagnostics.tsx'
|
|
25
|
+
import { DSH_THEME_TOKENS, syncDarkClass, ThemePresenter } from './theme.ts'
|
|
11
26
|
|
|
12
27
|
declare const __PRESMIUM_CSS__: string
|
|
13
28
|
|
|
@@ -21,69 +36,46 @@ if (typeof document !== 'undefined' && !document.querySelector(`style[data-plugi
|
|
|
21
36
|
|
|
22
37
|
export const inject = ['slots', 'theme', 'locale', 'shortcuts']
|
|
23
38
|
|
|
39
|
+
// The studio's one language source: the language chosen in Settings, else the browser's.
|
|
24
40
|
function workbenchLocale(active: string | undefined): Locale {
|
|
25
|
-
|
|
26
|
-
if (tag.startsWith('zh') || tag.startsWith('en')) return localeOf(tag)
|
|
27
|
-
return localeOf(navigator.language)
|
|
41
|
+
return localeOf(active) ?? localeOf(navigator.language) ?? 'en'
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
export function apply(ctx: DshClientContext): void {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
subscribe(listener: () => void) {
|
|
37
|
-
workspaceListeners.add(listener)
|
|
38
|
-
return () => { workspaceListeners.delete(listener) }
|
|
39
|
-
},
|
|
40
|
-
}
|
|
45
|
+
const nativeLayout = isBootEntryLoaded(UI_LAYOUT)
|
|
46
|
+
const reload = nativeLayout ? { reloading: false } : beginRootReload()
|
|
47
|
+
if (reload.reloading) return
|
|
48
|
+
const uiWorkspace = new Promise<DshClientContext['uiWorkspace']>(resolve => ctx.inject(['uiWorkspace'], inner => resolve(inner.uiWorkspace)))
|
|
49
|
+
const dshWorkspaces = createObservable<ReturnType<DshClientContext['workspaces']['list']['getSnapshot']>>({ items: [] })
|
|
41
50
|
// Register the root before the renderer boots; remote-backed workspace data follows later.
|
|
42
51
|
ctx.inject(['workspaces'], inner => inner.effect(() => {
|
|
43
|
-
const update = () =>
|
|
44
|
-
workspaceSnapshot = inner.workspaces.list.getSnapshot()
|
|
45
|
-
for (const listener of workspaceListeners) listener()
|
|
46
|
-
}
|
|
52
|
+
const update = () => dshWorkspaces.set(inner.workspaces.list.getSnapshot())
|
|
47
53
|
update()
|
|
48
54
|
return inner.workspaces.list.subscribe(update)
|
|
49
55
|
}, 'presmium: workspace list'))
|
|
50
|
-
const workbench = new Workbench({ locale: workbenchLocale(ctx.locale.getLocale().active) })
|
|
51
|
-
|
|
56
|
+
const workbench = new Workbench({ client: workspaceClient('api/presmium/', workspaceSubscriptions(ctx)), locale: workbenchLocale(ctx.locale.getLocale().active) })
|
|
57
|
+
workbench.client.openSpeechSettings = openSpeechSettings
|
|
58
|
+
ctx.effect(() => connectAuthoringClient(workbench), 'presmium: authoring client control')
|
|
59
|
+
ctx.effect(() => runtimeDiagnostics(workbench).start(), 'presmium: runtime diagnostics')
|
|
52
60
|
let showEditor = (layout: DshClientContext['layout']) => layout.selectPanel(FALLBACK_PANEL)
|
|
53
|
-
|
|
54
|
-
ctx.effect(() => () =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const document = workbench.state.document === '' && workbench.state.documents?.single === false ? null : workbench.state.document ?? null
|
|
62
|
-
const key = JSON.stringify([session.id, document])
|
|
63
|
-
if (key === previous) return
|
|
64
|
-
previous = key
|
|
65
|
-
pending?.abort()
|
|
66
|
-
pending = new AbortController()
|
|
67
|
-
void fetch('/api/presmium/context', {
|
|
68
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
69
|
-
body: JSON.stringify({ session: session.id, document }), signal: pending.signal,
|
|
70
|
-
}).catch(() => {})
|
|
71
|
-
}
|
|
72
|
-
const offSession = inner.sessions.list.subscribe(sync)
|
|
73
|
-
const offDocument = workbench.subscribe(sync)
|
|
74
|
-
sync()
|
|
75
|
-
return () => { offSession(); offDocument(); pending?.abort() }
|
|
76
|
-
}, 'presmium: selected document context (all layouts)'))
|
|
61
|
+
registerSourceLinks(ctx, workbench, layout => showEditor(layout))
|
|
62
|
+
ctx.effect(() => () => {
|
|
63
|
+
workbench.dispose()
|
|
64
|
+
if (!nativeLayout) saveRootReturnPoint(workbench)
|
|
65
|
+
}, 'presmium: workbench')
|
|
66
|
+
syncDocumentContext(ctx, workbench)
|
|
67
|
+
registerWorkbenchKeys(ctx, workbench)
|
|
68
|
+
registerAgentRequests(ctx, workbench, registerAssistantSetup(ctx, workbench))
|
|
77
69
|
ctx.effect(() => ctx.locale.subscribe(() => workbench.setLocale(workbenchLocale(ctx.locale.getLocale().active))), 'presmium: language')
|
|
78
|
-
ctx.effect(() => ctx.theme.overrideTokens(CLIENT_ID,
|
|
70
|
+
ctx.effect(() => ctx.theme.overrideTokens(CLIENT_ID, DSH_THEME_TOKENS), 'presmium: 书卷 tokens')
|
|
79
71
|
ctx.effect(() => {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
return ctx.on('theme/change',
|
|
72
|
+
const sync = (snapshot: DshThemeSnapshot) => workbench.setDark(syncDarkClass(snapshot))
|
|
73
|
+
sync(ctx.theme.getTheme())
|
|
74
|
+
return ctx.on('theme/change', sync)
|
|
83
75
|
}, 'presmium: light and dark')
|
|
84
76
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
77
|
+
if (nativeLayout) {
|
|
78
|
+
registerFallbackLayout(ctx, workbench)
|
|
87
79
|
} else {
|
|
88
80
|
ctx.effect(() => {
|
|
89
81
|
const presenter = new ThemePresenter()
|
|
@@ -97,11 +89,11 @@ export function apply(ctx: DshClientContext): void {
|
|
|
97
89
|
ctx.effect(() => {
|
|
98
90
|
const handle = createLayoutStore()
|
|
99
91
|
const instance = handle.create()
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const workspace =
|
|
104
|
-
if (workspace) (await
|
|
92
|
+
const globalPanels = createObservable<readonly DshSlotEntry[]>([])
|
|
93
|
+
registerNavigationShell(ctx, workbench, instance, globalPanels, async () => {
|
|
94
|
+
const { root } = await workbench.client.readWorkbenchInfo()
|
|
95
|
+
const workspace = dshWorkspaces.getSnapshot().items.find(item => root && isSamePath(item.path, root))
|
|
96
|
+
if (workspace) (await uiWorkspace).startSession(workspace.workspaceId)
|
|
105
97
|
})
|
|
106
98
|
const store = { ...handle, create: () => instance }
|
|
107
99
|
const mainKeys = () => ctx.slots.entries('main').flatMap(entry => (entry.options.key === undefined ? [] : [entry.options.key]))
|
|
@@ -115,16 +107,21 @@ export function apply(ctx: DshClientContext): void {
|
|
|
115
107
|
triggers[triggers.length - 1]?.click()
|
|
116
108
|
}
|
|
117
109
|
const disposePanelInfo = ctx.slots.provideRoot({ hooks: { panelInfo: layout.panelInfo } })
|
|
118
|
-
const disposeService = ctx.reflect.provide('layout', layout)
|
|
119
|
-
const nativeSidebar = window.__DSH_BOOT__?.entries.some(entry => entry.id === '@deepseek-ai/dsh-client-ui-sidebar')
|
|
120
110
|
const disposeRoot = ctx.slots.register({ name: 'root', locale: 'common', children: {
|
|
121
111
|
...ROOT_CHILDREN,
|
|
122
|
-
...(
|
|
123
|
-
}, store },
|
|
112
|
+
...(isBootEntryLoaded(UI_SIDEBAR) ? {} : { 'sidebar.panellist': { kind: 'list', scope: 'root' } as const }),
|
|
113
|
+
}, store }, rootLayoutFor({
|
|
114
|
+
workbench,
|
|
115
|
+
openSettings,
|
|
116
|
+
openDshWorkspace: id => uiWorkspace.then(service => service.openWorkspace(id, () => layout.preserveNextConversation())),
|
|
117
|
+
dshWorkspaces,
|
|
118
|
+
globalPanels,
|
|
119
|
+
}))
|
|
120
|
+
const disposeService = ctx.reflect.provide('layout', layout)
|
|
124
121
|
const binding = { code: 'KeyB', modifiers: ['primary', 'alt'] }
|
|
125
122
|
const disposeShortcut = ctx.shortcuts.register({
|
|
126
123
|
id: 'sidebar.left.toggle',
|
|
127
|
-
label: () =>
|
|
124
|
+
label: () => CLIENT_MESSAGES[workbench.state.locale].toggleSide,
|
|
128
125
|
aliases: ['sidebar', 'toggle left sidebar'],
|
|
129
126
|
defaults: {
|
|
130
127
|
'desktop:macos': { code: 'KeyB', modifiers: ['primary'] },
|
|
@@ -144,11 +141,17 @@ export function apply(ctx: DshClientContext): void {
|
|
|
144
141
|
disposeShortcut()
|
|
145
142
|
layout.dispose()
|
|
146
143
|
disposePanels()
|
|
144
|
+
disposeService()
|
|
147
145
|
disposeRoot()
|
|
148
146
|
disposePanelInfo()
|
|
149
|
-
disposeService()
|
|
150
147
|
}
|
|
151
148
|
}, 'presmium: the root layout and ctx.layout')
|
|
152
149
|
}
|
|
153
|
-
|
|
150
|
+
registerBrand(ctx, workbench)
|
|
151
|
+
registerMascot(ctx, workbench)
|
|
152
|
+
registerAuthorSettings(ctx)
|
|
153
|
+
registerSpeechSettings(ctx, workbench)
|
|
154
|
+
ctx.inject(['layout'], inner => inner.effect(() => connectLaunchTarget(workbench,
|
|
155
|
+
() => nativeLayout ? () => {} : restoreRootReturnPoint(workbench, reload.point),
|
|
156
|
+
() => showEditor(inner.layout)), 'presmium: launch target and root return point'))
|
|
154
157
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Workbench } from '@lincyaw/workbench'
|
|
2
|
+
import { restoreRootReturnPoint } from './root-reload.ts'
|
|
3
|
+
|
|
4
|
+
const PREFIX = '#presmium?'
|
|
5
|
+
const INTERACTIONS = ['pointerdown', 'keydown', 'beforeinput', 'wheel', 'touchmove']
|
|
6
|
+
|
|
7
|
+
interface LaunchTarget { directory: string, documentId: string, sectionId?: string }
|
|
8
|
+
|
|
9
|
+
/** This fragment belongs to Presmium; Harness's authenticated root and API routes keep their own paths. */
|
|
10
|
+
export function parseLaunchTarget(fragment: string): LaunchTarget | undefined {
|
|
11
|
+
if (!fragment.startsWith(PREFIX) || fragment.length > 8192) return
|
|
12
|
+
const params = new URLSearchParams(fragment.slice(PREFIX.length))
|
|
13
|
+
const keys = ['document', 'id', 'section', 'request']
|
|
14
|
+
if ([...params.keys()].some(key => !keys.includes(key)) || keys.some(key => params.getAll(key).length > 1)) return
|
|
15
|
+
const request = params.get('request')
|
|
16
|
+
if (request !== null && !/^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i.test(request)) return
|
|
17
|
+
const directory = params.get('document'), documentId = params.get('id'), sectionId = params.get('section')
|
|
18
|
+
if (directory === null || !documentId || sectionId === '' || /[\\:\u0000-\u001f\u007f]/.test(directory)
|
|
19
|
+
|| directory !== '' && directory.split('/').some(part => !part || part === '.' || part === '..')) return
|
|
20
|
+
return { directory, documentId, ...sectionId !== null ? { sectionId } : {} }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Explicit addresses outrank reload recovery; source identity and ordinary user navigation still own adoption. */
|
|
24
|
+
export function connectLaunchTarget(workbench: Workbench, restore: () => () => void, showDocument: () => void): () => void {
|
|
25
|
+
let closed = false, generation = 0
|
|
26
|
+
let stopPending: (() => void) | undefined, stopReturn: (() => void) | undefined
|
|
27
|
+
const open = (initial = false) => {
|
|
28
|
+
const request = ++generation, fragment = window.location.hash
|
|
29
|
+
stopPending?.(); stopPending = undefined
|
|
30
|
+
stopReturn?.()
|
|
31
|
+
if (initial && !fragment.startsWith(PREFIX)) { stopReturn = restore(); return }
|
|
32
|
+
// Keep the ordinary address retention, without opening an older checkpoint behind this explicit intent.
|
|
33
|
+
stopReturn = restoreRootReturnPoint(workbench, undefined)
|
|
34
|
+
const target = parseLaunchTarget(fragment)
|
|
35
|
+
if (!target) return
|
|
36
|
+
let active = true, ownOpening = false, committing = false
|
|
37
|
+
const beforeDocument = workbench.state.document
|
|
38
|
+
let navigation = workbench.documentNavigationVersion
|
|
39
|
+
let position = workbench.state.documentPositionToken, view = workbench.state.webView
|
|
40
|
+
const current = () => active && !closed && request === generation && window.location.hash === fragment
|
|
41
|
+
&& workbench.documentNavigationVersion === navigation && workbench.state.webView === view
|
|
42
|
+
const cancel = () => { active = false }
|
|
43
|
+
const interact = (event: Event) => { if (event.isTrusted) cancel() }
|
|
44
|
+
for (const name of INTERACTIONS) document.addEventListener(name, interact, true)
|
|
45
|
+
const stop = workbench.subscribe(() => {
|
|
46
|
+
if (committing) return
|
|
47
|
+
if (!current() || !ownOpening && workbench.state.documentPositionToken !== position) cancel()
|
|
48
|
+
})
|
|
49
|
+
const close = () => {
|
|
50
|
+
cancel(); stop()
|
|
51
|
+
for (const name of INTERACTIONS) document.removeEventListener(name, interact, true)
|
|
52
|
+
}
|
|
53
|
+
stopPending = close
|
|
54
|
+
const launch = async () => {
|
|
55
|
+
const [info] = await Promise.all([workbench.client.readWorkbenchInfo(), workbench.loadDocuments()])
|
|
56
|
+
if (!current() || !info.root || !workbench.state.documents?.documents.some(doc => doc.dir === target.directory)) return
|
|
57
|
+
// Only an already listed directory can be read. A stale ID or missing page never changes the current selection.
|
|
58
|
+
const answer = await workbench.client.readDocument(target.directory)
|
|
59
|
+
if (!current() || answer.content.id !== target.documentId
|
|
60
|
+
|| !workbench.state.documents?.documents.some(doc => doc.dir === target.directory)
|
|
61
|
+
|| target.sectionId && (!answer.content.sectionOrder.includes(target.sectionId) || !answer.content.sections[target.sectionId])) return
|
|
62
|
+
const identity = JSON.stringify([info.root, target.documentId])
|
|
63
|
+
ownOpening = true
|
|
64
|
+
committing = true
|
|
65
|
+
const opened = workbench.openDocument(target.directory, { expectedIdentity: identity })
|
|
66
|
+
navigation = workbench.documentNavigationVersion
|
|
67
|
+
position = workbench.state.documentPositionToken
|
|
68
|
+
view = workbench.state.webView
|
|
69
|
+
committing = false
|
|
70
|
+
await opened
|
|
71
|
+
if (!current() || workbench.state.document !== target.directory || workbench.state.authoring?.identity !== identity
|
|
72
|
+
|| workbench.state.authoring.content?.id !== target.documentId) return
|
|
73
|
+
const section = target.sectionId
|
|
74
|
+
if (section && (!workbench.state.authoring.content.sectionOrder.includes(section) || !workbench.state.authoring.content.sections[section])) return
|
|
75
|
+
// A retained document keeps its editing or playback view; a newly opened document starts in Doc.
|
|
76
|
+
close()
|
|
77
|
+
showDocument()
|
|
78
|
+
if (beforeDocument !== target.directory && workbench.state.webView === 'document' && section) workbench.goToSection(section)
|
|
79
|
+
}
|
|
80
|
+
void launch().catch(() => { /* Invalid or unavailable addresses leave the ordinary document UI available. */ }).finally(() => {
|
|
81
|
+
close()
|
|
82
|
+
if (stopPending === close) stopPending = undefined
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
const changed = () => open()
|
|
86
|
+
window.addEventListener('hashchange', changed)
|
|
87
|
+
open(true)
|
|
88
|
+
return () => { closed = true; generation++; stopPending?.(); stopReturn?.(); window.removeEventListener('hashchange', changed) }
|
|
89
|
+
}
|
package/src/client/layout.ts
CHANGED
|
@@ -1,58 +1,69 @@
|
|
|
1
1
|
import { defineStore } from '@deepseek-ai/dsh-client-store'
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const CENTRE_MIN = 400
|
|
4
4
|
export const RIGHTBAR_MIN = 300
|
|
5
5
|
export const RIGHTBAR_MAX_RATIO = 0.7
|
|
6
6
|
export const RIGHTBAR_DEFAULT_RATIO = 0.45
|
|
7
7
|
export const SIDE_COLLAPSE = 1024
|
|
8
8
|
export const NARROW = 768
|
|
9
|
-
export const ACTIVITY = 48
|
|
10
9
|
export const SIDE_DEFAULT = 264
|
|
11
10
|
export const SIDE_MIN = 200
|
|
12
11
|
export const SIDE_MAX = 420
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
12
|
+
export const CONVERSATION_DEFAULT = 400
|
|
13
|
+
export const CONVERSATION_MIN = 320
|
|
14
|
+
export const CONVERSATION_MAX = 640
|
|
15
|
+
/** The width of one resize handle between two columns: a hairline, with a wider area to grab. */
|
|
16
|
+
export const HANDLE = 1
|
|
16
17
|
|
|
17
18
|
export const clamp = (px: number, min: number, max: number) => Math.min(max, Math.max(min, Math.round(px)))
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
export
|
|
20
|
+
/** The width left for the rightbar column: the viewport less the side panel, the conversation, the centre's minimum and `handles` resize handles. */
|
|
21
|
+
export function rightbarRoom(viewport: number, side: number, conversation: number, handles: number): number {
|
|
22
|
+
return viewport - side - conversation - CENTRE_MIN - handles * HANDLE
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What the side panel shows: the chat history (DeepSeek Harness's sessions). */
|
|
26
|
+
export type Activity = 'sessions'
|
|
27
|
+
/** What a narrow screen shows, one at a time. */
|
|
28
|
+
export type NarrowPane = 'documents' | 'preview' | 'sessions' | 'conversation'
|
|
21
29
|
|
|
22
30
|
export interface LayoutState {
|
|
23
31
|
panelInfo: { activePanelId: string | null }
|
|
24
32
|
layoutInfo: {
|
|
25
33
|
viewportWidth: number
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
isSideOpen: boolean
|
|
35
|
+
/** On a screen narrower than SIDE_COLLAPSE the side panel opens over the centre. */
|
|
36
|
+
isSideExpanded: boolean
|
|
37
|
+
sideWidth: number
|
|
38
|
+
conversationWidth: number
|
|
39
|
+
isConversationOpen: boolean
|
|
40
|
+
rightbarWidth: number | null
|
|
41
|
+
isRightbarOpen: boolean
|
|
42
|
+
hasRightbarTrack: boolean
|
|
43
|
+
isRightbarFullscreen: boolean
|
|
35
44
|
activity: Activity
|
|
36
|
-
|
|
45
|
+
narrowPane: NarrowPane
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
const openConversationWidth = (info: LayoutState['layoutInfo']) => (info.isConversationOpen ? info.conversationWidth : 0)
|
|
50
|
+
|
|
40
51
|
function revealConversation(d: LayoutState): void {
|
|
41
52
|
d.panelInfo.activePanelId = null
|
|
42
|
-
d.layoutInfo.
|
|
43
|
-
d.layoutInfo.
|
|
44
|
-
if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) d.layoutInfo.
|
|
53
|
+
d.layoutInfo.isConversationOpen = true
|
|
54
|
+
d.layoutInfo.narrowPane = 'conversation'
|
|
55
|
+
if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) d.layoutInfo.isSideExpanded = false
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
function showHistory(d: LayoutState): void {
|
|
48
59
|
d.panelInfo.activePanelId = null
|
|
49
60
|
d.layoutInfo.activity = 'sessions'
|
|
50
|
-
d.layoutInfo.
|
|
51
|
-
d.layoutInfo.
|
|
61
|
+
d.layoutInfo.isSideOpen = true
|
|
62
|
+
d.layoutInfo.narrowPane = 'sessions'
|
|
52
63
|
if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) {
|
|
53
|
-
d.layoutInfo.
|
|
54
|
-
d.layoutInfo.
|
|
55
|
-
} else d.layoutInfo.
|
|
64
|
+
d.layoutInfo.isSideExpanded = d.layoutInfo.viewportWidth >= NARROW
|
|
65
|
+
d.layoutInfo.isConversationOpen = false
|
|
66
|
+
} else d.layoutInfo.isConversationOpen = true
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
export function createLayoutStore() {
|
|
@@ -61,17 +72,17 @@ export function createLayoutStore() {
|
|
|
61
72
|
panelInfo: { activePanelId: null },
|
|
62
73
|
layoutInfo: {
|
|
63
74
|
viewportWidth: window.innerWidth,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
activity: '
|
|
74
|
-
|
|
75
|
+
isSideOpen: false,
|
|
76
|
+
isSideExpanded: false,
|
|
77
|
+
sideWidth: SIDE_DEFAULT,
|
|
78
|
+
conversationWidth: CONVERSATION_DEFAULT,
|
|
79
|
+
isConversationOpen: false,
|
|
80
|
+
rightbarWidth: null,
|
|
81
|
+
isRightbarOpen: false,
|
|
82
|
+
hasRightbarTrack: false,
|
|
83
|
+
isRightbarFullscreen: false,
|
|
84
|
+
activity: 'sessions',
|
|
85
|
+
narrowPane: 'documents',
|
|
75
86
|
},
|
|
76
87
|
}),
|
|
77
88
|
actions: {
|
|
@@ -82,76 +93,78 @@ export function createLayoutStore() {
|
|
|
82
93
|
if (d.panelInfo.activePanelId !== null && !ids.includes(d.panelInfo.activePanelId)) d.panelInfo.activePanelId = null
|
|
83
94
|
},
|
|
84
95
|
setViewportWidth: (d: LayoutState, width: number) => {
|
|
85
|
-
if ((d.layoutInfo.viewportWidth < SIDE_COLLAPSE) !== (width < SIDE_COLLAPSE)) d.layoutInfo.
|
|
96
|
+
if ((d.layoutInfo.viewportWidth < SIDE_COLLAPSE) !== (width < SIDE_COLLAPSE)) d.layoutInfo.isSideExpanded = false
|
|
86
97
|
d.layoutInfo.viewportWidth = width
|
|
87
|
-
if (d.layoutInfo.
|
|
88
|
-
d.layoutInfo.
|
|
89
|
-
d.layoutInfo.
|
|
98
|
+
if (d.layoutInfo.hasRightbarTrack && rightbarRoom(width, d.layoutInfo.sideWidth, openConversationWidth(d.layoutInfo), 3) < RIGHTBAR_MIN) {
|
|
99
|
+
d.layoutInfo.isSideOpen = false
|
|
100
|
+
d.layoutInfo.isSideExpanded = false
|
|
90
101
|
}
|
|
91
102
|
},
|
|
92
103
|
toggleSidebar: (d: LayoutState) => {
|
|
93
104
|
if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) {
|
|
94
|
-
d.layoutInfo.
|
|
95
|
-
if (d.layoutInfo.
|
|
105
|
+
d.layoutInfo.isSideExpanded = !d.layoutInfo.isSideExpanded
|
|
106
|
+
if (d.layoutInfo.isSideExpanded) d.layoutInfo.isConversationOpen = false
|
|
96
107
|
}
|
|
97
|
-
else d.layoutInfo.
|
|
108
|
+
else d.layoutInfo.isSideOpen = !d.layoutInfo.isSideOpen
|
|
98
109
|
},
|
|
99
|
-
|
|
100
|
-
d.layoutInfo.
|
|
110
|
+
setSideWidth: (d: LayoutState, px: number) => {
|
|
111
|
+
d.layoutInfo.sideWidth = clamp(px, SIDE_MIN, SIDE_MAX)
|
|
101
112
|
},
|
|
102
|
-
|
|
103
|
-
d.layoutInfo.
|
|
113
|
+
setConversationWidth: (d: LayoutState, px: number) => {
|
|
114
|
+
d.layoutInfo.conversationWidth = clamp(px, CONVERSATION_MIN, Math.min(CONVERSATION_MAX, d.layoutInfo.viewportWidth * 0.5))
|
|
104
115
|
},
|
|
105
|
-
|
|
106
|
-
d.layoutInfo.
|
|
107
|
-
if (open && d.layoutInfo.viewportWidth < SIDE_COLLAPSE) d.layoutInfo.
|
|
116
|
+
setConversationOpen: (d: LayoutState, open: boolean) => {
|
|
117
|
+
d.layoutInfo.isConversationOpen = open
|
|
118
|
+
if (open && d.layoutInfo.viewportWidth < SIDE_COLLAPSE) d.layoutInfo.isSideExpanded = false
|
|
108
119
|
},
|
|
109
120
|
revealConversation,
|
|
110
121
|
showHistory,
|
|
111
122
|
toggleConversations: (d: LayoutState) => {
|
|
112
123
|
const info = d.layoutInfo
|
|
113
124
|
if (info.viewportWidth < NARROW) {
|
|
114
|
-
if (info.
|
|
125
|
+
if (info.narrowPane === 'conversation') showHistory(d)
|
|
115
126
|
else revealConversation(d)
|
|
116
127
|
} else {
|
|
117
|
-
const historyShown = info.activity === 'sessions' && (info.viewportWidth < SIDE_COLLAPSE ? info.
|
|
128
|
+
const historyShown = info.activity === 'sessions' && (info.viewportWidth < SIDE_COLLAPSE ? info.isSideExpanded : info.isSideOpen)
|
|
118
129
|
if (historyShown && info.viewportWidth < SIDE_COLLAPSE) revealConversation(d)
|
|
119
|
-
else if (historyShown && info.
|
|
130
|
+
else if (historyShown && info.isConversationOpen) info.isSideOpen = false
|
|
120
131
|
else showHistory(d)
|
|
121
132
|
}
|
|
122
133
|
},
|
|
123
|
-
|
|
124
|
-
d.layoutInfo.
|
|
134
|
+
setRightbarWidth: (d: LayoutState, px: number) => {
|
|
135
|
+
d.layoutInfo.rightbarWidth = clamp(px, RIGHTBAR_MIN, Math.max(RIGHTBAR_MIN, d.layoutInfo.viewportWidth * RIGHTBAR_MAX_RATIO))
|
|
125
136
|
},
|
|
126
137
|
openRightbar: (d: LayoutState, track: boolean, fullscreen: boolean) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
const info = d.layoutInfo
|
|
139
|
+
if (track && rightbarRoom(info.viewportWidth, info.sideWidth, openConversationWidth(info), 3) < RIGHTBAR_MIN) {
|
|
140
|
+
info.isSideOpen = false
|
|
141
|
+
info.isSideExpanded = false
|
|
130
142
|
}
|
|
131
|
-
if (track &&
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
if (track && rightbarRoom(info.viewportWidth, 0, openConversationWidth(info), 2) < RIGHTBAR_MIN) info.isConversationOpen = false
|
|
144
|
+
info.rightbarWidth ??= Math.max(RIGHTBAR_MIN, Math.round(info.viewportWidth * RIGHTBAR_DEFAULT_RATIO * 0.6))
|
|
145
|
+
info.isRightbarOpen = true
|
|
146
|
+
info.hasRightbarTrack = track
|
|
147
|
+
info.isRightbarFullscreen = fullscreen
|
|
136
148
|
},
|
|
137
149
|
closeRightbar: (d: LayoutState) => {
|
|
138
|
-
d.layoutInfo.
|
|
139
|
-
d.layoutInfo.
|
|
140
|
-
d.layoutInfo.
|
|
150
|
+
d.layoutInfo.isRightbarOpen = false
|
|
151
|
+
d.layoutInfo.hasRightbarTrack = false
|
|
152
|
+
d.layoutInfo.isRightbarFullscreen = false
|
|
141
153
|
},
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
/** An activity of the activity bar: opens the side panel on it, or toggles it on a wide screen. */
|
|
155
|
+
selectActivity: (d: LayoutState, activity: Activity) => {
|
|
156
|
+
if (d.layoutInfo.activity === activity && d.layoutInfo.viewportWidth >= SIDE_COLLAPSE) d.layoutInfo.isSideOpen = !d.layoutInfo.isSideOpen
|
|
157
|
+
else d.layoutInfo.isSideOpen = true
|
|
145
158
|
d.layoutInfo.activity = activity
|
|
146
159
|
if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) {
|
|
147
|
-
d.layoutInfo.
|
|
148
|
-
if (d.layoutInfo.
|
|
160
|
+
d.layoutInfo.isSideExpanded = d.layoutInfo.viewportWidth >= NARROW
|
|
161
|
+
if (d.layoutInfo.isSideExpanded) d.layoutInfo.isConversationOpen = false
|
|
149
162
|
}
|
|
150
|
-
d.layoutInfo.
|
|
163
|
+
d.layoutInfo.narrowPane = activity
|
|
151
164
|
},
|
|
152
|
-
|
|
153
|
-
d.layoutInfo.
|
|
154
|
-
d.layoutInfo.
|
|
165
|
+
showNarrowPane: (d: LayoutState, pane: NarrowPane) => {
|
|
166
|
+
d.layoutInfo.narrowPane = pane
|
|
167
|
+
d.layoutInfo.isSideExpanded = false
|
|
155
168
|
},
|
|
156
169
|
},
|
|
157
170
|
})
|
|
@@ -186,11 +199,11 @@ export class LayoutController {
|
|
|
186
199
|
this.#preserveNextConversation = true
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
/**
|
|
202
|
+
/** A link to a place of the document shows the document without revealing the conversation. */
|
|
190
203
|
showDocument(): void {
|
|
191
204
|
this.#navigation.abort()
|
|
192
205
|
this.#actions.selectPanel(null)
|
|
193
|
-
this.#actions.
|
|
206
|
+
this.#actions.showNarrowPane('preview')
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
beginNavigation(): AbortSignal {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PmMascot, PmSegmented, setMascotEnabled, useAssistantMascot, useMascotEnabled, WorkbenchProvider } from '@lincyaw/workbench'
|
|
2
|
+
import type { Workbench } from '@lincyaw/workbench'
|
|
3
|
+
import { MASCOT_SETTING, MASCOT_SLOT } from './contract.ts'
|
|
4
|
+
import { useClientMessages } from './messages.ts'
|
|
5
|
+
|
|
6
|
+
/** The session header's standard prop: a selector over the bound session's snapshot. */
|
|
7
|
+
type UseSession = <T>(select: (snapshot: { running?: boolean }) => T) => T
|
|
8
|
+
|
|
9
|
+
function Kitten({ running }: { running: boolean }) {
|
|
10
|
+
return <span className="pmw-conversation-mascot"><PmMascot size="small" action={useAssistantMascot(running)} /></span>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function MascotSetting({ workbench }: { workbench: Workbench }) {
|
|
14
|
+
const t = useClientMessages(workbench)
|
|
15
|
+
const on = useMascotEnabled()
|
|
16
|
+
return (
|
|
17
|
+
<div className="pmw-mascot-setting pm-ui" data-pmw-setting="mascot">
|
|
18
|
+
<div className="pmw-mascot-setting__copy">
|
|
19
|
+
<p className="pmw-mascot-setting__title">{t.mascot}</p>
|
|
20
|
+
<p className="pmw-mascot-setting__hint">{t.mascotHint}</p>
|
|
21
|
+
</div>
|
|
22
|
+
<PmSegmented label={t.mascot} value={on ? 'show' : 'hide'} options={[{ value: 'show', label: t.mascotShow }, { value: 'hide', label: t.mascotHide }]} onChange={value => setMascotEnabled(value === 'show')} />
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Puts the kitten in the conversation's header, where it acts out what the assistant is doing, and its switch in
|
|
28
|
+
* Settings → General. */
|
|
29
|
+
export function registerMascot(ctx: DshClientContext, workbench: Workbench): void {
|
|
30
|
+
ctx.effect(() => ctx.slots.inject(MASCOT_SLOT.name, () => ctx.slots.register({ name: MASCOT_SLOT.name, id: MASCOT_SLOT.id, order: -100 }, ({ useSession }: { useSession: UseSession }) => {
|
|
31
|
+
const running = useSession(snapshot => Boolean(snapshot.running))
|
|
32
|
+
return <WorkbenchProvider workbench={workbench}><Kitten running={running} /></WorkbenchProvider>
|
|
33
|
+
})), 'presmium: the kitten beside the conversation')
|
|
34
|
+
ctx.effect(() => ctx.slots.inject(MASCOT_SETTING.name, () => ctx.slots.register({ name: MASCOT_SETTING.name, id: MASCOT_SETTING.id, order: 1000 }, () => (
|
|
35
|
+
<WorkbenchProvider workbench={workbench}><MascotSetting workbench={workbench} /></WorkbenchProvider>
|
|
36
|
+
))), 'presmium: the kitten\'s switch in Settings')
|
|
37
|
+
}
|