@lincyaw/dsh 0.0.0-stage → 0.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +26 -2
  3. package/build/client.mjs +72 -0
  4. package/build/dist/client.js +1390 -0
  5. package/build/dist/client.json +274 -0
  6. package/build/ensure.mjs +39 -0
  7. package/cordis.patch.yml +26 -0
  8. package/entries/context.mjs +7 -0
  9. package/entries/events.mjs +7 -0
  10. package/entries/load.mjs +11 -0
  11. package/entries/policy.mjs +7 -0
  12. package/entries/routes.mjs +7 -0
  13. package/entries/service.mjs +7 -0
  14. package/entries/skills.mjs +7 -0
  15. package/entries/telemetry.mjs +7 -0
  16. package/entries/tools.mjs +7 -0
  17. package/entries/turns.mjs +7 -0
  18. package/entries/workbench.mjs +9 -0
  19. package/entries/workspaces.mjs +7 -0
  20. package/package.json +108 -3
  21. package/profile.patch.yml +25 -0
  22. package/src/client/RootLayout.tsx +332 -0
  23. package/src/client/agent-requests.ts +64 -0
  24. package/src/client/assistant-setup.tsx +185 -0
  25. package/src/client/author-settings.ts +57 -0
  26. package/src/client/brand.tsx +97 -0
  27. package/src/client/contract.ts +82 -0
  28. package/src/client/document-context.ts +39 -0
  29. package/src/client/dsh.d.ts +155 -0
  30. package/src/client/fallback.tsx +53 -0
  31. package/src/client/frame.css +505 -0
  32. package/src/client/index.tsx +157 -0
  33. package/src/client/launch-target.ts +89 -0
  34. package/src/client/layout.ts +230 -0
  35. package/src/client/mascot.tsx +37 -0
  36. package/src/client/messages.ts +171 -0
  37. package/src/client/navigation-shell.tsx +74 -0
  38. package/src/client/observable.ts +16 -0
  39. package/src/client/paths.ts +17 -0
  40. package/src/client/root-reload.ts +163 -0
  41. package/src/client/runtime-diagnostics.tsx +228 -0
  42. package/src/client/settings-opener.ts +26 -0
  43. package/src/client/source-links.tsx +71 -0
  44. package/src/client/speech-settings.tsx +7 -0
  45. package/src/client/styles.css +45 -0
  46. package/src/client/theme.ts +151 -0
  47. package/src/client/workbench-keys.ts +36 -0
  48. package/src/client/workspace-events.ts +114 -0
  49. package/src/node/bindings.ts +345 -0
  50. package/src/node/context.ts +166 -0
  51. package/src/node/dsh-types.ts +124 -0
  52. package/src/node/fake-collector.ts +46 -0
  53. package/src/node/fake-model.ts +80 -0
  54. package/src/node/launch.ts +176 -0
  55. package/src/node/policy.ts +56 -0
  56. package/src/node/routes.ts +56 -0
  57. package/src/node/server-routes.ts +3 -0
  58. package/src/node/service.ts +131 -0
  59. package/src/node/skills.ts +24 -0
  60. package/src/node/telemetry.ts +126 -0
  61. package/src/node/tools.ts +66 -0
  62. package/src/node/turns.ts +176 -0
  63. package/src/node/watchdog.mjs +36 -0
  64. package/src/node/workbench.ts +88 -0
  65. package/src/node/workspace-events.ts +167 -0
  66. package/src/node/workspaces.ts +10 -0
  67. package/src/workspace-events.ts +31 -0
  68. package/web.patch.yml +161 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,228 @@
