@lincyaw/dsh 0.0.0-stage → 0.1.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 (55) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +22 -2
  3. package/build/client.mjs +66 -0
  4. package/build/dist/client.js +90 -0
  5. package/build/dist/client.json +70 -0
  6. package/build/ensure.mjs +30 -0
  7. package/cordis.patch.yml +17 -0
  8. package/package.json +95 -3
  9. package/profile.patch.yml +25 -0
  10. package/rows/context.mjs +7 -0
  11. package/rows/load.mjs +8 -0
  12. package/rows/policy.mjs +7 -0
  13. package/rows/routes.mjs +7 -0
  14. package/rows/service.mjs +7 -0
  15. package/rows/skills.mjs +7 -0
  16. package/rows/tools.mjs +7 -0
  17. package/rows/turns.mjs +7 -0
  18. package/rows/workbench.mjs +9 -0
  19. package/rows/workspaces.mjs +7 -0
  20. package/skills/presmium-author/SKILL.md +95 -0
  21. package/skills/presmium-author/references/assistance.md +27 -0
  22. package/skills/presmium-author/references/code.md +52 -0
  23. package/skills/presmium-author/references/directives.md +592 -0
  24. package/skills/presmium-author/references/publishing.md +60 -0
  25. package/skills/presmium-author/references/writing.md +72 -0
  26. package/skills/presmium-present/SKILL.md +69 -0
  27. package/skills/presmium-present/references/classroom.md +46 -0
  28. package/skills/presmium-present/references/delivery.md +63 -0
  29. package/skills/presmium-present/references/lecture.md +58 -0
  30. package/src/client/Frame.tsx +314 -0
  31. package/src/client/brand.tsx +90 -0
  32. package/src/client/chrome.tsx +79 -0
  33. package/src/client/contract.ts +35 -0
  34. package/src/client/dsh.d.ts +129 -0
  35. package/src/client/fallback.tsx +49 -0
  36. package/src/client/frame.css +345 -0
  37. package/src/client/index.tsx +154 -0
  38. package/src/client/layout.ts +217 -0
  39. package/src/client/messages.ts +78 -0
  40. package/src/client/source.tsx +81 -0
  41. package/src/client/styles.css +3 -0
  42. package/src/client/theme.ts +149 -0
  43. package/src/node/context.ts +55 -0
  44. package/src/node/dsh.ts +93 -0
  45. package/src/node/launch.ts +59 -0
  46. package/src/node/policy.ts +59 -0
  47. package/src/node/routes.ts +46 -0
  48. package/src/node/service.ts +280 -0
  49. package/src/node/skills.ts +46 -0
  50. package/src/node/tools.ts +43 -0
  51. package/src/node/turns/index.ts +165 -0
  52. package/src/node/watchdog.mjs +36 -0
  53. package/src/node/workbench.ts +43 -0
  54. package/src/node/workspaces.ts +10 -0
  55. package/web.patch.yml +190 -0
