-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Add AI-generated worktree branch naming with safe Git branch rename flow #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
8416ecd
Add AI-generated worktree branch naming and safe branch rename
juliusmarminge 06912c3
Move worktree branch auto-rename into provider turn reactor
juliusmarminge 61e1bab
Scope upstream refresh in checkout effect lifecycle
juliusmarminge 0df68cc
Remove git.renameBranch RPC and harden branch rename args
juliusmarminge 9b0fe5e
Generate temporary worktree branch names from UUID tokens
juliusmarminge d3d0a08
Resolve persisted attachment paths across server pipelines
juliusmarminge 44e409f
nit
juliusmarminge 4bc43a0
data uri for app server
juliusmarminge ad79b5a
nit
juliusmarminge 700579b
fine there
juliusmarminge 0e146e2
Handle branch-name generation failures as typed errors
juliusmarminge e64ad4b
Use Effect Schema to decode Codex structured outputs
juliusmarminge e1a8ee1
Handle parameterized base64 image data URLs
juliusmarminge 875e6ed
Inject test ServerConfig across orchestration and git layers
juliusmarminge cd732a2
fix: address PR review feedback
juliusmarminge 5850315
Switch image attachments to persisted ID-based paths
juliusmarminge 88dc6c9
Harden attachment ID resolution and missing-file handling
juliusmarminge fb92741
Read stdin before validating required image flag in codex test stub
juliusmarminge dadda0d
Reuse attachment thread segment sanitizer in projection pipeline
juliusmarminge afc0c78
Fix attachment cleanup to match exact thread segments
juliusmarminge 39f0786
Normalize attachment thread segments to lowercase
juliusmarminge 150e171
Remove unused attachment route path helpers
juliusmarminge File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import path from "node:path"; | ||
|
|
||
| export const ATTACHMENTS_ROUTE_PREFIX = "/attachments"; | ||
|
|
||
| export function normalizeAttachmentRelativePath(rawRelativePath: string): string | null { | ||
| const normalized = path.normalize(rawRelativePath).replace(/^[/\\]+/, ""); | ||
| if (normalized.length === 0 || normalized.startsWith("..") || normalized.includes("\0")) { | ||
| return null; | ||
| } | ||
| return normalized.replace(/\\/g, "/"); | ||
| } | ||
|
|
||
| export function resolveAttachmentRelativePath(input: { | ||
| readonly stateDir: string; | ||
| readonly relativePath: string; | ||
| }): string | null { | ||
| const normalizedRelativePath = normalizeAttachmentRelativePath(input.relativePath); | ||
| if (!normalizedRelativePath) { | ||
| return null; | ||
| } | ||
|
|
||
| const attachmentsRoot = path.resolve(path.join(input.stateDir, "attachments")); | ||
| const filePath = path.resolve(path.join(attachmentsRoot, normalizedRelativePath)); | ||
| if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) { | ||
| return null; | ||
| } | ||
| return filePath; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
|
|
||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { | ||
| createAttachmentId, | ||
| parseThreadSegmentFromAttachmentId, | ||
| resolveAttachmentPathById, | ||
| } from "./attachmentStore.ts"; | ||
|
|
||
| describe("attachmentStore", () => { | ||
| it("sanitizes thread ids when creating attachment ids", () => { | ||
| const attachmentId = createAttachmentId("thread.folder/unsafe space"); | ||
| expect(attachmentId).toBeTruthy(); | ||
| if (!attachmentId) { | ||
| return; | ||
| } | ||
|
|
||
| const threadSegment = parseThreadSegmentFromAttachmentId(attachmentId); | ||
| expect(threadSegment).toBeTruthy(); | ||
| expect(threadSegment).toMatch(/^[a-z0-9_-]+$/i); | ||
| expect(threadSegment).not.toContain("."); | ||
| expect(threadSegment).not.toContain("%"); | ||
| expect(threadSegment).not.toContain("/"); | ||
| }); | ||
|
|
||
| it("parses exact thread segments from attachment ids without prefix collisions", () => { | ||
| const fooId = "foo-00000000-0000-4000-8000-000000000001"; | ||
| const fooBarId = "foo-bar-00000000-0000-4000-8000-000000000002"; | ||
|
|
||
| expect(parseThreadSegmentFromAttachmentId(fooId)).toBe("foo"); | ||
| expect(parseThreadSegmentFromAttachmentId(fooBarId)).toBe("foo-bar"); | ||
| }); | ||
|
|
||
| it("normalizes created thread segments to lowercase", () => { | ||
| const attachmentId = createAttachmentId("Thread.Foo"); | ||
| expect(attachmentId).toBeTruthy(); | ||
| if (!attachmentId) { | ||
| return; | ||
| } | ||
| expect(parseThreadSegmentFromAttachmentId(attachmentId)).toBe("thread-foo"); | ||
| }); | ||
|
|
||
| it("resolves attachment path by id using the extension that exists on disk", () => { | ||
| const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-")); | ||
| try { | ||
| const attachmentId = "thread-1-attachment"; | ||
| const attachmentsDir = path.join(stateDir, "attachments"); | ||
| fs.mkdirSync(attachmentsDir, { recursive: true }); | ||
| const pngPath = path.join(attachmentsDir, `${attachmentId}.png`); | ||
| fs.writeFileSync(pngPath, Buffer.from("hello")); | ||
|
|
||
| const resolved = resolveAttachmentPathById({ | ||
| stateDir, | ||
| attachmentId, | ||
| }); | ||
| expect(resolved).toBe(pngPath); | ||
| } finally { | ||
| fs.rmSync(stateDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
|
|
||
| it("returns null when no attachment file exists for the id", () => { | ||
| const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-")); | ||
| try { | ||
| const resolved = resolveAttachmentPathById({ | ||
| stateDir, | ||
| attachmentId: "thread-1-missing", | ||
| }); | ||
| expect(resolved).toBeNull(); | ||
| } finally { | ||
| fs.rmSync(stateDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import { randomUUID } from "node:crypto"; | ||
| import { existsSync } from "node:fs"; | ||
|
|
||
| import type { ChatAttachment } from "@t3tools/contracts"; | ||
|
|
||
| import { | ||
| normalizeAttachmentRelativePath, | ||
| resolveAttachmentRelativePath, | ||
| } from "./attachmentPaths.ts"; | ||
| import { inferImageExtension, SAFE_IMAGE_FILE_EXTENSIONS } from "./imageMime.ts"; | ||
|
|
||
| const ATTACHMENT_FILENAME_EXTENSIONS = [...SAFE_IMAGE_FILE_EXTENSIONS, ".bin"]; | ||
| const ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS = 80; | ||
| const ATTACHMENT_ID_THREAD_SEGMENT_PATTERN = "[a-z0-9_]+(?:-[a-z0-9_]+)*"; | ||
| const ATTACHMENT_ID_UUID_PATTERN = | ||
| "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; | ||
| const ATTACHMENT_ID_PATTERN = new RegExp( | ||
| `^(${ATTACHMENT_ID_THREAD_SEGMENT_PATTERN})-(${ATTACHMENT_ID_UUID_PATTERN})$`, | ||
| "i", | ||
| ); | ||
|
|
||
| export function toSafeThreadAttachmentSegment(threadId: string): string | null { | ||
| const segment = threadId | ||
| .trim() | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9_-]+/gi, "-") | ||
| .replace(/-+/g, "-") | ||
| .replace(/^[-_]+|[-_]+$/g, "") | ||
| .slice(0, ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS) | ||
| .replace(/[-_]+$/g, ""); | ||
| if (segment.length === 0) { | ||
| return null; | ||
| } | ||
| return segment; | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| export function createAttachmentId(threadId: string): string | null { | ||
| const threadSegment = toSafeThreadAttachmentSegment(threadId); | ||
| if (!threadSegment) { | ||
| return null; | ||
| } | ||
| return `${threadSegment}-${randomUUID()}`; | ||
| } | ||
|
|
||
| export function parseThreadSegmentFromAttachmentId(attachmentId: string): string | null { | ||
| const normalizedId = normalizeAttachmentRelativePath(attachmentId); | ||
| if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) { | ||
| return null; | ||
| } | ||
| const match = normalizedId.match(ATTACHMENT_ID_PATTERN); | ||
| if (!match) { | ||
| return null; | ||
| } | ||
| return match[1]?.toLowerCase() ?? null; | ||
| } | ||
|
|
||
| export function attachmentRelativePath(attachment: ChatAttachment): string { | ||
| switch (attachment.type) { | ||
| case "image": { | ||
| const extension = inferImageExtension({ | ||
| mimeType: attachment.mimeType, | ||
| fileName: attachment.name, | ||
| }); | ||
| return `${attachment.id}${extension}`; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export function resolveAttachmentPath(input: { | ||
| readonly stateDir: string; | ||
| readonly attachment: ChatAttachment; | ||
| }): string | null { | ||
| return resolveAttachmentRelativePath({ | ||
| stateDir: input.stateDir, | ||
| relativePath: attachmentRelativePath(input.attachment), | ||
| }); | ||
| } | ||
|
|
||
| export function resolveAttachmentPathById(input: { | ||
| readonly stateDir: string; | ||
| readonly attachmentId: string; | ||
| }): string | null { | ||
| const normalizedId = normalizeAttachmentRelativePath(input.attachmentId); | ||
| if (!normalizedId || normalizedId.includes("/") || normalizedId.includes(".")) { | ||
|
macroscopeapp[bot] marked this conversation as resolved.
|
||
| return null; | ||
| } | ||
| for (const extension of ATTACHMENT_FILENAME_EXTENSIONS) { | ||
| const maybePath = resolveAttachmentRelativePath({ | ||
| stateDir: input.stateDir, | ||
| relativePath: `${normalizedId}${extension}`, | ||
| }); | ||
| if (maybePath && existsSync(maybePath)) { | ||
| return maybePath; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
juliusmarminge marked this conversation as resolved.
|
||
|
|
||
| export function parseAttachmentIdFromRelativePath(relativePath: string): string | null { | ||
| const normalized = normalizeAttachmentRelativePath(relativePath); | ||
| if (!normalized || normalized.includes("/")) { | ||
| return null; | ||
| } | ||
| const extensionIndex = normalized.lastIndexOf("."); | ||
| if (extensionIndex <= 0) { | ||
| return null; | ||
| } | ||
| const id = normalized.slice(0, extensionIndex); | ||
| return id.length > 0 && !id.includes(".") ? id : null; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
馃煛 Medium
src/attachmentStore.ts:16Thread segment sanitization allows hyphens, causing prefix collisions during cleanup. Thread
fooproduces segmentfoo, so.startsWith('foo-')incorrectly matches files from threadfoo-bar(segmentfoo-bar-uuid). Consider replacing hyphens with a different character or using a hash-based approach to ensure unique, non-colliding prefixes.馃殌 Reply "fix it for me" or copy this AI Prompt for your agent: