@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.
- package/LICENSE +202 -0
- package/README.md +26 -2
- package/build/client.mjs +72 -0
- package/build/dist/client.js +1390 -0
- package/build/dist/client.json +274 -0
- package/build/ensure.mjs +39 -0
- package/cordis.patch.yml +26 -0
- package/entries/context.mjs +7 -0
- package/entries/events.mjs +7 -0
- package/entries/load.mjs +11 -0
- package/entries/policy.mjs +7 -0
- package/entries/routes.mjs +7 -0
- package/entries/service.mjs +7 -0
- package/entries/skills.mjs +7 -0
- package/entries/telemetry.mjs +7 -0
- package/entries/tools.mjs +7 -0
- package/entries/turns.mjs +7 -0
- package/entries/workbench.mjs +9 -0
- package/entries/workspaces.mjs +7 -0
- package/package.json +108 -3
- package/profile.patch.yml +25 -0
- 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 +97 -0
- package/src/client/contract.ts +82 -0
- package/src/client/document-context.ts +39 -0
- package/src/client/dsh.d.ts +155 -0
- package/src/client/fallback.tsx +53 -0
- package/src/client/frame.css +505 -0
- package/src/client/index.tsx +157 -0
- package/src/client/launch-target.ts +89 -0
- package/src/client/layout.ts +230 -0
- package/src/client/mascot.tsx +37 -0
- package/src/client/messages.ts +171 -0
- package/src/client/navigation-shell.tsx +74 -0
- 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-links.tsx +71 -0
- package/src/client/speech-settings.tsx +7 -0
- package/src/client/styles.css +45 -0
- package/src/client/theme.ts +151 -0
- 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 +166 -0
- package/src/node/dsh-types.ts +124 -0
- package/src/node/fake-collector.ts +46 -0
- package/src/node/fake-model.ts +80 -0
- package/src/node/launch.ts +176 -0
- package/src/node/policy.ts +56 -0
- package/src/node/routes.ts +56 -0
- package/src/node/server-routes.ts +3 -0
- package/src/node/service.ts +131 -0
- package/src/node/skills.ts +24 -0
- package/src/node/telemetry.ts +126 -0
- package/src/node/tools.ts +66 -0
- package/src/node/turns.ts +176 -0
- package/src/node/watchdog.mjs +36 -0
- package/src/node/workbench.ts +88 -0
- package/src/node/workspace-events.ts +167 -0
- package/src/node/workspaces.ts +10 -0
- package/src/workspace-events.ts +31 -0
- package/web.patch.yml +161 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { LOCAL_AUTHOR } from '@lincyaw/kernel'
|
|
2
|
+
import type { DshContext, DshOtelScalar } from './dsh-types.ts'
|
|
3
|
+
import type { OperationClient, OperationRecord, PresmiumTelemetry } from './telemetry.ts'
|
|
4
|
+
|
|
5
|
+
export const name = 'presmium-workbench'
|
|
6
|
+
export const inject = ['presmium', 'connection', 'webServer', 'presmiumTelemetry']
|
|
7
|
+
|
|
8
|
+
const ICON = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#0F5F5C"/><path d="M11 8h7.5a6.5 6.5 0 0 1 0 13H15v4h-4zM15 12v5h3.5a2.5 2.5 0 0 0 0-5z" fill="#F7F5F0" fill-rule="evenodd"/></svg>')}`
|
|
9
|
+
|
|
10
|
+
export function brandIndex(html: string): string {
|
|
11
|
+
return html
|
|
12
|
+
.replace(/<title>[^<]*<\/title>/, '<title>Presmium</title>')
|
|
13
|
+
.replace(/<link rel="(?:icon|manifest)"[^>]*>\s*/g, '')
|
|
14
|
+
.replace('</head>', `<link rel="icon" type="image/svg+xml" href="${ICON}" />\n</head>`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface WebServer {
|
|
18
|
+
tapIndex(transform: (html: string) => string): () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const OPERATION = /^[a-z][a-z0-9.-]{0,63}$/
|
|
22
|
+
const RECORDS = 500
|
|
23
|
+
const ATTRIBUTES = 64
|
|
24
|
+
const TEXT = 100_000
|
|
25
|
+
|
|
26
|
+
const text = (value: unknown, limit: number): string | undefined => (typeof value === 'string' && value ? value.slice(0, limit) : undefined)
|
|
27
|
+
|
|
28
|
+
/** The page's report of the author's operations, kept to names, times and scalar attributes of bounded size. */
|
|
29
|
+
export function readOperations(body: unknown): { client: OperationClient, records: OperationRecord[] } | undefined {
|
|
30
|
+
if (!body || typeof body !== 'object') return undefined
|
|
31
|
+
const { client, records } = body as { client?: Record<string, unknown>, records?: unknown }
|
|
32
|
+
const id = text(client?.id, 64)
|
|
33
|
+
if (!id || !Array.isArray(records) || records.length > RECORDS) return undefined
|
|
34
|
+
const read: OperationRecord[] = []
|
|
35
|
+
for (const record of records as Record<string, unknown>[]) {
|
|
36
|
+
if (!record || typeof record.name !== 'string' || !OPERATION.test(record.name) || typeof record.time !== 'number' || typeof record.seq !== 'number') return undefined
|
|
37
|
+
const attributes: Record<string, DshOtelScalar> = {}
|
|
38
|
+
for (const [key, value] of Object.entries(record.attributes ?? {}).slice(0, ATTRIBUTES)) {
|
|
39
|
+
if (typeof value === 'string') attributes[key.slice(0, 128)] = value.slice(0, TEXT)
|
|
40
|
+
else if ((typeof value === 'number' && Number.isFinite(value)) || typeof value === 'boolean') attributes[key.slice(0, 128)] = value
|
|
41
|
+
}
|
|
42
|
+
read.push({ name: record.name, time: record.time, seq: record.seq, attributes })
|
|
43
|
+
}
|
|
44
|
+
const browser = text(client?.browser, 64)
|
|
45
|
+
const locale = text(client?.locale, 32)
|
|
46
|
+
return { client: { id, ...browser ? { browser } : {}, ...locale ? { locale } : {} }, records: read }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function apply(ctx: DshContext & { webServer: WebServer, presmiumTelemetry: PresmiumTelemetry }): void {
|
|
50
|
+
ctx.effect(() => ctx.webServer.tapIndex(brandIndex))
|
|
51
|
+
ctx.connection.fetch.register({
|
|
52
|
+
path: '/api/presmium/context', methods: ['POST'], requestBody: 'buffered',
|
|
53
|
+
async fetch(request) {
|
|
54
|
+
const body = await request.json().catch(() => null) as { session?: unknown, document?: unknown } | null
|
|
55
|
+
if (!body || typeof body.session !== 'string' || !body.session || (body.document !== null && typeof body.document !== 'string')) return new Response('Invalid context', { status: 400 })
|
|
56
|
+
try {
|
|
57
|
+
await ctx.presmium.bindings.selectDocument(body.session, body.document)
|
|
58
|
+
return Response.json({ session: body.session, document: body.document })
|
|
59
|
+
} catch {
|
|
60
|
+
return new Response('Unknown document', { status: 400 })
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
// The workbench's operations, for the operator's collector: the page learns whether to record them, then sends them in batches.
|
|
65
|
+
ctx.connection.fetch.register({
|
|
66
|
+
path: '/api/presmium/telemetry',
|
|
67
|
+
methods: ['GET', 'POST'],
|
|
68
|
+
requestBody: 'buffered',
|
|
69
|
+
async fetch(request) {
|
|
70
|
+
const telemetry = ctx.presmiumTelemetry
|
|
71
|
+
if (request.method === 'GET') return Response.json({ enabled: telemetry.enabled })
|
|
72
|
+
if (!telemetry.enabled) return new Response(null, { status: 204 })
|
|
73
|
+
const read = readOperations(await request.json().catch(() => null))
|
|
74
|
+
if (!read) return new Response('Invalid operations', { status: 400 })
|
|
75
|
+
telemetry.operations(read.client, read.records)
|
|
76
|
+
return new Response(null, { status: 204 })
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
ctx.connection.fetch.register({
|
|
80
|
+
path: '/api/presmium/workbench',
|
|
81
|
+
methods: ['GET'],
|
|
82
|
+
requestBody: 'buffered',
|
|
83
|
+
async fetch() {
|
|
84
|
+
const { url, root } = await ctx.presmium.server.ready
|
|
85
|
+
return new Response(JSON.stringify({ mcp: `${url}/mcp`, root, member: LOCAL_AUTHOR }), { headers: { 'Content-Type': 'application/json' } })
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http'
|
|
2
|
+
import type { Duplex } from 'node:stream'
|
|
3
|
+
import { EventSource } from 'eventsource'
|
|
4
|
+
import { WebSocketServer, WebSocket } from 'ws'
|
|
5
|
+
import { AUTHORING_CLIENT_EVENT_MAX_BYTES } from '@lincyaw/kernel'
|
|
6
|
+
import type { AuthoringClientLease, AuthoringClientEvent } from '@lincyaw/kernel'
|
|
7
|
+
import { validateAuthoringClientLease } from '@lincyaw/kernel/node'
|
|
8
|
+
import type { DshContext } from './dsh-types.ts'
|
|
9
|
+
import { SERVER_ROUTES, WORKSPACE_ROUTES } from './server-routes.ts'
|
|
10
|
+
import { WORKSPACE_EVENTS_PATH, workspaceEventCommand } from '../workspace-events.ts'
|
|
11
|
+
import type { WorkspaceEventMessage } from '../workspace-events.ts'
|
|
12
|
+
|
|
13
|
+
export const name = 'presmium-events'
|
|
14
|
+
export const inject = ['presmium', 'connection', 'webServer']
|
|
15
|
+
|
|
16
|
+
/** The documented WebServer upgrade and Connection admission faces. */
|
|
17
|
+
interface EventContext extends DshContext {
|
|
18
|
+
webServer: { registerUpgrade(route: { path: string, handler(request: IncomingMessage, socket: Duplex, head: Buffer): void }): () => void }
|
|
19
|
+
connection: DshContext['connection'] & {
|
|
20
|
+
admit(request: IncomingMessage): { rejection: 401 | 403 } | { peer: { principal: { kind: string }, signal: AbortSignal } }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ROUTES = {
|
|
25
|
+
document: { path: `${WORKSPACE_ROUTES}document/events`, event: 'document' },
|
|
26
|
+
diagnostics: { path: `${SERVER_ROUTES}diagnostics/events`, event: 'diagnostics' },
|
|
27
|
+
review: { path: `${WORKSPACE_ROUTES}review`, event: 'review' },
|
|
28
|
+
ir: { path: `${WORKSPACE_ROUTES}ir/events`, event: 'revision' },
|
|
29
|
+
jobs: { path: `${WORKSPACE_ROUTES}jobs/events`, event: 'job' },
|
|
30
|
+
} as const
|
|
31
|
+
const MAX_SUBSCRIPTIONS = 16
|
|
32
|
+
const MAX_BUFFERED_BYTES = 2 << 20
|
|
33
|
+
|
|
34
|
+
/** Borrow the framework listener and trust fence; never take ownership of their server or Peer. */
|
|
35
|
+
export function apply(ctx: EventContext): void {
|
|
36
|
+
const server = new WebSocketServer({ noServer: true, maxPayload: AUTHORING_CLIENT_EVENT_MAX_BYTES, perMessageDeflate: false })
|
|
37
|
+
const sockets = new Map<WebSocket, () => void>()
|
|
38
|
+
const unregister = ctx.webServer.registerUpgrade({
|
|
39
|
+
path: WORKSPACE_EVENTS_PATH,
|
|
40
|
+
handler(request, socket, head) {
|
|
41
|
+
const admission = ctx.connection.admit(request)
|
|
42
|
+
const rejected = 'rejection' in admission ? admission.rejection : admission.peer.principal.kind === 'owner' ? undefined : 403
|
|
43
|
+
if (rejected !== undefined) {
|
|
44
|
+
socket.end(`HTTP/1.1 ${rejected} Rejected\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
if (!('peer' in admission) || admission.peer.signal.aborted) { socket.destroy(); return }
|
|
48
|
+
const peer = admission.peer
|
|
49
|
+
server.handleUpgrade(request, socket, head, ws => {
|
|
50
|
+
const subscriptions = new Map<string, { stop(): void, publish?(command: Extract<import('../workspace-events.ts').WorkspaceEventCommand, { type: 'authoring-client-publish' }>): Promise<void> }>()
|
|
51
|
+
let closed = false
|
|
52
|
+
const cleanup = () => {
|
|
53
|
+
if (closed) return
|
|
54
|
+
closed = true
|
|
55
|
+
peer.signal.removeEventListener('abort', revoked)
|
|
56
|
+
for (const entry of subscriptions.values()) entry.stop()
|
|
57
|
+
subscriptions.clear()
|
|
58
|
+
}
|
|
59
|
+
const revoked = () => { cleanup(); ws.terminate() }
|
|
60
|
+
const send = (message: WorkspaceEventMessage) => {
|
|
61
|
+
if (closed || ws.readyState !== WebSocket.OPEN) return
|
|
62
|
+
const payload = JSON.stringify(message)
|
|
63
|
+
if (ws.bufferedAmount + Buffer.byteLength(payload) > MAX_BUFFERED_BYTES) { cleanup(); ws.close(1013, 'Workspace event consumer is too slow'); return }
|
|
64
|
+
ws.send(payload, error => { if (error) revoked() })
|
|
65
|
+
}
|
|
66
|
+
sockets.set(ws, cleanup)
|
|
67
|
+
peer.signal.addEventListener('abort', revoked, { once: true })
|
|
68
|
+
if (peer.signal.aborted) { revoked(); return }
|
|
69
|
+
ws.on('close', () => { cleanup(); sockets.delete(ws) })
|
|
70
|
+
ws.on('error', revoked)
|
|
71
|
+
ws.on('message', (data, binary) => {
|
|
72
|
+
let raw: unknown
|
|
73
|
+
try { raw = binary ? undefined : JSON.parse(data.toString()) } catch { raw = undefined }
|
|
74
|
+
const parsed = workspaceEventCommand.safeParse(raw)
|
|
75
|
+
if (!parsed.success) { cleanup(); ws.close(1008, 'Invalid workspace subscription'); return }
|
|
76
|
+
const command = parsed.data
|
|
77
|
+
if (command.type === 'unsubscribe') {
|
|
78
|
+
subscriptions.get(command.id)?.stop()
|
|
79
|
+
subscriptions.delete(command.id)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (command.type === 'authoring-client-publish') {
|
|
83
|
+
const entry = subscriptions.get(command.id)
|
|
84
|
+
void (entry?.publish ? entry.publish(command) : Promise.reject()).then(() => {
|
|
85
|
+
if (subscriptions.get(command.id) === entry) send({ type: 'published', id: command.id, sequence: command.sequence, ok: true })
|
|
86
|
+
}, () => { if (subscriptions.get(command.id) === entry) send({ type: 'published', id: command.id, sequence: command.sequence, ok: false }) })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (subscriptions.has(command.id) || subscriptions.size >= MAX_SUBSCRIPTIONS) { cleanup(); ws.close(1008, 'Workspace subscription limit'); return }
|
|
90
|
+
const controller = new AbortController()
|
|
91
|
+
let source: EventSource | undefined
|
|
92
|
+
const entry: { stop(): void, publish?(command: Extract<import('../workspace-events.ts').WorkspaceEventCommand, { type: 'authoring-client-publish' }>): Promise<void> } = { stop: () => { controller.abort(); source?.close() } }
|
|
93
|
+
subscriptions.set(command.id, entry)
|
|
94
|
+
const current = () => !closed && !controller.signal.aborted && subscriptions.get(command.id) === entry
|
|
95
|
+
void ctx.presmium.server.ready.then(endpoint => {
|
|
96
|
+
if (!current()) return
|
|
97
|
+
if (command.topic === 'authoring-client') {
|
|
98
|
+
// Keep the client credential on the trusted host, off browser messages and MCP reads.
|
|
99
|
+
let lease: AuthoringClientLease | undefined
|
|
100
|
+
let publication: Promise<void> = Promise.resolve()
|
|
101
|
+
let publications = 0
|
|
102
|
+
const post = async (kind: string, value: unknown) => {
|
|
103
|
+
const path = `${WORKSPACE_ROUTES}authoring-clients/${kind}?${new URLSearchParams({ document: command.document })}`
|
|
104
|
+
const response = await ctx.presmium.server.request(path, { method: 'POST', body: value, signal: controller.signal })
|
|
105
|
+
if (!response.ok) throw new Error('client-publication-refused')
|
|
106
|
+
return response.json() as Promise<unknown>
|
|
107
|
+
}
|
|
108
|
+
entry.publish = value => {
|
|
109
|
+
if (publications >= 8) return Promise.reject(new Error('client-publication-limit'))
|
|
110
|
+
publications++
|
|
111
|
+
const next = publication.then(async () => {
|
|
112
|
+
if (!current() || !lease || value.connectionId !== lease.connectionId || !value.value || typeof value.value !== 'object'
|
|
113
|
+
|| !('sequence' in value.value) || value.value.sequence !== value.sequence) throw new Error('client-connection-changed')
|
|
114
|
+
await post(value.kind === 'ack' ? 'ack' : 'report', { connection: lease, report: value.value })
|
|
115
|
+
}).finally(() => { publications-- })
|
|
116
|
+
publication = next.catch(() => {})
|
|
117
|
+
return next
|
|
118
|
+
}
|
|
119
|
+
void post('register', { clientId: command.clientId, epoch: command.epoch, snapshot: command.snapshot }).then(value => {
|
|
120
|
+
if (!current()) return
|
|
121
|
+
if (!validateAuthoringClientLease(value)) throw new Error('client-registration-invalid')
|
|
122
|
+
lease = value
|
|
123
|
+
const query = new URLSearchParams({ document: command.document, clientId: lease.clientId, epoch: lease.epoch, connectionId: lease.connectionId })
|
|
124
|
+
const path = `${WORKSPACE_ROUTES}authoring-clients/events?${query}`
|
|
125
|
+
source = new EventSource(new URL(path, endpoint.url), {
|
|
126
|
+
fetch: (_address, init) => ctx.presmium.server.request(path, { headers: { ...Object.fromEntries(new Headers(init?.headers)), 'X-Presmium-Authoring-Client': value.credential }, signal: init?.signal ? AbortSignal.any([controller.signal, init.signal]) : controller.signal }),
|
|
127
|
+
})
|
|
128
|
+
source.addEventListener('error', () => { if (current()) send({ id: command.id, type: 'error' }) })
|
|
129
|
+
source.addEventListener('authoring-client', event => {
|
|
130
|
+
if (!current()) return
|
|
131
|
+
let data: AuthoringClientEvent
|
|
132
|
+
try { data = JSON.parse(event.data) } catch { return }
|
|
133
|
+
if (data.type === 'connected' && lease) lease = { ...lease, connectionId: data.connectionId }
|
|
134
|
+
send({ id: command.id, type: 'event', data: event.data })
|
|
135
|
+
})
|
|
136
|
+
}).catch(() => { if (current()) send({ id: command.id, type: 'error' }) })
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
const route = command.topic === 'viewer-site' ? { path: '/__app/events', events: ['ir', 'reload'] }
|
|
140
|
+
: command.topic === 'viewer-control' ? { path: `${SERVER_ROUTES}control`, events: ['ready', 'approval', 'settled', 'go', 'session'] }
|
|
141
|
+
: command.topic === 'approvals' ? { path: `${SERVER_ROUTES}control`, events: ['approval', 'settled'] }
|
|
142
|
+
: { path: ROUTES[command.topic].path, events: [ROUTES[command.topic].event] }
|
|
143
|
+
const query = new URLSearchParams({ document: command.document })
|
|
144
|
+
if (command.topic === 'viewer-site') { query.set('app', command.app); query.set('lang', command.lang); query.set('form', command.form) }
|
|
145
|
+
if (command.topic === 'viewer-control') query.set('window', command.window)
|
|
146
|
+
if (command.topic === 'approvals') query.set('observe', 'approvals')
|
|
147
|
+
const path = `${route.path}?${query}`
|
|
148
|
+
source = new EventSource(new URL(path, endpoint.url), {
|
|
149
|
+
fetch: (_address, init) => ctx.presmium.server.request(path, {
|
|
150
|
+
headers: Object.fromEntries(new Headers(init?.headers)),
|
|
151
|
+
signal: init?.signal ? AbortSignal.any([controller.signal, init.signal]) : controller.signal,
|
|
152
|
+
}),
|
|
153
|
+
})
|
|
154
|
+
source.addEventListener('open', () => { if (current()) send({ id: command.id, type: 'ready' }) })
|
|
155
|
+
source.addEventListener('error', () => { if (current()) send({ id: command.id, type: 'error' }) })
|
|
156
|
+
for (const name of route.events) source.addEventListener(name, event => { if (current()) send({ id: command.id, type: 'event', ...command.topic === 'viewer-site' || command.topic === 'viewer-control' || command.topic === 'approvals' ? { event: name } : {}, data: event.data }) })
|
|
157
|
+
}).catch(() => { if (current()) send({ id: command.id, type: 'error' }) })
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
ctx.effect(() => () => {
|
|
163
|
+
unregister()
|
|
164
|
+
for (const [socket, cleanup] of sockets) { cleanup(); socket.terminate() }
|
|
165
|
+
server.close()
|
|
166
|
+
})
|
|
167
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
import type { DshContext } from './dsh-types.ts'
|
|
3
|
+
|
|
4
|
+
export const name = 'presmium-workspaces'
|
|
5
|
+
export const inject = ['presmium', 'workspaceRegistry']
|
|
6
|
+
|
|
7
|
+
export async function apply(ctx: DshContext): Promise<void> {
|
|
8
|
+
const { root } = await ctx.presmium.server.ready
|
|
9
|
+
await ctx.workspaceRegistry.create(root, basename(root))
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/** One authenticated Studio socket carries these existing workspace subscriptions. */
|
|
4
|
+
export const WORKSPACE_EVENTS_PATH = '/api/presmium/events'
|
|
5
|
+
export const WORKSPACE_EVENT_TOPICS = ['document', 'diagnostics', 'review', 'ir', 'jobs'] as const
|
|
6
|
+
const subscriptionId = z.string().min(1).max(80)
|
|
7
|
+
|
|
8
|
+
const viewerSite = z.strictObject({ type: z.literal('subscribe'), id: subscriptionId, topic: z.literal('viewer-site'), document: z.string().max(1024), app: z.string().max(200), lang: z.string().max(80), form: z.enum(['web', 'slides']) })
|
|
9
|
+
const viewerControl = z.strictObject({ type: z.literal('subscribe'), id: subscriptionId, topic: z.literal('viewer-control'), document: z.string().max(1024), window: z.string().regex(/^[a-zA-Z0-9-]{1,80}$/) })
|
|
10
|
+
/** The host's pending approvals for the workbench, which is not a viewer window. */
|
|
11
|
+
const approvals = z.strictObject({ type: z.literal('subscribe'), id: subscriptionId, topic: z.literal('approvals'), document: z.string().max(1024) })
|
|
12
|
+
|
|
13
|
+
export const workspaceEventCommand = z.union([
|
|
14
|
+
z.strictObject({ type: z.literal('subscribe'), id: subscriptionId, topic: z.literal('authoring-client'), document: z.string().max(1024), clientId: subscriptionId, epoch: subscriptionId, snapshot: z.unknown() }),
|
|
15
|
+
z.strictObject({ type: z.literal('authoring-client-publish'), id: subscriptionId, connectionId: subscriptionId, sequence: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), kind: z.enum(['state', 'ack']), value: z.unknown() }),
|
|
16
|
+
viewerSite, viewerControl, approvals,
|
|
17
|
+
z.strictObject({ type: z.literal('subscribe'), id: subscriptionId, topic: z.enum(WORKSPACE_EVENT_TOPICS), document: z.string().max(1024) }),
|
|
18
|
+
z.strictObject({ type: z.literal('unsubscribe'), id: subscriptionId }),
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
/** Ready describes the actual upstream connection, rather than a fabricated domain snapshot. */
|
|
22
|
+
export const workspaceEventMessage = z.discriminatedUnion('type', [
|
|
23
|
+
z.strictObject({ type: z.literal('published'), id: subscriptionId, sequence: z.number().int().nonnegative(), ok: z.boolean() }),
|
|
24
|
+
z.strictObject({ type: z.literal('ready'), id: subscriptionId }),
|
|
25
|
+
z.strictObject({ type: z.literal('error'), id: subscriptionId }),
|
|
26
|
+
z.strictObject({ type: z.literal('event'), id: subscriptionId, event: z.string().max(40).optional(), data: z.string() }),
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
export type WorkspaceEventTopic = typeof WORKSPACE_EVENT_TOPICS[number] | 'viewer-site' | 'viewer-control' | 'approvals' | 'authoring-client'
|
|
30
|
+
export type WorkspaceEventCommand = z.infer<typeof workspaceEventCommand>
|
|
31
|
+
export type WorkspaceEventMessage = z.infer<typeof workspaceEventMessage>
|
package/web.patch.yml
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
- insert:
|
|
2
|
+
- id: presmium-workspaces
|
|
3
|
+
name: '@lincyaw/dsh/workspaces'
|
|
4
|
+
- id: presmium-routes
|
|
5
|
+
name: '@lincyaw/dsh/routes'
|
|
6
|
+
|
|
7
|
+
- id: workspace-controller
|
|
8
|
+
config:
|
|
9
|
+
documentsDirectory: !!js (process.env.DSH_HOME || `${process.env.XDG_STATE_HOME || `${process.env.HOME}/.local/state`}/presmium/studio`) + '/documents'
|
|
10
|
+
|
|
11
|
+
- id: harness-dev-context
|
|
12
|
+
disabled: true
|
|
13
|
+
|
|
14
|
+
- insert:
|
|
15
|
+
- id: presmium-workbench
|
|
16
|
+
name: '@lincyaw/dsh'
|
|
17
|
+
|
|
18
|
+
- id: ui-layout
|
|
19
|
+
disabled: true
|
|
20
|
+
- id: ui-sidebar
|
|
21
|
+
disabled: true
|
|
22
|
+
- id: ui-brand-official
|
|
23
|
+
disabled: true
|
|
24
|
+
- id: ui-settings-account
|
|
25
|
+
disabled: true
|
|
26
|
+
- id: account-controller
|
|
27
|
+
disabled: true
|
|
28
|
+
- id: deepseek-account
|
|
29
|
+
disabled: true
|
|
30
|
+
- id: llm-deepseek-account
|
|
31
|
+
disabled: true
|
|
32
|
+
- id: web-search-deepseek
|
|
33
|
+
disabled: true
|
|
34
|
+
- id: ui-settings-web-search
|
|
35
|
+
disabled: true
|
|
36
|
+
- id: ui-settings-session-log
|
|
37
|
+
disabled: true
|
|
38
|
+
# Settings pages an author has no use for: the built-in plugins (the plugin rows of every session) and the agent presets
|
|
39
|
+
# (the workbench's sessions use the authoring preset). Technical rows of the General page stay behind the coding view
|
|
40
|
+
# (src/client/author-settings.ts).
|
|
41
|
+
- id: ui-settings-plugins
|
|
42
|
+
disabled: true
|
|
43
|
+
- id: ui-agent-preset
|
|
44
|
+
disabled: true
|
|
45
|
+
# The changed-files and delivery cards under the AI's replies, with the guidance to link every file the reply names: the
|
|
46
|
+
# author sees no files. The workbench makes the changed blocks blink on the document instead.
|
|
47
|
+
- id: ui-deliverables
|
|
48
|
+
disabled: true
|
|
49
|
+
|
|
50
|
+
# 内置的“创作”(authoring)预设:工作台中 Presmium 助手的角色,它面对的作者,以及跟作者说话的方式。预设的
|
|
51
|
+
# persona 遮蔽部署级的人设(profile.patch.yml),所以这里重述 Presmium 的角色;写作的原则在
|
|
52
|
+
# presmium-author 技能中,改写内容时加载。预设只带讲义需要的工具:没有定时任务、目标、子 agent 与文件卡片(present)。
|
|
53
|
+
- insert:
|
|
54
|
+
- id: preset-authoring
|
|
55
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
56
|
+
config:
|
|
57
|
+
id: authoring
|
|
58
|
+
# The preset picker (ui-agent-preset) is off, so authors never see a preset's name; the brand stands in for it in
|
|
59
|
+
# both languages, and a description in one language is left out.
|
|
60
|
+
name: Presmium
|
|
61
|
+
order: 0
|
|
62
|
+
plugins:
|
|
63
|
+
- id: persona
|
|
64
|
+
name: '@deepseek-ai/dsh-persona'
|
|
65
|
+
config:
|
|
66
|
+
prefix: |
|
|
67
|
+
You are Presmium's assistant for the authors of lectures, powered by the {{model}} model. You work with an author in Presmium's workbench: you answer their questions about their documents and make the changes they ask for, in their language.
|
|
68
|
+
|
|
69
|
+
The author is a teacher who may know no code. The workbench shows a document as its readers see it, as a web page or as slides; the author edits its text there, leaves comments on it and asks you for changes. They do not see, and need not know, source files, Markdown, paths or line numbers, git, builds, tools or logs.
|
|
70
|
+
|
|
71
|
+
- The author's messages say what to do. When they point at something (selected text, a picked block), the message names the section and quotes the passage as readers see them, and the document context's focus gives its source range. A message of comments quotes each comment with its place, and comment N is the focus `comment-N`. A comment is the author's own words about that place: one that asks a question is answered, one that asks for a change is carried out, and several are handled one by one.
|
|
72
|
+
- Speak in the terms of what the author sees: a section by its title, the slide, the paragraph, the sentence, the result of the code. Do not tell them how things are done inside Presmium: no file names or paths, line numbers, JSON or Markdown syntax, directive or plugin names, the IR, builds, diagnostic codes, commands, tool names, the sandbox, permissions or git. When something could not be done or checked, say in plain words what they cannot have yet and what they can do.
|
|
73
|
+
- When you have changed the document, say in a sentence or two what readers will now see, and where. The change is a version of the document that the author can undo, or accept or reject first when the workspace asks for review.
|
|
74
|
+
- id: agent-instructions
|
|
75
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
76
|
+
config:
|
|
77
|
+
maxBytes: 65536
|
|
78
|
+
- id: time-context
|
|
79
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
80
|
+
- id: tool-bash
|
|
81
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
82
|
+
disabled: !!js process.platform === 'win32'
|
|
83
|
+
- id: tool-pwsh
|
|
84
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
85
|
+
disabled: !!js process.platform !== 'win32'
|
|
86
|
+
- id: tool-fs
|
|
87
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
88
|
+
- id: tool-fs-search
|
|
89
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
90
|
+
config:
|
|
91
|
+
sampleOverCapGlobResults: false
|
|
92
|
+
- id: tool-jobs
|
|
93
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
94
|
+
- id: skill-filesystem
|
|
95
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
96
|
+
- id: tool-skill
|
|
97
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
98
|
+
- id: planning
|
|
99
|
+
name: cordis:group
|
|
100
|
+
group: true
|
|
101
|
+
isolate:
|
|
102
|
+
planMode: true
|
|
103
|
+
config:
|
|
104
|
+
- id: plan-mode
|
|
105
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
106
|
+
config:
|
|
107
|
+
section: |
|
|
108
|
+
You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
109
|
+
|
|
110
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
111
|
+
|
|
112
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
113
|
+
|
|
114
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
115
|
+
|
|
116
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
117
|
+
|
|
118
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
119
|
+
- id: compaction
|
|
120
|
+
name: cordis:group
|
|
121
|
+
group: true
|
|
122
|
+
isolate:
|
|
123
|
+
compaction: true
|
|
124
|
+
toolResultPruner: true
|
|
125
|
+
config:
|
|
126
|
+
- id: compaction-basic
|
|
127
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
128
|
+
- id: command-compact
|
|
129
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
130
|
+
- id: tool-result-pruner
|
|
131
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
132
|
+
config:
|
|
133
|
+
thresholdChars: 8192
|
|
134
|
+
headChars: 4096
|
|
135
|
+
tailChars: 1024
|
|
136
|
+
- id: tool-ask-user
|
|
137
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
138
|
+
- id: tool-todo
|
|
139
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
140
|
+
config:
|
|
141
|
+
allowParallelInProgress: true
|
|
142
|
+
- id: tool-web
|
|
143
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
144
|
+
config:
|
|
145
|
+
fetch: true
|
|
146
|
+
searchTimeoutMs: 60000
|
|
147
|
+
- id: tool-plugin-manager
|
|
148
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
149
|
+
disabled: true
|
|
150
|
+
- id: tool-plugin-market
|
|
151
|
+
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
152
|
+
disabled: true
|
|
153
|
+
|
|
154
|
+
- id: agent-preset-registry
|
|
155
|
+
config:
|
|
156
|
+
default: authoring
|
|
157
|
+
|
|
158
|
+
# Static shell boot does not need one HTTP/1 event stream in every open Studio tab.
|
|
159
|
+
# Source, diagnostics, review and jobs use Presmium's scoped event connection.
|
|
160
|
+
- id: client-hmr
|
|
161
|
+
disabled: true
|