From dc522064ce6a351074bb7a418f4e881b76edcff8 Mon Sep 17 00:00:00 2001 From: cruldra Date: Fri, 24 Jul 2026 00:12:28 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=94=AF=E6=8C=81=E5=AF=BC=E5=85=A5=E5=B7=B2?= =?UTF-8?q?=20/rename=20=E7=9A=84=E5=91=BD=E5=90=8D=E4=BC=9A=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vscode/src/panel/KanbanPanel.ts | 8 ++ vscode/src/panel/handlers/managedSessions.ts | 46 +++++++ vscode/src/panel/messages.ts | 3 + .../sessions/importableNamedSessions.test.ts | 124 ++++++++++++++++++ .../src/sessions/importableNamedSessions.ts | 96 ++++++++++++++ vscode/webview-ui/src/App.tsx | 8 ++ .../webview-ui/src/components/BottomTabs.tsx | 8 ++ .../src/components/ManagedSessionsPanel.tsx | 121 +++++++++++++++-- .../src/hooks/useManagedSessions.ts | 30 +++++ vscode/webview-ui/src/lib/messages.ts | 3 + 10 files changed, 438 insertions(+), 9 deletions(-) create mode 100644 vscode/src/sessions/importableNamedSessions.test.ts create mode 100644 vscode/src/sessions/importableNamedSessions.ts diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index 25dbf85..5b5fcd0 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -556,6 +556,14 @@ export class KanbanWebviewPanel { managedSessions.handleManagedSessionsCloseTab(this, msg.sessionId) return } + if (msg.type === 'managed-sessions/list-importable') { + void managedSessions.handleManagedSessionsListImportable(this) + return + } + if (msg.type === 'managed-sessions/import') { + void managedSessions.handleManagedSessionsImport(this, msg.sessionId, msg.name) + return + } if (msg.type === 'pr-files/get') { void prFiles.handleGetPrFiles(this, msg.issueNumber) return diff --git a/vscode/src/panel/handlers/managedSessions.ts b/vscode/src/panel/handlers/managedSessions.ts index f4afff0..db5ca68 100644 --- a/vscode/src/panel/handlers/managedSessions.ts +++ b/vscode/src/panel/handlers/managedSessions.ts @@ -4,6 +4,7 @@ import { window, workspace } from 'vscode' import { buildCcCommand } from '../../cc/ccCommand' import { pollForNewSession, projectsDirFor } from '../../cc/sessionWatcher' import { logger } from '../../logging/logger' +import { listImportableNamedSessions } from '../../sessions/importableNamedSessions' import { readManagedSessions, writeManagedSessions } from '../../sessions/managedStore' import { resolveProfilePath } from '../../cc/profiles' @@ -270,3 +271,48 @@ export async function handleManagedSessionsDelete(panel: KanbanWebviewPanel, ses panel.managedTerminals.delete(sessionId) await pushManagedSessions(panel) } + +/** + * 列出当前工作区可导入的命名会话(jsonl 有 custom-title 且尚未在 managed store)。 + */ +export async function handleManagedSessionsListImportable(panel: KanbanWebviewPanel): Promise { + const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath + if (!workspaceRoot) { + panel.postMessage({ type: 'managed-sessions/importable', sessions: [] }) + return + } + const sessions = await listImportableNamedSessions(workspaceRoot) + panel.postMessage({ type: 'managed-sessions/importable', sessions }) +} + +/** + * 把已 /rename 的会话登记进 managed store(不写 profilePath、不 resume)。 + * id 已存在时幂等,仍推送全量列表。 + */ +export async function handleManagedSessionsImport( + panel: KanbanWebviewPanel, + sessionId: string, + name: string, +): Promise { + const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath + if (!workspaceRoot) { + void window.showErrorMessage('请先打开一个工作区文件夹') + return + } + const trimmedId = sessionId.trim() + const trimmedName = name.trim() + if (!trimmedId || !trimmedName) + return + + const data = await readManagedSessions(workspaceRoot) + const existing = data.sessions.find(s => s.id === trimmedId) + if (!existing) { + data.sessions.push({ + id: trimmedId, + name: trimmedName, + createdAt: Date.now(), + }) + await writeManagedSessions(workspaceRoot, data) + } + await pushManagedSessions(panel) +} diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index 6e11b11..74e05a5 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -104,6 +104,7 @@ export type ExtensionToWebview | { type: 'issue/remove', issueNumber: number } | { type: 'profiles/show', data: ProfilesData } | { type: 'managed-sessions/show', data: ManagedSessionsShowData } + | { type: 'managed-sessions/importable', sessions: Array<{ id: string, name: string, mtimeMs: number }> } | { type: 'pr-files/show', issueNumber: number, files: PrFile[], confirmed: string[], summaries: Record, error?: string } | { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string } | { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string } @@ -185,6 +186,8 @@ export type WebviewToExtension | { type: 'managed-sessions/resume', sessionId: string } | { type: 'managed-sessions/delete', sessionId: string } | { type: 'managed-sessions/close-tab', sessionId: string } + | { type: 'managed-sessions/list-importable' } + | { type: 'managed-sessions/import', sessionId: string, name: string } | { type: 'pr-files/get', issueNumber: number } | { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string } | { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string } diff --git a/vscode/src/sessions/importableNamedSessions.test.ts b/vscode/src/sessions/importableNamedSessions.test.ts new file mode 100644 index 0000000..c94e718 --- /dev/null +++ b/vscode/src/sessions/importableNamedSessions.test.ts @@ -0,0 +1,124 @@ +import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs' +import * as os from 'node:os' +import * as path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + extractLastCustomTitle, + listImportableNamedSessions, +} from './importableNamedSessions' +import { writeManagedSessions } from './managedStore' + +const tempDirs: string[] = [] + +afterEach(() => { + while (tempDirs.length > 0) { + const dir = tempDirs.pop() + if (dir) + rmSync(dir, { recursive: true, force: true }) + } +}) + +function tempDir(prefix: string): string { + const dir = mkdtempSync(path.join(os.tmpdir(), prefix)) + tempDirs.push(dir) + return dir +} + +describe('extractLastCustomTitle', () => { + it('多条 custom-title 时取最后一条', () => { + const text = [ + JSON.stringify({ type: 'custom-title', customTitle: '旧名', sessionId: 'a' }), + JSON.stringify({ type: 'user', message: 'hi' }), + JSON.stringify({ type: 'custom-title', customTitle: '新名', sessionId: 'a' }), + '', + ].join('\n') + expect(extractLastCustomTitle(text)).toBe('新名') + }) + + it('无 custom-title 返回 null', () => { + const text = [ + JSON.stringify({ type: 'user', message: 'hi' }), + JSON.stringify({ type: 'assistant', message: 'yo' }), + ].join('\n') + expect(extractLastCustomTitle(text)).toBeNull() + }) + + it('仅 ai-title 忽略,返回 null', () => { + const text = JSON.stringify({ type: 'ai-title', title: 'AI 起的名' }) + expect(extractLastCustomTitle(text)).toBeNull() + }) + + it('customTitle 仅空白时忽略', () => { + const text = [ + JSON.stringify({ type: 'custom-title', customTitle: ' ' }), + JSON.stringify({ type: 'custom-title', customTitle: '\t' }), + ].join('\n') + expect(extractLastCustomTitle(text)).toBeNull() + }) + + it('trim 非空 customTitle', () => { + const text = JSON.stringify({ type: 'custom-title', customTitle: ' 有空格 ' }) + expect(extractLastCustomTitle(text)).toBe('有空格') + }) + + it('非法 json 行跳过', () => { + const text = [ + 'not-json', + JSON.stringify({ type: 'custom-title', customTitle: 'ok' }), + '{broken', + ].join('\n') + expect(extractLastCustomTitle(text)).toBe('ok') + }) +}) + +describe('listImportableNamedSessions', () => { + it('过滤已在 managed store 的 id,按 mtime 降序', async () => { + const workspaceRoot = tempDir('importable-ws-') + const projectsDir = tempDir('importable-proj-') + + const older = path.join(projectsDir, 'sess-old.jsonl') + const newer = path.join(projectsDir, 'sess-new.jsonl') + const managed = path.join(projectsDir, 'sess-managed.jsonl') + const unnamed = path.join(projectsDir, 'sess-unnamed.jsonl') + + writeFileSync(older, `${JSON.stringify({ type: 'custom-title', customTitle: '旧会话' })}\n`) + writeFileSync(newer, `${JSON.stringify({ type: 'custom-title', customTitle: '新会话' })}\n`) + writeFileSync(managed, `${JSON.stringify({ type: 'custom-title', customTitle: '已登记' })}\n`) + writeFileSync(unnamed, `${JSON.stringify({ type: 'user', message: 'no title' })}\n`) + + // 固定 mtime:newer 更晚 + const oldMs = Date.now() - 60_000 + const newMs = Date.now() - 10_000 + utimesSync(older, new Date(oldMs), new Date(oldMs)) + utimesSync(newer, new Date(newMs), new Date(newMs)) + utimesSync(managed, new Date(newMs), new Date(newMs)) + + mkdirSync(path.join(workspaceRoot, '.spx'), { recursive: true }) + await writeManagedSessions(workspaceRoot, { + sessions: [ + { id: 'sess-managed', name: '已登记', createdAt: 1 }, + ], + }) + + // 子目录内 jsonl 不应被扫到 + const sub = path.join(projectsDir, 'subagents') + mkdirSync(sub) + writeFileSync( + path.join(sub, 'agent.jsonl'), + `${JSON.stringify({ type: 'custom-title', customTitle: '子代理' })}\n`, + ) + + const result = await listImportableNamedSessions(workspaceRoot, projectsDir) + expect(result.map(r => r.id)).toEqual(['sess-new', 'sess-old']) + expect(result[0]).toMatchObject({ id: 'sess-new', name: '新会话' }) + expect(result[1]).toMatchObject({ id: 'sess-old', name: '旧会话' }) + expect(result[0].mtimeMs).toBeGreaterThan(result[1].mtimeMs) + }) + + it('空目录返回 []', async () => { + const workspaceRoot = tempDir('importable-empty-ws-') + const projectsDir = tempDir('importable-empty-proj-') + const result = await listImportableNamedSessions(workspaceRoot, projectsDir) + expect(result).toEqual([]) + }) +}) diff --git a/vscode/src/sessions/importableNamedSessions.ts b/vscode/src/sessions/importableNamedSessions.ts new file mode 100644 index 0000000..0fa4ff9 --- /dev/null +++ b/vscode/src/sessions/importableNamedSessions.ts @@ -0,0 +1,96 @@ +/** + * 从 Claude projects 目录扫描已 /rename(jsonl 含 custom-title)且尚未登记到 + * managed store 的会话,供「导入」候选列表使用。 + */ + +import { promises as fsp } from 'node:fs' +import * as path from 'node:path' +import { projectsDirFor } from '../cc/sessionWatcher' +import { readManagedSessions } from './managedStore' + +export interface ImportableNamedSession { + id: string + name: string + mtimeMs: number +} + +/** 从 jsonl 文本提取最后一条 custom-title(trim 后非空),否则 null */ +export function extractLastCustomTitle(jsonlText: string): string | null { + let last: string | null = null + const lines = jsonlText.split(/\r?\n/) + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) + continue + let parsed: unknown + try { + parsed = JSON.parse(trimmed) + } + catch { + continue + } + if (!parsed || typeof parsed !== 'object') + continue + const rec = parsed as { type?: unknown, customTitle?: unknown } + if (rec.type !== 'custom-title') + continue + if (typeof rec.customTitle !== 'string') + continue + const title = rec.customTitle.trim() + if (!title) + continue + last = title + } + return last +} + +/** + * 列出可导入命名会话:扫 projectsDir 顶层 *.jsonl,过滤已在 managed 的 id,mtime 降序。 + * `projectsDir` 可选,便于测试注入;默认 `projectsDirFor(workspaceRoot)`。 + */ +export async function listImportableNamedSessions( + workspaceRoot: string, + projectsDir?: string, +): Promise { + const dir = projectsDir ?? projectsDirFor(workspaceRoot) + let entries: import('node:fs').Dirent[] + try { + entries = await fsp.readdir(dir, { withFileTypes: true }) + } + catch { + return [] + } + + const managed = await readManagedSessions(workspaceRoot) + const managedIds = new Set(managed.sessions.map(s => s.id)) + + const results: ImportableNamedSession[] = [] + for (const ent of entries) { + if (!ent.isFile() || !ent.name.endsWith('.jsonl')) + continue + const id = ent.name.slice(0, -'.jsonl'.length) + if (!id || managedIds.has(id)) + continue + const filePath = path.join(dir, ent.name) + let text: string + let mtimeMs: number + try { + const [raw, st] = await Promise.all([ + fsp.readFile(filePath, 'utf8'), + fsp.stat(filePath), + ]) + text = raw + mtimeMs = st.mtimeMs + } + catch { + continue + } + const name = extractLastCustomTitle(text) + if (!name) + continue + results.push({ id, name, mtimeMs }) + } + + results.sort((a, b) => b.mtimeMs - a.mtimeMs) + return results +} diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index f5f405e..d4e129a 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -83,6 +83,10 @@ export function App() { resumeManagedSession, deleteManagedSession, closeManagedSessionTab, + listImportableNamedSessions, + importNamedSession, + importableSessions, + importableLoading, } = useManagedSessions() const [showNewIssueModal, setShowNewIssueModal] = useState(false) const [showLogs, setShowLogs] = useState(false) @@ -373,6 +377,10 @@ export function App() { onManagedSessionResume={resumeManagedSession} onManagedSessionDelete={deleteManagedSession} onManagedSessionCloseTab={closeManagedSessionTab} + importableSessions={importableSessions} + importableLoading={importableLoading} + onListImportableSessions={listImportableNamedSessions} + onImportNamedSession={importNamedSession} /> diff --git a/vscode/webview-ui/src/components/BottomTabs.tsx b/vscode/webview-ui/src/components/BottomTabs.tsx index 54e774b..84c9b25 100644 --- a/vscode/webview-ui/src/components/BottomTabs.tsx +++ b/vscode/webview-ui/src/components/BottomTabs.tsx @@ -60,6 +60,10 @@ interface BottomTabsProps { onManagedSessionResume: (id: string) => void onManagedSessionDelete: (id: string) => void onManagedSessionCloseTab: (id: string) => void + importableSessions: Array<{ id: string, name: string, mtimeMs: number }> + importableLoading: boolean + onListImportableSessions: () => void + onImportNamedSession: (sessionId: string, name: string) => void } export function BottomTabs(props: BottomTabsProps) { @@ -143,6 +147,10 @@ export function BottomTabs(props: BottomTabsProps) { onResume={props.onManagedSessionResume} onDelete={props.onManagedSessionDelete} onCloseTab={props.onManagedSessionCloseTab} + importableSessions={props.importableSessions} + importableLoading={props.importableLoading} + onListImportable={props.onListImportableSessions} + onImport={props.onImportNamedSession} /> )} {tab === 'changes' && ( diff --git a/vscode/webview-ui/src/components/ManagedSessionsPanel.tsx b/vscode/webview-ui/src/components/ManagedSessionsPanel.tsx index 52a1a6a..4e20efb 100644 --- a/vscode/webview-ui/src/components/ManagedSessionsPanel.tsx +++ b/vscode/webview-ui/src/components/ManagedSessionsPanel.tsx @@ -9,7 +9,7 @@ import type { ClaudeProfile } from '../hooks/useIssues' import type { ManagedSession, ManagedSessionsData } from '../types' -import { Plus, Terminal, Trash2, X } from 'lucide-react' +import { Download, Plus, Terminal, Trash2, X } from 'lucide-react' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' interface ManagedSessionsPanelProps { @@ -20,6 +20,10 @@ interface ManagedSessionsPanelProps { onResume: (id: string) => void onDelete: (id: string) => void onCloseTab: (id: string) => void + importableSessions: Array<{ id: string, name: string, mtimeMs: number }> + importableLoading: boolean + onListImportable: () => void + onImport: (sessionId: string, name: string) => void } function basename(p?: string): string { @@ -45,10 +49,24 @@ function formatTime(ts: number): string { } } -export function ManagedSessionsPanel({ data, profiles, onCreate, onRename, onResume, onDelete, onCloseTab }: ManagedSessionsPanelProps) { +export function ManagedSessionsPanel({ + data, + profiles, + onCreate, + onRename, + onResume, + onDelete, + onCloseTab, + importableSessions, + importableLoading, + onListImportable, + onImport, +}: ManagedSessionsPanelProps) { const [selectedProfile, setSelectedProfile] = useState('') const [name, setName] = useState('') const [prompt, setPrompt] = useState('') + const [importOpen, setImportOpen] = useState(false) + const importWrapRef = useRef(null) // 默认选中官方 profile(profiles 异步到达后;用户手动改选后不覆盖)。 useEffect(() => { @@ -58,6 +76,29 @@ export function ManagedSessionsPanel({ data, profiles, onCreate, onRename, onRes setSelectedProfile((official ?? profiles[0]).path) }, [profiles, selectedProfile]) + // 弹层:外点击 / Esc 关闭 + useEffect(() => { + if (!importOpen) + return + function onKey(e: KeyboardEvent): void { + if (e.key === 'Escape') + setImportOpen(false) + } + function onPointer(e: MouseEvent): void { + const el = importWrapRef.current + if (!el) + return + if (e.target instanceof Node && !el.contains(e.target)) + setImportOpen(false) + } + window.addEventListener('keydown', onKey) + window.addEventListener('mousedown', onPointer) + return () => { + window.removeEventListener('keydown', onKey) + window.removeEventListener('mousedown', onPointer) + } + }, [importOpen]) + const sessions = useMemo( () => [...data.sessions].sort((a, b) => b.createdAt - a.createdAt), [data.sessions], @@ -75,6 +116,16 @@ export function ManagedSessionsPanel({ data, profiles, onCreate, onRename, onRes setPrompt('') }, [canCreate, name, prompt, selectedProfile, onCreate]) + const openImport = useCallback((): void => { + setImportOpen(true) + onListImportable() + }, [onListImportable]) + + const pickImport = useCallback((sessionId: string, sessionName: string): void => { + onImport(sessionId, sessionName) + setImportOpen(false) + }, [onImport]) + return (
{/* 上方表单 */} @@ -97,13 +148,65 @@ export function ManagedSessionsPanel({ data, profiles, onCreate, onRename, onRes
名字 - setName(e.target.value)} - placeholder="会话名字(可选)" - className="min-w-0 flex-1 rounded border border-[var(--vscode-input-border,var(--vscode-panel-border))] bg-[var(--vscode-input-background)] px-2 py-1 text-xs text-[var(--vscode-input-foreground)] outline-none placeholder:text-[var(--vscode-input-placeholderForeground)] focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]" - /> +
+
+ setName(e.target.value)} + placeholder="会话名字(可选)" + className="min-w-0 flex-1 rounded border border-[var(--vscode-input-border,var(--vscode-panel-border))] bg-[var(--vscode-input-background)] px-2 py-1 text-xs text-[var(--vscode-input-foreground)] outline-none placeholder:text-[var(--vscode-input-placeholderForeground)] focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]" + /> + +
+ {importOpen && ( +
+ {importableLoading + ? ( +
+ 加载中… +
+ ) + : importableSessions.length === 0 + ? ( +
+ 没有可导入的命名会话 +
+ ) + : ( +
    + {importableSessions.map(s => ( +
  • + +
  • + ))} +
+ )} +
+ )} +
提示词 diff --git a/vscode/webview-ui/src/hooks/useManagedSessions.ts b/vscode/webview-ui/src/hooks/useManagedSessions.ts index c057c3a..d4f2e71 100644 --- a/vscode/webview-ui/src/hooks/useManagedSessions.ts +++ b/vscode/webview-ui/src/hooks/useManagedSessions.ts @@ -12,6 +12,12 @@ import type { ManagedSessionsData } from '../types' import { useCallback, useEffect, useState } from 'react' import { onMessage, postMessage } from '../lib/vscode' +export interface ImportableNamedSession { + id: string + name: string + mtimeMs: number +} + export interface UseManagedSessionsResult { managedSessions: ManagedSessionsData createManagedSession: (profilePath: string, name?: string, prompt?: string) => void @@ -19,15 +25,25 @@ export interface UseManagedSessionsResult { resumeManagedSession: (id: string) => void deleteManagedSession: (id: string) => void closeManagedSessionTab: (id: string) => void + listImportableNamedSessions: () => void + importNamedSession: (sessionId: string, name: string) => void + importableSessions: ImportableNamedSession[] + importableLoading: boolean } export function useManagedSessions(): UseManagedSessionsResult { const [managedSessions, setManagedSessions] = useState({ sessions: [] }) + const [importableSessions, setImportableSessions] = useState([]) + const [importableLoading, setImportableLoading] = useState(false) useEffect(() => { const cleanup = onMessage((msg) => { if (msg.type === 'managed-sessions/show') setManagedSessions(msg.data) + if (msg.type === 'managed-sessions/importable') { + setImportableSessions(msg.sessions) + setImportableLoading(false) + } }) postMessage({ type: 'managed-sessions/get' }) return cleanup @@ -53,6 +69,16 @@ export function useManagedSessions(): UseManagedSessionsResult { postMessage({ type: 'managed-sessions/close-tab', sessionId: id }) }, []) + const listImportableNamedSessions = useCallback((): void => { + setImportableLoading(true) + setImportableSessions([]) + postMessage({ type: 'managed-sessions/list-importable' }) + }, []) + + const importNamedSession = useCallback((sessionId: string, name: string): void => { + postMessage({ type: 'managed-sessions/import', sessionId, name }) + }, []) + return { managedSessions, createManagedSession, @@ -60,5 +86,9 @@ export function useManagedSessions(): UseManagedSessionsResult { resumeManagedSession, deleteManagedSession, closeManagedSessionTab, + listImportableNamedSessions, + importNamedSession, + importableSessions, + importableLoading, } } diff --git a/vscode/webview-ui/src/lib/messages.ts b/vscode/webview-ui/src/lib/messages.ts index ec511e2..17a2cc3 100644 --- a/vscode/webview-ui/src/lib/messages.ts +++ b/vscode/webview-ui/src/lib/messages.ts @@ -119,6 +119,7 @@ export type ExtensionToWebview | { type: 'issue/remove', issueNumber: number } | { type: 'profiles/show', data: ProfilesData } | { type: 'managed-sessions/show', data: ManagedSessionsData } + | { type: 'managed-sessions/importable', sessions: Array<{ id: string, name: string, mtimeMs: number }> } | { type: 'pr-files/show', issueNumber: number, files: PrFile[], confirmed: string[], summaries: Record, error?: string } | { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string } | { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string } @@ -200,6 +201,8 @@ export type WebviewToExtension | { type: 'managed-sessions/resume', sessionId: string } | { type: 'managed-sessions/delete', sessionId: string } | { type: 'managed-sessions/close-tab', sessionId: string } + | { type: 'managed-sessions/list-importable' } + | { type: 'managed-sessions/import', sessionId: string, name: string } | { type: 'pr-files/get', issueNumber: number } | { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string } | { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string }