cc-viewer 1.7.23 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +19 -0
  2. package/cli.js +92 -11
  3. package/concepts/ar/GlobalSettings.md +2 -2
  4. package/concepts/da/GlobalSettings.md +2 -2
  5. package/concepts/de/GlobalSettings.md +2 -2
  6. package/concepts/en/GlobalSettings.md +2 -2
  7. package/concepts/es/GlobalSettings.md +2 -2
  8. package/concepts/fr/GlobalSettings.md +2 -2
  9. package/concepts/it/GlobalSettings.md +2 -2
  10. package/concepts/ja/GlobalSettings.md +2 -2
  11. package/concepts/ko/GlobalSettings.md +2 -2
  12. package/concepts/no/GlobalSettings.md +2 -2
  13. package/concepts/pl/GlobalSettings.md +2 -2
  14. package/concepts/pt-BR/GlobalSettings.md +2 -2
  15. package/concepts/ru/GlobalSettings.md +2 -2
  16. package/concepts/th/GlobalSettings.md +2 -2
  17. package/concepts/tr/GlobalSettings.md +2 -2
  18. package/concepts/uk/GlobalSettings.md +2 -2
  19. package/concepts/zh/GlobalSettings.md +2 -2
  20. package/concepts/zh-TW/GlobalSettings.md +2 -2
  21. package/dist/assets/{App-CusqoEAB.js → App-CCu0y2Cq.js} +2 -2
  22. package/dist/assets/{MdxEditorPanel-e5DlfcqJ.js → MdxEditorPanel-BBhgTmDk.js} +1 -1
  23. package/dist/assets/{Mobile-B8xwCLgr.js → Mobile-vN4lyiaz.js} +1 -1
  24. package/dist/assets/{ProxyStatsModal-_w677xPM.js → ProxyStatsModal-BOflqr0R.js} +1 -1
  25. package/dist/assets/index-C3BRHDVF.js +2 -0
  26. package/dist/assets/seqResourceLoaders-DI0G5nu1.css +41 -0
  27. package/dist/assets/seqResourceLoaders-DpYqp6SP.js +2 -0
  28. package/dist/index.html +1 -1
  29. package/findcc.js +17 -8
  30. package/node_modules/@ccv/core/package.json +13 -0
  31. package/{server/lib → node_modules/@ccv/core/src}/approval-modal-prefs.js +1 -1
  32. package/node_modules/@ccv/core/src/clearCheckpoint.js +7 -0
  33. package/{src/utils → node_modules/@ccv/core/src}/contentFilter.js +207 -141
  34. package/{server/lib → node_modules/@ccv/core/src}/context-rules.js +2 -2
  35. package/{server/lib → node_modules/@ccv/core/src}/delta-reconstructor.js +60 -4
  36. package/{server/lib → node_modules/@ccv/core/src}/error-report.js +5 -4
  37. package/{server/lib → node_modules/@ccv/core/src}/session-boundary.js +3 -3
  38. package/{src/utils → node_modules/@ccv/core/src}/teammateDetector.js +9 -0
  39. package/{server/lib → node_modules/@ccv/core/src}/tools-xml-formatter.js +3 -3
  40. package/{server/lib → node_modules/@ccv/core/src}/v2-transcript-normalizer.js +1 -1
  41. package/{server/lib → node_modules/@ccv/core/src}/voice-pack-events.js +2 -2
  42. package/package.json +11 -97
  43. package/server/_paths.js +28 -7
  44. package/server/i18n.js +60 -0
  45. package/server/interceptor.js +36 -11
  46. package/server/lib/adapters/dingtalk-adapter.js +2 -2
  47. package/server/lib/adapters/discord-adapter.js +2 -2
  48. package/server/lib/adapters/feishu-adapter.js +2 -2
  49. package/server/lib/adapters/wecom-adapter.js +2 -2
  50. package/server/lib/approval-policy.js +29 -0
  51. package/server/lib/{ask-store.js → ask/ask-store.js} +3 -3
  52. package/server/lib/{perm-bridge.js → ask/perm-bridge.js} +5 -5
  53. package/server/lib/chat-queue.js +341 -0
  54. package/server/lib/cli-inject.js +1 -1
  55. package/server/lib/context-watcher.js +3 -3
  56. package/server/lib/create_system_prompt.js +0 -9
  57. package/server/lib/enrich-plan-input.js +1 -1
  58. package/server/lib/ensure-hooks.js +2 -2
  59. package/server/lib/file-access-policy.js +6 -1
  60. package/server/lib/file-api.js +17 -1
  61. package/server/lib/{dingtalk-bridge.js → im/dingtalk-bridge.js} +1 -1
  62. package/server/lib/{im-append-system.js → im/im-append-system.js} +5 -13
  63. package/server/lib/{im-bridge-core.js → im/im-bridge-core.js} +12 -6
  64. package/server/lib/{im-config.js → im/im-config.js} +1 -1
  65. package/server/lib/im/im-deny.js +4 -0
  66. package/server/lib/{im-lang.js → im/im-lang.js} +1 -1
  67. package/server/lib/{im-lock.js → im/im-lock.js} +2 -2
  68. package/server/lib/{im-process-manager.js → im/im-process-manager.js} +1 -1
  69. package/server/lib/{im-senders.js → im/im-senders.js} +1 -1
  70. package/server/lib/{im-skills.js → im/im-skills.js} +2 -2
  71. package/server/lib/interceptor-core.js +8 -5
  72. package/server/lib/kv-cache-analyzer.js +6 -5
  73. package/server/lib/launch-config.js +321 -0
  74. package/server/lib/log-file-utils.js +21 -0
  75. package/server/lib/log-management.js +6 -17
  76. package/server/lib/log-stream.js +1 -1
  77. package/server/lib/log-watcher.js +1 -1
  78. package/server/lib/model-system-prompts.js +1 -1
  79. package/server/lib/prefs-store.js +2 -2
  80. package/server/lib/project-prefs.js +6 -6
  81. package/server/lib/project-state.js +15 -0
  82. package/server/lib/{proxy-retry.js → proxy/proxy-retry.js} +2 -2
  83. package/server/lib/{proxy-stats.js → proxy/proxy-stats.js} +3 -3
  84. package/server/lib/sdk-adapter.js +6 -74
  85. package/server/lib/sdk-manager.js +383 -278
  86. package/server/lib/skills-api.js +1 -1
  87. package/server/lib/spawn-model-resolver.js +21 -6
  88. package/server/lib/stats-worker.js +2 -2
  89. package/server/lib/system-prompt-files.js +11 -10
  90. package/server/lib/system-prompt-render.js +23 -7
  91. package/server/lib/system-prompt-snapshots.js +452 -0
  92. package/server/lib/updater.js +63 -3
  93. package/server/lib/user-prompt-extract.js +5 -5
  94. package/server/lib/v2/adapter.js +3 -3
  95. package/server/lib/v2/conversation-store.js +1 -1
  96. package/server/lib/v2/convert.js +3 -3
  97. package/server/lib/v2/jsonl-read.js +1 -1
  98. package/server/lib/v2/live-feed.js +3 -3
  99. package/server/lib/v2/meta-rows.js +3 -3
  100. package/server/lib/v2/replay.js +1 -1
  101. package/server/lib/v2/session-list.js +1 -1
  102. package/server/lib/v2/session-owner.js +1 -1
  103. package/server/lib/v2/v2-writer.js +26 -1
  104. package/server/lib/v2/verify.js +1 -1
  105. package/server/lib/voice-pack-manager.js +4 -5
  106. package/server/lib/wire-compress.js +1 -1
  107. package/server/proxy.js +5 -5
  108. package/server/pty-manager.js +282 -184
  109. package/server/routes/ask-perm.js +1 -1
  110. package/server/routes/dingtalk.js +3 -3
  111. package/server/routes/events.js +2 -2
  112. package/server/routes/im.js +5 -5
  113. package/server/routes/preferences.js +3 -3
  114. package/server/routes/workspaces.js +1 -1
  115. package/server/server.js +207 -58
  116. package/server/system-prompt-templates/systemPromptModel.md +0 -8
  117. package/server/system-prompt-templates/systemPromptVariables.ar.md +0 -7
  118. package/server/system-prompt-templates/systemPromptVariables.da.md +0 -7
  119. package/server/system-prompt-templates/systemPromptVariables.de.md +0 -7
  120. package/server/system-prompt-templates/systemPromptVariables.es.md +0 -7
  121. package/server/system-prompt-templates/systemPromptVariables.fr.md +0 -7
  122. package/server/system-prompt-templates/systemPromptVariables.it.md +0 -7
  123. package/server/system-prompt-templates/systemPromptVariables.ja.md +0 -7
  124. package/server/system-prompt-templates/systemPromptVariables.ko.md +0 -7
  125. package/server/system-prompt-templates/systemPromptVariables.md +0 -7
  126. package/server/system-prompt-templates/systemPromptVariables.no.md +0 -7
  127. package/server/system-prompt-templates/systemPromptVariables.pl.md +0 -7
  128. package/server/system-prompt-templates/systemPromptVariables.pt-BR.md +0 -7
  129. package/server/system-prompt-templates/systemPromptVariables.ru.md +0 -7
  130. package/server/system-prompt-templates/systemPromptVariables.th.md +0 -7
  131. package/server/system-prompt-templates/systemPromptVariables.tr.md +0 -7
  132. package/server/system-prompt-templates/systemPromptVariables.uk.md +0 -7
  133. package/server/system-prompt-templates/systemPromptVariables.zh-TW.md +0 -7
  134. package/server/system-prompt-templates/systemPromptVariables.zh.md +0 -7
  135. package/dist/assets/index-D6FmFsIz.js +0 -2
  136. package/dist/assets/seqResourceLoaders-3K77Znll.js +0 -2
  137. package/dist/assets/seqResourceLoaders-BgD_Rwvc.css +0 -41
  138. package/src/utils/apiUrl.js +0 -26
  139. package/src/utils/askFallback.js +0 -21
  140. package/src/utils/askOptionDesc.js +0 -55
  141. package/src/utils/askPortalMatcher.js +0 -54
  142. package/src/utils/autoApproveOptions.js +0 -27
  143. package/src/utils/avatarAnimationPostPass.js +0 -49
  144. package/src/utils/builtinPresets.js +0 -18
  145. package/src/utils/clearCheckpoint.js +0 -7
  146. package/src/utils/commandValidator.js +0 -45
  147. package/src/utils/contextRaw.js +0 -18
  148. package/src/utils/contextSidebarNavigation.js +0 -16
  149. package/src/utils/contextTurns.js +0 -117
  150. package/src/utils/displayScaleHelper.js +0 -47
  151. package/src/utils/effectiveModel.js +0 -17
  152. package/src/utils/entry-slim.js +0 -563
  153. package/src/utils/entryCache.js +0 -144
  154. package/src/utils/errorReport.js +0 -6
  155. package/src/utils/fileExpandedPathsStorage.js +0 -70
  156. package/src/utils/fileIcons.jsx +0 -36
  157. package/src/utils/fileOpen.js +0 -37
  158. package/src/utils/formatters.js +0 -48
  159. package/src/utils/gitApi.js +0 -50
  160. package/src/utils/gitTreeBuilder.js +0 -19
  161. package/src/utils/helpers.js +0 -854
  162. package/src/utils/identityHeal.js +0 -108
  163. package/src/utils/imConnState.js +0 -59
  164. package/src/utils/imOrigin.js +0 -24
  165. package/src/utils/imTr.js +0 -7
  166. package/src/utils/imageCompress.js +0 -83
  167. package/src/utils/imageDownscale.js +0 -28
  168. package/src/utils/imageResize.js +0 -112
  169. package/src/utils/ingestPipeline.js +0 -47
  170. package/src/utils/isProxyMode.js +0 -11
  171. package/src/utils/lazyWithReload.js +0 -53
  172. package/src/utils/markdown.js +0 -30
  173. package/src/utils/markdownIncremental.js +0 -39
  174. package/src/utils/markdownProfiler.js +0 -113
  175. package/src/utils/mdExtensionDetect.js +0 -64
  176. package/src/utils/memoryLinkParser.js +0 -44
  177. package/src/utils/modalMask.js +0 -15
  178. package/src/utils/modelSuggestions.js +0 -48
  179. package/src/utils/monotime.js +0 -6
  180. package/src/utils/pinnedMenu.js +0 -55
  181. package/src/utils/presetShortcuts.js +0 -14
  182. package/src/utils/projectAlias.js +0 -135
  183. package/src/utils/promptClassifier.js +0 -112
  184. package/src/utils/promptDetect.js +0 -196
  185. package/src/utils/promptNav.js +0 -60
  186. package/src/utils/ptyChunkBuilder.js +0 -251
  187. package/src/utils/quickMenuHoverIntent.js +0 -35
  188. package/src/utils/rateLimitParser.js +0 -119
  189. package/src/utils/readResultPool.js +0 -106
  190. package/src/utils/refreshCachedItemProp.js +0 -30
  191. package/src/utils/resizeCalc.js +0 -21
  192. package/src/utils/resolveLocalized.js +0 -17
  193. package/src/utils/searchApi.js +0 -47
  194. package/src/utils/searchReplace.js +0 -51
  195. package/src/utils/seqResourceLoaders.js +0 -79
  196. package/src/utils/sessionManager.js +0 -482
  197. package/src/utils/sessionMerge.js +0 -239
  198. package/src/utils/skillModalController.js +0 -111
  199. package/src/utils/skillsParser.js +0 -116
  200. package/src/utils/slashCommandLabels.js +0 -80
  201. package/src/utils/splitDragCalc.js +0 -73
  202. package/src/utils/stickyBottomController.js +0 -643
  203. package/src/utils/svgSanitize.js +0 -48
  204. package/src/utils/systemTags.js +0 -47
  205. package/src/utils/tClaude.js +0 -20
  206. package/src/utils/teamModalBuilder.js +0 -351
  207. package/src/utils/teamSessionParser.js +0 -190
  208. package/src/utils/teammateAvatars.js +0 -194
  209. package/src/utils/termDiag.js +0 -114
  210. package/src/utils/terminalClipboard.js +0 -117
  211. package/src/utils/terminalWriteQueue.js +0 -398
  212. package/src/utils/thinkingMerge.js +0 -49
  213. package/src/utils/toolCatalog.js +0 -30
  214. package/src/utils/toolResultBuilder.js +0 -411
  215. package/src/utils/toolResultClassifier.js +0 -16
  216. package/src/utils/toolResultCore.js +0 -194
  217. package/src/utils/toolsDiff.js +0 -44
  218. package/src/utils/toolsXmlFormatter.js +0 -3
  219. package/src/utils/ultraplanController.js +0 -109
  220. package/src/utils/ultraplanExperts.js +0 -86
  221. package/src/utils/ultraplanTemplates.js +0 -154
  222. package/src/utils/userImageRefs.js +0 -49
  223. package/src/utils/v3Assembler.js +0 -125
  224. package/src/utils/v3Rows.js +0 -49
  225. package/src/utils/voicePackPlayer.js +0 -233
  226. package/src/utils/webSearchGrouping.js +0 -111
  227. package/src/utils/workflowFormat.js +0 -52
  228. package/src/utils/workflowRuns.js +0 -118
  229. package/src/utils/workflowStore.js +0 -94
  230. /package/{src/utils → node_modules/@ccv/core/src}/requestType.js +0 -0
  231. /package/server/lib/{ask-bridge.js → ask/ask-bridge.js} +0 -0
  232. /package/server/lib/{ask-constants.js → ask/ask-constants.js} +0 -0
  233. /package/server/lib/{ask-reaper.js → ask/ask-reaper.js} +0 -0
  234. /package/server/lib/{dingtalk-config.js → im/dingtalk-config.js} +0 -0
  235. /package/server/lib/{im-log-watcher.js → im/im-log-watcher.js} +0 -0
  236. /package/server/lib/{proxy-env.js → proxy/proxy-env.js} +0 -0
  237. /package/server/lib/{proxy-errors.js → proxy/proxy-errors.js} +0 -0
