SLOPSHOPPER

desktop-pet

Claude Desktop の Code タブで、入力欄の上に好きなキャラと吹き出しを出す

newbandguardpromptprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · desktop-pet
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ぷに Done. I made refresh reject expired claims, added an audit … ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
ぷに Done. I made refresh reject expired claims, added an audit …
README

claude-desktop-pet

Claude Desktop の Code タブで、入力欄の上に好きなキャラと吹き出しを出す Claude Code の mod です。Claude の返答の最後のひとことを吹き出しに出し、作業中や返答の中身に合わせて表情を切り替えます。

Anthropic の公式ツールではありません。

見本キャラ「ぷに」の5つの表情

できること

  • 入力欄の上の帯に、キャラの絵と吹き出しを出す
  • 作業中は作業用の表情にして、吹き出しに「書いてます…」「調べてます…」など、使っているツールに合わせたひとことを出す
  • 返答が終わったら、最後の段落を吹き出しに出し、その中の言葉で表情を選ぶ
  • ターミナル版の Claude Code では絵を描けないので、名前とひとことだけを出す
  • Windows では、返答が終わったときと権限の確認待ちのときに、キャラの表情つきで通知を出す(下の「Windows の通知」)

キャラの名前、色、ひとこと、表情を決める言葉、絵は、すべて characters/<キャラ>/ にまとまっています。mod 本体を触らずに、自分のキャラに差し替えられます。

見本のキャラ

フォルダキャラ絵
characters/simple-pet/ぷに。最初から組み込まれている手描きの SVG。MIT
characters/kurato-ai/倉戸あい。AI で絵を作るとどうなるかの見本Magnific で生成。ライセンスの対象外

倉戸あいの5つの表情

使い方

  1. このリポジトリを clone する
  2. 使うキャラを組み込む(最初から simple-pet が組み込まれている)
   node tools/build-character.mjs characters/simple-pet
  1. ~/.claude/settings.json の env に、mod のフォルダを書く
   {
     "env": {
       "CLAUDE_CODE_PLUGIN_DIRS": "C:/path/to/claude-desktop-pet/mod/desktop-pet",
       "CLAUDE_CODE_PLUGIN_DIR_WATCH": "1"
     }
   }

CLAUDE_CODE_PLUGIN_DIR_WATCH は、mod のファイルを直したときに読み込み直すための設定です。

  1. Claude Desktop の Code タブで、新しい session を開く

session の途中で mod を読み込んでも、Desktop には表示されないことがあります。表示されないときは、まず session を開き直してください。

吹き出しを活かすには、Claude に「返答の最後に短いひとことを置く」話し方をしてもらう必要があります。~/.claude/CLAUDE.md に書くテンプレートを examples/CLAUDE.md-snippet.md に置いています。

Windows の通知

Claude Desktop 自身の通知の代わりに、キャラの表情つきの通知を出します。

  • 送り主は「Claude」(アイコンは、PC に入っている Claude Desktop のロゴを読み込んで使う)
  • 1行目(太字)に session のタイトル、2行目にキャラのひとこと、横にそのときの表情
  • 返答が終わったとき、エラーで止まったとき、権限の確認待ちのときに出す
  • 出すかどうかは次のように決める
その session を表示しているかClaude のウィンドウ通知
表示していない(別の session を見ている)前面でも後ろでも出す
表示している後ろ出す
表示している前面出さない

使うときの注意です。

  • Claude Desktop 自身の通知は、Windows の「設定 → システム → 通知」で切ってください。 そのままだと両方が出ます。一覧に「Claude」が2つ並ぶことがあるので、Claude Desktop 本体のほうを切ります
  • Windows PowerShell 5.1(Windows に最初から入っている powershell.exe)で動かします。Windows 以外では何もしません
  • 通知を出さないなら、character.json の notify.enabled を false にします
  • 通知がおかしいときは、mod/desktop-pet/notify/notify.log(mod 側)と ps1-trace.log(スクリプト側)に記録が残っています

