20260902_Deno Desktopで 最強のDiffツールを作ろう

142 Views

September 04, 26

スライド概要

toranoana.deno#26での登壇資料です。

profile-image

虎の穴ラボ株式会社は、主にとらのあな関連サービスのシステム開発を専門に担う、エンジニアの会社です。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

Deno Desktopで 最強のDiffツールを作ろう toranoana.deno | 虎の穴ラボ株式会社 藤原 佳顕 Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 1

2.

本日の流れ 1. 自己紹介 2. Diffツール自作の動機 3. 作ったものの概要 4. Denoの活用について 5. まとめと今後 Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 2

3.

自己紹介ページ (藤原) 藤原 佳顕(ふじわら よしあき) : yoshiaki fujiwara、8年目 ‣ ‣ 新規事業担当( Fantia、Creatia)、アーキテクトチーム (CSIRTも)、AI推進チーム 出身 ‣ 大学:情報系(数学より) ‣ 前職:独立系ソフトウェア会社、主に GISとWeb、ライブラリ開発 ‣ TypeScript、Ruby on Rails、C#、C++、React、Vue、Angular ‣ 入社理由 ‣ ‣ 自分がスキルアップできそうな場所に行きたい オタク系の話ができるところに行きたい 好きなモノ ‣ ‣ ‣ シューティングゲーム、格闘ゲーム、アトラスのゲーム SF小説 プログラミング Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 3

4.

Diffツール自作の動機 ● ● ● コードを書く速度は爆速になっている ○ Claude Code, Cursor, Aider が一瞬で数百行のパッチを大量生成 ○ 差分の確認をしている時間が長い ○ 直接PR作ってGitHub上などで確認することもあるが、手元で確認できるツールが欲 しい ○ 個人的にはWinMergeが好きだが、Macにはない 加えて、AI生成差分に適応したものにしたい: ○ 1. 大量のノイズ差分: インデント変更・コメント書き換えで重要ロジックが埋もれる ○ 2. サイレントな破壊的変更: シグネチャ変更、try-catch消去、APIキー直書き ○ 3. プロンプトコンテキストの喪失: 「この差分、何をお願いして作らせたんだっけ?」 結論: 「AIのコードを安全・快適に審査する専用Diffツール」を作りたい Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 4

5.

作ったものの概要 ● ● ● 既存Diffツールのペイン: ○ WinMerge: 爆速&最強キーバインドだが Windows専用 ○ VS Code 内蔵Diff: ディレクトリ比較が重い。エディタ系の機能は別にいらない ○ git diff (CLI): リッチな3-Wayマージやインタラクティブ編集が難しい 求めたもの: WinMerge級のキー操作 × クロスプラットフォーム × AIレビュー支援 それを作る土台に Deno を選んだ理由(先出し): ○ 大きい点:Deno2.9でDeno Desktopが追加されたので使いたかった ○ ✓ deno desktop で TypeScriptだけ でGUIアプリが書ける(Rust不要・Node不要) ○ ✓ ファイルI/O・プロセス起動・HTTPサーバーが ランタイム標準API で完結 ○ ✓ fmt / lint / check / test が同梱 — ツール開発の立ち上がりが最速 Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 5

6.

作ったものの概要 CLIから一発起動: ● $ diffrex base.ts target.ts --prompt "JWT認証を追加" ● diffrexはアプリの名前 主な特徴: ● 🖥 Deno Desktop製: 軽量WebViewで高速起動、deno desktop -o で配布 ● 🧠 AI Context Header / 🔇 Noise Filter / ⚠ Risk Heuristics ● ⌨ One-key Review: A(承認)・R(拒否)・E(編集) ● 📁 ディレクトリ比較 / 🔀 3-Wayマージ ● 🖼 画像・JSON・CSV Diff / 🌲 AST Semantic Diff Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 6

7.

作ったものの概要 Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 7

8.

