{"record":{"id":"d49bb5f7e8d64216","repo":"paperclipai/paperclip","slug":"discard-is-unavailable","errorCode":null,"errorMessage":"Discard is unavailable.","messagePattern":"Discard is unavailable\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ui/src/components/TaskChatThread.tsx","lineNumber":2728,"sourceCode":"                  }}\n                  onSteer={async (commentId, revision) => {\n                    if (!onSteerQueuedComment)\n                      throw new Error(\"Steering is unavailable.\");\n                    await onSteerQueuedComment(commentId, revision);\n                  }}\n                  onInterrupt={\n                    onInterruptQueued && queuedMessageQueue.targetRunId\n                      ? async () => {\n                          await onInterruptQueued(\n                            queuedMessageQueue.targetRunId!,\n                          );\n                        }\n                      : undefined\n                  }\n                  onDiscard={async (commentId, revision) => {\n                    if (commentId.startsWith(\"optimistic-\")) {\n                      if (!onCancelQueued)\n                        throw new Error(\"Discard is unavailable.\");\n                      onCancelQueued(commentId);\n                      return;\n                    }\n                    if (!onDiscardQueuedComment)\n                      throw new Error(\"Discard is unavailable.\");\n                    await onDiscardQueuedComment(commentId, revision);\n                    if (queuedEdit?.commentId === commentId)\n                      setQueuedEdit(null);\n                  }}\n                />\n              ) : null}\n              <div className=\"relative z-10\">\n                <TaskChatComposer\n                  onAdd={handleThreadAdd}\n                  onStop={liveRun ? onCancelRun : undefined}\n                  stopPending={stopPending}\n                  stopScope={stopScope}\n                  workMode={issueWorkMode}","sourceCodeStart":2710,"sourceCodeEnd":2746,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/ui/src/components/TaskChatThread.tsx#L2710-L2746","documentation":"In TaskChatThread's onDiscard handler there are two failure paths that throw \"Discard is unavailable.\" This instance (line 2728) fires for comments whose id starts with the \"optimistic-\" prefix: those are local, not-yet-acknowledged queue entries that can only be removed via the onCancelQueued callback. If that prop was not supplied, the component throws rather than attempting to discard a comment that does not exist server-side.","triggerScenarios":"User discards a queued message that was added optimistically (id starts with \"optimistic-\") while the onCancelQueued prop is undefined in the embedding TaskChatThread.","commonSituations":"Custom embed of TaskChatThread without optimistic-cancel wiring; a parent that wires onDiscardQueuedComment but not onCancelQueued; stale optimistic entries from a failed queue post being discarded before server confirmation.","solutions":["Pass the onCancelQueued prop to TaskChatThread at the call site.","Ensure the parent's optimistic queue-add path also wires the cancel handler symmetrically.","If optimistic queuing is not needed, render the queue without optimistic ids so discard goes through onDiscardQueuedComment.","Guard the discard button to only render when the matching cancel/discard callback exists."],"exampleFix":"// before\n<TaskChatThread issueId={id} onDiscardQueuedComment={discard} />\n\n// after\n<TaskChatThread\n  issueId={id}\n  onDiscardQueuedComment={discard}\n  onCancelQueued={(optimisticId) => removeOptimisticComment(optimisticId)}\n/>","handlingStrategy":"validation","validationCode":"const canCancelOptimistic =\n  commentId.startsWith(\"optimistic-\") && typeof onCancelQueued === \"function\";\nif (commentId.startsWith(\"optimistic-\") && !canCancelOptimistic) disableDiscardButton();","typeGuard":"const isOptimistic = (id: string) => id.startsWith(\"optimistic-\");\nconst hasCancel = (p: unknown): p is (id: string) => void => typeof p === \"function\";","tryCatchPattern":"try {\n  await onDiscard(commentId, revision);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Discard is unavailable.\") {\n    toast(\"Discarding is not wired up for this view.\");\n  } else throw e;\n}","preventionTips":["Wire onCancelQueued whenever optimistic queue entries are created.","Pair every optimistic add with its cancel path in the same component.","Disable discard for optimistic ids when no cancel handler exists."],"tags":["react","ui","missing-prop","optimistic-ui"],"backgroundTag":"missing-required-argument","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}