dsh-git-changes 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.en.md +119 -0
- package/README.md +137 -2
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/assets/vendor/monaco/LICENSE +21 -0
- package/assets/vendor/monaco/ThirdPartyNotices.txt +448 -0
- package/assets/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.js +7 -0
- package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-cn.js +7 -0
- package/assets/vendor/monaco/vs/base/worker/workerMain.js +26 -0
- package/assets/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/assets/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/assets/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/assets/vendor/monaco/vs/editor/editor.main.css +6 -0
- package/assets/vendor/monaco/vs/editor/editor.main.js +744 -0
- package/assets/vendor/monaco/vs/editor/editor.main.nls.js +28 -0
- package/assets/vendor/monaco/vs/editor/editor.main.nls.zh-cn.js +30 -0
- package/assets/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/assets/vendor/monaco/vs/language/css/cssWorker.js +81 -0
- package/assets/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/assets/vendor/monaco/vs/language/html/htmlWorker.js +453 -0
- package/assets/vendor/monaco/vs/language/json/jsonMode.js +15 -0
- package/assets/vendor/monaco/vs/language/json/jsonWorker.js +36 -0
- package/assets/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/assets/vendor/monaco/vs/language/typescript/tsWorker.js +37016 -0
- package/assets/vendor/monaco/vs/loader.js +10 -0
- package/cordis.patch.yml +3 -0
- package/docs/images/git-changes-diff.png +0 -0
- package/docs/regression-checklist.md +47 -0
- package/icon.svg +10 -0
- package/lib/client.js +9418 -0
- package/lib/host/assets.js +161 -0
- package/lib/host/commit-message.js +158 -0
- package/lib/host/files-index.js +206 -0
- package/lib/host/git-write.js +194 -0
- package/lib/host/git.js +1147 -0
- package/lib/host/lsp.js +842 -0
- package/lib/host/routes.js +976 -0
- package/lib/host/session.js +84 -0
- package/lib/host/util.js +107 -0
- package/lib/index.js +33 -0
- package/licenses/dsh-vscode-mode-LICENSE +21 -0
- package/licenses/vscode-theme-seti-LICENSE +21 -0
- package/licenses/vscode-theme-seti-ThirdPartyNotices.txt +32 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -4
- package/src/client/00-const.js +79 -0
- package/src/client/05-utils.js +277 -0
- package/src/client/10-i18n.js +548 -0
- package/src/client/12-vscode-icons.js +460 -0
- package/src/client/15-theme.js +509 -0
- package/src/client/20-monaco.js +1404 -0
- package/src/client/22-lsp.js +592 -0
- package/src/client/25-tree.js +563 -0
- package/src/client/28-changes.js +1163 -0
- package/src/client/30-page.js +2259 -0
- package/src/client/35-pill.js +185 -0
- package/src/client/40-commands.js +248 -0
- package/src/client/45-css.js +357 -0
- package/src/client/47-pickers.js +457 -0
- package/src/client/50-apply.js +207 -0
- package/src/client/99-footer.js +62 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static route for the vendored Monaco AMD build.
|
|
3
|
+
*
|
|
4
|
+
* `ctx.webServer.register` routes carry no authentication of their own, so the
|
|
5
|
+
* handler applies the Host Connection fence first (fail closed when the service
|
|
6
|
+
* is unavailable), then resolves the request path against the package's
|
|
7
|
+
* `assets/vendor/monaco` directory with realpath containment.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, realpath, stat } from 'node:fs/promises'
|
|
10
|
+
import { extname, isAbsolute, relative, resolve } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prefix claimed by the plugin; namespaced to avoid any shipped route.
|
|
15
|
+
* NOTE: `ctx.webServer.match()` matches `pathname === prefix || pathname.startsWith(prefix + '/')`,
|
|
16
|
+
* so the registered prefix must NOT carry a trailing slash or nested files never match.
|
|
17
|
+
*/
|
|
18
|
+
export const VENDOR_PREFIX = '/dsh-git-changes/monaco'
|
|
19
|
+
|
|
20
|
+
/** Content types present in the AMD build (js/css/ttf). */
|
|
21
|
+
const VENDOR_MIME = {
|
|
22
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
23
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
24
|
+
'.css': 'text/css; charset=utf-8',
|
|
25
|
+
'.json': 'application/json; charset=utf-8',
|
|
26
|
+
'.map': 'application/json; charset=utf-8',
|
|
27
|
+
'.ttf': 'font/ttf',
|
|
28
|
+
'.woff': 'font/woff',
|
|
29
|
+
'.woff2': 'font/woff2',
|
|
30
|
+
'.svg': 'image/svg+xml',
|
|
31
|
+
'.png': 'image/png',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Apply the Connection request policy; returns the rejection status, or
|
|
36
|
+
* undefined when admitted. Never admits on a capability mismatch.
|
|
37
|
+
* @param {object} ctx host context
|
|
38
|
+
* @param {import('node:http').IncomingMessage} req request
|
|
39
|
+
* @returns {number | undefined}
|
|
40
|
+
*/
|
|
41
|
+
function rejectionOf(ctx, req) {
|
|
42
|
+
try {
|
|
43
|
+
const connection = ctx.connection
|
|
44
|
+
if (connection && typeof connection.requestRejection === 'function') {
|
|
45
|
+
const rejection = connection.requestRejection(req)
|
|
46
|
+
return rejection === 401 || rejection === 403 ? rejection : undefined
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
/* fall through to fail-closed */
|
|
50
|
+
}
|
|
51
|
+
return 503
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Serve one file under the Monaco vendor directory.
|
|
56
|
+
* @param {string} root absolute vendor root
|
|
57
|
+
* @param {import('node:http').IncomingMessage} req request
|
|
58
|
+
* @param {import('node:http').ServerResponse} res response
|
|
59
|
+
*/
|
|
60
|
+
async function serveVendor(root, req, res) {
|
|
61
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
62
|
+
res.statusCode = 405
|
|
63
|
+
res.end()
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const pathname = new URL(req.url ?? '/', 'http://localhost').pathname
|
|
68
|
+
if (pathname !== VENDOR_PREFIX && !pathname.startsWith(VENDOR_PREFIX + '/')) {
|
|
69
|
+
res.statusCode = 404
|
|
70
|
+
res.end()
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
let raw
|
|
74
|
+
try {
|
|
75
|
+
raw = decodeURIComponent(pathname.slice(VENDOR_PREFIX.length)).replace(/^\/+/, '')
|
|
76
|
+
} catch {
|
|
77
|
+
res.statusCode = 400
|
|
78
|
+
res.end()
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (!raw || raw.includes('\u0000')) {
|
|
82
|
+
res.statusCode = 404
|
|
83
|
+
res.end()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const segments = raw.split('/')
|
|
87
|
+
if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
88
|
+
res.statusCode = 403
|
|
89
|
+
res.end()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
const canonicalRoot = await realpath(root)
|
|
93
|
+
const target = await realpath(resolve(canonicalRoot, raw))
|
|
94
|
+
const rel = relative(canonicalRoot, target)
|
|
95
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel)) {
|
|
96
|
+
res.statusCode = 403
|
|
97
|
+
res.end()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
const info = await stat(target)
|
|
101
|
+
if (!info.isFile()) {
|
|
102
|
+
res.statusCode = 404
|
|
103
|
+
res.end()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const etag = 'W/"' + info.mtimeMs.toString(16) + '-' + info.size.toString(16) + '"'
|
|
107
|
+
res.setHeader('etag', etag)
|
|
108
|
+
res.setHeader('cache-control', 'private, max-age=3600')
|
|
109
|
+
if (req.headers['if-none-match'] === etag) {
|
|
110
|
+
res.statusCode = 304
|
|
111
|
+
res.end()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
res.statusCode = 200
|
|
115
|
+
res.setHeader('content-type', VENDOR_MIME[extname(target).toLowerCase()] ?? 'application/octet-stream')
|
|
116
|
+
res.setHeader('content-length', info.size)
|
|
117
|
+
if (req.method === 'HEAD') {
|
|
118
|
+
res.end()
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
res.end(await readFile(target))
|
|
122
|
+
} catch {
|
|
123
|
+
if (!res.headersSent) res.statusCode = 404
|
|
124
|
+
res.end()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Register the authenticated Monaco asset prefix route.
|
|
130
|
+
* @param {object} ctx host plugin context
|
|
131
|
+
*/
|
|
132
|
+
export function registerVendorRoute(ctx) {
|
|
133
|
+
const root = fileURLToPath(new URL('../../assets/vendor/monaco/', import.meta.url))
|
|
134
|
+
const handler = (req, res) => {
|
|
135
|
+
const rejection = rejectionOf(ctx, req)
|
|
136
|
+
if (rejection !== undefined) {
|
|
137
|
+
res.statusCode = rejection
|
|
138
|
+
res.setHeader('cache-control', 'no-store')
|
|
139
|
+
res.end()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
void serveVendor(root, req, res)
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const web = ctx.webServer
|
|
146
|
+
if (!web || typeof web.register !== 'function') {
|
|
147
|
+
ctx.logger?.warn?.('dsh-git-changes: webServer unavailable; Monaco assets cannot be served')
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
const dispose = web.register({ kind: 'prefix', path: VENDOR_PREFIX, handler })
|
|
151
|
+
ctx.effect(() => () => {
|
|
152
|
+
try {
|
|
153
|
+
dispose()
|
|
154
|
+
} catch {
|
|
155
|
+
/* webserver teardown may already own the route */
|
|
156
|
+
}
|
|
157
|
+
}, 'dsh-git-changes: Monaco vendor route')
|
|
158
|
+
} catch (error) {
|
|
159
|
+
ctx.logger?.warn?.(`dsh-git-changes: Monaco vendor route unavailable (${String(error)})`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI commit-message generation for the Git review panel.
|
|
3
|
+
*
|
|
4
|
+
* The Host owns the credentials and the model route, so the browser half never
|
|
5
|
+
* sees a provider key. We reuse the DSH `llm` service with the user's default
|
|
6
|
+
* model and stream one short completion; nothing here writes to the repository.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Diff characters handed to the model (tail clipped, never mid-line). */
|
|
10
|
+
const DIFF_MAX_CHARS = 16000
|
|
11
|
+
/** Upper bound for one generated message. */
|
|
12
|
+
const MESSAGE_MAX_CHARS = 2000
|
|
13
|
+
/** Subject length cap enforced on the client commit box too. */
|
|
14
|
+
const SUBJECT_MAX_CHARS = 120
|
|
15
|
+
/** Generation timeout. */
|
|
16
|
+
const GENERATE_TIMEOUT_MS = 60000
|
|
17
|
+
|
|
18
|
+
const SYSTEM_PROMPT = [
|
|
19
|
+
'你负责根据 unified diff 生成 Git 提交信息。只输出提交信息本身,必须使用简体中文。',
|
|
20
|
+
'格式遵循 Conventional Commits:`<type>(<scope>): <摘要>`,type 取 feat/fix/refactor/perf/docs/test/build/chore 之一,scope 可选。',
|
|
21
|
+
'首行只能有一行,不超过 50 个中文字符(硬上限 72 字符),用祈使语气,句尾不加句号。',
|
|
22
|
+
'需要时在空行后写正文:用中文短句说明改了什么、为什么,每行约 72 字符换行。',
|
|
23
|
+
'不要输出 markdown 围栏、引号、解释或「提交信息:」之类标签;即使 diff 全是英文代码,描述也必须用中文。',
|
|
24
|
+
].join(' ')
|
|
25
|
+
|
|
26
|
+
/** Whether the text contains at least one CJK ideograph. */
|
|
27
|
+
function hasChinese(text) {
|
|
28
|
+
return /[\u3400-\u9fff]/.test(String(text || ''))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clip one diff to the model input budget without exceeding it.
|
|
33
|
+
* @param {string} text unified diff text
|
|
34
|
+
* @returns {string} clipped text
|
|
35
|
+
*/
|
|
36
|
+
function clipDiff(text) {
|
|
37
|
+
const value = String(text || '')
|
|
38
|
+
if (value.length <= DIFF_MAX_CHARS) return value
|
|
39
|
+
return value.slice(0, DIFF_MAX_CHARS) + '\n... (diff truncated)'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize whatever the model returned into a commit message.
|
|
44
|
+
* @param {string} raw raw model text
|
|
45
|
+
* @returns {string} sanitized message (possibly empty)
|
|
46
|
+
*/
|
|
47
|
+
export function sanitizeCommitMessage(raw) {
|
|
48
|
+
let text = String(raw || '').replace(/\r\n?/g, '\n').trim()
|
|
49
|
+
text = text.replace(/^```[a-zA-Z0-9_-]*\n?/, '').replace(/\n?```$/, '').trim()
|
|
50
|
+
if (
|
|
51
|
+
(text.startsWith('"') && text.endsWith('"')) ||
|
|
52
|
+
(text.startsWith('`') && text.endsWith('`')) ||
|
|
53
|
+
(text.startsWith('“') && text.endsWith('”'))
|
|
54
|
+
) {
|
|
55
|
+
text = text.slice(1, -1).trim()
|
|
56
|
+
}
|
|
57
|
+
text = text.replace(/^(commit message|message|提交信息|commit)\s*[::]\s*/i, '').trim()
|
|
58
|
+
if (text === '') return ''
|
|
59
|
+
const lines = text.split('\n')
|
|
60
|
+
const subject = (lines.shift() || '').trim().slice(0, SUBJECT_MAX_CHARS)
|
|
61
|
+
const body = lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
62
|
+
const message = [subject, body].filter(Boolean).join('\n\n')
|
|
63
|
+
return message.slice(0, MESSAGE_MAX_CHARS)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the provider/model route to generate with.
|
|
68
|
+
* @param {object} ctx Host context
|
|
69
|
+
* @returns {{provider:string, model:string}|null} resolved selection
|
|
70
|
+
*/
|
|
71
|
+
function resolveModel(ctx) {
|
|
72
|
+
const selection = ctx.get('agentDefaultModel')?.currentSelection?.()
|
|
73
|
+
const provider = selection && typeof selection.provider === 'string' ? selection.provider : ''
|
|
74
|
+
const model = selection && typeof selection.model === 'string' ? selection.model : ''
|
|
75
|
+
if (provider === '' || model === '') return null
|
|
76
|
+
return { provider, model }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate a commit message from the staged (or unstaged) changes.
|
|
81
|
+
* @param {object} ctx Host context
|
|
82
|
+
* @param {{git:import('./git.js').GitRunner, cwd:string, signal?:AbortSignal}} request request facts
|
|
83
|
+
* @returns {Promise<object>} `{ok:true, message}` or `{ok:false, code, detail?}`
|
|
84
|
+
*/
|
|
85
|
+
export async function generateCommitMessage(ctx, request) {
|
|
86
|
+
const { git, cwd, signal } = request
|
|
87
|
+
const llm = ctx.get('llm')
|
|
88
|
+
if (!llm || typeof llm.stream !== 'function') return { ok: false, code: 'llm-unavailable' }
|
|
89
|
+
const model = resolveModel(ctx)
|
|
90
|
+
if (!model) return { ok: false, code: 'model-unavailable' }
|
|
91
|
+
|
|
92
|
+
const staged = await git.run(['diff', '--cached', '--no-color', '--no-ext-diff', '--no-textconv', '-U3', '--', '.'], { cwd, signal })
|
|
93
|
+
if (staged.exitCode !== 0) return { ok: false, code: 'diff-failed', detail: staged.stderr.trim().slice(0, 400) }
|
|
94
|
+
let diff = staged.stdout
|
|
95
|
+
if (diff.trim() === '') {
|
|
96
|
+
const unstaged = await git.run(['diff', '--no-color', '--no-ext-diff', '--no-textconv', '-U3', '--', '.'], { cwd, signal })
|
|
97
|
+
if (unstaged.exitCode !== 0) return { ok: false, code: 'diff-failed', detail: unstaged.stderr.trim().slice(0, 400) }
|
|
98
|
+
diff = unstaged.stdout
|
|
99
|
+
}
|
|
100
|
+
if (diff.trim() === '') return { ok: false, code: 'no-changes' }
|
|
101
|
+
|
|
102
|
+
const userText = '为下面的 diff 写一条提交信息(简体中文,遵循 Conventional Commits 规范):\n\n' + clipDiff(diff)
|
|
103
|
+
const attempt = async (system, text) => {
|
|
104
|
+
const controller = new AbortController()
|
|
105
|
+
const timeout = setTimeout(() => controller.abort(), GENERATE_TIMEOUT_MS)
|
|
106
|
+
const composite = typeof AbortSignal.any === 'function' && signal
|
|
107
|
+
? AbortSignal.any([signal, controller.signal])
|
|
108
|
+
: controller.signal
|
|
109
|
+
let output = ''
|
|
110
|
+
let failure = null
|
|
111
|
+
try {
|
|
112
|
+
const stream = llm.stream({
|
|
113
|
+
provider: model.provider,
|
|
114
|
+
model: model.model,
|
|
115
|
+
system,
|
|
116
|
+
messages: [{
|
|
117
|
+
role: 'user',
|
|
118
|
+
content: [{ type: 'text', text }],
|
|
119
|
+
}],
|
|
120
|
+
temperature: 0.2,
|
|
121
|
+
maxTokens: 400,
|
|
122
|
+
signal: composite,
|
|
123
|
+
})
|
|
124
|
+
for await (const chunk of stream) {
|
|
125
|
+
if (!chunk) continue
|
|
126
|
+
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') output += chunk.text
|
|
127
|
+
else if (chunk.type === 'finish' && chunk.reason && chunk.reason.kind === 'error') failure = chunk.reason.failure
|
|
128
|
+
else if (chunk.type === 'finish' && chunk.reason && chunk.reason.kind === 'aborted') failure = chunk.reason.failure
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return { output: '', failure: null, error }
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timeout)
|
|
134
|
+
}
|
|
135
|
+
return { output, failure, error: null }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let result = await attempt(SYSTEM_PROMPT, userText)
|
|
139
|
+
if (result.error && !signal?.aborted) {
|
|
140
|
+
return { ok: false, code: 'generate-failed', detail: String(result.error?.message || result.error).slice(0, 400) }
|
|
141
|
+
}
|
|
142
|
+
let message = sanitizeCommitMessage(result.output)
|
|
143
|
+
// Some models still answer English for an English diff; retry once with an
|
|
144
|
+
// explicit Chinese-only instruction before giving up.
|
|
145
|
+
if (message !== '' && !hasChinese(message)) {
|
|
146
|
+
const strict = '重要:提交信息必须使用简体中文,禁止输出英文描述。\n\n' + userText
|
|
147
|
+
result = await attempt(SYSTEM_PROMPT, strict)
|
|
148
|
+
if (result.error && !signal?.aborted) {
|
|
149
|
+
return { ok: false, code: 'generate-failed', detail: String(result.error?.message || result.error).slice(0, 400) }
|
|
150
|
+
}
|
|
151
|
+
message = sanitizeCommitMessage(result.output)
|
|
152
|
+
}
|
|
153
|
+
if (signal?.aborted) return { ok: false, code: 'aborted' }
|
|
154
|
+
if (message === '') {
|
|
155
|
+
return { ok: false, code: 'generate-failed', detail: result.failure?.message ? String(result.failure.message).slice(0, 400) : 'empty completion' }
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, message }
|
|
158
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only workspace index + text search for Quick Open and Search.
|
|
3
|
+
*
|
|
4
|
+
* Both surfaces walk the session workspace through `workspaceFiles.list` and
|
|
5
|
+
* read through `workspaceFiles.readBytes`; nothing is written and every walk is
|
|
6
|
+
* bounded by depth, entry count, file size, and total scan budget.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Directories never indexed (dependency/build/VCS noise). */
|
|
10
|
+
const SKIP_DIRS = new Set([
|
|
11
|
+
'node_modules', '.git', '.hg', '.svn', 'dist', 'build', 'out', '.next', '.nuxt',
|
|
12
|
+
'.cache', '.turbo', '.parcel-cache', 'target', 'obj', 'bin', 'vendor', '.venv',
|
|
13
|
+
'venv', '__pycache__', '.idea', '.vscode', 'coverage', '.dsh', '.pnpm-store',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
/** File extensions never read by search. */
|
|
17
|
+
const BINARY_EXT = new Set([
|
|
18
|
+
'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'pdf', 'zip', 'gz', 'tgz', '7z', 'rar',
|
|
19
|
+
'tar', 'exe', 'dll', 'so', 'dylib', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'mp3', 'mp4',
|
|
20
|
+
'mov', 'avi', 'webm', 'sqlite', 'db', 'class', 'jar', 'wasm', 'o', 'obj', 'a', 'lib', 'pdb',
|
|
21
|
+
'bcmap', 'pfb', 'zstd', 'zst', 'asar',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
const INDEX_TTL_MS = 30000
|
|
25
|
+
const MAX_FILES = 8000
|
|
26
|
+
const MAX_DEPTH = 14
|
|
27
|
+
const MAX_ENTRIES_PER_DIR = 2000
|
|
28
|
+
const MAX_SEARCH_FILES = 1500
|
|
29
|
+
const MAX_SEARCH_BYTES = 24 * 1024 * 1024
|
|
30
|
+
const MAX_FILE_BYTES = 512 * 1024
|
|
31
|
+
const MAX_RESULTS = 200
|
|
32
|
+
const SEARCH_TIME_BUDGET_MS = 8000
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Join a parent posix path and a child name.
|
|
36
|
+
* @param {string} parent parent path ('' for root)
|
|
37
|
+
* @param {string} name child name
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function joinPath(parent, name) {
|
|
41
|
+
return parent === '' ? name : parent + '/' + name
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether a file name is searchable text.
|
|
46
|
+
* @param {string} name basename
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function searchableName(name) {
|
|
50
|
+
const dot = name.toLowerCase().lastIndexOf('.')
|
|
51
|
+
if (dot < 0) return true
|
|
52
|
+
return !BINARY_EXT.has(name.toLowerCase().slice(dot + 1))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Per-session cached workspace file index (paths only, cwd-relative posix).
|
|
57
|
+
*/
|
|
58
|
+
export class WorkspaceIndex {
|
|
59
|
+
/**
|
|
60
|
+
* @param {object} ctx Host context
|
|
61
|
+
* @param {number} [ttlMs] cache lifetime
|
|
62
|
+
*/
|
|
63
|
+
constructor(ctx, ttlMs = INDEX_TTL_MS) {
|
|
64
|
+
this.ctx = ctx
|
|
65
|
+
this.ttlMs = ttlMs
|
|
66
|
+
this.cache = new Map()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Drop one session's cached index.
|
|
71
|
+
* @param {string} sessionId session identity
|
|
72
|
+
*/
|
|
73
|
+
invalidate(sessionId) {
|
|
74
|
+
this.cache.delete(sessionId)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Walk the workspace breadth-first under the given caps.
|
|
79
|
+
* @param {{sessionId:string, workspaceRoot:string}} scope workspace scope
|
|
80
|
+
* @param {AbortSignal} signal cancellation
|
|
81
|
+
* @returns {Promise<{files:string[], truncated:boolean}>}
|
|
82
|
+
*/
|
|
83
|
+
async walk(scope, signal) {
|
|
84
|
+
const files = []
|
|
85
|
+
const queue = [{ dir: '', depth: 0 }]
|
|
86
|
+
let truncated = false
|
|
87
|
+
while (queue.length > 0) {
|
|
88
|
+
if (signal?.aborted) throw new Error('aborted')
|
|
89
|
+
const next = queue.shift()
|
|
90
|
+
let listing
|
|
91
|
+
try {
|
|
92
|
+
listing = await this.ctx.workspaceFiles.list(scope, next.dir === '' ? scope.workspaceRoot : next.dir, signal)
|
|
93
|
+
} catch {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
const entries = Array.isArray(listing?.entries) ? listing.entries.slice(0, MAX_ENTRIES_PER_DIR) : []
|
|
97
|
+
if (listing?.truncated === true || entries.length < (listing?.entries?.length ?? 0)) truncated = true
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const child = joinPath(next.dir, String(entry.name))
|
|
100
|
+
if (entry.type === 'directory') {
|
|
101
|
+
if (SKIP_DIRS.has(String(entry.name).toLowerCase())) continue
|
|
102
|
+
if (next.depth + 1 > MAX_DEPTH) {
|
|
103
|
+
truncated = true
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
queue.push({ dir: child, depth: next.depth + 1 })
|
|
107
|
+
} else if (entry.type === 'file') {
|
|
108
|
+
files.push(child)
|
|
109
|
+
if (files.length >= MAX_FILES) {
|
|
110
|
+
truncated = true
|
|
111
|
+
return { files, truncated }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { files, truncated }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Cached index read.
|
|
121
|
+
* @param {{sessionId:string, workspaceRoot:string}} scope workspace scope
|
|
122
|
+
* @param {AbortSignal} signal cancellation
|
|
123
|
+
* @param {boolean} [force] bypass cache
|
|
124
|
+
* @returns {Promise<{files:string[], truncated:boolean, cached:boolean}>}
|
|
125
|
+
*/
|
|
126
|
+
async get(scope, signal, force = false) {
|
|
127
|
+
const key = scope.sessionId + '\n' + scope.workspaceRoot
|
|
128
|
+
const now = Date.now()
|
|
129
|
+
const hit = this.cache.get(key)
|
|
130
|
+
if (!force && hit && now - hit.at < this.ttlMs) return { files: hit.files, truncated: hit.truncated, cached: true }
|
|
131
|
+
const walked = await this.walk(scope, signal)
|
|
132
|
+
this.cache.set(key, { at: now, files: walked.files, truncated: walked.truncated })
|
|
133
|
+
return { ...walked, cached: false }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Case-insensitive substring search across indexed text files.
|
|
139
|
+
* @param {object} ctx Host context
|
|
140
|
+
* @param {WorkspaceIndex} index workspace index
|
|
141
|
+
* @param {{sessionId:string, workspaceRoot:string}} scope workspace scope
|
|
142
|
+
* @param {{query:string, limit?:number, signal?:AbortSignal}} options search options
|
|
143
|
+
* @returns {Promise<{results:Array<object>, scanned:number, truncated:boolean, elapsedMs:number}>}
|
|
144
|
+
*/
|
|
145
|
+
export async function searchWorkspace(ctx, index, scope, options) {
|
|
146
|
+
const query = String(options.query || '').trim()
|
|
147
|
+
if (query.length === 0) return { results: [], scanned: 0, truncated: false, elapsedMs: 0 }
|
|
148
|
+
const limit = Math.min(MAX_RESULTS, Math.max(1, Number(options.limit) || MAX_RESULTS))
|
|
149
|
+
const startedAt = Date.now()
|
|
150
|
+
const signal = options.signal
|
|
151
|
+
const walk = await index.get(scope, signal)
|
|
152
|
+
const results = []
|
|
153
|
+
let scanned = 0
|
|
154
|
+
let bytes = 0
|
|
155
|
+
let truncated = walk.truncated
|
|
156
|
+
const needle = query.toLowerCase()
|
|
157
|
+
for (const path of walk.files) {
|
|
158
|
+
if (signal?.aborted) break
|
|
159
|
+
if (Date.now() - startedAt > SEARCH_TIME_BUDGET_MS) {
|
|
160
|
+
truncated = true
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
if (results.length >= limit) {
|
|
164
|
+
truncated = true
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
if (!searchableName(path)) continue
|
|
168
|
+
if (scanned >= MAX_SEARCH_FILES) {
|
|
169
|
+
truncated = true
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
scanned += 1
|
|
173
|
+
let read
|
|
174
|
+
try {
|
|
175
|
+
read = await ctx.workspaceFiles.readBytes(scope, path, {}, signal)
|
|
176
|
+
} catch {
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if (!read || !(read.data instanceof Uint8Array)) continue
|
|
180
|
+
if (read.data.byteLength > MAX_FILE_BYTES) {
|
|
181
|
+
truncated = true
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
bytes += read.data.byteLength
|
|
185
|
+
if (bytes > MAX_SEARCH_BYTES) {
|
|
186
|
+
truncated = true
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
const text = new TextDecoder('utf-8', { fatal: false }).decode(read.data)
|
|
190
|
+
if (text.indexOf('\u0000') >= 0) continue
|
|
191
|
+
const lines = text.split(/\r\n|\r|\n/)
|
|
192
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
193
|
+
const line = lines[lineIndex]
|
|
194
|
+
const at = line.toLowerCase().indexOf(needle)
|
|
195
|
+
if (at < 0) continue
|
|
196
|
+
results.push({
|
|
197
|
+
path,
|
|
198
|
+
line: lineIndex + 1,
|
|
199
|
+
column: at + 1,
|
|
200
|
+
text: line.length > 240 ? line.slice(0, 240) + '…' : line,
|
|
201
|
+
})
|
|
202
|
+
if (results.length >= limit) break
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { results, scanned, truncated, elapsedMs: Date.now() - startedAt }
|
|
206
|
+
}
|