@jaychang1989/dsh-webchat 0.3.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 (45) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +18 -0
  3. package/README.md +78 -0
  4. package/README.zh.md +83 -0
  5. package/cordis.patch.yml +22 -0
  6. package/lib/client.js +1725 -0
  7. package/lib/client.js.map +1 -0
  8. package/lib/index.js +3887 -0
  9. package/lib/types/client/api.d.ts +74 -0
  10. package/lib/types/client/controller.d.ts +19 -0
  11. package/lib/types/client/index.d.ts +30 -0
  12. package/lib/types/client/locales.d.ts +169 -0
  13. package/lib/types/client/mount.d.ts +23 -0
  14. package/lib/types/client/panel/Markdown.d.ts +15 -0
  15. package/lib/types/client/panel/WebChatPanel.d.ts +28 -0
  16. package/lib/types/client/sidebar-entry-core.d.ts +60 -0
  17. package/lib/types/client/sidebar-entry.d.ts +22 -0
  18. package/lib/types/engine/engine.d.ts +247 -0
  19. package/lib/types/engine/html-md.d.ts +23 -0
  20. package/lib/types/index.d.ts +63 -0
  21. package/lib/types/protocol.d.ts +107 -0
  22. package/lib/types/routes.d.ts +22 -0
  23. package/lib/types/store.d.ts +69 -0
  24. package/lib/types/tools.d.ts +27 -0
  25. package/lib/types/transfer.d.ts +138 -0
  26. package/package.json +96 -0
  27. package/src/client/api.ts +90 -0
  28. package/src/client/controller.ts +43 -0
  29. package/src/client/css-modules.d.ts +5 -0
  30. package/src/client/index.ts +75 -0
  31. package/src/client/locales.ts +172 -0
  32. package/src/client/mount.tsx +125 -0
  33. package/src/client/panel/Markdown.tsx +251 -0
  34. package/src/client/panel/WebChatPanel.tsx +760 -0
  35. package/src/client/panel/panel.module.css +936 -0
  36. package/src/client/sidebar-entry-core.ts +207 -0
  37. package/src/client/sidebar-entry.ts +49 -0
  38. package/src/engine/engine.ts +1385 -0
  39. package/src/engine/html-md.ts +126 -0
  40. package/src/index.ts +212 -0
  41. package/src/protocol.ts +116 -0
  42. package/src/routes.ts +307 -0
  43. package/src/store.ts +217 -0
  44. package/src/tools.ts +257 -0
  45. package/src/transfer.ts +602 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Minimal HTML → Markdown converter used to scrape DeepSeek's rendered
