refactor: address comments

This commit is contained in:
Catriel Müller
2026-05-01 19:23:10 -03:00
parent c9f68644cc
commit 3384b6736c
23 changed files with 182 additions and 75 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "اللقطة بطيئة",
"snapshot.slowRepo.question":
"يتيح لك نظام اللقطات في Kilo التراجع عن كل تغيير في الملفات يقوم به Kilo خلال المحادثة. لا يتم المساس بتاريخ git لديك — يتم تخزين اللقطات بشكل منفصل.\n\nتستغرق اللقطة الأولى لهذا المستودع وقتًا أطول من المتوقع. على قواعد الشيفرات الضخمة قد يؤدي ذلك إلى تأخير كل دورة بشكل ملحوظ.\n\nيمكنك الانتظار أو تعطيل اللقطات لهذا المشروع والاعتماد على git بدلاً منها. يتم حفظ هذا الاختيار في `.kilo/kilo.json`.",
"يستغرق تهيئة نظام اللقطات وقتًا طويلاً، غالبًا بسبب حجم المستودع.\n\nهل تريد تعطيل اللقطات لهذا المستودع؟",
"snapshot.slowRepo.answer.continue": "المتابعة مع اللقطات",
"snapshot.slowRepo.answer.continue.description":
"استمر في الانتظار حتى تكتمل اللقطة. تكون الدورات اللاحقة سريعة بعد إنشاء اللقطة الأولى.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Snapshot está lento",
"snapshot.slowRepo.question":
"O sistema de snapshots do Kilo permite desfazer e refazer cada alteração de arquivo que o Kilo faz durante a conversa. Seu histórico do git não é afetado — os snapshots são armazenados separadamente.\n\nO snapshot inicial deste repositório está demorando mais do que o esperado. Em bases de código muito grandes isso pode atrasar cada turno significativamente.\n\nVocê pode continuar aguardando ou desativar os snapshots para este projeto e contar apenas com o git. Essa escolha é salva em `.kilo/kilo.json`.",
"Está demorando muito para inicializar o sistema de snapshots, provavelmente por causa do tamanho do repositório.\n\nDeseja desativar os snapshots para este repositório?",
"snapshot.slowRepo.answer.continue": "Continuar com snapshots",
"snapshot.slowRepo.answer.continue.description":
"Aguarde a conclusão do snapshot. Os próximos turnos serão rápidos depois que o snapshot inicial for criado.",
+1 -1
View File
@@ -88,7 +88,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Snapshot je spor",
"snapshot.slowRepo.question":
"Kilo sistem snapshotova omogućava vam da poništite ili vratite svaku promjenu fajla koju Kilo napravi tokom razgovora. Vaša git historija ostaje netaknuta — snapshotovi se čuvaju odvojeno.\n\nPočetni snapshot ovog repozitorija traje duže nego očekivano. Na veoma velikim bazama koda ovo može znatno odgoditi svaki potez.\n\nMožete nastaviti čekati ili onemogućiti snapshotove za ovaj projekat i osloniti se samo na git. Ovaj izbor se čuva u `.kilo/kilo.json`.",
"Inicijalizacija sistema snapshotova traje dugo, vjerovatno zbog veličine repozitorija.\n\nŽelite li onemogućiti snapshotove za ovaj repozitorij?",
"snapshot.slowRepo.answer.continue": "Nastavi sa snapshotovima",
"snapshot.slowRepo.answer.continue.description":
"Sačekaj da se snapshot završi. Naredni potezi su brzi kada se početni snapshot jednom napravi.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Snapshot er langsomt",
"snapshot.slowRepo.question":
"Kilos snapshot-system lader dig fortryde og genskabe hver filændring, Kilo laver under samtalen. Din git-historik forbliver urørt — snapshots gemmes separat.\n\nDet indledende snapshot af denne repository tager længere end forventet. På meget store kodebaser kan det forsinke hver tur betydeligt.\n\nDu kan vente videre eller deaktivere snapshots for dette projekt og bruge git i stedet. Dette valg gemmes i `.kilo/kilo.json`.",
"Det tager lang tid at initialisere snapshot-systemet, sandsynligvis på grund af størrelsen på repositoryet.\n\nVil du deaktivere snapshots for dette repository?",
"snapshot.slowRepo.answer.continue": "Fortsæt med snapshots",
"snapshot.slowRepo.answer.continue.description":
"Vent, indtil snapshot'et er færdigt. Efterfølgende ture er hurtige, når det indledende snapshot er bygget.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Snapshot ist langsam",
"snapshot.slowRepo.question":
"Das Snapshot-System von Kilo ermöglicht Rückgängig/Wiederherstellen für jede Dateiänderung, die Kilo während der Konversation vornimmt. Deine git-Historie bleibt unberührt – Snapshots werden separat gespeichert.\n\nDer erste Snapshot dieses Repositorys dauert länger als erwartet. Bei sehr großen Codebasen kann das jede Runde spürbar verzögern.\n\nDu kannst weiter warten oder Snapshots für dieses Projekt deaktivieren und dich auf git verlassen. Diese Auswahl wird in `.kilo/kilo.json` gespeichert.",
"Die Initialisierung des Snapshot-Systems dauert lange, wahrscheinlich aufgrund der Größe des Repositorys.\n\nMöchtest du Snapshots für dieses Repository deaktivieren?",
"snapshot.slowRepo.answer.continue": "Snapshots beibehalten",
"snapshot.slowRepo.answer.continue.description":
"Auf den Abschluss des Snapshots warten. Nachfolgende Runden sind schnell, sobald der initiale Snapshot erstellt ist.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "La instantánea es lenta",
"snapshot.slowRepo.question":
"El sistema de instantáneas de Kilo te permite deshacer y rehacer cada cambio de archivo que Kilo realiza durante la conversación. Tu historial de git no se modifica: las instantáneas se almacenan por separado.\n\nLa instantánea inicial de este repositorio está tardando más de lo esperado. En bases de código muy grandes esto puede retrasar cada turno significativamente.\n\nPuedes seguir esperando o desactivar las instantáneas para este proyecto y confiar solo en git. Esta elección se guarda en `.kilo/kilo.json`.",
"Está tardando mucho en inicializar el sistema de instantáneas, probablemente por el tamaño del repositorio.\n\n¿Quieres desactivar las instantáneas para este repositorio?",
"snapshot.slowRepo.answer.continue": "Continuar con instantáneas",
"snapshot.slowRepo.answer.continue.description":
"Sigue esperando hasta que termine la instantánea. Los turnos siguientes serán rápidos una vez creada la instantánea inicial.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Instantané lent",
"snapshot.slowRepo.question":
"Le système d'instantanés de Kilo vous permet d'annuler et de rétablir chaque modification de fichier effectuée par Kilo pendant la conversation. Votre historique git n'est pas touché — les instantanés sont stockés séparément.\n\nL'instantané initial de ce dépôt prend plus de temps que prévu. Sur des bases de code très volumineuses, cela peut ralentir considérablement chaque tour.\n\nVous pouvez continuer à attendre ou désactiver les instantanés pour ce projet et ne compter que sur git. Ce choix est enregistré dans `.kilo/kilo.json`.",
"L'initialisation du système d'instantanés prend beaucoup de temps, probablement en raison de la taille du dépôt.\n\nVoulez-vous désactiver les instantanés pour ce dépôt ?",
"snapshot.slowRepo.answer.continue": "Continuer avec les instantanés",
"snapshot.slowRepo.answer.continue.description":
"Attendez la fin de l'instantané. Les tours suivants sont rapides une fois l'instantané initial créé.",
+1 -1
View File
@@ -81,7 +81,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "スナップショットが遅い",
"snapshot.slowRepo.question":
"Kilo のスナップショット機能は、会話中に Kilo が行ったすべてのファイル変更を取り消し・やり直しできます。git の履歴には影響せず、スナップショットは別に保存されます。\n\nこのリポジトリの初回スナップショットに予想以上の時間がかかっています。非常に大きなコードベースでは各ターンが大きく遅延する可能性があります。\n\nこのまま待つか、このプロジェクトではスナップショットを無効にして git のみに任せることもできます。選択内容は `.kilo/kilo.json` に保存されます。",
"リポジトリのサイズのためか、スナップショットシステムの初期化に時間がかかっています。\n\nこのリポジトリのスナップショットを無効にしますか?",
"snapshot.slowRepo.answer.continue": "スナップショットを続行",
"snapshot.slowRepo.answer.continue.description":
"スナップショットが完了するまで待機します。初回のスナップショットが作成された後は、以降のターンは高速になります。",
+1 -1
View File
@@ -81,7 +81,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "스냅샷이 느립니다",
"snapshot.slowRepo.question":
"Kilo의 스냅샷 시스템을 사용하면 대화 중에 Kilo가 만든 모든 파일 변경을 실행 취소하거나 다시 실행할 수 있습니다. git 히스토리는 영향을 받지 않으며 스냅샷은 별도로 저장됩니다.\n\n이 저장소의 초기 스냅샷이 예상보다 오래 걸리고 있습니다. 매우 큰 코드베이스에서는 매 턴이 크게 지연될 수 있습니다.\n\n계속 기다리거나 이 프로젝트에 대해 스냅샷을 비활성화하고 git만 사용할 수 있습니다. 이 선택은 `.kilo/kilo.json`에 저장됩니다.",
"리포지토리 크기 때문인지 스냅샷 시스템 초기화에 시간이 오래 걸리고 있습니다.\n\n이 리포지토리에서 스냅샷을 비활성화하시겠습니까?",
"snapshot.slowRepo.answer.continue": "스냅샷 계속 사용",
"snapshot.slowRepo.answer.continue.description":
"스냅샷이 완료될 때까지 기다리세요. 초기 스냅샷이 만들어지면 이후 턴은 빠릅니다.",
+1 -1
View File
@@ -85,7 +85,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Snapshot is traag",
"snapshot.slowRepo.question":
"Het snapshot-systeem van Kilo laat je elke bestandswijziging die Kilo tijdens het gesprek maakt ongedaan maken of opnieuw doen. Je git-geschiedenis blijft onaangetast — snapshots worden apart opgeslagen.\n\nDe eerste snapshot van deze repository duurt langer dan verwacht. Op zeer grote codebases kan dit elke beurt flink vertragen.\n\nJe kunt blijven wachten of snapshots voor dit project uitschakelen en alleen op git vertrouwen. Deze keuze wordt opgeslagen in `.kilo/kilo.json`.",
"Het initialiseren van het snapshot-systeem duurt lang, waarschijnlijk vanwege de grootte van de repository.\n\nWil je snapshots voor deze repository uitschakelen?",
"snapshot.slowRepo.answer.continue": "Doorgaan met snapshots",
"snapshot.slowRepo.answer.continue.description":
"Wacht tot de snapshot klaar is. Volgende beurten zijn snel zodra de eerste snapshot is gemaakt.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Snapshot er tregt",
"snapshot.slowRepo.question":
"Kilos snapshot-system lar deg angre og gjøre om hver filendring Kilo gjør i løpet av samtalen. Git-historikken din forblir urørt — snapshots lagres separat.\n\nDet første snapshotet av dette depotet tar lengre tid enn forventet. På svært store kodebaser kan det forsinke hver runde betydelig.\n\nDu kan fortsette å vente eller deaktivere snapshots for dette prosjektet og stole på git i stedet. Valget lagres i `.kilo/kilo.json`.",
"Det tar lang tid å initialisere snapshot-systemet, sannsynligvis på grunn av størrelsen på depotet.\n\nVil du deaktivere snapshots for dette depotet?",
"snapshot.slowRepo.answer.continue": "Fortsett med snapshots",
"snapshot.slowRepo.answer.continue.description":
"Vent til snapshotet er ferdig. Påfølgende runder er raske når det første snapshotet er bygget.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Snapshot jest wolny",
"snapshot.slowRepo.question":
"System snapshotów Kilo umożliwia cofanie i przywracanie każdej zmiany plików wykonanej przez Kilo podczas rozmowy. Twoja historia git pozostaje nietknięta — snapshoty są przechowywane osobno.\n\nPoczątkowy snapshot tego repozytorium trwa dłużej niż oczekiwano. W bardzo dużych bazach kodu może to znacząco opóźniać każdą turę.\n\nMożesz dalej czekać lub wyłączyć snapshoty dla tego projektu i polegać wyłącznie na git. Ten wybór jest zapisywany w `.kilo/kilo.json`.",
"Inicjalizacja systemu snapshotów trwa długo, prawdopodobnie ze względu na rozmiar repozytorium.\n\nCzy chcesz wyłączyć snapshoty dla tego repozytorium?",
"snapshot.slowRepo.answer.continue": "Kontynuuj ze snapshotami",
"snapshot.slowRepo.answer.continue.description":
"Poczekaj, aż snapshot się zakończy. Kolejne tury są szybkie, gdy początkowy snapshot jest już zbudowany.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Снимок выполняется медленно",
"snapshot.slowRepo.question":
"Система снимков Kilo позволяет отменять и повторять каждое изменение файла, сделанное Kilo во время беседы. Ваша история git не затрагивается — снимки хранятся отдельно.\n\nПервоначальный снимок этого репозитория занимает больше времени, чем ожидалось. На очень больших кодовых базах это может значительно задерживать каждый ход.\n\nВы можете продолжать ждать или отключить снимки для этого проекта и полагаться только на git. Этот выбор сохраняется в `.kilo/kilo.json`.",
"Инициализация системы снимков занимает много времени, вероятно, из-за размера репозитория.\n\nОтключить снимки для этого репозитория?",
"snapshot.slowRepo.answer.continue": "Продолжить со снимками",
"snapshot.slowRepo.answer.continue.description":
"Подождите, пока снимок не завершится. Последующие ходы выполняются быстро после создания первоначального снимка.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "สแน็ปช็อตช้า",
"snapshot.slowRepo.question":
"ระบบสแน็ปช็อตของ Kilo ช่วยให้คุณยกเลิกและทำซ้ำการเปลี่ยนแปลงไฟล์ทุกครั้งที่ Kilo ทำระหว่างการสนทนา ประวัติ git ของคุณจะไม่ถูกแตะต้อง — สแน็ปช็อตจะถูกจัดเก็บแยกต่างหาก\n\nสแน็ปช็อตแรกของที่เก็บนี้ใช้เวลานานกว่าที่คาดไว้ ในโค้ดเบสขนาดใหญ่มาก สิ่งนี้อาจทำให้แต่ละเทิร์นล่าช้าอย่างมาก\n\nคุณสามารถรอต่อหรือปิดการใช้งานสแน็ปช็อตสำหรับโปรเจกต์นี้และพึ่งพา git แทน ตัวเลือกนี้จะถูกบันทึกใน `.kilo/kilo.json`",
"การเริ่มต้นระบบสแน็ปช็อตใช้เวลานาน น่าจะเป็นเพราะขนาดของที่เก็บข้อมูล\n\nต้องการปิดสแน็ปช็อตสำหรับที่เก็บข้อมูลนี้หรือไม่?",
"snapshot.slowRepo.answer.continue": "ใช้สแน็ปช็อตต่อ",
"snapshot.slowRepo.answer.continue.description":
"รอจนกว่าสแน็ปช็อตจะเสร็จ เทิร์นถัดไปจะเร็วขึ้นเมื่อสร้างสแน็ปช็อตแรกเสร็จแล้ว",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Anlık görüntü yavaş",
"snapshot.slowRepo.question":
"Kilo'nun anlık görüntü sistemi, konuşma sırasında Kilo'nun yaptığı her dosya değişikliğini geri almanıza ve yeniden yapmanıza olanak tanır. git geçmişinize dokunulmaz — anlık görüntüler ayrı bir yerde saklanır.\n\nBu deponun ilk anlık görüntüsü beklenenden uzun sürüyor. Çok büyük kod tabanlarında bu, her turu önemli ölçüde geciktirebilir.\n\nBeklemeye devam edebilir veya bu proje için anlık görüntüleri devre dışı bırakıp yalnızca git'e güvenebilirsiniz. Bu seçim `.kilo/kilo.json` dosyasına kaydedilir.",
"Anlık görüntü sisteminin başlatılması uzun sürüyor, muhtemelen deponun boyutu nedeniyle.\n\nBu depo için anlık görüntüleri devre dışı bırakmak istiyor musunuz?",
"snapshot.slowRepo.answer.continue": "Anlık görüntülerle devam et",
"snapshot.slowRepo.answer.continue.description":
"Anlık görüntü tamamlanana kadar bekleyin. İlk anlık görüntü oluşturulduktan sonra sonraki turlar hızlıdır.",
+1 -1
View File
@@ -83,7 +83,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "Знімок виконується повільно",
"snapshot.slowRepo.question":
"Система знімків Kilo дає змогу скасовувати та повторювати кожну зміну файлу, зроблену Kilo під час розмови. Ваша історія git не змінюється — знімки зберігаються окремо.\n\nПочатковий знімок цього репозиторію триває довше, ніж очікувалось. На дуже великих кодових базах це може суттєво уповільнювати кожен хід.\n\nВи можете далі чекати або вимкнути знімки для цього проєкту й покладатися лише на git. Цей вибір зберігається у `.kilo/kilo.json`.",
"Ініціалізація системи знімків займає багато часу, ймовірно, через розмір репозиторію.\n\nВимкнути знімки для цього репозиторію?",
"snapshot.slowRepo.answer.continue": "Продовжити зі знімками",
"snapshot.slowRepo.answer.continue.description":
"Зачекайте, поки знімок завершиться. Наступні ходи будуть швидкими, щойно початковий знімок буде створений.",
+1 -1
View File
@@ -80,7 +80,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "快照速度较慢",
"snapshot.slowRepo.question":
"Kilo 的快照系统让你可以撤销和重做 Kilo 在对话期间所做的每一次文件更改。你的 git 历史记录不会受到影响——快照会单独保存。\n\n这个仓库的初始快照耗时比预期更长。在非常大的代码库上,这可能会显著延迟每一轮操作。\n\n你可以继续等待,或者为这个项目禁用快照,仅依赖 git。此选择会保存到 `.kilo/kilo.json`。",
"初始化快照系统耗时较长,可能是由于仓库的大小。\n\n是否要为此仓库禁用快照?",
"snapshot.slowRepo.answer.continue": "继续使用快照",
"snapshot.slowRepo.answer.continue.description": "等待快照完成。初始快照构建好之后,后续回合会很快。",
"snapshot.slowRepo.answer.disable": "为此项目禁用",
+1 -1
View File
@@ -80,7 +80,7 @@ export const dict = {
// Slow-repo snapshot prompt
"snapshot.slowRepo.header": "快照速度較慢",
"snapshot.slowRepo.question":
"Kilo 的快照系統讓你可以撤銷和重做 Kilo 在對話期間所做的每一次檔案變更。你的 git 歷史紀錄不會受到影響——快照會分開儲存。\n\n這個儲存庫的初始快照所花費的時間比預期更長。在非常大的程式碼庫上,這可能會顯著延遲每一輪操作。\n\n你可以繼續等待,或是為這個專案停用快照,僅依賴 git。此選擇會儲存至 `.kilo/kilo.json`。",
"初始化快照系統耗時較長,可能是由於儲存庫的大小。\n\n是否要為此儲存庫停用快照?",
"snapshot.slowRepo.answer.continue": "繼續使用快照",
"snapshot.slowRepo.answer.continue.description": "等待快照完成。初始快照建立後,後續回合會很快。",
"snapshot.slowRepo.answer.disable": "為此專案停用",
@@ -1260,6 +1260,19 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
return props.turnDiffSummary
})
// Assistant message is still in-flight when `time.completed` hasn't been set.
// Used as a render guard for synthetic status parts so stale ones don't
// linger in the scrollback after a hard-kill of the host process.
const streaming = createMemo(
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
)
// Synthetic text parts (e.g. "Initializing snapshot…" from the slow-repo
// guard) are transient status indicators. Hide them once the owning message
// stops streaming so a hard-killed turn doesn't leave a stuck spinner line
// in the chat history on the next reload.
const showSyntheticPart = createMemo(() => !part().synthetic || streaming())
const showCopy = createMemo(() => {
// Synthetic text parts (e.g. "Initializing snapshot…" from the slow-repo
// guard) are transient status indicators, not assistant output — they
@@ -1308,7 +1321,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
}
return (
<Show when={throttledText()}>
<Show when={throttledText() && showSyntheticPart()}>
<div data-component="text-part">
<div data-slot="text-part-body">
<Markdown text={throttledText()} cacheKey={part().id} onClick={handleMarkdownClick} />
@@ -44,6 +44,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Question } from "@/question"
import type { MessageID, PartID, SessionID } from "@/session/schema"
import { PartID as PartIDSchema } from "@/session/schema"
import type { MessageV2 } from "@/session/message-v2"
import { KilocodeConfig } from "@/kilocode/config/config"
import { ConfigParse } from "@/config/parse"
import { Log } from "@/util"
@@ -55,6 +56,31 @@ import type { Config } from "@/config"
// `Session.Service` at module load races with our own initialization and
// throws "Cannot access 'Service' before initialization". The session
// runtime is built lazily on first use inside the default hooks.
//
// The type-only import of `MessageV2` above is erased at compile time, so it
// doesn't participate in the runtime cycle — it just lets us type the narrow
// part-API shim below without `as any`.
/**
* Narrow typed view of the `Session.Service` methods we actually call from
* `defaultHooks`. Keeping this local instead of importing `Session.Interface`
* avoids the value-level cycle described above, while still giving us
* compile-time checking on payload shape. If `Session.Service` ever renames
* these methods the cast in `sessionRuntime()` below will fail typecheck
* instead of blowing up at runtime.
*/
interface SessionPartAPI {
readonly updatePart: <T extends MessageV2.Part>(part: T) => Effect.Effect<T>
readonly removePart: (input: {
sessionID: SessionID
messageID: MessageID
partID: PartID
}) => Effect.Effect<PartID>
}
type SessionRuntime = {
runPromise: <A>(fn: (svc: SessionPartAPI) => Effect.Effect<A>) => Promise<A>
}
export namespace KiloSnapshotTrack {
const log = Log.create({ service: "snapshot.track" })
@@ -305,6 +331,13 @@ export namespace KiloSnapshotTrack {
)
if (progressFiber) yield* Fiber.interrupt(progressFiber)
yield* clearProgress()
// Reset `asked` only when the snapshot actually succeeded. That way a
// future slow turn (e.g. a new massive worktree gets added) still
// surfaces the dialog instead of silently disabling snapshots on a
// user who just explicitly said "keep them on". If the fiber failed
// (finished === undefined), we leave `asked` sticky to avoid prompt
// spam on a repo that repeatedly errors.
if (finished) input.state.asked = false
return finished
}
@@ -333,32 +366,38 @@ export namespace KiloSnapshotTrack {
const fsRt = makeRuntime(AppFileSystem.Service, AppFileSystem.defaultLayer)
// Lazy to break a module-load cycle with @/session/index.ts.
let cachedSessionRt: ReturnType<typeof makeRuntime<unknown, unknown, never>> | undefined
async function sessionRuntime(): Promise<ReturnType<typeof makeRuntime<unknown, unknown, never>>> {
// Lazy to break a module-load cycle with @/session/index.ts. The single
// cast on the `makeRuntime(...)` result narrows the fully generic runtime
// to the small `SessionPartAPI` surface defined above.
let cachedSessionRt: SessionRuntime | undefined
async function sessionRuntime(): Promise<SessionRuntime> {
if (cachedSessionRt) return cachedSessionRt
const mod = await import("@/session")
cachedSessionRt = makeRuntime(mod.Session.Service, mod.Session.defaultLayer) as ReturnType<
typeof makeRuntime<unknown, unknown, never>
>
cachedSessionRt = makeRuntime(mod.Session.Service, mod.Session.defaultLayer) as unknown as SessionRuntime
return cachedSessionRt
}
/** Build the synthetic progress part payload so both start/update share one shape. */
const progressPart = (input: {
sessionID: SessionID
messageID: MessageID
partID: PartID
text: string
}): MessageV2.TextPart => ({
id: input.partID,
messageID: input.messageID,
sessionID: input.sessionID,
type: "text",
text: input.text,
synthetic: true,
})
export const defaultHooks: Hooks = {
async startProgress(input) {
const partID = PartIDSchema.ascending()
try {
const rt = await sessionRuntime()
await rt.runPromise((svc) =>
(svc as any).updatePart({
id: partID,
messageID: input.messageID,
sessionID: input.sessionID,
type: "text",
text: input.text,
synthetic: true,
}),
)
await rt.runPromise((svc) => svc.updatePart(progressPart({ ...input, partID })))
return { sessionID: input.sessionID, messageID: input.messageID, partID }
} catch (err) {
log.warn("failed to publish snapshot progress part", { err })
@@ -370,14 +409,14 @@ export namespace KiloSnapshotTrack {
try {
const rt = await sessionRuntime()
await rt.runPromise((svc) =>
(svc as any).updatePart({
id: input.handle.partID,
messageID: input.handle.messageID,
sessionID: input.handle.sessionID,
type: "text",
text: input.text,
synthetic: true,
}),
svc.updatePart(
progressPart({
sessionID: input.handle.sessionID,
messageID: input.handle.messageID,
partID: input.handle.partID,
text: input.text,
}),
),
)
} catch (err) {
log.warn("failed to update snapshot progress part", { err })
@@ -388,7 +427,7 @@ export namespace KiloSnapshotTrack {
try {
const rt = await sessionRuntime()
await rt.runPromise((svc) =>
(svc as any).removePart({
svc.removePart({
sessionID: input.handle.sessionID,
messageID: input.handle.messageID,
partID: input.handle.partID,
@@ -444,21 +483,23 @@ export namespace KiloSnapshotTrack {
async persistDisable() {
const directory = await currentDirectory()
if (!directory) return
// Every field on Config.Info is Schema.optional(...), so a single-key
// object is structurally a valid Config.Info — no cast needed.
const patch: Config.Info = { snapshot: false }
await fsRt.runPromise((fs) =>
Effect.gen(function* () {
yield* KilocodeConfig.updateProjectConfig({
fs,
directory: directory.directory,
worktree: directory.worktree,
// Config.Info is deeply typed elsewhere — we just need one field.
config: { snapshot: false } as Config.Info,
config: patch,
read: (file) =>
fs.readFileString(file).pipe(
Effect.map((s) => s as string | undefined),
Effect.catch(() => Effect.succeed<string | undefined>(undefined)),
),
parse: (input, file) => ConfigParse.jsonc(input, file) as Config.Info,
patch: patchJsonc,
patch: patchTopLevelJsonc,
writable: (c) => c,
})
}),
@@ -467,21 +508,28 @@ export namespace KiloSnapshotTrack {
}
/**
* Minimal `patchJsonc` variant kept local to this module so it does not
* depend on the (unexported) helper inside `config/config.ts`. We only
* write a single top-level boolean here, so the full flattening logic is
* not required.
* Minimal JSONC patcher kept local to this module so it does not depend on
* the (unexported) helper inside `config/config.ts`.
*
* This writes each top-level key in `patch` as a single `modify()` edit,
* which means nested object values are replaced wholesale rather than
* merged key-by-key. That's fine for the `{ snapshot: false }` payload we
* currently send, but any future caller that needs deep merging should
* promote `config/config.ts::patchJsonc` to a shared helper and use that
* instead. We assert on non-primitive values so a misuse fails loudly in
* development rather than silently clobbering a user's nested config.
*/
function patchJsonc(input: string, patch: Config.Info): string {
let out = input
for (const [key, value] of Object.entries(patch)) {
if (value === undefined) continue
function patchTopLevelJsonc(input: string, patch: Config.Info): string {
return Object.entries(patch).reduce((out, [key, value]) => {
if (value === undefined) return out
if (value !== null && typeof value === "object") {
log.warn("patchTopLevelJsonc called with a non-scalar value; nested keys will be replaced wholesale", { key })
}
const edits = modify(out, [key], value, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
})
out = applyEdits(out, edits)
}
return out
return applyEdits(out, edits)
}, input)
}
/**
+7 -4
View File
@@ -406,9 +406,10 @@ export const layer: Layer.Layer<
case "start-step":
ctx.stepStart = performance.now() // kilocode_change
// kilocode_change start - pass sessionID + messageID so the slow-repo prompt/progress indicator can attach
if (!ctx.snapshot)
// kilocode_change start - pass sessionID + messageID so the slow-repo prompt/progress indicator can attach
ctx.snapshot = yield* snapshot.track({ sessionID: ctx.sessionID, messageID: ctx.assistantMessage.id }) // kilocode_change end - pass sessionID + messageID
ctx.snapshot = yield* snapshot.track({ sessionID: ctx.sessionID, messageID: ctx.assistantMessage.id })
// kilocode_change end
yield* session.updatePart({
id: PartID.ascending(),
messageID: ctx.assistantMessage.id,
@@ -424,13 +425,15 @@ export const layer: Layer.Layer<
usage: value.usage,
metadata: value.providerMetadata,
})
// kilocode_change start
// kilocode_change start - guard against finish-step without start-step:
// ctx.stepStart is 0 until `start-step` fires, which would feed a
// huge bogus `elapsed` into telemetry. Fall back to now().
KiloSessionProcessor.trackStep({
sessionID: ctx.sessionID,
model: ctx.model,
tokens: usage.tokens,
cost: usage.cost,
elapsed: Math.round(performance.now() - ctx.stepStart),
elapsed: Math.round(performance.now() - (ctx.stepStart || performance.now())),
})
// kilocode_change end
ctx.assistantMessage.finish = value.finishReason
@@ -22,6 +22,14 @@ const slowInner = (ms: number, hash = "slowhash") =>
// Build an inner snapshot that never completes unless interrupted.
const hangInner = () => Effect.promise(() => new Promise<string | undefined>(() => {}))
// Build an inner snapshot that fails with a typed Effect error. The double
// cast mirrors how the real `Snapshot.track` Effect is shaped: callers see
// `Effect.Effect<string | undefined>` (error channel `never`), but failures
// can still flow through because the production code path uses `Effect.catch`
// to absorb them. Centralizing the cast here keeps per-test code readable.
const failingInner = (err: Error) =>
Effect.fail(err as unknown as never) as unknown as Effect.Effect<string | undefined>
type Event = { kind: "start"; text: string } | { kind: "update"; text: string } | { kind: "end" }
interface Calls {
@@ -126,7 +134,9 @@ describe("KiloSnapshotTrack.wrap", () => {
expect(result).toBe("finished-late")
expect(calls.ask).toBe(1)
expect(calls.persist).toBe(0)
expect(state.asked).toBe(true)
// After a successful "continue" the guard resets `asked` so a subsequent
// slow turn still gets the dialog instead of being silently disabled.
expect(state.asked).toBe(false)
expect(state.disabledForSession).toBe(false)
})
@@ -239,7 +249,7 @@ describe("KiloSnapshotTrack.wrap", () => {
expect(secondCalls.ask).toBe(0)
})
test("second call after a slow+continue answer does not re-prompt", async () => {
test("second slow call after a successful continue re-asks instead of silently disabling", async () => {
const state = KiloSnapshotTrack.makeState()
const { hooks, calls } = makeHooks("continue")
@@ -257,10 +267,12 @@ describe("KiloSnapshotTrack.wrap", () => {
)
expect(first).toBe("hash-1")
expect(calls.ask).toBe(1)
expect(state.asked).toBe(true)
// Reset semantics: successful continue clears `asked` so a future slow
// turn gets the dialog again instead of being silently disabled.
expect(state.asked).toBe(false)
expect(state.disabledForSession).toBe(false)
// Second call: still slow → we already asked, so skip silently
// Second call: still slow → dialog again → user picks continue again → finishes
const second = await Effect.runPromise(
KiloSnapshotTrack.wrap({
inner: slowInner(80, "hash-2"),
@@ -272,9 +284,37 @@ describe("KiloSnapshotTrack.wrap", () => {
progressDelayMs: 5,
}),
)
expect(second).toBeUndefined()
expect(calls.ask).toBe(1) // unchanged
expect(state.disabledForSession).toBe(true)
expect(second).toBe("hash-2")
expect(calls.ask).toBe(2) // re-asked
expect(state.disabledForSession).toBe(false)
})
test("continue path keeps `asked` sticky when the fiber finished with no hash", async () => {
const state = KiloSnapshotTrack.makeState()
const { hooks, calls } = makeHooks("continue")
// Simulate the fiber eventually completing but with no hash (e.g.
// snapshot disabled mid-flight or non-git repo). The continue path waits
// for it, so we get undefined back — and we must not reset `asked`,
// otherwise repeated failures would keep re-prompting the user every
// turn.
const noHashInner = Effect.promise(
() => new Promise<string | undefined>((resolve) => setTimeout(() => resolve(undefined), 80)),
)
const first = await Effect.runPromise(
KiloSnapshotTrack.wrap({
inner: noHashInner,
state,
sessionID: SESSION,
messageID: MESSAGE,
hooks,
timeoutMs: 20,
progressDelayMs: 5,
}),
)
expect(first).toBeUndefined()
expect(calls.ask).toBe(1)
expect(state.asked).toBe(true)
})
test("inner typed failure is caught and returned as undefined", async () => {
@@ -285,13 +325,9 @@ describe("KiloSnapshotTrack.wrap", () => {
// what Effect.catch inside wrap() is designed to handle. Untyped defects
// (e.g. rejected Promises without Effect.tryPromise) are NOT caught; they
// propagate and surface as test failures.
const errorInner = Effect.fail(new Error("boom") as unknown as never) as unknown as Effect.Effect<
string | undefined
>
const result = await Effect.runPromise(
KiloSnapshotTrack.wrap({
inner: errorInner,
inner: failingInner(new Error("boom")),
state,
sessionID: SESSION,
messageID: MESSAGE,
+8 -1
View File
@@ -1475,6 +1475,13 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
// are transient status indicators, not assistant output — they must never
// carry the copy button, and they must not "steal" last-part status from
// the actual assistant response that precedes them.
//
// On a clean turn the backend removes the part via `removePart` once the
// snapshot finishes, so it never reaches this branch. But if the host
// process is hard-killed mid-snapshot the part stays in storage and
// re-appears on session reload; hiding it when the owning message is no
// longer streaming keeps the scrollback clean in that edge case.
const showSyntheticPart = createMemo(() => !part().synthetic || streaming())
const isLastTextPart = createMemo(() => {
const last = (data.store.part?.[props.message.id] ?? [])
.filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim() && !item.synthetic)
@@ -1499,7 +1506,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
}
return (
<Show when={text()}>
<Show when={text() && showSyntheticPart()}>
<div data-component="text-part">
<div data-slot="text-part-body">
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>