作ったものの概要 構成要素のほぼすべてが Denoランタイム標準 API + JSR @std: ● CLI引数解析: jsr:@std/cli ● ディレクトリ走査 : jsr:@std/fs + Deno.stat ● 静的解析: web-tree-sitter (WASM) ● 安全なファイル I/O: Deno.writeFile / Deno.rename ● OSネイティブ連携 : Deno.Command ● 配信 & IPC: Deno.serve (HTTP + WebSocket) ● ウィンドウ: Deno.BrowserWindow npmから借りたのは Preact と CodeMirror だけ 外部プロセスもフレームワークも無し [CLI 実行: diffrex src/ dst/ --prompt "..."] │ ▼ ┌─────────────────────────────────────────── ───┐ │ Deno Backend(Deno標準API + JSR @std) │ │ ├─ CLI引数解析 ....... jsr:@std/cli │ │ ├─ ディレクトリ走査 .. jsr:@std/fs+Deno.stat│ │ ├─ 静的解析 .......... web-tree-sitter(WASM)│ │ ├─ ファイルI/O ....... Deno.writeFile/rename│ │ ├─ OS連携 ............ Deno.Command │ │ └─ 配信 & IPC ........ Deno.serve (HTTP+WS) │ └─────────────────────┬───────────────────── ───┘ │ WebSocket IPC (型付きJSON) ▼ ┌─────────────────────────────────────────── ───┐ │ Deno.BrowserWindow(OS標準WebView) │ │ ├─ Model: DiffSessionModel (deno testable) │ │ ├─ View: Preact JSX + CodeMirror 6 MergeView│ │ └─ Controller: キーバインド調停 / マージ実行 │ └─────────────────────────────────────────── ───┘ Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 8

9.

Denoの活用について なぜ Deno でデスクトップアプリを作るのか? ● ● ● Electron / Tauri と比較したときの圧倒的気軽さ : ○ vs Electron: node_modulesの巨大さ・数百 MBの配布サイズ・ビルド待ちがゼロ ○ vs Tauri: 高速だがRustツールチェーンの学習と環境構築が必要 Deno Desktop (Deno v2.9+): ○ ✓ Rust も Node もインストール不要( Deno本体さえあれば一発起動) ○ ✓ TypeScriptがビルド設定ゼロでそのまま動く ○ ✓ OS標準WebView(Windows: WebView2, macOS: WebKit)で超軽量・省メモリ Denoランタイムが提供する強力な道具箱 : ○ 🛠 オールインワン : Linter, Formatter, Test, Check が最初から付属 ○ 🔒 明示的パーミッション : 読み書きパスや実行コマンドの制限で安全性を担保 ○ 📦 JSR / npm / WASM の併用: Web標準APIとエコシステムの全部取り Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 9

10.
[beta]
Denoの活用について :WebSocketの活用

●

●

●

deno desktop による起動部分 (Deno v2.9+):
○ Deno.BrowserWindow API が提供される
○ 画面立ち上げと同時にローカルWebSocket
サーバーを生成
Web標準 Import Attributes (with { type: 'text' }):
○ DenoがネイティブサポートするWeb標準仕様
○ HTML, CSS, JSをバンドラーなしでそのままイ
ンポート可能
○ webpack等のアセットローダー設定が一切不
要
Deno.serve もランタイム標準:
○ ポート 0 指定で安全な空きポート自動割当
○ デスクトップアプリとする場合どのポートが使わ
れているかわからないため必要

// src/desktop/window.ts (抜粋)
import indexHtml from "../ui/index.html" with { type: "text" };
import stylesCss from "../ui/styles.css" with { type: "text" };
import bundleJs from "../ui/bundle.js" with { type: "text" };

export async function launchDesktopApp(session: DiffSessionData) {
// ① ローカルに一時的 WebSocketサーバーをポート 0 (自動割当 ) で起動
const server = Deno.serve({ port: 0 }, handleHttpAndWs);
const port = (server.addr as Deno.NetAddr).port;

// ② Deno Desktop ウィンドウを生成
// @ts-ignore: Deno.BrowserWindow is available under deno desktop
const win = new Deno.BrowserWindow({
title: "Diffrex",
width: 1400,
height: 900,
});
win.loadURL(`http://localhost:${port}/?wsPort=${port}`);
}

Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved.

10

11.

Denoの活用について :WebSocketの活用 ● Deno Backend(フルアクセス): ○ ファイル読み書き / ディレクトリ走査 ○ Diff計算 / Noise / Risk解析 ○ OSコマンド実行 / プロセス終了コード管理 ○ ただし画面を描けない ● WebView UI(サンドボックス): ○ DOM / CodeMirror 6 描画、キー・マウス入力 ○ ローカルファイルへの直接アクセスは不可❌ ■ → 両者の能力を交換する専用線 = WebSocket Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 11

12.

