Takes prompts typed in Kisel's session card and submits them to this session

<img src="docs/img/hero.png" alt="Kisel" width="900">
<a href="https://github.com/Tiutiunov/Kisel/releases/latest"><img src="https://img.shields.io/github/v/release/Tiutiunov/Kisel?label=%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F&color=39C5BB" alt="Последняя версия"></a> <img src="https://img.shields.io/badge/Windows-10%20%2F%2011-E0405A" alt="Windows 10 и 11"> <img src="https://img.shields.io/badge/Qt%206-QML%20%2B%20C%2B%2B20-B9DC6B" alt="Qt 6"> <a href="README.en.md"><img src="https://img.shields.io/badge/README-English-F5A3C0" alt="English"></a>
<h3 align="center"> <a href="https://github.com/Tiutiunov/Kisel/releases/latest">Скачать для Windows</a> </h3>
Kisel это маленькая тёмная таблетка у края экрана, в которой сидят пять моти. Изначально она нужна была для одного: видеть, чем занят Claude Code, не переключаясь в терминал, и отвечать на его «можно я запущу эту команду?» одним кликом. Потом в таблетку переехали плеер, уведомления, монитор железа и замер интернета, и теперь я её просто не закрываю.
Внутри нет ни одной картинки. Персонажи, иконки, искры, даже звуки сделаны кодом.
<img src="docs/img/cast.png" alt="Miku, Rin, Luka, Zundamon и Teto" width="900">
| Чем занята | Какая она | |
|---|---|---|
| Miku | Claude Code: сессии, разрешения, вопросы, чат | Старательная, напевает под нос. Шлёпнешь, надуется |
| Rin | Уведомления Windows: машет табличкой с названием программы | Шумная. От шлепка взрывается |
| Luka | Интернет: пинг, скорость, загрузки, замер | Спокойная. На шлепок смотрит как на идиота |
| Zundamon | Музыка из Spotify | Хвастун, но паникует первым |
| Teto | Процессор, видеокарта, память | Самодовольная, пока её не похвалишь |
Главную выбираешь кликом по скамейке справа. Остальные никуда не деваются: сидят рядом, моргают, иногда корчат рожи.
Кто чем занят, можно переставить в настройках: хоть Тето на Claude Code, хоть Мику на музыку. Характер и любимая еда остаются при персонаже, работа переходит.
288 на 32 пикселя, живёт у любого края экрана. Что в ней показано, зависит от того, что сейчас происходит.
<img src="docs/img/bar.png" alt="Таблетка в разных состояниях" width="900">
Пара вещей, на которые ушло больше времени, чем стоило бы:
<img src="docs/img/bar-side.png" alt="Таблетка у бокового края" width="900">
Клик по главной раскрывает карточку. У каждой своя.
<table> <tr> <td width="50%"><img src="docs/img/card-miku.png" alt="Карточка Мику"><br><b>Miku.</b> Что делает последняя сессия и три кольца лимитов: пять часов, неделя и заполненность контекста. Под ними написано, когда лимит сбросится.</td> <td width="50%"><img src="docs/img/card-permission.png" alt="Запрос разрешения"><br><b>Разрешения.</b> Claude хочет что-то запустить или поправить, и карточка открывается сама. За тебя Kisel не отвечает никогда, только твой клик.</td> </tr> <tr> <td><img src="docs/img/card-question.png" alt="Вопрос от Claude"><br><b>Вопросы.</b> Варианты кнопками, ответ уходит обратно в Claude Code.</td> <td><img src="docs/img/card-done.png" alt="Задача готова"><br><b>Готово.</b> Мику празднует, вокруг летает лук-порей. У каждой своя любимая еда.</td> </tr> <tr> <td><img src="docs/img/card-zunda.png" alt="Плеер"><br><b>Zundamon.</b> Обложка, название, кнопки, перемотка. Берёт данные из системного медиа-сеанса Windows, так что Spotify трогать не надо.</td> <td><img src="docs/img/card-teto.png" alt="Монитор"><br><b>Teto.</b> Нагрузка за последнюю минуту и кнопка, которая чистит память. Сколько освободила, расскажет сама и не без гордости.</td> </tr> <tr> <td colspan="2"><img src="docs/img/card-chat.png" alt="Чат"><br><b>Чат.</b> Пишешь здесь, а уходит это в твою запущенную сессию Claude Code как обычный промпт. Ответы возвращаются сюда же, слева лимиты и модель. Ключ API не нужен.</td> </tr> <tr> <td><img src="docs/img/card-luka.png" alt="Сеть"><br><b>Luka.</b> Пинг и текущая скорость. Кнопка Test меряет канал без захода на speedtest. Чем быстрее интернет, тем она довольнее, на медленном расстраивается.</td> <td><img src="docs/img/card-rin.png" alt="Уведомления"><br><b>Rin.</b> Держит табличку с названием программы, пока не посмотришь. Клик по ней открывает эту программу. Про новую версию Kisel тоже сообщает она.</td> </tr> </table>
<img src="docs/img/card-settings.png" alt="Настройки" width="620">
Настройки разложены по вкладкам: Claude Code, сервисы, персонажи, вид и звук, место, сам Kisel.
Все анимации и сценки можно погонять отдельно: открой в браузере design/miku-editor.html. Там же ползунки для внешности.
Скачай KiselSetup со страницы релизов и запусти. Ставится в профиль пользователя, права администратора не нужны. Нужна Windows 10 (1803+) или 11.
Установщик не подписан, сертификат стоит денег. Поэтому SmartScreen ругнётся: жми «Подробнее», потом «Выполнить в любом случае».
Обновляется сам. Раз в сутки спрашивает GitHub, не вышло ли чего, и если вышло, Рин поднимает табличку с номером версии. Дальше одна кнопка в настройках: скачает, сверит контрольную сумму, поставит и перезапустится. Проверку можно выключить там же.
Удаляется через «Приложения» в параметрах Windows. Хуки из настроек Claude Code при этом убираются, твои настройки остаются.
Карточка → шестерёнка → Connect Claude Code. Kisel покажет, что именно допишет в ~/.claude/settings.json, и не тронет файл, пока не нажмёшь. Перед записью делает копию.
Дальше Claude Code на каждое событие дёргает крошечную kisel-hook, а она передаёт его Киселю. Если Kisel не запущен, хук молча выходит и Claude Code ничего не замечает. Если хуки пропали или устарели после обновления, Kisel скажет об этом и предложит починить.
Там же, в настройках, кнопка Install mods. Она ставит в Claude Code два плагина, которые идут вместе с Kisel:
kisel-prompts связывает сессию с чатом Kisel: промпты туда, ответы и лимиты обратно.cache-band показывает над полем ввода в Claude Code, сколько токенов лежит в кеше.Kisel для этого запускает родные команды claude plugin ... и показывает их заранее, так что нужен установленный claude в командной строке. Моды загружаются в сессиях, запущенных после установки.
По желанию Kisel показывает в профиле Discord «Работает с Claude Code» с таймером. Выключено, пока не включишь в настройках. Название сессии показывается, только если включить и это. Общается он только с Discord на твоём компьютере.
Секреты (токен GitHub для виджета) лежат в диспетчере учётных данных Windows, в файлы не пишутся.
Телеметрии нет. В сеть Kisel ходит за виджетом GitHub, пингует 1.1.1.1 для Луки, раз в сутки проверяет обновления и по кнопке меряет скорость через Cloudflare. Всё это отключается или не запускается само. Чат идёт через твою локальную сессию Claude Code, а не через сеть Kisel.
Подробно, по пунктам: docs/PRIVACY.md. Условия использования: docs/TERMS.md.
Из уведомлений читается только название программы и количество. Текст не читается.
Настройки лежат в %LOCALAPPDATA%\kisel\kisel.conf, обычный текстовый файл.
Нужны Visual Studio 2022 с C++, CMake, Ninja и Qt 6.5+ для MSVC. Из «x64 Native Tools Command Prompt»:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=C:\Qt\6.10.3\msvc2022_64
cmake --build build
cmake --install build --prefix dist
dist\bin\kisel.exe
Установщик: cmake --build build --target installer.
Без Claude Code можно посмотреть так: kisel.exe --demo разыграет выдуманную сессию. kisel.exe --speed-test просто померит интернет и напечатает результат. Как всё устроено внутри, написано в docs/ARCHITECTURE.md.
Kisel начал Антон Куклин как спутник для KDE Plasma. Эта ветка — версия для Windows, которая с тех пор обросла всем остальным. Форму персонажей подсмотрели у Mochi из Coucou.
Код под MIT. Это касается только кода.
Персонажи не мои. Kisel это фанатская некоммерческая штука, с правообладателями она не связана и ими не одобрена. Нарисовано всё своим кодом и в своём стиле, чужих иллюстраций, голосов и музыки в проекте нет.
この作品はピアプロ・キャラクター・ライセンスに基づいてクリプトン・フューチャー・メディア株式会社のキャラクター「初音ミク」「鏡音リン」「巡音ルカ」を描いたものです。
Kisel не имеет отношения к Anthropic. Claude и Claude Code это их товарные знаки, и маленький оранжевый зверёк, которого Мику обнимает за работой, тоже их персонаж из Claude Code.
Внутри Qt 6 по LGPL v3, подключён динамически: библиотеки лежат рядом с kisel.exe как есть, их можно заменить своей сборкой. Исходники Qt на download.qt.io. Тексты лицензий ставятся в папку licenses, остальное перечислено в THIRD-PARTY-NOTICES.md.
hooks/register.ts 105 lines1import type { Register } from 'claude-code'
2
3import { clean, folder, pending, reply } from './inbox'
4
5export const register: Register = on => {
6 let stop: (() => void) | undefined
7 let inbox: string | undefined // this session's folder, once known
8 let said = 0
9
10 // What Claude says in the main conversation goes back to Kisel, one file per reply,
11 // so its chat shows both sides. Only while Kisel has made the folder (it is running).
12 on('turn.step', async function* ($, e, next) {
13 const result = yield* next(e)
14 const text = e.agentId === undefined ? reply(result.answer) : null
15
16 if (text !== null && inbox !== undefined) {
17 try {
18 if (await $.fs.exists(inbox)) {
19 await $.fs.write(`${inbox}/${await $.clock.now()}-${said++}.reply`, text)
20 }
21 } catch {
22 // Kisel's folder went away mid-write: nothing to tell
23 }
24 }
25
26 return result
27 })
28
29 on('session.start', async ($, e, next) => {
30 const result = await next(e)
31 stop?.()
32
33 const home = await $.env.get('USERPROFILE')
34 if (!home) {
35 return result // (not Windows, or no Kisel folder to share)
36 }
37
38 const dir = folder(home, await $.session.id())
39 inbox = dir
40 const done = new Set<string>() // handed over by this run (a receipt may fail to write)
41 let isBusy = false
42 let tick = 0
43 let lastReport = ''
44
45 const look = async (): Promise<void> => {
46 if (isBusy) {
47 return
48 }
49
50 isBusy = true
51
52 try {
53 // Kisel makes the folder when it first sees the session; until then there is nothing to do.
54 if (!(await $.fs.exists(dir))) {
55 return
56 }
57
58 if (tick++ % 2 === 0) {
59 await $.fs.write(`${dir}/alive`, String(await $.clock.now()))
60 }
61
62 // every ten looks: what the session knows of the account's limits, for the rings in Kisel's chat
63 if (tick % 10 === 1) {
64 const usage = await $.session.usage()
65 const report = JSON.stringify({
66 model: await $.session.model(),
67 limits: usage.rateLimits.map(l => ({ kind: l.kind, used: l.percentUsed, resetsAt: l.resetsAt ?? '' })),
68 context:
69 usage.context.percent === undefined
70 ? undefined
71 : { used: usage.context.percent, tokens: usage.context.tokens ?? 0, window: usage.context.window },
72 })
73
74 if (report !== lastReport) {
75 await $.fs.write(`${dir}/limits`, report)
76 lastReport = report
77 }
78 }
79
80 const names = (await $.fs.list(dir)).map(entry => entry.name)
81
82 for (const name of pending(names, done)) {
83 const text = clean(await $.fs.read(`${dir}/${name}.prompt`))
84 done.add(name)
85 await $.fs.write(`${dir}/${name}.taken`, '')
86
87 if (text !== null) {
88 // (the person's own words, typed in Kisel: read bare, in a turn of its own once the session is idle)
89 void $.prompt.submit({ text, asUser: true })
90 }
91 }
92 } catch {
93 // a file half-way through being replaced, or the folder just removed: the next look sees it whole
94 } finally {
95 isBusy = false
96 }
97 }
98
99 const timer = $.clock.every(1000, () => void look())
100 stop = () => timer.cancel()
101
102 return result
103 })
104}
105hooks/inbox.ts 53 lines1// The folder Kisel and this mod share for one session (Kisel's side is src/core/PromptRelay):
2// alive this mod's heartbeat
3// <n>.prompt a prompt Kisel wrote
4// <n>.taken this mod's receipt for it
5
6//
7// It is in the home folder, not under AppData: the Claude desktop app is a packaged app,
8// and Windows keeps what it (and this mod inside it) writes under AppData in a private
9// copy that Kisel never sees. (Kisel names the same folder in src/core/Paths.h.)
10
11const BACKSLASH = String.fromCharCode(92)
12
13export const folder = (home: string, sessionId: string): string => {
14 let base = home.split(BACKSLASH).join('/')
15
16 while (base.endsWith('/')) {
17 base = base.slice(0, -1)
18 }
19
20 return `${base}/.kisel/inbox/${sessionId}`
21}
22
23// The prompts still to be handed over, oldest first: a `.prompt` without its `.taken`,
24// not handed over already by this run. (Their names begin with the time Kisel wrote them.)
25export const pending = (names: readonly string[], done: ReadonlySet<string>): string[] => {
26 const taken = new Set(names.filter(n => n.endsWith('.taken')).map(n => n.slice(0, -'.taken'.length)))
27
28 return names
29 .filter(n => n.endsWith('.prompt'))
30 .map(n => n.slice(0, -'.prompt'.length))
31 .filter(n => !taken.has(n) && !done.has(n))
32 .sort()
33}
34
35// What Claude said, for Kisel's chat: nothing when it said nothing (a step that only
36// called tools), and cut where a bubble would stop being a bubble.
37export const reply = (answer: string): string | null => {
38 const body = answer.trim()
39
40 if (body === '') {
41 return null
42 }
43
44 return body.length > 12000 ? `${body.slice(0, 12000)}\n…` : body
45}
46
47// A prompt is text a person typed into a one-line field: bounded, and never empty.
48export const clean = (text: string): string | null => {
49 const body = text.trim()
50
51 return body === '' || body.length > 20000 ? null : body
52}
53