waypoi 0.0.0

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 (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,148 @@
1
+ export type PeekEmbeddedMedia = {
2
+ path: string
3
+ source: 'request' | 'response'
4
+ mime: string
5
+ kind: 'image' | 'audio' | 'binary'
6
+ url: string
7
+ origin: 'b64_json' | 'data_url'
8
+ sizeHint: number
9
+ }
10
+
11
+ type MediaSource = PeekEmbeddedMedia['source']
12
+
13
+ const DATA_URL_RE = /^data:([^;,]+)(?:;[^,]*)?,(.*)$/i
14
+
15
+ export function extractEmbeddedMedia(source: MediaSource, value: unknown): PeekEmbeddedMedia[] {
16
+ const results: PeekEmbeddedMedia[] = []
17
+ const seen = new Set<string>()
18
+ walkValue(value, source, '$', results, seen)
19
+ return results
20
+ }
21
+
22
+ export function redactEmbeddedMedia(value: unknown): unknown {
23
+ return redactValue(value)
24
+ }
25
+
26
+ function walkValue(
27
+ value: unknown,
28
+ source: MediaSource,
29
+ path: string,
30
+ results: PeekEmbeddedMedia[],
31
+ seen: Set<string>,
32
+ ): void {
33
+ if (typeof value === 'string') {
34
+ const parsed = parseDataUrl(value)
35
+ if (parsed) {
36
+ pushMedia(results, seen, {
37
+ path,
38
+ source,
39
+ mime: parsed.mime,
40
+ kind: kindFromMime(parsed.mime),
41
+ url: value,
42
+ origin: 'data_url',
43
+ sizeHint: parsed.payload.length,
44
+ })
45
+ }
46
+ return
47
+ }
48
+
49
+ if (Array.isArray(value)) {
50
+ value.forEach((item, index) => walkValue(item, source, `${path}[${index}]`, results, seen))
51
+ return
52
+ }
53
+
54
+ if (!value || typeof value !== 'object') {
55
+ return
56
+ }
57
+
58
+ const record = value as Record<string, unknown>
59
+ const b64Json = typeof record.b64_json === 'string' ? record.b64_json : null
60
+ const siblingDataUrl = findSiblingDataUrl(record)
61
+ if (b64Json) {
62
+ const mime = siblingDataUrl?.mime ?? inferMime(record) ?? 'image/png'
63
+ pushMedia(results, seen, {
64
+ path: `${path}.b64_json`,
65
+ source,
66
+ mime,
67
+ kind: kindFromMime(mime),
68
+ url: siblingDataUrl?.url ?? `data:${mime};base64,${b64Json}`,
69
+ origin: 'b64_json',
70
+ sizeHint: b64Json.length,
71
+ })
72
+ }
73
+
74
+ for (const [key, child] of Object.entries(record)) {
75
+ if (b64Json && siblingDataUrl?.url === child && (key === 'url' || key === 'image_url' || key === 'audio_url')) {
76
+ continue
77
+ }
78
+ walkValue(child, source, `${path}.${key}`, results, seen)
79
+ }
80
+ }
81
+
82
+ function redactValue(value: unknown): unknown {
83
+ if (typeof value === 'string') {
84
+ const parsed = parseDataUrl(value)
85
+ if (!parsed) return value
86
+ return `[data URL omitted: ${parsed.mime}, ${parsed.payload.length} chars]`
87
+ }
88
+ if (Array.isArray(value)) {
89
+ return value.map(redactValue)
90
+ }
91
+ if (!value || typeof value !== 'object') {
92
+ return value
93
+ }
94
+
95
+ const record = value as Record<string, unknown>
96
+ const next: Record<string, unknown> = {}
97
+ const inferredMime = inferMime(record) ?? findSiblingDataUrl(record)?.mime ?? 'image/png'
98
+ for (const [key, child] of Object.entries(record)) {
99
+ if (key === 'b64_json' && typeof child === 'string') {
100
+ next[key] = `[base64 media omitted: ${inferredMime}, ${child.length} chars]`
101
+ continue
102
+ }
103
+ next[key] = redactValue(child)
104
+ }
105
+ return next
106
+ }
107
+
108
+ function pushMedia(results: PeekEmbeddedMedia[], seen: Set<string>, media: PeekEmbeddedMedia) {
109
+ const key = `${media.source}:${media.path}:${media.url}`
110
+ if (seen.has(key)) return
111
+ seen.add(key)
112
+ results.push(media)
113
+ }
114
+
115
+ function findSiblingDataUrl(record: Record<string, unknown>): { mime: string; payload: string; url: string } | null {
116
+ for (const key of ['url', 'image_url', 'audio_url']) {
117
+ const value = record[key]
118
+ if (typeof value !== 'string') continue
119
+ const parsed = parseDataUrl(value)
120
+ if (parsed) return { ...parsed, url: value }
121
+ }
122
+ return null
123
+ }
124
+
125
+ function inferMime(record: Record<string, unknown>): string | null {
126
+ for (const key of ['mime', 'mime_type', 'content_type', 'media_type']) {
127
+ const value = record[key]
128
+ if (typeof value === 'string' && value.includes('/')) {
129
+ return value
130
+ }
131
+ }
132
+ return null
133
+ }
134
+
135
+ function parseDataUrl(value: string): { mime: string; payload: string } | null {
136
+ const match = value.match(DATA_URL_RE)
137
+ if (!match) return null
138
+ return {
139
+ mime: match[1].trim().toLowerCase(),
140
+ payload: match[2],
141
+ }
142
+ }
143
+
144
+ function kindFromMime(mime: string): PeekEmbeddedMedia['kind'] {
145
+ if (mime.startsWith('image/')) return 'image'
146
+ if (mime.startsWith('audio/')) return 'audio'
147
+ return 'binary'
148
+ }
@@ -0,0 +1,128 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ applyAutoTitleToSessions,
5
+ createDeferredAutoTitleCandidate,
6
+ flushDeferredAutoTitle,
7
+ isDefaultSessionName,
8
+ } from './sessionAutoTitle'
9
+
10
+ test('recognizes default session names', () => {
11
+ assert.equal(isDefaultSessionName('Session 3/17/2026'), true)
12
+ assert.equal(isDefaultSessionName('Trip planning'), false)
13
+ })
14
+
15
+ test('creates deferred title candidate only for default sessions with text', () => {
16
+ assert.deepEqual(
17
+ createDeferredAutoTitleCandidate('session-1', 'Session 3/17/2026', ' hello world '),
18
+ { sessionId: 'session-1', seedText: 'hello world' }
19
+ )
20
+ assert.equal(createDeferredAutoTitleCandidate('session-1', 'Trip planning', 'hello world'), null)
21
+ assert.equal(createDeferredAutoTitleCandidate('session-1', 'Session 3/17/2026', ' '), null)
22
+ assert.equal(createDeferredAutoTitleCandidate(null, 'Session 3/17/2026', 'hello world'), null)
23
+ })
24
+
25
+ test('flushDeferredAutoTitle runs once and updates status lifecycle', async () => {
26
+ const generationStates: Array<string | null> = []
27
+ const resolved: Array<{ name: string }> = []
28
+ let cleared = 0
29
+ const calls: Array<{ sessionId: string; model?: string; seedText?: string }> = []
30
+
31
+ const didRun = await flushDeferredAutoTitle({
32
+ sessionId: 'session-1',
33
+ sessionName: 'Session 3/17/2026',
34
+ model: 'gpt-test',
35
+ queuedCandidate: { sessionId: 'session-1', seedText: 'hello world' },
36
+ generatingSessionId: null,
37
+ autoTitleSession: async (sessionId, payload) => {
38
+ calls.push({ sessionId, ...payload })
39
+ return { name: 'Hello world summary', titleStatus: 'generated' }
40
+ },
41
+ onGenerationChange: (sessionId) => generationStates.push(sessionId),
42
+ onResolved: (response) => resolved.push({ name: response.name }),
43
+ clearQueuedCandidate: () => {
44
+ cleared += 1
45
+ },
46
+ })
47
+
48
+ assert.equal(didRun, true)
49
+ assert.deepEqual(calls, [
50
+ { sessionId: 'session-1', model: 'gpt-test', seedText: 'hello world' },
51
+ ])
52
+ assert.deepEqual(generationStates, ['session-1', null])
53
+ assert.deepEqual(resolved, [{ name: 'Hello world summary' }])
54
+ assert.equal(cleared, 1)
55
+ })
56
+
57
+ test('flushDeferredAutoTitle skips duplicate or stale candidates', async () => {
58
+ let called = false
59
+
60
+ const staleRun = await flushDeferredAutoTitle({
61
+ sessionId: 'session-1',
62
+ sessionName: 'Session 3/17/2026',
63
+ queuedCandidate: { sessionId: 'session-2', seedText: 'hello world' },
64
+ generatingSessionId: null,
65
+ autoTitleSession: async () => {
66
+ called = true
67
+ return { name: 'unused' }
68
+ },
69
+ onGenerationChange: () => undefined,
70
+ onResolved: () => undefined,
71
+ clearQueuedCandidate: () => undefined,
72
+ })
73
+
74
+ const duplicateRun = await flushDeferredAutoTitle({
75
+ sessionId: 'session-1',
76
+ sessionName: 'Session 3/17/2026',
77
+ queuedCandidate: { sessionId: 'session-1', seedText: 'hello world' },
78
+ generatingSessionId: 'session-1',
79
+ autoTitleSession: async () => {
80
+ called = true
81
+ return { name: 'unused' }
82
+ },
83
+ onGenerationChange: () => undefined,
84
+ onResolved: () => undefined,
85
+ clearQueuedCandidate: () => undefined,
86
+ })
87
+
88
+ assert.equal(staleRun, false)
89
+ assert.equal(duplicateRun, false)
90
+ assert.equal(called, false)
91
+ })
92
+
93
+ test('flushDeferredAutoTitle clears status on failure and non-default names do not retry', async () => {
94
+ const generationStates: Array<string | null> = []
95
+ let cleared = 0
96
+ const errors: unknown[] = []
97
+
98
+ const failedRun = await flushDeferredAutoTitle({
99
+ sessionId: 'session-1',
100
+ sessionName: 'Session 3/17/2026',
101
+ queuedCandidate: { sessionId: 'session-1', seedText: 'hello world' },
102
+ generatingSessionId: null,
103
+ autoTitleSession: async () => {
104
+ throw new Error('boom')
105
+ },
106
+ onGenerationChange: (sessionId) => generationStates.push(sessionId),
107
+ onResolved: () => undefined,
108
+ clearQueuedCandidate: () => {
109
+ cleared += 1
110
+ },
111
+ onError: (error) => errors.push(error),
112
+ })
113
+
114
+ assert.equal(failedRun, false)
115
+ assert.deepEqual(generationStates, ['session-1', null])
116
+ assert.equal(cleared, 1)
117
+ assert.equal(errors.length, 1)
118
+
119
+ const sessions = applyAutoTitleToSessions(
120
+ [{ id: 'session-1', name: 'Session 3/17/2026', messageCount: 1, createdAt: '', updatedAt: '' }],
121
+ 'session-1',
122
+ { name: 'Trip planning', titleStatus: 'generated', titleUpdatedAt: 'now' }
123
+ )
124
+ assert.equal(
125
+ createDeferredAutoTitleCandidate('session-1', sessions[0].name, 'another message'),
126
+ null
127
+ )
128
+ })
@@ -0,0 +1,106 @@
1
+ import type { SessionListItem } from '../api/client'
2
+
3
+ export interface DeferredAutoTitleCandidate {
4
+ sessionId: string
5
+ seedText: string
6
+ }
7
+
8
+ export interface AutoTitleSessionResponse {
9
+ name: string
10
+ titleStatus?: 'pending' | 'generated' | 'manual' | 'failed'
11
+ titleUpdatedAt?: string
12
+ }
13
+
14
+ export const DEFAULT_SESSION_NAME_PATTERN = /^Session\s+\d{1,2}\/\d{1,2}\/\d{2,4}$/
15
+
16
+ export function isDefaultSessionName(sessionName: string): boolean {
17
+ return DEFAULT_SESSION_NAME_PATTERN.test(sessionName)
18
+ }
19
+
20
+ export function createDeferredAutoTitleCandidate(
21
+ sessionId: string | null,
22
+ sessionName: string,
23
+ seedText: string
24
+ ): DeferredAutoTitleCandidate | null {
25
+ const trimmed = seedText.trim()
26
+ if (!sessionId || !trimmed || !isDefaultSessionName(sessionName)) {
27
+ return null
28
+ }
29
+ return {
30
+ sessionId,
31
+ seedText: trimmed,
32
+ }
33
+ }
34
+
35
+ export function applyAutoTitleToSessions(
36
+ sessions: SessionListItem[],
37
+ sessionId: string,
38
+ response: AutoTitleSessionResponse
39
+ ): SessionListItem[] {
40
+ return sessions.map((item) =>
41
+ item.id === sessionId
42
+ ? {
43
+ ...item,
44
+ name: response.name,
45
+ titleStatus: response.titleStatus,
46
+ titleUpdatedAt: response.titleUpdatedAt,
47
+ }
48
+ : item
49
+ )
50
+ }
51
+
52
+ interface FlushDeferredAutoTitleArgs {
53
+ sessionId: string
54
+ sessionName: string
55
+ model?: string
56
+ queuedCandidate: DeferredAutoTitleCandidate | null
57
+ generatingSessionId: string | null
58
+ autoTitleSession: (
59
+ sessionId: string,
60
+ payload: { model?: string; seedText?: string }
61
+ ) => Promise<AutoTitleSessionResponse>
62
+ onGenerationChange: (sessionId: string | null) => void
63
+ onResolved: (response: AutoTitleSessionResponse) => void
64
+ clearQueuedCandidate: () => void
65
+ onError?: (error: unknown) => void
66
+ }
67
+
68
+ export async function flushDeferredAutoTitle({
69
+ sessionId,
70
+ sessionName,
71
+ model,
72
+ queuedCandidate,
73
+ generatingSessionId,
74
+ autoTitleSession,
75
+ onGenerationChange,
76
+ onResolved,
77
+ clearQueuedCandidate,
78
+ onError,
79
+ }: FlushDeferredAutoTitleArgs): Promise<boolean> {
80
+ if (!queuedCandidate || queuedCandidate.sessionId !== sessionId) {
81
+ return false
82
+ }
83
+ if (generatingSessionId === sessionId) {
84
+ return false
85
+ }
86
+ if (!isDefaultSessionName(sessionName)) {
87
+ clearQueuedCandidate()
88
+ return false
89
+ }
90
+
91
+ clearQueuedCandidate()
92
+ onGenerationChange(sessionId)
93
+ try {
94
+ const response = await autoTitleSession(sessionId, {
95
+ model,
96
+ seedText: queuedCandidate.seedText,
97
+ })
98
+ onResolved(response)
99
+ return true
100
+ } catch (error) {
101
+ onError?.(error)
102
+ return false
103
+ } finally {
104
+ onGenerationChange(null)
105
+ }
106
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Settings Store
3
+ *
4
+ * Manages user preferences with localStorage persistence.
5
+ */
6
+
7
+ export type ImageSize = '256x256' | '512x512' | '1024x1024' | '1024x1792' | '1792x1024'
8
+
9
+ export interface UserSettings {
10
+ defaultImageSize: ImageSize
11
+ // Future settings can be added here
12
+ }
13
+
14
+ const STORAGE_KEY = 'waypoi-settings'
15
+
16
+ const DEFAULT_SETTINGS: UserSettings = {
17
+ defaultImageSize: '1024x1024',
18
+ }
19
+
20
+ // Available image size options
21
+ export const IMAGE_SIZE_OPTIONS: { value: ImageSize; label: string; aspect: string }[] = [
22
+ { value: '256x256', label: '256×256', aspect: '1:1' },
23
+ { value: '512x512', label: '512×512', aspect: '1:1' },
24
+ { value: '1024x1024', label: '1024×1024', aspect: '1:1' },
25
+ { value: '1024x1792', label: '1024×1792', aspect: '9:16 (Portrait)' },
26
+ { value: '1792x1024', label: '1792×1024', aspect: '16:9 (Landscape)' },
27
+ ]
28
+
29
+ export function loadSettings(): UserSettings {
30
+ try {
31
+ const stored = localStorage.getItem(STORAGE_KEY)
32
+ if (stored) {
33
+ const parsed = JSON.parse(stored)
34
+ return { ...DEFAULT_SETTINGS, ...parsed }
35
+ }
36
+ } catch (error) {
37
+ console.error('Failed to load settings:', error)
38
+ }
39
+ return DEFAULT_SETTINGS
40
+ }
41
+
42
+ export function saveSettings(settings: UserSettings): void {
43
+ try {
44
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
45
+ } catch (error) {
46
+ console.error('Failed to save settings:', error)
47
+ }
48
+ }
49
+
50
+ export function updateSetting<K extends keyof UserSettings>(
51
+ key: K,
52
+ value: UserSettings[K]
53
+ ): UserSettings {
54
+ const settings = loadSettings()
55
+ settings[key] = value
56
+ saveSettings(settings)
57
+ return settings
58
+ }
@@ -0,0 +1,223 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ @layer base {
8
+ :root {
9
+ /* Waypoi Industrial Dark Theme
10
+ Inspired by control room interfaces and technical dashboards
11
+ Deep charcoal base with amber/gold accents */
12
+
13
+ --background: 220 15% 6%;
14
+ --foreground: 40 15% 92%;
15
+
16
+ --card: 220 15% 8%;
17
+ --card-foreground: 40 15% 92%;
18
+
19
+ --popover: 220 15% 10%;
20
+ --popover-foreground: 40 15% 92%;
21
+
22
+ /* Amber/Gold primary - warm industrial accent */
23
+ --primary: 38 92% 50%;
24
+ --primary-foreground: 220 15% 6%;
25
+
26
+ --secondary: 220 10% 15%;
27
+ --secondary-foreground: 40 10% 75%;
28
+
29
+ --muted: 220 10% 12%;
30
+ --muted-foreground: 220 10% 50%;
31
+
32
+ --accent: 38 70% 45%;
33
+ --accent-foreground: 220 15% 6%;
34
+
35
+ --destructive: 0 72% 51%;
36
+ --destructive-foreground: 0 0% 100%;
37
+
38
+ --success: 142 71% 45%;
39
+ --success-foreground: 142 71% 10%;
40
+
41
+ --warning: 38 92% 50%;
42
+ --warning-foreground: 38 92% 10%;
43
+
44
+ --border: 220 10% 18%;
45
+ --input: 220 10% 14%;
46
+ --ring: 38 92% 50%;
47
+
48
+ --radius: 0.375rem;
49
+ }
50
+
51
+ * {
52
+ @apply border-border;
53
+ }
54
+
55
+ body {
56
+ @apply bg-background text-foreground font-sans antialiased;
57
+ font-feature-settings: "rlig" 1, "calt" 1;
58
+ }
59
+
60
+ /* Custom scrollbar - minimal and dark */
61
+ ::-webkit-scrollbar {
62
+ width: 6px;
63
+ height: 6px;
64
+ }
65
+
66
+ ::-webkit-scrollbar-track {
67
+ @apply bg-transparent;
68
+ }
69
+
70
+ ::-webkit-scrollbar-thumb {
71
+ @apply bg-border rounded-full;
72
+ }
73
+
74
+ ::-webkit-scrollbar-thumb:hover {
75
+ @apply bg-muted-foreground/50;
76
+ }
77
+ }
78
+
79
+ @layer components {
80
+ /* Industrial panel styling */
81
+ .panel {
82
+ @apply bg-card border border-border rounded-md;
83
+ box-shadow:
84
+ inset 0 1px 0 0 hsl(220 10% 20% / 0.5),
85
+ 0 4px 6px -1px hsl(0 0% 0% / 0.3);
86
+ }
87
+
88
+ .panel-header {
89
+ @apply px-4 py-3 border-b border-border flex items-center gap-3;
90
+ background: linear-gradient(
91
+ 180deg,
92
+ hsl(220 15% 10%) 0%,
93
+ hsl(220 15% 8%) 100%
94
+ );
95
+ }
96
+
97
+ .panel-title {
98
+ @apply text-sm font-mono font-medium uppercase tracking-wider text-muted-foreground;
99
+ }
100
+
101
+ /* Status indicators */
102
+ .status-dot {
103
+ @apply w-2 h-2 rounded-full;
104
+ }
105
+
106
+ .status-dot-live {
107
+ @apply bg-success animate-pulse-glow;
108
+ }
109
+
110
+ .status-dot-degraded {
111
+ @apply bg-warning;
112
+ }
113
+
114
+ .status-dot-down {
115
+ @apply bg-destructive;
116
+ }
117
+
118
+ /* Terminal-style input */
119
+ .terminal-input {
120
+ @apply font-mono bg-input border-0 rounded-sm px-3 py-2 text-sm;
121
+ @apply focus:ring-1 focus:ring-primary focus:outline-none;
122
+ @apply placeholder:text-muted-foreground/50;
123
+ }
124
+
125
+ /* Metric card */
126
+ .metric-card {
127
+ @apply panel p-4 flex flex-col gap-1;
128
+ }
129
+
130
+ .metric-label {
131
+ @apply text-2xs font-mono uppercase tracking-widest text-muted-foreground;
132
+ }
133
+
134
+ .metric-value {
135
+ @apply text-2xl font-mono font-semibold text-foreground tabular-nums;
136
+ }
137
+
138
+ .metric-unit {
139
+ @apply text-sm text-muted-foreground ml-1;
140
+ }
141
+
142
+ /* Chat message bubbles */
143
+ .message-user {
144
+ @apply bg-primary/10 border border-primary/20 rounded-lg px-4 py-3;
145
+ }
146
+
147
+ .message-assistant {
148
+ @apply bg-secondary border border-border rounded-lg px-4 py-3;
149
+ }
150
+
151
+ .message-system {
152
+ @apply bg-muted/50 border border-dashed border-border rounded-lg px-4 py-3 text-muted-foreground;
153
+ }
154
+
155
+ /* Code blocks in chat */
156
+ .prose-code {
157
+ @apply font-mono text-sm bg-muted rounded px-1.5 py-0.5;
158
+ }
159
+
160
+ /* Glowing accent for important elements */
161
+ .glow-accent {
162
+ box-shadow: 0 0 20px hsl(38 92% 50% / 0.15);
163
+ }
164
+
165
+ /* Navigation active state */
166
+ .nav-active {
167
+ @apply bg-primary/10 text-primary border-l-2 border-primary;
168
+ }
169
+
170
+ /* Loading skeleton with industrial shimmer */
171
+ .skeleton {
172
+ @apply bg-muted animate-pulse rounded;
173
+ }
174
+ }
175
+
176
+ @layer utilities {
177
+ /* Text gradient for headings */
178
+ .text-gradient {
179
+ @apply bg-clip-text text-transparent;
180
+ background-image: linear-gradient(
181
+ 135deg,
182
+ hsl(40 15% 92%) 0%,
183
+ hsl(38 92% 50%) 100%
184
+ );
185
+ }
186
+
187
+ /* Noise texture overlay */
188
+ .noise-overlay {
189
+ position: relative;
190
+ }
191
+
192
+ .noise-overlay::before {
193
+ content: "";
194
+ position: absolute;
195
+ inset: 0;
196
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
197
+ opacity: 0.03;
198
+ pointer-events: none;
199
+ }
200
+
201
+ /* Grid pattern background */
202
+ .grid-pattern {
203
+ background-image:
204
+ linear-gradient(hsl(220 10% 15% / 0.5) 1px, transparent 1px),
205
+ linear-gradient(90deg, hsl(220 10% 15% / 0.5) 1px, transparent 1px);
206
+ background-size: 20px 20px;
207
+ }
208
+
209
+ /* Scanline effect for retro feel */
210
+ .scanlines::after {
211
+ content: "";
212
+ position: absolute;
213
+ inset: 0;
214
+ background: repeating-linear-gradient(
215
+ 0deg,
216
+ transparent,
217
+ transparent 2px,
218
+ hsl(0 0% 0% / 0.03) 2px,
219
+ hsl(0 0% 0% / 0.03) 4px
220
+ );
221
+ pointer-events: none;
222
+ }
223
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module 'd3-sankey'
4
+
5
+ declare module '*.css' {
6
+ const content: string
7
+ export default content
8
+ }