@@ -1,239 +0,0 @@
1
- // Wire format 协议详见 docs/WIRE_FORMAT.md(服务端 entry 形态 / 关键字段 / 已知特殊窗口)
2
- // messageFingerprint / findReverseAnchor moved VERBATIM to the shared client-safe
3
- // module server/lib/session-boundary.js (wire-v2 S1) — re-exported below so the
4
- // existing test/consumer API of this module is unchanged.
5
- import { isPostClearCheckpoint, messageFingerprint, findReverseAnchor } from '../../server/lib/session-boundary.js';
6
- import { getEffectiveModel } from './effectiveModel.js';
7
-
8
- export { messageFingerprint };
9
-
10
- /**
11
- * merge 入口守卫(KEEP IN SYNC: server/lib/delta-reconstructor.js 标记写入点):
12
- * 重建层标记的脏条目不得进入 mainAgentSessions 合并——
13
- * - `_staleReorder`:完成序倒置的乱序条目(内容已被更新条目取代);
14
- * - `_reconstructBroken`:重建结果与 _totalMessageCount 不符且无法修复(拼接会翻倍/错位);
15
- * - 批量路径额外跳过 `inProgress`:孤立占位条目的 body.messages 是裸 delta 切片
16
- * (批量 reconstructEntries 不为 inProgress 重建全量),merge 会触发 rebuild 截断。
17
- * SSE 实时路径不拦 inProgress——watcher 增量重建器已为其拼出全量 messages,
18
- * 无 live-port 配置下"提问气泡请求时即显示"依赖这一行为。
19
- * AppBase 的 SSE 与批量两个 merge 入口、以及单测共用此谓词,防三处逻辑漂移。
20
- *
21
- * 批量路径的降级例外见 shouldDegradeBrokenMerge:当 broken 条目恰好是它那个
22
- * session 的唯一载体(slim 只留最后一条 main)时,其 messages 是可信前缀,
23
- * 降级合并比整会话空白更可取(会话打 _partialData 标记,ChatView 显示提示条)。
24
- *
25
- * @param {object} entry
26
- * @param {object} [options]
27
- * @param {boolean} [options.batch=false] - 批量(强刷/历史加载)路径
28
- * @returns {boolean} true = 该条目不应参与 session 合并
29
- */
30
- export function isMergeBlockedEntry(entry, options = {}) {
31
- if (!entry) return true;
32
- if (entry._staleReorder || entry._reconstructBroken) return true;
33
- if (options.batch && entry.inProgress) return true;
34
- return false;
35
- }
36
-
37
- /**
38
- * 批量路径降级例外:broken 载体能否以"可信前缀"身份合并进 mainAgentSessions。
39
- *
40
- * 仅在两条安全路径放行(两条都只会走进 mergeMainAgentSessions 的创建分支,
41
- * 不会触发 anchor/rebuild 等可能翻倍的对齐逻辑):
42
- * - prevSessions 为空:会话直接创建,无对齐风险;
43
- * - 双方都有 _seqEpoch 且不同:确定性会话边界,走 epochChanged append 分支
44
- * (三部分守卫防 v1 无 epoch 的条目被误放行)。
45
- *
46
- * `_staleReorder` 明确不降级(流内稍后有权威副本自建会话);inProgress 不降级
47
- * (批量下其 messages 是裸切片,不是可信前缀)。v1 / 无 epoch 会话的 broken
48
- * 载体也刻意不降级(放松守卫会让 broken 条目掉进同会话分支,错位前缀 → 翻倍)。
49
- *
50
- * @param {object} entry
51
- * @param {Array} prevSessions - 当前已合并的 sessions
52
- * @returns {boolean} true = 允许降级合并
53
- */
54
- export function shouldDegradeBrokenMerge(entry, prevSessions) {
55
- if (!entry || !entry._reconstructBroken) return false;
56
- if (entry._staleReorder || entry.inProgress) return false;
57
- if (!Array.isArray(prevSessions) || prevSessions.length === 0) return true;
58
- const e = entry._seqEpoch;
59
- if (!e) return false; // v1 / epoch-less carriers never degrade
60
- // Reject when ANY already-merged session shares the entry's epoch (a same-
61
- // session broken carrier preceded by a healthy one must not degrade into the
62
- // same-session merge branch). Today batches are single-session per epoch, so
63
- // this is a defensive closure for future non-monotonic epoch streams.
64
- return prevSessions.every((s) => !s._seqEpoch || s._seqEpoch !== e);
65
- }
66
-
67
- /**
68
- * 增量合并 mainAgent sessions。
69
- *
70
- * 核心算法:反向锚点对齐。以 `newMessages[0]` 为锚点,从 `lastSession.messages` 末尾
71
- * 反向扫;命中后多块连续 fp 等价校验。三种结果:
72
- * - 命中且 overlapLen === newLen:流式 no-op / suffix subset,messages 引用稳定(保 WeakMap 缓存)。
73
- * - 命中且 overlapLen < newLen:push `newMessages[overlapLen..]`,引用稳定。
74
- * - 未命中:newLen<curLen → rebuild(/compact summary);newLen===curLen → 整段 append(Plan Mode 全新片段);
75
- * newLen>curLen → 严格前缀扩展语义(push tail),fp 加固后真正存在重叠的窗口必被 anchor 命中。
76
- * - 降级 partial 基底(lastSession._partialData)且 anchor 未命中:任何长度的全量条目都整段替换
77
- * (partial 前缀有中段缺口、不可信,push tail 会翻倍)。
78
- *
79
- * 顶部守卫(isPostClearCheckpoint / userId / transient filter)维持 1.6.245 行为不变。
80
- *
81
- * @param {Array} prevSessions
82
- * @param {object} entry
83
- * @param {object} [options]
84
- * @param {boolean} [options.skipTransientFilter=false] - SSE 实时追加路径设为 true。
85
- * @returns {Array}
86
- */
87
- export function mergeMainAgentSessions(prevSessions, entry, options = {}) {
88
- const newMessages = entry.body.messages;
89
- const newResponse = entry.response;
90
- const userId = entry.body.metadata?.user_id || null;
91
- // Task B — v2 session identity. `_seqEpoch` = "v2:<sid>" (adapter, main
92
- // entries only). When it changes, this is unambiguously a DIFFERENT session,
93
- // regardless of user_id or message count — a definitive boundary that the
94
- // heuristics below (same-user merge, transient filter) would otherwise miss
95
- // when a short prior session precedes the current one (the cold-load
96
- // fallback → live supersede case). null-safe: v1 leftovers have no epoch.
97
- const seqEpoch = entry._seqEpoch || null;
98
-
99
- const entryTimestamp = entry.timestamp || null;
100
- // Session-level model identity: ChatView falls back to it when per-message producer resolution
101
- // is null (e.g. the session's carrier entry is still in-flight and filtered out) — prevents the
102
- // "MainAgent" + generic-avatar flash on carried-over history. Latest wins: an inProgress carrier
103
- // stamps body.model, completion re-stamps the authoritative response.body.model.
104
- const entryModel = getEffectiveModel(entry);
105
-
106
- if (prevSessions.length === 0) {
107
- return [{
108
- userId, messages: newMessages, response: newResponse, entryTimestamp, model: entryModel, _seqEpoch: seqEpoch,
109
- // Degraded-broken carrier: truthful prefix with a mid-session hole. Only
110
- // the create branches carry it (a same-session merge onto a partial base
111
- // is defused by the whole-replace below); ChatView renders the banner.
112
- ...(entry._partialData === true && { _partialData: true }),
113
- }];
114
- }
115
-
116
- const lastSession = prevSessions[prevSessions.length - 1];
117
-
118
- const prevMsgCount = lastSession.messages ? lastSession.messages.length : 0;
119
- const isNewConversation = prevMsgCount > 0 && newMessages.length < prevMsgCount * 0.5 && (prevMsgCount - newMessages.length) > 4;
120
- const sameUser = userId !== null && userId === lastSession.userId;
121
- // Three-part guard (NEVER two-part): only split when BOTH sides carry an
122
- // epoch and they differ — `undefined !== "v2:x"` would wrongly split a v1
123
- // leftover or a main-looking sub that lacks _seqEpoch.
124
- const epochChanged = !!(seqEpoch && lastSession._seqEpoch && seqEpoch !== lastSession._seqEpoch);
125
-
126
- // /clear 后的首个 checkpoint:始终是新会话起点。
127
- // 同 device 下 sameUser 永远 true,否则会被下面的 same-session 分支吞掉;
128
- // 也不能被 transient 过滤掉(即便 newMessages.length === 1)。
129
- // epoch 变化语义等同 post-clear(确定性会话边界),走同一 append 分支且绕过
130
- // 下方 transient 过滤(短的新会话也不能被丢弃,否则与 batch 腿分歧)。
131
- if (isPostClearCheckpoint(entry, prevMsgCount) || epochChanged) {
132
- for (let i = 0; i < newMessages.length; i++) {
133
- if (!newMessages[i]._timestamp) newMessages[i]._timestamp = entryTimestamp;
134
- }
135
- return [...prevSessions, {
136
- userId, messages: newMessages, response: newResponse, entryTimestamp, model: entryModel, _seqEpoch: seqEpoch,
137
- ...(entry._partialData === true && { _partialData: true }),
138
- }];
139
- }
140
-
141
- if (!options.skipTransientFilter && isNewConversation && newMessages.length <= 4 && prevMsgCount > 4) {
142
- return prevSessions;
143
- }
144
-
145
- if (sameUser || (userId === lastSession.userId && !isNewConversation)) {
146
- const curLen = prevMsgCount;
147
- const newLen = newMessages.length;
148
- if (!lastSession.messages) lastSession.messages = [];
149
-
150
- // fp 缓存:预算 newMessages 的 fp 数组一次,传给 findReverseAnchor 避免多块连续校验
151
- // 时反复调用 messageFingerprint(流式 5000+ 次/秒 SSE 路径节省 25-100ms 累计延迟)。
152
- // curFpsCache 与 findReverseAnchor 共享:anchor miss 时等长分支的对位比较直接复用
153
- // 反向扫已经算过的 curMsgs fp,零重复计算。
154
- const newFps = newLen > 0 ? newMessages.map(messageFingerprint) : null;
155
- const curFpsCache = new Array(curLen);
156
- const anchor = findReverseAnchor(newMessages, lastSession.messages, newFps, curFpsCache);
157
-
158
- if (anchor) {
159
- const tailStart = anchor.overlapLen;
160
- // tailStart === newLen:流式 no-op / suffix subset,messages 引用不动。
161
- if (tailStart < newLen) {
162
- for (let i = tailStart; i < newLen; i++) {
163
- if (!newMessages[i]._timestamp) newMessages[i]._timestamp = entryTimestamp;
164
- lastSession.messages.push(newMessages[i]);
165
- }
166
- }
167
- // Anchor 对齐成功:新全量条目与 partial 前缀吻合,数据已恢复完整。
168
- delete lastSession._partialData;
169
- } else if (lastSession._partialData) {
170
- // 降级合并的 partial 会话(中段缺口,前缀不可信):anchor 未命中时任何
171
- // 同长/更长的全量条目都是权威真值,整段替换而非前缀扩展——否则会 push 出
172
- // 尾部重复(partial 里已含 416..418,新全量 419 会被 append 一遍)。
173
- for (let i = 0; i < newLen; i++) {
174
- if (!newMessages[i]._timestamp) newMessages[i]._timestamp = entryTimestamp;
175
- }
176
- lastSession.messages = newMessages;
177
- delete lastSession._partialData;
178
- } else if (newLen < curLen) {
179
- // /compact summary 等真重建:替换 messages 引用。
180
- for (let i = 0; i < newLen; i++) {
181
- if (!newMessages[i]._timestamp) newMessages[i]._timestamp = entryTimestamp;
182
- }
183
- lastSession.messages = newMessages;
184
- } else if (newLen === curLen) {
185
- // 等长且 anchor 未命中:两种形态必须区分(docs/WIRE_FORMAT.md §3.1 / §3.7)——
186
- // (a) Plan Mode 全新短窗口:内容与累积历史无关,整段 append 保留历史;
187
- // (b) 同会话近似拷贝:末位原地替换 / 中段编辑,但服务端信号缺失(完成序倒置让无信号
188
- // 的 stale checkpoint 后落盘、或旧日志无信号)。此形态整段 append 会让对话翻倍
189
- // (mainAgent 整段重复 bug 的翻倍终点),必须替换。
190
- let aligned = 0;
191
- // 严格多数(非简单 plurality):对位 fp 相等数 ≥ floor(N/2)+1 才判近似拷贝→替换。
192
- // 近似拷贝逐位几乎全等;Plan Mode 新窗口对位相等 ≈ 0。取严格多数是替换误判的
193
- // 安全边界——N=2→需 2 条全等、N=3→2、N=4→3;宁可对"一半相同"的模糊形态保守
194
- // append(残余形态:末位短暂陈旧),也不冒错杀新窗口历史的风险。调阈值前先想清
195
- // 反例:近拷贝带 1 条中段编辑(N=2 时 1/2 不足多数 → append → 翻倍回归)。
196
- const STRICT_MAJORITY = Math.floor(newLen / 2) + 1;
197
- for (let i = 0; i < newLen && aligned < STRICT_MAJORITY; i++) {
198
- let cfp = curFpsCache[i];
199
- if (cfp === undefined) {
200
- cfp = messageFingerprint(lastSession.messages[i]);
201
- curFpsCache[i] = cfp;
202
- }
203
- if (newFps[i] === cfp) aligned++;
204
- }
205
- if (aligned >= STRICT_MAJORITY) {
206
- // 近似拷贝 → 整段替换(等价于无信号版 in-place replace)。
207
- // 引用更换会使 ChatView WeakMap 渲染缓存失效一次性重渲染,预期内。
208
- for (let i = 0; i < newLen; i++) {
209
- if (!newMessages[i]._timestamp) newMessages[i]._timestamp = entryTimestamp;
210
- }
211
- lastSession.messages = newMessages;
212
- } else {
213
- // Plan Mode 2-msg 全替换窗口,整段 append。
214
- for (let i = 0; i < newLen; i++) {
215
- if (!newMessages[i]._timestamp) newMessages[i]._timestamp = entryTimestamp;
216
- lastSession.messages.push(newMessages[i]);
217
- }
218
- }
219
- } else {
220
- // newLen > curLen 且 anchor (单点 newMsgs[0]) 未命中:保守回退至"严格前缀扩展"语义——
221
- // 只 push newMessages[curLen..]。fp 加固后真正存在重叠的窗口必被 anchor 命中走上面分支;
222
- // 这里 anchor 未命中意味着确实无重叠(罕见,仅艺测/再快照场景),保留旧推 tail 行为防回归。
223
- for (let i = curLen; i < newLen; i++) {
224
- if (!newMessages[i]._timestamp) newMessages[i]._timestamp = entryTimestamp;
225
- lastSession.messages.push(newMessages[i]);
226
- }
227
- }
228
-
229
- lastSession.response = newResponse;
230
- lastSession.entryTimestamp = entryTimestamp;
231
- if (entryModel) lastSession.model = entryModel; // latest wins; model-less entries keep the stamp
232
- return [...prevSessions];
233
- } else {
234
- return [...prevSessions, {
235
- userId, messages: newMessages, response: newResponse, entryTimestamp, model: entryModel, _seqEpoch: seqEpoch,
236
- ...(entry._partialData === true && { _partialData: true }),
237
- }];
238
- }
239
- }
@@ -1,111 +0,0 @@
1
- // Skill 管理弹窗的「开关 / 永久删除」共享控制器。
2
- // AppHeader 与 Mobile 是同形态的类组件(同样的 state._skillsModal / state._fsSkills、
3
- // this.setState、this.reloadFsSkills()),原本各持一份逐字相同的 handler,极易漂移
4
- // (评审已发现 Mobile 漏了一条镜像注释)。这里抽成以组件实例为 host 的纯函数,两端只留一行委派。
5
- // ImSkillsModal 是函数组件 + IM 端点 + _tr 文案,状态形态不同,刻意不并入。
6
- import { message } from 'antd';
7
- import { apiUrl } from './apiUrl';
8
- import { t } from '../i18n';
9
- import { skillKey, skillOrderKey } from './skillsParser';
10
-
11
- // 把 reload 的权威结果按当前显示顺序(skillOrderKey 保位)合并回 _skillsModal.skills。
12
- // skillOrderKey 只取 source+name(开关/删除都不改它),所以行原地不动;modal 没见过的新条目排末尾。
13
- export function mergePreservingOrder(prevSkills, resultSkills) {
14
- const orderMap = new Map(prevSkills.map((s, i) => [skillOrderKey(s), i]));
15
- return [...resultSkills].sort((a, b) => {
16
- const ai = orderMap.get(skillOrderKey(a));
17
- const bi = orderMap.get(skillOrderKey(b));
18
- if (ai === undefined && bi === undefined) return 0;
19
- if (ai === undefined) return 1;
20
- if (bi === undefined) return -1;
21
- return ai - bi;
22
- });
23
- }
24
-
25
- // 切换 skill 启用状态:乐观翻 Switch + 失败回滚 + reload 保位对齐。
26
- export async function handleSkillToggle(host, skill) {
27
- const key = skillKey(skill);
28
- if (host.state._skillsModal?.toggling?.has(key)) return;
29
- const enable = !skill.enabled;
30
- // 用 skillKey(含 path)匹配——同名同 source 的重复两份路径不同,只翻被点那行,不牵连孪生。
31
- const flipEnabled = (target) => (s) => (skillKey(s) === key) ? { ...s, enabled: target } : s;
32
- host.setState(prev => {
33
- const next = new Set(prev._skillsModal.toggling); next.add(key);
34
- return { _skillsModal: { ...prev._skillsModal, toggling: next, skills: prev._skillsModal.skills.map(flipEnabled(enable)) } };
35
- });
36
- try {
37
- const r = await fetch(apiUrl('/api/skills/toggle'), {
38
- method: 'POST',
39
- headers: { 'Content-Type': 'application/json' },
40
- body: JSON.stringify({ source: skill.source, name: skill.name, enable }),
41
- });
42
- const data = await r.json();
43
- if (!r.ok) {
44
- host.setState(prev => ({ _skillsModal: { ...prev._skillsModal, skills: prev._skillsModal.skills.map(flipEnabled(!enable)) } }));
45
- if (data.code === 'DUPLICATE') message.error(t('ui.skillToggleDuplicate', { name: skill.name }));
46
- else message.error(t('ui.skillToggleFailed', { reason: data.error || 'unknown' }));
47
- return;
48
- }
49
- // 乐观翻 _fsSkills(popover chip 源)里这条的 enabled——reload 失败也能立即反映用户动作;
50
- // reload 成功会用权威数据覆盖。
51
- host.setState(prev => ({
52
- _fsSkills: Array.isArray(prev._fsSkills)
53
- ? prev._fsSkills.map(s => (skillKey(s) === key) ? { ...s, enabled: enable } : s)
54
- : prev._fsSkills,
55
- }));
56
- const result = await host.reloadFsSkills();
57
- if (result.ok) {
58
- host.setState(prev => ({ _skillsModal: { ...prev._skillsModal, skills: mergePreservingOrder(prev._skillsModal.skills, result.skills) } }));
59
- }
60
- } catch (e) {
61
- host.setState(prev => ({ _skillsModal: { ...prev._skillsModal, skills: prev._skillsModal.skills.map(flipEnabled(!enable)) } }));
62
- message.error(t('ui.skillToggleFailed', { reason: e.message }));
63
- } finally {
64
- host.setState(prev => {
65
- const next = new Set(prev._skillsModal.toggling); next.delete(key);
66
- return { _skillsModal: { ...prev._skillsModal, toggling: next } };
67
- });
68
- }
69
- }
70
-
71
- // 永久删除单个 skill:成功后乐观移除该行 + reload 保位对齐。
72
- // reload 合并回 _skillsModal.skills 很关键:否则删掉「重复对」其中一份后,存活那行仍带旧
73
- // duplicate ⚠ 徽标,要等关弹窗重开才消失(listSkills 重算后该标记自然清掉)。
74
- export async function handleSkillDelete(host, skill) {
75
- const key = skillKey(skill);
76
- // 复用 toggling Set 作「该行操作进行中」标记:防二次确认后的连点重入,
77
- // 并让 SkillsManagerModal 在删除期间给删除按钮转圈 + 禁用同行 Switch(与 toggle 一致的忙态反馈)。
78
- if (host.state._skillsModal?.toggling?.has(key)) return;
79
- host.setState(prev => {
80
- const next = new Set(prev._skillsModal.toggling); next.add(key);
81
- return { _skillsModal: { ...prev._skillsModal, toggling: next } };
82
- });
83
- try {
84
- const r = await fetch(apiUrl('/api/skills/delete'), {
85
- method: 'POST',
86
- headers: { 'Content-Type': 'application/json' },
87
- body: JSON.stringify({ source: skill.source, name: skill.name, enabled: skill.enabled }),
88
- });
89
- const data = await r.json().catch(() => ({}));
90
- if (!r.ok) {
91
- message.error(t('ui.skillDeleteFailed', { reason: data.error || 'unknown' }));
92
- return;
93
- }
94
- message.success(t('ui.skillDeleteSuccess', { name: skill.name }));
95
- host.setState(prev => ({
96
- _skillsModal: { ...prev._skillsModal, skills: (prev._skillsModal.skills || []).filter(s => skillKey(s) !== key) },
97
- _fsSkills: Array.isArray(prev._fsSkills) ? prev._fsSkills.filter(s => skillKey(s) !== key) : prev._fsSkills,
98
- }));
99
- const result = await host.reloadFsSkills();
100
- if (result.ok) {
101
- host.setState(prev => ({ _skillsModal: { ...prev._skillsModal, skills: mergePreservingOrder(prev._skillsModal.skills, result.skills) } }));
102
- }
103
- } catch (e) {
104
- message.error(t('ui.skillDeleteFailed', { reason: e.message }));
105
- } finally {
106
- host.setState(prev => {
107
- const next = new Set(prev._skillsModal.toggling); next.delete(key);
108
- return { _skillsModal: { ...prev._skillsModal, toggling: next } };
109
- });
110
- }
111
- }
@@ -1,116 +0,0 @@
1
- // Claude Code 硬编码的 builtin skill 名单(本地无文件、不可禁用)。
2
- // 与 server/lib/skills-api.js 的 BUILTIN_NAMES 保持一致;前端用于 popover chip 过滤。
3
- export const BUILTIN_SKILL_NAMES = new Set([
4
- 'update-config', 'keybindings-help', 'simplify', 'fewer-permission-prompts',
5
- 'loop', 'schedule', 'claude-api', 'init', 'review', 'security-review',
6
- ]);
7
-
8
- // 把 /api/skills 返回的 skill 对象映射到 Claude Code system-reminder 里的显示名。
9
- // 插件 skill: `<pluginShort>:<skillDir>`;其它源用裸名。pluginName 形如 `short@marketplace`。
10
- export function skillToDisplayName(s) {
11
- if (!s || typeof s.name !== 'string') return '';
12
- if (s.source === 'plugin' && typeof s.pluginName === 'string' && s.pluginName.includes('@')) {
13
- const short = s.pluginName.split('@')[0];
14
- if (short) return `${short}:${s.name}`;
15
- }
16
- return s.name;
17
- }
18
-
19
- // Skill 的稳定唯一标识——用于开关弹窗的 toggling Set key、乐观更新匹配、reload 排序 orderMap、
20
- // 以及 React 渲染 key。可切换 skill(user/project)用其磁盘 path:同名两份分别住在 skills/ 与
21
- // skills-skip/,路径天然不同,避免「同名同 source」共用 key 导致两行串台(一起转圈/一起翻转)。
22
- // builtin(path:null)回落 source-name(名字唯一),仍稳定,供只读 chip 的排序兜底。
23
- export function skillKey(s) {
24
- return (s && s.path) ? s.path : `${s?.source}-${s?.name}`;
25
- }
26
-
27
- // 跨「开关」稳定的标识——只用 source+name(开关把 skill 在 skills/ ↔ skills-skip/ 间搬,path 会变,
28
- // 但 source/name 不变)。专用于「toggle 后保位」:开关一个 skill 后它在列表里原地不动,不会因为 path
29
- // 变了而被 orderMap 当成新条目甩到末尾。与 skillKey(含 path、用于重复态去重)刻意不同。
30
- export function skillOrderKey(s) {
31
- return `${s?.source}-${s?.name}`;
32
- }
33
-
34
- // 弹窗「默认排序」——仅在打开面板时套用(开关期间不重排,避免抖动让用户找不到 skill)。
35
- // 规则:项目级(project) 排在用户级(user) 之前,其余源其后;同源按名字 localeCompare 保证确定性
36
- // (彻底消除每次刷新 readdir 顺序漂移导致的乱序)。返回新数组,不改入参。
37
- const SKILL_SOURCE_RANK = { project: 0, user: 1, plugin: 2, builtin: 3 };
38
- export function sortSkillsDefault(skills) {
39
- if (!Array.isArray(skills)) return [];
40
- return [...skills].sort((a, b) => {
41
- const ra = SKILL_SOURCE_RANK[a?.source] ?? 9;
42
- const rb = SKILL_SOURCE_RANK[b?.source] ?? 9;
43
- if (ra !== rb) return ra - rb;
44
- return String(a?.name || '').localeCompare(String(b?.name || ''));
45
- });
46
- }
47
-
48
- // 把 /api/skills 的权威结果与历史 system-reminder 解析结果合并为「当前在用」chip 列表。
49
- // - fsSkills=null → 返回 null,由调用方回退到历史解析
50
- // - 只保留 enabled=true 且非 builtin 的条目
51
- // - description 优先 fs(SKILL.md frontmatter),缺失时回退历史 system-reminder 的 desc,再缺则空串
52
- // - 按显示名去重(user/project 同名只保留最后一条;plugin 因为带前缀天然不冲突)
53
- export function mergeActiveSkills(fsSkills, historicalSkills) {
54
- if (!Array.isArray(fsSkills)) return null;
55
- const descMap = new Map((historicalSkills || []).map(s => [s.name, s.description]));
56
- const out = new Map();
57
- for (const s of fsSkills) {
58
- if (!s || !s.enabled || s.source === 'builtin') continue;
59
- const name = skillToDisplayName(s);
60
- if (!name || BUILTIN_SKILL_NAMES.has(name)) continue;
61
- out.set(name, { name, description: s.description || descMap.get(name) || '' });
62
- }
63
- return [...out.values()];
64
- }
65
-
66
- // 从 <system-reminder> 内部文本里抽出 skills 列表。
67
- // 识别 header 句 "skills are available for use with the Skill tool" 后,按行扫描
68
- // `- <name>: <desc>` 格式;name 内允许冒号(plugin:foo / skill-creator:skill-creator
69
- // 都按原样保留),按首个 ': '(冒号+空格)切分。description 可跨多行,遇空行 / 下一个 `- `
70
- // flush;遇非 dash 非续行非空文本视为列表结束。
71
- //
72
- // 纯函数,无传递依赖 —— 可直接被 node --test 导入(避免 src/utils/contentFilter.js
73
- // 里缺 .js 后缀的 import 引起的 Node ESM resolver 失败)。
74
- export function parseLoadedSkills(innerText) {
75
- if (typeof innerText !== 'string') return [];
76
- const headerIdx = innerText.indexOf('skills are available for use with the Skill tool');
77
- if (headerIdx < 0) return [];
78
- const lineStart = innerText.indexOf('\n', headerIdx);
79
- if (lineStart < 0) return [];
80
- const tail = innerText.slice(lineStart + 1);
81
- const lines = tail.split(/\r?\n/);
82
- const skills = [];
83
- let current = null;
84
- const flush = () => {
85
- if (current && current.name) skills.push(current);
86
- current = null;
87
- };
88
- for (const line of lines) {
89
- if (/^-\s+/.test(line)) {
90
- flush();
91
- const rest = line.replace(/^-\s+/, '');
92
- const colonSpaceIdx = rest.indexOf(': ');
93
- if (colonSpaceIdx > 0) {
94
- current = {
95
- name: rest.slice(0, colonSpaceIdx).trim(),
96
- description: rest.slice(colonSpaceIdx + 2).trim(),
97
- };
98
- } else {
99
- const name = rest.replace(/:\s*$/, '').trim();
100
- current = { name, description: '' };
101
- }
102
- } else if (current && line.trim()) {
103
- // 续行:保留换行符。弹窗用 white-space: pre-wrap 渲染,
104
- // 让多段落 description(如 test-spec-designer 的触发场景/关键词)保持可读的结构。
105
- current.description = current.description
106
- ? current.description + '\n' + line.trim()
107
- : line.trim();
108
- } else if (!line.trim()) {
109
- flush();
110
- } else {
111
- break;
112
- }
113
- }
114
- flush();
115
- return skills;
116
- }
@@ -1,80 +0,0 @@
1
- import { t } from '../i18n.js';
2
-
3
- // 命令清单与 Claude Code CLI upstream 公开 slash 命令对齐;upstream 增删命令时
4
- // 需手动同步本表 + src/i18n.js 的 "ui.slashCommand.*" 翻译块。未在表中的命令
5
- // (含用户自定义 ~/.claude/commands/*.md)→ 返回 null → 调用方回落到原文渲染。
6
- // 命令大小写敏感(与 CLI 保持一致),exact-match 不做大小写规约。
7
- // 翻译歧义点见 src/i18n.js "ui.slashCommand.*" 块顶部注释(单一信息源)。
8
- const COMMAND_KEYS = Object.freeze({
9
- '/clear': 'ui.slashCommand.clear',
10
- '/compact': 'ui.slashCommand.compact',
11
- '/theme': 'ui.slashCommand.theme',
12
- '/cost': 'ui.slashCommand.cost',
13
- '/usage': 'ui.slashCommand.usage',
14
- '/context': 'ui.slashCommand.context',
15
- '/model': 'ui.slashCommand.model',
16
- '/effort': 'ui.slashCommand.effort',
17
- '/login': 'ui.slashCommand.login',
18
- '/logout': 'ui.slashCommand.logout',
19
- '/status': 'ui.slashCommand.status',
20
- '/help': 'ui.slashCommand.help',
21
- '/init': 'ui.slashCommand.init',
22
- '/agents': 'ui.slashCommand.agents',
23
- '/config': 'ui.slashCommand.config',
24
- '/memory': 'ui.slashCommand.memory',
25
- '/permissions': 'ui.slashCommand.permissions',
26
- '/hooks': 'ui.slashCommand.hooks',
27
- '/plugins': 'ui.slashCommand.plugins',
28
- '/release-notes': 'ui.slashCommand.releaseNotes',
29
- '/upgrade': 'ui.slashCommand.upgrade',
30
- '/bug': 'ui.slashCommand.bug',
31
- '/doctor': 'ui.slashCommand.doctor',
32
- '/mcp': 'ui.slashCommand.mcp',
33
- '/vim': 'ui.slashCommand.vim',
34
- '/export': 'ui.slashCommand.export',
35
- '/pr-comments': 'ui.slashCommand.prComments',
36
- '/review': 'ui.slashCommand.review',
37
- '/security-review': 'ui.slashCommand.securityReview',
38
- '/ide': 'ui.slashCommand.ide',
39
- '/resume': 'ui.slashCommand.resume',
40
- '/terminal-setup': 'ui.slashCommand.terminalSetup',
41
- '/migrate-installer': 'ui.slashCommand.migrateInstaller',
42
- });
43
-
44
- // 裸命令名(如 "/model")+ 可选参数(如 "/model opus")。
45
- // 参数前只允许 ASCII 空格/制表符 — 避免 U+2028/U+2029 等异常空白被
46
- // 当成参数分隔符绕过多行守卫。
47
- const COMMAND_PATTERN = /^(\/[a-z][a-z0-9-]*)([ \t]+.*)?$/;
48
- // 拒任意 Unicode 换行(LF / CR / U+2028 LINE SEP / U+2029 PARAGRAPH SEP)。
49
- const ANY_LINEBREAK = new RegExp('[\\n\\r\\u2028\\u2029]');
50
- // bidi-control(U+202A LRE / U+202B RLE / U+202C PDF / U+202D LRO / U+202E RLO /
51
- // U+2066 LRI / U+2067 RLI / U+2068 FSI / U+2069 PDI):进入气泡文本会翻转剩余
52
- // 字符视觉方向,撕裂气泡/相邻消息布局。在拼接 rest 前 strip 掉。
53
- const BIDI_CONTROL = new RegExp('[\\u202A-\\u202E\\u2066-\\u2069]', 'g');
54
-
55
- // 内部 matcher:返回 { cmd, rest } 或 null。两个 export 共享同一套校验逻辑,
56
- // 不引入第二份 regex 路径。
57
- function matchCommand(text) {
58
- if (typeof text !== 'string') return null;
59
- if (ANY_LINEBREAK.test(text)) return null;
60
- const trimmed = text.trim();
61
- const match = COMMAND_PATTERN.exec(trimmed);
62
- if (!match) return null;
63
- const cmd = match[1];
64
- if (!COMMAND_KEYS[cmd]) return null;
65
- return { cmd, rest: (match[2] || '').replace(BIDI_CONTROL, '') };
66
- }
67
-
68
- export function getSlashCommandLabel(text) {
69
- const m = matchCommand(text);
70
- if (!m) return null;
71
- const label = t(COMMAND_KEYS[m.cmd]);
72
- return m.rest ? `${label}${m.rest}` : label;
73
- }
74
-
75
- // 返回裸命令(如 "/model"),用于 Tooltip 等次要展示位,避免参数(可能含
76
- // /login <token> 等敏感数据)在 hover/title 处被泄漏。
77
- export function getSlashCommandTooltip(text) {
78
- const m = matchCommand(text);
79
- return m ? m.cmd : null;
80
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Pure geometry for the ChatView terminal/sidebar drag-resize with snap lines.
3
- * Extracted from ChatView's handleSplitMouseDown/handleSidebarMouseDown so the
4
- * math is unit-testable; the drag lifecycle (document listeners, body cursor)
5
- * lives in chat/controllers/splitDragController.js.
6
- *
7
- * Snap-line object shape is shared by both axes and consumed by
8
- * SnapLineOverlay: { cols, terminalPx, linePosition }. For the sidebar axis
9
- * cols/terminalPx/linePosition are all the same pixel width (legacy shape).
10
- */
11
-
12
- // Terminal font is 13px Menlo/Monaco; a character cell is ~7.8px wide.
13
- export const TERMINAL_CHAR_WIDTH = 7.8;
14
- export const TERMINAL_SNAP_COLS = [60, 80, 100, 120];
15
- export const SIDEBAR_SNAP_WIDTHS = [180, 240, 300, 360];
16
- export const SNAP_THRESHOLD_PX = 60;
17
- export const RESIZER_WIDTH_PX = 5;
18
-
19
- /**
20
- * Snap lines for the terminal split, positioned so the terminal lands on a
21
- * standard column count. Lines outside the usable band (terminal wider than
22
- * 75% or narrower than 15% of the container) are dropped.
23
- */
24
- export function computeTerminalSnapLines(containerWidth, {
25
- charWidth = TERMINAL_CHAR_WIDTH,
26
- cols = TERMINAL_SNAP_COLS,
27
- resizerWidth = RESIZER_WIDTH_PX,
28
- } = {}) {
29
- if (!containerWidth || containerWidth <= 0) return [];
30
- return cols.map(c => {
31
- const terminalPx = c * charWidth;
32
- const totalTerminalWidth = terminalPx + resizerWidth;
33
- if (totalTerminalWidth > containerWidth * 0.75 || totalTerminalWidth < containerWidth * 0.15) return null;
34
- // Line position = container width - terminal pixel width - resizer width.
35
- return { cols: c, terminalPx, linePosition: containerWidth - terminalPx - resizerWidth };
36
- }).filter(snap => snap !== null);
37
- }
38
-
39
- /** Sidebar snap lines; widths at or beyond 40% of the container are dropped. */
40
- export function computeSidebarSnapLines(containerWidth, widths = SIDEBAR_SNAP_WIDTHS) {
41
- if (!containerWidth || containerWidth <= 0) return [];
42
- return widths
43
- .filter(w => w < containerWidth * 0.4)
44
- .map(w => ({ cols: w, terminalPx: w, linePosition: w }));
45
- }
46
-
47
- /** Terminal width from a drag position: right-edge anchored, 200px..75% band. */
48
- export function clampTerminalWidth(rectRight, clientX, containerWidth) {
49
- return Math.max(200, Math.min(containerWidth * 0.75, rectRight - clientX));
50
- }
51
-
52
- /** Sidebar width from a drag position: left-edge anchored, 160px..40% band. */
53
- export function clampSidebarWidth(clientX, rectLeft, containerWidth) {
54
- return Math.max(160, Math.min(containerWidth * 0.4, clientX - rectLeft));
55
- }
56
-
57
- /**
58
- * Nearest snap line to `position` (already in the same coordinate space as
59
- * linePosition), or null when none is within the threshold.
60
- */
61
- export function findActiveSnapLine(position, snapLines, threshold = SNAP_THRESHOLD_PX) {
62
- if (!snapLines || snapLines.length === 0) return null;
63
- let minDistance = Infinity;
64
- let closestSnap = null;
65
- for (const snap of snapLines) {
66
- const distance = Math.abs(position - snap.linePosition);
67
- if (distance < minDistance) {
68
- minDistance = distance;
69
- closestSnap = snap;
70
- }
71
- }
72
- return (closestSnap && minDistance < threshold) ? closestSnap : null;
73
- }