3
+ * `.ds-markdown` replies. Covers the shapes DeepSeek renders (headings,
4
+ * paragraphs, code blocks with syntax classes, inline code, bold/italic,
5
+ * links, images, lists, tables, blockquotes, math placeholders). Anything
6
+ * unrecognized falls back to its text content — fidelity loss degrades
7
+ * gracefully instead of dropping text.
8
+ */
9
+
10
+ /** Escape text that could be parsed as markdown markup. */
11
+ function escapeText(text: string): string {
12
+ return text
13
+ .replace(/([\\`*_[\]<>])/g, '\\$1')
14
+ .replace(/\n{3,}/g, '\n\n')
15
+ }
16
+
17
+ interface NodeLike {
18
+ readonly tagName?: string
19
+ readonly nodeType?: number
20
+ readonly textContent?: string | null
21
+ readonly children?: readonly NodeLike[]
22
+ readonly attributes?: Record<string, string | undefined>
23
+ readonly innerHTML?: string
24
+ readonly className?: string
25
+ readonly parent?: NodeLike
26
+ }
27
+
28
+ const TEXT_NODE = 3
29
+ const ELEMENT_NODE = 1
30
+
31
+ /** Convert one DOM subtree (element or text) to markdown. */
32
+ export function htmlToMarkdown(root: NodeLike): string {
33
+ if (root === null || root === undefined) return ''
34
+ if (root.nodeType === TEXT_NODE) return escapeText(root.textContent ?? '')
35
+ const tag = (root.tagName ?? '').toLowerCase()
36
+ const text = (children: readonly NodeLike[] | undefined): string =>
37
+ (children ?? []).map(htmlToMarkdown).join('')
38
+
39
+ switch (tag) {
40
+ case 'br': return '\n'
41
+ case 'hr': return '\n---\n'
42
+ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': {
43
+ const level = Number(tag[1])
44
+ return `\n${'#'.repeat(level)} ${text(root.children).trim()}\n`
45
+ }
46
+ case 'p': return `\n${text(root.children).trim()}\n`
47
+ case 'strong': case 'b': return `**${text(root.children).trim()}**`
48
+ case 'em': case 'i': return `*${text(root.children).trim()}*`
49
+ case 'code': {
50
+ // Code block vs inline: <pre><code> is a block; bare <code> is inline.
51
+ const parentPre = root.parent?.tagName?.toLowerCase() === 'pre'
52
+ if (parentPre) return text(root.children)
53
+ return `\`${(root.textContent ?? '').replace(/`/g, '\\`')}\``
54
+ }
55
+ case 'pre': {
56
+ const code = root.children?.find(child => (child.tagName ?? '').toLowerCase() === 'code') ?? root
57
+ const raw = code.textContent ?? ''
58
+ const className = code.className ?? ''
59
+ const language = /language-([a-zA-Z0-9_+-]+)/.exec(className)?.[1] ?? ''
60
+ const fence = '```'
61
+ return `\n${fence}${language}\n${raw.replace(/\n$/, '')}\n${fence}\n`
62
+ }
63
+ case 'a': {
64
+ const href = root.attributes?.href
65
+ const label = text(root.children).trim()
66
+ if (href === undefined || href === '' || href.startsWith('javascript:')) return label
67
+ return `[${label || href}](${href})`
68
+ }
69
+ case 'img': {
70
+ const src = root.attributes?.src
71
+ const alt = root.attributes?.alt ?? ''
72
+ return src === undefined ? '' : `![${alt}](${src})`
73
+ }
74
+ case 'ul': return `\n${(root.children ?? []).map(child => {
75
+ if ((child.tagName ?? '').toLowerCase() === 'li') return `- ${text(child.children).trim()}`
76
+ return htmlToMarkdown(child)
77
+ }).join('\n')}\n`
78
+ case 'ol': {
79
+ let index = 1
80
+ return `\n${(root.children ?? []).map(child => {
81
+ if ((child.tagName ?? '').toLowerCase() === 'li') return `${index++}. ${text(child.children).trim()}`
82
+ return htmlToMarkdown(child)
83
+ }).join('\n')}\n`
84
+ }
85
+ case 'li': return text(root.children).trim()
86
+ case 'blockquote': return `\n> ${text(root.children).trim().replace(/\n/g, '\n> ')}\n`
87
+ case 'table': {
88
+ const rows = (root.children ?? []).filter(child => (child.tagName ?? '').toLowerCase() === 'tr')
89
+ if (rows.length === 0) return text(root.children).trim()
90
+ const cellsOf = (row: NodeLike): string[] => (row.children ?? [])
91
+ .filter(child => ['th', 'td'].includes((child.tagName ?? '').toLowerCase()))
92
+ .map(cell => text(cell.children).trim().replace(/\|/g, '\\|'))
93
+ const header = cellsOf(rows[0])
94
+ const body = rows.slice(1).map(cellsOf)
95
+ const width = Math.max(header.length, ...body.map(row => row.length))
96
+ const pad = (cells: string[]): string => {
97
+ const filled = [...cells]
98
+ while (filled.length < width) filled.push('')
99
+ return `| ${filled.join(' | ')} |`
100
+ }
101
+ const lines = [pad(header), `| ${Array.from({ length: width }, () => '---').join(' | ')} |`, ...body.map(pad)]
102
+ return `\n${lines.join('\n')}\n`
103
+ }
104
+ case 'tr': case 'td': case 'th': case 'thead': case 'tbody': case 'tfoot':
105
+ return text(root.children)
106
+ case 'details': return `\n<details>\n${text(root.children)}\n</details>\n`
107
+ case 'summary': return `**${text(root.children).trim()}**`
108
+ case 'input': {
109
+ const checked = root.attributes?.checked !== undefined
110
+ return checked ? '[x] ' : '[ ] '
111
+ }
112
+ case 'math': return `$${root.textContent ?? ''}$`
113
+ case 'svg': case 'button': case 'script': case 'style': return ''
114
+ case 'div': case 'span': case 'section': case 'article': case 'main':
115
+ return text(root.children)
116
+ default: {
117
+ const direct = text(root.children)
118
+ return direct
119
+ }
120
+ }
121
+ }
122
+
123
+ /** Convert serialized DOM (from the in-page scraper) to markdown. */
124
+ export function serializeToMarkdown(root: NodeLike): string {
125
+ return htmlToMarkdown(root).replace(/\n{3,}/g, '\n\n').trim()
126
+ }
package/src/index.ts ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * dsh-webchat — host half. Mounts the DeepSeek web engine (a real browser at
3
+ * chat.deepseek.com driven through its own page, persistent login profile),
4
+ * the /api/dsh-webchat route family, the agent tools (webchat_status,
5
+ * webchat_send, webchat_import, webchat_transfer), the harness transfer
6
+ * (seed a new session with a web transcript) and a system-prompt
7
+ * announcement. The browser half (./client) renders the chat panel. All
8
+ * transport rides the official NPM SDK packages — no dsh source changes.
9
+ */
10
+
11
+ import type { Context } from '@deepseek-ai/cordis'
12
+ import z from 'schemastery'
13
+ import type {} from '@deepseek-ai/dsh-host-webserver'
14
+ import type {} from '@deepseek-ai/dsh-system-prompt'
15
+ import type {} from '@deepseek-ai/dsh-tools'
16
+ import { DeepSeekWebEngine } from './engine/engine.ts'
17
+ import { makeRoutes } from './routes.ts'
18
+ import { TranscriptStore } from './store.ts'
19
+ import { webChatImportTool, webChatRecoverTool, webChatSendTool, webChatStatusTool, webChatTransferTool } from './tools.ts'
20
+ import type { WorkspaceRef } from './tools.ts'
21
+ import type { DistillConfig } from './transfer.ts'
22
+
23
+ /** Stable cordis plugin name. */
24
+ export const name = 'webchat'
25
+
26
+ /** Services required before the web-chat surfaces can mount. */
27
+ export const inject = ['webServer', 'tools', 'systemPrompt', 'sessions']
28
+
29
+ /**
30
+ * Label of the web-chat capability. dsh ≥ 0.1.7 addresses settings forms by
31
+ * profile entry id and derives them from this package's exported `Config`, so
32
+ * this value is only the plugin's own label (the browser half spells the same
33
+ * value for its locale namespace) — not a settings-service namespace.
34
+ */
35
+ export const WEBCHAT_SETTINGS_NAMESPACE = 'dsh-webchat'
36
+
37
+ /** Plugin config, validated by the same-named schemastery schema. */
38
+ export interface Config {
39
+ /** When true (default), a system-prompt section announces the plugin to every agent. */
40
+ announceToAgent?: boolean
41
+ /** Master switch for the plugin (routes, tools, prompt section). */
42
+ enabled?: boolean
43
+ /** Browser channel hint ('chrome' | 'msedge' | 'chromium' | 'auto'). */
44
+ browserChannel?: string
45
+ /** Explicit browser executable path. */
46
+ browserExecutablePath?: string
47
+ /** Proxy mode: 'direct' | 'system' | 'http://host:port'. */
48
+ browserProxy?: string
49
+ /**
50
+ * Run the chat browser headless (invisible). Default true: only the one-time
51
+ * login window is visible, and it auto-closes once logged in. Set false to
52
+ * keep a visible browser window during chatting too.
53
+ */
54
+ browserHeadless?: boolean
55
+ /** Max ms to wait for a web reply. */
56
+ replyTimeoutMs?: number
57
+ /** Data directory override (tests). */
58
+ dataDir?: string
59
+ /** When true (default), distill the transcript into an executable task brief before transfer. */
60
+ transferDistill?: boolean
61
+ /** Provider route for the transfer distillation call (empty = auto-detect). */
62
+ transferProvider?: string
63
+ /** Model id for the transfer distillation call (empty = auto-detect). */
64
+ transferModel?: string
65
+ /** Output-token cap for the final distillation brief (default 4096). */
66
+ transferMaxTokens?: number
67
+ /** Output-token cap per chunk summary in long-conversation map-reduce (default 1024). */
68
+ transferChunkTokens?: number
69
+ }
70
+
71
+ export const Config: z<Config> = z.object({
72
+ announceToAgent: z.boolean().default(true),
73
+ enabled: z.boolean().default(true),
74
+ browserChannel: z.string().default('auto'),
75
+ browserExecutablePath: z.string().default(''),
76
+ browserProxy: z.string().default('direct'),
77
+ browserHeadless: z.boolean().default(true),
78
+ replyTimeoutMs: z.number().default(180_000),
79
+ dataDir: z.string().default(''),
80
+ transferDistill: z.boolean().default(true),
81
+ transferProvider: z.string().default(''),
82
+ transferModel: z.string().default(''),
83
+ transferMaxTokens: z.number().default(4096),
84
+ transferChunkTokens: z.number().default(1024),
85
+ })
86
+
87
+ /** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
88
+ const DEFAULT_ANNOUNCE = true
89
+
90
+ /** Order of the announcement section within the tool-guidance band. */
91
+ const SECTION_ORDER = 155
92
+
93
+ /** Model-facing announcement: plugin presence, capabilities, and limits. */
94
+ export const WEBCHAT_GUIDANCE = '本机已安装 dsh-webchat 插件(Codex ChatGPT 模式 · DeepSeek 网页端聊天):侧边栏「网页聊天」入口;通过真实浏览器驱动 chat.deepseek.com(DeepSeek 网页模型,深度思考/智能搜索开关,网页登录会话,无需 API 额度)。能力:webchat_status 查看登录/会话状态、webchat_send 通过网页端发送消息并流式获取回复(可附带本地图片路径做多模态提问)、webchat_recover 把网页端已有会话同步/恢复到本地、webchat_import 把存储的网页对话导入为 markdown 上下文、webchat_transfer 把网页对话蒸馏成可执行任务简报并创建新 harness 会话(首条消息即任务简报,而非原始聊天记录),或经 targetSessionId 把简报作为新消息追加到已有会话延续同一任务;GUI 面板可将对话随时导出为工作区文件或转入 harness 会话。限制:首次使用需用户在弹出的浏览器窗口完成 DeepSeek 网页登录;网页端受 DeepSeek 官方风控,操作失败或页面改版时返回错误而非崩溃;面板提供「深度思考(R1)」与「智能搜索」开关(网页端无模型选择器)。用户提到「网页聊天 / 网页端 / ChatGPT 模式 / deepseek web / 转移到 harness」时即指本插件,请据此协作。'
95
+
96
+ /** Convert resolved config to engine config. */
97
+ function engineConfigOf(resolve: () => Config): ConstructorParameters<typeof DeepSeekWebEngine>[1] {
98
+ const value = resolve()
99
+ const dataDir = value.dataDir?.trim() ?? ''
100
+ return {
101
+ dataDir: dataDir !== '' ? dataDir : defaultDataDirOf(),
102
+ channel: value.browserChannel === 'auto' ? undefined : (value.browserChannel || undefined),
103
+ executablePath: value.browserExecutablePath !== '' ? value.browserExecutablePath : undefined,
104
+ proxy: value.browserProxy,
105
+ headless: value.browserHeadless,
106
+ replyTimeoutMs: value.replyTimeoutMs,
107
+ }
108
+ }
109
+
110
+ /** Default plugin data dir (mirrors store.defaultDataDir). */
111
+ function defaultDataDirOf(): string {
112
+ const home = process.env.DSH_HOME ?? process.env.HOME ?? '.'
113
+ return `${home}/.dsh/dsh-webchat`
114
+ }
115
+
116
+ /**
117
+ * Mount the engine, routes, tools, and announcement.
118
+ * @param ctx - host plugin context carrying webServer/tools/systemPrompt.
119
+ * @param config - resolved plugin config (schema defaults applied by the loader).
120
+ */
121
+ export function apply(ctx: Context, config?: Config): void {
122
+ let current: () => Config = () => config ?? {}
123
+ const resolve = (): Config => ({
124
+ announceToAgent: current().announceToAgent ?? DEFAULT_ANNOUNCE,
125
+ enabled: current().enabled ?? true,
126
+ browserChannel: current().browserChannel ?? 'auto',
127
+ browserExecutablePath: current().browserExecutablePath ?? '',
128
+ browserProxy: current().browserProxy ?? 'direct',
129
+ browserHeadless: current().browserHeadless ?? true,
130
+ replyTimeoutMs: current().replyTimeoutMs ?? 180_000,
131
+ dataDir: current().dataDir ?? '',
132
+ transferDistill: current().transferDistill ?? true,
133
+ transferProvider: current().transferProvider ?? '',
134
+ transferModel: current().transferModel ?? '',
135
+ transferMaxTokens: current().transferMaxTokens ?? 4_096,
136
+ transferChunkTokens: current().transferChunkTokens ?? 1_024,
137
+ })
138
+
139
+ const distillConfigOf = (value: Config): DistillConfig => ({
140
+ distill: value.transferDistill ?? true,
141
+ provider: (value.transferProvider ?? '').trim(),
142
+ model: (value.transferModel ?? '').trim(),
143
+ maxTokens: value.transferMaxTokens ?? 4_096,
144
+ chunkTokens: value.transferChunkTokens ?? 1_024,
145
+ })
146
+
147
+ const store = new TranscriptStore({ dataDir: resolve().dataDir !== '' ? resolve().dataDir : undefined })
148
+ const engine = new DeepSeekWebEngine(store, engineConfigOf(resolve))
149
+ ctx.effect(() => () => {
150
+ void engine.disposeBrowser()
151
+ }, 'dsh-webchat: engine')
152
+
153
+ const routes = makeRoutes({ ctx, engine, store, distill: distillConfigOf(resolve()) })
154
+ const listWorkspaces = (): WorkspaceRef[] | undefined => {
155
+ const registry = ctx.get('workspaceRegistry') as { list(): Array<{ id: string; path: string; title: string }> } | undefined
156
+ if (registry === undefined) return undefined
157
+ return registry.list().map(ws => ({ id: ws.id, path: ws.path, title: ws.title }))
158
+ }
159
+
160
+ const tools = [
161
+ webChatStatusTool(engine, store, listWorkspaces),
162
+ webChatSendTool(engine),
163
+ webChatRecoverTool(engine),
164
+ webChatImportTool(store),
165
+ webChatTransferTool(ctx, store, distillConfigOf(resolve())),
166
+ ]
167
+
168
+ let disposeSection: (() => void) | undefined
169
+ let disposeRoutes: (() => void) | undefined
170
+ let disposeTools: (() => void) | undefined
171
+
172
+ // Register (or drop) every surface to match the current source. Each group
173
+ // is kept under one disposer: re-registering first tears the old one down
174
+ // so duplicate-name registrations never throw.
175
+ const sync = (): void => {
176
+ if (disposeSection !== undefined) { disposeSection(); disposeSection = undefined }
177
+ if (disposeRoutes !== undefined) { disposeRoutes(); disposeRoutes = undefined }
178
+ if (disposeTools !== undefined) { disposeTools(); disposeTools = undefined }
179
+ const value = resolve()
180
+ if (!value.enabled) return
181
+ if (value.announceToAgent) {
182
+ disposeSection = ctx.systemPrompt.section({
183
+ name: 'plugin:dsh-webchat',
184
+ order: SECTION_ORDER,
185
+ text: WEBCHAT_GUIDANCE,
186
+ })
187
+ }
188
+ disposeRoutes = ctx.effect(
189
+ () => {
190
+ const disposers = routes.map(route => ctx.webServer.register(route))
191
+ return () => { for (const dispose of disposers) dispose() }
192
+ },
193
+ 'dsh-webchat: routes',
194
+ )
195
+ disposeTools = ctx.effect(
196
+ () => {
197
+ const disposers = tools.map(tool => ctx.tools.register(tool))
198
+ return () => { for (const dispose of disposers) dispose() }
199
+ },
200
+ 'dsh-webchat: tools',
201
+ )
202
+ }
203
+
204
+ // dsh ≥ 0.1.7 owns settings forms itself: one per profile entry, addressed by
205
+ // the entry id, schema = this package's exported `Config`, and a form write
206
+ // re-applies the entry (profile `patchReload: live`). The retired
207
+ // installSettingsSection helper no longer exists in @deepseek-ai/dsh-settings
208
+ // (0.2.0 exports only SettingsForms / SettingsConflictError / redactSecrets),
209
+ // so the Loader-provided `config` above is the whole source of truth and one
210
+ // registration pass per apply is enough.
211
+ sync()
212
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Shared wire surface of the dsh-webchat plugin: API paths, transcript data
3
+ * model, and engine status views. Imported by both the host routes and the
4
+ * browser panel (the client bundle inlines it — no shared runtime identity).
5
+ */
6
+
7
+ /** One transcript message (markdown text). */
8
+ export interface WebChatMessage {
9
+ /** Local stable id. */
10
+ readonly id: string
11
+ readonly role: 'user' | 'assistant'
12
+ /** Markdown body. */
13
+ readonly content: string
14
+ /** Unix epoch ms. */
15
+ readonly ts: number
16
+ /** True while the engine is still streaming this message. */
17
+ readonly streaming?: boolean
18
+ /** Set when the exchange failed (content may be partial). */
19
+ readonly error?: string
20
+ /** Local absolute paths of images attached to a user message. */
21
+ readonly attachments?: string[]
22
+ }
23
+
24
+ /** One web chat session transcript (persisted locally). */
25
+ export interface WebChatTranscript {
26
+ readonly id: string
27
+ title: string
28
+ readonly createdAt: number
29
+ updatedAt: number
30
+ /** Model the chat currently runs on the web (deepseek-chat / deepseek-reasoner). */
31
+ model: string
32
+ readonly messages: WebChatMessage[]
33
+ /** True while an assistant reply is streaming. */
34
+ streaming: boolean
35
+ }
36
+
37
+ /**
38
+ * Structured engine error codes so callers (panel / agent tools) can act on a
39
+ * failure instead of only printing it: NEED_LOGIN → prompt the login window;
40
+ * PAGE_CHANGED → the DeepSeek page/stream protocol moved and the plugin likely
41
+ * needs an upgrade; TIMEOUT → generation exceeded the reply window; NETWORK →
42
+ * browser launch / HTTP / connectivity failure.
43
+ */
44
+ export type WebChatErrorCode = 'NEED_LOGIN' | 'PAGE_CHANGED' | 'TIMEOUT' | 'NETWORK'
45
+
46
+ /** Coarse engine state for the panel. */
47
+ export type EngineState = 'stopped' | 'launching' | 'ready' | 'error'
48
+
49
+ /** Snapshot the panel polls. */
50
+ export interface WebChatState {
51
+ readonly engine: EngineState
52
+ readonly engineError?: string
53
+ /** null = unknown yet; false = browser up but not logged in; true = chat page ready. */
54
+ readonly loggedIn: boolean | null
55
+ readonly pageUrl?: string
56
+ /** Deep-think (R1) toggle state read from the page. */
57
+ readonly deepThink: boolean
58
+ /** Internet-search toggle state read from the page. */
59
+ readonly search: boolean
60
+ readonly activeChatId?: string
61
+ readonly chats: WebChatTranscript[]
62
+ /** True while a message is being sent / replied (serialized engine busy). */
63
+ readonly busy: boolean
64
+ /** Last send error, transient for the panel to display. */
65
+ readonly lastError?: string
66
+ /** Structured code for the last error (so the panel can show a targeted action). */
67
+ readonly lastErrorCode?: WebChatErrorCode
68
+ }
69
+
70
+ /** Engine-level operation results surfaced to agent tools. */
71
+ export interface SendResult {
72
+ readonly ok: boolean
73
+ readonly chatId?: string
74
+ readonly reply?: string
75
+ readonly error?: string
76
+ /** Structured error code (when ok is false) for actionable handling. */
77
+ readonly code?: WebChatErrorCode
78
+ }
79
+
80
+ /** Result of transferring a transcript into harness mode. */
81
+ export interface TransferResult {
82
+ readonly ok: boolean
83
+ readonly sessionId?: string
84
+ readonly filePath?: string
85
+ /** True when the session was attached to a workspace (grouped); false = ungrouped. */
86
+ readonly attached?: boolean
87
+ /** Workspace id the session landed in, when attached. */
88
+ readonly workspaceId?: string
89
+ readonly error?: string
90
+ }
91
+
92
+ /**
93
+ * How a transfer seeds the new harness session: 'distill' condenses the web
94
+ * conversation into an executable task brief via the harness LLM; 'raw'
95
+ * replays the full transcript verbatim. The web panel lets the user choose.
96
+ */
97
+ export type TransferMode = 'distill' | 'raw'
98
+
99
+ /** API path constants shared by host routes and the browser panel. */
100
+ export const WEBChat_API = {
101
+ state: '/api/dsh-webchat/state',
102
+ openLogin: '/api/dsh-webchat/open-login',
103
+ closeBrowser: '/api/dsh-webchat/close-browser',
104
+ newChat: '/api/dsh-webchat/new-chat',
105
+ send: '/api/dsh-webchat/send',
106
+ stop: '/api/dsh-webchat/stop',
107
+ deepThink: '/api/dsh-webchat/deep-think',
108
+ search: '/api/dsh-webchat/search',
109
+ transfer: '/api/dsh-webchat/transfer',
110
+ exportFile: '/api/dsh-webchat/export',
111
+ renameChat: '/api/dsh-webchat/rename',
112
+ deleteChat: '/api/dsh-webchat/delete',
113
+ clearChats: '/api/dsh-webchat/clear',
114
+ webChats: '/api/dsh-webchat/web-chats',
115
+ recover: '/api/dsh-webchat/recover',
116
+ } as const