feat(vscode): URI handler 外部触发开 claude 会话 tab

This commit is contained in:
2026-08-20 13:53:12 +08:00
parent 7b6e263b84
commit 416861fc75
5 changed files with 163 additions and 32 deletions
+62 -30
View File
@@ -8,6 +8,7 @@ import { listImportableNamedSessions } from '../../sessions/importableNamedSessi
import { readManagedSessions, writeManagedSessions } from '../../sessions/managedStore'
import { inferProfilePathForSession } from '../../sessions/sessionProfileInference'
import { resolveProfilePath } from '../../cc/profiles'
import { resolveTerminalLocation } from './terminals'
/** 默认会话名:有 prompt 取前 20 字符,否则用短 id(前 8 位)。 */
function defaultSessionName(sessionId: string, prompt?: string): string {
@@ -53,42 +54,51 @@ export async function handleManagedSessionsGet(panel: KanbanWebviewPanel): Promi
await pushManagedSessions(panel)
}
export interface CreateCcSessionTabOptions {
workspaceRoot: string
/** 传给 resolveProfilePath 的原始 profile 路径(空串走默认 profile)。 */
profilePath: string
name?: string
prompt?: string
/** 终端 cwd;缺省 workspaceRoot(会话 jsonl 落在该目录对应的 projects 子目录)。 */
cwd?: string
/** 有 panel 时登记存活终端并推全量列表;panel 外触发(如 URI handler)传 null。 */
panel: KanbanWebviewPanel | null
}
/**
* 从会话管理 tab 创建一个新的 cc 会话
* - cwd 用项目根(workspaceRoot,非 worktree)。
* 创建 cc 会话终端 tab 并登记 managed store(核心逻辑,不要求 panel)
* - 启动命令统一由 buildCcCommand 构建(claude --dangerously-skip-permissions
* --settings '<profilePath>'),prompt 非空时作为位置参数传入。
* - 用 pollForNewSession 捕获新 sessionId(共享 projects 目录用轮询更可靠),
* 落进 .spx/session-names.json推全量列表。
* 落进 workspaceRoot 的 .spx/session-names.json;有 panel 时再登记终端并推全量列表。
*
* 校验失败时 showErrorMessage 并返回 undefined;成功返回终端名。
*/
export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, profilePath: string, name?: string, prompt?: string): Promise<void> {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot) {
void window.showErrorMessage('请先打开一个工作区文件夹')
return
}
export async function createCcSessionTab(opts: CreateCcSessionTabOptions): Promise<{ terminalName: string } | undefined> {
const { workspaceRoot, panel } = opts
const effectiveProfilePath = resolveProfilePath(
profilePath && profilePath.trim() !== '' ? profilePath : undefined,
opts.profilePath && opts.profilePath.trim() !== '' ? opts.profilePath : undefined,
)
// 单引号会破坏下面的 shell 单引号包裹,防御性拒绝(与现有 handler 一致)。
if (effectiveProfilePath.includes('\'')) {
void window.showErrorMessage(
`创建会话失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
)
return
return undefined
}
const trimmedName = (name ?? '').trim()
const trimmedName = (opts.name ?? '').trim()
if (trimmedName.includes('\'')) {
void window.showErrorMessage('创建会话失败:会话名字含单引号,拒绝执行')
return
return undefined
}
const trimmedPrompt = (prompt ?? '').trim()
const trimmedPrompt = (opts.prompt ?? '').trim()
if (trimmedPrompt.includes('\'')) {
void window.showErrorMessage('创建会话失败:首个提示词含单引号,拒绝执行')
return
return undefined
}
// 首个提示词:填了 prompt 用 prompt;否则填了 name 用 /rename <name>;否则交互式无提示词。
@@ -98,8 +108,9 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro
else if (trimmedName)
effectivePrompt = `/rename ${trimmedName}`
// 项目根的 claude projects 子目录;先 mkdir 让 watcher 不会错过 create 事件。
const projDir = projectsDirFor(workspaceRoot)
// cc 按进程 cwd 决定 projects 子目录;先 mkdir 让 watcher 不会错过 create 事件。
const effectiveCwd = opts.cwd ?? workspaceRoot
const projDir = projectsDirFor(effectiveCwd)
try {
await fsp.mkdir(projDir, { recursive: true })
}
@@ -114,8 +125,8 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro
// (VS Code 终端创建后不能改名,所以只在 createTerminal 时设定。)
const terminal = window.createTerminal({
name: trimmedName || 'cc-会话',
cwd: workspaceRoot,
location: panel.resolveTerminalLocation(false),
cwd: effectiveCwd,
location: resolveTerminalLocation(panel, false),
})
terminal.show(false)
logger.add({
@@ -126,15 +137,8 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt: effectivePrompt || undefined })
terminal.sendText(cmd)
logger.add({
level: 'info',
source: 'panel',
message: '已从会话管理 tab 启动 cc 会话',
})
void window.showInformationMessage('已创建 cc 会话')
// Fire-and-forget:会话 jsonl 出现后写进 store、登记终端并推全量列表。
// Fire-and-forget:会话 jsonl 出现后写进 store;有 panel 再登记终端并推全量列表。
watchPromise.then(async (sid) => {
if (!sid) {
logger.add({
@@ -158,9 +162,11 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro
createdAt: Date.now(),
})
await writeManagedSessions(workspaceRoot, data)
// 捕获到 sid 后登记终端,再推 show,让列表的 tabOpen 立即为 true。
panel.managedTerminals.set(sid, terminal)
await pushManagedSessions(panel)
if (panel) {
// 捕获到 sid 后登记终端,再推 show,让列表的 tabOpen 立即为 true。
panel.managedTerminals.set(sid, terminal)
await pushManagedSessions(panel)
}
}
catch (err) {
console.warn('[superpowers] failed to persist managed session:', err)
@@ -168,6 +174,32 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro
}).catch((err) => {
console.warn('[superpowers] managed session watch failed:', err)
})
return { terminalName: terminal.name }
}
/**
* 从会话管理 tab 创建一个新的 cc 会话:
* - cwd 用项目根(workspaceRoot,非 worktree)。
* - 其余逻辑(组命令 / 捕获 sessionId / store 登记)在 createCcSessionTab。
*/
export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, profilePath: string, name?: string, prompt?: string): Promise<void> {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot) {
void window.showErrorMessage('请先打开一个工作区文件夹')
return
}
const created = await createCcSessionTab({ workspaceRoot, profilePath, name, prompt, panel })
if (!created)
return
logger.add({
level: 'info',
source: 'panel',
message: '已从会话管理 tab 启动 cc 会话',
})
void window.showInformationMessage('已创建 cc 会话')
}
/**
+1 -1
View File
@@ -7,7 +7,7 @@ import type { KanbanWebviewPanel } from '../KanbanPanel'
import { ViewColumn, window } from 'vscode'
import { logger } from '../../logging/logger'
export function resolveTerminalLocation(panel: KanbanWebviewPanel, preserveFocus: boolean): TerminalEditorLocationOptions {
export function resolveTerminalLocation(panel: KanbanWebviewPanel | null, preserveFocus: boolean): TerminalEditorLocationOptions {
void panel
// Pin all plugin-managed terminals to editor group 2 (right side of the
// kanban panel in column 1). VS Code creates the group on demand if it