@kirosnn/mosaic 0.0.7

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 (154) hide show
  1. package/.mosaic/mosaic.local.jsonc +0 -0
  2. package/MOSAIC.md +188 -0
  3. package/README.md +127 -0
  4. package/docs/mosaic.png +0 -0
  5. package/package.json +42 -0
  6. package/src/agent/Agent.ts +131 -0
  7. package/src/agent/context.ts +96 -0
  8. package/src/agent/index.ts +2 -0
  9. package/src/agent/prompts/systemPrompt.ts +138 -0
  10. package/src/agent/prompts/toolsPrompt.ts +139 -0
  11. package/src/agent/provider/anthropic.ts +122 -0
  12. package/src/agent/provider/google.ts +124 -0
  13. package/src/agent/provider/mistral.ts +117 -0
  14. package/src/agent/provider/ollama.ts +531 -0
  15. package/src/agent/provider/openai.ts +220 -0
  16. package/src/agent/provider/xai.ts +122 -0
  17. package/src/agent/tools/bash.ts +20 -0
  18. package/src/agent/tools/definitions.ts +27 -0
  19. package/src/agent/tools/edit.ts +23 -0
  20. package/src/agent/tools/executor.ts +751 -0
  21. package/src/agent/tools/explore.ts +18 -0
  22. package/src/agent/tools/exploreExecutor.ts +320 -0
  23. package/src/agent/tools/glob.ts +16 -0
  24. package/src/agent/tools/grep.ts +19 -0
  25. package/src/agent/tools/index.ts +4 -0
  26. package/src/agent/tools/list.ts +20 -0
  27. package/src/agent/tools/question.ts +20 -0
  28. package/src/agent/tools/read.ts +15 -0
  29. package/src/agent/tools/write.ts +21 -0
  30. package/src/agent/types.ts +155 -0
  31. package/src/components/App.tsx +174 -0
  32. package/src/components/CommandsModal.tsx +77 -0
  33. package/src/components/CustomInput.tsx +328 -0
  34. package/src/components/Main.tsx +1112 -0
  35. package/src/components/Notification.tsx +91 -0
  36. package/src/components/SelectList.tsx +47 -0
  37. package/src/components/Setup.tsx +528 -0
  38. package/src/components/ShortcutsModal.tsx +67 -0
  39. package/src/components/Welcome.tsx +39 -0
  40. package/src/components/main/ApprovalPanel.tsx +134 -0
  41. package/src/components/main/ChatPage.tsx +516 -0
  42. package/src/components/main/HomePage.tsx +111 -0
  43. package/src/components/main/QuestionPanel.tsx +85 -0
  44. package/src/components/main/ThinkingIndicator.tsx +101 -0
  45. package/src/components/main/types.ts +55 -0
  46. package/src/components/main/wrapText.ts +41 -0
  47. package/src/index.tsx +212 -0
  48. package/src/utils/approvalBridge.ts +129 -0
  49. package/src/utils/commands/echo.ts +22 -0
  50. package/src/utils/commands/help.ts +25 -0
  51. package/src/utils/commands/index.ts +68 -0
  52. package/src/utils/commands/init.ts +68 -0
  53. package/src/utils/commands/redo.ts +74 -0
  54. package/src/utils/commands/registry.ts +29 -0
  55. package/src/utils/commands/sessions.ts +129 -0
  56. package/src/utils/commands/types.ts +20 -0
  57. package/src/utils/commands/undo.ts +75 -0
  58. package/src/utils/commands/web.ts +77 -0
  59. package/src/utils/config.ts +357 -0
  60. package/src/utils/diff.ts +201 -0
  61. package/src/utils/diffRendering.tsx +62 -0
  62. package/src/utils/exploreBridge.ts +87 -0
  63. package/src/utils/fileChangeTracker.ts +98 -0
  64. package/src/utils/fileChangesBridge.ts +18 -0
  65. package/src/utils/history.ts +106 -0
  66. package/src/utils/markdown.tsx +232 -0
  67. package/src/utils/models.ts +304 -0
  68. package/src/utils/questionBridge.ts +122 -0
  69. package/src/utils/terminalUtils.ts +25 -0
  70. package/src/utils/toolFormatting.ts +384 -0
  71. package/src/utils/undoRedo.ts +429 -0
  72. package/src/utils/undoRedoBridge.ts +45 -0
  73. package/src/utils/undoRedoDb.ts +338 -0
  74. package/src/utils/uninstall.ts +45 -0
  75. package/src/utils/version.ts +3 -0
  76. package/src/web/app.tsx +606 -0
  77. package/src/web/assets/css/ChatPage.css +212 -0
  78. package/src/web/assets/css/FileExplorer.css +202 -0
  79. package/src/web/assets/css/HomePage.css +119 -0
  80. package/src/web/assets/css/Markdown.css +178 -0
  81. package/src/web/assets/css/MessageItem.css +160 -0
  82. package/src/web/assets/css/Sidebar.css +208 -0
  83. package/src/web/assets/css/SidebarModal.css +137 -0
  84. package/src/web/assets/css/ThinkingIndicator.css +47 -0
  85. package/src/web/assets/css/ToolMessage.css +148 -0
  86. package/src/web/assets/css/global.css +226 -0
  87. package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
  88. package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
  89. package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
  90. package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
  91. package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
  92. package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
  93. package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
  94. package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
  95. package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
  96. package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
  97. package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
  98. package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
  99. package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
  100. package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
  101. package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
  102. package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
  103. package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
  104. package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
  106. package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
  107. package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
  108. package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
  109. package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
  110. package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
  111. package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
  112. package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
  113. package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
  114. package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
  115. package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
  116. package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
  117. package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
  118. package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
  119. package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
  120. package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
  121. package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
  122. package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
  123. package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
  124. package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
  125. package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
  126. package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
  127. package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
  128. package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
  129. package/src/web/assets/images/favicon-v2.svg +6 -0
  130. package/src/web/assets/images/favicon.png +0 -0
  131. package/src/web/assets/images/foruse.svg +5 -0
  132. package/src/web/assets/images/logo_black.svg +5 -0
  133. package/src/web/assets/images/logo_white.svg +5 -0
  134. package/src/web/assets/images/logoblack.png +0 -0
  135. package/src/web/assets/images/logowhite.png +0 -0
  136. package/src/web/build.ts +23 -0
  137. package/src/web/components/ApprovalPanel.tsx +191 -0
  138. package/src/web/components/ChatPage.tsx +273 -0
  139. package/src/web/components/FileExplorer.tsx +162 -0
  140. package/src/web/components/HomePage.tsx +121 -0
  141. package/src/web/components/MessageItem.tsx +178 -0
  142. package/src/web/components/Modal.tsx +30 -0
  143. package/src/web/components/QuestionPanel.tsx +149 -0
  144. package/src/web/components/Setup.tsx +211 -0
  145. package/src/web/components/Sidebar.tsx +292 -0
  146. package/src/web/components/ThinkingIndicator.tsx +85 -0
  147. package/src/web/logo_black.svg +5 -0
  148. package/src/web/logo_white.svg +5 -0
  149. package/src/web/router.ts +46 -0
  150. package/src/web/server.tsx +662 -0
  151. package/src/web/storage.ts +92 -0
  152. package/src/web/types.ts +17 -0
  153. package/src/web/utils.ts +61 -0
  154. package/tsconfig.json +33 -0
