/session-idかボタンで、このスレッドのセッションID(local_…)をコピーする。/merge-to-mainのafter:に渡すため

<img src="docs/images/100percent-vibe-coded.webp" align="right" width="140" alt="100% VIBE-CODED"> <img src="docs/images/spacer.png" align="left" width="140" height="1" alt="">
<img src="docs/images/icon-light.webp" width="128" height="128" alt="Sciaのアイコン">
Scia(シア、/ˈʃi.a/)はストリーミングAPIに対応した、手触りの良いiOS向けMastodonクライアントです。
自分のメインクライアントとして使うために作っています。現在は基本的な機能が完成していないためTestFlightやApp Storeで配布していません。準備ができ次第配布する予定です。
<img src="docs/images/screenshot-timeline.webp" width="260" alt="読んでいる途中に届いた新着が上に溜まっているホームのタイムライン"> <img src="docs/images/screenshot-compose.webp" width="260" alt="タイムラインの上で写真を3枚添付し、ハッシュタグの候補を出した投稿欄"> <img src="docs/images/screenshot-ticker.webp" width="260" alt="ステータスバーの場所に流れたお気に入りの通知">
何を作り何を作らないかはdocs/vision.md、進み具合はdocs/roadmap.mdにあります(未整理)。
必要なもの:
make lintとmake checkで使う)cwebp(READMEのアイコンを描き出すmake readme-iconsと、スクリーンショットを撮るmake readme-screenshotsで使う)brew install xcodegen swiftlint webp
Scia.xcodeprojはコミットしていないので、project.ymlから生成します。
make generate
シミュレーター向けには、そのままビルドできます。
make build
実機で動かすときは、Config/Local.xcconfigに署名のチームを書きます。このファイルはコミットしません。
DEVELOPMENT_TEAM = <チームID>
Packages/SciaCore: UIに依存しないロジック(モデル、APIクライアント、ストリーミング、タイムラインの状態管理)UIKit/AppKit/SwiftUIをimportしないApp/Sources: iOSアプリ(UIKitライフサイクル)project.yml: XcodeGenの定義make test-core
SciaCoreのテストをmacOS上で走らせます。シミュレーターは要りませんが、macOS 26以降が必要です。
make test SIM='iPhone 16 Pro'
アプリのテストをシミュレーターで走らせます。make checkは、lint、SciaCoreのテスト、ビルド、警告の検出をまとめて行います。
ソースコードはMIT No Attribution(MIT-0)で公開しています。ただし、名称とアプリアイコン・ロゴは除きます。
App/Resources/Twemoji.ttfは、Twemojiの絵(Copyright Twitter, Inc and other contributors、CC BY 4.0)からTools/build-twemoji-font.pyで作った書体です。
撮影用のシナリオのアイコンと写真の模様を描くApp/Sources/Debug/Scenario/ScenarioArtwork.swiftは、Boring Avatars(Copyright (c) 2021 boringdesigners)の描き方を移したもので、ファイルの頭に載せたMIT Licenseが適用されます。
「Scia」という名称と、アプリアイコン・ロゴ(App/Resources/AppIcon.icon、App/Resources/AppIconLight.icon、そこから描き出したdocs/images/icon-*.webp)は、MIT-0の対象に含めません。
ソースコードは自由に改変・再配布できますが、改変したものやビルドしたものを配布するときは、Sciaとは別の名称とアイコン・ロゴに差し替えてください。作者が作ったものと見分けられるようにするためです。自分の端末で動かすためにビルドするだけなら、差し替えなくて構いません。
hooks/register.tsx 43 lines1import type { EngineInterface, Register, RenderSurface } from 'claude-code'
2
3// /merge-to-mainのafter:に渡す、このスレッドのセッションIDをコピーする。前提にしたいスレッドで押し、後のスレッドへの合図に貼る
4const copySessionId = async ($: EngineInterface, surface?: RenderSurface) => {
5 const id = await $.env.get('CLAUDE_CODE_HOST_SESSION_ID')
6 if (id === undefined) {
7 $.ui.toast('セッションIDを取得できませんでした(CLAUDE_CODE_HOST_SESSION_IDが未設定)')
8 return
9 }
10 const copied = await $.ui.copy({ text: id, surface })
11 $.ui.toast(copied.isCopied ? `コピーしました: ${id}` : `コピーできませんでした(${copied.reason}): ${id}`)
12}
13
14export const register: Register = on => {
15 on('session.start', async ($, e, next) => {
16 await $.command.register({
17 name: 'session-id',
18 description: 'このスレッドのセッションID(local_…)をコピーする',
19 immediate: true,
20 })
21 return next(e)
22 })
23
24 // textを返すとモデルが読むtranscriptの行になるため、結果はtoastだけで伝える
25 on('command.run', { command: 'session-id' }, async $ => {
26 await copySessionId($)
27 return {}
28 })
29
30 // デスクトップアプリは実行中のターンへ打ったコマンドを順番待ちにするため、入力欄を通らないボタンも置く
31 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
32 const engine = await next(e)
33 const { Box, Button } = $.ui.resolve(e)
34
35 return (
36 <Box flexDirection="row" gap={1}>
37 {engine}
38 <Button key="session-id" label="ID" dimColor onPress={press => copySessionId($, press.surface)} />
39 </Box>
40 )
41 })
42}
43