@minimalcorp/tsunagi 0.1.35 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  3. package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +2 -2
  14. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  21. package/.next/standalone/apps/web/.next/server/app/index.rsc +3 -3
  22. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  23. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +3 -3
  24. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  25. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  26. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  27. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
  28. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  30. package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  31. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
  32. package/.next/standalone/apps/web/.next/server/app/settings.rsc +3 -3
  33. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  34. package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  35. package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  36. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  37. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  38. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  39. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  40. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__08c7fz~._.js → [root-of-the-server]__04g~n~m._.js} +2 -2
  42. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ujaeyr._.js +3 -0
  43. package/.next/standalone/apps/web/.next/server/chunks/ssr/_016-lxb._.js +3 -0
  44. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +2 -2
  45. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hh~0f6._.js +3 -0
  46. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +1 -1
  47. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +5 -5
  48. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0gs4d2e._.js → node_modules_06vr-0a._.js} +1 -1
  49. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0llz_9l._.js → node_modules_0c6z_cw._.js} +1 -1
  50. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_00b-qxe._.js → node_modules_0fcqekz._.js} +1 -1
  51. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0kmpnnk._.js → node_modules_0h0imx2._.js} +1 -1
  52. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0pi.005._.js → node_modules_0jo-w3s._.js} +1 -1
  53. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0b547kr._.js → node_modules_0k7u4.c._.js} +1 -1
  54. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0y3g5lf._.js → node_modules_0pbr55t._.js} +1 -1
  55. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0~n91z-._.js → node_modules_0zkg-x0._.js} +1 -1
  56. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_lucide-react_dist_esm_icons_0zerlct._.js → node_modules_lucide-react_dist_esm_icons_0qttwc4._.js} +2 -2
  57. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  58. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  59. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  60. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  61. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  62. package/.next/standalone/apps/web/.next/static/chunks/{0vlse5ga2r0n7.js → 094hkpbli0k9s.js} +13 -13
  63. package/.next/standalone/apps/web/.next/static/chunks/0nl-qnylbdcjh.js +5 -0
  64. package/.next/standalone/apps/web/.next/static/chunks/0pg---_6a_4ls.css +4 -0
  65. package/.next/standalone/apps/web/.next/static/chunks/0q5sjjz8en7-~.js +1 -0
  66. package/.next/standalone/apps/web/.next/static/chunks/11.9g_~.43gp6.js +1 -0
  67. package/.next/standalone/apps/web/.next/static/chunks/{13f6kj~4r5x0p.js → 13x7kvv5vziy0.js} +1 -1
  68. package/.next/standalone/apps/web/.next/static/chunks/{0x3kp2s554oy5.js → 15~pc9ql1ab38.js} +8 -8
  69. package/dist/server/generated/prisma/browser.d.ts +5 -0
  70. package/dist/server/generated/prisma/client.d.ts +5 -0
  71. package/dist/server/generated/prisma/internal/class.d.ts +11 -0
  72. package/dist/server/generated/prisma/internal/class.js +4 -4
  73. package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +83 -1
  74. package/dist/server/generated/prisma/internal/prismaNamespace.js +7 -1
  75. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +7 -0
  76. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +7 -1
  77. package/dist/server/generated/prisma/models/AppSetting.d.ts +939 -0
  78. package/dist/server/generated/prisma/models/AppSetting.js +1 -0
  79. package/dist/server/generated/prisma/models.d.ts +1 -0
  80. package/dist/server/index.js +2 -0
  81. package/dist/server/lib/ollama-settings.d.ts +21 -0
  82. package/dist/server/lib/ollama-settings.js +116 -0
  83. package/dist/server/lib/services/task-service.js +3 -9
  84. package/dist/server/pty-manager.d.ts +5 -1
  85. package/dist/server/pty-manager.js +15 -7
  86. package/dist/server/routes/onboarding.js +6 -1
  87. package/dist/server/routes/settings.d.ts +2 -0
  88. package/dist/server/routes/settings.js +44 -0
  89. package/dist/server/routes/tasks.js +5 -1
  90. package/dist/server/routes/terminal.js +20 -3
  91. package/npm-shrinkwrap.json +2 -2
  92. package/package.json +1 -1
  93. package/prisma/migrations/20260924000000_add_app_settings/migration.sql +7 -0
  94. package/prisma/schema.prisma +9 -1
  95. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0mct.7z._.js +0 -3
  96. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0lpms0u._.js +0 -3
  97. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0owas.l._.js +0 -3
  98. package/.next/standalone/apps/web/.next/static/chunks/00e8eue.21.8h.css +0 -4
  99. package/.next/standalone/apps/web/.next/static/chunks/08u0u06ip_aj0.js +0 -5
  100. package/.next/standalone/apps/web/.next/static/chunks/0j3485~p9pn9n.js +0 -1
  101. package/.next/standalone/apps/web/.next/static/chunks/0kmlu-0djf-rx.js +0 -1
  102. /package/.next/standalone/apps/web/.next/static/{7OpUnOx5deFmG2Iu_gF5o → XlHkzVdPy8Ez_iG8HMze4}/_buildManifest.js +0 -0
  103. /package/.next/standalone/apps/web/.next/static/{7OpUnOx5deFmG2Iu_gF5o → XlHkzVdPy8Ez_iG8HMze4}/_clientMiddlewareManifest.js +0 -0
  104. /package/.next/standalone/apps/web/.next/static/{7OpUnOx5deFmG2Iu_gF5o → XlHkzVdPy8Ez_iG8HMze4}/_ssgManifest.js +0 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -2,4 +2,5 @@ export type * from './models/Repository.js';
2
2
  export type * from './models/Task.js';
3
3
  export type * from './models/Tab.js';
4
4
  export type * from './models/EnvironmentVariable.js';
5
+ export type * from './models/AppSetting.js';
5
6
  export type * from './commonInputTypes.js';
@@ -18,6 +18,7 @@ import { editorRoutes } from './routes/editor.js';
18
18
  import { whisperRoutes } from './routes/whisper.js';
19
19
  import { stopWhisperServerOnExit } from './lib/whisper-process.js';
20
20
  import { llmRoutes } from './routes/llm.js';
21
+ import { settingsRoutes } from './routes/settings.js';
21
22
  import { stopLlmServerOnExit } from './lib/llm-process.js';
22
23
  import { createBasicAuth } from './basic-auth.js';
23
24
  // Fastify は単一の公開エンドポイント。Next.js は内部ポートで動かしプロキシする。
