@mzzsfy/dsh-shell-select 0.4.0 → 0.4.3
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/README.md +26 -11
- package/cordis.patch.yml +3 -189
- package/package.json +10 -4
- package/src/api.mjs +7 -1
- package/src/client.js +177 -450
- package/src/config.mjs +66 -0
- package/src/executor.mjs +43 -12
- package/src/index.js +7 -1
- package/src/prompt-projection.mjs +34 -0
- package/src/pwsh-takeover.mjs +23 -0
- package/src/render.mjs +5 -1
- package/src/resolve.mjs +35 -23
- package/src/tool.mjs +51 -28
- package/src/web-routes.mjs +7 -2
- package/test/background-owner.test.mjs +14 -0
- package/test/client-card.test.mjs +153 -17
- package/test/client-id.test.mjs +0 -56
- package/test/client-render.test.mjs +38 -13
- package/test/config-entry.test.mjs +2 -1
- package/test/config.test.mjs +41 -3
- package/test/deny-output-schema.test.mjs +53 -0
- package/test/executor-seam.test.mjs +2 -1
- package/test/executor.test.mjs +96 -2
- package/test/official-contract.test.mjs +208 -0
- package/test/path-guidance.test.mjs +28 -0
- package/test/path-rescue.test.mjs +173 -0
- package/test/preset-drift.test.mjs +30 -139
- package/test/prompt-projection.test.mjs +120 -0
- package/test/pwsh-takeover.test.mjs +41 -0
- package/test/render.test.mjs +39 -0
- package/test/resolve.test.mjs +43 -13
- package/test/web-routes.test.mjs +15 -2
- package/test/client-drift.test.mjs +0 -27
- package/test/switch-guard.test.mjs +0 -37
package/src/client.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// dsh-shell-select Client
|
|
1
|
+
// dsh-shell-select Client 半区:tool.call.toolview shell 族卡片(官方 BashRow/
|
|
2
|
+
// terminalCardModel 同构 + 复制/折行/客户端徽章增强)。
|
|
2
3
|
// 以 DSH client-modules 自注册格式发布:__ModuleLoader__.load({id, factory});
|
|
3
|
-
//
|
|
4
|
+
// 客户端名册经 webServer 路由 /api/shell-select/config 读取(配置写入走宿主
|
|
5
|
+
// settings JSON,浏览器侧无设置页)。
|
|
4
6
|
|
|
5
7
|
window.__ModuleLoader__.load({
|
|
6
8
|
id: '@mzzsfy/dsh-shell-select',
|
|
@@ -41,9 +43,6 @@ window.__ModuleLoader__.load({
|
|
|
41
43
|
opacity: .7, display: 'inline-block', flex: 'none',
|
|
42
44
|
},
|
|
43
45
|
})),
|
|
44
|
-
IconChevron: createElementOf(['IconChevronDownOutlineRegular', 'IconChevronDownOutlineMedium', 'IconChevronDownOutline14'], ({ size }) => h('span', {
|
|
45
|
-
style: { fontSize: (size ?? 14) - 3, lineHeight: 1, userSelect: 'none' },
|
|
46
|
-
}, '▾')),
|
|
47
46
|
IconInspect: createElementOf(['IconInspectOutlineRegular', 'IconInspectOutlineMedium', 'IconInspectOutline12'], () => h('span', {
|
|
48
47
|
style: { fontSize: 10, lineHeight: 1, opacity: .8 },
|
|
49
48
|
}, 'ⓘ')),
|
|
@@ -51,15 +50,6 @@ window.__ModuleLoader__.load({
|
|
|
51
50
|
|
|
52
51
|
const API = {
|
|
53
52
|
config: '/api/shell-select/config',
|
|
54
|
-
probe: '/api/shell-select/probe',
|
|
55
|
-
detect: '/api/shell-select/detect',
|
|
56
|
-
}
|
|
57
|
-
const KINDS = ['pwsh', 'bash', 'cmd', 'wsl']
|
|
58
|
-
const KIND_LABELS = {
|
|
59
|
-
pwsh: 'PowerShell',
|
|
60
|
-
bash: 'POSIX bash(git bash / msys2 / cygwin)',
|
|
61
|
-
cmd: 'CMD',
|
|
62
|
-
wsl: 'WSL bash',
|
|
63
53
|
}
|
|
64
54
|
|
|
65
55
|
async function api(path, options) {
|
|
@@ -96,37 +86,122 @@ window.__ModuleLoader__.load({
|
|
|
96
86
|
}
|
|
97
87
|
// LOGIC-END clientDisplayName
|
|
98
88
|
|
|
89
|
+
// 仅在宿主明确报告 Windows 后,把可识别的 MSYS 路径映射回宿主文件路径。
|
|
90
|
+
// 文件地址本身不经 URL round-trip,避免改写未识别的编码与后缀。
|
|
91
|
+
// LOGIC-BEGIN msysToHostPath
|
|
92
|
+
function msysToHostPath(value, facts) {
|
|
93
|
+
if (typeof value !== 'string' || value === '') return value
|
|
94
|
+
const env = facts ?? {}
|
|
95
|
+
if (env.home && (value === '~' || value.startsWith('~/'))) {
|
|
96
|
+
return value === '~' ? env.home : `${env.home}/${value.slice(2)}`
|
|
97
|
+
}
|
|
98
|
+
const drive = /^\/([a-z])\/(.*)$/i.exec(value)
|
|
99
|
+
if (drive !== null) return `${drive[1].toUpperCase()}:/${drive[2]}`
|
|
100
|
+
const bareDrive = /^\/([a-z])$/i.exec(value)
|
|
101
|
+
if (bareDrive !== null) return `${bareDrive[1].toUpperCase()}:/`
|
|
102
|
+
if (env.tmpDir && (value === '/tmp' || value.startsWith('/tmp/'))) {
|
|
103
|
+
return value === '/tmp' ? env.tmpDir : env.tmpDir + value.slice(4)
|
|
104
|
+
}
|
|
105
|
+
return value
|
|
106
|
+
}
|
|
107
|
+
// LOGIC-END msysToHostPath
|
|
108
|
+
|
|
109
|
+
// LOGIC-BEGIN rescueFileAddress
|
|
110
|
+
function rescueFileAddress(address, msysToHostPath, facts) {
|
|
111
|
+
const prefix = 'dsh-resource://file/'
|
|
112
|
+
if (typeof address !== 'string' || !address.startsWith(prefix)) return address
|
|
113
|
+
const tail = address.slice(prefix.length)
|
|
114
|
+
const cut = tail.search(/[?#]/)
|
|
115
|
+
const suffix = cut === -1 ? '' : tail.slice(cut)
|
|
116
|
+
const parts = (cut === -1 ? tail : tail.slice(0, cut)).split('/')
|
|
117
|
+
const scope = parts.shift()
|
|
118
|
+
if (scope !== 'session' && scope !== 'absolute') return address
|
|
119
|
+
const sessionId = scope === 'session' ? parts.shift() : undefined
|
|
120
|
+
if (scope === 'session' && (!sessionId || parts.length === 0)) return address
|
|
121
|
+
let decodedSessionId = sessionId
|
|
122
|
+
if (scope === 'session') {
|
|
123
|
+
try {
|
|
124
|
+
decodedSessionId = decodeURIComponent(sessionId)
|
|
125
|
+
} catch {
|
|
126
|
+
return address
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let segments
|
|
130
|
+
try {
|
|
131
|
+
segments = parts.map(decodeURIComponent)
|
|
132
|
+
} catch {
|
|
133
|
+
return address
|
|
134
|
+
}
|
|
135
|
+
if (scope === 'absolute' && segments[0] === '' && segments.length > 1) return address
|
|
136
|
+
const path = scope === 'absolute' ? `/${segments.join('/')}` : segments.join('/')
|
|
137
|
+
const translated = msysToHostPath(path, facts)
|
|
138
|
+
if (translated === path) return address
|
|
139
|
+
const encoded = translated.split('/').map((segment) => encodeURIComponent(segment).replace(/%3A/gi, ':')).join('/')
|
|
140
|
+
const rebuilt = scope === 'session'
|
|
141
|
+
? `${prefix}session/${encodeURIComponent(decodedSessionId)}/${encoded}`
|
|
142
|
+
: `${prefix}absolute/${encoded.replace(/^\/+/, '')}`
|
|
143
|
+
return rebuilt + suffix
|
|
144
|
+
}
|
|
145
|
+
// LOGIC-END rescueFileAddress
|
|
146
|
+
|
|
147
|
+
const PATHMAP_URL = '/api/shell-select/pathmap'
|
|
148
|
+
let pathMap = null
|
|
149
|
+
let pathMapRequested = false
|
|
150
|
+
function loadPathMap() {
|
|
151
|
+
if (pathMapRequested) return
|
|
152
|
+
pathMapRequested = true
|
|
153
|
+
fetch(PATHMAP_URL, { headers: { accept: 'application/json' } })
|
|
154
|
+
.then((response) => response.ok ? response.json() : null)
|
|
155
|
+
.then((facts) => {
|
|
156
|
+
if (facts && typeof facts.platform === 'string') pathMap = facts
|
|
157
|
+
else pathMapRequested = false
|
|
158
|
+
})
|
|
159
|
+
.catch((error) => {
|
|
160
|
+
pathMapRequested = false
|
|
161
|
+
console.warn('shell-select: pathmap fetch failed', error)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
// LOGIC-END loadPathMap
|
|
165
|
+
|
|
166
|
+
function installPathRescue(ctx) {
|
|
167
|
+
if (typeof ctx.inject !== 'function') return
|
|
168
|
+
loadPathMap()
|
|
169
|
+
ctx.inject(['sidebarRight'], (sctx) => {
|
|
170
|
+
const controller = sctx?.sidebarRight
|
|
171
|
+
if (!controller || controller.shellSelectPathRescue === true || typeof controller.openResource !== 'function') return
|
|
172
|
+
if (typeof sctx.effect !== 'function') return
|
|
173
|
+
const openResource = controller.openResource
|
|
174
|
+
const openResourceIn = controller.openResourceIn
|
|
175
|
+
let active = true
|
|
176
|
+
const rescue = (address) => {
|
|
177
|
+
if (!active) return address
|
|
178
|
+
if (pathMap === null) loadPathMap()
|
|
179
|
+
return pathMap?.platform === 'win32'
|
|
180
|
+
? rescueFileAddress(address, msysToHostPath, pathMap)
|
|
181
|
+
: address
|
|
182
|
+
}
|
|
183
|
+
controller.openResource = function openResourceWithPathRescue(address, options) {
|
|
184
|
+
return openResource.call(this, rescue(address), options)
|
|
185
|
+
}
|
|
186
|
+
const wrappedOpenResource = controller.openResource
|
|
187
|
+
if (typeof openResourceIn === 'function') {
|
|
188
|
+
controller.openResourceIn = function openResourceInWithPathRescue(sessionId, address, options) {
|
|
189
|
+
return openResourceIn.call(this, sessionId, rescue(address), options)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const wrappedOpenResourceIn = controller.openResourceIn
|
|
193
|
+
controller.shellSelectPathRescue = true
|
|
194
|
+
const restore = () => {
|
|
195
|
+
active = false
|
|
196
|
+
if (controller.openResource === wrappedOpenResource) controller.openResource = openResource
|
|
197
|
+
if (typeof openResourceIn === 'function' && controller.openResourceIn === wrappedOpenResourceIn) controller.openResourceIn = openResourceIn
|
|
198
|
+
if (controller.shellSelectPathRescue === true) delete controller.shellSelectPathRescue
|
|
199
|
+
}
|
|
200
|
+
sctx.effect(() => restore, 'shell-select sidebar path rescue')
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
99
204
|
const CSS = [
|
|
100
|
-
'.sls-card { display:flex; flex-direction:column; gap:10px; }',
|
|
101
|
-
'.sls-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }',
|
|
102
|
-
'.sls-entry { display:flex; flex-direction:column; gap:6px; padding:10px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); border-radius:8px; }',
|
|
103
|
-
'.sls-entry__head { display:flex; align-items:center; gap:8px; }',
|
|
104
|
-
'.sls-grid { display:grid; grid-template-columns: 90px 1fr; gap:4px 8px; align-items:center; }',
|
|
105
|
-
'.sls-grid__label { opacity:.75; font-size:12px; }',
|
|
106
|
-
'.sls-input { padding:4px 8px; border-radius:6px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); background:transparent; color:inherit; min-width:0; }',
|
|
107
|
-
'.sls-input--wide { width:100%; }',
|
|
108
|
-
'.sls-btn { padding:3px 10px; border-radius:6px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); background:transparent; color:inherit; cursor:pointer; font-size:12px; }',
|
|
109
|
-
'.sls-btn:disabled { opacity:.5; cursor:default; }',
|
|
110
|
-
'.sls-btn--primary { border-color: transparent; background: var(--sls-accent, #4b7bcc); color:#fff; }',
|
|
111
|
-
'.sls-badge { font-size:11px; padding:1px 8px; border-radius:999px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); opacity:.85; }',
|
|
112
|
-
'.sls-badge--ok { color:#2e9e5b; border-color:#2e9e5b; }',
|
|
113
|
-
'.sls-badge--bad { color:#c44; border-color:#c44; }',
|
|
114
|
-
'.sls-badge--default { color:#fff; background:var(--sls-accent, #4b7bcc); border-color:transparent; }',
|
|
115
|
-
'.sls-hint { font-size:12px; opacity:.7; }',
|
|
116
|
-
'.sls-error { font-size:12px; color:#c44; white-space:pre-wrap; }',
|
|
117
|
-
'.sls-args { font-size:12px; font-family:var(--sls-mono, monospace); }',
|
|
118
|
-
// 命令黑名单:逐条规则编辑器(每条规则独立行,行级即时校验)
|
|
119
|
-
'.sls-rules { display:flex; flex-direction:column; gap:6px; padding:10px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); border-radius:8px; }',
|
|
120
|
-
'.sls-rules__head { display:flex; align-items:center; gap:8px; }',
|
|
121
|
-
'.sls-rules__title { font-weight:500; }',
|
|
122
|
-
'.sls-rules__note { font-size:12px; opacity:.7; }',
|
|
123
|
-
'.sls-rule { display:flex; align-items:center; gap:6px; }',
|
|
124
|
-
'.sls-rule__no { flex:none; width:18px; text-align:right; font-size:12px; opacity:.45; user-select:none; }',
|
|
125
|
-
'.sls-rule__input { flex:1; min-width:0; padding:4px 8px; border-radius:6px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); background:transparent; color:inherit; font-size:12px; font-family:var(--sls-mono, monospace); }',
|
|
126
|
-
'.sls-rule__input:focus { outline:none; border-color:var(--sls-accent, #4b7bcc); }',
|
|
127
|
-
'.sls-rule--bad .sls-rule__input { border-color:#c44; }',
|
|
128
|
-
'.sls-rule__err { flex:none; max-width:40%; font-size:11px; color:#c44; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }',
|
|
129
|
-
'.sls-rule__del { flex:none; padding:2px 8px; }',
|
|
130
205
|
// tool.call.toolview 卡片(key 'shell'):官方 terminal 行同构 + 复制/折行增强。
|
|
131
206
|
// 色彩对齐官方 ToolRow:标题 label-secondary、icon 盒 label-tertiary(实证自
|
|
132
207
|
// 官方行 computed style);token 缺失时回退 inherit 不致不可读
|
|
@@ -134,17 +209,13 @@ window.__ModuleLoader__.load({
|
|
|
134
209
|
'.sls-tv__row { display:flex; align-items:center; gap:7px; padding:2px 0; cursor:default; }',
|
|
135
210
|
'.sls-tv__row--exp { cursor:pointer; user-select:none; }',
|
|
136
211
|
'.sls-tv__lead { display:flex; align-items:center; gap:4px; color:var(--dsw-alias-label-tertiary, inherit); }',
|
|
137
|
-
// 官方
|
|
212
|
+
// 官方 ToolRow 同构:行首仅 icon,无展开箭头(可展开性由指针与整行点击承载);
|
|
138
213
|
// 错误/进行中状态点绝对定位覆盖 icon(红点掩盖 icon)
|
|
139
214
|
'.sls-tv__leadStack { position:relative; width:16px; height:16px; display:inline-flex; align-items:center; justify-content:center; flex:none; }',
|
|
140
|
-
'.sls-tv__iconIdle { display:inline-flex;
|
|
215
|
+
'.sls-tv__iconIdle { display:inline-flex; }',
|
|
141
216
|
'.sls-tv__stateCover { position:absolute; inset:0; margin:auto; display:inline-flex; align-items:center; justify-content:center; background:transparent; }',
|
|
142
217
|
'.sls-tv__stateCover::before { content:""; position:absolute; inset:-2px; border-radius:50%; background:var(--dsw-alias-bg-primary, #fff); }',
|
|
143
218
|
'.sls-tv__stateCover > span { position:relative; }',
|
|
144
|
-
'.sls-tv__row:hover .sls-tv__iconIdle { opacity:0; }',
|
|
145
|
-
'.sls-tv__chev { opacity:.45; transition:transform .15s ease, opacity .1s ease; }',
|
|
146
|
-
'.sls-tv__row:hover .sls-tv__chev { opacity:1; }',
|
|
147
|
-
'.sls-tv__row[data-open="1"] .sls-tv__chev { transform:rotate(-90deg); }',
|
|
148
219
|
'.sls-tv__sr { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }',
|
|
149
220
|
'.sls-tv__title { font-weight:400; }',
|
|
150
221
|
'.sls-tv__sep { width:2px; height:2px; border-radius:1px; background:var(--dsw-alias-label-caption, currentColor); opacity:.8; flex:none; }',
|
|
@@ -152,13 +223,15 @@ window.__ModuleLoader__.load({
|
|
|
152
223
|
'.sls-tv__sum--err { color:var(--dsw-alias-state-error-primary, #d4553f); }',
|
|
153
224
|
'.sls-tv__body { margin:6px 0 4px; border:1px solid rgba(128,128,128,.28); border-radius:8px; overflow:hidden; }',
|
|
154
225
|
'.sls-tv__head { display:flex; align-items:center; gap:8px; padding:6px 10px; border-bottom:1px solid rgba(128,128,128,.18); background:rgba(128,128,128,.05); }',
|
|
155
|
-
|
|
156
|
-
'.sls-
|
|
157
|
-
'.sls-
|
|
226
|
+
// 头部单行片段禁折行:cwd 行宽不足时收缩出省略号,徽章不收缩不折行
|
|
227
|
+
'.sls-tv__cwd { font-family:var(--sls-mono, monospace); font-size:12px; opacity:.7; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }',
|
|
228
|
+
'.sls-tv__takeover { font-size:11px; padding:0 7px; border-radius:999px; border:1px solid currentColor; opacity:.75; flex:none; white-space:nowrap; }',
|
|
229
|
+
'.sls-tv__badge { font-size:11px; padding:0 7px; border-radius:999px; border:1px solid rgba(128,128,128,.35); opacity:.85; flex:none; white-space:nowrap; }',
|
|
230
|
+
'.sls-tv__pill { font-size:12px; color:#d4553f; flex:none; white-space:nowrap; }',
|
|
158
231
|
'.sls-tv__pill--bg { color:var(--dsw-alias-label-tertiary, inherit); }',
|
|
159
|
-
'.sls-tv__duration { font-family:var(--sls-mono, monospace); font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; font-variant-numeric:tabular-nums; }',
|
|
232
|
+
'.sls-tv__duration { font-family:var(--sls-mono, monospace); font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; white-space:nowrap; font-variant-numeric:tabular-nums; }',
|
|
160
233
|
// 运行中脉冲文字:零信息窗口(参数未到)的活体反馈
|
|
161
|
-
'.sls-tv__pulse { font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; animation:sls-pulse 1.5s ease-in-out infinite; }',
|
|
234
|
+
'.sls-tv__pulse { font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; white-space:nowrap; animation:sls-pulse 1.5s ease-in-out infinite; }',
|
|
162
235
|
'@keyframes sls-pulse { 0%,100% { opacity:.35; } 50% { opacity:1; } }',
|
|
163
236
|
'.sls-tv__sp { flex:1; }',
|
|
164
237
|
'.sls-tv__copy { display:flex; align-items:center; gap:2px; flex:none; }',
|
|
@@ -171,380 +244,13 @@ window.__ModuleLoader__.load({
|
|
|
171
244
|
'.sls-tv__out { margin:0; padding:8px 10px; border-top:1px solid rgba(128,128,128,.18); font-family:var(--sls-mono, monospace); font-size:12.5px; white-space:pre; overflow-x:auto; max-height:320px; overflow-y:auto; }',
|
|
172
245
|
'.sls-tv__inspect { display:flex; align-items:center; gap:4px; padding:4px 10px; border:none; border-top:1px solid rgba(128,128,128,.18); background:transparent; color:inherit; opacity:.6; cursor:pointer; font-size:12px; }',
|
|
173
246
|
'.sls-tv__inspect:hover { opacity:1; }',
|
|
174
|
-
// 开关:隐藏原生 checkbox,选中态 track 与 thumb 位移用过渡呈现(仓库 client 规约)
|
|
175
|
-
'.sls-switch { display:inline-flex; align-items:center; cursor:pointer; }',
|
|
176
|
-
'.sls-switch input[type="checkbox"] { position:absolute; opacity:0; width:0; height:0; }',
|
|
177
|
-
'.sls-switch__track { position:relative; width:34px; height:19px; border-radius:999px; box-sizing:border-box;',
|
|
178
|
-
' background:var(--sls-track, rgba(128,128,128,0.35));',
|
|
179
|
-
' border:1px solid var(--sls-border, rgba(128,128,128,0.35));',
|
|
180
|
-
' transition:background 0.15s, border-color 0.15s; }',
|
|
181
|
-
'.sls-switch__thumb { position:absolute; top:50%; left:2px; width:13px; height:13px; border-radius:50%;',
|
|
182
|
-
' background:var(--sls-thumb, rgba(128,128,128,0.6));',
|
|
183
|
-
' transform:translateY(-50%); transition:left 0.15s, background 0.15s; }',
|
|
184
|
-
'.sls-switch:hover .sls-switch__track { border-color:var(--sls-accent, #4b7bcc); }',
|
|
185
|
-
'.sls-switch input[type="checkbox"]:checked + .sls-switch__track { background:var(--sls-accent, #4b7bcc); border-color:var(--sls-accent, #4b7bcc); }',
|
|
186
|
-
'.sls-switch input[type="checkbox"]:checked + .sls-switch__track .sls-switch__thumb { left:17px; background:var(--sls-bg-base, #fff); }',
|
|
187
|
-
'.sls-switch input[type="checkbox"]:focus-visible + .sls-switch__track { outline:2px solid var(--sls-accent, #4b7bcc); outline-offset:1px; }',
|
|
188
247
|
].join('\n')
|
|
189
248
|
|
|
190
|
-
// 开关的 checkbox + 轨道对,checkbox 语义保留仅视觉隐藏
|
|
191
|
-
function switchToggle(props) {
|
|
192
|
-
return [
|
|
193
|
-
h('input', { type: 'checkbox', ...props }),
|
|
194
|
-
h('span', { className: 'sls-switch__track' }, h('span', { className: 'sls-switch__thumb' })),
|
|
195
|
-
]
|
|
196
|
-
}
|
|
197
|
-
|
|
198
249
|
function h(type, props) {
|
|
199
250
|
const children = Array.prototype.slice.call(arguments, 2)
|
|
200
251
|
return React.createElement.apply(React, [type, props || null].concat(children))
|
|
201
252
|
}
|
|
202
253
|
|
|
203
|
-
// 保存负载:仅取设置 schema 字段,剥离探测态
|
|
204
|
-
function toSection(entries, defaultId, denyRules) {
|
|
205
|
-
return {
|
|
206
|
-
shells: entries.map((entry) => ({
|
|
207
|
-
id: entry.id.trim(),
|
|
208
|
-
kind: entry.kind,
|
|
209
|
-
path: entry.path.trim(),
|
|
210
|
-
args: entry.argsText.split(/\s+/).filter((item) => item.length > 0),
|
|
211
|
-
login: entry.kind === 'bash' ? entry.login === true : false,
|
|
212
|
-
distro: entry.distro ?? '',
|
|
213
|
-
env: parseEnvText(entry.envText),
|
|
214
|
-
})),
|
|
215
|
-
default: defaultId,
|
|
216
|
-
deny: denyRules.filter((rule) => rule.length > 0),
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 逐条规则的正则校验:返回非法条目 [{index, error}](index 为规则序,供行级标错与保存拦截复用)
|
|
221
|
-
// LOGIC-BEGIN denyPatternIssues
|
|
222
|
-
function denyPatternIssues(rules) {
|
|
223
|
-
const issues = []
|
|
224
|
-
for (let index = 0; index < rules.length; index += 1) {
|
|
225
|
-
if (rules[index].length === 0) continue
|
|
226
|
-
try {
|
|
227
|
-
new RegExp(rules[index])
|
|
228
|
-
} catch (error) {
|
|
229
|
-
issues.push({ index, error: String(error?.message ?? error) })
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return issues
|
|
233
|
-
}
|
|
234
|
-
// LOGIC-END denyPatternIssues
|
|
235
|
-
|
|
236
|
-
// 环境文本(每行 K=V)→ 记录;空行与缺 = 的行忽略,值保留原样含空格与 =。
|
|
237
|
-
// LOGIC-BEGIN parseEnvText
|
|
238
|
-
function parseEnvText(text) {
|
|
239
|
-
const env = {}
|
|
240
|
-
for (const line of String(text ?? '').split('\n')) {
|
|
241
|
-
const separator = line.indexOf('=')
|
|
242
|
-
if (separator <= 0) continue
|
|
243
|
-
const key = line.slice(0, separator).trim()
|
|
244
|
-
if (key.length === 0) continue
|
|
245
|
-
env[key] = line.slice(separator + 1)
|
|
246
|
-
}
|
|
247
|
-
return env
|
|
248
|
-
}
|
|
249
|
-
// LOGIC-END parseEnvText
|
|
250
|
-
|
|
251
|
-
// 环境文本中被忽略的行(空行以外):缺 = 的行在保存校验时报错,防静默丢数据
|
|
252
|
-
// LOGIC-BEGIN invalidEnvLines
|
|
253
|
-
function invalidEnvLines(text) {
|
|
254
|
-
return String(text ?? '').split('\n')
|
|
255
|
-
.map((line) => line.trim())
|
|
256
|
-
.filter((line) => line.length > 0 && line.indexOf('=') <= 0)
|
|
257
|
-
}
|
|
258
|
-
// LOGIC-END invalidEnvLines
|
|
259
|
-
|
|
260
|
-
// 服务器清单 → 编辑态(argsText 汇成一串便于编辑;envText 每行 K=V;deny 为逐条规则数组)
|
|
261
|
-
function toEntries(section) {
|
|
262
|
-
return section.shells.map((entry) => ({
|
|
263
|
-
id: entry.id,
|
|
264
|
-
kind: entry.kind,
|
|
265
|
-
path: entry.path,
|
|
266
|
-
argsText: (entry.args ?? []).join(' '),
|
|
267
|
-
login: entry.login === true,
|
|
268
|
-
distro: entry.distro ?? '',
|
|
269
|
-
envText: Object.entries(entry.env ?? {}).map(([key, value]) => `${key}=${value}`).join('\n'),
|
|
270
|
-
available: entry.available,
|
|
271
|
-
resolved: entry.path,
|
|
272
|
-
}))
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 名单数组 → 编辑态数组(去空白行;加载侧解析)
|
|
276
|
-
function patternText(list) {
|
|
277
|
-
return (Array.isArray(list) ? list : []).map((rule) => String(rule).trim()).filter((rule) => rule.length > 0)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function ShellSelectApp() {
|
|
281
|
-
const [entries, setEntries] = useState(null)
|
|
282
|
-
const [defaultId, setDefaultId] = useState('')
|
|
283
|
-
const [dirty, setDirty] = useState(false)
|
|
284
|
-
const [notice, setNotice] = useState(null)
|
|
285
|
-
const [busy, setBusy] = useState(false)
|
|
286
|
-
const [denyRules, setDenyRules] = useState([])
|
|
287
|
-
|
|
288
|
-
useEffect(() => {
|
|
289
|
-
let disposed = false
|
|
290
|
-
api(API.config).then((section) => {
|
|
291
|
-
if (disposed) return
|
|
292
|
-
setEntries(toEntries(section))
|
|
293
|
-
setDefaultId(section.default)
|
|
294
|
-
setDenyRules(patternText(section.deny))
|
|
295
|
-
}).catch((error) => {
|
|
296
|
-
if (!disposed) setNotice('加载失败:' + error.message)
|
|
297
|
-
})
|
|
298
|
-
return () => {
|
|
299
|
-
disposed = true
|
|
300
|
-
}
|
|
301
|
-
}, [])
|
|
302
|
-
|
|
303
|
-
const patchEntry = (index, changes) => {
|
|
304
|
-
setDirty(true)
|
|
305
|
-
setEntries(entries.map((entry, at) => (at === index ? { ...entry, ...changes } : entry)))
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const addEntry = () => {
|
|
309
|
-
setDirty(true)
|
|
310
|
-
setEntries([...entries, { id: '', kind: 'bash', path: '', argsText: '', login: false, distro: '', envText: '', available: undefined, resolved: undefined }])
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const removeEntry = (index) => {
|
|
314
|
-
const target = entries[index]
|
|
315
|
-
setDirty(true)
|
|
316
|
-
setEntries(entries.filter((_, at) => at !== index))
|
|
317
|
-
if (defaultId === target.id) setDefaultId(entries.length > 1 ? entries[0].id : '')
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const patchRule = (index, value) => {
|
|
321
|
-
setDirty(true)
|
|
322
|
-
setDenyRules(denyRules.map((rule, at) => (at === index ? value : rule)))
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const addRule = () => {
|
|
326
|
-
setDirty(true)
|
|
327
|
-
setDenyRules([...denyRules, ''])
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const removeRule = (index) => {
|
|
331
|
-
setDirty(true)
|
|
332
|
-
setDenyRules(denyRules.filter((_, at) => at !== index))
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const save = async () => {
|
|
336
|
-
setBusy(true)
|
|
337
|
-
setNotice(null)
|
|
338
|
-
try {
|
|
339
|
-
const section = toSection(entries, defaultId, denyRules)
|
|
340
|
-
const ids = section.shells.map((entry) => entry.id)
|
|
341
|
-
if (ids.some((id) => id.length === 0)) throw new Error('存在空 id 条目')
|
|
342
|
-
if (new Set(ids).size !== ids.length) throw new Error('id 重复:' + ids.join(', '))
|
|
343
|
-
if (!ids.includes(section.default)) throw new Error('默认客户端不在列表中')
|
|
344
|
-
const invalid = entries.map((entry) => ({ entry, lines: invalidEnvLines(entry.envText) }))
|
|
345
|
-
.find(({ lines }) => lines.length > 0)
|
|
346
|
-
if (invalid !== undefined) throw new Error(`环境变量行缺少 =(条目 ${invalid.entry.id || '(未命名)'}):${invalid.lines.join(' ; ')}`)
|
|
347
|
-
const issue = denyPatternIssues(section.deny)[0]
|
|
348
|
-
if (issue !== undefined) {
|
|
349
|
-
throw new Error(`命令黑名单第 ${issue.index + 1} 条正则非法:${issue.error}`)
|
|
350
|
-
}
|
|
351
|
-
const payload = await api(API.config, { method: 'POST', body: JSON.stringify(section) })
|
|
352
|
-
setEntries(toEntries({ shells: payload.resolved.shells }))
|
|
353
|
-
setDefaultId(payload.resolved.default)
|
|
354
|
-
setDenyRules(patternText(payload.resolved.deny))
|
|
355
|
-
setDirty(false)
|
|
356
|
-
setNotice('已保存,工具描述与默认客户端即时生效')
|
|
357
|
-
} catch (error) {
|
|
358
|
-
setNotice('保存失败:' + error.message)
|
|
359
|
-
} finally {
|
|
360
|
-
setBusy(false)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const probe = async (index) => {
|
|
365
|
-
const target = entries[index]
|
|
366
|
-
setBusy(true)
|
|
367
|
-
setNotice(null)
|
|
368
|
-
try {
|
|
369
|
-
const payload = await api(API.probe, { method: 'POST', body: JSON.stringify({ path: target.path }) })
|
|
370
|
-
patchEntry(index, { available: payload.exists, resolved: target.path })
|
|
371
|
-
} catch (error) {
|
|
372
|
-
setNotice('探测失败:' + error.message)
|
|
373
|
-
} finally {
|
|
374
|
-
setBusy(false)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const detect = async (kind) => {
|
|
379
|
-
setBusy(true)
|
|
380
|
-
setNotice(null)
|
|
381
|
-
try {
|
|
382
|
-
const payload = await api(API.detect, { method: 'POST', body: JSON.stringify({ kinds: [kind] }) })
|
|
383
|
-
setNotice(payload.found.length > 0
|
|
384
|
-
? kind + ' 候选:' + payload.found.map((item) => item.path).join(' ; ')
|
|
385
|
-
: kind + ' 未发现候选')
|
|
386
|
-
} catch (error) {
|
|
387
|
-
setNotice('探测失败:' + error.message)
|
|
388
|
-
} finally {
|
|
389
|
-
setBusy(false)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (entries === null) {
|
|
394
|
-
return h('div', { className: 'sls-card' },
|
|
395
|
-
h('span', { className: 'sls-hint' }, notice ?? '加载中…'))
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return h('div', { className: 'sls-card' },
|
|
399
|
-
h('div', { className: 'sls-row' },
|
|
400
|
-
h('span', { className: 'sls-hint' }, '命令行客户端清单。路径留空自动探测;模型可通过工具的 shell 参数选择任一条目,缺省用默认。'),
|
|
401
|
-
),
|
|
402
|
-
entries.map((entry, index) => h('div', { className: 'sls-entry', key: index },
|
|
403
|
-
h('div', { className: 'sls-entry__head' },
|
|
404
|
-
h('label', { className: 'sls-row' },
|
|
405
|
-
h('input', {
|
|
406
|
-
type: 'radio',
|
|
407
|
-
name: 'sls-default',
|
|
408
|
-
checked: defaultId === entry.id && entry.id.length > 0,
|
|
409
|
-
onChange: () => {
|
|
410
|
-
setDirty(true)
|
|
411
|
-
setDefaultId(entry.id)
|
|
412
|
-
},
|
|
413
|
-
disabled: entry.id.trim().length === 0,
|
|
414
|
-
title: '设为默认客户端(模型缺省 shell 参数时使用)',
|
|
415
|
-
}),
|
|
416
|
-
h('span', { className: 'sls-hint' }, '默认'),
|
|
417
|
-
),
|
|
418
|
-
h('span', {
|
|
419
|
-
className: 'sls-badge' + (entry.available === true ? ' sls-badge--ok' : entry.available === false ? ' sls-badge--bad' : ''),
|
|
420
|
-
title: entry.resolved !== undefined && entry.resolved.length > 0 ? '解析路径:' + entry.resolved : '路径尚未解析',
|
|
421
|
-
}, entry.available === true ? '可用' : entry.available === false ? '不可用' : '未知'),
|
|
422
|
-
entry.id === defaultId && entry.id.length > 0 ? h('span', { className: 'sls-badge sls-badge--default' }, '默认') : null,
|
|
423
|
-
h('span', { style: { flex: 1 } }),
|
|
424
|
-
h('button', {
|
|
425
|
-
className: 'sls-btn',
|
|
426
|
-
disabled: busy || entry.path.trim().length === 0,
|
|
427
|
-
title: '检查该路径是否存在',
|
|
428
|
-
onClick: () => void probe(index),
|
|
429
|
-
}, '探测路径'),
|
|
430
|
-
h('button', {
|
|
431
|
-
className: 'sls-btn',
|
|
432
|
-
disabled: busy,
|
|
433
|
-
title: '移除该条目',
|
|
434
|
-
onClick: () => removeEntry(index),
|
|
435
|
-
}, '移除'),
|
|
436
|
-
),
|
|
437
|
-
h('div', { className: 'sls-grid' },
|
|
438
|
-
h('span', { className: 'sls-grid__label' }, 'id'),
|
|
439
|
-
h('input', {
|
|
440
|
-
className: 'sls-input',
|
|
441
|
-
value: entry.id,
|
|
442
|
-
placeholder: 'pwsh / git-bash / …(模型看到的 shell 参数值,亦作显示名)',
|
|
443
|
-
onChange: (event) => patchEntry(index, { id: event.target.value }),
|
|
444
|
-
}),
|
|
445
|
-
h('span', { className: 'sls-grid__label' }, '形态'),
|
|
446
|
-
h('div', { className: 'sls-row' },
|
|
447
|
-
h('select', {
|
|
448
|
-
className: 'sls-input',
|
|
449
|
-
value: entry.kind,
|
|
450
|
-
onChange: (event) => patchEntry(index, { kind: event.target.value }),
|
|
451
|
-
}, KINDS.map((kind) => h('option', { value: kind, key: kind }, KIND_LABELS[kind]))),
|
|
452
|
-
h('button', {
|
|
453
|
-
className: 'sls-btn',
|
|
454
|
-
disabled: busy,
|
|
455
|
-
title: '扫描本机常见安装位置',
|
|
456
|
-
onClick: () => void detect(entry.kind),
|
|
457
|
-
}, '扫描本机'),
|
|
458
|
-
),
|
|
459
|
-
h('span', { className: 'sls-grid__label' }, '路径'),
|
|
460
|
-
h('input', {
|
|
461
|
-
className: 'sls-input sls-input--wide',
|
|
462
|
-
value: entry.path,
|
|
463
|
-
placeholder: '留空自动探测,如 C:\\Program Files\\Git\\bin\\bash.exe',
|
|
464
|
-
onChange: (event) => patchEntry(index, { path: event.target.value }),
|
|
465
|
-
}),
|
|
466
|
-
h('span', { className: 'sls-grid__label' }, '参数'),
|
|
467
|
-
h('input', {
|
|
468
|
-
className: 'sls-input sls-input--wide sls-args',
|
|
469
|
-
value: entry.argsText,
|
|
470
|
-
placeholder: '留空用形态默认;自定义模板以空格分隔,{command} 为命令占位',
|
|
471
|
-
onChange: (event) => patchEntry(index, { argsText: event.target.value }),
|
|
472
|
-
}),
|
|
473
|
-
...(entry.kind === 'bash' ? [
|
|
474
|
-
h('span', { className: 'sls-grid__label' }, '登录壳'),
|
|
475
|
-
h('label', { className: 'sls-row sls-switch', title: '-lc 登录壳拉起 /etc/profile(只注入 /usr/bin;/mingw64/bin 需在下方的环境里配 MSYSTEM=MINGW64)' },
|
|
476
|
-
...switchToggle({
|
|
477
|
-
checked: entry.login === true,
|
|
478
|
-
onChange: (event) => patchEntry(index, { login: event.target.checked }),
|
|
479
|
-
}),
|
|
480
|
-
h('span', { className: 'sls-hint' }, '-lc 登录壳'),
|
|
481
|
-
),
|
|
482
|
-
] : []),
|
|
483
|
-
...(entry.kind === 'wsl' ? [
|
|
484
|
-
h('span', { className: 'sls-grid__label' }, '发行版'),
|
|
485
|
-
h('input', {
|
|
486
|
-
className: 'sls-input',
|
|
487
|
-
value: entry.distro ?? '',
|
|
488
|
-
placeholder: '留空用默认发行版,如 Ubuntu-22.04',
|
|
489
|
-
onChange: (event) => patchEntry(index, { distro: event.target.value }),
|
|
490
|
-
}),
|
|
491
|
-
] : []),
|
|
492
|
-
h('span', { className: 'sls-grid__label' }, '环境'),
|
|
493
|
-
h('textarea', {
|
|
494
|
-
className: 'sls-input sls-args',
|
|
495
|
-
rows: 2,
|
|
496
|
-
value: entry.envText,
|
|
497
|
-
placeholder: '每行 K=V,如 MSYSTEM=MINGW64;wsl 形自动经 WSLENV 透传',
|
|
498
|
-
onChange: (event) => patchEntry(index, { envText: event.target.value }),
|
|
499
|
-
}),
|
|
500
|
-
),
|
|
501
|
-
)),
|
|
502
|
-
h('div', { className: 'sls-rules' },
|
|
503
|
-
h('div', { className: 'sls-rules__head' },
|
|
504
|
-
h('span', { className: 'sls-rules__title' }, '命令黑名单'),
|
|
505
|
-
h('span', { className: 'sls-badge' + (denyRules.length > 0 ? ' sls-badge--ok' : ' sls-badge--bad') },
|
|
506
|
-
denyRules.length > 0 ? denyRules.length + ' 条规则' : '未生效'),
|
|
507
|
-
),
|
|
508
|
-
h('span', { className: 'sls-rules__note' },
|
|
509
|
-
'每条规则为一条正则,对整条命令全文匹配(大小写不敏感),命中即拒,无豁免;精细放行写在模式内用前瞻,如 rm -rf\\s+(?!\\S*node_modules)。'),
|
|
510
|
-
denyRules.length === 0
|
|
511
|
-
? h('span', { className: 'sls-rules__note' }, '未配置,命中拦截不生效,模型可执行任意命令。')
|
|
512
|
-
: denyRules.map((rule, index) => {
|
|
513
|
-
const issue = denyPatternIssues([rule])[0]
|
|
514
|
-
return h('div', { className: 'sls-rule' + (issue !== undefined ? ' sls-rule--bad' : ''), key: index },
|
|
515
|
-
h('span', { className: 'sls-rule__no' }, index + 1),
|
|
516
|
-
h('input', {
|
|
517
|
-
className: 'sls-rule__input',
|
|
518
|
-
value: rule,
|
|
519
|
-
placeholder: '正则,如 ^format\\s',
|
|
520
|
-
onChange: (event) => patchRule(index, event.target.value),
|
|
521
|
-
}),
|
|
522
|
-
issue !== undefined ? h('span', { className: 'sls-rule__err', title: issue.error }, issue.error) : null,
|
|
523
|
-
h('button', {
|
|
524
|
-
className: 'sls-btn sls-rule__del',
|
|
525
|
-
title: '删除该条规则',
|
|
526
|
-
onClick: () => removeRule(index),
|
|
527
|
-
}, '删除'),
|
|
528
|
-
)
|
|
529
|
-
}),
|
|
530
|
-
h('div', { className: 'sls-row' },
|
|
531
|
-
h('button', { className: 'sls-btn', onClick: addRule }, '添加规则'),
|
|
532
|
-
),
|
|
533
|
-
),
|
|
534
|
-
h('div', { className: 'sls-row' },
|
|
535
|
-
h('button', { className: 'sls-btn', disabled: busy, onClick: addEntry }, '添加客户端'),
|
|
536
|
-
h('span', { style: { flex: 1 } }),
|
|
537
|
-
dirty ? h('span', { className: 'sls-hint' }, '有未保存改动') : null,
|
|
538
|
-
h('button', {
|
|
539
|
-
className: 'sls-btn sls-btn--primary',
|
|
540
|
-
disabled: busy || !dirty,
|
|
541
|
-
onClick: () => void save(),
|
|
542
|
-
}, '保存'),
|
|
543
|
-
),
|
|
544
|
-
notice !== null ? h('div', { className: 'sls-hint' }, notice) : null,
|
|
545
|
-
)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
254
|
// ── tool.call.toolview 卡片(key 'shell')──────────────────────────────
|
|
549
255
|
// 官方 BashRow/terminalCardModel 的 shellCall 白名单只认 bash|pwsh,无法复用,
|
|
550
256
|
// 模型派生自带:数据自 argsRaw + 结果文本尾部退出标记派生(官方 block 无
|
|
@@ -616,11 +322,25 @@ window.__ModuleLoader__.load({
|
|
|
616
322
|
}
|
|
617
323
|
// LOGIC-END hasSpillNotice
|
|
618
324
|
|
|
325
|
+
// 执行事实标记(host renderResult 落,格式见 render.mjs):卡片徽章第一优先级,
|
|
326
|
+
// 配置事后变更/跨宿主查看不溯往改写历史调用
|
|
327
|
+
// LOGIC-BEGIN parseShellMark
|
|
328
|
+
function parseShellMark(text) {
|
|
329
|
+
return /\[shell: ([^\]\n]+)\]/.exec(text)?.[1]
|
|
330
|
+
}
|
|
331
|
+
// LOGIC-END parseShellMark
|
|
332
|
+
|
|
333
|
+
// LOGIC-BEGIN stripShellMark
|
|
334
|
+
function stripShellMark(text) {
|
|
335
|
+
return text.split('\n').filter((line) => !/^\[shell: [^\]]+\]$/.test(line)).join('\n')
|
|
336
|
+
}
|
|
337
|
+
// LOGIC-END stripShellMark
|
|
338
|
+
|
|
619
339
|
// block → 卡片模型(官方 terminalCardModel 同构,数据源 argsRaw + content 文本)。
|
|
620
340
|
// generic = 后台 ack / isError / 溢出预览 / persistent 形(无 description,
|
|
621
341
|
// 官方 shellCall persistent→generic 同构),交回退行;terminal = 全量卡。
|
|
622
342
|
// LOGIC-BEGIN shellCardModel
|
|
623
|
-
function shellCardModel(block, sessionCwd) {
|
|
343
|
+
function shellCardModel(block, sessionCwd, toolKey) {
|
|
624
344
|
const settled = 'kind' in block
|
|
625
345
|
const call = settled ? block.call : block
|
|
626
346
|
let args = null
|
|
@@ -632,37 +352,45 @@ window.__ModuleLoader__.load({
|
|
|
632
352
|
const description = typeof args?.description === 'string' && args.description.trim() !== '' ? args.description : undefined
|
|
633
353
|
const cwdFull = displayCwd(typeof args?.workdir === 'string' ? args.workdir : undefined, sessionCwd)
|
|
634
354
|
const cwdDir = cwdFull !== undefined ? lastSegment(cwdFull) : undefined
|
|
635
|
-
//
|
|
636
|
-
|
|
355
|
+
// 官方钉死客户端工具(bash/pwsh)徽章即工具名:其输出无标记、参数无 shell
|
|
356
|
+
// 字段,落默认读数会误标(官方 pwsh 调用显示当前默认客户端的事故根因)
|
|
357
|
+
const PINNED_CLIENT = { pwsh: 'pwsh', bash: 'bash' }
|
|
358
|
+
// shell 工具徽章优先级:结果标记(执行事实)→ 显式参数 → default 读数(旧块兜底,
|
|
359
|
+
// 配置即当前解析事实);读数推导仅兜无标记历史块,不覆盖事实
|
|
360
|
+
const requested = typeof args?.shell === 'string' ? args.shell : undefined
|
|
361
|
+
const shellNameOf = (mark) => PINNED_CLIENT[toolKey] ?? mark ?? clientDisplayName(requested)
|
|
637
362
|
if (command === '') return { kind: 'generic', command: '', summary: undefined, output: null, running: !settled, alert: undefined }
|
|
638
363
|
|
|
639
364
|
if (!settled) {
|
|
640
365
|
const startedAt = typeof block.time === 'number' ? block.time : undefined
|
|
641
|
-
// 运行中一律全量卡:阻塞时展开即可见命令与时长,不落无命令的 generic
|
|
642
|
-
|
|
366
|
+
// 运行中一律全量卡:阻塞时展开即可见命令与时长,不落无命令的 generic 回退行;
|
|
367
|
+
// 无结果文本,徽章按读数推导,落定后由标记校正
|
|
368
|
+
return { kind: 'terminal', status: 'running', command, description, cwdDir, cwdFull, shellName: shellNameOf(undefined), output: undefined, exitCode: undefined, signal: undefined, code: undefined, startedAt }
|
|
643
369
|
}
|
|
644
370
|
|
|
645
371
|
const contentText = (block.content ?? []).map((part) => (part.type === 'text' ? part.text : '')).filter((text) => text !== '').join('\n')
|
|
372
|
+
const intercepted = toolKey === 'pwsh' && (block.content ?? []).some((part) => part.type === 'shell-select-takeover' && part.tool === 'pwsh')
|
|
373
|
+
const shellName = shellNameOf(parseShellMark(contentText))
|
|
374
|
+
const outputText = stripShellMark(contentText)
|
|
646
375
|
// 后台 ack:独立卡呈现命令全文与任务号徽标,ack 原文作输出(不再落 generic 简版行)
|
|
647
376
|
if (args.run_in_background === true) {
|
|
648
377
|
const jobId = /started background job (\S+)/.exec(contentText)?.[1]
|
|
649
|
-
return { kind: 'background', status: 'background', command, description, cwdDir, cwdFull, shellName, output:
|
|
378
|
+
return { kind: 'background', status: 'background', command, description, cwdDir, cwdFull, shellName, intercepted, output: outputText, jobId }
|
|
650
379
|
}
|
|
651
380
|
// 空结果落 generic:官方 singleResultText 无文本即回通用卡,避免空输出伪 done 终端卡;
|
|
652
381
|
// isError/error 块带 alert(错误红点语义),其余(空输出/无描述/截断)中性无状态点
|
|
653
|
-
if (
|
|
382
|
+
if (outputText === '' || block.isError === true || block.error !== undefined || description === undefined || hasSpillNotice(contentText)) {
|
|
654
383
|
const alert = block.isError === true || block.error !== undefined ? 'error' : undefined
|
|
655
|
-
return { kind: 'generic', command, summary: description, output:
|
|
384
|
+
return { kind: 'generic', command, summary: description, output: outputText, running: false, alert, intercepted }
|
|
656
385
|
}
|
|
657
|
-
const tail = parseExitTail(
|
|
386
|
+
const tail = parseExitTail(outputText)
|
|
658
387
|
const status = tail.signal !== undefined ? 'signaled' : tail.exitCode !== 0 ? 'failed' : 'done'
|
|
659
|
-
return { kind: 'terminal', status, command, description, cwdDir, cwdFull, shellName, output: tail.output, exitCode: tail.exitCode, signal: tail.signal, code: undefined }
|
|
388
|
+
return { kind: 'terminal', status, command, description, cwdDir, cwdFull, shellName, intercepted, output: tail.output, exitCode: tail.exitCode, signal: tail.signal, code: undefined }
|
|
660
389
|
}
|
|
661
390
|
// LOGIC-END shellCardModel
|
|
662
391
|
|
|
663
|
-
// 官方
|
|
664
|
-
//
|
|
665
|
-
// 红点叠 icon 之上(掩盖 icon,官方状态点语义)
|
|
392
|
+
// 官方 ToolRow leading 同构:行首仅 icon;错误/进行中状态红点叠 icon 之上
|
|
393
|
+
// (掩盖 icon,官方状态点语义)
|
|
666
394
|
function leadingStack({ status, icons }) {
|
|
667
395
|
const failed = status === 'failed' || status === 'signaled' || status === 'generic-error'
|
|
668
396
|
const dot = failed ? icons.StateDot({ state: 'error' }) : status === 'generic-warn' ? icons.StateDot({ state: 'warning' }) : status === 'running' ? icons.StateDot({ state: 'ongoing' }) : null
|
|
@@ -770,11 +498,11 @@ window.__ModuleLoader__.load({
|
|
|
770
498
|
},
|
|
771
499
|
h('span', { className: 'sls-tv__lead' },
|
|
772
500
|
leadingStack({ status, icons: TOOLVIEW_ICONS }),
|
|
773
|
-
expandable ? h('span', { className: 'sls-tv__chev', 'data-open': open ? '1' : '0', style: { display: 'inline-flex', transform: open ? 'rotate(-90deg)' : 'none' } }, TOOLVIEW_ICONS.IconChevron({ size: 14 })) : null,
|
|
774
501
|
),
|
|
775
502
|
h('span', { className: 'sls-tv__title' }, 'Shell'),
|
|
776
503
|
summary !== '' ? h('span', { className: 'sls-tv__sep', 'aria-hidden': true }) : null,
|
|
777
504
|
summary !== '' ? h('span', { className: 'sls-tv__sum' + (status === 'generic-error' ? ' sls-tv__sum--err' : '') }, summary) : null,
|
|
505
|
+
model.intercepted ? h(TakeoverBadge, { en }) : null,
|
|
778
506
|
status === 'running' ? h('span', { className: 'sls-tv__pulse' }, en ? 'running…' : '运行中…') : null,
|
|
779
507
|
),
|
|
780
508
|
open && expandable ? h('pre', { className: 'sls-tv__out', style: { border: '1px solid rgba(128,128,128,.28)', borderRadius: 8 } }, model.command + (model.output !== null && model.output !== '' ? `\n${model.output}` : '')) : null,
|
|
@@ -782,6 +510,10 @@ window.__ModuleLoader__.load({
|
|
|
782
510
|
)
|
|
783
511
|
}
|
|
784
512
|
|
|
513
|
+
function TakeoverBadge({ en }) {
|
|
514
|
+
return h('span', { className: 'sls-tv__takeover', title: en ? 'Direct pwsh call handled by shell-select' : '直接 pwsh 调用由 shell-select 接管' }, en ? 'Intercepted' : '插件接管')
|
|
515
|
+
}
|
|
516
|
+
|
|
785
517
|
function CommandBody({ command }) {
|
|
786
518
|
const lines = command === '' ? [''] : command.split('\n')
|
|
787
519
|
const width = String(lines.length).length
|
|
@@ -809,6 +541,7 @@ window.__ModuleLoader__.load({
|
|
|
809
541
|
),
|
|
810
542
|
model.cwdDir !== undefined ? h('span', { className: 'sls-tv__cwd', title: 'pwd: ' + (model.cwdFull ?? model.cwdDir) }, model.cwdDir) : null,
|
|
811
543
|
model.shellName !== undefined ? h('span', { className: 'sls-tv__badge', title: en ? 'Shell client' : 'Shell 客户端' }, model.shellName) : null,
|
|
544
|
+
model.intercepted ? h(TakeoverBadge, { en }) : null,
|
|
812
545
|
h('span', { className: 'sls-tv__sp' }),
|
|
813
546
|
pill !== undefined ? h('span', { className: 'sls-tv__pill' + (meta.pillTone === 'bg' ? ' sls-tv__pill--bg' : '') }, pill) : null,
|
|
814
547
|
h('span', { className: 'sls-tv__copy' },
|
|
@@ -833,9 +566,9 @@ window.__ModuleLoader__.load({
|
|
|
833
566
|
}
|
|
834
567
|
|
|
835
568
|
function ShellToolRow(props) {
|
|
836
|
-
const { block, cwd, inspect } = props
|
|
569
|
+
const { block, cwd, inspect, toolKey } = props
|
|
837
570
|
const en = detectEnglish()
|
|
838
|
-
const model = shellCardModel(block, cwd)
|
|
571
|
+
const model = shellCardModel(block, cwd, toolKey)
|
|
839
572
|
// hooks 恒序:必须在任何提前 return 之前完整执行(真实 React 的 hooks
|
|
840
573
|
// 链表按调用序对位,generic 行与 terminal 行的提前 return 分叉会让
|
|
841
574
|
// 后续 hook 错位,行组件树被整棵卸载——卡片全消失事故根因)
|
|
@@ -847,7 +580,6 @@ window.__ModuleLoader__.load({
|
|
|
847
580
|
return () => { disposed = true }
|
|
848
581
|
}, [])
|
|
849
582
|
if (model.kind === 'generic') return h(GenericShellRow, { model, inspect, en })
|
|
850
|
-
const expandable = true
|
|
851
583
|
const srStatus = statusTextOf(model.status, en)
|
|
852
584
|
const summary = model.description !== undefined ? model.description.split('\n')[0] : (en ? '(no description)' : '(无描述)')
|
|
853
585
|
const failed = model.status === 'failed' || model.status === 'signaled'
|
|
@@ -857,7 +589,6 @@ window.__ModuleLoader__.load({
|
|
|
857
589
|
role: 'button',
|
|
858
590
|
tabIndex: 0,
|
|
859
591
|
'aria-expanded': open,
|
|
860
|
-
'data-open': open ? '1' : '0',
|
|
861
592
|
onClick: () => setOpen((value) => !value),
|
|
862
593
|
onKeyDown: (event) => {
|
|
863
594
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
@@ -868,12 +599,12 @@ window.__ModuleLoader__.load({
|
|
|
868
599
|
},
|
|
869
600
|
h('span', { className: 'sls-tv__lead' },
|
|
870
601
|
leadingStack({ status: model.status, icons: TOOLVIEW_ICONS }),
|
|
871
|
-
h('span', { className: 'sls-tv__chev', style: { display: 'inline-flex', transform: open ? 'rotate(-90deg)' : 'none' } }, TOOLVIEW_ICONS.IconChevron({ size: 14 })),
|
|
872
602
|
),
|
|
873
603
|
srStatus !== null ? h('span', { className: 'sls-tv__sr' }, srStatus) : null,
|
|
874
604
|
h('span', { className: 'sls-tv__title' }, 'Shell'),
|
|
875
605
|
h('span', { className: 'sls-tv__sep', 'aria-hidden': true }),
|
|
876
606
|
h('span', { className: 'sls-tv__sum' + (failed ? ' sls-tv__sum--err' : '') }, summary),
|
|
607
|
+
model.intercepted ? h(TakeoverBadge, { en }) : null,
|
|
877
608
|
model.status === 'running' ? h(RunningDuration, { startedAt: model.startedAt, en }) : null,
|
|
878
609
|
model.status === 'background' && model.jobId !== undefined
|
|
879
610
|
? h('span', { className: 'sls-tv__badge', title: en ? 'Background job' : '后台任务' }, en ? `bg ${model.jobId}` : `后台 ${model.jobId}`)
|
|
@@ -886,6 +617,7 @@ window.__ModuleLoader__.load({
|
|
|
886
617
|
return {
|
|
887
618
|
inject: ['slots'],
|
|
888
619
|
apply(ctx) {
|
|
620
|
+
installPathRescue(ctx)
|
|
889
621
|
ctx.effect(() => {
|
|
890
622
|
const style = document.createElement('style')
|
|
891
623
|
// 自带 data-plugin:缺失时宿主 claimStyles 会误收,插件 HMR 重建即误删
|
|
@@ -894,22 +626,17 @@ window.__ModuleLoader__.load({
|
|
|
894
626
|
document.head.appendChild(style)
|
|
895
627
|
return () => style.remove()
|
|
896
628
|
}, 'shell-select styles')
|
|
897
|
-
ctx.slots.inject('settings.section', () =>
|
|
898
|
-
ctx.slots.register(
|
|
899
|
-
{ name: 'settings.section', id: 'shell-select', order: 82, label: 'Shell 管理' },
|
|
900
|
-
() => React.createElement(ShellSelectApp),
|
|
901
|
-
))
|
|
902
629
|
// 替换 shell 族工具行渲染:keyed hit 优先于 GenericToolCard 兜底;
|
|
903
630
|
// priority -1 阴影官方同 key 注册(低值先渲染)。pwsh/bash 为官方工具
|
|
904
|
-
// 名(死态窗口 guard 代挂官方 tool-pwsh
|
|
905
|
-
//
|
|
906
|
-
//
|
|
631
|
+
// 名(死态窗口 guard 代挂官方 tool-pwsh 时,其调用行同样获得增强卡);
|
|
632
|
+
// key 以 toolKey 注入卡片,官方钉死客户端工具的徽章按工具名钉死,
|
|
633
|
+
// 不落默认客户端读数。
|
|
907
634
|
const TOOLVIEW_KEYS = ['shell', 'pwsh', 'bash']
|
|
908
635
|
for (const toolKey of TOOLVIEW_KEYS) {
|
|
909
636
|
ctx.slots.inject('tool.call.toolview', () =>
|
|
910
637
|
ctx.slots.register(
|
|
911
638
|
{ name: 'tool.call.toolview', key: toolKey, priority: -1 },
|
|
912
|
-
(props) => React.createElement(ShellToolRow, props),
|
|
639
|
+
(props) => React.createElement(ShellToolRow, { ...props, toolKey }),
|
|
913
640
|
))
|
|
914
641
|
}
|
|
915
642
|
},
|