继续处理会话区多语言
This commit is contained in:
@@ -5,6 +5,7 @@ import type { ComponentProps } from "react"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { ArrowDownIcon, DownloadIcon } from "lucide-react"
|
import { ArrowDownIcon, DownloadIcon } from "lucide-react"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
import { useCallback } from "react"
|
import { useCallback } from "react"
|
||||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"
|
||||||
|
|
||||||
@@ -42,32 +43,37 @@ export type MessageThreadEmptyStateProps = ComponentProps<"div"> & {
|
|||||||
|
|
||||||
export const MessageThreadEmptyState = ({
|
export const MessageThreadEmptyState = ({
|
||||||
className,
|
className,
|
||||||
title = "No messages yet",
|
title,
|
||||||
description = "Start a conversation to see messages here",
|
description,
|
||||||
icon,
|
icon,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: MessageThreadEmptyStateProps) => (
|
}: MessageThreadEmptyStateProps) => {
|
||||||
<div
|
const t = useTranslations("Folder.chat.messageThread")
|
||||||
className={cn(
|
return (
|
||||||
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
|
<div
|
||||||
className
|
className={cn(
|
||||||
)}
|
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
|
||||||
{...props}
|
className
|
||||||
>
|
)}
|
||||||
{children ?? (
|
{...props}
|
||||||
<>
|
>
|
||||||
{icon && <div className="text-muted-foreground">{icon}</div>}
|
{children ?? (
|
||||||
<div className="space-y-1">
|
<>
|
||||||
<h3 className="font-medium text-sm">{title}</h3>
|
{icon && <div className="text-muted-foreground">{icon}</div>}
|
||||||
{description && (
|
<div className="space-y-1">
|
||||||
<p className="text-muted-foreground text-sm">{description}</p>
|
<h3 className="font-medium text-sm">{title ?? t("emptyTitle")}</h3>
|
||||||
)}
|
{(description ?? t("emptyDescription")) && (
|
||||||
</div>
|
<p className="text-muted-foreground text-sm">
|
||||||
</>
|
{description ?? t("emptyDescription")}
|
||||||
)}
|
</p>
|
||||||
</div>
|
)}
|
||||||
)
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export type MessageThreadScrollButtonProps = ComponentProps<typeof Button>
|
export type MessageThreadScrollButtonProps = ComponentProps<typeof Button>
|
||||||
|
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ export const ToolOutput = ({
|
|||||||
errorText,
|
errorText,
|
||||||
...props
|
...props
|
||||||
}: ToolOutputProps) => {
|
}: ToolOutputProps) => {
|
||||||
|
const t = useTranslations("Folder.chat.tool")
|
||||||
if (!(output || errorText)) {
|
if (!(output || errorText)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -348,7 +349,7 @@ export const ToolOutput = ({
|
|||||||
return (
|
return (
|
||||||
<div className={cn("space-y-2", className)} {...props}>
|
<div className={cn("space-y-2", className)} {...props}>
|
||||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
{errorText ? "Error" : "Result"}
|
{errorText ? t("error") : t("result")}
|
||||||
</h4>
|
</h4>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -1084,6 +1084,7 @@ function sanitizeLiveTitle(title: string | null | undefined): string | null {
|
|||||||
|
|
||||||
/** Edit tool: file path + unified diff view */
|
/** Edit tool: file path + unified diff view */
|
||||||
function EditToolInput({ input }: { input: Record<string, unknown> }) {
|
function EditToolInput({ input }: { input: Record<string, unknown> }) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const filePath = str(input, "file_path")
|
const filePath = str(input, "file_path")
|
||||||
const oldString = str(input, "old_string") ?? ""
|
const oldString = str(input, "old_string") ?? ""
|
||||||
const newString = str(input, "new_string") ?? ""
|
const newString = str(input, "new_string") ?? ""
|
||||||
@@ -1109,11 +1110,11 @@ function EditToolInput({ input }: { input: Record<string, unknown> }) {
|
|||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<FilePenLineIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
<FilePenLineIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||||
<span className="break-all font-mono text-foreground">
|
<span className="break-all font-mono text-foreground">
|
||||||
{filePath ?? "unknown"}
|
{filePath ?? t("unknown")}
|
||||||
</span>
|
</span>
|
||||||
{replaceAll && (
|
{replaceAll && (
|
||||||
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
||||||
REPLACE ALL
|
{t("replaceAll")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1124,6 +1125,7 @@ function EditToolInput({ input }: { input: Record<string, unknown> }) {
|
|||||||
|
|
||||||
/** Edit tool (changes payload): file list + summary + combined diff view */
|
/** Edit tool (changes payload): file list + summary + combined diff view */
|
||||||
function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) {
|
function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const { additions, deletions, diffCode } = useMemo(() => {
|
const { additions, deletions, diffCode } = useMemo(() => {
|
||||||
let additions = 0
|
let additions = 0
|
||||||
let deletions = 0
|
let deletions = 0
|
||||||
@@ -1167,7 +1169,7 @@ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
||||||
<span>Files: {changes.length}</span>
|
<span>{t("filesCount", { count: changes.length })}</span>
|
||||||
{additions > 0 && <span>+{additions}</span>}
|
{additions > 0 && <span>+{additions}</span>}
|
||||||
{deletions > 0 && <span>-{deletions}</span>}
|
{deletions > 0 && <span>-{deletions}</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -1175,7 +1177,7 @@ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) {
|
|||||||
{changes.slice(0, 8).map((change, index) => (
|
{changes.slice(0, 8).map((change, index) => (
|
||||||
<div key={`${change.path}-${index}`} className="flex gap-2 text-xs">
|
<div key={`${change.path}-${index}`} className="flex gap-2 text-xs">
|
||||||
<span className="shrink-0 rounded bg-blue-500/15 px-1.5 py-0.5 font-medium uppercase text-blue-600">
|
<span className="shrink-0 rounded bg-blue-500/15 px-1.5 py-0.5 font-medium uppercase text-blue-600">
|
||||||
update
|
{t("update")}
|
||||||
</span>
|
</span>
|
||||||
<span className="break-all font-mono text-foreground">
|
<span className="break-all font-mono text-foreground">
|
||||||
{change.path}
|
{change.path}
|
||||||
@@ -1184,7 +1186,7 @@ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) {
|
|||||||
))}
|
))}
|
||||||
{changes.length > 8 && (
|
{changes.length > 8 && (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
+{changes.length - 8} more files
|
{t("moreFiles", { count: changes.length - 8 })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1195,6 +1197,7 @@ function EditChangesToolInput({ changes }: { changes: EditChangePreview[] }) {
|
|||||||
|
|
||||||
/** Bash / exec_command: terminal-style command display */
|
/** Bash / exec_command: terminal-style command display */
|
||||||
function BashToolInput({ input }: { input: Record<string, unknown> }) {
|
function BashToolInput({ input }: { input: Record<string, unknown> }) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const command =
|
const command =
|
||||||
commandFromUnknownValue(input) ??
|
commandFromUnknownValue(input) ??
|
||||||
str(input, "command") ??
|
str(input, "command") ??
|
||||||
@@ -1216,8 +1219,8 @@ function BashToolInput({ input }: { input: Record<string, unknown> }) {
|
|||||||
{displayCommand && <CodeBlock code={displayCommand} language="bash" />}
|
{displayCommand && <CodeBlock code={displayCommand} language="bash" />}
|
||||||
{(timeout || background) && (
|
{(timeout || background) && (
|
||||||
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
||||||
{timeout && <span>Timeout: {timeout}ms</span>}
|
{timeout && <span>{t("timeoutMs", { timeout })}</span>}
|
||||||
{background && <span>Background: true</span>}
|
{background && <span>{t("backgroundTrue")}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1232,6 +1235,7 @@ function FileToolInput({
|
|||||||
toolName: string
|
toolName: string
|
||||||
input: Record<string, unknown>
|
input: Record<string, unknown>
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const name = toolName.toLowerCase()
|
const name = toolName.toLowerCase()
|
||||||
const filePath =
|
const filePath =
|
||||||
str(input, "file_path") ?? str(input, "path") ?? str(input, "notebook_path")
|
str(input, "file_path") ?? str(input, "path") ?? str(input, "notebook_path")
|
||||||
@@ -1263,15 +1267,15 @@ function FileToolInput({
|
|||||||
)}
|
)}
|
||||||
{(offset != null || limit != null || pages) && (
|
{(offset != null || limit != null || pages) && (
|
||||||
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
||||||
{offset != null && <span>Offset: {offset}</span>}
|
{offset != null && <span>{t("offset", { offset })}</span>}
|
||||||
{limit != null && <span>Limit: {limit}</span>}
|
{limit != null && <span>{t("limit", { limit })}</span>}
|
||||||
{pages && <span>Pages: {pages}</span>}
|
{pages && <span>{t("pages", { pages })}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(cellType || editMode) && (
|
{(cellType || editMode) && (
|
||||||
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
||||||
{editMode && <span>Mode: {editMode}</span>}
|
{editMode && <span>{t("mode", { mode: editMode })}</span>}
|
||||||
{cellType && <span>Cell: {cellType}</span>}
|
{cellType && <span>{t("cell", { cell: cellType })}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(name === "write" || name === "notebookedit") &&
|
{(name === "write" || name === "notebookedit") &&
|
||||||
@@ -1295,6 +1299,7 @@ function SearchToolInput({
|
|||||||
toolName: string
|
toolName: string
|
||||||
input: Record<string, unknown>
|
input: Record<string, unknown>
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const name = toolName.toLowerCase()
|
const name = toolName.toLowerCase()
|
||||||
const pattern = str(input, "pattern")
|
const pattern = str(input, "pattern")
|
||||||
const path = str(input, "path")
|
const path = str(input, "path")
|
||||||
@@ -1315,27 +1320,30 @@ function SearchToolInput({
|
|||||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||||
{path && (
|
{path && (
|
||||||
<span>
|
<span>
|
||||||
Path: <span className="font-mono text-foreground">{path}</span>
|
{t("pathLabel")}{" "}
|
||||||
|
<span className="font-mono text-foreground">{path}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{glob && (
|
{glob && (
|
||||||
<span>
|
<span>
|
||||||
Glob: <span className="font-mono text-foreground">{glob}</span>
|
{t("globLabel")}{" "}
|
||||||
|
<span className="font-mono text-foreground">{glob}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{fileType && (
|
{fileType && (
|
||||||
<span>
|
<span>
|
||||||
Type: <span className="font-mono text-foreground">{fileType}</span>
|
{t("typeLabel")}{" "}
|
||||||
|
<span className="font-mono text-foreground">{fileType}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{name === "grep" && outputMode && (
|
{name === "grep" && outputMode && (
|
||||||
<span>
|
<span>
|
||||||
Output:{" "}
|
{t("outputLabel")}{" "}
|
||||||
<span className="font-mono text-foreground">{outputMode}</span>
|
<span className="font-mono text-foreground">{outputMode}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{caseInsensitive && <span>Case insensitive</span>}
|
{caseInsensitive && <span>{t("caseInsensitive")}</span>}
|
||||||
{multiline && <span>Multiline</span>}
|
{multiline && <span>{t("multiline")}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -1349,6 +1357,7 @@ function WebToolInput({
|
|||||||
toolName: string
|
toolName: string
|
||||||
input: Record<string, unknown>
|
input: Record<string, unknown>
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const name = toolName.toLowerCase()
|
const name = toolName.toLowerCase()
|
||||||
const url = str(input, "url")
|
const url = str(input, "url")
|
||||||
const query = str(input, "query")
|
const query = str(input, "query")
|
||||||
@@ -1375,7 +1384,7 @@ function WebToolInput({
|
|||||||
{prompt && (
|
{prompt && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
Prompt
|
{t("promptLabel")}
|
||||||
</span>
|
</span>
|
||||||
<div className="rounded-md bg-muted/50 p-3 text-xs prose prose-sm dark:prose-invert max-w-none [&_ul]:list-inside [&_ol]:list-inside">
|
<div className="rounded-md bg-muted/50 p-3 text-xs prose prose-sm dark:prose-invert max-w-none [&_ul]:list-inside [&_ol]:list-inside">
|
||||||
<MessageResponse>{prompt}</MessageResponse>
|
<MessageResponse>{prompt}</MessageResponse>
|
||||||
@@ -1388,6 +1397,7 @@ function WebToolInput({
|
|||||||
|
|
||||||
/** Task tools: description / subject focused */
|
/** Task tools: description / subject focused */
|
||||||
function TaskToolInput({ input }: { input: Record<string, unknown> }) {
|
function TaskToolInput({ input }: { input: Record<string, unknown> }) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const subject = str(input, "subject")
|
const subject = str(input, "subject")
|
||||||
const taskId = str(input, "taskId")
|
const taskId = str(input, "taskId")
|
||||||
const status = str(input, "status")
|
const status = str(input, "status")
|
||||||
@@ -1401,7 +1411,7 @@ function TaskToolInput({ input }: { input: Record<string, unknown> }) {
|
|||||||
{subject && (
|
{subject && (
|
||||||
<div className="flex items-baseline gap-2 text-xs">
|
<div className="flex items-baseline gap-2 text-xs">
|
||||||
<span className="shrink-0 font-medium text-muted-foreground">
|
<span className="shrink-0 font-medium text-muted-foreground">
|
||||||
Subject
|
{t("subjectLabel")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-foreground">{subject}</span>
|
<span className="text-foreground">{subject}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1409,7 +1419,7 @@ function TaskToolInput({ input }: { input: Record<string, unknown> }) {
|
|||||||
{taskId && (
|
{taskId && (
|
||||||
<div className="flex items-baseline gap-2 text-xs">
|
<div className="flex items-baseline gap-2 text-xs">
|
||||||
<span className="shrink-0 font-medium text-muted-foreground">
|
<span className="shrink-0 font-medium text-muted-foreground">
|
||||||
Task
|
{t("taskLabel")}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-foreground">
|
<span className="font-mono text-foreground">
|
||||||
#{taskId}
|
#{taskId}
|
||||||
@@ -1419,7 +1429,8 @@ function TaskToolInput({ input }: { input: Record<string, unknown> }) {
|
|||||||
)}
|
)}
|
||||||
{agentName && (
|
{agentName && (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Name: <span className="font-mono text-foreground">{agentName}</span>
|
{t("nameLabel")}{" "}
|
||||||
|
<span className="font-mono text-foreground">{agentName}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1487,6 +1498,7 @@ function TodoWriteToolInput({ input }: { input: Record<string, unknown> }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ApplyPatchToolInput({ input }: { input: string }) {
|
function ApplyPatchToolInput({ input }: { input: string }) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const { files, additions, deletions } = useMemo(
|
const { files, additions, deletions } = useMemo(
|
||||||
() => parseApplyPatchInput(input),
|
() => parseApplyPatchInput(input),
|
||||||
[input]
|
[input]
|
||||||
@@ -1501,7 +1513,7 @@ function ApplyPatchToolInput({ input }: { input: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
||||||
<span>Files: {files.length}</span>
|
<span>{t("filesCount", { count: files.length })}</span>
|
||||||
{additions > 0 && <span>+{additions}</span>}
|
{additions > 0 && <span>+{additions}</span>}
|
||||||
{deletions > 0 && <span>-{deletions}</span>}
|
{deletions > 0 && <span>-{deletions}</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -1521,7 +1533,7 @@ function ApplyPatchToolInput({ input }: { input: string }) {
|
|||||||
))}
|
))}
|
||||||
{files.length > 8 && (
|
{files.length > 8 && (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
+{files.length - 8} more files
|
{t("moreFiles", { count: files.length - 8 })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { ChevronRight, ChevronDown, Wrench, AlertCircle } from "lucide-react"
|
import { ChevronRight, ChevronDown, Wrench, AlertCircle } from "lucide-react"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
interface ToolCallBlockProps {
|
interface ToolCallBlockProps {
|
||||||
@@ -17,6 +18,7 @@ export function ToolCallBlock({
|
|||||||
content,
|
content,
|
||||||
isError = false,
|
isError = false,
|
||||||
}: ToolCallBlockProps) {
|
}: ToolCallBlockProps) {
|
||||||
|
const t = useTranslations("Folder.chat.toolCallBlock")
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,7 +42,7 @@ export function ToolCallBlock({
|
|||||||
{type === "tool_use" ? (
|
{type === "tool_use" ? (
|
||||||
<>
|
<>
|
||||||
<Wrench className="h-3 w-3 shrink-0 text-muted-foreground" />
|
<Wrench className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||||
<span className="font-medium">{toolName || "Tool"}</span>
|
<span className="font-medium">{toolName || t("tool")}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -49,7 +51,9 @@ export function ToolCallBlock({
|
|||||||
) : (
|
) : (
|
||||||
<Wrench className="h-3 w-3 shrink-0 text-muted-foreground" />
|
<Wrench className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="font-medium">{isError ? "Error" : "Result"}</span>
|
<span className="font-medium">
|
||||||
|
{isError ? t("error") : t("result")}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react"
|
} from "react"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
import { useFolderContext } from "@/contexts/folder-context"
|
import { useFolderContext } from "@/contexts/folder-context"
|
||||||
import { useWorkspaceContext } from "@/contexts/workspace-context"
|
import { useWorkspaceContext } from "@/contexts/workspace-context"
|
||||||
import { saveFolderOpenedConversations } from "@/lib/tauri"
|
import { saveFolderOpenedConversations } from "@/lib/tauri"
|
||||||
@@ -107,6 +108,7 @@ interface TabProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TabProvider({ children }: TabProviderProps) {
|
export function TabProvider({ children }: TabProviderProps) {
|
||||||
|
const t = useTranslations("Folder.tabContext")
|
||||||
const { activateConversationPane } = useWorkspaceContext()
|
const { activateConversationPane } = useWorkspaceContext()
|
||||||
const {
|
const {
|
||||||
folder,
|
folder,
|
||||||
@@ -131,7 +133,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
kind: "conversation" as const,
|
kind: "conversation" as const,
|
||||||
conversationId: selectedConversation.id,
|
conversationId: selectedConversation.id,
|
||||||
agentType: selectedConversation.agentType,
|
agentType: selectedConversation.agentType,
|
||||||
title: "Loading...",
|
title: t("loadingConversation"),
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -188,7 +190,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
kind: "conversation" as const,
|
kind: "conversation" as const,
|
||||||
conversationId: oc.conversation_id,
|
conversationId: oc.conversation_id,
|
||||||
agentType: oc.agent_type,
|
agentType: oc.agent_type,
|
||||||
title: "Loading...",
|
title: t("loadingConversation"),
|
||||||
isPinned: oc.is_pinned,
|
isPinned: oc.is_pinned,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -204,7 +206,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [folder, restoredFolderId])
|
}, [folder, restoredFolderId, t])
|
||||||
|
|
||||||
// Sync restored active tab to FolderProvider (deferred to avoid
|
// Sync restored active tab to FolderProvider (deferred to avoid
|
||||||
// updating parent during child render)
|
// updating parent during child render)
|
||||||
@@ -280,7 +282,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
`${tab.agentType}-${tab.conversationId}`
|
`${tab.agentType}-${tab.conversationId}`
|
||||||
)
|
)
|
||||||
if (conv) {
|
if (conv) {
|
||||||
const newTitle = conv.title || "Untitled conversation"
|
const newTitle = conv.title || t("untitledConversation")
|
||||||
const newStatus = conv.status as ConversationStatus | undefined
|
const newStatus = conv.status as ConversationStatus | undefined
|
||||||
if (tab.title !== newTitle || tab.status !== newStatus) {
|
if (tab.title !== newTitle || tab.status !== newStatus) {
|
||||||
return { ...tab, title: newTitle, status: newStatus }
|
return { ...tab, title: newTitle, status: newStatus }
|
||||||
@@ -289,7 +291,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
}
|
}
|
||||||
return tab
|
return tab
|
||||||
})
|
})
|
||||||
}, [rawTabs, conversationMap])
|
}, [rawTabs, conversationMap, t])
|
||||||
|
|
||||||
const syncFolderContext = useCallback(
|
const syncFolderContext = useCallback(
|
||||||
(tab: TabItem | null) => {
|
(tab: TabItem | null) => {
|
||||||
@@ -347,7 +349,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
conversationsRef.current.find(
|
conversationsRef.current.find(
|
||||||
(c) => c.id === conversationId && c.agent_type === agentType
|
(c) => c.id === conversationId && c.agent_type === agentType
|
||||||
)?.title ??
|
)?.title ??
|
||||||
"Untitled conversation"
|
t("untitledConversation")
|
||||||
|
|
||||||
const tabId = makeConversationTabId(agentType, conversationId)
|
const tabId = makeConversationTabId(agentType, conversationId)
|
||||||
activateTabId = tabId
|
activateTabId = tabId
|
||||||
@@ -381,7 +383,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
selectConversation(conversationId, agentType)
|
selectConversation(conversationId, agentType)
|
||||||
activateConversationPane()
|
activateConversationPane()
|
||||||
},
|
},
|
||||||
[activateConversationPane, selectConversation]
|
[activateConversationPane, selectConversation, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const makeReplacementNewConversationTab = useCallback(
|
const makeReplacementNewConversationTab = useCallback(
|
||||||
@@ -389,11 +391,11 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
id: makeNewConversationTabId(),
|
id: makeNewConversationTabId(),
|
||||||
kind: "new_conversation",
|
kind: "new_conversation",
|
||||||
agentType: preferred?.agentType ?? "codex",
|
agentType: preferred?.agentType ?? "codex",
|
||||||
title: "New Conversation",
|
title: t("newConversation"),
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
workingDir: preferred?.workingDir ?? folder?.path,
|
workingDir: preferred?.workingDir ?? folder?.path,
|
||||||
}),
|
}),
|
||||||
[folder?.path]
|
[folder?.path, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const closeTab = useCallback(
|
const closeTab = useCallback(
|
||||||
@@ -517,7 +519,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
id: tabId,
|
id: tabId,
|
||||||
kind: "new_conversation",
|
kind: "new_conversation",
|
||||||
agentType,
|
agentType,
|
||||||
title: "New Conversation",
|
title: t("newConversation"),
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
workingDir,
|
workingDir,
|
||||||
}
|
}
|
||||||
@@ -527,7 +529,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
startNewConversation(agentType, workingDir)
|
startNewConversation(agentType, workingDir)
|
||||||
activateConversationPane()
|
activateConversationPane()
|
||||||
},
|
},
|
||||||
[activateConversationPane, startNewConversation, syncFolderContext]
|
[activateConversationPane, startNewConversation, syncFolderContext, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const linkTabConversation = useCallback(
|
const linkTabConversation = useCallback(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
import { useAcpActions } from "@/contexts/acp-connections-context"
|
import { useAcpActions } from "@/contexts/acp-connections-context"
|
||||||
import { useTaskContext } from "@/contexts/task-context"
|
import { useTaskContext } from "@/contexts/task-context"
|
||||||
import { useConnection, type UseConnectionReturn } from "@/hooks/use-connection"
|
import { useConnection, type UseConnectionReturn } from "@/hooks/use-connection"
|
||||||
@@ -48,6 +49,7 @@ export function useConnectionLifecycle({
|
|||||||
workingDir,
|
workingDir,
|
||||||
sessionId,
|
sessionId,
|
||||||
}: UseConnectionLifecycleOptions): UseConnectionLifecycleReturn {
|
}: UseConnectionLifecycleOptions): UseConnectionLifecycleReturn {
|
||||||
|
const t = useTranslations("Folder.chat.connectionLifecycle")
|
||||||
const { setActiveKey, touchActivity } = useAcpActions()
|
const { setActiveKey, touchActivity } = useAcpActions()
|
||||||
const { addTask, updateTask, removeTask } = useTaskContext()
|
const { addTask, updateTask, removeTask } = useTaskContext()
|
||||||
const conn = useConnection(contextKey)
|
const conn = useConnection(contextKey)
|
||||||
@@ -170,7 +172,11 @@ export function useConnectionLifecycle({
|
|||||||
const id = `acp-connect-${Date.now()}`
|
const id = `acp-connect-${Date.now()}`
|
||||||
taskIdRef.current = id
|
taskIdRef.current = id
|
||||||
const agent = AGENT_LABELS[agentType]
|
const agent = AGENT_LABELS[agentType]
|
||||||
addTask(id, `Connecting to ${agent}`, `Establishing connection`)
|
addTask(
|
||||||
|
id,
|
||||||
|
t("tasks.connectingTitle", { agent }),
|
||||||
|
t("tasks.connectingDescription")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
updateTask(taskIdRef.current, { status: "running" })
|
updateTask(taskIdRef.current, { status: "running" })
|
||||||
} else if (status === "connected" || status === "prompting") {
|
} else if (status === "connected" || status === "prompting") {
|
||||||
@@ -182,7 +188,7 @@ export function useConnectionLifecycle({
|
|||||||
if (taskIdRef.current) {
|
if (taskIdRef.current) {
|
||||||
updateTask(taskIdRef.current, {
|
updateTask(taskIdRef.current, {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error: "Connection failed",
|
error: t("errors.connectionFailed"),
|
||||||
})
|
})
|
||||||
taskIdRef.current = null
|
taskIdRef.current = null
|
||||||
}
|
}
|
||||||
@@ -192,7 +198,7 @@ export function useConnectionLifecycle({
|
|||||||
taskIdRef.current = null
|
taskIdRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [status, addTask, updateTask, removeTask, agentType])
|
}, [status, addTask, updateTask, removeTask, agentType, t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "prompting") return
|
if (status === "prompting") return
|
||||||
@@ -235,8 +241,8 @@ export function useConnectionLifecycle({
|
|||||||
const agent = AGENT_LABELS[agentType]
|
const agent = AGENT_LABELS[agentType]
|
||||||
addTask(
|
addTask(
|
||||||
id,
|
id,
|
||||||
`Loading ${agent} selectors`,
|
t("tasks.loadingSelectorsTitle", { agent }),
|
||||||
"Fetching mode and session config options"
|
t("tasks.loadingSelectorsDescription")
|
||||||
)
|
)
|
||||||
updateTask(id, { status: "running" })
|
updateTask(id, { status: "running" })
|
||||||
}
|
}
|
||||||
@@ -257,6 +263,7 @@ export function useConnectionLifecycle({
|
|||||||
addTask,
|
addTask,
|
||||||
updateTask,
|
updateTask,
|
||||||
clearSelectorTask,
|
clearSelectorTask,
|
||||||
|
t,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Clean up lingering task on unmount (e.g. tab closed while connecting)
|
// Clean up lingering task on unmount (e.g. tab closed while connecting)
|
||||||
|
|||||||
@@ -919,6 +919,11 @@
|
|||||||
"kindFile": "file"
|
"kindFile": "file"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tabContext": {
|
||||||
|
"loadingConversation": "Loading...",
|
||||||
|
"untitledConversation": "Untitled conversation",
|
||||||
|
"newConversation": "New Conversation"
|
||||||
|
},
|
||||||
"fileTreeTab": {
|
"fileTreeTab": {
|
||||||
"workspace": "Workspace",
|
"workspace": "Workspace",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
@@ -1056,6 +1061,21 @@
|
|||||||
"diffDescriptionConflict": "{path} · disk vs unsaved"
|
"diffDescriptionConflict": "{path} · disk vs unsaved"
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
|
"connectionLifecycle": {
|
||||||
|
"tasks": {
|
||||||
|
"connectingTitle": "Connecting to {agent}",
|
||||||
|
"connectingDescription": "Establishing connection",
|
||||||
|
"loadingSelectorsTitle": "Loading {agent} selectors",
|
||||||
|
"loadingSelectorsDescription": "Fetching mode and session config options"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"connectionFailed": "Connection failed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageThread": {
|
||||||
|
"emptyTitle": "No messages yet",
|
||||||
|
"emptyDescription": "Start a conversation to see messages here"
|
||||||
|
},
|
||||||
"chatInput": {
|
"chatInput": {
|
||||||
"connecting": "Connecting...",
|
"connecting": "Connecting...",
|
||||||
"agentResponding": "Agent is responding...",
|
"agentResponding": "Agent is responding...",
|
||||||
@@ -1140,6 +1160,8 @@
|
|||||||
},
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"parameters": "Parameters",
|
"parameters": "Parameters",
|
||||||
|
"error": "Error",
|
||||||
|
"result": "Result",
|
||||||
"status": {
|
"status": {
|
||||||
"approvalRequested": "Awaiting Approval",
|
"approvalRequested": "Awaiting Approval",
|
||||||
"approvalResponded": "Responded",
|
"approvalResponded": "Responded",
|
||||||
@@ -1150,9 +1172,36 @@
|
|||||||
"outputError": "Error"
|
"outputError": "Error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"toolCallBlock": {
|
||||||
|
"tool": "Tool",
|
||||||
|
"error": "Error",
|
||||||
|
"result": "Result"
|
||||||
|
},
|
||||||
"contentParts": {
|
"contentParts": {
|
||||||
"showingTailOutput": "Showing tail output while streaming for performance.",
|
"showingTailOutput": "Showing tail output while streaming for performance.",
|
||||||
"result": "Result"
|
"result": "Result",
|
||||||
|
"unknown": "unknown",
|
||||||
|
"replaceAll": "REPLACE ALL",
|
||||||
|
"filesCount": "Files: {count}",
|
||||||
|
"update": "update",
|
||||||
|
"moreFiles": "+{count} more files",
|
||||||
|
"timeoutMs": "Timeout: {timeout}ms",
|
||||||
|
"backgroundTrue": "Background: true",
|
||||||
|
"offset": "Offset: {offset}",
|
||||||
|
"limit": "Limit: {limit}",
|
||||||
|
"pages": "Pages: {pages}",
|
||||||
|
"mode": "Mode: {mode}",
|
||||||
|
"cell": "Cell: {cell}",
|
||||||
|
"pathLabel": "Path:",
|
||||||
|
"globLabel": "Glob:",
|
||||||
|
"typeLabel": "Type:",
|
||||||
|
"outputLabel": "Output:",
|
||||||
|
"caseInsensitive": "Case insensitive",
|
||||||
|
"multiline": "Multiline",
|
||||||
|
"promptLabel": "Prompt",
|
||||||
|
"subjectLabel": "Subject",
|
||||||
|
"taskLabel": "Task",
|
||||||
|
"nameLabel": "Name:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"diffPreview": {
|
"diffPreview": {
|
||||||
|
|||||||
@@ -919,6 +919,11 @@
|
|||||||
"kindFile": "文件"
|
"kindFile": "文件"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tabContext": {
|
||||||
|
"loadingConversation": "加载中...",
|
||||||
|
"untitledConversation": "未命名会话",
|
||||||
|
"newConversation": "新建会话"
|
||||||
|
},
|
||||||
"fileTreeTab": {
|
"fileTreeTab": {
|
||||||
"workspace": "工作区",
|
"workspace": "工作区",
|
||||||
"retry": "重试",
|
"retry": "重试",
|
||||||
@@ -1056,6 +1061,21 @@
|
|||||||
"diffDescriptionConflict": "{path} · 磁盘与未保存内容"
|
"diffDescriptionConflict": "{path} · 磁盘与未保存内容"
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
|
"connectionLifecycle": {
|
||||||
|
"tasks": {
|
||||||
|
"connectingTitle": "正在连接 {agent}",
|
||||||
|
"connectingDescription": "正在建立连接",
|
||||||
|
"loadingSelectorsTitle": "正在加载 {agent} 选择项",
|
||||||
|
"loadingSelectorsDescription": "正在获取模式和会话配置选项"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"connectionFailed": "连接失败"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageThread": {
|
||||||
|
"emptyTitle": "暂无消息",
|
||||||
|
"emptyDescription": "开始一个会话后,消息会显示在这里"
|
||||||
|
},
|
||||||
"chatInput": {
|
"chatInput": {
|
||||||
"connecting": "连接中...",
|
"connecting": "连接中...",
|
||||||
"agentResponding": "Agent 正在响应...",
|
"agentResponding": "Agent 正在响应...",
|
||||||
@@ -1140,6 +1160,8 @@
|
|||||||
},
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"parameters": "参数",
|
"parameters": "参数",
|
||||||
|
"error": "错误",
|
||||||
|
"result": "结果",
|
||||||
"status": {
|
"status": {
|
||||||
"approvalRequested": "等待授权",
|
"approvalRequested": "等待授权",
|
||||||
"approvalResponded": "已响应",
|
"approvalResponded": "已响应",
|
||||||
@@ -1150,9 +1172,36 @@
|
|||||||
"outputError": "错误"
|
"outputError": "错误"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"toolCallBlock": {
|
||||||
|
"tool": "工具",
|
||||||
|
"error": "错误",
|
||||||
|
"result": "结果"
|
||||||
|
},
|
||||||
"contentParts": {
|
"contentParts": {
|
||||||
"showingTailOutput": "为保证性能,流式输出时仅显示尾部内容。",
|
"showingTailOutput": "为保证性能,流式输出时仅显示尾部内容。",
|
||||||
"result": "结果"
|
"result": "结果",
|
||||||
|
"unknown": "未知",
|
||||||
|
"replaceAll": "全部替换",
|
||||||
|
"filesCount": "文件:{count}",
|
||||||
|
"update": "更新",
|
||||||
|
"moreFiles": "+{count} 个更多文件",
|
||||||
|
"timeoutMs": "超时:{timeout}ms",
|
||||||
|
"backgroundTrue": "后台:true",
|
||||||
|
"offset": "偏移:{offset}",
|
||||||
|
"limit": "限制:{limit}",
|
||||||
|
"pages": "页码:{pages}",
|
||||||
|
"mode": "模式:{mode}",
|
||||||
|
"cell": "单元:{cell}",
|
||||||
|
"pathLabel": "路径:",
|
||||||
|
"globLabel": "Glob:",
|
||||||
|
"typeLabel": "类型:",
|
||||||
|
"outputLabel": "输出:",
|
||||||
|
"caseInsensitive": "忽略大小写",
|
||||||
|
"multiline": "多行",
|
||||||
|
"promptLabel": "提示词",
|
||||||
|
"subjectLabel": "主题",
|
||||||
|
"taskLabel": "任务",
|
||||||
|
"nameLabel": "名称:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"diffPreview": {
|
"diffPreview": {
|
||||||
|
|||||||
@@ -919,6 +919,11 @@
|
|||||||
"kindFile": "檔案"
|
"kindFile": "檔案"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tabContext": {
|
||||||
|
"loadingConversation": "載入中...",
|
||||||
|
"untitledConversation": "未命名會話",
|
||||||
|
"newConversation": "新增會話"
|
||||||
|
},
|
||||||
"fileTreeTab": {
|
"fileTreeTab": {
|
||||||
"workspace": "工作區",
|
"workspace": "工作區",
|
||||||
"retry": "重試",
|
"retry": "重試",
|
||||||
@@ -1056,6 +1061,21 @@
|
|||||||
"diffDescriptionConflict": "{path} · 磁碟與未儲存內容"
|
"diffDescriptionConflict": "{path} · 磁碟與未儲存內容"
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
|
"connectionLifecycle": {
|
||||||
|
"tasks": {
|
||||||
|
"connectingTitle": "正在連線 {agent}",
|
||||||
|
"connectingDescription": "正在建立連線",
|
||||||
|
"loadingSelectorsTitle": "正在載入 {agent} 選擇項",
|
||||||
|
"loadingSelectorsDescription": "正在取得模式與會話設定選項"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"connectionFailed": "連線失敗"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageThread": {
|
||||||
|
"emptyTitle": "暫無訊息",
|
||||||
|
"emptyDescription": "開始一個會話後,訊息會顯示在這裡"
|
||||||
|
},
|
||||||
"chatInput": {
|
"chatInput": {
|
||||||
"connecting": "連線中...",
|
"connecting": "連線中...",
|
||||||
"agentResponding": "Agent 正在回應...",
|
"agentResponding": "Agent 正在回應...",
|
||||||
@@ -1140,6 +1160,8 @@
|
|||||||
},
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"parameters": "參數",
|
"parameters": "參數",
|
||||||
|
"error": "錯誤",
|
||||||
|
"result": "結果",
|
||||||
"status": {
|
"status": {
|
||||||
"approvalRequested": "等待授權",
|
"approvalRequested": "等待授權",
|
||||||
"approvalResponded": "已回應",
|
"approvalResponded": "已回應",
|
||||||
@@ -1150,9 +1172,36 @@
|
|||||||
"outputError": "錯誤"
|
"outputError": "錯誤"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"toolCallBlock": {
|
||||||
|
"tool": "工具",
|
||||||
|
"error": "錯誤",
|
||||||
|
"result": "結果"
|
||||||
|
},
|
||||||
"contentParts": {
|
"contentParts": {
|
||||||
"showingTailOutput": "為確保效能,串流輸出時僅顯示尾端內容。",
|
"showingTailOutput": "為確保效能,串流輸出時僅顯示尾端內容。",
|
||||||
"result": "結果"
|
"result": "結果",
|
||||||
|
"unknown": "未知",
|
||||||
|
"replaceAll": "全部替換",
|
||||||
|
"filesCount": "檔案:{count}",
|
||||||
|
"update": "更新",
|
||||||
|
"moreFiles": "+{count} 個更多檔案",
|
||||||
|
"timeoutMs": "逾時:{timeout}ms",
|
||||||
|
"backgroundTrue": "背景:true",
|
||||||
|
"offset": "位移:{offset}",
|
||||||
|
"limit": "限制:{limit}",
|
||||||
|
"pages": "頁碼:{pages}",
|
||||||
|
"mode": "模式:{mode}",
|
||||||
|
"cell": "儲存格:{cell}",
|
||||||
|
"pathLabel": "路徑:",
|
||||||
|
"globLabel": "Glob:",
|
||||||
|
"typeLabel": "類型:",
|
||||||
|
"outputLabel": "輸出:",
|
||||||
|
"caseInsensitive": "不區分大小寫",
|
||||||
|
"multiline": "多行",
|
||||||
|
"promptLabel": "提示詞",
|
||||||
|
"subjectLabel": "主題",
|
||||||
|
"taskLabel": "任務",
|
||||||
|
"nameLabel": "名稱:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"diffPreview": {
|
"diffPreview": {
|
||||||
|
|||||||
Reference in New Issue
Block a user