自分のキャラを作る

  1. characters/simple-pet/ をコピーして、characters/<自分のキャラ>/ を作る
  2. character.json を書き換える(項目は docs/character-format.md)
  3. svg/ に表情ごとの絵を置く。少なくとも normal.svg と、character.json で名前を出した表情の絵が要る。通知を使うなら、icons/ に表情ごとの PNG(256px ほどの正方形。顔のまわりを切り出すと見やすい)も置く
  4. 絵が大きいときは、1枚 131,072 文字以内に縮める
   node tools/optimize-svg.mjs in.svg characters/<自分のキャラ>/svg/happy.svg
  1. 組み込んで、新しい session を開く
   node tools/build-character.mjs characters/<自分のキャラ>

キャラの話し方の決め方、AI で絵を作る手順、つまずきやすいところは docs/making-of.md にまとめています。

確認

claude plugin validate mod/desktop-pet
claude plugin test mod/desktop-pet

テストは組み込んだキャラの character.json から期待する値を取るので、どのキャラを組み込んでも同じテストで確かめられます。

フォルダの中身

パス中身
mod/desktop-pet/mod 本体。hooks/register.tsx が描画とイベント処理、hooks/character.ts が組み込んだキャラ(生成したファイル)
mod/desktop-pet/notify/Windows の通知を出す notify.ps1 と、組み込んだキャラの表情アイコン
characters/<キャラ>/character.json、表情ごとの svg/ と icons/、preview.png
tools/build-character.mjsキャラのフォルダから hooks/character.ts を作り、アイコンを notify/icons/ にコピーする。足りない絵や大きすぎる絵があれば止まる
tools/optimize-svg.mjsSVG を Claude Code の上限に収まるよう縮める
docs/making-of.mdキャラ作りから mod の完成までの手順と、つまずいたところ
docs/character-format.mdcharacter.json の書き方
examples/CLAUDE.md-snippet.mdキャラの話し方を CLAUDE.md に書くテンプレート

ライセンス

MIT です。ただし characters/kurato-ai/ の絵(svg/、icons/、preview.png)は Magnific で生成したもので、ライセンスの対象外です。Claude のロゴはリポジトリに含めず、通知を出す PC の Claude Desktop から読み込んで使います。