1
+ import { Component, useSyncExternalStore } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import { isObservedWorkspaceFailure, PmButton, PmNotice } from '@lincyaw/workbench'
4
+ import type { Workbench, WorkspaceRequestEvent } from '@lincyaw/workbench'
5
+ import type { AuthoringDiagnostics, CanonicalSource } from '@lincyaw/kernel'
6
+ import { createObservable } from './observable.ts'
7
+ import { useClientMessages } from './messages.ts'
8
+
9
+ interface RuntimeIssue {
10
+ key: string
11
+ document?: string
12
+ checkId: string
13
+ message: string
14
+ feedbackOwner?: WorkspaceRequestEvent['feedbackOwner']
15
+ authoringSource?: CanonicalSource
16
+ }
17
+ interface RuntimeSnapshot { issues: readonly RuntimeIssue[], reportFailed: boolean }
18
+ const instances = new WeakMap<Workbench, WorkbenchRuntimeDiagnostics>()
19
+ const LIMIT = 20
20
+
21
+ /** Diagnostics never include request payloads, stack traces or URL credentials. */
22
+ export function runtimeMessage(value: unknown): string {
23
+ const text = value instanceof Error ? value.message : typeof value === 'string' ? value : ''
24
+ return (text || 'An unexpected workbench error occurred.')
25
+ .replace(/[a-z][a-z\d+.-]*:\/\/[^\s"'<>]+/gi, value => {
26
+ try { const url = new URL(value); return `${url.origin === 'null' ? `${url.protocol}//${url.host}` : url.origin}${url.pathname}` } catch { return '[URL]' }
27
+ })
28
+ .replace(/["']?authorization["']?\s*[:=]\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/gi, '[credential redacted]')
29
+ .replace(/\bauthorization\s*[:=]\s*[^\r\n;,]+/gi, '[credential redacted]')
30
+ .replace(/\b(?:bearer|basic)\s+[^\s"',;&]+/gi, '[credential redacted]')
31
+ .replace(/["']?(?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|token|password|secret)["']?\s*[:=]\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/gi, '[credential redacted]')
32
+ .replace(/\b(?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|token|password|secret)\b\s*[:=]\s*[^\s"',;&]+/gi, '[credential redacted]')
33
+ .replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, '[credential redacted]')
34
+ .slice(0, 600)
35
+ }
36
+ const aborted = (value: unknown) => value instanceof Error && value.name === 'AbortError'
37
+
38
+ /** Root-window failures stay separate from the authored iframe's diagnostic contribution. */
39
+ export class WorkbenchRuntimeDiagnostics {
40
+ readonly state = createObservable<RuntimeSnapshot>({ issues: [], reportFailed: false })
41
+ readonly #workbench: Workbench
42
+ readonly #instance = `workbench:${globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}`
43
+ readonly #seen = new WeakSet<object>()
44
+ readonly #attempted = new Map<string, string>()
45
+ readonly #requests = new Map<number, { document?: string, key: string, feedbackOwner?: WorkspaceRequestEvent['feedbackOwner'], authoringSource?: CanonicalSource }>()
46
+ readonly #latestRequests = new Map<string, number>()
47
+ #reports = Promise.resolve()
48
+ #timer?: ReturnType<typeof setTimeout>
49
+ #stop?: () => void
50
+ #active = false
51
+ #document?: string
52
+
53
+ constructor(workbench: Workbench) { this.#workbench = workbench }
54
+
55
+ start(): () => void {
56
+ if (this.#stop) return this.#stop
57
+ this.#active = true
58
+ const error = (event: ErrorEvent | Event) => {
59
+ if (event instanceof ErrorEvent) this.capture(event.error ?? event.message)
60
+ else if (event.target instanceof HTMLScriptElement || event.target instanceof HTMLLinkElement) {
61
+ const source = event.target instanceof HTMLScriptElement ? event.target.src : event.target.href
62
+ this.capture(`A workbench resource could not be loaded: ${source}`)
63
+ }
64
+ }
65
+ const rejection = (event: PromiseRejectionEvent) => this.capture(event.reason)
66
+ window.addEventListener('error', error, true)
67
+ window.addEventListener('unhandledrejection', rejection)
68
+ const unwatchRequests = this.#workbench.client.subscribeRequests?.(event => this.#request(event))
69
+ const unwatchWorkbench = this.#workbench.subscribe(() => this.#schedule())
70
+ this.#schedule()
71
+ this.#stop = () => {
72
+ this.#active = false
73
+ clearTimeout(this.#timer)
74
+ this.#timer = undefined
75
+ window.removeEventListener('error', error, true)
76
+ window.removeEventListener('unhandledrejection', rejection)
77
+ unwatchRequests?.()
78
+ unwatchWorkbench()
79
+ this.#requests.clear()
80
+ this.#latestRequests.clear()
81
+ if (this.#document !== undefined) this.#send(this.#document, [])
82
+ this.#stop = undefined
83
+ }
84
+ return this.#stop
85
+ }
86
+
87
+ capture(error: unknown): void {
88
+ if (!this.#active || aborted(error) || isObservedWorkspaceFailure(error)) return
89
+ if (typeof error === 'object' && error !== null) {
90
+ if (this.#seen.has(error)) return
91
+ this.#seen.add(error)
92
+ }
93
+ const document = this.#workbench.state.document
94
+ const message = runtimeMessage(error)
95
+ this.#add({ key: JSON.stringify(['error', document, message]), document, checkId: 'presmium.workbench.error', message, authoringSource: this.#source(document) })
96
+ }
97
+
98
+ #source(document: string | undefined): CanonicalSource | undefined {
99
+ const value = this.#workbench.state.authoring
100
+ if (document === undefined || value?.directory !== document || !value.content || !value.version || !value.fingerprint) return
101
+ return { documentId: value.content.id, version: value.version, fingerprint: value.fingerprint }
102
+ }
103
+
104
+ #request(event: WorkspaceRequestEvent): void {
105
+ if (event.kind === 'start') {
106
+ const document = event.document === '' ? this.#workbench.state.document : event.document
107
+ const key = JSON.stringify(['request', document, event.method, event.path, event.feedbackOwner])
108
+ this.#requests.set(event.requestId, { document, key, feedbackOwner: event.feedbackOwner, authoringSource: this.#source(document) })
109
+ this.#latestRequests.delete(key)
110
+ this.#latestRequests.set(key, event.requestId)
111
+ while (this.#requests.size > LIMIT * 8) this.#requests.delete(this.#requests.keys().next().value!)
112
+ while (this.#latestRequests.size > LIMIT * 4) this.#latestRequests.delete(this.#latestRequests.keys().next().value!)
113
+ return
114
+ }
115
+ const request = this.#requests.get(event.requestId)
116
+ this.#requests.delete(event.requestId)
117
+ if (!request || this.#latestRequests.get(request.key) !== event.requestId || event.kind === 'cancel') return
118
+ const { document, key, feedbackOwner, authoringSource } = request
119
+ if (event.kind === 'recovery') {
120
+ const issues = this.state.getSnapshot().issues
121
+ if (issues.some(issue => issue.key === key)) this.#replace(issues.filter(issue => issue.key !== key))
122
+ return
123
+ }
124
+ const message = `${event.method} /api/presmium/${event.path}: ${event.status === undefined ? 'the workspace service could not be reached' : `HTTP ${event.status}`}`
125
+ this.#add({ key, document, checkId: 'presmium.workbench.request', message, authoringSource, ...event.status !== undefined && feedbackOwner ? { feedbackOwner } : {} })
126
+ }
127
+
128
+ #add(issue: RuntimeIssue): void {
129
+ const issues = this.state.getSnapshot().issues
130
+ const known = issues.find(known => known.key === issue.key)
131
+ if (known) {
132
+ if (known.message !== issue.message || known.feedbackOwner !== issue.feedbackOwner || JSON.stringify(known.authoringSource) !== JSON.stringify(issue.authoringSource)) this.#replace(issues.map(value => value === known ? issue : value))
133
+ return
134
+ }
135
+ this.#replace([...issues, issue].slice(-LIMIT))
136
+ }
137
+ #replace(issues: readonly RuntimeIssue[]): void {
138
+ this.state.set({ ...this.state.getSnapshot(), issues })
139
+ this.#schedule()
140
+ }
141
+
142
+ visible(): readonly RuntimeIssue[] {
143
+ const { document } = this.#workbench.state
144
+ return this.state.getSnapshot().issues.filter(issue => (issue.document === undefined || issue.document === document) && issue.feedbackOwner !== 'inline-preview')
145
+ }
146
+
147
+ dismiss(): void {
148
+ const document = this.#workbench.state.document
149
+ this.#replace(this.state.getSnapshot().issues.filter(issue => issue.document !== undefined && issue.document !== document))
150
+ }
151
+ retry(): void {
152
+ const document = this.#workbench.state.document
153
+ if (document !== undefined) this.#attempted.delete(document)
154
+ this.#schedule()
155
+ }
156
+
157
+ #schedule(): void {
158
+ if (!this.#active || this.#timer !== undefined) return
159
+ this.#timer = setTimeout(() => {
160
+ this.#timer = undefined
161
+ const { document } = this.#workbench.state
162
+ if (this.#document !== undefined && this.#document !== document) this.#send(this.#document, [])
163
+ this.#document = document
164
+ if (document === undefined) return
165
+ const findings: AuthoringDiagnostics['findings'] = this.state.getSnapshot().issues.filter(issue => issue.document === document).map(({ checkId, message, authoringSource }) => ({ checkId, message, severity: 'error', ...authoringSource ? { authoringSource } : {} }))
166
+ this.#send(document, findings)
167
+ }, 100)
168
+ }
169
+
170
+ #send(document: string, findings: AuthoringDiagnostics['findings']): void {
171
+ const signature = JSON.stringify(findings)
172
+ if (this.#attempted.get(document) === signature) return
173
+ this.#attempted.delete(document)
174
+ this.#attempted.set(document, signature)
175
+ while (this.#attempted.size > LIMIT * 2) this.#attempted.delete(this.#attempted.keys().next().value!)
176
+ const report: AuthoringDiagnostics = { document, instance: this.#instance, findings }
177
+ const current = () => this.#active && this.#workbench.state.document === document && this.#attempted.get(document) === signature
178
+ this.#reports = this.#reports.then(async () => {
179
+ // A queued failure may belong to a document that was closed before delivery.
180
+ // Empty reports still clear this instance's old contribution in queue order.
181
+ if (findings.length && !current()) {
182
+ if (this.#attempted.get(document) === signature) this.#attempted.delete(document)
183
+ return
184
+ }
185
+ try {
186
+ const result = await this.#workbench.client.reportAuthoringDiagnostics(report) as { accepted?: boolean }
187
+ const failed = result?.accepted !== true
188
+ if (current()) this.state.set({ ...this.state.getSnapshot(), reportFailed: failed })
189
+ } catch {
190
+ if (current()) this.state.set({ ...this.state.getSnapshot(), reportFailed: true })
191
+ }
192
+ })
193
+ }
194
+ }
195
+
196
+ export function runtimeDiagnostics(workbench: Workbench): WorkbenchRuntimeDiagnostics {
197
+ let diagnostics = instances.get(workbench)
198
+ if (!diagnostics) { diagnostics = new WorkbenchRuntimeDiagnostics(workbench); instances.set(workbench, diagnostics) }
199
+ return diagnostics
200
+ }
201
+
202
+ export function RuntimeDiagnosticsNotice({ workbench, fatal = false }: { workbench: Workbench, fatal?: boolean }) {
203
+ const diagnostics = runtimeDiagnostics(workbench)
204
+ const snapshot = useSyncExternalStore(diagnostics.state.subscribe, diagnostics.state.getSnapshot)
205
+ useSyncExternalStore(workbench.subscribe, workbench.getState)
206
+ const t = useClientMessages(workbench)
207
+ const issues = diagnostics.visible()
208
+ if (!fatal && !issues.length) return null
209
+ return <div className="pmw-runtime-feedback"><PmNotice tone="danger">
210
+ <p>{fatal ? t.runtimeStopped : t.runtimeProblems}</p>
211
+ {issues.length > 0 && <details><summary>{t.runtimeDetails}</summary><ul>{issues.map(issue => <li key={issue.key}>{issue.message}</li>)}</ul></details>}
212
+ {snapshot.reportFailed && <p>{t.runtimeReportFailed}</p>}
213
+ <div className="pmw-runtime-feedback__actions">
214
+ {snapshot.reportFailed && <PmButton small onClick={() => diagnostics.retry()}>{t.runtimeRetry}</PmButton>}
215
+ {!fatal && <PmButton small onClick={() => diagnostics.dismiss()}>{t.runtimeDismiss}</PmButton>}
216
+ <PmButton small icon="refresh" onClick={() => location.reload()}>{t.runtimeReload}</PmButton>
217
+ </div>
218
+ </PmNotice></div>
219
+ }
220
+
221
+ export class WorkbenchRuntimeBoundary extends Component<{ workbench: Workbench, children: ReactNode }, { failed: boolean }> {
222
+ state = { failed: false }
223
+ static getDerivedStateFromError() { return { failed: true } }
224
+ componentDidCatch(error: Error): void { runtimeDiagnostics(this.props.workbench).capture(error) }
225
+ render() {
226
+ return this.state.failed ? <main className="pmw-runtime-fallback pm-ui"><RuntimeDiagnosticsNotice workbench={this.props.workbench} fatal /></main> : this.props.children
227
+ }
228
+ }
@@ -0,0 +1,26 @@
1
+ import { MODEL_SETTINGS } from './contract.ts'
2
+
3
+ type OpenSection = (id: string) => void
4
+
5
+ let remembered: OpenSection | undefined
6
+
7
+ /**
8
+ * Keeps the settings shell's `openSection`, which it hands only to the onboarding steps it mounts (Presmium's welcome
9
+ * notice and the step in place of the DeepSeek sign-in, both at every page load), so other places can open a section.
10
+ */
11
+ export function rememberOpenSection(openSection: OpenSection | undefined): void {
12
+ if (openSection) remembered = openSection
13
+ }
14
+
15
+ /** Opens Settings at Models, as the welcome notice's "Set up a model" does; false before the shell has offered a way. */
16
+ export function openModelSettings(): boolean {
17
+ if (!remembered) return false
18
+ remembered(MODEL_SETTINGS.section)
19
+ return true
20
+ }
21
+
22
+ export function openSpeechSettings(): boolean {
23
+ if (!remembered) return false
24
+ remembered('presmium-speech')
25
+ return true
26
+ }
@@ -0,0 +1,71 @@
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
+ import { normalizePath } from './paths.ts'
5
+
6
+ const ID = '@lincyaw/dsh/source'
7
+ // A document's Markdown is shown as the document; other files keep Harness's own previews.
8
+ const DOCUMENT_SOURCE = /\.(?:md|markdown)$/i
9
+ const RESERVED = new Set(['node_modules', 'dist', '.git', '.presmium'])
10
+
11
+ /** Routes Harness's links to a document's Markdown (with a line) to that place in the open document. */
12
+ export function registerSourceLinks(ctx: DshClientContext, workbench: Workbench, showEditor: (layout: DshClientContext['layout']) => void): void {
13
+ ctx.inject(['sidebarRightTabs', 'sidebarRight', 'sessions', 'layout'], inner => inner.effect(() => {
14
+ let root: string | undefined
15
+ let active = true
16
+ void workbench.client.readWorkbenchInfo().then(answer => { if (active && answer.root) root = normalizePath(answer.root) }).catch(() => {})
17
+ /** The file a link names, relative to the workspace root, and the document it is in (none: the open one stays). */
18
+ const locate = (address: string): { path: string, document?: string } | undefined => {
19
+ const file = parseFileAddress(address)
20
+ if (!root || file?.scope !== 'session') return
21
+ const session = inner.sessions.list.getSnapshot().byId[file.sessionId]
22
+ if (!session?.cwd || normalizePath(session.cwd) !== root || !DOCUMENT_SOURCE.test(file.path)) return
23
+ const absolute = normalizePath(resolveWorkspacePath(session.cwd, file.path))
24
+ if (!absolute || !isAbsoluteWorkspacePath(absolute) || !absolute.startsWith(`${root}/`)) return
25
+ const path = absolute.slice(root.length + 1)
26
+ if (path.split('/').some(part => RESERVED.has(part))) return
27
+ const documents = workbench.state.documents
28
+ if (!documents) return
29
+ const document = [...documents.documents].sort((a, b) => b.dir.length - a.dir.length).find(doc => !doc.dir || path.startsWith(`${doc.dir}/`))?.dir
30
+ if (document === undefined) return
31
+ return { path, document }
32
+ }
33
+ function SourceLink({ useTabInfo }: DshSourceTabProps) {
34
+ const { tab } = useTabInfo()
35
+ const opened = useRef<number>()
36
+ useLayoutEffect(() => {
37
+ if (!tab.visible || opened.current === tab.navigation.revision) return
38
+ const target = locate(tab.contentId)
39
+ if (!target) return
40
+ opened.current = tab.navigation.revision
41
+ const line = tab.navigation.params?.line
42
+ tab.actions.close()
43
+ if (inner.sidebarRight.isExpanded()) inner.sidebarRight.toggleExpanded()
44
+ showEditor(inner.layout)
45
+ void workbench.revealSource(target.path, typeof line === 'number' && Number.isInteger(line) && line > 0 ? line : 1)
46
+ }, [tab.visible, tab.contentId, tab.navigation.revision])
47
+ return <span data-pmw-source-route="" hidden />
48
+ }
49
+ // Previously saved Harness text tabs keep their kind when restored. Route those too.
50
+ function RestoredSource({ absolutePath }: { absolutePath: string }) {
51
+ const documents = useSyncExternalStore(workbench.subscribe, () => workbench.state.documents)
52
+ useLayoutEffect(() => {
53
+ const tab = inner.sidebarRight.active()
54
+ if (tab?.kind !== 'text') return
55
+ const target = locate(tab.contentId)
56
+ if (!target || normalizePath(absolutePath) !== normalizePath(resolveWorkspacePath(root, target.path))) return
57
+ inner.sidebarRight.close(tab.id)
58
+ if (inner.sidebarRight.isExpanded()) inner.sidebarRight.toggleExpanded()
59
+ showEditor(inner.layout)
60
+ void workbench.revealSource(target.path)
61
+ }, [absolutePath, documents])
62
+ return null
63
+ }
64
+ 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) })
65
+ let offBody: (() => void) | undefined
66
+ inner.slots.inject('sidebar.right.pane.tab', () => { offBody = inner.slots.register({ name: 'sidebar.right.pane.tab', key: ID }, SourceLink) })
67
+ let offRestored: (() => void) | undefined
68
+ inner.slots.inject('sidebar.right.tab.document.actions', () => { offRestored = inner.slots.register({ name: 'sidebar.right.tab.document.actions', id: ID }, RestoredSource) })
69
+ return () => { active = false; offRestored?.(); offBody?.(); offType() }
70
+ }, 'presmium: links to a document\'s source show the document'))
71
+ }
@@ -0,0 +1,7 @@
1
+ import { SpeechSettingsPanel, SPEECH_SETTINGS_MESSAGES, WorkbenchProvider } from '@lincyaw/workbench'
2
+ import type { Workbench } from '@lincyaw/workbench'
3
+
4
+ /** The framework owns Settings navigation; Presmium owns its operator service page. */
5
+ export function registerSpeechSettings(ctx: DshClientContext, workbench: Workbench): void {
6
+ ctx.effect(() => ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'presmium-speech', order: 30, label: () => SPEECH_SETTINGS_MESSAGES[workbench.state.locale].title }, () => <WorkbenchProvider workbench={workbench}><SpeechSettingsPanel /></WorkbenchProvider>)), 'presmium: voice service settings')
7
+ }
@@ -0,0 +1,45 @@
1
+ @import '@lincyaw/design/design.css';
2
+ @import '@lincyaw/workbench/styles.css';
3
+ @import './frame.css';
4
+
5
+ /* The kitten among the conversation header's actions. */
6
+ .pmw-conversation-mascot {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ }
10
+
11
+ /* The kitten's row in Settings → General, laid out as Harness's own rows: its name and note, then its choice. */
12
+ .pmw-mascot-setting {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ gap: var(--pm-space-4);
17
+ padding-block: var(--pm-space-4);
18
+ border-top: var(--pm-size-hairline) solid var(--pm-color-line);
19
+ }
20
+
21
+ .pmw-mascot-setting__copy {
22
+ display: grid;
23
+ gap: var(--pm-space-1);
24
+ min-width: 0;
25
+ }
26
+
27
+ .pmw-mascot-setting__title {
28
+ margin: 0;
29
+ color: var(--pm-color-ink);
30
+ font-size: var(--pm-text-sm);
31
+ }
32
+
33
+ .pmw-mascot-setting__hint {
34
+ margin: 0;
35
+ color: var(--pm-color-muted);
36
+ font-size: var(--pm-text-xs);
37
+ }
38
+
39
+ .pmw-mascot-setting .pm-ui-segmented {
40
+ flex: none;
41
+ }
42
+
43
+ @media (max-width: 767px) {
44
+ .pmw-mascot-setting { flex-wrap: wrap; }
45
+ }