@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/messages.ts
CHANGED
|
@@ -1,78 +1,171 @@
|
|
|
1
|
-
import type { Locale } from '@lincyaw/workbench'
|
|
1
|
+
import type { Locale, Workbench } from '@lincyaw/workbench'
|
|
2
|
+
import { useSyncExternalStore } from 'react'
|
|
2
3
|
|
|
3
4
|
const zh = {
|
|
4
5
|
product: 'Presmium',
|
|
5
|
-
activity: '
|
|
6
|
+
activity: '应用栏',
|
|
6
7
|
skip: '跳到内容',
|
|
7
|
-
documents: '
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
documents: '文稿',
|
|
9
|
+
document: '当前文稿',
|
|
10
|
+
assistant: 'AI 助手',
|
|
11
|
+
more: '更多',
|
|
12
|
+
conversation: 'AI 助手',
|
|
11
13
|
newConversation: '新建对话',
|
|
12
|
-
conversationHistory: '
|
|
13
|
-
closeHistory: '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
preview: '预览',
|
|
18
|
-
changes: '修改',
|
|
14
|
+
conversationHistory: '对话记录',
|
|
15
|
+
closeHistory: '隐藏对话记录',
|
|
16
|
+
hideConversation: '隐藏 AI 助手',
|
|
17
|
+
preview: '文稿',
|
|
18
|
+
history: '版本历史',
|
|
19
19
|
settings: '设置',
|
|
20
|
-
help: '
|
|
20
|
+
help: '帮助',
|
|
21
|
+
mascot: '小猫',
|
|
22
|
+
mascotHint: 'AI 助手工作时,对话和文稿旁的小猫会演示它在做什么:思考、阅读、修改或等你确认。',
|
|
23
|
+
mascotShow: '显示',
|
|
24
|
+
mascotHide: '隐藏',
|
|
21
25
|
close: '关闭',
|
|
22
|
-
toggleSide: '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
toggleSide: '显示或隐藏对话记录',
|
|
27
|
+
keys: {
|
|
28
|
+
save: '保存',
|
|
29
|
+
undo: '撤消',
|
|
30
|
+
redo: '恢复',
|
|
31
|
+
play: '播放/暂停(放映、视频制作)',
|
|
32
|
+
step: '上一步/下一步(放映、视频制作)',
|
|
33
|
+
overview: '放映:总览',
|
|
34
|
+
fullscreen: '放映:全屏',
|
|
35
|
+
},
|
|
26
36
|
resize: '拖动调整宽度',
|
|
27
|
-
centre: '
|
|
37
|
+
centre: '文稿',
|
|
28
38
|
welcomeTitle: '欢迎使用 Presmium',
|
|
29
|
-
welcome: '
|
|
39
|
+
welcome: '打开文稿即可编辑全文、调整页面并添加批注,修改自动保存,可以撤消。放映前会用已保存的版本准备放映;配音和视频制作会先检查所需的服务。使用 AI 助手前请设置模型。',
|
|
30
40
|
start: '开始使用',
|
|
31
41
|
models: '设置模型',
|
|
32
|
-
|
|
42
|
+
helpItems: [
|
|
43
|
+
'直接在文稿中输入即可修改。修改自动保存,文稿上方显示“已保存”。',
|
|
44
|
+
'选中文字会出现格式工具栏:段落样式、加粗、对齐、颜色、“插入链接”与“新建批注”。“新建批注”在选中的文字上留下批注,“批注”窗格列出全部批注。',
|
|
45
|
+
'鼠标移到一段内容上,旁边的“插入内容块”可插入标题、列表、表格、图片等内容,“打开内容块菜单”可删除这一段或修改它的颜色。',
|
|
46
|
+
'“导航窗格”列出全部页面:“新建页面”添加一页,页面旁的“更多”可以重命名、复制、上移、下移或删除这一页。',
|
|
47
|
+
'“撤消”({undo})与“恢复”({redo})逐步撤消或恢复修改。“版本历史”列出保存过的每次修改,“撤消此更改”单独撤消其中一次。',
|
|
48
|
+
'“放映”按幻灯片逐步展示文稿;“导出”可导出网站、幻灯片网站、Notebook、幻灯片 PDF、幻灯片图片或幻灯片 PowerPoint;“视频制作”制作讲授视频。',
|
|
49
|
+
'“AI 助手”显示或隐藏与 AI 的对话。用自己的话告诉它要改什么,它处理的是当前打开的文稿。第一次使用前,请在“设置”的“模型”中设置模型。',
|
|
50
|
+
],
|
|
51
|
+
runtimeProblems: '有操作无法完成。请查看详细信息,或重试刚才的操作。',
|
|
52
|
+
runtimeStopped: 'Presmium 暂时无法显示。请重新加载后继续。',
|
|
53
|
+
runtimeDetails: '详细信息',
|
|
54
|
+
runtimeReportFailed: '暂时无法把诊断发送给工作区服务,下面的详细信息仍可查看。',
|
|
55
|
+
runtimeRetry: '重新发送诊断',
|
|
56
|
+
runtimeDismiss: '关闭这条提示',
|
|
57
|
+
runtimeReload: '重新加载',
|
|
58
|
+
repairStale: '文稿或对话已切换,请在当前文稿中重新发送。',
|
|
59
|
+
repairRequest: '请修复当前文稿中以下无法正确显示的内容。诊断是观测到的结果,请核对文稿内容后再修复。',
|
|
60
|
+
needsModelTitle: 'AI 助手需要先设置模型',
|
|
61
|
+
needsModel: '设置一个模型后,就能用自己的话告诉 AI 要改什么。已经写下的请求会保留在这里。',
|
|
62
|
+
needsModelTurn: '这次请求没有发出:还没有可用的模型。设置模型后重新发送即可。',
|
|
63
|
+
needsModelRequest: 'AI 助手需要先设置模型:在 AI 助手中选择“设置模型”,设置好后重试。',
|
|
64
|
+
keyRejectedTitle: '模型没有接受当前的密钥',
|
|
65
|
+
keyRejected: '无法完成这次请求:模型服务拒绝了设置中的密钥。请在模型设置中检查或重新填写,然后重新发送。',
|
|
66
|
+
quotaTitle: '模型的额度用完了',
|
|
67
|
+
quota: '无法完成这次请求:模型服务的额度不足。请充值,或在模型设置中换一个模型,然后重新发送。',
|
|
68
|
+
turnFailedTitle: '无法完成这次请求',
|
|
69
|
+
turnFailed: 'AI 助手无法完成这次请求。请重新发送;如果反复出现,请新建对话。',
|
|
70
|
+
busy: '模型服务暂时繁忙。请稍后重新发送。',
|
|
71
|
+
tooLong: '这段对话太长了,模型读不下。请新建对话,再把要改的内容告诉 AI。',
|
|
72
|
+
turnStoppedTitle: '这次请求中途停止',
|
|
73
|
+
turnStopped: 'Presmium 在 AI 处理时重新启动了。请重新发送。',
|
|
74
|
+
checkModels: '检查模型设置',
|
|
75
|
+
failureDetails: '详细信息',
|
|
33
76
|
}
|
|
34
77
|
|
|
35
|
-
export type
|
|
78
|
+
export type ClientMessages = typeof zh
|
|
36
79
|
|
|
37
|
-
const en:
|
|
80
|
+
const en: ClientMessages = {
|
|
38
81
|
product: 'Presmium',
|
|
39
|
-
activity: '
|
|
40
|
-
skip: 'Skip to
|
|
82
|
+
activity: 'App Bar',
|
|
83
|
+
skip: 'Skip to Content',
|
|
41
84
|
documents: 'Documents',
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
hideConversation: 'Hide
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
changes: 'Changes',
|
|
85
|
+
document: 'Document',
|
|
86
|
+
assistant: 'AI Assistant',
|
|
87
|
+
more: 'More',
|
|
88
|
+
conversation: 'AI Assistant',
|
|
89
|
+
newConversation: 'New Chat',
|
|
90
|
+
conversationHistory: 'Chat History',
|
|
91
|
+
closeHistory: 'Hide Chat History',
|
|
92
|
+
hideConversation: 'Hide AI Assistant',
|
|
93
|
+
preview: 'Document',
|
|
94
|
+
history: 'Version History',
|
|
53
95
|
settings: 'Settings',
|
|
54
|
-
help: '
|
|
96
|
+
help: 'Help',
|
|
97
|
+
mascot: 'Kitten',
|
|
98
|
+
mascotHint: 'While the AI Assistant works, a kitten beside the chat and the document acts out what it is doing: thinking, reading, changing the document, or waiting for you.',
|
|
99
|
+
mascotShow: 'Show',
|
|
100
|
+
mascotHide: 'Hide',
|
|
55
101
|
close: 'Close',
|
|
56
|
-
toggleSide: '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
102
|
+
toggleSide: 'Show or Hide Chat History',
|
|
103
|
+
keys: {
|
|
104
|
+
save: 'Save',
|
|
105
|
+
undo: 'Undo',
|
|
106
|
+
redo: 'Redo',
|
|
107
|
+
play: 'Play or Pause (Presentation, Video Production)',
|
|
108
|
+
step: 'Previous or Next Step (Presentation, Video Production)',
|
|
109
|
+
overview: 'Presentation: Overview',
|
|
110
|
+
fullscreen: 'Presentation: Full Screen',
|
|
111
|
+
},
|
|
60
112
|
resize: 'Drag to resize',
|
|
61
|
-
centre: '
|
|
113
|
+
centre: 'Document',
|
|
62
114
|
welcomeTitle: 'Welcome to Presmium',
|
|
63
|
-
welcome: 'Open a
|
|
64
|
-
start: '
|
|
65
|
-
models: 'Set
|
|
66
|
-
|
|
115
|
+
welcome: 'Open a document to edit its full text, arrange its pages, and add comments. Changes save automatically and can be undone. Presenting prepares the saved version first; voiceover and Video Production check the services they need first. Set up a model before using the AI Assistant.',
|
|
116
|
+
start: 'Get Started',
|
|
117
|
+
models: 'Set Up a Model',
|
|
118
|
+
helpItems: [
|
|
119
|
+
'Type directly in the document to change it. Changes save automatically, and the top of the document shows Saved.',
|
|
120
|
+
'Select text to show the formatting toolbar: paragraph style, Bold, alignment, Colors, Insert Link, and New Comment. New Comment leaves a comment on the selected text; the Comments pane lists them all.',
|
|
121
|
+
'Point at a paragraph: Insert Block, beside it, inserts a heading, list, table, picture, and more; Open Block Menu deletes the paragraph or changes its colors.',
|
|
122
|
+
'The Navigation Pane lists the pages: New Page adds one, and More beside a page renames, duplicates, moves, or deletes it.',
|
|
123
|
+
'Undo ({undo}) and Redo ({redo}) step back and forth through your changes. Version History lists every saved change; Undo This Change reverses one of them.',
|
|
124
|
+
'Present shows the document as slides, step by step; Export makes a website, slides website, Notebook, slides as PDF, slides as pictures, or slides as PowerPoint; Video Production makes a lecture video with voiceover.',
|
|
125
|
+
'AI Assistant shows or hides the chat with the AI. Tell it in your own words what to change; it works on the document you have open. Before the first use, set up a model under Models in Settings.',
|
|
126
|
+
],
|
|
127
|
+
runtimeProblems: 'An operation couldn’t finish. Check the details, or retry the operation.',
|
|
128
|
+
runtimeStopped: 'Presmium can’t display right now. Reload to continue.',
|
|
129
|
+
runtimeDetails: 'Details',
|
|
130
|
+
runtimeReportFailed: 'Couldn’t send the diagnostics to the workspace service. The details below are still available.',
|
|
131
|
+
runtimeRetry: 'Send Diagnostics Again',
|
|
132
|
+
runtimeDismiss: 'Close',
|
|
133
|
+
runtimeReload: 'Reload',
|
|
134
|
+
repairStale: 'The document or chat changed. Send it again from the current document.',
|
|
135
|
+
repairRequest: 'Please fix the content below that does not display correctly in the current document. The diagnostics are what was observed; check the document’s content before fixing it.',
|
|
136
|
+
needsModelTitle: 'The AI Assistant needs a model',
|
|
137
|
+
needsModel: 'Set up a model, then tell the AI in your own words what to change. Anything you have typed stays here.',
|
|
138
|
+
needsModelTurn: 'This request was not sent: no model is set up yet. Set one up, then send it again.',
|
|
139
|
+
needsModelRequest: 'The AI Assistant needs a model first: choose Set Up a Model in the AI Assistant, then try again.',
|
|
140
|
+
keyRejectedTitle: 'The model didn’t accept your API key',
|
|
141
|
+
keyRejected: 'Couldn’t finish this request: the model service refused the API key in Settings. Check or re-enter it in the model settings, then send again.',
|
|
142
|
+
quotaTitle: 'The model’s quota is used up',
|
|
143
|
+
quota: 'Couldn’t finish this request: the model service has no quota left. Top up, or choose another model in the model settings, then send again.',
|
|
144
|
+
turnFailedTitle: 'Couldn’t finish this request',
|
|
145
|
+
turnFailed: 'The AI Assistant couldn’t finish this request. Send it again; if this keeps happening, start a new chat.',
|
|
146
|
+
busy: 'The model service is busy right now. Wait a moment, then send again.',
|
|
147
|
+
tooLong: 'This chat has grown too long for the model. Start a new chat and tell the AI again what to change.',
|
|
148
|
+
turnStoppedTitle: 'This request stopped midway',
|
|
149
|
+
turnStopped: 'Presmium restarted while the AI was working. Send it again.',
|
|
150
|
+
checkModels: 'Check Model Settings',
|
|
151
|
+
failureDetails: 'Details',
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const CLIENT_MESSAGES: Record<Locale, ClientMessages> = { 'zh-CN': zh, 'en': en }
|
|
155
|
+
|
|
156
|
+
export function useClientMessages(workbench: Workbench): ClientMessages {
|
|
157
|
+
const locale = useSyncExternalStore(workbench.subscribe, () => workbench.getState().locale)
|
|
158
|
+
return CLIENT_MESSAGES[locale]
|
|
67
159
|
}
|
|
68
160
|
|
|
69
|
-
|
|
161
|
+
/** The languages the client part adds to DeepSeek Harness's language list, with Presmium's copy for them. */
|
|
162
|
+
export const BRAND_LANGUAGES = [{ id: 'zh-CN', label: '简体中文(Presmium)', fallback: 'zh' }, { id: 'en-US', label: 'English (Presmium)', fallback: 'en' }] as const
|
|
70
163
|
|
|
71
164
|
export const BRAND_COPY: Record<'zh-CN' | 'en-US', Record<string, Record<string, string>>> = {
|
|
72
165
|
'zh-CN': {
|
|
73
|
-
conversation: { 'hero.headline': '
|
|
166
|
+
conversation: { 'hero.headline': '有什么想改的,告诉我', 'hero.preview': 'AI 助手', 'placeholder.hero': '例如:给第二节补一个生活中的例子', 'placeholder.default': '继续告诉 AI 要改什么…', 'placeholder.workspace': '先打开一份文稿' },
|
|
74
167
|
},
|
|
75
168
|
'en-US': {
|
|
76
|
-
conversation: { 'hero.headline': '
|
|
169
|
+
conversation: { 'hero.headline': 'Tell me what to change', 'hero.preview': 'AI Assistant', 'placeholder.hero': 'For example: add an everyday example to section 2', 'placeholder.default': 'Tell the AI what to change next…', 'placeholder.workspace': 'Open a document first' },
|
|
77
170
|
},
|
|
78
171
|
}
|
|
@@ -1,41 +1,36 @@
|
|
|
1
1
|
import { PmIconButton } from '@lincyaw/workbench'
|
|
2
2
|
import type { Workbench } from '@lincyaw/workbench'
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
-
import {
|
|
4
|
+
import { isBootEntryLoaded, UI_SIDEBAR } from './contract.ts'
|
|
5
5
|
import type { LayoutActions, LayoutState } from './layout.ts'
|
|
6
6
|
import { NARROW, SIDE_COLLAPSE } from './layout.ts'
|
|
7
|
+
import { useClientMessages } from './messages.ts'
|
|
7
8
|
|
|
8
9
|
interface SidebarProps {
|
|
9
10
|
renderSlot(name: string, props?: Record<string, unknown>, options?: Record<string, unknown>): ReactNode
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
13
|
+
/**
|
|
14
|
+
* Replaces Harness's navigation shell through public slots: the history sidebar, the conversation's header actions,
|
|
15
|
+
* and the global panels (`sidebar.panellist`), which it keeps in `globalPanels` for the activity bar. Harness still
|
|
16
|
+
* owns the session browser and its dialogs.
|
|
17
|
+
*/
|
|
18
|
+
export function registerNavigationShell(ctx: DshClientContext, workbench: Workbench, layout: { actions: LayoutActions, getSnapshot(): LayoutState }, globalPanels: { set(entries: readonly DshSlotEntry[]): void }, startSession: () => Promise<void>): void {
|
|
19
|
+
const { actions } = layout
|
|
20
20
|
// An already running host retains its old plugin roster until it is restarted.
|
|
21
|
-
if (!
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
})
|
|
26
|
-
ctx.slots.inject('sidebar.panellist', () => {
|
|
27
|
-
panels = ctx.slots.entries('sidebar.panellist')
|
|
28
|
-
for (const listener of listeners) listener()
|
|
29
|
-
})
|
|
21
|
+
if (!isBootEntryLoaded(UI_SIDEBAR)) ctx.effect(() => {
|
|
22
|
+
const updatePanels = () => globalPanels.set(ctx.slots.entries('sidebar.panellist'))
|
|
23
|
+
const offPanels = ctx.slots.subscribe('sidebar.panellist', updatePanels)
|
|
24
|
+
ctx.slots.inject('sidebar.panellist', updatePanels)
|
|
30
25
|
const closeHistory = () => {
|
|
31
|
-
if (
|
|
26
|
+
if (layout.getSnapshot().layoutInfo.viewportWidth < SIDE_COLLAPSE) actions.revealConversation()
|
|
32
27
|
else actions.toggleSidebar()
|
|
33
28
|
}
|
|
34
29
|
function Sidebar({ renderSlot }: SidebarProps) {
|
|
35
|
-
const t =
|
|
30
|
+
const t = useClientMessages(workbench)
|
|
36
31
|
return <section className="pmw-history" aria-label={t.conversationHistory}>
|
|
37
32
|
<header className="pmw-history__header"><h2>{t.conversationHistory}</h2><PmIconButton icon="close" small label={t.closeHistory} onClick={closeHistory} /></header>
|
|
38
|
-
<div className="pmw-history__browser">{renderSlot('sidebar.workspaces', { wide: true, expandSidebar: () => actions.
|
|
33
|
+
<div className="pmw-history__browser">{renderSlot('sidebar.workspaces', { wide: true, expandSidebar: () => actions.selectActivity('sessions') })}</div>
|
|
39
34
|
<div hidden>{renderSlot('sidebar.settings', { wide: false })}</div>
|
|
40
35
|
</section>
|
|
41
36
|
}
|
|
@@ -54,13 +49,14 @@ export function chrome(ctx: DshClientContext, workbench: Workbench, actions: Lay
|
|
|
54
49
|
}, 'presmium: one navigation shell')
|
|
55
50
|
ctx.effect(() => {
|
|
56
51
|
function ConversationActions() {
|
|
57
|
-
const t =
|
|
52
|
+
const t = useClientMessages(workbench)
|
|
58
53
|
return <div className="pmw-chat-actions pm-ui" role="group" aria-label={t.conversation}>
|
|
54
|
+
<span className="pmw-chat-actions__title" aria-hidden="true">{t.conversation}</span>
|
|
59
55
|
<PmIconButton icon="plus" label={t.newConversation} small onClick={() => void startSession()} />
|
|
60
56
|
<PmIconButton icon="history" label={t.conversationHistory} small onClick={() => actions.showHistory()} />
|
|
61
57
|
<PmIconButton icon="close" label={t.hideConversation} small onClick={() => {
|
|
62
|
-
if (
|
|
63
|
-
else actions.
|
|
58
|
+
if (layout.getSnapshot().layoutInfo.viewportWidth < NARROW) actions.showNarrowPane(workbench.state.document === undefined ? 'documents' : 'preview')
|
|
59
|
+
else actions.setConversationOpen(false)
|
|
64
60
|
}} />
|
|
65
61
|
</div>
|
|
66
62
|
}
|
|
@@ -75,5 +71,4 @@ export function chrome(ctx: DshClientContext, workbench: Workbench, actions: Lay
|
|
|
75
71
|
})
|
|
76
72
|
return () => off?.()
|
|
77
73
|
}, 'presmium: remove the duplicate sidebar opener')
|
|
78
|
-
return panelStore
|
|
79
74
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A value with listeners, in the shape of DeepSeek Harness's observables. */
|
|
2
|
+
export function createObservable<T>(initial: T): DshObservable<T> & { set(value: T): void } {
|
|
3
|
+
let value = initial
|
|
4
|
+
const listeners = new Set<() => void>()
|
|
5
|
+
return {
|
|
6
|
+
getSnapshot: () => value,
|
|
7
|
+
subscribe(listener: () => void) {
|
|
8
|
+
listeners.add(listener)
|
|
9
|
+
return () => { listeners.delete(listener) }
|
|
10
|
+
},
|
|
11
|
+
set(next: T) {
|
|
12
|
+
value = next
|
|
13
|
+
for (const listener of listeners) listener()
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A path with `/` separators and without `.`, `..` or empty parts; undefined when `..` climbs above its start. */
|
|
2
|
+
export function normalizePath(path: string): string | undefined {
|
|
3
|
+
const parts: string[] = []
|
|
4
|
+
for (const part of path.replace(/\\/g, '/').split('/')) {
|
|
5
|
+
if (!part || part === '.') continue
|
|
6
|
+
if (part === '..') {
|
|
7
|
+
if (!parts.length || parts.at(-1)?.endsWith(':')) return
|
|
8
|
+
parts.pop()
|
|
9
|
+
} else parts.push(part)
|
|
10
|
+
}
|
|
11
|
+
return `${path.startsWith('/') ? '/' : ''}${parts.join('/')}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isSamePath(a: string, b: string): boolean {
|
|
15
|
+
const normalized = normalizePath(a)
|
|
16
|
+
return normalized !== undefined && normalized === normalizePath(b)
|
|
17
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { Workbench } from '@lincyaw/workbench'
|
|
2
|
+
|
|
3
|
+
const KEY = 'presmium.root-reload.v1'
|
|
4
|
+
const DOCUMENT_KEY = 'presmium.open-document.v1'
|
|
5
|
+
const CODE = Symbol('Presmium root client code')
|
|
6
|
+
|
|
7
|
+
interface ReturnPoint {
|
|
8
|
+
workspace: string
|
|
9
|
+
directory: string
|
|
10
|
+
documentId: string
|
|
11
|
+
sectionId?: string
|
|
12
|
+
stepId?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Checkpoint { version: 1, armed: boolean, point?: ReturnPoint }
|
|
16
|
+
|
|
17
|
+
function isReturnPoint(point: unknown): point is ReturnPoint {
|
|
18
|
+
return Boolean(point && typeof point === 'object' && 'workspace' in point && typeof point.workspace === 'string'
|
|
19
|
+
&& 'directory' in point && typeof point.directory === 'string' && 'documentId' in point && typeof point.documentId === 'string'
|
|
20
|
+
&& (!('sectionId' in point) || typeof point.sectionId === 'string') && (!('stepId' in point) || typeof point.stepId === 'string'))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readOpenDocument(): ReturnPoint | undefined {
|
|
24
|
+
try {
|
|
25
|
+
const point: unknown = JSON.parse(sessionStorage.getItem(DOCUMENT_KEY) ?? 'null')
|
|
26
|
+
if (isReturnPoint(point)) return point
|
|
27
|
+
} catch { /* Invalid or unavailable storage cannot authorize navigation. */ }
|
|
28
|
+
try { sessionStorage.removeItem(DOCUMENT_KEY) } catch { /* Editing remains available. */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare global {
|
|
32
|
+
interface Window {
|
|
33
|
+
/** A page-lifetime flag, never a retained plugin, context or React root. */
|
|
34
|
+
__presmiumRootReload?: symbol | 'reloading'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function checkpoint(): Checkpoint | undefined {
|
|
39
|
+
const value: unknown = JSON.parse(sessionStorage.getItem(KEY) ?? 'null')
|
|
40
|
+
if (!value || typeof value !== 'object' || !('version' in value) || value.version !== 1 || !('armed' in value) || typeof value.armed !== 'boolean') return
|
|
41
|
+
if (!('point' in value) || value.point === undefined) return { version: 1, armed: value.armed }
|
|
42
|
+
const point = value.point
|
|
43
|
+
if (!isReturnPoint(point)) return
|
|
44
|
+
return value as Checkpoint
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Root-owner code replacement needs a new shell mount in this Harness version. */
|
|
48
|
+
export function beginRootReload(): { reloading: boolean, point?: ReturnPoint } {
|
|
49
|
+
if (window.__presmiumRootReload && window.__presmiumRootReload !== CODE) {
|
|
50
|
+
if (window.__presmiumRootReload === 'reloading') return { reloading: true }
|
|
51
|
+
try {
|
|
52
|
+
const saved = checkpoint()
|
|
53
|
+
if (!saved) throw new Error('missing root return checkpoint')
|
|
54
|
+
sessionStorage.setItem(KEY, JSON.stringify({ ...saved, armed: true } satisfies Checkpoint))
|
|
55
|
+
window.__presmiumRootReload = 'reloading'
|
|
56
|
+
window.location.reload()
|
|
57
|
+
} catch {
|
|
58
|
+
// Do not navigate away when recovery storage could not be confirmed.
|
|
59
|
+
console.error('Presmium could not preserve the root reload checkpoint; automatic reload was stopped.')
|
|
60
|
+
}
|
|
61
|
+
return { reloading: true }
|
|
62
|
+
}
|
|
63
|
+
window.__presmiumRootReload = CODE
|
|
64
|
+
const opened = readOpenDocument()
|
|
65
|
+
try {
|
|
66
|
+
const saved = checkpoint()
|
|
67
|
+
sessionStorage.removeItem(KEY)
|
|
68
|
+
const point = saved?.armed && saved.point ? saved.point : opened
|
|
69
|
+
return { reloading: false, ...point ? { point } : {} }
|
|
70
|
+
} catch {
|
|
71
|
+
return { reloading: false, ...opened ? { point: opened } : {} }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Keep a pending address through another reload; only explicit navigation or settlement retires it. */
|
|
76
|
+
function retainOpenDocument(workbench: Workbench, returning?: ReturnPoint): { open<T>(start: () => T): T, settle(): void, close(): void } {
|
|
77
|
+
let previous: string | undefined
|
|
78
|
+
let pending = Boolean(returning)
|
|
79
|
+
let opening = false, closed = false
|
|
80
|
+
const navigation = workbench.documentNavigationVersion
|
|
81
|
+
let remembered: ReturnPoint | undefined
|
|
82
|
+
const write = (point: ReturnPoint | undefined) => {
|
|
83
|
+
const next = point ? JSON.stringify(point) : ''
|
|
84
|
+
if (next === previous) return
|
|
85
|
+
try {
|
|
86
|
+
if (point) sessionStorage.setItem(DOCUMENT_KEY, next)
|
|
87
|
+
else sessionStorage.removeItem(DOCUMENT_KEY)
|
|
88
|
+
previous = next
|
|
89
|
+
remembered = point
|
|
90
|
+
} catch {
|
|
91
|
+
// Do not keep an earlier address after a later explicit choice could not be stored.
|
|
92
|
+
try { sessionStorage.removeItem(DOCUMENT_KEY) } catch { /* Editing does not depend on address storage. */ }
|
|
93
|
+
previous = undefined
|
|
94
|
+
remembered = undefined
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (returning) write({ workspace: returning.workspace, directory: returning.directory, documentId: returning.documentId })
|
|
98
|
+
const retain = () => {
|
|
99
|
+
if (closed || opening) return
|
|
100
|
+
const { document, authoring } = workbench.state
|
|
101
|
+
let point: ReturnPoint | undefined
|
|
102
|
+
if (document !== undefined && authoring?.directory === document && authoring.identity && authoring.content) {
|
|
103
|
+
try {
|
|
104
|
+
const identity: unknown = JSON.parse(authoring.identity)
|
|
105
|
+
if (Array.isArray(identity) && identity.length === 2 && typeof identity[0] === 'string' && identity[1] === authoring.content.id) {
|
|
106
|
+
point = { workspace: identity[0], directory: document, documentId: authoring.content.id }
|
|
107
|
+
}
|
|
108
|
+
} catch { /* An unknown identity cannot authorize opening another document. */ }
|
|
109
|
+
}
|
|
110
|
+
if (point) write(point)
|
|
111
|
+
else if (document === undefined) {
|
|
112
|
+
if (!pending || workbench.documentNavigationVersion !== navigation) write(undefined)
|
|
113
|
+
} else if (remembered?.directory !== document) write(undefined)
|
|
114
|
+
}
|
|
115
|
+
const stop = workbench.subscribe(retain)
|
|
116
|
+
retain()
|
|
117
|
+
return {
|
|
118
|
+
// Opening synchronously emits an intermediate Home while retiring the previous subscriptions.
|
|
119
|
+
open<T>(start: () => T): T { opening = true; try { return start() } finally { opening = false; retain() } },
|
|
120
|
+
settle() { if (!closed) { pending = false; retain() } },
|
|
121
|
+
close() { closed = true; stop() },
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Store only a return address; document drafts keep their existing persistence and review rules. */
|
|
126
|
+
export function saveRootReturnPoint(workbench: Workbench): void {
|
|
127
|
+
const state = workbench.state, source = state.authoring
|
|
128
|
+
try {
|
|
129
|
+
if (source?.storageFailure) throw new Error('document recovery storage unavailable')
|
|
130
|
+
let point: ReturnPoint | undefined
|
|
131
|
+
if (state.document !== undefined && source?.identity && source.content) {
|
|
132
|
+
const identity: unknown = JSON.parse(source.identity)
|
|
133
|
+
if (Array.isArray(identity) && identity.length === 2 && typeof identity[0] === 'string' && identity[1] === source.content.id) {
|
|
134
|
+
const sectionId = state.position?.section, section = sectionId ? source.content.sections[sectionId] : undefined
|
|
135
|
+
const stepId = section?.stepOrder[state.position?.step ?? 0]
|
|
136
|
+
point = { workspace: identity[0], directory: state.document, documentId: source.content.id, ...section ? { sectionId, stepId } : {} }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
sessionStorage.setItem(KEY, JSON.stringify({ version: 1, armed: false, ...point ? { point } : {} } satisfies Checkpoint))
|
|
140
|
+
} catch {
|
|
141
|
+
// A previous checkpoint must not authorize a reload after this save failed.
|
|
142
|
+
try { sessionStorage.removeItem(KEY) } catch { /* Storage is unavailable. */ }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Restore only within this navigation; final source adoption checks the expected workspace and identity. */
|
|
147
|
+
export function restoreRootReturnPoint(workbench: Workbench, point: ReturnPoint | undefined): () => void {
|
|
148
|
+
let active = true
|
|
149
|
+
const retaining = retainOpenDocument(workbench, point), navigation = workbench.documentNavigationVersion
|
|
150
|
+
const stop = workbench.subscribe(() => { if (workbench.documentNavigationVersion !== navigation) active = false })
|
|
151
|
+
const restore = async () => {
|
|
152
|
+
if (!point) return
|
|
153
|
+
const info = await workbench.client.readWorkbenchInfo()
|
|
154
|
+
if (!active || info.root !== point.workspace || workbench.state.document !== undefined) return
|
|
155
|
+
stop()
|
|
156
|
+
await retaining.open(() => workbench.openDocument(point.directory, {
|
|
157
|
+
expectedIdentity: JSON.stringify([point.workspace, point.documentId]),
|
|
158
|
+
...point.sectionId ? { fallbackReturnPoint: { sectionId: point.sectionId, ...point.stepId ? { stepId: point.stepId } : {} } } : {},
|
|
159
|
+
}))
|
|
160
|
+
}
|
|
161
|
+
void restore().catch(() => { /* The normal home remains available when the return address no longer exists. */ }).finally(() => { stop(); retaining.settle() })
|
|
162
|
+
return () => { active = false; stop(); retaining.close() }
|
|
163
|
+
}
|