Bağlam doluluğunu durum satırında gösterir; eşik aşılınca işi güvenli bir noktada bitirtir, devam notu yazdırır ve sorarak yeni sohbete geçer (/devir).

Kendi işimde her gün kullandığım Claude Code modları. Kod yazmadan, Claude'a anlatarak yaptırdım; burada herkes için paylaşıyorum.
🎥 Videoda nasıl çalıştıklarını gösterdim: https://youtu.be/-B0hXhOzuUI 🏫 Soru, destek ve yeni araçlar için ücretsiz Skool topluluğu: https://www.skool.com/mustafa-dikmen-ile-yapay-zeka-1691/about
| Mod | Ne yapar |
|---|---|
| otomatik-devir | Sohbetin bağlamı dolmaya yaklaşınca işi güvenli bir yerde bitirtir, devam notu yazdırır, sohbeti temizleyip işe kaldığı yerden devam ettirir. |
| ilerleme-cubugu | Süren işin ilerlemesini yazı kutusunun üstünde çubuk olarak gösterir: kaç adımın bittiği, yüzde kaç olduğu ve şu an hangi adımda olunduğu. |
"Mod", Claude Code'un içine takılan küçük bir eklentidir. Claude Code'un görünüşünü ve davranışını değiştirir.
Gerekli: Claude Code 2.1.287 veya üstü. Terminalde claude --version yazıp bakın; eskiyse claude update.
Claude Code'u açın ve şu metni olduğu gibi yapıştırın. Komut çalıştırma izni isteyecek, onaylayın.
Şu 3 komutu sırayla çalıştır, başka hiçbir şey kurma veya değiştirme: (1) claude plugin marketplace add muctebadikmen/claude-code-araclari (2) claude plugin install otomatik-devir@muctebadikmen --scope user (3) claude plugin install ilerleme-cubugu@muctebadikmen --scope user. Sonunda claude plugin list çalıştır ve 2 modun da "enabled" olduğunu söyle. Hata olursa dur ve bana hatayı anlat.
claude plugin marketplace add muctebadikmen/claude-code-araclari
claude plugin install otomatik-devir@muctebadikmen --scope user
claude plugin install ilerleme-cubugu@muctebadikmen --scope user
claude plugin list
--scope user "tüm projelerimde geçerli olsun" demektir. Listede ikisini de "enabled" görmelisiniz. Sonra Claude Code'da yeni bir sohbet açın ya da /reload-plugins yazın.
İsterseniz yalnız birini kurabilirsiniz; iki mod birbirinden bağımsız çalışır.
Modları geliştirdikçe buraya yüklüyorum. Yeni sürümü almak için:
claude plugin marketplace update muctebadikmen
claude plugin update otomatik-devir@muctebadikmen
claude plugin update ilerleme-cubugu@muctebadikmen
Sonra Claude Code'u yeniden başlatın.
Alt satırdaki durum çubuğunda Devir: %X / eşik %60 görürsünüz (ilk yanıttan önce %0 görünür). Doluluk eşiği aşınca mod, işi güvenli bir noktada bitirtir ve devam notu yazdırır (.workflow/HANDOFF.md). Sonra "Yeni sohbete geçeyim mi?" diye sorar: g = Geç, b = Bekle. "Geç" dediğinizde sohbeti temizler (/clear), devam prompt'unu yapıştırır ve iş kaldığı yerden sürer.
| Komut | Ne yapar |
|---|---|
/devir | Durumu gösterir |
/devir esik 70 | Eşiği %70 yapar (1-99) |
/devir otomatik | Sormadan yeni sohbete geçer |
/devir sor | Geri sorarak geçmeye döner (varsayılan) |
/devir simdi | Devri hemen başlatır |
/devir gec | Bekleyen devirde yeni sohbete geçer |
/devir kapat / /devir ac | Modu kapatır / açar |
Kendi devir becerisi (my-skill-handoff) olanlarda mod onu kullanır. Olmayanlarda kendi hazır devir talimatını gönderir; ekstra bir şey kurmanız gerekmez.
İki kaynaktan çubuk çizer:
PLAN.md, .workflow/PLAN.md, .workflow/STATE.md ya da .workflow/plans/<klasör>/PLAN.md. Kutucuklar (- [x], - [ ]), numaralı adımlar ve - id: T1 biçimli planlar okunur. Plan dosyasının çubuğu o dosyalardan bahseden sohbete bağlanır: mesajınızda PLAN.md ya da STATE.md geçtiğinde ya da Claude bu dosyaları okuyup yazdığında çubuk çıkar. Aynı projede açtığınız alakasız bir sohbette eski plan görünmez.Videodaki gibi başlatmak için örnek mesaj:
.workflow/PLAN.md ve .workflow/STATE.md: benim için bu formatta bir plan yap ve bu planı uygula. <işin tarifi>
Çubuğa tıklarsanız adım listesi açılır: ✓ biten, ● şu anki, ○ bekleyen.
| Komut | Ne yapar |
|---|---|
/ilerleme detay | Adım listesini açar / kapatır (çubuğa tıklamak da aynı işi yapar) |
/ilerleme gizle | Çubuğu gizler |
/ilerleme goster | Çubuğu geri getirir |
İki mod birlikte çalışır: devirden sonra yeni sohbete yapıştırılan devam prompt'u plan ya da devir dosyalarından (PLAN.md, STATE.md, HANDOFF.md) bahsettiğinde çubuk yeni sohbette de geri gelir. Bahsetmiyorsa Claude o dosyaları okuduğu anda gelir.
claude plugin uninstall otomatik-devir@muctebadikmen --scope user
claude plugin uninstall ilerleme-cubugu@muctebadikmen --scope user
claude plugin marketplace remove muctebadikmen
Modlar Claude Code'un içinde sizin yetkilerinizle çalışır, bu yüzden kurmadan önce bakmanız doğru. Kodun tamamı bu depoda; modlar/<mod>/hooks/ klasörlerinde okuyabilirsiniz. Depoyu indirip bir modu kendiniz de denetleyebilirsiniz:
claude plugin validate modlar/otomatik-devir
claude plugin validate modlar/ilerleme-cubugu
Çıktı, modun hangi işlemleri yaptığını listeler. İki modun listesinde de ağ çağrısı yoktur. ilerleme-cubugu yalnızca proje klasöründeki plan dosyalarını okur, hiçbir dosyaya yazmaz. otomatik-devir devam notunu Claude'a yazdırır, kendisi dosya yazmaz; devir anında sohbeti temizleyip devam prompt'unu gönderir.
esik, simdi (eşik çalışmaz). Devir yanıtı bir kod bloğuyla bitmezse "devam prompt'u bulunamadı" der. Sohbet temizlenemezse devam metni panoya kopyalanır ve ekranda "/clear yazıp yeni sohbete yapıştır" yazar.Her modun testleri modlar/<mod>/tests/ içinde. Çalıştırmak için mod klasöründe:
claude plugin test .
MIT. Kullanın, değiştirin, paylaşın. Sorun görürseniz bir issue açın ya da Skool topluluğunda yazın.
hooks/register.tsx 546 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderSurface } from 'claude-code'
3
4import type { OtomatikDevirConv, OtomatikDevirPhase } from '../types'
5
6// ---------------------------------------------------------------------------
7// Settings ($.store, across sessions) and the per-conversation record ($.state)
8// ---------------------------------------------------------------------------
9
10const conv = atom({ plugin: 'otomatik-devir', key: 'conv' } as const, null)
11
12const SKILL = 'my-skill-handoff'
13const DEFAULT_THRESHOLD = 60
14
15// Used when no `my-skill-handoff` command exists (most people): the mod reads
16// the resume prompt from the LAST code block of the reply, so the prompt asks
17// for it to come last, alone.
18const FALLBACK_PROMPT =
19 "Bu sohbetin devam notunu yaz: hedef, yapılanlar, kalanlar, alınan kararlar ve dikkat edilecekler. .workflow/HANDOFF.md dosyasına kaydet (klasör yoksa oluştur). En sonda, yeni sohbete yapıştırılacak devam prompt'unu tek bir kod bloğu içinde ver; ondan sonra başka kod bloğu yazma."
20
21/**
22 * Turkish dative suffix for a whole number 1-99 as it is read aloud ("yüzde
23 * altmış'a", "yüzde yetmiş'e", "yüzde elli'ye"): vowel harmony and the
24 * buffer y follow the last spoken word, so a fixed "'a" is wrong for 70, 50...
25 */
26const dative = (n: number): string => {
27 const units = ['', "'e", "'ye", "'e", "'e", "'e", "'ya", "'ye", "'e", "'a"]
28 const tens = ['', "'a", "'ye", "'a", "'a", "'ye", "'a", "'e", "'e", "'a"]
29 const unit = n % 10
30
31 return unit !== 0 ? (units[unit] ?? "'a") : (tens[Math.floor(n / 10) % 10] ?? "'a")
32}
33
34export const noteText = (threshold: number): string =>
35 `Bağlam %${threshold}${dative(threshold)} ulaştı. Şu anki adımı güvenli bir noktada bitir, yarım dosya bırakma, yeni işe başlama ve dur.`
36
37type Mode = 'sor' | 'otomatik'
38type Settings = { threshold: number; mode: Mode; isEnabled: boolean }
39
40const readSettings = async ($: EngineInterface): Promise<Settings> => {
41 const [esik, mod, acik] = await Promise.all([
42 $.store.get('esik'),
43 $.store.get('mod'),
44 $.store.get('acik'),
45 ])
46 const threshold =
47 typeof esik === 'number' && Number.isInteger(esik) && esik >= 1 && esik <= 99
48 ? esik
49 : DEFAULT_THRESHOLD
50
51 return {
52 threshold,
53 mode: mod === 'otomatik' ? 'otomatik' : 'sor',
54 isEnabled: acik !== false,
55 }
56}
57
58/** Context fill as a whole percentage, or undefined before the first response. */
59const contextPercent = async ($: EngineInterface): Promise<number | undefined> => {
60 const { context } = await $.session.usage()
61 if (typeof context.percent === 'number') return context.percent
62 if (typeof context.tokens === 'number' && context.window > 0) {
63 return Math.round((context.tokens / context.window) * 100)
64 }
65
66 return undefined
67}
68
69const statusText = (settings: Settings, percent: number | undefined): string =>
70 settings.isEnabled
71 ? `Devir: %${percent ?? 0} / eşik %${settings.threshold}`
72 : 'Devir: kapalı'
73
74const refreshStatus = async ($: EngineInterface, percent?: number): Promise<void> => {
75 const settings = await readSettings($)
76 $.ui.status(statusText(settings, percent ?? (await contextPercent($))))
77}
78
79/** The record of the conversation now running; a new session id starts it over at idle. */
80const currentConv = async ($: EngineInterface): Promise<OtomatikDevirConv> => {
81 const sid = await $.session.id()
82 const held = await read($, conv)
83 if (held !== null && held.sid === sid) return held
84 const fresh: OtomatikDevirConv = { sid, phase: 'idle' }
85 await update($, conv, () => fresh)
86
87 return fresh
88}
89
90const patchConv = async (
91 $: EngineInterface,
92 sid: string,
93 patch: Partial<OtomatikDevirConv>,
94): Promise<void> => {
95 await update($, conv, (held): OtomatikDevirConv =>
96 held !== null && held.sid === sid ? { ...held, ...patch } : { sid, phase: 'idle', ...patch },
97 )
98}
99
100const setPhase = ($: EngineInterface, sid: string, phase: OtomatikDevirPhase) =>
101 patchConv($, sid, { phase })
102
103/**
104 * Re-arms the trigger in the same conversation: `done` (and, with `orWarned`,
105 * `warned`) goes back to `idle`, atomically. Called when the fill is measured
106 * under the threshold again (a /compact), or the person changes the threshold
107 * or switches the mod on. Without it a finished, aborted or failed handoff left
108 * the mod silent until a new session id appeared.
109 */
110const rearm = async ($: EngineInterface, sid: string, orWarned = false): Promise<boolean> =>
111 claim(
112 $,
113 sid,
114 held => held.phase === 'done' || (orWarned && held.phase === 'warned'),
115 { phase: 'idle' },
116 )
117
118/**
119 * Check-and-set in one `update` (ifVersion, retried on a miss): applies `patch`
120 * only when `may` allows the record as it stands, and says whether this call
121 * won. Two racing callers (parallel tool results, a double press, `/devir simdi`
122 * landing beside a turn end) cannot both pass. A record of another sid is
123 * left alone.
124 */
125const claim = async (
126 $: EngineInterface,
127 sid: string,
128 may: (held: OtomatikDevirConv) => boolean,
129 patch: Partial<OtomatikDevirConv>,
130): Promise<boolean> => {
131 let isClaimed = false
132 await update($, conv, (held): OtomatikDevirConv | null => {
133 if (held !== null && held.sid !== sid) {
134 isClaimed = false
135 return held
136 }
137 const now: OtomatikDevirConv = held ?? { sid, phase: 'idle' }
138 isClaimed = may(now)
139 return isClaimed ? { ...now, ...patch } : held
140 })
141
142 return isClaimed
143}
144
145// ---------------------------------------------------------------------------
146// Handoff steps
147// ---------------------------------------------------------------------------
148
149/**
150 * The text of the last non-empty fenced code block (``` or ~~~, any length),
151 * fence and info string stripped. Line-based, as CommonMark reads fences: a
152 * block closes only on a line of the same fence character, at least as long,
153 * with nothing after it, so inner shorter fences and ```lang lines stay in the
154 * body; an empty block cannot pair a closing fence with the next opening one;
155 * a fence left open runs to the end of the text.
156 */
157const lastFencedBlock = (text: string): string | undefined => {
158 let last: string | undefined
159 let open: { char: string; size: number; lines: string[] } | undefined
160 const close = (): void => {
161 const body = open?.lines.join('\n').trim() ?? ''
162 if (body !== '') last = body
163 open = undefined
164 }
165 for (const line of text.split(/\r?\n/)) {
166 const fence = /^[ \t]*(`{3,}|~{3,})(.*)$/.exec(line)
167 const marks = fence?.[1] ?? ''
168 const rest = fence?.[2] ?? ''
169 const char = marks.charAt(0)
170 if (open === undefined) {
171 // A backtick fence's info string cannot hold a backtick (CommonMark).
172 if (fence !== null && !(char === '`' && rest.includes('`'))) {
173 open = { char, size: marks.length, lines: [] }
174 }
175 continue
176 }
177 if (fence !== null && char === open.char && marks.length >= open.size && rest.trim() === '') {
178 close()
179 continue
180 }
181 open.lines.push(line)
182 }
183 if (open !== undefined) close()
184
185 return last
186}
187
188const hasHandoffSkill = async ($: EngineInterface): Promise<boolean> => {
189 try {
190 if ((await $.command.list()).some(command => command.name === SKILL)) return true
191 } catch {
192 // fall through to the file checks
193 }
194 const home = await $.env.get('HOME')
195 const places = [
196 home === undefined ? undefined : `${home}/.claude/skills/${SKILL}/SKILL.md`,
197 `${await $.session.root()}/.claude/skills/${SKILL}/SKILL.md`,
198 ]
199 for (const place of places) {
200 if (place !== undefined && (await $.fs.exists(place).catch(() => false))) return true
201 }
202
203 return false
204}
205
206/** Starts the handoff turn: the skill when it exists, else the fallback prompt. */
207const startHandoff = async ($: EngineInterface, sid: string): Promise<void> => {
208 // One handoff at a time; clearing `grace` keeps the handoff turn's end from
209 // being eaten by the grace branch of turn.complete.
210 const isClaimed = await claim($, sid, held => held.phase !== 'handoff_running', {
211 phase: 'handoff_running',
212 grace: false,
213 })
214 if (!isClaimed) return
215 $.ui.toast('Devir başlıyor…')
216 if (await hasHandoffSkill($)) {
217 try {
218 await $.command.run({ command: SKILL })
219
220 return
221 } catch {
222 $.ui.log(`otomatik-devir: /${SKILL} çalıştırılamadı, yedek prompt gönderiliyor`, {
223 to: 'debug',
224 })
225 }
226 }
227 try {
228 const sent = await $.prompt.submit({ text: FALLBACK_PROMPT, asUser: true })
229 if (sent.drop !== undefined) throw new Error(`prompt dropped: ${sent.drop}`)
230 } catch (error) {
231 // No handoff turn will come: do not leave the phase at handoff_running,
232 // where the next unrelated turn would be parsed as the handoff.
233 await setPhase($, sid, 'done')
234 $.ui.log(`otomatik-devir: devir başlatılamadı: ${String(error)}`, { to: 'debug' })
235 }
236}
237
238/** Clears the chat and submits the continuation prompt; on failure, clipboard + toast. */
239const switchChat = async (
240 $: EngineInterface,
241 sid: string,
242 prompt: string,
243 surface?: RenderSurface,
244): Promise<void> => {
245 // Only one switch per prompt: a double press of [Geç] (or Geç racing the
246 // otomatik switch) must not run /clear and submit twice.
247 const isClaimed = await claim($, sid, held => held.phase === 'awaiting_choice', {
248 phase: 'done',
249 })
250 if (!isClaimed) return
251 let isSubmitted = false
252 try {
253 await $.command.run({ command: 'clear' })
254 const fresh = await $.session.id()
255 if (fresh === sid) throw new Error('clear did not start a new conversation')
256 // The continuation turn of the fresh chat is exempt from triggering.
257 await update($, conv, (): OtomatikDevirConv => ({ sid: fresh, phase: 'idle', grace: true }))
258 const sent = await $.prompt.submit({ text: prompt, asUser: true })
259 if (sent.drop !== undefined) throw new Error(`prompt dropped: ${sent.drop}`)
260 isSubmitted = true
261 } catch {
262 const copied = await $.ui
263 .copy(surface === undefined ? { text: prompt } : { text: prompt, surface })
264 .catch(() => ({ isCopied: false as const }))
265 $.ui.toast(
266 copied.isCopied
267 ? "Devam prompt'u panoya kopyalandı. /clear yazıp yeni sohbete yapıştır."
268 : "Yeni sohbete yapıştır: devam prompt'u aşağıda.",
269 { timeoutMs: 15000 },
270 )
271 if (!copied.isCopied) $.ui.log(`Yeni sohbete yapıştır:\n${prompt}`)
272 // Still in the same conversation (the clear did not happen): keep the
273 // prompt claimable so `/devir gec` can try again.
274 const now = await $.session.id().catch(() => sid)
275 if (now === sid) await patchConv($, sid, { phase: 'awaiting_choice', bandHidden: true })
276 }
277 if (isSubmitted) await refreshStatus($).catch(() => undefined)
278}
279
280/** Work that must not run inside the dispatch the turn waits on. */
281const later = ($: EngineInterface, work: () => Promise<void>): void => {
282 $.clock.after(50, () => {
283 work().catch(error => $.ui.log(`otomatik-devir: ${String(error)}`, { to: 'debug' }))
284 })
285}
286
287// ---------------------------------------------------------------------------
288// Hooks
289// ---------------------------------------------------------------------------
290
291export const register: Register = on => {
292 on('session.start', async ($, e, next) => {
293 await $.command.register({
294 name: 'devir',
295 description: 'Otomatik devir: durum, esik <n>, otomatik, sor, simdi, kapat, ac, gec',
296 argumentHint: '[esik <n> | otomatik | sor | simdi | kapat | ac | gec]',
297 })
298 await currentConv($)
299 await refreshStatus($)
300
301 return next(e)
302 })
303
304 // A /clear (or any end) drops this conversation's record.
305 on('session.end', async ($, e, next) => {
306 await update($, conv, held => (held !== null && held.sid === e.sessionId ? null : held))
307
308 return next(e)
309 })
310
311 // Status line on every measured change of the fill.
312 // A fill measured under the threshold again (e.g. after /compact) re-arms a
313 // `done` record of this conversation.
314 on('session.measure', async ($, e, next) => {
315 await refreshStatus($, e.context.percent)
316 const percent = e.context.percent
317 if (percent !== undefined) {
318 const settings = await readSettings($)
319 if (percent < settings.threshold) {
320 const held = await read($, conv)
321 if (held !== null && held.phase === 'done') await rearm($, held.sid)
322 }
323 }
324
325 return next(e)
326 })
327
328 // Mid-turn: once a tool result of the main loop is stored, check the fill
329 // and, the first time the threshold is crossed, append the hidden note.
330 on('session.append', { door: 'tool-result' }, async ($, e, next) => {
331 const stored = await next(e)
332 if (e.agentId !== undefined) return stored
333 const settings = await readSettings($)
334 if (!settings.isEnabled) return stored
335 const held = await currentConv($)
336 if (held.phase !== 'idle' || held.grace === true) return stored
337 const percent = await contextPercent($)
338 if (percent === undefined || percent < settings.threshold) return stored
339 // Parallel tool results run this hook side by side: only the one that
340 // moves idle -> warned appends the note, so it is injected once.
341 const isClaimed = await claim(
342 $,
343 held.sid,
344 now => now.phase === 'idle' && now.grace !== true,
345 { phase: 'warned' },
346 )
347 if (!isClaimed) return stored
348 $.ui.status(statusText(settings, percent))
349 // Not awaited: the row joins after the stored tool result, outside this chain.
350 void $.session
351 .append({ message: { type: 'user', content: [{ type: 'text', text: noteText(settings.threshold) }] } })
352 .catch(error => $.ui.log(`otomatik-devir: not eklenemedi: ${String(error)}`, { to: 'debug' }))
353
354 return stored
355 })
356
357 on('turn.complete', async ($, e, next) => {
358 const result = await next(e)
359 if (e.agentId !== undefined) return result
360 const settings = await readSettings($)
361 const percent = await contextPercent($)
362 $.ui.status(statusText(settings, percent))
363 const held = await currentConv($)
364
365 if (held.grace === true) {
366 await patchConv($, held.sid, { grace: false })
367
368 return result
369 }
370
371 if (held.phase === 'handoff_running') {
372 if (e.isAborted) {
373 await setPhase($, held.sid, 'done')
374 $.ui.toast('Devir iptal edildi.')
375
376 return result
377 }
378 let prompt = lastFencedBlock(e.answer)
379 if (prompt === undefined) {
380 const last = (await $.session.messages()).filter(m => m.role === 'assistant').at(-1)
381 prompt = last === undefined ? undefined : lastFencedBlock(last.text)
382 }
383 if (prompt === undefined) {
384 await setPhase($, held.sid, 'done')
385 $.ui.toast("Devir: devam prompt'u bulunamadı.")
386
387 return result
388 }
389 await patchConv($, held.sid, { phase: 'awaiting_choice', prompt, bandHidden: false })
390 if (settings.mode === 'otomatik') {
391 const text = prompt
392 later($, () => switchChat($, held.sid, text))
393 }
394
395 return result
396 }
397
398 const isCrossed = percent !== undefined && percent >= settings.threshold
399 if (held.phase === 'done') {
400 // Back to idle once the fill is under the threshold; while it is still
401 // over, stay quiet instead of starting the same handoff again every turn.
402 if (percent !== undefined && !isCrossed) await rearm($, held.sid)
403
404 return result
405 }
406 const isDue =
407 held.phase === 'warned' || (held.phase === 'idle' && settings.isEnabled && isCrossed)
408 if (isDue && !e.isAborted && settings.isEnabled) {
409 later($, () => startHandoff($, held.sid))
410 }
411
412 return result
413 })
414
415 on('command.run', { command: 'devir' }, async ($, e) => {
416 const [verb = '', value] = e.args.trim().split(/\s+/)
417 const settings = await readSettings($)
418 const held = await currentConv($)
419
420 switch (verb.toLowerCase()) {
421 case 'esik': {
422 const n = Number(value)
423 if (!Number.isInteger(n) || n < 1 || n > 99) {
424 return { text: 'Kullanım: /devir esik <1-99>' }
425 }
426 await $.store.set('esik', n)
427 // A new threshold is a fresh start: a `warned` or `done` record re-arms.
428 await rearm($, held.sid, true)
429 await refreshStatus($)
430
431 return { text: `Devir eşiği %${n} olarak ayarlandı.` }
432 }
433 case 'otomatik':
434 await $.store.set('mod', 'otomatik')
435
436 return { text: 'Devir modu: otomatik (sormadan geçer).' }
437 case 'sor':
438 await $.store.set('mod', 'sor')
439
440 return { text: 'Devir modu: sor.' }
441 case 'kapat':
442 await $.store.set('acik', false)
443 await refreshStatus($)
444
445 return { text: 'Otomatik devir kapatıldı.' }
446 case 'ac':
447 await $.store.set('acik', true)
448 await rearm($, held.sid)
449 await refreshStatus($)
450
451 return { text: 'Otomatik devir açıldı.' }
452 case 'simdi':
453 if (held.phase === 'handoff_running') return { text: 'Devir zaten çalışıyor.' }
454 later($, () => startHandoff($, held.sid))
455
456 return { text: 'Devir başlatılıyor.' }
457 case 'gec': {
458 // switchChat only runs from awaiting_choice: say so instead of
459 // answering "geçiliyor" and then doing nothing (e.g. after `done`).
460 if (held.phase === 'handoff_running') {
461 return { text: "Devir hâlâ çalışıyor; devam prompt'u henüz hazır değil." }
462 }
463 if (held.phase !== 'awaiting_choice' || held.prompt === undefined) {
464 return {
465 text: `Geçilecek bekleyen devir yok (aşama: ${held.phase}). Yeni devir için: /devir simdi`,
466 }
467 }
468 const text = held.prompt
469 later($, () => switchChat($, held.sid, text))
470
471 return { text: 'Yeni sohbete geçiliyor.' }
472 }
473 case '': {
474 const percent = await contextPercent($)
475
476 return {
477 text: `${statusText(settings, percent)} · mod: ${settings.mode} · aşama: ${held.phase}`,
478 }
479 }
480 default:
481 return { text: 'Kullanım: /devir [esik <n> | otomatik | sor | simdi | kapat | ac | gec]' }
482 }
483 })
484
485 // The question band: "Yeni sohbete geçeyim mi?" [Geç] [Bekle]
486 //
487 // The context indicator lives in the status line ($.ui.status: one line per
488 // plugin, no contention); only this question uses the AbovePrompt band,
489 // which every mod shares (ilerleme-cubugu draws there too, blast-radius /
490 // Silme Kalkanı falls back to it in a narrow terminal).
491 //
492 // Coexistence contract: nothing to show -> return next(e) untouched;
493 // otherwise this row FIRST, then the drawing of the plugins below (next(e)),
494 // in a column Box, never swallowed. While a turn runs (isWorking) the row
495 // stays hidden: a blast-radius hold is mid-turn and owns the keys then (its
496 // own autoFocus, hotkeys 1/2); this question only matters once the handoff
497 // turn has ended. Hotkeys are letters (g, b), so digits stay free for others.
498 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
499 if (e.props.hasSurvey || e.props.isWorking) return next(e)
500 const held = await read($, conv)
501 if (
502 held === null ||
503 held.phase !== 'awaiting_choice' ||
504 held.bandHidden === true ||
505 held.prompt === undefined
506 ) {
507 return next(e)
508 }
509
510 const below = await next(e)
511 const { Box, Button, Text } = $.ui.resolve(e)
512 const sid = held.sid
513 const prompt = held.prompt
514
515 return (
516 <Box flexDirection="column">
517 <Box key="devir">
518 <Text>Yeni sohbete geçeyim mi? </Text>
519 <Button
520 key="gec"
521 label="Geç"
522 hotkey="g"
523 variant="primary"
524 autoFocus
525 onPress={press => {
526 later($, () => switchChat($, sid, prompt, press.surface))
527 }}
528 />
529 <Text> </Text>
530 <Button
531 key="bekle"
532 label="Bekle"
533 hotkey="b"
534 onPress={() => {
535 void update($, conv, now =>
536 now !== null && now.sid === sid ? { ...now, bandHidden: true } : now,
537 )
538 }}
539 />
540 </Box>
541 {below ?? null}
542 </Box>
543 )
544 })
545}
546types/index.d.ts 25 lines1export type OtomatikDevirPhase =
2 | 'idle'
3 | 'warned'
4 | 'handoff_running'
5 | 'awaiting_choice'
6 | 'done'
7
8export type OtomatikDevirConv = {
9 /** The conversation (session id) this record belongs to; a /clear mints a new one. */
10 sid: string
11 phase: OtomatikDevirPhase
12 /** The continuation prompt taken from the handoff reply. */
13 prompt?: string
14 /** True after [Bekle]: the band is hidden, the prompt kept for /devir gec. */
15 bandHidden?: boolean
16 /** True for the continuation turn in a fresh chat: it never triggers. */
17 grace?: boolean
18}
19
20declare module 'claude-code' {
21 interface PluginState {
22 'otomatik-devir': { conv: OtomatikDevirConv | null }
23 }
24}
25