@@ -0,0 +1,217 @@
1
+ import { defineStore } from '@deepseek-ai/dsh-client-store'
2
+
3
+ export const CENTER_MIN = 400
4
+ export const RIGHTBAR_MIN = 300
5
+ export const RIGHTBAR_MAX_RATIO = 0.7
6
+ export const RIGHTBAR_DEFAULT_RATIO = 0.45
7
+ export const SIDE_COLLAPSE = 1024
8
+ export const NARROW = 768
9
+ export const ACTIVITY = 48
10
+ export const SIDE_DEFAULT = 264
11
+ export const SIDE_MIN = 200
12
+ export const SIDE_MAX = 420
13
+ export const DOCK_DEFAULT = 400
14
+ export const DOCK_MIN = 320
15
+ export const DOCK_MAX = 640
16
+
17
+ export const clamp = (px: number, min: number, max: number) => Math.min(max, Math.max(min, Math.round(px)))
18
+
19
+ export type Activity = 'documents' | 'files' | 'changes' | 'sessions'
20
+ export type NarrowView = 'documents' | 'files' | 'source' | 'preview' | 'changes' | 'sessions' | 'conversation'
21
+
22
+ export interface LayoutState {
23
+ panelInfo: { activePanelId: string | null }
24
+ layoutInfo: {
25
+ viewportWidth: number
26
+ sideOpen: boolean
27
+ sideExpanded: boolean
28
+ side: number
29
+ dock: number
30
+ dockOpen: boolean
31
+ rightbar: number | null
32
+ rightbarShown: boolean
33
+ rightbarTrack: boolean
34
+ rightbarFullscreen: boolean
35
+ activity: Activity
36
+ view: NarrowView
37
+ }
38
+ }
39
+
40
+ function revealConversation(d: LayoutState): void {
41
+ d.panelInfo.activePanelId = null
42
+ d.layoutInfo.dockOpen = true
43
+ d.layoutInfo.view = 'conversation'
44
+ if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) d.layoutInfo.sideExpanded = false
45
+ }
46
+
47
+ function showHistory(d: LayoutState): void {
48
+ d.panelInfo.activePanelId = null
49
+ d.layoutInfo.activity = 'sessions'
50
+ d.layoutInfo.sideOpen = true
51
+ d.layoutInfo.view = 'sessions'
52
+ if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) {
53
+ d.layoutInfo.sideExpanded = d.layoutInfo.viewportWidth >= NARROW
54
+ d.layoutInfo.dockOpen = false
55
+ } else d.layoutInfo.dockOpen = true
56
+ }
57
+
58
+ export function createLayoutStore() {
59
+ return defineStore({
60
+ init: (): LayoutState => ({
61
+ panelInfo: { activePanelId: null },
62
+ layoutInfo: {
63
+ viewportWidth: window.innerWidth,
64
+ sideOpen: false,
65
+ sideExpanded: false,
66
+ side: SIDE_DEFAULT,
67
+ dock: DOCK_DEFAULT,
68
+ dockOpen: true,
69
+ rightbar: null,
70
+ rightbarShown: false,
71
+ rightbarTrack: false,
72
+ rightbarFullscreen: false,
73
+ activity: 'files',
74
+ view: 'documents',
75
+ },
76
+ }),
77
+ actions: {
78
+ selectPanel: (d: LayoutState, id: string | null) => {
79
+ d.panelInfo.activePanelId = id
80
+ },
81
+ retainMainPanels: (d: LayoutState, ids: readonly string[]) => {
82
+ if (d.panelInfo.activePanelId !== null && !ids.includes(d.panelInfo.activePanelId)) d.panelInfo.activePanelId = null
83
+ },
84
+ setViewportWidth: (d: LayoutState, width: number) => {
85
+ if ((d.layoutInfo.viewportWidth < SIDE_COLLAPSE) !== (width < SIDE_COLLAPSE)) d.layoutInfo.sideExpanded = false
86
+ d.layoutInfo.viewportWidth = width
87
+ if (d.layoutInfo.rightbarTrack && width - ACTIVITY - d.layoutInfo.side - (d.layoutInfo.dockOpen ? d.layoutInfo.dock : 0) - CENTER_MIN - 24 < RIGHTBAR_MIN) {
88
+ d.layoutInfo.sideOpen = false
89
+ d.layoutInfo.sideExpanded = false
90
+ }
91
+ },
92
+ toggleSidebar: (d: LayoutState) => {
93
+ if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) {
94
+ d.layoutInfo.sideExpanded = !d.layoutInfo.sideExpanded
95
+ if (d.layoutInfo.sideExpanded) d.layoutInfo.dockOpen = false
96
+ }
97
+ else d.layoutInfo.sideOpen = !d.layoutInfo.sideOpen
98
+ },
99
+ setSide: (d: LayoutState, px: number) => {
100
+ d.layoutInfo.side = clamp(px, SIDE_MIN, SIDE_MAX)
101
+ },
102
+ setDock: (d: LayoutState, px: number) => {
103
+ d.layoutInfo.dock = clamp(px, DOCK_MIN, Math.min(DOCK_MAX, d.layoutInfo.viewportWidth * 0.5))
104
+ },
105
+ setDockOpen: (d: LayoutState, open: boolean) => {
106
+ d.layoutInfo.dockOpen = open
107
+ if (open && d.layoutInfo.viewportWidth < SIDE_COLLAPSE) d.layoutInfo.sideExpanded = false
108
+ },
109
+ revealConversation,
110
+ showHistory,
111
+ toggleConversations: (d: LayoutState) => {
112
+ const info = d.layoutInfo
113
+ if (info.viewportWidth < NARROW) {
114
+ if (info.view === 'conversation') showHistory(d)
115
+ else revealConversation(d)
116
+ } else {
117
+ const historyShown = info.activity === 'sessions' && (info.viewportWidth < SIDE_COLLAPSE ? info.sideExpanded : info.sideOpen)
118
+ if (historyShown && info.viewportWidth < SIDE_COLLAPSE) revealConversation(d)
119
+ else if (historyShown && info.dockOpen) info.sideOpen = false
120
+ else showHistory(d)
121
+ }
122
+ },
123
+ setRightbar: (d: LayoutState, px: number) => {
124
+ d.layoutInfo.rightbar = clamp(px, RIGHTBAR_MIN, Math.max(RIGHTBAR_MIN, d.layoutInfo.viewportWidth * RIGHTBAR_MAX_RATIO))
125
+ },
126
+ openRightbar: (d: LayoutState, track: boolean, fullscreen: boolean) => {
127
+ if (track && d.layoutInfo.viewportWidth - ACTIVITY - d.layoutInfo.side - (d.layoutInfo.dockOpen ? d.layoutInfo.dock : 0) - CENTER_MIN - 24 < RIGHTBAR_MIN) {
128
+ d.layoutInfo.sideOpen = false
129
+ d.layoutInfo.sideExpanded = false
130
+ }
131
+ if (track && d.layoutInfo.viewportWidth - ACTIVITY - (d.layoutInfo.dockOpen ? d.layoutInfo.dock : 0) - CENTER_MIN - 16 < RIGHTBAR_MIN) d.layoutInfo.dockOpen = false
132
+ d.layoutInfo.rightbar ??= Math.max(RIGHTBAR_MIN, Math.round(d.layoutInfo.viewportWidth * RIGHTBAR_DEFAULT_RATIO * 0.6))
133
+ d.layoutInfo.rightbarShown = true
134
+ d.layoutInfo.rightbarTrack = track
135
+ d.layoutInfo.rightbarFullscreen = fullscreen
136
+ },
137
+ closeRightbar: (d: LayoutState) => {
138
+ d.layoutInfo.rightbarShown = false
139
+ d.layoutInfo.rightbarTrack = false
140
+ d.layoutInfo.rightbarFullscreen = false
141
+ },
142
+ choose: (d: LayoutState, activity: Activity) => {
143
+ if (d.layoutInfo.activity === activity && d.layoutInfo.viewportWidth >= SIDE_COLLAPSE) d.layoutInfo.sideOpen = !d.layoutInfo.sideOpen
144
+ else d.layoutInfo.sideOpen = true
145
+ d.layoutInfo.activity = activity
146
+ if (d.layoutInfo.viewportWidth < SIDE_COLLAPSE) {
147
+ d.layoutInfo.sideExpanded = d.layoutInfo.viewportWidth >= NARROW
148
+ if (d.layoutInfo.sideExpanded) d.layoutInfo.dockOpen = false
149
+ }
150
+ d.layoutInfo.view = activity
151
+ },
152
+ show: (d: LayoutState, view: NarrowView) => {
153
+ d.layoutInfo.view = view
154
+ d.layoutInfo.sideExpanded = false
155
+ },
156
+ },
157
+ })
158
+ }
159
+
160
+ export type LayoutStore = ReturnType<typeof createLayoutStore>
161
+ export type LayoutActions = ReturnType<LayoutStore['create']>['actions']
162
+
163
+ export class LayoutController {
164
+ #navigation = new AbortController()
165
+ #preserveNextConversation = false
166
+ readonly #actions: LayoutActions
167
+ readonly #hasMainPanel: (id: string) => boolean
168
+ readonly panelInfo: DshObservable<LayoutState['panelInfo']>
169
+
170
+ constructor(actions: LayoutActions, hasMainPanel: (id: string) => boolean, panelInfo: DshObservable<LayoutState['panelInfo']>) {
171
+ this.#actions = actions
172
+ this.#hasMainPanel = hasMainPanel
173
+ this.panelInfo = panelInfo
174
+ }
175
+
176
+ selectPanel(id: string | null): void {
177
+ if (id !== null && !this.#hasMainPanel(id)) throw new Error(`layout.selectPanel: main panel "${id}" is not registered`)
178
+ this.#navigation.abort()
179
+ this.#actions.selectPanel(id)
180
+ if (id === null && !this.#preserveNextConversation) this.#actions.revealConversation()
181
+ this.#preserveNextConversation = false
182
+ }
183
+
184
+ /** Initial workspace binding must not replace the author's current view. Called by openWorkspace's beforeOpen. */
185
+ preserveNextConversation(): void {
186
+ this.#preserveNextConversation = true
187
+ }
188
+
189
+ /** Source navigation selects the document without revealing the conversation. */
190
+ showDocument(): void {
191
+ this.#navigation.abort()
192
+ this.#actions.selectPanel(null)
193
+ this.#actions.show('source')
194
+ }
195
+
196
+ beginNavigation(): AbortSignal {
197
+ this.#navigation.abort()
198
+ this.#navigation = new AbortController()
199
+ return this.#navigation.signal
200
+ }
201
+
202
+ toggleSidebar(): void {
203
+ this.#actions.toggleSidebar()
204
+ }
205
+
206
+ openRightbar(track: boolean, fullscreen: boolean): void {
207
+ this.#actions.openRightbar(track, fullscreen)
208
+ }
209
+
210
+ closeRightbar(): void {
211
+ this.#actions.closeRightbar()
212
+ }
213
+
214
+ dispose(): void {
215
+ this.#navigation.abort()
216
+ }
217
+ }
@@ -0,0 +1,78 @@
1
+ import type { Locale } from '@lincyaw/workbench'
2
+
3
+ const zh = {
4
+ product: 'Presmium',
5
+ activity: '活动栏',
6
+ skip: '跳到内容',
7
+ documents: '文档',
8
+ files: '文件',
9
+ sessions: '会话',
10
+ conversation: '对话',
11
+ newConversation: '新建对话',
12
+ conversationHistory: '历史对话',
13
+ closeHistory: '收起历史对话',
14
+ tools: '工具',
15
+ hideConversation: '收起对话',
16
+ source: '源码',
17
+ preview: '预览',
18
+ changes: '修改',
19
+ settings: '设置',
20
+ help: '使用说明',
21
+ close: '关闭',
22
+ toggleSide: '展开/收起侧栏',
23
+ dark: '切换明暗',
24
+ side: '侧栏',
25
+ status: '状态栏',
26
+ resize: '拖动调整宽度',
27
+ centre: '工作区',
28
+ welcomeTitle: '欢迎使用 Presmium',
29
+ welcome: '打开讲义即可预览,按需要切换源码或并排查看。右侧对话可以收起;选中文字或点选预览中的块,就能把关注内容带给 AI。不配模型也能编辑、保存与预览。',
30
+ start: '开始使用',
31
+ models: '设置模型',
32
+ helpText: '文档顶部可选“预览”“并排”“源码”;对话顶部可新建对话、打开历史。活动栏统一使用“历史对话”入口,选择历史会话会直接显示聊天,收起后也可从这里恢复。Mod+Alt+B 收起侧栏。选中文字或用“点选”选择一块,顶部显示带给 AI 的关注内容;可用关闭按钮清除。',
33
+ }
34
+
35
+ export type FrameMessages = typeof zh
36
+
37
+ const en: FrameMessages = {
38
+ product: 'Presmium',
39
+ activity: 'Activity bar',
40
+ skip: 'Skip to content',
41
+ documents: 'Documents',
42
+ files: 'Files',
43
+ sessions: 'Sessions',
44
+ conversation: 'Conversation',
45
+ newConversation: 'New chat',
46
+ conversationHistory: 'Chat history',
47
+ closeHistory: 'Hide chat history',
48
+ tools: 'Tools',
49
+ hideConversation: 'Hide conversation',
50
+ source: 'Source',
51
+ preview: 'Preview',
52
+ changes: 'Changes',
53
+ settings: 'Settings',
54
+ help: 'How to use',
55
+ close: 'Close',
56
+ toggleSide: 'Toggle the side panel',
57
+ dark: 'Toggle light and dark',
58
+ side: 'Side panel',
59
+ status: 'Status bar',
60
+ resize: 'Drag to resize',
61
+ centre: 'Workspace',
62
+ welcomeTitle: 'Welcome to Presmium',
63
+ welcome: 'Open a lecture to preview it, and switch to source or a side by side view when needed. The conversation can be hidden. Select text or pick a block to give the AI context. Editing, saving and previewing need no model.',
64
+ start: 'Start',
65
+ models: 'Set up a model',
66
+ helpText: 'Choose Preview, Side by side or Source above the document. Start a new chat or open history from the conversation header. Chat history in the activity bar is the shared entry for history and chat. Selecting a history row reveals its conversation, including when chat was hidden. Mod+Alt+B toggles the sidebar. Select text or pick a block to add conversation context, shown above the document. Clear it with its close button.',
67
+ }
68
+
69
+ export const FRAME_MESSAGES: Record<Locale, FrameMessages> = { 'zh-CN': zh, 'en': en }
70
+
71
+ export const BRAND_COPY: Record<'zh-CN' | 'en-US', Record<string, Record<string, string>>> = {
72
+ 'zh-CN': {
73
+ conversation: { 'hero.headline': '写讲义,从这里开始', 'hero.preview': '工作台' },
74
+ },
75
+ 'en-US': {
76
+ conversation: { 'hero.headline': 'Write your lecture', 'hero.preview': 'Studio' },
77
+ },
78
+ }
@@ -0,0 +1,81 @@
1
+ import { isAbsoluteWorkspacePath, parseFileAddress, resolveWorkspacePath, workspaceTitleOf } from '@deepseek-ai/dsh-util-workspace-path'
2
+ import type { Workbench } from '@lincyaw/workbench'
3
+ import { useLayoutEffect, useRef, useSyncExternalStore } from 'react'
4
+
5
+ const ID = '@lincyaw/dsh/source'
6
+ // These formats keep Harness's rich previews. Text files share the workbench editor.
7
+ const PREVIEW = /\.(?:png|jpe?g|gif|webp|avif|ico|bmp|svg|pdf|html?|csv|tsv|wasm|zip|gz|tgz|bz2|xz|7z|woff2?|ttf|otf|eot|mp3|mp4|m4a|wav|ogg|oga|webm|mov|avi|mkv|bin|so|dylib|dll|exe|pyc|xlsx?|docx?|pptx?|parquet|npy|npz|pkl|db|sqlite)$/i
8
+ const RESERVED = new Set(['node_modules', 'dist', '.git', '.presmium'])
9
+
10
+ function normalized(path: string): string | undefined {
11
+ const parts: string[] = []
12
+ for (const part of path.replace(/\\/g, '/').split('/')) {
13
+ if (!part || part === '.') continue
14
+ if (part === '..') {
15
+ if (!parts.length || parts.at(-1)?.endsWith(':')) return
16
+ parts.pop()
17
+ } else parts.push(part)
18
+ }
19
+ return `${path.startsWith('/') ? '/' : ''}${parts.join('/')}`
20
+ }
21
+
22
+ /** Use the Harness resolver for file links and line references, then open the canonical editor. */
23
+ export function sourceLinks(ctx: DshClientContext, workbench: Workbench, showEditor: (layout: DshClientContext['layout']) => void): void {
24
+ ctx.inject(['sidebarRightTabs', 'sidebarRight', 'sessions', 'layout'], inner => inner.effect(() => {
25
+ let root: string | undefined
26
+ let active = true
27
+ void workbench.client.workbench().then(answer => { if (active && answer.root) root = normalized(answer.root) }).catch(() => {})
28
+ const locate = (address: string): { path: string, document: string } | undefined => {
29
+ const file = parseFileAddress(address)
30
+ if (!root || file?.scope !== 'session') return
31
+ const session = inner.sessions.list.getSnapshot().byId[file.sessionId]
32
+ if (!session?.cwd || normalized(session.cwd) !== root || PREVIEW.test(file.path)) return
33
+ const absolute = normalized(resolveWorkspacePath(session.cwd, file.path))
34
+ if (!absolute || !isAbsoluteWorkspacePath(absolute) || !absolute.startsWith(`${root}/`)) return
35
+ const path = absolute.slice(root.length + 1)
36
+ if (path.split('/').some(part => RESERVED.has(part))) return
37
+ const documents = workbench.state.documents
38
+ if (!documents) return
39
+ const document = [...documents.documents].sort((a, b) => b.dir.length - a.dir.length).find(doc => !doc.dir || path.startsWith(`${doc.dir}/`))?.dir
40
+ if (document === undefined && documents.single) return
41
+ return { path, document: document ?? '' }
42
+ }
43
+ function SourceLink({ useTabInfo }: DshSourceTabProps) {
44
+ const { tab } = useTabInfo()
45
+ const opened = useRef<number>()
46
+ useLayoutEffect(() => {
47
+ if (!tab.visible || opened.current === tab.navigation.revision) return
48
+ const target = locate(tab.contentId)
49
+ if (!target) return
50
+ opened.current = tab.navigation.revision
51
+ const line = tab.navigation.params?.line
52
+ tab.actions.close()
53
+ if (inner.sidebarRight.isExpanded()) inner.sidebarRight.toggleExpanded()
54
+ showEditor(inner.layout)
55
+ void workbench.openSource(target.document, target.path, typeof line === 'number' && Number.isInteger(line) && line > 0 ? { line, mark: true } : undefined)
56
+ }, [tab.visible, tab.contentId, tab.navigation.revision])
57
+ return <span data-pmw-source-route="" hidden />
58
+ }
59
+ // Previously saved Harness text tabs keep their kind when restored. Route those too.
60
+ function RestoredSource({ absolutePath }: { absolutePath: string }) {
61
+ const documents = useSyncExternalStore(workbench.subscribe, () => workbench.state.documents)
62
+ useLayoutEffect(() => {
63
+ const tab = inner.sidebarRight.active()
64
+ if (tab?.kind !== 'text') return
65
+ const target = locate(tab.contentId)
66
+ if (!target || normalized(absolutePath) !== normalized(resolveWorkspacePath(root, target.path))) return
67
+ inner.sidebarRight.close(tab.id)
68
+ if (inner.sidebarRight.isExpanded()) inner.sidebarRight.toggleExpanded()
69
+ showEditor(inner.layout)
70
+ void workbench.openSource(target.document, target.path)
71
+ }, [absolutePath, documents])
72
+ return null
73
+ }
74
+ const offType = inner.sidebarRightTabs.register({ id: ID, kind: 'presmium-source-link', priority: 'extension', patterns: ['dsh-resource://file/session/**'], canOpen: address => Boolean(locate(address)), title: (address = '') => workspaceTitleOf(parseFileAddress(address)?.path ?? address) })
75
+ let offBody: (() => void) | undefined
76
+ inner.slots.inject('sidebar.right.pane.tab', () => { offBody = inner.slots.register({ name: 'sidebar.right.pane.tab', key: ID }, SourceLink) })
77
+ let offRestored: (() => void) | undefined
78
+ inner.slots.inject('sidebar.right.tab.document.actions', () => { offRestored = inner.slots.register({ name: 'sidebar.right.tab.document.actions', id: ID }, RestoredSource) })
79
+ return () => { active = false; offRestored?.(); offBody?.(); offType() }
80
+ }, 'presmium: shared source editor'))
81
+ }
@@ -0,0 +1,3 @@
1
+ @import '@lincyaw/design/design.css';
2
+ @import '@lincyaw/workbench/styles.css';
3
+ @import './frame.css';
@@ -0,0 +1,149 @@
1
+ import { color } from '@lincyaw/design'
2
+ import type { ColorName } from '@lincyaw/design'
3
+
4
+ type Pair = { light: string, dark: string }
5
+ const same = (value: string): Pair => ({ light: value, dark: value })
6
+ const pm = (name: ColorName): Pair => same(color(name))
7
+ const mix = (name: ColorName, percent: number, other = 'transparent'): Pair => same(`color-mix(in srgb, ${color(name)} ${percent}%, ${other})`)
8
+
9
+ export const TOKENS: Record<string, Pair> = {
10
+ '--dsw-alias-bg-base': pm('bg'),
11
+ '--dsw-alias-bg-document-preview': pm('surface'),
12
+ '--dsw-alias-bg-layer-1': pm('surface'),
13
+ '--dsw-alias-bg-layer-2': pm('surface'),
14
+ '--dsw-alias-bg-layer-3': pm('sunk'),
15
+ '--dsw-alias-bg-mask-1': { light: 'rgba(28, 27, 25, 0.32)', dark: 'rgba(0, 0, 0, 0.55)' },
16
+ '--dsw-alias-bg-mask-drop': { light: 'rgba(247, 245, 240, 0.7)', dark: 'rgba(22, 22, 20, 0.7)' },
17
+ '--dsw-alias-bg-module-platform': pm('sunk'),
18
+ '--dsw-alias-bg-overlay': pm('surface'),
19
+ '--dsw-alias-bg-skeleton': mix('ink', 6),
20
+ '--dsw-alias-border-inverted': same('transparent'),
21
+ '--dsw-alias-border-l1': pm('line'),
22
+ '--dsw-alias-border-l2': pm('line'),
23
+ '--dsw-alias-border-l2-darkmode-thin': pm('line'),
24
+ '--dsw-alias-border-l3': pm('line'),
25
+ '--dsw-alias-border-l4': pm('line-strong'),
26
+ '--dsw-alias-brand-primary': pm('ink'),
27
+ '--dsw-alias-brand-primary-new-colorprimary-new-color': pm('accent'),
28
+ '--dsw-alias-button-contrast-fill': pm('ink'),
29
+ '--dsw-alias-button-elevated-fill': pm('surface'),
30
+ '--dsw-alias-button-floating-fill': pm('surface'),
31
+ '--dsw-alias-button-floating-hover': pm('sunk'),
32
+ '--dsw-alias-button-ghost-active-border': pm('line-strong'),
33
+ '--dsw-alias-button-ghost-active-fill': pm('selected'),
34
+ '--dsw-alias-button-info-fill': { light: '#0f5f5c', dark: '#2c7a74' },
35
+ '--dsw-alias-button-info-hover': { light: '#0b4a47', dark: '#256963' },
36
+ '--dsw-alias-button-primary-fill': pm('ink'),
37
+ '--dsw-alias-button-primary-hover': mix('ink', 85, color('bg')),
38
+ '--dsw-alias-file-diff-added-bg': pm('ok-soft'),
39
+ '--dsw-alias-file-diff-added-gutter': pm('ok-soft'),
40
+ '--dsw-alias-file-diff-added-marker': pm('add'),
41
+ '--dsw-alias-file-diff-deleted-bg': pm('danger-soft'),
42
+ '--dsw-alias-file-diff-deleted-gutter': pm('danger-soft'),
43
+ '--dsw-alias-file-diff-deleted-marker': pm('del'),
44
+ '--dsw-alias-interactive-bg-active': pm('selected'),
45
+ '--dsw-alias-interactive-bg-hover': mix('ink', 6),
46
+ '--dsw-alias-interactive-bg-hover-danger': pm('danger-soft'),
47
+ '--dsw-alias-interactive-bg-hover-solid': pm('sunk'),
48
+ '--dsw-alias-label-caption': pm('muted'),
49
+ '--dsw-alias-label-deep-diving': pm('accent'),
50
+ '--dsw-alias-label-deep-diving-shimmer': pm('muted'),
51
+ '--dsw-alias-label-dimmed': pm('line-strong'),
52
+ '--dsw-alias-label-primary': pm('ink'),
53
+ '--dsw-alias-label-primary-bluish': pm('ink'),
54
+ '--dsw-alias-label-primary-dimmed': pm('ink'),
55
+ '--dsw-alias-label-primary-foreground': pm('bg'),
56
+ '--dsw-alias-label-primary-inverted': pm('bg'),
57
+ '--dsw-alias-label-secondary': pm('muted'),
58
+ '--dsw-alias-label-tertiary': pm('muted'),
59
+ '--dsw-alias-link': pm('accent'),
60
+ '--dsw-alias-markdown-citation': pm('sunk'),
61
+ '--dsw-alias-markdown-code-block': pm('code'),
62
+ '--dsw-alias-markdown-code-block-banner': pm('sunk'),
63
+ '--dsw-alias-menu-icon': pm('ink'),
64
+ '--dsw-alias-onboarding-accent': pm('accent'),
65
+ '--dsw-alias-onboarding-card-fill': pm('surface'),
66
+ '--dsw-alias-onboarding-checkbox-border': pm('line-strong'),
67
+ '--dsw-alias-onboarding-secondary-fill': pm('surface'),
68
+ '--dsw-alias-scrollbar-bg-l1': pm('line'),
69
+ '--dsw-alias-scrollbar-bg-l2': pm('line'),
70
+ '--dsw-alias-scrollbar-hover-l1': pm('line-strong'),
71
+ '--dsw-alias-scrollbar-hover-l2': pm('line-strong'),
72
+ '--dsw-alias-settings-card-fill': pm('surface'),
73
+ '--dsw-alias-settings-card-stroke': pm('line'),
74
+ '--dsw-alias-state-business-primary': pm('accent'),
75
+ '--dsw-alias-state-business-tertiary': pm('accent-soft'),
76
+ '--dsw-alias-state-error-primary': pm('danger'),
77
+ '--dsw-alias-state-error-secondary': pm('danger'),
78
+ '--dsw-alias-state-success-primary': pm('ok'),
79
+ '--dsw-alias-state-success-secondary': pm('ok'),
80
+ '--dsw-alias-state-success-tertiary': pm('ok-soft'),
81
+ '--dsw-alias-state-warn-label': pm('warn'),
82
+ '--dsw-alias-state-warn-primary': pm('warn'),
83
+ '--dsw-alias-state-warn-secondary': pm('warn'),
84
+ '--dsw-alias-state-warn-tertiary': pm('warn-soft'),
85
+ '--dsw-alias-tooltip-bg': { light: '#1c1b19', dark: '#3a3833' },
86
+ '--dsw-alias-turn-trigger-bg': pm('code'),
87
+ '--dsw-alias-turn-trigger-bg-hover': mix('ink', 6),
88
+ '--dsw-specific-sidebar-fill': pm('sunk'),
89
+ '--dsw-specific-sidebar-nav-item-active': pm('selected'),
90
+ '--dsw-specific-sidebar-nav-item-active-accent': pm('accent'),
91
+ '--dsw-specific-sidebar-nav-item-hover': mix('ink', 6),
92
+ '--dsw-specific-bubble': pm('accent-soft'),
93
+ '--dsw-specific-bubble-highlight': pm('selected'),
94
+ '--dsw-specific-input-major': pm('surface'),
95
+ '--dsw-specific-menu': pm('surface'),
96
+ '--dsw-specific-selector': pm('surface'),
97
+ '--dsw-focus-ring-color': pm('accent'),
98
+ }
99
+
100
+ const SANS = 'var(--pm-font-sans)'
101
+ const MONO = 'var(--pm-font-mono)'
102
+ const FONT_SIZES = { text: '--dsh-content-font-size', code: '--dsh-code-font-size', terminal: '--dsh-terminal-font-size' } as const
103
+ const FONT_FAMILIES = { text: '--dsh-font-family-text', code: '--dsh-font-family-code', terminal: '--dsh-font-family-terminal' } as const
104
+
105
+ export class ThemePresenter {
106
+ #applied: string[] = []
107
+ readonly #meta = document.createElement('meta')
108
+
109
+ constructor() {
110
+ this.#meta.name = 'theme-color'
111
+ }
112
+
113
+ apply(snapshot: DshThemeSnapshot): void {
114
+ const scheme = snapshot.active.colorScheme
115
+ const root = document.documentElement
116
+ const body = document.body
117
+ root.style.colorScheme = scheme
118
+ root.setAttribute('data-ds-theme-source', snapshot.preference === 'system' ? 'system' : scheme)
119
+ if (scheme === 'dark') body.setAttribute('data-ds-dark-theme', '')
120
+ else body.removeAttribute('data-ds-dark-theme')
121
+ for (const [role, name] of Object.entries(FONT_SIZES)) body.style.setProperty(name, `${snapshot.fontSizes[role as keyof typeof FONT_SIZES]}px`)
122
+ for (const [role, name] of Object.entries(FONT_FAMILIES)) body.style.setProperty(name, snapshot.fontFamilies[role as keyof typeof FONT_FAMILIES] || (role === 'text' ? SANS : MONO))
123
+ for (const name of this.#applied) body.style.removeProperty(name)
124
+ this.#applied = []
125
+ for (const [name, value] of Object.entries(snapshot.active.tokens)) {
126
+ body.style.setProperty(name, value)
127
+ this.#applied.push(name)
128
+ }
129
+ this.#meta.content = getComputedStyle(body).backgroundColor
130
+ if (!this.#meta.isConnected) document.head.append(this.#meta)
131
+ }
132
+
133
+ dispose(): void {
134
+ const root = document.documentElement
135
+ const body = document.body
136
+ root.style.removeProperty('color-scheme')
137
+ root.removeAttribute('data-ds-theme-source')
138
+ body.removeAttribute('data-ds-dark-theme')
139
+ for (const name of [...Object.values(FONT_SIZES), ...Object.values(FONT_FAMILIES), ...this.#applied]) body.style.removeProperty(name)
140
+ this.#applied = []
141
+ this.#meta.remove()
142
+ }
143
+ }
144
+
145
+ export function followScheme(snapshot: DshThemeSnapshot): boolean {
146
+ const dark = snapshot.active.colorScheme === 'dark'
147
+ document.documentElement.classList.toggle('dark', dark)
148
+ return dark
149
+ }
@@ -0,0 +1,55 @@
1
+ import type { DshAgent, DshContext } from './dsh.ts'
2
+ import type { PresmiumSession } from './service.ts'
3
+
4
+ export const name = 'presmium-context'
5
+ export const inject = ['presmium', 'systemPrompt']
6
+
7
+ export const GUIDE = [
8
+ 'This is a Presmium workspace: lecture documents written in MyST Markdown, each in a directory of its own, which Presmium compiles to an intermediate representation and renders as a web page, slides and a notebook.',
9
+ 'Your working directory is the workspace root and stays the same when the author switches documents. You can read and write files throughout this workspace; resolve document paths relative to this root. The presmium__* tools read the compiled document (its sections, steps, code, outputs and diagnostics) and operate it (run a code block, build, move the presentation); they ask the author first where Presmium\'s approval policy says so.',
10
+ 'Load the presmium-author skill before you write MyST directives. The <presmium-document> context says where the document and the author are, and what the author points at (focus): start from there.',
11
+ 'Your changes are reviewed by the author before they are committed; do not commit them yourself.',
12
+ ].join('\n')
13
+
14
+ const ORDER = 9000
15
+
16
+ async function read(session: PresmiumSession, tool: string): Promise<any> {
17
+ const result = await session.client.call(tool, {}).catch(() => undefined)
18
+ if (!result || result.isError) return undefined
19
+ try {
20
+ return JSON.parse(result.text)
21
+ } catch {
22
+ return undefined
23
+ }
24
+ }
25
+
26
+ export async function snapshot(session: PresmiumSession): Promise<string> {
27
+ const [info, listed, position, focus] = await Promise.all(['deck_info', 'list_sections', 'get_position', 'get_focus'].map(tool => read(session, tool)))
28
+ const where = session.document.dir || '.'
29
+ const lines = [`Document ${JSON.stringify(where)} (path relative to the workspace root; working directory remains the workspace root): "${info?.title ?? session.document.title}"${info?.lang ? `, language ${info.lang}` : ''}${info?.entry ? `, entry ${info.entry}` : ''}.`]
30
+ if (info?.diagnostics) lines.push(`Diagnostics: ${info.diagnostics.errors} errors, ${info.diagnostics.warnings} warnings (presmium__get_diagnostics lists them).`)
31
+ if (listed?.sections) lines.push('Sections (id: title):', ...(listed.sections as { id: string, title: string }[]).map(s => `- ${s.id}: ${s.title}`))
32
+ const at = position?.position
33
+ lines.push(at ? `Position: section ${at.section ?? '?'}, step ${at.step}${at.view ? `, ${at.view} view` : ''} (reported ${at.reported}).` : 'Position: no window shows the document.')
34
+ lines.push(focus?.focus?.length ? `Focus, as presmium__get_focus answers: ${JSON.stringify(focus)}` : 'Focus: the author points at nothing; the position is where they are.')
35
+ return `<presmium-document>\n${lines.join('\n')}\n</presmium-document>`
36
+ }
37
+
38
+ export function apply(ctx: DshContext): void {
39
+ ctx.systemPrompt.section({ name: 'presmium:guide', order: ORDER, interpolate: false, text: GUIDE })
40
+ const latest = new Map<string, string>()
41
+ const refresh = async (agent: DshAgent) => {
42
+ const session = await ctx.presmium.refresh(agent)
43
+ latest.set(agent.session.id, `Workspace root: ${JSON.stringify((await ctx.presmium.ready).root)}.\n` + (session ? await snapshot(session).catch(() => '') : '<presmium-document>No document selected. Work from the workspace root.</presmium-document>'))
44
+ }
45
+
46
+ ctx.on('agent/created', async ({ agent }: { agent: DshAgent }) => {
47
+ await refresh(agent)
48
+ agent.ctx.systemPrompt.context({ name: 'presmium:document', order: ORDER, interpolate: false, text: () => latest.get(agent.session.id) ?? '' })
49
+ })
50
+ ctx.on('agent/pre-step', async (payload: { agent: DshAgent }, next: () => Promise<unknown>) => {
51
+ await refresh(payload.agent)
52
+ return next()
53
+ })
54
+ ctx.on('session/disposed', (session: { id: string }) => void latest.delete(session.id))
55
+ }