@@ -97,6 +98,7 @@ async function start() {
97
98
  await fastify.register(editorRoutes, { prefix: '/api' });
98
99
  await fastify.register(whisperRoutes, { prefix: '/api' });
99
100
  await fastify.register(llmRoutes, { prefix: '/api' });
101
+ await fastify.register(settingsRoutes, { prefix: '/api' });
100
102
  // catch-all リバースプロキシ: /api・/socket.io・/health 以外を内部 Next.js へ転送。
101
103
  // - /api/* と /health は上で定義済みルートが wildcard より優先される。
102
104
  // - /socket.io は Socket.IO が HTTP サーバ層で先取りするためここには来ない。
@@ -0,0 +1,21 @@
1
+ import type { OllamaSettings } from '@minimalcorp/tsunagi-shared';
2
+ /** Ollama タブでは Anthropic の認証情報を環境変数ごと取り除く(ANTHROPIC_AUTH_TOKEN と競合するため) */
3
+ export declare const OLLAMA_UNSET_ENV_KEYS: string[];
4
+ export declare function defaultOllamaSettings(): OllamaSettings;
5
+ export declare function getOllamaSettings(): Promise<OllamaSettings>;
6
+ export declare function saveOllamaSettings(settings: OllamaSettings): Promise<OllamaSettings>;
7
+ /** リクエストボディを検証して OllamaSettings に正規化する。不正ならエラーメッセージを返す */
8
+ export declare function parseOllamaSettings(body: unknown): {
9
+ settings: OllamaSettings;
10
+ } | {
11
+ error: string;
12
+ };
13
+ /** Ollama タブを起動できる状態か(オンボーディング完了判定にも使う) */
14
+ export declare function isOllamaReady(settings: OllamaSettings): boolean;
15
+ /**
16
+ * Ollama の Anthropic 互換 API で Claude Code を動かすための環境変数。
17
+ * DB の環境変数より後に適用し、同名キーを上書きする。
18
+ */
19
+ export declare function buildOllamaEnv(settings: OllamaSettings): Record<string, string>;
20
+ /** Ollama に pull 済みのモデル名一覧を取得する */
21
+ export declare function listOllamaModels(baseUrl: string): Promise<string[]>;
@@ -0,0 +1,116 @@
1
+ import { prisma } from './db.js';
2
+ const SETTING_KEY = 'ollama';
3
+ const DEFAULT_CONTEXT_TOKENS = 65536;
4
+ // Docker 内から host の Ollama に繋ぐ場合は compose で host.docker.internal を指定する
5
+ const DEFAULT_BASE_URL = process.env.TSUNAGI_OLLAMA_URL || 'http://localhost:11434';
6
+ const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
7
+ /** Ollama タブでは Anthropic の認証情報を環境変数ごと取り除く(ANTHROPIC_AUTH_TOKEN と競合するため) */
8
+ export const OLLAMA_UNSET_ENV_KEYS = ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'];
9
+ export function defaultOllamaSettings() {
10
+ return {
11
+ enabled: false,
12
+ baseUrl: DEFAULT_BASE_URL,
13
+ model: '',
14
+ contextTokens: DEFAULT_CONTEXT_TOKENS,
15
+ extraEnv: {},
16
+ };
17
+ }
18
+ export async function getOllamaSettings() {
19
+ const row = await prisma.appSetting.findUnique({ where: { key: SETTING_KEY } });
20
+ if (!row)
21
+ return defaultOllamaSettings();
22
+ try {
23
+ return { ...defaultOllamaSettings(), ...JSON.parse(row.value) };
24
+ }
25
+ catch {
26
+ return defaultOllamaSettings();
27
+ }
28
+ }
29
+ export async function saveOllamaSettings(settings) {
30
+ const value = JSON.stringify(settings);
31
+ await prisma.appSetting.upsert({
32
+ where: { key: SETTING_KEY },
33
+ create: { key: SETTING_KEY, value },
34
+ update: { value },
35
+ });
36
+ return settings;
37
+ }
38
+ /** リクエストボディを検証して OllamaSettings に正規化する。不正ならエラーメッセージを返す */
39
+ export function parseOllamaSettings(body) {
40
+ if (typeof body !== 'object' || body === null)
41
+ return { error: 'Invalid body' };
42
+ const b = body;
43
+ if (typeof b.enabled !== 'boolean')
44
+ return { error: 'enabled must be boolean' };
45
+ const baseUrl = typeof b.baseUrl === 'string' ? b.baseUrl.trim().replace(/\/+$/, '') : '';
46
+ try {
47
+ const url = new URL(baseUrl);
48
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
49
+ throw new Error();
50
+ }
51
+ catch {
52
+ return { error: 'baseUrl must be a valid http(s) URL' };
53
+ }
54
+ if (typeof b.model !== 'string')
55
+ return { error: 'model must be string' };
56
+ const contextTokens = Number(b.contextTokens);
57
+ if (!Number.isInteger(contextTokens) || contextTokens <= 0) {
58
+ return { error: 'contextTokens must be a positive integer' };
59
+ }
60
+ const extraEnv = {};
61
+ if (b.extraEnv !== undefined) {
62
+ if (typeof b.extraEnv !== 'object' || b.extraEnv === null || Array.isArray(b.extraEnv)) {
63
+ return { error: 'extraEnv must be an object' };
64
+ }
65
+ for (const [key, value] of Object.entries(b.extraEnv)) {
66
+ if (!ENV_KEY_PATTERN.test(key))
67
+ return { error: `Invalid env key: ${key}` };
68
+ if (typeof value !== 'string')
69
+ return { error: `Env value must be string: ${key}` };
70
+ extraEnv[key] = value;
71
+ }
72
+ }
73
+ return {
74
+ settings: { enabled: b.enabled, baseUrl, model: b.model.trim(), contextTokens, extraEnv },
75
+ };
76
+ }
77
+ /** Ollama タブを起動できる状態か(オンボーディング完了判定にも使う) */
78
+ export function isOllamaReady(settings) {
79
+ return settings.enabled && settings.model !== '';
80
+ }
81
+ /**
82
+ * Ollama の Anthropic 互換 API で Claude Code を動かすための環境変数。
83
+ * DB の環境変数より後に適用し、同名キーを上書きする。
84
+ */
85
+ export function buildOllamaEnv(settings) {
86
+ const { baseUrl, model, contextTokens, extraEnv } = settings;
87
+ return {
88
+ ANTHROPIC_BASE_URL: baseUrl,
89
+ // Ollama は値を検証しないが、未設定だと Claude Code がログインを要求する
90
+ ANTHROPIC_AUTH_TOKEN: 'ollama',
91
+ ANTHROPIC_MODEL: model,
92
+ ANTHROPIC_DEFAULT_OPUS_MODEL: model,
93
+ ANTHROPIC_DEFAULT_SONNET_MODEL: model,
94
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
95
+ CLAUDE_CODE_SUBAGENT_MODEL: model,
96
+ // 未知のモデルは 200k 扱いになり auto-compact が効かないため、Ollama 側の値に揃える
97
+ CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(contextTokens),
98
+ // リクエスト毎に変わる attribution がローカル側の KV キャッシュを無効化するため外す
99
+ CLAUDE_CODE_ATTRIBUTION_HEADER: '0',
100
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
101
+ // ローカルLLMはプロンプト読み込み中(数分)にバイトを返さないため、既定の 5 分の
102
+ // 無通信タイムアウト / stream watchdog で打ち切られ再送される。上限(30分)まで延ばす
103
+ API_FORCE_IDLE_TIMEOUT: '0',
104
+ CLAUDE_STREAM_IDLE_TIMEOUT_MS: '1800000',
105
+ API_TIMEOUT_MS: '3600000',
106
+ ...extraEnv,
107
+ };
108
+ }
109
+ /** Ollama に pull 済みのモデル名一覧を取得する */
110
+ export async function listOllamaModels(baseUrl) {
111
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(5000) });
112
+ if (!res.ok)
113
+ throw new Error(`Ollama responded ${res.status}`);
114
+ const json = (await res.json());
115
+ return (json.models ?? []).map((m) => m.name).sort();
116
+ }
@@ -150,19 +150,13 @@ export async function createTask(params, options) {
150
150
  console.error('Failed to create worktree:', error);
151
151
  await taskRepo.updateTask(newTask.id, { worktreeStatus: 'error' });
152
152
  }
153
- // 4. 初期Tab作成
154
- try {
155
- await taskRepo.createTab(newTask.id);
156
- }
157
- catch (error) {
158
- console.error('Failed to create initial tab:', error);
159
- }
160
- // 5. 更新後のタスクを取得
153
+ // 初期Tabは作らない。タブはユーザーがタスク詳細で起動先(Claude / Ollama 等)を選んで作る
154
+ // 4. 更新後のタスクを取得
161
155
  const updatedTask = await taskRepo.getTask(newTask.id);
162
156
  if (!updatedTask) {
163
157
  throw new TaskServiceError('Failed to retrieve created task', 'INTERNAL_ERROR');
164
158
  }
165
- // 6. Socket.IO通知
159
+ // 5. Socket.IO通知
166
160
  options?.io?.emit('task:created', { task: updatedTask });
167
161
  return { task: updatedTask };
168
162
  }
