SLOPSHOPPER

scia-session-id

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

newspinnercommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · scia-session-id
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ scia-session-id │ ⏺ Read(src/auth.ts) │ セッションIDを取得できませんでした(CLAUDE_CODE_HOST_SESSION_IDが未設定) │ ⎿ 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 › /session-id ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⟨Claude Code's own drawing⟩ [ ID ]
README

<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

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="ステータスバーの場所に流れたお気に入りの通知">

特徴

  • ストリーミングはもちろん、ストリーミングを見ながら投稿や反応、リンク先の閲覧ができる
  • 通知タブを開かなくても、自分への反応がステータスバーの場所に流れて気づける
  • バックグラウンドからの復帰や再接続のときも取りこぼしや重複なくタイムラインがつながる
  • HIGに沿った美しく滑らかなインターフェイス
  • アニメーションの途中でも操作を受け付けどこで止めても破綻しない
  • 必要なリクエストだけを送りスペックの低いサーバーでも快適に使える

何を作り何を作らないかはdocs/vision.md、進み具合はdocs/roadmap.mdにあります(未整理)。

動作環境

  • iOS, iPadOS 26以降
  • macOS 26以降(Designed for iPadビルド)
  • Mastodon 4.7.3でのみ動作確認

ビルド

必要なもの:

  • Xcode 26以降
  • XcodeGen
  • SwiftLint(make lintとmake checkで使う)
  • WebPの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とは別の名称とアイコン・ロゴに差し替えてください。作者が作ったものと見分けられるようにするためです。自分の端末で動かすためにビルドするだけなら、差し替えなくて構いません。

Source 1 files
hooks/register.tsx 43 lines
1import 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