Denoの活用について :WebSocketの活用 ● ● ● 具体的なWebSocketの目的: ○ 1. セッションデータの供給: 起動時のDiff結果・ファイルツリーをUIへプッシュ ○ 2. オンデマンド処理の依頼: 「このファイルのDiffをちょうだい」(重い計算の遅延実行) ○ 3. 特権操作の代行依頼: ファイル保存・ダイアログ表示・プロセス終了(Exit Code) ○ 4. 状態の即時通知: 保存結果・エラーを「サーバー側から」UIへ返す なぜ HTTP(fetch)だけではダメ?: ○ 双方向性: dialog:result のように「バックエンド側の完了イベント」をプッシュしたい ○ 接続 = 生存監視: WebSocketの切断でUIが「バックエンド喪失」を即検知できる ○ 順序保証された1本の会話: セッションを持つアプリでは常時接続の方が単純 なぜ独自プロトコルではなくWebSocket?: ○ WebView側で追加ライブラリなしに使える唯一のWeb標準双方向通信 ○ Deno側も Deno.upgradeWebSocket がランタイム標準 — 依存ゼロで両端が揃う Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 12

13.
[beta]
Denoの活用について :WebSocketの活用
たった1つの Deno.serve ハンドラの中に:
① /ws → Deno.upgradeWebSocket でWS接続へ昇格
② / → index.html 配信
③ /bundle.js → UIバンドル配信
●
Expressもwsパッケージも不要!
●
接続管理:
○
接続中ソケットは Set<WebSocket> で管理
○
broadcast() で全クライアントへ一斉送信
○
readyState チェックで安全に送信
●
安全なポート設計:
○
ポート0 = OSに空きポートを自動割当させる
○
127.0.0.1 バインド = 外部マシンから接続不可

const handler = (req: Request): Response => {
const url = new URL(req.url);
if (url.pathname === "/ws") { // ① WSアップグレード
const { socket, response } = Deno.upgradeWebSocket(req);
socket.onopen = () => activeSockets.add(socket);
socket.onmessage = async (event) => {
const parsed = parseIncomingMessage(String(event.data));
if (!parsed) return;
switch (parsed.type) { /* ui:ready, save:request... */ }
};
socket.onclose = () => activeSockets.delete(socket);
return response;
}
if (url.pathname === "/") { /* ② index.html 配信 */ }
if (url.pathname === "/bundle.js") { /* ③ JS配信 */ }
return new Response("Not Found", { status: 404 });
};

Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved.

13

14.
[beta]
Denoの活用について
Deno.Command でゼロ依存ネイティブダイアログ

export async function openDirectoryDialog(title = "フォルダを選択 "): Promise<string |
null> {
if (Deno.build.os === "windows") { // OS判定もランタイム標準

課題: よくあるデスクトップアプリのようにエクスプローラーやファイ
ンダーでファイルやディレクトリを選びたい
解決: Deno.Command で各OS標準のシェル・コマンドを呼び出
し
●
Windows: PowerShell + WinForms
FolderBrowserDialog
●
macOS: osascript (AppleScript)
●
Linux: zenity

const psScript = `
Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
$dialog.Description = "${title}"
$topForm = New-Object System.Windows.Forms.Form; $topForm.TopMost = $true
if ($dialog.ShowDialog($topForm) -eq [System.Windows.Forms.DialogResult]::OK) {
Write-Output $dialog.SelectedPath
}
`;

Denoの効きどころ:
●
OS判定も Deno.build.os でランタイム標準
●
--allow-run で実行可能コマンドを絞れる(パーミッションモ
デル)
●
追加のnpmパッケージ・ネイティブアドオンはゼロ

const cmd = new Deno.Command("powershell", {
args: ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command",
psScript],
stdout: "piped",
});
const { stdout } = await cmd.output();

Windowsでの工夫:
●
-WindowStyle Hidden で黒いコンソール窓を非表示化
●
TopMost = $true でダイアログが背面に隠れるのを防止

const path = new TextDecoder().decode(stdout).trim();
return path.length > 0 ? path : null;
}
// macOS は osascript、Linux は zenity で実行 ...
}

Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved.

14

15.

