From 6cf053dcd0a178a14cecfadc632497ce227c04cf Mon Sep 17 00:00:00 2001 From: cruldra Date: Tue, 13 Oct 2026 10:54:16 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(vscode):=20=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=A0=91=E5=BC=B9=E6=A1=86=E8=B7=AF=E5=BE=84=E5=A4=AA?= =?UTF-8?q?=E9=95=BF=E6=97=B6=E4=B8=8D=E5=86=8D=E6=8C=A4=E4=B9=B1=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E3=80=81=E6=BA=A2=E5=87=BA=E5=8D=A1=E7=89=87=EF=BC=9A?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=8D=E5=9C=A8=E5=89=8D=E3=80=81=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=B7=A1=E8=89=B2=E7=9C=81=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/codeReader/CallTreeDialog.test.tsx | 39 ++++++++++- .../src/codeReader/CallTreeDialog.tsx | 31 +++++++-- .../webview-ui/src/codeReader/codeReader.css | 68 ++++++++++++++++--- 3 files changed, 119 insertions(+), 19 deletions(-) diff --git a/vscode/webview-ui/src/codeReader/CallTreeDialog.test.tsx b/vscode/webview-ui/src/codeReader/CallTreeDialog.test.tsx index dda6450..e6ea7a4 100644 --- a/vscode/webview-ui/src/codeReader/CallTreeDialog.test.tsx +++ b/vscode/webview-ui/src/codeReader/CallTreeDialog.test.tsx @@ -8,7 +8,7 @@ import { act, useState } from 'react' import { createRoot } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { BlockLevel } from './BlockTree' -import { CallTreeDialog } from './CallTreeDialog' +import { CallTreeDialog, splitWhere } from './CallTreeDialog' vi.mock('./useOccurrences', async () => { const { createContext } = await import('react') @@ -155,7 +155,7 @@ describe('调用树弹框', () => { expect(dialog).not.toBeNull() expect(dialog!.textContent).toContain('调用树') expect(dialog!.textContent).toContain('mask_phone') - expect(dialog!.textContent).toContain('utils/phone.py:28') + expect(dialog!.querySelector('.cr-call-title [data-tooltip="utils/phone.py:28"]')).not.toBeNull() expect(dialog!.textContent).not.toContain('读取用户资料') expect(dialog!.querySelector('[role="tablist"]')).toBeNull() @@ -274,4 +274,39 @@ describe('调用树弹框', () => { await act(async () => rows[1].click()) expect(jumps).toEqual([{ uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 }]) }) + + it('标题和卡片的位置拆成「文件名:行号」和淡色的所在目录,悬停看完整路径;卡片第一行放名字和 detail,第二行只放位置;每层缩进 16px', async () => { + const where = 'backend/src/luffy_agent/agent/runtime/runner.py:1325' + const run = node('run', { + detail: 'class SessionRunner (runner.py)', + where, + children: [node('mask_phone', { current: true, where: 'phone.py:28' })], + }) + await render( {}} onJump={() => {}} />) + const title = document.body.querySelector('.cr-call-title')! + const place = title.querySelector('[data-tooltip]')! + expect(place.dataset.tooltip).toBe(where) + expect(place.querySelector('.cr-call-file')!.textContent).toBe('runner.py:1325') + expect(place.querySelector('.cr-call-dir')!.textContent).toBe('backend/src/luffy_agent/agent/runtime') + + const [outer, inner] = [...document.body.querySelectorAll('button.cr-call-card')] + const lines = (card: Element) => [...card.children].map(line => [...line.querySelectorAll('.cr-call-name, .cr-call-detail, .cr-call-pill, .cr-call-file, .cr-call-dir')].map(part => part.textContent)) + expect(lines(outer)).toEqual([['run', 'class SessionRunner (runner.py)'], ['runner.py:1325', 'backend/src/luffy_agent/agent/runtime']]) + expect(outer.children[1].getAttribute('data-tooltip')).toBe(where) + expect(lines(inner)).toEqual([['mask_phone', '当前'], ['phone.py:28']]) + + const rows = [...document.body.querySelectorAll('.cr-call-row')] + expect(rows.map(row => row.style.paddingLeft)).toEqual(['0px', '16px']) + }) +}) + +describe('位置拆成文件名和所在目录', () => { + it('按最后一个 / 拆:「文件名:行号」和它所在的目录', () => { + expect(splitWhere('backend/src/luffy_agent/agent/runtime/runner.py:1325')).toEqual({ file: 'runner.py:1325', dir: 'backend/src/luffy_agent/agent/runtime' }) + expect(splitWhere('web/src/lib/agents.ts:11')).toEqual({ file: 'agents.ts:11', dir: 'web/src/lib' }) + }) + + it('没有目录时只有文件名', () => { + expect(splitWhere('runner.py:12')).toEqual({ file: 'runner.py:12', dir: '' }) + }) }) diff --git a/vscode/webview-ui/src/codeReader/CallTreeDialog.tsx b/vscode/webview-ui/src/codeReader/CallTreeDialog.tsx index d07e764..1d1cada 100644 --- a/vscode/webview-ui/src/codeReader/CallTreeDialog.tsx +++ b/vscode/webview-ui/src/codeReader/CallTreeDialog.tsx @@ -61,7 +61,7 @@ export function CallTreeDialog({ kind, badge, name, where, status, trees, contra
调用树 {name && {name}} - {where && {where}} + {where && } @@ -117,30 +117,30 @@ function ContractNote({ miss }: { miss: ContractMiss }): ReactElement { } const NODE_ICONS: Record = { class: Box, request: Send } +/** 每深一层往右缩这么多:六七层深时卡片还够宽 */ +const INDENT_PX = 16 function CallNodes({ node, depth, path, onJump }: { node: CallTreeNode, depth: number, path: string, onJump: (jump: CallJump) => void }): ReactElement { const Icon = NODE_ICONS[node.kind] ?? Braces return ( <> -
+
{node.children.map((child, index) => ( ))} {node.otherCalls !== undefined && node.otherCalls > 0 && ( -
+
{`另外 ${node.otherCalls} 处调用`}
@@ -148,3 +148,20 @@ function CallNodes({ node, depth, path, onJump }: { node: CallTreeNode, depth: n ) } + +/** 位置照 VS Code 快速打开的样子摆:「文件名:行号」在前,所在目录淡色跟在后面、放不下末尾省略,悬停看完整路径。 */ +function CallWhere({ where }: { where: string }): ReactElement { + const { file, dir } = splitWhere(where) + return ( + + {file} + {dir && {dir}} + + ) +} + +/** `目录/文件名:行号` 按最后一个 `/` 拆成文件名(带行号)和所在目录;没有目录时 dir 是空的。 */ +export function splitWhere(where: string): { file: string, dir: string } { + const slash = where.lastIndexOf('/') + return { file: where.slice(slash + 1), dir: where.slice(0, Math.max(slash, 0)) } +} diff --git a/vscode/webview-ui/src/codeReader/codeReader.css b/vscode/webview-ui/src/codeReader/codeReader.css index a231250..4577854 100644 --- a/vscode/webview-ui/src/codeReader/codeReader.css +++ b/vscode/webview-ui/src/codeReader/codeReader.css @@ -1312,7 +1312,7 @@ del.cr-diff-word { .cr-call-dialog { display: flex; flex-direction: column; - width: min(640px, 100%); + width: min(880px, 100%); max-height: 100%; overflow: hidden; border: 1px solid var(--vscode-editorWidget-border, var(--vscode-widget-border, transparent)); @@ -1321,23 +1321,56 @@ del.cr-diff-word { color: var(--vscode-editorWidget-foreground, var(--vscode-foreground)); box-shadow: 0 8px 28px var(--vscode-widget-shadow); } +/* 标题一行:「调用树」和符号名整个留着,位置占剩下的宽度 */ .cr-call-title { display: flex; flex-shrink: 0; align-items: center; gap: 8px; - height: 44px; - padding: 0 14px; + min-height: 44px; + padding: 8px 14px; border-bottom: 1px solid var(--vscode-widget-border, var(--vscode-panel-border, transparent)); } .cr-call-heading { + flex-shrink: 0; font-weight: 600; + white-space: nowrap; } .cr-call-symbol, .cr-call-name { color: var(--kind); font-family: var(--vscode-editor-font-family, monospace); font-weight: 600; + white-space: nowrap; +} +.cr-call-symbol { + flex-shrink: 0; +} +/* 位置:文件名先放全,目录拿剩下的宽度、放不下末尾省略;文件名自己也放不下时才省略 */ +.cr-call-where { + display: flex; + flex: 1; + align-items: baseline; + gap: 8px; + min-width: 0; + font-family: var(--vscode-editor-font-family, monospace); + font-size: 11px; +} +.cr-call-file, +.cr-call-dir { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.cr-call-file { + flex: 0 1 auto; + min-width: 0; + color: var(--vscode-foreground); +} +.cr-call-dir { + flex: 1 1 0; + min-width: 0; + color: var(--vscode-disabledForeground, var(--vscode-descriptionForeground)); } .cr-call-loc { color: var(--vscode-disabledForeground, var(--vscode-descriptionForeground)); @@ -1349,6 +1382,7 @@ del.cr-diff-word { align-items: center; justify-content: center; width: 24px; + flex-shrink: 0; height: 24px; margin-left: auto; border: 0; @@ -1447,19 +1481,33 @@ del.cr-diff-word { border-color: var(--kind); background: color-mix(in srgb, var(--kind) 14%, transparent); } -.cr-call-line, -.cr-call-sub { +/* 卡片第一行:图标、名字、detail 占名字后面剩下的宽度,「当前」靠右;第二行只放位置 */ +.cr-call-line { display: flex; align-items: center; gap: 8px; -} -.cr-call-line { color: var(--kind); } -.cr-call-sub { - margin-top: 2px; +.cr-call-line > svg { + flex-shrink: 0; +} +.cr-call-name { + overflow: hidden; + flex: 0 1 auto; + min-width: 0; + text-overflow: ellipsis; +} +.cr-call-detail { + overflow: hidden; + flex: 1 1 0; + min-width: 0; color: var(--vscode-descriptionForeground); - font-size: 12px; + font-size: 11px; + white-space: nowrap; + text-overflow: ellipsis; +} +.cr-call-card .cr-call-where { + margin-top: 2px; } .cr-call-pill { display: flex;