| ... | @@ -17,7 +17,10 @@ import { | ... | @@ -17,7 +17,10 @@ import { |
| 17 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect"; | 17 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect"; |
| 18 | | 18 | |
| 19 | import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; | 19 | import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; |
| 20 | import { resolveAttachmentRelativePath, resolveAttachmentRoutePath } from "../../attachmentPaths.ts"; | 20 | import { |
| | 21 | resolveAttachmentRelativePath, |
| | 22 | resolveAttachmentRoutePath, |
| | 23 | } from "../../attachmentPaths.ts"; |
| 21 | import { ServerConfig } from "../../config.ts"; | 24 | import { ServerConfig } from "../../config.ts"; |
| 22 | import { GitCore } from "../../git/Services/GitCore.ts"; | 25 | import { GitCore } from "../../git/Services/GitCore.ts"; |
| 23 | import { TextGeneration } from "../../git/Services/TextGeneration.ts"; | 26 | import { TextGeneration } from "../../git/Services/TextGeneration.ts"; |
| ... | @@ -229,7 +232,8 @@ const make = Effect.gen(function* () { | ... | @@ -229,7 +232,8 @@ const make = Effect.gen(function* () { |
| 229 | const existingSessionId = thread.session?.providerSessionId; | 232 | const existingSessionId = thread.session?.providerSessionId; |
| 230 | if (existingSessionId) { | 233 | if (existingSessionId) { |
| 231 | const approvalPolicyChanged = | 234 | const approvalPolicyChanged = |
| 232 | options?.approvalPolicy !== undefined && options.approvalPolicy !== thread.session?.approvalPolicy; | 235 | options?.approvalPolicy !== undefined && |
| | 236 | options.approvalPolicy !== thread.session?.approvalPolicy; |
| 233 | const sandboxModeChanged = | 237 | const sandboxModeChanged = |
| 234 | options?.sandboxMode !== undefined && options.sandboxMode !== thread.session?.sandboxMode; | 238 | options?.sandboxMode !== undefined && options.sandboxMode !== thread.session?.sandboxMode; |
| 235 | | 239 | |
| ... | @@ -237,7 +241,9 @@ const make = Effect.gen(function* () { | ... | @@ -237,7 +241,9 @@ const make = Effect.gen(function* () { |
| 237 | return existingSessionId; | 241 | return existingSessionId; |
| 238 | } | 242 | } |
| 239 | | 243 | |
| 240 | const restartedSession = yield* startProviderSession(thread.session?.providerThreadId ?? null); | 244 | const restartedSession = yield* startProviderSession( |
| | 245 | thread.session?.providerThreadId ?? null, |
| | 246 | ); |
| 241 | yield* bindSessionToThread(restartedSession); | 247 | yield* bindSessionToThread(restartedSession); |
| 242 | yield* providerService.stopSession({ sessionId: existingSessionId }).pipe( | 248 | yield* providerService.stopSession({ sessionId: existingSessionId }).pipe( |
| 243 | Effect.catchCause((cause) => | 249 | Effect.catchCause((cause) => |
| ... | @@ -286,13 +292,11 @@ const make = Effect.gen(function* () { | ... | @@ -286,13 +292,11 @@ const make = Effect.gen(function* () { |
| 286 | }); | 292 | }); |
| 287 | }); | 293 | }); |
| 288 | | 294 | |
| 289 | const resolvePersistedAttachments = Effect.fnUntraced(function* ( | 295 | const resolvePersistedAttachments = Effect.fnUntraced(function* (input: { |
| 290 | input: { | 296 | readonly threadId: ThreadId; |
| 291 | readonly threadId: ThreadId; | 297 | readonly messageId: string; |
| 292 | readonly messageId: string; | 298 | readonly attachments: ReadonlyArray<ChatAttachment> | undefined; |
| 293 | readonly attachments: ReadonlyArray<ChatAttachment> | undefined; | 299 | }) { |
| 294 | }, | | |
| 295 | ) { | | |
| 296 | if (!input.attachments || input.attachments.length === 0) { | 300 | if (!input.attachments || input.attachments.length === 0) { |
| 297 | return input.attachments ?? []; | 301 | return input.attachments ?? []; |
| 298 | } | 302 | } |
| ... | @@ -303,54 +307,52 @@ const make = Effect.gen(function* () { | ... | @@ -303,54 +307,52 @@ const make = Effect.gen(function* () { |
| 303 | const threadSegment = encodeURIComponent(input.threadId); | 307 | const threadSegment = encodeURIComponent(input.threadId); |
| 304 | const messageSegment = encodeURIComponent(input.messageId); | 308 | const messageSegment = encodeURIComponent(input.messageId); |
| 305 | | 309 | |
| 306 | return yield* Effect.forEach( | 310 | return yield* Effect.forEach(Array.from(input.attachments.entries()), ([index, attachment]) => |
| 307 | Array.from(input.attachments.entries()), | 311 | Effect.gen(function* () { |
| 308 | ([index, attachment]) => | 312 | if (attachment.type !== "image") { |
| 309 | Effect.gen(function* () { | 313 | return attachment; |
| 310 | if (attachment.type !== "image") { | 314 | } |
| 311 | return attachment; | | |
| 312 | } | | |
| 313 | | | |
| 314 | const resolvedRoutePath = resolveAttachmentRoutePath({ | | |
| 315 | stateDir: serverConfig.value.stateDir, | | |
| 316 | dataUrl: attachment.dataUrl, | | |
| 317 | }); | | |
| 318 | const resolvedMaterializedPath = | | |
| 319 | resolvedRoutePath ?? | | |
| 320 | (() => { | | |
| 321 | const parsed = parseBase64DataUrl(attachment.dataUrl); | | |
| 322 | if (!parsed || !parsed.mimeType.startsWith("image/")) { | | |
| 323 | return null; | | |
| 324 | } | | |
| 325 | const extension = inferImageExtension({ | | |
| 326 | mimeType: parsed.mimeType, | | |
| 327 | fileName: attachment.name, | | |
| 328 | }); | | |
| 329 | return resolveAttachmentRelativePath({ | | |
| 330 | stateDir: serverConfig.value.stateDir, | | |
| 331 | relativePath: `${threadSegment}/${messageSegment}-${index}${extension}`, | | |
| 332 | }); | | |
| 333 | })(); | | |
| 334 | | | |
| 335 | if (!resolvedMaterializedPath) { | | |
| 336 | return attachment; | | |
| 337 | } | | |
| 338 | | 315 | |
| 339 | const isFile = yield* Effect.sync(() => { | 316 | const resolvedRoutePath = resolveAttachmentRoutePath({ |
| 340 | try { | 317 | stateDir: serverConfig.value.stateDir, |
| 341 | return fs.statSync(resolvedMaterializedPath).isFile(); | 318 | dataUrl: attachment.dataUrl, |
| 342 | } catch { | 319 | }); |
| 343 | return false; | 320 | const resolvedMaterializedPath = |
| | 321 | resolvedRoutePath ?? |
| | 322 | (() => { |
| | 323 | const parsed = parseBase64DataUrl(attachment.dataUrl); |
| | 324 | if (!parsed || !parsed.mimeType.startsWith("image/")) { |
| | 325 | return null; |
| 344 | } | 326 | } |
| 345 | }); | 327 | const extension = inferImageExtension({ |
| 346 | if (!isFile) { | 328 | mimeType: parsed.mimeType, |
| 347 | return attachment; | 329 | fileName: attachment.name, |
| | 330 | }); |
| | 331 | return resolveAttachmentRelativePath({ |
| | 332 | stateDir: serverConfig.value.stateDir, |
| | 333 | relativePath: `${threadSegment}/${messageSegment}-${index}${extension}`, |
| | 334 | }); |
| | 335 | })(); |
| | 336 | |
| | 337 | if (!resolvedMaterializedPath) { |
| | 338 | return attachment; |
| | 339 | } |
| | 340 | |
| | 341 | const isFile = yield* Effect.sync(() => { |
| | 342 | try { |
| | 343 | return fs.statSync(resolvedMaterializedPath).isFile(); |
| | 344 | } catch { |
| | 345 | return false; |
| 348 | } | 346 | } |
| 349 | return { | 347 | }); |
| 350 | ...attachment, | 348 | if (!isFile) { |
| 351 | dataUrl: resolvedMaterializedPath, | 349 | return attachment; |
| 352 | } satisfies ChatAttachment; | 350 | } |
| 353 | }), | 351 | return { |
| | 352 | ...attachment, |
| | 353 | dataUrl: resolvedMaterializedPath, |
| | 354 | } satisfies ChatAttachment; |
| | 355 | }), |
| 354 | ); | 356 | ); |
| 355 | }); | 357 | }); |
| 356 | | 358 | |
| ... | @@ -382,51 +384,45 @@ const make = Effect.gen(function* () { | ... | @@ -382,51 +384,45 @@ const make = Effect.gen(function* () { |
| 382 | const oldBranch = input.branch; | 384 | const oldBranch = input.branch; |
| 383 | const cwd = input.worktreePath; | 385 | const cwd = input.worktreePath; |
| 384 | const attachments = input.attachments ?? []; | 386 | const attachments = input.attachments ?? []; |
| 385 | const renameEffect = textGeneration | 387 | yield* textGeneration |
| 386 | .generateBranchName({ | 388 | .generateBranchName({ |
| 387 | cwd, | 389 | cwd, |
| 388 | message: input.messageText, | 390 | message: input.messageText, |
| 389 | ...(attachments.length > 0 ? { attachments } : {}), | 391 | ...(attachments.length > 0 ? { attachments } : {}), |
| 390 | }) | 392 | }) |
| 391 | .pipe( | 393 | .pipe( |
| | 394 | Effect.catch((error) => |
| | 395 | Effect.logWarning( |
| | 396 | "provider command reactor failed to generate worktree branch name; skipping rename", |
| | 397 | { threadId: input.threadId, cwd, oldBranch, reason: error.message }, |
| | 398 | ), |
| | 399 | ), |
| 392 | Effect.flatMap((generated) => { | 400 | Effect.flatMap((generated) => { |
| 393 | if (!generated.branch) { | 401 | if (!generated) return Effect.void; |
| 394 | return Effect.void; | 402 | |
| 395 | } | | |
| 396 | const targetBranch = buildGeneratedWorktreeBranchName(generated.branch); | 403 | const targetBranch = buildGeneratedWorktreeBranchName(generated.branch); |
| 397 | if (targetBranch === oldBranch) { | 404 | if (targetBranch === oldBranch) return Effect.void; |
| 398 | return Effect.void; | 405 | |
| 399 | } | 406 | return Effect.flatMap( |
| 400 | return git | 407 | git.renameBranch({ cwd, oldBranch, newBranch: targetBranch }), |
| 401 | .renameBranch({ | 408 | (renamed) => |
| 402 | cwd, | 409 | orchestrationEngine.dispatch({ |
| 403 | oldBranch, | 410 | type: "thread.meta.update", |
| 404 | newBranch: targetBranch, | 411 | commandId: serverCommandId("worktree-branch-rename"), |
| 405 | }) | 412 | threadId: input.threadId, |
| 406 | .pipe( | 413 | branch: renamed.branch, |
| 407 | Effect.flatMap((renamed) => | 414 | worktreePath: cwd, |
| 408 | orchestrationEngine.dispatch({ | 415 | }), |
| 409 | type: "thread.meta.update", | 416 | ); |
| 410 | commandId: serverCommandId("worktree-branch-rename"), | | |
| 411 | threadId: input.threadId, | | |
| 412 | branch: renamed.branch, | | |
| 413 | worktreePath: cwd, | | |
| 414 | }), | | |
| 415 | ), | | |
| 416 | Effect.asVoid, | | |
| 417 | ); | | |
| 418 | }), | 417 | }), |
| 419 | Effect.catchCause((cause) => | 418 | Effect.catchCause((cause) => |
| 420 | Effect.logWarning("provider command reactor failed to generate or rename worktree branch", { | 419 | Effect.logWarning( |
| 421 | threadId: input.threadId, | 420 | "provider command reactor failed to generate or rename worktree branch", |
| 422 | cwd, | 421 | { threadId: input.threadId, cwd, oldBranch, cause: Cause.pretty(cause) }, |
| 423 | oldBranch, | 422 | ), |
| 424 | cause: Cause.pretty(cause), | | |
| 425 | }), | | |
| 426 | ), | 423 | ), |
| | 424 | Effect.forkScoped, |
| 427 | ); | 425 | ); |
| 428 | | | |
| 429 | yield* Effect.forkScoped(renameEffect); | | |
| 430 | }); | 426 | }); |
| 431 | | 427 | |
| 432 | const processTurnStartRequested = Effect.fnUntraced(function* ( | 428 | const processTurnStartRequested = Effect.fnUntraced(function* ( |