dsh-ptc-plus 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/README.zh.md +137 -0
  4. package/assets/dsh-ptc-plus-banner.webp +0 -0
  5. package/assets/ptc-plus-repair.png +0 -0
  6. package/assets/ptc-plus-settings.png +0 -0
  7. package/client.js +366 -0
  8. package/cordis.patch.yml +5 -0
  9. package/docs/README.md +21 -0
  10. package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
  11. package/docs/adr/0002-limit-work-map-scope.md +3 -0
  12. package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
  13. package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
  14. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
  15. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
  16. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
  17. package/docs/adr/0008-define-agent-operating-contract.md +25 -0
  18. package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
  19. package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
  20. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
  21. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
  22. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
  23. package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
  24. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
  25. package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
  26. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
  27. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
  28. package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
  29. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
  30. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
  31. package/docs/adr/README.md +18 -0
  32. package/docs/architecture.md +134 -0
  33. package/docs/capability-projection.md +48 -0
  34. package/docs/client-ui.md +47 -0
  35. package/docs/durability-design.md +265 -0
  36. package/docs/evaluation.md +62 -0
  37. package/docs/installation.md +82 -0
  38. package/docs/program-data-plane.md +61 -0
  39. package/docs/publishing.md +37 -0
  40. package/docs/runtime-reference.md +119 -0
  41. package/docs/value-wire.md +61 -0
  42. package/index.js +532 -0
  43. package/internal/ast-traversal.js +20 -0
  44. package/internal/binding-descriptors.js +95 -0
  45. package/internal/binding-pattern.js +54 -0
  46. package/internal/builtin-esm-sync.js +9 -0
  47. package/internal/cell-analysis.js +467 -0
  48. package/internal/cell-rewriter.js +520 -0
  49. package/internal/config-spec.js +179 -0
  50. package/internal/cordis-tools-owner.js +363 -0
  51. package/internal/diagnostic.js +133 -0
  52. package/internal/direct-surface-owner.js +326 -0
  53. package/internal/edit-transport-owner.js +287 -0
  54. package/internal/failure-reporting.js +139 -0
  55. package/internal/kernel-worker.js +484 -0
  56. package/internal/module-policy.js +46 -0
  57. package/internal/program-bindings.js +187 -0
  58. package/internal/record-utils.js +53 -0
  59. package/internal/recovery-tips.js +81 -0
  60. package/internal/rejected-cell-editor.js +328 -0
  61. package/internal/repl-convenience.js +201 -0
  62. package/internal/runtime-bridge-owner.js +417 -0
  63. package/internal/runtime-config.js +77 -0
  64. package/internal/runtime-contexts.js +31 -0
  65. package/internal/sdk-projection.js +88 -0
  66. package/internal/session-cell-executor.js +612 -0
  67. package/internal/session-journal.js +889 -0
  68. package/internal/session-log-view.js +80 -0
  69. package/internal/session-runtime.js +536 -0
  70. package/internal/session-state.js +45 -0
  71. package/internal/source-position-map.js +224 -0
  72. package/internal/tool-call-canonicalizer.js +339 -0
  73. package/internal/value-wire.js +421 -0
  74. package/internal/worker-client.js +218 -0
  75. package/internal/worker-cwd-virtualization.js +155 -0
  76. package/package.json +100 -0
  77. package/src/client.js +165 -0
@@ -0,0 +1,155 @@
1
+ import { isAbsolute, parse, resolve, sep } from 'node:path'
2
+ import { promisify } from 'node:util'
3
+
4
+ const FILE_SYSTEM_PATH_ARGUMENTS = Object.freeze({
5
+ access: [0], accessSync: [0], appendFile: [0], appendFileSync: [0],
6
+ chmod: [0], chmodSync: [0], chown: [0], chownSync: [0],
7
+ copyFile: [0, 1], copyFileSync: [0, 1], cp: [0, 1], cpSync: [0, 1],
8
+ createReadStream: [0], createWriteStream: [0], exists: [0], existsSync: [0],
9
+ lchmod: [0], lchmodSync: [0], lchown: [0], lchownSync: [0],
10
+ link: [0, 1], linkSync: [0, 1], lstat: [0], lstatSync: [0],
11
+ lutimes: [0], lutimesSync: [0], mkdir: [0], mkdirSync: [0],
12
+ open: [0], openAsBlob: [0], openSync: [0], opendir: [0], opendirSync: [0],
13
+ readFile: [0], readFileSync: [0], readdir: [0], readdirSync: [0],
14
+ readlink: [0], readlinkSync: [0], realpath: [0], realpathSync: [0],
15
+ rename: [0, 1], renameSync: [0, 1], rm: [0], rmSync: [0],
16
+ rmdir: [0], rmdirSync: [0], stat: [0], statSync: [0], statfs: [0], statfsSync: [0],
17
+ symlink: [1], symlinkSync: [1], truncate: [0], truncateSync: [0],
18
+ unlink: [0], unlinkSync: [0], utimes: [0], utimesSync: [0],
19
+ watch: [0], watchFile: [0], writeFile: [0], writeFileSync: [0],
20
+ })
21
+ const FILE_SYSTEM_PREFIX_ARGUMENTS = Object.freeze([
22
+ 'mkdtemp', 'mkdtempSync', 'mkdtempDisposable', 'mkdtempDisposableSync',
23
+ ])
24
+ const FILE_SYSTEM_PROJECTED_PROPERTIES = Object.freeze({
25
+ exists: [promisify.custom],
26
+ realpath: ['native'],
27
+ realpathSync: ['native'],
28
+ })
29
+ const CHILD_PROCESS_PROJECTED_PROPERTIES = Object.freeze({
30
+ exec: [promisify.custom],
31
+ execFile: [promisify.custom],
32
+ })
33
+
34
+ function wrapBuiltinCallable(original, projectArguments, projectedProperties = []) {
35
+ const wrapped = {
36
+ invoke(...args) {
37
+ return Reflect.apply(original, this, projectArguments(args))
38
+ },
39
+ }.invoke
40
+ const descriptors = Object.getOwnPropertyDescriptors(original)
41
+ const projected = new Set(projectedProperties)
42
+ for (const property of Reflect.ownKeys(descriptors)) {
43
+ const descriptor = descriptors[property]
44
+ if (descriptor.value === original) {
45
+ descriptors[property] = { ...descriptor, value: wrapped }
46
+ } else if (projected.has(property)) {
47
+ descriptors[property] = {
48
+ ...descriptor,
49
+ value: wrapBuiltinCallable(descriptor.value, projectArguments),
50
+ }
51
+ }
52
+ }
53
+ return Object.defineProperties(wrapped, descriptors)
54
+ }
55
+
56
+ function wrapBuiltin(owner, name, projectArguments, projectedProperties) {
57
+ const original = owner?.[name]
58
+ if (typeof original !== 'function') return
59
+ owner[name] = wrapBuiltinCallable(original, projectArguments, projectedProperties)
60
+ }
61
+
62
+ /** Install one session's cwd projection into Node builtins loaded by the worker. */
63
+ export function installWorkerCwdVirtualization(sessionCwd, originalRequire) {
64
+ if (sessionCwd === undefined) return
65
+ const nativeResolve = resolve
66
+ const bufferPrefix = Buffer.from(sessionCwd + (/[\\/]$/.test(sessionCwd) ? '' : sep))
67
+ const sessionPath = (value) => {
68
+ if (typeof value === 'string') return isAbsolute(value) ? value : nativeResolve(sessionCwd, value)
69
+ if (Buffer.isBuffer(value)) {
70
+ const absolute = isAbsolute(String.fromCharCode(...value.subarray(0, 3)))
71
+ return absolute ? value : Buffer.concat([bufferPrefix, value])
72
+ }
73
+ return value
74
+ }
75
+ const sessionPathPrefix = (value) => {
76
+ if (typeof value !== 'string' || isAbsolute(value) || parse(value).root !== '') return sessionPath(value)
77
+ return sessionCwd + (/[\\/]$/.test(sessionCwd) ? '' : sep) + value
78
+ }
79
+ const projectPaths = (args, indices) => {
80
+ const next = [...args]
81
+ for (const index of indices) next[index] = sessionPath(next[index])
82
+ return next
83
+ }
84
+ const projectPrefix = (args) => {
85
+ const next = [...args]
86
+ next[0] = sessionPathPrefix(next[0])
87
+ return next
88
+ }
89
+ const sessionChildOptions = options => (
90
+ options === null || typeof options !== 'object' || options.cwd !== undefined
91
+ ? options
92
+ : { ...options, cwd: sessionCwd }
93
+ )
94
+ const withDefaultOptions = (args, index) => {
95
+ const next = [...args]
96
+ next[index] = sessionChildOptions(next[index]) ?? { cwd: sessionCwd }
97
+ return next
98
+ }
99
+ const childProcessOptions = (name, args) => {
100
+ if (name === 'exec' || name === 'execSync') {
101
+ const next = [...args]
102
+ if (typeof next[1] === 'function') next.splice(1, 0, { cwd: sessionCwd })
103
+ else next[1] = sessionChildOptions(next[1]) ?? { cwd: sessionCwd }
104
+ return next
105
+ }
106
+ if (name === 'execFile' || name === 'execFileSync') {
107
+ if (args.length === 1 || typeof args[1] === 'function') {
108
+ const next = [...args]
109
+ next.splice(1, 0, { cwd: sessionCwd })
110
+ return next
111
+ }
112
+ if (typeof args[2] === 'function') {
113
+ if (args[1] !== undefined && !Array.isArray(args[1])) return withDefaultOptions(args, 1)
114
+ const next = [...args]
115
+ next.splice(2, 0, { cwd: sessionCwd })
116
+ return next
117
+ }
118
+ if (args[1] !== undefined && !Array.isArray(args[1])) return withDefaultOptions(args, 1)
119
+ return withDefaultOptions(args, 2)
120
+ }
121
+ return args.length === 1 || (args[1] !== undefined && !Array.isArray(args[1]))
122
+ ? withDefaultOptions(args, 1)
123
+ : withDefaultOptions(args, 2)
124
+ }
125
+ const sessionGlobOptions = (args) => {
126
+ const next = [...args]
127
+ if (typeof next[1] === 'function') next.splice(1, 0, { cwd: sessionCwd })
128
+ else if (next[1] === undefined) next[1] = { cwd: sessionCwd }
129
+ else if (next[1] !== null && typeof next[1] === 'object' && next[1].cwd === undefined) {
130
+ next[1] = { ...next[1], cwd: sessionCwd }
131
+ }
132
+ return next
133
+ }
134
+
135
+ process.cwd = () => sessionCwd
136
+ const fs = originalRequire('node:fs')
137
+ const path = originalRequire('node:path')
138
+ for (const [name, indices] of Object.entries(FILE_SYSTEM_PATH_ARGUMENTS)) {
139
+ wrapBuiltin(fs, name, args => projectPaths(args, indices), FILE_SYSTEM_PROJECTED_PROPERTIES[name])
140
+ wrapBuiltin(fs.promises, name, args => projectPaths(args, indices))
141
+ }
142
+ for (const name of FILE_SYSTEM_PREFIX_ARGUMENTS) {
143
+ wrapBuiltin(fs, name, projectPrefix)
144
+ wrapBuiltin(fs.promises, name, projectPrefix)
145
+ }
146
+ for (const name of ['glob', 'globSync']) {
147
+ wrapBuiltin(fs, name, sessionGlobOptions)
148
+ wrapBuiltin(fs.promises, name, sessionGlobOptions)
149
+ }
150
+ wrapBuiltin(path, 'resolve', args => [sessionCwd, ...args])
151
+ const childProcess = originalRequire('node:child_process')
152
+ for (const name of ['exec', 'execFile', 'execFileSync', 'execSync', 'fork', 'spawn', 'spawnSync']) {
153
+ wrapBuiltin(childProcess, name, args => childProcessOptions(name, args), CHILD_PROCESS_PROJECTED_PROPERTIES[name])
154
+ }
155
+ }
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "dsh-ptc-plus",
3
+ "version": "0.2.1",
4
+ "description": "A session-bound agent-native REPL for DeepSeek Harness PTC mode.",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "client.js",
15
+ "src",
16
+ "internal",
17
+ "docs",
18
+ "assets/dsh-ptc-plus-banner.webp",
19
+ "assets/ptc-plus-repair.png",
20
+ "assets/ptc-plus-settings.png",
21
+ "cordis.patch.yml",
22
+ "README.md",
23
+ "README.zh.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": "^22.19.0 || >=24.0.0"
28
+ },
29
+ "dependencies": {
30
+ "@babel/code-frame": "^7.29.7",
31
+ "@babel/parser": "^7.29.0",
32
+ "@babel/traverse": "^7.29.0",
33
+ "@deepseek-ai/schemastery": "^3.18.1",
34
+ "acorn": "^8.15.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@deepseek-ai/dsh-settings": "next",
38
+ "@deepseek-ai/dsh-skill-filesystem": "next",
39
+ "@deepseek-ai/dsh-tool-cordis": "next",
40
+ "@deepseek-ai/dsh-tools": "next"
41
+ },
42
+ "scripts": {
43
+ "build": "node scripts/client-bundle.mjs build",
44
+ "build:check": "node scripts/client-bundle.mjs check",
45
+ "prepack": "npm run build:check",
46
+ "hooks:install": "node scripts/review-findings.mjs install-hook",
47
+ "review:new": "node scripts/review-findings.mjs create",
48
+ "review:validate": "node scripts/review-findings.mjs validate",
49
+ "review:gate": "node scripts/review-findings.mjs gate",
50
+ "session:migrate": "node scripts/migrate-session-log.mjs",
51
+ "coverage": "node --test --experimental-test-module-mocks --experimental-test-coverage --test-concurrency=1 --test-coverage-include=\"index.js\" --test-coverage-include=\"internal/*.js\" --test-coverage-lines=100 --test-coverage-branches=95 --test-coverage-functions=100 \"test/*.test.js\"",
52
+ "verify": "npm run review:validate && npm run build:check && node --check index.js && node --check internal/kernel-worker.js && node --check scripts/migrate-session-log.mjs && node --check scripts/expensive-headless-acceptance.mjs && node --check scripts/ab-headless-trajectories.mjs && npm run coverage",
53
+ "check": "node scripts/review-findings.mjs check",
54
+ "test:expensive": "node scripts/expensive-headless-acceptance.mjs",
55
+ "test:ab": "node scripts/ab-headless-trajectories.mjs"
56
+ },
57
+ "keywords": [
58
+ "deepseek-harness",
59
+ "dsh",
60
+ "dsh-plugin",
61
+ "ptc",
62
+ "ptc-mode",
63
+ "repl"
64
+ ],
65
+ "license": "MIT",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/muyuanjin/dsh-ptc-plus.git"
69
+ },
70
+ "homepage": "https://github.com/muyuanjin/dsh-ptc-plus#readme",
71
+ "bugs": {
72
+ "url": "https://github.com/muyuanjin/dsh-ptc-plus/issues"
73
+ },
74
+ "dsh": {
75
+ "bundle": {
76
+ "patch": "./cordis.patch.yml"
77
+ },
78
+ "client": {
79
+ "platform": "web",
80
+ "external": [
81
+ "react"
82
+ ],
83
+ "inject": [
84
+ "@deepseek-ai/dsh-client-runtime",
85
+ "@deepseek-ai/dsh-client-ui-settings",
86
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
87
+ "@deepseek-ai/dsh-client-connection",
88
+ "@deepseek-ai/dsh-client-ui-primitives"
89
+ ]
90
+ }
91
+ },
92
+ "devDependencies": {
93
+ "@deepseek-ai/dsh-settings": "next",
94
+ "@deepseek-ai/dsh-skill-filesystem": "next",
95
+ "@deepseek-ai/dsh-tool-cordis": "next",
96
+ "@deepseek-ai/dsh-tools": "next",
97
+ "esbuild": "0.28.2",
98
+ "yaml": "^2.9.0"
99
+ }
100
+ }
package/src/client.js ADDED
@@ -0,0 +1,165 @@
1
+ import { CONFIG_FIELDS, SETTINGS_NAMESPACE } from '../internal/config-spec.js'
2
+
3
+ const CLIENT_STYLE_ID = 'ptc-plus-client-style'
4
+ const CLIENT_CSS = `
5
+ .ptcPlusCard{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:var(--dsw-alias-bg-layer-3,#fff);border-radius:8px;list-style:none;overflow:hidden}
6
+ .ptcPlusHeader{appearance:none;width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background-color .16s ease}
7
+ .ptcPlusHeader:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusHeader:focus-visible,.ptcPlusButton:focus-visible,.ptcPlusInput:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:-2px}
8
+ .ptcPlusHeadText{display:flex;flex:1;min-width:0;flex-direction:column;align-items:flex-start;gap:1px}.ptcPlusName{font-size:14px;font-weight:600;line-height:20px}.ptcPlusDescription{color:var(--dsw-alias-label-tertiary,#74777d);font-size:12px;line-height:18px;overflow-wrap:anywhere}.ptcPlusStatus{display:inline-flex;align-items:center;flex:none;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:500;line-height:16px}.ptcPlusStatus[data-enabled=true]{color:var(--dsw-alias-state-success-primary,#16794f);background:var(--dsw-alias-state-success-tertiary,#e7f7ef)}.ptcPlusStatus[data-enabled=false]{color:var(--dsw-alias-label-tertiary,#74777d);background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}
9
+ .ptcPlusChevron{display:flex;color:var(--dsw-alias-label-tertiary,#74777d);transition:transform .18s ease}.ptcPlusChevron[data-open=true]{transform:rotate(180deg)}.ptcPlusBody{display:grid;grid-template-rows:0fr;transition:grid-template-rows .2s ease}.ptcPlusBody[data-open=true]{grid-template-rows:1fr}.ptcPlusBodyInner{min-height:0;overflow:hidden}.ptcPlusFields{margin:0 16px;padding:8px 0 12px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}
10
+ .ptcPlusRow{display:flex;align-items:center;gap:12px;min-height:48px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}.ptcPlusRow:first-child{border-top:0}.ptcPlusMain{flex:1;min-width:0}.ptcPlusLabel{font-size:14px;font-weight:500;line-height:20px}.ptcPlusDetail,.ptcPlusMessage{color:var(--dsw-alias-label-tertiary,#74777d);font-size:12px;line-height:18px;overflow-wrap:anywhere}.ptcPlusInput{box-sizing:border-box;min-width:72px;width:140px;padding:5px 8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace}.ptcPlusCheck{width:18px;height:18px;accent-color:var(--dsw-alias-interactive-primary,#4d6bfe)}
11
+ .ptcPlusFooter{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:8px}.ptcPlusButton{min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:6px;background:transparent;color:inherit;cursor:pointer;font:500 13px/20px inherit;transition:background-color .16s ease,border-color .16s ease}.ptcPlusButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusButton:disabled,.ptcPlusInput:disabled,.ptcPlusCheck:disabled{cursor:not-allowed;opacity:.55}.ptcPlusActive{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-state-success-primary,#16794f);font-size:12px;line-height:18px;white-space:nowrap}
12
+ @media(max-width:560px){.ptcPlusHeader{padding:12px}.ptcPlusFields{margin:0 12px}.ptcPlusRow{align-items:flex-start;flex-direction:column;gap:6px;padding:10px 0}.ptcPlusInput{width:100%}.ptcPlusFooter{align-items:stretch;flex-direction:column}.ptcPlusButton{width:100%}}
13
+ @media(prefers-reduced-motion:reduce){.ptcPlusHeader,.ptcPlusChevron,.ptcPlusBody,.ptcPlusButton{transition:none}}
14
+ `
15
+
16
+ window.__ModuleLoader__.load({
17
+ // Replaced by the bundle entry with the package name from package.json.
18
+ id: __PTC_PLUS_CLIENT_MODULE_ID__,
19
+ factory: (require) => {
20
+ const React = require('react')
21
+ const {
22
+ IconCheckOutline14,
23
+ IconChevronDownOutline14,
24
+ IconSettingsOutline16,
25
+ } = require('@deepseek-ai/dsh-client-ui-primitives')
26
+ const module = { exports: {} }
27
+ const h = React.createElement
28
+
29
+ function installStyles() {
30
+ if (document.getElementById(CLIENT_STYLE_ID) !== null) return () => {}
31
+ const style = document.createElement('style')
32
+ style.id = CLIENT_STYLE_ID
33
+ style.textContent = CLIENT_CSS
34
+ document.head.append(style)
35
+ return () => style.remove()
36
+ }
37
+
38
+ function fieldInput(field, value, disabled, onChange) {
39
+ if (field.type === 'boolean') {
40
+ return h('input', {
41
+ type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value === true,
42
+ disabled, 'aria-label': field.label,
43
+ onChange: event => onChange(field, event.target.checked),
44
+ })
45
+ }
46
+ return h('input', {
47
+ type: 'number', className: 'ptcPlusInput',
48
+ value: Number.isSafeInteger(value) ? String(value) : '',
49
+ min: field.min, max: field.max, step: 1, disabled, 'aria-label': field.label,
50
+ onChange: event => {
51
+ const input = event.target.value
52
+ const parsed = input === '' ? '' : Number(input)
53
+ onChange(field, Number.isSafeInteger(parsed) ? parsed : input)
54
+ },
55
+ })
56
+ }
57
+
58
+ function apply(ctx) {
59
+ const preferenceScope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE })
60
+ ctx.effect(installStyles, 'ptc-plus: client styles')
61
+
62
+ function PTCPlusSettingsCard() {
63
+ const [open, setOpen] = React.useState(false)
64
+ const [status, setStatus] = React.useState('')
65
+ const [pending, setPending] = React.useState(() => new Set())
66
+ const writeTail = React.useRef(Promise.resolve())
67
+ const subscribe = React.useCallback(listener => preferenceScope.subscribe(listener), [])
68
+ const getSnapshot = React.useCallback(() => preferenceScope.getSnapshot(), [])
69
+ const snapshot = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
70
+ const value = snapshot.status === 'ready' ? (snapshot.value ?? {}) : {}
71
+ const enabled = value.enabled === true
72
+ const unavailable = snapshot.status !== 'ready' || snapshot.writable !== true
73
+ const persist = (field, nextValue) => {
74
+ if (unavailable || pending.has(field.key)) return
75
+ const operation = writeTail.current.then(async () => {
76
+ const before = preferenceScope.getSnapshot()
77
+ if (before.status !== 'ready' || before.writable !== true) return
78
+ if (field.key !== 'enabled' && before.value?.enabled !== true) return
79
+ if (before.value?.[field.key] === nextValue) return
80
+ setPending(current => new Set(current).add(field.key))
81
+ setStatus('')
82
+ try {
83
+ await preferenceScope.set(field.key, nextValue)
84
+ const after = preferenceScope.getSnapshot()
85
+ if (after.status !== 'ready' || after.value?.[field.key] !== nextValue) {
86
+ setStatus('设置未生效,请检查设置冲突')
87
+ } else {
88
+ setStatus('设置已立即生效')
89
+ }
90
+ } catch (error) {
91
+ setStatus('设置失败:' + (error instanceof Error ? error.message : String(error)))
92
+ } finally {
93
+ setPending(current => {
94
+ const next = new Set(current)
95
+ next.delete(field.key)
96
+ return next
97
+ })
98
+ }
99
+ })
100
+ writeTail.current = operation.catch(() => {})
101
+ }
102
+ const fieldDisabled = field => unavailable
103
+ || pending.has(field.key)
104
+ || (field.key !== 'enabled' && !enabled)
105
+ return h('li', { className: 'ptcPlusCard' },
106
+ h('button', {
107
+ type: 'button', className: 'ptcPlusHeader', 'aria-expanded': open,
108
+ 'aria-label': open ? '收起 PTC Plus 设置' : '展开 PTC Plus 设置',
109
+ 'aria-controls': 'ptc-plus-settings-body', onClick: () => setOpen(current => !current),
110
+ },
111
+ h(IconSettingsOutline16, { size: 16 }),
112
+ h('span', { className: 'ptcPlusHeadText' },
113
+ h('span', { className: 'ptcPlusName' }, 'PTC Plus'),
114
+ h('span', { className: 'ptcPlusDescription' }, 'PTC 模式的会话级 TypeScript REPL。')),
115
+ h('span', { className: 'ptcPlusStatus', 'data-enabled': enabled }, enabled ? '已启用' : '已停用'),
116
+ h('span', { className: 'ptcPlusChevron', 'data-open': open, 'aria-hidden': true }, h(IconChevronDownOutline14, { size: 14 }))),
117
+ h('div', { id: 'ptc-plus-settings-body', className: 'ptcPlusBody', 'data-open': open, 'aria-hidden': !open },
118
+ h('div', { className: 'ptcPlusBodyInner' }, h('div', { className: 'ptcPlusFields' },
119
+ snapshot.status === 'loading'
120
+ ? h('p', { className: 'ptcPlusMessage' }, '正在同步设置...')
121
+ : snapshot.status === 'unavailable'
122
+ ? h('p', { className: 'ptcPlusMessage' }, '当前 DSH 实例未提供设置服务')
123
+ : [
124
+ ...CONFIG_FIELDS.map(field => h('div', { key: field.key, className: 'ptcPlusRow' },
125
+ h('div', { className: 'ptcPlusMain' },
126
+ h('div', { className: 'ptcPlusLabel' }, field.label),
127
+ field.description === '' ? null : h('div', { className: 'ptcPlusDetail' }, field.description)),
128
+ fieldInput(field, value[field.key], fieldDisabled(field), persist))),
129
+ h('div', { key: 'footer', className: 'ptcPlusFooter' },
130
+ h('span', { className: 'ptcPlusMessage', role: 'status' }, status || (snapshot.writable ? '设置会在修改后立即生效' : '当前设置为只读'))),
131
+ ]))),
132
+ )
133
+ }
134
+
135
+ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
136
+ name: 'settings.plugin.item', key: SETTINGS_NAMESPACE,
137
+ }, PTCPlusSettingsCard))
138
+
139
+ ctx.inject(['slots', 'sessions'], (scope) => {
140
+ function PTCPlusSessionIndicator({ sessionId }) {
141
+ const sessions = React.useSyncExternalStore(
142
+ listener => scope.sessions.list.subscribe(listener),
143
+ () => scope.sessions.list.getSnapshot(),
144
+ () => scope.sessions.list.getSnapshot(),
145
+ )
146
+ const settings = React.useSyncExternalStore(
147
+ listener => preferenceScope.subscribe(listener),
148
+ () => preferenceScope.getSnapshot(),
149
+ () => preferenceScope.getSnapshot(),
150
+ )
151
+ if (sessions.byId?.[sessionId]?.agentPreset !== 'code'
152
+ || settings.status !== 'ready' || settings.value?.enabled !== true) return null
153
+ return h('span', { className: 'ptcPlusActive', title: 'PTC Plus 已启用' },
154
+ h(IconCheckOutline14, { size: 14 }), 'PTC Plus')
155
+ }
156
+ scope.slots.inject('conversation.session.header.actions', () => scope.slots.register({
157
+ name: 'conversation.session.header.actions', id: 'ptc-plus-active', order: -9,
158
+ }, PTCPlusSessionIndicator))
159
+ })
160
+ }
161
+
162
+ module.exports = { apply, inject: ['settingsScope', 'slots', 'sessions'] }
163
+ return module.exports
164
+ },
165
+ })