@@ -20,7 +20,11 @@ declare class PtyManager {
20
20
  setActiveSocket(sessionId: string, socketId: string): void;
21
21
  getActiveSocket(sessionId: string): string | undefined;
22
22
  clearActiveSocket(sessionId: string, socketId: string): void;
23
- createSession(sessionId: string, cwd: string, env?: Record<string, string>): PtySession;
23
+ /**
24
+ * @param unsetKeys 外側 Terminal や DB 由来でも内部ターミナルに渡さない環境変数キー
25
+ * (例: Ollama タブでは Anthropic の認証トークンを取り除く)
26
+ */
27
+ createSession(sessionId: string, cwd: string, env?: Record<string, string>, unsetKeys?: string[]): PtySession;
24
28
  getSession(sessionId: string): PtySession | undefined;
25
29
  deleteSession(sessionId: string): void;
26
30
  /**
@@ -37,7 +37,11 @@ class PtyManager {
37
37
  this.activeSocketIds.delete(sessionId);
38
38
  }
39
39
  }
40
- createSession(sessionId, cwd, env) {
40
+ /**
41
+ * @param unsetKeys 外側 Terminal や DB 由来でも内部ターミナルに渡さない環境変数キー
42
+ * (例: Ollama タブでは Anthropic の認証トークンを取り除く)
43
+ */
44
+ createSession(sessionId, cwd, env, unsetKeys = []) {
41
45
  if (this.sessions.has(sessionId)) {
42
46
  throw new Error(`Session already exists: ${sessionId}`);
43
47
  }
@@ -54,17 +58,21 @@ class PtyManager {
54
58
  if (process.env.TSUNAGI_OUTER_NODE_ENV !== undefined) {
55
59
  baseEnv.NODE_ENV = process.env.TSUNAGI_OUTER_NODE_ENV;
56
60
  }
61
+ const spawnEnv = {
62
+ ...baseEnv,
63
+ ...env,
64
+ TERM: 'xterm-256color',
65
+ COLORTERM: 'truecolor',
66
+ };
67
+ for (const key of unsetKeys) {
68
+ delete spawnEnv[key];
69
+ }
57
70
  const ptyProcess = pty.spawn(shell, [], {
58
71
  name: 'xterm-256color',
59
72
  cols: 80,
60
73
  rows: 24,
61
74
  cwd,
62
- env: {
63
- ...baseEnv,
64
- ...env,
65
- TERM: 'xterm-256color',
66
- COLORTERM: 'truecolor',
67
- },
75
+ env: spawnEnv,
68
76
  });
69
77
  const now = Date.now();
70
78
  const session = {
@@ -1,11 +1,16 @@
1
1
  import { getEnv } from '../lib/repositories/environment.js';
2
+ import { getOllamaSettings, isOllamaReady } from '../lib/ollama-settings.js';
2
3
  export async function onboardingRoutes(fastify) {
3
4
  // GET /onboarding/status
4
5
  fastify.get('/onboarding/status', async (_request, reply) => {
5
6
  try {
6
7
  const globalEnv = await getEnv('global');
7
8
  const hasGlobalToken = Boolean(globalEnv.ANTHROPIC_API_KEY || globalEnv.CLAUDE_CODE_OAUTH_TOKEN);
8
- return reply.status(200).send({ data: { completed: hasGlobalToken, hasGlobalToken } });
9
+ // 実験的機能の Ollama を設定済みなら、Anthropic のトークンがなくても claude を起動できる
10
+ const ollamaReady = isOllamaReady(await getOllamaSettings());
11
+ return reply.status(200).send({
12
+ data: { completed: hasGlobalToken || ollamaReady, hasGlobalToken, ollamaReady },
13
+ });
9
14
  }
10
15
  catch (error) {
11
16
  fastify.log.error(error, 'GET /onboarding/status error');
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function settingsRoutes(fastify: FastifyInstance): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { getOllamaSettings, listOllamaModels, parseOllamaSettings, saveOllamaSettings, } from '../lib/ollama-settings.js';
2
+ export async function settingsRoutes(fastify) {
3
+ // GET /settings/ollama
4
+ fastify.get('/settings/ollama', async (_request, reply) => {
5
+ try {
6
+ return reply.status(200).send({ data: await getOllamaSettings() });
7
+ }
8
+ catch (error) {
9
+ fastify.log.error(error, 'GET /settings/ollama error');
10
+ return reply.status(500).send({ error: 'Failed to fetch ollama settings' });
11
+ }
12
+ });
13
+ // PUT /settings/ollama
14
+ fastify.put('/settings/ollama', async (request, reply) => {
15
+ const parsed = parseOllamaSettings(request.body);
16
+ if ('error' in parsed) {
17
+ return reply.status(400).send({ error: parsed.error });
18
+ }
19
+ try {
20
+ return reply.status(200).send({ data: await saveOllamaSettings(parsed.settings) });
21
+ }
22
+ catch (error) {
23
+ fastify.log.error(error, 'PUT /settings/ollama error');
24
+ return reply.status(500).send({ error: 'Failed to save ollama settings' });
25
+ }
26
+ });
27
+ // GET /settings/ollama/models - Ollama に pull 済みのモデル一覧
28
+ // ブラウザから直接叩くと CORS / Docker 内外のホスト名差異が問題になるためサーバー経由にする
29
+ fastify.get('/settings/ollama/models', async (request, reply) => {
30
+ const baseUrl = (request.query.baseUrl || (await getOllamaSettings()).baseUrl).replace(/\/+$/, '');
31
+ if (!/^https?:\/\//.test(baseUrl)) {
32
+ return reply.status(400).send({ error: 'baseUrl must be a valid http(s) URL' });
33
+ }
34
+ try {
35
+ return reply.status(200).send({ data: { models: await listOllamaModels(baseUrl) } });
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ return reply
40
+ .status(502)
41
+ .send({ error: `Ollama に接続できません (${baseUrl}): ${message}` });
42
+ }
43
+ });
44
+ }
@@ -1,6 +1,7 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
2
  import * as taskRepo from '../lib/repositories/task.js';
3
3
  import * as worktreeManager from '../lib/worktree-manager.js';
4
+ import { ptyManager } from '../pty-manager.js';
4
5
  import { listTasks, createTask, getTask, updateTask, deleteTask, TaskServiceError, } from '../lib/services/task-service.js';
5
6
  export async function tasksRoutes(fastify) {
6
7
  const io = fastify.io;
@@ -327,7 +328,8 @@ export async function tasksRoutes(fastify) {
327
328
  fastify.post('/tasks/:id/tabs', async (request, reply) => {
328
329
  try {
329
330
  const { id: taskId } = request.params;
330
- const mode = request.body?.mode === 'terminal' ? 'terminal' : 'claude';
331
+ const requested = request.body?.mode;
332
+ const mode = requested === 'terminal' || requested === 'ollama' ? requested : 'claude';
331
333
  const task = await taskRepo.getTask(taskId);
332
334
  if (!task) {
333
335
  return reply.status(404).send({ error: 'Task not found' });
@@ -366,6 +368,8 @@ export async function tasksRoutes(fastify) {
366
368
  if (!success) {
367
369
  return reply.status(404).send({ error: 'Tab not found' });
368
370
  }
371
+ // タブの PTY(=claude プロセス) も止める。残すと GC(30分) まで動き続けるため
372
+ ptyManager.deleteSession(tab_id);
369
373
  return reply.status(200).send({ data: { success: true } });
370
374
  }
371
375
  catch (error) {
@@ -5,6 +5,7 @@ import { ptyManager } from '../pty-manager.js';
5
5
  import { prisma } from '../lib/db.js';
6
6
  import { getEnv } from '../lib/repositories/environment.js';
7
7
  import { ensureClaudeOnboardingCompleted } from '../lib/claude-config-guard.js';
8
+ import { OLLAMA_UNSET_ENV_KEYS, buildOllamaEnv, getOllamaSettings, } from '../lib/ollama-settings.js';
8
9
  // サーバーはプロジェクトルートから起動されるため process.cwd() でルートを取得
9
10
  const TSUNAGI_EDITOR_PATH = path.resolve(process.cwd(), 'scripts/monaco-editor.sh');
10
11
  // Fastify(API) の公開ポート。index.ts と同じ既定値・同じ環境変数(TSUNAGI_SERVER_PORT)を見る。
@@ -205,18 +206,34 @@ export async function terminalRoutes(fastify) {
205
206
  // 環境変数を取得する(global → owner → repo、後勝ちで上書き)。
206
207
  // Plannerタブ等、Taskに紐づかないタブは Tab レコードが存在しないため global のみになる。
207
208
  let dbEnv = {};
209
+ let tabMode;
208
210
  try {
209
211
  const tab = await prisma.tab.findUnique({
210
212
  where: { tabId: sessionId },
211
213
  include: { task: true },
212
214
  });
215
+ tabMode = tab?.mode;
213
216
  dbEnv = tab ? await getEnv('repo', tab.task.owner, tab.task.repo) : await getEnv('global');
214
217
  fastify.log.info({ count: Object.keys(dbEnv).length, owner: tab?.task.owner, repo: tab?.task.repo }, 'Loaded env vars');
215
218
  }
216
219
  catch (err) {
217
220
  fastify.log.warn({ err }, 'Failed to load env vars');
218
221
  }
219
- // 優先順位: リクエストで渡されたenv > DB env(global/owner/repo) > tsunagi独自のEDITOR設定
222
+ // Ollama タブ: Anthropic の代わりに Ollama の Anthropic 互換 API で claude を動かす。
223
+ // DB の環境変数より後に適用し、Anthropic の認証トークンは環境変数ごと取り除く。
224
+ let providerEnv = {};
225
+ let unsetKeys = [];
226
+ if (tabMode === 'ollama') {
227
+ const ollama = await getOllamaSettings();
228
+ if (!ollama.model) {
229
+ return reply
230
+ .status(400)
231
+ .send({ error: 'Ollama のモデルが未設定です。Settings で設定してください' });
232
+ }
233
+ providerEnv = buildOllamaEnv(ollama);
234
+ unsetKeys = OLLAMA_UNSET_ENV_KEYS;
235
+ }
236
+ // 優先順位: リクエストで渡されたenv > Ollama等のprovider env > DB env(global/owner/repo) > tsunagi独自のEDITOR設定
220
237
  // tsunagi-editor.sh をデフォルトにすることで Ctrl+G が Monaco Modal を開く。
221
238
  // DB / リクエストで EDITOR が明示設定されている場合はそちらが優先される。
222
239
  const tsunagiDefaultEnv = {
@@ -226,8 +243,8 @@ export async function terminalRoutes(fastify) {
226
243
  // ホストなので localhost。単一ポート化で API は SERVER_PORT(既定 2791)。
227
244
  TSUNAGI_API_BASE: `http://localhost:${SERVER_PORT}`,
228
245
  };
229
- const mergedEnv = { ...tsunagiDefaultEnv, ...dbEnv, ...env };
230
- const session = ptyManager.createSession(sessionId, workingDir, mergedEnv);
246
+ const mergedEnv = { ...tsunagiDefaultEnv, ...dbEnv, ...providerEnv, ...env };
247
+ const session = ptyManager.createSession(sessionId, workingDir, mergedEnv, unsetKeys);
231
248
  // コマンドが指定されていればPTY起動後にシェルへ書き込む
232
249
  if (command) {
233
250
  const cmd = command.endsWith('\n') ? command : command + '\n';
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@minimalcorp/tsunagi",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@minimalcorp/tsunagi",
9
- "version": "0.1.35",
9
+ "version": "0.1.36",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "os": [
12
12
  "darwin",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minimalcorp/tsunagi",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "Multi-repo GitHub project management with Claude AI integration, designed for visualizing and controlling AI-driven development locally.",
5
5
  "keywords": [
6
6
  "tsunagi",
@@ -0,0 +1,7 @@
1
+ -- CreateTable
2
+ -- アプリ全体の設定(例: key='ollama' に実験的機能 Ollama の設定JSON)
3
+ CREATE TABLE "app_settings" (
4
+ "key" TEXT NOT NULL PRIMARY KEY,
5
+ "value" TEXT NOT NULL,
6
+ "updated_at" DATETIME NOT NULL
7
+ );
@@ -60,7 +60,7 @@ model Tab {
60
60
  taskId String @map("task_id")
61
61
  order Int // タブ表示用の連番
62
62
  status String // 'idle' | 'running' | 'success' | 'error'
63
- mode String @default("claude") // 'terminal' | 'claude'
63
+ mode String @default("claude") // 'terminal' | 'claude' | 'ollama'(ollama: ローカルLLM(Ollama)で起動するclaude)
64
64
  createdAt DateTime @default(now()) @map("created_at")
65
65
  startedAt DateTime @map("started_at")
66
66
  completedAt DateTime? @map("completed_at")
@@ -94,3 +94,11 @@ model EnvironmentVariable {
94
94
  @@map("environment_variables")
95
95
  }
96
96
 
97
+ // AppSetting - アプリ全体の設定(key単位でJSONを保存)
98
+ model AppSetting {
99
+ key String @id
100
+ value String // JSON
101
+ updatedAt DateTime @updatedAt @map("updated_at")
102
+
103
+ @@map("app_settings")
104
+ }
@@ -1,3 +0,0 @@
1
- module.exports=[24361,(a,b,c)=>{b.exports=a.x("util",()=>require("util"))},84454,65486,80056,43715,a=>{"use strict";var b=a.i(30685),c=(0,a.i(17508).default)(Object.keys,Object),d=Object.prototype.hasOwnProperty;let e=function(a){if(!(0,b.default)(a))return c(a);var e=[];for(var f in Object(a))d.call(a,f)&&"constructor"!=f&&e.push(f);return e};a.s(["default",0,e],65486);var f=a.i(37991),g=a.i(11253),h=(0,f.default)(g.default,"DataView"),i=a.i(38141),j=(0,f.default)(g.default,"Promise"),k=(0,f.default)(g.default,"Set");a.s(["default",0,k],80056);var l=(0,f.default)(g.default,"WeakMap"),m=a.i(35102),n=a.i(12098),o="[object Map]",p="[object Promise]",q="[object Set]",r="[object WeakMap]",s="[object DataView]",t=(0,n.default)(h),u=(0,n.default)(i.default),v=(0,n.default)(j),w=(0,n.default)(k),x=(0,n.default)(l),y=m.default;(h&&y(new h(new ArrayBuffer(1)))!=s||i.default&&y(new i.default)!=o||j&&y(j.resolve())!=p||k&&y(new k)!=q||l&&y(new l)!=r)&&(y=function(a){var b=(0,m.default)(a),c="[object Object]"==b?a.constructor:void 0,d=c?(0,n.default)(c):"";if(d)switch(d){case t:return s;case u:return o;case v:return p;case w:return q;case x:return r}return b});let z=y;a.s(["default",0,z],43715);var A=a.i(62244),B=a.i(66723),C=a.i(68378),D=a.i(12999),E=a.i(56814),F=Object.prototype.hasOwnProperty;a.s(["default",0,function(a){if(null==a)return!0;if((0,C.default)(a)&&((0,B.default)(a)||"string"==typeof a||"function"==typeof a.splice||(0,D.default)(a)||(0,E.default)(a)||(0,A.default)(a)))return!a.length;var c=z(a);if("[object Map]"==c||"[object Set]"==c)return!a.size;if((0,b.default)(a))return!e(a).length;for(var d in a)if(F.call(a,d))return!1;return!0}],84454)},54799,(a,b,c)=>{b.exports=a.x("crypto",()=>require("crypto"))},46786,(a,b,c)=>{b.exports=a.x("os",()=>require("os"))},4446,(a,b,c)=>{b.exports=a.x("net",()=>require("net"))},73160,a=>{"use strict";var b=a.i(14562),c=a.i(34603);a.i(75930);var d=a.i(71731),e=(0,c.__name)(a=>{let{securityLevel:c}=(0,b.getConfig2)(),e=(0,d.select)("body");if("sandbox"===c){let b=(0,d.select)(`#i${a}`),c=b.node()?.contentDocument??document;e=(0,d.select)(c.body)}return e.select(`#${a}`)},"selectSvgElement");a.s(["selectSvgElement",0,e])},50160,a=>{a.v("/_next/static/media/icon.0fkix7.hkmsz4.png"+(globalThis.NEXT_CLIENT_ASSET_SUFFIX||""))},33354,(a,b,c)=>{"use strict";c._=function(a){return a&&a.__esModule?a:{default:a}}},15618,49594,a=>{"use strict";let b=(0,a.i(60137).default)("plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);a.s(["Plus",0,b],15618);let c=[{bg:"bg-blue-100 dark:bg-blue-900/30",text:"text-blue-700 dark:text-blue-300"},{bg:"bg-emerald-100 dark:bg-emerald-900/30",text:"text-emerald-700 dark:text-emerald-300"},{bg:"bg-violet-100 dark:bg-violet-900/30",text:"text-violet-700 dark:text-violet-300"},{bg:"bg-amber-100 dark:bg-amber-900/30",text:"text-amber-700 dark:text-amber-300"},{bg:"bg-rose-100 dark:bg-rose-900/30",text:"text-rose-700 dark:text-rose-300"},{bg:"bg-cyan-100 dark:bg-cyan-900/30",text:"text-cyan-700 dark:text-cyan-300"},{bg:"bg-orange-100 dark:bg-orange-900/30",text:"text-orange-700 dark:text-orange-300"},{bg:"bg-teal-100 dark:bg-teal-900/30",text:"text-teal-700 dark:text-teal-300"},{bg:"bg-indigo-100 dark:bg-indigo-900/30",text:"text-indigo-700 dark:text-indigo-300"},{bg:"bg-pink-100 dark:bg-pink-900/30",text:"text-pink-700 dark:text-pink-300"},{bg:"bg-lime-100 dark:bg-lime-900/30",text:"text-lime-700 dark:text-lime-300"},{bg:"bg-fuchsia-100 dark:bg-fuchsia-900/30",text:"text-fuchsia-700 dark:text-fuchsia-300"}];a.s(["getRepoColor",0,function(a,b){let d=function(a){let b=5381;for(let c=0;c<a.length;c++)b=33*b^a.charCodeAt(c);return b>>>0}(`${a}/${b}`)%c.length;return c[d]}],49594)},10948,a=>{"use strict";let b=(0,a.i(60137).default)("git-branch",[["line",{x1:"6",x2:"6",y1:"3",y2:"15",key:"17qcm7"}],["circle",{cx:"18",cy:"6",r:"3",key:"1h7g24"}],["circle",{cx:"6",cy:"18",r:"3",key:"fqmcym"}],["path",{d:"M18 9a9 9 0 0 1-9 9",key:"n2h4wq"}]]);a.s(["GitBranch",0,b],10948)},92025,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_katex_dist_katex_mjs_0zuygmo._.js"].map(b=>a.l(b))).then(()=>b(48986)))},69e3,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_lodash-es_0yap81y._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_dagre-KV5264BT_mjs_0aqvo3i._.js","server/chunks/ssr/node_modules_lodash-es_091xgk5._.js","server/chunks/ssr/node_modules_dagre-d3-es_src_dagre_index_097t5l0.js","server/chunks/ssr/node_modules_103v9xp._.js"].map(b=>a.l(b))).then(()=>b(55868)))},71331,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0hsalo_._.js","server/chunks/ssr/node_modules_cytoscape_dist_cytoscape_esm_mjs_0642y5z._.js"].map(b=>a.l(b))).then(()=>b(3671)))},99987,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_chunk-YZCP3GAM_mjs_04sswi2._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_c4Diagram-AHTNJAMY_mjs_0mw4ioe._.js"].map(b=>a.l(b))).then(()=>b(42357)))},48308,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_08av5m5._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_flowDiagram-DWJPFMVM_mjs_11yfm2v._.js"].map(b=>a.l(b))).then(()=>b(26594)))},74667,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_07xyphe._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_erDiagram-SMLLAGMA_mjs_0ti8x~s._.js"].map(b=>a.l(b))).then(()=>b(35543)))},15498,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0w48uk8._.js","server/chunks/ssr/[root-of-the-server]__0qtqq5d._.js","server/chunks/ssr/node_modules_0gvdioh._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/0_lp_modules_mermaid_dist_chunks_mermaid_core_gitGraphDiagram-UUTBAWPF_mjs_0dvv8~.._.js"].map(b=>a.l(b))).then(()=>b(2421)))},24500,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0q8gl61._.js","server/chunks/ssr/node_modules_0t03q3z._.js"].map(b=>a.l(b))).then(()=>b(37521)))},78102,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0b6fjwn._.js","server/chunks/ssr/[root-of-the-server]__0ff1foz._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/node_modules_0gvdioh._.js"].map(b=>a.l(b))).then(()=>b(86618)))},94433,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_07-e6tb._.js","server/chunks/ssr/[root-of-the-server]__0n2qong._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/node_modules_0gvdioh._.js"].map(b=>a.l(b))).then(()=>b(28018)))},65122,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0m8d9.a._.js","server/chunks/ssr/0_lp_modules_mermaid_dist_chunks_mermaid_core_quadrantDiagram-34T5L4WZ_mjs_05wge~v._.js"].map(b=>a.l(b))).then(()=>b(40628)))},6687,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0j1i92p._.js","server/chunks/ssr/0_lp_modules_mermaid_dist_chunks_mermaid_core_xychartDiagram-5P7HB3ND_mjs_0wwuvw2._.js"].map(b=>a.l(b))).then(()=>b(7451)))},18402,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_04x.vut._.js","server/chunks/ssr/0t6k_mermaid_dist_chunks_mermaid_core_requirementDiagram-MS252O5E_mjs_048lezu._.js"].map(b=>a.l(b))).then(()=>b(90431)))},92739,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_0w_54yy._.js","server/chunks/ssr/0_lp_modules_mermaid_dist_chunks_mermaid_core_sequenceDiagram-FGHM5R23_mjs_12c7k-5._.js"].map(b=>a.l(b))).then(()=>b(67397)))},32256,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_0o900rn._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_chunk-4TB4RGXK_mjs_0ie15wz._.js"].map(b=>a.l(b))).then(()=>b(55252)))},51050,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_03f~che._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_chunk-4TB4RGXK_mjs_0ie15wz._.js"].map(b=>a.l(b))).then(()=>b(12084)))},33430,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0mwl~df._.js","server/chunks/ssr/node_modules_lodash-es_091xgk5._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_stateDiagram-FHFEXIEX_mjs_0mg2~ri._.js","server/chunks/ssr/node_modules_dagre-d3-es_src_dagre_index_097t5l0.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_chunk-OYMX7WX6_mjs_087i3.l._.js","server/chunks/ssr/node_modules_103v9xp._.js"].map(b=>a.l(b))).then(()=>b(55905)))},43021,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_0cje6no._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_chunk-OYMX7WX6_mjs_087i3.l._.js"].map(b=>a.l(b))).then(()=>b(79891)))},21261,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0_09.d-._.js","server/chunks/ssr/0_lp_modules_mermaid_dist_chunks_mermaid_core_journeyDiagram-VCZTEJTY_mjs_0q7b_tk._.js"].map(b=>a.l(b))).then(()=>b(47598)))},50244,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_d3-shape_src_arc_135n374.js","server/chunks/ssr/0t6k_mermaid_dist_chunks_mermaid_core_timeline-definition-GMOUNBTQ_mjs_0y7flug._.js"].map(b=>a.l(b))).then(()=>b(98138)))},54532,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_04x.vut._.js","server/chunks/ssr/0t6k_mermaid_dist_chunks_mermaid_core_mindmap-definition-QFDTVHPH_mjs_0ie03qp._.js"].map(b=>a.l(b))).then(()=>b(73348)))},42598,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_chunk-FMBD7UC4_mjs_0dox0y0._.js","server/chunks/ssr/0_lp_modules_mermaid_dist_chunks_mermaid_core_kanban-definition-6JOO6SKY_mjs_0b8_92c._.js"].map(b=>a.l(b))).then(()=>b(78776)))},18652,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_d3-scale_src_0~kn3a6._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_sankeyDiagram-XADWPNL6_mjs_0akor_o._.js"].map(b=>a.l(b))).then(()=>b(88909)))},9099,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0k8xmjr._.js","server/chunks/ssr/[root-of-the-server]__0hhfi7s._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/node_modules_0gvdioh._.js"].map(b=>a.l(b))).then(()=>b(20145)))},35140,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0yoat72._.js","server/chunks/ssr/[root-of-the-server]__0hhfi7s._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/node_modules_0gvdioh._.js"].map(b=>a.l(b))).then(()=>b(42910)))},56125,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0omx0o-._.js","server/chunks/ssr/node_modules_0heie6p._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_blockDiagram-DXYQGD6D_mjs_13mbtfc._.js","server/chunks/ssr/node_modules_lodash-es_0egjqeu._.js"].map(b=>a.l(b))).then(()=>b(68669)))},54269,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0fpqbi2._.js","server/chunks/ssr/[root-of-the-server]__0cl-0al._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/node_modules_0gvdioh._.js"].map(b=>a.l(b))).then(()=>b(58204)))},97375,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0bix9lu._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/[root-of-the-server]__0e2_bho._.js","server/chunks/ssr/node_modules_0gvdioh._.js","server/chunks/ssr/node_modules_0sqcrg2._.js","server/chunks/ssr/node_modules_cytoscape_dist_cytoscape_esm_mjs_0642y5z._.js"].map(b=>a.l(b))).then(()=>b(29821)))},70868,a=>{a.v(b=>Promise.all(["server/chunks/ssr/0_lp_modules_mermaid_dist_chunks_mermaid_core_ishikawaDiagram-UXIWVN3A_mjs_0gpauwj._.js"].map(b=>a.l(b))).then(()=>b(5822)))},5494,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_vennDiagram-DHZGUBPP_mjs_013wte~._.js"].map(b=>a.l(b))).then(()=>b(65580)))},41872,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0r9vehj._.js","server/chunks/ssr/[root-of-the-server]__0sha5xx._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/node_modules_0gvdioh._.js","server/chunks/ssr/node_modules_mermaid_dist_chunks_mermaid_core_diagram-G4DWMVQ6_mjs_108p.po._.js"].map(b=>a.l(b))).then(()=>b(52172)))},40744,a=>{a.v(b=>Promise.all(["server/chunks/ssr/node_modules_0bix9lu._.js","server/chunks/ssr/[root-of-the-server]__0e2_bho._.js","server/chunks/ssr/node_modules_0gvdioh._.js","server/chunks/ssr/node_modules_lodash-es_01jun1r._.js","server/chunks/ssr/0_lp_modules_mermaid_dist_chunks_mermaid_core_wardleyDiagram-NUSXRM2D_mjs_0579rrr._.js"].map(b=>a.l(b))).then(()=>b(2410)))}];
2
-
3
- //# sourceMappingURL=%5Broot-of-the-server%5D__0mct.7z._.js.map
@@ -1,3 +0,0 @@
1
- module.exports=[210,a=>{"use strict";let b=(0,a.i(60137).default)("arrow-left",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]);a.s(["ArrowLeft",0,b],210)},99464,a=>{"use strict";let b=(0,a.i(60137).default)("pencil",[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]]);a.s(["Pencil",0,b],99464)},82434,a=>{"use strict";let b=(0,a.i(60137).default)("bot",[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]]);a.s(["Bot",0,b],82434)},92e3,a=>{"use strict";let b=(0,a.i(60137).default)("circle-alert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);a.s(["AlertCircle",0,b],92e3)},4975,48124,a=>{"use strict";var b=a.i(60137);let c=(0,b.default)("mic",[["path",{d:"M12 19v3",key:"npa21l"}],["path",{d:"M19 10v2a7 7 0 0 1-14 0v-2",key:"1vc78b"}],["rect",{x:"9",y:"2",width:"6",height:"13",rx:"3",key:"s6n7sd"}]]);a.s(["Mic",0,c],4975);let d=(0,b.default)("square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);a.s(["Square",0,d],48124)},36620,a=>{"use strict";let b=(0,a.i(60137).default)("circle-question-mark",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);a.s(["CircleHelp",0,b],36620)},92552,a=>{"use strict";var b=a.i(87924),c=a.i(760);a.s(["Card",0,function({className:a,size:d="default",...e}){return(0,b.jsx)("div",{"data-slot":"card","data-size":d,className:(0,c.cn)("group/card flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",a),...e})},"CardContent",0,function({className:a,...d}){return(0,b.jsx)("div",{"data-slot":"card-content",className:(0,c.cn)("px-4 group-data-[size=sm]/card:px-3",a),...d})},"CardDescription",0,function({className:a,...d}){return(0,b.jsx)("div",{"data-slot":"card-description",className:(0,c.cn)("text-xs/relaxed text-muted-foreground",a),...d})},"CardHeader",0,function({className:a,...d}){return(0,b.jsx)("div",{"data-slot":"card-header",className:(0,c.cn)("group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",a),...d})},"CardTitle",0,function({className:a,...d}){return(0,b.jsx)("div",{"data-slot":"card-title",className:(0,c.cn)("font-heading text-sm font-medium",a),...d})}])},56026,19768,a=>{"use strict";a.s(["LLM_SYSTEM_PROMPT_STORAGE_KEY",()=>z,"VoiceInputButton",()=>B,"WHISPER_PROMPT_STORAGE_KEY",()=>y],56026);var b=a.i(87924),c=a.i(72131),d=a.i(96221),e=a.i(4975),f=a.i(48124),g=a.i(39524),h=a.i(91399),i=a.i(6828);a.s(["LOCAL_LLM_ENABLED_STORAGE_KEY",()=>s,"LocalLlmSection",()=>x],19768);var j=a.i(92e3),k=a.i(82434),l=a.i(67453),m=a.i(36620),n=a.i(92552),o=a.i(90002),p=a.i(65025),q=a.i(97463);let r="tsunagi:local-llm-enabled",s=r,t={not_running:"停止中",installing_deps:"依存関係をインストール中...",downloading_model:"モデルをダウンロード中...",starting_server:"サーバーを起動中... (モデルのロードに数十秒かかる場合があります)",running:"実行中",running_external:"実行中 (tsunagi外で起動)",error:"エラー"},u=["installing_deps","downloading_model","starting_server"],v=["running","running_external"];function w(a){let b=a/0x40000000;return b>=.1?`${b.toFixed(2)} GB`:`${(a/1048576).toFixed(0)} MB`}function x(){let[a,e]=(0,c.useState)(!1),[s,x]=(0,c.useState)(!1),[y,A]=(0,c.useState)(null),[B,C]=(0,c.useState)(""),D=(0,c.useRef)(null),E=(0,c.useCallback)(a=>{e(a),localStorage.setItem(r,String(a))},[]),F=(0,c.useCallback)(a=>{C(a),localStorage.setItem(z,a)},[]),G=(0,c.useCallback)(async()=>{let a=await fetch((0,h.apiUrl)("/api/llm/server/status")),b=await a.json();return A(b),b},[]);(0,c.useEffect)(()=>{e("true"===localStorage.getItem(r)),C(localStorage.getItem(z)??""),G()},[G]),(0,c.useEffect)(()=>{y&&v.includes(y.step)&&!a&&(E(!0),i.toaster.create({type:"success",title:"ローカルLLMが有効化されました"}))},[y,a,E]),(0,c.useEffect)(()=>(y&&u.includes(y.step)&&!D.current&&(D.current=setInterval(()=>{G().then(a=>{!u.includes(a.step)&&D.current&&(clearInterval(D.current),D.current=null)})},1e3)),()=>{D.current&&(clearInterval(D.current),D.current=null)}),[y,G]);let H=(0,c.useCallback)(()=>{G(),x(!0)},[G]),I=(0,c.useCallback)(async()=>{try{let a=await fetch((0,h.apiUrl)("/api/llm/server/start"),{method:"POST"});if(!a.ok){let b=await a.json().catch(()=>null);throw Error(b?.error||`HTTPエラー: ${a.status}`)}A(await a.json())}catch(a){i.toaster.create({type:"error",title:"サーバーの起動に失敗しました",description:a instanceof Error?a.message:String(a)})}},[]),J=(0,c.useCallback)(async()=>{try{let a=await fetch((0,h.apiUrl)("/api/llm/server/stop"),{method:"POST"});if(!a.ok){let b=await a.json().catch(()=>null);throw Error(b?.error||`HTTPエラー: ${a.status}`)}A(await a.json())}catch(a){i.toaster.create({type:"error",title:"サーバーの停止に失敗しました",description:a instanceof Error?a.message:String(a)})}},[]),K=(0,c.useCallback)(()=>{E(!1),y&&v.includes(y.step)&&J()},[E,J,y]),L=y?.serverDir,M=y?.downloadProgress,N=M?M.downloadedBytes/M.totalBytes*100:0,O=null!==y,P=O&&v.includes(y.step);return(0,b.jsxs)(n.Card,{children:[(0,b.jsxs)(n.CardHeader,{children:[(0,b.jsxs)("div",{className:"flex items-center gap-1",children:[(0,b.jsx)(n.CardTitle,{children:"ローカルLLM (実験的機能)"}),(0,b.jsx)(g.Button,{variant:"ghost",size:"icon-sm",onClick:H,title:"ローカルLLMについて",children:(0,b.jsx)(m.CircleHelp,{})})]}),(0,b.jsxs)(n.CardDescription,{children:["ローカルで動作するLLM(Qwen3-30B-A3B, MoE)を利用します。",(0,b.jsx)("strong",{className:"font-medium text-foreground",children:"有効にすると、音声入力の文字起こし結果がこのLLMで自動整形されるようになります"}),"(無効時は文字起こし結果をそのまま使用)。"]})]}),(0,b.jsx)(n.CardContent,{className:"flex flex-col gap-3",children:a?(0,b.jsxs)(b.Fragment,{children:[(0,b.jsxs)("div",{className:"flex flex-wrap items-center gap-2",children:[O?P?(0,b.jsxs)("span",{className:"flex items-center gap-2 text-success",children:[(0,b.jsx)(l.CheckCircle2,{className:"size-4"}),"ローカルLLM: 有効"]}):(0,b.jsxs)("span",{className:"flex items-center gap-2 text-warning",children:[(0,b.jsx)(j.AlertCircle,{className:"size-4"}),"ローカルLLM: 有効化済み(サーバー停止中)"]}):(0,b.jsxs)("span",{className:"flex items-center gap-2 text-muted-foreground",children:[(0,b.jsx)(d.Loader2,{className:"size-4 animate-spin"}),"状態を確認中..."]}),y&&(0,b.jsxs)("span",{className:"text-xs/relaxed text-muted-foreground",children:["(",t[y.step],")"]}),O&&!P&&(0,b.jsxs)(g.Button,{size:"default",onClick:H,children:[(0,b.jsx)(k.Bot,{}),"サーバーを起動"]}),(0,b.jsx)(g.Button,{size:"default",variant:"outline",onClick:K,children:"無効にする"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("label",{className:"mb-1 block text-xs font-medium text-muted-foreground",children:"整形システムプロンプト (任意)"}),(0,b.jsx)(q.Textarea,{value:B,onChange:a=>F(a.target.value),placeholder:"例: 句読点の追加と誤字修正のみ行い、言い回しは変えないでください。",className:"min-h-16 text-xs"}),(0,b.jsx)("p",{className:"mt-1 text-[0.65rem] text-muted-foreground",children:"文字起こし結果をLLMで整形する際の指示です。空欄の場合は既定のプロンプト(句読点の追加・誤字修正のみを行う指示)を使用します。"})]})]}):(0,b.jsxs)(g.Button,{size:"default",onClick:H,children:[(0,b.jsx)(k.Bot,{}),"ローカルLLMを有効化する"]})}),(0,b.jsx)(o.Dialog,{open:s,onOpenChange:({open:a})=>x(a),title:"ローカルLLMについて",maxWidth:"2xl",children:(0,b.jsxs)("div",{className:"space-y-4 text-sm",children:[(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"font-medium text-foreground",children:"実験的機能です"}),(0,b.jsx)("p",{className:"text-muted-foreground",children:"音声入力の文字起こし結果を、ローカルで動作するLLM(mlx-lm / Qwen3-30B-A3B, MoE構成)で整形します。MoE構成のため実計算に使うアクティブパラメータは約3Bで、denseな同規模モデルより高速に動作します。精度・速度は環境に依存し、今後変更される可能性があります。"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"font-medium text-foreground",children:"対応環境"}),(0,b.jsx)("p",{className:"text-muted-foreground",children:"Apple Silicon Mac (M1/M2/M3/M4) のみ対応。Windows/Linuxでは利用できません。メモリ32GB以上を推奨します。"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"font-medium text-foreground",children:"必要なもの(事前に手動でインストール)"}),(0,b.jsx)("ul",{className:"list-disc pl-5 text-muted-foreground",children:(0,b.jsx)("li",{children:"Python 3.9以降(Xcode Command Line Tools または Homebrew経由で入手可能)"})})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"mb-1 font-medium text-foreground",children:"セットアップ・起動"}),(0,b.jsx)("p",{className:"text-muted-foreground",children:"下のボタンから、依存関係のインストール・モデルのダウンロード・サーバー起動まで自動で行われます(初回はモデルサイズが大きく数分〜数十分かかります)。"}),y&&!L&&(0,b.jsx)("p",{className:"mt-1 text-destructive",children:"llm-serverが見つかりませんでした。インストールが壊れている可能性があります。"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"mb-1 font-medium text-foreground",children:"アンインストール"}),(0,b.jsxs)("p",{className:"text-muted-foreground",children:["ローカルLLMのためにダウンロードされるもの(Pythonの依存関係・LLMモデル、合計約17GB)は全て",(0,b.jsx)("code",{className:"rounded bg-muted px-1",children:"~/.tsunagi/llm"}),"に保存されます。不要になった場合はこのディレクトリを削除するだけで、関連リソースを完全に削除できます。"]})]}),(0,b.jsxs)("div",{className:"border-t border-border pt-4",children:[(!y||"not_running"===y.step||"error"===y.step)&&(0,b.jsxs)("div",{className:"flex flex-col gap-2",children:[(0,b.jsxs)(g.Button,{size:"default",onClick:()=>void I(),disabled:!y,children:[y?(0,b.jsx)(k.Bot,{}):(0,b.jsx)(d.Loader2,{className:"animate-spin"}),"LLMサーバーを起動"]}),y?.step==="error"&&y.error&&(0,b.jsx)("p",{className:"text-xs/relaxed text-destructive",children:y.error})]}),y?.step==="installing_deps"&&(0,b.jsxs)(g.Button,{size:"default",disabled:!0,children:[(0,b.jsx)(d.Loader2,{className:"animate-spin"}),"依存関係をインストール中... (数分かかる場合があります)"]}),y?.step==="downloading_model"&&(0,b.jsxs)("div",{className:"flex flex-col gap-2",children:[(0,b.jsxs)("div",{className:"flex items-center gap-2 text-xs/relaxed text-muted-foreground",children:[(0,b.jsx)(d.Loader2,{className:"size-4 animate-spin"}),"モデルをダウンロード中... (約17GB)"]}),M&&(0,b.jsxs)(b.Fragment,{children:[(0,b.jsx)(p.Progress,{value:N}),(0,b.jsxs)("p",{className:"text-xs/relaxed text-muted-foreground",children:[w(M.downloadedBytes)," / ",w(M.totalBytes)," (",N.toFixed(0),"%) ・ ",function(a){if(null===a)return"残り時間を計算中...";if(a<60)return`残り約${a}秒`;let b=Math.floor(a/60);return`残り約${b}分${a%60}秒`}(M.etaSeconds)]})]})]}),y?.step==="starting_server"&&(0,b.jsxs)(g.Button,{size:"default",disabled:!0,children:[(0,b.jsx)(d.Loader2,{className:"animate-spin"}),"サーバーを起動中... (モデルのロードに数十秒かかる場合があります)"]}),y&&v.includes(y.step)&&(0,b.jsxs)("div",{className:"flex flex-wrap items-center gap-2",children:[(0,b.jsxs)("span",{className:"flex items-center gap-2 text-success",children:[(0,b.jsx)(l.CheckCircle2,{className:"size-4"}),"サーバーは起動しています"]}),(0,b.jsxs)(g.Button,{size:"default",variant:"outline",onClick:()=>void J(),children:[(0,b.jsx)(f.Square,{}),"サーバーを停止"]})]})]})]})})]})}let y="tsunagi:whisper-prompt",z="tsunagi:llm-system-prompt",A=["running","running_external"];function B({onTranscribed:a}){let[j,k]=(0,c.useState)(!1),[l,m]=(0,c.useState)("idle"),[n,o]=(0,c.useState)(0),[p,q]=(0,c.useState)(null),r=(0,c.useRef)(null),t=(0,c.useRef)([]),u=(0,c.useRef)(null),v=(0,c.useRef)(null),w=(0,c.useRef)(null),x=(0,c.useRef)(0),C=(0,c.useRef)(0);(0,c.useEffect)(()=>{k("true"===localStorage.getItem("tsunagi:voice-input-enabled"))},[]),(0,c.useEffect)(()=>{if(!j)return;let a=!1,b=async()=>{try{let b=await fetch((0,h.apiUrl)("/api/whisper/server/status")),c=await b.json();a||q(c.step)}catch{a||q("not_running")}};b();let c=setInterval(b,5e3);return()=>{a=!0,clearInterval(c)}},[j]);let D=(0,c.useCallback)(()=>{w.current&&(clearInterval(w.current),w.current=null),v.current?.close(),v.current=null,o(0)},[]);(0,c.useEffect)(()=>()=>{D()},[D]);let E=(0,c.useCallback)(a=>{let b=new AudioContext,c=b.createMediaStreamSource(a),d=b.createAnalyser();d.fftSize=2048,c.connect(d),v.current=b,x.current=0,C.current=0;let e=new Float32Array(d.fftSize);w.current=setInterval(()=>{d.getFloatTimeDomainData(e);let a=0;for(let b=0;b<e.length;b++)a+=e[b]*e[b];let b=Math.sqrt(a/e.length),c=Math.min(1,Math.max(0,((b>0?20*Math.log10(b):-60)- -60)/50));C.current=Math.max(C.current,c),x.current=.3*x.current+.7*c,o(x.current)},50)},[]),F=(0,c.useCallback)(async b=>{m("transcribing");try{let c=new FormData,d=localStorage.getItem(y);d&&c.append("prompt",d);let e="true"===localStorage.getItem(s);c.append("useLlm",String(e));let f=localStorage.getItem(z);f&&c.append("systemPrompt",f),c.append("file",b,"recording.webm");let g=await fetch((0,h.apiUrl)("/api/whisper/transcribe"),{method:"POST",body:c});if(!g.ok){let a=await g.json().catch(()=>null);throw Error(a?.error||`HTTPエラー: ${g.status}`)}let j=await g.json();j.warning&&i.toaster.create({type:"error",title:"LLM整形をスキップしました",description:j.warning}),j.text&&a(j.text)}catch(a){i.toaster.create({type:"error",title:"文字起こしに失敗しました",description:a instanceof Error?a.message:String(a)})}finally{m("idle")}},[a]),G=(0,c.useCallback)(async()=>{try{let a=await navigator.mediaDevices.getUserMedia({audio:!0});u.current=a,t.current=[],E(a);let b=new MediaRecorder(a,{audioBitsPerSecond:128e3});b.ondataavailable=a=>{a.data.size>0&&t.current.push(a.data)},b.onstop=()=>{u.current?.getTracks().forEach(a=>a.stop()),u.current=null;let a=C.current;if(D(),a<.15){i.toaster.create({type:"error",title:"音声が検出されませんでした",description:"マイクに向かって話してから、もう一度お試しください。"}),m("idle");return}let b=new Blob(t.current,{type:"audio/webm"});F(b)},r.current=b,b.start(),m("recording")}catch(a){i.toaster.create({type:"error",title:"マイクにアクセスできませんでした",description:a instanceof Error?a.message:String(a)})}},[F,E,D]),H=(0,c.useCallback)(()=>{r.current?.stop()},[]);if(!j)return null;let I=null!==p&&A.includes(p);return(0,b.jsxs)("div",{className:"recording"===l?"inline-flex items-center gap-2 rounded-md bg-destructive/10 p-1":"inline-flex",children:[(0,b.jsx)(g.Button,{size:"icon",variant:"recording"===l?"destructive":"default",onClick:"recording"===l?H:()=>void G(),disabled:"transcribing"===l||"idle"===l&&!I,title:"recording"===l?"停止して入力":"idle"!==l||I?"音声入力":"Whisperサーバーが起動していません(Settingsから起動してください)",children:"transcribing"===l?(0,b.jsx)(d.Loader2,{className:"w-4 h-4 animate-spin"}):"recording"===l?(0,b.jsx)(f.Square,{className:"w-4 h-4"}):(0,b.jsx)(e.Mic,{className:"w-4 h-4"})}),"recording"===l&&(0,b.jsx)("div",{"aria-hidden":!0,className:"h-1.5 w-16 overflow-hidden rounded-full bg-background/60",children:(0,b.jsx)("div",{className:"h-full rounded-full bg-destructive",style:{width:`${Math.round(100*n)}%`}})})]})}},11989,a=>{"use strict";let b=(0,a.i(60137).default)("chevron-right",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);a.s(["default",0,b])}];
2
-
3
- //# sourceMappingURL=_0lpms0u._.js.map
@@ -1,3 +0,0 @@
1
- module.exports=[210,a=>{"use strict";let b=(0,a.i(60137).default)("arrow-left",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]);a.s(["ArrowLeft",0,b],210)},11989,a=>{"use strict";let b=(0,a.i(60137).default)("chevron-right",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);a.s(["default",0,b])},99464,a=>{"use strict";let b=(0,a.i(60137).default)("pencil",[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]]);a.s(["Pencil",0,b],99464)},92e3,a=>{"use strict";let b=(0,a.i(60137).default)("circle-alert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);a.s(["AlertCircle",0,b],92e3)},36620,a=>{"use strict";let b=(0,a.i(60137).default)("circle-question-mark",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);a.s(["CircleHelp",0,b],36620)},4975,48124,a=>{"use strict";var b=a.i(60137);let c=(0,b.default)("mic",[["path",{d:"M12 19v3",key:"npa21l"}],["path",{d:"M19 10v2a7 7 0 0 1-14 0v-2",key:"1vc78b"}],["rect",{x:"9",y:"2",width:"6",height:"13",rx:"3",key:"s6n7sd"}]]);a.s(["Mic",0,c],4975);let d=(0,b.default)("square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);a.s(["Square",0,d],48124)},92552,a=>{"use strict";var b=a.i(87924),c=a.i(760);a.s(["Card",0,function({className:a,size:d="default",...e}){return(0,b.jsx)("div",{"data-slot":"card","data-size":d,className:(0,c.cn)("group/card flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",a),...e})},"CardContent",0,function({className:a,...d}){return(0,b.jsx)("div",{"data-slot":"card-content",className:(0,c.cn)("px-4 group-data-[size=sm]/card:px-3",a),...d})},"CardDescription",0,function({className:a,...d}){return(0,b.jsx)("div",{"data-slot":"card-description",className:(0,c.cn)("text-xs/relaxed text-muted-foreground",a),...d})},"CardHeader",0,function({className:a,...d}){return(0,b.jsx)("div",{"data-slot":"card-header",className:(0,c.cn)("group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",a),...d})},"CardTitle",0,function({className:a,...d}){return(0,b.jsx)("div",{"data-slot":"card-title",className:(0,c.cn)("font-heading text-sm font-medium",a),...d})}])},82434,a=>{"use strict";let b=(0,a.i(60137).default)("bot",[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]]);a.s(["Bot",0,b],82434)},56026,19768,a=>{"use strict";a.s(["LLM_SYSTEM_PROMPT_STORAGE_KEY",()=>z,"VoiceInputButton",()=>B,"WHISPER_PROMPT_STORAGE_KEY",()=>y],56026);var b=a.i(87924),c=a.i(72131),d=a.i(96221),e=a.i(4975),f=a.i(48124),g=a.i(39524),h=a.i(91399),i=a.i(6828);a.s(["LOCAL_LLM_ENABLED_STORAGE_KEY",()=>s,"LocalLlmSection",()=>x],19768);var j=a.i(92e3),k=a.i(82434),l=a.i(67453),m=a.i(36620),n=a.i(92552),o=a.i(90002),p=a.i(65025),q=a.i(97463);let r="tsunagi:local-llm-enabled",s=r,t={not_running:"停止中",installing_deps:"依存関係をインストール中...",downloading_model:"モデルをダウンロード中...",starting_server:"サーバーを起動中... (モデルのロードに数十秒かかる場合があります)",running:"実行中",running_external:"実行中 (tsunagi外で起動)",error:"エラー"},u=["installing_deps","downloading_model","starting_server"],v=["running","running_external"];function w(a){let b=a/0x40000000;return b>=.1?`${b.toFixed(2)} GB`:`${(a/1048576).toFixed(0)} MB`}function x(){let[a,e]=(0,c.useState)(!1),[s,x]=(0,c.useState)(!1),[y,A]=(0,c.useState)(null),[B,C]=(0,c.useState)(""),D=(0,c.useRef)(null),E=(0,c.useCallback)(a=>{e(a),localStorage.setItem(r,String(a))},[]),F=(0,c.useCallback)(a=>{C(a),localStorage.setItem(z,a)},[]),G=(0,c.useCallback)(async()=>{let a=await fetch((0,h.apiUrl)("/api/llm/server/status")),b=await a.json();return A(b),b},[]);(0,c.useEffect)(()=>{e("true"===localStorage.getItem(r)),C(localStorage.getItem(z)??""),G()},[G]),(0,c.useEffect)(()=>{y&&v.includes(y.step)&&!a&&(E(!0),i.toaster.create({type:"success",title:"ローカルLLMが有効化されました"}))},[y,a,E]),(0,c.useEffect)(()=>(y&&u.includes(y.step)&&!D.current&&(D.current=setInterval(()=>{G().then(a=>{!u.includes(a.step)&&D.current&&(clearInterval(D.current),D.current=null)})},1e3)),()=>{D.current&&(clearInterval(D.current),D.current=null)}),[y,G]);let H=(0,c.useCallback)(()=>{G(),x(!0)},[G]),I=(0,c.useCallback)(async()=>{try{let a=await fetch((0,h.apiUrl)("/api/llm/server/start"),{method:"POST"});if(!a.ok){let b=await a.json().catch(()=>null);throw Error(b?.error||`HTTPエラー: ${a.status}`)}A(await a.json())}catch(a){i.toaster.create({type:"error",title:"サーバーの起動に失敗しました",description:a instanceof Error?a.message:String(a)})}},[]),J=(0,c.useCallback)(async()=>{try{let a=await fetch((0,h.apiUrl)("/api/llm/server/stop"),{method:"POST"});if(!a.ok){let b=await a.json().catch(()=>null);throw Error(b?.error||`HTTPエラー: ${a.status}`)}A(await a.json())}catch(a){i.toaster.create({type:"error",title:"サーバーの停止に失敗しました",description:a instanceof Error?a.message:String(a)})}},[]),K=(0,c.useCallback)(()=>{E(!1),y&&v.includes(y.step)&&J()},[E,J,y]),L=y?.serverDir,M=y?.downloadProgress,N=M?M.downloadedBytes/M.totalBytes*100:0,O=null!==y,P=O&&v.includes(y.step);return(0,b.jsxs)(n.Card,{children:[(0,b.jsxs)(n.CardHeader,{children:[(0,b.jsxs)("div",{className:"flex items-center gap-1",children:[(0,b.jsx)(n.CardTitle,{children:"ローカルLLM (実験的機能)"}),(0,b.jsx)(g.Button,{variant:"ghost",size:"icon-sm",onClick:H,title:"ローカルLLMについて",children:(0,b.jsx)(m.CircleHelp,{})})]}),(0,b.jsxs)(n.CardDescription,{children:["ローカルで動作するLLM(Qwen3-30B-A3B, MoE)を利用します。",(0,b.jsx)("strong",{className:"font-medium text-foreground",children:"有効にすると、音声入力の文字起こし結果がこのLLMで自動整形されるようになります"}),"(無効時は文字起こし結果をそのまま使用)。"]})]}),(0,b.jsx)(n.CardContent,{className:"flex flex-col gap-3",children:a?(0,b.jsxs)(b.Fragment,{children:[(0,b.jsxs)("div",{className:"flex flex-wrap items-center gap-2",children:[O?P?(0,b.jsxs)("span",{className:"flex items-center gap-2 text-success",children:[(0,b.jsx)(l.CheckCircle2,{className:"size-4"}),"ローカルLLM: 有効"]}):(0,b.jsxs)("span",{className:"flex items-center gap-2 text-warning",children:[(0,b.jsx)(j.AlertCircle,{className:"size-4"}),"ローカルLLM: 有効化済み(サーバー停止中)"]}):(0,b.jsxs)("span",{className:"flex items-center gap-2 text-muted-foreground",children:[(0,b.jsx)(d.Loader2,{className:"size-4 animate-spin"}),"状態を確認中..."]}),y&&(0,b.jsxs)("span",{className:"text-xs/relaxed text-muted-foreground",children:["(",t[y.step],")"]}),O&&!P&&(0,b.jsxs)(g.Button,{size:"default",onClick:H,children:[(0,b.jsx)(k.Bot,{}),"サーバーを起動"]}),(0,b.jsx)(g.Button,{size:"default",variant:"outline",onClick:K,children:"無効にする"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("label",{className:"mb-1 block text-xs font-medium text-muted-foreground",children:"整形システムプロンプト (任意)"}),(0,b.jsx)(q.Textarea,{value:B,onChange:a=>F(a.target.value),placeholder:"例: 句読点の追加と誤字修正のみ行い、言い回しは変えないでください。",className:"min-h-16 text-xs"}),(0,b.jsx)("p",{className:"mt-1 text-[0.65rem] text-muted-foreground",children:"文字起こし結果をLLMで整形する際の指示です。空欄の場合は既定のプロンプト(句読点の追加・誤字修正のみを行う指示)を使用します。"})]})]}):(0,b.jsxs)(g.Button,{size:"default",onClick:H,children:[(0,b.jsx)(k.Bot,{}),"ローカルLLMを有効化する"]})}),(0,b.jsx)(o.Dialog,{open:s,onOpenChange:({open:a})=>x(a),title:"ローカルLLMについて",maxWidth:"2xl",children:(0,b.jsxs)("div",{className:"space-y-4 text-sm",children:[(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"font-medium text-foreground",children:"実験的機能です"}),(0,b.jsx)("p",{className:"text-muted-foreground",children:"音声入力の文字起こし結果を、ローカルで動作するLLM(mlx-lm / Qwen3-30B-A3B, MoE構成)で整形します。MoE構成のため実計算に使うアクティブパラメータは約3Bで、denseな同規模モデルより高速に動作します。精度・速度は環境に依存し、今後変更される可能性があります。"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"font-medium text-foreground",children:"対応環境"}),(0,b.jsx)("p",{className:"text-muted-foreground",children:"Apple Silicon Mac (M1/M2/M3/M4) のみ対応。Windows/Linuxでは利用できません。メモリ32GB以上を推奨します。"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"font-medium text-foreground",children:"必要なもの(事前に手動でインストール)"}),(0,b.jsx)("ul",{className:"list-disc pl-5 text-muted-foreground",children:(0,b.jsx)("li",{children:"Python 3.9以降(Xcode Command Line Tools または Homebrew経由で入手可能)"})})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"mb-1 font-medium text-foreground",children:"セットアップ・起動"}),(0,b.jsx)("p",{className:"text-muted-foreground",children:"下のボタンから、依存関係のインストール・モデルのダウンロード・サーバー起動まで自動で行われます(初回はモデルサイズが大きく数分〜数十分かかります)。"}),y&&!L&&(0,b.jsx)("p",{className:"mt-1 text-destructive",children:"llm-serverが見つかりませんでした。インストールが壊れている可能性があります。"})]}),(0,b.jsxs)("div",{children:[(0,b.jsx)("p",{className:"mb-1 font-medium text-foreground",children:"アンインストール"}),(0,b.jsxs)("p",{className:"text-muted-foreground",children:["ローカルLLMのためにダウンロードされるもの(Pythonの依存関係・LLMモデル、合計約17GB)は全て",(0,b.jsx)("code",{className:"rounded bg-muted px-1",children:"~/.tsunagi/llm"}),"に保存されます。不要になった場合はこのディレクトリを削除するだけで、関連リソースを完全に削除できます。"]})]}),(0,b.jsxs)("div",{className:"border-t border-border pt-4",children:[(!y||"not_running"===y.step||"error"===y.step)&&(0,b.jsxs)("div",{className:"flex flex-col gap-2",children:[(0,b.jsxs)(g.Button,{size:"default",onClick:()=>void I(),disabled:!y,children:[y?(0,b.jsx)(k.Bot,{}):(0,b.jsx)(d.Loader2,{className:"animate-spin"}),"LLMサーバーを起動"]}),y?.step==="error"&&y.error&&(0,b.jsx)("p",{className:"text-xs/relaxed text-destructive",children:y.error})]}),y?.step==="installing_deps"&&(0,b.jsxs)(g.Button,{size:"default",disabled:!0,children:[(0,b.jsx)(d.Loader2,{className:"animate-spin"}),"依存関係をインストール中... (数分かかる場合があります)"]}),y?.step==="downloading_model"&&(0,b.jsxs)("div",{className:"flex flex-col gap-2",children:[(0,b.jsxs)("div",{className:"flex items-center gap-2 text-xs/relaxed text-muted-foreground",children:[(0,b.jsx)(d.Loader2,{className:"size-4 animate-spin"}),"モデルをダウンロード中... (約17GB)"]}),M&&(0,b.jsxs)(b.Fragment,{children:[(0,b.jsx)(p.Progress,{value:N}),(0,b.jsxs)("p",{className:"text-xs/relaxed text-muted-foreground",children:[w(M.downloadedBytes)," / ",w(M.totalBytes)," (",N.toFixed(0),"%) ・ ",function(a){if(null===a)return"残り時間を計算中...";if(a<60)return`残り約${a}秒`;let b=Math.floor(a/60);return`残り約${b}分${a%60}秒`}(M.etaSeconds)]})]})]}),y?.step==="starting_server"&&(0,b.jsxs)(g.Button,{size:"default",disabled:!0,children:[(0,b.jsx)(d.Loader2,{className:"animate-spin"}),"サーバーを起動中... (モデルのロードに数十秒かかる場合があります)"]}),y&&v.includes(y.step)&&(0,b.jsxs)("div",{className:"flex flex-wrap items-center gap-2",children:[(0,b.jsxs)("span",{className:"flex items-center gap-2 text-success",children:[(0,b.jsx)(l.CheckCircle2,{className:"size-4"}),"サーバーは起動しています"]}),(0,b.jsxs)(g.Button,{size:"default",variant:"outline",onClick:()=>void J(),children:[(0,b.jsx)(f.Square,{}),"サーバーを停止"]})]})]})]})})]})}let y="tsunagi:whisper-prompt",z="tsunagi:llm-system-prompt",A=["running","running_external"];function B({onTranscribed:a}){let[j,k]=(0,c.useState)(!1),[l,m]=(0,c.useState)("idle"),[n,o]=(0,c.useState)(0),[p,q]=(0,c.useState)(null),r=(0,c.useRef)(null),t=(0,c.useRef)([]),u=(0,c.useRef)(null),v=(0,c.useRef)(null),w=(0,c.useRef)(null),x=(0,c.useRef)(0),C=(0,c.useRef)(0);(0,c.useEffect)(()=>{k("true"===localStorage.getItem("tsunagi:voice-input-enabled"))},[]),(0,c.useEffect)(()=>{if(!j)return;let a=!1,b=async()=>{try{let b=await fetch((0,h.apiUrl)("/api/whisper/server/status")),c=await b.json();a||q(c.step)}catch{a||q("not_running")}};b();let c=setInterval(b,5e3);return()=>{a=!0,clearInterval(c)}},[j]);let D=(0,c.useCallback)(()=>{w.current&&(clearInterval(w.current),w.current=null),v.current?.close(),v.current=null,o(0)},[]);(0,c.useEffect)(()=>()=>{D()},[D]);let E=(0,c.useCallback)(a=>{let b=new AudioContext,c=b.createMediaStreamSource(a),d=b.createAnalyser();d.fftSize=2048,c.connect(d),v.current=b,x.current=0,C.current=0;let e=new Float32Array(d.fftSize);w.current=setInterval(()=>{d.getFloatTimeDomainData(e);let a=0;for(let b=0;b<e.length;b++)a+=e[b]*e[b];let b=Math.sqrt(a/e.length),c=Math.min(1,Math.max(0,((b>0?20*Math.log10(b):-60)- -60)/50));C.current=Math.max(C.current,c),x.current=.3*x.current+.7*c,o(x.current)},50)},[]),F=(0,c.useCallback)(async b=>{m("transcribing");try{let c=new FormData,d=localStorage.getItem(y);d&&c.append("prompt",d);let e="true"===localStorage.getItem(s);c.append("useLlm",String(e));let f=localStorage.getItem(z);f&&c.append("systemPrompt",f),c.append("file",b,"recording.webm");let g=await fetch((0,h.apiUrl)("/api/whisper/transcribe"),{method:"POST",body:c});if(!g.ok){let a=await g.json().catch(()=>null);throw Error(a?.error||`HTTPエラー: ${g.status}`)}let j=await g.json();j.warning&&i.toaster.create({type:"error",title:"LLM整形をスキップしました",description:j.warning}),j.text&&a(j.text)}catch(a){i.toaster.create({type:"error",title:"文字起こしに失敗しました",description:a instanceof Error?a.message:String(a)})}finally{m("idle")}},[a]),G=(0,c.useCallback)(async()=>{try{let a=await navigator.mediaDevices.getUserMedia({audio:!0});u.current=a,t.current=[],E(a);let b=new MediaRecorder(a,{audioBitsPerSecond:128e3});b.ondataavailable=a=>{a.data.size>0&&t.current.push(a.data)},b.onstop=()=>{u.current?.getTracks().forEach(a=>a.stop()),u.current=null;let a=C.current;if(D(),a<.15){i.toaster.create({type:"error",title:"音声が検出されませんでした",description:"マイクに向かって話してから、もう一度お試しください。"}),m("idle");return}let b=new Blob(t.current,{type:"audio/webm"});F(b)},r.current=b,b.start(),m("recording")}catch(a){i.toaster.create({type:"error",title:"マイクにアクセスできませんでした",description:a instanceof Error?a.message:String(a)})}},[F,E,D]),H=(0,c.useCallback)(()=>{r.current?.stop()},[]);if(!j)return null;let I=null!==p&&A.includes(p);return(0,b.jsxs)("div",{className:"recording"===l?"inline-flex items-center gap-2 rounded-md bg-destructive/10 p-1":"inline-flex",children:[(0,b.jsx)(g.Button,{size:"icon",variant:"recording"===l?"destructive":"default",onClick:"recording"===l?H:()=>void G(),disabled:"transcribing"===l||"idle"===l&&!I,title:"recording"===l?"停止して入力":"idle"!==l||I?"音声入力":"Whisperサーバーが起動していません(Settingsから起動してください)",children:"transcribing"===l?(0,b.jsx)(d.Loader2,{className:"w-4 h-4 animate-spin"}):"recording"===l?(0,b.jsx)(f.Square,{className:"w-4 h-4"}):(0,b.jsx)(e.Mic,{className:"w-4 h-4"})}),"recording"===l&&(0,b.jsx)("div",{"aria-hidden":!0,className:"h-1.5 w-16 overflow-hidden rounded-full bg-background/60",children:(0,b.jsx)("div",{className:"h-full rounded-full bg-destructive",style:{width:`${Math.round(100*n)}%`}})})]})}}];
2
-
3
- //# sourceMappingURL=_0owas.l._.js.map