Denoの活用について :Diff表示について 基本のDiff表示については CodeMirrorを利用 ● CodeMirrorのDiffは文字オフセット基準・エ ディタ内専用 → バックエンド解析には使えな い ● Noise/Risk解析や3-Wayマージは 「行の集合としての Hunk」を入力にしたい ● Denoバックエンドで動く依存ゼロの純関数な ら、deno test で網羅的に検証できる どちらも心臓部は同じ「 Myers Diffアルゴリズム」 ┌───────────────────────────────────────────┐ │ 系統①: 表示用 Diff │ │ CodeMirror 6 @codemirror/merge │ │ │ │ 目的: エディタ内の差分ハイライト・ │ │ 左右アライメント │ │ 流れ: Prefix/Suffix刈り取り │ │ → 文字単位Myers → presentableDiff │ │ (5,000行中数行の変更なら99%を探索前に │ │ 除外 → 数十msで完了) │ └───────────────────────────────────────────┘ ┌───────────────────────────────────────────┐ │ 系統②: 解析用 Diff │ │ 自前実装 src/core/diff.ts (Deno側) │ │ │ │ 目的: Noise判定・Risk解析・3-Wayマージの │ │ 「意味の単位」を作る │ │ 流れ: splitLines │ │ → computeLineDiff (行単位Myers) │ │ → extractHunks (Hunk化) │ │ → 解析エンジンへ │ └───────────────────────────────────────────┘ Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 15

16.

Denoの活用について :Diff表示について Myers Diffについて 問題設定: 配列Aを配列Bに変換する 最小の編集手順( delete/insertの列)を求める 編集グラフ: 横軸にA、縦軸にBを置いた グリッド上の経路探索と等価 Myersの貪欲法(1986): - 編集コスト d = 0, 1, 2, ... を1つずつ増やし ながら、各対角線 k = x - y 上で 「どこまで到達できるか」 (frontier) だけを記録 - 到達点から貪欲に対角線を滑り降りる (一致行はタダで進める) - ゴール到達時の d が編集距離 - 計算量 O((N+M)・D) 嬉しい性質: 「差分が少ないほど速い」 → Diffツールの実利用(大半は小さな変更)に最適 B: c a t ▶ A ┌─┬─┬─┬─┐ c │↘│ │ │ │ → 右移動 = Aの行を delete a │ │↘│ │ │ ↓ 下移動 = Bの行を insert r │ │ │ │ │ ↘ 対角線 = 行が一致 t │ │ │ │↘│ (equal, コスト0!) ▼ └─┴─┴─┴─┘ ゴール(右下)までの経路のうち 「斜めを最大限使う」経路 = 最短のDiff d=0: 斜めだけでどこまで行ける? d=1: 削除or挿入を1回使うとどこまで? d=2: 2回使うと…(ゴールに着いたら終了) D = 差分の量。変更が少ないファイルほど 探索が早く終わる → AIレビューの現場と好相性 Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 16

17.
[beta]
Denoの活用について :Diff表示について

// src/core/analysis/noise.ts (抜粋)
export function analyzeHunkNoise(
leftLines: string[],

Diff表示上の工夫
🔇 ノイズ差分の自動折りたたみ :
●
AIが勝手に変えたインデント・コメントだけの差
分を自動検知
● 空白正規化 + コメント除去後に一致すれば「ノイ
ズ」→ 起動時に折りたたみ
⚠ 破壊的変更を暴く Risk Heuristics:
● 🚨 Danger: 10行以上の連続コード削除(実装
の丸ごと蒸発)
● 🚨 Danger: 関数 / クラス / 型のシグネチャ変
更(後方互換性破壊)
● ⚠ Warning: try-catch や .catch() の削除(エ
ラー握りつぶし)
● ⚠ Warning: sk-..., ghp_..., AKIA... の追加
(シークレット混入)
Denoポイント:
● 解析エンジンは純関数の集まり → deno test で
網羅的にテスト

rightLines: string[],
): NoiseAnalysisResult {
// ① 空白・インデント・コメントを除去して正規化
const normLeft = leftLines.map(stripCommentsAndWhitespace).join("\n");
const normRight = rightLines.map(stripCommentsAndWhitespace).join("\n");

// ② 正規化後の一致判定
if (normLeft === normRight) {
return {
isNoise: true,
category: "whitespace_or_comment",
foldByDefault: true, // 起動時に自動折りたたみ!
};
}

return { isNoise: false, category: "logic_change", foldByDefault: false };
}

Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved.

17

18.
[beta]
// src/core/analysis/ast/ast_parser.ts (抜粋・簡略 )

Denoの活用について :Diff表示について

import Parser from "web-tree-sitter";

// npm: 指定で Denoが直接解決

