init claude-code
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { z } from 'zod/v4'
|
||||
import { getSessionId, setOriginalCwd } from '../../bootstrap/state.js'
|
||||
import { clearSystemPromptSections } from '../../constants/systemPromptSections.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { Tool } from '../../Tool.js'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
import { clearMemoryFileCaches } from '../../utils/claudemd.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { findCanonicalGitRoot } from '../../utils/git.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { getPlanSlug, getPlansDirectory } from '../../utils/plans.js'
|
||||
import { setCwd } from '../../utils/Shell.js'
|
||||
import { saveWorktreeState } from '../../utils/sessionStorage.js'
|
||||
import {
|
||||
createWorktreeForSession,
|
||||
getCurrentWorktreeSession,
|
||||
validateWorktreeSlug,
|
||||
} from '../../utils/worktree.js'
|
||||
import { ENTER_WORKTREE_TOOL_NAME } from './constants.js'
|
||||
import { getEnterWorktreeToolPrompt } from './prompt.js'
|
||||
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
name: z
|
||||
.string()
|
||||
.superRefine((s, ctx) => {
|
||||
try {
|
||||
validateWorktreeSlug(s)
|
||||
} catch (e) {
|
||||
ctx.addIssue({ code: 'custom', message: (e as Error).message })
|
||||
}
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional name for the worktree. Each "/"-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total. A random name is generated if not provided.',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
worktreePath: z.string(),
|
||||
worktreeBranch: z.string().optional(),
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
export const EnterWorktreeTool: Tool<InputSchema, Output> = buildTool({
|
||||
name: ENTER_WORKTREE_TOOL_NAME,
|
||||
searchHint: 'create an isolated git worktree and switch into it',
|
||||
maxResultSizeChars: 100_000,
|
||||
async description() {
|
||||
return 'Creates an isolated worktree (via git or configured hooks) and switches the session into it'
|
||||
},
|
||||
async prompt() {
|
||||
return getEnterWorktreeToolPrompt()
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
userFacingName() {
|
||||
return 'Creating worktree'
|
||||
},
|
||||
shouldDefer: true,
|
||||
toAutoClassifierInput(input) {
|
||||
return input.name ?? ''
|
||||
},
|
||||
renderToolUseMessage,
|
||||
renderToolResultMessage,
|
||||
async call(input) {
|
||||
// Validate not already in a worktree created by this session
|
||||
if (getCurrentWorktreeSession()) {
|
||||
throw new Error('Already in a worktree session')
|
||||
}
|
||||
|
||||
// Resolve to main repo root so worktree creation works from within a worktree
|
||||
const mainRepoRoot = findCanonicalGitRoot(getCwd())
|
||||
if (mainRepoRoot && mainRepoRoot !== getCwd()) {
|
||||
process.chdir(mainRepoRoot)
|
||||
setCwd(mainRepoRoot)
|
||||
}
|
||||
|
||||
const slug = input.name ?? getPlanSlug()
|
||||
|
||||
const worktreeSession = await createWorktreeForSession(getSessionId(), slug)
|
||||
|
||||
process.chdir(worktreeSession.worktreePath)
|
||||
setCwd(worktreeSession.worktreePath)
|
||||
setOriginalCwd(getCwd())
|
||||
saveWorktreeState(worktreeSession)
|
||||
// Clear cached system prompt sections so env_info_simple recomputes with worktree context
|
||||
clearSystemPromptSections()
|
||||
// Clear memoized caches that depend on CWD
|
||||
clearMemoryFileCaches()
|
||||
getPlansDirectory.cache.clear?.()
|
||||
|
||||
logEvent('tengu_worktree_created', {
|
||||
mid_session: true,
|
||||
})
|
||||
|
||||
const branchInfo = worktreeSession.worktreeBranch
|
||||
? ` on branch ${worktreeSession.worktreeBranch}`
|
||||
: ''
|
||||
|
||||
return {
|
||||
data: {
|
||||
worktreePath: worktreeSession.worktreePath,
|
||||
worktreeBranch: worktreeSession.worktreeBranch,
|
||||
message: `Created worktree at ${worktreeSession.worktreePath}${branchInfo}. The session is now working in the worktree. Use ExitWorktree to leave mid-session, or exit the session to be prompted.`,
|
||||
},
|
||||
}
|
||||
},
|
||||
mapToolResultToToolResultBlockParam({ message }, toolUseID) {
|
||||
return {
|
||||
type: 'tool_result',
|
||||
content: message,
|
||||
tool_use_id: toolUseID,
|
||||
}
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { ToolProgressData } from '../../Tool.js';
|
||||
import type { ProgressMessage } from '../../types/message.js';
|
||||
import type { ThemeName } from '../../utils/theme.js';
|
||||
import type { Output } from './EnterWorktreeTool.js';
|
||||
export function renderToolUseMessage(): React.ReactNode {
|
||||
return 'Creating worktree…';
|
||||
}
|
||||
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], _options: {
|
||||
theme: ThemeName;
|
||||
}): React.ReactNode {
|
||||
return <Box flexDirection="column">
|
||||
<Text>
|
||||
Switched to worktree on branch <Text bold>{output.worktreeBranch}</Text>
|
||||
</Text>
|
||||
<Text dimColor>{output.worktreePath}</Text>
|
||||
</Box>;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIlRleHQiLCJUb29sUHJvZ3Jlc3NEYXRhIiwiUHJvZ3Jlc3NNZXNzYWdlIiwiVGhlbWVOYW1lIiwiT3V0cHV0IiwicmVuZGVyVG9vbFVzZU1lc3NhZ2UiLCJSZWFjdE5vZGUiLCJyZW5kZXJUb29sUmVzdWx0TWVzc2FnZSIsIm91dHB1dCIsIl9wcm9ncmVzc01lc3NhZ2VzRm9yTWVzc2FnZSIsIl9vcHRpb25zIiwidGhlbWUiLCJ3b3JrdHJlZUJyYW5jaCIsIndvcmt0cmVlUGF0aCJdLCJzb3VyY2VzIjpbIlVJLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgVG9vbFByb2dyZXNzRGF0YSB9IGZyb20gJy4uLy4uL1Rvb2wuanMnXG5pbXBvcnQgdHlwZSB7IFByb2dyZXNzTWVzc2FnZSB9IGZyb20gJy4uLy4uL3R5cGVzL21lc3NhZ2UuanMnXG5pbXBvcnQgdHlwZSB7IFRoZW1lTmFtZSB9IGZyb20gJy4uLy4uL3V0aWxzL3RoZW1lLmpzJ1xuaW1wb3J0IHR5cGUgeyBPdXRwdXQgfSBmcm9tICcuL0VudGVyV29ya3RyZWVUb29sLmpzJ1xuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFVzZU1lc3NhZ2UoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuICdDcmVhdGluZyB3b3JrdHJlZeKApidcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xSZXN1bHRNZXNzYWdlKFxuICBvdXRwdXQ6IE91dHB1dCxcbiAgX3Byb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlOiBQcm9ncmVzc01lc3NhZ2U8VG9vbFByb2dyZXNzRGF0YT5bXSxcbiAgX29wdGlvbnM6IHsgdGhlbWU6IFRoZW1lTmFtZSB9LFxuKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgIDxUZXh0PlxuICAgICAgICBTd2l0Y2hlZCB0byB3b3JrdHJlZSBvbiBicmFuY2ggPFRleHQgYm9sZD57b3V0cHV0Lndvcmt0cmVlQnJhbmNofTwvVGV4dD5cbiAgICAgIDwvVGV4dD5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPntvdXRwdXQud29ya3RyZWVQYXRofTwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLGNBQWM7QUFDeEMsY0FBY0MsZ0JBQWdCLFFBQVEsZUFBZTtBQUNyRCxjQUFjQyxlQUFlLFFBQVEsd0JBQXdCO0FBQzdELGNBQWNDLFNBQVMsUUFBUSxzQkFBc0I7QUFDckQsY0FBY0MsTUFBTSxRQUFRLHdCQUF3QjtBQUVwRCxPQUFPLFNBQVNDLG9CQUFvQkEsQ0FBQSxDQUFFLEVBQUVQLEtBQUssQ0FBQ1EsU0FBUyxDQUFDO0VBQ3RELE9BQU8sb0JBQW9CO0FBQzdCO0FBRUEsT0FBTyxTQUFTQyx1QkFBdUJBLENBQ3JDQyxNQUFNLEVBQUVKLE1BQU0sRUFDZEssMkJBQTJCLEVBQUVQLGVBQWUsQ0FBQ0QsZ0JBQWdCLENBQUMsRUFBRSxFQUNoRVMsUUFBUSxFQUFFO0VBQUVDLEtBQUssRUFBRVIsU0FBUztBQUFDLENBQUMsQ0FDL0IsRUFBRUwsS0FBSyxDQUFDUSxTQUFTLENBQUM7RUFDakIsT0FDRSxDQUFDLEdBQUcsQ0FBQyxhQUFhLENBQUMsUUFBUTtBQUMvQixNQUFNLENBQUMsSUFBSTtBQUNYLHVDQUF1QyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQ0UsTUFBTSxDQUFDSSxjQUFjLENBQUMsRUFBRSxJQUFJO0FBQy9FLE1BQU0sRUFBRSxJQUFJO0FBQ1osTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQ0osTUFBTSxDQUFDSyxZQUFZLENBQUMsRUFBRSxJQUFJO0FBQ2hELElBQUksRUFBRSxHQUFHLENBQUM7QUFFViIsImlnbm9yZUxpc3QiOltdfQ==
|
||||
@@ -0,0 +1 @@
|
||||
export const ENTER_WORKTREE_TOOL_NAME = 'EnterWorktree'
|
||||
@@ -0,0 +1,30 @@
|
||||
export function getEnterWorktreeToolPrompt(): string {
|
||||
return `Use this tool ONLY when the user explicitly asks to work in a worktree. This tool creates an isolated git worktree and switches the current session into it.
|
||||
|
||||
## When to Use
|
||||
|
||||
- The user explicitly says "worktree" (e.g., "start a worktree", "work in a worktree", "create a worktree", "use a worktree")
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- The user asks to create a branch, switch branches, or work on a different branch — use git commands instead
|
||||
- The user asks to fix a bug or work on a feature — use normal git workflow unless they specifically mention worktrees
|
||||
- Never use this tool unless the user explicitly mentions "worktree"
|
||||
|
||||
## Requirements
|
||||
|
||||
- Must be in a git repository, OR have WorktreeCreate/WorktreeRemove hooks configured in settings.json
|
||||
- Must not already be in a worktree
|
||||
|
||||
## Behavior
|
||||
|
||||
- In a git repository: creates a new git worktree inside \`.claude/worktrees/\` with a new branch based on HEAD
|
||||
- Outside a git repository: delegates to WorktreeCreate/WorktreeRemove hooks for VCS-agnostic isolation
|
||||
- Switches the session's working directory to the new worktree
|
||||
- Use ExitWorktree to leave the worktree mid-session (keep or remove). On session exit, if still in the worktree, the user will be prompted to keep or remove it
|
||||
|
||||
## Parameters
|
||||
|
||||
- \`name\` (optional): A name for the worktree. If not provided, a random name is generated.
|
||||
`
|
||||
}
|
||||
Reference in New Issue
Block a user