✨ feat(vscode): 代码阅读入口、文件树与预览编辑器

This commit is contained in:
cruldra committed 2026-09-24 21:36:00 +08:00
1 parent 4d34b8c715
commit 2b823a32ca
24 files changed
+768 -72

No files matched your search

+10 -54
View File
@@ -11,9 +11,6 @@ import type { SessionTabKind } from './handlers/terminals'
import type { IssueOwnershipFacts } from './issueOwnership'
import type { KanbanScope } from './kanbanScope'
import type { ExtensionToWebview, WebviewToExtension } from './messages'
import { randomBytes } from 'node:crypto'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { env, TabInputTerminal, ThemeColor, Uri, ViewColumn, window, workspace } from 'vscode'
import { deleteToken, getToken } from '../auth/secrets'
import { importPluginIntoFigma } from '../figma/importPlugin'
@@ -28,6 +25,7 @@ import { logger } from '../logging/logger'
import { annotateMissingSessionIds } from '../sessions/sessionFiles'
import { getEffectiveCommitProfilePath, getEffectivePrDiffSummaryProfilePath, getSettings } from '../settings/store'
import { webhookCoordinator } from '../webhook/coordinator'
import { makeNonce, webviewHtml, webviewUiRoot } from '../webview/html'
import { loadYouTrackIssues } from '../youtrack/issueLoader'
import * as handoffFlow from './handlers/handoffFlow'
import * as issues from './handlers/issues'
@@ -240,7 +238,7 @@ export class KanbanWebviewPanel {
this.panel.webview.options = {
enableScripts: true,
localResourceRoots: [
Uri.joinPath(this.context.extensionUri, 'dist', 'webview-ui'),
webviewUiRoot(this.context.extensionUri),
],
}
@@ -347,7 +345,7 @@ export class KanbanWebviewPanel {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
Uri.joinPath(context.extensionUri, 'dist', 'webview-ui'),
webviewUiRoot(context.extensionUri),
],
},
)
@@ -1308,56 +1306,14 @@ export class KanbanWebviewPanel {
}
private buildHtml(): string {
const distRoot = Uri.joinPath(this.context.extensionUri, 'dist', 'webview-ui')
const indexPath = path.join(distRoot.fsPath, 'index.html')
let html = fs.readFileSync(indexPath, 'utf-8')
const nonce = makeNonce()
const cspSource = this.panel.webview.cspSource
// 重写 src 和 href 的相对路径为 webview URI。vite 产物是固定名(无 hash),
// webview 按 URL 缓存资源——必须按文件 mtime 加 ?v= 版本号,否则 reload 后
// 仍会跑缓存里的旧 index.js / index.css。
html = html.replace(/(src|href)="(\/[^"]+|[^"/][^"]*)"/g, (_m, attr, p) => {
const cleaned = p.replace(/^\.?\//, '')
const fileUri = Uri.joinPath(distRoot, cleaned)
let version = ''
try {
version = `?v=${fs.statSync(fileUri.fsPath).mtimeMs}`
}
catch {
// 资源文件不存在则不加版本号
}
const uri = this.panel.webview.asWebviewUri(fileUri)
return `${attr}="${uri}${version}"`
})
// 给所有 <script> 标签注入 nonce
html = html.replace(/<script(\s[^>]*)?>/g, (_m, attrs) => {
const a = attrs ?? ''
return `<script nonce="${nonce}"${a}>`
})
// 注入 CSP
const csp = `default-src 'none'; img-src ${cspSource} data:; style-src ${cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}'; font-src ${cspSource};`
html = html.replace(
/<head>/,
`<head>\n <meta http-equiv="Content-Security-Policy" content="${csp}">`,
)
const html = webviewHtml(this.panel.webview, this.context.extensionUri, 'index.html', nonce)
const snapshot = parseKanbanSnapshot(this.context.workspaceState.get(KANBAN_SNAPSHOT_KEY))
if (snapshot) {
html = html.replace(
'</body>',
` ${snapshotScriptTag(nonce, snapshot)}\n </body>`,
)
}
return html
if (!snapshot)
return html
return html.replace(
'</body>',
` ${snapshotScriptTag(nonce, snapshot)}\n </body>`,
)
}
}
// internal: handler 模块访问
export function makeNonce(): string {
return randomBytes(16).toString('base64').replace(/[+/=]/g, '')
}