export function detectLanguageFromFilename(filename: string) {

●

●

●

●

行ベース Diffの限界: AIリファクタで最頻出の2パターンが「大
量の削除+追加」に見えてしまう
○
関数を末尾に移動しただけ → Myers的には全行削
除+全行追加
○
変数を x → deltaX に一括リネームしただけ → 触れ
た行が全部差分
Tree-sitter とは: GitHub発のインクリメンタル・エラー耐性
パーサー基盤
○
文法ごとにパーサーをWASMバイナリ として配布 →
どのランタイムでも同じ挙動
Diffrexでの構成( Denoの効きどころ) :
○
npm:web-tree-sitter を deno.json の1行で導入
○
6言語(TypeScript / JavaScript / Python / Rust /
Go / Ruby)のWASMを vendor/tree-sitter/ にオフラ
イン同梱
○
deno task setup:wasmsで一括ダウンロード — 実行
時はネットワーク不要
Graceful Fallback(段階的縮退) :
○
WASM不在・未対応言語・パース失敗 → すべて
null を返して行 Diffのみで動作継続

if (lower.endsWith(".ts") || lower.endsWith(".tsx")) return "typescript";
if (lower.endsWith(".py")) return "python";
// .js / .rs / .go / .rb ... 6言語に対応。未対応は

null

}

export async function loadLanguage(lang: SupportedLanguage) {
if (loadedLanguages.has(lang)) return loadedLanguages.get(lang)!; // キャッシュ
// WASMをローカルから読込

const bytes = await Deno.readFile(

join(Deno.cwd(), "vendor", "tree-sitter", `tree-sitter-${lang}.wasm`));
const language = await Parser.Language.load(bytes); // WASMバイナリをロード
loadedLanguages.set(lang, language);
return language;
}

export async function parseCodeToAst(code: string, langOrFilename: string) {
const language = await loadLanguage(lang);
if (!language) return null;

// ★ Graceful Fallback: 落とさず縮退

const parser = new Parser();
parser.setLanguage(language);
return { tree: parser.parse(code), parser, language: lang };
}

Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved.

18

19.

