✨ 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, '')
}
+1 -1
View File
@@ -30,7 +30,7 @@ import {
import { logger } from '../../logging/logger'
import { annotateMissingSessionIds, defaultCodexSessionsDir } from '../../sessions/sessionFiles'
import { getSettings } from '../../settings/store'
import { makeNonce } from '../KanbanPanel'
import { makeNonce } from '../../webview/html'
import { cleanupFeatureBranch } from './branchCleanup'
import {
canAcceptHandoff,
+1 -1
View File
@@ -36,9 +36,9 @@ import { mergeStateJsonComment } from '../../gitea/stateJson'
import { logger } from '../../logging/logger'
import { getEffectivePrDiffSummaryProfilePath, getSettings } from '../../settings/store'
import { webhookCoordinator } from '../../webhook/coordinator'
import { makeNonce } from '../../webview/html'
import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor'
import { evaluateLockedReason } from '../issueLock'
import { makeNonce } from '../KanbanPanel'
import { cleanupFeatureBranch } from './branchCleanup'
import { clearSessionConfirmText, clearSessionDoneText, SESSION_KINDS } from './sessionKinds'
import * as sessions from './sessions'
+1 -1
View File
@@ -24,7 +24,7 @@ import { readStateJsonComment } from '../../gitea/stateJson'
import { readFileSummaries, readSummarySession, writeFileSummary, writeSummarySession } from '../../sessions/prFileSummaryStore'
import { readConfirmedSigs, writeConfirmedSigs } from '../../sessions/prReviewStore'
import { getEffectivePrDiffSummaryProfilePath } from '../../settings/store'
import { makeNonce } from '../KanbanPanel'
import { makeNonce } from '../../webview/html'
/**
* 解析当前工作区的 Gitea 仓库与 token。三者任一缺失返回 undefined,
+1 -1
View File
@@ -22,9 +22,9 @@ import { logger } from '../../logging/logger'
import { claudeSessionFileExists, codexSessionFileExists, defaultCodexSessionsDir } from '../../sessions/sessionFiles'
import { getSettings } from '../../settings/store'
import { webhookCoordinator } from '../../webhook/coordinator'
import { makeNonce } from '../../webview/html'
import { decideAutoReviewPath } from '../autoReviewDecision'
import { pickBrainstormContinuePath } from '../brainstormContinue'
import { makeNonce } from '../KanbanPanel'
import { cleanupFeatureBranch } from './branchCleanup'
import { planResumeCwd, resumeTerminalName } from './resumeTarget'
import { submitToSessionTerminal } from './terminals'
+1 -1
View File
@@ -15,8 +15,8 @@ import { logger } from '../../logging/logger'
import { readProfiles, writeProfiles } from '../../profiles/store'
import { getEffectiveCommitProfilePath, getEffectivePrDiffSummaryProfilePath, getSettings, saveSettings } from '../../settings/store'
import { webhookCoordinator } from '../../webhook/coordinator'
import { makeNonce } from '../../webview/html'
import { youtrackHost } from '../../youtrack/issueLoader'
import { makeNonce } from '../KanbanPanel'
export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
host: string
+1 -1
View File
@@ -10,7 +10,7 @@ import { findEnvFiles, lockEnvFiles, unlockEnvFiles } from '../../files/envLock'
import { missingGitIdentityKeys, readGitIdentity } from '../../git/identity'
import { logger } from '../../logging/logger'
import { getEffectiveCommitProfilePath } from '../../settings/store'
import { makeNonce } from '../KanbanPanel'
import { makeNonce } from '../../webview/html'
import { dismiss, toast } from './handoffFlow'
/**
+1 -1
View File
@@ -13,7 +13,7 @@ import {
} from '../../git/worktreeHooks'
import { logger } from '../../logging/logger'
import { getSettings } from '../../settings/store'
import { makeNonce } from '../KanbanPanel'
import { makeNonce } from '../../webview/html'
import { cleanupFeatureBranch } from './branchCleanup'
/**
+1 -1
View File
@@ -14,11 +14,11 @@ import { ThemeIcon, window, workspace } from 'vscode'
import { getYouTrackToken } from '../../auth/secrets'
import { logger } from '../../logging/logger'
import { getSettings } from '../../settings/store'
import { makeNonce } from '../../webview/html'
import { applyCommand, listIssues, listProjects, resolvedStateCommand } from '../../youtrack/api'
import { readImportedIds, writeImportedIds } from '../../youtrack/importStore'
import { youtrackHost } from '../../youtrack/issueLoader'
import { mergeStateComment } from '../../youtrack/stateComment'
import { makeNonce } from '../KanbanPanel'
function toast(panel: KanbanWebviewPanel, level: 'info' | 'success' | 'error', message: string): void {
panel.postMessage({ type: 'toast/show', id: makeNonce(), level, message, dismissOnTimer: 6000 })