dsh-vscode-mode 0.1.23 → 0.1.26

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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * dsh-vscode-mode client — 右键菜单 viewport 定位纯函数。
3
+ * 将目标行锚点与视口边界计算从 React 渲染层拆出,便于复用和单测。
4
+ * 作者 ddj 2026-08-28
5
+ */
6
+
7
+ export interface MenuPosition {
8
+ left: number
9
+ top: number
10
+ }
11
+
12
+ export interface MenuAnchorRect {
13
+ top: number
14
+ right: number
15
+ width: number
16
+ height: number
17
+ }
18
+
19
+ /**
20
+ * 将菜单坐标限制在 viewport 内。
21
+ * @author ddj 2026年08月28号
22
+ * @param x 期望的 viewport x 坐标
23
+ * @param y 期望的 viewport y 坐标
24
+ * @param viewportWidth viewport 宽度
25
+ * @param viewportHeight viewport 高度
26
+ * @param menuWidth 菜单估算宽度
27
+ * @param menuHeight 菜单估算高度
28
+ * @returns 限制后的菜单位置
29
+ */
30
+ export function clampMenuPosition(
31
+ x: number,
32
+ y: number,
33
+ viewportWidth: number,
34
+ viewportHeight: number,
35
+ menuWidth: number,
36
+ menuHeight: number,
37
+ ): MenuPosition {
38
+ const maxLeft = Math.max(4, viewportWidth - menuWidth)
39
+ const maxTop = Math.max(4, viewportHeight - menuHeight)
40
+ return {
41
+ left: Math.max(4, Math.min(x, maxLeft)),
42
+ top: Math.max(4, Math.min(y, maxTop)),
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 将文件树行右键菜单锚定到目标行右侧;无法测量时回退到鼠标坐标。
48
+ * @author ddj 2026年08月28号
49
+ * @param rect 目标行的布局矩形
50
+ * @param fallbackX 鼠标 viewport x 坐标
51
+ * @param fallbackY 鼠标 viewport y 坐标
52
+ * @returns 菜单初始位置
53
+ */
54
+ export function rowMenuPosition(
55
+ rect: MenuAnchorRect | null | undefined,
56
+ fallbackX: number,
57
+ fallbackY: number,
58
+ ): MenuPosition {
59
+ if (!rect || rect.width <= 0 || rect.height <= 0) {
60
+ return { left: fallbackX, top: fallbackY }
61
+ }
62
+ return { left: rect.right + 4, top: rect.top }
63
+ }
package/src/routes.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * 迁移自原 src/index.ts 的路由部分,路径/行为一字不改。
4
4
  * 作者 ddj 2026-08-20
5
5
  */
6
- import { readFile } from 'node:fs/promises'
6
+ import { readFile, stat } from 'node:fs/promises'
7
7
  import { dirname, extname, join, normalize, resolve, sep } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import { ROUTE_PREFIX, noteOwnRoute, resetOwnRoutes, routeConflict } from './compat.js'
@@ -124,10 +124,11 @@ export function registerRoutes(
124
124
  }
125
125
 
126
126
  // Monaco Editor AMD 构建:/edrv/vendor/* → assets/vendor/*(路径穿越防护 + 按扩展名 MIME)
127
+ // ETag 协商缓存:弱 etag(mtime+size),配合 max-age 让浏览器 304 复用,插件升级内容变化自动失效。
127
128
  ctx.effect(() => guarded({
128
129
  kind: 'prefix',
129
130
  path: VENDOR_PREFIX,
130
- handler: async (req: { method?: string; url?: string }, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string | Uint8Array) => void }) => {
131
+ handler: async (req: { method?: string; url?: string; headers?: Record<string, string | string[] | undefined> }, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string | Uint8Array) => void }) => {
131
132
  try {
132
133
  if (req.method !== 'GET' && req.method !== 'HEAD') {
133
134
  res.statusCode = 405
@@ -143,10 +144,25 @@ export function registerRoutes(
143
144
  res.end()
144
145
  return
145
146
  }
147
+ const info = await stat(target).catch(() => null)
148
+ if (!info || !info.isFile()) { res.statusCode = 404; res.end(); return }
149
+ const etag = 'W/"' + info.mtimeMs.toString(16) + '-' + info.size.toString(16) + '"'
150
+ const reqEtag = (() => {
151
+ const raw = req.headers?.['if-none-match']
152
+ if (Array.isArray(raw)) return raw[0]
153
+ return raw
154
+ })()
155
+ if (reqEtag === etag) {
156
+ res.statusCode = 304
157
+ res.setHeader('etag', etag)
158
+ res.end()
159
+ return
160
+ }
146
161
  const body = await readFile(target)
147
162
  res.statusCode = 200
148
163
  res.setHeader('content-type', VENDOR_MIME[extname(target)] ?? 'application/octet-stream')
149
164
  res.setHeader('cache-control', 'public, max-age=3600')
165
+ res.setHeader('etag', etag)
150
166
  res.end(body)
151
167
  } catch (error) {
152
168
  res.statusCode = 404
package/src/rpc.ts CHANGED
@@ -34,6 +34,8 @@ import { revealInExplorer } from './reveal.js'
34
34
 
35
35
  /** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
36
36
  const debugWriteQueues = new Map<string, Promise<void>>()
37
+ /** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
38
+ const DEBUG_LOG_CAP = 512 * 1024
37
39
 
38
40
  /** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
39
41
  function recView(record: DiffRecord): RecordView {
@@ -300,7 +302,11 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
300
302
  const target = await fs.resolve('.dsh-edit-review-debug.log', { cwd: sc.cwd })
301
303
  const old = await fs.readText(target).catch(() => '')
302
304
  const line = new Date().toISOString() + ' ' + text + '\n'
303
- await fs.writeText(target, (old || '') + line, void 0, void 0, policyOf(ctx, sc.session))
305
+ // 超上限时截断保留尾部:单文件体积有界,每次追加的读改写成本不随会话时长增长
306
+ const next = old.length + line.length > DEBUG_LOG_CAP
307
+ ? old.slice(-Math.floor(DEBUG_LOG_CAP / 2)) + line
308
+ : old + line
309
+ await fs.writeText(target, next, void 0, void 0, policyOf(ctx, sc.session))
304
310
  } catch (e) { /* 写日志失败忽略 */ }
305
311
  })
306
312
  debugWriteQueues.set(sc.cwd, task)