zerochat 7.3.0__py3-none-any.whl

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 (130) hide show
  1. zerochat-7.3.0.dist-info/METADATA +70 -0
  2. zerochat-7.3.0.dist-info/RECORD +130 -0
  3. zerochat-7.3.0.dist-info/WHEEL +5 -0
  4. zerochat-7.3.0.dist-info/entry_points.txt +2 -0
  5. zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
  6. zerochat-7.3.0.dist-info/top_level.txt +2 -0
  7. zerochat.py +2004 -0
  8. zerochat_runtime/__init__.py +1 -0
  9. zerochat_runtime/assets/css/base.css +199 -0
  10. zerochat_runtime/assets/css/components/composer.css +1258 -0
  11. zerochat_runtime/assets/css/components/debug.css +777 -0
  12. zerochat_runtime/assets/css/components/header.css +97 -0
  13. zerochat_runtime/assets/css/components/icons.css +39 -0
  14. zerochat_runtime/assets/css/components/markdown.css +685 -0
  15. zerochat_runtime/assets/css/components/messages.css +473 -0
  16. zerochat_runtime/assets/css/components/modals.css +1319 -0
  17. zerochat_runtime/assets/css/components/sidebar.css +622 -0
  18. zerochat_runtime/assets/css/components/tools.css +1223 -0
  19. zerochat_runtime/assets/css/layout.css +171 -0
  20. zerochat_runtime/assets/css/print.css +120 -0
  21. zerochat_runtime/assets/css/styles.css +18 -0
  22. zerochat_runtime/assets/css/theme-overrides.css +555 -0
  23. zerochat_runtime/assets/css/tokens.css +165 -0
  24. zerochat_runtime/assets/help/architecture.html +247 -0
  25. zerochat_runtime/assets/help/composer.html +134 -0
  26. zerochat_runtime/assets/help/conversations.html +105 -0
  27. zerochat_runtime/assets/help/debug.html +108 -0
  28. zerochat_runtime/assets/help/en/architecture.html +247 -0
  29. zerochat_runtime/assets/help/en/composer.html +134 -0
  30. zerochat_runtime/assets/help/en/conversations.html +104 -0
  31. zerochat_runtime/assets/help/en/debug.html +105 -0
  32. zerochat_runtime/assets/help/en/gemini-free.html +39 -0
  33. zerochat_runtime/assets/help/en/index.html +191 -0
  34. zerochat_runtime/assets/help/en/learning.html +71 -0
  35. zerochat_runtime/assets/help/en/mcp.html +143 -0
  36. zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
  37. zerochat_runtime/assets/help/en/profiles.html +173 -0
  38. zerochat_runtime/assets/help/en/rag.html +130 -0
  39. zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
  40. zerochat_runtime/assets/help/en/tools-agent.html +133 -0
  41. zerochat_runtime/assets/help/en/webllm.html +355 -0
  42. zerochat_runtime/assets/help/gemini-free.html +39 -0
  43. zerochat_runtime/assets/help/help.css +604 -0
  44. zerochat_runtime/assets/help/help.js +51 -0
  45. zerochat_runtime/assets/help/index.html +194 -0
  46. zerochat_runtime/assets/help/learning.html +71 -0
  47. zerochat_runtime/assets/help/mcp.html +143 -0
  48. zerochat_runtime/assets/help/openrouter-free.html +39 -0
  49. zerochat_runtime/assets/help/profiles.html +174 -0
  50. zerochat_runtime/assets/help/rag.html +130 -0
  51. zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
  52. zerochat_runtime/assets/help/tools-agent.html +133 -0
  53. zerochat_runtime/assets/help/webllm.html +359 -0
  54. zerochat_runtime/assets/js/agent-core.js +1553 -0
  55. zerochat_runtime/assets/js/api.js +844 -0
  56. zerochat_runtime/assets/js/app.js +2558 -0
  57. zerochat_runtime/assets/js/attachments.js +220 -0
  58. zerochat_runtime/assets/js/charts.js +338 -0
  59. zerochat_runtime/assets/js/chat-engine.js +566 -0
  60. zerochat_runtime/assets/js/config-store.js +207 -0
  61. zerochat_runtime/assets/js/context-manager.js +584 -0
  62. zerochat_runtime/assets/js/conversation-service.js +484 -0
  63. zerochat_runtime/assets/js/cookies.js +688 -0
  64. zerochat_runtime/assets/js/data-reset-service.js +136 -0
  65. zerochat_runtime/assets/js/debug.js +508 -0
  66. zerochat_runtime/assets/js/defaults.js +16 -0
  67. zerochat_runtime/assets/js/export.js +179 -0
  68. zerochat_runtime/assets/js/file-parser.js +2088 -0
  69. zerochat_runtime/assets/js/generation-controller.js +417 -0
  70. zerochat_runtime/assets/js/i18n.js +1483 -0
  71. zerochat_runtime/assets/js/icons.js +156 -0
  72. zerochat_runtime/assets/js/ingestionEngine.js +436 -0
  73. zerochat_runtime/assets/js/markdown.js +588 -0
  74. zerochat_runtime/assets/js/mcp.js +1271 -0
  75. zerochat_runtime/assets/js/message-turns.js +62 -0
  76. zerochat_runtime/assets/js/profile-backup.js +118 -0
  77. zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
  78. zerochat_runtime/assets/js/profile-repository.js +213 -0
  79. zerochat_runtime/assets/js/providers-webllm.js +486 -0
  80. zerochat_runtime/assets/js/providers.js +1560 -0
  81. zerochat_runtime/assets/js/rag-index.js +295 -0
  82. zerochat_runtime/assets/js/rag-service.js +530 -0
  83. zerochat_runtime/assets/js/rag-ui.js +997 -0
  84. zerochat_runtime/assets/js/ragStorage.js +676 -0
  85. zerochat_runtime/assets/js/sandbox.js +449 -0
  86. zerochat_runtime/assets/js/state.js +704 -0
  87. zerochat_runtime/assets/js/storage-db.js +144 -0
  88. zerochat_runtime/assets/js/tool-cards.js +330 -0
  89. zerochat_runtime/assets/js/tool-security.js +653 -0
  90. zerochat_runtime/assets/js/tools/README.md +50 -0
  91. zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
  92. zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
  93. zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
  94. zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
  95. zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
  96. zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
  97. zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
  98. zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
  99. zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
  100. zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
  101. zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
  102. zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
  103. zerochat_runtime/assets/js/ui-composer.js +319 -0
  104. zerochat_runtime/assets/js/ui-conversation.js +655 -0
  105. zerochat_runtime/assets/js/ui-dialogs.js +132 -0
  106. zerochat_runtime/assets/js/ui-generation-status.js +119 -0
  107. zerochat_runtime/assets/js/ui-inspector.js +804 -0
  108. zerochat_runtime/assets/js/ui-mcp.js +604 -0
  109. zerochat_runtime/assets/js/ui-profiles.js +824 -0
  110. zerochat_runtime/assets/js/ui-reasoning.js +146 -0
  111. zerochat_runtime/assets/js/ui-settings.js +825 -0
  112. zerochat_runtime/assets/js/ui-shell.js +215 -0
  113. zerochat_runtime/assets/js/ui-sidebar.js +429 -0
  114. zerochat_runtime/assets/js/ui-telemetry.js +404 -0
  115. zerochat_runtime/assets/js/ui-transfer.js +262 -0
  116. zerochat_runtime/assets/js/utils.js +216 -0
  117. zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
  118. zerochat_runtime/assets/js/web-browser.js +569 -0
  119. zerochat_runtime/assets/js/web-search.js +519 -0
  120. zerochat_runtime/assets/manifest.webmanifest +20 -0
  121. zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
  122. zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
  123. zerochat_runtime/assets/services/lsp/installer.json +9 -0
  124. zerochat_runtime/assets/services/lsp/service.json +22 -0
  125. zerochat_runtime/assets/services/memory/installer.json +9 -0
  126. zerochat_runtime/assets/services/memory/service.json +24 -0
  127. zerochat_runtime/assets/services/playwright/installer.json +10 -0
  128. zerochat_runtime/assets/services/playwright/service.json +39 -0
  129. zerochat_runtime/assets/sw.js +164 -0
  130. zerochat_runtime/assets/zerochat.html +671 -0
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Módulo de Gestión del Estado Global (ChatState) para ZeroChat.
3
+ * Implementa un Store reactivo, desacoplado, predecible y con una única fuente de verdad,
4
+ * sin dependencias de frameworks externos.
5
+ */
6
+ (function (root, factory) {
7
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
8
+ module.exports = factory(require('./message-turns.js'), require('./defaults.js'));
9
+ } else {
10
+ root.ChatState = factory(root.ChatMessageTurns, root.ChatDefaults);
11
+ }
12
+ }(typeof self !== 'undefined' ? self : this, function (MessageTurns, Defaults) {
13
+ 'use strict';
14
+
15
+ const DEFAULT_THEME = Defaults.DEFAULT_THEME;
16
+
17
+ function clone(obj) {
18
+ if (obj === null || typeof obj !== 'object') return obj;
19
+ if (Array.isArray(obj)) return obj.map(clone);
20
+ const copy = {};
21
+ for (const key of Object.keys(obj)) {
22
+ copy[key] = clone(obj[key]);
23
+ }
24
+ return copy;
25
+ }
26
+
27
+ function isShallowEqual(a, b) {
28
+ if (Object.is(a, b)) return true;
29
+ if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false;
30
+ const keysA = Object.keys(a);
31
+ const keysB = Object.keys(b);
32
+ if (keysA.length !== keysB.length) return false;
33
+ for (let i = 0; i < keysA.length; i++) {
34
+ const k = keysA[i];
35
+ if (!Object.prototype.hasOwnProperty.call(b, k) || !Object.is(a[k], b[k])) {
36
+ return false;
37
+ }
38
+ }
39
+ return true;
40
+ }
41
+
42
+ const CANONICAL_SLICES = Object.freeze([
43
+ 'config',
44
+ 'sessions',
45
+ 'messages',
46
+ 'streaming',
47
+ 'agent',
48
+ 'telemetry',
49
+ 'ui',
50
+ 'mcp',
51
+ 'toolSecurity'
52
+ ]);
53
+
54
+ function createInitialState(overrides = {}) {
55
+ const DEFAULT_SYSTEM_DATA_PROMPT = '[Format: Always use standard Markdown and plain text. Never use LaTeX syntax or delimiters ($ or $$); write mathematics, formulas, and numbers directly in readable text using standard symbols (+, -, ×, /, =).]';
56
+ const defaultState = {
57
+ // 1. Configuración de la Aplicación y Preferencias
58
+ config: {
59
+ schemaVersion: 2,
60
+ activeProfile: null,
61
+ apiUrl: 'http://localhost:1234/v1',
62
+ apiType: 'openai',
63
+ model: '',
64
+ systemPrompt: '',
65
+ systemDataPrompt: DEFAULT_SYSTEM_DATA_PROMPT,
66
+ temperature: '0.7',
67
+ reasoningEffort: 'medium',
68
+ reasoningTransport: 'auto',
69
+ maxAgentTurns: 15,
70
+ theme: DEFAULT_THEME,
71
+ language: 'es',
72
+ enabledTools: {
73
+ execute_javascript: true,
74
+ search_web: true,
75
+ fetch_web_page: true,
76
+ download_pdf: true,
77
+ render_chart: true,
78
+ agent_checkpoint: false
79
+ },
80
+ enableRawLogs: false,
81
+ enableDebugMessages: false,
82
+ enableContextCache: true,
83
+ activeRagBranchId: '',
84
+ activeRagBranchIds: [],
85
+ modelReasoningConfig: null,
86
+ mcpHost: '127.0.0.1',
87
+ mcpPort: 6388,
88
+ webllmConfig: {
89
+ context_window_size: 'default',
90
+ prefill_chunk_size: 'default'
91
+ }
92
+ },
93
+
94
+ // 2. Sesiones y Conversación Activa
95
+ sessions: {
96
+ activeId: typeof crypto !== 'undefined' && crypto.randomUUID ? ('session_' + crypto.randomUUID()) : ('session_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7)),
97
+ list: []
98
+ },
99
+
100
+ // 3. Historial de Mensajes de la Sesión Activa
101
+ messages: [],
102
+
103
+ // 4. Estado de Generación / Streaming
104
+ streaming: {
105
+ isGenerating: false,
106
+ stats: null,
107
+ status: 'idle', // 'idle' | 'streaming' | 'thinking' | 'done' | 'error'
108
+ error: null
109
+ },
110
+
111
+ // 5. Estado Agéntico y Herramientas
112
+ agent: {
113
+ activeTurnIndex: 0,
114
+ currentTool: null,
115
+ loopWarning: false,
116
+ ragSystemContext: ''
117
+ },
118
+
119
+ // 6. Telemetría de Contexto y Tokens
120
+ telemetry: {
121
+ stats: null,
122
+ diagnostics: null,
123
+ lastTurnStats: null
124
+ },
125
+
126
+ // 7. Estado de la Interfaz (UI)
127
+ ui: {
128
+ notices: [],
129
+ noticeSequence: 0,
130
+ noticeResult: null,
131
+ sidebarOpen: false,
132
+ reasoningMenuOpen: false,
133
+ debugPanelOpen: false,
134
+ activeModal: null, // null | 'settings' | 'export' | 'debug_interceptor'
135
+ attachedFiles: [],
136
+ webllm: { catalog: [], contextKey: '', operation: null },
137
+ generationStatus: { phase: 'idle', percent: null, startedAt: null }
138
+ },
139
+
140
+ // 8. Estado de Integración MCP (mcp-proxy)
141
+ mcp: {
142
+ status: 'disconnected', // 'disconnected' | 'connecting' | 'checking' | 'connected' | 'error'
143
+ host: '127.0.0.1',
144
+ port: 6388,
145
+ endpoint: 'http://127.0.0.1:6388/sse',
146
+ serverInfo: null,
147
+ tools: [],
148
+ lastConnected: null,
149
+ lastVerified: null,
150
+ latencyMs: null,
151
+ error: null
152
+ },
153
+
154
+ // 9. Políticas de Seguridad de Herramientas (MCP y Built-in)
155
+ toolSecurity: {
156
+ globalMcpPolicy: 'ask',
157
+ authorizedCount: 0,
158
+ tools: {}
159
+ }
160
+ };
161
+
162
+ if (overrides && typeof overrides === 'object') {
163
+ for (const k of Object.keys(overrides)) {
164
+ if (!CANONICAL_SLICES.includes(k)) {
165
+ throw new Error(`[ChatState] Slice no canónico en overrides: "${k}". Slices permitidos: ${CANONICAL_SLICES.join(', ')}`);
166
+ }
167
+ if (typeof overrides[k] === 'object' && overrides[k] !== null && !Array.isArray(overrides[k])) {
168
+ defaultState[k] = Object.assign({}, defaultState[k], overrides[k]);
169
+ } else {
170
+ defaultState[k] = overrides[k];
171
+ }
172
+ }
173
+ }
174
+
175
+ return defaultState;
176
+ }
177
+
178
+ function createStore(initialConfigOverrides = {}) {
179
+ let state = createInitialState(initialConfigOverrides);
180
+ const listeners = new Set();
181
+ let isEmitting = false;
182
+
183
+ function validateSliceKey(key) {
184
+ if (!CANONICAL_SLICES.includes(key)) {
185
+ throw new Error(`[ChatState] Intento de escritura en slice no canónico: "${key}". Slices permitidos: ${CANONICAL_SLICES.join(', ')}`);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Retorna una instantánea inmutable/clonada del estado global completo.
191
+ */
192
+ function getState() {
193
+ return clone(state);
194
+ }
195
+
196
+ /**
197
+ * Retorna una instantánea clonada de una sección específica del estado.
198
+ */
199
+ function get(sliceKey) {
200
+ if (typeof sliceKey !== 'string' || !(sliceKey in state)) {
201
+ return undefined;
202
+ }
203
+ return clone(state[sliceKey]);
204
+ }
205
+
206
+ /**
207
+ * Actualiza el estado global de forma atómica y notifica a los suscriptores si hubo cambios reales.
208
+ * Admite un objeto parcial o una función updater `(prevState) => partialState`.
209
+ */
210
+ function setState(partialOrUpdater) {
211
+ const prevState = state;
212
+ const updates = typeof partialOrUpdater === 'function' ? partialOrUpdater(clone(state)) : partialOrUpdater;
213
+
214
+ if (!updates || typeof updates !== 'object') {
215
+ return getState();
216
+ }
217
+
218
+ let hasChanged = false;
219
+ const nextState = Object.assign({}, state);
220
+
221
+ for (const key of Object.keys(updates)) {
222
+ validateSliceKey(key);
223
+ const prevVal = state[key];
224
+ const nextVal = updates[key];
225
+
226
+ // Fusión limpia para slices de tipo objeto
227
+ if (
228
+ typeof prevVal === 'object' && prevVal !== null && !Array.isArray(prevVal) &&
229
+ typeof nextVal === 'object' && nextVal !== null && !Array.isArray(nextVal)
230
+ ) {
231
+ const mergedSlice = Object.assign({}, prevVal, nextVal);
232
+ if (!isShallowEqual(prevVal, mergedSlice)) {
233
+ nextState[key] = mergedSlice;
234
+ hasChanged = true;
235
+ }
236
+ } else if (!isShallowEqual(prevVal, nextVal)) {
237
+ nextState[key] = clone(nextVal);
238
+ hasChanged = true;
239
+ }
240
+ }
241
+
242
+ if (hasChanged) {
243
+ state = nextState;
244
+ notifyListeners(state, prevState);
245
+ }
246
+
247
+ return getState();
248
+ }
249
+
250
+ /**
251
+ * Actualiza una sección/slice específica del estado.
252
+ * Ejemplo: `store.set('streaming', { isGenerating: true })`
253
+ */
254
+ function set(sliceKey, update) {
255
+ if (typeof sliceKey !== 'string') return getState();
256
+ validateSliceKey(sliceKey);
257
+
258
+ const prevSlice = state[sliceKey];
259
+ let nextSlice;
260
+
261
+ if (typeof update === 'function') {
262
+ nextSlice = update(clone(prevSlice));
263
+ } else if (
264
+ typeof prevSlice === 'object' && prevSlice !== null && !Array.isArray(prevSlice) &&
265
+ typeof update === 'object' && update !== null && !Array.isArray(update)
266
+ ) {
267
+ nextSlice = Object.assign({}, prevSlice, update);
268
+ } else {
269
+ nextSlice = update;
270
+ }
271
+
272
+ return setState({ [sliceKey]: nextSlice });
273
+ }
274
+
275
+ /**
276
+ * Suscribe un listener a cambios del estado.
277
+ * Puede suscribirse a todo el estado, a una clave slice, o a un selector derivado.
278
+ * Retorna una función unsubscribe para cancelar la suscripción.
279
+ *
280
+ * Ejemplos:
281
+ * - `subscribe((newState, prevState) => { ... })`
282
+ * - `subscribe('config', (newConfig, prevConfig) => { ... })`
283
+ * - `subscribe(state => state.streaming.isGenerating, (isGenerating, prevVal) => { ... })`
284
+ */
285
+ function subscribe(selectorOrListener, optionalListener) {
286
+ let listenerEntry;
287
+
288
+ if (typeof selectorOrListener === 'function' && typeof optionalListener === 'function') {
289
+ // Modo selector: (state) => selector(state), callback: (selected, prevSelected)
290
+ const selector = selectorOrListener;
291
+ const callback = optionalListener;
292
+ let lastSelectedValue = selector(clone(state));
293
+
294
+ listenerEntry = (nextState) => {
295
+ const currentSelectedValue = selector(nextState);
296
+ if (!isShallowEqual(lastSelectedValue, currentSelectedValue)) {
297
+ const prev = lastSelectedValue;
298
+ lastSelectedValue = currentSelectedValue;
299
+ callback(currentSelectedValue, prev);
300
+ }
301
+ };
302
+ } else if (typeof selectorOrListener === 'string' && typeof optionalListener === 'function') {
303
+ // Modo clave de slice: 'streaming', callback: (newStreaming, prevStreaming)
304
+ const sliceKey = selectorOrListener;
305
+ const callback = optionalListener;
306
+ let lastSliceValue = state[sliceKey];
307
+
308
+ listenerEntry = (nextSnapshot, prevSnapshot, rawNext, rawPrev) => {
309
+ if (rawNext && rawPrev && rawNext[sliceKey] === rawPrev[sliceKey]) return;
310
+ const currentSliceValue = nextSnapshot[sliceKey];
311
+ if (!isShallowEqual(lastSliceValue, currentSliceValue)) {
312
+ const prev = lastSliceValue;
313
+ lastSliceValue = currentSliceValue;
314
+ callback(clone(currentSliceValue), clone(prev));
315
+ }
316
+ };
317
+ } else if (typeof selectorOrListener === 'function') {
318
+ // Modo listener global: callback(newState, prevState)
319
+ listenerEntry = selectorOrListener;
320
+ } else {
321
+ throw new Error('ChatState.subscribe: Argumentos inválidos');
322
+ }
323
+
324
+ listeners.add(listenerEntry);
325
+
326
+ return function unsubscribe() {
327
+ listeners.delete(listenerEntry);
328
+ };
329
+ }
330
+
331
+ function notifyListeners(nextState, prevState) {
332
+ if (isEmitting) return;
333
+ isEmitting = true;
334
+ try {
335
+ const nextSnapshot = clone(nextState);
336
+ const prevSnapshot = clone(prevState);
337
+ listeners.forEach(fn => {
338
+ try {
339
+ fn(nextSnapshot, prevSnapshot, nextState, prevState);
340
+ } catch (e) {
341
+ console.error('ChatState listener error:', e);
342
+ }
343
+ });
344
+ } finally {
345
+ isEmitting = false;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Reinicia el estado a los valores por defecto.
351
+ */
352
+ function reset(overrides = {}) {
353
+ const prevState = state;
354
+ state = createInitialState(overrides);
355
+ notifyListeners(state, prevState);
356
+ return getState();
357
+ }
358
+
359
+ function isConversationBusy() {
360
+ return Boolean(state.streaming && state.streaming.isGenerating);
361
+ }
362
+
363
+ /**
364
+ * Inicializa una conversación nueva sin alterar configuración ni conexiones.
365
+ * Se deniega durante una inferencia para evitar que callbacks pendientes
366
+ * escriban sobre la sesión nueva.
367
+ */
368
+ function initializeConversation({ sessionId, sessions = [], messages = [] } = {}) {
369
+ if (isConversationBusy()) return { ok: false, reason: 'generation-active' };
370
+ if (typeof sessionId !== 'string' || !sessionId.trim()) return { ok: false, reason: 'invalid-session-id' };
371
+ if (!Array.isArray(sessions) || !Array.isArray(messages)) return { ok: false, reason: 'invalid-conversation-state' };
372
+
373
+ setState({
374
+ sessions: { activeId: sessionId, list: clone(sessions) },
375
+ messages: clone(messages),
376
+ streaming: { isGenerating: false, stats: null, status: 'idle', error: null },
377
+ agent: { activeTurnIndex: 0, currentTool: null, loopWarning: false, ragSystemContext: '' },
378
+ telemetry: { stats: null, diagnostics: null, lastTurnStats: null },
379
+ ui: Object.assign({}, state.ui, { reasoningMenuOpen: false, debugPanelOpen: false, activeModal: null, attachedFiles: [], notices: [] })
380
+ });
381
+
382
+ return { ok: true, state: getState() };
383
+ }
384
+
385
+ /**
386
+ * Reemplaza o conmuta la conversación activa de forma atómica y segura.
387
+ * Acepta { sessionId, messages, sessions } o argumentos posicionales (sessionId, messages, sessions).
388
+ */
389
+ function replaceConversation(firstArg, optionalMessages, optionalSessions) {
390
+ let sessionId, messages, sessions;
391
+ if (firstArg && typeof firstArg === 'object' && !Array.isArray(firstArg)) {
392
+ sessionId = firstArg.sessionId;
393
+ messages = firstArg.messages;
394
+ sessions = firstArg.sessions;
395
+ } else {
396
+ sessionId = firstArg;
397
+ messages = optionalMessages;
398
+ sessions = optionalSessions;
399
+ }
400
+
401
+ if (isConversationBusy()) return { ok: false, reason: 'generation-active' };
402
+ if (typeof sessionId !== 'string' || !sessionId.trim()) return { ok: false, reason: 'invalid-session-id' };
403
+ if (messages !== undefined && !Array.isArray(messages)) return { ok: false, reason: 'invalid-messages' };
404
+ if (sessions !== undefined && !Array.isArray(sessions)) return { ok: false, reason: 'invalid-sessions' };
405
+
406
+ const nextSessions = {
407
+ activeId: sessionId,
408
+ list: sessions !== undefined ? clone(sessions) : clone(state.sessions.list)
409
+ };
410
+
411
+ setState({
412
+ sessions: nextSessions,
413
+ messages: Array.isArray(messages) ? clone(messages) : [],
414
+ streaming: { isGenerating: false, stats: null, status: 'idle', error: null },
415
+ agent: { activeTurnIndex: 0, currentTool: null, loopWarning: false, ragSystemContext: '' },
416
+ telemetry: { stats: null, diagnostics: null, lastTurnStats: null },
417
+ ui: Object.assign({}, state.ui, { reasoningMenuOpen: false, debugPanelOpen: false, activeModal: null, attachedFiles: [], notices: [] })
418
+ });
419
+
420
+ return { ok: true, state: getState() };
421
+ }
422
+
423
+ /**
424
+ * Añade un mensaje individual al historial de la sesión activa tras validación.
425
+ */
426
+ function appendMessage(message) {
427
+ if (!message || typeof message !== 'object') {
428
+ throw new Error('[ChatState] appendMessage: El mensaje debe ser un objeto válido.');
429
+ }
430
+ if (!message.role || typeof message.role !== 'string') {
431
+ throw new Error('[ChatState] appendMessage: El mensaje debe tener una propiedad "role" válida.');
432
+ }
433
+ const cloned = clone(message);
434
+ if (!cloned.id) {
435
+ cloned.id = 'msg_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
436
+ }
437
+ setState({ messages: [...state.messages, cloned] });
438
+ return cloned;
439
+ }
440
+
441
+ /**
442
+ * Reemplaza el historial de mensajes completo de la sesión activa.
443
+ */
444
+ function replaceMessages(messages) {
445
+ if (!Array.isArray(messages)) {
446
+ throw new Error('[ChatState] replaceMessages: messages debe ser un Array.');
447
+ }
448
+ setState({ messages: clone(messages) });
449
+ return getState().messages;
450
+ }
451
+
452
+ /**
453
+ * Elimina un turno de conversación y sanea tool calls y respuestas huérfanas.
454
+ */
455
+ function removeTurn(criteria = {}) {
456
+ if (isConversationBusy()) return { ok: false, reason: 'generation-active', removedCount: 0 };
457
+ const currentMessages = state.messages;
458
+ const initialCount = currentMessages.length;
459
+
460
+ let filtered;
461
+ if (typeof criteria === 'function') {
462
+ filtered = currentMessages.filter((m, idx) => !criteria(m, idx));
463
+ } else {
464
+ const msgId = criteria.msgId || '';
465
+ const baseId = criteria.baseId || (msgId && msgId.includes('_') ? msgId.split('_').slice(0, 2).join('_') : '');
466
+ // Preserve the legacy State selection default; the engine uses suffix-based IDs.
467
+ filtered = MessageTurns.removeSelectedTurn(currentMessages, { ...criteria, msgId, baseId });
468
+ }
469
+
470
+ const removedCount = initialCount - filtered.length;
471
+ if (removedCount > 0) {
472
+ setState({ messages: filtered });
473
+ }
474
+ return { ok: true, removedCount, messages: getState().messages };
475
+ }
476
+
477
+ /**
478
+ * Guarda o actualiza los metadatos de una sesión en el listado de sesiones.
479
+ */
480
+ function saveSessionMetadata(sessionMeta) {
481
+ if (!sessionMeta || typeof sessionMeta !== 'object' || !sessionMeta.id) {
482
+ throw new Error('[ChatState] saveSessionMetadata: Se requiere un objeto de sesión con id válido.');
483
+ }
484
+ const list = clone(state.sessions.list);
485
+ const idx = list.findIndex(s => s.id === sessionMeta.id);
486
+ const clonedMeta = clone(sessionMeta);
487
+ if (idx >= 0) {
488
+ list[idx] = Object.assign({}, list[idx], clonedMeta);
489
+ } else {
490
+ list.unshift(clonedMeta);
491
+ }
492
+ setState({
493
+ sessions: {
494
+ activeId: state.sessions.activeId,
495
+ list
496
+ }
497
+ });
498
+ return clonedMeta;
499
+ }
500
+
501
+ /**
502
+ * Elimina una sesión del listado. Si es la activa, pasa a la siguiente o null.
503
+ */
504
+ function removeSession(sessionId) {
505
+ if (typeof sessionId !== 'string' || !sessionId) return { ok: false, reason: 'invalid-id' };
506
+ if (isConversationBusy() && state.sessions.activeId === sessionId) {
507
+ return { ok: false, reason: 'generation-active' };
508
+ }
509
+ const list = state.sessions.list.filter(s => s.id !== sessionId);
510
+ let nextActiveId = state.sessions.activeId;
511
+ if (nextActiveId === sessionId) {
512
+ nextActiveId = list.length > 0 ? list[0].id : null;
513
+ }
514
+ setState({
515
+ sessions: {
516
+ activeId: nextActiveId,
517
+ list
518
+ },
519
+ ui: Object.assign({}, state.ui, { notices: nextActiveId === state.sessions.activeId ? state.ui.notices : [] })
520
+ });
521
+ return { ok: true, activeId: nextActiveId, list };
522
+ }
523
+
524
+ /**
525
+ * Importa una conversación con sus metadatos e historial de forma atómica.
526
+ */
527
+ function importConversation(sessionMeta, history = []) {
528
+ if (isConversationBusy()) return { ok: false, reason: 'generation-active' };
529
+ if (!sessionMeta || typeof sessionMeta !== 'object' || !sessionMeta.id) {
530
+ return { ok: false, reason: 'invalid-session-meta' };
531
+ }
532
+ if (!Array.isArray(history)) {
533
+ return { ok: false, reason: 'invalid-history' };
534
+ }
535
+
536
+ const list = clone(state.sessions.list);
537
+ const idx = list.findIndex(s => s.id === sessionMeta.id);
538
+ const clonedMeta = clone(sessionMeta);
539
+ if (idx >= 0) {
540
+ list[idx] = Object.assign({}, list[idx], clonedMeta);
541
+ } else {
542
+ list.unshift(clonedMeta);
543
+ }
544
+
545
+ setState({
546
+ sessions: { activeId: clonedMeta.id, list },
547
+ messages: clone(history),
548
+ streaming: { isGenerating: false, stats: null, status: 'idle', error: null },
549
+ agent: { activeTurnIndex: 0, currentTool: null, loopWarning: false, ragSystemContext: '' },
550
+ telemetry: { stats: null, diagnostics: null, lastTurnStats: null },
551
+ ui: Object.assign({}, state.ui, { reasoningMenuOpen: false, debugPanelOpen: false, activeModal: null, attachedFiles: [], notices: [] })
552
+ });
553
+
554
+ return { ok: true, state: getState() };
555
+ }
556
+
557
+ function enqueueNotice(notice) {
558
+ if (!notice || typeof notice.message !== 'string' || typeof notice.title !== 'string' ||
559
+ !['info', 'success', 'error'].includes(notice.type) ||
560
+ (notice.mode !== undefined && !['alert', 'confirm', 'prompt'].includes(notice.mode))) throw new TypeError('Invalid notice');
561
+ notice = { ...notice, mode: notice.mode || 'alert', message: notice.message.slice(0, 10000), title: notice.title.slice(0, 200) };
562
+ const queue = state.ui.notices || [];
563
+ const existing = notice.mode === 'alert' && queue.find(item => item.mode === 'alert' && item.message === notice.message && item.title === notice.title && item.type === notice.type);
564
+ if (existing) return existing.id;
565
+ if (queue.length >= 50) throw new RangeError('Notice queue is full');
566
+ const id = (state.ui.noticeSequence || 0) + 1;
567
+ const item = { id, message: notice.message, title: notice.title, type: notice.type, mode: notice.mode };
568
+ if (notice.mode === 'prompt') item.value = String(notice.value ?? '').slice(0, 10000);
569
+ if (notice.acceptText) item.acceptText = String(notice.acceptText).slice(0, 100);
570
+ if (notice.cancelText) item.cancelText = String(notice.cancelText).slice(0, 100);
571
+ if (notice.checkbox) item.checkbox = String(notice.checkbox).slice(0, 500);
572
+ if (notice.checkboxDefault !== undefined) item.checkboxDefault = Boolean(notice.checkboxDefault);
573
+ setState({ ui: Object.assign({}, state.ui, { notices: queue.concat(item), noticeSequence: id }) });
574
+ return id;
575
+ }
576
+
577
+ function dismissNotice(id, accepted = false, value = null, checkboxChecked = false) {
578
+ if (!(state.ui.notices || []).some(item => item.id === id)) return;
579
+ const targetNotice = state.ui.notices.find(item => item.id === id);
580
+ const result = { id, accepted: accepted === true };
581
+ if (targetNotice && targetNotice.mode === 'prompt') {
582
+ result.value = accepted === true ? String(value ?? '').slice(0, 10000) : null;
583
+ }
584
+ if (targetNotice && targetNotice.checkbox !== undefined) {
585
+ result.checkboxChecked = Boolean(checkboxChecked);
586
+ }
587
+ setState({ ui: Object.assign({}, state.ui, { notices: state.ui.notices.filter(item => item.id !== id), noticeResult: result }) });
588
+ }
589
+
590
+ /** Define los archivos adjuntos de la UI en el slice `ui.attachedFiles`. */
591
+ function setAttachments(files) {
592
+ if (!Array.isArray(files)) {
593
+ throw new Error('[ChatState] setAttachments: files debe ser un Array.');
594
+ }
595
+ setState({
596
+ ui: Object.assign({}, state.ui, { attachedFiles: clone(files) })
597
+ });
598
+ return getState().ui.attachedFiles;
599
+ }
600
+
601
+ /**
602
+ * Limpia los archivos adjuntos de la UI.
603
+ */
604
+ function clearAttachments() {
605
+ return setAttachments([]);
606
+ }
607
+
608
+ /**
609
+ * Actualiza el estado de progreso general de generación en `ui.generationStatus`.
610
+ */
611
+ function setGenerationStatus(update = {}) {
612
+ const raw = typeof update === 'string' ? { text: update } : (update || {});
613
+ const current = (state.ui && state.ui.generationStatus) || { phase: 'idle', percent: null, startedAt: null };
614
+ const phase = String(raw.phase || (raw.text || raw.message ? 'custom' : current.phase || 'idle'));
615
+ const phaseChanged = phase !== current.phase;
616
+ const next = phaseChanged
617
+ ? {
618
+ text: '',
619
+ message: '',
620
+ detail: '',
621
+ percent: null,
622
+ ...raw,
623
+ phase,
624
+ startedAt: Date.now()
625
+ }
626
+ : {
627
+ ...current,
628
+ ...raw,
629
+ phase,
630
+ startedAt: current.startedAt || Date.now()
631
+ };
632
+ setState({
633
+ ui: Object.assign({}, state.ui, { generationStatus: next })
634
+ });
635
+ return getState().ui.generationStatus;
636
+ }
637
+
638
+ /**
639
+ * Limpia y reinicia el estado de progreso general de generación a inactivo.
640
+ */
641
+ function clearGenerationStatus() {
642
+ const next = { phase: 'idle', percent: null, text: '', message: '', detail: '', startedAt: null };
643
+ setState({
644
+ ui: Object.assign({}, state.ui, { generationStatus: next })
645
+ });
646
+ return getState().ui.generationStatus;
647
+ }
648
+
649
+ return {
650
+ getState,
651
+ get,
652
+ setState,
653
+ set,
654
+ subscribe,
655
+ reset,
656
+ isConversationBusy,
657
+ initializeConversation,
658
+ replaceConversation,
659
+ appendMessage,
660
+ replaceMessages,
661
+ removeTurn,
662
+ saveSessionMetadata,
663
+ removeSession,
664
+ importConversation,
665
+ setAttachments,
666
+ clearAttachments,
667
+ setGenerationStatus,
668
+ clearGenerationStatus,
669
+ enqueueNotice,
670
+ dismissNotice,
671
+ CANONICAL_SLICES
672
+ };
673
+ }
674
+
675
+ // Instancia singleton por defecto para la aplicación
676
+ const defaultStore = createStore();
677
+
678
+ return {
679
+ createStore,
680
+ createInitialState,
681
+ CANONICAL_SLICES,
682
+ getState: defaultStore.getState,
683
+ get: defaultStore.get,
684
+ setState: defaultStore.setState,
685
+ set: defaultStore.set,
686
+ subscribe: defaultStore.subscribe,
687
+ reset: defaultStore.reset,
688
+ isConversationBusy: defaultStore.isConversationBusy,
689
+ initializeConversation: defaultStore.initializeConversation,
690
+ replaceConversation: defaultStore.replaceConversation,
691
+ appendMessage: defaultStore.appendMessage,
692
+ replaceMessages: defaultStore.replaceMessages,
693
+ removeTurn: defaultStore.removeTurn,
694
+ saveSessionMetadata: defaultStore.saveSessionMetadata,
695
+ removeSession: defaultStore.removeSession,
696
+ importConversation: defaultStore.importConversation,
697
+ setAttachments: defaultStore.setAttachments,
698
+ clearAttachments: defaultStore.clearAttachments,
699
+ setGenerationStatus: defaultStore.setGenerationStatus,
700
+ clearGenerationStatus: defaultStore.clearGenerationStatus,
701
+ enqueueNotice: defaultStore.enqueueNotice,
702
+ dismissNotice: defaultStore.dismissNotice
703
+ };
704
+ }));