Source 3 files
hooks/register.tsx 186 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Mood } from '../types'
5import { character } from './character'
6
7const line = atom({ plugin: 'desktop-pet', key: 'line' } as const, null)
8const activity = atom({ plugin: 'desktop-pet', key: 'activity' } as const, null)
9const mood = atom({ plugin: 'desktop-pet', key: 'mood' } as const, 'normal')
10
11const greeting = character.userCall === '' ? character.greeting : `${character.userCall}、${character.greeting}`
12const activityRules = character.activities.map(({ tool, line }) => ({ pattern: new RegExp(tool), line }))
13
14// 返答の最後の段落を、吹き出し用のひとことにする。表、箇条書き、コード、出典は飛ばす。
15export const closingLine = (answer: string): string | null => {
16  const paragraphs = answer
17    .split(/\n\s*\n/)
18    .map(p => p.trim())
19    .filter(p => p !== '')
20    .filter(p => !/^(```|\||- |\* |\d+\. |#|Sources:)/.test(p))
21
22  const last = paragraphs.at(-1)
23  // 確認の質問は本文で読んでほしいので、吹き出しには出さない
24  if (last === undefined || /[??]$/.test(last)) {
25    return null
26  }
27
28  const plain = last
29    .replace(/^>\s?/gm, '')
30    .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
31    .replace(/[*_`]/g, '')
32    .replace(/\s*\n\s*/g, ' ')
33
34  return plain.length > character.maxLine ? `${plain.slice(0, character.maxLine - 1)}…` : plain
35}
36
37// ひとことに含まれる言葉から表情を決める。character.json の moods の上にあるものほど優先する。
38export const moodFor = (said: string): Mood =>
39  character.moods.find(({ words }) => words.some(word => said.includes(word)))?.mood ?? 'normal'
40
41// 作業中に吹き出しへ出すひとこと。ツール名から決める。
42export const activityFor = (tool: string): string =>
43  activityRules.find(({ pattern }) => pattern.test(tool))?.line ?? character.thinkingLine
44
45// Desktop は、表示している session にだけ画面をつなぐ(別の session に切り替えると離れる)
46async function isShownOnDesktop($: EngineInterface): Promise<boolean> {
47  try {
48    return (await $.session.surfaces()).includes('desktop')
49  } catch {
50    return false
51  }
52}
53
54// 通知を出そうとした結果を notify/notify.log に残す(最新の 50 行)。記録に失敗しても止めない
55async function logNotify($: EngineInterface, text: string) {
56  try {
57    const path = `${$.plugin.root}/notify/notify.log`
58    const old = (await $.fs.exists(path)) ? (await $.fs.read(path)).split('\n').filter(l => l !== '') : []
59    const stamp = new Date(await $.clock.now()).toISOString()
60    await $.fs.write(path, [...old, `${stamp} ${text}`].slice(-50).join('\n') + '\n')
61  } catch {
62    // 記録できなくても、通知や作業には関係しない
63  }
64}
65
66function notifyArgv($: EngineInterface, args: string[]): string[] {
67  return ['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', `${$.plugin.root}/notify/notify.ps1`, ...args]
68}
69
70// Windows の通知を、キャラのそのときの表情のアイコンで出す。Windows 以外では powershell.exe がなく、何もしない
71async function notify($: EngineInterface, body: string, face: Mood) {
72  if (!character.notify.enabled) {
73    return
74  }
75  let sessionId = '?'
76  try {
77    sessionId = await $.session.id()
78    // この session を表示していなければ、Claude のウィンドウが前面でも通知する。
79    // 表示していれば、Claude のウィンドウが前面かどうかの判断を notify.ps1 に任せる
80    const shown = await isShownOnDesktop($)
81    const force = shown ? [] : ['-Force']
82    const result = await $.process.run(
83      notifyArgv($, ['-Body', body, '-Mood', face, '-SessionId', sessionId, '-FallbackTitle', character.name, ...force]),
84      { timeoutMs: 15000 },
85    )
86    const stderr = result.stderr.trim().replace(/\s+/g, ' ').slice(0, 300)
87    await logNotify($, `${sessionId.slice(0, 8)} shown=${shown} exit=${result.exitCode}${stderr === '' ? '' : ` stderr=${stderr}`}`)
88  } catch (error) {
89    await logNotify($, `${sessionId.slice(0, 8)} failed: ${String(error).slice(0, 300)}`)
90  }
91}
92
93// 送り主「Claude」を先に登録しておく。初めての通知の直前に登録すると、Windows が名前とアイコンを覚え損ねる
94async function registerSender($: EngineInterface) {
95  if (!character.notify.enabled) {
96    return
97  }
98  try {
99    await $.process.run(notifyArgv($, ['-RegisterOnly']), { timeoutMs: 15000 })
100  } catch {
101    // Windows 以外や、登録できなかったときは何もしない
102  }
103}
104
105export const register: Register = on => {
106  on('session.start', async ($, e, next) => {
107    void registerSender($)
108
109    return next(e)
110  })
111
112  on('classic.Notification', async ($, e, next) => {
113    if (e.notification_type.includes('permission')) {
114      const call = character.userCall === '' ? '' : `${character.userCall}、`
115      void notify($, `${call}${character.notify.permissionLine}`, 'worried' in character.svgs ? 'worried' : 'normal')
116    }
117
118    return next(e)
119  })
120
121  on('prompt.submit', async ($, e, next) => {
122    await update($, activity, () => character.thinkingLine)
123
124    return next(e)
125  })
126
127  on('tool.call', async ($, e, next) => {
128    await update($, activity, () => activityFor(e.tool))
129
130    return next(e)
131  })
132
133  on('turn.complete', async ($, e, next) => {
134    if (e.agentId === undefined) {
135      await update($, activity, () => null)
136      if (e.reason === 'answer') {
137        const said = closingLine(e.answer)
138        if (said !== null) {
139          await update($, line, () => said)
140          await update($, mood, () => moodFor(said))
141        }
142        void notify($, said ?? character.notify.doneLine, said === null ? 'normal' : moodFor(said))
143      } else if (e.reason === 'error') {
144        await update($, line, () => character.errorLine)
145        await update($, mood, () => character.errorMood)
146        void notify($, character.errorLine, character.errorMood)
147      }
148    }
149
150    return next(e)
151  })
152
153  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
154    if (e.props.hasSurvey) {
155      return next(e)
156    }
157
158    const doing = e.props.isWorking ? await read($, activity) : null
159    const said = doing ?? (await read($, line)) ?? greeting
160    const face = doing !== null ? character.workingMood : await read($, mood)
161
162    if (e.surface !== 'desktop') {
163      const { Box, Text } = $.ui.resolve(e)
164
165      return (
166        <Box>
167          <Text color={character.color} bold>{character.label} </Text>
168          <Text dimColor={doing !== null} wrap="truncate-end">{said}</Text>
169        </Box>
170      )
171    }
172
173    const { Box, Svg, Text } = $.ui.resolve(e)
174    const svg = character.svgs[face] ?? character.svgs['normal'] ?? ''
175
176    return (
177      <Box flexDirection="row" alignItems="center" gap={1}>
178        <Svg source={svg} alt={`${character.name}(${face})`} width={character.size} height={character.size} />
179        <Box borderStyle="round" borderColor={character.color} paddingX={1} flexShrink={1}>
180          <Text dimColor={doing !== null} wrap="wrap">{said}</Text>
181        </Box>
182      </Box>
183    )
184  })
185}
186
hooks/character.ts 89 lines
1// characters/simple-pet から tools/build-character.mjs で生成。手で編集しない
2import type { Character } from '../types'
3
4export const character: Character = {
5  "name": "ぷに",
6  "label": "ぷに",
7  "userCall": "",
8  "color": "#5fb8a3",
9  "size": 72,
10  "maxLine": 60,
11  "greeting": "今日もいっしょにがんばろうね。",
12  "errorLine": "あわわ、エラーで止まっちゃった…",
13  "thinkingLine": "かんがえ中…",
14  "notify": {
15    "enabled": true,
16    "doneLine": "おわったよ。",
17    "permissionLine": "たしかめてほしいことがあるよ。"
18  },
19  "activities": [
20    {
21      "tool": "^(Edit|Write|NotebookEdit)$",
22      "line": "かきかき中…"
23    },
24    {
25      "tool": "^(Read|Grep|Glob)$",
26      "line": "よみよみ中…"
27    },
28    {
29      "tool": "^(Bash|PowerShell)$",
30      "line": "コマンド実行中…"
31    },
32    {
33      "tool": "^(WebSearch|WebFetch)$",
34      "line": "しらべもの中…"
35    },
36    {
37      "tool": "^(Agent|Workflow)$",
38      "line": "なかまと手分け中…"
39    },
40    {
41      "tool": "^mcp__",
42      "line": "そとのサービスとお話し中…"
43    }
44  ],
45  "workingMood": "serious",
46  "errorMood": "troubled",
47  "moods": [
48    {
49      "mood": "troubled",
50      "words": [
51        "すみません",
52        "ごめん",
53        "失敗",
54        "エラー",
55        "だめでした",
56        "あわわ"
57      ]
58    },
59    {
60      "mood": "worried",
61      "words": [
62        "休んで",
63        "無理しないで",
64        "心配",
65        "大丈夫",
66        "疲れ"
67      ]
68    },
69    {
70      "mood": "happy",
71      "words": [
72        "やった",
73        "よかった",
74        "できました",
75        "できた",
76        "通りました",
77        "ありがとう"
78      ]
79    }
80  ],
81  "svgs": {
82    "happy": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><g stroke=\"#2f3a40\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 52L8 30\" fill=\"none\" stroke-width=\"6\"/><path d=\"M80 52L92 30\" fill=\"none\" stroke-width=\"6\"/><ellipse cx=\"38\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><ellipse cx=\"62\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><path d=\"M50 22C74 22 86 44 86 64C86 80 72 88 50 88C28 88 14 80 14 64C14 44 26 22 50 22Z\" fill=\"#8fd6c4\"/><path d=\"M30 36C34 30 40 27 45 27\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"4\"/><path d=\"M33 55Q38 49 43 55\" fill=\"none\"/><path d=\"M57 55Q62 49 67 55\" fill=\"none\"/><ellipse cx=\"30\" cy=\"64\" rx=\"5\" ry=\"3\" fill=\"#f4a6a6\" stroke=\"none\"/><ellipse cx=\"70\" cy=\"64\" rx=\"5\" ry=\"3\" fill=\"#f4a6a6\" stroke=\"none\"/><path d=\"M43 62Q50 74 57 62Z\" fill=\"#e0605e\"/><path d=\"M14 9L16.1 13.9L21 16L16.1 18.1L14 23L11.9 18.1L7 16L11.9 13.9Z\" fill=\"#ffd34d\" stroke=\"none\"/><path d=\"M86 8L87.8 12.2L92 14L87.8 15.8L86 20L84.2 15.8L80 14L84.2 12.2Z\" fill=\"#ffd34d\" stroke=\"none\"/><path d=\"M50 3L51.5 6.5L55 8L51.5 9.5L50 13L48.5 9.5L45 8L48.5 6.5Z\" fill=\"#ffd34d\" stroke=\"none\"/></g></svg>",
83    "normal": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><g stroke=\"#2f3a40\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><ellipse cx=\"38\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><ellipse cx=\"62\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><path d=\"M50 22C74 22 86 44 86 64C86 80 72 88 50 88C28 88 14 80 14 64C14 44 26 22 50 22Z\" fill=\"#8fd6c4\"/><path d=\"M30 36C34 30 40 27 45 27\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"4\"/><circle cx=\"38\" cy=\"54\" r=\"3.5\" fill=\"#2f3a40\" stroke=\"none\"/><circle cx=\"62\" cy=\"54\" r=\"3.5\" fill=\"#2f3a40\" stroke=\"none\"/><ellipse cx=\"30\" cy=\"64\" rx=\"5\" ry=\"3\" fill=\"#f4a6a6\" stroke=\"none\"/><ellipse cx=\"70\" cy=\"64\" rx=\"5\" ry=\"3\" fill=\"#f4a6a6\" stroke=\"none\"/><path d=\"M45 64Q50 68 55 64\" fill=\"none\"/></g></svg>",
84    "serious": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><g stroke=\"#2f3a40\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><ellipse cx=\"38\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><ellipse cx=\"62\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><path d=\"M50 22C74 22 86 44 86 64C86 80 72 88 50 88C28 88 14 80 14 64C14 44 26 22 50 22Z\" fill=\"#8fd6c4\"/><path d=\"M30 36C34 30 40 27 45 27\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"4\"/><circle cx=\"38\" cy=\"54\" r=\"9\" fill=\"#ffffff\" fill-opacity=\"0.6\"/><circle cx=\"62\" cy=\"54\" r=\"9\" fill=\"#ffffff\" fill-opacity=\"0.6\"/><path d=\"M47 54L53 54\" fill=\"none\"/><circle cx=\"38\" cy=\"55\" r=\"3\" fill=\"#2f3a40\" stroke=\"none\"/><circle cx=\"62\" cy=\"55\" r=\"3\" fill=\"#2f3a40\" stroke=\"none\"/><path d=\"M31 41L44 44\" fill=\"none\"/><path d=\"M69 41L56 44\" fill=\"none\"/><path d=\"M45 68L55 68\" fill=\"none\"/><path d=\"M57 48L61 46\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"2\"/></g></svg>",
85    "troubled": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><g stroke=\"#2f3a40\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M78 50L90 34L82 26\" fill=\"none\" stroke-width=\"6\"/><ellipse cx=\"38\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><ellipse cx=\"62\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><path d=\"M50 22C74 22 86 44 86 64C86 80 72 88 50 88C28 88 14 80 14 64C14 44 26 22 50 22Z\" fill=\"#8fd6c4\"/><path d=\"M30 36C34 30 40 27 45 27\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"4\"/><circle cx=\"38\" cy=\"54\" r=\"3.5\" fill=\"#2f3a40\" stroke=\"none\"/><circle cx=\"62\" cy=\"54\" r=\"3.5\" fill=\"#2f3a40\" stroke=\"none\"/><ellipse cx=\"30\" cy=\"64\" rx=\"5\" ry=\"3\" fill=\"#f4a6a6\" stroke=\"none\"/><ellipse cx=\"70\" cy=\"64\" rx=\"5\" ry=\"3\" fill=\"#f4a6a6\" stroke=\"none\"/><path d=\"M33 46L43 44\" fill=\"none\"/><path d=\"M57 44L67 46\" fill=\"none\"/><path d=\"M42 66Q46 62 50 66Q54 70 58 66\" fill=\"none\"/><path d=\"M24 26Q18 36 24 40Q30 36 24 26Z\" fill=\"#8ec5ff\"/></g></svg>",
86    "worried": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><g stroke=\"#2f3a40\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"rotate(-12 50 80)\"><ellipse cx=\"38\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><ellipse cx=\"62\" cy=\"88\" rx=\"8\" ry=\"4\" fill=\"#6fbfab\"/><path d=\"M50 22C74 22 86 44 86 64C86 80 72 88 50 88C28 88 14 80 14 64C14 44 26 22 50 22Z\" fill=\"#8fd6c4\"/><path d=\"M30 36C34 30 40 27 45 27\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"4\"/><circle cx=\"38\" cy=\"55\" r=\"4.5\" fill=\"#2f3a40\" stroke=\"none\"/><circle cx=\"62\" cy=\"55\" r=\"4.5\" fill=\"#2f3a40\" stroke=\"none\"/><circle cx=\"39.5\" cy=\"53.5\" r=\"1.5\" fill=\"#ffffff\" stroke=\"none\"/><circle cx=\"63.5\" cy=\"53.5\" r=\"1.5\" fill=\"#ffffff\" stroke=\"none\"/><path d=\"M31 47Q36 46 43 42\" fill=\"none\"/><path d=\"M69 47Q64 46 57 42\" fill=\"none\"/><ellipse cx=\"30\" cy=\"64\" rx=\"5\" ry=\"3\" fill=\"#f4a6a6\" stroke=\"none\"/><ellipse cx=\"70\" cy=\"64\" rx=\"5\" ry=\"3\" fill=\"#f4a6a6\" stroke=\"none\"/><path d=\"M46 69Q50 66 54 69\" fill=\"none\"/></g></svg>"
87  }
88}
89
types/index.d.ts 29 lines
1// 表情の名前。characters/<キャラ>/svg/ のファイル名(拡張子なし)。normal は必須
2export type Mood = string
3
4// characters/<キャラ>/character.json の中身に、表情ごとの SVG を足したもの
5export type Character = {
6  name: string
7  label: string
8  userCall: string
9  color: string
10  size: number
11  maxLine: number
12  greeting: string
13  errorLine: string
14  thinkingLine: string
15  // Windows の通知。返答が終わったとき、権限の確認待ちのときに出す
16  notify: { enabled: boolean; doneLine: string; permissionLine: string }
17  activities: { tool: string; line: string }[]
18  workingMood: Mood
19  errorMood: Mood
20  moods: { mood: Mood; words: string[] }[]
21  svgs: Record<Mood, string>
22}
23
24declare module 'claude-code' {
25  interface PluginState {
26    'desktop-pet': { line: string | null; activity: string | null; mood: Mood }
27  }
28}
29