Denoの活用について :Diff表示について ● ● ● Step 1: `extractBlockNodes` — ASTから関数・クラス・メソッドを抽出し、 3つの指紋を付与 ○ normalizedText: コメント・空白を除去した本文 (内容が同じか ) ○ syntaxSignature: ノード型名だけを深さ 8まで列挙(識別子名を含まない )(構造が同じか ) ○ identifiers: 出現順の識別子リスト (名前がどう変わったか ) Move検出: `normalizedText` が一致 & 行位置が異なるペア → 「移動しただけ」 ○ findMoveForHunk`がHunkの行範囲と突き合わせ、該当 Hunkに **[Move]** バッジを注釈 Rename検出: `syntaxSignature` 一致 & 識別子数一致 → 置換マップを検証 Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 19

20.
[beta]
// src/core/analysis/ast/rename_detector.ts (抜粋・簡略)
export function detectBlockRename(leftNode: ASTBlockNode, rightNode: ASTBlockNode) {
// ① 構文構造が完全一致しているか(識別子名は見ない)
if (leftNode.syntaxSignature !== rightNode.syntaxSignature) return null;
// ② 識別子の個数が一致しているか
if (leftNode.identifiers.length !== rightNode.identifiers.length) return null;

// ③ 出現位置の対応で置換マップを構築 + 双方向の一貫性チェック
const renameMap = {}; const reverseMap = {}; let renameCount = 0;
for (let i = 0; i < leftNode.identifiers.length; i++) {
const l = leftNode.identifiers[i], r = rightNode.identifiers[i];
if (l === r) continue;
if (renameMap[l] && renameMap[l] !== r) return null;

// 非一貫 → ロジック変更かも

if (reverseMap[r] && reverseMap[r] !== l) return null; // 逆写像も検証
renameMap[l] = r; reverseMap[r] = l; renameCount++;
}
if (renameCount === 0 || renameCount > 5) return null;

// 過剰リネームは疑う

return { renameMap, summaryTag: `[Rename] x -> deltaX, y -> deltaY` };
}
Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved.

20

21.

Denoの活用について : GUI MVC UI部分の実装は MVCで実装 ReactもReduxも使わず、50行のピュアTS Observerで 状態管理: ● Modelが能動的なSubjectとしてViewへ通知す る三角形構造 なぜMVCなのか: 1. CodeMirror 6 の内部Stateと衝突しない ○ 仮想DOMの全体再描画を回避 2. 外部依存ゼロ — RxJS / MobX 不要 3. deno test でそのままテストできる ○ ModelとControllerはDOM非依存 ○ ブラウザも jsdom も不要で高速に単体テ スト! ○ deno task test 一発、セットアップゼロ 【現代の Web MVC(リクエスト-レスポンス型)】 ユーザー ─(HTTP Req)─► Controller ─► Model(受動的/DB) ─► View(HTML生成) ─► 破棄 ※ 一方向に流れて終わり。リクエスト毎に使い捨て 【原初GUI MVC(Smalltalk-80 / 1970年代の デスクトップGUIの原型)】 ユーザー入力(キー・マウス・IPC) │ ▼ Controller ──(更新指示)──► Model │ (Active Domain State) │ │ │ (エディタ直接操作) ▼ Observer Patternで │ │ 自己通知 └──────────────────► View(永続的 / Observer) ※ 三角形の循環フロー。全員が生き続け、 Modelが変更を「自分から」知らせる Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 21

22.
[beta]
// src/ui/model/observable.ts — 外部依存ゼロのリアクティブ基盤
export class Observable<T> {

Denoの活用について : GUI MVC

Observable(全51行・依存ゼロ) :
● subscribe が購読解除関数を返す
○ クリーンアップが素直に書ける
● notify は try-catch で1つのObserverの失敗が全
体を止めないよう防御

private observers: Set<(value: T) => void> = new Set();

subscribe(observer: (value: T) => void): () => void {
this.observers.add(observer);
return () => this.observers.delete(observer);
}

notify(value: T): void {
for (const o of this.observers) {

Active Model = 状態 + ロジック + 自己通知:
● 状態変更メソッドの最後で必ず notify(this)

try { o(value); } catch (e) { console.error(e); }
}
}

useModel = 原初MVCとPreactの橋渡し:
● 通知が来たらダミー stateをインクリメントして再描
画をトリガーするだけ

}

// Model が能動的に View へ通知する( Smalltalk-80 スタイル)
export class DiffSessionModel extends Observable<DiffSessionModel> {
private activeHunkIndex = 0;

Viewは「参照して描くだけ」 :
● const model = useModel(sessionModel);

selectNextHunk(): void {
this.activeHunkIndex++;

通知粒度は「Model全体」と割り切り :
● 差分検知はPreactの仮想DOMに任せて実装を最
小化

this.notify(this); // 変更された自分自身を Viewへ能動通知!
}
}

Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved.

22

23.
[beta]
Denoでデスクトップ GUIを作って得られた知見

⚠ ハマりどころ & 工夫した点

⭕ ここが最高だった(メリット)
●
●
●

●
●

Electron特有の起動時のモッサリ感が皆無
clone して deno task dev:desktop だけで
開発開始
TextEncoder, crypto.randomUUID,
ReadableStream, WebSocket がバックエン
ドでもそのまま動く
deno test でバックエンドもフロントの Model
も一瞬でテスト完結
デスクトップアプリの UI調整がWebフロント
並みの速度で回る

WebView内のダイアログ制限 :
● <input type='file'> のパス隠蔽を
Deno.Command で迂回
Deno Desktop APIはまだ発展途上 (v2.9時
点):
● トレイ常駐や細かいウィンドウ制御は今
後の進化に期待
ビルドの割り切り :
● UIだけ esbuild (+deno-loader) で
bundle.js に1ファイル化
● Import Attributes で埋め込み

Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved.

23

24.

まとめ ● ● ● ● 🦖 Deno Desktop はデスクトップツール開発の「新しい強力な選択肢」 ○ Rust不要・Node不要・ビルド設定不要。 TypeScriptとWeb標準APIだけで実用GUIが作れた 🛡 Denoの効きどころ: ○ Deno.BrowserWindow + Import Attributes / Deno.serve + WebSocket IPC ○ Deno.rename の原子的保存 / Deno.Command のゼロ依存OS連携 ○ Deno.stat & ストリームの Tiered Diffing / JSR × npm × WASM 混載 ○ --hmr 開発から deno desktop -o 配布まで 💡 Denoの「オールインワン」はデスクトップ開発でこそ光る : ○ fmt / lint / check / test / task / compile — deno コマンドだけで完結 🤖 AIコーディング時代だからこそ、人間に優しいツールを、軽量なランタイムで : ○ AIレビューの認知負荷を下げるツールを、 Denoなら最速で形にできる! Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 24

25.

ご清聴ありがとうございました! リポジトリ: https://github.com/zonuko/diffrex Copyright (C) 2026 Toranoana Lab Inc. All Rights Reserved. 25