From 97d61ce597b7a357509fa889a13468a4e9d4f443 Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:52:29 -0700 Subject: [PATCH 1/2] Add a /title command to manually name Claude Code sessions See https://github.com/anthropics/claude-code/issues/2112 --- src/utils/patches/conversationTitle.ts | 542 +++++++++++++++++++++++++ src/utils/patches/index.ts | 10 +- src/utils/patches/slashCommands.ts | 75 ++++ src/utils/patches/toolsets.ts | 69 +--- 4 files changed, 632 insertions(+), 64 deletions(-) create mode 100644 src/utils/patches/conversationTitle.ts create mode 100644 src/utils/patches/slashCommands.ts diff --git a/src/utils/patches/conversationTitle.ts b/src/utils/patches/conversationTitle.ts new file mode 100644 index 0000000..c18d737 --- /dev/null +++ b/src/utils/patches/conversationTitle.ts @@ -0,0 +1,542 @@ +// Conversation title management patch for Claude Code +// Adds ability to set conversation titles and persist them + +import { showDiff, getReactVar } from './index.js'; +import { writeSlashCommandDefinition as writeSlashCmd } from './slashCommands.js'; + +// ============================================================================ +// SUB PATCH 1: Add /title slash command +// ============================================================================ + +/** + * Sub-patch 1: Write the /title slash command definition + */ +export const writeTitleSlashCommand = (oldFile: string): string | null => { + const reactVar = getReactVar(oldFile); + if (!reactVar) { + console.error('patch: conversationTitle: failed to find React variable'); + return null; + } + + // Generate the slash command definition + const commandDef = `, { + type: "local", + name: "title", + description: "Set the conversation title", + isEnabled: () => !0, + isHidden: !1, + async call(A, B, I) { + if (!A) + throw new Error("Please specify a conversation title."); + CUR_CONVERSATION_TITLE = A; + setTerminalTitleOverride(A); + return { + type: "text", + value: \`Conversation title set to \\x1b[1m\${A}\\x1b[0m\`, + } + }, + userFacingName() { + return "title"; + }, +}`; + + return writeSlashCmd(oldFile, commandDef); +}; + +// ============================================================================ +// SUB PATCH 2: Insert custom naming functions (175 lines from insertionCode.js) +// ============================================================================ + +/** + * Sub-patch 2a: Find location to insert custom naming functions + * Searches for the class definition with summaries, messages, checkpoints, fileHistorySnapshots + */ +export const findCustomNamingFunctionsLocation = ( + fileContents: string +): number | null => { + // Match: class [$\w]+{summaries;(?:customTitles;)?messages;checkpoints;fileHistorySnapshots; + const classPattern = + /class ([$\w]+)\{summaries;(?:customTitles;)?messages;checkpoints;fileHistorySnapshots;/; + const match = fileContents.match(classPattern); + + if (!match || match.index === undefined) { + console.error( + 'patch: conversationTitle: findCustomNamingFunctionsLocation: failed to find class pattern' + ); + return null; + } + + return match.index; +}; + +/** + * Sub-patch 2b: Write the custom naming functions (insertionCode.js content) + */ +export const writeCustomNamingFunctions = (oldFile: string): string | null => { + const location = findCustomNamingFunctionsLocation(oldFile); + if (location === null) { + console.error( + 'patch: conversationTitle: failed to find custom naming functions location' + ); + return null; + } + + // The entire insertion code from insertionCode.js (175 lines) + const insertionCode = `import { + join as pathJoin, + basename as pathBasename, + dirname as pathDirname, +} from "node:path"; +import { homedir as osHomedir } from "node:os"; +import { + statSync as fsStatSync, + readFileSync as fsReadFileSync, + writeFileSync as fsWriteFileSync, + readdirSync as fsReaddirSync, + mkdirSync as fsMkdirSync, + renameSync as fsRenameSync, +} from "node:fs"; +import { randomUUID as cryptoRandomUUID } from "node:crypto"; + +function getTweakccBaseDir() { + // Prioritize ~/.tweakcc which is the original and default. Only respect + // XDG_CONFIG_HOME if it doesn't exist. + let dir; + let homedirTweakcc = pathJoin(osHomedir(), ".tweakcc"); + try { + if (fsStatSync(homedirTweakcc).isDirectory()) { + dir = homedirTweakcc; + } + } catch (e) { + if (e.code == "ENOENT") { + // Doesn't exist. Move on and see if the XDG one exists. + } else { + throw new Error('cannot stat ' + homedirTweakcc + ': ' + e); + } + } + + // Try XDG. + if (process.env.XDG_CONFIG_HOME) { + // XDG_CONFIG_HOME is set. If it's set and ~/.tweakcc doesn't exist, prefer it. + const xdgTweakcc = pathJoin(process.env.XDG_CONFIG_HOME, "tweakcc"); + dir = xdgTweakcc; + } + + // Create the dir. + fsMkdirSync(dir, { recursive: true }); + return dir; +} + +const findSummaryEntryForLeafUuid = (filePath, messageUuid) => { + const lines = fsReadFileSync(filePath, "utf8") + .split("\\n") + .map((l) => JSON.parse(l.trim())); + for (const line of lines) { + if (line.type == "summary" && line.uuid == messageUuid) { + return line; + } + } + return null; +}; + +const getSummaryFileForLeafMessage = ( + projectDirectory, + projectSlug, + messageUuid +) => { + try { + // File contains the uuid + const summaryFileId = fsReadFileSync( + pathJoin(getTweakccBaseDir(), "named-sessions", projectSlug, messageUuid), + "utf8" + ).trim(); + return pathJoin(projectDirectory, summaryFileId + '.jsonl'); + } catch (e) { + // File not found or can't be accessed, etc. Ignore. + } + + // Just read each file and try to find it. + for (const file of fsReaddirSync(projectDirectory)) { + try { + const pth = pathJoin(projectDirectory, file); + const summaryObj = findSummaryEntryForLeafUuid(pth); + if (summaryObj != null && summaryObj.tweakcc != null) { + // It needs to be a tweakcc one. + return pth; + } + } catch {} + } + + // It doesn't exist. + return null; +}; + +const setTerminalTitleOverride = (title) => { + process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE = 1; + if (process.platform === "win32") process.title = 'Claude: ' + title; + else process.stdout.write('\\x1B]0;Claude: ' + title + '\\x07'); +}; + +let CUR_CONVERSATION_TITLE = ""; +function onNewMessage(projectDir, projectSlug, msg) { + const tweakcc = getTweakccBaseDir(); + + if (msg.parentUuid) { + const path = getSummaryFileForLeafMessage( + projectDir, + projectSlug, + msg.parentUuid + ); + if (path) { + // There's an old file. Update it to the new file. + const summaryObj = findSummaryEntryForLeafUuid(path); + summaryObj.leafUuid = msg.uuid; + if (CUR_CONVERSATION_TITLE != "") { + summaryObj.summary = CUR_CONVERSATION_TITLE; + } else { + CUR_CONVERSATION_TITLE = summaryObj.summary; + } + setTerminalTitleOverride(CUR_CONVERSATION_TITLE); + fsWriteFileSync(path, JSON.stringify(summaryObj)); + + // Update the cache; it points from message ID to summary file ID. + fsMkdirSync(pathJoin(tweakcc, "named-sessions", projectSlug), { + recursive: true, + }); + const oldPath = pathJoin( + tweakcc, + "named-sessions", + projectSlug, + msg.parentUuid + ); + const newPath = pathJoin( + tweakcc, + "named-sessions", + projectSlug, + msg.uuid + ); + try { + fsRenameSync(oldPath, newPath); + return; + } catch (e) { + if (e.code == "ENOENT") { + // named-sessions/{projectSlug} exists, so the error is because the old named-session doesn't exist. + // So we need to create the new file later. + // DO NOT return. + } else { + throw new Error('cannot rename ' + oldPath + ' -> ' + newPath + ': ' + e); + } + } + } + } + + // Only create our summary entry if a custom title has been set. Because we want the auto + // title generation to kick in if the user hasn't set a title, and the auto title generation + // won't generate titles for sessions older than the most recently + if (CUR_CONVERSATION_TITLE != "") { + setTerminalTitleOverride(CUR_CONVERSATION_TITLE); + const uuid = cryptoRandomUUID(); + + // Create the summary file. + const newFilePath = pathJoin(projectDir, uuid + '.jsonl'); + const summaryObj = { + type: "summary", + summary: CUR_CONVERSATION_TITLE, + leafUuid: msg.uuid, + // This is important. + tweakcc: null, + }; + fsWriteFileSync(newFilePath, JSON.stringify(summaryObj)); + + fsMkdirSync(pathJoin(tweakcc, "named-sessions", projectSlug), { + recursive: true, + }); + fsWriteFileSync( + pathJoin(tweakcc, "named-sessions", projectSlug, msg.uuid), + uuid + ); + } +} + +`; + + const newFile = + oldFile.slice(0, location) + insertionCode + oldFile.slice(location); + + showDiff(oldFile, newFile, insertionCode, location, location); + + return newFile; +}; + +// ============================================================================ +// SUB PATCH 3: Add message append entry interceptor +// ============================================================================ + +/** + * Sub-patch 3a: Find location to insert append entry interceptor + */ +export const findAppendEntryInterceptorLocation = ( + fileContents: string +): { location: number; messageVar: string } | null => { + // Match: if(![$\w]+\.has\(([$\w]+)\.uuid\)\){if([$\w]+\.appendFileSync( + const pattern = + /(if\(![$\w]+\.has\(([$\w]+)\.uuid\)\)\{)if\([$\w]+\.appendFileSync\(/; + const match = fileContents.match(pattern); + + if (!match || match.index === undefined) { + console.error( + 'patch: conversationTitle: findAppendEntryInterceptorLocation: failed to find pattern' + ); + return null; + } + + // Insertion point is after match[1], which means at match.index + match[1].length + const location = match.index + match[1].length; + const messageVar = match[2]; + + return { location, messageVar }; +}; + +/** + * Sub-patch 3b: Write the append entry interceptor + */ +export const writeAppendEntryInterceptor = (oldFile: string): string | null => { + const result = findAppendEntryInterceptorLocation(oldFile); + if (!result) { + console.error( + 'patch: conversationTitle: failed to find append entry interceptor location' + ); + return null; + } + + const { location, messageVar } = result; + + const code = ` +const projectDir = pathDirname(this.sessionFile); +const projectSlug = pathBasename(projectDir); +onNewMessage(projectDir, projectSlug, ${messageVar}); +`; + + const newFile = oldFile.slice(0, location) + code + oldFile.slice(location); + + showDiff(oldFile, newFile, code, location, location); + + return newFile; +}; + +// ============================================================================ +// SUB PATCH 4: Add tweakcc summary check +// ============================================================================ + +/** + * Sub-patch 4a: Find tweakcc summary check locations + */ +export const findTweakccSummaryCheckLocations = ( + fileContents: string +): { + orLocation: number; + loopLocation: number; + messageVar: string; + fileListVar: string; +} | null => { + // First, find the continue statement + const continuePattern = /if\([$\w]+\.has\(([$\w]+)\.uuid\)\)continue;/; + const continueMatch = fileContents.match(continuePattern); + + if (!continueMatch || continueMatch.index === undefined) { + console.error( + 'patch: conversationTitle: findTweakccSummaryCheckLocations: failed to find continue pattern' + ); + return null; + } + + const messageVar = continueMatch[1]; + const orLocation = + continueMatch.index + continueMatch[0].length - ')continue;'.length; + + // Now search in the past 200 chars for the for loop + const searchStart = Math.max(0, continueMatch.index - 200); + const searchText = fileContents.substring(searchStart, continueMatch.index); + + const loopPattern = /for\(let [$\w]+ of ([$\w]+)\)try/; + const loopMatch = searchText.match(loopPattern); + + if (!loopMatch) { + console.error( + 'patch: conversationTitle: findTweakccSummaryCheckLocations: failed to find loop pattern' + ); + return null; + } + + const fileListVar = loopMatch[1]; + const loopLocation = searchStart + (loopMatch.index ?? 0); + + return { orLocation, loopLocation, messageVar, fileListVar }; +}; + +/** + * Sub-patch 4b: Write tweakcc summary check + */ +export const writeTweakccSummaryCheck = (oldFile: string): string | null => { + const locations = findTweakccSummaryCheckLocations(oldFile); + if (!locations) { + console.error( + 'patch: conversationTitle: failed to find tweakcc summary check locations' + ); + return null; + } + + const { orLocation, loopLocation, messageVar, fileListVar } = locations; + + // Apply modifications in reverse order to preserve indices + let newFile = oldFile; + + // First, insert at loopLocation (before the loop) + const loopCode = `const tweakccSummaries = new Set(); +for (const file of ${fileListVar}) { + const contents = fsReadFileSync(file, "utf8").trim(); + if (contents.includes("\\n")) continue; + let obj; + try { + obj = JSON.parse(contents); + } catch { + // Skip invalid files. + continue; + } + if (obj.type != "summary" || !obj.hasOwnProperty("tweakcc")) continue; + tweakccSummaries.add(obj.leafUuid); +} +`; + + newFile = + newFile.slice(0, loopLocation) + loopCode + newFile.slice(loopLocation); + + showDiff(oldFile, newFile, loopCode, loopLocation, loopLocation); + + // Adjust orLocation for the insertion we just made + const adjustedOrLocation = orLocation + loopCode.length; + + // Then, insert at orLocation (in the continue condition) + const orCode = `||tweakccSummaries.has(${messageVar}.uuid)`; + const newFile2 = + newFile.slice(0, adjustedOrLocation) + + orCode + + newFile.slice(adjustedOrLocation); + + showDiff(newFile, newFile2, orCode, adjustedOrLocation, adjustedOrLocation); + + return newFile2; +}; + +// ============================================================================ +// SUB PATCH 5: Enable rename conversation command +// ============================================================================ + +/** + * Sub-patch 5: Enable the "rename conversation" slash command + */ +export const enableRenameConversationCommand = ( + oldFile: string +): string | null => { + // Find: description:"Rename the current conversation",isEnabled:()=>!1, + const pattern = + /description:"Rename the current conversation",isEnabled:\(\)=>!1,/; + const match = oldFile.match(pattern); + + if (!match) { + console.error( + 'patch: conversationTitle: enableRenameConversationCommand: failed to find pattern' + ); + return null; + } + + if (match.index === undefined) { + console.error( + 'patch: conversationTitle: enableRenameConversationCommand: match.index is undefined' + ); + return null; + } + + // Replace !1 with !0 + const oldPattern = + 'description:"Rename the current conversation",isEnabled:()=>!1,'; + const newPattern = + 'description:"Rename the current conversation",isEnabled:()=>!0,'; + + const newFile = oldFile.replace(oldPattern, newPattern); + + if (newFile === oldFile) { + console.error( + 'patch: conversationTitle: enableRenameConversationCommand: replacement failed' + ); + return null; + } + + showDiff( + oldFile, + newFile, + newPattern, + match.index, + match.index + oldPattern.length + ); + + return newFile; +}; + +// ============================================================================ +// MAIN ORCHESTRATOR +// ============================================================================ + +/** + * Apply all conversation title patches to the file + */ +export const writeConversationTitle = (oldFile: string): string | null => { + let result: string | null = oldFile; + + // Step 1: Write /title slash command + result = writeTitleSlashCommand(result); + if (!result) { + console.error( + 'patch: conversationTitle: step 1 failed (writeTitleSlashCommand)' + ); + return null; + } + + // Step 2: Write custom naming functions + result = writeCustomNamingFunctions(result); + if (!result) { + console.error( + 'patch: conversationTitle: step 2 failed (writeCustomNamingFunctions)' + ); + return null; + } + + // Step 3: Write append entry interceptor + result = writeAppendEntryInterceptor(result); + if (!result) { + console.error( + 'patch: conversationTitle: step 3 failed (writeAppendEntryInterceptor)' + ); + return null; + } + + // Step 4: Write tweakcc summary check + result = writeTweakccSummaryCheck(result); + if (!result) { + console.error( + 'patch: conversationTitle: step 4 failed (writeTweakccSummaryCheck)' + ); + return null; + } + + // Step 5: Enable rename conversation command + result = enableRenameConversationCommand(result); + if (!result) { + console.error( + 'patch: conversationTitle: step 5 failed (enableRenameConversationCommand)' + ); + return null; + } + + return result; +}; diff --git a/src/utils/patches/index.ts b/src/utils/patches/index.ts index dc6873e..9e4d401 100644 --- a/src/utils/patches/index.ts +++ b/src/utils/patches/index.ts @@ -52,6 +52,7 @@ import { writePatchesAppliedIndication } from './patchesAppliedIndication.js'; import { applySystemPrompts } from './systemPrompts.js'; import { writeFixLspSupport } from './fixLspSupport.js'; import { writeToolsets } from './toolsets.js'; +import { writeConversationTitle } from './conversationTitle.js'; export interface LocationResult { startIndex: number; @@ -78,6 +79,10 @@ export const showDiff = ( startIndex: number, endIndex: number ): void => { + if (!isDebug()) { + return; + } + const contextStart = Math.max(0, startIndex - 20); const contextEndOld = Math.min(oldFileContents.length, endIndex + 20); const contextEndNew = Math.min( @@ -99,7 +104,7 @@ export const showDiff = ( contextEndNew ); - if (isDebug() && oldChanged !== newChanged) { + if (oldChanged !== newChanged) { console.log('\n--- Diff ---'); console.log('OLD:', oldBefore + `\x1b[31m${oldChanged}\x1b[0m` + oldAfter); console.log('NEW:', newBefore + `\x1b[32m${newChanged}\x1b[0m` + newAfter); @@ -527,6 +532,9 @@ export const applyCustomization = async ( content = result; } + // Apply conversation title management (always enabled) + if ((result = writeConversationTitle(content))) content = result; + // Write the modified content back if (ccInstInfo.nativeInstallationPath) { // For native installations: repack the modified claude.js back into the binary diff --git a/src/utils/patches/slashCommands.ts b/src/utils/patches/slashCommands.ts new file mode 100644 index 0000000..adf4d50 --- /dev/null +++ b/src/utils/patches/slashCommands.ts @@ -0,0 +1,75 @@ +// Utilities for working with slash commands in Claude Code + +import { showDiff } from './index.js'; + +/** + * Find the end position of the slash command array using stack machine + */ +export const findSlashCommandListEndPosition = ( + fileContents: string +): number | null => { + // Find the array with 30+ elements (slash commands list) + const arrayStartPattern = /=>\[([$a-zA-Z_][$\w]{1,2},){30}/; + const match = fileContents.match(arrayStartPattern); + + if (!match || match.index === undefined) { + console.error( + 'patch: findSlashCommandListEndPosition: failed to find arrayStartPattern' + ); + return null; + } + + // Find the '[' in the match + const bracketIndex = fileContents.indexOf('[', match.index); + if (bracketIndex === -1) { + console.error( + 'patch: findSlashCommandListEndPosition: failed to find bracketIndex' + ); + return null; + } + + // Use stack machine to find the matching ']' + let level = 1; // We're already inside the array + let i = bracketIndex + 1; + + while (i < fileContents.length && level > 0) { + if (fileContents[i] === '[') { + level++; + } else if (fileContents[i] === ']') { + level--; + if (level === 0) { + return i; // This is the end of the array + } + } + i++; + } + + console.error( + 'patch: findSlashCommandListEndPosition: failed to find matching closing-bracket' + ); + return null; +}; + +/** + * Generic function to write a slash command definition + */ +export const writeSlashCommandDefinition = ( + oldFile: string, + commandDef: string +): string | null => { + const arrayEnd = findSlashCommandListEndPosition(oldFile); + if (arrayEnd === null) { + console.error( + 'patch: writeSlashCommandDefinition: failed to find slash command array end position' + ); + return null; + } + + // Insert before the closing ']' + const newFile = + oldFile.slice(0, arrayEnd) + commandDef + oldFile.slice(arrayEnd); + + showDiff(oldFile, newFile, commandDef, arrayEnd, arrayEnd); + + return newFile; +}; diff --git a/src/utils/patches/toolsets.ts b/src/utils/patches/toolsets.ts index 4567298..008536a 100644 --- a/src/utils/patches/toolsets.ts +++ b/src/utils/patches/toolsets.ts @@ -7,6 +7,10 @@ import { findBoxComponent, getReactVar, } from './index.js'; +import { + findSlashCommandListEndPosition, + writeSlashCommandDefinition as writeSlashCommandDefinitionToArray, +} from './slashCommands.js'; import { Toolset } from '../types.js'; // ============================================================================ @@ -188,54 +192,6 @@ export const getToolFetchingUseMemoLocation = ( }; }; -/** - * Find the end position of the slash command array using stack machine - */ -export const findSlashCommandListEndPosition = ( - fileContents: string -): number | null => { - // Find the array with 30+ elements (slash commands list) - const arrayStartPattern = /=>\[([$a-zA-Z_][$\w]{1,2},){30}/; - const match = fileContents.match(arrayStartPattern); - - if (!match || match.index === undefined) { - console.error( - 'patch: findSlashCommandListEndPosition: failed to find arrayStartPattern' - ); - return null; - } - - // Find the '[' in the match - const bracketIndex = fileContents.indexOf('[', match.index); - if (bracketIndex === -1) { - console.error( - 'patch: findSlashCommandListEndPosition: failed to find bracketIndex' - ); - return null; - } - - // Use stack machine to find the matching ']' - let level = 1; // We're already inside the array - let i = bracketIndex + 1; - - while (i < fileContents.length && level > 0) { - if (fileContents[i] === '[') { - level++; - } else if (fileContents[i] === ']') { - level--; - if (level === 0) { - return i; // This is the end of the array - } - } - i++; - } - - console.error( - 'patch: findSlashCommandListEndPosition: failed to find matching closing-bracket' - ); - return null; -}; - /** * Find the top-level position before the slash command list * This is where we'll insert the toolset component definition @@ -580,14 +536,6 @@ export const writeToolsetComponentDefinition = ( * Sub-patch 4: Add the slash command definition */ export const writeSlashCommandDefinition = (oldFile: string): string | null => { - const arrayEnd = findSlashCommandListEndPosition(oldFile); - if (arrayEnd === null) { - console.error( - 'patch: toolsets: failed to find slash command array end position' - ); - return null; - } - const reactVar = getReactVar(oldFile); if (!reactVar) { console.error('patch: toolsets: failed to find React variable'); @@ -611,13 +559,8 @@ export const writeSlashCommandDefinition = (oldFile: string): string | null => { } }`; - // Insert before the closing ']' - const newFile = - oldFile.slice(0, arrayEnd) + commandDef + oldFile.slice(arrayEnd); - - showDiff(oldFile, newFile, commandDef, arrayEnd, arrayEnd); - - return newFile; + // Use the imported function to write the command definition + return writeSlashCommandDefinitionToArray(oldFile, commandDef); }; // ============================================================================ From 5631ed392ea6ece74face52d937318341f07d977 Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:04:47 -0700 Subject: [PATCH 2/2] doc --- CHANGELOG.md | 2 ++ README.md | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7489a63..599e9aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Add `/title` and `/rename` slash commands to CC for manual session naming per [CC#2112](https://github.com/anthropics/claude-code/issues/2112) (#199) - @bl-ue + ## [v3.0.2](https://github.com/Piebald-AI/tweakcc/releases/tag/v3.0.2) - 2025-11-13 - Create an example `config.json` if it doesn't exist and cli.js isn't found (#195) - @bl-ue diff --git a/README.md b/README.md index d741b07..a2bfce7 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ tweakcc is a CLI tool that upgrades your Claude Code experience. Customize its system prompts, add custom themes, create toolsets, and personalize the UI. > [!important] -> **NEW in 3.0.0:** tweakcc now supports Claude Code native installations! It also adds a new feature to CC called **toolsets**—try them out in Claude Code with the `/toolsets` command after you create them in tweakcc. Lastly it makes [Claude Code's native LSP server support work.](https://www.reddit.com/r/ClaudeAI/comments/1otdfo9/lsp_is_coming_to_claude_code_and_you_can_try_it/) +> **NEW in 3.1.0:** tweakcc now enables you to manually name your Claude Code sessions—run `npx tweakc --apply` now to get a `/title my chat title` slash command (or `/rename`)! > [!tip] -> **NEW in 2.0.0:** tweakcc can now customize all of Claude Code's system prompts! +> **NEW in 3.0.0:** tweakcc now supports Claude Code native installations! It also adds a new feature to CC called **toolsets**—try them out in Claude Code with the `/toolsets` command after you create them in tweakcc. Lastly it makes [Claude Code's native LSP server support work.](https://www.reddit.com/r/ClaudeAI/comments/1otdfo9/lsp_is_coming_to_claude_code_and_you_can_try_it/)