L2 v0.8.0 source

sidebar

Transcript'in yanında tek bir paylaşılan pane açan ve diğer her mod'un oraya yazdığını çizen bir Claude Code Mod'u. Tek bir sidebar vardır, mod başına bir pane değil: bir mod $.sidebar.set(...) çağrısını satır ve button'lardan oluşan bir section ile yapar, bu mod da onu çizer.

Ne yapar

  1. /sidebar pane'i açar, /sidebar off kapatır. Seçim $.store içinde tutulur, yani daha sonra başlayan bir session sidebar'ı kendiliğinden yeniden açar.
  2. Açıkken herhangi bir mod bir section yazar: $.sidebar.set({ consumer, key, title, lines, buttons, until, order }) true cevaplar. Kapalıyken hiçbir şey tutulmaz ve çağrı false cevaplar, yani mod kendi transcript satırını ya da status line'ını göstermeye devam eder.
  3. Bir section kalın bir başlık (<consumer>: <title>, bir stream entry'sinde ayrıca saat), satırları (ok yeşil, warn sarı, error kırmızı, dim soluk) ve button'ları olarak çizilir. Pane iki parçadır: üstte duran section'lar (until: 'session', sonra until: 'turn', her grup order, sonra consumer, sonra key sırasıyla) ve altlarında stream.
  4. Stream, until: 'stream' ile yazılanlardır: bulguların bir log'u, en yenisi önce, duran section'ların hemen altında. Bir entry başka birinin yerini almaz, yani aynı mod ve key iki kere iki entry olarak okunur. Bir stream entry'sinin başlığı ayrıca yazıldığı gün ve saati taşır, makinenin kendi time zone'unda (edit-loop: edit loop (21.09 14:32)), böylece kişi log'u sonradan okur; duran bir section saat taşımaz, çünkü her ölçümde yeniden yazılır. Turun sonunda hiçbir şey bir entry'yi düşürmez: bir entry yalnız yenileri onu pane'in son satırının ötesine ittiğinde gider. Daha uzun bir terminal stream'in daha fazlasını tutar, daha kısası daha azını. Stream'in satırları paylaşılır: birden fazla mod oraya yazarken her biri en fazla kendi payı kadar satır çizer, yani konuşkan bir mod başka bir mod'un bulgusunu pane'den itemez. Bir payın artırdığı satırlar onun geri tuttuğu entry'lere gider ve tek başına yazan bir mod bütün alanı alır.
  5. Bir button bir slash komutu çalıştırır: { label: 'stop', command: 'bg-tasks', args: 'stop b1' } için [ stop ] basışı /bg-tasks stop b1 komutunu kişi gibi çalıştırır ve komutun ilk cevap satırı pane'in altında görünür. Button'u sunan mod o komutu kendisi karşılar. Etiket işaretçi altında kırmızıya döner, yani bir basışın ne çalıştıracağı basıştan önce açıktır.
  6. Üç ömür: session mod onu değiştirene ya da temizleyene kadar üstte durur, stream altındaki log'a katılır, turn tur bitince gider.
  7. Her stream entry'si ayrıca bu projenin kendi log dosyasına yazılır, ~/.claude/sidebar/<project>-<YYYY-MM-DD>.log, satır başına bir JSON object. Pane açıldığında o projenin log'larının en yeni 10 entry'si stream'e geri gelir, her biri ilk yazıldığı gün ve saatle, yani yarın başlayan bir session dün bulunanı yine gösterir. Geri gelen bir entry log'a yeniden yazılmaz. /sidebar log dosyanın path'ini ve en yeni 10 entry'sini yazar.

Diğer mod'ların kullandığı API

plugin.json içinde "dependencies": ["sidebar"] bildirmeyin. Bildirilen bir dependency serttir: kişide sidebar kurulu değilken engine mod'unuzu hiç yüklemez (2.1.278 üzerinde ölçüldü). API'yi bir guard arkasından çağırın; böylece mod'unuz bu mod olsun olmasın çalışır:

/** The finding the person reads: the sidebar while it is open, else the mod's own transcript line. */
async function toPerson($: EngineInterface, findings: readonly string[], line: string): Promise<void> {
  try {
    const taken = await $.sidebar.set({
      consumer: 'my-mod',              // your mod's name, drawn in the section heading
      key: 'src-users.ts',             // names the section inside your mod; [A-Za-z0-9._:-]
      title: 'SQL built from strings', // the heading beside the consumer
      lines: findings.map(text => ({ text, kind: 'error' })), // kind: 'ok' | 'warn' | 'error' | 'dim', or absent
      buttons: [{ label: 'fix', command: 'my-mod', args: 'fix src/users.ts' }], // optional
      until: 'stream',                 // 'stream' logs it, 'session' keeps it standing, 'turn' drops it at the turn's end
      order: 50,                       // smaller is higher inside your group; 100 when absent
    })
    if (taken) return
  } catch {
    // The sidebar mod is not installed, so $.sidebar is missing and the call throws.
  }
  $.ui.log(line)
}

set, section tutulup çizildiğinde true, sidebar kapalıyken false cevaplar, yani tek bir if (taken) return hem kapalı hem eksik durumu kapsar. clear({ consumer, key }) o key'in duran section'ını ve her stream entry'sini kaldırır, isOpen() pane'in açık olup olmadığını cevaplar.

types/index.d.ts contract'tır: SidebarSection, SidebarLine, SidebarButton, SidebarUntil ve Sidebar. /plugin-types onu etkin her plugin için .claude/types/claude-code-plugins/ altına kopyalar, yani $.sidebar mod'unuzda elle hiçbir şey kopyalamadan type'lanır. Ona karşı geliştirmek için claude --plugin-dir <your mod> --plugin-dir <path to sidebar> kullanın.

Bir claude plugin test dosyasında test engine'i engine.create çalıştırmaz, bu yüzden noun'u inline bir plugin ile stub'layın ve çağrılarını world'de cevaplayın:

const SIDEBAR: Plugin = {
  name: 'sidebar',
  register(on) {
    const stub = async (): Promise<never> => { throw new Error('answered by the test world') }
    on('engine.create', async (_, e, next) => ({ ...(await next(e)), sidebar: { set: stub, clear: stub, isOpen: stub } }))
  },
}
// then in the test: on('sidebar.set', (_, e) => ({ value: true }))

Section başına sınırlar: 50 satır ve 5 button; dışarıda kalan satırlar pane'de sayılır. Pane, surface'in body'sine verdiği kadar satır çizer, en fazla 200. Stream consumer başına en yeni 20 entry'yi ve toplamda 100 tanesini tutar, satırlar bunların kaçını gösterirse göstersin, ve çizdiği satırlar oraya yazan consumer'lar arasında paylaşılır. Pane'in genişliğinden uzun bir satır kesilir. consumer, key ya da title değeri başka biçimde olan bir section, çağıran mod'un okuduğu bir hata ile reddedilir.

Komut

/sidebar            pane'i açar, açıksa kapatır
/sidebar on | off   aynısı, adlandırılmış
/sidebar status     on ya da off, kaç section durduğu ve stream'in kaç entry tuttuğu
/sidebar log        bu projenin bugünkü log'unun path'i ve en yeni 10 entry'si

Kurulum

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install sidebar@kilimcininkoroglu-mods

Function hook'lar early access. Flag olmadan hiçbir şey yüklenmez. Flag'i kalıcı yapmak için ~/.claude/settings.json dosyasına ekleyin:

{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }

Kurulumdan sonra

  1. Claude Code'u yeniden başlatın.
  2. Bir kere /sidebar çalıştırın. O andan sonra /sidebar off çalıştırana kadar her session onu açar.

Nereye uzanır

Claude Code 2.1.278 üzerinde claude plugin validate ile doğrulandı:

❯ types ./types/index.d.ts declares on $: $.sidebar
❯ ./register.tsx hooks: engine.create, session.start, command.run{command=sidebar}, ui.render{component=Pane}, ui.close, turn.complete
❯ ./register.tsx calls: $.clock.now, $.command.register, $.command.run (via pressButton), $.env.get (via openLog), $.fs.list (via logFiles), $.fs.read (via readOrEmpty), $.fs.write, $.session.cwd (via openLog, restoreLog), $.store.get, $.store.set, $.ui.close (via closePane), $.ui.invalidate, $.ui.open (via openPane), $.ui.panes (via closePane), $.ui.resolve
❯ ./register.tsx env writes: nothing
❯ ./register.tsx env reads: HOME

Reach L2, bir dosya yazar.

1. Okur:     diğer mod'ların verdiği section'ları, bir stream entry'sinin kendi saati için saati, HOME, session'ın dizinini ve ~/.claude/sidebar altındaki bu projenin kendi log dosyalarını
2. Çalıştırır: bir button'ın adlandırdığı slash komutunu, $.command.run üzerinden, yalnız kişinin basışıyla
3. Gönderir: hiçbir şey
4. Saklar:   $.store içinde sidebar'ın açık olup olmadığını; ~/.claude/sidebar içinde proje ve gün başına bir log dosyası, diğer mod'ların yazdığı stream entry'lerini tutar
5. Düşman girdi: bir section başka bir plugin'den gelir ve veri olarak okunur: consumer, key ve title kontrol edilir, başka biçimde her satır ve button düşürülür, metin tek satıra katlanır ve genişliğe wrap edilir, sayılar sınırlanır. Bir log satırı da aynı okunur, yani elle düzenlenmiş ya da kesilmiş bir dosya yalnız o satırı kaybeder, başka bir şeyi değil.

Sınırlar

  • Log dizini 0.7.0'a kadar ~/.claude/stream idi ve 0.8.0'dan itibaren ~/.claude/sidebar, yani her mod'un kendi dizini mod'un adını taşır. Hiçbir şey taşınmaz: eski dosyalar diskte okunmadan kalır. Daha eski günlerin geri gelmesini istiyorsanız onları kendiniz taşıyın: mv ~/.claude/stream/* ~/.claude/sidebar/.

  • Pane yalnız fullscreen layout altında transcript'in yanına konur; diğer durumda prompt'un üstünde açılır.

  • Sidebar'ı saklanan seçimden açan bir session onu kişi olarak değil plugin olarak açar: engine böyle bir pane'i 144 terminal kolonunun altında çizmez, kişi o pane'i bir kere kendisi açtıysa 110'un altında. O session'daki /sidebar onu her genişlikte yerleştirir.

  • Sidebar'ı kapatmak her section'ı ve bütün stream'i düşürür. Yeniden açıldığında hiçbiri geri gelmez; her mod kendi section'ını sonraki güncellemede yazar.

  • Pane'deki stream session boyunca durur; diskteki log bir restart'tan sonra kalan şeydir ve yalnız en yeni 10 entry'si geri gelir.

  • Log proje ve gün başınadır. Proje, session'ın başladığı dizinin son parçasıdır, yani bir repository'nin iki checkout'u bir log dosyası paylaşır.

  • Bir günün dosyası en yeni 500 satırını tutar. Eski bir günün dosyasını hiçbir şey kaldırmaz; onu temizlemek sizin işinizdir.

  • Her entry'de bütün dosya yeniden yazılır, çünkü engine'in $.fs arayüzünde append yoktur. Başarısız bir yazma geçilir ve pane çalışmaya devam eder.

  • Bir stream entry'sinin saati, mod'un onu yazdığı andır, $.clock.now() ile okunur ve makinenin kendi time zone'unda çizilir. Bulgunun olduğu an değildir ve sonradan değişmez.

  • Pane'in genişliğinden uzun bir satır, sığan son boşlukta, en fazla 4 satıra wrap edilir; ilkinden sonraki her satır iki boşluk girintilidir. O 4 satırdan uzun bir satırın son satırı ile kesilir. Bir başlık wrap edilmez, kesilir.

  • Stream consumer başına 20, toplamda 100 entry tutar. Kendi sayısını aşan bir mod kendi en eski entry'sini düşürür, hiçbir zaman başka bir mod'unkini değil.

  • Bir button yalnız bir slash komutu çalıştırabilir. Button isteyen bir mod onun için bir komut sunmak zorundadır.

  • Pane'in scroll penceresi engine'e aittir; bu mod kendi scroll'unu eklemez.

Geliştirme

make install     # eslint, typescript, typescript-eslint
make lint        # complexity limiti 10, üstünde build'i düşürür
make typecheck   # /plugin-types ile üretilen .claude/types/ gerekir
make validate
make test        # claude plugin test

sidebar

A Claude Code Mod that opens one shared pane beside the transcript and draws what every other mod writes into it. There is one sidebar, not one pane per mod: a mod calls $.sidebar.set(...) with a section of lines and buttons, and this mod draws it.

What it does

  1. /sidebar opens the pane and /sidebar off closes it. The choice is kept in $.store, so a session started later opens the sidebar again by itself.
  2. While it is open, any mod writes a section: $.sidebar.set({ consumer, key, title, lines, buttons, until, order }) answers true. While it is closed nothing is kept and the call answers false, so the mod keeps showing its own transcript line or status line instead.
  3. A section is drawn as a bold heading (<consumer>: <title>, plus the time for a stream entry), its lines (ok green, warn yellow, error red, dim faint) and its buttons. The pane has two parts: the standing sections at the top (until: 'session', then until: 'turn', each group by order, then consumer, then key), and the stream under them.
  4. The stream is what until: 'stream' writes: a log of findings, newest first, right under the standing sections. An entry never replaces another, so the same mod and key twice reads as two entries. A stream entry's heading also carries the day and time it was written, in the machine's own time zone (edit-loop: edit loop (21.09 14:32)), so the person reads the log after the fact; a standing section carries none, because it is rewritten at every measure. Nothing drops an entry at the turn's end: an entry leaves only when newer ones push it past the pane's last row. A taller terminal holds more of the stream, a shorter one less. The stream's rows are shared: while several mods write into it, each one draws at most its own share of the rows, so a talkative mod cannot push another mod's finding off the pane. The rows a share leaves over go to the entries it held back, and a mod writing alone takes the whole area.
  5. A button runs a slash command: pressing [ stop ] of { label: 'stop', command: 'bg-tasks', args: 'stop b1' } runs /bg-tasks stop b1 as the person would, and the command's first answer line shows at the foot of the pane. The mod that offers the button serves that command itself. The label turns red under the pointer, so what a press would run is plain before the press.
  6. The three lifetimes: session stands at the top until the mod replaces or clears it, stream joins the log under it, turn goes when the turn ends.
  7. Every stream entry is also written to this project's own log file, ~/.claude/sidebar/<project>-<YYYY-MM-DD>.log, one JSON object per line. When the pane opens, the newest 10 entries of that project's logs come back into the stream, each with the day and time it was first written, so a session started tomorrow still shows what yesterday found. A restored entry is not written to the log again. /sidebar log prints the file's path and its newest 10 entries.

The API other mods use

Do not declare "dependencies": ["sidebar"] in plugin.json. A declared dependency is a hard one: the engine does not load your mod at all when the person has no sidebar installed (measured on 2.1.278). Call the API behind a guard instead, and your mod works with or without this one:

/** The finding the person reads: the sidebar while it is open, else the mod's own transcript line. */
async function toPerson($: EngineInterface, findings: readonly string[], line: string): Promise<void> {
  try {
    const taken = await $.sidebar.set({
      consumer: 'my-mod',              // your mod's name, drawn in the section heading
      key: 'src-users.ts',             // names the section inside your mod; [A-Za-z0-9._:-]
      title: 'SQL built from strings', // the heading beside the consumer
      lines: findings.map(text => ({ text, kind: 'error' })), // kind: 'ok' | 'warn' | 'error' | 'dim', or absent
      buttons: [{ label: 'fix', command: 'my-mod', args: 'fix src/users.ts' }], // optional
      until: 'stream',                 // 'stream' logs it, 'session' keeps it standing, 'turn' drops it at the turn's end
      order: 50,                       // smaller is higher inside your group; 100 when absent
    })
    if (taken) return
  } catch {
    // The sidebar mod is not installed, so $.sidebar is missing and the call throws.
  }
  $.ui.log(line)
}

set answers true when the section was kept and drawn, and false when the sidebar is closed, so one if (taken) return covers both the closed and the missing case. clear({ consumer, key }) removes your standing section of that key and every stream entry of it, and isOpen() answers whether the pane is up.

types/index.d.ts is the contract: SidebarSection, SidebarLine, SidebarButton, SidebarUntil and Sidebar. /plugin-types copies it into .claude/types/claude-code-plugins/ for every enabled plugin, so $.sidebar is typed in your mod with nothing copied by hand. Develop against it with claude --plugin-dir <your mod> --plugin-dir <path to sidebar>.

In a claude plugin test file the test engine runs no engine.create, so stub the noun with an inline plugin and answer its calls in the world:

const SIDEBAR: Plugin = {
  name: 'sidebar',
  register(on) {
    const stub = async (): Promise<never> => { throw new Error('answered by the test world') }
    on('engine.create', async (_, e, next) => ({ ...(await next(e)), sidebar: { set: stub, clear: stub, isOpen: stub } }))
  },
}
// then in the test: on('sidebar.set', (_, e) => ({ value: true }))

Limits per section: 50 lines and 5 buttons; the lines left out are counted in the pane. The pane draws as many rows as the surface gave its body, and at most 200. The stream holds its newest 20 entries per consumer and 100 in all, however few of them the rows show, and the rows it draws are shared between the consumers writing into it. A line longer than the pane's width is cut. A section whose consumer, key or title is of another shape is refused with an error the calling mod reads.

Command

/sidebar            opens the pane, or closes it while it is open
/sidebar on | off   the same, named
/sidebar status     on or off, how many sections are up and how many entries the stream holds
/sidebar log        the path of this project's log of today, and its newest 10 entries

Install

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install sidebar@kilimcininkoroglu-mods

Function hooks are early access. Nothing loads without the flag. To keep it on, add this to ~/.claude/settings.json:

{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }

After installing

  1. Restart Claude Code.
  2. Run /sidebar once. From then on every session opens it until you run /sidebar off.

What it can reach

Validated with claude plugin validate on Claude Code 2.1.278:

❯ types ./types/index.d.ts declares on $: $.sidebar
❯ ./register.tsx hooks: engine.create, session.start, command.run{command=sidebar}, ui.render{component=Pane}, ui.close, turn.complete
❯ ./register.tsx calls: $.clock.now, $.command.register, $.command.run (via pressButton), $.env.get (via openLog), $.fs.list (via logFiles), $.fs.read (via readOrEmpty), $.fs.write, $.session.cwd (via openLog, restoreLog), $.store.get, $.store.set, $.ui.close (via closePane), $.ui.invalidate, $.ui.open (via openPane), $.ui.panes (via closePane), $.ui.resolve
❯ ./register.tsx env writes: nothing
❯ ./register.tsx env reads: HOME

Reach L2, it writes a file.

1. Reads:    the sections other mods hand over, the clock for a stream entry's own time, HOME, the session's directory, and this project's own log files under ~/.claude/sidebar
2. Runs:     the slash command a button names, through $.command.run, on the person's press only
3. Sends:    nothing
4. Persists: in $.store, whether the sidebar is open; in ~/.claude/sidebar, one log file per project and day, holding the stream entries other mods wrote
5. Hostile input: a section comes from another plugin and is read as data: the consumer, key and title are checked, every line and button of another shape is dropped, the text is folded to one line and wrapped to the width, and the counts are capped. A log line is read the same way, so a hand-edited or truncated file loses that line and nothing else.

Limits

  • The log directory was ~/.claude/stream up to 0.7.0 and is ~/.claude/sidebar from 0.8.0, so every mod's own directory carries the mod's name. Nothing is migrated: the old files stay on disk unread. Move them yourself to keep the restore of older days: mv ~/.claude/stream/* ~/.claude/sidebar/.

  • The pane is placed beside the transcript only under the fullscreen layout; otherwise it opens above the prompt.

  • A session that opens the sidebar from the stored choice opens it as the plugin, not as the person: the engine leaves such a pane undrawn below 144 terminal columns, 110 once the person opened that pane themselves. /sidebar in that session places it at any width.

  • Closing the sidebar drops every section and the whole stream. Neither comes back when it is opened again; each mod writes its own at the next update.

  • The stream in the pane holds the session; the log on disk is what survives a restart, and only its newest 10 entries come back.

  • The log is per project and per day. The project is the last part of the directory the session started in, so two checkouts of one repository share a log file.

  • One day's file keeps its newest 500 lines. Nothing removes an old day's file; that is yours to clean.

  • The whole file is rewritten at each entry, because the engine's $.fs has no append. A write that fails is passed over and the pane keeps working.

  • A stream entry's time is the moment the mod wrote it, read from $.clock.now() and drawn in the machine's own time zone. It is not the moment the finding happened, and it does not change afterwards.

  • A line longer than the pane's width is wrapped, at the last space that fits, over at most 4 rows, each row after the first indented by two spaces. A line longer than those 4 rows has its last row cut with . A heading is cut, not wrapped.

  • The stream keeps 20 entries per consumer and 100 in all. Over its own count a mod drops its own oldest entry, never another mod's.

  • A button can only run a slash command. A mod that wants a button must serve a command for it.

  • The pane's scroll window belongs to the engine; this mod adds no scrolling of its own.

Development

make install     # eslint, typescript, typescript-eslint
make lint        # complexity limit 10, fails the build above it
make typecheck   # needs .claude/types/ from /plugin-types
make validate
make test        # claude plugin test