@@ -0,0 +1,304 @@
1
+ export type ModelsDevProviderId = string;
2
+ export type ModelsDevModelId = string;
3
+
4
+ export type ModelsDevModalities = {
5
+ input: string[];
6
+ output: string[];
7
+ };
8
+
9
+ export type ModelsDevCost = {
10
+ input?: number;
11
+ output?: number;
12
+ cache_read?: number;
13
+ cache_write?: number;
14
+ [k: string]: number | undefined;
15
+ };
16
+
17
+ export type ModelsDevLimit = {
18
+ context?: number;
19
+ input?: number;
20
+ output?: number;
21
+ [k: string]: number | undefined;
22
+ };
23
+
24
+ export type ModelsDevModel = {
25
+ id: ModelsDevModelId;
26
+ name: string;
27
+ family?: string;
28
+
29
+ attachment?: boolean;
30
+ reasoning?: boolean;
31
+ tool_call?: boolean;
32
+ structured_output?: boolean;
33
+ temperature?: boolean;
34
+
35
+ knowledge?: string;
36
+ release_date?: string;
37
+ last_updated?: string;
38
+
39
+ modalities?: ModelsDevModalities;
40
+ open_weights?: boolean;
41
+
42
+ cost?: ModelsDevCost;
43
+ limit?: ModelsDevLimit;
44
+
45
+ [k: string]: unknown;
46
+ };
47
+
48
+ export type ModelsDevProvider = {
49
+ id: ModelsDevProviderId;
50
+ env?: string[];
51
+ npm?: string;
52
+ name?: string;
53
+ doc?: string;
54
+ models: Record<ModelsDevModelId, ModelsDevModel>;
55
+ [k: string]: unknown;
56
+ };
57
+
58
+ export type ModelsDevApiResponse = Record<ModelsDevProviderId, ModelsDevProvider>;
59
+
60
+ export type ModelsDevClientOptions = {
61
+ url?: string;
62
+ ttlMs?: number;
63
+ fetchFn?: typeof fetch;
64
+ };
65
+
66
+ export type ModelsDevSearchQuery = {
67
+ providerId?: string;
68
+ modelId?: string;
69
+ family?: string;
70
+ nameIncludes?: string;
71
+ supports?: Partial<
72
+ Pick<
73
+ ModelsDevModel,
74
+ "attachment" | "reasoning" | "tool_call" | "structured_output" | "temperature" | "open_weights"
75
+ >
76
+ >;
77
+ modalities?: Partial<ModelsDevModalities>;
78
+ };
79
+
80
+ export type ModelsDevSearchResult = {
81
+ provider: ModelsDevProvider;
82
+ model: ModelsDevModel;
83
+ };
84
+
85
+ function isRecord(v: unknown): v is Record<string, unknown> {
86
+ return typeof v === "object" && v !== null && !Array.isArray(v);
87
+ }
88
+
89
+ function toLower(s: string): string {
90
+ return s.toLowerCase();
91
+ }
92
+
93
+ function includesAll(haystack: string[], needles: string[]): boolean {
94
+ const set = new Set(haystack.map(toLower));
95
+ for (const n of needles) if (!set.has(toLower(n))) return false;
96
+ return true;
97
+ }
98
+
99
+ export class ModelsDevClient {
100
+ private readonly url: string;
101
+ private readonly ttlMs: number;
102
+ private readonly fetchFn: typeof fetch;
103
+
104
+ private cache: { at: number; data: ModelsDevApiResponse } | null = null;
105
+ private inflight: Promise<ModelsDevApiResponse> | null = null;
106
+
107
+ constructor(options: ModelsDevClientOptions = {}) {
108
+ this.url = options.url ?? "https://models.dev/api.json";
109
+ this.ttlMs = options.ttlMs ?? 5 * 60_000;
110
+ this.fetchFn = (options.fetchFn ?? ((...args: Parameters<typeof fetch>) => fetch(...args))) as typeof fetch;
111
+ }
112
+
113
+ async getAll(options: { refresh?: boolean } = {}): Promise<ModelsDevApiResponse> {
114
+ const now = Date.now();
115
+ const refresh = options.refresh === true;
116
+
117
+ if (!refresh && this.cache && now - this.cache.at <= this.ttlMs) {
118
+ return this.cache.data;
119
+ }
120
+
121
+ if (this.inflight) return this.inflight;
122
+
123
+ this.inflight = (async () => {
124
+ const res = await this.fetchFn(this.url, { method: "GET", headers: { accept: "application/json" } });
125
+ if (!res.ok) throw new Error(`models.dev request failed (${res.status})`);
126
+ const json = (await res.json()) as unknown;
127
+ const data = this.validate(json);
128
+ this.cache = { at: Date.now(), data };
129
+ return data;
130
+ })();
131
+
132
+ try {
133
+ return await this.inflight;
134
+ } finally {
135
+ this.inflight = null;
136
+ }
137
+ }
138
+
139
+ async getProvider(providerId: ModelsDevProviderId, options: { refresh?: boolean } = {}): Promise<ModelsDevProvider | null> {
140
+ const data = await this.getAll(options);
141
+ return data[providerId] ?? null;
142
+ }
143
+
144
+ async getModel(
145
+ providerId: ModelsDevProviderId,
146
+ modelId: ModelsDevModelId,
147
+ options: { refresh?: boolean } = {}
148
+ ): Promise<ModelsDevModel | null> {
149
+ const provider = await this.getProvider(providerId, options);
150
+ return provider?.models?.[modelId] ?? null;
151
+ }
152
+
153
+ async getModelById(modelId: ModelsDevModelId, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult | null> {
154
+ const data = await this.getAll(options);
155
+ // Try exact match first
156
+ for (const provider of Object.values(data)) {
157
+ const model = provider.models?.[modelId];
158
+ if (model) return { provider, model };
159
+ }
160
+
161
+ // Try semantic/partial match
162
+ // e.g. gpt-5.2-2025-12-11 should match gpt-5.2 or vice versa
163
+ const lowerSearch = modelId.toLowerCase();
164
+
165
+ for (const provider of Object.values(data)) {
166
+ const models = provider.models ?? {};
167
+ for (const [id, model] of Object.entries(models)) {
168
+ const lowerId = id.toLowerCase();
169
+ // If the known model ID is a prefix of our search (e.g. search gpt-5.2-v1 matches model gpt-5.2)
170
+ // OR if our search is a prefix of the known model ID (e.g. search gpt-5.2 matches model gpt-5.2-preview)
171
+ // OR if one contains the other
172
+ if (lowerSearch.includes(lowerId) || lowerId.includes(lowerSearch)) {
173
+ return { provider, model };
174
+ }
175
+ }
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ async listProviders(options: { refresh?: boolean } = {}): Promise<ModelsDevProvider[]> {
182
+ const data = await this.getAll(options);
183
+ return Object.values(data);
184
+ }
185
+
186
+ async listModels(providerId?: ModelsDevProviderId, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult[]> {
187
+ const data = await this.getAll(options);
188
+
189
+ const providers = providerId ? (data[providerId] ? [data[providerId]!] : []) : Object.values(data);
190
+ const out: ModelsDevSearchResult[] = [];
191
+
192
+ for (const p of providers) {
193
+ for (const m of Object.values(p.models ?? {})) out.push({ provider: p, model: m });
194
+ }
195
+
196
+ return out;
197
+ }
198
+
199
+ async search(query: ModelsDevSearchQuery, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult[]> {
200
+ const data = await this.getAll(options);
201
+ const out: ModelsDevSearchResult[] = [];
202
+
203
+ const providerFilter = query.providerId ? data[query.providerId] : null;
204
+ const providers = providerFilter ? [providerFilter] : Object.values(data);
205
+
206
+ const nameIncludes = query.nameIncludes?.trim() ? toLower(query.nameIncludes.trim()) : null;
207
+
208
+ for (const provider of providers) {
209
+ if (query.providerId && provider.id !== query.providerId) continue;
210
+
211
+ const models = provider.models ?? {};
212
+ for (const model of Object.values(models)) {
213
+ if (query.modelId && model.id !== query.modelId) continue;
214
+ if (query.family && model.family !== query.family) continue;
215
+
216
+ if (nameIncludes) {
217
+ const name = typeof model.name === "string" ? toLower(model.name) : "";
218
+ const id = typeof model.id === "string" ? toLower(model.id) : "";
219
+ if (!name.includes(nameIncludes) && !id.includes(nameIncludes)) continue;
220
+ }
221
+
222
+ if (query.supports) {
223
+ let ok = true;
224
+ for (const [k, v] of Object.entries(query.supports)) {
225
+ if ((model as any)[k] !== v) {
226
+ ok = false;
227
+ break;
228
+ }
229
+ }
230
+ if (!ok) continue;
231
+ }
232
+
233
+ if (query.modalities) {
234
+ const mods = model.modalities;
235
+ if (!mods) continue;
236
+
237
+ if (query.modalities.input && !includesAll(mods.input ?? [], query.modalities.input)) continue;
238
+ if (query.modalities.output && !includesAll(mods.output ?? [], query.modalities.output)) continue;
239
+ }
240
+
241
+ out.push({ provider, model });
242
+ }
243
+ }
244
+
245
+ return out;
246
+ }
247
+
248
+ private validate(json: unknown): ModelsDevApiResponse {
249
+ if (!isRecord(json)) throw new Error("models.dev response is not an object");
250
+
251
+ const out: ModelsDevApiResponse = {};
252
+ for (const [providerId, providerValue] of Object.entries(json)) {
253
+ if (!isRecord(providerValue)) continue;
254
+
255
+ const models = providerValue.models;
256
+ if (!isRecord(models)) continue;
257
+
258
+ const normalizedProvider: ModelsDevProvider = {
259
+ ...(providerValue as any),
260
+ id: typeof providerValue.id === "string" ? providerValue.id : providerId,
261
+ models: models as any,
262
+ };
263
+
264
+ out[providerId] = normalizedProvider;
265
+ }
266
+
267
+ return out;
268
+ }
269
+ }
270
+
271
+ export const modelsDev = new ModelsDevClient();
272
+
273
+ export async function getModelsDevData(options: { refresh?: boolean } = {}): Promise<ModelsDevApiResponse> {
274
+ return modelsDev.getAll(options);
275
+ }
276
+
277
+ export async function getModelsDevProvider(providerId: string, options: { refresh?: boolean } = {}): Promise<ModelsDevProvider | null> {
278
+ return modelsDev.getProvider(providerId, options);
279
+ }
280
+
281
+ export async function getModelsDevModel(
282
+ providerId: string,
283
+ modelId: string,
284
+ options: { refresh?: boolean } = {}
285
+ ): Promise<ModelsDevModel | null> {
286
+ return modelsDev.getModel(providerId, modelId, options);
287
+ }
288
+
289
+ export async function findModelsDevModelById(
290
+ modelId: string,
291
+ options: { refresh?: boolean } = {}
292
+ ): Promise<ModelsDevSearchResult | null> {
293
+ return modelsDev.getModelById(modelId, options);
294
+ }
295
+
296
+ export async function searchModelsDev(query: ModelsDevSearchQuery, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult[]> {
297
+ return modelsDev.search(query, options);
298
+ }
299
+
300
+ export function modelAcceptsImages(model: ModelsDevModel): boolean {
301
+ if (!model.modalities) return false;
302
+ const { input } = model.modalities;
303
+ return Array.isArray(input) && input.includes("image");
304
+ }
@@ -0,0 +1,122 @@
1
+ export interface QuestionOption {
2
+ label: string;
3
+ value?: string | null;
4
+ }
5
+
6
+ export interface QuestionRequest {
7
+ id: string;
8
+ prompt: string;
9
+ options: QuestionOption[];
10
+ }
11
+
12
+ export interface QuestionAnswer {
13
+ id: string;
14
+ index: number;
15
+ label: string;
16
+ value: string | null;
17
+ customText?: string;
18
+ }
19
+
20
+ type QuestionListener = (request: QuestionRequest | null) => void;
21
+
22
+ let currentRequest: QuestionRequest | null = null;
23
+ let listeners = new Set<QuestionListener>();
24
+ let pendingResolve: ((answer: QuestionAnswer) => void) | null = null;
25
+ let pendingReject: ((reason?: any) => void) | null = null;
26
+
27
+ function notify(): void {
28
+ for (const listener of listeners) {
29
+ listener(currentRequest);
30
+ }
31
+ }
32
+
33
+ function createId(): string {
34
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
35
+ }
36
+
37
+ export function subscribeQuestion(listener: QuestionListener): () => void {
38
+ listeners.add(listener);
39
+ listener(currentRequest);
40
+ return () => {
41
+ listeners.delete(listener);
42
+ };
43
+ }
44
+
45
+ export function getCurrentQuestion(): QuestionRequest | null {
46
+ return currentRequest;
47
+ }
48
+
49
+ export async function askQuestion(prompt: string, options: QuestionOption[]): Promise<QuestionAnswer> {
50
+ if (pendingResolve) {
51
+ throw new Error('A question is already pending');
52
+ }
53
+
54
+ if (!prompt || !prompt.trim()) {
55
+ throw new Error('Prompt is required');
56
+ }
57
+
58
+ if (!Array.isArray(options) || options.length === 0) {
59
+ throw new Error('At least one option is required');
60
+ }
61
+
62
+ const request: QuestionRequest = {
63
+ id: createId(),
64
+ prompt,
65
+ options,
66
+ };
67
+
68
+ currentRequest = request;
69
+ notify();
70
+
71
+ const answer = await new Promise<QuestionAnswer>((resolve, reject) => {
72
+ pendingResolve = resolve;
73
+ pendingReject = reject;
74
+ });
75
+
76
+ return answer;
77
+ }
78
+
79
+ export function answerQuestion(index: number, customText?: string): void {
80
+ if (!currentRequest || !pendingResolve) return;
81
+
82
+ const option = currentRequest.options[index];
83
+
84
+ const answer: QuestionAnswer = customText
85
+ ? {
86
+ id: currentRequest.id,
87
+ index: currentRequest.options.length,
88
+ label: 'Custom response',
89
+ value: null,
90
+ customText,
91
+ }
92
+ : !option
93
+ ? undefined!
94
+ : {
95
+ id: currentRequest.id,
96
+ index,
97
+ label: option.label,
98
+ value: option.value ?? null,
99
+ customText,
100
+ };
101
+
102
+ if (!answer) return;
103
+
104
+ const resolve = pendingResolve;
105
+ pendingResolve = null;
106
+ pendingReject = null;
107
+ currentRequest = null;
108
+ notify();
109
+ resolve(answer);
110
+ }
111
+
112
+ export function cancelQuestion(): void {
113
+ if (!currentRequest || !pendingReject) return;
114
+
115
+ const reject = pendingReject;
116
+ pendingResolve = null;
117
+ pendingReject = null;
118
+ currentRequest = null;
119
+ notify();
120
+
121
+ reject(new Error('Interrupted by user'));
122
+ }
@@ -0,0 +1,25 @@
1
+ export function setTerminalTitle(title: string): void {
2
+ process.stdout.write(`\x1b]0;${title}\x07`);
3
+ }
4
+
5
+ export function clearTerminal(): void {
6
+ if (!process.stdout.isTTY) {
7
+ return;
8
+ }
9
+
10
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
11
+ }
12
+
13
+ export function extractTitleFromResponse(content: string): string | null {
14
+ const titleMatch = content.match(/<title>([^<]+)<\/title>/i);
15
+ if (titleMatch && titleMatch[1]) {
16
+ return titleMatch[1].trim();
17
+ }
18
+ return null;
19
+ }
20
+
21
+ export function removeTitleFromContent(content: string): string {
22
+ let cleaned = content.replace(/<title>[^<]*<\/title>\s*/gi, '');
23
+ cleaned = cleaned.trimStart();
24
+ return cleaned;
25
+ }