@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.
- package/LICENSE +202 -0
- package/README.md +22 -2
- package/build/client.mjs +66 -0
- package/build/dist/client.js +90 -0
- package/build/dist/client.json +70 -0
- package/build/ensure.mjs +30 -0
- package/cordis.patch.yml +17 -0
- package/package.json +95 -3
- package/profile.patch.yml +25 -0
- package/rows/context.mjs +7 -0
- package/rows/load.mjs +8 -0
- package/rows/policy.mjs +7 -0
- package/rows/routes.mjs +7 -0
- package/rows/service.mjs +7 -0
- package/rows/skills.mjs +7 -0
- package/rows/tools.mjs +7 -0
- package/rows/turns.mjs +7 -0
- package/rows/workbench.mjs +9 -0
- package/rows/workspaces.mjs +7 -0
- package/skills/presmium-author/SKILL.md +95 -0
- package/skills/presmium-author/references/assistance.md +27 -0
- package/skills/presmium-author/references/code.md +52 -0
- package/skills/presmium-author/references/directives.md +592 -0
- package/skills/presmium-author/references/publishing.md +60 -0
- package/skills/presmium-author/references/writing.md +72 -0
- package/skills/presmium-present/SKILL.md +69 -0
- package/skills/presmium-present/references/classroom.md +46 -0
- package/skills/presmium-present/references/delivery.md +63 -0
- package/skills/presmium-present/references/lecture.md +58 -0
- package/src/client/Frame.tsx +314 -0
- package/src/client/brand.tsx +90 -0
- package/src/client/chrome.tsx +79 -0
- package/src/client/contract.ts +35 -0
- package/src/client/dsh.d.ts +129 -0
- package/src/client/fallback.tsx +49 -0
- package/src/client/frame.css +345 -0
- package/src/client/index.tsx +154 -0
- package/src/client/layout.ts +217 -0
- package/src/client/messages.ts +78 -0
- package/src/client/source.tsx +81 -0
- package/src/client/styles.css +3 -0
- package/src/client/theme.ts +149 -0
- package/src/node/context.ts +55 -0
- package/src/node/dsh.ts +93 -0
- package/src/node/launch.ts +59 -0
- package/src/node/policy.ts +59 -0
- package/src/node/routes.ts +46 -0
- package/src/node/service.ts +280 -0
- package/src/node/skills.ts +46 -0
- package/src/node/tools.ts +43 -0
- package/src/node/turns/index.ts +165 -0
- package/src/node/watchdog.mjs +36 -0
- package/src/node/workbench.ts +43 -0
- package/src/node/workspaces.ts +10 -0
- 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,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
|
+
}
|