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