feat(chat): add previous checkpoint navigation controls and i18n (#12139)
Some checks are pending
Code QA Roo Code / check-translations (push) Waiting to run
Code QA Roo Code / knip (push) Waiting to run
Code QA Roo Code / compile (push) Waiting to run
Code QA Roo Code / platform-unit-test (ubuntu-latest) (push) Waiting to run
Code QA Roo Code / platform-unit-test (windows-latest) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Nightly Publish / publish-nightly (push) Waiting to run

This commit is contained in:
Chiranjeevisantosh Madugundi 2026-04-20 15:12:10 -07:00 committed by GitHub
parent 3e202ebf5b
commit 2bb826039b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 251 additions and 15 deletions

View file

@ -10,6 +10,24 @@ import { copyPaths, copyWasms, copyLocales, setupLocaleWatcher } from "@roo-code
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
async function removeDirWithRetries(dirPath, retries = 5, retryDelayMs = 200) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
await fs.promises.rm(dirPath, { recursive: true, force: true })
return
} catch (error) {
const isRetryable = error?.code === "ENOTEMPTY" || error?.code === "EBUSY" || error?.code === "EPERM"
const isLastAttempt = attempt === retries
if (!isRetryable || isLastAttempt) {
throw error
}
await new Promise((resolve) => globalThis.setTimeout(resolve, retryDelayMs * (attempt + 1)))
}
}
}
async function main() {
const name = "extension"
const production = process.argv.includes("--production")
@ -36,7 +54,7 @@ async function main() {
if (fs.existsSync(distDir)) {
console.log(`[${name}] Cleaning dist directory: ${distDir}`)
fs.rmSync(distDir, { recursive: true, force: true })
await removeDirWithRetries(distDir)
}
/**

View file

@ -124,6 +124,7 @@ interface ChatRowProps {
isFollowUpAutoApprovalPaused?: boolean
editable?: boolean
hasCheckpoint?: boolean
onJumpToPreviousCheckpoint?: () => void
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
@ -177,6 +178,7 @@ export const ChatRowContent = ({
onBatchFileResponse,
isFollowUpAnswered,
isFollowUpAutoApprovalPaused,
onJumpToPreviousCheckpoint,
}: ChatRowContentProps) => {
const { t, i18n } = useTranslation()
@ -1341,6 +1343,7 @@ export const ChatRowContent = ({
commitHash={message.text!}
currentHash={currentCheckpoint}
checkpoint={message.checkpoint}
onJumpToPreviousCheckpoint={onJumpToPreviousCheckpoint}
/>
)
case "condense_context":

View file

@ -1261,6 +1261,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return result
}, [isCondensing, visibleMessages])
const checkpointIndices = useMemo(() => {
const indices: number[] = []
for (let i = 0; i < groupedMessages.length; i++) {
if (groupedMessages[i]?.say === "checkpoint_saved") {
indices.push(i)
}
}
return indices
}, [groupedMessages])
const hasLatestCheckpoint = checkpointIndices.length > 0
const checkpointJumpCursorRef = useRef<number | null>(null)
useEffect(() => {
checkpointJumpCursorRef.current = null
}, [task?.ts, checkpointIndices])
// Scroll lifecycle is managed by a dedicated hook to keep ChatView focused
// on message handling and UI orchestration.
const {
@ -1394,6 +1411,29 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "cancelAutoApproval" })
}, [])
const handleScrollToBottomAndResetCheckpointCursor = useCallback(() => {
checkpointJumpCursorRef.current = null
handleScrollToBottomClick()
}, [handleScrollToBottomClick])
const handleScrollToLatestCheckpoint = useCallback(() => {
if (checkpointIndices.length === 0) {
return
}
const previousCursor = checkpointJumpCursorRef.current
const nextCursor = previousCursor === null ? checkpointIndices.length - 1 : Math.max(0, previousCursor - 1)
const nextCheckpointIndex = checkpointIndices[nextCursor]
checkpointJumpCursorRef.current = nextCursor
enterUserBrowsingHistory("keyboard-nav-up")
virtuosoRef.current?.scrollToIndex({
index: nextCheckpointIndex,
align: "center",
behavior: "smooth",
})
}, [checkpointIndices, enterUserBrowsingHistory])
const itemContent = useCallback(
(index: number, messageOrGroup: ClineMessage) => {
const hasCheckpoint = modifiedMessages.some((message) => message.say === "checkpoint_saved")
@ -1430,6 +1470,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
})()
}
hasCheckpoint={hasCheckpoint}
onJumpToPreviousCheckpoint={handleScrollToLatestCheckpoint}
/>
)
},
@ -1447,6 +1488,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
isFollowUpAutoApprovalPaused,
enableButtons,
primaryButtonText,
handleScrollToLatestCheckpoint,
],
)
@ -1641,14 +1683,27 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
showScrollToBottom ? "opacity-100" : enableButtons ? "opacity-100" : "opacity-50"
}`}>
{showScrollToBottom ? (
<StandardTooltip content={t("chat:scrollToBottom")}>
<Button
variant="secondary"
className="flex-[2]"
onClick={handleScrollToBottomClick}>
<span className="codicon codicon-chevron-down"></span>
</Button>
</StandardTooltip>
<>
<StandardTooltip content={t("chat:scrollToBottom")}>
<Button
variant="secondary"
className={hasLatestCheckpoint ? "flex-1 mr-[6px]" : "flex-[2]"}
onClick={handleScrollToBottomAndResetCheckpointCursor}>
<span className="codicon codicon-chevron-down"></span>
</Button>
</StandardTooltip>
{hasLatestCheckpoint && (
<StandardTooltip content={t("chat:scrollToLatestCheckpoint")}>
<Button
variant="secondary"
className="flex-1 ml-[6px]"
onClick={handleScrollToLatestCheckpoint}
aria-label={t("chat:scrollToLatestCheckpoint")}>
<span className="codicon codicon-history"></span>
</Button>
</StandardTooltip>
)}
</>
) : (
<>
{primaryButtonText && (

View file

@ -43,6 +43,11 @@ interface MockVirtuosoProps {
interface VirtuosoHarnessState {
scrollCalls: number
scrollToIndexArgs: Array<{
index: number | "LAST"
align?: "end" | "start" | "center"
behavior?: "auto" | "smooth"
}>
atBottomAfterCalls: number
signalDelayMs: number
emitFalseOnDataChange: boolean
@ -54,6 +59,7 @@ interface VirtuosoHarnessState {
const harness = vi.hoisted<VirtuosoHarnessState>(() => ({
scrollCalls: 0,
scrollToIndexArgs: [],
atBottomAfterCalls: Number.POSITIVE_INFINITY,
signalDelayMs: 20,
emitFalseOnDataChange: true,
@ -152,8 +158,9 @@ vi.mock("react-virtuoso", () => {
}
useImperativeHandle(ref, () => ({
scrollToIndex: () => {
scrollToIndex: (options) => {
harness.scrollCalls += 1
harness.scrollToIndexArgs.push(options)
const reachedBottom = harness.scrollCalls >= harness.atBottomAfterCalls
const timeoutId = window.setTimeout(() => {
atBottomRef.current?.(reachedBottom)
@ -215,6 +222,23 @@ const buildMessages = (baseTs: number): ClineMessage[] => [
{ type: "say", say: "text", ts: baseTs + 2, text: "row-2" },
]
const buildMessagesWithCheckpoint = (baseTs: number): ClineMessage[] => [
{ type: "say", say: "text", ts: baseTs, text: "task" },
{ type: "say", say: "text", ts: baseTs + 1, text: "row-1" },
{ type: "say", say: "checkpoint_saved", ts: baseTs + 2, text: "checkpoint-1" },
{ type: "say", say: "text", ts: baseTs + 3, text: "row-2" },
]
const buildMessagesWithMultipleCheckpoints = (baseTs: number): ClineMessage[] => [
{ type: "say", say: "text", ts: baseTs, text: "task" },
{ type: "say", say: "checkpoint_saved", ts: baseTs + 1, text: "checkpoint-1" },
{ type: "say", say: "text", ts: baseTs + 2, text: "row-2" },
{ type: "say", say: "checkpoint_saved", ts: baseTs + 3, text: "checkpoint-2" },
{ type: "say", say: "text", ts: baseTs + 4, text: "row-4" },
{ type: "say", say: "checkpoint_saved", ts: baseTs + 5, text: "checkpoint-3" },
{ type: "say", say: "text", ts: baseTs + 6, text: "row-6" },
]
const resolveFollowOutput = (isAtBottom: boolean): "auto" | false => {
const followOutput = harness.followOutput
if (typeof followOutput === "function") {
@ -254,19 +278,19 @@ const renderView = () =>
</ExtensionStateContextProvider>,
)
const hydrate = async (atBottomAfterCalls: number) => {
const hydrate = async (atBottomAfterCalls: number, clineMessages = buildMessages(Date.now() - 3_000)) => {
harness.atBottomAfterCalls = atBottomAfterCalls
renderView()
await act(async () => {
await Promise.resolve()
})
await act(async () => {
postState(buildMessages(Date.now() - 3_000))
postState(clineMessages)
})
await waitFor(() => {
const list = document.querySelector("[data-testid='virtuoso-item-list']")
expect(list).toBeTruthy()
expect(list?.getAttribute("data-count")).toBe("2")
expect(list?.getAttribute("data-count")).toBe(String(Math.max(0, clineMessages.length - 1)))
})
}
@ -317,9 +341,19 @@ const getScrollToBottomButton = (): HTMLButtonElement => {
return button
}
const getScrollToCheckpointButton = (): HTMLButtonElement => {
const button = document.querySelector("button[aria-label='chat:scrollToLatestCheckpoint']")
if (!(button instanceof HTMLButtonElement)) {
throw new Error("Expected scroll-to-checkpoint button")
}
return button
}
describe("ChatView scroll behavior regression coverage", () => {
beforeEach(() => {
harness.scrollCalls = 0
harness.scrollToIndexArgs = []
harness.atBottomAfterCalls = Number.POSITIVE_INFINITY
harness.signalDelayMs = 20
harness.emitFalseOnDataChange = true
@ -503,4 +537,71 @@ describe("ChatView scroll behavior regression coverage", () => {
})
await waitFor(() => expect(document.querySelector(".codicon-chevron-down")).toBeNull(), { timeout: 1_200 })
})
it("shows jump-to-checkpoint button and scrolls to latest checkpoint", async () => {
await hydrate(2, buildMessagesWithCheckpoint(Date.now() - 3_000))
await waitForCalls(2)
await waitForCallsSettled()
await act(async () => {
fireEvent.keyDown(window, { key: "PageUp" })
})
await waitFor(() => expect(document.querySelector(".codicon-chevron-down")).toBeTruthy(), {
timeout: 1_200,
})
const checkpointButton = document.querySelector("button[aria-label='chat:scrollToLatestCheckpoint']")
expect(checkpointButton).toBeInstanceOf(HTMLButtonElement)
const callsBeforeClick = harness.scrollCalls
await act(async () => {
;(checkpointButton as HTMLButtonElement).click()
})
expect(harness.scrollCalls).toBe(callsBeforeClick + 1)
expect(harness.scrollToIndexArgs.at(-1)).toMatchObject({
index: 1,
align: "center",
behavior: "smooth",
})
})
it("repeated checkpoint clicks step backward through previous checkpoints", async () => {
await hydrate(2, buildMessagesWithMultipleCheckpoints(Date.now() - 3_000))
await waitForCalls(2)
await waitForCallsSettled()
await act(async () => {
fireEvent.keyDown(window, { key: "PageUp" })
})
await waitFor(() => expect(document.querySelector(".codicon-chevron-down")).toBeTruthy(), {
timeout: 1_200,
})
const checkpointButton = getScrollToCheckpointButton()
await act(async () => {
;(checkpointButton as HTMLButtonElement).click()
})
expect(harness.scrollToIndexArgs.at(-1)).toMatchObject({ index: 4, align: "center", behavior: "smooth" })
await act(async () => {
;(checkpointButton as HTMLButtonElement).click()
})
expect(harness.scrollToIndexArgs.at(-1)).toMatchObject({ index: 2, align: "center", behavior: "smooth" })
await act(async () => {
;(checkpointButton as HTMLButtonElement).click()
})
expect(harness.scrollToIndexArgs.at(-1)).toMatchObject({ index: 0, align: "center", behavior: "smooth" })
// Once at the oldest checkpoint, additional clicks keep targeting it.
await act(async () => {
;(checkpointButton as HTMLButtonElement).click()
})
expect(harness.scrollToIndexArgs.at(-1)).toMatchObject({ index: 0, align: "center", behavior: "smooth" })
})
})

View file

@ -12,6 +12,7 @@ type CheckpointMenuBaseProps = {
ts: number
commitHash: string
checkpoint: Checkpoint
onJumpToPreviousCheckpoint?: () => void
}
type CheckpointMenuControlledProps = {
onOpenChange: (open: boolean) => void
@ -21,7 +22,13 @@ type CheckpointMenuUncontrolledProps = {
}
type CheckpointMenuProps = CheckpointMenuBaseProps & (CheckpointMenuControlledProps | CheckpointMenuUncontrolledProps)
export const CheckpointMenu = ({ ts, commitHash, checkpoint, onOpenChange }: CheckpointMenuProps) => {
export const CheckpointMenu = ({
ts,
commitHash,
checkpoint,
onOpenChange,
onJumpToPreviousCheckpoint,
}: CheckpointMenuProps) => {
const { t } = useTranslation()
const [internalRestoreOpen, setInternalRestoreOpen] = useState(false)
const [restoreConfirming, setRestoreConfirming] = useState(false)
@ -165,6 +172,16 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, onOpenChange }: Che
</div>
</PopoverContent>
</Popover>
<StandardTooltip content={t("chat:scrollToLatestCheckpoint")}>
<Button
variant="ghost"
size="icon"
onClick={onJumpToPreviousCheckpoint}
data-testid="jump-previous-checkpoint-btn"
aria-label={t("chat:scrollToLatestCheckpoint")}>
<span className="codicon codicon-chevron-up" />
</Button>
</StandardTooltip>
<Popover open={moreOpen} onOpenChange={(open) => setMoreOpen(open)} data-testid="more-popover">
<StandardTooltip content={t("chat:task.seeMore")}>
<PopoverTrigger asChild>

View file

@ -11,9 +11,15 @@ type CheckpointSavedProps = {
commitHash: string
currentHash?: string
checkpoint?: Record<string, unknown>
onJumpToPreviousCheckpoint?: () => void
}
export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: CheckpointSavedProps) => {
export const CheckpointSaved = ({
checkpoint,
currentHash,
onJumpToPreviousCheckpoint,
...props
}: CheckpointSavedProps) => {
const { t } = useTranslation()
const isCurrent = currentHash === props.commitHash
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
@ -100,6 +106,7 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
commitHash={props.commitHash}
checkpoint={metadata}
onOpenChange={handlePopoverOpenChange}
onJumpToPreviousCheckpoint={onJumpToPreviousCheckpoint}
/>
</div>
</div>

View file

@ -185,4 +185,21 @@ describe("CheckpointSaved popover visibility", () => {
expect(getMenu().className).toContain("hidden")
})
})
it("renders jump-to-previous-checkpoint control and triggers callback", async () => {
const onJumpToPreviousCheckpoint = vi.fn()
const { getByTestId, container } = render(
<CheckpointSaved {...baseProps} onJumpToPreviousCheckpoint={onJumpToPreviousCheckpoint} />,
)
const getParentDiv = () =>
container.querySelector("[class*='flex items-center justify-between']") as HTMLElement
fireEvent.mouseEnter(getParentDiv())
const jumpButton = await waitFor(() => getByTestId("jump-previous-checkpoint-btn"))
await userEvent.click(jumpButton)
expect(onJumpToPreviousCheckpoint).toHaveBeenCalledTimes(1)
})
})

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Afegeix el missatge a la cua (s'enviarà quan acabi la tasca actual)",
"scrollToBottom": "Desplaça't al final del xat",
"scrollToLatestCheckpoint": "Desplaça't al checkpoint anterior",
"about": "Roo Code és tot un equip de desenvolupament d'IA al teu editor.",
"docs": "Consulta els nostres <DocsLink>documents</DocsLink> per a més informació.",
"onboarding": "La teva llista de tasques en aquest espai de treball està buida.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Nachricht zur Warteschlange hinzufügen (wird nach Abschluss der aktuellen Aufgabe gesendet)",
"scrollToBottom": "Zum Chat-Ende scrollen",
"scrollToLatestCheckpoint": "Zum vorherigen Checkpoint scrollen",
"about": "Roo Code ist ein ganzes KI-Entwicklerteam in deinem Editor.",
"docs": "Schau in unsere <DocsLink>Dokumentation</DocsLink>, um mehr zu erfahren.",
"onboarding": "Deine Aufgabenliste in diesem Arbeitsbereich ist leer.",

View file

@ -124,6 +124,7 @@
"placeholder": "Edit your message..."
},
"scrollToBottom": "Scroll to bottom of chat",
"scrollToLatestCheckpoint": "Scroll to previous checkpoint",
"about": "Roo is a whole AI dev team in your editor",
"docs": "Check our <DocsLink>docs</DocsLink> to get started",
"onboarding": "What would you like to do?",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Agregar mensaje a la cola (se enviará después de que termine la tarea actual)",
"scrollToBottom": "Desplazarse al final del chat",
"scrollToLatestCheckpoint": "Desplazarse al checkpoint anterior",
"about": "Roo Code es todo un equipo de desarrollo de IA en tu editor.",
"docs": "Consulta nuestra <DocsLink>documentación</DocsLink> para saber más.",
"onboarding": "Tu lista de tareas en este espacio de trabajo está vacía.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Ajouter le message à la file d'attente (sera envoyé après la fin de la tâche en cours)",
"scrollToBottom": "Défiler jusqu'au bas du chat",
"scrollToLatestCheckpoint": "Défiler jusqu'au checkpoint précédent",
"about": "Roo Code est une équipe complète de développeurs IA dans votre éditeur.",
"docs": "Consultez notre <DocsLink>documentation</DocsLink> pour en savoir plus.",
"onboarding": "Votre liste de tâches dans cet espace de travail est vide.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "संदेश को कतार में जोड़ें (वर्तमान कार्य पूरा होने के बाद भेजा जाएगा)",
"scrollToBottom": "चैट के निचले हिस्से तक स्क्रॉल करें",
"scrollToLatestCheckpoint": "पिछले चेकपॉइंट तक स्क्रॉल करें",
"about": "Roo Code आपके संपादक में एक पूरी AI देव टीम है।",
"docs": "और जानने के लिए हमारे <DocsLink>दस्तावेज़</DocsLink> देखें।",
"onboarding": "इस कार्यक्षेत्र में आपकी कार्य सूची खाली है।",

View file

@ -127,6 +127,7 @@
},
"enqueueMessage": "Tambahkan pesan ke antrean (akan dikirim setelah tugas saat ini selesai)",
"scrollToBottom": "Gulir ke bawah chat",
"scrollToLatestCheckpoint": "Gulir ke checkpoint sebelumnya",
"about": "Roo Code adalah seluruh tim pengembang AI di editor Anda.",
"docs": "Lihat <DocsLink>dokumentasi</DocsLink> kami untuk mempelajari lebih lanjut.",
"onboarding": "Daftar tugas Anda di ruang kerja ini kosong.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Aggiungi il messaggio alla coda (sarà inviato dopo che l'attività corrente sarà terminata)",
"scrollToBottom": "Scorri fino alla fine della chat",
"scrollToLatestCheckpoint": "Scorri al checkpoint precedente",
"about": "Roo Code è un intero team di sviluppo AI nel tuo editor.",
"docs": "Consulta la nostra <DocsLink>documentazione</DocsLink> per saperne di più.",
"onboarding": "La tua lista di attività in questo spazio di lavoro è vuota.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "メッセージをキューに追加(現在のタスク完了後に送信されます)",
"scrollToBottom": "チャットの最下部にスクロール",
"scrollToLatestCheckpoint": "前のチェックポイントまでスクロール",
"about": "Roo Codeは、エディタに常駐するAI開発チームです。",
"docs": "詳細については、<DocsLink>ドキュメント</DocsLink>をご確認ください。",
"onboarding": "このワークスペースのタスクリストは空です。",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "메시지를 대기열에 추가 (현재 작업 완료 후 전송)",
"scrollToBottom": "채팅 하단으로 스크롤",
"scrollToLatestCheckpoint": "이전 체크포인트로 스크롤",
"about": "Roo Code는 편집기 안에 있는 전체 AI 개발팀입니다.",
"docs": "더 알아보려면 <DocsLink>문서</DocsLink>를 확인하세요.",
"onboarding": "이 작업 공간의 작업 목록이 비어 있습니다.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Bericht aan de wachtrij toevoegen (wordt verzonden nadat de huidige taak is voltooid)",
"scrollToBottom": "Scroll naar onderaan de chat",
"scrollToLatestCheckpoint": "Scroll naar het vorige checkpoint",
"about": "Roo Code is een heel AI-ontwikkelteam in je editor.",
"docs": "Bekijk onze <DocsLink>documentatie</DocsLink> voor meer informatie.",
"onboarding": "Je takenlijst in deze werkruimte is leeg.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Dodaj wiadomość do kolejki (zostanie wysłana po zakończeniu bieżącego zadania)",
"scrollToBottom": "Przewiń do dołu czatu",
"scrollToLatestCheckpoint": "Przewiń do poprzedniego punktu kontrolnego",
"about": "Roo Code to cały zespół deweloperów AI w Twoim edytorze.",
"docs": "Sprawdź naszą <DocsLink>dokumentację</DocsLink>, aby dowiedzieć się więcej.",
"onboarding": "Twoja lista zadań w tym obszarze roboczym jest pusta.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Adicionar mensagem à fila (será enviada após a conclusão da tarefa atual)",
"scrollToBottom": "Rolar para o final do chat",
"scrollToLatestCheckpoint": "Rolar para o checkpoint anterior",
"about": "Roo Code é uma equipe inteira de desenvolvimento de IA em seu editor.",
"docs": "Confira nossa <DocsLink>documentação</DocsLink> para saber mais.",
"onboarding": "Sua lista de tarefas neste espaço de trabalho está vazia.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Добавить сообщение в очередь (будет отправлено после завершения текущей задачи)",
"scrollToBottom": "Прокрутить чат вниз",
"scrollToLatestCheckpoint": "Прокрутить к предыдущему чекпоинту",
"about": "Roo Code — это целая команда разработчиков ИИ в вашем редакторе.",
"docs": "Ознакомьтесь с нашей <DocsLink>документацией</DocsLink>, чтобы узнать больше.",
"onboarding": "Ваш список задач в этом рабочем пространстве пуст.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Mesajı kuyruğa ekle (mevcut görev tamamlandıktan sonra gönderilecek)",
"scrollToBottom": "Sohbetin altına kaydır",
"scrollToLatestCheckpoint": "Önceki checkpoint'e kaydır",
"about": "Roo Code, düzenleyicinizdeki bütün bir yapay zeka geliştirme ekibidir.",
"docs": "Daha fazla bilgi için <DocsLink>belgelerimize</DocsLink> göz atın.",
"onboarding": "Bu çalışma alanındaki görev listeniz boş.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "Thêm tin nhắn vào hàng đợi (sẽ gửi sau khi nhiệm vụ hiện tại hoàn tất)",
"scrollToBottom": "Cuộn xuống cuối cuộc trò chuyện",
"scrollToLatestCheckpoint": "Cuộn đến checkpoint trước đó",
"about": "Roo Code là một đội ngũ phát triển AI đầy đủ trong trình chỉnh sửa của bạn.",
"docs": "Kiểm tra <DocsLink>tài liệu</DocsLink> của chúng tôi để tìm hiểu thêm.",
"onboarding": "Danh sách nhiệm vụ của bạn trong không gian làm việc này đang trống.",

View file

@ -97,6 +97,7 @@
},
"enqueueMessage": "将消息加入队列(当前任务完成后发送)",
"scrollToBottom": "滚动到聊天底部",
"scrollToLatestCheckpoint": "滚动到上一个检查点",
"about": "Roo Code 是您编辑器中的整个 AI 开发团队。",
"docs": "查看我们的 <DocsLink>文档</DocsLink> 了解更多信息。",
"onboarding": "此工作区中的任务列表为空。",

View file

@ -124,6 +124,7 @@
"placeholder": "編輯訊息..."
},
"scrollToBottom": "捲動至對話底部",
"scrollToLatestCheckpoint": "捲動至上一個檢查點",
"about": "Roo 是編輯器中的完整 AI 開發團隊。",
"docs": "請參閱 <DocsLink>說明文件</DocsLink> 開始使用。",
"onboarding": "想要做什麼呢?",