@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,138 @@
1
+ /**
2
+ * Harness-mode transfer: turn a web-chat transcript into development context.
3
+ *
4
+ * This is the "Continue in Codex" / ChatGPT-mode analog, and like Codex it is
5
+ * a CONTEXT HANDOFF rather than a raw replay: the exploration-phase web
6
+ * conversation is distilled (via the harness LLM) into an executable task
7
+ * brief — the execution-phase state representation the agent actually needs —
8
+ * and that brief seeds a fresh harness session. The raw transcript is kept as
9
+ * the fallback when distillation is unavailable.
10
+ *
11
+ * Two targets:
12
+ * - new harness session — a COLD persisted session seeded with the distilled
13
+ * brief (or raw transcript), so it shows up in the GUI list and resumes;
14
+ * - workspace file — the raw transcript rendered to markdown in the target
15
+ * project directory, so any agent can read it with file tools.
16
+ *
17
+ * Session creation writes directly through the session-persistence backend
18
+ * (`sessionPersistence.create` + `append`), NOT `ctx.sessions.create()`:
19
+ * the store's `create` produces a LIVE session owned by the calling fiber,
20
+ * which the GUI then refuses to resume ("cannot prepare … while it is live").
21
+ * A cold persisted session is exactly what the GUI's resume path expects.
22
+ */
23
+ import type { Context } from '@deepseek-ai/cordis';
24
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
25
+ import type { Workspace } from '@deepseek-ai/dsh-workspace';
26
+ import type { TransferMode, WebChatMessage, WebChatTranscript } from './protocol.ts';
27
+ /** Render the message list of a transcript (no header) to markdown. */
28
+ export declare function renderMessagesMarkdown(messages: WebChatMessage[], options?: {
29
+ excludeThinking?: boolean;
30
+ }): string;
31
+ /** Render one transcript to markdown for harness consumption. */
32
+ export declare function renderTranscriptMarkdown(transcript: WebChatTranscript, options?: {
33
+ excludeThinking?: boolean;
34
+ }): string;
35
+ /**
36
+ * Framing that turns the distilled brief into established context for the
37
+ * agent — the analog of Codex's "another model started to solve this problem
38
+ * and produced a summary; use it to build on the work already done".
39
+ */
40
+ export declare const HANDOFF_PREAMBLE = "\u8FD9\u662F\u4E00\u6B21\u4ECE DeepSeek \u7F51\u9875\u7AEF\u4F1A\u8BDD\uFF08chat.deepseek.com\uFF09\u8F6C\u6765\u7684\u4E0A\u4E0B\u6587\u4EA4\u63A5\u3002\u4E0B\u9762\u7684\u4EFB\u52A1\u7B80\u62A5\u5DF2\u628A\u8BE5\u5BF9\u8BDD\u63D0\u70BC\u4E3A\u53EF\u6267\u884C\u7684\u4EFB\u52A1\u4E0A\u4E0B\u6587\u2014\u2014\u628A\u5B83\u5F53\u4F5C\u65E2\u5B9A\u76EE\u6807\u4E0E\u80CC\u666F\uFF0C\u76F4\u63A5\u5728\u5176\u57FA\u7840\u4E0A\u7EE7\u7EED\uFF0C\u4E0D\u8981\u590D\u8FF0\u3002";
41
+ /** Transfer distillation settings (resolved from the plugin config surface). */
42
+ export interface DistillConfig {
43
+ /** When true (default), distill the transcript into a task brief via ctx.llm. */
44
+ distill: boolean;
45
+ /** Provider route for the distillation call; empty = auto-detect. */
46
+ provider: string;
47
+ /** Model id for the distillation call; empty = auto-detect. */
48
+ model: string;
49
+ /** Output-token cap for the final brief (single-shot / reduce). Default 4096. */
50
+ maxTokens?: number;
51
+ /** Output-token cap for each per-chunk map summary. Default 1024. */
52
+ chunkTokens?: number;
53
+ }
54
+ /** Default output-token cap for the final distillation brief. */
55
+ export declare const DEFAULT_TRANSFER_MAX_TOKENS = 4096;
56
+ /** Default output-token cap for one chunk summary in the map phase. */
57
+ export declare const DEFAULT_TRANSFER_CHUNK_TOKENS = 1024;
58
+ /** Character budget per chunk when splitting a long transcript for map-reduce. */
59
+ export declare const CHUNK_CHAR_BUDGET = 12000;
60
+ /** A successful distillation result. */
61
+ export interface DistillResult {
62
+ brief: string;
63
+ provider: string;
64
+ model: string;
65
+ }
66
+ /**
67
+ * Split a transcript's messages into chunks of at most `budget` characters
68
+ * (sum of `message.content.length`), never splitting a single message: a
69
+ * message larger than the budget becomes its own (oversized) chunk. Streaming
70
+ * assistant messages are skipped (they were never completed).
71
+ */
72
+ export declare function chunkTranscript(transcript: WebChatTranscript, budget?: number): WebChatMessage[][];
73
+ /**
74
+ * Distill a web transcript into an executable task brief via the harness LLM.
75
+ * Long transcripts are distilled with map-reduce: each chunk is summarized
76
+ * (map, capped at `chunkTokens`), then the summaries are merged into the final
77
+ * brief (reduce, capped at `maxTokens`). Short transcripts take a single shot.
78
+ * Returns undefined (so callers fall back to the raw transcript) when the LLM
79
+ * service, a provider/model, or a clean completion is unavailable.
80
+ */
81
+ export declare function distillTranscriptToBrief(ctx: Context, transcript: WebChatTranscript, config: DistillConfig): Promise<DistillResult | undefined>;
82
+ /** Build a user-message surface event carrying the handoff text at a given seq. */
83
+ export declare function transcriptUserMessageEvent(markdown: string, seq: number): SessionEvent<'user/message'>;
84
+ /** Build the seed user-message event carrying the handoff text (seq 0). */
85
+ export declare function transcriptSeedEvent(markdown: string): SessionEvent<'user/message'>;
86
+ export interface TransferWorkspaceTarget {
87
+ /** Stable workspace id (from the registry / GUI picker); wins over `path`. */
88
+ workspaceId?: string;
89
+ /** Directory path to use as the session cwd (optionally resolves to a workspace). */
90
+ path?: string;
91
+ }
92
+ export interface TransferToSessionInput {
93
+ transcript: WebChatTranscript;
94
+ cwd?: string;
95
+ /** Target workspace; when set, the session is grouped under it (attached). */
96
+ workspace?: TransferWorkspaceTarget;
97
+ /**
98
+ * Existing harness session to CONTINUE instead of creating a new one. When
99
+ * set, the distilled brief (or raw transcript) is appended as a fresh user
100
+ * message to that session rather than seeding a new session.
101
+ */
102
+ targetSessionId?: string;
103
+ }
104
+ /** Resolved transfer destination: the session cwd plus an optional owning workspace. */
105
+ export interface ResolvedTransferTarget {
106
+ cwd: string;
107
+ workspace?: Workspace;
108
+ }
109
+ /** The result of creating a transferred harness session. */
110
+ export interface TransferToSessionResult {
111
+ sessionId: string;
112
+ distilled: boolean;
113
+ /** True when the session was attached to a workspace; false = ungrouped. */
114
+ attached: boolean;
115
+ /** Workspace id the session landed in, when attached. */
116
+ workspaceId?: string;
117
+ }
118
+ /**
119
+ * Create a new COLD harness session seeded with a distilled task brief (or the
120
+ * raw transcript when the user chooses 'raw' / distillation is unavailable),
121
+ * written straight through the session-persistence backend so the GUI lists it
122
+ * and can resume it later (no live-store ownership). When `input.workspace`
123
+ * names a registered workspace, the session's cwd is set to that workspace's
124
+ * canonical path and the session is attached to the workspace's account, so
125
+ * the GUI groups it under that workspace instead of "ungrouped".
126
+ *
127
+ * `mode` is the user's explicit choice; when undefined the plugin config
128
+ * default (`transferDistill`) applies.
129
+ */
130
+ export declare function transferToHarnessSession(ctx: Context, input: TransferToSessionInput, config: DistillConfig, mode?: TransferMode): Promise<TransferToSessionResult>;
131
+ export interface ExportTranscriptInput {
132
+ transcript: WebChatTranscript;
133
+ cwd?: string;
134
+ }
135
+ /** Write the transcript markdown into the target directory; returns the path. */
136
+ export declare function exportTranscriptFile(input: ExportTranscriptInput): {
137
+ filePath: string;
138
+ };
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@jaychang1989/dsh-webchat",
3
+ "version": "0.3.0",
4
+ "description": "Codex-ChatGPT-mode style web chat for DeepSeek Harness: chat through chat.deepseek.com with your DeepSeek web session instead of API billing, then transfer the conversation into a harness session as development context — or let the harness agent chat through the same web session. Maintained fork retargeted at dsh 0.2.x.",
5
+ "keywords": [
6
+ "dsh",
7
+ "deepseek-harness",
8
+ "deepseek",
9
+ "webchat",
10
+ "chatgpt-mode",
11
+ "deepseek-web",
12
+ "harness-plugin",
13
+ "plugin"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/jaychang1989/dsh-webchat.git"
18
+ },
19
+ "homepage": "https://github.com/jaychang1989/dsh-webchat#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/jaychang1989/dsh-webchat/issues"
22
+ },
23
+ "author": "jaychang1989",
24
+ "contributors": [
25
+ "xmuwenxiang (original dsh-web-chat, Apache-2.0)"
26
+ ],
27
+ "license": "Apache-2.0",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "type": "module",
32
+ "engines": {
33
+ "node": ">=22",
34
+ "dsh": ">=0.2.0-rc.1"
35
+ },
36
+ "main": "lib/index.js",
37
+ "types": "lib/types/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./lib/types/index.d.ts",
41
+ "default": "./lib/index.js"
42
+ },
43
+ "./client": {
44
+ "types": "./lib/types/client/index.d.ts",
45
+ "default": "./lib/client.js"
46
+ },
47
+ "./src/*": "./src/*",
48
+ "./package.json": "./package.json"
49
+ },
50
+ "dsh": {
51
+ "bundle": {
52
+ "patch": "./cordis.patch.yml"
53
+ },
54
+ "client": {
55
+ "platform": "web"
56
+ }
57
+ },
58
+ "dependencies": {
59
+ "playwright-core": "^1.63.0"
60
+ },
61
+ "peerDependencies": {
62
+ "@deepseek-ai/dsh-llm": ">=0.2.0-rc.1",
63
+ "@deepseek-ai/dsh-session": ">=0.2.0-rc.1",
64
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.1",
65
+ "react": "^18.2.0",
66
+ "react-dom": "^18.2.0"
67
+ },
68
+ "devDependencies": {
69
+ "@deepseek-ai/cordis": "^4.0.2",
70
+ "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
71
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
72
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
73
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
74
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
75
+ "@deepseek-ai/dsh-workspace": "^0.2.0-rc.1",
76
+ "@types/node": "^22.20.0",
77
+ "@types/react": "~18.3.1",
78
+ "@types/react-dom": "^18.3.5",
79
+ "schemastery": "^3.18.0",
80
+ "typescript": "~5.7.2"
81
+ },
82
+ "files": [
83
+ "lib/**/*.js",
84
+ "lib/**/*.js.map",
85
+ "lib/**/*.d.ts",
86
+ "lib/**/*.d.ts.map",
87
+ "src",
88
+ "cordis.patch.yml",
89
+ "NOTICE",
90
+ "README.md",
91
+ "README.zh.md"
92
+ ],
93
+ "scripts": {
94
+ "test": "node --test"
95
+ }
96
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Browser-side API client for the /api/dsh-webchat route family. The only
3
+ * data access path the panel components use — plain fetch, same origin.
4
+ */
5
+
6
+ import { WEBChat_API, type TransferMode, type WebChatState } from '../protocol.ts'
7
+
8
+ /** Shape every /api/dsh-webchat response carries: ok plus optional error. */
9
+ interface ApiResult {
10
+ ok: boolean
11
+ error?: string
12
+ }
13
+
14
+ /** Endpoint payloads always extend ApiResult. */
15
+ type EndpointResult<T> = T & ApiResult
16
+
17
+ async function request<T>(path: string, body?: unknown): Promise<EndpointResult<T>> {
18
+ const response = await fetch(path, {
19
+ method: body === undefined ? 'GET' : 'POST',
20
+ headers: body === undefined ? undefined : { 'content-type': 'application/json' },
21
+ body: body === undefined ? undefined : JSON.stringify(body),
22
+ })
23
+ const payload = (await response.json().catch(() => ({}))) as EndpointResult<T>
24
+ if (!response.ok && payload.ok !== true) {
25
+ return { ...payload, ok: false, error: payload.error ?? `HTTP ${response.status}` }
26
+ }
27
+ return payload
28
+ }
29
+
30
+ export class WebChatApi {
31
+ state(): Promise<EndpointResult<WebChatState>> {
32
+ return request<WebChatState>(WEBChat_API.state)
33
+ }
34
+
35
+ openLogin(): Promise<EndpointResult<{ ok: boolean }>> {
36
+ return request<{ ok: boolean }>(WEBChat_API.openLogin)
37
+ }
38
+
39
+ closeBrowser(): Promise<EndpointResult<{ ok: boolean }>> {
40
+ return request<{ ok: boolean }>(WEBChat_API.closeBrowser)
41
+ }
42
+
43
+ newChat(): Promise<EndpointResult<{ ok: boolean; chatId?: string }>> {
44
+ return request<{ ok: boolean; chatId?: string }>(WEBChat_API.newChat)
45
+ }
46
+
47
+ send(text: string, images?: string[]): Promise<EndpointResult<{ ok: boolean; chatId?: string }>> {
48
+ return request<{ ok: boolean; chatId?: string }>(WEBChat_API.send, { text, images })
49
+ }
50
+
51
+ stop(): Promise<EndpointResult<{ ok: boolean }>> {
52
+ return request<{ ok: boolean }>(WEBChat_API.stop)
53
+ }
54
+
55
+ setDeepThink(enabled: boolean): Promise<EndpointResult<{ ok: boolean }>> {
56
+ return request<{ ok: boolean }>(WEBChat_API.deepThink, { enabled })
57
+ }
58
+
59
+ setSearch(enabled: boolean): Promise<EndpointResult<{ ok: boolean }>> {
60
+ return request<{ ok: boolean }>(WEBChat_API.search, { enabled })
61
+ }
62
+
63
+ transfer(chatId: string, cwd?: string, mode?: TransferMode, workspaceId?: string, targetSessionId?: string): Promise<EndpointResult<{ ok: boolean; sessionId?: string; distilled?: boolean; attached?: boolean; continued?: boolean; workspaceId?: string }>> {
64
+ return request<{ ok: boolean; sessionId?: string; distilled?: boolean; attached?: boolean; continued?: boolean; workspaceId?: string }>(WEBChat_API.transfer, { chatId, cwd, mode, workspaceId, targetSessionId })
65
+ }
66
+
67
+ exportFile(chatId: string, cwd?: string): Promise<EndpointResult<{ ok: boolean; filePath?: string }>> {
68
+ return request<{ ok: boolean; filePath?: string }>(WEBChat_API.exportFile, { chatId, cwd })
69
+ }
70
+
71
+ renameChat(chatId: string, title: string): Promise<EndpointResult<{ ok: boolean }>> {
72
+ return request<{ ok: boolean }>(WEBChat_API.renameChat, { chatId, title })
73
+ }
74
+
75
+ deleteChat(chatId: string): Promise<EndpointResult<{ ok: boolean }>> {
76
+ return request<{ ok: boolean }>(WEBChat_API.deleteChat, { chatId })
77
+ }
78
+
79
+ clearChats(): Promise<EndpointResult<{ ok: boolean; count?: number }>> {
80
+ return request<{ ok: boolean; count?: number }>(WEBChat_API.clearChats)
81
+ }
82
+
83
+ webChats(): Promise<EndpointResult<{ ok: boolean; web: Array<{ title: string }>; missing: string[] }>> {
84
+ return request<{ ok: boolean; web: Array<{ title: string }>; missing: string[] }>(WEBChat_API.webChats)
85
+ }
86
+
87
+ recover(title: string): Promise<EndpointResult<{ ok: boolean; chatId?: string; title?: string; created?: boolean }>> {
88
+ return request<{ ok: boolean; chatId?: string; title?: string; created?: boolean }>(WEBChat_API.recover, { title })
89
+ }
90
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Panel controller: minimal external state the sidebar entry and the panel
3
+ * share (open/closed), plus a tiny pub/sub. The chat data itself lives in the
4
+ * panel component's polling loop.
5
+ */
6
+
7
+ export interface PanelSnapshot {
8
+ readonly panelOpen: boolean
9
+ }
10
+
11
+ const INITIAL: PanelSnapshot = { panelOpen: false }
12
+
13
+ /** Plain pub/sub store (no React dependency — used by the DOM sidebar entry). */
14
+ export class PanelController {
15
+ private snapshot: PanelSnapshot = INITIAL
16
+ private readonly listeners = new Set<() => void>()
17
+
18
+ getSnapshot(): PanelSnapshot {
19
+ return this.snapshot
20
+ }
21
+
22
+ subscribe(listener: () => void): () => void {
23
+ this.listeners.add(listener)
24
+ return () => { this.listeners.delete(listener) }
25
+ }
26
+
27
+ private set(patch: Partial<PanelSnapshot>): void {
28
+ this.snapshot = { ...this.snapshot, ...patch }
29
+ for (const listener of [...this.listeners]) listener()
30
+ }
31
+
32
+ open(): void {
33
+ if (!this.snapshot.panelOpen) this.set({ panelOpen: true })
34
+ }
35
+
36
+ close(): void {
37
+ if (this.snapshot.panelOpen) this.set({ panelOpen: false })
38
+ }
39
+
40
+ toggle(): void {
41
+ this.set({ panelOpen: !this.snapshot.panelOpen })
42
+ }
43
+ }
@@ -0,0 +1,5 @@
1
+ /** CSS Modules ambient declaration for the tsdown virtual loader. */
2
+ declare module '*.module.css' {
3
+ const classes: Record<string, string>
4
+ export default classes
5
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Browser-half entry for the dsh-webchat plugin — runs inside the dsh web GUI.
3
+ *
4
+ * Registers the locale dictionaries and mounts the two DOM surfaces: the
5
+ * sidebar entry row (toggles the panel) and the web-chat panel in the center
6
+ * column. Failure policy: DOM mounting problems are logged, never thrown —
7
+ * the web shell fails the whole boot when a plugin apply throws, and an
8
+ * external plugin must not take the GUI down.
9
+ *
10
+ * Export discipline (packages/client rule): the /client surface carries what
11
+ * cordis loading needs plus types only — all value exports stay internal.
12
+ */
13
+ import type { ClientContext, ISessions, IWorkspaces } from '@deepseek-ai/dsh-client-runtime/client'
14
+ // Type-only: pulls the locale plugin's Context merge (ctx.locale).
15
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
16
+ // Type-only: pulls the LocaleNamespaceMap merge table.
17
+ import type {} from '@deepseek-ai/dsh-client-ui-slots'
18
+ import { WebChatApi } from './api.ts'
19
+ import { PanelController } from './controller.ts'
20
+ import { en, zh, type WebChatKey } from './locales.ts'
21
+ import { mountPanel } from './mount.tsx'
22
+ import { mountSidebarEntry } from './sidebar-entry.ts'
23
+
24
+ /** Locale namespace this plugin owns. */
25
+ const NS = 'dsh-webchat'
26
+
27
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
28
+ interface LocaleNamespaceMap {
29
+ /** dsh-webchat surface copy. */
30
+ 'dsh-webchat': WebChatKey
31
+ }
32
+ }
33
+
34
+ /** Required services (fiber inject waiting — the runtime must be up first). */
35
+ export const inject = ['slots', 'locale', 'sessions', 'workspaces']
36
+
37
+ /** Type-only surface (export discipline: no value exports beyond the plugin contract). */
38
+ export type { WebChatPanelProps } from './panel/WebChatPanel.tsx'
39
+ export type { WebChatKey } from './locales.ts'
40
+
41
+ /**
42
+ * Mount the web-chat panel.
43
+ * @param ctx - client root context (locale + sessions services).
44
+ */
45
+ export function apply(ctx: ClientContext): void {
46
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-webchat: dictionaries')
47
+
48
+ const controller = new PanelController()
49
+ const api = new WebChatApi()
50
+ // The host-side dsh-session package also merges `Context.sessions` into
51
+ // cordis types; the client runtime's ISessions is the authoritative shape
52
+ // here, so read it through a narrow cast rather than trusting the merged
53
+ // type (which the host package wins during a shared typecheck).
54
+ const sessions = (ctx as unknown as { sessions: ISessions }).sessions
55
+ const workspaces = (ctx as unknown as { workspaces: IWorkspaces }).workspaces
56
+ const tt = ctx.locale.bind(NS)
57
+ const currentCwd = (): string | undefined => {
58
+ const snapshot = sessions.list.getSnapshot()
59
+ const current = snapshot.current
60
+ if (current === undefined) return undefined
61
+ return snapshot.byId[current]?.cwd
62
+ }
63
+
64
+ const disposers: Array<() => void> = []
65
+ try {
66
+ disposers.push(mountSidebarEntry({ controller, tt }))
67
+ disposers.push(mountPanel({ sessions, workspaces, api, controller, tt, currentCwd }))
68
+ } catch (error) {
69
+ // DOM failures degrade the panel, never the GUI.
70
+ console.warn('[dsh-webchat] mount failed:', error)
71
+ }
72
+ ctx.effect(() => () => {
73
+ for (const dispose of disposers.splice(0)) dispose()
74
+ }, 'dsh-webchat: ui mounts')
75
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * dsh-webchat surface copy: zh is the key source, en mirrors every key. The
3
+ * key union (not an object shape) is what the locale machinery wants —
4
+ * LocaleKeysOf<N> intersects the namespace with `string`, so the namespace
5
+ * value must itself be the key union (same pattern as dsh-ssh).
6
+ */
7
+
8
+ export const zh = {
9
+ 'entry.label': '网页聊天',
10
+ 'entry.tooltip': 'DeepSeek 网页端聊天(deepseek-chat,Codex ChatGPT 模式)',
11
+ 'panel.title': 'DeepSeek 网页端聊天',
12
+ 'status.engine.stopped': '浏览器未启动',
13
+ 'status.engine.launching': '浏览器启动中…',
14
+ 'status.engine.ready': '浏览器就绪',
15
+ 'status.engine.error': '浏览器错误',
16
+ 'status.loggedIn': '已登录',
17
+ 'status.notLoggedIn': '未登录',
18
+ 'status.unknown': '状态未知',
19
+ 'action.openLogin': '打开登录窗口',
20
+ 'action.closeBrowser': '关闭浏览器',
21
+ 'action.newChat': '新对话',
22
+ 'action.recover': '从网页恢复',
23
+ 'action.refresh': '刷新',
24
+ 'action.send': '发送',
25
+ 'action.stop': '停止生成',
26
+ 'action.transferToHarness': '转移到 Harness',
27
+ 'action.exportFile': '导出为文件',
28
+ 'transfer.mode.distill': '蒸馏为任务简报',
29
+ 'transfer.mode.raw': '原始记录',
30
+ 'transfer.mode.hint': '蒸馏:用 harness LLM 把对话提炼成可执行任务简报;原始记录:原样放入新会话首条消息',
31
+ 'transfer.workspace.label': '目标工作区',
32
+ 'transfer.workspace.ungrouped': '未分组',
33
+ 'transfer.workspace.new': '新建工作区…',
34
+ 'transfer.workspace.hint': '转移到所选工作区;未分组则不归入任何工作区',
35
+ 'transfer.workspace.created': '已创建并选中工作区 {title}',
36
+ 'transfer.workspace.createFailed': '创建工作区失败:{error}',
37
+ 'transfer.session.label': '延续到会话',
38
+ 'transfer.session.hint': '选择已有 harness 会话以延续(把简报作为新消息追加);默认新建会话',
39
+ 'transfer.session.new': '新建会话',
40
+ 'transfer.attached.failed': '会话已创建({sessionId}),但归入工作区失败,现为未分组',
41
+ 'handoff.eyebrow': '交付到 Harness',
42
+ 'toggle.deepThink': '深度思考',
43
+ 'toggle.search': '智能搜索',
44
+ 'toggle.deepThink.hint': '开启 R1 深度思考',
45
+ 'toggle.search.hint': '开启联网检索',
46
+ 'sidebar.empty': '暂无对话',
47
+ 'chats.count': '{count} 条',
48
+ 'role.you': '你',
49
+ 'chat.rename': '重命名',
50
+ 'chat.delete': '删除',
51
+ 'chat.delete.confirm': '确认删除?',
52
+ 'chat.clearAll': '清空全部',
53
+ 'chat.clearAll.confirm': '确认清空全部?',
54
+ 'chat.rename.placeholder': '输入新标题…',
55
+ 'chat.rename.ok': '确定',
56
+ 'chat.rename.cancel': '取消',
57
+ 'chat.manage.hint': '管理本地保存的聊天记录(不影响网页端)',
58
+ 'toast.rename.done': '已重命名',
59
+ 'toast.rename.failed': '重命名失败:{error}',
60
+ 'toast.delete.done': '已删除该对话',
61
+ 'toast.delete.failed': '删除失败:{error}',
62
+ 'toast.clear.done': '已清空全部对话',
63
+ 'toast.clear.failed': '清空失败:{error}',
64
+ 'model.deepseek-chat': 'DeepSeek',
65
+ 'model.deepseek-reasoner': 'DeepSeek',
66
+ 'composer.placeholder': '给 DeepSeek 发送消息…(将发送到网页端)',
67
+ 'composer.hint': 'Enter 发送 · Shift+Enter 换行',
68
+ 'composer.busy': '正在生成回复…',
69
+ 'composer.notLoggedIn': '请先登录 DeepSeek 网页端',
70
+ 'composer.attach': '图片',
71
+ 'composer.attach.hint': '添加本地图片(绝对路径,逗号/换行分隔)',
72
+ 'composer.attach.placeholder': '图片绝对路径,逗号或换行分隔',
73
+ 'empty.eyebrow': 'DeepSeek 网页端 · 桥接开发',
74
+ 'empty.title': '先聊,再交付',
75
+ 'empty.body': '登录网页端即可开始对话;任何对话都能蒸馏成任务简报,转入 Harness 会话继续开发。',
76
+ 'transfer.done': '已创建 Harness 会话 {sessionId},正在为你打开…',
77
+ 'transfer.continued': '已延续 Harness 会话 {sessionId},正在为你打开…',
78
+ 'transfer.failed': '转移到 Harness 失败:{error}',
79
+ 'export.done': '已导出到工作区文件:{filePath}',
80
+ 'export.failed': '导出失败:{error}',
81
+ 'recover.hint': '列出网页端尚未导入本地的会话',
82
+ 'recover.empty': '网页端没有未导入的会话',
83
+ 'recover.done': '已恢复「{title}」',
84
+ 'msg.empty': '(空)',
85
+ 'msg.error': '(该回复可能不完整:{error})',
86
+ 'streaming': '生成中',
87
+ } as const
88
+
89
+ /** Bilingual balance enforced: en must cover every zh key. */
90
+ export const en = {
91
+ 'entry.label': 'Web Chat',
92
+ 'entry.tooltip': 'DeepSeek web chat (deepseek-chat, Codex ChatGPT mode)',
93
+ 'panel.title': 'DeepSeek Web Chat',
94
+ 'status.engine.stopped': 'Browser not started',
95
+ 'status.engine.launching': 'Starting browser…',
96
+ 'status.engine.ready': 'Browser ready',
97
+ 'status.engine.error': 'Browser error',
98
+ 'status.loggedIn': 'Logged in',
99
+ 'status.notLoggedIn': 'Not logged in',
100
+ 'status.unknown': 'Unknown',
101
+ 'action.openLogin': 'Open login window',
102
+ 'action.closeBrowser': 'Close browser',
103
+ 'action.newChat': 'New chat',
104
+ 'action.recover': 'Recover from web',
105
+ 'action.refresh': 'Refresh',
106
+ 'action.send': 'Send',
107
+ 'action.stop': 'Stop',
108
+ 'action.transferToHarness': 'Transfer to Harness',
109
+ 'action.exportFile': 'Export to file',
110
+ 'transfer.mode.distill': 'Distill to task brief',
111
+ 'transfer.mode.raw': 'Raw transcript',
112
+ 'transfer.mode.hint': 'Distill: condense the chat into an executable brief via the harness LLM; Raw: replay the full transcript as the session\'s first message',
113
+ 'transfer.workspace.label': 'Target workspace',
114
+ 'transfer.workspace.ungrouped': 'Ungrouped',
115
+ 'transfer.workspace.new': 'New workspace…',
116
+ 'transfer.workspace.hint': 'Transfer into the chosen workspace; ungrouped leaves it out of any workspace',
117
+ 'transfer.workspace.created': 'Created and selected workspace {title}',
118
+ 'transfer.workspace.createFailed': 'Create workspace failed: {error}',
119
+ 'transfer.session.label': 'Continue into session',
120
+ 'transfer.session.hint': 'Continue into an existing harness session (append the brief as a new message); default creates a new one',
121
+ 'transfer.session.new': 'New session',
122
+ 'transfer.attached.failed': 'Session created ({sessionId}) but attaching to workspace failed; it is ungrouped',
123
+ 'handoff.eyebrow': 'Handoff to Harness',
124
+ 'toggle.deepThink': 'DeepThink',
125
+ 'toggle.search': 'Search',
126
+ 'toggle.deepThink.hint': 'Enable R1 deep think',
127
+ 'toggle.search.hint': 'Enable web search',
128
+ 'sidebar.empty': 'No chats',
129
+ 'chats.count': '{count} msgs',
130
+ 'role.you': 'You',
131
+ 'chat.rename': 'Rename',
132
+ 'chat.delete': 'Delete',
133
+ 'chat.delete.confirm': 'Delete?',
134
+ 'chat.clearAll': 'Clear all',
135
+ 'chat.clearAll.confirm': 'Clear all?',
136
+ 'chat.rename.placeholder': 'New title…',
137
+ 'chat.rename.ok': 'OK',
138
+ 'chat.rename.cancel': 'Cancel',
139
+ 'chat.manage.hint': 'Manage locally saved transcripts (does not affect the web)',
140
+ 'toast.rename.done': 'Renamed',
141
+ 'toast.rename.failed': 'Rename failed: {error}',
142
+ 'toast.delete.done': 'Chat deleted',
143
+ 'toast.delete.failed': 'Delete failed: {error}',
144
+ 'toast.clear.done': 'All chats cleared',
145
+ 'toast.clear.failed': 'Clear failed: {error}',
146
+ 'model.deepseek-chat': 'DeepSeek',
147
+ 'model.deepseek-reasoner': 'DeepSeek',
148
+ 'composer.placeholder': 'Message DeepSeek… (sent via the web)',
149
+ 'composer.hint': 'Enter to send · Shift+Enter for newline',
150
+ 'composer.busy': 'Generating…',
151
+ 'composer.notLoggedIn': 'Sign in to DeepSeek web first',
152
+ 'composer.attach': 'Images',
153
+ 'composer.attach.hint': 'Attach local images (absolute paths, comma/newline separated)',
154
+ 'composer.attach.placeholder': 'Image absolute paths (comma or newline separated)',
155
+ 'empty.eyebrow': 'DeepSeek Web · Bridge to Code',
156
+ 'empty.title': 'Chat first, hand off later',
157
+ 'empty.body': 'Sign in to the web and start chatting; any chat can be distilled into a task brief and handed off to Harness.',
158
+ 'transfer.done': 'Created Harness session {sessionId}; opening it…',
159
+ 'transfer.continued': 'Continued Harness session {sessionId}; opening it…',
160
+ 'transfer.failed': 'Transfer failed: {error}',
161
+ 'export.done': 'Exported to workspace file: {filePath}',
162
+ 'export.failed': 'Export failed: {error}',
163
+ 'recover.hint': 'List web conversations not yet imported locally',
164
+ 'recover.empty': 'No unimported web conversations',
165
+ 'recover.done': 'Recovered "{title}"',
166
+ 'msg.empty': '(empty)',
167
+ 'msg.error': '(reply may be incomplete: {error})',
168
+ 'streaming': 'generating',
169
+ } satisfies Record<keyof typeof zh, string>
170
+
171
+ /** Key union of the web-chat surface copy. */
172
+ export type WebChatKey = keyof typeof zh