mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 19:43:15 +00:00
Clears ~83 pre-existing eslint errors in the UI: auto-fixable rules (array-type, import/order, sort-imports, type-only imports, redundant assertions/conditions) plus manual fixes for unnecessary conditions and banned @ts-nocheck directives. The 8 ported components excluded from tsconfig are now also ignored by eslint so type-aware linting no longer fails to parse them. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
202 lines
6.1 KiB
TypeScript
202 lines
6.1 KiB
TypeScript
import { memo } from "react"
|
|
import { MessageCircle } from "lucide-react"
|
|
import { Markdown } from "./Markdown"
|
|
import type { ReactNode } from "react"
|
|
import type { ToolExecutionChunk } from "@/lib/agents/types"
|
|
|
|
interface ReplyCardProps {
|
|
chunk: ToolExecutionChunk
|
|
}
|
|
|
|
function headerLabel(
|
|
isLinear: boolean,
|
|
status: ToolExecutionChunk["status"]
|
|
): string {
|
|
const pending = status === "in_progress" || status === "pending"
|
|
if (isLinear) return pending ? "Commenting on Linear…" : "Commented on Linear"
|
|
return pending ? "Replying in Slack…" : "Replied in Slack"
|
|
}
|
|
|
|
const SLACK_TOKEN = /<([^>]+)>/g
|
|
const LINK_CLASS =
|
|
"text-[color:var(--ui-accent)] underline decoration-[color:var(--ui-accent)]/50 break-words [overflow-wrap:anywhere]"
|
|
|
|
type SlackTextObject = { type?: string; text?: string }
|
|
type SlackBlock = {
|
|
type?: string
|
|
text?: SlackTextObject
|
|
elements?: Array<{ type?: string; text?: SlackTextObject }>
|
|
}
|
|
|
|
function isSlackTextObject(value: unknown): value is SlackTextObject {
|
|
return (
|
|
!!value &&
|
|
typeof value === "object" &&
|
|
typeof (value as SlackTextObject).text === "string"
|
|
)
|
|
}
|
|
|
|
function isSlackBlockArray(value: unknown): value is Array<SlackBlock> {
|
|
return (
|
|
Array.isArray(value) &&
|
|
value.every((block) => !!block && typeof block === "object")
|
|
)
|
|
}
|
|
|
|
// Slack mrkdwn isn't standard Markdown — rewrite its link/mention syntax for display
|
|
// rather than feeding it to the Markdown renderer (which mis-renders *bold* etc.).
|
|
function renderSlackBody(text: string): Array<ReactNode> {
|
|
const nodes: Array<ReactNode> = []
|
|
let lastIndex = 0
|
|
let key = 0
|
|
SLACK_TOKEN.lastIndex = 0
|
|
for (
|
|
let match = SLACK_TOKEN.exec(text);
|
|
match;
|
|
match = SLACK_TOKEN.exec(text)
|
|
) {
|
|
if (match.index > lastIndex) nodes.push(text.slice(lastIndex, match.index))
|
|
const token = match[1] ?? ""
|
|
const sep = token.indexOf("|")
|
|
const target = sep === -1 ? token : token.slice(0, sep)
|
|
const label = sep === -1 ? "" : token.slice(sep + 1)
|
|
if (target.startsWith("@") || target.startsWith("#")) {
|
|
const sigil = target[0]
|
|
nodes.push(
|
|
<span key={key++} className="text-[color:var(--ui-accent)]">
|
|
{sigil}
|
|
{label || target.slice(1)}
|
|
</span>
|
|
)
|
|
} else if (target.startsWith("!")) {
|
|
nodes.push(
|
|
<span key={key++} className="text-[color:var(--ui-accent)]">
|
|
@{label || target.slice(1)}
|
|
</span>
|
|
)
|
|
} else {
|
|
nodes.push(
|
|
<a
|
|
key={key++}
|
|
href={target}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className={LINK_CLASS}
|
|
>
|
|
{label || target}
|
|
</a>
|
|
)
|
|
}
|
|
lastIndex = match.index + match[0].length
|
|
}
|
|
if (lastIndex < text.length) nodes.push(text.slice(lastIndex))
|
|
return nodes
|
|
}
|
|
|
|
function blocksFromOptions(
|
|
message: string,
|
|
options: unknown
|
|
): Array<SlackBlock> | null {
|
|
if (!Array.isArray(options)) return null
|
|
const cleanOptions = options.filter(
|
|
(option): option is string =>
|
|
typeof option === "string" && option.trim().length > 0
|
|
)
|
|
if (cleanOptions.length === 0) return null
|
|
return [
|
|
{ type: "section", text: { type: "mrkdwn", text: message } },
|
|
{
|
|
type: "actions",
|
|
elements: cleanOptions.slice(0, 5).map((option) => ({
|
|
type: "button",
|
|
text: { type: "plain_text", text: option },
|
|
})),
|
|
},
|
|
]
|
|
}
|
|
|
|
function renderSlackBlocks(blocks: Array<SlackBlock>): ReactNode {
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
{blocks.map((block, index) => {
|
|
if (
|
|
(block.type === "section" || block.type === "context") &&
|
|
isSlackTextObject(block.text)
|
|
) {
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="[overflow-wrap:anywhere] break-words whitespace-pre-wrap"
|
|
>
|
|
{renderSlackBody(block.text.text ?? "")}
|
|
</div>
|
|
)
|
|
}
|
|
if (block.type === "actions" && Array.isArray(block.elements)) {
|
|
return (
|
|
<div key={index} className="flex flex-wrap gap-2">
|
|
{block.elements.map((element, elementIndex) => {
|
|
const label = isSlackTextObject(element.text)
|
|
? element.text.text
|
|
: element.type || "Action"
|
|
return (
|
|
<span
|
|
key={elementIndex}
|
|
className="rounded-md border border-[var(--ui-border)] bg-[var(--ui-panel)] px-2 py-1 text-[12px] text-[color:var(--ui-text)]"
|
|
>
|
|
{label}
|
|
</span>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
if (block.type === "divider") {
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="border-t border-[var(--ui-border-subtle)]"
|
|
/>
|
|
)
|
|
}
|
|
return null
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const ReplyCard = memo(function ReplyCard({ chunk }: ReplyCardProps) {
|
|
const isLinear = chunk.toolKind === "linear"
|
|
const body =
|
|
((isLinear ? chunk.input?.comment_body : chunk.input?.message) as string) ||
|
|
""
|
|
const blocks = !isLinear
|
|
? isSlackBlockArray(chunk.input?.blocks)
|
|
? chunk.input.blocks
|
|
: blocksFromOptions(body, chunk.input?.options)
|
|
: null
|
|
|
|
return (
|
|
<div className="my-1">
|
|
<div className="flex items-center gap-1.5 py-1 text-[12px] text-[color:var(--ui-text-muted)]">
|
|
<MessageCircle className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
<span>{headerLabel(isLinear, chunk.status)}</span>
|
|
</div>
|
|
{body && (
|
|
<div className="overflow-hidden rounded-xl border border-[var(--ui-border-subtle)] bg-[var(--ui-code-bubble)]">
|
|
<div className="max-h-[250px] overflow-auto px-3 py-2 text-[13px] text-[color:var(--ui-text)]">
|
|
{isLinear ? (
|
|
<Markdown content={body} />
|
|
) : blocks ? (
|
|
renderSlackBlocks(blocks)
|
|
) : (
|
|
<div className="[overflow-wrap:anywhere] break-words whitespace-pre-wrap">
|
|
{renderSlackBody(body)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})
|