dsh-code-server-app 0.3.58 → 0.3.63
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-Apache-2.0.txt +201 -0
- package/README.en.md +215 -98
- package/README.md +186 -94
- package/THIRD_PARTY_NOTICES.md +60 -0
- package/assets/extensions/dshcs-editor-bridge/extension.js +192 -222
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +43 -39
- package/assets/extensions/dshcs-editor-bridge/lib/fim-completion.js +264 -0
- package/assets/extensions/dshcs-editor-bridge/package.json +2 -2
- package/lib/bridge-session.mjs +55 -5
- package/lib/bridge.mjs +14 -7
- package/lib/client.js +1061 -90
- package/lib/fim-adapter.mjs +603 -0
- package/lib/index.js +366 -205
- package/package.json +21 -41
- package/vendor/VENDOR.json +5 -5
- package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +0 -416
- package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +0 -22
- package/assets/extensions/dshcs-editor-bridge/webview/src/app.jsx +0 -242
- package/assets/extensions/dshcs-editor-bridge/webview/src/approval.jsx +0 -61
- package/assets/extensions/dshcs-editor-bridge/webview/src/official-tokens.css +0 -576
- package/assets/extensions/dshcs-editor-bridge/webview/src/panel.css +0 -380
- package/assets/extensions/dshcs-editor-bridge/webview/src/thread.jsx +0 -160
- package/assets/extensions/dshcs-editor-bridge/webview/thread.css +0 -1
- package/assets/extensions/dshcs-editor-bridge/webview/thread.js +0 -465
package/lib/client.js
CHANGED
|
@@ -660,6 +660,18 @@ function normalizeClaimExtensions(text) {
|
|
|
660
660
|
return seen.join(';')
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
+
/**
|
|
664
|
+
* FIM 停顿毫秒数的归一化(0.3.62):与宿主 lib/fim-adapter.mjs 的 `clampDebounce` **同语义**。
|
|
665
|
+
* 客户端也夹一次的理由:输入框里可以是任何东西(空、负数、10^9),用户在点保存之前就该看到
|
|
666
|
+
* "保存后会变成什么",而不是保存完发现值被宿主悄悄改掉 —— 这与认领类型那行显示"实际规则"同一考虑。
|
|
667
|
+
*/
|
|
668
|
+
function normalizeFimDebounce(value) {
|
|
669
|
+
if (value === undefined || value === null || value === '') return 250
|
|
670
|
+
var n = typeof value === 'number' ? value : Number(value)
|
|
671
|
+
if (!isFinite(n)) return 250
|
|
672
|
+
return Math.min(3000, Math.max(100, Math.round(n)))
|
|
673
|
+
}
|
|
674
|
+
|
|
663
675
|
/**
|
|
664
676
|
* 解析策略文本。
|
|
665
677
|
* @param text - 归一化前或后的文本都接受。
|
|
@@ -1325,6 +1337,20 @@ let React = require('react')
|
|
|
1325
1337
|
React.createElement('span', { className: 'dshcs-hint' }, props.children)
|
|
1326
1338
|
)
|
|
1327
1339
|
}
|
|
1340
|
+
/** 数字输入(0.3.62 起,用于"停顿毫秒数"这类有上下限的整数)。
|
|
1341
|
+
* 为什么不用文本框:用户在点保存**之前**就该看到 min/max 这层约束;真正的夹取在
|
|
1342
|
+
* 保存时 `normalizeFimDebounce`(与宿主 clampDebounce 同语义),两者一致才不会"保存后被悄悄改掉"。 */
|
|
1343
|
+
function csNumber(props) {
|
|
1344
|
+
return React.createElement('label', { className: 'dshcs-text' },
|
|
1345
|
+
React.createElement('input', {
|
|
1346
|
+
type: 'number',
|
|
1347
|
+
value: props.value === undefined || props.value === null ? '' : String(props.value),
|
|
1348
|
+
min: props.min, max: props.max, step: props.step === undefined ? 10 : props.step,
|
|
1349
|
+
disabled: props.disabled === true, spellCheck: false, autoComplete: 'off',
|
|
1350
|
+
onChange: function (event) { props.onChange(event.target.value) },
|
|
1351
|
+
})
|
|
1352
|
+
)
|
|
1353
|
+
}
|
|
1328
1354
|
/** 多行文本输入(0.3.52 起,用于"认领类型"这类清单)。
|
|
1329
1355
|
* 为什么不是单行 input:0.3.51 之后默认值有 98 条排除项(≈700 字符),单行框只能看到开头,
|
|
1330
1356
|
* 剩下全靠横向滚动 —— 既看不全也改不动。这里改用 textarea,并**按内容自动长高**(上限 12 行),
|
|
@@ -1423,6 +1449,10 @@ let React = require('react')
|
|
|
1423
1449
|
keepResident: value.keepResident !== false,
|
|
1424
1450
|
claimExtensions: typeof value.claimExtensions === 'string' ? value.claimExtensions : DEFAULT_CLAIM_EXTENSIONS,
|
|
1425
1451
|
fullscreenOnOpen: value.fullscreenOnOpen !== false,
|
|
1452
|
+
fim: value.fim === true,
|
|
1453
|
+
fimDebounceMs: normalizeFimDebounce(value.fimDebounceMs),
|
|
1454
|
+
fimMultiline: value.fimMultiline !== false,
|
|
1455
|
+
fimDisableGlobs: typeof value.fimDisableGlobs === 'string' ? value.fimDisableGlobs : '',
|
|
1426
1456
|
}
|
|
1427
1457
|
// ---- 旧版 DSH(无右侧栏服务):本页是唯一的提示出口,不显示任何设置项 ----
|
|
1428
1458
|
if (liveStore != null && liveStore.sidebarUi === 'legacy') {
|
|
@@ -1447,9 +1477,17 @@ let React = require('react')
|
|
|
1447
1477
|
var overriddenKeep = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'keepResident')
|
|
1448
1478
|
var overriddenClaim = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'claimExtensions')
|
|
1449
1479
|
var overriddenFullscreen = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fullscreenOnOpen')
|
|
1480
|
+
var overriddenFim = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fim')
|
|
1481
|
+
var overriddenFimDebounce = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimDebounceMs')
|
|
1482
|
+
var overriddenFimMultiline = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimMultiline')
|
|
1483
|
+
var overriddenFimGlobs = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimDisableGlobs')
|
|
1450
1484
|
var dirty = draft !== null && (draft.keepResident !== loaded.keepResident
|
|
1451
1485
|
|| normalizeClaimExtensions(draft.claimExtensions) !== normalizeClaimExtensions(loaded.claimExtensions)
|
|
1452
|
-
|| draft.fullscreenOnOpen !== loaded.fullscreenOnOpen
|
|
1486
|
+
|| draft.fullscreenOnOpen !== loaded.fullscreenOnOpen
|
|
1487
|
+
|| draft.fim !== loaded.fim
|
|
1488
|
+
|| normalizeFimDebounce(draft.fimDebounceMs) !== normalizeFimDebounce(loaded.fimDebounceMs)
|
|
1489
|
+
|| draft.fimMultiline !== loaded.fimMultiline
|
|
1490
|
+
|| draft.fimDisableGlobs !== loaded.fimDisableGlobs)
|
|
1453
1491
|
var saveDisabled = !dirty || saving
|
|
1454
1492
|
var state = {
|
|
1455
1493
|
available: true,
|
|
@@ -1473,6 +1511,10 @@ let React = require('react')
|
|
|
1473
1511
|
await props.scope.set('keepResident', d.keepResident === true)
|
|
1474
1512
|
await props.scope.set('claimExtensions', normalizeClaimExtensions(d.claimExtensions))
|
|
1475
1513
|
await props.scope.set('fullscreenOnOpen', d.fullscreenOnOpen === true)
|
|
1514
|
+
await props.scope.set('fim', d.fim === true)
|
|
1515
|
+
await props.scope.set('fimDebounceMs', normalizeFimDebounce(d.fimDebounceMs))
|
|
1516
|
+
await props.scope.set('fimMultiline', d.fimMultiline === true)
|
|
1517
|
+
await props.scope.set('fimDisableGlobs', typeof d.fimDisableGlobs === 'string' ? d.fimDisableGlobs : '')
|
|
1476
1518
|
setDraft(null)
|
|
1477
1519
|
syncStatusToStore()
|
|
1478
1520
|
} catch (e) {
|
|
@@ -1490,10 +1532,18 @@ let React = require('react')
|
|
|
1490
1532
|
await props.scope.unset('keepResident')
|
|
1491
1533
|
await props.scope.unset('claimExtensions')
|
|
1492
1534
|
await props.scope.unset('fullscreenOnOpen')
|
|
1535
|
+
await props.scope.unset('fim')
|
|
1536
|
+
await props.scope.unset('fimDebounceMs')
|
|
1537
|
+
await props.scope.unset('fimMultiline')
|
|
1538
|
+
await props.scope.unset('fimDisableGlobs')
|
|
1493
1539
|
} else {
|
|
1494
1540
|
await props.scope.set('keepResident', value.keepResident === undefined || value.keepResident === null ? true : value.keepResident)
|
|
1495
1541
|
await props.scope.set('claimExtensions', typeof value.claimExtensions === 'string' ? normalizeClaimExtensions(value.claimExtensions) : DEFAULT_CLAIM_EXTENSIONS)
|
|
1496
1542
|
await props.scope.set('fullscreenOnOpen', value.fullscreenOnOpen === undefined || value.fullscreenOnOpen === null ? true : value.fullscreenOnOpen)
|
|
1543
|
+
await props.scope.set('fim', value.fim === true)
|
|
1544
|
+
await props.scope.set('fimDebounceMs', normalizeFimDebounce(value.fimDebounceMs))
|
|
1545
|
+
await props.scope.set('fimMultiline', value.fimMultiline !== false)
|
|
1546
|
+
await props.scope.set('fimDisableGlobs', typeof value.fimDisableGlobs === 'string' ? value.fimDisableGlobs : '')
|
|
1497
1547
|
}
|
|
1498
1548
|
setDraft(null)
|
|
1499
1549
|
syncStatusToStore()
|
|
@@ -1503,8 +1553,8 @@ let React = require('react')
|
|
|
1503
1553
|
setSaving(false)
|
|
1504
1554
|
}
|
|
1505
1555
|
var fields = [
|
|
1506
|
-
//
|
|
1507
|
-
// (入口在右侧栏「开始」页的 guide 入口框;诊断看 host
|
|
1556
|
+
// 一行一个可改设置(0.2.11 起):认领类型 + 打开即全屏 + FIM 补全(0.3.61,含 0.3.62 的三个子项)
|
|
1557
|
+
// + 后台常驻。入口/依赖安装/环境检测三行已移除(入口在右侧栏「开始」页的 guide 入口框;诊断看 host 日志)。
|
|
1508
1558
|
React.createElement('div', { className: 'dshcs-field' },
|
|
1509
1559
|
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1510
1560
|
React.createElement('span', { className: 'dshcs-fieldLabel' }, '认领类型(按扩展名,分号分隔)'),
|
|
@@ -1565,6 +1615,92 @@ let React = require('react')
|
|
|
1565
1615
|
'只影响"打开那一刻":切走再切回、再次打开文件 tab 会重新切全屏;'
|
|
1566
1616
|
+ 'DSH 未向插件开放模式接口,本项由点击右侧栏自身的「全屏」按钮实现。')
|
|
1567
1617
|
),
|
|
1618
|
+
React.createElement('div', { className: 'dshcs-field' },
|
|
1619
|
+
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1620
|
+
React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM 补全(实验性)'),
|
|
1621
|
+
overriddenFim === true
|
|
1622
|
+
? React.createElement(csBadges, {
|
|
1623
|
+
overridden: true, disabled: snapshot.writable !== true,
|
|
1624
|
+
overriddenLabel: '已覆盖', resetLabel: '恢复默认',
|
|
1625
|
+
onReset: function () { doReset() },
|
|
1626
|
+
})
|
|
1627
|
+
: null
|
|
1628
|
+
),
|
|
1629
|
+
React.createElement(csCheck, {
|
|
1630
|
+
checked: draft !== null ? draft.fim : loaded.fim,
|
|
1631
|
+
disabled: snapshot.writable !== true,
|
|
1632
|
+
onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fim: v === true }) }); setFailed(false) },
|
|
1633
|
+
}, '开启后,在编辑器里打字停顿约 0.3 秒会向模型要一次「续写」,以灰字显示在光标后:Tab 接受、Esc 丢弃。默认关闭'),
|
|
1634
|
+
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
|
|
1635
|
+
'实验性功能,默认关闭。三点须知:'
|
|
1636
|
+
+ '① 它是本插件**唯一会把内容发出去**的能力 —— 每次补全会把光标附近的一小段代码(有上限)交给模型,'
|
|
1637
|
+
+ '桥的其余部分只读编辑器状态、不外发内容;'
|
|
1638
|
+
+ '② 这条调用不计入 DSH 自己的 token 计量(它不是一次会话请求),'
|
|
1639
|
+
+ '用量只在 IDE 状态栏的 DSH 项上是可见的;'
|
|
1640
|
+
+ '③ 模型偶尔会在"并不需要补"的位置硬凑一段,忽略即可(Esc 或继续打字都会让它消失)。'
|
|
1641
|
+
+ '关掉本项后扩展会立刻注销补全 provider,不再发出任何补全请求。')
|
|
1642
|
+
),
|
|
1643
|
+
React.createElement('div', { className: 'dshcs-field' },
|
|
1644
|
+
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1645
|
+
React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 停顿毫秒数(停下多久才请求)'),
|
|
1646
|
+
overriddenFimDebounce === true
|
|
1647
|
+
? React.createElement(csBadges, {
|
|
1648
|
+
overridden: true, disabled: snapshot.writable !== true,
|
|
1649
|
+
overriddenLabel: '已覆盖', resetLabel: '恢复默认',
|
|
1650
|
+
onReset: function () { doReset() },
|
|
1651
|
+
})
|
|
1652
|
+
: null
|
|
1653
|
+
),
|
|
1654
|
+
React.createElement(csNumber, {
|
|
1655
|
+
value: draft !== null ? draft.fimDebounceMs : loaded.fimDebounceMs,
|
|
1656
|
+
min: 100, max: 3000, step: 50, disabled: snapshot.writable !== true,
|
|
1657
|
+
onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimDebounceMs: v }) }); setFailed(false) },
|
|
1658
|
+
}),
|
|
1659
|
+
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
|
|
1660
|
+
'有效范围 100–3000ms,保存时按它夹取(与宿主同一套规则)。实测这条端点往返 112–416ms,'
|
|
1661
|
+
+ '所以停顿基本就是"感知到的延迟":太小会在打字过程中反复触发,太大要等很久才出现灰字。')
|
|
1662
|
+
),
|
|
1663
|
+
React.createElement('div', { className: 'dshcs-field' },
|
|
1664
|
+
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1665
|
+
React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 允许多行补全'),
|
|
1666
|
+
overriddenFimMultiline === true
|
|
1667
|
+
? React.createElement(csBadges, {
|
|
1668
|
+
overridden: true, disabled: snapshot.writable !== true,
|
|
1669
|
+
overriddenLabel: '已覆盖', resetLabel: '恢复默认',
|
|
1670
|
+
onReset: function () { doReset() },
|
|
1671
|
+
})
|
|
1672
|
+
: null
|
|
1673
|
+
),
|
|
1674
|
+
React.createElement(csCheck, {
|
|
1675
|
+
checked: draft !== null ? draft.fimMultiline : loaded.fimMultiline,
|
|
1676
|
+
disabled: snapshot.writable !== true,
|
|
1677
|
+
onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimMultiline: v === true }) }); setFailed(false) },
|
|
1678
|
+
}, '允许补出多行(关掉后只取第一行;首行为空 = 这次不补)'),
|
|
1679
|
+
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
|
|
1680
|
+
'默认开。实测模型在"并不需要补"的位置会硬凑一段,多行会把这种噪声放大 —— 想更克制就关掉它。')
|
|
1681
|
+
),
|
|
1682
|
+
React.createElement('div', { className: 'dshcs-field' },
|
|
1683
|
+
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1684
|
+
React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 按 glob 禁用(分号或换行分隔)'),
|
|
1685
|
+
overriddenFimGlobs === true
|
|
1686
|
+
? React.createElement(csBadges, {
|
|
1687
|
+
overridden: true, disabled: snapshot.writable !== true,
|
|
1688
|
+
overriddenLabel: '已覆盖', resetLabel: '恢复默认',
|
|
1689
|
+
onReset: function () { doReset() },
|
|
1690
|
+
})
|
|
1691
|
+
: null
|
|
1692
|
+
),
|
|
1693
|
+
React.createElement(csTextarea, {
|
|
1694
|
+
value: draft !== null ? draft.fimDisableGlobs : loaded.fimDisableGlobs,
|
|
1695
|
+
disabled: snapshot.writable !== true,
|
|
1696
|
+
placeholder: '例如 *.md;**/dist/**;vendor/**',
|
|
1697
|
+
onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimDisableGlobs: v }) }); setFailed(false) },
|
|
1698
|
+
}),
|
|
1699
|
+
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
|
|
1700
|
+
'留空 = 不禁用。语法:`*` 不跨目录、`**` 跨目录、不含 `/` 的模式只匹配文件名、`/` 结尾视作 `/**`。'
|
|
1701
|
+
+ '例:`*.md`(所有 Markdown)、`vendor/**`、`**/dist/**`、`**/*.min.js`。'
|
|
1702
|
+
+ '两边都会判:扩展侧先判(根本不发请求),宿主侧再判一遍。')
|
|
1703
|
+
),
|
|
1568
1704
|
React.createElement('div', { className: 'dshcs-field' },
|
|
1569
1705
|
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1570
1706
|
React.createElement('span', { className: 'dshcs-fieldLabel' }, '后台常驻(切标签不重载)'),
|
|
@@ -1638,20 +1774,52 @@ let React = require('react')
|
|
|
1638
1774
|
return React.createElement(csSettingsPage, Object.assign({}, props, { seat: 'bundle-config' }))
|
|
1639
1775
|
}
|
|
1640
1776
|
|
|
1641
|
-
// ---------- 「问 DSH」对话框(0.3.24)----------
|
|
1642
|
-
//
|
|
1777
|
+
// ---------- 「问 DSH」对话框(0.3.24;0.3.59 起面板本体就是这份源码)----------
|
|
1778
|
+
// 对话**不在编辑器的侧栏/tab 里**,而是作为一个浮在 DSH 页面上的对话框(用户的原话:
|
|
1779
|
+
// "对话不要用侧边栏,还是改成对话框形式")。这里就是那个对话框:**外壳**(标题栏 / 拖动 / 关闭)
|
|
1780
|
+
// + **面板本体**(对话流 / 授权卡片 / 输入框)都在本文件里。
|
|
1643
1781
|
// - 定时问 host `/ask/state?rev=N`,没变就只回一个数字(不重传对话流);
|
|
1644
|
-
// - 打开时懒加载面板产物(`/ask/bundle` → thread.js / thread.css 文本)并注入:
|
|
1645
|
-
// CSS 进一个 <style>(面板样式只在 `.dshcs-panel` 里生效,不碰 DSH 自己的 body),
|
|
1646
|
-
// JS 进一个 <script>(它调用 acquireVsCodeApi —— 这里给它一个路由到本模块的替身);
|
|
1647
1782
|
// - 面板里的 ask / approve / close 消息 → host 的 `/ask/send|approve|close`。
|
|
1783
|
+
//
|
|
1784
|
+
// 0.3.24–0.3.58 的面板本体是**另一个构建产物**:host 读盘 webview/thread.js + thread.css 交给
|
|
1785
|
+
// `/ask/bundle`,这里把 CSS 塞进 <style>、JS 塞进 <script> 执行(还得先给它一个假的
|
|
1786
|
+
// acquireVsCodeApi)。那套机制唯一的存在理由,是"面板要跟 DSH 界面用同一个官方渲染器,
|
|
1787
|
+
// 而编辑器 webview 里没有 DSH 的模块表" —— 可对话框本来就跑在 **DSH 页面里**:
|
|
1788
|
+
// 壳的模块表(dsh-web-frontend 的 staticModules)已经冻结了
|
|
1789
|
+
// react / react/jsx-runtime / react-dom / react-dom/client / @deepseek-ai/dsh-client-ui-primitives / …
|
|
1790
|
+
// 所以 0.3.59 直接 require 它们,面板就是普通源码:
|
|
1791
|
+
// · 一次打包都不需要(改这个文件即改面板);
|
|
1792
|
+
// · 用的是**当前页面这一份**渲染器 ⇒ 不可能再有"面板内置 alpha.1、而界面是 alpha.2"
|
|
1793
|
+
// 的排版差异(那正是老 app.jsx 要专门显示一行版本告警的原因);
|
|
1794
|
+
// · 代码高亮走 DSH 自己的 shiki 懒加载语法集(产物里只能带 typescript/shellscript/json 三套);
|
|
1795
|
+
// · 公式的 KaTeX 样式与 --dsw-* 设计令牌都由页面提供(official-tokens.css 那 28KB 副本不再需要)。
|
|
1796
|
+
// 拿不到模块表(异常宿主)时**降级而不是白屏**:正文退成 <pre>、按钮退成原生 button,
|
|
1797
|
+
// 并在控制台说清原因 —— 空白面板是最难查的一种失败。
|
|
1648
1798
|
var ASK_POLL_MS = 900
|
|
1649
1799
|
var ask = {
|
|
1650
1800
|
rev: -1,
|
|
1651
1801
|
open: false,
|
|
1652
1802
|
mounted: false,
|
|
1653
1803
|
pending: null,
|
|
1654
|
-
el: null, body: null, title: null, timer: null,
|
|
1804
|
+
el: null, body: null, title: null, timer: null,
|
|
1805
|
+
/** React 根(只在第一次打开时建一次)。 */
|
|
1806
|
+
root: null,
|
|
1807
|
+
/** 面板挂上来的"把宿主状态推进视图"函数(挂载完成前为 null)。 */
|
|
1808
|
+
push: null,
|
|
1809
|
+
/** `askPrimitives()` 的解析结果(只算一次)。 */
|
|
1810
|
+
renderer: null,
|
|
1811
|
+
/** 上一条提问的投递方式提示(宿主回话里带 delivery;用来消除"追问没反应"的错觉)。 */
|
|
1812
|
+
deliveryNote: null,
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/** 投递结果 → 状态行文案(投递方式跟 DSH 设置 `ui-conversation.busyEnter` 走,见 host 的 pickBusyEnter)。
|
|
1816
|
+
* **必须同时看 busy**:空闲时宿主也用 followup(两种方式等价),但那时消息是**立刻开新一轮**;
|
|
1817
|
+
* 只看 delivery 会说成"排到下一轮",与事实不符(实测踩到:空闲时发一条,面板却写"当前轮结束后…")。 */
|
|
1818
|
+
function askDeliveryNote(delivery, busy) {
|
|
1819
|
+
if (busy !== true) return '已发送:DSH 会立刻开始新一轮。'
|
|
1820
|
+
if (delivery === 'steer') return '已插入当前轮:它会在下一个步骤边界读到这条追问。'
|
|
1821
|
+
if (delivery === 'queue') return '已排入下一轮:当前轮结束后会作为新的一轮回答。'
|
|
1822
|
+
return '已发送,等待 DSH 处理。'
|
|
1655
1823
|
}
|
|
1656
1824
|
|
|
1657
1825
|
/** 面板 → 对话框壳的消息通道(替身 acquireVsCodeApi 把消息交到这里)。 */
|
|
@@ -1659,8 +1827,15 @@ let React = require('react')
|
|
|
1659
1827
|
if (message === null || typeof message !== 'object') return
|
|
1660
1828
|
if (message.type === 'ready') { askPush(true); return }
|
|
1661
1829
|
if (message.type === 'ask' && typeof message.text === 'string') {
|
|
1830
|
+
ask.deliveryNote = null
|
|
1662
1831
|
api('/code-server/ask/send', { text: message.text, mode: ask.pending === null ? undefined : ask.pending.mode })
|
|
1663
|
-
.then(function (result) {
|
|
1832
|
+
.then(function (result) {
|
|
1833
|
+
// 宿主按 DSH 的设置决定"排队"还是"插入当前轮";把这个结果说出来 ——
|
|
1834
|
+
// 否则用户在主界面看不到消息的那段时间里会以为它丢了(实测就是这么反馈的)。
|
|
1835
|
+
if (result != null && result.ok === true) ask.deliveryNote = askDeliveryNote(result.delivery, result.busy)
|
|
1836
|
+
ask.rev = -1
|
|
1837
|
+
askPush(true)
|
|
1838
|
+
})
|
|
1664
1839
|
.catch(function () { /* 下一趟轮询会带回状态 */ })
|
|
1665
1840
|
return
|
|
1666
1841
|
}
|
|
@@ -1677,12 +1852,15 @@ let React = require('react')
|
|
|
1677
1852
|
if (message.type === 'close') askClose()
|
|
1678
1853
|
}
|
|
1679
1854
|
|
|
1680
|
-
/** 把 host 的状态推进面板(
|
|
1855
|
+
/** 把 host 的状态推进面板(面板挂载时把自己登记到 `ask.push`)。 */
|
|
1681
1856
|
function askPush(force) {
|
|
1682
|
-
if (ask.
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1857
|
+
if (ask.push === null) {
|
|
1858
|
+
// 面板还没挂上(第一次打开时 React 挂载是异步的)→ 稍后补一次,别丢掉这份状态。
|
|
1859
|
+
if (force === true) setTimeout(function () { askPush(false) }, 120)
|
|
1860
|
+
return
|
|
1861
|
+
}
|
|
1862
|
+
// 投入方式是**客户端侧**知道的(宿主 /ask/send 的回话),宿主状态里没有它 ⇒ 随状态一起带上。
|
|
1863
|
+
ask.push(ask.pending === null ? null : Object.assign({}, ask.pending, { deliveryNote: ask.deliveryNote }))
|
|
1686
1864
|
}
|
|
1687
1865
|
|
|
1688
1866
|
/** 状态行里说一句(面板自己在 statusText 里显示;这里只兜底日志)。 */
|
|
@@ -1761,95 +1939,864 @@ let React = require('react')
|
|
|
1761
1939
|
head.addEventListener('pointerup', function () { drag = null })
|
|
1762
1940
|
}
|
|
1763
1941
|
|
|
1942
|
+
// ---- 官方部件(模块表;解析失败就降级,绝不白屏)----
|
|
1943
|
+
|
|
1764
1944
|
/**
|
|
1765
|
-
*
|
|
1945
|
+
* 从官方 primitives 里取一个部件。
|
|
1766
1946
|
*
|
|
1767
|
-
*
|
|
1768
|
-
*
|
|
1769
|
-
*
|
|
1947
|
+
* **必须函数与 memo 对象都认**:官方 `MarkdownText` 是 `React.memo(...)` 的产物 —— 它的值是一个
|
|
1948
|
+
* `{$$typeof: Symbol(react.memo), type: …}` **对象**,不是函数。0.3.59 的第一版写成
|
|
1949
|
+
* `typeof x === 'function'` 才认,于是实机上正文**整个退成了 `<pre>`**(而 `DisclosureRow` /
|
|
1950
|
+
* `Button` / 图标都是函数,照常工作,于是看起来"面板好好的")。这条只有真页面能测出来 ——
|
|
1951
|
+
* 单测的桩当时写成了函数。现在两个形状都收,桩也改成了 memo 形状。
|
|
1770
1952
|
*/
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1953
|
+
function askComponent(primitives, name) {
|
|
1954
|
+
if (primitives === null || primitives === undefined) return null
|
|
1955
|
+
var value = primitives[name]
|
|
1956
|
+
if (value === null || value === undefined) return null
|
|
1957
|
+
if (typeof value === 'function') return value
|
|
1958
|
+
// React.memo / forwardRef / lazy:对象且带 $$typeof ⇒ createElement 认它。
|
|
1959
|
+
if (typeof value === 'object' && value.$$typeof !== undefined) return value
|
|
1960
|
+
return null
|
|
1961
|
+
}
|
|
1774
1962
|
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1963
|
+
/** 面板要用的官方部件:React 根 + DSH 官方 UI primitives。
|
|
1964
|
+
*
|
|
1965
|
+
* 两个都从**壳的模块表**取(`dsh-web-frontend` 的 staticModules 冻结的那几个种子词):
|
|
1966
|
+
* · `react-dom/client` —— 我们自己的根(对话框容器是我们的 div,不能借用 DSH 的根);
|
|
1967
|
+
* · `@deepseek-ai/dsh-client-ui-primitives` —— MarkdownText / DisclosureRow / Button / 图标,
|
|
1968
|
+
* 与 DSH 界面**同一份实例**:排版、代码高亮、公式都与界面一致,而且不会版本错配。
|
|
1969
|
+
*
|
|
1970
|
+
* 取不到(异常宿主 / 未来 DSH 改了这个种子词)= 降级:正文 <pre>、按钮原生 button。
|
|
1971
|
+
* 这是刻意的:面板少一层排版也不能开不出窗。
|
|
1972
|
+
*/
|
|
1973
|
+
function askPrimitives() {
|
|
1974
|
+
if (ask.renderer === null) {
|
|
1975
|
+
var out = { createRoot: null, primitives: null, why: '' }
|
|
1976
|
+
try {
|
|
1977
|
+
out.createRoot = require('react-dom/client').createRoot
|
|
1978
|
+
} catch (e) {
|
|
1979
|
+
out.why = 'react-dom/client(' + (e !== null && e !== undefined && e.message !== undefined ? e.message : String(e)) + ')'
|
|
1980
|
+
}
|
|
1981
|
+
try {
|
|
1982
|
+
out.primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
1983
|
+
} catch (e) {
|
|
1984
|
+
out.why += (out.why === '' ? '' : '; ') + 'dsh-client-ui-primitives(' + (e !== null && e !== undefined && e.message !== undefined ? e.message : String(e)) + ')'
|
|
1985
|
+
}
|
|
1986
|
+
if (out.createRoot == null || out.primitives == null) {
|
|
1987
|
+
console.warn('[code-server] 「问 DSH」面板降级为纯文本渲染:模块表里取不到 ' + out.why)
|
|
1988
|
+
}
|
|
1989
|
+
ask.renderer = out
|
|
1785
1990
|
}
|
|
1991
|
+
return ask.renderer
|
|
1786
1992
|
}
|
|
1787
1993
|
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1994
|
+
// ---- 面板视图状态(纯函数;与 host `/ask/state` 的扁平载荷一一对应)----
|
|
1995
|
+
|
|
1996
|
+
/** 授权窗口的兜底值(毫秒);host 每趟都会给真实值。 */
|
|
1997
|
+
var ASK_HOLD_MS = 8000
|
|
1998
|
+
|
|
1999
|
+
/** 面板视图的首帧状态(形状与 askApplyPayload 的输出一致)。 */
|
|
2000
|
+
function askViewState() {
|
|
2001
|
+
return {
|
|
2002
|
+
entries: [], approvals: [], approvalHoldMs: ASK_HOLD_MS,
|
|
2003
|
+
contextText: '', statusText: '', status: 'idle', error: null,
|
|
2004
|
+
sessionId: null, available: null, threadError: null, uiVersion: null,
|
|
2005
|
+
/** 客户端侧提示:上一条提问是"排队"还是"插入当前轮"(宿主状态里没有这个字段)。 */
|
|
2006
|
+
deliveryNote: null,
|
|
1793
2007
|
}
|
|
1794
2008
|
}
|
|
1795
2009
|
|
|
1796
|
-
/**
|
|
1797
|
-
function
|
|
1798
|
-
if (
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2010
|
+
/** 把宿主一趟状态并进视图(**只认白名单字段**,宿主塞别的东西也进不来)。 */
|
|
2011
|
+
function askApplyPayload(view, payload) {
|
|
2012
|
+
if (payload === null || typeof payload !== 'object' || payload.type !== 'state') return view
|
|
2013
|
+
return {
|
|
2014
|
+
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
2015
|
+
approvals: Array.isArray(payload.approvals) ? payload.approvals : [],
|
|
2016
|
+
approvalHoldMs: Number.isSafeInteger(payload.approvalHoldMs) && payload.approvalHoldMs > 0
|
|
2017
|
+
? payload.approvalHoldMs
|
|
2018
|
+
: ASK_HOLD_MS,
|
|
2019
|
+
contextText: typeof payload.contextText === 'string' ? payload.contextText : '',
|
|
2020
|
+
statusText: typeof payload.statusText === 'string' ? payload.statusText : '',
|
|
2021
|
+
status: typeof payload.status === 'string' ? payload.status : 'idle',
|
|
2022
|
+
error: typeof payload.error === 'string' ? payload.error : null,
|
|
2023
|
+
sessionId: typeof payload.sessionId === 'string' ? payload.sessionId : null,
|
|
2024
|
+
available: payload.available === undefined ? null : payload.available,
|
|
2025
|
+
threadError: typeof payload.threadError === 'string' ? payload.threadError : null,
|
|
2026
|
+
uiVersion: typeof payload.uiVersion === 'string' ? payload.uiVersion : null,
|
|
2027
|
+
deliveryNote: typeof payload.deliveryNote === 'string' ? payload.deliveryNote : null,
|
|
1803
2028
|
}
|
|
1804
|
-
api('/code-server/ask/bundle').then(function (bundle) {
|
|
1805
|
-
if (bundle == null || bundle.ok !== true) return
|
|
1806
|
-
askBundleCache = { css: bundle.css, js: bundle.js }
|
|
1807
|
-
askBundleToStorage(askBundleCache)
|
|
1808
|
-
console.log('[code-server] ask bundle preloaded (' + Math.round((bundle.js.length + bundle.css.length) / 1024) + 'KB)')
|
|
1809
|
-
}).catch(function () { /* 预热失败不打扰:真正打开时会再取一次 */ })
|
|
1810
2029
|
}
|
|
1811
2030
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2031
|
+
// ---- 面板样式 ----
|
|
2032
|
+
//
|
|
2033
|
+
// 原样搬 webview/src/panel.css(0.2.3 起那套外观),只做三处**必要**改动:
|
|
2034
|
+
// ① 不再 @import official-tokens.css —— 设计令牌由 DSH 页面自己提供(z 那 576 行副本是给
|
|
2035
|
+
// webview 用的,页面里本来就有,重定义一次只是幂等);
|
|
2036
|
+
// ② 去掉 `:root { color-scheme }` —— 往宿主页面注入 CSS 时**只能**碰 .dshcs-* 自己的选择器,
|
|
2037
|
+
// 改 :root/body 会动到整个 DSH 界面(这条由 test-ask-panel-inline 的选择器白名单守着);
|
|
2038
|
+
// ③ 表单控件的 --vscode-* 变量在 DSH 页面里不存在,补上页面令牌作为 fallback(否则按钮/输入框
|
|
2039
|
+
// 的 background 是"计算值无效"⇒ 透明,这是老面板在页面里看起来发灰的真实原因)。
|
|
2040
|
+
// 按钮改走官方 primitives 的 Button(与界面同一套),`.dshcs-btn` 只在降级路径下生效。
|
|
2041
|
+
var ASK_CSS = `
|
|
2042
|
+
.dshcs-panel {
|
|
2043
|
+
height: 100%;
|
|
2044
|
+
margin: 0;
|
|
2045
|
+
background: var(--vscode-editor-background, var(--dsw-alias-bg-base, #1e1e1e));
|
|
2046
|
+
color: var(--vscode-foreground, var(--dsw-alias-label-primary, #ccc));
|
|
2047
|
+
font-family: var(--vscode-font-family, var(--dsw-font-family, sans-serif));
|
|
2048
|
+
font-size: var(--vscode-font-size, 13px);
|
|
2049
|
+
overflow: hidden;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
.dshcs-app {
|
|
2053
|
+
display: flex;
|
|
2054
|
+
flex-direction: column;
|
|
2055
|
+
height: 100%;
|
|
2056
|
+
width: 100%;
|
|
2057
|
+
max-width: 900px;
|
|
2058
|
+
margin: 0 auto;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
.dshcs-header {
|
|
2062
|
+
display: flex;
|
|
2063
|
+
align-items: baseline;
|
|
2064
|
+
gap: 8px;
|
|
2065
|
+
padding: 8px 12px;
|
|
2066
|
+
border-bottom: 1px solid var(--vscode-panel-border, var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.35)));
|
|
2067
|
+
font-size: 12px;
|
|
2068
|
+
opacity: 0.9;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
.dshcs-header .dshcs-title { font-weight: 600; letter-spacing: 0.04em; }
|
|
2072
|
+
|
|
2073
|
+
.dshcs-header .dshcs-where {
|
|
2074
|
+
flex: 1;
|
|
2075
|
+
overflow: hidden;
|
|
2076
|
+
text-overflow: ellipsis;
|
|
2077
|
+
white-space: nowrap;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
.dshcs-header .dshcs-close {
|
|
2081
|
+
border: none;
|
|
2082
|
+
background: transparent;
|
|
2083
|
+
color: inherit;
|
|
2084
|
+
cursor: pointer;
|
|
2085
|
+
font-size: 13px;
|
|
2086
|
+
line-height: 1;
|
|
2087
|
+
padding: 2px 6px;
|
|
2088
|
+
border-radius: 4px;
|
|
2089
|
+
opacity: 0.75;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
.dshcs-header .dshcs-close:hover {
|
|
2093
|
+
background: var(--vscode-toolbar-hoverBackground, var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.2)));
|
|
2094
|
+
opacity: 1;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/* ---- 思考行(默认收起;外形照抄官方 ReasoningRow 的排版语言)---- */
|
|
2098
|
+
|
|
2099
|
+
.dshcs-think { display: flex; flex-direction: column; margin: 2px 0 6px; }
|
|
2100
|
+
|
|
2101
|
+
.dshcs-think .dshcs-think-separator {
|
|
2102
|
+
flex: none;
|
|
2103
|
+
width: 2px;
|
|
2104
|
+
height: 2px;
|
|
2105
|
+
margin: 0 8px;
|
|
2106
|
+
border-radius: 1px;
|
|
2107
|
+
background: var(--dsw-alias-label-caption, currentColor);
|
|
2108
|
+
display: inline-block;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
.dshcs-think .dshcs-think-summary {
|
|
2112
|
+
min-width: 0;
|
|
2113
|
+
overflow: hidden;
|
|
2114
|
+
flex: 1 1 auto;
|
|
2115
|
+
color: var(--dsw-alias-label-tertiary, var(--vscode-descriptionForeground, rgba(128, 128, 128, 0.95)));
|
|
2116
|
+
font-size: var(--dsh-content-font-size-secondary, 13px);
|
|
2117
|
+
white-space: nowrap;
|
|
2118
|
+
text-overflow: ellipsis;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
.dshcs-think .dshcs-think-body {
|
|
2122
|
+
padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
|
|
2123
|
+
color: var(--dsw-alias-label-tertiary, var(--vscode-descriptionForeground, rgba(128, 128, 128, 0.95)));
|
|
2124
|
+
font-size: var(--dsh-content-font-size-secondary, 13px);
|
|
2125
|
+
line-height: 1.55;
|
|
2126
|
+
white-space: pre-wrap;
|
|
2127
|
+
word-break: break-word;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
.dshcs-warn {
|
|
2131
|
+
margin: 8px 12px 0;
|
|
2132
|
+
padding: 6px 8px;
|
|
2133
|
+
border: 1px solid var(--vscode-inputValidation-warningBorder, #b89500);
|
|
2134
|
+
background: var(--vscode-inputValidation-warningBackground, rgba(184, 149, 0, 0.15));
|
|
2135
|
+
border-radius: 4px;
|
|
2136
|
+
font-size: 12px;
|
|
2137
|
+
white-space: pre-wrap;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
.dshcs-log { flex: 1; overflow-y: auto; padding: 12px 14px 4px; }
|
|
2141
|
+
|
|
2142
|
+
.dshcs-empty { opacity: 0.6; font-size: 12px; padding: 4px 0; }
|
|
2143
|
+
|
|
2144
|
+
/* ---- 消息条目:排版交给官方渲染器,这里只管气泡与间距 ---- */
|
|
2145
|
+
|
|
2146
|
+
.dshcs-msg { margin-bottom: 14px; }
|
|
2147
|
+
|
|
2148
|
+
.dshcs-user { display: flex; justify-content: flex-end; }
|
|
2149
|
+
|
|
2150
|
+
.dshcs-user-stack {
|
|
2151
|
+
max-width: 88%;
|
|
2152
|
+
display: flex;
|
|
2153
|
+
flex-direction: column;
|
|
2154
|
+
align-items: stretch;
|
|
2155
|
+
gap: 2px;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
.dshcs-context .dshcs-context-summary {
|
|
2159
|
+
min-width: 0;
|
|
2160
|
+
overflow: hidden;
|
|
2161
|
+
flex: 1 1 auto;
|
|
2162
|
+
color: var(--dsw-alias-label-tertiary, var(--vscode-descriptionForeground, rgba(128, 128, 128, 0.95)));
|
|
2163
|
+
font-size: var(--dsh-content-font-size-secondary, 13px);
|
|
2164
|
+
white-space: nowrap;
|
|
2165
|
+
text-overflow: ellipsis;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
.dshcs-context .dshcs-context-body {
|
|
2169
|
+
margin: 2px 0 4px;
|
|
2170
|
+
padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
|
|
2171
|
+
color: var(--dsw-alias-label-tertiary, var(--vscode-descriptionForeground, rgba(128, 128, 128, 0.95)));
|
|
2172
|
+
font-size: var(--dsh-content-font-size-secondary, 12px);
|
|
2173
|
+
font-family: var(--ds-font-family-code, monospace);
|
|
2174
|
+
line-height: 1.5;
|
|
2175
|
+
white-space: pre-wrap;
|
|
2176
|
+
word-break: break-word;
|
|
2177
|
+
max-height: 40vh;
|
|
2178
|
+
overflow: auto;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
.dshcs-bubble {
|
|
2182
|
+
padding: 6px 10px;
|
|
2183
|
+
border-radius: 8px;
|
|
2184
|
+
background: var(--vscode-input-background, var(--dsw-alias-bg-layer-2, rgba(128, 128, 128, 0.12)));
|
|
2185
|
+
border: 1px solid var(--vscode-input-border, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25)));
|
|
2186
|
+
white-space: pre-wrap;
|
|
2187
|
+
word-break: break-word;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
.dshcs-bubble[data-status="sending"] { opacity: 0.6; }
|
|
2191
|
+
|
|
2192
|
+
.dshcs-bubble[data-status="error"] {
|
|
2193
|
+
border-color: var(--vscode-inputValidation-errorBorder, #be1100);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
.dshcs-assistant { word-break: break-word; }
|
|
2197
|
+
|
|
2198
|
+
.dshcs-tool {
|
|
2199
|
+
display: flex;
|
|
2200
|
+
align-items: baseline;
|
|
2201
|
+
gap: 8px;
|
|
2202
|
+
margin: 4px 0 10px;
|
|
2203
|
+
padding: 4px 8px;
|
|
2204
|
+
border-left: 2px solid var(--vscode-panel-border, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4)));
|
|
2205
|
+
font-size: 12px;
|
|
2206
|
+
opacity: 0.85;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
.dshcs-tool .dshcs-tool-name { font-family: var(--ds-font-family-code, var(--vscode-editor-font-family, monospace)); }
|
|
2210
|
+
|
|
2211
|
+
.dshcs-tool .dshcs-tool-summary {
|
|
2212
|
+
flex: 1;
|
|
2213
|
+
overflow: hidden;
|
|
2214
|
+
text-overflow: ellipsis;
|
|
2215
|
+
white-space: nowrap;
|
|
2216
|
+
opacity: 0.8;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
.dshcs-tool .dshcs-tool-state[data-state="error"] {
|
|
2220
|
+
color: var(--vscode-errorForeground, #f48771);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/* ---- 授权卡片:桥里唯一的"非只读"操作,必须一眼看清 ---- */
|
|
2224
|
+
|
|
2225
|
+
.dshcs-approval {
|
|
2226
|
+
margin: 8px 12px;
|
|
2227
|
+
padding: 8px 10px;
|
|
2228
|
+
border: 1px solid var(--vscode-inputValidation-warningBorder, #b89500);
|
|
2229
|
+
background: var(--vscode-inputValidation-warningBackground, rgba(184, 149, 0, 0.12));
|
|
2230
|
+
border-radius: 6px;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
.dshcs-approval[data-decided] { opacity: 0.75; }
|
|
2234
|
+
|
|
2235
|
+
.dshcs-approval-head {
|
|
2236
|
+
display: flex;
|
|
2237
|
+
align-items: baseline;
|
|
2238
|
+
gap: 8px;
|
|
2239
|
+
font-size: 12px;
|
|
2240
|
+
margin-bottom: 4px;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
.dshcs-approval-title { font-weight: 600; }
|
|
2244
|
+
|
|
2245
|
+
.dshcs-approval-timer { flex: 1; text-align: right; opacity: 0.8; }
|
|
2246
|
+
|
|
2247
|
+
.dshcs-approval-body { font-size: 12px; margin-bottom: 6px; word-break: break-word; }
|
|
2248
|
+
|
|
2249
|
+
.dshcs-approval-tool { font-family: var(--ds-font-family-code, var(--vscode-editor-font-family, monospace)); }
|
|
2250
|
+
|
|
2251
|
+
.dshcs-approval-reason { margin-top: 2px; opacity: 0.9; white-space: pre-wrap; }
|
|
2252
|
+
|
|
2253
|
+
.dshcs-approval-actions {
|
|
2254
|
+
display: flex;
|
|
2255
|
+
align-items: center;
|
|
2256
|
+
gap: 8px;
|
|
2257
|
+
flex-wrap: wrap;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
.dshcs-approval-note { font-size: 12px; opacity: 0.85; }
|
|
2261
|
+
|
|
2262
|
+
/* ---- 状态行 + 输入框 ---- */
|
|
2263
|
+
|
|
2264
|
+
.dshcs-status {
|
|
2265
|
+
padding: 0 12px 6px;
|
|
2266
|
+
font-size: 12px;
|
|
2267
|
+
min-height: 16px;
|
|
2268
|
+
opacity: 0.85;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
.dshcs-status[data-status="error"] {
|
|
2272
|
+
color: var(--vscode-errorForeground, #f48771);
|
|
2273
|
+
opacity: 1;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
.dshcs-footer {
|
|
2277
|
+
display: flex;
|
|
2278
|
+
align-items: flex-end;
|
|
2279
|
+
gap: 8px;
|
|
2280
|
+
padding: 8px 12px;
|
|
2281
|
+
border-top: 1px solid var(--vscode-panel-border, var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.35)));
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
.dshcs-footer textarea {
|
|
2285
|
+
flex: 1;
|
|
2286
|
+
min-height: 46px;
|
|
2287
|
+
max-height: 40vh;
|
|
2288
|
+
resize: vertical;
|
|
2289
|
+
box-sizing: border-box;
|
|
2290
|
+
padding: 6px 8px;
|
|
2291
|
+
border: 1px solid var(--vscode-input-border, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));
|
|
2292
|
+
border-radius: 4px;
|
|
2293
|
+
color: var(--vscode-input-foreground, var(--dsw-alias-label-primary, inherit));
|
|
2294
|
+
background: var(--vscode-input-background, var(--dsw-alias-bg-layer-2, transparent));
|
|
2295
|
+
font-family: inherit;
|
|
2296
|
+
font-size: inherit;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
.dshcs-footer textarea:focus {
|
|
2300
|
+
outline: 1px solid var(--vscode-focusBorder, var(--dsw-alias-border-l3, rgba(128, 128, 128, 0.7)));
|
|
2301
|
+
outline-offset: -1px;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
/* 降级路径的按钮 / 折叠行(拿不到官方 primitives 时才会渲染出来) */
|
|
2305
|
+
|
|
2306
|
+
.dshcs-btn {
|
|
2307
|
+
padding: 5px 12px;
|
|
2308
|
+
border: 1px solid var(--vscode-button-border, transparent);
|
|
2309
|
+
border-radius: 4px;
|
|
2310
|
+
cursor: pointer;
|
|
2311
|
+
color: var(--vscode-button-foreground, #fff);
|
|
2312
|
+
background: var(--vscode-button-background, var(--dsw-alias-button-contrast-fill, #4176e6));
|
|
2313
|
+
font-family: inherit;
|
|
2314
|
+
font-size: 12px;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
.dshcs-btn-secondary {
|
|
2318
|
+
color: var(--vscode-button-secondaryForeground, var(--dsw-alias-label-primary, #ccc));
|
|
2319
|
+
background: var(--vscode-button-secondaryBackground, var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.2)));
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
.dshcs-btn:disabled { opacity: 0.5; cursor: default; }
|
|
2323
|
+
|
|
2324
|
+
.dshcs-row-toggle {
|
|
2325
|
+
border: none;
|
|
2326
|
+
background: transparent;
|
|
2327
|
+
color: inherit;
|
|
2328
|
+
cursor: pointer;
|
|
2329
|
+
padding: 0 0 2px;
|
|
2330
|
+
font: inherit;
|
|
2331
|
+
font-size: var(--dsh-content-font-size-secondary, 13px);
|
|
2332
|
+
opacity: 0.9;
|
|
2333
|
+
text-align: left;
|
|
2334
|
+
}
|
|
2335
|
+
`
|
|
2336
|
+
|
|
2337
|
+
// ---- 面板组件(手写;等价于 0.3.58 之前 webview/src 下那三个 .jsx)----
|
|
2338
|
+
|
|
2339
|
+
/** 官方渲染器的界面文案(面板自己给中文;DSH 界面走它自己的 locale)。
|
|
2340
|
+
* **必须是常量**:MarkdownText 的流式缓存按 labels 的引用身份判失效,每帧新建会一直重解析。 */
|
|
2341
|
+
var ASK_LABELS = { code: { copyLabel: '复制', copiedLabel: '已复制' }, footnotes: '脚注' }
|
|
2342
|
+
|
|
2343
|
+
/** 工具状态 / 授权状态 → 中文。 */
|
|
2344
|
+
var ASK_TOOL_STATE = { running: '运行中', ok: '完成', error: '失败' }
|
|
2345
|
+
var ASK_APPROVAL_STATE = {
|
|
2346
|
+
asked: '等待授权', 'allowed-once': '已允许一次', rejected: '已拒绝', cancelled: '已取消', unavailable: '无人处理',
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
/** 折叠摘要取首行(还在流式时取最新一行)—— 与官方 ReasoningRow 同一规则。 */
|
|
2350
|
+
function askSummaryLine(text, running) {
|
|
2351
|
+
var source = running ? String(text).replace(/\s+$/, '') : String(text)
|
|
2352
|
+
if (source === '') return ''
|
|
2353
|
+
var index = running ? source.lastIndexOf('\n') : source.indexOf('\n')
|
|
2354
|
+
var line = index === -1 ? source : (running ? source.slice(index + 1) : source.slice(0, index))
|
|
2355
|
+
return line.split('**').join('')
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
/** 剩余时间文案(超过一分钟 mm:ss:秒数跳动会让人紧张)。 */
|
|
2359
|
+
function askRemainingText(item, holdMs, now) {
|
|
2360
|
+
var at = Number.isSafeInteger(item.at) ? item.at : now
|
|
2361
|
+
var left = Math.max(0, at + holdMs - now)
|
|
2362
|
+
if (left === 0) return '已交回 DSH 界面'
|
|
2363
|
+
var totalSeconds = Math.ceil(left / 1000)
|
|
2364
|
+
var minutes = Math.floor(totalSeconds / 60)
|
|
2365
|
+
var seconds = totalSeconds % 60
|
|
2366
|
+
return minutes > 0
|
|
2367
|
+
? minutes + ' 分 ' + String(seconds).padStart(2, '0') + ' 秒后交回 DSH 界面'
|
|
2368
|
+
: seconds + ' 秒后交回 DSH 界面'
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
/**
|
|
2372
|
+
* 一行「思考」/「上下文」(默认收起,点行即展开)—— 外形与交互照抄官方 ReasoningRow。
|
|
2373
|
+
*
|
|
2374
|
+
* 官方部件在就用官方的(DisclosureRow + 官方图标);拿不到就退成一个朴素的按钮行:
|
|
2375
|
+
* **能看、能点、能展开**,只是没有那套图标与排版。
|
|
2376
|
+
*/
|
|
2377
|
+
function AskDisclosureRow(props) {
|
|
2378
|
+
var state = React.useState(false)
|
|
2379
|
+
var expanded = state[0]
|
|
2380
|
+
var setExpanded = state[1]
|
|
2381
|
+
var primitives = askPrimitives().primitives
|
|
2382
|
+
var Row = askComponent(primitives, 'DisclosureRow')
|
|
2383
|
+
var Icon = primitives !== null && typeof props.iconName === 'string' ? askComponent(primitives, props.iconName) : null
|
|
2384
|
+
|
|
2385
|
+
var collapsed = React.createElement(
|
|
2386
|
+
React.Fragment,
|
|
2387
|
+
null,
|
|
2388
|
+
props.separator === true
|
|
2389
|
+
? React.createElement('span', { className: 'dshcs-think-separator', 'aria-hidden': true })
|
|
2390
|
+
: null,
|
|
2391
|
+
React.createElement('span', { className: props.summaryClass }, props.summaryText),
|
|
2392
|
+
)
|
|
2393
|
+
var body = React.createElement(props.bodyTag, { className: props.bodyClass }, props.bodyText)
|
|
2394
|
+
|
|
2395
|
+
if (Row === null) {
|
|
2396
|
+
return React.createElement(
|
|
2397
|
+
'div',
|
|
2398
|
+
{ className: props.rootClass, 'data-expanded': expanded || undefined },
|
|
2399
|
+
React.createElement(
|
|
2400
|
+
'button',
|
|
2401
|
+
{ type: 'button', className: 'dshcs-row-toggle', onClick: function () { setExpanded(!expanded) } },
|
|
2402
|
+
(expanded ? '▾ ' : '▸ ') + props.title,
|
|
2403
|
+
),
|
|
2404
|
+
collapsed,
|
|
2405
|
+
expanded ? body : null,
|
|
2406
|
+
)
|
|
2407
|
+
}
|
|
2408
|
+
return React.createElement(
|
|
2409
|
+
'div',
|
|
2410
|
+
{ className: props.rootClass, 'data-expanded': expanded || undefined, 'data-state': props.running === true ? 'running' : 'ok' },
|
|
2411
|
+
React.createElement(
|
|
2412
|
+
Row,
|
|
2413
|
+
{
|
|
2414
|
+
icon: Icon === null ? null : React.createElement(Icon, props.iconSize === 16 ? { size: 16 } : { size: 14 }),
|
|
2415
|
+
title: props.title,
|
|
2416
|
+
open: expanded,
|
|
2417
|
+
expandable: true,
|
|
2418
|
+
expandOnRowClick: true,
|
|
2419
|
+
onToggle: function () { setExpanded(!expanded) },
|
|
2420
|
+
collapsedContent: collapsed,
|
|
2421
|
+
},
|
|
2422
|
+
body,
|
|
2423
|
+
),
|
|
2424
|
+
)
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
/** 官方按钮(拿不到就退成原生 button,并打上 .dshcs-btn)。 */
|
|
2428
|
+
function askButton(props, text) {
|
|
2429
|
+
var primitives = askPrimitives().primitives
|
|
2430
|
+
var Button = askComponent(primitives, 'Button')
|
|
2431
|
+
if (Button === null) {
|
|
2432
|
+
return React.createElement('button', {
|
|
2433
|
+
type: 'button',
|
|
2434
|
+
className: props.secondary === true ? 'dshcs-btn dshcs-btn-secondary' : 'dshcs-btn',
|
|
2435
|
+
disabled: props.disabled === true,
|
|
2436
|
+
onClick: props.onClick,
|
|
2437
|
+
}, text)
|
|
2438
|
+
}
|
|
2439
|
+
return React.createElement(Button, {
|
|
2440
|
+
variant: props.secondary === true ? 'outline' : 'primary',
|
|
2441
|
+
size: 'sm',
|
|
2442
|
+
disabled: props.disabled === true,
|
|
2443
|
+
onClick: props.onClick,
|
|
2444
|
+
}, text)
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/**
|
|
2448
|
+
* 一个对话条目(等价于 thread.jsx 的 ThreadEntry)。
|
|
2449
|
+
*
|
|
2450
|
+
* 助手正文**不自己排版**:交给官方 `MarkdownText`(与 DSH 界面同一份:micromark/mdast 管线、
|
|
2451
|
+
* shiki 高亮、KaTeX、增量流式解析)。拿不到官方渲染器时才退回 <pre>(可读,但不假装排版)。
|
|
2452
|
+
*/
|
|
2453
|
+
function AskThreadEntry(props) {
|
|
2454
|
+
var entry = props.entry
|
|
2455
|
+
if (entry === null || typeof entry !== 'object') return null
|
|
2456
|
+
var text = typeof entry.text === 'string' ? entry.text : ''
|
|
2457
|
+
|
|
2458
|
+
if (entry.role === 'user') {
|
|
2459
|
+
// 别的工具注入的上下文不是"用户说的"(0.3.43):按官方的「上下文注入」折叠显示。
|
|
2460
|
+
if (typeof entry.sourceKind === 'string' && entry.sourceKind !== '') {
|
|
2461
|
+
return React.createElement(
|
|
2462
|
+
'div',
|
|
2463
|
+
{ className: 'dshcs-msg dshcs-injection' },
|
|
2464
|
+
React.createElement(AskDisclosureRow, {
|
|
2465
|
+
rootClass: 'dshcs-context', title: '上下文注入', iconName: 'IconContextInjectionOutline16', iconSize: 16,
|
|
2466
|
+
summaryText: String(text).split('\n')[0].replace(/^From the editor:\s*/i, ''),
|
|
2467
|
+
summaryClass: 'dshcs-context-summary', bodyClass: 'dshcs-context-body', bodyTag: 'pre', bodyText: text,
|
|
2468
|
+
}),
|
|
2469
|
+
)
|
|
1828
2470
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2471
|
+
var context = typeof entry.context === 'string' ? entry.context : ''
|
|
2472
|
+
return React.createElement(
|
|
2473
|
+
'div',
|
|
2474
|
+
{ className: 'dshcs-msg dshcs-user' },
|
|
2475
|
+
React.createElement(
|
|
2476
|
+
'div',
|
|
2477
|
+
{ className: 'dshcs-user-stack' },
|
|
2478
|
+
context === '' ? null : React.createElement(AskDisclosureRow, {
|
|
2479
|
+
rootClass: 'dshcs-context', title: '上下文', iconName: 'IconContextInjectionOutline16', iconSize: 16,
|
|
2480
|
+
summaryText: context.split('\n')[0].replace(/^From the editor:\s*/i, ''),
|
|
2481
|
+
summaryClass: 'dshcs-context-summary', bodyClass: 'dshcs-context-body', bodyTag: 'pre', bodyText: context,
|
|
2482
|
+
}),
|
|
2483
|
+
React.createElement('div', {
|
|
2484
|
+
className: 'dshcs-bubble',
|
|
2485
|
+
'data-status': entry.status === undefined || entry.status === null ? undefined : entry.status,
|
|
2486
|
+
}, text),
|
|
2487
|
+
),
|
|
2488
|
+
)
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
if (entry.role === 'assistant') {
|
|
2492
|
+
var thinking = typeof entry.thinking === 'string' ? entry.thinking : ''
|
|
2493
|
+
// 正文还没到(整轮都是思考)→ 思考行标"思考中";正文到了就标"思考"(官方同一判据)。
|
|
2494
|
+
var running = entry.streaming === true && text === ''
|
|
2495
|
+
var primitives = askPrimitives().primitives
|
|
2496
|
+
var Markdown = askComponent(primitives, 'MarkdownText')
|
|
2497
|
+
var bodyElement = text === ''
|
|
2498
|
+
? null
|
|
2499
|
+
: (Markdown === null
|
|
2500
|
+
? React.createElement('pre', { className: 'dshcs-md-fallback' }, text)
|
|
2501
|
+
: React.createElement(Markdown, { text: text, streaming: entry.streaming === true, labels: ASK_LABELS }))
|
|
2502
|
+
return React.createElement(
|
|
2503
|
+
'div',
|
|
2504
|
+
{ className: 'dshcs-msg dshcs-assistant' },
|
|
2505
|
+
thinking === '' ? null : React.createElement(AskDisclosureRow, {
|
|
2506
|
+
rootClass: 'dshcs-think', title: running ? '思考中' : '思考', iconName: 'IconThinkOutline14', iconSize: 14,
|
|
2507
|
+
running: running, separator: true, summaryText: askSummaryLine(thinking, running),
|
|
2508
|
+
summaryClass: 'dshcs-think-summary', bodyClass: 'dshcs-think-body', bodyTag: 'div', bodyText: thinking,
|
|
2509
|
+
}),
|
|
2510
|
+
bodyElement,
|
|
2511
|
+
)
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
if (entry.role === 'tool') {
|
|
2515
|
+
return React.createElement(
|
|
2516
|
+
'div',
|
|
2517
|
+
{ className: 'dshcs-tool', 'data-state': entry.status === undefined || entry.status === null ? 'running' : entry.status },
|
|
2518
|
+
React.createElement('span', { className: 'dshcs-tool-name' }, entry.name === undefined || entry.name === null ? 'tool' : entry.name),
|
|
2519
|
+
React.createElement('span', { className: 'dshcs-tool-summary' }, entry.summary === undefined || entry.summary === null ? '' : entry.summary),
|
|
2520
|
+
React.createElement(
|
|
2521
|
+
'span',
|
|
2522
|
+
{ className: 'dshcs-tool-state', 'data-state': entry.status === undefined || entry.status === null ? 'running' : entry.status },
|
|
2523
|
+
ASK_TOOL_STATE[entry.status] !== undefined ? ASK_TOOL_STATE[entry.status] : (entry.status === undefined || entry.status === null ? '' : entry.status),
|
|
2524
|
+
),
|
|
2525
|
+
)
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
if (entry.role === 'approval') {
|
|
2529
|
+
var state = entry.status === undefined || entry.status === null ? 'asked' : entry.status
|
|
2530
|
+
return React.createElement(
|
|
2531
|
+
'div',
|
|
2532
|
+
{ className: 'dshcs-tool dshcs-approval-row', 'data-state': state },
|
|
2533
|
+
React.createElement('span', { className: 'dshcs-tool-name' }, entry.toolName !== undefined && entry.toolName !== null ? entry.toolName : (entry.name !== undefined && entry.name !== null ? entry.name : 'tool')),
|
|
2534
|
+
React.createElement('span', { className: 'dshcs-tool-summary' }, entry.summary === undefined || entry.summary === null ? '' : entry.summary),
|
|
2535
|
+
React.createElement('span', { className: 'dshcs-tool-state' }, ASK_APPROVAL_STATE[state] !== undefined ? ASK_APPROVAL_STATE[state] : state),
|
|
2536
|
+
)
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
return null
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
/**
|
|
2543
|
+
* 待决授权卡片(等价于 approval.jsx)。
|
|
2544
|
+
*
|
|
2545
|
+
* 两条实测教训都在这里:卡片**不自己判过期**(按钮能不能点由"宿主还挂着这条请求"决定,
|
|
2546
|
+
* 请求被交回官方链路时它会从 approvals 里消失 → 变成审计行),并且只对这一次动作有效,
|
|
2547
|
+
* 没有"以后都允许"这种入口。
|
|
2548
|
+
*/
|
|
2549
|
+
function AskApprovalCard(props) {
|
|
2550
|
+
var item = props.item
|
|
2551
|
+
var choice = props.decided === undefined ? null : props.decided
|
|
2552
|
+
var locked = choice !== null
|
|
2553
|
+
return React.createElement(
|
|
2554
|
+
'div',
|
|
2555
|
+
{ className: 'dshcs-approval', 'data-decided': choice === null ? undefined : choice },
|
|
2556
|
+
React.createElement(
|
|
2557
|
+
'div',
|
|
2558
|
+
{ className: 'dshcs-approval-head' },
|
|
2559
|
+
React.createElement('span', { className: 'dshcs-approval-title' }, '需要你的授权'),
|
|
2560
|
+
React.createElement('span', { className: 'dshcs-approval-timer' }, askRemainingText(item, props.holdMs, props.now)),
|
|
2561
|
+
),
|
|
2562
|
+
React.createElement(
|
|
2563
|
+
'div',
|
|
2564
|
+
{ className: 'dshcs-approval-body' },
|
|
2565
|
+
React.createElement('span', { className: 'dshcs-approval-tool' }, item.toolName),
|
|
2566
|
+
item.reason === '' || item.reason === undefined ? null : React.createElement('div', { className: 'dshcs-approval-reason' }, item.reason),
|
|
2567
|
+
),
|
|
2568
|
+
React.createElement(
|
|
2569
|
+
'div',
|
|
2570
|
+
{ className: 'dshcs-approval-actions' },
|
|
2571
|
+
askButton({ disabled: locked, onClick: function () { props.onDecide(item.id, 'allowed-once') } }, '允许一次'),
|
|
2572
|
+
askButton({ secondary: true, disabled: locked, onClick: function () { props.onDecide(item.id, 'rejected') } }, '拒绝'),
|
|
2573
|
+
React.createElement(
|
|
2574
|
+
'span',
|
|
2575
|
+
{ className: 'dshcs-approval-note' },
|
|
2576
|
+
choice === null
|
|
2577
|
+
? '只对这一次动作有效;关掉面板即交回 DSH 界面'
|
|
2578
|
+
: (choice === 'allowed-once' ? '已允许一次(DSH 继续执行)' : '已拒绝'),
|
|
2579
|
+
),
|
|
2580
|
+
),
|
|
2581
|
+
)
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
/** 面板本体:**纯展示** —— 状态全部由 AskRoot 通过 props 给进来,可以直接喂一棵假 view 单测。 */
|
|
2585
|
+
function AskPanel(props) {
|
|
2586
|
+
var view = props.view
|
|
2587
|
+
var entries = view.entries
|
|
2588
|
+
// 状态行优先级:面板自己的脚本错误 > 宿主给的 statusText > 客户端侧的投递提示(排队/插入当前轮)。
|
|
2589
|
+
var status = props.scriptError !== null
|
|
2590
|
+
? props.scriptError
|
|
2591
|
+
: (view.statusText !== '' ? view.statusText : (view.deliveryNote === null ? '' : view.deliveryNote))
|
|
2592
|
+
return React.createElement(
|
|
2593
|
+
'div',
|
|
2594
|
+
{ className: 'dshcs-app' },
|
|
2595
|
+
React.createElement(
|
|
2596
|
+
'header',
|
|
2597
|
+
{ className: 'dshcs-header' },
|
|
2598
|
+
React.createElement('span', { className: 'dshcs-title' }, 'DSH'),
|
|
2599
|
+
React.createElement('span', { className: 'dshcs-where' }, view.contextText),
|
|
2600
|
+
React.createElement('button', {
|
|
2601
|
+
type: 'button', className: 'dshcs-close', title: '关闭(Shift+Esc)', 'aria-label': '关闭', onClick: props.onClose,
|
|
2602
|
+
}, '✕'),
|
|
2603
|
+
),
|
|
2604
|
+
props.scriptError === null ? null : React.createElement('div', { className: 'dshcs-warn' }, props.scriptError),
|
|
2605
|
+
view.threadError === null ? null : React.createElement('div', { className: 'dshcs-warn' }, view.threadError),
|
|
2606
|
+
React.createElement(
|
|
2607
|
+
'main',
|
|
2608
|
+
{ className: 'dshcs-log', ref: props.logRef, onScroll: props.onScroll },
|
|
2609
|
+
entries.length === 0
|
|
2610
|
+
? React.createElement('div', { className: 'dshcs-empty' }, '在下面提问:DSH 的回答会像 DSH 界面那样显示在这里(思考折叠、正文按官方渲染)。')
|
|
2611
|
+
: entries.map(function (entry, index) {
|
|
2612
|
+
return React.createElement(AskThreadEntry, {
|
|
2613
|
+
key: String(entry.role) + '-' + index + '-' + (entry.callId !== undefined && entry.callId !== null ? entry.callId : (entry.approvalId !== undefined && entry.approvalId !== null ? entry.approvalId : '')),
|
|
2614
|
+
entry: entry,
|
|
2615
|
+
})
|
|
2616
|
+
}),
|
|
2617
|
+
),
|
|
2618
|
+
view.approvals.map(function (item) {
|
|
2619
|
+
return React.createElement(AskApprovalCard, {
|
|
2620
|
+
key: item.id, item: item, holdMs: view.approvalHoldMs, now: props.now,
|
|
2621
|
+
decided: props.decided[item.id] === undefined ? null : props.decided[item.id],
|
|
2622
|
+
onDecide: props.onDecide,
|
|
2623
|
+
})
|
|
2624
|
+
}),
|
|
2625
|
+
React.createElement('div', { className: 'dshcs-status', 'data-status': view.status }, status),
|
|
2626
|
+
React.createElement(
|
|
2627
|
+
'footer',
|
|
2628
|
+
{ className: 'dshcs-footer' },
|
|
2629
|
+
React.createElement('textarea', {
|
|
2630
|
+
value: props.draft,
|
|
2631
|
+
placeholder: '问 DSH…(Enter 发送,Shift+Enter 换行)',
|
|
2632
|
+
onChange: function (event) { props.onDraft(event.target.value) },
|
|
2633
|
+
onKeyDown: props.onKeyDown,
|
|
2634
|
+
autoFocus: true,
|
|
2635
|
+
}),
|
|
2636
|
+
askButton({ disabled: props.sending === true || String(props.draft).trim() === '', onClick: props.onSubmit }, '发送'),
|
|
2637
|
+
),
|
|
2638
|
+
)
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
/** 渲染兜底:面板渲染抛错时显示原因,而不是留一块空白(空白是最难查的失败)。 */
|
|
2642
|
+
function askErrorView(error) {
|
|
2643
|
+
var message = error !== null && error !== undefined && error.message !== undefined ? error.message : String(error)
|
|
2644
|
+
return React.createElement('div', { className: 'dshcs-panel' },
|
|
2645
|
+
React.createElement('div', { className: 'dshcs-warn' }, '面板渲染出错:' + message))
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
/** 错误边界(React 只在类组件里提供它)。 */
|
|
2649
|
+
class AskBoundary extends React.Component {
|
|
2650
|
+
constructor(props) {
|
|
2651
|
+
super(props)
|
|
2652
|
+
this.state = { error: null }
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
componentDidCatch(error) {
|
|
2656
|
+
console.error('[code-server] 「问 DSH」面板渲染出错:', error)
|
|
2657
|
+
this.setState({ error: error })
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
render() {
|
|
2661
|
+
if (this.state.error !== null) return askErrorView(this.state.error)
|
|
2662
|
+
return this.props.children === undefined ? null : this.props.children
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
/** 面板的根:持有视图状态,并把宿主的每趟状态推进来。 */
|
|
2667
|
+
function AskRoot() {
|
|
2668
|
+
var viewState = React.useState(askViewState())
|
|
2669
|
+
var view = viewState[0]
|
|
2670
|
+
var setView = viewState[1]
|
|
2671
|
+
var nowState = React.useState(function () { return Date.now() })
|
|
2672
|
+
var now = nowState[0]
|
|
2673
|
+
var setNow = nowState[1]
|
|
2674
|
+
var draftState = React.useState('')
|
|
2675
|
+
var sendingState = React.useState(false)
|
|
2676
|
+
var errorState = React.useState(null)
|
|
2677
|
+
var decidedState = React.useState({})
|
|
2678
|
+
var draft = draftState[0]
|
|
2679
|
+
var setDraft = draftState[1]
|
|
2680
|
+
var sending = sendingState[0]
|
|
2681
|
+
var setSending = sendingState[1]
|
|
2682
|
+
var scriptError = errorState[0]
|
|
2683
|
+
var decided = decidedState[0]
|
|
2684
|
+
var setDecided = decidedState[1]
|
|
2685
|
+
var logRef = React.useRef(null)
|
|
2686
|
+
var stickRef = React.useRef(true)
|
|
2687
|
+
|
|
2688
|
+
// 宿主 → 面板:壳直接调(同一个 window,不走 postMessage —— DSH 页面里 postMessage 是公共广播)。
|
|
2689
|
+
React.useEffect(function () {
|
|
2690
|
+
ask.push = function (payload) {
|
|
2691
|
+
if (payload === null || typeof payload !== 'object') return
|
|
2692
|
+
setView(function (prev) { return askApplyPayload(prev, Object.assign({}, payload, { type: 'state' })) })
|
|
1832
2693
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2694
|
+
if (ask.pending !== null) ask.push(ask.pending)
|
|
2695
|
+
return function () { ask.push = null }
|
|
2696
|
+
}, [])
|
|
2697
|
+
|
|
2698
|
+
// 授权倒计时:只在有卡片时跑。
|
|
2699
|
+
React.useEffect(function () {
|
|
2700
|
+
if (view.approvals.length === 0) return undefined
|
|
2701
|
+
var timer = window.setInterval(function () { setNow(Date.now()) }, 250)
|
|
2702
|
+
return function () { window.clearInterval(timer) }
|
|
2703
|
+
}, [view.approvals.length])
|
|
2704
|
+
|
|
2705
|
+
// 已提交的决策:卡片消失后清掉(不然 decided 会一直涨)。
|
|
2706
|
+
React.useEffect(function () {
|
|
2707
|
+
setDecided(function (prev) {
|
|
2708
|
+
var ids = {}
|
|
2709
|
+
view.approvals.forEach(function (item) { ids[item.id] = true })
|
|
2710
|
+
var next = {}
|
|
2711
|
+
var changed = false
|
|
2712
|
+
Object.keys(prev).forEach(function (id) {
|
|
2713
|
+
if (ids[id] === true) next[id] = prev[id]
|
|
2714
|
+
else changed = true
|
|
2715
|
+
})
|
|
2716
|
+
return changed ? next : prev
|
|
2717
|
+
})
|
|
2718
|
+
}, [view.approvals])
|
|
2719
|
+
|
|
2720
|
+
// 贴底滚动:用户主动往上翻时不打扰。
|
|
2721
|
+
React.useEffect(function () {
|
|
2722
|
+
var node = logRef.current
|
|
2723
|
+
if (node !== null && node !== undefined && stickRef.current) node.scrollTop = node.scrollHeight
|
|
2724
|
+
}, [view.entries])
|
|
2725
|
+
|
|
2726
|
+
// 任何一趟宿主状态回来都解除"发送中"的按钮锁(0.3.44 的教训:对话框形态下宿主的 status
|
|
2727
|
+
// **一直是 idle**,盯着 status 变化的写法会让按钮第一次发送后永久变灰)。
|
|
2728
|
+
React.useEffect(function () { setSending(false) }, [view])
|
|
2729
|
+
// 兜底:某一趟状态没回来(宿主卡住/请求失败)时最多 15 秒也解锁。
|
|
2730
|
+
React.useEffect(function () {
|
|
2731
|
+
if (sending !== true) return undefined
|
|
2732
|
+
var timer = window.setTimeout(function () { setSending(false) }, 15000)
|
|
2733
|
+
return function () { window.clearTimeout(timer) }
|
|
2734
|
+
}, [sending])
|
|
2735
|
+
|
|
2736
|
+
var submit = function () {
|
|
2737
|
+
var text = String(draft).trim()
|
|
2738
|
+
if (text === '' || sending) return
|
|
2739
|
+
setSending(true)
|
|
2740
|
+
setDraft('')
|
|
2741
|
+
askOnMessage({ type: 'ask', text: text })
|
|
2742
|
+
}
|
|
2743
|
+
var onKeyDown = function (event) {
|
|
2744
|
+
if (event.key === 'Enter' && event.shiftKey === false) {
|
|
2745
|
+
event.preventDefault()
|
|
2746
|
+
submit()
|
|
1839
2747
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2748
|
+
}
|
|
2749
|
+
var onScroll = function () {
|
|
2750
|
+
var node = logRef.current
|
|
2751
|
+
if (node === null || node === undefined) return
|
|
2752
|
+
stickRef.current = node.scrollHeight - node.scrollTop - node.clientHeight < 48
|
|
2753
|
+
}
|
|
2754
|
+
var onDecide = function (id, outcome) {
|
|
2755
|
+
setDecided(function (prev) {
|
|
2756
|
+
var next = Object.assign({}, prev)
|
|
2757
|
+
next[id] = outcome
|
|
2758
|
+
return next
|
|
2759
|
+
})
|
|
2760
|
+
askOnMessage({ type: 'approve', id: id, outcome: outcome })
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
return React.createElement(
|
|
2764
|
+
AskBoundary,
|
|
2765
|
+
null,
|
|
2766
|
+
React.createElement(AskPanel, {
|
|
2767
|
+
view: view, now: now, draft: draft, sending: sending, scriptError: scriptError, decided: decided,
|
|
2768
|
+
logRef: logRef, onScroll: onScroll, onDraft: setDraft, onKeyDown: onKeyDown,
|
|
2769
|
+
onSubmit: submit, onDecide: onDecide, onClose: function () { askOnMessage({ type: 'close' }) },
|
|
2770
|
+
}),
|
|
2771
|
+
)
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
/** 挂载面板(只做一次):样式进 <head>,React 根挂在对话框的 body 上。 */
|
|
2775
|
+
function askMount() {
|
|
2776
|
+
if (ask.mounted) return
|
|
2777
|
+
askEnsureShell()
|
|
2778
|
+
if (document.getElementById('dshcs-ask-style') === null) {
|
|
2779
|
+
var style = document.createElement('style')
|
|
2780
|
+
style.id = 'dshcs-ask-style'
|
|
2781
|
+
style.textContent = ASK_CSS
|
|
2782
|
+
document.head.appendChild(style)
|
|
2783
|
+
}
|
|
2784
|
+
var renderer = askPrimitives()
|
|
2785
|
+
if (renderer.createRoot === null) {
|
|
2786
|
+
// 连 React 都取不到:至少把"为什么"写在面板里,不要开一扇空白窗。
|
|
2787
|
+
var why = document.createElement('div')
|
|
2788
|
+
why.className = 'dshcs-warn'
|
|
2789
|
+
why.textContent = '面板无法挂载:模块表里取不到 react-dom/client(' + renderer.why + ')'
|
|
2790
|
+
ask.body.appendChild(why)
|
|
2791
|
+
askNotice('面板无法挂载:' + renderer.why)
|
|
1847
2792
|
ask.mounted = true
|
|
1848
|
-
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2793
|
+
return
|
|
2794
|
+
}
|
|
2795
|
+
ask.root = renderer.createRoot(ask.body)
|
|
2796
|
+
ask.root.render(React.createElement(AskBoundary, null, React.createElement(AskRoot, null)))
|
|
2797
|
+
ask.mounted = true
|
|
2798
|
+
console.log('[code-server] 「问 DSH」面板已挂载'
|
|
2799
|
+
+ (renderer.primitives === null ? '(降级:无官方渲染器)' : '(官方渲染器:页面模块表)'))
|
|
1853
2800
|
}
|
|
1854
2801
|
|
|
1855
2802
|
/** 轮询 host 状态:open 就显示并推状态,关了或没变化就什么都不做。 */
|
|
@@ -1877,8 +2824,8 @@ let React = require('react')
|
|
|
1877
2824
|
if (ask.timer !== null) return
|
|
1878
2825
|
ask.timer = setInterval(askPoll, ASK_POLL_MS)
|
|
1879
2826
|
setTimeout(askPoll, 800)
|
|
1880
|
-
//
|
|
1881
|
-
|
|
2827
|
+
// 0.3.42 的"空闲预热面板产物"随注入机制一起去掉了:面板现在就是本文件里的组件,
|
|
2828
|
+
// 页面加载插件时已经解析完,没有 1.4MB 的"取文本 + 解析 + 执行"要等。
|
|
1882
2829
|
console.log('[code-server] ask dialog poller started (every ' + ASK_POLL_MS + 'ms)')
|
|
1883
2830
|
}
|
|
1884
2831
|
|
|
@@ -2025,7 +2972,7 @@ let React = require('react')
|
|
|
2025
2972
|
|
|
2026
2973
|
// ---- 测试钩子(唯一为测试而存在的代码;DSH 永不设置该标志)----
|
|
2027
2974
|
// scripts/client-bundle-harness.mjs 的 testHooks 选项会先设 window.__dshcsTestHooks = true,
|
|
2028
|
-
//
|
|
2975
|
+
// 于是几个"纯函数/纯树"单元套件(workspace 解析、全屏动作、问 DSH 面板)可以直接调这里的内部函数,
|
|
2029
2976
|
// 不需要为了可测而把模块拆出去(拆出去就意味着又要有构建)。
|
|
2030
2977
|
if (typeof window !== 'undefined' && window.__dshcsTestHooks === true) {
|
|
2031
2978
|
module.exports.__internals = {
|
|
@@ -2045,6 +2992,30 @@ if (typeof window !== 'undefined' && window.__dshcsTestHooks === true) {
|
|
|
2045
2992
|
PREVIEW_FRIENDLY_EXTENSIONS: PREVIEW_FRIENDLY_EXTENSIONS,
|
|
2046
2993
|
EXECUTABLE_EXTENSIONS: EXECUTABLE_EXTENSIONS,
|
|
2047
2994
|
OFFICE_EXTENSIONS: OFFICE_EXTENSIONS,
|
|
2995
|
+
// 「问 DSH」面板(0.3.59 起手写;面板的每个选择器/每次降级都要能被钉住)
|
|
2996
|
+
ASK_CSS: ASK_CSS,
|
|
2997
|
+
ASK_LABELS: ASK_LABELS,
|
|
2998
|
+
askViewState: askViewState,
|
|
2999
|
+
askApplyPayload: askApplyPayload,
|
|
3000
|
+
askPrimitives: askPrimitives,
|
|
3001
|
+
askComponent: askComponent,
|
|
3002
|
+
askButton: askButton,
|
|
3003
|
+
askSummaryLine: askSummaryLine,
|
|
3004
|
+
askDeliveryNote: askDeliveryNote,
|
|
3005
|
+
askRemainingText: askRemainingText,
|
|
3006
|
+
askErrorView: askErrorView,
|
|
3007
|
+
AskBoundary: AskBoundary,
|
|
3008
|
+
AskDisclosureRow: AskDisclosureRow,
|
|
3009
|
+
AskThreadEntry: AskThreadEntry,
|
|
3010
|
+
AskApprovalCard: AskApprovalCard,
|
|
3011
|
+
AskPanel: AskPanel,
|
|
3012
|
+
AskRoot: AskRoot,
|
|
3013
|
+
askMount: askMount,
|
|
3014
|
+
askPush: askPush,
|
|
3015
|
+
askOnMessage: askOnMessage,
|
|
3016
|
+
askClose: askClose,
|
|
3017
|
+
askEnsureShell: askEnsureShell,
|
|
3018
|
+
askState: ask,
|
|
2048
3019
|
}
|
|
2049
3020
|
}
|
|
2050
3021
|
|